Compare commits

..

57 Commits

Author SHA1 Message Date
taizan-hokuto
cf6ed24864 Merge branch 'release/v0.0.9' 2020-07-13 01:55:13 +09:00
taizan-hokuto
167c8acb93 Incerment version 2020-07-13 01:52:38 +09:00
taizan-hokuto
75a31bd245 Merge branch 'feature/emoji_embedding' into develop 2020-07-13 01:45:07 +09:00
taizan-hokuto
366d75c2bb Update README 2020-07-13 01:44:49 +09:00
taizan-hokuto
b7ff2b6537 Restore logging settings 2020-07-13 00:59:20 +09:00
taizan-hokuto
5dfd883fc9 Remove unnecessary line 2020-07-12 23:47:02 +09:00
taizan-hokuto
133a8afb27 Make it possible to embed custom emojis in HTML 2020-07-12 23:24:43 +09:00
taizan-hokuto
971e4bdf39 Add finalize function to processor 2020-07-12 23:23:05 +09:00
taizan-hokuto
f78bfde59e Merge branch 'hotfix/type_comment' 2020-06-18 02:07:46 +09:00
taizan-hokuto
a7379fd93f Merge tag 'type_comment' into develop 2020-06-18 02:07:46 +09:00
taizan-hokuto
1cc3661d35 Fix comment 2020-06-18 02:06:27 +09:00
taizan-hokuto
6c781483a9 Merge branch 'release/v0.0.8' 2020-06-18 00:17:54 +09:00
taizan-hokuto
5c3280f858 Merge tag 'v0.0.8' into develop
v0.0.8
2020-06-18 00:17:54 +09:00
taizan-hokuto
7500f79de0 Increment version 2020-06-18 00:16:57 +09:00
taizan-hokuto
94d4eebd0f Implement raise_for_status() 2020-06-17 23:56:07 +09:00
taizan-hokuto
2474207691 Format code 2020-06-04 23:10:26 +09:00
taizan-hokuto
e6dbc8772e Merge branch 'feature/use_protbuf' into develop 2020-05-31 22:58:20 +09:00
taizan-hokuto
8f91e031f3 Modify tests 2020-05-31 22:57:28 +09:00
taizan-hokuto
870d1f3fbe Modify parameters for archived chat 2020-05-31 22:57:12 +09:00
taizan-hokuto
141dbcd2da Lint 2020-05-31 19:45:01 +09:00
taizan-hokuto
6eb848f1c9 Modify checking timestamp 2020-05-31 19:43:09 +09:00
taizan-hokuto
8d7fc03fe0 Remove unecessary parser 2020-05-31 01:13:15 +09:00
taizan-hokuto
970e63cb38 Use protocol buffers 2020-05-31 00:33:46 +09:00
taizan-hokuto
1c817b6476 Merge branch 'release/v0.0.7.2' 2020-05-22 02:39:53 +09:00
taizan-hokuto
51eff10eeb Merge tag 'v0.0.7.2' into develop
v0.0.7.2
2020-05-22 02:39:53 +09:00
taizan-hokuto
18b88200a8 Increment version 2020-05-22 02:29:41 +09:00
taizan-hokuto
c95d70a232 Merge branch 'hotfix/#7_cli_index_outof_range' 2020-05-22 02:28:28 +09:00
taizan-hokuto
7640586591 Merge branch 'master' into develop 2020-05-22 02:28:28 +09:00
taizan-hokuto
f7ec14e166 Fix for #7 2020-05-22 02:27:52 +09:00
taizan-hokuto
a4dacdb7d7 Merge tag 'v0.0.7.1' into develop
v0.0.7.1
2020-05-06 01:24:55 +09:00
taizan-hokuto
785a82b618 Merge branch 'release/v0.0.7.1' 2020-05-06 01:24:54 +09:00
taizan-hokuto
faf886eebd Increment version 2020-05-06 01:24:30 +09:00
taizan-hokuto
8a627414cb Merge tag 'sponsor_text' into develop 2020-05-06 01:23:37 +09:00
taizan-hokuto
d14262cbcb Merge branch 'hotfix/sponsor_text' 2020-05-06 01:23:37 +09:00
taizan-hokuto
da7c694dfb Modify parsing membership 2020-05-06 01:23:19 +09:00
taizan-hokuto
9aa35b9756 Merge tag 'v0.0.7' into develop
v0.0.7
2020-05-05 22:59:16 +09:00
taizan-hokuto
f0a1a509a0 Merge branch 'release/v0.0.7' 2020-05-05 22:59:16 +09:00
taizan-hokuto
5ebca605ac Increment version 2020-05-05 22:58:29 +09:00
taizan-hokuto
3826b32ab9 Merge tag 'membership_renderer' into develop 2020-05-05 22:51:16 +09:00
taizan-hokuto
a46c82d3c0 Merge branch 'hotfix/membership_renderer' 2020-05-05 22:51:16 +09:00
taizan-hokuto
206d052907 Modify parsing membership 2020-05-05 22:47:12 +09:00
taizan-hokuto
141d7a9299 Merge tag 'termination' into develop 2020-05-05 21:18:46 +09:00
taizan-hokuto
04457eaa5c Merge branch 'hotfix/termination' 2020-05-05 21:18:46 +09:00
taizan-hokuto
bd32c75833 Modify termination 2020-05-05 21:16:06 +09:00
taizan-hokuto
84bae4ad2a Modify bytes combination 2020-04-18 00:55:56 +09:00
taizan-hokuto
d72608bf0a Merge tag 'json_decode_error' into develop
v0.0.6.6
2020-03-14 09:43:37 +09:00
taizan-hokuto
3243d69d7a Merge branch 'hotfix/json_decode_error' 2020-03-14 09:43:37 +09:00
taizan-hokuto
6e1b735ebc Increment version 2020-03-14 09:42:53 +09:00
taizan-hokuto
c54481dad5 Add header html and show progress 2020-03-14 09:26:28 +09:00
taizan-hokuto
78604c84d4 Fix testdata path separator 2020-03-14 08:16:19 +09:00
taizan-hokuto
21d93613a2 Handling JSONDecodeError 2020-03-14 08:00:31 +09:00
taizan-hokuto
56bf721330 Merge tag 'argparse' into develop
v0.0.6.5
2020-03-10 01:58:25 +09:00
taizan-hokuto
5f50598f79 Merge branch 'hotfix/argparse' 2020-03-10 01:58:24 +09:00
taizan-hokuto
5e8c438c6b Increment version 2020-03-10 01:57:55 +09:00
taizan-hokuto
23e47f6fb0 Fix parsing video_id which starts with '-' 2020-03-10 01:57:01 +09:00
taizan-hokuto
74dfe0a612 Modify requirements.txt 2020-03-10 01:06:36 +09:00
taizan-hokuto
725af25d81 Merge tag 'v0.0.6.4' into develop
v0.0.6.4
2020-03-08 23:43:01 +09:00
71 changed files with 4168 additions and 1447 deletions

View File

@@ -7,10 +7,10 @@ pytchat is a python library for fetching youtube live chat.
pytchat is a python library for fetching youtube live chat pytchat is a python library for fetching youtube live chat
without using youtube api, Selenium or BeautifulSoup. without using youtube api, Selenium or BeautifulSoup.
pytchatはAPIを使わずにYouTubeチャットを取得するための軽量pythonライブラリです。 pytchatはAPIを使わずにYouTubeチャットを取得するためのpythonライブラリです。
Other features: Other features:
+ Customizable chat data processors including youtube api compatible one. + Customizable [chat data processors](https://github.com/taizan-hokuto/pytchat/wiki/ChatProcessor) 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.
@@ -27,7 +27,7 @@ pip install pytchat
### CLI ### CLI
One-liner command. One-liner command.
Save chat data to html. Save chat data to html, with embedded custom emojis.
```bash ```bash
$ pytchat -v ZJ6Q4U_Vg6s -o "c:/temp/" $ pytchat -v ZJ6Q4U_Vg6s -o "c:/temp/"
@@ -148,6 +148,20 @@ def main():
if __name__ == '__main__': if __name__ == '__main__':
main() main()
``` ```
### Extract archived chat data as [HTML](https://github.com/taizan-hokuto/pytchat/wiki/HTMLArchiver) or [tab separated values](https://github.com/taizan-hokuto/pytchat/wiki/TSVArchiver).
```python
from pytchat import HTMLArchiver, Extractor
video_id = "*******"
ex = Extractor(
video_id,
div=10,
processor=HTMLArchiver("c:/test.html")
)
ex.extract()
print("finished.")
```
## Structure of Default Processor ## Structure of Default Processor
Each item can be got with `items` function. Each item can be got with `items` function.
@@ -175,7 +189,7 @@ Each item can be got with `items` function.
<tr> <tr>
<td>messageEx</td> <td>messageEx</td>
<td>str</td> <td>str</td>
<td>list of message texts and emoji URLs.</td> <td>list of message texts and emoji dicts(id, txt, url).</td>
</tr> </tr>
<tr> <tr>
<td>timestamp</td> <td>timestamp</td>

View File

@@ -1,8 +1,8 @@
""" """
pytchat is a python library for fetching youtube live chat without using yt api, Selenium, or BeautifulSoup. pytchat is a lightweight python library to browse youtube livechat without Selenium or BeautifulSoup.
""" """
__copyright__ = 'Copyright (C) 2019 taizan-hokuto' __copyright__ = 'Copyright (C) 2019 taizan-hokuto'
__version__ = '0.0.6.4' __version__ = '0.0.9'
__license__ = 'MIT' __license__ = 'MIT'
__author__ = 'taizan-hokuto' __author__ = 'taizan-hokuto'
__author_email__ = '55448286+taizan-hokuto@users.noreply.github.com' __author_email__ = '55448286+taizan-hokuto@users.noreply.github.com'
@@ -28,3 +28,5 @@ from .api import (
SuperchatCalculator, SuperchatCalculator,
VideoInfo VideoInfo
) )
# flake8: noqa

View File

@@ -14,3 +14,5 @@ from .processors.speed.calculator import SpeedCalculator
from .processors.superchat.calculator import SuperchatCalculator from .processors.superchat.calculator import SuperchatCalculator
from .tool.extract.extractor import Extractor from .tool.extract.extractor import Extractor
from .tool.videoinfo import VideoInfo from .tool.videoinfo import VideoInfo
# flake8: noqa

View File

@@ -1,11 +1,7 @@
import argparse import argparse
import os
from pathlib import Path from pathlib import Path
from typing import List, Callable
from .arguments import Arguments from .arguments import Arguments
from .. exceptions import InvalidVideoIdException, NoContents
from .. exceptions import InvalidVideoIdException, NoContentsException
from .. processors.tsv_archiver import TSVArchiver
from .. processors.html_archiver import HTMLArchiver from .. processors.html_archiver import HTMLArchiver
from .. tool.extract.extractor import Extractor from .. tool.extract.extractor import Extractor
from .. tool.videoinfo import VideoInfo from .. tool.videoinfo import VideoInfo
@@ -18,13 +14,16 @@ https://github.com/PetterKraabol/Twitch-Chat-Downloader
(MIT License) (MIT License)
''' '''
def main(): def main():
# Arguments # Arguments
parser = argparse.ArgumentParser(description=f'pytchat v{__version__}') parser = argparse.ArgumentParser(description=f'pytchat v{__version__}')
parser.add_argument('-v', f'--{Arguments.Name.VIDEO}', type=str, parser.add_argument('-v', f'--{Arguments.Name.VIDEO}', type=str,
help='Video IDs separated by commas without space') help='Video IDs separated by commas without space.\n'
'If ID starts with a hyphen (-), enclose the ID in square brackets.')
parser.add_argument('-o', f'--{Arguments.Name.OUTPUT}', type=str, parser.add_argument('-o', f'--{Arguments.Name.OUTPUT}', type=str,
help='Output directory (end with "/")', default='./') help='Output directory (end with "/"). default="./"', default='./')
parser.add_argument(f'--{Arguments.Name.VERSION}', action='store_true', parser.add_argument(f'--{Arguments.Name.VERSION}', action='store_true',
help='Settings version') help='Settings version')
Arguments(parser.parse_args().__dict__) Arguments(parser.parse_args().__dict__)
@@ -35,17 +34,27 @@ def main():
# Extractor # Extractor
if Arguments().video_ids: if Arguments().video_ids:
for video_id in Arguments().video_ids: for video_id in Arguments().video_ids:
if '[' in video_id:
video_id = video_id.replace('[', '').replace(']', '')
try: try:
info = VideoInfo(video_id) info = VideoInfo(video_id)
print(f"Extracting...\n" print(f"Extracting...\n"
f" video_id: {video_id}\n" f" video_id: {video_id}\n"
f" channel: {info.get_channel_name()}\n" f" channel: {info.get_channel_name()}\n"
f" title: {info.get_title()}") f" title: {info.get_title()}")
path = Path(Arguments().output + video_id + '.html')
print(f"output path: {path.resolve()}")
Extractor(video_id, Extractor(video_id,
processor = HTMLArchiver(Arguments().output+video_id+'.html') processor=HTMLArchiver(
Arguments().output + video_id + '.html'),
callback=_disp_progress
).extract() ).extract()
print("Extraction end.\n") print("\nExtraction end.\n")
except (InvalidVideoIdException, NoContentsException) as e: except (InvalidVideoIdException, NoContents) as e:
print(e) print(e)
return return
parser.print_help() parser.print_help()
def _disp_progress(a, b):
print('.', end="", flush=True)

View File

@@ -8,6 +8,7 @@ https://github.com/PetterKraabol/Twitch-Chat-Downloader
(MIT License) (MIT License)
''' '''
class Arguments(metaclass=Singleton): class Arguments(metaclass=Singleton):
""" """
Arguments singleton Arguments singleton

View File

@@ -4,6 +4,8 @@ Petter Kraabøl's Twitch-Chat-Downloader
https://github.com/PetterKraabol/Twitch-Chat-Downloader https://github.com/PetterKraabol/Twitch-Chat-Downloader
(MIT License) (MIT License)
''' '''
class Singleton(type): class Singleton(type):
""" """
Abstract class for singletons Abstract class for singletons

View File

@@ -1,11 +1,9 @@
import logging import logging
from . import mylogger from . import mylogger
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/83.0.4103.106 Safari/537.36'}
def logger(module_name: str, loglevel=None): def logger(module_name: str, loglevel=None):
module_logger = mylogger.get_logger(module_name, loglevel=loglevel) module_logger = mylogger.get_logger(module_name, loglevel=loglevel)
return module_logger return module_logger

View File

@@ -1,11 +1,11 @@
from logging import NullHandler, getLogger, StreamHandler, FileHandler, Formatter from logging import NullHandler, getLogger, StreamHandler, FileHandler
import logging import logging
from datetime import datetime from datetime import datetime
def get_logger(modname, loglevel=logging.DEBUG): def get_logger(modname, loglevel=logging.DEBUG):
logger = getLogger(modname) logger = getLogger(modname)
if loglevel == None: if loglevel is None:
logger.addHandler(NullHandler()) logger.addHandler(NullHandler())
return logger return logger
logger.setLevel(loglevel) logger.setLevel(loglevel)
@@ -22,10 +22,10 @@ def get_logger(modname,loglevel=logging.DEBUG):
handler2.setLevel(logging.ERROR) handler2.setLevel(logging.ERROR)
handler2.setFormatter(my_formatter) handler2.setFormatter(my_formatter)
logger.addHandler(handler2) logger.addHandler(handler2)
return logger return logger
class MyFormatter(logging.Formatter): class MyFormatter(logging.Formatter):
def format(self, record): def format(self, record):
timestamp = ( timestamp = (

View File

@@ -1,5 +1,7 @@
import asyncio import asyncio
class Buffer(asyncio.Queue): class Buffer(asyncio.Queue):
''' '''
チャットデータを格納するバッファの役割を持つFIFOキュー チャットデータを格納するバッファの役割を持つFIFOキュー
@@ -10,6 +12,7 @@ class Buffer(asyncio.Queue):
格納するチャットブロックの最大個数。0の場合は無限。 格納するチャットブロックの最大個数。0の場合は無限。
最大値を超える場合は古いチャットブロックから破棄される。 最大値を超える場合は古いチャットブロックから破棄される。
''' '''
def __init__(self, maxsize=0): def __init__(self, maxsize=0):
super().__init__(maxsize) super().__init__(maxsize)

View File

@@ -1,7 +1,6 @@
import aiohttp, asyncio import aiohttp
import datetime import asyncio
import json import json
import random
import signal import signal
import time import time
import traceback import traceback
@@ -12,7 +11,7 @@ from asyncio import Queue
from .buffer import Buffer from .buffer import Buffer
from ..parser.live import Parser from ..parser.live import Parser
from .. import config from .. import config
from ..exceptions import ChatParseException,IllegalFunctionCall from .. import exceptions
from ..paramgen import liveparam, arcparam from ..paramgen import liveparam, arcparam
from ..processors.default.processor import DefaultProcessor from ..processors.default.processor import DefaultProcessor
from ..processors.combinator import Combinator from ..processors.combinator import Combinator
@@ -75,7 +74,7 @@ class LiveChatAsync:
_setup_finished = False _setup_finished = False
def __init__(self, video_id, def __init__(self, video_id,
seektime = 0, seektime=-1,
processor=DefaultProcessor(), processor=DefaultProcessor(),
buffer=None, buffer=None,
interruptable=True, interruptable=True,
@@ -87,7 +86,7 @@ class LiveChatAsync:
topchat_only=False, topchat_only=False,
logger=config.logger(__name__), logger=config.logger(__name__),
): ):
self.video_id = video_id self._video_id = video_id
self.seektime = seektime self.seektime = seektime
if isinstance(processor, tuple): if isinstance(processor, tuple):
self.processor = Combinator(processor) self.processor = Combinator(processor)
@@ -103,28 +102,26 @@ class LiveChatAsync:
self._parser = Parser(is_replay=self._is_replay) self._parser = Parser(is_replay=self._is_replay)
self._pauser = Queue() self._pauser = Queue()
self._pauser.put_nowait(None) self._pauser.put_nowait(None)
self._setup()
self._first_fetch = True self._first_fetch = True
self._fetch_url = "live_chat/get_live_chat?continuation=" self._fetch_url = "live_chat/get_live_chat?continuation="
self._topchat_only = topchat_only self._topchat_only = topchat_only
self._logger = logger self._logger = logger
self.exception = None
LiveChatAsync._logger = logger LiveChatAsync._logger = logger
if not LiveChatAsync._setup_finished:
LiveChatAsync._setup_finished = True
if exception_handler: if exception_handler:
self._set_exception_handler(exception_handler) self._set_exception_handler(exception_handler)
if interruptable: if interruptable:
signal.signal(signal.SIGINT, signal.signal(signal.SIGINT,
(lambda a, b: asyncio.create_task( (lambda a, b: asyncio.create_task(
LiveChatAsync.shutdown(None,signal.SIGINT,b)) LiveChatAsync.shutdown(None, signal.SIGINT, b))))
)) self._setup()
def _setup(self): def _setup(self):
# direct modeがTrueでcallback未設定の場合例外発生。 # direct modeがTrueでcallback未設定の場合例外発生。
if self._direct_mode: if self._direct_mode:
if self._callback is None: if self._callback is None:
raise IllegalFunctionCall( raise exceptions.IllegalFunctionCall(
"When direct_mode=True, callback parameter is required.") "When direct_mode=True, callback parameter is required.")
else: else:
# direct modeがFalseでbufferが未設定ならばデフォルトのbufferを作成 # direct modeがFalseでbufferが未設定ならばデフォルトのbufferを作成
@@ -139,18 +136,18 @@ class LiveChatAsync:
loop.create_task(self._callback_loop(self._callback)) loop.create_task(self._callback_loop(self._callback))
# _listenループタスクの開始 # _listenループタスクの開始
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
listen_task = loop.create_task(self._startlisten()) self.listen_task = loop.create_task(self._startlisten())
# add_done_callbackの登録 # add_done_callbackの登録
if self._done_callback is None: if self._done_callback is None:
listen_task.add_done_callback(self.finish) self.listen_task.add_done_callback(self._finish)
else: else:
listen_task.add_done_callback(self._done_callback) self.listen_task.add_done_callback(self._done_callback)
async def _startlisten(self): async def _startlisten(self):
"""Fetch first continuation parameter, """Fetch first continuation parameter,
create and start _listen loop. create and start _listen loop.
""" """
initial_continuation = liveparam.getparam(self.video_id,3) initial_continuation = liveparam.getparam(self._video_id, 3)
await self._listen(initial_continuation) await self._listen(initial_continuation)
async def _listen(self, continuation): async def _listen(self, continuation):
@@ -172,13 +169,14 @@ class LiveChatAsync:
timeout = metadata['timeoutMs'] / 1000 timeout = metadata['timeoutMs'] / 1000
chat_component = { chat_component = {
"video_id" : self.video_id, "video_id": self._video_id,
"timeout": timeout, "timeout": timeout,
"chatdata": chatdata "chatdata": chatdata
} }
time_mark = time.time() time_mark = time.time()
if self._direct_mode: if self._direct_mode:
processed_chat = self.processor.process([chat_component]) processed_chat = self.processor.process(
[chat_component])
if isinstance(processed_chat, tuple): if isinstance(processed_chat, tuple):
await self._callback(*processed_chat) await self._callback(*processed_chat)
else: else:
@@ -188,14 +186,15 @@ class LiveChatAsync:
diff_time = timeout - (time.time() - time_mark) diff_time = timeout - (time.time() - time_mark)
await asyncio.sleep(diff_time) await asyncio.sleep(diff_time)
continuation = metadata.get('continuation') continuation = metadata.get('continuation')
except ChatParseException as e: except exceptions.ChatParseException as e:
self._logger.debug(f"[{self.video_id}]{str(e)}") self._logger.debug(f"[{self._video_id}]{str(e)}")
return raise
except (TypeError, json.JSONDecodeError): except (TypeError, json.JSONDecodeError):
self._logger.error(f"{traceback.format_exc(limit = -1)}") self._logger.error(f"{traceback.format_exc(limit = -1)}")
return raise
self._logger.debug(f"[{self.video_id}]finished fetching chat.") self._logger.debug(f"[{self._video_id}]finished fetching chat.")
raise exceptions.ChatDataFinished
async def _check_pause(self, continuation): async def _check_pause(self, continuation):
if self._pauser.empty(): if self._pauser.empty():
@@ -207,7 +206,7 @@ class LiveChatAsync:
self._pauser.put_nowait(None) self._pauser.put_nowait(None)
if not self._is_replay: if not self._is_replay:
continuation = liveparam.getparam( continuation = liveparam.getparam(
self.video_id, 3, self._topchat_only) self._video_id, 3, self._topchat_only)
return continuation return continuation
async def _get_contents(self, continuation, session, headers): async def _get_contents(self, continuation, session, headers):
@@ -219,9 +218,7 @@ class LiveChatAsync:
------- -------
'continuationContents' which includes metadata & chatdata. 'continuationContents' which includes metadata & chatdata.
''' '''
livechat_json = (await livechat_json = await self._get_livechat_json(continuation, session, headers)
self._get_livechat_json(continuation, session, headers)
)
contents = self._parser.get_contents(livechat_json) contents = self._parser.get_contents(livechat_json)
if self._first_fetch: if self._first_fetch:
if contents is None or self._is_replay: if contents is None or self._is_replay:
@@ -229,7 +226,7 @@ class LiveChatAsync:
self._parser.is_replay = True self._parser.is_replay = True
self._fetch_url = "live_chat_replay/get_live_chat_replay?continuation=" self._fetch_url = "live_chat_replay/get_live_chat_replay?continuation="
continuation = arcparam.getparam( continuation = arcparam.getparam(
self.video_id, self.seektime, self._topchat_only) self._video_id, self.seektime, self._topchat_only)
livechat_json = (await self._get_livechat_json( livechat_json = (await self._get_livechat_json(
continuation, session, headers)) continuation, session, headers))
reload_continuation = self._parser.reload_continuation( reload_continuation = self._parser.reload_continuation(
@@ -260,7 +257,7 @@ class LiveChatAsync:
await asyncio.sleep(1) await asyncio.sleep(1)
continue continue
else: else:
self._logger.error(f"[{self.video_id}]" self._logger.error(f"[{self._video_id}]"
f"Exceeded retry count. status_code={status_code}") f"Exceeded retry count. status_code={status_code}")
return None return None
return livechat_json return livechat_json
@@ -290,9 +287,12 @@ class LiveChatAsync:
: Processorによって加工されたチャットデータ : Processorによって加工されたチャットデータ
""" """
if self._callback is None: if self._callback is None:
if self.is_alive():
items = await self._buffer.get() items = await self._buffer.get()
return self.processor.process(items) return self.processor.process(items)
raise IllegalFunctionCall( else:
return []
raise exceptions.IllegalFunctionCall(
"既にcallbackを登録済みのため、get()は実行できません。") "既にcallbackを登録済みのため、get()は実行できません。")
def is_replay(self): def is_replay(self):
@@ -313,22 +313,37 @@ class LiveChatAsync:
def is_alive(self): def is_alive(self):
return self._is_alive return self._is_alive
def finish(self,sender): def _finish(self, sender):
'''Listener終了時のコールバック''' '''Listener終了時のコールバック'''
try: try:
self.terminate() self._task_finished()
except CancelledError: except CancelledError:
self._logger.debug(f'[{self.video_id}]cancelled:{sender}') self._logger.debug(f'[{self._video_id}]cancelled:{sender}')
def terminate(self): def terminate(self):
if self._pauser.empty():
self._pauser.put_nowait(None)
self._is_alive = False
self._buffer.put_nowait({})
self.processor.finalize()
def _task_finished(self):
''' '''
Listenerを終了する。 Listenerを終了する。
''' '''
self._is_alive = False if self.is_alive():
if self._direct_mode == False: self.terminate()
#bufferにダミーオブジェクトを入れてis_alive()を判定させる try:
self._buffer.put_nowait({'chatdata':'','timeout':0}) self.listen_task.result()
self._logger.info(f'[{self.video_id}]finished.') except Exception as e:
self.exception = e
if not isinstance(e, exceptions.ChatParseException):
self._logger.error(f'Internal exception - {type(e)}{str(e)}')
self._logger.info(f'[{self._video_id}]終了しました')
def raise_for_status(self):
if self.exception is not None:
raise self.exception
@classmethod @classmethod
def _set_exception_handler(cls, handler): def _set_exception_handler(cls, handler):
@@ -342,7 +357,7 @@ class LiveChatAsync:
asyncio.current_task()] asyncio.current_task()]
[task.cancel() for task in tasks] [task.cancel() for task in tasks]
cls._logger.debug(f"complete remaining tasks...") cls._logger.debug("complete remaining tasks...")
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()

View File

@@ -1,6 +1,7 @@
import queue import queue
class Buffer(queue.Queue): class Buffer(queue.Queue):
''' '''
チャットデータを格納するバッファの役割を持つFIFOキュー チャットデータを格納するバッファの役割を持つFIFOキュー
@@ -11,6 +12,7 @@ class Buffer(queue.Queue):
格納するチャットブロックの最大個数。0の場合は無限。 格納するチャットブロックの最大個数。0の場合は無限。
最大値を超える場合は古いチャットブロックから破棄される。 最大値を超える場合は古いチャットブロックから破棄される。
''' '''
def __init__(self, maxsize=0): def __init__(self, maxsize=0):
super().__init__(maxsize=maxsize) super().__init__(maxsize=maxsize)

View File

@@ -1,17 +1,16 @@
import requests import requests
import datetime
import json import json
import random
import signal import signal
import time import time
import traceback import traceback
import urllib.parse import urllib.parse
from concurrent.futures import CancelledError, ThreadPoolExecutor from concurrent.futures import CancelledError, ThreadPoolExecutor
from queue import Queue from queue import Queue
from threading import Event
from .buffer import Buffer from .buffer import Buffer
from ..parser.live import Parser from ..parser.live import Parser
from .. import config from .. import config
from ..exceptions import ChatParseException,IllegalFunctionCall from .. import exceptions
from ..paramgen import liveparam, arcparam from ..paramgen import liveparam, arcparam
from ..processors.default.processor import DefaultProcessor from ..processors.default.processor import DefaultProcessor
from ..processors.combinator import Combinator from ..processors.combinator import Combinator
@@ -72,11 +71,9 @@ class LiveChat:
''' '''
_setup_finished = False _setup_finished = False
#チャット監視中のListenerのリスト
_listeners = []
def __init__(self, video_id, def __init__(self, video_id,
seektime = 0, seektime=-1,
processor=DefaultProcessor(), processor=DefaultProcessor(),
buffer=None, buffer=None,
interruptable=True, interruptable=True,
@@ -87,7 +84,7 @@ class LiveChat:
topchat_only=False, topchat_only=False,
logger=config.logger(__name__) logger=config.logger(__name__)
): ):
self.video_id = video_id self._video_id = video_id
self.seektime = seektime self.seektime = seektime
if isinstance(processor, tuple): if isinstance(processor, tuple):
self.processor = Combinator(processor) self.processor = Combinator(processor)
@@ -103,25 +100,21 @@ class LiveChat:
self._parser = Parser(is_replay=self._is_replay) self._parser = Parser(is_replay=self._is_replay)
self._pauser = Queue() self._pauser = Queue()
self._pauser.put_nowait(None) self._pauser.put_nowait(None)
self._setup()
self._first_fetch = True self._first_fetch = True
self._fetch_url = "live_chat/get_live_chat?continuation=" self._fetch_url = "live_chat/get_live_chat?continuation="
self._topchat_only = topchat_only self._topchat_only = topchat_only
self._event = Event()
self._logger = logger self._logger = logger
LiveChat._logger = logger self.exception = None
if not LiveChat._setup_finished:
LiveChat._setup_finished = True
if interruptable: if interruptable:
signal.signal(signal.SIGINT, (lambda a, b: signal.signal(signal.SIGINT, lambda a, b: self.terminate())
(LiveChat.shutdown(None,signal.SIGINT,b)) self._setup()
))
LiveChat._listeners.append(self)
def _setup(self): def _setup(self):
# direct modeがTrueでcallback未設定の場合例外発生。 # direct modeがTrueでcallback未設定の場合例外発生。
if self._direct_mode: if self._direct_mode:
if self._callback is None: if self._callback is None:
raise IllegalFunctionCall( raise exceptions.IllegalFunctionCall(
"When direct_mode=True, callback parameter is required.") "When direct_mode=True, callback parameter is required.")
else: else:
# direct modeがFalseでbufferが未設定ならばデフォルトのbufferを作成 # direct modeがFalseでbufferが未設定ならばデフォルトのbufferを作成
@@ -134,19 +127,19 @@ class LiveChat:
# callbackを呼ぶループタスクの開始 # callbackを呼ぶループタスクの開始
self._executor.submit(self._callback_loop, self._callback) self._executor.submit(self._callback_loop, self._callback)
# _listenループタスクの開始 # _listenループタスクの開始
listen_task = self._executor.submit(self._startlisten) self.listen_task = self._executor.submit(self._startlisten)
# add_done_callbackの登録 # add_done_callbackの登録
if self._done_callback is None: if self._done_callback is None:
listen_task.add_done_callback(self.finish) self.listen_task.add_done_callback(self._finish)
else: else:
listen_task.add_done_callback(self._done_callback) self.listen_task.add_done_callback(self._done_callback)
def _startlisten(self): def _startlisten(self):
time.sleep(0.1) # sleep shortly to prohibit skipping fetching data time.sleep(0.1) # sleep shortly to prohibit skipping fetching data
"""Fetch first continuation parameter, """Fetch first continuation parameter,
create and start _listen loop. create and start _listen loop.
""" """
initial_continuation = liveparam.getparam(self.video_id,3) initial_continuation = liveparam.getparam(self._video_id, 3)
self._listen(initial_continuation) self._listen(initial_continuation)
def _listen(self, continuation): def _listen(self, continuation):
@@ -162,19 +155,18 @@ class LiveChat:
with requests.Session() as session: with requests.Session() as session:
while(continuation and self._is_alive): while(continuation and self._is_alive):
continuation = self._check_pause(continuation) continuation = self._check_pause(continuation)
contents = self._get_contents( contents = self._get_contents(continuation, session, headers)
continuation, session, headers)
metadata, chatdata = self._parser.parse(contents) metadata, chatdata = self._parser.parse(contents)
timeout = metadata['timeoutMs'] / 1000 timeout = metadata['timeoutMs'] / 1000
chat_component = { chat_component = {
"video_id" : self.video_id, "video_id": self._video_id,
"timeout": timeout, "timeout": timeout,
"chatdata": chatdata "chatdata": chatdata
} }
time_mark = time.time() time_mark = time.time()
if self._direct_mode: if self._direct_mode:
processed_chat = self.processor.process([chat_component]) processed_chat = self.processor.process(
[chat_component])
if isinstance(processed_chat, tuple): if isinstance(processed_chat, tuple):
self._callback(*processed_chat) self._callback(*processed_chat)
else: else:
@@ -182,16 +174,17 @@ class LiveChat:
else: else:
self._buffer.put(chat_component) self._buffer.put(chat_component)
diff_time = timeout - (time.time() - time_mark) diff_time = timeout - (time.time() - time_mark)
time.sleep(diff_time if diff_time > 0 else 0) self._event.wait(diff_time if diff_time > 0 else 0)
continuation = metadata.get('continuation') continuation = metadata.get('continuation')
except ChatParseException as e: except exceptions.ChatParseException as e:
self._logger.debug(f"[{self.video_id}]{str(e)}") self._logger.debug(f"[{self._video_id}]{str(e)}")
return raise
except (TypeError, json.JSONDecodeError): except (TypeError, json.JSONDecodeError):
self._logger.error(f"{traceback.format_exc(limit=-1)}") self._logger.error(f"{traceback.format_exc(limit=-1)}")
return raise
self._logger.debug(f"[{self.video_id}]finished fetching chat.") self._logger.debug(f"[{self._video_id}]finished fetching chat.")
raise exceptions.ChatDataFinished
def _check_pause(self, continuation): def _check_pause(self, continuation):
if self._pauser.empty(): if self._pauser.empty():
@@ -202,7 +195,7 @@ class LiveChat:
''' '''
self._pauser.put_nowait(None) self._pauser.put_nowait(None)
if not self._is_replay: if not self._is_replay:
continuation = liveparam.getparam(self.video_id,3) continuation = liveparam.getparam(self._video_id, 3)
return continuation return continuation
def _get_contents(self, continuation, session, headers): def _get_contents(self, continuation, session, headers):
@@ -224,9 +217,8 @@ class LiveChat:
self._parser.is_replay = True self._parser.is_replay = True
self._fetch_url = "live_chat_replay/get_live_chat_replay?continuation=" self._fetch_url = "live_chat_replay/get_live_chat_replay?continuation="
continuation = arcparam.getparam( continuation = arcparam.getparam(
self.video_id, self.seektime, self._topchat_only) self._video_id, self.seektime, self._topchat_only)
livechat_json = ( self._get_livechat_json( livechat_json = (self._get_livechat_json(continuation, session, headers))
continuation, session, headers))
reload_continuation = self._parser.reload_continuation( reload_continuation = self._parser.reload_continuation(
self._parser.get_contents(livechat_json)) self._parser.get_contents(livechat_json))
if reload_continuation: if reload_continuation:
@@ -255,9 +247,9 @@ class LiveChat:
time.sleep(1) time.sleep(1)
continue continue
else: else:
self._logger.error(f"[{self.video_id}]" self._logger.error(f"[{self._video_id}]"
f"Exceeded retry count. status_code={status_code}") f"Exceeded retry count. status_code={status_code}")
return None raise exceptions.RetryExceedMaxCount()
return livechat_json return livechat_json
def _callback_loop(self, callback): def _callback_loop(self, callback):
@@ -285,9 +277,12 @@ class LiveChat:
: Processorによって加工されたチャットデータ : Processorによって加工されたチャットデータ
""" """
if self._callback is None: if self._callback is None:
if self.is_alive():
items = self._buffer.get() items = self._buffer.get()
return self.processor.process(items) return self.processor.process(items)
raise IllegalFunctionCall( else:
return []
raise exceptions.IllegalFunctionCall(
"既にcallbackを登録済みのため、get()は実行できません。") "既にcallbackを登録済みのため、get()は実行できません。")
def is_replay(self): def is_replay(self):
@@ -308,25 +303,35 @@ class LiveChat:
def is_alive(self): def is_alive(self):
return self._is_alive return self._is_alive
def finish(self,sender): def _finish(self, sender):
'''Listener終了時のコールバック''' '''Listener終了時のコールバック'''
try: try:
self.terminate() self._task_finished()
except CancelledError: except CancelledError:
self._logger.debug(f'[{self.video_id}]cancelled:{sender}') self._logger.debug(f'[{self._video_id}]cancelled:{sender}')
def terminate(self): def terminate(self):
if self._pauser.empty():
self._pauser.put_nowait(None)
self._is_alive = False
self._buffer.put({})
self._event.set()
self.processor.finalize()
def _task_finished(self):
''' '''
Listenerを終了する。 Listenerを終了する。
''' '''
self._is_alive = False if self.is_alive():
if self._direct_mode == False: self.terminate()
#bufferにダミーオブジェクトを入れてis_alive()を判定させる try:
self._buffer.put({'chatdata':'','timeout':0}) self.listen_task.result()
self._logger.info(f'[{self.video_id}]finished.') except Exception as e:
self.exception = e
if not isinstance(e, exceptions.ChatParseException):
self._logger.error(f'Internal exception - {type(e)}{str(e)}')
self._logger.info(f'[{self._video_id}]終了しました')
@classmethod def raise_for_status(self):
def shutdown(cls, event, sig = None, handler=None): if self.exception is not None:
cls._logger.debug("shutdown...") raise self.exception
for t in LiveChat._listeners:
t._is_alive = False

View File

@@ -1,46 +1,64 @@
class ChatParseException(Exception): class ChatParseException(Exception):
''' '''
チャットデータをパースするライブラリが投げる例外の基底クラス Base exception thrown by the parser
''' '''
pass pass
class NoYtinitialdataException(ChatParseException):
'''
配信ページ内にチャットデータurlが見つからないときに投げる例外
'''
pass
class ResponseContextError(ChatParseException): class ResponseContextError(ChatParseException):
''' '''
配信ページでチャットデータ無効の時に投げる例外 Thrown when chat data is invalid.
'''
pass
class NoLivechatRendererException(ChatParseException):
'''
チャットデータのJSON中にlivechatRendererがない時に投げる例外
''' '''
pass pass
class NoContentsException(ChatParseException): class NoContents(ChatParseException):
''' '''
チャットデータのJSON中にContinuationContentsがない時に投げる例外 Thrown when ContinuationContents is missing in JSON.
''' '''
pass pass
class NoContinuationsException(ChatParseException):
class NoContinuation(ChatParseException):
''' '''
チャットデータのContinuationContents中にcontinuationがない時に投げる例外 Thrown when continuation is missing in ContinuationContents.
''' '''
pass pass
class IllegalFunctionCall(Exception): class IllegalFunctionCall(Exception):
''' '''
set_callback()を実行済みにもかかわらず Thrown when get() is called even though
get()を呼び出した場合の例外 set_callback() has been executed.
''' '''
pass pass
class InvalidVideoIdException(Exception): class InvalidVideoIdException(Exception):
'''
Thrown when the video_id is not exist (VideoInfo).
'''
pass
class UnknownConnectionError(Exception):
pass
class RetryExceedMaxCount(Exception):
'''
thrown when the number of retries exceeds the maximum value.
'''
pass
class ChatDataFinished(ChatParseException):
pass
class ReceivedUnknownContinuation(ChatParseException):
pass
class FailedExtractContinuation(ChatDataFinished):
pass pass

View File

@@ -1,107 +1,55 @@
from base64 import urlsafe_b64encode as b64enc from .pb.header_pb2 import Header
from functools import reduce from .pb.replay_pb2 import Continuation
import math from urllib.parse import quote
import random import base64
import urllib.parse
''' '''
Generate continuation parameter of youtube replay chat. Generate continuation parameter of youtube replay chat.
Author: taizan-hokuto (2019) @taizan205 Author: taizan-hokuto
ver 0.0.1 2019.10.05 ver 0.0.1 2019.10.05 : Initial release.
ver 0.0.2 2020.05.30 : Use Protocol Buffers.
''' '''
def _gen_vid(video_id):
"""generate video_id parameter.
Parameter
---------
video_id : str
Return def _gen_vid(video_id) -> str:
--------- header = Header()
bytes : base64 encoded video_id parameter. header.info.video.id = video_id
""" header.terminator = 1
header_magic = b'\x0A\x0F\x1A\x0D\x0A' return base64.urlsafe_b64encode(header.SerializeToString()).decode()
header_id = video_id.encode()
header_sep_1 = b'\x1A\x13\xEA\xA8\xDD\xB9\x01\x0D\x0A\x0B'
header_terminator = b'\x20\x01'
item = [
header_magic,
_nval(len(header_id)),
header_id,
header_sep_1,
header_id,
header_terminator
]
return urllib.parse.quote( def _build(video_id, seektime, topchat_only) -> str:
b64enc(reduce(lambda x, y: x+y, item)).decode() chattype = 1
).encode() timestamp = 0
if topchat_only:
chattype = 4
def _nval(val): fetch_before_start = 3
"""convert value to byte array"""
if val<0: raise ValueError
buf = b''
while val >> 7:
m = val & 0xFF | 0x80
buf += m.to_bytes(1,'big')
val >>= 7
buf += val.to_bytes(1,'big')
return buf
def _build(video_id, seektime, topchat_only):
switch_01 = b'\x04' if topchat_only else b'\x01'
if seektime < 0: if seektime < 0:
times =_nval(0) fetch_before_start = 4
switch = b'\x04'
elif seektime == 0: elif seektime == 0:
times =_nval(1) timestamp = 1
switch = b'\x03'
else: else:
times =_nval(int(seektime*1000000)) timestamp = int(seektime * 1000000)
switch = b'\x03' continuation = Continuation()
parity = b'\x00' entity = continuation.entity
entity.header = _gen_vid(video_id)
entity.timestamp = timestamp
entity.s6 = 0
entity.s7 = 0
entity.s8 = 0
entity.s9 = fetch_before_start
entity.s10 = ''
entity.s12 = chattype
entity.chattype.value = chattype
entity.s15 = 0
return quote(
base64.urlsafe_b64encode(continuation.SerializeToString()).decode())
header_magic= b'\xA2\x9D\xB0\xD3\x04'
sep_0 = b'\x1A'
vid = _gen_vid(video_id)
time_tag = b'\x28'
timestamp1 = times
sep_1 = b'\x30\x00\x38\x00\x40\x00\x48'
sep_2 = b'\x52\x1C\x08\x00\x10\x00\x18\x00\x20\x00'
chkstr = b'\x2A\x0E\x73\x74\x61\x74\x69\x63\x63\x68\x65\x63\x6B\x73\x75\x6D\x40'
sep_3 = b'\x00\x58\x03\x60'
sep_4 = b'\x68' + parity + b'\x72\x04\x08'
sep_5 = b'\x10' + parity + b'\x78\x00'
body = [
sep_0,
_nval(len(vid)),
vid,
time_tag,
timestamp1,
sep_1,
switch,
sep_2,
chkstr,
sep_3,
switch_01,
sep_4,
switch_01,
sep_5
]
body = reduce(lambda x, y: x+y, body) def getparam(video_id, seektime=-1, topchat_only=False) -> str:
return urllib.parse.quote(
b64enc( header_magic +
_nval(len(body)) +
body
).decode()
)
def getparam(video_id, seektime = 0, topchat_only = False):
''' '''
Parameter Parameter
--------- ---------

View File

@@ -1,7 +1,5 @@
from base64 import urlsafe_b64encode as b64enc from base64 import urlsafe_b64encode as b64enc
from functools import reduce from functools import reduce
import math
import random
import urllib.parse import urllib.parse
''' '''
@@ -12,6 +10,7 @@ Author: taizan-hokuto (2019) @taizan205
ver 0.0.1 2019.10.05 ver 0.0.1 2019.10.05
''' '''
def _gen_vid_long(video_id): def _gen_vid_long(video_id):
"""generate video_id parameter. """generate video_id parameter.
Parameter Parameter
@@ -40,6 +39,7 @@ def _gen_vid_long(video_id):
b64enc(reduce(lambda x, y: x + y, item)).decode() b64enc(reduce(lambda x, y: x + y, item)).decode()
).encode() ).encode()
def _gen_vid(video_id): def _gen_vid(video_id):
"""generate video_id parameter. """generate video_id parameter.
Parameter Parameter
@@ -65,9 +65,11 @@ def _gen_vid(video_id):
b64enc(reduce(lambda x, y: x + y, item)).decode() b64enc(reduce(lambda x, y: x + y, item)).decode()
).encode() ).encode()
def _nval(val): def _nval(val):
"""convert value to byte array""" """convert value to byte array"""
if val<0: raise ValueError if val < 0:
raise ValueError
buf = b'' buf = b''
while val >> 7: while val >> 7:
m = val & 0xFF | 0x80 m = val & 0xFF | 0x80
@@ -76,6 +78,7 @@ def _nval(val):
buf += val.to_bytes(1, 'big') buf += val.to_bytes(1, 'big')
return buf return buf
def _build(video_id, seektime, topchat_only): def _build(video_id, seektime, topchat_only):
switch_01 = b'\x04' if topchat_only else b'\x01' switch_01 = b'\x04' if topchat_only else b'\x01'
if seektime < 0: if seektime < 0:
@@ -85,9 +88,7 @@ def _build(video_id, seektime, topchat_only):
else: else:
times = _nval(int(seektime * 1000)) times = _nval(int(seektime * 1000))
if seektime > 0: if seektime > 0:
_len_time = ( b'\x5A' _len_time = b'\x5A' + (len(times) + 1).to_bytes(1, 'big') + b'\x10'
+ (len(times)+1).to_bytes(1,'big')
+ b'\x10')
else: else:
_len_time = b'' _len_time = b''
@@ -114,12 +115,11 @@ def _build(video_id, seektime, topchat_only):
body = reduce(lambda x, y: x + y, body) body = reduce(lambda x, y: x + y, body)
return urllib.parse.quote( return urllib.parse.quote(
b64enc( header_magic + b64enc(header_magic + _nval(len(body)) + body
_nval(len(body)) +
body
).decode() ).decode()
) )
def getparam(video_id, seektime=0.0, topchat_only=False): def getparam(video_id, seektime=0.0, topchat_only=False):
''' '''
Parameter Parameter

View File

@@ -1,17 +1,21 @@
from base64 import urlsafe_b64encode as b64enc from .pb.header_pb2 import Header
from functools import reduce from .pb.live_pb2 import Continuation
import time from urllib.parse import quote
import base64
import random import random
import urllib.parse import time
''' '''
Generate continuation parameter of youtube live chat. Generate continuation parameter of youtube live chat.
Author: taizan-hokuto (2019) @taizan205 Author: taizan-hokuto
ver 0.0.1 2019.10.05 ver 0.0.1 2019.10.05 : Initial release.
ver 0.0.2 2020.05.30 : Use Protocol Buffers.
''' '''
def _gen_vid(video_id):
def _gen_vid(video_id) -> str:
"""generate video_id parameter. """generate video_id parameter.
Parameter Parameter
--------- ---------
@@ -19,130 +23,51 @@ def _gen_vid(video_id):
Return Return
--------- ---------
byte[] : base64 encoded video_id parameter. str : base64 encoded video_id parameter.
""" """
header_magic = b'\x0A\x0F\x0A\x0D\x0A' header = Header()
header_id = video_id.encode() header.info.video.id = video_id
header_sep_1 = b'\x1A' header.terminator = 1
header_sep_2 = b'\x43\xAA\xB9\xC1\xBD\x01\x3D\x0A' return base64.urlsafe_b64encode(header.SerializeToString()).decode()
header_suburl = ('https://www.youtube.com/live_chat?v='
f'{video_id}&is_popout=1').encode()
header_terminator = b'\x20\x02'
item = [
header_magic,
_nval(len(header_id)),
header_id,
header_sep_1,
header_sep_2,
_nval(len(header_suburl)),
header_suburl,
header_terminator
]
return urllib.parse.quote( def _build(video_id, ts1, ts2, ts3, ts4, ts5, topchat_only) -> str:
b64enc(reduce(lambda x, y: x+y, item)).decode() chattype = 1
).encode() if topchat_only:
chattype = 4
continuation = Continuation()
entity = continuation.entity
def _tzparity(video_id,times): entity.header = _gen_vid(video_id)
t=0 entity.timestamp1 = ts1
for i,s in enumerate(video_id): entity.s6 = 0
ss = ord(s) entity.s7 = 0
if(ss % 2 == 0): entity.s8 = 1
t += ss*(12-i) entity.body.b1 = 0
else: entity.body.b2 = 0
t ^= ss*i entity.body.b3 = 0
entity.body.b4 = 0
entity.body.b7 = ''
entity.body.b8 = 0
entity.body.b9 = ''
entity.body.timestamp2 = ts2
entity.body.b11 = 3
entity.body.b15 = 0
entity.timestamp3 = ts3
entity.timestamp4 = ts4
entity.s13 = chattype
entity.chattype.value = chattype
entity.s17 = 0
entity.str19.value = 0
entity.timestamp5 = ts5
return ((times^t) % 2).to_bytes(1,'big') return quote(
base64.urlsafe_b64encode(continuation.SerializeToString()).decode()
def _nval(val):
"""convert value to byte array"""
if val<0: raise ValueError
buf = b''
while val >> 7:
m = val & 0xFF | 0x80
buf += m.to_bytes(1,'big')
val >>= 7
buf += val.to_bytes(1,'big')
return buf
def _build(video_id, _ts1, _ts2, _ts3, _ts4, _ts5, topchat_only):
#_short_type2
switch_01 = b'\x04' if topchat_only else b'\x01'
parity = _tzparity(video_id, _ts1^_ts2^_ts3^_ts4^_ts5)
header_magic= b'\xD2\x87\xCC\xC8\x03'
sep_0 = b'\x1A'
vid = _gen_vid(video_id)
time_tag = b'\x28'
timestamp1 = _nval(_ts1)
sep_1 = b'\x30\x00\x38\x00\x40\x02\x4A'
un_len = b'\x2B'
sep_2 = b'\x08'+parity+b'\x10\x00\x18\x00\x20\x00'
chkstr = b'\x2A\x0E\x73\x74\x61\x74\x69\x63\x63\x68\x65\x63\x6B\x73\x75\x6D'
sep_3 = b'\x3A\x00\x40\x00\x4A'
sep_4_len = b'\x02'
sep_4 = b'\x08\x01'
ts_2_start = b'\x50'
timestamp2 = _nval(_ts2)
ts_2_end = b'\x58'
sep_5 = b'\x03'
ts_3_start = b'\x50'
timestamp3 = _nval(_ts3)
ts_3_end = b'\x58'
timestamp4 = _nval(_ts4)
sep_6 = b'\x68'
#switch
sep_7 = b'\x82\x01\x04\x08'
#switch
sep_8 = b'\x10\x00'
sep_9 = b'\x88\x01\x00\xA0\x01'
timestamp5 = _nval(_ts5)
body = [
sep_0,
_nval(len(vid)),
vid,
time_tag,
timestamp1,
sep_1,
un_len,
sep_2,
chkstr,
sep_3,
sep_4_len,
sep_4,
ts_2_start,
timestamp2,
ts_2_end,
sep_5,
ts_3_start,
timestamp3,
ts_3_end,
timestamp4,
sep_6,
switch_01,#
sep_7,
switch_01,#
sep_8,
sep_9,
timestamp5
]
body = reduce(lambda x, y: x+y, body)
return urllib.parse.quote(
b64enc( header_magic +
_nval(len(body)) +
body
).decode()
) )
def _times(past_sec): def _times(past_sec):
n = int(time.time()) n = int(time.time())
_ts1 = n - random.uniform(0, 1 * 3) _ts1 = n - random.uniform(0, 1 * 3)
_ts2 = n - random.uniform(0.01, 0.99) _ts2 = n - random.uniform(0.01, 0.99)
_ts3 = n - past_sec + random.uniform(0, 1) _ts3 = n - past_sec + random.uniform(0, 1)
@@ -151,7 +76,7 @@ def _times(past_sec):
return list(map(lambda x: int(x * 1000000), [_ts1, _ts2, _ts3, _ts4, _ts5])) return list(map(lambda x: int(x * 1000000), [_ts1, _ts2, _ts3, _ts4, _ts5]))
def getparam(video_id, past_sec = 0, topchat_only = False): def getparam(video_id, past_sec=0, topchat_only=False) -> str:
''' '''
Parameter Parameter
--------- ---------
@@ -161,4 +86,3 @@ def getparam(video_id, past_sec = 0, topchat_only = False):
if True, fetch only 'top chat' if True, fetch only 'top chat'
''' '''
return _build(video_id, *_times(past_sec), topchat_only) return _build(video_id, *_times(past_sec), topchat_only)

View File

View File

@@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: header.proto
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor.FileDescriptor(
name='header.proto',
package='',
syntax='proto3',
serialized_options=None,
create_key=_descriptor._internal_create_key,
serialized_pb=b'\n\x0cheader.proto\"\x13\n\x05Video\x12\n\n\x02id\x18\x01 \x01(\t\"#\n\nHeaderInfo\x12\x15\n\x05video\x18\x01 \x01(\x0b\x32\x06.Video\"7\n\x06Header\x12\x19\n\x04info\x18\x01 \x01(\x0b\x32\x0b.HeaderInfo\x12\x12\n\nterminator\x18\x04 \x01(\x05\x62\x06proto3'
)
_VIDEO = _descriptor.Descriptor(
name='Video',
full_name='Video',
filename=None,
file=DESCRIPTOR,
containing_type=None,
create_key=_descriptor._internal_create_key,
fields=[
_descriptor.FieldDescriptor(
name='id', full_name='Video.id', index=0,
number=1, type=9, cpp_type=9, label=1,
has_default_value=False, default_value=b"".decode('utf-8'),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
],
extensions=[
],
nested_types=[],
enum_types=[
],
serialized_options=None,
is_extendable=False,
syntax='proto3',
extension_ranges=[],
oneofs=[
],
serialized_start=16,
serialized_end=35,
)
_HEADERINFO = _descriptor.Descriptor(
name='HeaderInfo',
full_name='HeaderInfo',
filename=None,
file=DESCRIPTOR,
containing_type=None,
create_key=_descriptor._internal_create_key,
fields=[
_descriptor.FieldDescriptor(
name='video', full_name='HeaderInfo.video', index=0,
number=1, type=11, cpp_type=10, label=1,
has_default_value=False, default_value=None,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
],
extensions=[
],
nested_types=[],
enum_types=[
],
serialized_options=None,
is_extendable=False,
syntax='proto3',
extension_ranges=[],
oneofs=[
],
serialized_start=37,
serialized_end=72,
)
_HEADER = _descriptor.Descriptor(
name='Header',
full_name='Header',
filename=None,
file=DESCRIPTOR,
containing_type=None,
create_key=_descriptor._internal_create_key,
fields=[
_descriptor.FieldDescriptor(
name='info', full_name='Header.info', index=0,
number=1, type=11, cpp_type=10, label=1,
has_default_value=False, default_value=None,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='terminator', full_name='Header.terminator', index=1,
number=4, type=5, cpp_type=1, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
],
extensions=[
],
nested_types=[],
enum_types=[
],
serialized_options=None,
is_extendable=False,
syntax='proto3',
extension_ranges=[],
oneofs=[
],
serialized_start=74,
serialized_end=129,
)
_HEADERINFO.fields_by_name['video'].message_type = _VIDEO
_HEADER.fields_by_name['info'].message_type = _HEADERINFO
DESCRIPTOR.message_types_by_name['Video'] = _VIDEO
DESCRIPTOR.message_types_by_name['HeaderInfo'] = _HEADERINFO
DESCRIPTOR.message_types_by_name['Header'] = _HEADER
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
Video = _reflection.GeneratedProtocolMessageType('Video', (_message.Message,), {
'DESCRIPTOR' : _VIDEO,
'__module__' : 'header_pb2'
# @@protoc_insertion_point(class_scope:Video)
})
_sym_db.RegisterMessage(Video)
HeaderInfo = _reflection.GeneratedProtocolMessageType('HeaderInfo', (_message.Message,), {
'DESCRIPTOR' : _HEADERINFO,
'__module__' : 'header_pb2'
# @@protoc_insertion_point(class_scope:HeaderInfo)
})
_sym_db.RegisterMessage(HeaderInfo)
Header = _reflection.GeneratedProtocolMessageType('Header', (_message.Message,), {
'DESCRIPTOR' : _HEADER,
'__module__' : 'header_pb2'
# @@protoc_insertion_point(class_scope:Header)
})
_sym_db.RegisterMessage(Header)
# @@protoc_insertion_point(module_scope)

View File

@@ -0,0 +1,381 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: live.proto
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor.FileDescriptor(
name='live.proto',
package='live',
syntax='proto3',
serialized_options=None,
create_key=_descriptor._internal_create_key,
serialized_pb=b'\n\nlive.proto\x12\x04live\"\x88\x01\n\x04\x42ody\x12\n\n\x02\x62\x31\x18\x01 \x01(\x05\x12\n\n\x02\x62\x32\x18\x02 \x01(\x05\x12\n\n\x02\x62\x33\x18\x03 \x01(\x05\x12\n\n\x02\x62\x34\x18\x04 \x01(\x05\x12\n\n\x02\x62\x37\x18\x07 \x01(\t\x12\n\n\x02\x62\x38\x18\x08 \x01(\x05\x12\n\n\x02\x62\x39\x18\t \x01(\t\x12\x12\n\ntimestamp2\x18\n \x01(\x03\x12\x0b\n\x03\x62\x31\x31\x18\x0b \x01(\x05\x12\x0b\n\x03\x62\x31\x35\x18\x0f \x01(\x05\"\x19\n\x08\x43hatType\x12\r\n\x05value\x18\x01 \x01(\x05\"\x16\n\x05STR19\x12\r\n\x05value\x18\x01 \x01(\x05\"\x8a\x02\n\x12\x43ontinuationEntity\x12\x0e\n\x06header\x18\x03 \x01(\t\x12\x12\n\ntimestamp1\x18\x05 \x01(\x03\x12\n\n\x02s6\x18\x06 \x01(\x05\x12\n\n\x02s7\x18\x07 \x01(\x05\x12\n\n\x02s8\x18\x08 \x01(\x05\x12\x18\n\x04\x62ody\x18\t \x01(\x0b\x32\n.live.Body\x12\x12\n\ntimestamp3\x18\n \x01(\x03\x12\x12\n\ntimestamp4\x18\x0b \x01(\x03\x12\x0b\n\x03s13\x18\r \x01(\x05\x12 \n\x08\x63hattype\x18\x10 \x01(\x0b\x32\x0e.live.ChatType\x12\x0b\n\x03s17\x18\x11 \x01(\x05\x12\x1a\n\x05str19\x18\x13 \x01(\x0b\x32\x0b.live.STR19\x12\x12\n\ntimestamp5\x18\x14 \x01(\x03\";\n\x0c\x43ontinuation\x12+\n\x06\x65ntity\x18\xfa\xc0\x89\x39 \x01(\x0b\x32\x18.live.ContinuationEntityb\x06proto3'
)
_BODY = _descriptor.Descriptor(
name='Body',
full_name='live.Body',
filename=None,
file=DESCRIPTOR,
containing_type=None,
create_key=_descriptor._internal_create_key,
fields=[
_descriptor.FieldDescriptor(
name='b1', full_name='live.Body.b1', index=0,
number=1, type=5, cpp_type=1, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='b2', full_name='live.Body.b2', index=1,
number=2, type=5, cpp_type=1, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='b3', full_name='live.Body.b3', index=2,
number=3, type=5, cpp_type=1, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='b4', full_name='live.Body.b4', index=3,
number=4, type=5, cpp_type=1, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='b7', full_name='live.Body.b7', index=4,
number=7, type=9, cpp_type=9, label=1,
has_default_value=False, default_value=b"".decode('utf-8'),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='b8', full_name='live.Body.b8', index=5,
number=8, type=5, cpp_type=1, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='b9', full_name='live.Body.b9', index=6,
number=9, type=9, cpp_type=9, label=1,
has_default_value=False, default_value=b"".decode('utf-8'),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='timestamp2', full_name='live.Body.timestamp2', index=7,
number=10, type=3, cpp_type=2, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='b11', full_name='live.Body.b11', index=8,
number=11, type=5, cpp_type=1, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='b15', full_name='live.Body.b15', index=9,
number=15, type=5, cpp_type=1, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
],
extensions=[
],
nested_types=[],
enum_types=[
],
serialized_options=None,
is_extendable=False,
syntax='proto3',
extension_ranges=[],
oneofs=[
],
serialized_start=21,
serialized_end=157,
)
_CHATTYPE = _descriptor.Descriptor(
name='ChatType',
full_name='live.ChatType',
filename=None,
file=DESCRIPTOR,
containing_type=None,
create_key=_descriptor._internal_create_key,
fields=[
_descriptor.FieldDescriptor(
name='value', full_name='live.ChatType.value', index=0,
number=1, type=5, cpp_type=1, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
],
extensions=[
],
nested_types=[],
enum_types=[
],
serialized_options=None,
is_extendable=False,
syntax='proto3',
extension_ranges=[],
oneofs=[
],
serialized_start=159,
serialized_end=184,
)
_STR19 = _descriptor.Descriptor(
name='STR19',
full_name='live.STR19',
filename=None,
file=DESCRIPTOR,
containing_type=None,
create_key=_descriptor._internal_create_key,
fields=[
_descriptor.FieldDescriptor(
name='value', full_name='live.STR19.value', index=0,
number=1, type=5, cpp_type=1, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
],
extensions=[
],
nested_types=[],
enum_types=[
],
serialized_options=None,
is_extendable=False,
syntax='proto3',
extension_ranges=[],
oneofs=[
],
serialized_start=186,
serialized_end=208,
)
_CONTINUATIONENTITY = _descriptor.Descriptor(
name='ContinuationEntity',
full_name='live.ContinuationEntity',
filename=None,
file=DESCRIPTOR,
containing_type=None,
create_key=_descriptor._internal_create_key,
fields=[
_descriptor.FieldDescriptor(
name='header', full_name='live.ContinuationEntity.header', index=0,
number=3, type=9, cpp_type=9, label=1,
has_default_value=False, default_value=b"".decode('utf-8'),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='timestamp1', full_name='live.ContinuationEntity.timestamp1', index=1,
number=5, type=3, cpp_type=2, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='s6', full_name='live.ContinuationEntity.s6', index=2,
number=6, type=5, cpp_type=1, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='s7', full_name='live.ContinuationEntity.s7', index=3,
number=7, type=5, cpp_type=1, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='s8', full_name='live.ContinuationEntity.s8', index=4,
number=8, type=5, cpp_type=1, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='body', full_name='live.ContinuationEntity.body', index=5,
number=9, type=11, cpp_type=10, label=1,
has_default_value=False, default_value=None,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='timestamp3', full_name='live.ContinuationEntity.timestamp3', index=6,
number=10, type=3, cpp_type=2, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='timestamp4', full_name='live.ContinuationEntity.timestamp4', index=7,
number=11, type=3, cpp_type=2, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='s13', full_name='live.ContinuationEntity.s13', index=8,
number=13, type=5, cpp_type=1, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='chattype', full_name='live.ContinuationEntity.chattype', index=9,
number=16, type=11, cpp_type=10, label=1,
has_default_value=False, default_value=None,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='s17', full_name='live.ContinuationEntity.s17', index=10,
number=17, type=5, cpp_type=1, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='str19', full_name='live.ContinuationEntity.str19', index=11,
number=19, type=11, cpp_type=10, label=1,
has_default_value=False, default_value=None,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='timestamp5', full_name='live.ContinuationEntity.timestamp5', index=12,
number=20, type=3, cpp_type=2, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
],
extensions=[
],
nested_types=[],
enum_types=[
],
serialized_options=None,
is_extendable=False,
syntax='proto3',
extension_ranges=[],
oneofs=[
],
serialized_start=211,
serialized_end=477,
)
_CONTINUATION = _descriptor.Descriptor(
name='Continuation',
full_name='live.Continuation',
filename=None,
file=DESCRIPTOR,
containing_type=None,
create_key=_descriptor._internal_create_key,
fields=[
_descriptor.FieldDescriptor(
name='entity', full_name='live.Continuation.entity', index=0,
number=119693434, type=11, cpp_type=10, label=1,
has_default_value=False, default_value=None,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
],
extensions=[
],
nested_types=[],
enum_types=[
],
serialized_options=None,
is_extendable=False,
syntax='proto3',
extension_ranges=[],
oneofs=[
],
serialized_start=479,
serialized_end=538,
)
_CONTINUATIONENTITY.fields_by_name['body'].message_type = _BODY
_CONTINUATIONENTITY.fields_by_name['chattype'].message_type = _CHATTYPE
_CONTINUATIONENTITY.fields_by_name['str19'].message_type = _STR19
_CONTINUATION.fields_by_name['entity'].message_type = _CONTINUATIONENTITY
DESCRIPTOR.message_types_by_name['Body'] = _BODY
DESCRIPTOR.message_types_by_name['ChatType'] = _CHATTYPE
DESCRIPTOR.message_types_by_name['STR19'] = _STR19
DESCRIPTOR.message_types_by_name['ContinuationEntity'] = _CONTINUATIONENTITY
DESCRIPTOR.message_types_by_name['Continuation'] = _CONTINUATION
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
Body = _reflection.GeneratedProtocolMessageType('Body', (_message.Message,), {
'DESCRIPTOR' : _BODY,
'__module__' : 'live_pb2'
# @@protoc_insertion_point(class_scope:live.Body)
})
_sym_db.RegisterMessage(Body)
ChatType = _reflection.GeneratedProtocolMessageType('ChatType', (_message.Message,), {
'DESCRIPTOR' : _CHATTYPE,
'__module__' : 'live_pb2'
# @@protoc_insertion_point(class_scope:live.ChatType)
})
_sym_db.RegisterMessage(ChatType)
STR19 = _reflection.GeneratedProtocolMessageType('STR19', (_message.Message,), {
'DESCRIPTOR' : _STR19,
'__module__' : 'live_pb2'
# @@protoc_insertion_point(class_scope:live.STR19)
})
_sym_db.RegisterMessage(STR19)
ContinuationEntity = _reflection.GeneratedProtocolMessageType('ContinuationEntity', (_message.Message,), {
'DESCRIPTOR' : _CONTINUATIONENTITY,
'__module__' : 'live_pb2'
# @@protoc_insertion_point(class_scope:live.ContinuationEntity)
})
_sym_db.RegisterMessage(ContinuationEntity)
Continuation = _reflection.GeneratedProtocolMessageType('Continuation', (_message.Message,), {
'DESCRIPTOR' : _CONTINUATION,
'__module__' : 'live_pb2'
# @@protoc_insertion_point(class_scope:live.Continuation)
})
_sym_db.RegisterMessage(Continuation)
# @@protoc_insertion_point(module_scope)

View File

@@ -0,0 +1,215 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: replay.proto
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor.FileDescriptor(
name='replay.proto',
package='replay',
syntax='proto3',
serialized_options=None,
create_key=_descriptor._internal_create_key,
serialized_pb=b'\n\x0creplay.proto\x12\x06replay\"\x19\n\x08\x43hatType\x12\r\n\x05value\x18\x01 \x01(\x05\"\xb2\x01\n\x12\x43ontinuationEntity\x12\x0e\n\x06header\x18\x03 \x01(\t\x12\x11\n\ttimestamp\x18\x05 \x01(\x03\x12\n\n\x02s6\x18\x06 \x01(\x05\x12\n\n\x02s7\x18\x07 \x01(\x05\x12\n\n\x02s8\x18\x08 \x01(\x05\x12\n\n\x02s9\x18\t \x01(\x05\x12\x0b\n\x03s10\x18\n \x01(\t\x12\x0b\n\x03s12\x18\x0c \x01(\x05\x12\"\n\x08\x63hattype\x18\x0e \x01(\x0b\x32\x10.replay.ChatType\x12\x0b\n\x03s15\x18\x0f \x01(\x05\"=\n\x0c\x43ontinuation\x12-\n\x06\x65ntity\x18\xd4\x83\xb6J \x01(\x0b\x32\x1a.replay.ContinuationEntityb\x06proto3'
)
_CHATTYPE = _descriptor.Descriptor(
name='ChatType',
full_name='replay.ChatType',
filename=None,
file=DESCRIPTOR,
containing_type=None,
create_key=_descriptor._internal_create_key,
fields=[
_descriptor.FieldDescriptor(
name='value', full_name='replay.ChatType.value', index=0,
number=1, type=5, cpp_type=1, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
],
extensions=[
],
nested_types=[],
enum_types=[
],
serialized_options=None,
is_extendable=False,
syntax='proto3',
extension_ranges=[],
oneofs=[
],
serialized_start=24,
serialized_end=49,
)
_CONTINUATIONENTITY = _descriptor.Descriptor(
name='ContinuationEntity',
full_name='replay.ContinuationEntity',
filename=None,
file=DESCRIPTOR,
containing_type=None,
create_key=_descriptor._internal_create_key,
fields=[
_descriptor.FieldDescriptor(
name='header', full_name='replay.ContinuationEntity.header', index=0,
number=3, type=9, cpp_type=9, label=1,
has_default_value=False, default_value=b"".decode('utf-8'),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='timestamp', full_name='replay.ContinuationEntity.timestamp', index=1,
number=5, type=3, cpp_type=2, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='s6', full_name='replay.ContinuationEntity.s6', index=2,
number=6, type=5, cpp_type=1, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='s7', full_name='replay.ContinuationEntity.s7', index=3,
number=7, type=5, cpp_type=1, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='s8', full_name='replay.ContinuationEntity.s8', index=4,
number=8, type=5, cpp_type=1, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='s9', full_name='replay.ContinuationEntity.s9', index=5,
number=9, type=5, cpp_type=1, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='s10', full_name='replay.ContinuationEntity.s10', index=6,
number=10, type=9, cpp_type=9, label=1,
has_default_value=False, default_value=b"".decode('utf-8'),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='s12', full_name='replay.ContinuationEntity.s12', index=7,
number=12, type=5, cpp_type=1, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='chattype', full_name='replay.ContinuationEntity.chattype', index=8,
number=14, type=11, cpp_type=10, label=1,
has_default_value=False, default_value=None,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
_descriptor.FieldDescriptor(
name='s15', full_name='replay.ContinuationEntity.s15', index=9,
number=15, type=5, cpp_type=1, label=1,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
],
extensions=[
],
nested_types=[],
enum_types=[
],
serialized_options=None,
is_extendable=False,
syntax='proto3',
extension_ranges=[],
oneofs=[
],
serialized_start=52,
serialized_end=230,
)
_CONTINUATION = _descriptor.Descriptor(
name='Continuation',
full_name='replay.Continuation',
filename=None,
file=DESCRIPTOR,
containing_type=None,
create_key=_descriptor._internal_create_key,
fields=[
_descriptor.FieldDescriptor(
name='entity', full_name='replay.Continuation.entity', index=0,
number=156074452, type=11, cpp_type=10, label=1,
has_default_value=False, default_value=None,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key),
],
extensions=[
],
nested_types=[],
enum_types=[
],
serialized_options=None,
is_extendable=False,
syntax='proto3',
extension_ranges=[],
oneofs=[
],
serialized_start=232,
serialized_end=293,
)
_CONTINUATIONENTITY.fields_by_name['chattype'].message_type = _CHATTYPE
_CONTINUATION.fields_by_name['entity'].message_type = _CONTINUATIONENTITY
DESCRIPTOR.message_types_by_name['ChatType'] = _CHATTYPE
DESCRIPTOR.message_types_by_name['ContinuationEntity'] = _CONTINUATIONENTITY
DESCRIPTOR.message_types_by_name['Continuation'] = _CONTINUATION
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
ChatType = _reflection.GeneratedProtocolMessageType('ChatType', (_message.Message,), {
'DESCRIPTOR' : _CHATTYPE,
'__module__' : 'replay_pb2'
# @@protoc_insertion_point(class_scope:replay.ChatType)
})
_sym_db.RegisterMessage(ChatType)
ContinuationEntity = _reflection.GeneratedProtocolMessageType('ContinuationEntity', (_message.Message,), {
'DESCRIPTOR' : _CONTINUATIONENTITY,
'__module__' : 'replay_pb2'
# @@protoc_insertion_point(class_scope:replay.ContinuationEntity)
})
_sym_db.RegisterMessage(ContinuationEntity)
Continuation = _reflection.GeneratedProtocolMessageType('Continuation', (_message.Message,), {
'DESCRIPTOR' : _CONTINUATION,
'__module__' : 'replay_pb2'
# @@protoc_insertion_point(class_scope:replay.Continuation)
})
_sym_db.RegisterMessage(Continuation)
# @@protoc_insertion_point(module_scope)

View File

@@ -0,0 +1,14 @@
syntax = "proto3";
message Video {
string id = 1;
}
message HeaderInfo {
Video video = 1;
}
message Header {
HeaderInfo info = 1;
int32 terminator = 4;
}

View File

@@ -0,0 +1,45 @@
syntax = "proto3";
package live;
message Body {
int32 b1 = 1;
int32 b2 = 2;
int32 b3 = 3;
int32 b4 = 4;
string b7 = 7;
int32 b8 = 8;
string b9 = 9;
int64 timestamp2 = 10;
int32 b11 = 11;
int32 b15 = 15;
}
message ChatType {
int32 value = 1;
}
message STR19 {
int32 value = 1;
}
message ContinuationEntity {
string header = 3;
int64 timestamp1 = 5;
int32 s6 = 6;
int32 s7 = 7;
int32 s8 = 8;
Body body = 9;
int64 timestamp3 = 10;
int64 timestamp4 = 11;
int32 s13 = 13;
ChatType chattype = 16;
int32 s17 = 17;
STR19 str19 = 19;
int64 timestamp5 = 20;
}
message Continuation {
ContinuationEntity entity = 119693434;
}

View File

@@ -0,0 +1,24 @@
syntax = "proto3";
package replay;
message ChatType {
int32 value = 1;
}
message ContinuationEntity {
string header = 3;
int64 timestamp = 5;
int32 s6 = 6;
int32 s7 = 7;
int32 s8 = 8;
int32 s9 = 9;
string s10 = 10;
int32 s12 = 12;
ChatType chattype = 14;
int32 s15 = 15;
}
message Continuation {
ContinuationEntity entity = 156074452;
}

View File

@@ -4,12 +4,8 @@ pytchat.parser.live
Parser of live chat JSON. Parser of live chat JSON.
""" """
import json from .. import exceptions
from .. exceptions import (
ResponseContextError,
NoContentsException,
NoContinuationsException,
ChatParseException )
class Parser: class Parser:
@@ -20,10 +16,10 @@ class Parser:
def get_contents(self, jsn): def get_contents(self, jsn):
if jsn is None: if jsn is None:
raise ChatParseException('Called with none JSON object.') raise exceptions.IllegalFunctionCall('Called with none JSON object.')
if jsn['response']['responseContext'].get('errors'): if jsn['response']['responseContext'].get('errors'):
raise ResponseContextError('The video_id would be wrong,' raise exceptions.ResponseContextError(
'or video is deleted or private.') 'The video_id would be wrong, or video is deleted or private.')
contents = jsn['response'].get('continuationContents') contents = jsn['response'].get('continuationContents')
return contents return contents
@@ -46,33 +42,37 @@ class Parser:
if contents is None: if contents is None:
'''Broadcasting end or cannot fetch chat stream''' '''Broadcasting end or cannot fetch chat stream'''
raise NoContentsException('Chat data stream is empty.') raise exceptions.NoContents('Chat data stream is empty.')
cont = contents['liveChatContinuation']['continuations'][0] cont = contents['liveChatContinuation']['continuations'][0]
if cont is None: if cont is None:
raise NoContinuationsException('No Continuation') raise exceptions.NoContinuation('No Continuation')
metadata = (cont.get('invalidationContinuationData') or metadata = (cont.get('invalidationContinuationData')
cont.get('timedContinuationData') or or cont.get('timedContinuationData')
cont.get('reloadContinuationData') or or cont.get('reloadContinuationData')
cont.get('liveChatReplayContinuationData') or cont.get('liveChatReplayContinuationData')
) )
if metadata is None: if metadata is None:
if cont.get("playerSeekContinuationData"): if cont.get("playerSeekContinuationData"):
raise ChatParseException('Finished chat data') raise exceptions.ChatDataFinished('Finished chat data')
unknown = list(cont.keys())[0] unknown = list(cont.keys())[0]
if unknown: if unknown:
raise ChatParseException(f"Received unknown continuation type:{unknown}") raise exceptions.ReceivedUnknownContinuation(
f"Received unknown continuation type:{unknown}")
else: else:
raise ChatParseException('Cannot extract continuation data') raise exceptions.FailedExtractContinuation('Cannot extract continuation data')
return self._create_data(metadata, contents) return self._create_data(metadata, contents)
def reload_continuation(self, contents): def reload_continuation(self, contents):
""" """
When `seektime = 0` or seektime is abbreviated , When `seektime == 0` or seektime is abbreviated ,
check if fetched chat json has no chat data. check if fetched chat json has no chat data.
If so, try to fetch playerSeekContinuationData. If so, try to fetch playerSeekContinuationData.
This function must be run only first fetching. This function must be run only first fetching.
""" """
if contents is None:
'''Broadcasting end or cannot fetch chat stream'''
raise exceptions.NoContents('Chat data stream is empty.')
cont = contents['liveChatContinuation']['continuations'][0] cont = contents['liveChatContinuation']['continuations'][0]
if cont.get("liveChatReplayContinuationData"): if cont.get("liveChatReplayContinuationData"):
# chat data exist. # chat data exist.
@@ -81,7 +81,7 @@ class Parser:
init_cont = cont.get("playerSeekContinuationData") init_cont = cont.get("playerSeekContinuationData")
if init_cont: if init_cont:
return init_cont.get("continuation") return init_cont.get("continuation")
raise ChatParseException('Finished chat data') raise exceptions.ChatDataFinished('Finished chat data')
def _create_data(self, metadata, contents): def _create_data(self, metadata, contents):
actions = contents['liveChatContinuation'].get('actions') actions = contents['liveChatContinuation'].get('actions')

View File

@@ -1,76 +0,0 @@
import json
from .. import config
from .. exceptions import (
ResponseContextError,
NoContentsException,
NoContinuationsException )
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'):
raise ResponseContextError('動画に接続できません。'
'動画IDが間違っているか、動画が削除非公開の可能性があります。')
contents=jsn['response'].get('continuationContents')
#配信が終了した場合、もしくはチャットデータが取得できない場合
if contents is None:
raise NoContentsException('チャットデータを取得できませんでした。')
cont = contents['liveChatContinuation']['continuations'][0]
if cont is None:
raise NoContinuationsException('Continuationがありません。')
metadata = cont.get('liveChatReplayContinuationData')
if metadata is None:
unknown = list(cont.keys())[0]
if unknown != "playerSeekContinuationData":
logger.debug(f"Received unknown continuation type:{unknown}")
metadata = cont.get(unknown)
actions = contents['liveChatContinuation'].get('actions')
if actions is None:
#後続のチャットデータなし
return {"continuation":None,"timeout":0,"chatdata":[]}
interval = self.get_interval(actions)
metadata.setdefault("timeoutMs",interval)
"""アーカイブ済みチャットはライブチャットと構造が異なっているため、以下の行により
ライブチャットと同じ形式にそろえる"""
chatdata = [action["replayChatItemAction"]["actions"][0] for action in actions]
return metadata, chatdata
def get_interval(self, actions: list):
if actions is None:
return 0
start = int(actions[0]["replayChatItemAction"]["videoOffsetTimeMsec"])
last = int(actions[-1]["replayChatItemAction"]["videoOffsetTimeMsec"])
return (last - start)

View File

@@ -3,6 +3,7 @@ class ChatProcessor:
Abstract class that processes chat data. Abstract class that processes chat data.
Receive chat data (actions) from Listener. Receive chat data (actions) from Listener.
''' '''
def process(self, chat_components: list): def process(self, chat_components: list):
''' '''
Interface that represents processing of chat data. Interface that represents processing of chat data.
@@ -21,7 +22,9 @@ class ChatProcessor:
''' '''
pass pass
def finalize(self, *args, **kwargs):
'''
Interface for finalizing the process.
Called when chat fetching finished.
'''
pass

View File

@@ -1,5 +1,6 @@
from .chat_processor import ChatProcessor from .chat_processor import ChatProcessor
class Combinator(ChatProcessor): class Combinator(ChatProcessor):
''' '''
Combinator combines multiple chat processors. Combinator combines multiple chat processors.
@@ -35,5 +36,3 @@ class Combinator(ChatProcessor):
''' '''
return tuple(processor.process(chat_components) return tuple(processor.process(chat_components)
for processor in self.processors) for processor in self.processors)

View File

@@ -1,13 +1,13 @@
import datetime
import time
from .renderer.textmessage import LiveChatTextMessageRenderer 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 .renderer.membership import LiveChatMembershipItemRenderer
from .. chat_processor import ChatProcessor from .. chat_processor import ChatProcessor
from ... import config from ... import config
logger = config.logger(__name__) logger = config.logger(__name__)
class CompatibleProcessor(ChatProcessor): class CompatibleProcessor(ChatProcessor):
def process(self, chat_components: list): def process(self, chat_components: list):
@@ -24,11 +24,15 @@ class CompatibleProcessor(ChatProcessor):
timeout += chat_component.get('timeout', 0) timeout += chat_component.get('timeout', 0)
chatdata = chat_component.get('chatdata') chatdata = chat_component.get('chatdata')
if chatdata is None: break if chatdata is None:
break
for action in chatdata: for action in chatdata:
if action is None: continue if action is None:
if action.get('addChatItemAction') is None: continue continue
if action['addChatItemAction'].get('item') is None: continue 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: if chat:
@@ -47,11 +51,12 @@ class CompatibleProcessor(ChatProcessor):
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
rd = {} rd = {}
try: try:
renderer = self.get_renderer(item) renderer = self.get_renderer(item)
if renderer == None: if renderer is None:
return None return None
rd["kind"] = "youtube#liveChatMessage" rd["kind"] = "youtube#liveChatMessage"
@@ -75,7 +80,8 @@ class CompatibleProcessor(ChatProcessor):
renderer = LiveChatPaidStickerRenderer(item) renderer = LiveChatPaidStickerRenderer(item)
elif item.get("liveChatLegacyPaidMessageRenderer"): elif item.get("liveChatLegacyPaidMessageRenderer"):
renderer = LiveChatLegacyPaidMessageRenderer(item) renderer = LiveChatLegacyPaidMessageRenderer(item)
elif item.get("liveChatMembershipItemRenderer"):
renderer = LiveChatMembershipItemRenderer(item)
else: else:
renderer = None renderer = None
return renderer return renderer

View File

@@ -1,11 +1,12 @@
import datetime, pytz import datetime
import pytz
class BaseRenderer: class BaseRenderer:
def __init__(self, item, chattype): def __init__(self, item, chattype):
self.renderer = list(item.values())[0] self.renderer = list(item.values())[0]
self.chattype = chattype self.chattype = chattype
def get_snippet(self): def get_snippet(self):
message = self.get_message(self.renderer) message = self.get_message(self.renderer)
@@ -22,7 +23,6 @@ class BaseRenderer:
} }
} }
def get_authordetails(self): def get_authordetails(self):
authorExternalChannelId = self.renderer.get("authorExternalChannelId") authorExternalChannelId = self.renderer.get("authorExternalChannelId")
# parse subscriber type # parse subscriber type
@@ -40,7 +40,6 @@ class BaseRenderer:
"isChatModerator": isChatModerator "isChatModerator": isChatModerator
} }
def get_message(self, renderer): def get_message(self, renderer):
message = '' message = ''
if renderer.get("message"): if renderer.get("message"):
@@ -80,4 +79,3 @@ class BaseRenderer:
dt = datetime.datetime.fromtimestamp(int(timestamp) / 1000000) dt = datetime.datetime.fromtimestamp(int(timestamp) / 1000000)
return dt.astimezone(pytz.utc).isoformat( return dt.astimezone(pytz.utc).isoformat(
timespec='milliseconds').replace('+00:00', 'Z') timespec='milliseconds').replace('+00:00', 'Z')

View File

@@ -1,4 +1,6 @@
from .base import BaseRenderer from .base import BaseRenderer
class LiveChatLegacyPaidMessageRenderer(BaseRenderer): class LiveChatLegacyPaidMessageRenderer(BaseRenderer):
def __init__(self, item): def __init__(self, item):
super().__init__(item, "newSponsorEvent") super().__init__(item, "newSponsorEvent")
@@ -34,10 +36,7 @@ class LiveChatLegacyPaidMessageRenderer(BaseRenderer):
"isChatModerator": isChatModerator "isChatModerator": isChatModerator
} }
def get_message(self, renderer): def get_message(self, renderer):
message = (renderer["eventText"]["runs"][0]["text"] message = (renderer["eventText"]["runs"][0]["text"]
) + ' / ' + (renderer["detailText"]["simpleText"]) ) + ' / ' + (renderer["detailText"]["simpleText"])
return message return message

View File

@@ -0,0 +1,40 @@
from .base import BaseRenderer
class LiveChatMembershipItemRenderer(BaseRenderer):
def __init__(self, item):
super().__init__(item, "newSponsorEvent")
def get_snippet(self):
message = self.get_message(self.renderer)
return {
"type": self.chattype,
"liveChatId": "",
"authorChannelId": self.renderer.get("authorExternalChannelId"),
"publishedAt": self.get_publishedat(self.renderer.get("timestampUsec", 0)),
"hasDisplayContent": True,
"displayMessage": message,
}
def get_authordetails(self):
authorExternalChannelId = self.renderer.get("authorExternalChannelId")
# parse subscriber type
isVerified, isChatOwner, _, isChatModerator = (
self.get_badges(self.renderer)
)
return {
"channelId": authorExternalChannelId,
"channelUrl": "http://www.youtube.com/channel/" + authorExternalChannelId,
"displayName": self.renderer["authorName"]["simpleText"],
"profileImageUrl": self.renderer["authorPhoto"]["thumbnails"][1]["url"],
"isVerified": isVerified,
"isChatOwner": isChatOwner,
"isChatSponsor": True,
"isChatModerator": isChatModerator
}
def get_message(self, renderer):
message = ''.join([mes.get("text", "")
for mes in renderer["headerSubtext"]["runs"]])
return message, [message]

View File

@@ -3,6 +3,7 @@ from . import currency
from .base import BaseRenderer from .base import BaseRenderer
superchat_regex = re.compile(r"^(\D*)(\d{1,3}(,\d{3})*(\.\d*)*\b)$") superchat_regex = re.compile(r"^(\D*)(\d{1,3}(,\d{3})*(\.\d*)*\b)$")
class LiveChatPaidMessageRenderer(BaseRenderer): class LiveChatPaidMessageRenderer(BaseRenderer):
def __init__(self, item): def __init__(self, item):
super().__init__(item, "superChatEvent") super().__init__(item, "superChatEvent")

View File

@@ -3,6 +3,7 @@ from . import currency
from .base import BaseRenderer from .base import BaseRenderer
superchat_regex = re.compile(r"^(\D*)(\d{1,3}(,\d{3})*(\.\d*)*\b)$") superchat_regex = re.compile(r"^(\D*)(\d{1,3}(,\d{3})*(\.\d*)*\b)$")
class LiveChatPaidStickerRenderer(BaseRenderer): class LiveChatPaidStickerRenderer(BaseRenderer):
def __init__(self, item): def __init__(self, item):
super().__init__(item, "superStickerEvent") super().__init__(item, "superStickerEvent")
@@ -44,5 +45,3 @@ class LiveChatPaidStickerRenderer(BaseRenderer):
symbol = "" symbol = ""
amountMicros = 0 amountMicros = 0
return amountDisplayString, symbol, amountMicros return amountDisplayString, symbol, amountMicros

View File

@@ -1,4 +1,6 @@
from .base import BaseRenderer from .base import BaseRenderer
class LiveChatTextMessageRenderer(BaseRenderer): class LiveChatTextMessageRenderer(BaseRenderer):
def __init__(self, item): def __init__(self, item):
super().__init__(item, "textMessageEvent") super().__init__(item, "textMessageEvent")

View File

@@ -4,10 +4,13 @@ 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 .renderer.membership import LiveChatMembershipItemRenderer
from .. chat_processor import ChatProcessor from .. chat_processor import ChatProcessor
from ... import config from ... import config
logger = config.logger(__name__) logger = config.logger(__name__)
class Chatdata: class Chatdata:
def __init__(self, chatlist: list, timeout: float): def __init__(self, chatlist: list, timeout: float):
self.items = chatlist self.items = chatlist
@@ -25,6 +28,7 @@ class Chatdata:
return return
await asyncio.sleep(self.interval / len(self.items)) await asyncio.sleep(self.interval / len(self.items))
class DefaultProcessor(ChatProcessor): class DefaultProcessor(ChatProcessor):
def process(self, chat_components: list): def process(self, chat_components: list):
@@ -35,27 +39,30 @@ class DefaultProcessor(ChatProcessor):
for component in chat_components: for component in chat_components:
timeout += component.get('timeout', 0) timeout += component.get('timeout', 0)
chatdata = component.get('chatdata') chatdata = component.get('chatdata')
if chatdata is None: continue if chatdata is None:
continue
for action in chatdata: for action in chatdata:
if action is None: continue if action is None:
if action.get('addChatItemAction') is None: continue continue
if action['addChatItemAction'].get('item') is None: continue 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: 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 is None:
return None return None
renderer.get_snippet() renderer.get_snippet()
@@ -74,6 +81,8 @@ class DefaultProcessor(ChatProcessor):
renderer = LiveChatPaidStickerRenderer(item) renderer = LiveChatPaidStickerRenderer(item)
elif item.get("liveChatLegacyPaidMessageRenderer"): elif item.get("liveChatLegacyPaidMessageRenderer"):
renderer = LiveChatLegacyPaidMessageRenderer(item) renderer = LiveChatLegacyPaidMessageRenderer(item)
elif item.get("liveChatMembershipItemRenderer"):
renderer = LiveChatMembershipItemRenderer(item)
else: else:
renderer = None renderer = None
return renderer return renderer

View File

@@ -1,6 +1,10 @@
from datetime import datetime from datetime import datetime
class Author: class Author:
pass pass
class BaseRenderer: class BaseRenderer:
def __init__(self, item, chattype): def __init__(self, item, chattype):
self.renderer = list(item.values())[0] self.renderer = list(item.values())[0]
@@ -38,8 +42,6 @@ class BaseRenderer:
self.author.name = self.renderer["authorName"]["simpleText"] self.author.name = self.renderer["authorName"]["simpleText"]
self.author.imageUrl = self.renderer["authorPhoto"]["thumbnails"][1]["url"] self.author.imageUrl = self.renderer["authorPhoto"]["thumbnails"][1]["url"]
def get_message(self, renderer): def get_message(self, renderer):
message = '' message = ''
message_ex = [] message_ex = []
@@ -50,14 +52,16 @@ class BaseRenderer:
if r: if r:
if r.get('emoji'): if r.get('emoji'):
message += r['emoji'].get('shortcuts', [''])[0] message += r['emoji'].get('shortcuts', [''])[0]
message_ex.append(r['emoji']['image']['thumbnails'][1].get('url')) message_ex.append({
'id': r['emoji'].get('emojiId').split('/')[-1],
'txt': r['emoji'].get('shortcuts', [''])[0],
'url': r['emoji']['image']['thumbnails'][0].get('url')
})
else: else:
message += r.get('text', '') message += r.get('text', '')
message_ex.append(r.get('text', '')) message_ex.append(r.get('text', ''))
return message, message_ex return message, message_ex
def get_badges(self, renderer): def get_badges(self, renderer):
self.author.type = '' self.author.type = ''
isVerified = False isVerified = False
@@ -82,12 +86,9 @@ class BaseRenderer:
self.get_badgeurl(badge) self.get_badgeurl(badge)
return isVerified, isChatOwner, isChatSponsor, isChatModerator return isVerified, isChatOwner, isChatSponsor, isChatModerator
def get_badgeurl(self, badge): def get_badgeurl(self, badge):
self.author.badgeUrl = badge["liveChatAuthorBadgeRenderer"]["customThumbnail"]["thumbnails"][0]["url"] self.author.badgeUrl = badge["liveChatAuthorBadgeRenderer"]["customThumbnail"]["thumbnails"][0]["url"]
def get_datetime(self, timestamp): def get_datetime(self, timestamp):
dt = datetime.fromtimestamp(timestamp / 1000000) dt = datetime.fromtimestamp(timestamp / 1000000)
return dt.strftime('%Y-%m-%d %H:%M:%S') return dt.strftime('%Y-%m-%d %H:%M:%S')

View File

@@ -1,18 +1,15 @@
from .base import BaseRenderer from .base import BaseRenderer
class LiveChatLegacyPaidMessageRenderer(BaseRenderer): class LiveChatLegacyPaidMessageRenderer(BaseRenderer):
def __init__(self, item): def __init__(self, item):
super().__init__(item, "newSponsor") super().__init__(item, "newSponsor")
def get_authordetails(self): def get_authordetails(self):
super().get_authordetails() super().get_authordetails()
self.author.isChatSponsor = True self.author.isChatSponsor = True
def get_message(self, renderer): def get_message(self, renderer):
message = (renderer["eventText"]["runs"][0]["text"] message = (renderer["eventText"]["runs"][0]["text"]
) + ' / ' + (renderer["detailText"]["simpleText"]) ) + ' / ' + (renderer["detailText"]["simpleText"])
return message return message

View File

@@ -0,0 +1,15 @@
from .base import BaseRenderer
class LiveChatMembershipItemRenderer(BaseRenderer):
def __init__(self, item):
super().__init__(item, "newSponsor")
def get_authordetails(self):
super().get_authordetails()
self.author.isChatSponsor = True
def get_message(self, renderer):
message = ''.join([mes.get("text", "")
for mes in renderer["headerSubtext"]["runs"]])
return message, [message]

View File

@@ -3,11 +3,11 @@ from . import currency
from .base import BaseRenderer from .base import BaseRenderer
superchat_regex = re.compile(r"^(\D*)(\d{1,3}(,\d{3})*(\.\d*)*\b)$") superchat_regex = re.compile(r"^(\D*)(\d{1,3}(,\d{3})*(\.\d*)*\b)$")
class LiveChatPaidMessageRenderer(BaseRenderer): class LiveChatPaidMessageRenderer(BaseRenderer):
def __init__(self, item): def __init__(self, item):
super().__init__(item, "superChat") super().__init__(item, "superChat")
def get_snippet(self): def get_snippet(self):
super().get_snippet() super().get_snippet()
amountDisplayString, symbol, amount = ( amountDisplayString, symbol, amount = (
@@ -15,11 +15,10 @@ class LiveChatPaidMessageRenderer(BaseRenderer):
) )
self.amountValue = amount self.amountValue = amount
self.amountString = amountDisplayString self.amountString = amountDisplayString
self.currency= currency.symbols[symbol]["fxtext"] if currency.symbols.get(symbol) else symbol self.currency = currency.symbols[symbol]["fxtext"] if currency.symbols.get(
symbol) else symbol
self.bgColor = self.renderer.get("bodyBackgroundColor", 0) self.bgColor = self.renderer.get("bodyBackgroundColor", 0)
def get_amountdata(self, renderer): def get_amountdata(self, renderer):
amountDisplayString = renderer["purchaseAmountText"]["simpleText"] amountDisplayString = renderer["purchaseAmountText"]["simpleText"]
m = superchat_regex.search(amountDisplayString) m = superchat_regex.search(amountDisplayString)

View File

@@ -3,11 +3,11 @@ from . import currency
from .base import BaseRenderer from .base import BaseRenderer
superchat_regex = re.compile(r"^(\D*)(\d{1,3}(,\d{3})*(\.\d*)*\b)$") superchat_regex = re.compile(r"^(\D*)(\d{1,3}(,\d{3})*(\.\d*)*\b)$")
class LiveChatPaidStickerRenderer(BaseRenderer): class LiveChatPaidStickerRenderer(BaseRenderer):
def __init__(self, item): def __init__(self, item):
super().__init__(item, "superSticker") super().__init__(item, "superSticker")
def get_snippet(self): def get_snippet(self):
super().get_snippet() super().get_snippet()
amountDisplayString, symbol, amount = ( amountDisplayString, symbol, amount = (
@@ -15,11 +15,11 @@ class LiveChatPaidStickerRenderer(BaseRenderer):
) )
self.amountValue = amount self.amountValue = amount
self.amountString = amountDisplayString self.amountString = amountDisplayString
self.currency = currency.symbols[symbol]["fxtext"] if currency.symbols.get(symbol) else symbol self.currency = currency.symbols[symbol]["fxtext"] if currency.symbols.get(
symbol) else symbol
self.bgColor = self.renderer.get("moneyChipBackgroundColor", 0) self.bgColor = self.renderer.get("moneyChipBackgroundColor", 0)
self.sticker = "https:"+self.renderer["sticker"]["thumbnails"][0]["url"] self.sticker = "https:" + \
self.renderer["sticker"]["thumbnails"][0]["url"]
def get_amountdata(self, renderer): def get_amountdata(self, renderer):
amountDisplayString = renderer["purchaseAmountText"]["simpleText"] amountDisplayString = renderer["purchaseAmountText"]["simpleText"]
@@ -31,9 +31,3 @@ class LiveChatPaidStickerRenderer(BaseRenderer):
symbol = "" symbol = ""
amount = 0.0 amount = 0.0
return amountDisplayString, symbol, amount return amountDisplayString, symbol, amount

View File

@@ -1,4 +1,6 @@
from .base import BaseRenderer from .base import BaseRenderer
class LiveChatTextMessageRenderer(BaseRenderer): class LiveChatTextMessageRenderer(BaseRenderer):
def __init__(self, item): def __init__(self, item):
super().__init__(item, "textMessage") super().__init__(item, "textMessage")

View File

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

View File

@@ -1,25 +1,55 @@
import csv
import os import os
import re import re
import requests
from base64 import standard_b64encode
from .chat_processor import ChatProcessor from .chat_processor import ChatProcessor
from .default.processor import DefaultProcessor from .default.processor import DefaultProcessor
PATTERN = re.compile(r"(.*)\(([0-9]+)\)$") PATTERN = re.compile(r"(.*)\(([0-9]+)\)$")
fmt_headers = ['datetime','elapsed','authorName','message','superchat'
,'type','authorChannel'] fmt_headers = ['datetime', 'elapsed', 'authorName',
'message', 'superchat', 'type', 'authorChannel']
HEADER_HTML = '''
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<html>
<head>
'''
TABLE_CSS = '''
table.css {
border-collapse: collapse;
}
table.css thead{
border-collapse: collapse;
border: 1px solid #000
}
table.css tr td{
padding: 0.3em;
border: 1px solid #000
}
table.css th{
padding: 0.3em;
border: 1px solid #000
}
'''
class HTMLArchiver(ChatProcessor): class HTMLArchiver(ChatProcessor):
''' '''
HtmlArchiver saves chat data as HTML table format. HTMLArchiver saves chat data as HTML table format.
''' '''
def __init__(self, save_path): def __init__(self, save_path):
super().__init__() super().__init__()
self.save_path = self._checkpath(save_path) self.save_path = self._checkpath(save_path)
with open(self.save_path, mode='a', encoding = 'utf-8') as f:
f.write('<table border="1" style="border-collapse: collapse">')
f.writelines(self._parse_html_header(fmt_headers))
self.processor = DefaultProcessor() self.processor = DefaultProcessor()
self.emoji_table = {} # table for custom emojis. key: emoji_id, value: base64 encoded image binary.
self.header = [HEADER_HTML]
self.body = ['<body>\n', '<table class="css">\n', self._parse_table_header(fmt_headers)]
def _checkpath(self, filepath): def _checkpath(self, filepath):
splitter = os.path.splitext(os.path.basename(filepath)) splitter = os.path.splitext(os.path.basename(filepath))
@@ -50,43 +80,58 @@ class HTMLArchiver(ChatProcessor):
""" """
if chat_components is None or len(chat_components) == 0: if chat_components is None or len(chat_components) == 0:
return return
self.body.extend(
with open(self.save_path, mode='a', encoding = 'utf-8') as f: (self._parse_html_line((
chats = self.processor.process(chat_components).items
for c in chats:
f.writelines(
self._parse_html_line([
c.datetime, c.datetime,
c.elapsedTime, c.elapsedTime,
c.author.name, c.author.name,
c.message, self._parse_message(c.messageEx),
c.amountString, c.amountString,
c.author.type, c.author.type,
c.author.channelId] c.author.channelId)
) for c in self.processor.process(chat_components).items)
) )
)
'''
#Palliative treatment#
Comment out below line to prevent the table
display from collapsing.
'''
#f.write('</table>')
def _parse_html_line(self, raw_line): def _parse_html_line(self, raw_line):
html = '' return ''.join(('<tr>',
html+=' <tr>' ''.join(''.join(('<td>', cell, '</td>')) for cell in raw_line),
for cell in raw_line: '</tr>\n'))
html+='<td>'+cell+'</td>'
html+='</tr>\n'
return html
def _parse_html_header(self,raw_line): def _parse_table_header(self, raw_line):
html = '' return ''.join(('<thead><tr>',
html+='<thead>\n' ''.join(''.join(('<th>', cell, '</th>')) for cell in raw_line),
html+=' <tr>' '</tr></thead>\n'))
for cell in raw_line:
html+='<th>'+cell+'</th>'
html+='</tr>\n'
html+='</thead>\n'
return html
def _parse_message(self, message_items: list) -> str:
return ''.join(''.join(('<span class="', self._set_emoji_table(item), '" title="', item['txt'], '"></span>'))
if type(item) is dict else item
for item in message_items)
def _encode_img(self, url):
resp = requests.get(url)
return standard_b64encode(resp.content).decode()
def _set_emoji_table(self, item: dict):
emoji_id = item['id']
if emoji_id not in self.emoji_table:
self.emoji_table.setdefault(emoji_id, self._encode_img(item['url']))
return emoji_id
def _stylecode(self, name, code, width, height):
return ''.join((".", name, " { display: inline-block; background-image: url(data:image/png;base64,",
code, "); background-repeat: no-repeat; width: ",
str(width), "; height: ", str(height), ";}"))
def _create_styles(self):
return '\n'.join(('<style type="text/css">',
TABLE_CSS,
'\n'.join(self._stylecode(key, self.emoji_table[key], 24, 24)
for key in self.emoji_table.keys()),
'</style>\n'))
def finalize(self):
self.header.extend([self._create_styles(), '</head>\n'])
self.body.extend(['</table>\n</body>'])
with open(self.save_path, mode='a', encoding='utf-8') as f:
f.writelines(self.header)
f.writelines(self.body)

View File

@@ -1,4 +1,3 @@
import datetime
import json import json
import os import os
import re import re
@@ -6,6 +5,7 @@ from .chat_processor import ChatProcessor
PATTERN = re.compile(r"(.*)\(([0-9]+)\)$") PATTERN = re.compile(r"(.*)\(([0-9]+)\)$")
class JsonfileArchiver(ChatProcessor): class JsonfileArchiver(ChatProcessor):
""" """
JsonfileArchiver saves chat data as text of JSON lines. JsonfileArchiver saves chat data as text of JSON lines.
@@ -17,6 +17,7 @@ class JsonfileArchiver(ChatProcessor):
it is automatically saved under a different name it is automatically saved under a different name
with suffix '(number)' with suffix '(number)'
""" """
def __init__(self, save_path): def __init__(self, save_path):
super().__init__() super().__init__()
self.save_path = self._checkpath(save_path) self.save_path = self._checkpath(save_path)
@@ -32,14 +33,18 @@ class JsonfileArchiver(ChatProcessor):
total_lines : int : total_lines : int :
count of total lines written to the file. count of total lines written to the file.
""" """
if chat_components is None: return if chat_components is None:
return
with open(self.save_path, mode='a', encoding='utf-8') as f: with open(self.save_path, mode='a', encoding='utf-8') as f:
for component in chat_components: for component in chat_components:
if component is None: continue if component is None:
continue
chatdata = component.get('chatdata') chatdata = component.get('chatdata')
if chatdata is None: continue if chatdata is None:
continue
for action in chatdata: for action in chatdata:
if action is None: continue if action is None:
continue
json_line = json.dumps(action, ensure_ascii=False) json_line = json.dumps(action, ensure_ascii=False)
f.writelines(json_line + '\n') f.writelines(json_line + '\n')
self.line_counter += 1 self.line_counter += 1
@@ -62,5 +67,3 @@ class JsonfileArchiver(ChatProcessor):
body = f'{body}({str(counter)})' body = f'{body}({str(counter)})'
newpath = os.path.join(os.path.dirname(filepath), body + extention) newpath = os.path.join(os.path.dirname(filepath), body + extention)
return newpath return newpath

View File

@@ -1,10 +1,6 @@
import json
import os
import traceback
import datetime
import time
from .chat_processor import ChatProcessor from .chat_processor import ChatProcessor
##version 2
class SimpleDisplayProcessor(ChatProcessor): class SimpleDisplayProcessor(ChatProcessor):
def process(self, chat_components: list): def process(self, chat_components: list):
@@ -17,28 +13,34 @@ class SimpleDisplayProcessor(ChatProcessor):
timeout += component.get('timeout', 0) timeout += component.get('timeout', 0)
chatdata = component.get('chatdata') chatdata = component.get('chatdata')
if chatdata is None:break if chatdata is None:
break
for action in chatdata: for action in chatdata:
if action is None:continue if action is None:
if action.get('addChatItemAction') is None:continue continue
if action['addChatItemAction'].get('item') is None:continue if action.get('addChatItemAction') is None:
continue
if action['addChatItemAction'].get('item') is None:
continue
root = action['addChatItemAction']['item'].get('liveChatTextMessageRenderer') root = action['addChatItemAction']['item'].get(
'liveChatTextMessageRenderer')
if root: if root:
author_name = root['authorName']['simpleText'] author_name = root['authorName']['simpleText']
message = self._parse_message(root.get('message')) message = self._parse_message(root.get('message'))
purchase_amount_text = '' purchase_amount_text = ''
else: else:
root = ( action['addChatItemAction']['item'].get('liveChatPaidMessageRenderer') or root = (action['addChatItemAction']['item'].get('liveChatPaidMessageRenderer')
action['addChatItemAction']['item'].get('liveChatPaidStickerRenderer') ) or action['addChatItemAction']['item'].get('liveChatPaidStickerRenderer'))
if root: if root:
author_name = root['authorName']['simpleText'] author_name = root['authorName']['simpleText']
message = self._parse_message(root.get('message')) message = self._parse_message(root.get('message'))
purchase_amount_text = root['purchaseAmountText']['simpleText'] purchase_amount_text = root['purchaseAmountText']['simpleText']
else: else:
continue continue
chatlist.append(f'[{author_name}]: {message} {purchase_amount_text}') chatlist.append(
f'[{author_name}]: {message} {purchase_amount_text}')
return {"timeout": timeout, "chatlist": chatlist} return {"timeout": timeout, "chatlist": chatlist}
def _parse_message(self, message): def _parse_message(self, message):
@@ -57,5 +59,3 @@ class SimpleDisplayProcessor(ChatProcessor):
return tmp return tmp
else: else:
return '' return ''

View File

@@ -5,6 +5,8 @@ Calculate speed of chat.
""" """
import time import time
from .. chat_processor import ChatProcessor from .. chat_processor import ChatProcessor
class RingQueue: class RingQueue:
""" """
リング型キュー リング型キュー
@@ -77,6 +79,7 @@ class RingQueue:
def item_count(self): def item_count(self):
return len(self.items) return len(self.items)
class SpeedCalculator(ChatProcessor, RingQueue): class SpeedCalculator(ChatProcessor, RingQueue):
""" """
チャットの勢いを計算する。 チャットの勢いを計算する。
@@ -106,7 +109,6 @@ class SpeedCalculator(ChatProcessor, RingQueue):
self.speed = self._calc_speed() self.speed = self._calc_speed()
return self.speed return self.speed
def _calc_speed(self): def _calc_speed(self):
""" """
RingQueue内のチャット勢い算出用データリストを元に、 RingQueue内のチャット勢い算出用データリストを元に、
@@ -120,8 +122,7 @@ class SpeedCalculator(ChatProcessor, RingQueue):
# キュー内の総チャット数 # キュー内の総チャット数
total = sum(item['chat_count'] for item in self.items) total = sum(item['chat_count'] for item in self.items)
# キュー内の最初と最後のチャットの時間差 # キュー内の最初と最後のチャットの時間差
duration = (self.items[self.last_pos]['endtime'] duration = (self.items[self.last_pos]['endtime'] - self.items[self.first_pos]['starttime'])
- self.items[self.first_pos]['starttime'])
if duration != 0: if duration != 0:
return int(total * 60 / duration) return int(total * 60 / duration)
return 0 return 0
@@ -200,4 +201,3 @@ class SpeedCalculator(ChatProcessor, RingQueue):
'starttime': int(starttime / 1000000), 'starttime': int(starttime / 1000000),
'endtime': int(endtime / 1000000) 'endtime': int(endtime / 1000000)
}) })

View File

@@ -15,10 +15,12 @@ items_sticker = [
'liveChatPaidStickerRenderer' 'liveChatPaidStickerRenderer'
] ]
class SuperchatCalculator(ChatProcessor): class SuperchatCalculator(ChatProcessor):
""" """
Calculate the amount of SuperChat by currency. Calculate the amount of SuperChat by currency.
""" """
def __init__(self): def __init__(self):
self.results = {} self.results = {}
@@ -34,11 +36,13 @@ class SuperchatCalculator(ChatProcessor):
return self.results return self.results
for component in chat_components: for component in chat_components:
chatdata = component.get('chatdata') chatdata = component.get('chatdata')
if chatdata is None: continue if chatdata is None:
continue
for action in chatdata: for action in chatdata:
renderer = self._get_item(action, items_paid) or \ renderer = self._get_item(action, items_paid) or \
self._get_item(action, items_sticker) self._get_item(action, items_sticker)
if renderer is None: continue if renderer is None:
continue
symbol, amount = self._parse(renderer) symbol, amount = self._parse(renderer)
self.results.setdefault(symbol, 0) self.results.setdefault(symbol, 0)
self.results[symbol] += amount self.results[symbol] += amount
@@ -69,6 +73,3 @@ class SuperchatCalculator(ChatProcessor):
continue continue
return None return None
return dict_body return dict_body

View File

@@ -5,8 +5,9 @@ from .chat_processor import ChatProcessor
from .default.processor import DefaultProcessor from .default.processor import DefaultProcessor
PATTERN = re.compile(r"(.*)\(([0-9]+)\)$") PATTERN = re.compile(r"(.*)\(([0-9]+)\)$")
fmt_headers = ['datetime','elapsed','authorName','message','superchatAmount' fmt_headers = ['datetime', 'elapsed', 'authorName', 'message',
,'authorType','authorChannel'] 'superchatAmount', 'authorType', 'authorChannel']
class TSVArchiver(ChatProcessor): class TSVArchiver(ChatProcessor):
''' '''
@@ -64,7 +65,3 @@ class TSVArchiver(ChatProcessor):
c.author.type, c.author.type,
c.author.channelId c.author.channelId
]) ])

View File

@@ -7,12 +7,16 @@ from . worker import ExtractWorker
from . patch import Patch from . patch import Patch
from ... import config from ... import config
from ... paramgen import arcparam from ... paramgen import arcparam
from ... exceptions import UnknownConnectionError
from concurrent.futures import CancelledError from concurrent.futures import CancelledError
from json import JSONDecodeError
from urllib.parse import quote from urllib.parse import quote
headers = config.headers headers = config.headers
REPLAY_URL = "https://www.youtube.com/live_chat_replay/" \ REPLAY_URL = "https://www.youtube.com/live_chat_replay/" \
"get_live_chat_replay?continuation=" "get_live_chat_replay?continuation="
MAX_RETRY_COUNT = 3
def _split(start, end, count, min_interval_sec=120): def _split(start, end, count, min_interval_sec=120):
""" """
@@ -25,7 +29,6 @@ def _split(start, end, count, min_interval_sec = 120):
-------- --------
List of the offset of each block's first chat data. List of the offset of each block's first chat data.
""" """
if not (isinstance(start, int) or isinstance(start, float)) or \ if not (isinstance(start, int) or isinstance(start, float)) or \
not (isinstance(end, int) or isinstance(end, float)): not (isinstance(end, int) or isinstance(end, float)):
raise ValueError("start/end must be int or float") raise ValueError("start/end must be int or float")
@@ -37,7 +40,8 @@ def _split(start, end, count, min_interval_sec = 120):
raise ValueError("count must be equal to or greater than 1.") raise ValueError("count must be equal to or greater than 1.")
if (end - start) / count < min_interval_sec: if (end - start) / count < min_interval_sec:
count = int((end - start) / min_interval_sec) count = int((end - start) / min_interval_sec)
if count == 0 : count = 1 if count == 0:
count = 1
interval = (end - start) / count interval = (end - start) / count
if count == 1: if count == 1:
@@ -45,8 +49,10 @@ def _split(start, end, count, min_interval_sec = 120):
return sorted(list(set([int(start + interval * j) return sorted(list(set([int(start + interval * j)
for j in range(count)]))) for j in range(count)])))
def ready_blocks(video_id, duration, div, callback): def ready_blocks(video_id, duration, div, callback):
if div <= 0: raise ValueError if div <= 0:
raise ValueError
async def _get_blocks(video_id, duration, div, callback): async def _get_blocks(video_id, duration, div, callback):
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
@@ -57,9 +63,18 @@ def ready_blocks(video_id, duration, div, callback):
async def _create_block(session, video_id, seektime, callback): async def _create_block(session, video_id, seektime, callback):
continuation = arcparam.getparam(video_id, seektime=seektime) continuation = arcparam.getparam(video_id, seektime=seektime)
url = f"{REPLAY_URL}{quote(continuation)}&pbj=1" url = f"{REPLAY_URL}{quote(continuation)}&pbj=1"
for _ in range(MAX_RETRY_COUNT):
try:
async with session.get(url, headers=headers) as resp: async with session.get(url, headers=headers) as resp:
text = await resp.text() text = await resp.text()
next_continuation, actions = parser.parse(json.loads(text)) next_continuation, actions = parser.parse(json.loads(text))
break
except JSONDecodeError:
await asyncio.sleep(3)
else:
cancel()
raise UnknownConnectionError("Abort: Unknown connection error.")
if actions: if actions:
first = parser.get_offset(actions[0]) first = parser.get_offset(actions[0])
last = parser.get_offset(actions[-1]) last = parser.get_offset(actions[-1])
@@ -71,6 +86,7 @@ def ready_blocks(video_id, duration, div, callback):
first=first, first=first,
last=last last=last
) )
""" """
fetch initial blocks. fetch initial blocks.
""" """
@@ -79,6 +95,7 @@ def ready_blocks(video_id, duration, div, callback):
_get_blocks(video_id, duration, div, callback)) _get_blocks(video_id, duration, div, callback))
return blocks return blocks
def fetch_patch(callback, blocks, video_id): def fetch_patch(callback, blocks, video_id):
async def _allocate_workers(): async def _allocate_workers():
@@ -95,9 +112,18 @@ def fetch_patch(callback, blocks, video_id):
async def _fetch(continuation, session) -> Patch: async def _fetch(continuation, session) -> Patch:
url = f"{REPLAY_URL}{quote(continuation)}&pbj=1" url = f"{REPLAY_URL}{quote(continuation)}&pbj=1"
for _ in range(MAX_RETRY_COUNT):
try:
async with session.get(url, headers=config.headers) as resp: async with session.get(url, headers=config.headers) as resp:
chat_json = await resp.text() chat_json = await resp.text()
continuation, actions = parser.parse(json.loads(chat_json)) continuation, actions = parser.parse(json.loads(chat_json))
break
except JSONDecodeError:
await asyncio.sleep(3)
else:
cancel()
raise UnknownConnectionError("Abort: Unknown connection error.")
if actions: if actions:
last = parser.get_offset(actions[-1]) last = parser.get_offset(actions[-1])
first = parser.get_offset(actions[0]) first = parser.get_offset(actions[0])
@@ -105,6 +131,7 @@ def fetch_patch(callback, blocks, video_id):
callback(actions, last - first) callback(actions, last - first)
return Patch(actions, continuation, first, last) return Patch(actions, continuation, first, last)
return Patch(continuation=continuation) return Patch(continuation=continuation)
""" """
allocate workers and assign blocks. allocate workers and assign blocks.
""" """
@@ -114,6 +141,7 @@ def fetch_patch(callback, blocks, video_id):
except CancelledError: except CancelledError:
pass pass
async def _shutdown(): async def _shutdown():
print("\nshutdown...") print("\nshutdown...")
tasks = [t for t in asyncio.all_tasks() tasks = [t for t in asyncio.all_tasks()
@@ -125,7 +153,7 @@ async def _shutdown():
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
def cancel(): def cancel():
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
loop.create_task(_shutdown()) loop.create_task(_shutdown())

View File

@@ -1,4 +1,3 @@
from . import parser
class Block: class Block:
"""Block object represents something like a box """Block object represents something like a box
to join chunk of chatdata. to join chunk of chatdata.

View File

@@ -1,5 +1,6 @@
from . import parser from . import parser
def check_duplicate(chatdata): def check_duplicate(chatdata):
max_range = len(chatdata) - 1 max_range = len(chatdata) - 1
tbl_offset = [None] * max_range tbl_offset = [None] * max_range
@@ -15,10 +16,8 @@ def check_duplicate(chatdata):
def is_duplicate(i, j): def is_duplicate(i, j):
return ( return (
tbl_offset[i] == tbl_offset[j] tbl_offset[i] == tbl_offset[j]
and and tbl_id[i] == tbl_id[j]
tbl_id[i] == tbl_id[j] and tbl_type[i] == tbl_type[j]
and
tbl_type[i] == tbl_type[j]
) )
print("creating table...") print("creating table...")
create_table(chatdata, max_range) create_table(chatdata, max_range)
@@ -53,8 +52,7 @@ def check_duplicate_offset(chatdata):
def is_duplicate(i, j): def is_duplicate(i, j):
return ( return (
tbl_offset[i] == tbl_offset[j] tbl_offset[i] == tbl_offset[j]
and and tbl_id[i] == tbl_id[j]
tbl_id[i] == tbl_id[j]
) )
print("creating table...") print("creating table...")
@@ -69,8 +67,10 @@ def check_duplicate_offset(chatdata):
for i in range(max_range - 1) for i in range(max_range - 1)
if is_duplicate(i, i + 1)] if is_duplicate(i, i + 1)]
def remove_duplicate_head(blocks): def remove_duplicate_head(blocks):
if len(blocks) == 1 : return blocks if len(blocks) == 0 or len(blocks) == 1:
return blocks
def is_duplicate_head(index): def is_duplicate_head(index):
@@ -85,19 +85,19 @@ def remove_duplicate_head(blocks):
type_1 = parser.get_type(blocks[index + 1].chat_data[0]) type_1 = parser.get_type(blocks[index + 1].chat_data[0])
return ( return (
blocks[index].first == blocks[index + 1].first blocks[index].first == blocks[index + 1].first
and and id_0 == id_1
id_0 == id_1 and type_0 == type_1
and
type_0 == type_1
) )
ret = [blocks[i] for i in range(len(blocks) - 1) ret = [blocks[i] for i in range(len(blocks) - 1)
if (len(blocks[i].chat_data)>0 and if (len(blocks[i].chat_data) > 0
not is_duplicate_head(i) )] and not is_duplicate_head(i))]
ret.append(blocks[-1]) ret.append(blocks[-1])
return ret return ret
def remove_duplicate_tail(blocks): def remove_duplicate_tail(blocks):
if len(blocks) == 1 : return blocks if len(blocks) == 0 or len(blocks) == 1:
return blocks
def is_duplicate_tail(index): def is_duplicate_tail(index):
if len(blocks[index].chat_data) == 0: if len(blocks[index].chat_data) == 0:
@@ -110,23 +110,23 @@ def remove_duplicate_tail(blocks):
type_1 = parser.get_type(blocks[index].chat_data[-1]) type_1 = parser.get_type(blocks[index].chat_data[-1])
return ( return (
blocks[index - 1].last == blocks[index].last blocks[index - 1].last == blocks[index].last
and and id_0 == id_1
id_0 == id_1 and type_0 == type_1
and
type_0 == type_1
) )
ret = [blocks[i] for i in range(0, len(blocks)) ret = [blocks[i] for i in range(0, len(blocks))
if i == 0 or not is_duplicate_tail(i)] if i == 0 or not is_duplicate_tail(i)]
return ret return ret
def remove_overlap(blocks): def remove_overlap(blocks):
""" """
Fix overlapped blocks after ready_blocks(). Fix overlapped blocks after ready_blocks().
Align the last offset of each block to the first offset Align the last offset of each block to the first offset
of next block (equals `end` offset of each block). of next block (equals `end` offset of each block).
""" """
if len(blocks) == 1 : return blocks if len(blocks) == 0 or len(blocks) == 1:
return blocks
for block in blocks: for block in blocks:
if block.is_last: if block.is_last:
@@ -146,8 +146,8 @@ def remove_overlap(blocks):
return blocks return blocks
def _dump(blocks): def _dump(blocks):
print(f"---------- first last end---") print("---------- first last end---")
for i, block in enumerate(blocks): for i, block in enumerate(blocks):
print(f"block[{i:3}] {block.first:>10} {block.last:>10} {block.end:>10}") print(
f"block[{i:3}] {block.first:>10} {block.last:>10} {block.end:>10}")

View File

@@ -1,6 +1,5 @@
from . import asyncdl from . import asyncdl
from . import duplcheck from . import duplcheck
from . import parser
from .. videoinfo import VideoInfo from .. videoinfo import VideoInfo
from ... import config from ... import config
from ... exceptions import InvalidVideoIdException from ... exceptions import InvalidVideoIdException
@@ -8,6 +7,7 @@ from ... exceptions import InvalidVideoIdException
logger = config.logger(__name__) logger = config.logger(__name__)
headers = config.headers headers = config.headers
class Extractor: class Extractor:
def __init__(self, video_id, div=1, callback=None, processor=None): def __init__(self, video_id, div=1, callback=None, processor=None):
if not isinstance(div, int) or div < 1: if not isinstance(div, int) or div < 1:
@@ -40,6 +40,7 @@ class Extractor:
return self return self
def _set_block_end(self): def _set_block_end(self):
if len(self.blocks) > 0:
for i in range(len(self.blocks) - 1): for i in range(len(self.blocks) - 1):
self.blocks[i].end = self.blocks[i + 1].first self.blocks[i].end = self.blocks[i + 1].first
self.blocks[-1].end = self.duration * 1000 self.blocks[-1].end = self.duration * 1000
@@ -82,10 +83,13 @@ class Extractor:
data = self._execute_extract_operations() data = self._execute_extract_operations()
if self.processor is None: if self.processor is None:
return data return data
return self.processor.process( ret = self.processor.process(
[{'video_id':None,'timeout':1,'chatdata' : (action [{'video_id': None,
["replayChatItemAction"]["actions"][0] for action in data)}] 'timeout': 1,
'chatdata': (action["replayChatItemAction"]["actions"][0] for action in data)}]
) )
self.processor.finalize()
return ret
def cancel(self): def cancel(self):
asyncdl.cancel() asyncdl.cancel()

View File

@@ -1,12 +1,9 @@
import json
from ... import config from ... import config
from ... exceptions import ( from ... import exceptions
ResponseContextError,
NoContentsException,
NoContinuationsException )
logger = config.logger(__name__) logger = config.logger(__name__)
def parse(jsn): def parse(jsn):
""" """
Parse replay chat data. Parse replay chat data.
@@ -23,15 +20,15 @@ def parse(jsn):
if jsn is None: if jsn is None:
raise ValueError("parameter JSON is None") raise ValueError("parameter JSON is None")
if jsn['response']['responseContext'].get('errors'): if jsn['response']['responseContext'].get('errors'):
raise ResponseContextError( raise exceptions.ResponseContextError(
'video_id is invalid or private/deleted.') 'video_id is invalid or private/deleted.')
contents = jsn['response'].get('continuationContents') contents = jsn['response'].get('continuationContents')
if contents is None: if contents is None:
raise NoContentsException('No chat data.') raise exceptions.NoContents('No chat data.')
cont = contents['liveChatContinuation']['continuations'][0] cont = contents['liveChatContinuation']['continuations'][0]
if cont is None: if cont is None:
raise NoContinuationsException('No Continuation') raise exceptions.NoContinuation('No Continuation')
metadata = cont.get('liveChatReplayContinuationData') metadata = cont.get('liveChatReplayContinuationData')
if metadata: if metadata:
continuation = metadata.get("continuation") continuation = metadata.get("continuation")
@@ -43,12 +40,12 @@ def parse(jsn):
def get_offset(item): def get_offset(item):
return int(item['replayChatItemAction']["videoOffsetTimeMsec"]) return int(item['replayChatItemAction']["videoOffsetTimeMsec"])
def get_id(item): def get_id(item):
return list((list(item['replayChatItemAction']["actions"][0].values() return list((list(item['replayChatItemAction']["actions"][0].values()
)[0])['item'].values())[0].get('id') )[0])['item'].values())[0].get('id')
def get_type(item): def get_type(item):
return list((list(item['replayChatItemAction']["actions"][0].values() return list((list(item['replayChatItemAction']["actions"][0].values()
)[0])['item'].keys())[0] )[0])['item'].keys())[0]

View File

@@ -2,6 +2,7 @@ from . import parser
from . block import Block from . block import Block
from typing import NamedTuple from typing import NamedTuple
class Patch(NamedTuple): class Patch(NamedTuple):
""" """
Patch represents chunk of chat data Patch represents chunk of chat data
@@ -12,6 +13,7 @@ class Patch(NamedTuple):
first: int = None first: int = None
last: int = None last: int = None
def fill(block: Block, patch: Patch): def fill(block: Block, patch: Patch):
block_end = block.end block_end = block.end
if patch.last < block_end or block.is_last: if patch.last < block_end or block.is_last:
@@ -51,4 +53,3 @@ def set_patch(block:Block, patch:Patch):
block.chat_data.extend(patch.chats) block.chat_data.extend(patch.chats)
block.last = patch.last block.last = patch.last
block.remaining = block.end - block.last block.remaining = block.end - block.last

View File

@@ -1,8 +1,8 @@
from . import parser
from . block import Block from . block import Block
from . patch import Patch, fill, split from . patch import fill, split
from ... paramgen import arcparam from ... paramgen import arcparam
class ExtractWorker: class ExtractWorker:
""" """
ExtractWorker associates a download session with a block. ExtractWorker associates a download session with a block.
@@ -54,6 +54,7 @@ class ExtractWorker:
self.block.done = True self.block.done = True
self.block = _search_new_block(self) self.block = _search_new_block(self)
def _search_new_block(worker) -> Block: def _search_new_block(worker) -> Block:
index, undone_block = _get_undone_block(worker.blocks) index, undone_block = _get_undone_block(worker.blocks)
if undone_block is None: if undone_block is None:
@@ -74,6 +75,7 @@ def _search_new_block(worker) -> Block:
worker.blocks.insert(index + 1, new_block) worker.blocks.insert(index + 1, new_block)
return new_block return new_block
def _get_undone_block(blocks) -> (int, Block): def _get_undone_block(blocks) -> (int, Block):
min_interval_ms = 120000 min_interval_ms = 120000
max_remaining = 0 max_remaining = 0

View File

@@ -1,12 +1,12 @@
import json import re
from ... import config from ... import config
from ... exceptions import ( from ... exceptions import (
ResponseContextError, ResponseContextError,
NoContentsException, NoContents, NoContinuation)
NoContinuationsException )
logger = config.logger(__name__) logger = config.logger(__name__)
def parse(jsn): def parse(jsn):
""" """
Parse replay chat data. Parse replay chat data.
@@ -27,11 +27,11 @@ def parse(jsn):
'video_id is invalid or private/deleted.') 'video_id is invalid or private/deleted.')
contents = jsn["response"].get('continuationContents') contents = jsn["response"].get('continuationContents')
if contents is None: if contents is None:
raise NoContentsException('No chat data.') raise NoContents('No chat data.')
cont = contents['liveChatContinuation']['continuations'][0] cont = contents['liveChatContinuation']['continuations'][0]
if cont is None: if cont is None:
raise NoContinuationsException('No Continuation') raise NoContinuation('No Continuation')
metadata = cont.get('liveChatReplayContinuationData') metadata = cont.get('liveChatReplayContinuationData')
if metadata: if metadata:
continuation = metadata.get("continuation") continuation = metadata.get("continuation")
@@ -50,15 +50,21 @@ def parse(jsn):
def get_offset(item): def get_offset(item):
return int(item['replayChatItemAction']["videoOffsetTimeMsec"]) return int(item['replayChatItemAction']["videoOffsetTimeMsec"])
def get_id(item): def get_id(item):
return list((list(item['replayChatItemAction']["actions"][0].values() return list((list(item['replayChatItemAction']["actions"][0].values()
)[0])['item'].values())[0].get('id') )[0])['item'].values())[0].get('id')
def get_type(item): def get_type(item):
return list((list(item['replayChatItemAction']["actions"][0].values() return list((list(item['replayChatItemAction']["actions"][0].values()
)[0])['item'].keys())[0] )[0])['item'].keys())[0]
import re
_REGEX_YTINIT = re.compile("window\\[\"ytInitialData\"\\]\\s*=\\s*({.+?});\\s+")
_REGEX_YTINIT = re.compile(
"window\\[\"ytInitialData\"\\]\\s*=\\s*({.+?});\\s+")
def extract(text): def extract(text):
match = re.findall(_REGEX_YTINIT, str(text)) match = re.findall(_REGEX_YTINIT, str(text))

View File

@@ -2,7 +2,6 @@ import json
import re import re
import requests import requests
from .. import config from .. import config
from .. import util
from ..exceptions import InvalidVideoIdException from ..exceptions import InvalidVideoIdException
headers = config.headers headers = config.headers
@@ -63,6 +62,7 @@ item_moving_thumbnail = [
"url" "url"
] ]
class VideoInfo: class VideoInfo:
''' '''
VideoInfo object retrieves YouTube video information. VideoInfo object retrieves YouTube video information.
@@ -76,6 +76,7 @@ class VideoInfo:
InvalidVideoIdException : InvalidVideoIdException :
Occurs when video_id does not exist on YouTube. Occurs when video_id does not exist on YouTube.
''' '''
def __init__(self, video_id): def __init__(self, video_id):
self.video_id = video_id self.video_id = video_id
text = self._get_page_text(video_id) text = self._get_page_text(video_id)

View File

@@ -1,6 +1,9 @@
import requests,json,datetime import requests
import json
import datetime
from .. import config from .. import config
def extract(url): def extract(url):
_session = requests.Session() _session = requests.Session()
html = _session.get(url, headers=config.headers) html = _session.get(url, headers=config.headers)
@@ -10,6 +13,6 @@ def extract(url):
def save(data, filename, extention): def save(data, filename, extention):
with open(filename+"_"+(datetime.datetime.now().strftime('%Y-%m-%d %H-%M-%S') with open(filename + "_" + (datetime.datetime.now().strftime('%Y-%m-%d %H-%M-%S')) + extention,
)+extention,mode ='w',encoding='utf-8') as f: mode='w', encoding='utf-8') as f:
f.writelines(data) f.writelines(data)

View File

@@ -1,5 +1,5 @@
aiohttp aiohttp
argumentparser protobuf
pytz pytz
requests requests
urllib3 urllib3

View File

@@ -1,19 +1,22 @@
import pytest import json
from pytchat.parser.live import Parser import requests
import pytchat.config as config import pytchat.config as config
import requests, json
from pytchat.paramgen import arcparam from pytchat.paramgen import arcparam
from pytchat.parser.live import Parser
def test_arcparam_0(mocker): def test_arcparam_0(mocker):
param = arcparam.getparam("01234567890", -1) param = arcparam.getparam("01234567890", -1)
assert param == "op2w0wRyGjxDZzhhRFFvTE1ERXlNelExTmpjNE9UQWFFLXFvM2JrQkRRb0xNREV5TXpRMU5qYzRPVEFnQVElM0QlM0QoADAAOABAAEgEUhwIABAAGAAgACoOc3RhdGljY2hlY2tzdW1AAFgDYAFoAHIECAEQAHgA" assert param == "op2w0wQmGhxDZzhLRFFvTE1ERXlNelExTmpjNE9UQWdBUT09SARgAXICCAE%3D"
def test_arcparam_1(mocker): def test_arcparam_1(mocker):
param = arcparam.getparam("01234567890", seektime=100000) param = arcparam.getparam("01234567890", seektime=100000)
assert param == "op2w0wR3GjxDZzhhRFFvTE1ERXlNelExTmpjNE9UQWFFLXFvM2JrQkRRb0xNREV5TXpRMU5qYzRPVEFnQVElM0QlM0QogNDbw_QCMAA4AEAASANSHAgAEAAYACAAKg5zdGF0aWNjaGVja3N1bUAAWANgAWgAcgQIARAAeAA%3D" assert param == "op2w0wQtGhxDZzhLRFFvTE1ERXlNelExTmpjNE9UQWdBUT09KIDQ28P0AkgDYAFyAggB"
def test_arcparam_2(mocker): def test_arcparam_2(mocker):
param = arcparam.getparam("SsjCnHOk-Sk") param = arcparam.getparam("SsjCnHOk-Sk", seektime=100)
url = f"https://www.youtube.com/live_chat_replay/get_live_chat_replay?continuation={param}&pbj=1" url = f"https://www.youtube.com/live_chat_replay/get_live_chat_replay?continuation={param}&pbj=1"
resp = requests.Session().get(url, headers=config.headers) resp = requests.Session().get(url, headers=config.headers)
jsn = json.loads(resp.text) jsn = json.loads(resp.text)
@@ -25,4 +28,4 @@ def test_arcparam_2(mocker):
def test_arcparam_3(mocker): def test_arcparam_3(mocker):
param = arcparam.getparam("01234567890") param = arcparam.getparam("01234567890")
assert param == "op2w0wRyGjxDZzhhRFFvTE1ERXlNelExTmpjNE9UQWFFLXFvM2JrQkRRb0xNREV5TXpRMU5qYzRPVEFnQVElM0QlM0QoATAAOABAAEgDUhwIABAAGAAgACoOc3RhdGljY2hlY2tzdW1AAFgDYAFoAHIECAEQAHgA" assert param == "op2w0wQmGhxDZzhLRFFvTE1ERXlNelExTmpjNE9UQWdBUT09SARgAXICCAE%3D"

View File

@@ -1,9 +1,10 @@
import pytest
from pytchat.tool.mining import parser from pytchat.tool.mining import parser
import pytchat.config as config import pytchat.config as config
import requests, json import requests
import json
from pytchat.paramgen import arcparam_mining as arcparam from pytchat.paramgen import arcparam_mining as arcparam
def test_arcparam_e(mocker): def test_arcparam_e(mocker):
try: try:
arcparam.getparam("01234567890", -1) arcparam.getparam("01234567890", -1)
@@ -12,8 +13,6 @@ def test_arcparam_e(mocker):
assert True assert True
def test_arcparam_0(mocker): def test_arcparam_0(mocker):
param = arcparam.getparam("01234567890", 0) param = arcparam.getparam("01234567890", 0)
@@ -25,6 +24,7 @@ def test_arcparam_1(mocker):
print(param) print(param)
assert param == "op2w0wQzGiBDZzhhRFFvTE1ERXlNelExTmpjNE9UQWdBUSUzRCUzREABWgUQgMLXL2AEcgIIAXgB" assert param == "op2w0wQzGiBDZzhhRFFvTE1ERXlNelExTmpjNE9UQWdBUSUzRCUzREABWgUQgMLXL2AEcgIIAXgB"
def test_arcparam_2(mocker): def test_arcparam_2(mocker):
param = arcparam.getparam("PZz9NB0-Z64", 1) param = arcparam.getparam("PZz9NB0-Z64", 1)
url = f"https://www.youtube.com/live_chat_replay?continuation={param}&playerOffsetMs=1000&pbj=1" url = f"https://www.youtube.com/live_chat_replay?continuation={param}&playerOffsetMs=1000&pbj=1"
@@ -35,6 +35,7 @@ def test_arcparam_2(mocker):
print(test_id) print(test_id)
assert test_id == "ChwKGkNKSGE0YnFJeWVBQ0ZWcUF3Z0VkdGIwRm9R" assert test_id == "ChwKGkNKSGE0YnFJeWVBQ0ZWcUF3Z0VkdGIwRm9R"
def test_arcparam_3(mocker): def test_arcparam_3(mocker):
param = arcparam.getparam("01234567890") param = arcparam.getparam("01234567890")
assert param == "op2w0wQsGiBDZzhhRFFvTE1ERXlNelExTmpjNE9UQWdBUSUzRCUzREABYARyAggBeAE%3D" assert param == "op2w0wQsGiBDZzhhRFFvTE1ERXlNelExTmpjNE9UQWdBUSUzRCUzREABYARyAggBeAE%3D"

View File

@@ -36,7 +36,7 @@ def test_process_0():
chat_component = { chat_component = {
'video_id':'', 'video_id':'',
'timeout':10, 'timeout':10,
'chatdata':load_chatdata(r"tests\testdata\calculator\superchat_0.json") 'chatdata':load_chatdata(r"tests/testdata/calculator/superchat_0.json")
} }
assert SuperchatCalculator().process([chat_component])=={'': 6800.0, '': 2.0} assert SuperchatCalculator().process([chat_component])=={'': 6800.0, '': 2.0}
@@ -47,7 +47,7 @@ def test_process_1():
chat_component = { chat_component = {
'video_id':'', 'video_id':'',
'timeout':10, 'timeout':10,
'chatdata':load_chatdata(r"tests\testdata\calculator\text_only.json") 'chatdata':load_chatdata(r"tests/testdata/calculator/text_only.json")
} }
assert SuperchatCalculator().process([chat_component])=={} assert SuperchatCalculator().process([chat_component])=={}
@@ -59,7 +59,7 @@ def test_process_2():
chat_component = { chat_component = {
'video_id':'', 'video_id':'',
'timeout':10, 'timeout':10,
'chatdata':load_chatdata(r"tests\testdata\calculator\replay_end.json") 'chatdata':load_chatdata(r"tests/testdata/calculator/replay_end.json")
} }
assert False assert False
SuperchatCalculator().process([chat_component]) SuperchatCalculator().process([chat_component])

View File

@@ -1,19 +1,10 @@
import json import json
import pytest
import asyncio,aiohttp
from pytchat.parser.live import Parser from pytchat.parser.live import Parser
from pytchat.processors.compatible.processor import CompatibleProcessor from pytchat.processors.compatible.processor import CompatibleProcessor
from pytchat.exceptions import (
NoLivechatRendererException,NoYtinitialdataException,
ResponseContextError, NoContentsException)
from pytchat.processors.compatible.renderer.textmessage import LiveChatTextMessageRenderer
from pytchat.processors.compatible.renderer.paidmessage import LiveChatPaidMessageRenderer
from pytchat.processors.compatible.renderer.paidsticker import LiveChatPaidStickerRenderer
from pytchat.processors.compatible.renderer.legacypaid import LiveChatLegacyPaidMessageRenderer
parser = Parser(is_replay=False) parser = Parser(is_replay=False)
def test_textmessage(mocker): def test_textmessage(mocker):
'''api互換processorのテスト通常テキストメッセージ''' '''api互換processorのテスト通常テキストメッセージ'''
processor = CompatibleProcessor() processor = CompatibleProcessor()
@@ -40,10 +31,12 @@ def test_textmessage(mocker):
"kind", "etag", "id", "snippet", "authorDetails" "kind", "etag", "id", "snippet", "authorDetails"
} }
assert ret["items"][0]["snippet"].keys() == { assert ret["items"][0]["snippet"].keys() == {
'type', 'liveChatId', 'authorChannelId', 'publishedAt', 'hasDisplayContent', 'displayMessage', 'textMessageDetails' 'type', 'liveChatId', 'authorChannelId', 'publishedAt', 'hasDisplayContent', 'displayMessage',
'textMessageDetails'
} }
assert ret["items"][0]["authorDetails"].keys() == { assert ret["items"][0]["authorDetails"].keys() == {
'channelId', 'channelUrl', 'displayName', 'profileImageUrl', 'isVerified', 'isChatOwner', 'isChatSponsor', 'isChatModerator' 'channelId', 'channelUrl', 'displayName', 'profileImageUrl', 'isVerified', 'isChatOwner', 'isChatSponsor',
'isChatModerator'
} }
assert ret["items"][0]["snippet"]["textMessageDetails"].keys() == { assert ret["items"][0]["snippet"]["textMessageDetails"].keys() == {
'messageText' 'messageText'
@@ -51,6 +44,7 @@ def test_textmessage(mocker):
assert "LCC." in ret["items"][0]["id"] assert "LCC." in ret["items"][0]["id"]
assert ret["items"][0]["snippet"]["type"] == "textMessageEvent" assert ret["items"][0]["snippet"]["type"] == "textMessageEvent"
def test_newsponcer(mocker): def test_newsponcer(mocker):
'''api互換processorのテストメンバ新規登録''' '''api互換processorのテストメンバ新規登録'''
processor = CompatibleProcessor() processor = CompatibleProcessor()
@@ -81,7 +75,45 @@ def test_newsponcer(mocker):
} }
assert ret["items"][0]["authorDetails"].keys() == { assert ret["items"][0]["authorDetails"].keys() == {
'channelId', 'channelUrl', 'displayName', 'profileImageUrl', 'isVerified', 'isChatOwner', 'isChatSponsor', 'isChatModerator' 'channelId', 'channelUrl', 'displayName', 'profileImageUrl', 'isVerified', 'isChatOwner', 'isChatSponsor',
'isChatModerator'
}
assert "LCC." in ret["items"][0]["id"]
assert ret["items"][0]["snippet"]["type"] == "newSponsorEvent"
def test_newsponcer_rev(mocker):
'''api互換processorのテストメンバ新規登録'''
processor = CompatibleProcessor()
_json = _open_file("tests/testdata/compatible/newSponsor_rev.json")
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
data = {
"video_id": "",
"timeout": 7,
"chatdata": chatdata
}
ret = processor.process([data])
assert ret["kind"] == "youtube#liveChatMessageListResponse"
assert ret["pollingIntervalMillis"] == data["timeout"] * 1000
assert ret.keys() == {
"kind", "etag", "pageInfo", "nextPageToken", "pollingIntervalMillis", "items"
}
assert ret["pageInfo"].keys() == {
"totalResults", "resultsPerPage"
}
assert ret["items"][0].keys() == {
"kind", "etag", "id", "snippet", "authorDetails"
}
assert ret["items"][0]["snippet"].keys() == {
'type', 'liveChatId', 'authorChannelId', 'publishedAt', 'hasDisplayContent', 'displayMessage'
}
assert ret["items"][0]["authorDetails"].keys() == {
'channelId', 'channelUrl', 'displayName', 'profileImageUrl', 'isVerified', 'isChatOwner', 'isChatSponsor',
'isChatModerator'
} }
assert "LCC." in ret["items"][0]["id"] assert "LCC." in ret["items"][0]["id"]
assert ret["items"][0]["snippet"]["type"] == "newSponsorEvent" assert ret["items"][0]["snippet"]["type"] == "newSponsorEvent"
@@ -113,10 +145,12 @@ def test_superchat(mocker):
"kind", "etag", "id", "snippet", "authorDetails" "kind", "etag", "id", "snippet", "authorDetails"
} }
assert ret["items"][0]["snippet"].keys() == { assert ret["items"][0]["snippet"].keys() == {
'type', 'liveChatId', 'authorChannelId', 'publishedAt', 'hasDisplayContent', 'displayMessage', 'superChatDetails' 'type', 'liveChatId', 'authorChannelId', 'publishedAt', 'hasDisplayContent', 'displayMessage',
'superChatDetails'
} }
assert ret["items"][0]["authorDetails"].keys() == { assert ret["items"][0]["authorDetails"].keys() == {
'channelId', 'channelUrl', 'displayName', 'profileImageUrl', 'isVerified', 'isChatOwner', 'isChatSponsor', 'isChatModerator' 'channelId', 'channelUrl', 'displayName', 'profileImageUrl', 'isVerified', 'isChatOwner', 'isChatSponsor',
'isChatModerator'
} }
assert ret["items"][0]["snippet"]["superChatDetails"].keys() == { assert ret["items"][0]["snippet"]["superChatDetails"].keys() == {
'amountMicros', 'currency', 'amountDisplayString', 'tier', 'backgroundColor' 'amountMicros', 'currency', 'amountDisplayString', 'tier', 'backgroundColor'
@@ -124,6 +158,7 @@ def test_superchat(mocker):
assert "LCC." in ret["items"][0]["id"] assert "LCC." in ret["items"][0]["id"]
assert ret["items"][0]["snippet"]["type"] == "superChatEvent" assert ret["items"][0]["snippet"]["type"] == "superChatEvent"
def test_unregistered_currency(mocker): def test_unregistered_currency(mocker):
processor = CompatibleProcessor() processor = CompatibleProcessor()

View File

@@ -1,30 +1,21 @@
import pytest
from pytchat.parser.live import Parser
import json import json
import asyncio,aiohttp
from aioresponses import aioresponses from aioresponses import aioresponses
from pytchat.core_async.livechat import LiveChatAsync from pytchat.core_async.livechat import LiveChatAsync
from pytchat.exceptions import ( from pytchat.exceptions import ResponseContextError
NoLivechatRendererException,NoYtinitialdataException,
ResponseContextError,NoContentsException)
from pytchat.core_multithread.livechat import LiveChat
import unittest
from unittest import TestCase
def _open_file(path): def _open_file(path):
with open(path, mode='r', encoding='utf-8') as f: with open(path, mode='r', encoding='utf-8') as f:
return f.read() return f.read()
@aioresponses() @aioresponses()
def test_Async(*mock): def test_Async(*mock):
vid = '' vid = ''
_text = _open_file('tests/testdata/paramgen_firstread.json') _text = _open_file('tests/testdata/paramgen_firstread.json')
_text = json.loads(_text) _text = json.loads(_text)
mock[0].get(f"https://www.youtube.com/live_chat?v={vid}&is_popout=1", status=200, body=_text) mock[0].get(
f"https://www.youtube.com/live_chat?v={vid}&is_popout=1", status=200, body=_text)
try: try:
chat = LiveChatAsync(video_id='') chat = LiveChatAsync(video_id='')
assert chat.is_alive() assert chat.is_alive()
@@ -33,6 +24,7 @@ def test_Async(*mock):
except ResponseContextError: except ResponseContextError:
assert not chat.is_alive() assert not chat.is_alive()
def test_MultiThread(mocker): def test_MultiThread(mocker):
_text = _open_file('tests/testdata/paramgen_firstread.json') _text = _open_file('tests/testdata/paramgen_firstread.json')
_text = json.loads(_text) _text = json.loads(_text)
@@ -48,6 +40,3 @@ def test_MultiThread(mocker):
except ResponseContextError: except ResponseContextError:
chat.terminate() chat.terminate()
assert not chat.is_alive() assert not chat.is_alive()

View File

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

View File

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

View File

@@ -1,18 +1,17 @@
import pytest
from pytchat.parser.live import Parser from pytchat.parser.live import Parser
import json import json
import asyncio,aiohttp
from aioresponses import aioresponses from aioresponses import aioresponses
from pytchat.exceptions import ( from pytchat.exceptions import NoContents
NoLivechatRendererException,NoYtinitialdataException,
ResponseContextError, NoContentsException)
def _open_file(path): def _open_file(path):
with open(path, mode='r', encoding='utf-8') as f: with open(path, mode='r', encoding='utf-8') as f:
return f.read() return f.read()
parser = Parser(is_replay=False) parser = Parser(is_replay=False)
@aioresponses() @aioresponses()
def test_finishedlive(*mock): def test_finishedlive(*mock):
'''配信が終了した動画を正しく処理できるか''' '''配信が終了した動画を正しく処理できるか'''
@@ -23,9 +22,10 @@ def test_finishedlive(*mock):
try: try:
parser.parse(parser.get_contents(_text)) parser.parse(parser.get_contents(_text))
assert False assert False
except NoContentsException: except NoContents:
assert True assert True
@aioresponses() @aioresponses()
def test_parsejson(*mock): def test_parsejson(*mock):
'''jsonを正常にパースできるか''' '''jsonを正常にパースできるか'''
@@ -37,8 +37,9 @@ def test_parsejson(*mock):
parser.parse(parser.get_contents(_text)) parser.parse(parser.get_contents(_text))
jsn = _text jsn = _text
timeout = jsn["response"]["continuationContents"]["liveChatContinuation"]["continuations"][0]["timedContinuationData"]["timeoutMs"] timeout = jsn["response"]["continuationContents"]["liveChatContinuation"]["continuations"][0]["timedContinuationData"]["timeoutMs"]
continuation = jsn["response"]["continuationContents"]["liveChatContinuation"]["continuations"][0]["timedContinuationData"]["continuation"] continuation = jsn["response"]["continuationContents"]["liveChatContinuation"][
assert 5035 == timeout "continuations"][0]["timedContinuationData"]["continuation"]
assert "0ofMyAPiARp8Q2c4S0RRb0xhelJMZDBsWFQwdERkalFhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5YXpSTGQwbFhUMHREZGpRbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCiPz5-Os-PkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQgJqXjrPj5AJYA1CRwciOs-PkAli3pNq1k-PkAmgBggEECAEQAIgBAKABjbfnjrPj5AI%3D" == continuation assert timeout == 5035
except: assert continuation == "0ofMyAPiARp8Q2c4S0RRb0xhelJMZDBsWFQwdERkalFhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5YXpSTGQwbFhUMHREZGpRbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCiPz5-Os-PkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQgJqXjrPj5AJYA1CRwciOs-PkAli3pNq1k-PkAmgBggEECAEQAIgBAKABjbfnjrPj5AI%3D"
except Exception:
assert False assert False

View File

@@ -1,16 +1,10 @@
import json import json
import pytest
import asyncio,aiohttp
from pytchat.parser.live import Parser 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 from pytchat.processors.speed.calculator import SpeedCalculator
parser = Parser(is_replay=False) parser = Parser(is_replay=False)
def test_speed_1(mocker): def test_speed_1(mocker):
'''test speed calculation with normal json. '''test speed calculation with normal json.
test json has 15 chatdata, duration is 30 seconds, test json has 15 chatdata, duration is 30 seconds,
@@ -30,6 +24,7 @@ def test_speed_1(mocker):
ret = processor.process([data]) ret = processor.process([data])
assert 30 == ret assert 30 == ret
def test_speed_2(mocker): def test_speed_2(mocker):
'''test speed calculation with no valid chat data. '''test speed calculation with no valid chat data.
''' '''
@@ -44,7 +39,8 @@ def test_speed_2(mocker):
"chatdata": chatdata "chatdata": chatdata
} }
ret = processor.process([data]) ret = processor.process([data])
assert 0 == ret assert ret == 0
def test_speed_3(mocker): def test_speed_3(mocker):
'''test speed calculation with empty data. '''test speed calculation with empty data.
@@ -60,7 +56,7 @@ def test_speed_3(mocker):
"chatdata": chatdata "chatdata": chatdata
} }
ret = processor.process([data]) ret = processor.process([data])
assert 0 == ret assert ret == 0
def _open_file(path): def _open_file(path):

File diff suppressed because it is too large Load Diff