Compare commits

...

16 Commits

Author SHA1 Message Date
taizan-hokuto
385634b709 Fix MANIFEST.in 2020-01-12 14:55:29 +09:00
taizan-hokuto
c1a78a2743 Fix setup.py 2020-01-12 14:40:26 +09:00
taizan-hokuto
7961801e0c Increment version 2020-01-12 14:38:21 +09:00
taizan-hokuto
5fe4e7af04 Fix description 2020-01-12 14:26:15 +09:00
taizan-hokuto
892dfb8a91 Fix setup.py 2020-01-11 14:23:32 +09:00
taizan-hokuto
fddab22a1f Delete unnecessary file 2020-01-11 13:29:34 +09:00
taizan-hokuto
7194948066 Modify setup.py 2020-01-11 13:23:55 +09:00
taizan-hokuto
a836d92194 Increment version 2020-01-11 05:33:50 +09:00
taizan-hokuto
c408cb2713 Increment version 2020-01-11 05:04:10 +09:00
taizan-hokuto
c3d2238ead Merge branch 'feature/switch_topchat' into develop 2020-01-11 04:54:23 +09:00
taizan-hokuto
6c8d390fc7 Modify test 2020-01-11 04:41:39 +09:00
taizan-hokuto
ff1ee70d7e Implement 'topchat_only' parameter
: make it possible to select whether to get only top chat.
2020-01-11 04:22:48 +09:00
taizan-hokuto
404623546e Exclude test requirements 2020-01-10 01:10:12 +09:00
taizan-hokuto
3f9f64d19c Increment version 2020-01-09 00:17:18 +09:00
taizan-hokuto
7996c6adad Add test 2020-01-09 00:15:36 +09:00
taizan-hokuto
50d55da7dc Add currency 2020-01-08 23:55:50 +09:00
14 changed files with 231 additions and 71 deletions

View File

@@ -1,7 +1,5 @@
include requirements.txt include requirements.txt
include requirements_test.txt include requirements_test.txt
prune testrun*.py include README.MD
prune log.txt global-exclude tests/*
prune quote.txt global-exclude pytchat/testrun*.py
prune .gitignore
prun tests

View File

@@ -2,7 +2,7 @@
pytchat is a python library for fetching youtube live chat without using yt api, Selenium, or BeautifulSoup. pytchat is a python library for fetching youtube live chat without using yt api, Selenium, or BeautifulSoup.
""" """
__copyright__ = 'Copyright (C) 2019 taizan-hokuto' __copyright__ = 'Copyright (C) 2019 taizan-hokuto'
__version__ = '0.0.4.3' __version__ = '0.0.4.9'
__license__ = 'MIT' __license__ = 'MIT'
__author__ = 'taizan-hokuto' __author__ = 'taizan-hokuto'
__author_email__ = '55448286+taizan-hokuto@users.noreply.github.com' __author_email__ = '55448286+taizan-hokuto@users.noreply.github.com'

View File

@@ -63,6 +63,9 @@ class LiveChatAsync:
force_replay : bool force_replay : bool
Trueの場合、ライブチャットが取得できる場合であっても Trueの場合、ライブチャットが取得できる場合であっても
強制的にアーカイブ済みチャットを取得する。 強制的にアーカイブ済みチャットを取得する。
topchat_only : bool
Trueの場合、上位チャットのみ取得する。
Attributes Attributes
--------- ---------
@@ -81,7 +84,8 @@ class LiveChatAsync:
done_callback = None, done_callback = None,
exception_handler = None, exception_handler = None,
direct_mode = False, direct_mode = False,
force_replay = False force_replay = False,
topchat_only = False
): ):
self.video_id = video_id self.video_id = video_id
self.seektime = seektime self.seektime = seektime
@@ -102,6 +106,7 @@ class LiveChatAsync:
self._setup() self._setup()
self._first_fetch = True self._first_fetch = True
self._fetch_url = "live_chat/get_live_chat?continuation=" self._fetch_url = "live_chat/get_live_chat?continuation="
self._topchat_only = topchat_only
if not LiveChatAsync._setup_finished: if not LiveChatAsync._setup_finished:
LiveChatAsync._setup_finished = True LiveChatAsync._setup_finished = True
if exception_handler == None: if exception_handler == None:
@@ -200,7 +205,8 @@ class LiveChatAsync:
''' '''
self._pauser.put_nowait(None) self._pauser.put_nowait(None)
if not self._is_replay: if not self._is_replay:
continuation = liveparam.getparam(self.video_id,3) continuation = liveparam.getparam(
self.video_id, 3, self._topchat_only)
return continuation return continuation
async def _get_contents(self, continuation, session, headers): async def _get_contents(self, continuation, session, headers):
@@ -220,9 +226,9 @@ class LiveChatAsync:
if contents is None or self._is_replay: if contents is None or self._is_replay:
'''Try to fetch archive chat data.''' '''Try to fetch archive chat data.'''
self._parser.is_replay = True self._parser.is_replay = True
self._fetch_url = ("live_chat_replay/" self._fetch_url = "live_chat_replay/get_live_chat_replay?continuation="
"get_live_chat_replay?continuation=") continuation = arcparam.getparam(
continuation = arcparam.getparam(self.video_id, self.seektime) self.video_id, self.seektime, self._topchat_only)
livechat_json = (await self._get_livechat_json( livechat_json = (await self._get_livechat_json(
continuation, session, headers)) continuation, session, headers))
contents = self._parser.get_contents(livechat_json) contents = self._parser.get_contents(livechat_json)

View File

@@ -60,6 +60,9 @@ class LiveChat:
Trueの場合、ライブチャットが取得できる場合であっても Trueの場合、ライブチャットが取得できる場合であっても
強制的にアーカイブ済みチャットを取得する。 強制的にアーカイブ済みチャットを取得する。
topchat_only : bool
Trueの場合、上位チャットのみ取得する。
Attributes Attributes
--------- ---------
_executor : ThreadPoolExecutor _executor : ThreadPoolExecutor
@@ -80,7 +83,8 @@ class LiveChat:
callback = None, callback = None,
done_callback = None, done_callback = None,
direct_mode = False, direct_mode = False,
force_replay = False force_replay = False,
topchat_only = False
): ):
self.video_id = video_id self.video_id = video_id
self.seektime = seektime self.seektime = seektime
@@ -101,7 +105,7 @@ class LiveChat:
self._setup() self._setup()
self._first_fetch = True self._first_fetch = True
self._fetch_url = "live_chat/get_live_chat?continuation=" self._fetch_url = "live_chat/get_live_chat?continuation="
self._topchat_only = topchat_only
if not LiveChat._setup_finished: if not LiveChat._setup_finished:
LiveChat._setup_finished = True LiveChat._setup_finished = True
if interruptable: if interruptable:
@@ -214,8 +218,7 @@ class LiveChat:
if contents is None or self._is_replay: if contents is None or self._is_replay:
'''Try to fetch archive chat data.''' '''Try to fetch archive chat data.'''
self._parser.is_replay = True self._parser.is_replay = True
self._fetch_url = ("live_chat_replay/" self._fetch_url = "live_chat_replay/get_live_chat_replay?continuation="
"get_live_chat_replay?continuation=")
continuation = arcparam.getparam(self.video_id, self.seektime) continuation = arcparam.getparam(self.video_id, self.seektime)
livechat_json = ( self._get_livechat_json( livechat_json = ( self._get_livechat_json(
continuation, session, headers)) continuation, session, headers))
@@ -230,8 +233,7 @@ class LiveChat:
continuation = urllib.parse.quote(continuation) continuation = urllib.parse.quote(continuation)
livechat_json = None livechat_json = None
status_code = 0 status_code = 0
url =( url =f"https://www.youtube.com/{self._fetch_url}{continuation}&pbj=1"
f"https://www.youtube.com/{self._fetch_url}{continuation}&pbj=1")
for _ in range(MAX_RETRY + 1): for _ in range(MAX_RETRY + 1):
with session.get(url ,headers = headers) as resp: with session.get(url ,headers = headers) as resp:
try: try:

View File

@@ -1,31 +0,0 @@
import logging
import datetime
def get_logger(modname,mode=logging.DEBUG):
logger = logging.getLogger(modname)
if mode == None:
logger.addHandler(logging.NullHandler())
return logger
logger.setLevel(mode)
#create handler1 for showing info
handler1 = logging.StreamHandler()
my_formatter = MyFormatter()
handler1.setFormatter(my_formatter)
handler1.setLevel(mode)
logger.addHandler(handler1)
#create handler2 for recording log file
if mode <= logging.DEBUG:
handler2 = logging.FileHandler(filename="log.txt", encoding='utf-8')
handler2.setLevel(logging.ERROR)
handler2.setFormatter(my_formatter)
logger.addHandler(handler2)
return logger
class MyFormatter(logging.Formatter):
def format(self, record):
s =(datetime.datetime.fromtimestamp(record.created)).strftime("%m-%d %H:%M:%S")+'| '+ (record.module).ljust(15)+(' { '+record.funcName).ljust(20) +":"+str(record.lineno).rjust(4)+'} - '+record.getMessage()
return s

View File

@@ -52,8 +52,8 @@ def _nval(val):
buf += val.to_bytes(1,'big') buf += val.to_bytes(1,'big')
return buf return buf
def _build(video_id, seektime, topchatonly = False): def _build(video_id, seektime, topchat_only):
switch_01 = b'\x04' if topchatonly else b'\x01' switch_01 = b'\x04' if topchat_only else b'\x01'
if seektime < 0: if seektime < 0:
times =_nval(0) times =_nval(0)
switch = b'\x04' switch = b'\x04'
@@ -102,12 +102,14 @@ def _build(video_id, seektime, topchatonly = False):
).decode() ).decode()
) )
def getparam(video_id, seektime = 0): def getparam(video_id, seektime = 0, topchat_only = False):
''' '''
Parameter Parameter
--------- ---------
seektime : int seektime : int
unit:seconds unit:seconds
start position of fetching chat data. start position of fetching chat data.
topchat_only : bool
if True, fetch only 'top chat'
''' '''
return _build(video_id, seektime) return _build(video_id, seektime, topchat_only)

View File

@@ -66,9 +66,9 @@ def _nval(val):
buf += val.to_bytes(1,'big') buf += val.to_bytes(1,'big')
return buf return buf
def _build(video_id, _ts1, _ts2, _ts3, _ts4, _ts5, topchatonly = False): def _build(video_id, _ts1, _ts2, _ts3, _ts4, _ts5, topchat_only):
#_short_type2 #_short_type2
switch_01 = b'\x04' if topchatonly else b'\x01' switch_01 = b'\x04' if topchat_only else b'\x01'
parity = _tzparity(video_id, _ts1^_ts2^_ts3^_ts4^_ts5) parity = _tzparity(video_id, _ts1^_ts2^_ts3^_ts4^_ts5)
header_magic= b'\xD2\x87\xCC\xC8\x03' header_magic= b'\xD2\x87\xCC\xC8\x03'
@@ -155,12 +155,14 @@ def _times(past_sec):
return list(map(lambda x:int(x*1000000),[_ts1,_ts2,_ts3,_ts4,_ts5])) return list(map(lambda x:int(x*1000000),[_ts1,_ts2,_ts3,_ts4,_ts5]))
def getparam(video_id,past_sec = 0): def getparam(video_id, past_sec = 0, topchat_only = False):
''' '''
Parameter Parameter
--------- ---------
past_sec : int past_sec : int
seconds to load past chat data seconds to load past chat data
topchat_only : bool
if True, fetch only 'top chat'
''' '''
return _build(video_id,*_times(past_sec)) return _build(video_id,*_times(past_sec),topchat_only)

View File

@@ -26,7 +26,8 @@ class Parser:
if jsn is None: if jsn is None:
raise ChatParseException('Called with none JSON object.') raise ChatParseException('Called with none JSON object.')
if jsn['response']['responseContext'].get('errors'): if jsn['response']['responseContext'].get('errors'):
raise ResponseContextError('The video_id would be wrong, or video is deleted or private.') raise ResponseContextError('The video_id would be wrong,'
'or video is deleted or private.')
contents=jsn['response'].get('continuationContents') contents=jsn['response'].get('continuationContents')
return contents return contents
@@ -77,7 +78,8 @@ class Parser:
metadata.setdefault("timeoutMs",interval) metadata.setdefault("timeoutMs",interval)
"""Archived chat has different structures than live chat, """Archived chat has different structures than live chat,
so make it the same format.""" so make it the same format."""
chatdata = [action["replayChatItemAction"]["actions"][0] for action in actions] chatdata = [action["replayChatItemAction"]["actions"][0]
for action in actions]
else: else:
metadata.setdefault('timeoutMs', 10000) metadata.setdefault('timeoutMs', 10000)
chatdata = actions chatdata = actions

View File

@@ -33,5 +33,6 @@ symbols = {
"ARS\xa0": {"fxtext": "ARS", "jptext": "アルゼンチン・ペソ"}, "ARS\xa0": {"fxtext": "ARS", "jptext": "アルゼンチン・ペソ"},
"CLP\xa0": {"fxtext": "CLP", "jptext": "チリ・ペソ"}, "CLP\xa0": {"fxtext": "CLP", "jptext": "チリ・ペソ"},
"NOK\xa0": {"fxtext": "NOK", "jptext": "ノルウェー・クローネ"}, "NOK\xa0": {"fxtext": "NOK", "jptext": "ノルウェー・クローネ"},
"BAM\xa0": {"fxtext": "BAM", "jptext": "ボスニア・兌換マルカ"} "BAM\xa0": {"fxtext": "BAM", "jptext": "ボスニア・兌換マルカ"},
"SGD\xa0": {"fxtext": "SGD", "jptext": "シンガポール・ドル"}
} }

View File

@@ -33,5 +33,6 @@ symbols = {
"ARS\xa0": {"fxtext": "ARS", "jptext": "アルゼンチン・ペソ"}, "ARS\xa0": {"fxtext": "ARS", "jptext": "アルゼンチン・ペソ"},
"CLP\xa0": {"fxtext": "CLP", "jptext": "チリ・ペソ"}, "CLP\xa0": {"fxtext": "CLP", "jptext": "チリ・ペソ"},
"NOK\xa0": {"fxtext": "NOK", "jptext": "ノルウェー・クローネ"}, "NOK\xa0": {"fxtext": "NOK", "jptext": "ノルウェー・クローネ"},
"BAM\xa0": {"fxtext": "BAM", "jptext": "ボスニア・兌換マルカ"} "BAM\xa0": {"fxtext": "BAM", "jptext": "ボスニア・兌換マルカ"},
"SGD\xa0": {"fxtext": "SGD", "jptext": "シンガポール・ドル"}
} }

View File

@@ -1,6 +1,6 @@
from setuptools import setup, find_packages, Command from setuptools import setup, find_packages, Command
#from codecs import open as open_c #from codecs import open as open_c
from os import path, system, remove, rename from os import path, system, remove, rename, removedirs
import re import re
package_name = "pytchat" package_name = "pytchat"
@@ -13,6 +13,15 @@ def _requirements():
def _test_requirements(): def _test_requirements():
return [name.rstrip() for name in open(path.join(root_dir, 'requirements_test.txt')).readlines()] return [name.rstrip() for name in open(path.join(root_dir, 'requirements_test.txt')).readlines()]
txt= ''
with open('README.MD', 'r', encoding='utf-8') as f:
txt = f.read()
with open('README1.MD', 'w', encoding='utf-8', newline='\n') as f:
f.write(txt)
remove("README.MD")
rename("README1.MD","README.MD")
with open(path.join(root_dir, package_name, '__init__.py')) as f: with open(path.join(root_dir, package_name, '__init__.py')) as f:
init_text = f.read() init_text = f.read()
@@ -30,14 +39,7 @@ assert url
with open('README.MD', 'r', encoding='utf-8') as f:
txt = f.read()
with open('README1.MD', 'w', encoding='utf-8', newline='\n') as f:
f.write(txt)
remove("README.MD")
rename("README1.MD","README.MD")
with open('README.md', encoding='utf-8') as f: with open('README.md', encoding='utf-8') as f:
long_description = f.read() long_description = f.read()
@@ -45,7 +47,7 @@ with open('README.md', encoding='utf-8') as f:
setup( setup(
name=package_name, name=package_name,
packages=find_packages(), packages=find_packages(exclude=['*log.txt','*tests']),
version=version, version=version,
url=url, url=url,
author=author, author=author,
@@ -54,7 +56,7 @@ setup(
long_description_content_type='text/markdown', long_description_content_type='text/markdown',
license=license, license=license,
install_requires=_requirements(), install_requires=_requirements(),
tests_require=_test_requirements(), #tests_require=_test_requirements(),
description="a python library for fetching youtube live chat.", description="a python library for fetching youtube live chat.",
classifiers=[ classifiers=[
'Natural Language :: Japanese', 'Natural Language :: Japanese',

View File

@@ -124,6 +124,21 @@ def test_superchat(mocker):
assert "LCC." in ret["items"][0]["id"] assert "LCC." in ret["items"][0]["id"]
assert ret["items"][0]["snippet"]["type"]=="superChatEvent" assert ret["items"][0]["snippet"]["type"]=="superChatEvent"
def test_unregistered_currency(mocker):
processor = CompatibleProcessor()
_json = _open_file("tests/testdata/unregistered_currency.json")
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
data = {
"video_id" : "",
"timeout" : 7,
"chatdata" : chatdata
}
ret = processor.process([data])
assert ret["items"][0]["snippet"]["superChatDetails"]["currency"] == "[UNREGISTERD]"
def _open_file(path): def _open_file(path):
with open(path,mode ='r',encoding = 'utf-8') as f: with open(path,mode ='r',encoding = 'utf-8') as f:

View File

@@ -4,6 +4,6 @@ from pytchat.paramgen import liveparam
def test_liveparam_0(mocker): def test_liveparam_0(mocker):
_ts1= 1546268400 _ts1= 1546268400
param = liveparam._build("01234567890", param = liveparam._build("01234567890",
*([_ts1*1000000 for i in range(5)])) *([_ts1*1000000 for i in range(5)]), topchat_only=False)
test_param="0ofMyAPiARp8Q2c4S0RRb0xNREV5TXpRMU5qYzRPVEFhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5TURFeU16UTFOamM0T1RBbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCiAuNbVqsrfAjAAOABAAkorCAEQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQgLjW1arK3wJYA1CAuNbVqsrfAliAuNbVqsrfAmgBggEECAEQAIgBAKABgLjW1arK3wI%3D" test_param="0ofMyAPiARp8Q2c4S0RRb0xNREV5TXpRMU5qYzRPVEFhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5TURFeU16UTFOamM0T1RBbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCiAuNbVqsrfAjAAOABAAkorCAEQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQgLjW1arK3wJYA1CAuNbVqsrfAliAuNbVqsrfAmgBggEECAEQAIgBAKABgLjW1arK3wI%3D"
assert test_param == param assert test_param == param

View File

@@ -0,0 +1,160 @@
{
"timing": {
"info": {
"st": 164
}
},
"csn": "",
"response": {
"responseContext": {
"serviceTrackingParams": [{
"service": "CSI",
"params": [{
"key": "GetLiveChat_rid",
"value": ""
}, {
"key": "c",
"value": "WEB"
}, {
"key": "cver",
"value": "2.20191219.03.01"
}, {
"key": "yt_li",
"value": "0"
}]
}, {
"service": "GFEEDBACK",
"params": [{
"key": "e",
"value": ""
}, {
"key": "logged_in",
"value": "0"
}]
}, {
"service": "GUIDED_HELP",
"params": [{
"key": "logged_in",
"value": "0"
}]
}, {
"service": "ECATCHER",
"params": [{
"key": "client.name",
"value": "WEB"
}, {
"key": "client.version",
"value": "2.2"
}, {
"key": "innertube.build.changelist",
"value": "228"
}, {
"key": "innertube.build.experiments.source_version",
"value": "2858"
}, {
"key": "innertube.build.label",
"value": "youtube.ytfe.innertube_"
}, {
"key": "innertube.build.timestamp",
"value": "154"
}, {
"key": "innertube.build.variants.checksum",
"value": "e"
}, {
"key": "innertube.run.job",
"value": "ytfe-innertube-replica-only.ytfe"
}]
}],
"webResponseContextExtensionData": {
"ytConfigData": {
"csn": "ADw",
"visitorData": "%3D%3D"
}
}
},
"continuationContents": {
"liveChatContinuation": {
"continuations": [{
"timedContinuationData": {
"timeoutMs": 10000,
"continuation": "continuation"
}
}],
"actions": [{
"addChatItemAction": {
"item": {
"liveChatPaidMessageRenderer": {
"id": "dummy_id",
"timestampUsec": "1576850000000000",
"authorName": {
"simpleText": "author_name"
},
"authorPhoto": {
"thumbnails": [
{
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
"width": 32,
"height": 32
},
{
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
"width": 64,
"height": 64
}
]
},
"purchaseAmountText": {
"simpleText": "[UNREGISTERD]10,800"
},
"message": {
"runs": [
{
"text": "This is unregistered currency."
}
]
},
"headerBackgroundColor": 4291821568,
"headerTextColor": 4294967295,
"bodyBackgroundColor": 4293271831,
"bodyTextColor": 4294967295,
"authorExternalChannelId": "http://www.youtube.com/channel/author_channel_url",
"authorNameTextColor": 3019898879,
"contextMenuEndpoint": {
"commandMetadata": {
"webCommandMetadata": {
"ignoreNavigation": true
}
},
"liveChatItemContextMenuEndpoint": {
"params": "___params___"
}
},
"timestampColor": 2164260863,
"contextMenuAccessibility": {
"accessibilityData": {
"label": "コメントの操作"
}
}
}
}, "clientId": "00000000000000000000"
}
}
]}
},
"xsrf_token": "xsrf_token",
"url": "/live_chat/get_live_chat?continuation=0",
"endpoint": {
"commandMetadata": {
"webCommandMetadata": {
"url": "/live_chat/get_live_chat?continuation=0",
"rootVe": 0
}
},
"urlEndpoint": {
"url": "/live_chat/get_live_chat?continuation=0"
}
}
}
}