Compare commits

..

90 Commits

Author SHA1 Message Date
taizan-hokuto
f7d1830226 Merge branch 'release/v0.0.5.1.3' 2020-01-18 14:45:33 +09:00
taizan-hokuto
76b126faf2 Increment version 2020-01-18 14:43:06 +09:00
taizan-hokuto
bbd01d6523 Increment version 2020-01-18 13:12:52 +09:00
taizan-hokuto
f8fa0e394e Delete json_display_processor 2020-01-18 13:02:43 +09:00
taizan-hokuto
efdf07e3de Make it possible to set custom logger 2020-01-18 12:38:36 +09:00
taizan-hokuto
2573cc18de Fix setting exception_handler 2020-01-18 09:22:16 +09:00
taizan-hokuto
1c5852421b Undo _set_exception_handler 2020-01-18 09:18:20 +09:00
taizan-hokuto
970d111e1b Alert default processor attribute error
: delete default exception handler

Alert default processor attribute error
: delete default exception handler

Delete unnecessary lines

Delete unnecessary lines
2020-01-18 08:39:36 +09:00
taizan-hokuto
1643dd1ad1 Switch author type by icon type 2020-01-13 18:32:36 +09:00
taizan-hokuto
0272319fa6 Update README 2020-01-13 13:09:51 +09:00
taizan-hokuto
fb0edef136 Merge branch 'release/v0.0.5.0' 2020-01-13 09:38:10 +09:00
taizan-hokuto
260a2b35a9 Merge tag 'v0.0.5.0' into develop
v0.0.5.0
2020-01-13 09:38:10 +09:00
taizan-hokuto
e03d39475e Increment version 2020-01-13 09:37:34 +09:00
taizan-hokuto
2462b8aca0 Merge branch 'hotfix/fix_defaultprocessor_display_amount' 2020-01-13 08:19:42 +09:00
taizan-hokuto
a1024c8734 Merge tag 'fix_defaultprocessor_display_amount' into develop 2020-01-13 08:19:42 +09:00
taizan-hokuto
6b3ca00d35 Fix superchat/sticker display amount 2020-01-13 08:19:03 +09:00
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
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
36 changed files with 1537 additions and 402 deletions

View File

@@ -1,3 +1,5 @@
include requirements.txt
include requirements_test.txt
include README.MD
global-exclude tests/*
global-exclude pytchat/testrun*.py

132
README.md
View File

@@ -7,13 +7,16 @@ pytchat is a python library for fetching youtube live chat.
pytchat is a python library for fetching youtube live chat
without using youtube api, Selenium or BeautifulSoup.
pytchatはAPIを使わずにYouTubeチャットを取得するための軽量pythonライブラリです。
Other features:
+ 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.
For more detailed information, see [wiki](https://github.com/taizan-hokuto/pytchat/wiki).
For more detailed information, see [wiki](https://github.com/taizan-hokuto/pytchat/wiki). <br>
より詳細な解説は[wiki](https://github.com/taizan-hokuto/pytchat/wiki/Home-:)を参照してください。
## Install
```python
@@ -26,13 +29,17 @@ pip install pytchat
### on-demand mode
```python
from pytchat import LiveChat
livechat = LiveChat(video_id = "Zvp1pJpie4I")
chat = LiveChat("G1w62uEMZ74")
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()
while livechat.is_alive():
try:
chatdata = livechat.get()
for c in chatdata.items:
print(f"{c.datetime} [{c.author.name}]- {c.message}")
chatdata.tick()
except KeyboardInterrupt:
livechat.terminate()
break
```
### callback mode
@@ -40,86 +47,97 @@ while chat.is_alive():
from pytchat import LiveChat
import time
def main()
chat = LiveChat("G1w62uEMZ74", callback = func)
while chat.is_alive():
time.sleep(3)
def main():
livechat = LiveChat(video_id = "Zvp1pJpie4I", callback = disp)
while livechat.is_alive():
#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()
time.sleep(1)
livechat.terminate()
#callback function (automatically called)
def disp(chatdata):
for c in chatdata.items:
print(f"{c.datetime} [{c.author.name}]- {c.message}")
chatdata.tick()
if __name__ == '__main__':
main()
```
### 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)
livechat = LiveChatAsync("Zvp1pJpie4I", callback = func)
while livechat.is_alive():
#other background operation.
await asyncio.sleep(3)
#callback function is automatically called periodically.
async def func(data):
for c in data.items:
#callback function is automatically called.
async def func(chatdata):
for c in chatdata.items:
print(f"{c.datetime} [{c.author.name}]-{c.message} {c.amountString}")
await data.tick_async()
await chatdata.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",
chat = LiveChat("Zvp1pJpie4I",
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"]))
try:
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']))
except KeyboardInterrupt:
chat.terminate()
```
### 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)
while chat.is_alive():
await asyncio.sleep(3)
#other background operation here.
def main():
#seektime (seconds): start position of chat.
chat = LiveChat("ojes5ULOqhc", seektime = 60*30)
print('Replay from 30:00')
try:
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()
except KeyboardInterrupt:
chat.terminate()
#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
Each item can be got with items() function.
Each item can be got with `items` function.
<table>
<tr>
<th>name</th>

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.8'
__version__ = '0.0.5.1.3'
__license__ = 'MIT'
__author__ = 'taizan-hokuto'
__author_email__ = '55448286+taizan-hokuto@users.noreply.github.com'
@@ -11,13 +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,
SpeedCalculator
)
SpeedCalculator,
DummyProcessor
)

View File

@@ -8,3 +8,5 @@ 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,11 @@
import logging
LOGGER_MODE = None
from . import mylogger
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, loglevel = None):
module_logger = mylogger.get_logger(module_name, loglevel = loglevel)
return module_logger

View File

@@ -1,23 +1,24 @@
from logging import NullHandler, getLogger, StreamHandler, FileHandler, Formatter
import logging
import datetime
def get_logger(modname,mode=logging.DEBUG):
logger = logging.getLogger(modname)
if mode == None:
logger.addHandler(logging.NullHandler())
def get_logger(modname,loglevel=logging.DEBUG):
logger = getLogger(modname)
if loglevel == None:
logger.addHandler(NullHandler())
return logger
logger.setLevel(mode)
logger.setLevel(loglevel)
#create handler1 for showing info
handler1 = logging.StreamHandler()
handler1 = StreamHandler()
my_formatter = MyFormatter()
handler1.setFormatter(my_formatter)
handler1.setLevel(mode)
handler1.setLevel(loglevel)
logger.addHandler(handler1)
#create handler2 for recording log file
if mode <= logging.DEBUG:
handler2 = logging.FileHandler(filename="log.txt", encoding='utf-8')
if loglevel <= logging.DEBUG:
handler2 = FileHandler(filename="log.txt", encoding='utf-8')
handler2.setLevel(logging.ERROR)
handler2.setFormatter(my_formatter)

View File

@@ -8,18 +8,17 @@ 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
headers = config.headers
MAX_RETRY = 10
class LiveChatAsync:
@@ -30,6 +29,11 @@ class LiveChatAsync:
video_id : str
動画ID
seektime : int
(ライブチャット取得時は無視)
取得開始するアーカイブ済みチャットの経過時間(秒)
マイナス値を指定した場合は、配信開始前のチャットも取得する。
processor : ChatProcessor
チャットデータを加工するオブジェクト
@@ -53,7 +57,14 @@ class LiveChatAsync:
direct_mode : bool
Trueの場合、bufferを使わずにcallbackを呼ぶ。
Trueの場合、callbackの設定が必須
(設定していない場合IllegalFunctionCall例外を発生させる
(設定していない場合IllegalFunctionCall例外を発生させる
force_replay : bool
Trueの場合、ライブチャットが取得できる場合であっても
強制的にアーカイブ済みチャットを取得する。
topchat_only : bool
Trueの場合、上位チャットのみ取得する。
Attributes
---------
@@ -62,31 +73,47 @@ class LiveChatAsync:
'''
_setup_finished = False
_logger = config.logger(__name__)
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,
topchat_only = False,
logger = config.logger(__name__),
):
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="
self._topchat_only = topchat_only
self._logger = logger
LiveChatAsync._logger = logger
if not LiveChatAsync._setup_finished:
LiveChatAsync._setup_finished = True
if exception_handler == None:
self._set_exception_handler(self._handle_exception)
else:
if exception_handler:
self._set_exception_handler(exception_handler)
if interruptable:
signal.signal(signal.SIGINT,
@@ -99,7 +126,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 +148,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,36 +188,75 @@ 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()
self._logger.debug(f"[{self.video_id}]{str(e)}")
return
except (TypeError , json.JSONDecodeError) :
logger.error(f"{traceback.format_exc(limit = -1)}")
#self.terminate()
self._logger.error(f"{traceback.format_exc(limit = -1)}")
return
logger.debug(f"[{self.video_id}]チャット取得を終了しました。")
self._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, self._topchat_only)
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, self._topchat_only)
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) :
await asyncio.sleep(1)
continue
else:
logger.error(f"[{self.video_id}]"
self._logger.error(f"[{self.video_id}]"
f"Exceeded retry count. status_code={status_code}")
return None
return livechat_json
@@ -241,6 +288,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
@@ -249,7 +311,7 @@ class LiveChatAsync:
try:
self.terminate()
except CancelledError:
logger.debug(f'[{self.video_id}]cancelled:{sender}')
self._logger.debug(f'[{self.video_id}]cancelled:{sender}')
def terminate(self):
'''
@@ -258,31 +320,22 @@ 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})
self._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()
loop.create_task(cls.shutdown(None,None,None))
@classmethod
async def shutdown(cls, event, sig = None, handler=None):
logger.debug("シャットダウンしています")
cls._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"残っているタスクを終了しています")
cls._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,17 @@ 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
headers = config.headers
MAX_RETRY = 10
class LiveChat:
@@ -28,6 +27,11 @@ class LiveChat:
---------
video_id : str
動画ID
seektime : int
(ライブチャット取得時は無視)
取得開始するアーカイブ済みチャットの経過時間(秒)
マイナス値を指定した場合は、配信開始前のチャットも取得する。
processor : ChatProcessor
チャットデータを加工するオブジェクト
@@ -51,6 +55,13 @@ class LiveChat:
Trueの場合、callbackの設定が必須
(設定していない場合IllegalFunctionCall例外を発生させる
force_replay : bool
Trueの場合、ライブチャットが取得できる場合であっても
強制的にアーカイブ済みチャットを取得する。
topchat_only : bool
Trueの場合、上位チャットのみ取得する。
Attributes
---------
_executor : ThreadPoolExecutor
@@ -62,26 +73,43 @@ class LiveChat:
_setup_finished = False
#チャット監視中のListenerのリスト
_listeners= []
_listeners = []
_logger = config.logger(__name__)
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,
topchat_only = False,
logger = config.logger(__name__)
):
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="
self._topchat_only = topchat_only
self._logger = logger
LiveChat._logger = logger
if not LiveChat._setup_finished:
LiveChat._setup_finished = True
if interruptable:
@@ -95,7 +123,7 @@ class LiveChat:
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 +143,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,40 +181,73 @@ 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}\"")
self._logger.debug(f"[{self.video_id}]{str(e)}")
return
except (TypeError , json.JSONDecodeError) :
logger.error(f"{traceback.format_exc(limit = -1)}")
self._logger.error(f"{traceback.format_exc(limit = -1)}")
return
logger.debug(f"[{self.video_id}]チャット取得を終了しました。")
self._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")
url =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 :
time.sleep(1)
continue
else:
logger.error(f"[{self.video_id}]"
self._logger.error(f"[{self.video_id}]"
f"Exceeded retry count. status_code={status_code}")
return None
return livechat_json
@@ -236,6 +279,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
@@ -244,7 +302,7 @@ class LiveChat:
try:
self.terminate()
except CancelledError:
logger.debug(f'[{self.video_id}]cancelled:{sender}')
self._logger.debug(f'[{self.video_id}]cancelled:{sender}')
def terminate(self):
'''
@@ -253,19 +311,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})
self._logger.info(f'[{self.video_id}]finished.')
@classmethod
def shutdown(cls, event, sig = None, handler=None):
logger.debug("シャットダウンしています")
cls._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):
switch_01 = b'\x04' if topchatonly else b'\x01'
def _build(video_id, seektime, topchat_only):
switch_01 = b'\x04' if topchat_only 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,14 @@ def get(video_id, seektime = 0, topchatonly = False):
).decode()
)
def getparam(video_id, seektime = 0, topchat_only = False):
'''
Parameter
---------
seektime : int
unit:seconds
start position of fetching chat data.
topchat_only : bool
if True, fetch only 'top chat'
'''
return _build(video_id, seektime, topchat_only)

View File

@@ -66,9 +66,9 @@ def _nval(val):
buf += val.to_bytes(1,'big')
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
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)
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]))
def getparam(video_id,past_sec = 60):
def getparam(video_id, past_sec = 0, topchat_only = False):
'''
Parameter
---------
past_sec : int
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

@@ -1,66 +1,88 @@
"""
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 )
logger = mylogger.get_logger(__name__,mode=config.LOGGER_MODE)
NoContinuationsException,
ChatParseException )
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}")
metadata = cont.get(unknown)
metadata.setdefault('timeoutMs', 10000)
chatdata = contents['liveChatContinuation'].get('actions')
raise ChatParseException(f"Received unknown continuation type:{unknown}")
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

@@ -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

@@ -5,9 +5,8 @@ from .renderer.paidmessage import LiveChatPaidMessageRenderer
from .renderer.paidsticker import LiveChatPaidStickerRenderer
from .renderer.legacypaid import LiveChatLegacyPaidMessageRenderer
from .. chat_processor import ChatProcessor
from ... import mylogger
from ... import config
logger = mylogger.get_logger(__name__,mode=config.LOGGER_MODE)
logger = config.logger(__name__)
class CompatibleProcessor(ChatProcessor):

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

@@ -6,8 +6,7 @@ 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):
@@ -62,7 +61,7 @@ class DefaultProcessor(ChatProcessor):
renderer.get_snippet()
renderer.get_authordetails()
except (KeyError,TypeError,AttributeError) as e:
except (KeyError,TypeError) as e:
logger.error(f"{str(type(e))}-{str(e)} sitem:{str(sitem)}")
return None
return renderer

View File

@@ -1,5 +1,4 @@
from datetime import datetime
class Author:
pass
class BaseRenderer:
@@ -19,8 +18,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 +42,7 @@ class BaseRenderer:
def get_message(self,renderer):
message = ''
message_ex = []
if renderer.get("message"):
runs=renderer["message"].get("runs")
if runs:
@@ -51,22 +50,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
@@ -76,16 +66,17 @@ class BaseRenderer:
badges=renderer.get("authorBadges")
if badges:
for badge in badges:
author_type = badge["liveChatAuthorBadgeRenderer"]["accessibility"]["accessibilityData"]["label"]
if author_type == '確認済み':
isVerified = True
if author_type == '所有者':
isChatOwner = True
if 'メンバー' in author_type:
if badge["liveChatAuthorBadgeRenderer"].get("icon"):
author_type = badge["liveChatAuthorBadgeRenderer"]["icon"]["iconType"]
if author_type == 'VERIFIED':
isVerified = True
if author_type == 'OWNER':
isChatOwner = True
if author_type == 'MODERATOR':
isChatModerator = True
if badge["liveChatAuthorBadgeRenderer"].get("customThumbnail"):
isChatSponsor = True
self.get_badgeurl(badge)
if author_type == 'モデレーター':
isChatModerator = True
return isVerified, isChatOwner, isChatSponsor, isChatModerator

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

@@ -10,13 +10,9 @@ class LiveChatPaidMessageRenderer(BaseRenderer):
def get_snippet(self):
super().get_snippet()
self.author.name = self.renderer["authorName"]["simpleText"]
amountDisplayString, symbol, amount =(
self.get_amountdata(self.renderer)
)
self.message = self.get_message(self.renderer)
self.amountValue= amount
self.amountString = amountDisplayString
self.currency= currency.symbols[symbol]["fxtext"] if currency.symbols.get(symbol) else symbol

View File

@@ -10,13 +10,9 @@ class LiveChatPaidStickerRenderer(BaseRenderer):
def get_snippet(self):
super().get_snippet()
self.author.name = self.renderer["authorName"]["simpleText"]
amountDisplayString, symbol, amount =(
self.get_amountdata(self.renderer)
)
self.message = ""
self.amountValue = amount
self.amountString = amountDisplayString
self.currency = currency.symbols[symbol]["fxtext"] if currency.symbols.get(symbol) else symbol

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,13 +0,0 @@
import json
from .chat_processor import ChatProcessor
class JsonDisplayProcessor(ChatProcessor):
def process(self,chat_components: list):
if chat_components:
for component in chat_components:
chatdata = component.get('chatdata')
if chatdata:
for chat in chatdata:
print(json.dumps(chat,ensure_ascii=False)[:200])

View File

@@ -1,5 +1,5 @@
"""
speedmeter.py
speed_calculator.py
チャットの勢いを算出するChatProcessor
Calculate speed of chat.
"""

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, removedirs
import re
package_name = "pytchat"
@@ -13,6 +13,15 @@ def _requirements():
def _test_requirements():
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:
init_text = f.read()
@@ -28,6 +37,9 @@ assert author
assert author_email
assert url
with open('README.md', encoding='utf-8') as f:
long_description = f.read()
@@ -35,7 +47,7 @@ with open('README.md', encoding='utf-8') as f:
setup(
name=package_name,
packages=find_packages(),
packages=find_packages(exclude=['*log.txt','*tests']),
version=version,
url=url,
author=author,
@@ -44,7 +56,7 @@ setup(
long_description_content_type='text/markdown',
license=license,
install_requires=_requirements(),
tests_require=_test_requirements(),
#tests_require=_test_requirements(),
description="a python library for fetching youtube live chat.",
classifiers=[
'Natural Language :: Japanese',

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

@@ -4,6 +4,6 @@ from pytchat.paramgen import liveparam
def test_liveparam_0(mocker):
_ts1= 1546268400
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"
assert test_param == param

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

@@ -9,7 +9,7 @@ from pytchat.exceptions import (
from pytchat.processors.speed_calculator import SpeedCalculator
parser = Parser()
parser = Parser(is_replay =False)
def test_speed_1(mocker):
'''test speed calculation with normal json.
@@ -21,7 +21,7 @@ def test_speed_1(mocker):
_json = _open_file("tests/testdata/speed/speedtest_normal.json")
_, chatdata = parser.parse(json.loads(_json))
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
data = {
"video_id" : "",
"timeout" : 10,
@@ -37,7 +37,7 @@ def test_speed_2(mocker):
_json = _open_file("tests/testdata/speed/speedtest_undefined.json")
_, chatdata = parser.parse(json.loads(_json))
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
data = {
"video_id" : "",
"timeout" : 10,
@@ -53,7 +53,7 @@ def test_speed_3(mocker):
_json = _open_file("tests/testdata/speed/speedtest_empty.json")
_, chatdata = parser.parse(json.loads(_json))
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
data = {
"video_id" : "",
"timeout" : 10,

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

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