Compare commits

..

82 Commits

Author SHA1 Message Date
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
taizan-hokuto
c92e735715 Merge branch 'release/v0.0.4.3' 2020-01-08 01:27:06 +09:00
taizan-hokuto
d4a1d00e28 Increment version 2020-01-08 01:25:34 +09:00
taizan-hokuto
60c389f3f7 Change debug mode 2020-01-08 01:25:19 +09:00
taizan-hokuto
705bfe0bed Modify MANIFEST.in 2020-01-08 01:23:19 +09:00
taizan-hokuto
0f7a0218b6 Delete unnecessary lines 2020-01-08 01:12:58 +09:00
taizan-hokuto
89d2f8978f Modify README 2020-01-08 01:02:05 +09:00
taizan-hokuto
6befc2de95 Merge branch 'feature/force_replay_mode' into develop 2020-01-08 00:47:48 +09:00
taizan-hokuto
a0c5ea035a Fix comment 2020-01-08 00:46:23 +09:00
taizan-hokuto
3b27c81166 Add tests 2020-01-08 00:44:50 +09:00
taizan-hokuto
26fefddddf Implement force_replay_mode 2020-01-04 13:23:32 +09:00
taizan-hokuto
5d86fb4b71 Fix parameter switching and tests 2020-01-04 09:28:44 +09:00
taizan-hokuto
b5e302cdf3 Make it possible to retrieve chat before broadcast
by specifying negative number in seektime
2020-01-04 00:41:58 +09:00
taizan-hokuto
5d228589f1 Delete unnecessary file 2020-01-04 00:25:22 +09:00
taizan-hokuto
fd8ecec0c5 Merge branch 'feature/warning_deprecation_replaychat' into develop 2020-01-04 00:04:00 +09:00
taizan-hokuto
a1e48b56e6 Add warning for deprecating replaychat 2020-01-03 23:45:08 +09:00
taizan-hokuto
9c41536533 Merge branch 'feature/integrate' into develop 2020-01-03 22:12:15 +09:00
taizan-hokuto
2c684d04b5 Intgegrate replaychat into livechat (multithread) 2020-01-03 02:09:39 +09:00
taizan-hokuto
30708470f2 Change comments 2020-01-02 22:43:23 +09:00
taizan-hokuto
d742a9fdf3 Fix seek time param 2020-01-02 22:27:21 +09:00
taizan-hokuto
2fdd834caf Extract method _check_pause() 2020-01-02 22:08:26 +09:00
taizan-hokuto
4c558491a3 Change exception message 2020-01-02 21:17:37 +09:00
taizan-hokuto
0fc9d14780 Fix handling exception 2020-01-02 21:14:22 +09:00
taizan-hokuto
18400724b1 Modify metadata selection 2020-01-02 21:08:53 +09:00
taizan-hokuto
7b7323abf8 Delete debug line 2020-01-02 20:52:36 +09:00
taizan-hokuto
fc5979c025 Moved livechat_json part 2020-01-02 20:51:59 +09:00
taizan-hokuto
f4dc5e9d4a Delete unnecessary lines 2020-01-02 20:31:51 +09:00
taizan-hokuto
347707a514 Delete unnecessary lines 2020-01-02 20:08:45 +09:00
taizan-hokuto
7766a39c9c Integrate replaychat into livechat 2020-01-02 19:35:58 +09:00
taizan-hokuto
48b6f2c24e Add comment 2020-01-02 15:46:45 +09:00
taizan-hokuto
907f8aba0b Rename function name 2020-01-02 15:42:32 +09:00
taizan-hokuto
2616e4c4b5 Adjust amount of first fetching chat 2020-01-02 15:20:34 +09:00
taizan-hokuto
d6ea673f98 Fix getting arcparam when resume 2020-01-02 15:12:48 +09:00
taizan-hokuto
2bb481a228 Disable _pauser when callback is unset 2020-01-02 14:27:36 +09:00
taizan-hokuto
7308a87a61 Implement pause/pauser to livechat 2020-01-02 13:15:41 +09:00
taizan-hokuto
9751289eca Integrate _get_initial_continuation 2020-01-02 12:47:40 +09:00
taizan-hokuto
044fe97aa5 Merge branch 'release/0.0.4.2' 2020-01-02 10:25:26 +09:00
taizan-hokuto
28e3289790 Merge branch 'release/0.0.4.2' into develop 2020-01-02 10:25:06 +09:00
taizan-hokuto
585a4be7dc Increment version 2020-01-02 10:22:29 +09:00
taizan-hokuto
b84a82341e Fix README 2020-01-01 21:13:39 +09:00
taizan-hokuto
b4f3307b1c Fix comment 2020-01-01 20:08:07 +09:00
taizan-hokuto
be7ac97c62 Modify tuple comprehension 2019-12-31 01:01:27 +09:00
taizan-hokuto
f8de4e7e39 Merge branch 'hotfix' into develop 2019-12-30 19:12:17 +09:00
taizan-hokuto
ac0f052aa0 Merge branch 'hotfix' 2019-12-30 19:11:39 +09:00
taizan-hokuto
1cc0338a8e Export DefaultProcessor 2019-12-30 19:09:12 +09:00
taizan-hokuto
f6b8229998 Merge branch 'release' 2019-12-30 18:33:41 +09:00
taizan-hokuto
f8bcc8a453 Merge branch 'release' into develop 2019-12-30 18:32:43 +09:00
taizan-hokuto
f24c5f9e30 Increment version 2019-12-30 18:32:17 +09:00
taizan-hokuto
5268961854 Delete unnecessary lines of old logger 2019-12-30 17:54:53 +09:00
taizan-hokuto
733f754e11 Merge branch 'feature/fix_replaychat_is_alive' into develop 2019-12-30 17:32:50 +09:00
taizan-hokuto
582d0b749d Add comment 2019-12-30 17:32:23 +09:00
taizan-hokuto
b8bc00d880 Fix README 2019-12-30 17:27:25 +09:00
taizan-hokuto
ce96d94e23 Fix termination of fetching chat data 2019-12-30 17:10:34 +09:00
taizan-hokuto
7af92f14c0 Merge branch 'feature/config_logger' into develop 2019-12-30 15:01:32 +09:00
taizan-hokuto
7305e4178b Change description of getting logger 2019-12-30 14:38:02 +09:00
taizan-hokuto
a835d58e10 Merge branch 'feature/combinator' into develop 2019-12-30 11:04:25 +09:00
taizan-hokuto
4e956b8d84 Implement Combinator, DummyProcessor 2019-12-30 11:02:29 +09:00
taizan-hokuto
c4f1194a53 Merge branch 'feature/fix_massage_ex' into develop 2019-12-30 10:36:22 +09:00
taizan-hokuto
90b10a9f8f Integrate rendering message and message_ex 2019-12-27 02:19:08 +09:00
taizan-hokuto
b576c3f928 Merge branch 'develop' 2019-12-24 00:57:06 +09:00
taizan-hokuto
c0728e1366 Increment version 2019-12-24 00:55:00 +09:00
taizan-hokuto
fff09d4c27 Fix README 2019-12-24 00:52:53 +09:00
taizan-hokuto
810b6c8c6b Fix README 2019-12-24 00:29:10 +09:00
taizan-hokuto
dfada86caf Merge branch 'develop' 2019-12-22 02:56:54 +09:00
taizan-hokuto
91aa944df5 Increment version 2019-12-22 02:54:20 +09:00
taizan-hokuto
6ac5191e85 Change debug mode 2019-12-22 01:58:42 +09:00
taizan-hokuto
fff3e0371f Export SpeedCalculator 2019-12-22 01:39:35 +09:00
taizan-hokuto
a70efe8a67 Fix comments 2019-12-21 23:42:51 +09:00
taizan-hokuto
dc47f4debe Fix README 2019-12-21 22:40:08 +09:00
taizan-hokuto
ab5a2a8df2 Fix syntax error 2019-12-21 20:12:30 +09:00
taizan-hokuto
5a79f26fa7 Fix calculation algorithm 2019-12-21 20:06:55 +09:00
taizan-hokuto
18666199b7 Add test SpeedCalculator 2019-12-21 02:13:15 +09:00
taizan-hokuto
b357bccb98 Add test json 2019-12-20 23:57:10 +09:00
taizan-hokuto
3c1f079d5f Extends ChatProcessor explicitly 2019-12-20 21:33:32 +09:00
taizan-hokuto
289841a000 Export api speed_calculator 2019-12-20 01:13:50 +09:00
taizan-hokuto
ee0ff7fe74 Merge branch 'develop' 2019-12-19 23:14:37 +09:00
taizan-hokuto
c0870ce537 Fix README 2019-12-19 23:14:05 +09:00
taizan-hokuto
de6ef2490e Merge branch 'develop' 2019-12-19 02:03:09 +09:00
taizan-hokuto
b8bdbdc36f Increment version 2019-12-19 02:02:32 +09:00
taizan-hokuto
9f5d3f323e Fix README 2019-12-19 02:00:41 +09:00
37 changed files with 2048 additions and 380 deletions

View File

@@ -1,3 +1,7 @@
include requirements.txt
include requirements_test.txt
prune testrun*.py
prune log.txt
prune quote.txt
prune .gitignore
prun tests

125
README.md
View File

@@ -8,7 +8,7 @@ pytchat is a python library for fetching youtube live chat
without using youtube api, Selenium or BeautifulSoup.
Other features:
+ Customizable chat data processors including yt api compatible one.
+ Customizable chat data processors including youtube api compatible one.
+ Available on asyncio context.
+ Quick fetching of initial chat data by generating continuation params
instead of web scraping.
@@ -27,12 +27,12 @@ pip install pytchat
```python
from pytchat import LiveChat
chat = LiveChat("G1w62uEMZ74")
chat = LiveChat("rsHWP7IjMiw")
while chat.is_alive():
data = chat.get()
for c in data.items:
print(f"{c.datetime} [{c.author.name}]-{c.message} {c.amountString}")
data.tick()
data = chat.get()
for c in data.items:
print(f"{c.datetime} [{c.author.name}]-{c.message} {c.amountString}")
data.tick()
```
### callback mode
@@ -40,80 +40,83 @@ while chat.is_alive():
from pytchat import LiveChat
import time
def main()
chat = LiveChat("G1w62uEMZ74", callback = func)
while chat.is_alive():
time.sleep(3)
#other background operation.
#callback function is automatically called periodically.
def func(data):
for c in data.items:
print(f"{c.datetime} [{c.author.name}]-{c.message} {c.amountString}")
data.tick()
#callback function is automatically called.
def display(data):
for c in data.items:
print(f"{c.datetime} [{c.author.name}]-{c.message} {c.amountString}")
data.tick()
if __name__ == '__main__':
chat = LiveChat("rsHWP7IjMiw", callback = display)
while chat.is_alive():
#other background operation.
time.sleep(3)
```
### asyncio context:
```python
from pytchat import LiveChatAsync
from concurrent.futures import CancelledError
import asyncio
async def main():
chat = LiveChatAsync("G1w62uEMZ74", callback = func)
while chat.is_alive():
await asyncio.sleep(3)
#other background operation.
chat = LiveChatAsync("rsHWP7IjMiw", callback = func)
while chat.is_alive():
#other background operation.
await asyncio.sleep(3)
#callback function is automatically called periodically.
#callback function is automatically called.
async def func(data):
for c in data.items:
print(f"{c.datetime} [{c.author.name}]-{c.message} {c.amountString}")
await data.tick_async()
for c in data.items:
print(f"{c.datetime} [{c.author.name}]-{c.message} {c.amountString}")
await data.tick_async()
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
if __name__ == '__main__':
try:
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
except CancelledError:
pass
```
### youtube api compatible processor:
```python
from pytchat import LiveChat, CompatibleProcessor
import time
chat = LiveChat("G1w62uEMZ74",
processor = CompatibleProcessor() )
chat = LiveChat("rsHWP7IjMiw",
processor = CompatibleProcessor() )
while chat.is_alive():
data = chat.get()
polling = data["pollingIntervalMillis"]/1000
for c in data["items"]:
if c.get("snippet"):
print(f"[{c['authorDetails']['displayName']}]"
f"-{c['snippet']['displayMessage']}")
time.sleep(polling/len(data["items"]))
data = chat.get()
polling = data['pollingIntervalMillis']/1000
for c in data['items']:
if c.get('snippet'):
print(f"[{c['authorDetails']['displayName']}]"
f"-{c['snippet']['displayMessage']}")
time.sleep(polling/len(data['items']))
```
### replay:
### replay:
If specified video is not live,
automatically try to fetch archived chat data.
```python
from pytchat import ReplayChatAsync
import asyncio
from pytchat import LiveChat
async def main():
chat = ReplayChatAsync("G1w62uEMZ74", seektime = 1000, callback = func)
#other background operation here.
def main():
#seektime (seconds): start position of chat.
chat = ReplayChat("ojes5ULOqhc", seektime = 60*30)
while chat.is_alive():
data = chat.get()
for c in data.items:
print(f"{c.elapsedTime} [{c.author.name}]-{c.message} {c.amountString}")
data.tick()
#callback function is automatically called periodically.
async def func(data):
for count in range(0,len(data.items)):
c= data.items[count]
if count!=len(data.items):
tick=data.items[count+1].timestamp -data.items[count].timestamp
else:
tick=0
print(f"<{c.elapsedTime}> [{c.author.name}]-{c.message} {c.amountString}")
await asyncio.sleep(tick/1000)
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
if __name__ == '__main__':
main()
```
## Structure of Default Processor
@@ -152,26 +155,26 @@ Each item can be got with items() function.
<tr>
<td>datetime</td>
<td>str</td>
<td>ex. "2019-10-10 12:34:56"</td>
<td>e.g. "2019-10-10 12:34:56"</td>
</tr>
<td>elapsedTime</td>
<td>str</td>
<td>elapsed time. (ex. "1:02:27") *Replay Only.</td>
<td>elapsed time. (e.g. "1:02:27") *Replay Only.</td>
</tr>
<tr>
<td>amountValue</td>
<td>float</td>
<td>ex. 1,234.0</td>
<td>e.g. 1,234.0</td>
</tr>
<tr>
<td>amountString</td>
<td>str</td>
<td>ex. "$ 1,234"</td>
<td>e.g. "$ 1,234"</td>
</tr>
<tr>
<td>currency</td>
<td>str</td>
<td><a href="https://en.wikipedia.org/wiki/ISO_4217">ISO 4217 currency codes</a> (ex. "USD")</td>
<td><a href="https://en.wikipedia.org/wiki/ISO_4217">ISO 4217 currency codes</a> (e.g. "USD")</td>
</tr>
<tr>
<td>bgColor</td>
@@ -200,7 +203,7 @@ Structure of author object.
<tr>
<td>channelId</td>
<td>str</td>
<td>*chatter's channel ID. NOT broadcasting video's channel ID.</td>
<td>*chatter's channel ID.</td>
</tr>
<tr>
<td>channelUrl</td>

View File

@@ -2,7 +2,7 @@
pytchat is a python library for fetching youtube live chat without using yt api, Selenium, or BeautifulSoup.
"""
__copyright__ = 'Copyright (C) 2019 taizan-hokuto'
__version__ = '0.0.3.5'
__version__ = '0.0.4.4'
__license__ = 'MIT'
__author__ = 'taizan-hokuto'
__author_email__ = '55448286+taizan-hokuto@users.noreply.github.com'
@@ -11,12 +11,16 @@ __url__ = 'https://github.com/taizan-hokuto/pytchat'
__all__ = ["core_async","core_multithread","processors"]
from .api import (
config,
LiveChat,
LiveChatAsync,
ReplayChat,
ReplayChatAsync,
ChatProcessor,
CompatibleProcessor,
CompatibleProcessor,
DefaultProcessor,
SimpleDisplayProcessor,
JsonfileArchiveProcessor
)
JsonfileArchiveProcessor,
SpeedCalculator,
DummyProcessor
)

View File

@@ -7,4 +7,6 @@ from .processors.default.processor import DefaultProcessor
from .processors.compatible.processor import CompatibleProcessor
from .processors.simple_display_processor import SimpleDisplayProcessor
from .processors.jsonfile_archive_processor import JsonfileArchiveProcessor
from .processors.speed_calculator import SpeedCalculator
from .processors.dummy_processor import DummyProcessor
from . import config

View File

@@ -1,4 +1,13 @@
import logging
LOGGER_MODE = logging.ERROR
from . import mylogger
LOGGER_MODE = None
headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36'}
def logger(module_name: str):
module_logger = mylogger.get_logger(module_name, mode = LOGGER_MODE)
return module_logger

View File

@@ -0,0 +1,32 @@
from logging import NullHandler, getLogger, StreamHandler, FileHandler, Formatter
import logging
import datetime
def get_logger(modname,mode=logging.DEBUG):
logger = getLogger(modname)
if mode == None:
logger.addHandler(NullHandler())
return logger
logger.setLevel(mode)
#create handler1 for showing info
handler1 = 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 = 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

@@ -8,18 +8,18 @@ import traceback
import urllib.parse
from aiohttp.client_exceptions import ClientConnectorError
from concurrent.futures import CancelledError
from asyncio import Queue
from .buffer import Buffer
from ..parser.live import Parser
from .. import config
from .. import mylogger
from ..exceptions import ChatParseException,IllegalFunctionCall
from ..paramgen import liveparam
from ..paramgen import liveparam, arcparam
from ..processors.default.processor import DefaultProcessor
from ..processors.combinator import Combinator
logger = mylogger.get_logger(__name__,mode=config.LOGGER_MODE)
MAX_RETRY = 10
logger = config.logger(__name__)
headers = config.headers
MAX_RETRY = 10
class LiveChatAsync:
@@ -30,6 +30,11 @@ class LiveChatAsync:
video_id : str
動画ID
seektime : int
(ライブチャット取得時は無視)
取得開始するアーカイブ済みチャットの経過時間(秒)
マイナス値を指定した場合は、配信開始前のチャットも取得する。
processor : ChatProcessor
チャットデータを加工するオブジェクト
@@ -53,7 +58,11 @@ class LiveChatAsync:
direct_mode : bool
Trueの場合、bufferを使わずにcallbackを呼ぶ。
Trueの場合、callbackの設定が必須
(設定していない場合IllegalFunctionCall例外を発生させる
(設定していない場合IllegalFunctionCall例外を発生させる
force_replay : bool
Trueの場合、ライブチャットが取得できる場合であっても
強制的にアーカイブ済みチャットを取得する。
Attributes
---------
@@ -64,24 +73,35 @@ class LiveChatAsync:
_setup_finished = False
def __init__(self, video_id,
seektime = 0,
processor = DefaultProcessor(),
buffer = None,
interruptable = True,
callback = None,
done_callback = None,
exception_handler = None,
direct_mode = False):
direct_mode = False,
force_replay = False
):
self.video_id = video_id
self.processor = processor
self.seektime = seektime
if isinstance(processor, tuple):
self.processor = Combinator(processor)
else:
self.processor = processor
self._buffer = buffer
self._callback = callback
self._done_callback = done_callback
self._exception_handler = exception_handler
self._direct_mode = direct_mode
self._is_alive = True
self._parser = Parser()
self._is_replay = force_replay
self._parser = Parser(is_replay = self._is_replay)
self._pauser = Queue()
self._pauser.put_nowait(None)
self._setup()
self._first_fetch = True
self._fetch_url = "live_chat/get_live_chat?continuation="
if not LiveChatAsync._setup_finished:
LiveChatAsync._setup_finished = True
if exception_handler == None:
@@ -99,7 +119,7 @@ class LiveChatAsync:
if self._direct_mode:
if self._callback is None:
raise IllegalFunctionCall(
"direct_mode=Trueの場合callbackの設定が必須です。")
"When direct_mode=True, callback parameter is required.")
else:
#direct modeがFalseでbufferが未設定ならばデフォルトのbufferを作成
if self._buffer is None:
@@ -121,48 +141,29 @@ class LiveChatAsync:
listen_task.add_done_callback(self._done_callback)
async def _startlisten(self):
"""最初のcontinuationパラメータを取得し、
_listenループのタスクを作成し開始する
"""Fetch first continuation parameter,
create and start _listen loop.
"""
initial_continuation = await self._get_initial_continuation()
if initial_continuation is None:
self.terminate()
logger.debug(f"[{self.video_id}]No initial continuation.")
return
initial_continuation = liveparam.getparam(self.video_id,3)
await self._listen(initial_continuation)
async def _get_initial_continuation(self):
''' チャットデータ取得に必要な最初のcontinuationを取得する。'''
try:
initial_continuation = liveparam.getparam(self.video_id)
except ChatParseException as e:
self.terminate()
logger.debug(f"[{self.video_id}]Error:{str(e)}")
return
except KeyError:
logger.debug(f"[{self.video_id}]KeyError:"
f"{traceback.format_exc(limit = -1)}")
self.terminate()
return
return initial_continuation
async def _listen(self, continuation):
''' continuationに紐付いたチャットデータを取得し
チャットデータを格納、
次のcontinuaitonを取得してループする。
''' Fetch chat data and store them into buffer,
get next continuaiton parameter and loop.
Parameter
---------
continuation : str
次のチャットデータ取得に必要なパラメータ
parameter for next chat data
'''
try:
async with aiohttp.ClientSession() as session:
while(continuation and self._is_alive):
livechat_json = (await
self._get_livechat_json(continuation, session, headers)
)
metadata, chatdata = self._parser.parse( livechat_json )
continuation = await self._check_pause(continuation)
contents = await self._get_contents(
continuation, session, headers)
metadata, chatdata = self._parser.parse(contents)
timeout = metadata['timeoutMs']/1000
chat_component = {
"video_id" : self.video_id,
@@ -180,29 +181,67 @@ class LiveChatAsync:
await asyncio.sleep(diff_time)
continuation = metadata.get('continuation')
except ChatParseException as e:
logger.info(f"{str(e)}video_id:\"{self.video_id}\"")
#self.terminate()
logger.debug(f"[{self.video_id}]{str(e)}")
return
except (TypeError , json.JSONDecodeError) :
#self.terminate()
logger.error(f"{traceback.format_exc(limit = -1)}")
return
logger.debug(f"[{self.video_id}]チャット取得を終了しました。")
logger.debug(f"[{self.video_id}]finished fetching chat.")
async def _check_pause(self, continuation):
if self._pauser.empty():
'''pause'''
await self._pauser.get()
'''resume:
prohibit from blocking by putting None into _pauser.
'''
self._pauser.put_nowait(None)
if not self._is_replay:
continuation = liveparam.getparam(self.video_id,3)
return continuation
async def _get_contents(self, continuation, session, headers):
'''Get 'contents' dict from livechat json.
If contents is None at first fetching,
try to fetch archive chat data.
Return:
-------
'contents' dict which includes metadata & chatdata.
'''
livechat_json = (await
self._get_livechat_json(continuation, session, headers)
)
contents = self._parser.get_contents(livechat_json)
if self._first_fetch:
if contents is None or self._is_replay:
'''Try to fetch archive chat data.'''
self._parser.is_replay = True
self._fetch_url = ("live_chat_replay/"
"get_live_chat_replay?continuation=")
continuation = arcparam.getparam(self.video_id, self.seektime)
livechat_json = (await self._get_livechat_json(
continuation, session, headers))
contents = self._parser.get_contents(livechat_json)
self._first_fetch = False
return contents
async def _get_livechat_json(self, continuation, session, headers):
'''
チャットデータが格納されたjsonデータを取得する。
Get json which includes chat data.
'''
continuation = urllib.parse.quote(continuation)
livechat_json = None
status_code = 0
url =(
f"https://www.youtube.com/live_chat/get_live_chat?"
f"continuation={continuation}&pbj=1")
f"https://www.youtube.com/{self._fetch_url}{continuation}&pbj=1")
for _ in range(MAX_RETRY + 1):
async with session.get(url ,headers = headers) as resp:
try:
text = await resp.text()
status_code = resp.status
livechat_json = json.loads(text)
break
except (ClientConnectorError,json.JSONDecodeError) :
@@ -241,6 +280,21 @@ class LiveChatAsync:
raise IllegalFunctionCall(
"既にcallbackを登録済みのため、get()は実行できません。")
def is_replay(self):
return self._is_replay
def pause(self):
if self._callback is None:
return
if not self._pauser.empty():
self._pauser.get_nowait()
def resume(self):
if self._callback is None:
return
if self._pauser.empty():
self._pauser.put_nowait(None)
def is_alive(self):
return self._is_alive
@@ -258,18 +312,16 @@ class LiveChatAsync:
self._is_alive = False
if self._direct_mode == False:
#bufferにダミーオブジェクトを入れてis_alive()を判定させる
self._buffer.put_nowait({'chatdata':'','timeout':1})
logger.info(f'終了しました:[{self.video_id}]')
self._buffer.put_nowait({'chatdata':'','timeout':0})
logger.info(f'[{self.video_id}]finished.')
@classmethod
def _set_exception_handler(cls, handler):
loop = asyncio.get_event_loop()
#default handler: cls._handle_exception
loop.set_exception_handler(handler)
@classmethod
def _handle_exception(cls, loop, context):
#msg = context.get("exception", context["message"])
if not isinstance(context["exception"],CancelledError):
logger.error(f"Caught exception: {context}")
loop= asyncio.get_event_loop()
@@ -277,12 +329,12 @@ class LiveChatAsync:
@classmethod
async def shutdown(cls, event, sig = None, handler=None):
logger.debug("シャットダウンしています")
logger.debug("shutdown...")
tasks = [t for t in asyncio.all_tasks() if t is not
asyncio.current_task()]
[task.cancel() for task in tasks]
logger.debug(f"残っているタスクを終了しています")
logger.debug(f"complete remaining tasks...")
await asyncio.gather(*tasks,return_exceptions=True)
loop = asyncio.get_event_loop()
loop.stop()

View File

@@ -6,25 +6,35 @@ import signal
import time
import traceback
import urllib.parse
import warnings
from aiohttp.client_exceptions import ClientConnectorError
from concurrent.futures import CancelledError
from queue import Queue
from asyncio import Queue
from .buffer import Buffer
from ..parser.replay import Parser
from .. import config
from .. import mylogger
from ..exceptions import ChatParseException,IllegalFunctionCall
from ..paramgen import arcparam
from ..processors.default.processor import DefaultProcessor
from ..processors.combinator import Combinator
logger = mylogger.get_logger(__name__,mode=config.LOGGER_MODE)
MAX_RETRY = 10
logger = config.logger(__name__)
headers = config.headers
MAX_RETRY = 10
class ReplayChatAsync:
'''asyncio(aiohttp)を利用してYouTubeのチャットデータを取得する。
'''
### -----------------------------------------------------------
### [Warning] ReplayChatAsync is integrated into LiveChatAsync.
### This class is deprecated and will be removed at v0.0.5.0.
### ReplayChatAsyncはLiveChatAsyncに統合しました。
### このクラスはv0.0.5.0で廃止予定です。
### -----------------------------------------------------------
asyncio(aiohttp)を利用してYouTubeのチャットデータを取得する。
Parameter
---------
@@ -76,9 +86,18 @@ class ReplayChatAsync:
done_callback = None,
exception_handler = None,
direct_mode = False):
warnings.warn(""
f"\n{'-'*60}\n[WARNING] ReplayChatAsync is integrated "
f"into LiveChatAsync.\n{' '*5} This is deprecated and will"
f" be removed at v0.0.5.0.\n{'-'*60}\n"
)
self.video_id = video_id
self.seektime = seektime
self.processor = processor
if isinstance(processor, tuple):
self.processor = Combinator(processor)
else:
self.processor = processor
self._buffer = buffer
self._callback = callback
self._done_callback = done_callback
@@ -132,28 +151,9 @@ class ReplayChatAsync:
"""最初のcontinuationパラメータを取得し、
_listenループのタスクを作成し開始する
"""
initial_continuation = await self._get_initial_continuation()
if initial_continuation is None:
self.terminate()
logger.debug(f"[{self.video_id}]No initial continuation.")
return
initial_continuation = arcparam.getparam(self.video_id, self.seektime)
await self._listen(initial_continuation)
async def _get_initial_continuation(self):
''' チャットデータ取得に必要な最初のcontinuationを取得する。'''
try:
initial_continuation = arcparam.get(self.video_id,self.seektime)
except ChatParseException as e:
self.terminate()
logger.debug(f"[{self.video_id}]Error:{str(e)}")
return
except KeyError:
logger.debug(f"[{self.video_id}]KeyError:"
f"{traceback.format_exc(limit = -1)}")
self.terminate()
return
return initial_continuation
async def _listen(self, continuation):
''' continuationに紐付いたチャットデータを取得し
Bufferにチャットデータを格納、
@@ -168,11 +168,13 @@ class ReplayChatAsync:
async with aiohttp.ClientSession() as session:
while(continuation and self._is_alive):
if self._pauser.empty():
#pause
'''pause'''
await self._pauser.get()
#resume
#prohibit from blocking by putting None into _pauser.
'''resume:
prohibit from blocking by putting None into _pauser.
'''
self._pauser.put_nowait(None)
#when replay, not reacquire continuation param
livechat_json = (await
self._get_livechat_json(continuation, session, headers)
)
@@ -194,13 +196,16 @@ class ReplayChatAsync:
await asyncio.sleep(diff_time)
continuation = metadata.get('continuation')
except ChatParseException as e:
logger.info(f"{str(e)}video_id:\"{self.video_id}\"")
self.terminate()
logger.error(f"{str(e)}video_id:\"{self.video_id}\"")
return
except (TypeError , json.JSONDecodeError) :
self.terminate()
logger.error(f"{traceback.format_exc(limit = -1)}")
return
logger.debug(f"[{self.video_id}]チャット取得を終了しました。")
self.terminate()
async def _get_livechat_json(self, continuation, session, headers):
'''
@@ -256,14 +261,17 @@ class ReplayChatAsync:
"既にcallbackを登録済みのため、get()は実行できません。")
def pause(self):
if self._callback is None:
return
if not self._pauser.empty():
self._pauser.get()
self._pauser.get_nowait()
def resume(self):
if self._callback is None:
return
if self._pauser.empty():
self._pauser.put_nowait(None)
def is_alive(self):
return self._is_alive
@@ -282,17 +290,15 @@ class ReplayChatAsync:
if self._direct_mode == False:
#bufferにダミーオブジェクトを入れてis_alive()を判定させる
self._buffer.put_nowait({'chatdata':'','timeout':1})
logger.info(f'終了しました:[{self.video_id}]')
logger.info(f'[{self.video_id}]終了しました')
@classmethod
def _set_exception_handler(cls, handler):
loop = asyncio.get_event_loop()
#default handler: cls._handle_exception
loop.set_exception_handler(handler)
@classmethod
def _handle_exception(cls, loop, context):
#msg = context.get("exception", context["message"])
if not isinstance(context["exception"],CancelledError):
logger.error(f"Caught exception: {context}")
loop= asyncio.get_event_loop()

View File

@@ -7,18 +7,18 @@ import time
import traceback
import urllib.parse
from concurrent.futures import CancelledError, ThreadPoolExecutor
from queue import Queue
from .buffer import Buffer
from ..parser.live import Parser
from .. import config
from .. import mylogger
from ..exceptions import ChatParseException,IllegalFunctionCall
from ..paramgen import liveparam
from ..paramgen import liveparam, arcparam
from ..processors.default.processor import DefaultProcessor
from ..processors.combinator import Combinator
logger = mylogger.get_logger(__name__,mode=config.LOGGER_MODE)
MAX_RETRY = 10
logger = config.logger(__name__)
headers = config.headers
MAX_RETRY = 10
class LiveChat:
@@ -28,6 +28,11 @@ class LiveChat:
---------
video_id : str
動画ID
seektime : int
(ライブチャット取得時は無視)
取得開始するアーカイブ済みチャットの経過時間(秒)
マイナス値を指定した場合は、配信開始前のチャットも取得する。
processor : ChatProcessor
チャットデータを加工するオブジェクト
@@ -51,6 +56,10 @@ class LiveChat:
Trueの場合、callbackの設定が必須
(設定していない場合IllegalFunctionCall例外を発生させる
force_replay : bool
Trueの場合、ライブチャットが取得できる場合であっても
強制的にアーカイブ済みチャットを取得する。
Attributes
---------
_executor : ThreadPoolExecutor
@@ -64,23 +73,34 @@ class LiveChat:
#チャット監視中のListenerのリスト
_listeners= []
def __init__(self, video_id,
seektime = 0,
processor = DefaultProcessor(),
buffer = Buffer(maxsize = 20),
buffer = None,
interruptable = True,
callback = None,
done_callback = None,
direct_mode = False
direct_mode = False,
force_replay = False
):
self.video_id = video_id
self.processor = processor
self.seektime = seektime
if isinstance(processor, tuple):
self.processor = Combinator(processor)
else:
self.processor = processor
self._buffer = buffer
self._callback = callback
self._done_callback = done_callback
self._executor = ThreadPoolExecutor(max_workers=2)
self._direct_mode = direct_mode
self._is_alive = True
self._parser = Parser()
self._is_replay = force_replay
self._parser = Parser(is_replay = self._is_replay)
self._pauser = Queue()
self._pauser.put_nowait(None)
self._setup()
self._first_fetch = True
self._fetch_url = "live_chat/get_live_chat?continuation="
if not LiveChat._setup_finished:
LiveChat._setup_finished = True
@@ -91,11 +111,12 @@ class LiveChat:
LiveChat._listeners.append(self)
def _setup(self):
#logger.debug("setup")
#direct modeがTrueでcallback未設定の場合例外発生。
if self._direct_mode:
if self._callback is None:
raise IllegalFunctionCall(
"direct_mode=Trueの場合callbackの設定が必須です。")
"When direct_mode=True, callback parameter is required.")
else:
#direct modeがFalseでbufferが未設定ならばデフォルトのbufferを作成
if self._buffer is None:
@@ -115,48 +136,30 @@ class LiveChat:
listen_task.add_done_callback(self._done_callback)
def _startlisten(self):
"""最初のcontinuationパラメータを取得し、
_listenループのタスクを作成し開始する
time.sleep(0.1) #sleep shortly to prohibit skipping fetching data
"""Fetch first continuation parameter,
create and start _listen loop.
"""
initial_continuation = self._get_initial_continuation()
if initial_continuation is None:
self.terminate()
logger.debug(f"[{self.video_id}]No initial continuation.")
return
initial_continuation = liveparam.getparam(self.video_id,3)
self._listen(initial_continuation)
def _get_initial_continuation(self):
''' チャットデータ取得に必要な最初のcontinuationを取得する。'''
try:
initial_continuation = liveparam.getparam(self.video_id)
except ChatParseException as e:
self.terminate()
logger.debug(f"[{self.video_id}]Error:{str(e)}")
return
except KeyError:
logger.debug(f"[{self.video_id}]KeyError:"
f"{traceback.format_exc(limit = -1)}")
self.terminate()
return
return initial_continuation
def _listen(self, continuation):
''' continuationに紐付いたチャットデータを取得し
BUfferにチャットデータを格納、
次のcontinuaitonを取得してループする
''' Fetch chat data and store them into buffer,
get next continuaiton parameter and loop.
Parameter
---------
continuation : str
次のチャットデータ取得に必要なパラメータ
parameter for next chat data
'''
try:
with requests.Session() as session:
while(continuation and self._is_alive):
livechat_json = (
self._get_livechat_json(continuation, session, headers)
)
metadata, chatdata = self._parser.parse( livechat_json )
continuation = self._check_pause(continuation)
contents = self._get_contents(
continuation, session, headers)
metadata, chatdata = self._parser.parse(contents)
timeout = metadata['timeoutMs']/1000
chat_component = {
"video_id" : self.video_id,
@@ -171,33 +174,68 @@ class LiveChat:
else:
self._buffer.put(chat_component)
diff_time = timeout - (time.time()-time_mark)
if diff_time < 0 : diff_time=0
time.sleep(diff_time)
time.sleep(diff_time if diff_time > 0 else 0)
continuation = metadata.get('continuation')
except ChatParseException as e:
logger.info(f"{str(e)}video_id:\"{self.video_id}\"")
logger.debug(f"[{self.video_id}]{str(e)}")
return
except (TypeError , json.JSONDecodeError) :
logger.error(f"{traceback.format_exc(limit = -1)}")
return
logger.debug(f"[{self.video_id}]チャット取得を終了しました。")
logger.debug(f"[{self.video_id}]finished fetching chat.")
def _check_pause(self, continuation):
if self._pauser.empty():
'''pause'''
self._pauser.get()
'''resume:
prohibit from blocking by putting None into _pauser.
'''
self._pauser.put_nowait(None)
if not self._is_replay:
continuation = liveparam.getparam(self.video_id,3)
return continuation
def _get_contents(self, continuation, session, headers):
'''Get 'contents' dict from livechat json.
If contents is None at first fetching,
try to fetch archive chat data.
Return:
-------
'contents' dict which includes metadata & chatdata.
'''
livechat_json = (
self._get_livechat_json(continuation, session, headers)
)
contents = self._parser.get_contents(livechat_json)
if self._first_fetch:
if contents is None or self._is_replay:
'''Try to fetch archive chat data.'''
self._parser.is_replay = True
self._fetch_url = ("live_chat_replay/"
"get_live_chat_replay?continuation=")
continuation = arcparam.getparam(self.video_id, self.seektime)
livechat_json = ( self._get_livechat_json(
continuation, session, headers))
contents = self._parser.get_contents(livechat_json)
self._first_fetch = False
return contents
def _get_livechat_json(self, continuation, session, headers):
'''
チャットデータが格納されたjsonデータを取得する。
Get json which includes chat data.
'''
continuation = urllib.parse.quote(continuation)
livechat_json = None
status_code = 0
url =(
f"https://www.youtube.com/live_chat/get_live_chat?"
f"continuation={continuation}&pbj=1")
f"https://www.youtube.com/{self._fetch_url}{continuation}&pbj=1")
for _ in range(MAX_RETRY + 1):
with session.get(url ,headers = headers) as resp:
try:
text = resp.text
status_code = resp.status_code
livechat_json = json.loads(text)
break
except json.JSONDecodeError :
@@ -236,6 +274,21 @@ class LiveChat:
raise IllegalFunctionCall(
"既にcallbackを登録済みのため、get()は実行できません。")
def is_replay(self):
return self._is_replay
def pause(self):
if self._callback is None:
return
if not self._pauser.empty():
self._pauser.get()
def resume(self):
if self._callback is None:
return
if self._pauser.empty():
self._pauser.put_nowait(None)
def is_alive(self):
return self._is_alive
@@ -253,19 +306,11 @@ class LiveChat:
self._is_alive = False
if self._direct_mode == False:
#bufferにダミーオブジェクトを入れてis_alive()を判定させる
self._buffer.put({'chatdata':'','timeout':1})
logger.info(f'終了しました:[{self.video_id}]')
self._buffer.put({'chatdata':'','timeout':0})
logger.info(f'[{self.video_id}]finished.')
@classmethod
def shutdown(cls, event, sig = None, handler=None):
logger.debug("シャットダウンしています")
logger.debug("shutdown...")
for t in LiveChat._listeners:
t._is_alive = False
t._is_alive = False

View File

@@ -6,24 +6,32 @@ import signal
import time
import traceback
import urllib.parse
import warnings
from concurrent.futures import CancelledError, ThreadPoolExecutor
from queue import Queue
from .buffer import Buffer
from ..parser.replay import Parser
from .. import config
from .. import mylogger
from ..exceptions import ChatParseException,IllegalFunctionCall
from ..paramgen import arcparam
from ..processors.default.processor import DefaultProcessor
from ..processors.combinator import Combinator
logger = mylogger.get_logger(__name__,mode=config.LOGGER_MODE)
MAX_RETRY = 10
logger = config.logger(__name__)
headers = config.headers
MAX_RETRY = 10
class ReplayChat:
''' スレッドプールを利用してYouTubeのライブ配信のチャットデータを取得する
'''
### -----------------------------------------------------------
### [Warning] ReplayChat is integrated into LiveChat.
### This class is deprecated and will be removed at v0.0.5.0.
### ReplayChatはLiveChatに統合しました。
### このクラスはv0.0.5.0で廃止予定です。
### -----------------------------------------------------------
スレッドプールを利用してYouTubeのライブ配信のチャットデータを取得する
Parameter
---------
@@ -61,24 +69,35 @@ class ReplayChat:
チャットデータ取得ループ_listen用のスレッド
_is_alive : bool
チャット取得を終了したか
チャット取得を停止するためのフラグ
'''
_setup_finished = False
#チャット監視中のListenerのリスト
_listeners= []
def __init__(self, video_id,
seektime = 0,
processor = DefaultProcessor(),
buffer = Buffer(maxsize = 20),
buffer = None,
interruptable = True,
callback = None,
done_callback = None,
direct_mode = False
):
warnings.warn(""
f"\n{'-'*60}\n[WARNING] ReplayChat is integrated into LiveChat.\n"
f"{' '*5}This is deprecated and will be removed at v0.0.5.0.\n"
f"{'-'*60}\n"
)
self.video_id = video_id
self.seektime = seektime
self.processor = processor
if isinstance(processor, tuple):
self.processor = Combinator(processor)
else:
self.processor = processor
self._buffer = buffer
self._callback = callback
self._done_callback = done_callback
@@ -90,6 +109,7 @@ class ReplayChat:
self._pauser.put_nowait(None)
self._setup()
if not ReplayChat._setup_finished:
ReplayChat._setup_finished = True
if interruptable:
@@ -136,7 +156,7 @@ class ReplayChat:
def _get_initial_continuation(self):
''' チャットデータ取得に必要な最初のcontinuationを取得する。'''
try:
initial_continuation = arcparam.get(self.video_id,self.seektime)
initial_continuation = arcparam.getparam(self.video_id,self.seektime)
except ChatParseException as e:
self.terminate()
logger.debug(f"[{self.video_id}]Error:{str(e)}")
@@ -150,7 +170,7 @@ class ReplayChat:
def _listen(self, continuation):
''' continuationに紐付いたチャットデータを取得し
にチャットデータを格納、
BUfferにチャットデータを格納、
次のcontinuaitonを取得してループする
Parameter
@@ -189,9 +209,11 @@ class ReplayChat:
time.sleep(diff_time)
continuation = metadata.get('continuation')
except ChatParseException as e:
logger.error(f"{str(e)}動画ID:\"{self.video_id}\"")
self.terminate()
logger.error(f"{str(e)}video_id:\"{self.video_id}\"")
return
except (TypeError , json.JSONDecodeError) :
self.terminate()
logger.error(f"{traceback.format_exc(limit = -1)}")
return
@@ -220,6 +242,7 @@ class ReplayChat:
else:
logger.error(f"[{self.video_id}]"
f"Exceeded retry count. status_code={status_code}")
self.terminate()
return None
return livechat_json
@@ -266,7 +289,7 @@ class ReplayChat:
'''Listener終了時のコールバック'''
try:
self.terminate()
except CancelledError:
except RuntimeError:
logger.debug(f'[{self.video_id}]cancelled:{sender}')
def terminate(self):
@@ -277,7 +300,7 @@ class ReplayChat:
if self._direct_mode == False:
#bufferにダミーオブジェクトを入れてis_alive()を判定させる
self._buffer.put({'chatdata':'','timeout':1})
logger.info(f'終了しました:[{self.video_id}]')
logger.info(f'[{self.video_id}]終了しました')
@classmethod
def shutdown(cls, event, sig = None, handler=None):

View File

@@ -52,32 +52,18 @@ def _nval(val):
buf += val.to_bytes(1,'big')
return buf
def _tzparity(video_id,times):
t=0
for i,s in enumerate(video_id):
ss = ord(s)
if(ss % 2 == 0):
t += ss*(12-i)
else:
t ^= ss*i
return ((times^t) % 2).to_bytes(1,'big')
def get(video_id, seektime = 0, topchatonly = False):
def _build(video_id, seektime, topchatonly = False):
switch_01 = b'\x04' if topchatonly else b'\x01'
if seektime < 0:
raise ValueError('seektime is 0 or positive number.')
if seektime == 0:
times =_nval(1)
times =_nval(0)
switch = b'\x04'
elif seektime == 0:
times =_nval(1)
switch = b'\x03'
else:
times =_nval(int(seektime*1000000))
switch = b'\x03'
parity = _tzparity(video_id, seektime)
parity = b'\x00'
header_magic= b'\xA2\x9D\xB0\xD3\x04'
sep_0 = b'\x1A'
@@ -88,8 +74,8 @@ def get(video_id, seektime = 0, topchatonly = False):
sep_2 = b'\x52\x1C\x08\x00\x10\x00\x18\x00\x20\x00'
chkstr = b'\x2A\x0E\x73\x74\x61\x74\x69\x63\x63\x68\x65\x63\x6B\x73\x75\x6D\x40'
sep_3 = b'\x00\x58\x03\x60'
sep_4 = b'\x68'+parity+b'\x72\x04\x08'
sep_5 = b'\x10'+parity+b'\x78\x00'
sep_4 = b'\x68' + parity + b'\x72\x04\x08'
sep_5 = b'\x10' + parity + b'\x78\x00'
body = [
sep_0,
_nval(len(vid)),
@@ -116,5 +102,12 @@ def get(video_id, seektime = 0, topchatonly = False):
).decode()
)
def getparam(video_id, seektime = 0):
'''
Parameter
---------
seektime : int
unit:seconds
start position of fetching chat data.
'''
return _build(video_id, seektime)

View File

@@ -155,7 +155,7 @@ def _times(past_sec):
return list(map(lambda x:int(x*1000000),[_ts1,_ts2,_ts3,_ts4,_ts5]))
def getparam(video_id,past_sec = 60):
def getparam(video_id,past_sec = 0):
'''
Parameter
---------

View File

@@ -1,66 +1,91 @@
"""
pytchat.parser.live
~~~~~~~~~~~~~~~~~~~
This module is parser of live chat JSON.
Parser of live chat JSON.
"""
import json
from .. import config
from .. import mylogger
from .. exceptions import (
ResponseContextError,
NoContentsException,
NoContinuationsException )
NoContinuationsException,
ChatParseException )
logger = mylogger.get_logger(__name__,mode=config.LOGGER_MODE)
logger = config.logger(__name__)
class Parser:
def parse(self, jsn):
"""
このparse関数はLiveChat._listen() 関数から定期的に呼び出される。
引数jsnはYoutubeから取得したチャットデータの生JSONであり、
このparse関数によって与えられたJSONを以下に分割して返す。
+ timeout (次のチャットデータ取得までのインターバル)
+ chat dataチャットデータ本体
+ continuation (次のチャットデータ取得に必要となるパラメータ).
__slots__ = ['is_replay']
def __init__(self, is_replay):
self.is_replay = is_replay
def get_contents(self, jsn):
if jsn is None:
raise ChatParseException('Called with none JSON object.')
if jsn['response']['responseContext'].get('errors'):
raise ResponseContextError('The video_id would be wrong, or video is deleted or private.')
contents=jsn['response'].get('continuationContents')
return contents
def parse(self, contents):
"""
Parameter
----------
+ jsn : dict
+ Youtubeから取得したチャットデータのJSONオブジェクト。
pythonの辞書形式に変換済みの状態で渡される
+ contents : dict
+ JSON of chat data from YouTube.
Returns
-------
tuple:
+ metadata : dict
+ チャットデータに付随するメタデータ。timeout、 動画ID、continuationパラメータで構成される。
+ chatdata : list[dict]
+ チャットデータ本体のリスト。
+ timeout
+ video_id
+ continuation
+ chatdata : List[dict]
"""
if jsn is None:
return {'timeoutMs':0,'continuation':None},[]
if jsn['response']['responseContext'].get('errors'):
raise ResponseContextError('動画に接続できません。'
'動画IDが間違っているか、動画が削除非公開の可能性があります。')
contents=jsn['response'].get('continuationContents')
#配信が終了した場合、もしくはチャットデータが取得できない場合
if contents is None:
raise NoContentsException('チャットデータを取得できませんでした。')
'''Broadcasting end or cannot fetch chat stream'''
raise NoContentsException('Chat data stream is empty.')
cont = contents['liveChatContinuation']['continuations'][0]
if cont is None:
raise NoContinuationsException('Continuationがありません。')
raise NoContinuationsException('No Continuation')
metadata = (cont.get('invalidationContinuationData') or
cont.get('timedContinuationData') or
cont.get('reloadContinuationData')
cont.get('reloadContinuationData') or
cont.get('liveChatReplayContinuationData')
)
if metadata is None:
if cont.get("playerSeekContinuationData"):
raise ChatParseException('Finished chat data')
unknown = list(cont.keys())[0]
if unknown:
logger.error(f"Received unknown continuation type:{unknown}")
logger.debug(f"Received unknown continuation type:{unknown}")
metadata = cont.get(unknown)
metadata.setdefault('timeoutMs', 10000)
chatdata = contents['liveChatContinuation'].get('actions')
else:
raise ChatParseException('Cannot extract continuation data')
return self._create_data(metadata, contents)
def _create_data(self, metadata, contents):
actions = contents['liveChatContinuation'].get('actions')
if self.is_replay:
interval = self._get_interval(actions)
metadata.setdefault("timeoutMs",interval)
"""Archived chat has different structures than live chat,
so make it the same format."""
chatdata = [action["replayChatItemAction"]["actions"][0] for action in actions]
else:
metadata.setdefault('timeoutMs', 10000)
chatdata = actions
return metadata, chatdata
def _get_interval(self, actions: list):
if actions is None:
return 0
start = int(actions[0]["replayChatItemAction"]["videoOffsetTimeMsec"])
last = int(actions[-1]["replayChatItemAction"]["videoOffsetTimeMsec"])
return (last - start)

View File

@@ -1,14 +1,12 @@
import json
from .. import config
from .. import mylogger
from .. exceptions import (
ResponseContextError,
NoContentsException,
NoContinuationsException )
logger = mylogger.get_logger(__name__,mode=config.LOGGER_MODE)
logger = config.logger(__name__)
class Parser:
def parse(self, jsn):
@@ -53,12 +51,13 @@ class Parser:
metadata = cont.get('liveChatReplayContinuationData')
if metadata is None:
unknown = list(cont.keys())[0]
if unknown:
if unknown != "playerSeekContinuationData":
logger.debug(f"Received unknown continuation type:{unknown}")
metadata = cont.get(unknown)
actions = contents['liveChatContinuation'].get('actions')
if actions is None:
raise NoContentsException('チャットデータを取得できませんでした。')
#後続のチャットデータなし
return {"continuation":None,"timeout":0,"chatdata":[]}
interval = self.get_interval(actions)
metadata.setdefault("timeoutMs",interval)
"""アーカイブ済みチャットはライブチャットと構造が異なっているため、以下の行により

View File

@@ -10,14 +10,14 @@ class ChatProcessor:
Parameter
----------
chat_components: [LIST:component]
chat_components: List[component]
component : dict {
"video_id" : str
動画ID
"timeout" : int
次のチャットの再読み込みまでの時間(秒)
"chatdata" : list<object>
チャットデータactionsのリスト
"chatdata" : List[dict]
チャットデータのリスト
}
'''
pass

View File

@@ -0,0 +1,39 @@
from .chat_processor import ChatProcessor
class Combinator(ChatProcessor):
'''
Combinator combines multiple chat processors.
Specify processors as tuple at `processor` params of LiveChat object.
For example:
[constructor]
chat = LiveChat("video_id", processor = ( Processor1(), Processor2(), Processor3() ) )
[receive return values]
ret1, ret2, ret3 = chat.get()
The return values are tuple of processed chat data,
the order of return depends on parameter order.
Parameter
---------
processors : Tuple[ChatProcessor]
multiple processors for processing chat data
'''
def __init__(self, processors: tuple):
self.processors = processors
def process(self, chat_components: list):
'''
Called from LiveChat.get() function by user,
or LiveChat._listen() automatically.
Returns
-------
Tuple of chat data processed by each chat processor.
'''
return tuple(processor.process(chat_components)
for processor in self.processors)

View File

@@ -4,11 +4,11 @@ from .renderer.textmessage import LiveChatTextMessageRenderer
from .renderer.paidmessage import LiveChatPaidMessageRenderer
from .renderer.paidsticker import LiveChatPaidStickerRenderer
from .renderer.legacypaid import LiveChatLegacyPaidMessageRenderer
from ... import mylogger
from .. chat_processor import ChatProcessor
from ... import config
logger = mylogger.get_logger(__name__,mode=config.LOGGER_MODE)
logger = config.logger(__name__)
class CompatibleProcessor:
class CompatibleProcessor(ChatProcessor):
def process(self, chat_components: list):

View File

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

View File

@@ -4,9 +4,9 @@ from .renderer.textmessage import LiveChatTextMessageRenderer
from .renderer.paidmessage import LiveChatPaidMessageRenderer
from .renderer.paidsticker import LiveChatPaidStickerRenderer
from .renderer.legacypaid import LiveChatLegacyPaidMessageRenderer
from .. chat_processor import ChatProcessor
from ... import config
from ... import mylogger
logger = mylogger.get_logger(__name__,mode=config.LOGGER_MODE)
logger = config.logger(__name__)
class Chatdata:
def __init__(self,chatlist:list, timeout:float):
@@ -25,7 +25,7 @@ class Chatdata:
return
await asyncio.sleep(self.interval/len(self.items))
class DefaultProcessor:
class DefaultProcessor(ChatProcessor):
def process(self, chat_components: list):
chatlist = []

View File

@@ -19,8 +19,7 @@ class BaseRenderer:
else:
self.elapsedTime = ""
self.datetime = self.get_datetime(timestampUsec)
self.message = self.get_message(self.renderer)
self.messageEx = self.get_message_ex(self.renderer)
self.message ,self.messageEx = self.get_message(self.renderer)
self.id = self.renderer.get('id')
self.amountValue= 0.0
self.amountString = ""
@@ -44,6 +43,7 @@ class BaseRenderer:
def get_message(self,renderer):
message = ''
message_ex = []
if renderer.get("message"):
runs=renderer["message"].get("runs")
if runs:
@@ -51,22 +51,13 @@ class BaseRenderer:
if r:
if r.get('emoji'):
message += r['emoji'].get('shortcuts',[''])[0]
message_ex.append(r['emoji']['image']['thumbnails'][1].get('url'))
else:
message += r.get('text','')
return message
message_ex.append(r.get('text',''))
return message, message_ex
def get_message_ex(self,renderer):
message = []
if renderer.get("message"):
runs=renderer["message"].get("runs")
if runs:
for r in runs:
if r:
if r.get('emoji'):
message.append(r['emoji']['image']['thumbnails'][1].get('url'))
else:
message.append(r.get('text',''))
return message
def get_badges(self,renderer):
isVerified = False

View File

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

View File

@@ -0,0 +1,8 @@
from .chat_processor import ChatProcessor
class DummyProcessor(ChatProcessor):
'''
Dummy processor just returns received chat_components directly.
'''
def process(self, chat_components: list):
return chat_components

View File

@@ -1,10 +1,10 @@
"""
speedmeter.py
speed_calculator.py
チャットの勢いを算出するChatProcessor
Calculate speed of chat.
"""
import calendar, datetime, pytz
from .chat_processor import ChatProcessor
class RingQueue:
"""
リング型キュー
@@ -21,7 +21,7 @@ class RingQueue:
キュー内に余裕があるか。キュー内のアイテム個数が、キューの最大個数未満であればTrue。
"""
def __init__(self, capacity = 10):
def __init__(self, capacity):
"""
コンストラクタ
@@ -77,42 +77,47 @@ class RingQueue:
def item_count(self):
return len(self.items)
class SpeedCalculator(RingQueue):
class SpeedCalculator(ChatProcessor, RingQueue):
"""
チャットの勢いを計算するクラス
チャットの勢いを計算する
一定期間のチャットデータのうち、最初のチャットの投稿時刻と
最後のチャットの投稿時刻の差を、チャット数で割り返し
1分あたりの速度に換算する。
Parameter
----------
格納するチャットブロックの数
capacity : int
RingQueueに格納するチャット勢い算出用データの最大数
"""
def __init__(self, capacity, video_id):
def __init__(self, capacity = 10):
super().__init__(capacity)
self.video_id=video_id
self.speed = 0
def process(self, chat_components: list):
chatdata = []
if chat_components:
for component in chat_components:
chatdata = component.get('chatdata')
if chatdata is None:
return self.speed
self.speed = self.calc(chatdata)
return self.speed
if component.get("chatdata"):
chatdata.extend(component.get("chatdata"))
def _value(self):
self._put_chatdata(chatdata)
self.speed = self._calc_speed()
return self.speed
def _calc_speed(self):
"""
ActionsQueue内のチャットデータリストから
RingQueue内のチャット勢い算出用データリストを元に
チャット速度を計算して返す
Return
---------------------------
チャット速度(1分間で換算したチャット数)
チャット速度(1分間で換算したチャット数)
"""
try:
#キュー内のactionsの総チャット数
#キュー内の総チャット数
total = sum(item['chat_count'] for item in self.items)
#キュー内の最初と最後のチャットの時間差
duration = (self.items[self.last_pos]['endtime']
@@ -123,24 +128,20 @@ class SpeedCalculator(RingQueue):
except IndexError:
return 0
def _get_timestamp(self, action :dict):
def _put_chatdata(self, actions):
"""
チャットデータのtimestampUsecを読み取る
liveChatTickerSponsorItemRenderer等のtickerデータは時刻格納位置が
異なるため、時刻データなしとして扱う
チャットデータからタイムスタンプを読み取り、勢い測定用のデータを組み立て、
RingQueueに投入する。
200円以上のスパチャはtickerとmessageの2つのデータが生成されるが、
tickerの方は時刻データの場所が異なることを利用し、勢いの集計から除外している。
Parameter
---------
actions : List[dict]
チャットデータ(addChatItemAction) のリスト
"""
try:
item = action['addChatItemAction']['item']
timestamp = int(item[list(item.keys())[0]]['timestampUsec'])
except (KeyError,TypeError):
return None
return timestamp
def calc(self,actions):
def empty_data():
def _put_emptydata():
'''
データがない場合にゼロのデータをリングキューに入れる
チャットデータがない場合にのデータをキューに投入する。
'''
timestamp_now = calendar.timegm(datetime.datetime.
now(pytz.utc).utctimetuple())
@@ -149,12 +150,23 @@ class SpeedCalculator(RingQueue):
'starttime':int(timestamp_now),
'endtime':int(timestamp_now)
})
return self._value()
def _get_timestamp(action :dict):
"""
チャットデータから時刻データを取り出す。
"""
try:
item = action['addChatItemAction']['item']
timestamp = int(item[list(item.keys())[0]]['timestampUsec'])
except (KeyError,TypeError):
return None
return timestamp
if actions is None or len(actions)==0:
return empty_data
_put_emptydata()
return
#actions内の時刻データを持つチャットデータの数tickerは除く
#actions内の時刻データを持つチャットデータの数
counter=0
#actions内の最初のチャットデータの時刻
starttime= None
@@ -163,7 +175,7 @@ class SpeedCalculator(RingQueue):
for action in actions:
#チャットデータからtimestampUsecを読み取る
gettime = self._get_timestamp(action)
gettime = _get_timestamp(action)
#時刻のないデータだった場合は次の行のデータで読み取り試行
if gettime is None:
@@ -177,11 +189,12 @@ class SpeedCalculator(RingQueue):
endtime = gettime
#チャットの数をインクリメント
counter+=1
counter += 1
#チャット速度用のデータをリングキューに送る
#チャット速度用のデータをRingQueueに送る
if starttime is None or endtime is None:
return empty_data
_put_emptydata()
return
self.put({
'chat_count':counter,
@@ -189,4 +202,3 @@ class SpeedCalculator(RingQueue):
'endtime':int(endtime/1000000)
})
return self._value()

View File

@@ -9,7 +9,7 @@ def download(url):
json.dump(html.json(),f,ensure_ascii=False)
def save(data,filename):
with open(str(datetime.datetime.now().strftime('%Y-%m-%d %H-%M-%S')
)+filename,mode ='w',encoding='utf-8') as f:
def save(data,filename,extention):
with open(filename+"_"+(datetime.datetime.now().strftime('%Y-%m-%d %H-%M-%S')
)+extention,mode ='w',encoding='utf-8') as f:
f.writelines(data)

View File

@@ -1,6 +1,6 @@
from setuptools import setup, find_packages, Command
from codecs import open
from os import path, system
#from codecs import open as open_c
from os import path, system, remove, rename
import re
package_name = "pytchat"
@@ -28,6 +28,16 @@ assert author
assert author_email
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:
long_description = f.read()

View File

@@ -5,16 +5,16 @@ import requests, json
from pytchat.paramgen import arcparam
def test_arcparam_0(mocker):
param = arcparam.get("01234567890")
assert "op2w0wRyGjxDZzhhRFFvTE1ERXlNelExTmpjNE9UQWFFLXFvM2JrQkRRb0xNREV5TXpRMU5qYzRPVEFnQVElM0QlM0QoATAAOABAAEgEUhwIABAAGAAgACoOc3RhdGljY2hlY2tzdW1AAFgDYAFoAXIECAEQAXgA" == param
param = arcparam.getparam("01234567890",-1)
assert "op2w0wRyGjxDZzhhRFFvTE1ERXlNelExTmpjNE9UQWFFLXFvM2JrQkRRb0xNREV5TXpRMU5qYzRPVEFnQVElM0QlM0QoADAAOABAAEgEUhwIABAAGAAgACoOc3RhdGljY2hlY2tzdW1AAFgDYAFoAHIECAEQAHgA" == param
def test_arcparam_1(mocker):
param = arcparam.get("01234567890", seektime = 100000)
assert "op2w0wR3GjxDZzhhRFFvTE1ERXlNelExTmpjNE9UQWFFLXFvM2JrQkRRb0xNREV5TXpRMU5qYzRPVEFnQVElM0QlM0QogNDbw_QCMAA4AEAASANSHAgAEAAYACAAKg5zdGF0aWNjaGVja3N1bUAAWANgAWgBcgQIARABeAA%3D" == param
param = arcparam.getparam("01234567890", seektime = 100000)
assert "op2w0wR3GjxDZzhhRFFvTE1ERXlNelExTmpjNE9UQWFFLXFvM2JrQkRRb0xNREV5TXpRMU5qYzRPVEFnQVElM0QlM0QogNDbw_QCMAA4AEAASANSHAgAEAAYACAAKg5zdGF0aWNjaGVja3N1bUAAWANgAWgAcgQIARAAeAA%3D" == param
def test_arcparam_2(mocker):
param = arcparam.get("SsjCnHOk-Sk")
param = arcparam.getparam("SsjCnHOk-Sk")
url=f"https://www.youtube.com/live_chat_replay/get_live_chat_replay?continuation={param}&pbj=1"
resp = requests.Session().get(url,headers = config.headers)
jsn = json.loads(resp.text)
@@ -23,4 +23,7 @@ def test_arcparam_2(mocker):
test_id = chatdata[0]["addChatItemAction"]["item"]["liveChatTextMessageRenderer"]["id"]
print(test_id)
assert "CjoKGkNMYXBzZTdudHVVQ0Zjc0IxZ0FkTnFnQjVREhxDSnlBNHV2bnR1VUNGV0dnd2dvZDd3NE5aZy0w" == test_id
def test_arcparam_3(mocker):
param = arcparam.getparam("01234567890")
assert "op2w0wRyGjxDZzhhRFFvTE1ERXlNelExTmpjNE9UQWFFLXFvM2JrQkRRb0xNREV5TXpRMU5qYzRPVEFnQVElM0QlM0QoATAAOABAAEgDUhwIABAAGAAgACoOc3RhdGljY2hlY2tzdW1AAFgDYAFoAHIECAEQAHgA" == param

View File

@@ -12,7 +12,7 @@ from pytchat.processors.compatible.renderer.paidmessage import LiveChatPaidMessa
from pytchat.processors.compatible.renderer.paidsticker import LiveChatPaidStickerRenderer
from pytchat.processors.compatible.renderer.legacypaid import LiveChatLegacyPaidMessageRenderer
parser = Parser()
parser = Parser(is_replay=False)
def test_textmessage(mocker):
'''api互換processorのテスト通常テキストメッセージ'''
@@ -20,7 +20,7 @@ def test_textmessage(mocker):
_json = _open_file("tests/testdata/compatible/textmessage.json")
_, chatdata = parser.parse(json.loads(_json))
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
data = {
"video_id" : "",
"timeout" : 7,
@@ -57,7 +57,7 @@ def test_newsponcer(mocker):
_json = _open_file("tests/testdata/compatible/newSponsor.json")
_, chatdata = parser.parse(json.loads(_json))
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
data = {
"video_id" : "",
"timeout" : 7,
@@ -93,7 +93,7 @@ def test_superchat(mocker):
_json = _open_file("tests/testdata/compatible/superchat.json")
_, chatdata = parser.parse(json.loads(_json))
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
data = {
"video_id" : "",
"timeout" : 7,
@@ -124,6 +124,21 @@ def test_superchat(mocker):
assert "LCC." in ret["items"][0]["id"]
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):
with open(path,mode ='r',encoding = 'utf-8') as f:

125
tests/test_livechat_2.py Normal file
View File

@@ -0,0 +1,125 @@
import asyncio, aiohttp
import json
import pytest
import re
import requests
import sys
import time
from aioresponses import aioresponses
from concurrent.futures import CancelledError
from unittest import TestCase
from pytchat.core_multithread.livechat import LiveChat
from pytchat.core_async.livechat import LiveChatAsync
from pytchat.exceptions import (
NoLivechatRendererException,NoYtinitialdataException,
ResponseContextError,NoContentsException)
from pytchat.parser.live import Parser
from pytchat.processors.dummy_processor import DummyProcessor
def _open_file(path):
with open(path,mode ='r',encoding = 'utf-8') as f:
return f.read()
@aioresponses()
def test_async_live_stream(*mock):
async def test_loop(*mock):
pattern = re.compile(r'^https://www.youtube.com/live_chat/get_live_chat\?continuation=.*$')
_text = _open_file('tests/testdata/test_stream.json')
mock[0].get(pattern, status=200, body=_text)
chat = LiveChatAsync(video_id='', processor = DummyProcessor())
chats = await chat.get()
rawdata = chats[0]["chatdata"]
#assert fetching livachat data
assert list(rawdata[0]["addChatItemAction"]["item"].keys())[0] == "liveChatTextMessageRenderer"
assert list(rawdata[1]["addChatItemAction"]["item"].keys())[0] == "liveChatTextMessageRenderer"
assert list(rawdata[2]["addChatItemAction"]["item"].keys())[0] == "liveChatPlaceholderItemRenderer"
assert list(rawdata[3]["addLiveChatTickerItemAction"]["item"].keys())[0] == "liveChatTickerPaidMessageItemRenderer"
assert list(rawdata[4]["addChatItemAction"]["item"].keys())[0] == "liveChatPaidMessageRenderer"
assert list(rawdata[5]["addChatItemAction"]["item"].keys())[0] == "liveChatPaidStickerRenderer"
assert list(rawdata[6]["addLiveChatTickerItemAction"]["item"].keys())[0] == "liveChatTickerSponsorItemRenderer"
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(test_loop(*mock))
except CancelledError:
assert True
@aioresponses()
def test_async_replay_stream(*mock):
async def test_loop(*mock):
pattern_live = re.compile(r'^https://www.youtube.com/live_chat/get_live_chat\?continuation=.*$')
pattern_replay = re.compile(r'^https://www.youtube.com/live_chat_replay/get_live_chat_replay\?continuation=.*$')
#empty livechat -> switch to fetch replaychat
_text_live = _open_file('tests/testdata/finished_live.json')
_text_replay = _open_file('tests/testdata/chatreplay.json')
mock[0].get(pattern_live, status=200, body=_text_live)
mock[0].get(pattern_replay, status=200, body=_text_replay)
chat = LiveChatAsync(video_id='', processor = DummyProcessor())
chats = await chat.get()
rawdata = chats[0]["chatdata"]
#assert fetching replaychat data
assert list(rawdata[0]["addChatItemAction"]["item"].keys())[0] == "liveChatTextMessageRenderer"
assert list(rawdata[14]["addChatItemAction"]["item"].keys())[0] == "liveChatPaidMessageRenderer"
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(test_loop(*mock))
except CancelledError:
assert True
@aioresponses()
def test_async_force_replay(*mock):
async def test_loop(*mock):
pattern_live = re.compile(r'^https://www.youtube.com/live_chat/get_live_chat\?continuation=.*$')
pattern_replay = re.compile(r'^https://www.youtube.com/live_chat_replay/get_live_chat_replay\?continuation=.*$')
#valid live data, but force_replay = True
_text_live = _open_file('tests/testdata/test_stream.json')
#valid replay data
_text_replay = _open_file('tests/testdata/chatreplay.json')
mock[0].get(pattern_live, status=200, body=_text_live)
mock[0].get(pattern_replay, status=200, body=_text_replay)
#force replay
chat = LiveChatAsync(video_id='', processor = DummyProcessor(), force_replay = True)
chats = await chat.get()
rawdata = chats[0]["chatdata"]
# assert fetching replaychat data
assert list(rawdata[14]["addChatItemAction"]["item"].keys())[0] == "liveChatPaidMessageRenderer"
# assert not mix livechat data
assert list(rawdata[2]["addChatItemAction"]["item"].keys())[0] != "liveChatPlaceholderItemRenderer"
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(test_loop(*mock))
except CancelledError:
assert True
def test_multithread_live_stream(mocker):
_text = _open_file('tests/testdata/test_stream.json')
responseMock = mocker.Mock()
responseMock.status_code = 200
responseMock.text = _text
mocker.patch('requests.Session.get').return_value.__enter__.return_value = responseMock
chat = LiveChat(video_id='test_id', processor = DummyProcessor())
chats = chat.get()
rawdata = chats[0]["chatdata"]
#assert fetching livachat data
assert list(rawdata[0]["addChatItemAction"]["item"].keys())[0] == "liveChatTextMessageRenderer"
assert list(rawdata[1]["addChatItemAction"]["item"].keys())[0] == "liveChatTextMessageRenderer"
assert list(rawdata[2]["addChatItemAction"]["item"].keys())[0] == "liveChatPlaceholderItemRenderer"
assert list(rawdata[3]["addLiveChatTickerItemAction"]["item"].keys())[0] == "liveChatTickerPaidMessageItemRenderer"
assert list(rawdata[4]["addChatItemAction"]["item"].keys())[0] == "liveChatPaidMessageRenderer"
assert list(rawdata[5]["addChatItemAction"]["item"].keys())[0] == "liveChatPaidStickerRenderer"
assert list(rawdata[6]["addLiveChatTickerItemAction"]["item"].keys())[0] == "liveChatTickerSponsorItemRenderer"
chat.terminate()

View File

@@ -11,7 +11,7 @@ from pytchat.exceptions import (
def _open_file(path):
with open(path,mode ='r',encoding = 'utf-8') as f:
return f.read()
parser = Parser()
parser = Parser(is_replay = False)
@aioresponses()
def test_finishedlive(*mock):
@@ -21,7 +21,7 @@ def test_finishedlive(*mock):
_text = json.loads(_text)
try:
parser.parse(_text)
parser.parse(parser.get_contents(_text))
assert False
except NoContentsException:
assert True
@@ -34,7 +34,7 @@ def test_parsejson(*mock):
_text = json.loads(_text)
try:
parser.parse(_text)
parser.parse(parser.get_contents(_text))
jsn = _text
timeout = jsn["response"]["continuationContents"]["liveChatContinuation"]["continuations"][0]["timedContinuationData"]["timeoutMs"]
continuation = jsn["response"]["continuationContents"]["liveChatContinuation"]["continuations"][0]["timedContinuationData"]["continuation"]

View File

@@ -0,0 +1,68 @@
import json
import pytest
import asyncio,aiohttp
from pytchat.parser.live import Parser
from pytchat.processors.compatible.processor import CompatibleProcessor
from pytchat.exceptions import (
NoLivechatRendererException,NoYtinitialdataException,
ResponseContextError, NoContentsException)
from pytchat.processors.speed_calculator import SpeedCalculator
parser = Parser(is_replay =False)
def test_speed_1(mocker):
'''test speed calculation with normal json.
test json has 15 chatdata, duration is 30 seconds,
so the speed of chatdata is 30 chats/minute.
'''
processor = SpeedCalculator(capacity=30)
_json = _open_file("tests/testdata/speed/speedtest_normal.json")
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
data = {
"video_id" : "",
"timeout" : 10,
"chatdata" : chatdata
}
ret = processor.process([data])
assert 30 == ret
def test_speed_2(mocker):
'''test speed calculation with no valid chat data.
'''
processor = SpeedCalculator(capacity=30)
_json = _open_file("tests/testdata/speed/speedtest_undefined.json")
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
data = {
"video_id" : "",
"timeout" : 10,
"chatdata" : chatdata
}
ret = processor.process([data])
assert 0 == ret
def test_speed_3(mocker):
'''test speed calculation with empty data.
'''
processor = SpeedCalculator(capacity=30)
_json = _open_file("tests/testdata/speed/speedtest_empty.json")
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
data = {
"video_id" : "",
"timeout" : 10,
"chatdata" : chatdata
}
ret = processor.process([data])
assert 0 == ret
def _open_file(path):
with open(path,mode ='r',encoding = 'utf-8') as f:
return f.read()

164
tests/testdata/chat.json vendored Normal file
View File

@@ -0,0 +1,164 @@
{
"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": {
"liveChatTextMessageRenderer": {
"message": {
"runs": [{
"text": "message"
}]
},
"authorName": {
"simpleText": "authorName"
},
"authorPhoto": {
"thumbnails": [{
"url": "https://yt3.ggpht.com/photo.jpg",
"width": 32,
"height": 32
}, {
"url": "https://yt3.ggpht.com/photo.jpg",
"width": 64,
"height": 64
}]
},
"contextMenuEndpoint": {
"commandMetadata": {
"webCommandMetadata": {
"ignoreNavigation": true
}
},
"liveChatItemContextMenuEndpoint": {
"params": "params"
}
},
"id": "id",
"timestampUsec": "1576851922945411",
"authorBadges": [{
"liveChatAuthorBadgeRenderer": {
"customThumbnail": {
"thumbnails": [{
"url": "https://yt3.ggpht.com/photo.jpg"
}, {
"url": "https://yt3.ggpht.com/photo.jpg"
}]
},
"tooltip": "メンバー6 か月)",
"accessibility": {
"accessibilityData": {
"label": "メンバー6 か月)"
}
}
}
}],
"authorExternalChannelId": "UC",
"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"
}
}
}
}

View File

@@ -1 +1,112 @@
{"csn":"zeiIXfXHJYOA1d8Pyuaw4A4","response":{"responseContext":{"serviceTrackingParams":[{"service":"CSI","params":[{"key":"GetLiveChat_rid","value":"0x96761cd683987638"},{"key":"c","value":"WEB"},{"key":"cver","value":"2.20190920.05.01"},{"key":"yt_li","value":"0"}]},{"service":"GFEEDBACK","params":[{"key":"e","value":"23744176,23757412,23788838,23788875,23793834,23804281,23808952,23818920,23828084,23828243,23829335,23832543,23835014,23836965,23837741,23837772,23837957,23837993,23838272,23838302,23838823,23838823,23839284,23839362,23840216,23840243,23841118,23842662,23842986,23843283,23843289,23843534,23844042,24630096,9449243,9471235"},{"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.20190920"},{"key":"innertube.build.changelist","value":"270293990"},{"key":"innertube.build.experiments.source_version","value":"270377311"},{"key":"innertube.build.label","value":"youtube.ytfe.innertube_20190920_5_RC0"},{"key":"innertube.build.timestamp","value":"1568999515"},{"key":"innertube.build.variants.checksum","value":"669625af1d321c1e95dffac8db989afa"},{"key":"innertube.run.job","value":"ytfe-innertube-replica-only.ytfe"}]}],"webResponseContextExtensionData":{"ytConfigData":{"csn":"zeiIXfXHJYOA1d8Pyuaw4A4","visitorData":"CgtLWW1kYjAxZTBaRSjN0aPsBQ%3D%3D"}}}},"xsrf_token":"QUFFLUhqbnhXaGhpblNhWmEzdjJJR2JNeW02M01PQ0p6Z3xBQ3Jtc0ttekpfU1dhZlA4ZWJhSGNrOFN5ZGFFSmNSMjBWRERWYUtOSS03RG5sbDRaa01KWmZFd2pPZzNEdW10WThmUXRiQjRKQ1ZPUkd1b09nT0k5dEZJTGdFYWxEVGNOWkUzcGNEQjdTNnN2OTRjN1Qtc0haZlpSWGlxd1k4LUdnVEhVb1FtMW8yZHJfankzN1JhUFo3aFZvS0s4NkIzTGc=","url":"\/live_chat\/get_live_chat?continuation=0ofMyAORAhqsAUNqZ0tEUW9MWjAwdGEwMWFaMmRxY2xrcUp3b1lWVU53VGtneVdtc3laM2N6U2tKcVYwRkxVM2xhWTFGUkVndG5UUzFyVFZwbloycHlXUnBEcXJuQnZRRTlDanRvZEhSd2N6b3ZMM2QzZHk1NWIzVjBkV0psTG1OdmJTOXNhWFpsWDJOb1lYUV9kajFuVFMxclRWcG5aMnB5V1NacGMxOXdiM0J2ZFhROU1TQUMo5-aA_KPn5AIwADgAQAJKKwgAEAAYACAAKg5zdGF0aWNjaGVja3N1bToAQABKAggBUMKMlt2k5-QCWANQ5KzA_KPn5AJYt7rZo9Tm5AJoAYIBAggBiAEAoAHorZb_pOfkAg%253D%253D","endpoint":{"commandMetadata":{"webCommandMetadata":{"url":"/live_chat/get_live_chat?continuation=0ofMyAORAhqsAUNqZ0tEUW9MWjAwdGEwMWFaMmRxY2xrcUp3b1lWVU53VGtneVdtc3laM2N6U2tKcVYwRkxVM2xhWTFGUkVndG5UUzFyVFZwbloycHlXUnBEcXJuQnZRRTlDanRvZEhSd2N6b3ZMM2QzZHk1NWIzVjBkV0psTG1OdmJTOXNhWFpsWDJOb1lYUV9kajFuVFMxclRWcG5aMnB5V1NacGMxOXdiM0J2ZFhROU1TQUMo5-aA_KPn5AIwADgAQAJKKwgAEAAYACAAKg5zdGF0aWNjaGVja3N1bToAQABKAggBUMKMlt2k5-QCWANQ5KzA_KPn5AJYt7rZo9Tm5AJoAYIBAggBiAEAoAHorZb_pOfkAg%253D%253D"}},"urlEndpoint":{"url":"/live_chat/get_live_chat?continuation=0ofMyAORAhqsAUNqZ0tEUW9MWjAwdGEwMWFaMmRxY2xrcUp3b1lWVU53VGtneVdtc3laM2N6U2tKcVYwRkxVM2xhWTFGUkVndG5UUzFyVFZwbloycHlXUnBEcXJuQnZRRTlDanRvZEhSd2N6b3ZMM2QzZHk1NWIzVjBkV0psTG1OdmJTOXNhWFpsWDJOb1lYUV9kajFuVFMxclRWcG5aMnB5V1NacGMxOXdiM0J2ZFhROU1TQUMo5-aA_KPn5AIwADgAQAJKKwgAEAAYACAAKg5zdGF0aWNjaGVja3N1bToAQABKAggBUMKMlt2k5-QCWANQ5KzA_KPn5AJYt7rZo9Tm5AJoAYIBAggBiAEAoAHorZb_pOfkAg%253D%253D"}},"timing":{"info":{"st":64}}}
{
"csn": "zeiIXfXHJYOA1d8Pyuaw4A4",
"response": {
"responseContext": {
"serviceTrackingParams": [
{
"service": "CSI",
"params": [
{
"key": "GetLiveChat_rid",
"value": "0x96761cd683987638"
},
{
"key": "c",
"value": "WEB"
},
{
"key": "cver",
"value": "2.20190920.05.01"
},
{
"key": "yt_li",
"value": "0"
}
]
},
{
"service": "GFEEDBACK",
"params": [
{
"key": "e",
"value": "23744176,23757412,23788838,23788875,23793834,23804281,23808952,23818920,23828084,23828243,23829335,23832543,23835014,23836965,23837741,23837772,23837957,23837993,23838272,23838302,23838823,23838823,23839284,23839362,23840216,23840243,23841118,23842662,23842986,23843283,23843289,23843534,23844042,24630096,9449243,9471235"
},
{
"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.20190920"
},
{
"key": "innertube.build.changelist",
"value": "270293990"
},
{
"key": "innertube.build.experiments.source_version",
"value": "270377311"
},
{
"key": "innertube.build.label",
"value": "youtube.ytfe.innertube_20190920_5_RC0"
},
{
"key": "innertube.build.timestamp",
"value": "1568999515"
},
{
"key": "innertube.build.variants.checksum",
"value": "669625af1d321c1e95dffac8db989afa"
},
{
"key": "innertube.run.job",
"value": "ytfe-innertube-replica-only.ytfe"
}
]
}
],
"webResponseContextExtensionData": {
"ytConfigData": {
"csn": "zeiIXfXHJYOA1d8Pyuaw4A4",
"visitorData": "CgtLWW1kYjAxZTBaRSjN0aPsBQ%3D%3D"
}
}
}
},
"xsrf_token": "QUFFLUhqbnhXaGhpblNhWmEzdjJJR2JNeW02M01PQ0p6Z3xBQ3Jtc0ttekpfU1dhZlA4ZWJhSGNrOFN5ZGFFSmNSMjBWRERWYUtOSS03RG5sbDRaa01KWmZFd2pPZzNEdW10WThmUXRiQjRKQ1ZPUkd1b09nT0k5dEZJTGdFYWxEVGNOWkUzcGNEQjdTNnN2OTRjN1Qtc0haZlpSWGlxd1k4LUdnVEhVb1FtMW8yZHJfankzN1JhUFo3aFZvS0s4NkIzTGc=",
"url": "\/live_chat\/get_live_chat?continuation=0ofMyAORAhqsAUNqZ0tEUW9MWjAwdGEwMWFaMmRxY2xrcUp3b1lWVU53VGtneVdtc3laM2N6U2tKcVYwRkxVM2xhWTFGUkVndG5UUzFyVFZwbloycHlXUnBEcXJuQnZRRTlDanRvZEhSd2N6b3ZMM2QzZHk1NWIzVjBkV0psTG1OdmJTOXNhWFpsWDJOb1lYUV9kajFuVFMxclRWcG5aMnB5V1NacGMxOXdiM0J2ZFhROU1TQUMo5-aA_KPn5AIwADgAQAJKKwgAEAAYACAAKg5zdGF0aWNjaGVja3N1bToAQABKAggBUMKMlt2k5-QCWANQ5KzA_KPn5AJYt7rZo9Tm5AJoAYIBAggBiAEAoAHorZb_pOfkAg%253D%253D",
"endpoint": {
"commandMetadata": {
"webCommandMetadata": {
"url": "/live_chat/get_live_chat?continuation=0ofMyAORAhqsAUNqZ0tEUW9MWjAwdGEwMWFaMmRxY2xrcUp3b1lWVU53VGtneVdtc3laM2N6U2tKcVYwRkxVM2xhWTFGUkVndG5UUzFyVFZwbloycHlXUnBEcXJuQnZRRTlDanRvZEhSd2N6b3ZMM2QzZHk1NWIzVjBkV0psTG1OdmJTOXNhWFpsWDJOb1lYUV9kajFuVFMxclRWcG5aMnB5V1NacGMxOXdiM0J2ZFhROU1TQUMo5-aA_KPn5AIwADgAQAJKKwgAEAAYACAAKg5zdGF0aWNjaGVja3N1bToAQABKAggBUMKMlt2k5-QCWANQ5KzA_KPn5AJYt7rZo9Tm5AJoAYIBAggBiAEAoAHorZb_pOfkAg%253D%253D"
}
},
"urlEndpoint": {
"url": "/live_chat/get_live_chat?continuation=0ofMyAORAhqsAUNqZ0tEUW9MWjAwdGEwMWFaMmRxY2xrcUp3b1lWVU53VGtneVdtc3laM2N6U2tKcVYwRkxVM2xhWTFGUkVndG5UUzFyVFZwbloycHlXUnBEcXJuQnZRRTlDanRvZEhSd2N6b3ZMM2QzZHk1NWIzVjBkV0psTG1OdmJTOXNhWFpsWDJOb1lYUV9kajFuVFMxclRWcG5aMnB5V1NacGMxOXdiM0J2ZFhROU1TQUMo5-aA_KPn5AIwADgAQAJKKwgAEAAYACAAKg5zdGF0aWNjaGVja3N1bToAQABKAggBUMKMlt2k5-QCWANQ5KzA_KPn5AJYt7rZo9Tm5AJoAYIBAggBiAEAoAHorZb_pOfkAg%253D%253D"
}
},
"timing": {
"info": {
"st": 64
}
}
}

View File

@@ -0,0 +1,24 @@
{
"timing": {
"info": {
"st": 164
}
},
"csn": "",
"response": {
"responseContext": {
},
"continuationContents": {
"liveChatContinuation": {
"continuations": [{
"timedContinuationData": {
"timeoutMs": 10000,
"continuation": "continuation"
}
}]
}
}
}
}

View File

@@ -0,0 +1,188 @@
{
"timing": {
"info": {
"st": 164
}
},
"csn": "",
"response": {
"responseContext": {
},
"continuationContents": {
"liveChatContinuation": {
"continuations": [{
"timedContinuationData": {
"timeoutMs": 10000,
"continuation": "continuation"
}
}],
"actions": [{
"addChatItemAction": {
"item": {
"liveChatTextMessageRenderer": {
"timestampUsec": "1500000000000000"
}
}
}
},
{
"addChatItemAction": {
"item": {
"liveChatTextMessageRenderer": {
"timestampUsec": "1500000000000000"
}
}
}
},
{
"addChatItemAction": {
"item": {
"liveChatTextMessageRenderer": {
"timestampUsec": "1500000000000000"
}
}
}
},
{
"addChatItemAction": {
"item": {
"liveChatTextMessageRenderer": {
"timestampUsec": "1500000000000000"
}
}
}
},
{
"addChatItemAction": {
"item": {
"liveChatTextMessageRenderer": {
"timestampUsec": "1500000000000000"
}
}
}
},
{
"addChatItemAction": {
"item": {
"liveChatTextMessageRenderer": {
"timestampUsec": "1500000000000000"
}
}
}
},
{
"addChatItemAction": {
"item": {
"liveChatTextMessageRenderer": {
"timestampUsec": "1500000000000000"
}
}
}
},
{
"addChatItemAction": {
"item": {
"liveChatTextMessageRenderer": {
"timestampUsec": "1500000000000000"
}
}
}
},
{
"addChatItemAction": {
"item": {
"liveChatTextMessageRenderer": {
"timestampUsec": "1500000000000000"
}
}
}
},
{
"addChatItemAction": {
"item": {
"liveChatTextMessageRenderer": {
"timestampUsec": "1500000000000000"
}
}
}
},
{
"addChatItemAction": {
"item": {
"liveChatTextMessageRenderer": {
"timestampUsec": "1500000000000000"
}
}
}
},
{
"addChatItemAction": {
"item": {
"liveChatTextMessageRenderer": {
"timestampUsec": "1500000000000000"
}
}
}
},
{
"addChatItemAction": {
"item": {
"liveChatTextMessageRenderer": {
"timestampUsec": "1500000000000000"
}
}
}
},
{
"addChatItemAction": {
"item": {
"liveChatTextMessageRenderer": {
"timestampUsec": "1500000000000000"
}
}
}
},
{
"addChatItemAction": {
"item": {
"liveChatTextMessageRenderer": {
"timestampUsec": "1500000030000000"
}
}
}
}
]}
}
}
}

View File

@@ -0,0 +1,42 @@
{
"timing": {
"info": {
"st": 164
}
},
"csn": "",
"response": {
"responseContext": {
},
"continuationContents": {
"liveChatContinuation": {
"continuations": [{
"timedContinuationData": {
"timeoutMs": 10000,
"continuation": "continuation"
}
}],
"actions": [{
"addChatItemAction": {
"liveChatPlaceholderItemRenderer": {
"id": "",
"timestampUsec": "1500000000000000"
}
}
},
{
"addChatItemAction": {
"item": {
"liveChatPlaceholderItemRenderer": {
"id": "",
"timestampUsec": "1500000030000000"
}
}
}
}
]}
}
}
}

509
tests/testdata/test_stream.json vendored Normal file
View File

@@ -0,0 +1,509 @@
{
"response": {
"responseContext": {
"webResponseContextExtensionData": ""
},
"continuationContents": {
"liveChatContinuation": {
"continuations": [
{
"invalidationContinuationData": {
"invalidationId": {
"objectSource": 1000,
"objectId": "___objectId___",
"topic": "chat~00000000000~0000000",
"subscribeToGcmTopics": true,
"protoCreationTimestampMs": "1577804400000"
},
"timeoutMs": 5000,
"continuation": "___continuation___"
}
}
],
"actions": [
{
"addChatItemAction": {
"item": {
"liveChatTextMessageRenderer": {
"message": {
"runs": [
{
"text": "This is normal message."
}
]
},
"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
}
]
},
"contextMenuEndpoint": {
"commandMetadata": {
"webCommandMetadata": {
"ignoreNavigation": true
}
},
"liveChatItemContextMenuEndpoint": {
"params": "___params___"
}
},
"id": "dummy_id",
"timestampUsec": 0,
"authorExternalChannelId": "http://www.youtube.com/channel/author_channel_url",
"contextMenuAccessibility": {
"accessibilityData": {
"label": "コメントの操作"
}
}
}
},
"clientId": "dummy_client_id"
}
},
{
"addChatItemAction": {
"item": {
"liveChatTextMessageRenderer": {
"message": {
"runs": [
{
"text": "This is members's message"
}
]
},
"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
}
]
},
"contextMenuEndpoint": {
"commandMetadata": {
"webCommandMetadata": {
"ignoreNavigation": true
}
},
"liveChatItemContextMenuEndpoint": {
"params": "___params___"
}
},
"id": "dummy_id",
"timestampUsec": 0,
"authorBadges": [
{
"liveChatAuthorBadgeRenderer": {
"customThumbnail": {
"thumbnails": [
{
"url": "https://yt3.ggpht.com/X=s32-c-k"
},
{
"url": "https://yt3.ggpht.com/X=s32-c-k"
}
]
},
"tooltip": "メンバー2 か月)",
"accessibility": {
"accessibilityData": {
"label": "メンバー2 か月)"
}
}
}
}
],
"authorExternalChannelId": "http://www.youtube.com/channel/author_channel_url",
"contextMenuAccessibility": {
"accessibilityData": {
"label": "コメントの操作"
}
}
}
},
"clientId": "dummy_client_id"
}
},
{
"addChatItemAction": {
"item": {
"liveChatPlaceholderItemRenderer": {
"id": "dummy_id",
"timestampUsec": 0
}
},
"clientId": "dummy_client_id"
}
},
{
"addLiveChatTickerItemAction": {
"item": {
"liveChatTickerPaidMessageItemRenderer": {
"id": "dummy_id",
"amount": {
"simpleText": "¥10,000"
},
"amountTextColor": 4294967295,
"startBackgroundColor": 4293271831,
"endBackgroundColor": 4291821568,
"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
}
]
},
"durationSec": 3600,
"showItemEndpoint": {
"commandMetadata": {
"webCommandMetadata": {
"ignoreNavigation": true
}
},
"showLiveChatItemEndpoint": {
"renderer": {
"liveChatPaidMessageRenderer": {
"id": "dummy_id",
"timestampUsec": 0,
"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": "¥10,000"
},
"message": {
"runs": [
{
"text": "This is superchat message."
}
]
},
"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": "コメントの操作"
}
}
}
}
}
},
"authorExternalChannelId": "http://www.youtube.com/channel/author_channel_url",
"fullDurationSec": 3600
}
},
"durationSec": "3600"
}
},
{
"addChatItemAction": {
"item": {
"liveChatPaidMessageRenderer": {
"id": "dummy_id",
"timestampUsec": 0,
"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": "¥10,800"
},
"message": {
"runs": [
{
"text": "This is superchat message."
}
]
},
"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": "コメントの操作"
}
}
}
}
}
},
{
"addChatItemAction": {
"item": {
"liveChatPaidStickerRenderer": {
"id": "dummy_id",
"contextMenuEndpoint": {
"clickTrackingParams": "___clickTrackingParams___",
"commandMetadata": {
"webCommandMetadata": {
"ignoreNavigation": true
}
},
"liveChatItemContextMenuEndpoint": {
"params": "___params___"
}
},
"contextMenuAccessibility": {
"accessibilityData": {
"label": "コメントの操作"
}
},
"timestampUsec": 0,
"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
}
]
},
"authorName": {
"simpleText": "author_name"
},
"authorExternalChannelId": "http://www.youtube.com/channel/author_channel_url",
"sticker": {
"thumbnails": [
{
"url": "//lh3.googleusercontent.com/param_s=s40-rp",
"width": 40,
"height": 40
},
{
"url": "//lh3.googleusercontent.com/param_s=s80-rp",
"width": 80,
"height": 80
}
],
"accessibility": {
"accessibilityData": {
"label": "___sticker_label___"
}
}
},
"moneyChipBackgroundColor": 4280191205,
"moneyChipTextColor": 4294967295,
"purchaseAmountText": {
"simpleText": "¥150"
},
"stickerDisplayWidth": 40,
"stickerDisplayHeight": 40,
"backgroundColor": 4279592384,
"authorNameTextColor": 3019898879,
"trackingParams": "___trackingParams___"
}
}
}
},
{
"addLiveChatTickerItemAction": {
"item": {
"liveChatTickerSponsorItemRenderer": {
"id": "dummy_id",
"detailText": {
"runs": [
{
"text": "メンバー"
}
]
},
"detailTextColor": 4294967295,
"startBackgroundColor": 4279213400,
"endBackgroundColor": 4278943811,
"sponsorPhoto": {
"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
}
]
},
"durationSec": 300,
"showItemEndpoint": {
"commandMetadata": {
"webCommandMetadata": {
"ignoreNavigation": true
}
},
"showLiveChatItemEndpoint": {
"renderer": {
"liveChatMembershipItemRenderer": {
"id": "dummy_id",
"timestampUsec": 0,
"authorExternalChannelId": "http://www.youtube.com/channel/author_channel_url",
"headerSubtext": {
"runs": [
{
"text": "メンバーシップ"
},
{
"text": " へようこそ!"
}
]
},
"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
}
]
},
"authorBadges": [
{
"liveChatAuthorBadgeRenderer": {
"customThumbnail": {
"thumbnails": [
{
"url": "https://yt3.ggpht.com/X=s32-c-k"
},
{
"url": "https://yt3.ggpht.com/X=s32-c-k"
}
]
},
"tooltip": "新規メンバー",
"accessibility": {
"accessibilityData": {
"label": "新規メンバー"
}
}
}
}
],
"contextMenuEndpoint": {
"commandMetadata": {
"webCommandMetadata": {
"ignoreNavigation": true
}
},
"liveChatItemContextMenuEndpoint": {
"params": "___params___"
}
},
"contextMenuAccessibility": {
"accessibilityData": {
"label": "コメントの操作"
}
}
}
}
}
},
"authorExternalChannelId": "http://www.youtube.com/channel/author_channel_url",
"fullDurationSec": 300
}
},
"durationSec": "300"
}
}
]
}
}
}
}

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"
}
}
}
}