Compare commits

..

60 Commits

Author SHA1 Message Date
taizan-hokuto
f6b8229998 Merge branch 'release' 2019-12-30 18:33:41 +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
taizan-hokuto
cf9aae3322 Merge branch 'develop' 2019-12-19 01:57:08 +09:00
taizan-hokuto
6ac2315936 Increment version 2019-12-19 01:53:55 +09:00
taizan-hokuto
50c8e34080 Fix README 2019-12-19 01:51:31 +09:00
taizan-hokuto
2d3da91d51 Fix calculation algorithm 2019-12-19 01:21:49 +09:00
taizan-hokuto
3ac71985ff Implement SpeedCalculator 2019-12-17 21:29:20 +09:00
taizan-hokuto
13bdf0376b Merge branch 'develop' 2019-12-01 22:58:53 +09:00
taizan-hokuto
b2ffdaec0c Increment version 2019-12-01 22:54:43 +09:00
taizan-hokuto
c85786679f Merge branch 'feature/1' into develop 2019-12-01 22:50:06 +09:00
taizan-hokuto
c7a7886672 Fix superSticker rendering 2019-12-01 22:47:01 +09:00
taizan-hokuto
12996fb44d Merge branch 'feature/1' 2019-11-22 00:17:15 +09:00
taizan-hokuto
c884ef7288 Merge branch 'feature/1' into develop 2019-11-22 00:16:20 +09:00
taizan-hokuto
2cd9e98fc2 Increment version 2019-11-22 00:15:57 +09:00
taizan-hokuto
2ac4c99ab4 Increment version 2019-11-22 00:04:18 +09:00
taizan-hokuto
51bf8ad738 Update README 2019-11-21 23:37:35 +09:00
taizan-hokuto
2e70e74bcd Update README 2019-11-21 23:04:12 +09:00
taizan-hokuto
a39d6cb420 Use list comprehension 2019-11-21 22:46:15 +09:00
taizan-hokuto
5dd0cb45b7 Implement messageEx 2019-11-21 22:35:27 +09:00
taizan-hokuto
24873651a6 Fix comments 2019-11-21 20:47:42 +09:00
taizan-hokuto
0e060bf998 Use logger when errors occur 2019-11-20 23:59:16 +09:00
taizan-hokuto
817fed9d1d Make functions private. 2019-11-19 20:53:37 +09:00
taizan-hokuto
823f7fefa4 Fix comments 2019-11-19 20:36:54 +09:00
taizan-hokuto
aa894fc52b Fix comments 2019-11-15 00:58:36 +09:00
taizan-hokuto
6d775e5cd0 Merge branch 'hotfix' 2019-11-14 22:39:22 +09:00
taizan-hokuto
53b70ed86b Increment version 2019-11-14 22:38:25 +09:00
taizan-hokuto
68c707b7d6 Update README 2019-11-14 22:37:16 +09:00
taizan-hokuto
30aaa54a2f Change debug mode 2019-11-14 22:34:29 +09:00
27 changed files with 1070 additions and 184 deletions

125
README.md
View File

@@ -8,12 +8,12 @@ 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.
より詳細な説明は [wiki](https://github.com/taizan-hokuto/pytchat/wiki) をご参照ください。
For more detailed information, see [wiki](https://github.com/taizan-hokuto/pytchat/wiki).
## Install
```python
@@ -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,15 +40,17 @@ while chat.is_alive():
from pytchat import LiveChat
import time
chat = LiveChat("G1w62uEMZ74", callback = func)
while chat.is_alive():
#other background operation here.
time.sleep(3)
#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()
def func(data):
for c in data.items:
print(f"{c.datetime} [{c.author.name}]-{c.message} {c.amountString}")
data.tick()
#entry point
chat = LiveChat("rsHWP7IjMiw", callback = display)
while chat.is_alive():
time.sleep(3)
#other background operation.
```
### asyncio context:
@@ -57,65 +59,61 @@ from pytchat import LiveChatAsync
import asyncio
async def main():
chat = LiveChatAsync("G1w62uEMZ74", callback = func)
while chat.is_alive():
#other background operation here.
await asyncio.sleep(3)
chat = LiveChatAsync("rsHWP7IjMiw", callback = func)
while chat.is_alive():
await asyncio.sleep(3)
#other background operation.
#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())
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:
```python
from pytchat import ReplayChatAsync
import asyncio
from pytchat import ReplayChat
async def main():
chat = ReplayChatAsync("G1w62uEMZ74", seektime = 1000, callback = func)
while chat.is_alive():
#other background operation here.
await asyncio.sleep(3)
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()
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.timestampText}> [{c.author.name}]-{c.message} {c.amountString}")
await asyncio.sleep(tick/1000)
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
main()
```
## Chatdata Structure of Default Processor
Structure of each item which got from items() function.
## Structure of Default Processor
Each item can be got with items() function.
<table>
<tr>
<th>name</th>
@@ -137,6 +135,11 @@ Structure of each item which got from items() function.
<td>str</td>
<td>emojis are represented by ":(shortcut text):"</td>
</tr>
<tr>
<td>messageEx</td>
<td>str</td>
<td>list of message texts and emoji URLs.</td>
</tr>
<tr>
<td>timestamp</td>
<td>int</td>
@@ -145,26 +148,26 @@ Structure of each item which got from 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>timestampText</td>
<td>elapsedTime</td>
<td>str</td>
<td>elapsed time. (ex. "1:02:27")</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>
@@ -193,7 +196,7 @@ Structure of author object.
<tr>
<td>channelId</td>
<td>str</td>
<td></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.0'
__version__ = '0.0.4.0'
__license__ = 'MIT'
__author__ = 'taizan-hokuto'
__author_email__ = '55448286+taizan-hokuto@users.noreply.github.com'
@@ -11,6 +11,7 @@ __url__ = 'https://github.com/taizan-hokuto/pytchat'
__all__ = ["core_async","core_multithread","processors"]
from .api import (
config,
LiveChat,
LiveChatAsync,
ReplayChat,
@@ -18,5 +19,7 @@ from .api import (
ChatProcessor,
CompatibleProcessor,
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.DEBUG
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

@@ -3,7 +3,6 @@ import datetime
import json
import random
import signal
import threading
import time
import traceback
import urllib.parse
@@ -12,15 +11,14 @@ from concurrent.futures import CancelledError
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 ..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:
@@ -73,7 +71,10 @@ class LiveChatAsync:
exception_handler = None,
direct_mode = False):
self.video_id = video_id
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
@@ -123,7 +124,7 @@ class LiveChatAsync:
async def _startlisten(self):
"""最初のcontinuationパラメータを取得し、
_listenループ開始する
_listenループのタスクを作成し開始する
"""
initial_continuation = await self._get_initial_continuation()
if initial_continuation is None:
@@ -149,7 +150,7 @@ class LiveChatAsync:
async def _listen(self, continuation):
''' continuationに紐付いたチャットデータを取得し
チャットデータを格納、
Bufferにチャットデータを格納、
次のcontinuaitonを取得してループする。
Parameter
@@ -181,9 +182,11 @@ 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.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
@@ -212,6 +215,7 @@ class LiveChatAsync:
else:
logger.error(f"[{self.video_id}]"
f"Exceeded retry count. status_code={status_code}")
self.terminate()
return None
return livechat_json
@@ -286,13 +290,4 @@ class LiveChatAsync:
logger.debug(f"残っているタスクを終了しています")
await asyncio.gather(*tasks,return_exceptions=True)
loop = asyncio.get_event_loop()
loop.stop()
loop.stop()

View File

@@ -12,23 +12,28 @@ 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)
logger = config.logger(__name__)
MAX_RETRY = 10
headers = config.headers
class ReplayChatAsync:
''' aiohttpを利用してYouTubeのライブ配信のチャットデータを取得する
'''asyncio(aiohttp)を利用してYouTubeのチャットデータを取得する
Parameter
---------
video_id : str
動画ID
seektime : int
リプレイするチャットデータの開始時間(秒)
processor : ChatProcessor
チャットデータを加工するオブジェクト
@@ -46,6 +51,9 @@ class ReplayChatAsync:
done_callback : func
listener終了時に呼び出すコールバック。
exception_handler : func
例外を処理する関数
direct_mode : bool
Trueの場合、bufferを使わずにcallbackを呼ぶ。
Trueの場合、callbackの設定が必須
@@ -53,27 +61,27 @@ class ReplayChatAsync:
Attributes
---------
_executor : ThreadPoolExecutor
チャットデータ取得ループ_listen用のスレッド
_is_alive : bool
チャット取得を終了したか
チャット取得を停止するためのフラグ
'''
_setup_finished = False
def __init__(self, video_id,
seektime =0,
seektime = 0,
processor = DefaultProcessor(),
buffer = Buffer(maxsize = 20),
buffer = None,
interruptable = True,
callback = None,
done_callback = None,
exception_handler = None,
direct_mode = False):
self.video_id = video_id
self.seektime= seektime
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
@@ -151,8 +159,8 @@ class ReplayChatAsync:
async def _listen(self, continuation):
''' continuationに紐付いたチャットデータを取得し
にチャットデータを格納、
次のcontinuaitonを取得してループする
Bufferにチャットデータを格納、
次のcontinuaitonを取得してループする
Parameter
---------
@@ -163,10 +171,10 @@ class ReplayChatAsync:
async with aiohttp.ClientSession() as session:
while(continuation and self._is_alive):
if self._pauser.empty():
#pauseが呼ばれて_pauserが空状態のときは一時停止する
#pause
await self._pauser.get()
#resumeが呼ばれて_pauserにitemが入ったら再開する
#直後に_pauserにitem(None)を入れてブロックを防ぐ
#resume
#prohibit from blocking by putting None into _pauser.
self._pauser.put_nowait(None)
livechat_json = (await
self._get_livechat_json(continuation, session, headers)
@@ -186,17 +194,18 @@ class ReplayChatAsync:
else:
await self._buffer.put(chat_component)
diff_time = timeout - (time.time()-time_mark)
if diff_time < 0 : diff_time=0
await asyncio.sleep(diff_time)
continuation = metadata.get('continuation')
except ChatParseException as e:
logger.error(f"{str(e)}動画ID:\"{self.video_id}\"")
logger.error(f"{str(e)}video_id:\"{self.video_id}\"")
return
except (TypeError , json.JSONDecodeError) :
logger.error(f"{traceback.format_exc(limit = -1)}")
self.terminate()
return
logger.debug(f"[{self.video_id}]チャット取得を終了しました。")
self.terminate()
async def _get_livechat_json(self, continuation, session, headers):
'''
@@ -278,7 +287,7 @@ 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):

View File

@@ -10,15 +10,14 @@ from concurrent.futures import CancelledError, ThreadPoolExecutor
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 ..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:
@@ -57,7 +56,7 @@ class LiveChat:
チャットデータ取得ループ_listen用のスレッド
_is_alive : bool
チャット取得を終了したか
チャット取得を停止するためのフラグ
'''
_setup_finished = False
@@ -65,14 +64,17 @@ class LiveChat:
_listeners= []
def __init__(self, video_id,
processor = DefaultProcessor(),
buffer = Buffer(maxsize = 20),
buffer = None,
interruptable = True,
callback = None,
done_callback = None,
direct_mode = False
):
self.video_id = video_id
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
@@ -142,8 +144,8 @@ class LiveChat:
def _listen(self, continuation):
''' continuationに紐付いたチャットデータを取得し
にチャットデータを格納、
次のcontinuaitonを取得してループする
Bufferにチャットデータを格納、
次のcontinuaitonを取得してループする
Parameter
---------
@@ -157,7 +159,6 @@ class LiveChat:
self._get_livechat_json(continuation, session, headers)
)
metadata, chatdata = self._parser.parse( livechat_json )
#チャットデータを含むコンポーネントを組み立ててbufferに投入する
timeout = metadata['timeoutMs']/1000
chat_component = {
"video_id" : self.video_id,
@@ -171,18 +172,16 @@ class LiveChat:
)
else:
self._buffer.put(chat_component)
#次のchatを取得するまでsleepする
diff_time = timeout - (time.time()-time_mark)
if diff_time < 0 : diff_time=0
time.sleep(diff_time)
#次のチャットデータのcontinuationパラメータを取り出す。
continuation = metadata.get('continuation')
#whileループ先頭に戻る
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
@@ -211,6 +210,7 @@ class LiveChat:
else:
logger.error(f"[{self.video_id}]"
f"Exceeded retry count. status_code={status_code}")
self.terminate()
return None
return livechat_json
@@ -259,18 +259,10 @@ class LiveChat:
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):
logger.debug("シャットダウンしています")
for t in LiveChat._listeners:
t._is_alive = False
t._is_alive = False

View File

@@ -11,15 +11,14 @@ 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:
@@ -30,6 +29,9 @@ class ReplayChat:
video_id : str
動画ID
seektime : int
リプレイするチャットデータの開始時間(秒)
processor : ChatProcessor
チャットデータを加工するオブジェクト
@@ -58,24 +60,27 @@ class ReplayChat:
チャットデータ取得ループ_listen用のスレッド
_is_alive : bool
チャット取得を終了したか
チャット取得を停止するためのフラグ
'''
_setup_finished = False
#チャット監視中のListenerのリスト
_listeners= []
def __init__(self, video_id,
seektime =0,
seektime = 0,
processor = DefaultProcessor(),
buffer = Buffer(maxsize = 20),
buffer = None,
interruptable = True,
callback = None,
done_callback = None,
direct_mode = False
):
self.video_id = video_id
self.seektime= seektime
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
@@ -87,6 +92,7 @@ class ReplayChat:
self._pauser.put_nowait(None)
self._setup()
if not ReplayChat._setup_finished:
ReplayChat._setup_finished = True
if interruptable:
@@ -147,7 +153,7 @@ class ReplayChat:
def _listen(self, continuation):
''' continuationに紐付いたチャットデータを取得し
にチャットデータを格納、
BUfferにチャットデータを格納、
次のcontinuaitonを取得してループする
Parameter
@@ -159,16 +165,15 @@ class ReplayChat:
with requests.Session() as session:
while(continuation and self._is_alive):
if self._pauser.empty():
#pauseが呼ばれて_pauserが空状態のときは一時停止する
#pause
self._pauser.get()
#resumeが呼ばれて_pauserにitemが入ったら再開する
#直後に_pauserにitem(None)を入れてブロックを防ぐ
#resume
#prohibit from blocking by putting None into _pauser.
self._pauser.put_nowait(None)
livechat_json = (
self._get_livechat_json(continuation, session, headers)
)
metadata, chatdata = self._parser.parse( livechat_json )
#チャットデータを含むコンポーネントを組み立ててbufferに投入する
timeout = metadata['timeoutMs']/1000
chat_component = {
"video_id" : self.video_id,
@@ -187,9 +192,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
@@ -218,6 +225,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
@@ -264,7 +272,7 @@ class ReplayChat:
'''Listener終了時のコールバック'''
try:
self.terminate()
except CancelledError:
except RuntimeError:
logger.debug(f'[{self.video_id}]cancelled:{sender}')
def terminate(self):
@@ -275,7 +283,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

@@ -17,7 +17,7 @@ def get_logger(modname,mode=logging.DEBUG):
logger.addHandler(handler1)
#create handler2 for recording log file
if mode <= logging.DEBUG:
handler2 = logging.FileHandler(filename="log.txt")
handler2 = logging.FileHandler(filename="log.txt", encoding='utf-8')
handler2.setLevel(logging.ERROR)
handler2.setFormatter(my_formatter)

View File

@@ -1,17 +1,43 @@
"""
pytchat.parser.live
~~~~~~~~~~~~~~~~~~~
This module is 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)
logger = config.logger(__name__)
class Parser:
def parse(self, jsn):
"""
このparse関数はLiveChat._listen() 関数から定期的に呼び出される。
引数jsnはYoutubeから取得したチャットデータの生JSONであり、
このparse関数によって与えられたJSONを以下に分割して返す。
+ timeout (次のチャットデータ取得までのインターバル)
+ chat dataチャットデータ本体
+ continuation (次のチャットデータ取得に必要となるパラメータ).
Parameter
----------
+ jsn : dict
+ Youtubeから取得したチャットデータのJSONオブジェクト。
pythonの辞書形式に変換済みの状態で渡される
Returns
-------
+ metadata : dict
+ チャットデータに付随するメタデータ。timeout、 動画ID、continuationパラメータで構成される。
+ chatdata : list[dict]
+ チャットデータ本体のリスト。
"""
if jsn is None:
return {'timeoutMs':0,'continuation':None},[]
if jsn['response']['responseContext'].get('errors'):
@@ -32,7 +58,7 @@ class Parser:
if metadata is None:
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')

View File

@@ -1,17 +1,40 @@
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):
"""
このparse関数はReplayChat._listen() 関数から定期的に呼び出される。
引数jsnはYoutubeから取得したアーカイブ済みチャットデータの生JSONであり、
このparse関数によって与えられたJSONを以下に分割して返す。
+ timeout (次のチャットデータ取得までのインターバル)
+ chat dataチャットデータ本体
+ continuation (次のチャットデータ取得に必要となるパラメータ).
ライブ配信のチャットとアーカイブ済み動画のチャットは構造が若干異なっているが、
ライブチャットと同じデータ形式に変換することにより、
同じprocessorでライブとリプレイどちらでも利用できるようにしている。
Parameter
----------
+ jsn : dict
+ Youtubeから取得したチャットデータのJSONオブジェクト。
pythonの辞書形式に変換済みの状態で渡される
Returns
-------
+ metadata : dict
+ チャットデータに付随するメタデータ。timeout、 動画ID、continuationパラメータで構成される。
+ chatdata : list[dict]
+ チャットデータ本体のリスト。
"""
if jsn is None:
return {'timeoutMs':0,'continuation':None},[]
if jsn['response']['responseContext'].get('errors'):
@@ -28,17 +51,18 @@ 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)
chatdata = []
for action in actions:
chatdata.append(action["replayChatItemAction"]["actions"][0])
"""アーカイブ済みチャットはライブチャットと構造が異なっているため、以下の行により
ライブチャットと同じ形式にそろえる"""
chatdata = [action["replayChatItemAction"]["actions"][0] for action in actions]
return metadata, chatdata
def get_interval(self, actions: list):

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

@@ -4,7 +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
logger = config.logger(__name__)
class Chatdata:
def __init__(self,chatlist:list, timeout:float):
@@ -23,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 = []
@@ -40,32 +42,31 @@ class DefaultProcessor:
if action.get('addChatItemAction') is None: continue
if action['addChatItemAction'].get('item') is None: continue
chat = self.parse(action)
chat = self._parse(action)
if chat:
chatlist.append(chat)
return Chatdata(chatlist, float(timeout))
def parse(self, sitem):
def _parse(self, sitem):
action = sitem.get("addChatItemAction")
if action:
item = action.get("item")
if item is None: return None
try:
renderer = self.get_renderer(item)
renderer = self._get_renderer(item)
if renderer == None:
return None
renderer.get_snippet()
renderer.get_authordetails()
except (KeyError,TypeError,AttributeError) as e:
print(f"------{str(type(e))}-{str(e)}----------")
print(sitem)
logger.error(f"{str(type(e))}-{str(e)} sitem:{str(sitem)}")
return None
return renderer
def get_renderer(self, item):
def _get_renderer(self, item):
if item.get("liveChatTextMessageRenderer"):
renderer = LiveChatTextMessageRenderer(item)
elif item.get("liveChatPaidMessageRenderer"):

View File

@@ -15,11 +15,11 @@ class BaseRenderer:
self.timestamp = int(timestampUsec/1000)
tst = self.renderer.get("timestampText")
if tst:
self.timestampText = tst.get("simpleText")
self.elapsedTime = tst.get("simpleText")
else:
self.timestampText = ""
self.elapsedTime = ""
self.datetime = self.get_datetime(timestampUsec)
self.message = self.get_message(self.renderer)
self.message ,self.messageEx = self.get_message(self.renderer)
self.id = self.renderer.get('id')
self.amountValue= 0.0
self.amountString = ""
@@ -43,6 +43,7 @@ class BaseRenderer:
def get_message(self,renderer):
message = ''
message_ex = []
if renderer.get("message"):
runs=renderer["message"].get("runs")
if runs:
@@ -50,9 +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_badges(self,renderer):
isVerified = False

View File

@@ -1,12 +1,42 @@
import re
from . import currency
from .paidmessage import LiveChatPaidMessageRenderer
from .base import BaseRenderer
superchat_regex = re.compile(r"^(\D*)(\d{1,3}(,\d{3})*(\.\d*)*\b)$")
class LiveChatPaidStickerRenderer(LiveChatPaidMessageRenderer):
class LiveChatPaidStickerRenderer(BaseRenderer):
def __init__(self, item):
super().__init__(item, "superSticker")
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
self.bgColor = self.renderer.get("moneyChipBackgroundColor", 0)
self.sticker = "https:"+self.renderer["sticker"]["thumbnails"][0]["url"]
def get_amountdata(self,renderer):
amountDisplayString = renderer["purchaseAmountText"]["simpleText"]
m = superchat_regex.search(amountDisplayString)
if m:
symbol = m.group(1)
amount = float(m.group(2).replace(',',''))
else:
symbol = ""
amount = 0.0
return amountDisplayString, symbol, amount

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

@@ -0,0 +1,204 @@
"""
speedmeter.py
チャットの勢いを算出するChatProcessor
Calculate speed of chat.
"""
import calendar, datetime, pytz
from .chat_processor import ChatProcessor
class RingQueue:
"""
リング型キュー
Attributes
----------
items : list
格納されているアイテムのリスト。
first_pos : int
キュー内の一番古いアイテムを示すリストのインデックス。
last_pos : int
キュー内の一番新しいアイテムを示すリストのインデックス。
mergin : boolean
キュー内に余裕があるか。キュー内のアイテム個数が、キューの最大個数未満であればTrue。
"""
def __init__(self, capacity):
"""
コンストラクタ
Parameter
----------
capacity:このキューに格納するアイテムの最大個数。
格納時に最大個数を超える場合は一番古いアイテムから
上書きする。
"""
if capacity <= 0:
raise ValueError
self.items = list()
self.capacity = capacity
self.first_pos = 0
self.last_pos = 0
self.mergin = True
def put(self, item):
"""
引数itemに指定されたアイテムをこのキューに格納する。
キューの最大個数を超える場合は、一番古いアイテムの位置に上書きする。
Parameter
----------
item:格納するアイテム
"""
if self.mergin:
self.items.append(item)
self.last_pos = len(self.items)-1
if self.last_pos == self.capacity-1:
self.mergin = False
return
self.last_pos += 1
if self.last_pos > self.capacity-1:
self.last_pos = 0
self.items[self.last_pos] = item
self.first_pos += 1
if self.first_pos > self.capacity-1:
self.first_pos = 0
def get(self):
"""
キュー内の一番古いアイテムへの参照を返す
(アイテムは削除しない)
Return
----------
キュー内の一番古いアイテムへの参照
"""
return self.items[self.first_pos]
def item_count(self):
return len(self.items)
class SpeedCalculator(ChatProcessor, RingQueue):
"""
チャットの勢いを計算する。
一定期間のチャットデータのうち、最初のチャットの投稿時刻と
最後のチャットの投稿時刻の差を、チャット数で割り返し
1分あたりの速度に換算する。
Parameter
----------
capacity : int
RingQueueに格納するチャット勢い算出用データの最大数
"""
def __init__(self, capacity = 10):
super().__init__(capacity)
self.speed = 0
def process(self, chat_components: list):
chatdata = []
if chat_components:
for component in chat_components:
if component.get("chatdata"):
chatdata.extend(component.get("chatdata"))
self._put_chatdata(chatdata)
self.speed = self._calc_speed()
return self.speed
def _calc_speed(self):
"""
RingQueue内のチャット勢い算出用データリストを元に、
チャット速度を計算して返す
Return
---------------------------
チャット速度(1分間で換算したチャット数)
"""
try:
#キュー内の総チャット数
total = sum(item['chat_count'] for item in self.items)
#キュー内の最初と最後のチャットの時間差
duration = (self.items[self.last_pos]['endtime']
- self.items[self.first_pos]['starttime'])
if duration != 0:
return int(total*60/duration)
return 0
except IndexError:
return 0
def _put_chatdata(self, actions):
"""
チャットデータからタイムスタンプを読み取り、勢い測定用のデータを組み立て、
RingQueueに投入する。
200円以上のスパチャはtickerとmessageの2つのデータが生成されるが、
tickerの方は時刻データの場所が異なることを利用し、勢いの集計から除外している。
Parameter
---------
actions : List[dict]
チャットデータ(addChatItemAction) のリスト
"""
def _put_emptydata():
'''
チャットデータがない場合に空のデータをキューに投入する。
'''
timestamp_now = calendar.timegm(datetime.datetime.
now(pytz.utc).utctimetuple())
self.put({
'chat_count':0,
'starttime':int(timestamp_now),
'endtime':int(timestamp_now)
})
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:
_put_emptydata()
return
#actions内の時刻データを持つチャットデータの数
counter=0
#actions内の最初のチャットデータの時刻
starttime= None
#actions内の最後のチャットデータの時刻
endtime=None
for action in actions:
#チャットデータからtimestampUsecを読み取る
gettime = _get_timestamp(action)
#時刻のないデータだった場合は次の行のデータで読み取り試行
if gettime is None:
continue
#最初に有効な時刻を持つデータのtimestampをstarttimeに設定
if starttime is None:
starttime = gettime
#最後のtimestampを設定(途中で時刻のないデータの場合もあるので上書きしていく)
endtime = gettime
#チャットの数をインクリメント
counter += 1
#チャット速度用のデータをRingQueueに送る
if starttime is None or endtime is None:
_put_emptydata()
return
self.put({
'chat_count':counter,
'starttime':int(starttime/1000000),
'endtime':int(endtime/1000000)
})

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

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

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