Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b576c3f928 | ||
|
|
c0728e1366 | ||
|
|
fff09d4c27 | ||
|
|
810b6c8c6b | ||
|
|
dfada86caf | ||
|
|
91aa944df5 | ||
|
|
6ac5191e85 | ||
|
|
fff3e0371f | ||
|
|
a70efe8a67 | ||
|
|
dc47f4debe | ||
|
|
ab5a2a8df2 | ||
|
|
5a79f26fa7 | ||
|
|
18666199b7 | ||
|
|
b357bccb98 | ||
|
|
3c1f079d5f | ||
|
|
289841a000 | ||
|
|
ee0ff7fe74 | ||
|
|
c0870ce537 | ||
|
|
de6ef2490e | ||
|
|
b8bdbdc36f | ||
|
|
9f5d3f323e | ||
|
|
cf9aae3322 | ||
|
|
6ac2315936 | ||
|
|
50c8e34080 | ||
|
|
2d3da91d51 | ||
|
|
3ac71985ff | ||
|
|
13bdf0376b | ||
|
|
b2ffdaec0c | ||
|
|
c85786679f | ||
|
|
c7a7886672 | ||
|
|
12996fb44d | ||
|
|
c884ef7288 | ||
|
|
2cd9e98fc2 | ||
|
|
2ac4c99ab4 | ||
|
|
51bf8ad738 | ||
|
|
2e70e74bcd | ||
|
|
a39d6cb420 | ||
|
|
5dd0cb45b7 | ||
|
|
24873651a6 | ||
|
|
0e060bf998 | ||
|
|
817fed9d1d | ||
|
|
823f7fefa4 | ||
|
|
aa894fc52b |
103
README.md
103
README.md
@@ -8,7 +8,7 @@ pytchat is a python library for fetching youtube live chat
|
|||||||
without using youtube api, Selenium or BeautifulSoup.
|
without using youtube api, Selenium or BeautifulSoup.
|
||||||
|
|
||||||
Other features:
|
Other features:
|
||||||
+ Customizable chat data processors including yt api compatible one.
|
+ Customizable chat data processors including youtube api compatible one.
|
||||||
+ Available on asyncio context.
|
+ Available on asyncio context.
|
||||||
+ Quick fetching of initial chat data by generating continuation params
|
+ Quick fetching of initial chat data by generating continuation params
|
||||||
instead of web scraping.
|
instead of web scraping.
|
||||||
@@ -29,10 +29,10 @@ from pytchat import LiveChat
|
|||||||
|
|
||||||
chat = LiveChat("G1w62uEMZ74")
|
chat = LiveChat("G1w62uEMZ74")
|
||||||
while chat.is_alive():
|
while chat.is_alive():
|
||||||
data = chat.get()
|
data = chat.get()
|
||||||
for c in data.items:
|
for c in data.items:
|
||||||
print(f"{c.datetime} [{c.author.name}]-{c.message} {c.amountString}")
|
print(f"{c.datetime} [{c.author.name}]-{c.message} {c.amountString}")
|
||||||
data.tick()
|
data.tick()
|
||||||
```
|
```
|
||||||
|
|
||||||
### callback mode
|
### callback mode
|
||||||
@@ -40,15 +40,17 @@ while chat.is_alive():
|
|||||||
from pytchat import LiveChat
|
from pytchat import LiveChat
|
||||||
import time
|
import time
|
||||||
|
|
||||||
chat = LiveChat("G1w62uEMZ74", callback = func)
|
def main()
|
||||||
while chat.is_alive():
|
chat = LiveChat("G1w62uEMZ74", callback = func)
|
||||||
#other background operation here.
|
while chat.is_alive():
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
|
#other background operation.
|
||||||
|
|
||||||
|
#callback function is automatically called periodically.
|
||||||
def func(data):
|
def func(data):
|
||||||
for c in data.items:
|
for c in data.items:
|
||||||
print(f"{c.datetime} [{c.author.name}]-{c.message} {c.amountString}")
|
print(f"{c.datetime} [{c.author.name}]-{c.message} {c.amountString}")
|
||||||
data.tick()
|
data.tick()
|
||||||
```
|
```
|
||||||
|
|
||||||
### asyncio context:
|
### asyncio context:
|
||||||
@@ -57,15 +59,16 @@ from pytchat import LiveChatAsync
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
chat = LiveChatAsync("G1w62uEMZ74", callback = func)
|
chat = LiveChatAsync("G1w62uEMZ74", callback = func)
|
||||||
while chat.is_alive():
|
while chat.is_alive():
|
||||||
#other background operation here.
|
await asyncio.sleep(3)
|
||||||
await asyncio.sleep(3)
|
#other background operation.
|
||||||
|
|
||||||
|
#callback function is automatically called periodically.
|
||||||
async def func(data):
|
async def func(data):
|
||||||
for c in data.items:
|
for c in data.items:
|
||||||
print(f"{c.datetime} [{c.author.name}]-{c.message} {c.amountString}")
|
print(f"{c.datetime} [{c.author.name}]-{c.message} {c.amountString}")
|
||||||
await data.tick_async()
|
await data.tick_async()
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
loop.run_until_complete(main())
|
loop.run_until_complete(main())
|
||||||
@@ -77,16 +80,16 @@ loop.run_until_complete(main())
|
|||||||
from pytchat import LiveChat, CompatibleProcessor
|
from pytchat import LiveChat, CompatibleProcessor
|
||||||
|
|
||||||
chat = LiveChat("G1w62uEMZ74",
|
chat = LiveChat("G1w62uEMZ74",
|
||||||
processor = CompatibleProcessor() )
|
processor = CompatibleProcessor() )
|
||||||
|
|
||||||
while chat.is_alive():
|
while chat.is_alive():
|
||||||
data = chat.get()
|
data = chat.get()
|
||||||
polling = data["pollingIntervalMillis"]/1000
|
polling = data["pollingIntervalMillis"]/1000
|
||||||
for c in data["items"]:
|
for c in data["items"]:
|
||||||
if c.get("snippet"):
|
if c.get("snippet"):
|
||||||
print(f"[{c['authorDetails']['displayName']}]"
|
print(f"[{c['authorDetails']['displayName']}]"
|
||||||
f"-{c['snippet']['displayMessage']}")
|
f"-{c['snippet']['displayMessage']}")
|
||||||
time.sleep(polling/len(data["items"]))
|
time.sleep(polling/len(data["items"]))
|
||||||
|
|
||||||
```
|
```
|
||||||
### replay:
|
### replay:
|
||||||
@@ -95,27 +98,28 @@ from pytchat import ReplayChatAsync
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
chat = ReplayChatAsync("G1w62uEMZ74", seektime = 1000, callback = func)
|
chat = ReplayChatAsync("G1w62uEMZ74", seektime = 1000, callback = func)
|
||||||
while chat.is_alive():
|
while chat.is_alive():
|
||||||
#other background operation here.
|
await asyncio.sleep(3)
|
||||||
await asyncio.sleep(3)
|
#other background operation here.
|
||||||
|
|
||||||
|
#callback function is automatically called periodically.
|
||||||
async def func(data):
|
async def func(data):
|
||||||
for count in range(0,len(data.items)):
|
for count in range(0,len(data.items)):
|
||||||
c= data.items[count]
|
c= data.items[count]
|
||||||
if count!=len(data.items):
|
if count!=len(data.items):
|
||||||
tick=data.items[count+1].timestamp -data.items[count].timestamp
|
tick=data.items[count+1].timestamp -data.items[count].timestamp
|
||||||
else:
|
else:
|
||||||
tick=0
|
tick=0
|
||||||
print(f"<{c.timestampText}> [{c.author.name}]-{c.message} {c.amountString}")
|
print(f"<{c.elapsedTime}> [{c.author.name}]-{c.message} {c.amountString}")
|
||||||
await asyncio.sleep(tick/1000)
|
await asyncio.sleep(tick/1000)
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
loop.run_until_complete(main())
|
loop.run_until_complete(main())
|
||||||
```
|
```
|
||||||
|
|
||||||
## Chatdata Structure of Default Processor
|
## Structure of Default Processor
|
||||||
Structure of each item which got from items() function.
|
Each item can be got with items() function.
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>name</th>
|
<th>name</th>
|
||||||
@@ -137,6 +141,11 @@ Structure of each item which got from items() function.
|
|||||||
<td>str</td>
|
<td>str</td>
|
||||||
<td>emojis are represented by ":(shortcut text):"</td>
|
<td>emojis are represented by ":(shortcut text):"</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>messageEx</td>
|
||||||
|
<td>str</td>
|
||||||
|
<td>list of message texts and emoji URLs.</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>timestamp</td>
|
<td>timestamp</td>
|
||||||
<td>int</td>
|
<td>int</td>
|
||||||
@@ -145,26 +154,26 @@ Structure of each item which got from items() function.
|
|||||||
<tr>
|
<tr>
|
||||||
<td>datetime</td>
|
<td>datetime</td>
|
||||||
<td>str</td>
|
<td>str</td>
|
||||||
<td>ex. "2019-10-10 12:34:56"</td>
|
<td>e.g. "2019-10-10 12:34:56"</td>
|
||||||
</tr>
|
</tr>
|
||||||
<td>timestampText</td>
|
<td>elapsedTime</td>
|
||||||
<td>str</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>
|
||||||
<tr>
|
<tr>
|
||||||
<td>amountValue</td>
|
<td>amountValue</td>
|
||||||
<td>float</td>
|
<td>float</td>
|
||||||
<td>ex. 1,234.0</td>
|
<td>e.g. 1,234.0</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>amountString</td>
|
<td>amountString</td>
|
||||||
<td>str</td>
|
<td>str</td>
|
||||||
<td>ex. "$ 1,234"</td>
|
<td>e.g. "$ 1,234"</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>currency</td>
|
<td>currency</td>
|
||||||
<td>str</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>
|
||||||
<tr>
|
<tr>
|
||||||
<td>bgColor</td>
|
<td>bgColor</td>
|
||||||
@@ -193,7 +202,7 @@ Structure of author object.
|
|||||||
<tr>
|
<tr>
|
||||||
<td>channelId</td>
|
<td>channelId</td>
|
||||||
<td>str</td>
|
<td>str</td>
|
||||||
<td></td>
|
<td>*chatter's channel ID.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>channelUrl</td>
|
<td>channelUrl</td>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
pytchat is a python library for fetching youtube live chat without using yt api, Selenium, or BeautifulSoup.
|
pytchat is a python library for fetching youtube live chat without using yt api, Selenium, or BeautifulSoup.
|
||||||
"""
|
"""
|
||||||
__copyright__ = 'Copyright (C) 2019 taizan-hokuto'
|
__copyright__ = 'Copyright (C) 2019 taizan-hokuto'
|
||||||
__version__ = '0.0.3.1'
|
__version__ = '0.0.3.8'
|
||||||
__license__ = 'MIT'
|
__license__ = 'MIT'
|
||||||
__author__ = 'taizan-hokuto'
|
__author__ = 'taizan-hokuto'
|
||||||
__author_email__ = '55448286+taizan-hokuto@users.noreply.github.com'
|
__author_email__ = '55448286+taizan-hokuto@users.noreply.github.com'
|
||||||
@@ -18,5 +18,6 @@ from .api import (
|
|||||||
ChatProcessor,
|
ChatProcessor,
|
||||||
CompatibleProcessor,
|
CompatibleProcessor,
|
||||||
SimpleDisplayProcessor,
|
SimpleDisplayProcessor,
|
||||||
JsonfileArchiveProcessor
|
JsonfileArchiveProcessor,
|
||||||
|
SpeedCalculator
|
||||||
)
|
)
|
||||||
@@ -7,4 +7,4 @@ from .processors.default.processor import DefaultProcessor
|
|||||||
from .processors.compatible.processor import CompatibleProcessor
|
from .processors.compatible.processor import CompatibleProcessor
|
||||||
from .processors.simple_display_processor import SimpleDisplayProcessor
|
from .processors.simple_display_processor import SimpleDisplayProcessor
|
||||||
from .processors.jsonfile_archive_processor import JsonfileArchiveProcessor
|
from .processors.jsonfile_archive_processor import JsonfileArchiveProcessor
|
||||||
|
from .processors.speed_calculator import SpeedCalculator
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
LOGGER_MODE = logging.ERROR
|
LOGGER_MODE = None
|
||||||
headers = {
|
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'}
|
'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'}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import datetime
|
|||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
import signal
|
import signal
|
||||||
import threading
|
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
@@ -123,7 +122,7 @@ class LiveChatAsync:
|
|||||||
|
|
||||||
async def _startlisten(self):
|
async def _startlisten(self):
|
||||||
"""最初のcontinuationパラメータを取得し、
|
"""最初のcontinuationパラメータを取得し、
|
||||||
_listenループを開始する
|
_listenループのタスクを作成し開始する
|
||||||
"""
|
"""
|
||||||
initial_continuation = await self._get_initial_continuation()
|
initial_continuation = await self._get_initial_continuation()
|
||||||
if initial_continuation is None:
|
if initial_continuation is None:
|
||||||
@@ -287,12 +286,3 @@ class LiveChatAsync:
|
|||||||
await asyncio.gather(*tasks,return_exceptions=True)
|
await asyncio.gather(*tasks,return_exceptions=True)
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
loop.stop()
|
loop.stop()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,14 +21,19 @@ logger = mylogger.get_logger(__name__,mode=config.LOGGER_MODE)
|
|||||||
MAX_RETRY = 10
|
MAX_RETRY = 10
|
||||||
headers = config.headers
|
headers = config.headers
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ReplayChatAsync:
|
class ReplayChatAsync:
|
||||||
''' aiohttpを利用してYouTubeのライブ配信のチャットデータを取得する
|
'''asyncio(aiohttp)を利用してYouTubeのチャットデータを取得する。
|
||||||
|
|
||||||
Parameter
|
Parameter
|
||||||
---------
|
---------
|
||||||
video_id : str
|
video_id : str
|
||||||
動画ID
|
動画ID
|
||||||
|
|
||||||
|
seektime : int
|
||||||
|
リプレイするチャットデータの開始時間(秒)
|
||||||
|
|
||||||
processor : ChatProcessor
|
processor : ChatProcessor
|
||||||
チャットデータを加工するオブジェクト
|
チャットデータを加工するオブジェクト
|
||||||
|
|
||||||
@@ -46,6 +51,9 @@ class ReplayChatAsync:
|
|||||||
done_callback : func
|
done_callback : func
|
||||||
listener終了時に呼び出すコールバック。
|
listener終了時に呼び出すコールバック。
|
||||||
|
|
||||||
|
exception_handler : func
|
||||||
|
例外を処理する関数
|
||||||
|
|
||||||
direct_mode : bool
|
direct_mode : bool
|
||||||
Trueの場合、bufferを使わずにcallbackを呼ぶ。
|
Trueの場合、bufferを使わずにcallbackを呼ぶ。
|
||||||
Trueの場合、callbackの設定が必須
|
Trueの場合、callbackの設定が必須
|
||||||
@@ -53,26 +61,23 @@ class ReplayChatAsync:
|
|||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
---------
|
---------
|
||||||
_executor : ThreadPoolExecutor
|
|
||||||
チャットデータ取得ループ(_listen)用のスレッド
|
|
||||||
|
|
||||||
_is_alive : bool
|
_is_alive : bool
|
||||||
チャット取得を終了したか
|
チャット取得を停止するためのフラグ
|
||||||
'''
|
'''
|
||||||
|
|
||||||
_setup_finished = False
|
_setup_finished = False
|
||||||
|
|
||||||
def __init__(self, video_id,
|
def __init__(self, video_id,
|
||||||
seektime =0,
|
seektime = 0,
|
||||||
processor = DefaultProcessor(),
|
processor = DefaultProcessor(),
|
||||||
buffer = Buffer(maxsize = 20),
|
buffer = None,
|
||||||
interruptable = True,
|
interruptable = True,
|
||||||
callback = None,
|
callback = None,
|
||||||
done_callback = None,
|
done_callback = None,
|
||||||
exception_handler = None,
|
exception_handler = None,
|
||||||
direct_mode = False):
|
direct_mode = False):
|
||||||
self.video_id = video_id
|
self.video_id = video_id
|
||||||
self.seektime= seektime
|
self.seektime = seektime
|
||||||
self.processor = processor
|
self.processor = processor
|
||||||
self._buffer = buffer
|
self._buffer = buffer
|
||||||
self._callback = callback
|
self._callback = callback
|
||||||
@@ -151,8 +156,8 @@ class ReplayChatAsync:
|
|||||||
|
|
||||||
async def _listen(self, continuation):
|
async def _listen(self, continuation):
|
||||||
''' continuationに紐付いたチャットデータを取得し
|
''' continuationに紐付いたチャットデータを取得し
|
||||||
にチャットデータを格納、
|
Bufferにチャットデータを格納、
|
||||||
次のcontinuaitonを取得してループする
|
次のcontinuaitonを取得してループする。
|
||||||
|
|
||||||
Parameter
|
Parameter
|
||||||
---------
|
---------
|
||||||
@@ -163,10 +168,10 @@ class ReplayChatAsync:
|
|||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
while(continuation and self._is_alive):
|
while(continuation and self._is_alive):
|
||||||
if self._pauser.empty():
|
if self._pauser.empty():
|
||||||
#pauseが呼ばれて_pauserが空状態のときは一時停止する
|
#pause
|
||||||
await self._pauser.get()
|
await self._pauser.get()
|
||||||
#resumeが呼ばれて_pauserにitemが入ったら再開する
|
#resume
|
||||||
#直後に_pauserにitem(None)を入れてブロックを防ぐ
|
#prohibit from blocking by putting None into _pauser.
|
||||||
self._pauser.put_nowait(None)
|
self._pauser.put_nowait(None)
|
||||||
livechat_json = (await
|
livechat_json = (await
|
||||||
self._get_livechat_json(continuation, session, headers)
|
self._get_livechat_json(continuation, session, headers)
|
||||||
@@ -186,11 +191,10 @@ class ReplayChatAsync:
|
|||||||
else:
|
else:
|
||||||
await self._buffer.put(chat_component)
|
await self._buffer.put(chat_component)
|
||||||
diff_time = timeout - (time.time()-time_mark)
|
diff_time = timeout - (time.time()-time_mark)
|
||||||
if diff_time < 0 : diff_time=0
|
|
||||||
await asyncio.sleep(diff_time)
|
await asyncio.sleep(diff_time)
|
||||||
continuation = metadata.get('continuation')
|
continuation = metadata.get('continuation')
|
||||||
except ChatParseException as e:
|
except ChatParseException as e:
|
||||||
logger.error(f"{str(e)}(動画ID:\"{self.video_id}\")")
|
logger.info(f"{str(e)}(video_id:\"{self.video_id}\")")
|
||||||
return
|
return
|
||||||
except (TypeError , json.JSONDecodeError) :
|
except (TypeError , json.JSONDecodeError) :
|
||||||
logger.error(f"{traceback.format_exc(limit = -1)}")
|
logger.error(f"{traceback.format_exc(limit = -1)}")
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class LiveChat:
|
|||||||
チャットデータ取得ループ(_listen)用のスレッド
|
チャットデータ取得ループ(_listen)用のスレッド
|
||||||
|
|
||||||
_is_alive : bool
|
_is_alive : bool
|
||||||
チャット取得を終了したか
|
チャット取得を停止するためのフラグ
|
||||||
'''
|
'''
|
||||||
|
|
||||||
_setup_finished = False
|
_setup_finished = False
|
||||||
@@ -142,7 +142,7 @@ class LiveChat:
|
|||||||
|
|
||||||
def _listen(self, continuation):
|
def _listen(self, continuation):
|
||||||
''' continuationに紐付いたチャットデータを取得し
|
''' continuationに紐付いたチャットデータを取得し
|
||||||
にチャットデータを格納、
|
BUfferにチャットデータを格納、
|
||||||
次のcontinuaitonを取得してループする
|
次のcontinuaitonを取得してループする
|
||||||
|
|
||||||
Parameter
|
Parameter
|
||||||
@@ -157,7 +157,6 @@ class LiveChat:
|
|||||||
self._get_livechat_json(continuation, session, headers)
|
self._get_livechat_json(continuation, session, headers)
|
||||||
)
|
)
|
||||||
metadata, chatdata = self._parser.parse( livechat_json )
|
metadata, chatdata = self._parser.parse( livechat_json )
|
||||||
#チャットデータを含むコンポーネントを組み立ててbufferに投入する
|
|
||||||
timeout = metadata['timeoutMs']/1000
|
timeout = metadata['timeoutMs']/1000
|
||||||
chat_component = {
|
chat_component = {
|
||||||
"video_id" : self.video_id,
|
"video_id" : self.video_id,
|
||||||
@@ -171,16 +170,12 @@ class LiveChat:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self._buffer.put(chat_component)
|
self._buffer.put(chat_component)
|
||||||
#次のchatを取得するまでsleepする
|
|
||||||
diff_time = timeout - (time.time()-time_mark)
|
diff_time = timeout - (time.time()-time_mark)
|
||||||
if diff_time < 0 : diff_time=0
|
if diff_time < 0 : diff_time=0
|
||||||
time.sleep(diff_time)
|
time.sleep(diff_time)
|
||||||
#次のチャットデータのcontinuationパラメータを取り出す。
|
|
||||||
continuation = metadata.get('continuation')
|
continuation = metadata.get('continuation')
|
||||||
|
|
||||||
#whileループ先頭に戻る
|
|
||||||
except ChatParseException as e:
|
except ChatParseException as e:
|
||||||
logger.error(f"{str(e)}(動画ID:\"{self.video_id}\")")
|
logger.info(f"{str(e)}(video_id:\"{self.video_id}\")")
|
||||||
return
|
return
|
||||||
except (TypeError , json.JSONDecodeError) :
|
except (TypeError , json.JSONDecodeError) :
|
||||||
logger.error(f"{traceback.format_exc(limit = -1)}")
|
logger.error(f"{traceback.format_exc(limit = -1)}")
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ class ReplayChat:
|
|||||||
video_id : str
|
video_id : str
|
||||||
動画ID
|
動画ID
|
||||||
|
|
||||||
|
seektime : int
|
||||||
|
リプレイするチャットデータの開始時間(秒)
|
||||||
|
|
||||||
processor : ChatProcessor
|
processor : ChatProcessor
|
||||||
チャットデータを加工するオブジェクト
|
チャットデータを加工するオブジェクト
|
||||||
|
|
||||||
@@ -65,7 +68,7 @@ class ReplayChat:
|
|||||||
#チャット監視中のListenerのリスト
|
#チャット監視中のListenerのリスト
|
||||||
_listeners= []
|
_listeners= []
|
||||||
def __init__(self, video_id,
|
def __init__(self, video_id,
|
||||||
seektime =0,
|
seektime = 0,
|
||||||
processor = DefaultProcessor(),
|
processor = DefaultProcessor(),
|
||||||
buffer = Buffer(maxsize = 20),
|
buffer = Buffer(maxsize = 20),
|
||||||
interruptable = True,
|
interruptable = True,
|
||||||
@@ -74,7 +77,7 @@ class ReplayChat:
|
|||||||
direct_mode = False
|
direct_mode = False
|
||||||
):
|
):
|
||||||
self.video_id = video_id
|
self.video_id = video_id
|
||||||
self.seektime= seektime
|
self.seektime = seektime
|
||||||
self.processor = processor
|
self.processor = processor
|
||||||
self._buffer = buffer
|
self._buffer = buffer
|
||||||
self._callback = callback
|
self._callback = callback
|
||||||
@@ -159,16 +162,15 @@ class ReplayChat:
|
|||||||
with requests.Session() as session:
|
with requests.Session() as session:
|
||||||
while(continuation and self._is_alive):
|
while(continuation and self._is_alive):
|
||||||
if self._pauser.empty():
|
if self._pauser.empty():
|
||||||
#pauseが呼ばれて_pauserが空状態のときは一時停止する
|
#pause
|
||||||
self._pauser.get()
|
self._pauser.get()
|
||||||
#resumeが呼ばれて_pauserにitemが入ったら再開する
|
#resume
|
||||||
#直後に_pauserにitem(None)を入れてブロックを防ぐ
|
#prohibit from blocking by putting None into _pauser.
|
||||||
self._pauser.put_nowait(None)
|
self._pauser.put_nowait(None)
|
||||||
livechat_json = (
|
livechat_json = (
|
||||||
self._get_livechat_json(continuation, session, headers)
|
self._get_livechat_json(continuation, session, headers)
|
||||||
)
|
)
|
||||||
metadata, chatdata = self._parser.parse( livechat_json )
|
metadata, chatdata = self._parser.parse( livechat_json )
|
||||||
#チャットデータを含むコンポーネントを組み立ててbufferに投入する
|
|
||||||
timeout = metadata['timeoutMs']/1000
|
timeout = metadata['timeoutMs']/1000
|
||||||
chat_component = {
|
chat_component = {
|
||||||
"video_id" : self.video_id,
|
"video_id" : self.video_id,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ def get_logger(modname,mode=logging.DEBUG):
|
|||||||
logger.addHandler(handler1)
|
logger.addHandler(handler1)
|
||||||
#create handler2 for recording log file
|
#create handler2 for recording log file
|
||||||
if mode <= logging.DEBUG:
|
if mode <= logging.DEBUG:
|
||||||
handler2 = logging.FileHandler(filename="log.txt")
|
handler2 = logging.FileHandler(filename="log.txt", encoding='utf-8')
|
||||||
handler2.setLevel(logging.ERROR)
|
handler2.setLevel(logging.ERROR)
|
||||||
handler2.setFormatter(my_formatter)
|
handler2.setFormatter(my_formatter)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
"""
|
||||||
|
pytchat.parser.live
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
This module is parser of live chat JSON.
|
||||||
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from .. import config
|
from .. import config
|
||||||
from .. import mylogger
|
from .. import mylogger
|
||||||
@@ -12,6 +18,27 @@ logger = mylogger.get_logger(__name__,mode=config.LOGGER_MODE)
|
|||||||
|
|
||||||
class Parser:
|
class Parser:
|
||||||
def parse(self, jsn):
|
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:
|
if jsn is None:
|
||||||
return {'timeoutMs':0,'continuation':None},[]
|
return {'timeoutMs':0,'continuation':None},[]
|
||||||
if jsn['response']['responseContext'].get('errors'):
|
if jsn['response']['responseContext'].get('errors'):
|
||||||
|
|||||||
@@ -12,6 +12,31 @@ logger = mylogger.get_logger(__name__,mode=config.LOGGER_MODE)
|
|||||||
|
|
||||||
class Parser:
|
class Parser:
|
||||||
def parse(self, jsn):
|
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:
|
if jsn is None:
|
||||||
return {'timeoutMs':0,'continuation':None},[]
|
return {'timeoutMs':0,'continuation':None},[]
|
||||||
if jsn['response']['responseContext'].get('errors'):
|
if jsn['response']['responseContext'].get('errors'):
|
||||||
@@ -36,9 +61,9 @@ class Parser:
|
|||||||
raise NoContentsException('チャットデータを取得できませんでした。')
|
raise NoContentsException('チャットデータを取得できませんでした。')
|
||||||
interval = self.get_interval(actions)
|
interval = self.get_interval(actions)
|
||||||
metadata.setdefault("timeoutMs",interval)
|
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
|
return metadata, chatdata
|
||||||
|
|
||||||
def get_interval(self, actions: list):
|
def get_interval(self, actions: list):
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ class ChatProcessor:
|
|||||||
|
|
||||||
Parameter
|
Parameter
|
||||||
----------
|
----------
|
||||||
chat_components: [LIST:component]
|
chat_components: List[component]
|
||||||
component : dict {
|
component : dict {
|
||||||
"video_id" : str
|
"video_id" : str
|
||||||
動画ID
|
動画ID
|
||||||
"timeout" : int
|
"timeout" : int
|
||||||
次のチャットの再読み込みまでの時間(秒)
|
次のチャットの再読み込みまでの時間(秒)
|
||||||
"chatdata" : list<object>
|
"chatdata" : List[dict]
|
||||||
チャットデータ(actions)のリスト
|
チャットデータのリスト
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ from .renderer.textmessage import LiveChatTextMessageRenderer
|
|||||||
from .renderer.paidmessage import LiveChatPaidMessageRenderer
|
from .renderer.paidmessage import LiveChatPaidMessageRenderer
|
||||||
from .renderer.paidsticker import LiveChatPaidStickerRenderer
|
from .renderer.paidsticker import LiveChatPaidStickerRenderer
|
||||||
from .renderer.legacypaid import LiveChatLegacyPaidMessageRenderer
|
from .renderer.legacypaid import LiveChatLegacyPaidMessageRenderer
|
||||||
|
from .. chat_processor import ChatProcessor
|
||||||
from ... import mylogger
|
from ... import mylogger
|
||||||
from ... import config
|
from ... import config
|
||||||
logger = mylogger.get_logger(__name__,mode=config.LOGGER_MODE)
|
logger = mylogger.get_logger(__name__,mode=config.LOGGER_MODE)
|
||||||
|
|
||||||
class CompatibleProcessor:
|
class CompatibleProcessor(ChatProcessor):
|
||||||
|
|
||||||
def process(self, chat_components: list):
|
def process(self, chat_components: list):
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ from .renderer.textmessage import LiveChatTextMessageRenderer
|
|||||||
from .renderer.paidmessage import LiveChatPaidMessageRenderer
|
from .renderer.paidmessage import LiveChatPaidMessageRenderer
|
||||||
from .renderer.paidsticker import LiveChatPaidStickerRenderer
|
from .renderer.paidsticker import LiveChatPaidStickerRenderer
|
||||||
from .renderer.legacypaid import LiveChatLegacyPaidMessageRenderer
|
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)
|
||||||
|
|
||||||
class Chatdata:
|
class Chatdata:
|
||||||
def __init__(self,chatlist:list, timeout:float):
|
def __init__(self,chatlist:list, timeout:float):
|
||||||
@@ -23,7 +26,7 @@ class Chatdata:
|
|||||||
return
|
return
|
||||||
await asyncio.sleep(self.interval/len(self.items))
|
await asyncio.sleep(self.interval/len(self.items))
|
||||||
|
|
||||||
class DefaultProcessor:
|
class DefaultProcessor(ChatProcessor):
|
||||||
def process(self, chat_components: list):
|
def process(self, chat_components: list):
|
||||||
|
|
||||||
chatlist = []
|
chatlist = []
|
||||||
@@ -40,32 +43,31 @@ class DefaultProcessor:
|
|||||||
if action.get('addChatItemAction') is None: continue
|
if action.get('addChatItemAction') is None: continue
|
||||||
if action['addChatItemAction'].get('item') is None: continue
|
if action['addChatItemAction'].get('item') is None: continue
|
||||||
|
|
||||||
chat = self.parse(action)
|
chat = self._parse(action)
|
||||||
if chat:
|
if chat:
|
||||||
chatlist.append(chat)
|
chatlist.append(chat)
|
||||||
return Chatdata(chatlist, float(timeout))
|
return Chatdata(chatlist, float(timeout))
|
||||||
|
|
||||||
|
|
||||||
def parse(self, sitem):
|
def _parse(self, sitem):
|
||||||
|
|
||||||
action = sitem.get("addChatItemAction")
|
action = sitem.get("addChatItemAction")
|
||||||
if action:
|
if action:
|
||||||
item = action.get("item")
|
item = action.get("item")
|
||||||
if item is None: return None
|
if item is None: return None
|
||||||
try:
|
try:
|
||||||
renderer = self.get_renderer(item)
|
renderer = self._get_renderer(item)
|
||||||
if renderer == None:
|
if renderer == None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
renderer.get_snippet()
|
renderer.get_snippet()
|
||||||
renderer.get_authordetails()
|
renderer.get_authordetails()
|
||||||
except (KeyError,TypeError,AttributeError) as e:
|
except (KeyError,TypeError,AttributeError) as e:
|
||||||
print(f"------{str(type(e))}-{str(e)}----------")
|
logger.error(f"{str(type(e))}-{str(e)} sitem:{str(sitem)}")
|
||||||
print(sitem)
|
|
||||||
return None
|
return None
|
||||||
return renderer
|
return renderer
|
||||||
|
|
||||||
def get_renderer(self, item):
|
def _get_renderer(self, item):
|
||||||
if item.get("liveChatTextMessageRenderer"):
|
if item.get("liveChatTextMessageRenderer"):
|
||||||
renderer = LiveChatTextMessageRenderer(item)
|
renderer = LiveChatTextMessageRenderer(item)
|
||||||
elif item.get("liveChatPaidMessageRenderer"):
|
elif item.get("liveChatPaidMessageRenderer"):
|
||||||
|
|||||||
@@ -15,11 +15,12 @@ class BaseRenderer:
|
|||||||
self.timestamp = int(timestampUsec/1000)
|
self.timestamp = int(timestampUsec/1000)
|
||||||
tst = self.renderer.get("timestampText")
|
tst = self.renderer.get("timestampText")
|
||||||
if tst:
|
if tst:
|
||||||
self.timestampText = tst.get("simpleText")
|
self.elapsedTime = tst.get("simpleText")
|
||||||
else:
|
else:
|
||||||
self.timestampText = ""
|
self.elapsedTime = ""
|
||||||
self.datetime = self.get_datetime(timestampUsec)
|
self.datetime = self.get_datetime(timestampUsec)
|
||||||
self.message = self.get_message(self.renderer)
|
self.message = self.get_message(self.renderer)
|
||||||
|
self.messageEx = self.get_message_ex(self.renderer)
|
||||||
self.id = self.renderer.get('id')
|
self.id = self.renderer.get('id')
|
||||||
self.amountValue= 0.0
|
self.amountValue= 0.0
|
||||||
self.amountString = ""
|
self.amountString = ""
|
||||||
@@ -54,6 +55,19 @@ class BaseRenderer:
|
|||||||
message += r.get('text','')
|
message += r.get('text','')
|
||||||
return message
|
return message
|
||||||
|
|
||||||
|
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):
|
def get_badges(self,renderer):
|
||||||
isVerified = False
|
isVerified = False
|
||||||
isChatOwner = False
|
isChatOwner = False
|
||||||
|
|||||||
@@ -1,12 +1,42 @@
|
|||||||
import re
|
import re
|
||||||
from . import currency
|
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):
|
def __init__(self, item):
|
||||||
super().__init__(item, "superSticker")
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
204
pytchat/processors/speed_calculator.py
Normal file
204
pytchat/processors/speed_calculator.py
Normal 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)
|
||||||
|
})
|
||||||
|
|
||||||
68
tests/test_speed_calculator.py
Normal file
68
tests/test_speed_calculator.py
Normal 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
164
tests/testdata/chat.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
24
tests/testdata/speed/speedtest_empty.json
vendored
Normal file
24
tests/testdata/speed/speedtest_empty.json
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"timing": {
|
||||||
|
"info": {
|
||||||
|
"st": 164
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"csn": "",
|
||||||
|
"response": {
|
||||||
|
"responseContext": {
|
||||||
|
},
|
||||||
|
"continuationContents": {
|
||||||
|
"liveChatContinuation": {
|
||||||
|
"continuations": [{
|
||||||
|
"timedContinuationData": {
|
||||||
|
"timeoutMs": 10000,
|
||||||
|
"continuation": "continuation"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
188
tests/testdata/speed/speedtest_normal.json
vendored
Normal file
188
tests/testdata/speed/speedtest_normal.json
vendored
Normal 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"
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
42
tests/testdata/speed/speedtest_undefined.json
vendored
Normal file
42
tests/testdata/speed/speedtest_undefined.json
vendored
Normal 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"
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user