Compare commits

...

47 Commits

Author SHA1 Message Date
taizan-hokuto
cd6d522055 Merge branch 'release/v0.1.0' 2020-07-24 16:27:14 +09:00
taizan-hokuto
aa8a4fb592 Increment version 2020-07-24 16:26:09 +09:00
taizan-hokuto
dbde072828 Merge branch 'hotfix/fix_exception_handling' 2020-07-24 15:20:08 +09:00
taizan-hokuto
92a01aa4d9 Merge tag 'fix_exception_handling' into develop 2020-07-24 15:20:08 +09:00
taizan-hokuto
e3f9f95fb1 Fix exception handling 2020-07-24 15:19:32 +09:00
taizan-hokuto
fa02116ab4 Merge branch 'feature/url_pattern' into develop 2020-07-24 14:52:06 +09:00
taizan-hokuto
d8656161cd Update README 2020-07-24 14:04:13 +09:00
taizan-hokuto
174d9f27c0 Add tests 2020-07-24 14:03:20 +09:00
taizan-hokuto
0abf8dd9f0 Make it possible to extract video id from url 2020-07-24 14:03:07 +09:00
taizan-hokuto
5ab653a1b2 Merge branch 'feature/extend_processor' into develop 2020-07-23 16:35:37 +09:00
taizan-hokuto
6e6bb8e019 Add tests 2020-07-23 16:20:38 +09:00
taizan-hokuto
ee4b696fc5 Add colors attribute 2020-07-23 16:20:12 +09:00
taizan-hokuto
fd1d283caa Merge branch 'hotfix/meta_tag' 2020-07-13 23:04:19 +09:00
taizan-hokuto
85966186b5 Merge tag 'meta_tag' into develop
v0.0.9.1
2020-07-13 23:04:19 +09:00
taizan-hokuto
71341d2876 Increment version 2020-07-13 23:03:46 +09:00
taizan-hokuto
8882c82f8b Fix place of meta tag 2020-07-13 23:03:20 +09:00
taizan-hokuto
cf6ed24864 Merge branch 'release/v0.0.9' 2020-07-13 01:55:13 +09:00
taizan-hokuto
584b9c5591 Merge tag 'v0.0.9' into develop
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
79 changed files with 3122 additions and 1363 deletions

View File

@@ -7,7 +7,7 @@ pytchat is a python library for fetching youtube live chat.
pytchat is a python library for fetching youtube live chat
without using youtube api, Selenium or BeautifulSoup.
pytchatはAPIを使わずにYouTubeチャットを取得するためのpythonライブラリです。
pytchatはYouTubeチャットを閲覧するためのpythonライブラリです。
Other features:
+ Customizable [chat data processors](https://github.com/taizan-hokuto/pytchat/wiki/ChatProcessor) including youtube api compatible one.
@@ -27,13 +27,12 @@ pip install pytchat
### CLI
One-liner command.
Save chat data to html.
Save chat data to html, with embedded custom emojis.
```bash
$ pytchat -v ZJ6Q4U_Vg6s -o "c:/temp/"
$ pytchat -v https://www.youtube.com/watch?v=ZJ6Q4U_Vg6s -o "c:/temp/"
# options:
# -v : video_id
# -v : Video ID or URL that includes ID
# -o : output directory (default path: './')
# saved filename is [video_id].html
```
@@ -43,7 +42,8 @@ $ pytchat -v ZJ6Q4U_Vg6s -o "c:/temp/"
```python
from pytchat import LiveChat
livechat = LiveChat(video_id = "Zvp1pJpie4I")
# It is also possible to specify a URL that includes the video ID:
# livechat = LiveChat("https://www.youtube.com/watch?v=Zvp1pJpie4I")
while livechat.is_alive():
try:
chatdata = livechat.get()
@@ -148,6 +148,20 @@ def main():
if __name__ == '__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
Each item can be got with `items` function.
@@ -175,7 +189,7 @@ Each item can be got with `items` function.
<tr>
<td>messageEx</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>
<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'
__version__ = '0.0.7.1'
__version__ = '0.1.0'
__license__ = 'MIT'
__author__ = 'taizan-hokuto'
__author_email__ = '55448286+taizan-hokuto@users.noreply.github.com'
@@ -28,3 +28,5 @@ from .api import (
SuperchatCalculator,
VideoInfo
)
# flake8: noqa

View File

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

View File

@@ -1,11 +1,8 @@
import argparse
import os
from pathlib import Path
from typing import List, Callable
from pytchat.util.extract_video_id import extract_video_id
from .arguments import Arguments
from .. exceptions import InvalidVideoIdException, NoContentsException
from .. processors.tsv_archiver import TSVArchiver
from .. exceptions import InvalidVideoIdException, NoContents
from .. processors.html_archiver import HTMLArchiver
from .. tool.extract.extractor import Extractor
from .. tool.videoinfo import VideoInfo
@@ -18,43 +15,52 @@ https://github.com/PetterKraabol/Twitch-Chat-Downloader
(MIT License)
'''
def main():
# Arguments
parser = argparse.ArgumentParser(description=f'pytchat v{__version__}')
parser.add_argument('-v', f'--{Arguments.Name.VIDEO}', type=str,
help='Video IDs separated by commas without space.\n'
'If ID starts with a hyphen (-), enclose the ID in square brackets.')
# parser.add_argument('VideoID_or_URL', type=str, default='__NONE__',nargs='?',
# help='Video ID, or URL that includes id.\n'
# 'If ID starts with a hyphen (-), enclose the ID in square brackets.')
parser.add_argument('-v', f'--{Arguments.Name.VIDEO_IDS}', type=str,
help='Video ID (or URL that includes Video ID). You can specify multiple video IDs by separating them with commas without spaces.\n'
'If ID starts with a hyphen (-), enclose the ID in square brackets.')
parser.add_argument('-o', f'--{Arguments.Name.OUTPUT}', type=str,
help='Output directory (end with "/"). default="./"', default='./')
help='Output directory (end with "/"). default="./"', default='./')
parser.add_argument(f'--{Arguments.Name.VERSION}', action='store_true',
help='Settings version')
help='Show version')
Arguments(parser.parse_args().__dict__)
if Arguments().print_version:
print(f'pytchat v{__version__}')
print(f'pytchat v{__version__} © 2019 taizan-hokuto')
return
# Extractor
if Arguments().video_ids:
for video_id in Arguments().video_ids:
if '[' in video_id:
video_id = video_id.replace('[','').replace(']','')
video_id = video_id.replace('[', '').replace(']', '')
try:
info = VideoInfo(video_id)
print(f"Extracting...\n"
f" video_id: {video_id}\n"
f" channel: {info.get_channel_name()}\n"
f" title: {info.get_title()}")
path = Path(Arguments().output+video_id+'.html')
print(f"output path: {path.resolve()}")
path = Path(Arguments().output + video_id + '.html')
print(f" output path: {path.resolve()}")
Extractor(video_id,
processor = HTMLArchiver(Arguments().output+video_id+'.html'),
callback = _disp_progress
).extract()
processor=HTMLArchiver(
Arguments().output + video_id + '.html'),
callback=_disp_progress
).extract()
print("\nExtraction end.\n")
except (InvalidVideoIdException, NoContentsException) as e:
except InvalidVideoIdException:
print("Invalid Video ID or URL:", video_id)
except (TypeError, NoContents) as e:
print(e)
return
parser.print_help()
def _disp_progress(a,b):
print('.',end="",flush=True)
def _disp_progress(a, b):
print('.', end="", flush=True)

View File

@@ -8,6 +8,7 @@ https://github.com/PetterKraabol/Twitch-Chat-Downloader
(MIT License)
'''
class Arguments(metaclass=Singleton):
"""
Arguments singleton
@@ -15,11 +16,11 @@ class Arguments(metaclass=Singleton):
class Name:
VERSION: str = 'version'
OUTPUT: str = 'output'
VIDEO: str = 'video'
OUTPUT: str = 'output_dir'
VIDEO_IDS: str = 'video_id'
def __init__(self,
arguments: Optional[Dict[str, Union[str, bool, int]]] = None):
arguments: Optional[Dict[str, Union[str, bool, int]]] = None):
"""
Initialize arguments
:param arguments: Arguments from cli
@@ -34,6 +35,9 @@ class Arguments(metaclass=Singleton):
self.output: str = arguments[Arguments.Name.OUTPUT]
self.video_ids: List[int] = []
# Videos
if arguments[Arguments.Name.VIDEO]:
if arguments[Arguments.Name.VIDEO_IDS]:
self.video_ids = [video_id
for video_id in arguments[Arguments.Name.VIDEO].split(',')]
for video_id in arguments[Arguments.Name.VIDEO_IDS].split(',')]

View File

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

View File

@@ -1,11 +1,9 @@
import logging
from . import mylogger
headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36'}
'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):
module_logger = mylogger.get_logger(module_name, loglevel = loglevel)
def logger(module_name: str, loglevel=None):
module_logger = mylogger.get_logger(module_name, loglevel=loglevel)
return module_logger

View File

@@ -1,31 +1,31 @@
from logging import NullHandler, getLogger, StreamHandler, FileHandler, Formatter
from logging import NullHandler, getLogger, StreamHandler, FileHandler
import logging
from datetime import datetime
def get_logger(modname,loglevel=logging.DEBUG):
def get_logger(modname, loglevel=logging.DEBUG):
logger = getLogger(modname)
if loglevel == None:
if loglevel is None:
logger.addHandler(NullHandler())
return logger
logger.setLevel(loglevel)
#create handler1 for showing info
# create handler1 for showing info
handler1 = StreamHandler()
my_formatter = MyFormatter()
my_formatter = MyFormatter()
handler1.setFormatter(my_formatter)
handler1.setLevel(loglevel)
logger.addHandler(handler1)
#create handler2 for recording log file
# create handler2 for recording log file
if loglevel <= logging.DEBUG:
handler2 = FileHandler(filename="log.txt", encoding='utf-8')
handler2.setLevel(logging.ERROR)
handler2.setFormatter(my_formatter)
logger.addHandler(handler2)
return logger
class MyFormatter(logging.Formatter):
def format(self, record):
timestamp = (
@@ -35,4 +35,4 @@ class MyFormatter(logging.Formatter):
lineno = str(record.lineno).rjust(4)
message = record.getMessage()
return timestamp+'| '+module+' { '+funcname+':'+lineno+'} - '+message
return timestamp + '| ' + module + ' { ' + funcname + ':' + lineno + '} - ' + message

View File

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

View File

@@ -1,7 +1,6 @@
import aiohttp, asyncio
import datetime
import aiohttp
import asyncio
import json
import random
import signal
import time
import traceback
@@ -12,10 +11,11 @@ from asyncio import Queue
from .buffer import Buffer
from ..parser.live import Parser
from .. import config
from ..exceptions import ChatParseException,IllegalFunctionCall
from ..paramgen import liveparam, arcparam
from .. import exceptions
from ..paramgen import liveparam, arcparam
from ..processors.default.processor import DefaultProcessor
from ..processors.combinator import Combinator
from ..util.extract_video_id import extract_video_id
headers = config.headers
MAX_RETRY = 10
@@ -75,19 +75,19 @@ class LiveChatAsync:
_setup_finished = False
def __init__(self, video_id,
seektime = 0,
processor = DefaultProcessor(),
buffer = None,
interruptable = True,
callback = None,
done_callback = None,
exception_handler = None,
direct_mode = False,
force_replay = False,
topchat_only = False,
logger = config.logger(__name__),
):
self.video_id = video_id
seektime=-1,
processor=DefaultProcessor(),
buffer=None,
interruptable=True,
callback=None,
done_callback=None,
exception_handler=None,
direct_mode=False,
force_replay=False,
topchat_only=False,
logger=config.logger(__name__),
):
self._video_id = extract_video_id(video_id)
self.seektime = seektime
if isinstance(processor, tuple):
self.processor = Combinator(processor)
@@ -98,59 +98,57 @@ class LiveChatAsync:
self._done_callback = done_callback
self._exception_handler = exception_handler
self._direct_mode = direct_mode
self._is_alive = True
self._is_alive = True
self._is_replay = force_replay
self._parser = Parser(is_replay = self._is_replay)
self._parser = Parser(is_replay=self._is_replay)
self._pauser = Queue()
self._pauser.put_nowait(None)
self._setup()
self._first_fetch = True
self._fetch_url = "live_chat/get_live_chat?continuation="
self._topchat_only = topchat_only
self._logger = logger
self.exception = None
LiveChatAsync._logger = logger
if not LiveChatAsync._setup_finished:
LiveChatAsync._setup_finished = True
if exception_handler:
self._set_exception_handler(exception_handler)
if interruptable:
signal.signal(signal.SIGINT,
(lambda a, b:asyncio.create_task(
LiveChatAsync.shutdown(None,signal.SIGINT,b))
))
if exception_handler:
self._set_exception_handler(exception_handler)
if interruptable:
signal.signal(signal.SIGINT,
(lambda a, b: asyncio.create_task(
LiveChatAsync.shutdown(None, signal.SIGINT, b))))
self._setup()
def _setup(self):
#direct modeがTrueでcallback未設定の場合例外発生。
# direct modeがTrueでcallback未設定の場合例外発生。
if self._direct_mode:
if self._callback is None:
raise IllegalFunctionCall(
raise exceptions.IllegalFunctionCall(
"When direct_mode=True, callback parameter is required.")
else:
#direct modeがFalseでbufferが未設定ならばデフォルトのbufferを作成
# direct modeがFalseでbufferが未設定ならばデフォルトのbufferを作成
if self._buffer is None:
self._buffer = Buffer(maxsize = 20)
#callbackが指定されている場合はcallbackを呼ぶループタスクを作成
self._buffer = Buffer(maxsize=20)
# callbackが指定されている場合はcallbackを呼ぶループタスクを作成
if self._callback is None:
pass
else:
#callbackを呼ぶループタスクの開始
# callbackを呼ぶループタスクの開始
loop = asyncio.get_event_loop()
loop.create_task(self._callback_loop(self._callback))
#_listenループタスクの開始
# _listenループタスクの開始
loop = asyncio.get_event_loop()
listen_task = loop.create_task(self._startlisten())
#add_done_callbackの登録
self.listen_task = loop.create_task(self._startlisten())
# add_done_callbackの登録
if self._done_callback is None:
listen_task.add_done_callback(self.finish)
self.listen_task.add_done_callback(self._finish)
else:
listen_task.add_done_callback(self._done_callback)
self.listen_task.add_done_callback(self._done_callback)
async def _startlisten(self):
"""Fetch first continuation parameter,
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)
async def _listen(self, continuation):
@@ -168,34 +166,36 @@ class LiveChatAsync:
continuation = await self._check_pause(continuation)
contents = await self._get_contents(
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 = {
"video_id" : self.video_id,
"timeout" : timeout,
"chatdata" : chatdata
"video_id": self._video_id,
"timeout": timeout,
"chatdata": chatdata
}
time_mark =time.time()
time_mark = time.time()
if self._direct_mode:
processed_chat = self.processor.process([chat_component])
if isinstance(processed_chat,tuple):
processed_chat = self.processor.process(
[chat_component])
if isinstance(processed_chat, tuple):
await self._callback(*processed_chat)
else:
await self._callback(processed_chat)
else:
await self._buffer.put(chat_component)
diff_time = timeout - (time.time()-time_mark)
diff_time = timeout - (time.time() - time_mark)
await asyncio.sleep(diff_time)
continuation = metadata.get('continuation')
except ChatParseException as e:
self._logger.debug(f"[{self.video_id}]{str(e)}")
return
except (TypeError , json.JSONDecodeError) :
except exceptions.ChatParseException as e:
self._logger.debug(f"[{self._video_id}]{str(e)}")
raise
except (TypeError, json.JSONDecodeError):
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):
if self._pauser.empty():
@@ -207,7 +207,7 @@ class LiveChatAsync:
self._pauser.put_nowait(None)
if not self._is_replay:
continuation = liveparam.getparam(
self.video_id, 3, self._topchat_only)
self._video_id, 3, self._topchat_only)
return continuation
async def _get_contents(self, continuation, session, headers):
@@ -219,9 +219,7 @@ class LiveChatAsync:
-------
'continuationContents' which includes metadata & chatdata.
'''
livechat_json = (await
self._get_livechat_json(continuation, session, headers)
)
livechat_json = await self._get_livechat_json(continuation, session, headers)
contents = self._parser.get_contents(livechat_json)
if self._first_fetch:
if contents is None or self._is_replay:
@@ -229,13 +227,13 @@ class LiveChatAsync:
self._parser.is_replay = True
self._fetch_url = "live_chat_replay/get_live_chat_replay?continuation="
continuation = arcparam.getparam(
self.video_id, self.seektime, self._topchat_only)
livechat_json = (await self._get_livechat_json(
continuation, session, headers))
self._video_id, self.seektime, self._topchat_only)
livechat_json = (await self._get_livechat_json(
continuation, session, headers))
reload_continuation = self._parser.reload_continuation(
self._parser.get_contents(livechat_json))
if reload_continuation:
livechat_json = (await self._get_livechat_json(
livechat_json = (await self._get_livechat_json(
reload_continuation, session, headers))
contents = self._parser.get_contents(livechat_json)
self._is_replay = True
@@ -249,23 +247,23 @@ class LiveChatAsync:
continuation = urllib.parse.quote(continuation)
livechat_json = None
status_code = 0
url =f"https://www.youtube.com/{self._fetch_url}{continuation}&pbj=1"
url = f"https://www.youtube.com/{self._fetch_url}{continuation}&pbj=1"
for _ in range(MAX_RETRY + 1):
async with session.get(url ,headers = headers) as resp:
async with session.get(url, headers=headers) as resp:
try:
text = await resp.text()
livechat_json = json.loads(text)
break
except (ClientConnectorError,json.JSONDecodeError) :
except (ClientConnectorError, json.JSONDecodeError):
await asyncio.sleep(1)
continue
else:
self._logger.error(f"[{self.video_id}]"
f"Exceeded retry count. status_code={status_code}")
self._logger.error(f"[{self._video_id}]"
f"Exceeded retry count. status_code={status_code}")
return None
return livechat_json
async def _callback_loop(self,callback):
async def _callback_loop(self, callback):
""" コンストラクタでcallbackを指定している場合、バックグラウンドで
callbackに指定された関数に一定間隔でチャットデータを投げる。
@@ -290,9 +288,12 @@ class LiveChatAsync:
: Processorによって加工されたチャットデータ
"""
if self._callback is None:
items = await self._buffer.get()
return self.processor.process(items)
raise IllegalFunctionCall(
if self.is_alive():
items = await self._buffer.get()
return self.processor.process(items)
else:
return []
raise exceptions.IllegalFunctionCall(
"既にcallbackを登録済みのため、get()は実行できません。")
def is_replay(self):
@@ -313,22 +314,37 @@ class LiveChatAsync:
def is_alive(self):
return self._is_alive
def finish(self,sender):
def _finish(self, sender):
'''Listener終了時のコールバック'''
try:
self.terminate()
self._task_finished()
except CancelledError:
self._logger.debug(f'[{self.video_id}]cancelled:{sender}')
self._logger.debug(f'[{self._video_id}]cancelled:{sender}')
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を終了する。
'''
self._is_alive = False
if self._direct_mode == False:
#bufferにダミーオブジェクトを入れてis_alive()を判定させる
self._buffer.put_nowait({'chatdata':'','timeout':0})
self._logger.info(f'[{self.video_id}]finished.')
if self.is_alive():
self.terminate()
try:
self.listen_task.result()
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
def _set_exception_handler(cls, handler):
@@ -336,13 +352,13 @@ class LiveChatAsync:
loop.set_exception_handler(handler)
@classmethod
async def shutdown(cls, event, sig = None, handler=None):
async def shutdown(cls, event, sig=None, handler=None):
cls._logger.debug("shutdown...")
tasks = [t for t in asyncio.all_tasks() if t is not
asyncio.current_task()]
asyncio.current_task()]
[task.cancel() for task in tasks]
cls._logger.debug(f"complete remaining tasks...")
await asyncio.gather(*tasks,return_exceptions=True)
cls._logger.debug("complete remaining tasks...")
await asyncio.gather(*tasks, return_exceptions=True)
loop = asyncio.get_event_loop()
loop.stop()

View File

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

View File

@@ -1,20 +1,20 @@
import requests
import datetime
import json
import random
import signal
import time
import traceback
import urllib.parse
from concurrent.futures import CancelledError, ThreadPoolExecutor
from queue import Queue
from threading import Event
from .buffer import Buffer
from ..parser.live import Parser
from .. import config
from ..exceptions import ChatParseException, IllegalFunctionCall
from .. import exceptions
from ..paramgen import liveparam, arcparam
from ..processors.default.processor import DefaultProcessor
from ..processors.combinator import Combinator
from ..util.extract_video_id import extract_video_id
headers = config.headers
MAX_RETRY = 10
@@ -74,7 +74,7 @@ class LiveChat:
_setup_finished = False
def __init__(self, video_id,
seektime=0,
seektime=-1,
processor=DefaultProcessor(),
buffer=None,
interruptable=True,
@@ -85,7 +85,7 @@ class LiveChat:
topchat_only=False,
logger=config.logger(__name__)
):
self.video_id = video_id
self._video_id = extract_video_id(video_id)
self.seektime = seektime
if isinstance(processor, tuple):
self.processor = Combinator(processor)
@@ -104,7 +104,9 @@ class LiveChat:
self._first_fetch = True
self._fetch_url = "live_chat/get_live_chat?continuation="
self._topchat_only = topchat_only
self._event = Event()
self._logger = logger
self.exception = None
if interruptable:
signal.signal(signal.SIGINT, lambda a, b: self.terminate())
self._setup()
@@ -113,7 +115,7 @@ class LiveChat:
# direct modeがTrueでcallback未設定の場合例外発生。
if self._direct_mode:
if self._callback is None:
raise IllegalFunctionCall(
raise exceptions.IllegalFunctionCall(
"When direct_mode=True, callback parameter is required.")
else:
# direct modeがFalseでbufferが未設定ならばデフォルトのbufferを作成
@@ -126,19 +128,19 @@ class LiveChat:
# callbackを呼ぶループタスクの開始
self._executor.submit(self._callback_loop, self._callback)
# _listenループタスクの開始
listen_task = self._executor.submit(self._startlisten)
self.listen_task = self._executor.submit(self._startlisten)
# add_done_callbackの登録
if self._done_callback is None:
listen_task.add_done_callback(self.finish)
self.listen_task.add_done_callback(self._finish)
else:
listen_task.add_done_callback(self._done_callback)
self.listen_task.add_done_callback(self._done_callback)
def _startlisten(self):
time.sleep(0.1) # sleep shortly to prohibit skipping fetching data
"""Fetch first continuation parameter,
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)
def _listen(self, continuation):
@@ -154,13 +156,11 @@ class LiveChat:
with requests.Session() as session:
while(continuation and self._is_alive):
continuation = self._check_pause(continuation)
contents = self._get_contents(
continuation, session, headers)
contents = self._get_contents(continuation, session, headers)
metadata, chatdata = self._parser.parse(contents)
timeout = metadata['timeoutMs']/1000
timeout = metadata['timeoutMs'] / 1000
chat_component = {
"video_id": self.video_id,
"video_id": self._video_id,
"timeout": timeout,
"chatdata": chatdata
}
@@ -174,17 +174,18 @@ class LiveChat:
self._callback(processed_chat)
else:
self._buffer.put(chat_component)
diff_time = timeout - (time.time()-time_mark)
time.sleep(diff_time if diff_time > 0 else 0)
diff_time = timeout - (time.time() - time_mark)
self._event.wait(diff_time if diff_time > 0 else 0)
continuation = metadata.get('continuation')
except ChatParseException as e:
self._logger.debug(f"[{self.video_id}]{str(e)}")
return
except exceptions.ChatParseException as e:
self._logger.debug(f"[{self._video_id}]{str(e)}")
raise
except (TypeError, json.JSONDecodeError):
self._logger.error(f"{traceback.format_exc(limit = -1)}")
return
self._logger.error(f"{traceback.format_exc(limit=-1)}")
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):
if self._pauser.empty():
@@ -195,7 +196,7 @@ class LiveChat:
'''
self._pauser.put_nowait(None)
if not self._is_replay:
continuation = liveparam.getparam(self.video_id, 3)
continuation = liveparam.getparam(self._video_id, 3)
return continuation
def _get_contents(self, continuation, session, headers):
@@ -217,9 +218,8 @@ class LiveChat:
self._parser.is_replay = True
self._fetch_url = "live_chat_replay/get_live_chat_replay?continuation="
continuation = arcparam.getparam(
self.video_id, self.seektime, self._topchat_only)
livechat_json = (self._get_livechat_json(
continuation, session, headers))
self._video_id, self.seektime, self._topchat_only)
livechat_json = (self._get_livechat_json(continuation, session, headers))
reload_continuation = self._parser.reload_continuation(
self._parser.get_contents(livechat_json))
if reload_continuation:
@@ -248,9 +248,9 @@ class LiveChat:
time.sleep(1)
continue
else:
self._logger.error(f"[{self.video_id}]"
self._logger.error(f"[{self._video_id}]"
f"Exceeded retry count. status_code={status_code}")
return None
raise exceptions.RetryExceedMaxCount()
return livechat_json
def _callback_loop(self, callback):
@@ -278,9 +278,12 @@ class LiveChat:
: Processorによって加工されたチャットデータ
"""
if self._callback is None:
items = self._buffer.get()
return self.processor.process(items)
raise IllegalFunctionCall(
if self.is_alive():
items = self._buffer.get()
return self.processor.process(items)
else:
return []
raise exceptions.IllegalFunctionCall(
"既にcallbackを登録済みのため、get()は実行できません。")
def is_replay(self):
@@ -301,18 +304,35 @@ class LiveChat:
def is_alive(self):
return self._is_alive
def finish(self, sender):
def _finish(self, sender):
'''Listener終了時のコールバック'''
try:
self.terminate()
self._task_finished()
except CancelledError:
self._logger.debug(f'[{self.video_id}]cancelled:{sender}')
self._logger.debug(f'[{self._video_id}]cancelled:{sender}')
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を終了する。
'''
if self.is_alive():
self._is_alive = False
self._buffer.put({})
self._logger.info(f'[{self.video_id}]終了しました')
self.terminate()
try:
self.listen_task.result()
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

View File

@@ -4,11 +4,6 @@ class ChatParseException(Exception):
'''
pass
class NoYtinitialdataException(ChatParseException):
'''
Thrown when the video is not found.
'''
pass
class ResponseContextError(ChatParseException):
'''
@@ -16,37 +11,54 @@ class ResponseContextError(ChatParseException):
'''
pass
class NoLivechatRendererException(ChatParseException):
'''
Thrown when livechatRenderer is missing in JSON.
'''
pass
class NoContentsException(ChatParseException):
class NoContents(ChatParseException):
'''
Thrown when ContinuationContents is missing in JSON.
'''
pass
class NoContinuationsException(ChatParseException):
class NoContinuation(ChatParseException):
'''
Thrown when continuation is missing in ContinuationContents.
'''
pass
class IllegalFunctionCall(Exception):
'''
Thrown when get () is called even though
set_callback () has been executed.
Thrown when get() is called even though
set_callback() has been executed.
'''
pass
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

View File

@@ -1,111 +1,55 @@
from base64 import urlsafe_b64encode as b64enc
from functools import reduce
import math
import random
import urllib.parse
from .pb.header_pb2 import Header
from .pb.replay_pb2 import Continuation
from urllib.parse import quote
import base64
'''
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
---------
bytes : base64 encoded video_id parameter.
"""
header_magic = b'\x0A\x0F\x1A\x0D\x0A'
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(
b64enc(reduce(lambda x, y: x+y, item)).decode()
).encode()
def _gen_vid(video_id) -> str:
header = Header()
header.info.video.id = video_id
header.terminator = 1
return base64.urlsafe_b64encode(header.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, seektime, topchat_only) -> str:
chattype = 1
timestamp = 0
if topchat_only:
chattype = 4
def _build(video_id, seektime, topchat_only):
switch_01 = b'\x04' if topchat_only else b'\x01'
fetch_before_start = 3
if seektime < 0:
times = _nval(0)
switch = b'\x04'
fetch_before_start = 4
elif seektime == 0:
times = _nval(1)
switch = b'\x03'
timestamp = 1
else:
times = _nval(int(seektime*1000000))
switch = b'\x03'
parity = b'\x00'
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 = b''.join([
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
])
return urllib.parse.quote(
b64enc(header_magic +
_nval(len(body)) +
body
).decode()
)
timestamp = int(seektime * 1000000)
continuation = Continuation()
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())
def getparam(video_id, seektime=0, topchat_only=False):
def getparam(video_id, seektime=-1, topchat_only=False) -> str:
'''
Parameter
---------

View File

@@ -1,7 +1,5 @@
from base64 import urlsafe_b64encode as b64enc
from functools import reduce
import math
import random
import urllib.parse
'''
@@ -12,6 +10,7 @@ Author: taizan-hokuto (2019) @taizan205
ver 0.0.1 2019.10.05
'''
def _gen_vid_long(video_id):
"""generate video_id parameter.
Parameter
@@ -23,7 +22,7 @@ def _gen_vid_long(video_id):
byte[] : base64 encoded video_id parameter.
"""
header_magic = b'\x0A\x0F\x1A\x0D\x0A'
header_id = video_id.encode()
header_id = video_id.encode()
header_sep_1 = b'\x1A\x13\xEA\xA8\xDD\xB9\x01\x0D\x0A\x0B'
header_terminator = b'\x20\x01'
@@ -37,9 +36,10 @@ def _gen_vid_long(video_id):
]
return urllib.parse.quote(
b64enc(reduce(lambda x, y: x+y, item)).decode()
b64enc(reduce(lambda x, y: x + y, item)).decode()
).encode()
def _gen_vid(video_id):
"""generate video_id parameter.
Parameter
@@ -51,7 +51,7 @@ def _gen_vid(video_id):
bytes : base64 encoded video_id parameter.
"""
header_magic = b'\x0A\x0F\x1A\x0D\x0A'
header_id = video_id.encode()
header_id = video_id.encode()
header_terminator = b'\x20\x01'
item = [
@@ -62,20 +62,23 @@ def _gen_vid(video_id):
]
return urllib.parse.quote(
b64enc(reduce(lambda x, y: x+y, item)).decode()
b64enc(reduce(lambda x, y: x + y, item)).decode()
).encode()
def _nval(val):
"""convert value to byte array"""
if val<0: raise ValueError
if val < 0:
raise ValueError
buf = b''
while val >> 7:
m = val & 0xFF | 0x80
buf += m.to_bytes(1,'big')
buf += m.to_bytes(1, 'big')
val >>= 7
buf += val.to_bytes(1,'big')
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:
@@ -83,21 +86,19 @@ def _build(video_id, seektime, topchat_only):
if seektime == 0:
times = b''
else:
times =_nval(int(seektime*1000))
times = _nval(int(seektime * 1000))
if seektime > 0:
_len_time = ( b'\x5A'
+ (len(times)+1).to_bytes(1,'big')
+ b'\x10')
_len_time = b'\x5A' + (len(times) + 1).to_bytes(1, 'big') + b'\x10'
else:
_len_time = b''
header_magic = b'\xA2\x9D\xB0\xD3\x04'
sep_0 = b'\x1A'
vid = _gen_vid(video_id)
_tag = b'\x40\x01'
timestamp1 = times
sep_1 = b'\x60\x04\x72\x02\x08'
terminator = b'\x78\x01'
sep_0 = b'\x1A'
vid = _gen_vid(video_id)
_tag = b'\x40\x01'
timestamp1 = times
sep_1 = b'\x60\x04\x72\x02\x08'
terminator = b'\x78\x01'
body = [
sep_0,
@@ -111,16 +112,15 @@ def _build(video_id, seektime, topchat_only):
terminator
]
body = reduce(lambda x, y: x+y, body)
body = reduce(lambda x, y: x + y, body)
return urllib.parse.quote(
b64enc( header_magic +
_nval(len(body)) +
body
).decode()
)
return urllib.parse.quote(
b64enc(header_magic + _nval(len(body)) + body
).decode()
)
def getparam(video_id, seektime = 0.0, topchat_only = False):
def getparam(video_id, seektime=0.0, topchat_only=False):
'''
Parameter
---------

View File

@@ -1,19 +1,21 @@
from base64 import urlsafe_b64encode as b64enc
from functools import reduce
import time
from .pb.header_pb2 import Header
from .pb.live_pb2 import Continuation
from urllib.parse import quote
import base64
import random
import urllib.parse
import time
'''
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.
Parameter
---------
@@ -21,141 +23,60 @@ def _gen_vid(video_id):
Return
---------
byte[] : base64 encoded video_id parameter.
str : base64 encoded video_id parameter.
"""
header_magic = b'\x0A\x0F\x0A\x0D\x0A'
header_id = video_id.encode()
header_sep_1 = b'\x1A'
header_sep_2 = b'\x43\xAA\xB9\xC1\xBD\x01\x3D\x0A'
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(
b64enc(reduce(lambda x, y: x+y, item)).decode()
).encode()
header = Header()
header.info.video.id = video_id
header.terminator = 1
return base64.urlsafe_b64encode(header.SerializeToString()).decode()
def _tzparity(video_id, times):
t = 0
for i, s in enumerate(video_id):
ss = ord(s)
if(ss % 2 == 0):
t += ss*(12-i)
else:
t ^= ss*i
def _build(video_id, ts1, ts2, ts3, ts4, ts5, topchat_only) -> str:
chattype = 1
if topchat_only:
chattype = 4
continuation = Continuation()
entity = continuation.entity
return ((times ^ t) % 2).to_bytes(1, 'big')
entity.header = _gen_vid(video_id)
entity.timestamp1 = ts1
entity.s6 = 0
entity.s7 = 0
entity.s8 = 1
entity.body.b1 = 0
entity.body.b2 = 0
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
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 = b''.join([
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
])
return urllib.parse.quote(
b64enc(header_magic +
_nval(len(body)) +
body
).decode()
return quote(
base64.urlsafe_b64encode(continuation.SerializeToString()).decode()
)
def _times(past_sec):
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)
_ts3 = n - past_sec + random.uniform(0, 1)
_ts4 = n - random.uniform(10*60, 60*60)
_ts4 = n - random.uniform(10 * 60, 60 * 60)
_ts5 = n - random.uniform(0.01, 0.99)
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
---------

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.
"""
import json
from .. exceptions import (
ResponseContextError,
NoContentsException,
NoContinuationsException,
ChatParseException )
from .. import exceptions
class Parser:
@@ -20,11 +16,11 @@ class Parser:
def get_contents(self, jsn):
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'):
raise ResponseContextError('The video_id would be wrong,'
'or video is deleted or private.')
contents=jsn['response'].get('continuationContents')
raise exceptions.ResponseContextError(
'The video_id would be wrong, or video is deleted or private.')
contents = jsn['response'].get('continuationContents')
return contents
def parse(self, contents):
@@ -46,52 +42,56 @@ class Parser:
if contents is None:
'''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]
if cont is None:
raise NoContinuationsException('No Continuation')
metadata = (cont.get('invalidationContinuationData') or
cont.get('timedContinuationData') or
cont.get('reloadContinuationData') or
cont.get('liveChatReplayContinuationData')
raise exceptions.NoContinuation('No Continuation')
metadata = (cont.get('invalidationContinuationData')
or cont.get('timedContinuationData')
or cont.get('reloadContinuationData')
or cont.get('liveChatReplayContinuationData')
)
if metadata is None:
if cont.get("playerSeekContinuationData"):
raise ChatParseException('Finished chat data')
raise exceptions.ChatDataFinished('Finished chat data')
unknown = list(cont.keys())[0]
if unknown:
raise ChatParseException(f"Received unknown continuation type:{unknown}")
raise exceptions.ReceivedUnknownContinuation(
f"Received unknown continuation type:{unknown}")
else:
raise ChatParseException('Cannot extract continuation data')
raise exceptions.FailedExtractContinuation('Cannot extract continuation data')
return self._create_data(metadata, 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.
If so, try to fetch playerSeekContinuationData.
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]
if cont.get("liveChatReplayContinuationData"):
#chat data exist.
# chat data exist.
return None
#chat data do not exist, get playerSeekContinuationData.
# chat data do not exist, get playerSeekContinuationData.
init_cont = cont.get("playerSeekContinuationData")
if init_cont:
return init_cont.get("continuation")
raise ChatParseException('Finished chat data')
raise exceptions.ChatDataFinished('Finished chat data')
def _create_data(self, metadata, contents):
actions = contents['liveChatContinuation'].get('actions')
if self.is_replay:
interval = self._get_interval(actions)
metadata.setdefault("timeoutMs",interval)
metadata.setdefault("timeoutMs", interval)
"""Archived chat has different structures than live chat,
so make it the same format."""
chatdata = [action["replayChatItemAction"]["actions"][0]
for action in actions]
for action in actions]
else:
metadata.setdefault('timeoutMs', 10000)
chatdata = 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.
Receive chat data (actions) from Listener.
'''
def process(self, chat_components: list):
'''
Interface that represents processing of chat data.
@@ -21,7 +22,9 @@ class ChatProcessor:
'''
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
class Combinator(ChatProcessor):
'''
Combinator combines multiple chat processors.
@@ -34,6 +35,4 @@ class Combinator(ChatProcessor):
Tuple of chat data processed by each chat processor.
'''
return tuple(processor.process(chat_components)
for processor in self.processors)
for processor in self.processors)

View File

@@ -1,5 +1,3 @@
import datetime
import time
from .renderer.textmessage import LiveChatTextMessageRenderer
from .renderer.paidmessage import LiveChatPaidMessageRenderer
from .renderer.paidsticker import LiveChatPaidStickerRenderer
@@ -39,7 +37,7 @@ class CompatibleProcessor(ChatProcessor):
chat = self.parse(action)
if chat:
chatlist.append(chat)
ret["pollingIntervalMillis"] = int(timeout*1000)
ret["pollingIntervalMillis"] = int(timeout * 1000)
ret["pageInfo"] = {
"totalResults": len(chatlist),
"resultsPerPage": len(chatlist),
@@ -58,7 +56,7 @@ class CompatibleProcessor(ChatProcessor):
rd = {}
try:
renderer = self.get_renderer(item)
if renderer == None:
if renderer is None:
return None
rd["kind"] = "youtube#liveChatMessage"

View File

@@ -1,68 +1,67 @@
import datetime, pytz
import datetime
import pytz
class BaseRenderer:
def __init__(self, item, chattype):
self.renderer = list(item.values())[0]
self.chattype = chattype
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,
"type": self.chattype,
"liveChatId": "",
"authorChannelId": self.renderer.get("authorExternalChannelId"),
"publishedAt": self.get_publishedat(self.renderer.get("timestampUsec", 0)),
"hasDisplayContent": True,
"displayMessage": message,
"textMessageDetails": {
"messageText" : message
"messageText": message
}
}
def get_authordetails(self):
authorExternalChannelId = self.renderer.get("authorExternalChannelId")
#parse subscriber type
# parse subscriber type
isVerified, isChatOwner, isChatSponsor, 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" : isChatSponsor,
"isChatModerator" : isChatModerator
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": isChatSponsor,
"isChatModerator": isChatModerator
}
def get_message(self,renderer):
def get_message(self, renderer):
message = ''
if renderer.get("message"):
runs=renderer["message"].get("runs")
runs = renderer["message"].get("runs")
if runs:
for r in runs:
if r:
if r.get('emoji'):
message += r['emoji'].get('shortcuts',[''])[0]
message += r['emoji'].get('shortcuts', [''])[0]
else:
message += r.get('text','')
message += r.get('text', '')
return message
def get_badges(self,renderer):
def get_badges(self, renderer):
isVerified = False
isChatOwner = False
isChatSponsor = False
isChatModerator = False
badges=renderer.get("authorBadges")
badges = renderer.get("authorBadges")
if badges:
for badge in badges:
author_type = badge["liveChatAuthorBadgeRenderer"]["accessibility"]["accessibilityData"]["label"]
author_type = badge["liveChatAuthorBadgeRenderer"]["accessibility"]["accessibilityData"]["label"]
if author_type == '確認済み':
isVerified = True
if author_type == '所有者':
@@ -76,8 +75,7 @@ class BaseRenderer:
def get_id(self):
return self.renderer.get('id')
def get_publishedat(self,timestamp):
dt = datetime.datetime.fromtimestamp(int(timestamp)/1000000)
def get_publishedat(self, timestamp):
dt = datetime.datetime.fromtimestamp(int(timestamp) / 1000000)
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
class LiveChatLegacyPaidMessageRenderer(BaseRenderer):
def __init__(self, item):
super().__init__(item, "newSponsorEvent")
@@ -8,36 +10,33 @@ class LiveChatLegacyPaidMessageRenderer(BaseRenderer):
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,
"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
# 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
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):
def get_message(self, renderer):
message = (renderer["eventText"]["runs"][0]["text"]
)+' / '+(renderer["detailText"]["simpleText"])
) + ' / ' + (renderer["detailText"]["simpleText"])
return message

View File

@@ -25,7 +25,7 @@ class LiveChatMembershipItemRenderer(BaseRenderer):
)
return {
"channelId": authorExternalChannelId,
"channelUrl": "http://www.youtube.com/channel/"+authorExternalChannelId,
"channelUrl": "http://www.youtube.com/channel/" + authorExternalChannelId,
"displayName": self.renderer["authorName"]["simpleText"],
"profileImageUrl": self.renderer["authorPhoto"]["thumbnails"][1]["url"],
"isVerified": isVerified,
@@ -35,6 +35,6 @@ class LiveChatMembershipItemRenderer(BaseRenderer):
}
def get_message(self, renderer):
message = ''.join([mes.get("text", "") for mes in renderer["headerSubtext"]["runs"]])
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
superchat_regex = re.compile(r"^(\D*)(\d{1,3}(,\d{3})*(\.\d*)*\b)$")
class LiveChatPaidMessageRenderer(BaseRenderer):
def __init__(self, item):
super().__init__(item, "superChatEvent")
@@ -10,31 +11,31 @@ class LiveChatPaidMessageRenderer(BaseRenderer):
def get_snippet(self):
authorName = self.renderer["authorName"]["simpleText"]
message = self.get_message(self.renderer)
amountDisplayString, symbol, amountMicros =(
amountDisplayString, symbol, amountMicros = (
self.get_amountdata(self.renderer)
)
return {
"type" : self.chattype,
"liveChatId" : "",
"authorChannelId" : self.renderer.get("authorExternalChannelId"),
"publishedAt" : self.get_publishedat(self.renderer.get("timestampUsec",0)),
"hasDisplayContent" : True,
"displayMessage" : amountDisplayString+" from "+authorName+': \"'+ message+'\"',
"superChatDetails" : {
"amountMicros" : amountMicros,
"currency" : currency.symbols[symbol]["fxtext"] if currency.symbols.get(symbol) else symbol,
"amountDisplayString" : amountDisplayString,
"tier" : 0,
"backgroundColor" : self.renderer.get("bodyBackgroundColor", 0)
"type": self.chattype,
"liveChatId": "",
"authorChannelId": self.renderer.get("authorExternalChannelId"),
"publishedAt": self.get_publishedat(self.renderer.get("timestampUsec", 0)),
"hasDisplayContent": True,
"displayMessage": amountDisplayString + " from " + authorName + ': \"' + message + '\"',
"superChatDetails": {
"amountMicros": amountMicros,
"currency": currency.symbols[symbol]["fxtext"] if currency.symbols.get(symbol) else symbol,
"amountDisplayString": amountDisplayString,
"tier": 0,
"backgroundColor": self.renderer.get("bodyBackgroundColor", 0)
}
}
def get_amountdata(self,renderer):
def get_amountdata(self, renderer):
amountDisplayString = renderer["purchaseAmountText"]["simpleText"]
m = superchat_regex.search(amountDisplayString)
if m:
symbol = m.group(1)
amountMicros = int(float(m.group(2).replace(',',''))*1000000)
amountMicros = int(float(m.group(2).replace(',', '')) * 1000000)
else:
symbol = ""
amountMicros = 0

View File

@@ -3,46 +3,45 @@ from . import currency
from .base import BaseRenderer
superchat_regex = re.compile(r"^(\D*)(\d{1,3}(,\d{3})*(\.\d*)*\b)$")
class LiveChatPaidStickerRenderer(BaseRenderer):
def __init__(self, item):
super().__init__(item, "superStickerEvent")
def get_snippet(self):
authorName = self.renderer["authorName"]["simpleText"]
amountDisplayString, symbol, amountMicros =(
amountDisplayString, symbol, amountMicros = (
self.get_amountdata(self.renderer)
)
return {
"type" : self.chattype,
"liveChatId" : "",
"authorChannelId" : self.renderer.get("authorExternalChannelId"),
"publishedAt" : self.get_publishedat(self.renderer.get("timestampUsec",0)),
"hasDisplayContent" : True,
"displayMessage" : "Super Sticker " + amountDisplayString + " from "+authorName,
"superStickerDetails" : {
"superStickerMetaData" : {
"type": self.chattype,
"liveChatId": "",
"authorChannelId": self.renderer.get("authorExternalChannelId"),
"publishedAt": self.get_publishedat(self.renderer.get("timestampUsec", 0)),
"hasDisplayContent": True,
"displayMessage": "Super Sticker " + amountDisplayString + " from " + authorName,
"superStickerDetails": {
"superStickerMetaData": {
"stickerId": "",
"altText": "",
"language": ""
},
"amountMicros" : amountMicros,
"currency" : currency.symbols[symbol]["fxtext"] if currency.symbols.get(symbol) else symbol,
"amountDisplayString" : amountDisplayString,
"tier" : 0,
"backgroundColor" : self.renderer.get("bodyBackgroundColor", 0)
"amountMicros": amountMicros,
"currency": currency.symbols[symbol]["fxtext"] if currency.symbols.get(symbol) else symbol,
"amountDisplayString": amountDisplayString,
"tier": 0,
"backgroundColor": self.renderer.get("bodyBackgroundColor", 0)
}
}
def get_amountdata(self,renderer):
def get_amountdata(self, renderer):
amountDisplayString = renderer["purchaseAmountText"]["simpleText"]
m = superchat_regex.search(amountDisplayString)
if m:
symbol = m.group(1)
amountMicros = int(float(m.group(2).replace(',',''))*1000000)
amountMicros = int(float(m.group(2).replace(',', '')) * 1000000)
else:
symbol = ""
amountMicros = 0
return amountDisplayString, symbol, amountMicros

View File

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

View File

@@ -20,13 +20,13 @@ class Chatdata:
if self.interval == 0:
time.sleep(1)
return
time.sleep(self.interval/len(self.items))
time.sleep(self.interval / len(self.items))
async def tick_async(self):
if self.interval == 0:
await asyncio.sleep(1)
return
await asyncio.sleep(self.interval/len(self.items))
await asyncio.sleep(self.interval / len(self.items))
class DefaultProcessor(ChatProcessor):
@@ -62,7 +62,7 @@ class DefaultProcessor(ChatProcessor):
return None
try:
renderer = self._get_renderer(item)
if renderer == None:
if renderer is None:
return None
renderer.get_snippet()

View File

@@ -1,6 +1,10 @@
from datetime import datetime
class Author:
pass
class BaseRenderer:
def __init__(self, item, chattype):
self.renderer = list(item.values())[0]
@@ -10,65 +14,65 @@ class BaseRenderer:
def get_snippet(self):
self.type = self.chattype
self.id = self.renderer.get('id')
timestampUsec = int(self.renderer.get("timestampUsec",0))
self.timestamp = int(timestampUsec/1000)
timestampUsec = int(self.renderer.get("timestampUsec", 0))
self.timestamp = int(timestampUsec / 1000)
tst = self.renderer.get("timestampText")
if tst:
self.elapsedTime = tst.get("simpleText")
else:
self.elapsedTime = ""
self.datetime = self.get_datetime(timestampUsec)
self.message ,self.messageEx = self.get_message(self.renderer)
self.id = self.renderer.get('id')
self.amountValue= 0.0
self.message, self.messageEx = self.get_message(self.renderer)
self.id = self.renderer.get('id')
self.amountValue = 0.0
self.amountString = ""
self.currency= ""
self.currency = ""
self.bgColor = 0
def get_authordetails(self):
self.author.badgeUrl = ""
(self.author.isVerified,
self.author.isChatOwner,
self.author.isChatSponsor,
self.author.isChatModerator) = (
self.author.isChatOwner,
self.author.isChatSponsor,
self.author.isChatModerator) = (
self.get_badges(self.renderer)
)
self.author.channelId = self.renderer.get("authorExternalChannelId")
self.author.channelUrl = "http://www.youtube.com/channel/"+self.author.channelId
self.author.name = self.renderer["authorName"]["simpleText"]
self.author.imageUrl= self.renderer["authorPhoto"]["thumbnails"][1]["url"]
self.author.channelUrl = "http://www.youtube.com/channel/" + self.author.channelId
self.author.name = self.renderer["authorName"]["simpleText"]
self.author.imageUrl = self.renderer["authorPhoto"]["thumbnails"][1]["url"]
def get_message(self,renderer):
def get_message(self, renderer):
message = ''
message_ex = []
if renderer.get("message"):
runs=renderer["message"].get("runs")
runs = renderer["message"].get("runs")
if runs:
for r in runs:
if r:
if r.get('emoji'):
message += r['emoji'].get('shortcuts',[''])[0]
message_ex.append(r['emoji']['image']['thumbnails'][1].get('url'))
message += r['emoji'].get('shortcuts', [''])[0]
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:
message += r.get('text','')
message_ex.append(r.get('text',''))
message += r.get('text', '')
message_ex.append(r.get('text', ''))
return message, message_ex
def get_badges(self,renderer):
def get_badges(self, renderer):
self.author.type = ''
isVerified = False
isChatOwner = False
isChatSponsor = False
isChatModerator = False
badges=renderer.get("authorBadges")
badges = renderer.get("authorBadges")
if badges:
for badge in badges:
if badge["liveChatAuthorBadgeRenderer"].get("icon"):
author_type = badge["liveChatAuthorBadgeRenderer"]["icon"]["iconType"]
author_type = badge["liveChatAuthorBadgeRenderer"]["icon"]["iconType"]
self.author.type = author_type
if author_type == 'VERIFIED':
isVerified = True
@@ -82,12 +86,9 @@ class BaseRenderer:
self.get_badgeurl(badge)
return isVerified, isChatOwner, isChatSponsor, isChatModerator
def get_badgeurl(self,badge):
def get_badgeurl(self, badge):
self.author.badgeUrl = badge["liveChatAuthorBadgeRenderer"]["customThumbnail"]["thumbnails"][0]["url"]
def get_datetime(self,timestamp):
dt = datetime.fromtimestamp(timestamp/1000000)
def get_datetime(self, timestamp):
dt = datetime.fromtimestamp(timestamp / 1000000)
return dt.strftime('%Y-%m-%d %H:%M:%S')

View File

@@ -1,18 +1,15 @@
from .base import BaseRenderer
class LiveChatLegacyPaidMessageRenderer(BaseRenderer):
def __init__(self, item):
super().__init__(item, "newSponsor")
def get_authordetails(self):
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"]
)+' / '+(renderer["detailText"]["simpleText"])
return message
) + ' / ' + (renderer["detailText"]["simpleText"])
return message, [message]

View File

@@ -10,6 +10,6 @@ class LiveChatMembershipItemRenderer(BaseRenderer):
self.author.isChatSponsor = True
def get_message(self, renderer):
message = ''.join([mes.get("text", "") for mes in renderer["headerSubtext"]["runs"]])
message = ''.join([mes.get("text", "")
for mes in renderer["headerSubtext"]["runs"]])
return message, [message]

View File

@@ -3,30 +3,44 @@ from . import currency
from .base import BaseRenderer
superchat_regex = re.compile(r"^(\D*)(\d{1,3}(,\d{3})*(\.\d*)*\b)$")
class Colors:
pass
class LiveChatPaidMessageRenderer(BaseRenderer):
def __init__(self, item):
super().__init__(item, "superChat")
def get_snippet(self):
super().get_snippet()
amountDisplayString, symbol, amount =(
amountDisplayString, symbol, amount = (
self.get_amountdata(self.renderer)
)
self.amountValue= amount
self.amountValue = amount
self.amountString = amountDisplayString
self.currency= currency.symbols[symbol]["fxtext"] if currency.symbols.get(symbol) else symbol
self.bgColor= self.renderer.get("bodyBackgroundColor", 0)
self.currency = currency.symbols[symbol]["fxtext"] if currency.symbols.get(
symbol) else symbol
self.bgColor = self.renderer.get("bodyBackgroundColor", 0)
self.colors = self.get_colors()
def get_amountdata(self,renderer):
def get_amountdata(self, renderer):
amountDisplayString = renderer["purchaseAmountText"]["simpleText"]
m = superchat_regex.search(amountDisplayString)
if m:
symbol = m.group(1)
amount = float(m.group(2).replace(',',''))
amount = float(m.group(2).replace(',', ''))
else:
symbol = ""
amount = 0.0
return amountDisplayString, symbol, amount
def get_colors(self):
colors = Colors()
colors.headerBackgroundColor = self.renderer.get("headerBackgroundColor", 0)
colors.headerTextColor = self.renderer.get("headerTextColor", 0)
colors.bodyBackgroundColor = self.renderer.get("bodyBackgroundColor", 0)
colors.bodyTextColor = self.renderer.get("bodyTextColor", 0)
colors.timestampColor = self.renderer.get("timestampColor", 0)
colors.authorNameTextColor = self.renderer.get("authorNameTextColor", 0)
return colors

View File

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

View File

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

View File

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

View File

@@ -1,31 +1,55 @@
import csv
import os
import re
import requests
from base64 import standard_b64encode
from .chat_processor import ChatProcessor
from .default.processor import DefaultProcessor
PATTERN = re.compile(r"(.*)\(([0-9]+)\)$")
fmt_headers = ['datetime','elapsed','authorName','message','superchat'
,'type','authorChannel']
PATTERN = re.compile(r"(.*)\(([0-9]+)\)$")
fmt_headers = ['datetime', 'elapsed', 'authorName',
'message', 'superchat', 'type', 'authorChannel']
HEADER_HTML = '''
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
'''
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):
'''
HtmlArchiver saves chat data as HTML table format.
HTMLArchiver saves chat data as HTML table format.
'''
def __init__(self, save_path):
super().__init__()
self.save_path = self._checkpath(save_path)
with open(self.save_path, mode='a', encoding = 'utf-8') as f:
f.write(HEADER_HTML)
f.write('<table border="1" style="border-collapse: collapse">')
f.writelines(self._parse_html_header(fmt_headers))
self.processor = DefaultProcessor()
self.emoji_table = {} # tuble 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):
splitter = os.path.splitext(os.path.basename(filepath))
@@ -34,14 +58,14 @@ class HTMLArchiver(ChatProcessor):
newpath = filepath
counter = 0
while os.path.exists(newpath):
match = re.search(PATTERN,body)
match = re.search(PATTERN, body)
if match:
counter=int(match[2])+1
counter = int(match[2]) + 1
num_with_bracket = f'({str(counter)})'
body = f'{match[1]}{num_with_bracket}'
else:
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
def process(self, chat_components: list):
@@ -54,45 +78,60 @@ class HTMLArchiver(ChatProcessor):
total_lines : int :
count of total lines written to the file.
"""
if chat_components is None or len (chat_components) == 0:
if chat_components is None or len(chat_components) == 0:
return
with open(self.save_path, mode='a', encoding = 'utf-8') as f:
chats = self.processor.process(chat_components).items
for c in chats:
f.writelines(
self._parse_html_line([
c.datetime,
c.elapsedTime,
c.author.name,
c.message,
c.amountString,
c.author.type,
c.author.channelId]
)
)
'''
#Palliative treatment#
Comment out below line to prevent the table
display from collapsing.
'''
#f.write('</table>')
self.body.extend(
(self._parse_html_line((
c.datetime,
c.elapsedTime,
c.author.name,
self._parse_message(c.messageEx),
c.amountString,
c.author.type,
c.author.channelId)
) for c in self.processor.process(chat_components).items)
)
def _parse_html_line(self, raw_line):
html = ''
html+=' <tr>'
for cell in raw_line:
html+='<td>'+cell+'</td>'
html+='</tr>\n'
return html
return ''.join(('<tr>',
''.join(''.join(('<td>', cell, '</td>')) for cell in raw_line),
'</tr>\n'))
def _parse_html_header(self,raw_line):
html = ''
html+='<thead>\n'
html+=' <tr>'
for cell in raw_line:
html+='<th>'+cell+'</th>'
html+='</tr>\n'
html+='</thead>\n'
return html
def _parse_table_header(self, raw_line):
return ''.join(('<thead><tr>',
''.join(''.join(('<th>', cell, '</th>')) for cell in raw_line),
'</tr></thead>\n'))
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,10 +1,10 @@
import datetime
import json
import os
import re
from .chat_processor import ChatProcessor
PATTERN = re.compile(r"(.*)\(([0-9]+)\)$")
PATTERN = re.compile(r"(.*)\(([0-9]+)\)$")
class JsonfileArchiver(ChatProcessor):
"""
@@ -17,12 +17,13 @@ class JsonfileArchiver(ChatProcessor):
it is automatically saved under a different name
with suffix '(number)'
"""
def __init__(self,save_path):
def __init__(self, save_path):
super().__init__()
self.save_path = self._checkpath(save_path)
self.line_counter = 0
def process(self,chat_components: list):
def process(self, chat_components: list):
"""
Returns
----------
@@ -32,19 +33,23 @@ class JsonfileArchiver(ChatProcessor):
total_lines : int :
count of total lines written to the file.
"""
if chat_components is None: return
with open(self.save_path, mode='a', encoding = 'utf-8') as f:
if chat_components is None:
return
with open(self.save_path, mode='a', encoding='utf-8') as f:
for component in chat_components:
if component is None: continue
if component is None:
continue
chatdata = component.get('chatdata')
if chatdata is None: continue
if chatdata is None:
continue
for action in chatdata:
if action is None: continue
json_line = json.dumps(action, ensure_ascii = False)
f.writelines(json_line+'\n')
self.line_counter+=1
return { "save_path" : self.save_path,
"total_lines": self.line_counter }
if action is None:
continue
json_line = json.dumps(action, ensure_ascii=False)
f.writelines(json_line + '\n')
self.line_counter += 1
return {"save_path": self.save_path,
"total_lines": self.line_counter}
def _checkpath(self, filepath):
splitter = os.path.splitext(os.path.basename(filepath))
@@ -53,14 +58,12 @@ class JsonfileArchiver(ChatProcessor):
newpath = filepath
counter = 0
while os.path.exists(newpath):
match = re.search(PATTERN,body)
match = re.search(PATTERN, body)
if match:
counter=int(match[2])+1
counter = int(match[2]) + 1
num_with_bracket = f'({str(counter)})'
body = f'{match[1]}{num_with_bracket}'
else:
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

View File

@@ -1,10 +1,6 @@
import json
import os
import traceback
import datetime
import time
from .chat_processor import ChatProcessor
##version 2
class SimpleDisplayProcessor(ChatProcessor):
def process(self, chat_components: list):
@@ -12,36 +8,42 @@ class SimpleDisplayProcessor(ChatProcessor):
timeout = 0
if chat_components is None:
return {"timeout":timeout, "chatlist":chatlist}
return {"timeout": timeout, "chatlist": chatlist}
for component in chat_components:
timeout += component.get('timeout', 0)
chatdata = component.get('chatdata')
if chatdata is None:break
if chatdata is None:
break
for action in chatdata:
if action is None:continue
if action.get('addChatItemAction') is None:continue
if action['addChatItemAction'].get('item') is None:continue
if action 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:
author_name = root['authorName']['simpleText']
message = self._parse_message(root.get('message'))
purchase_amount_text = ''
else:
root = ( action['addChatItemAction']['item'].get('liveChatPaidMessageRenderer') or
action['addChatItemAction']['item'].get('liveChatPaidStickerRenderer') )
root = (action['addChatItemAction']['item'].get('liveChatPaidMessageRenderer')
or action['addChatItemAction']['item'].get('liveChatPaidStickerRenderer'))
if root:
author_name = root['authorName']['simpleText']
message = self._parse_message(root.get('message'))
purchase_amount_text = root['purchaseAmountText']['simpleText']
else:
continue
chatlist.append(f'[{author_name}]: {message} {purchase_amount_text}')
return {"timeout":timeout, "chatlist":chatlist}
chatlist.append(
f'[{author_name}]: {message} {purchase_amount_text}')
return {"timeout": timeout, "chatlist": chatlist}
def _parse_message(self,message):
def _parse_message(self, message):
if message is None:
return ''
if message.get('simpleText'):
@@ -51,11 +53,9 @@ class SimpleDisplayProcessor(ChatProcessor):
tmp = ''
for run in runs:
if run.get('emoji'):
tmp+=(run['emoji']['shortcuts'][0])
tmp += (run['emoji']['shortcuts'][0])
elif run.get('text'):
tmp+=(run['text'])
tmp += (run['text'])
return tmp
else:
return ''

View File

@@ -5,6 +5,8 @@ Calculate speed of chat.
"""
import time
from .. chat_processor import ChatProcessor
class RingQueue:
"""
リング型キュー
@@ -50,17 +52,17 @@ class RingQueue:
"""
if self.mergin:
self.items.append(item)
self.last_pos = len(self.items)-1
if self.last_pos == self.capacity-1:
self.last_pos = len(self.items) - 1
if self.last_pos == self.capacity - 1:
self.mergin = False
return
self.last_pos += 1
if self.last_pos > self.capacity-1:
if self.last_pos > self.capacity - 1:
self.last_pos = 0
self.items[self.last_pos] = item
self.first_pos += 1
if self.first_pos > self.capacity-1:
if self.first_pos > self.capacity - 1:
self.first_pos = 0
def get(self):
@@ -77,6 +79,7 @@ class RingQueue:
def item_count(self):
return len(self.items)
class SpeedCalculator(ChatProcessor, RingQueue):
"""
チャットの勢いを計算する。
@@ -91,7 +94,7 @@ class SpeedCalculator(ChatProcessor, RingQueue):
RingQueueに格納するチャット勢い算出用データの最大数
"""
def __init__(self, capacity = 10):
def __init__(self, capacity=10):
super().__init__(capacity)
self.speed = 0
@@ -106,7 +109,6 @@ class SpeedCalculator(ChatProcessor, RingQueue):
self.speed = self._calc_speed()
return self.speed
def _calc_speed(self):
"""
RingQueue内のチャット勢い算出用データリストを元に、
@@ -117,13 +119,12 @@ class SpeedCalculator(ChatProcessor, RingQueue):
チャット速度(1分間で換算したチャット数)
"""
try:
#キュー内の総チャット数
# キュー内の総チャット数
total = sum(item['chat_count'] for item in self.items)
#キュー内の最初と最後のチャットの時間差
duration = (self.items[self.last_pos]['endtime']
- self.items[self.first_pos]['starttime'])
# キュー内の最初と最後のチャットの時間差
duration = (self.items[self.last_pos]['endtime'] - self.items[self.first_pos]['starttime'])
if duration != 0:
return int(total*60/duration)
return int(total * 60 / duration)
return 0
except IndexError:
return 0
@@ -143,61 +144,60 @@ class SpeedCalculator(ChatProcessor, RingQueue):
'''
チャットデータがない場合に空のデータをキューに投入する。
'''
timestamp_now = int(time.time())
timestamp_now = int(time.time())
self.put({
'chat_count':0,
'starttime':int(timestamp_now),
'endtime':int(timestamp_now)
'chat_count': 0,
'starttime': int(timestamp_now),
'endtime': int(timestamp_now)
})
def _get_timestamp(action :dict):
def _get_timestamp(action: dict):
"""
チャットデータから時刻データを取り出す。
"""
try:
item = action['addChatItemAction']['item']
timestamp = int(item[list(item.keys())[0]]['timestampUsec'])
except (KeyError,TypeError):
except (KeyError, TypeError):
return None
return timestamp
if actions is None or len(actions)==0:
if actions is None or len(actions) == 0:
_put_emptydata()
return
#actions内の時刻データを持つチャットデータの数
counter=0
#actions内の最初のチャットデータの時刻
starttime= None
#actions内の最後のチャットデータの時刻
endtime=None
# actions内の時刻データを持つチャットデータの数
counter = 0
# actions内の最初のチャットデータの時刻
starttime = None
# actions内の最後のチャットデータの時刻
endtime = None
for action in actions:
#チャットデータからtimestampUsecを読み取る
# チャットデータからtimestampUsecを読み取る
gettime = _get_timestamp(action)
#時刻のないデータだった場合は次の行のデータで読み取り試行
# 時刻のないデータだった場合は次の行のデータで読み取り試行
if gettime is None:
continue
#最初に有効な時刻を持つデータのtimestampをstarttimeに設定
# 最初に有効な時刻を持つデータのtimestampをstarttimeに設定
if starttime is None:
starttime = gettime
#最後のtimestampを設定(途中で時刻のないデータの場合もあるので上書きしていく)
# 最後のtimestampを設定(途中で時刻のないデータの場合もあるので上書きしていく)
endtime = gettime
#チャットの数をインクリメント
# チャットの数をインクリメント
counter += 1
#チャット速度用のデータをRingQueueに送る
# チャット速度用のデータをRingQueueに送る
if starttime is None or endtime is None:
_put_emptydata()
return
self.put({
'chat_count':counter,
'starttime':int(starttime/1000000),
'endtime':int(endtime/1000000)
'chat_count': counter,
'starttime': int(starttime / 1000000),
'endtime': int(endtime / 1000000)
})

View File

@@ -15,10 +15,12 @@ items_sticker = [
'liveChatPaidStickerRenderer'
]
class SuperchatCalculator(ChatProcessor):
"""
Calculate the amount of SuperChat by currency.
"""
def __init__(self):
self.results = {}
@@ -34,14 +36,16 @@ class SuperchatCalculator(ChatProcessor):
return self.results
for component in chat_components:
chatdata = component.get('chatdata')
if chatdata is None: continue
if chatdata is None:
continue
for action in chatdata:
renderer = self._get_item(action, items_paid) or \
self._get_item(action, items_sticker)
if renderer is None: continue
self._get_item(action, items_sticker)
if renderer is None:
continue
symbol, amount = self._parse(renderer)
self.results.setdefault(symbol,0)
self.results[symbol]+=amount
self.results.setdefault(symbol, 0)
self.results[symbol] += amount
return self.results
def _parse(self, renderer):
@@ -49,7 +53,7 @@ class SuperchatCalculator(ChatProcessor):
m = superchat_regex.search(purchase_amount_text)
if m:
symbol = m.group(1)
amount = float(m.group(2).replace(',',''))
amount = float(m.group(2).replace(',', ''))
else:
symbol = ""
amount = 0.0
@@ -69,6 +73,3 @@ class SuperchatCalculator(ChatProcessor):
continue
return None
return dict_body

View File

@@ -4,9 +4,10 @@ import re
from .chat_processor import ChatProcessor
from .default.processor import DefaultProcessor
PATTERN = re.compile(r"(.*)\(([0-9]+)\)$")
fmt_headers = ['datetime','elapsed','authorName','message','superchatAmount'
,'authorType','authorChannel']
PATTERN = re.compile(r"(.*)\(([0-9]+)\)$")
fmt_headers = ['datetime', 'elapsed', 'authorName', 'message',
'superchatAmount', 'authorType', 'authorChannel']
class TSVArchiver(ChatProcessor):
'''
@@ -16,7 +17,7 @@ class TSVArchiver(ChatProcessor):
def __init__(self, save_path):
super().__init__()
self.save_path = self._checkpath(save_path)
with open(self.save_path, mode='a', encoding = 'utf-8') as f:
with open(self.save_path, mode='a', encoding='utf-8') as f:
writer = csv.writer(f, delimiter='\t')
writer.writerow(fmt_headers)
self.processor = DefaultProcessor()
@@ -28,14 +29,14 @@ class TSVArchiver(ChatProcessor):
newpath = filepath
counter = 0
while os.path.exists(newpath):
match = re.search(PATTERN,body)
match = re.search(PATTERN, body)
if match:
counter=int(match[2])+1
counter = int(match[2]) + 1
num_with_bracket = f'({str(counter)})'
body = f'{match[1]}{num_with_bracket}'
else:
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
def process(self, chat_components: list):
@@ -48,10 +49,10 @@ class TSVArchiver(ChatProcessor):
total_lines : int :
count of total lines written to the file.
"""
if chat_components is None or len (chat_components) == 0:
if chat_components is None or len(chat_components) == 0:
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:
writer = csv.writer(f, delimiter='\t')
chats = self.processor.process(chat_components).items
for c in chats:
@@ -64,7 +65,3 @@ class TSVArchiver(ChatProcessor):
c.author.type,
c.author.channelId
])

View File

@@ -17,7 +17,8 @@ REPLAY_URL = "https://www.youtube.com/live_chat_replay/" \
"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):
"""
Split section from `start` to `end` into `count` pieces,
and returns the beginning of each piece.
@@ -28,41 +29,43 @@ def _split(start, end, count, min_interval_sec = 120):
--------
List of the offset of each block's first chat data.
"""
if not (isinstance(start,int) or isinstance(start,float)) or \
not (isinstance(end,int) or isinstance(end,float)):
if not (isinstance(start, int) or isinstance(start, float)) or \
not (isinstance(end, int) or isinstance(end, float)):
raise ValueError("start/end must be int or float")
if not isinstance(count,int):
if not isinstance(count, int):
raise ValueError("count must be int")
if start>end:
if start > end:
raise ValueError("end must be equal to or greater than start.")
if count<1:
if count < 1:
raise ValueError("count must be equal to or greater than 1.")
if (end-start)/count < min_interval_sec:
count = int((end-start)/min_interval_sec)
if count == 0 : count = 1
interval= (end-start)/count
if (end - start) / count < min_interval_sec:
count = int((end - start) / min_interval_sec)
if count == 0:
count = 1
interval = (end - start) / count
if count == 1:
return [start]
return sorted( list(set( [int(start + interval*j)
for j in range(count) ])))
return sorted(list(set([int(start + interval * j)
for j in range(count)])))
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:
tasks = [_create_block(session, video_id, seektime, callback)
for seektime in _split(-1, duration, div)]
tasks = [_create_block(session, video_id, seektime, callback)
for seektime in _split(-1, duration, div)]
return await asyncio.gather(*tasks)
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"
for _ in range(MAX_RETRY_COUNT):
try :
async with session.get(url, headers = headers) as resp:
try:
async with session.get(url, headers=headers) as resp:
text = await resp.text()
next_continuation, actions = parser.parse(json.loads(text))
break
@@ -76,12 +79,12 @@ def ready_blocks(video_id, duration, div, callback):
first = parser.get_offset(actions[0])
last = parser.get_offset(actions[-1])
if callback:
callback(actions,last-first)
callback(actions, last - first)
return Block(
continuation = next_continuation,
chat_data = actions,
first = first,
last = last
continuation=next_continuation,
chat_data=actions,
first=first,
last=last
)
"""
@@ -92,13 +95,14 @@ def ready_blocks(video_id, duration, div, callback):
_get_blocks(video_id, duration, div, callback))
return blocks
def fetch_patch(callback, blocks, video_id):
async def _allocate_workers():
workers = [
ExtractWorker(
fetch = _fetch, block = block,
blocks = blocks, video_id = video_id
fetch=_fetch, block=block,
blocks=blocks, video_id=video_id
)
for block in blocks
]
@@ -106,11 +110,11 @@ def fetch_patch(callback, blocks, video_id):
tasks = [worker.run(session) for worker in workers]
return await asyncio.gather(*tasks)
async def _fetch(continuation,session) -> Patch:
async def _fetch(continuation, session) -> Patch:
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()
continuation, actions = parser.parse(json.loads(chat_json))
break
@@ -126,7 +130,7 @@ def fetch_patch(callback, blocks, video_id):
if callback:
callback(actions, last - first)
return Patch(actions, continuation, first, last)
return Patch(continuation = continuation)
return Patch(continuation=continuation)
"""
allocate workers and assign blocks.
@@ -137,10 +141,11 @@ def fetch_patch(callback, blocks, video_id):
except CancelledError:
pass
async def _shutdown():
print("\nshutdown...")
tasks = [t for t in asyncio.all_tasks()
if t is not asyncio.current_task()]
if t is not asyncio.current_task()]
for task in tasks:
task.cancel()
try:
@@ -148,7 +153,7 @@ async def _shutdown():
except asyncio.CancelledError:
pass
def cancel():
loop = asyncio.get_event_loop()
loop.create_task(_shutdown())

View File

@@ -1,4 +1,3 @@
from . import parser
class Block:
"""Block object represents something like a box
to join chunk of chatdata.
@@ -40,12 +39,12 @@ class Block:
while True, this block is excluded from duplicate split procedure.
"""
__slots__ = ['first','last','end','continuation','chat_data','remaining',
'done','is_last','during_split']
__slots__ = ['first', 'last', 'end', 'continuation', 'chat_data', 'remaining',
'done', 'is_last', 'during_split']
def __init__(self, first = 0, last = 0, end = 0,
continuation = '', chat_data = [], is_last = False,
during_split = False):
def __init__(self, first=0, last=0, end=0,
continuation='', chat_data=[], is_last=False,
during_split=False):
self.first = first
self.last = last
self.end = end

View File

@@ -1,7 +1,8 @@
from . import parser
def check_duplicate(chatdata):
max_range = len(chatdata)-1
max_range = len(chatdata) - 1
tbl_offset = [None] * max_range
tbl_id = [None] * max_range
tbl_type = [None] * max_range
@@ -15,27 +16,25 @@ def check_duplicate(chatdata):
def is_duplicate(i, j):
return (
tbl_offset[i] == tbl_offset[j]
and
tbl_id[i] == tbl_id[j]
and
tbl_type[i] == tbl_type[j]
and tbl_id[i] == tbl_id[j]
and tbl_type[i] == tbl_type[j]
)
print("creating table...")
create_table(chatdata,max_range)
create_table(chatdata, max_range)
print("searching duplicate data...")
return [{ "i":{
"index" : i, "id" : parser.get_id(chatdata[i]),
"offsetTime" : parser.get_offset(chatdata[i]),
"type" : parser.get_type(chatdata[i])
},
"j":{
"index" : j, "id" : parser.get_id(chatdata[j]),
"offsetTime" : parser.get_offset(chatdata[j]),
"type" : parser.get_type(chatdata[j])
}
}
for i in range(max_range) for j in range(i+1,max_range)
if is_duplicate(i,j)]
return [{"i": {
"index": i, "id": parser.get_id(chatdata[i]),
"offsetTime": parser.get_offset(chatdata[i]),
"type": parser.get_type(chatdata[i])
},
"j":{
"index": j, "id": parser.get_id(chatdata[j]),
"offsetTime": parser.get_offset(chatdata[j]),
"type": parser.get_type(chatdata[j])
}
}
for i in range(max_range) for j in range(i + 1, max_range)
if is_duplicate(i, j)]
def check_duplicate_offset(chatdata):
@@ -53,85 +52,86 @@ def check_duplicate_offset(chatdata):
def is_duplicate(i, j):
return (
tbl_offset[i] == tbl_offset[j]
and
tbl_id[i] == tbl_id[j]
and tbl_id[i] == tbl_id[j]
)
print("creating table...")
create_table(chatdata,max_range)
create_table(chatdata, max_range)
print("searching duplicate data...")
return [{
"index" : i, "id" : tbl_id[i],
"offsetTime" : tbl_offset[i],
"type:" : tbl_type[i]
}
for i in range(max_range-1)
if is_duplicate(i,i+1)]
"index": i, "id": tbl_id[i],
"offsetTime": tbl_offset[i],
"type:": tbl_type[i]
}
for i in range(max_range - 1)
if is_duplicate(i, i + 1)]
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):
if len(blocks[index].chat_data) == 0:
return True
elif len(blocks[index+1].chat_data) == 0:
elif len(blocks[index + 1].chat_data) == 0:
return False
id_0 = parser.get_id(blocks[index].chat_data[0])
id_1 = parser.get_id(blocks[index+1].chat_data[0])
id_1 = parser.get_id(blocks[index + 1].chat_data[0])
type_0 = parser.get_type(blocks[index].chat_data[0])
type_1 = parser.get_type(blocks[index+1].chat_data[0])
type_1 = parser.get_type(blocks[index + 1].chat_data[0])
return (
blocks[index].first == blocks[index+1].first
and
id_0 == id_1
and
type_0 == type_1
blocks[index].first == blocks[index + 1].first
and id_0 == id_1
and type_0 == type_1
)
ret = [blocks[i] for i in range(len(blocks)-1)
if (len(blocks[i].chat_data)>0 and
not is_duplicate_head(i) )]
ret = [blocks[i] for i in range(len(blocks) - 1)
if (len(blocks[i].chat_data) > 0
and not is_duplicate_head(i))]
ret.append(blocks[-1])
return ret
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):
if len(blocks[index].chat_data) == 0:
return True
elif len(blocks[index-1].chat_data) == 0:
elif len(blocks[index - 1].chat_data) == 0:
return False
id_0 = parser.get_id(blocks[index-1].chat_data[-1])
id_0 = parser.get_id(blocks[index - 1].chat_data[-1])
id_1 = parser.get_id(blocks[index].chat_data[-1])
type_0 = parser.get_type(blocks[index-1].chat_data[-1])
type_0 = parser.get_type(blocks[index - 1].chat_data[-1])
type_1 = parser.get_type(blocks[index].chat_data[-1])
return (
blocks[index-1].last == blocks[index].last
and
id_0 == id_1
and
type_0 == type_1
blocks[index - 1].last == blocks[index].last
and id_0 == id_1
and type_0 == type_1
)
ret = [blocks[i] for i in range(0,len(blocks))
if i == 0 or not is_duplicate_tail(i) ]
ret = [blocks[i] for i in range(0, len(blocks))
if i == 0 or not is_duplicate_tail(i)]
return ret
def remove_overlap(blocks):
"""
Fix overlapped blocks after ready_blocks().
Align the last offset of each block to the first offset
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:
if block.is_last:
break
if len(block.chat_data)==0:
if len(block.chat_data) == 0:
continue
block_end = block.end
if block.last >= block_end:
@@ -140,14 +140,14 @@ def remove_overlap(blocks):
break
block.chat_data.pop()
block.last = parser.get_offset(line)
block.remaining=0
block.done=True
block.remaining = 0
block.done = True
block.continuation = None
return blocks
def _dump(blocks):
print(f"---------- first last end---")
for i,block in enumerate(blocks):
print(f"block[{i:3}] {block.first:>10} {block.last:>10} {block.end:>10}")
print("---------- first last end---")
for i, block in enumerate(blocks):
print(
f"block[{i:3}] {block.first:>10} {block.last:>10} {block.end:>10}")

View File

@@ -1,20 +1,21 @@
from . import asyncdl
from . import duplcheck
from . import parser
from .. videoinfo import VideoInfo
from ... import config
from ... exceptions import InvalidVideoIdException
from ... util.extract_video_id import extract_video_id
logger = config.logger(__name__)
headers=config.headers
headers = config.headers
class Extractor:
def __init__(self, video_id, div = 1, callback = None, processor = None):
if not isinstance(div ,int) or div < 1:
def __init__(self, video_id, div=1, callback=None, processor=None):
if not isinstance(div, int) or div < 1:
raise ValueError('div must be positive integer.')
elif div > 10:
div = 10
self.video_id = video_id
self.video_id = extract_video_id(video_id)
self.div = div
self.callback = callback
self.processor = processor
@@ -40,10 +41,11 @@ class Extractor:
return self
def _set_block_end(self):
for i in range(len(self.blocks)-1):
self.blocks[i].end = self.blocks[i+1].first
self.blocks[-1].end = self.duration*1000
self.blocks[-1].is_last =True
if len(self.blocks) > 0:
for i in range(len(self.blocks) - 1):
self.blocks[i].end = self.blocks[i + 1].first
self.blocks[-1].end = self.duration * 1000
self.blocks[-1].is_last = True
return self
def _remove_overlap(self):
@@ -81,11 +83,14 @@ class Extractor:
return []
data = self._execute_extract_operations()
if self.processor is None:
return data
return self.processor.process(
[{'video_id':None,'timeout':1,'chatdata' : (action
["replayChatItemAction"]["actions"][0] for action in data)}]
)
return data
ret = self.processor.process(
[{'video_id': None,
'timeout': 1,
'chatdata': (action["replayChatItemAction"]["actions"][0] for action in data)}]
)
self.processor.finalize()
return ret
def cancel(self):
asyncdl.cancel()

View File

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

View File

@@ -2,17 +2,19 @@ from . import parser
from . block import Block
from typing import NamedTuple
class Patch(NamedTuple):
"""
Patch represents chunk of chat data
which is fetched by asyncdl.fetch_patch._fetch().
"""
chats : list = []
continuation : str = None
first : int = None
last : int = None
chats: list = []
continuation: str = None
first: int = None
last: int = None
def fill(block:Block, patch:Patch):
def fill(block: Block, patch: Patch):
block_end = block.end
if patch.last < block_end or block.is_last:
set_patch(block, patch)
@@ -23,15 +25,15 @@ def fill(block:Block, patch:Patch):
break
patch.chats.pop()
set_patch(block, patch._replace(
continuation = None,
last = line_offset
)
continuation=None,
last=line_offset
)
block.remaining=0
block.done=True
)
block.remaining = 0
block.done = True
def split(parent_block:Block, child_block:Block, patch:Patch):
def split(parent_block: Block, child_block: Block, patch: Patch):
parent_block.during_split = False
if patch.first <= parent_block.last:
''' When patch overlaps with parent_block,
@@ -46,9 +48,8 @@ def split(parent_block:Block, child_block:Block, patch:Patch):
fill(child_block, patch)
def set_patch(block:Block, patch:Patch):
def set_patch(block: Block, patch: Patch):
block.continuation = patch.continuation
block.chat_data.extend(patch.chats)
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 . patch import Patch, fill, split
from . patch import fill, split
from ... paramgen import arcparam
class ExtractWorker:
"""
ExtractWorker associates a download session with a block.
@@ -28,7 +28,7 @@ class ExtractWorker:
"""
__slots__ = ['block', 'fetch', 'blocks', 'video_id', 'parent_block']
def __init__(self, fetch, block, blocks, video_id ):
def __init__(self, fetch, block, blocks, video_id):
self.block = block
self.fetch = fetch
self.blocks = blocks
@@ -54,26 +54,28 @@ class ExtractWorker:
self.block.done = True
self.block = _search_new_block(self)
def _search_new_block(worker) -> Block:
index, undone_block = _get_undone_block(worker.blocks)
if undone_block is None:
return Block(continuation = None)
mean = (undone_block.last + undone_block.end)/2
continuation = arcparam.getparam(worker.video_id, seektime = mean/1000)
return Block(continuation=None)
mean = (undone_block.last + undone_block.end) / 2
continuation = arcparam.getparam(worker.video_id, seektime=mean / 1000)
worker.parent_block = undone_block
worker.parent_block.during_split = True
new_block = Block(
end = undone_block.end,
chat_data = [],
continuation = continuation,
during_split = True,
is_last = worker.parent_block.is_last)
end=undone_block.end,
chat_data=[],
continuation=continuation,
during_split=True,
is_last=worker.parent_block.is_last)
'''swap last block'''
if worker.parent_block.is_last:
worker.parent_block.is_last = False
worker.blocks.insert(index+1, new_block)
worker.blocks.insert(index + 1, new_block)
return new_block
def _get_undone_block(blocks) -> (int, Block):
min_interval_ms = 120000
max_remaining = 0

View File

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

View File

@@ -2,14 +2,14 @@ import json
import re
import requests
from .. import config
from .. import util
from ..exceptions import InvalidVideoIdException
from ..util.extract_video_id import extract_video_id
headers = config.headers
pattern = re.compile(r"yt\.setConfig\({'PLAYER_CONFIG': ({.*})}\);")
item_channel_id =[
item_channel_id = [
"videoDetails",
"embeddedPlayerOverlayVideoDetailsRenderer",
"channelThumbnailEndpoint",
@@ -29,7 +29,7 @@ item_response = [
"embedded_player_response"
]
item_author_image =[
item_author_image = [
"videoDetails",
"embeddedPlayerOverlayVideoDetailsRenderer",
"channelThumbnail",
@@ -63,6 +63,7 @@ item_moving_thumbnail = [
"url"
]
class VideoInfo:
'''
VideoInfo object retrieves YouTube video information.
@@ -76,20 +77,21 @@ class VideoInfo:
InvalidVideoIdException :
Occurs when video_id does not exist on YouTube.
'''
def __init__(self, video_id):
self.video_id = video_id
text = self._get_page_text(video_id)
self.video_id = extract_video_id(video_id)
text = self._get_page_text(self.video_id)
self._parse(text)
def _get_page_text(self, video_id):
url = f"https://www.youtube.com/embed/{video_id}"
resp = requests.get(url, headers = headers)
resp = requests.get(url, headers=headers)
resp.raise_for_status()
return resp.text
def _parse(self, text):
result = re.search(pattern, text)
res= json.loads(result.group(1))
res = json.loads(result.group(1))
response = self._get_item(res, item_response)
if response is None:
self._check_video_is_private(res.get("args"))
@@ -98,7 +100,7 @@ class VideoInfo:
raise InvalidVideoIdException(
f"No renderer found in video_id: [{self.video_id}].")
def _check_video_is_private(self,args):
def _check_video_is_private(self, args):
if args and args.get("video_id"):
raise InvalidVideoIdException(
f"video_id [{self.video_id}] is private or deleted.")
@@ -131,7 +133,7 @@ class VideoInfo:
def get_title(self):
if self._renderer.get("title"):
return [''.join(run["text"])
for run in self._renderer["title"]["runs"]][0]
for run in self._renderer["title"]["runs"]][0]
return None
def get_channel_id(self):

View File

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

View File

@@ -0,0 +1,25 @@
import re
from .. exceptions import InvalidVideoIdException
PATTERN = re.compile(r"((?<=(v|V)/)|(?<=be/)|(?<=(\?|\&)v=)|(?<=embed/))([\w-]+)")
YT_VIDEO_ID_LENGTH = 11
def extract_video_id(url_or_id: str) -> str:
ret = ''
if type(url_or_id) != str:
raise TypeError(f"{url_or_id}: URL or VideoID must be str, but {type(url_or_id)} is passed.")
if len(url_or_id) == YT_VIDEO_ID_LENGTH:
return url_or_id
match = re.search(PATTERN, url_or_id)
if match is None:
raise InvalidVideoIdException(url_or_id)
try:
ret = match.group(4)
except IndexError:
raise InvalidVideoIdException(url_or_id)
if ret is None or len(ret) != YT_VIDEO_ID_LENGTH:
raise InvalidVideoIdException(url_or_id)
return ret

View File

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

View File

@@ -1,28 +1,31 @@
import pytest
from pytchat.parser.live import Parser
import json
import requests
import pytchat.config as config
import requests, json
from pytchat.paramgen import arcparam
from pytchat.parser.live import Parser
def test_arcparam_0(mocker):
param = arcparam.getparam("01234567890",-1)
assert param == "op2w0wRyGjxDZzhhRFFvTE1ERXlNelExTmpjNE9UQWFFLXFvM2JrQkRRb0xNREV5TXpRMU5qYzRPVEFnQVElM0QlM0QoADAAOABAAEgEUhwIABAAGAAgACoOc3RhdGljY2hlY2tzdW1AAFgDYAFoAHIECAEQAHgA"
param = arcparam.getparam("01234567890", -1)
assert param == "op2w0wQmGhxDZzhLRFFvTE1ERXlNelExTmpjNE9UQWdBUT09SARgAXICCAE%3D"
def test_arcparam_1(mocker):
param = arcparam.getparam("01234567890", seektime = 100000)
assert param == "op2w0wR3GjxDZzhhRFFvTE1ERXlNelExTmpjNE9UQWFFLXFvM2JrQkRRb0xNREV5TXpRMU5qYzRPVEFnQVElM0QlM0QogNDbw_QCMAA4AEAASANSHAgAEAAYACAAKg5zdGF0aWNjaGVja3N1bUAAWANgAWgAcgQIARAAeAA%3D"
param = arcparam.getparam("01234567890", seektime=100000)
assert param == "op2w0wQtGhxDZzhLRFFvTE1ERXlNelExTmpjNE9UQWdBUT09KIDQ28P0AkgDYAFyAggB"
def test_arcparam_2(mocker):
param = arcparam.getparam("SsjCnHOk-Sk")
url=f"https://www.youtube.com/live_chat_replay/get_live_chat_replay?continuation={param}&pbj=1"
resp = requests.Session().get(url,headers = config.headers)
param = arcparam.getparam("SsjCnHOk-Sk", seektime=100)
url = f"https://www.youtube.com/live_chat_replay/get_live_chat_replay?continuation={param}&pbj=1"
resp = requests.Session().get(url, headers=config.headers)
jsn = json.loads(resp.text)
parser = Parser(is_replay=True)
contents= parser.get_contents(jsn)
contents = parser.get_contents(jsn)
_ , chatdata = parser.parse(contents)
test_id = chatdata[0]["addChatItemAction"]["item"]["liveChatTextMessageRenderer"]["id"]
assert test_id == "CjoKGkNMYXBzZTdudHVVQ0Zjc0IxZ0FkTnFnQjVREhxDSnlBNHV2bnR1VUNGV0dnd2dvZDd3NE5aZy0w"
def test_arcparam_3(mocker):
param = arcparam.getparam("01234567890")
assert param == "op2w0wRyGjxDZzhhRFFvTE1ERXlNelExTmpjNE9UQWFFLXFvM2JrQkRRb0xNREV5TXpRMU5qYzRPVEFnQVElM0QlM0QoATAAOABAAEgDUhwIABAAGAAgACoOc3RhdGljY2hlY2tzdW1AAFgDYAFoAHIECAEQAHgA"
assert param == "op2w0wQmGhxDZzhLRFFvTE1ERXlNelExTmpjNE9UQWdBUT09SARgAXICCAE%3D"

View File

@@ -1,40 +1,41 @@
import pytest
from pytchat.tool.mining import parser
import pytchat.config as config
import requests, json
import requests
import json
from pytchat.paramgen import arcparam_mining as arcparam
def test_arcparam_e(mocker):
try:
arcparam.getparam("01234567890",-1)
arcparam.getparam("01234567890", -1)
assert False
except ValueError:
assert True
def test_arcparam_0(mocker):
param = arcparam.getparam("01234567890",0)
param = arcparam.getparam("01234567890", 0)
assert param =="op2w0wQsGiBDZzhhRFFvTE1ERXlNelExTmpjNE9UQWdBUSUzRCUzREABYARyAggBeAE%3D"
assert param == "op2w0wQsGiBDZzhhRFFvTE1ERXlNelExTmpjNE9UQWdBUSUzRCUzREABYARyAggBeAE%3D"
def test_arcparam_1(mocker):
param = arcparam.getparam("01234567890", seektime = 100000)
param = arcparam.getparam("01234567890", seektime=100000)
print(param)
assert param == "op2w0wQzGiBDZzhhRFFvTE1ERXlNelExTmpjNE9UQWdBUSUzRCUzREABWgUQgMLXL2AEcgIIAXgB"
def test_arcparam_2(mocker):
param = arcparam.getparam("PZz9NB0-Z64",1)
url=f"https://www.youtube.com/live_chat_replay?continuation={param}&playerOffsetMs=1000&pbj=1"
resp = requests.Session().get(url,headers = config.headers)
param = arcparam.getparam("PZz9NB0-Z64", 1)
url = f"https://www.youtube.com/live_chat_replay?continuation={param}&playerOffsetMs=1000&pbj=1"
resp = requests.Session().get(url, headers=config.headers)
jsn = json.loads(resp.text)
_ , chatdata = parser.parse(jsn[1])
_, chatdata = parser.parse(jsn[1])
test_id = chatdata[0]["addChatItemAction"]["item"]["liveChatPaidMessageRenderer"]["id"]
print(test_id)
assert test_id == "ChwKGkNKSGE0YnFJeWVBQ0ZWcUF3Z0VkdGIwRm9R"
def test_arcparam_3(mocker):
param = arcparam.getparam("01234567890")
assert param == "op2w0wQsGiBDZzhhRFFvTE1ERXlNelExTmpjNE9UQWdBUSUzRCUzREABYARyAggBeAE%3D"

View File

@@ -1,17 +1,6 @@
import json
import pytest
import asyncio
import aiohttp
from pytchat.parser.live import Parser
from pytchat.processors.compatible.processor import CompatibleProcessor
from pytchat.exceptions import (
NoLivechatRendererException, NoYtinitialdataException,
ResponseContextError, NoContentsException)
from pytchat.processors.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)
@@ -31,21 +20,23 @@ def test_textmessage(mocker):
ret = processor.process([data])
assert ret["kind"] == "youtube#liveChatMessageListResponse"
assert ret["pollingIntervalMillis"] == data["timeout"]*1000
assert ret["pollingIntervalMillis"] == data["timeout"] * 1000
assert ret.keys() == {
"kind", "etag", "pageInfo", "nextPageToken", "pollingIntervalMillis", "items"
"kind", "etag", "pageInfo", "nextPageToken", "pollingIntervalMillis", "items"
}
assert ret["pageInfo"].keys() == {
"totalResults", "resultsPerPage"
"totalResults", "resultsPerPage"
}
assert ret["items"][0].keys() == {
"kind", "etag", "id", "snippet", "authorDetails"
"kind", "etag", "id", "snippet", "authorDetails"
}
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() == {
'channelId', 'channelUrl', 'displayName', 'profileImageUrl', 'isVerified', 'isChatOwner', 'isChatSponsor', 'isChatModerator'
'channelId', 'channelUrl', 'displayName', 'profileImageUrl', 'isVerified', 'isChatOwner', 'isChatSponsor',
'isChatModerator'
}
assert ret["items"][0]["snippet"]["textMessageDetails"].keys() == {
'messageText'
@@ -69,22 +60,23 @@ def test_newsponcer(mocker):
ret = processor.process([data])
assert ret["kind"] == "youtube#liveChatMessageListResponse"
assert ret["pollingIntervalMillis"] == data["timeout"]*1000
assert ret["pollingIntervalMillis"] == data["timeout"] * 1000
assert ret.keys() == {
"kind", "etag", "pageInfo", "nextPageToken", "pollingIntervalMillis", "items"
"kind", "etag", "pageInfo", "nextPageToken", "pollingIntervalMillis", "items"
}
assert ret["pageInfo"].keys() == {
"totalResults", "resultsPerPage"
"totalResults", "resultsPerPage"
}
assert ret["items"][0].keys() == {
"kind", "etag", "id", "snippet", "authorDetails"
"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'
'channelId', 'channelUrl', 'displayName', 'profileImageUrl', 'isVerified', 'isChatOwner', 'isChatSponsor',
'isChatModerator'
}
assert "LCC." in ret["items"][0]["id"]
assert ret["items"][0]["snippet"]["type"] == "newSponsorEvent"
@@ -105,22 +97,23 @@ def test_newsponcer_rev(mocker):
ret = processor.process([data])
assert ret["kind"] == "youtube#liveChatMessageListResponse"
assert ret["pollingIntervalMillis"] == data["timeout"]*1000
assert ret["pollingIntervalMillis"] == data["timeout"] * 1000
assert ret.keys() == {
"kind", "etag", "pageInfo", "nextPageToken", "pollingIntervalMillis", "items"
"kind", "etag", "pageInfo", "nextPageToken", "pollingIntervalMillis", "items"
}
assert ret["pageInfo"].keys() == {
"totalResults", "resultsPerPage"
"totalResults", "resultsPerPage"
}
assert ret["items"][0].keys() == {
"kind", "etag", "id", "snippet", "authorDetails"
"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'
'channelId', 'channelUrl', 'displayName', 'profileImageUrl', 'isVerified', 'isChatOwner', 'isChatSponsor',
'isChatModerator'
}
assert "LCC." in ret["items"][0]["id"]
assert ret["items"][0]["snippet"]["type"] == "newSponsorEvent"
@@ -141,21 +134,23 @@ def test_superchat(mocker):
ret = processor.process([data])
assert ret["kind"] == "youtube#liveChatMessageListResponse"
assert ret["pollingIntervalMillis"] == data["timeout"]*1000
assert ret["pollingIntervalMillis"] == data["timeout"] * 1000
assert ret.keys() == {
"kind", "etag", "pageInfo", "nextPageToken", "pollingIntervalMillis", "items"
"kind", "etag", "pageInfo", "nextPageToken", "pollingIntervalMillis", "items"
}
assert ret["pageInfo"].keys() == {
"totalResults", "resultsPerPage"
"totalResults", "resultsPerPage"
}
assert ret["items"][0].keys() == {
"kind", "etag", "id", "snippet", "authorDetails"
"kind", "etag", "id", "snippet", "authorDetails"
}
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() == {
'channelId', 'channelUrl', 'displayName', 'profileImageUrl', 'isVerified', 'isChatOwner', 'isChatSponsor', 'isChatModerator'
'channelId', 'channelUrl', 'displayName', 'profileImageUrl', 'isVerified', 'isChatOwner', 'isChatSponsor',
'isChatModerator'
}
assert ret["items"][0]["snippet"]["superChatDetails"].keys() == {
'amountMicros', 'currency', 'amountDisplayString', 'tier', 'backgroundColor'

View File

@@ -0,0 +1,228 @@
import json
from pytchat.parser.live import Parser
from pytchat.processors.default.processor import DefaultProcessor
def test_textmessage(mocker):
'''text message'''
processor = DefaultProcessor()
parser = Parser(is_replay=False)
_json = _open_file("tests/testdata/default/textmessage.json")
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
data = {
"video_id": "",
"timeout": 7,
"chatdata": chatdata
}
ret = processor.process([data]).items[0]
assert ret.chattype == "textMessage"
assert ret.id == "dummy_id"
assert ret.message == "dummy_message"
assert ret.timestamp == 1570678496000
assert ret.datetime == "2019-10-10 12:34:56"
assert ret.author.name == "author_name"
assert ret.author.channelId == "author_channel_id"
assert ret.author.channelUrl == "http://www.youtube.com/channel/author_channel_id"
assert ret.author.imageUrl == "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg"
assert ret.author.badgeUrl == ""
assert ret.author.isVerified is False
assert ret.author.isChatOwner is False
assert ret.author.isChatSponsor is False
assert ret.author.isChatModerator is False
def test_textmessage_replay_member(mocker):
'''text message replay member'''
processor = DefaultProcessor()
parser = Parser(is_replay=True)
_json = _open_file("tests/testdata/default/replay_member_text.json")
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
data = {
"video_id": "",
"timeout": 7,
"chatdata": chatdata
}
ret = processor.process([data]).items[0]
assert ret.chattype == "textMessage"
assert ret.type == "textMessage"
assert ret.id == "dummy_id"
assert ret.message == "dummy_message"
assert ret.messageEx == ["dummy_message"]
assert ret.timestamp == 1570678496000
assert ret.datetime == "2019-10-10 12:34:56"
assert ret.elapsedTime == "1:23:45"
assert ret.author.name == "author_name"
assert ret.author.channelId == "author_channel_id"
assert ret.author.channelUrl == "http://www.youtube.com/channel/author_channel_id"
assert ret.author.imageUrl == "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg"
assert ret.author.badgeUrl == "https://yt3.ggpht.com/X=s16-c-k"
assert ret.author.isVerified is False
assert ret.author.isChatOwner is False
assert ret.author.isChatSponsor is True
assert ret.author.isChatModerator is False
def test_superchat(mocker):
'''superchat'''
processor = DefaultProcessor()
parser = Parser(is_replay=False)
_json = _open_file("tests/testdata/default/superchat.json")
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
data = {
"video_id": "",
"timeout": 7,
"chatdata": chatdata
}
ret = processor.process([data]).items[0]
print(json.dumps(chatdata, ensure_ascii=False))
assert ret.chattype == "superChat"
assert ret.type == "superChat"
assert ret.id == "dummy_id"
assert ret.message == "dummy_message"
assert ret.messageEx == ["dummy_message"]
assert ret.timestamp == 1570678496000
assert ret.datetime == "2019-10-10 12:34:56"
assert ret.elapsedTime == ""
assert ret.amountValue == 800
assert ret.amountString == "¥800"
assert ret.currency == "JPY"
assert ret.bgColor == 4280150454
assert ret.author.name == "author_name"
assert ret.author.channelId == "author_channel_id"
assert ret.author.channelUrl == "http://www.youtube.com/channel/author_channel_id"
assert ret.author.imageUrl == "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg"
assert ret.author.badgeUrl == ""
assert ret.author.isVerified is False
assert ret.author.isChatOwner is False
assert ret.author.isChatSponsor is False
assert ret.author.isChatModerator is False
assert ret.colors.headerBackgroundColor == 4278239141
assert ret.colors.headerTextColor == 4278190080
assert ret.colors.bodyBackgroundColor == 4280150454
assert ret.colors.bodyTextColor == 4278190080
assert ret.colors.authorNameTextColor == 2315255808
assert ret.colors.timestampColor == 2147483648
def test_supersticker(mocker):
'''supersticker'''
processor = DefaultProcessor()
parser = Parser(is_replay=False)
_json = _open_file("tests/testdata/default/supersticker.json")
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
data = {
"video_id": "",
"timeout": 7,
"chatdata": chatdata
}
ret = processor.process([data]).items[0]
print(json.dumps(chatdata, ensure_ascii=False))
assert ret.chattype == "superSticker"
assert ret.type == "superSticker"
assert ret.id == "dummy_id"
assert ret.message == ""
assert ret.messageEx == []
assert ret.timestamp == 1570678496000
assert ret.datetime == "2019-10-10 12:34:56"
assert ret.elapsedTime == ""
assert ret.amountValue == 200
assert ret.amountString == "¥200"
assert ret.currency == "JPY"
assert ret.bgColor == 4278248959
assert ret.sticker == "https://lh3.googleusercontent.com/param_s=s72-rp"
assert ret.author.name == "author_name"
assert ret.author.channelId == "author_channel_id"
assert ret.author.channelUrl == "http://www.youtube.com/channel/author_channel_id"
assert ret.author.imageUrl == "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg"
assert ret.author.badgeUrl == ""
assert ret.author.isVerified is False
assert ret.author.isChatOwner is False
assert ret.author.isChatSponsor is False
assert ret.author.isChatModerator is False
assert ret.colors.backgroundColor == 4278237396
assert ret.colors.moneyChipBackgroundColor == 4278248959
assert ret.colors.moneyChipTextColor == 4278190080
assert ret.colors.authorNameTextColor == 3003121664
def test_sponsor(mocker):
'''sponsor(membership)'''
processor = DefaultProcessor()
parser = Parser(is_replay=False)
_json = _open_file("tests/testdata/default/newSponsor_current.json")
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
data = {
"video_id": "",
"timeout": 7,
"chatdata": chatdata
}
ret = processor.process([data]).items[0]
print(json.dumps(chatdata, ensure_ascii=False))
assert ret.chattype == "newSponsor"
assert ret.type == "newSponsor"
assert ret.id == "dummy_id"
assert ret.message == "新規メンバー"
assert ret.messageEx == ["新規メンバー"]
assert ret.timestamp == 1570678496000
assert ret.datetime == "2019-10-10 12:34:56"
assert ret.elapsedTime == ""
assert ret.bgColor == 0
assert ret.author.name == "author_name"
assert ret.author.channelId == "author_channel_id"
assert ret.author.channelUrl == "http://www.youtube.com/channel/author_channel_id"
assert ret.author.imageUrl == "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg"
assert ret.author.badgeUrl == "https://yt3.ggpht.com/X=s32-c-k"
assert ret.author.isVerified is False
assert ret.author.isChatOwner is False
assert ret.author.isChatSponsor is True
assert ret.author.isChatModerator is False
def test_sponsor_legacy(mocker):
'''lagacy sponsor(membership)'''
processor = DefaultProcessor()
parser = Parser(is_replay=False)
_json = _open_file("tests/testdata/default/newSponsor_lagacy.json")
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
data = {
"video_id": "",
"timeout": 7,
"chatdata": chatdata
}
ret = processor.process([data]).items[0]
print(json.dumps(chatdata, ensure_ascii=False))
assert ret.chattype == "newSponsor"
assert ret.type == "newSponsor"
assert ret.id == "dummy_id"
assert ret.message == "新規メンバー / ようこそ、author_name"
assert ret.messageEx == ["新規メンバー / ようこそ、author_name"]
assert ret.timestamp == 1570678496000
assert ret.datetime == "2019-10-10 12:34:56"
assert ret.elapsedTime == ""
assert ret.bgColor == 0
assert ret.author.name == "author_name"
assert ret.author.channelId == "author_channel_id"
assert ret.author.channelUrl == "http://www.youtube.com/channel/author_channel_id"
assert ret.author.imageUrl == "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg"
assert ret.author.badgeUrl == ""
assert ret.author.isVerified is False
assert ret.author.isChatOwner is False
assert ret.author.isChatSponsor is True
assert ret.author.isChatModerator is False
def _open_file(path):
with open(path, mode='r', encoding='utf-8') as f:
return f.read()

View File

@@ -0,0 +1,55 @@
from pytchat.util.extract_video_id import extract_video_id
from pytchat.exceptions import InvalidVideoIdException
VALID_TEST_PATTERNS = (
("ABC_EFG_IJK", "ABC_EFG_IJK"),
("vid_test_be", "vid_test_be"),
("https://www.youtube.com/watch?v=123_456_789", "123_456_789"),
("https://www.youtube.com/watch?v=123_456_789&t=123s", "123_456_789"),
("www.youtube.com/watch?v=123_456_789", "123_456_789"),
("watch?v=123_456_789", "123_456_789"),
("youtube.com/watch?v=123_456_789", "123_456_789"),
("http://youtu.be/ABC_EFG_IJK", "ABC_EFG_IJK"),
("youtu.be/ABC_EFG_IJK", "ABC_EFG_IJK"),
("https://www.youtube.com/watch?v=ABC_EFG_IJK&list=XYZ_ABC_12345&start_radio=1&t=1", "ABC_EFG_IJK"),
("https://www.youtube.com/embed/ABC_EFG_IJK", "ABC_EFG_IJK"),
("www.youtube.com/embed/ABC_EFG_IJK", "ABC_EFG_IJK"),
("youtube.com/embed/ABC_EFG_IJK", "ABC_EFG_IJK")
)
INVALID_TEST_PATTERNS = (
("", ""),
("0123456789", "0123456789"), # less than 11 letters id
("more_than_11_letter_string", "more_than_11_letter_string"),
("https://www.youtube.com/watch?v=more_than_11_letter_string", "more_than_11_letter_string"),
("https://www.youtube.com/channel/123_456_789", "123_456_789"),
)
TYPEERROR_TEST_PATTERNS = (
(100, 100), # not string
(["123_456_789"], "123_456_789"), # not string
)
def test_extract_valid_pattern():
for pattern in VALID_TEST_PATTERNS:
ret = extract_video_id(pattern[0])
assert ret == pattern[1]
def test_extract_invalid_pattern():
for pattern in INVALID_TEST_PATTERNS:
try:
extract_video_id(pattern[0])
assert False
except InvalidVideoIdException:
assert True
def test_extract_typeerror_pattern():
for pattern in TYPEERROR_TEST_PATTERNS:
try:
extract_video_id(pattern[0])
assert False
except TypeError:
assert True

View File

@@ -1,38 +1,30 @@
import pytest
from pytchat.parser.live import Parser
import json
import asyncio,aiohttp
from aioresponses import aioresponses
from pytchat.core_async.livechat import LiveChatAsync
from pytchat.exceptions import (
NoLivechatRendererException,NoYtinitialdataException,
ResponseContextError,NoContentsException)
from pytchat.exceptions import ResponseContextError
from pytchat.core_multithread.livechat import LiveChat
import unittest
from unittest import TestCase
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()
@aioresponses()
def test_Async(*mock):
vid=''
vid = '__test_id__'
_text = _open_file('tests/testdata/paramgen_firstread.json')
_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:
chat = LiveChatAsync(video_id='')
chat = LiveChatAsync(video_id='__test_id__')
assert chat.is_alive()
chat.terminate()
assert not chat.is_alive()
except ResponseContextError:
assert not chat.is_alive()
def test_MultiThread(mocker):
_text = _open_file('tests/testdata/paramgen_firstread.json')
_text = json.loads(_text)
@@ -41,13 +33,10 @@ def test_MultiThread(mocker):
responseMock.text = _text
mocker.patch('requests.Session.get').return_value = responseMock
try:
chat = LiveChatAsync(video_id='')
chat = LiveChatAsync(video_id='__test_id__')
assert chat.is_alive()
chat.terminate()
assert not chat.is_alive()
except ResponseContextError:
chat.terminate()
assert not chat.is_alive()

View File

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

View File

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

View File

@@ -1,17 +1,16 @@
import pytest
from pytchat.parser.live import Parser
import json
import asyncio,aiohttp
from aioresponses import aioresponses
from pytchat.exceptions import (
NoLivechatRendererException,NoYtinitialdataException,
ResponseContextError, NoContentsException)
from pytchat.exceptions import NoContents
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()
parser = Parser(is_replay = False)
parser = Parser(is_replay=False)
@aioresponses()
def test_finishedlive(*mock):
@@ -23,9 +22,10 @@ def test_finishedlive(*mock):
try:
parser.parse(parser.get_contents(_text))
assert False
except NoContentsException:
except NoContents:
assert True
@aioresponses()
def test_parsejson(*mock):
'''jsonを正常にパースできるか'''
@@ -37,8 +37,9 @@ def test_parsejson(*mock):
parser.parse(parser.get_contents(_text))
jsn = _text
timeout = jsn["response"]["continuationContents"]["liveChatContinuation"]["continuations"][0]["timedContinuationData"]["timeoutMs"]
continuation = jsn["response"]["continuationContents"]["liveChatContinuation"]["continuations"][0]["timedContinuationData"]["continuation"]
assert 5035 == timeout
assert "0ofMyAPiARp8Q2c4S0RRb0xhelJMZDBsWFQwdERkalFhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5YXpSTGQwbFhUMHREZGpRbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCiPz5-Os-PkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQgJqXjrPj5AJYA1CRwciOs-PkAli3pNq1k-PkAmgBggEECAEQAIgBAKABjbfnjrPj5AI%3D" == continuation
except:
continuation = jsn["response"]["continuationContents"]["liveChatContinuation"][
"continuations"][0]["timedContinuationData"]["continuation"]
assert timeout == 5035
assert continuation == "0ofMyAPiARp8Q2c4S0RRb0xhelJMZDBsWFQwdERkalFhUTZxNXdiMEJQUW83YUhSMGNITTZMeTkzZDNjdWVXOTFkSFZpWlM1amIyMHZiR2wyWlY5amFHRjBQM1k5YXpSTGQwbFhUMHREZGpRbWFYTmZjRzl3YjNWMFBURWdBZyUzRCUzRCiPz5-Os-PkAjAAOABAAUorCAAQABgAIAAqDnN0YXRpY2NoZWNrc3VtOgBAAEoCCAFQgJqXjrPj5AJYA1CRwciOs-PkAli3pNq1k-PkAmgBggEECAEQAIgBAKABjbfnjrPj5AI%3D"
except Exception:
assert False

View File

@@ -1,15 +1,9 @@
import json
import pytest
import asyncio,aiohttp
from pytchat.parser.live import Parser
from pytchat.processors.compatible.processor import CompatibleProcessor
from pytchat.exceptions import (
NoLivechatRendererException,NoYtinitialdataException,
ResponseContextError, NoContentsException)
from pytchat.processors.speed.calculator import SpeedCalculator
parser = Parser(is_replay =False)
parser = Parser(is_replay=False)
def test_speed_1(mocker):
'''test speed calculation with normal json.
@@ -23,13 +17,14 @@ def test_speed_1(mocker):
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
data = {
"video_id" : "",
"timeout" : 10,
"chatdata" : chatdata
"video_id": "",
"timeout": 10,
"chatdata": chatdata
}
ret = processor.process([data])
assert 30 == ret
def test_speed_2(mocker):
'''test speed calculation with no valid chat data.
'''
@@ -39,12 +34,13 @@ def test_speed_2(mocker):
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
data = {
"video_id" : "",
"timeout" : 10,
"chatdata" : chatdata
"video_id": "",
"timeout": 10,
"chatdata": chatdata
}
ret = processor.process([data])
assert 0 == ret
assert ret == 0
def test_speed_3(mocker):
'''test speed calculation with empty data.
@@ -55,14 +51,14 @@ def test_speed_3(mocker):
_, chatdata = parser.parse(parser.get_contents(json.loads(_json)))
data = {
"video_id" : "",
"timeout" : 10,
"chatdata" : chatdata
"video_id": "",
"timeout": 10,
"chatdata": chatdata
}
ret = processor.process([data])
assert 0 == ret
assert ret == 0
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()

View File

@@ -1,11 +1,12 @@
from pytchat.tool.videoinfo import VideoInfo
from pytchat.exceptions import InvalidVideoIdException
import pytest
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()
def _set_test_data(filepath, mocker):
_text = _open_file(filepath)
response_mock = mocker.Mock()
@@ -13,23 +14,25 @@ def _set_test_data(filepath, mocker):
response_mock.text = _text
mocker.patch('requests.get').return_value = response_mock
def test_archived_page(mocker):
_set_test_data('tests/testdata/videoinfo/archived_page.txt', mocker)
info = VideoInfo('test_id')
info = VideoInfo('__test_id__')
actual_thumbnail_url = 'https://i.ytimg.com/vi/fzI9FNjXQ0o/hqdefault.jpg'
assert info.video_id == 'test_id'
assert info.video_id == '__test_id__'
assert info.get_channel_name() == 'GitHub'
assert info.get_thumbnail() == actual_thumbnail_url
assert info.get_title() == 'GitHub Arctic Code Vault'
assert info.get_channel_id() == 'UC7c3Kb6jYCRj4JOHHZTxKsQ'
assert info.get_duration() == 148
def test_live_page(mocker):
_set_test_data('tests/testdata/videoinfo/live_page.txt', mocker)
info = VideoInfo('test_id')
info = VideoInfo('__test_id__')
'''live page :duration = 0'''
assert info.get_duration() == 0
assert info.video_id == 'test_id'
assert info.video_id == '__test_id__'
assert info.get_channel_name() == 'BGM channel'
assert info.get_thumbnail() == \
'https://i.ytimg.com/vi/fEvM-OUbaKs/hqdefault_live.jpg'
@@ -38,25 +41,26 @@ def test_live_page(mocker):
' - 24/7 Live Stream - Slow Jazz')
assert info.get_channel_id() == 'UCQINXHZqCU5i06HzxRkujfg'
def test_invalid_video_id(mocker):
'''Test case invalid video_id is specified.'''
_set_test_data(
'tests/testdata/videoinfo/invalid_video_id_page.txt', mocker)
try:
_ = VideoInfo('test_id')
_ = VideoInfo('__test_id__')
assert False
except InvalidVideoIdException:
assert True
def test_no_info(mocker):
'''Test case the video page has renderer, but no info.'''
_set_test_data(
'tests/testdata/videoinfo/no_info_page.txt', mocker)
info = VideoInfo('test_id')
assert info.video_id == 'test_id'
info = VideoInfo('__test_id__')
assert info.video_id == '__test_id__'
assert info.get_channel_name() is None
assert info.get_thumbnail() is None
assert info.get_title() is None
assert info.get_channel_id() is None
assert info.get_duration() is None

View File

@@ -0,0 +1,100 @@
{
"response": {
"responseContext": {
"webResponseContextExtensionData": ""
},
"continuationContents": {
"liveChatContinuation": {
"continuations": [
{
"invalidationContinuationData": {
"invalidationId": {
"objectSource": 1000,
"objectId": "___objectId___",
"topic": "chat~00000000000~0000000",
"subscribeToGcmTopics": true,
"protoCreationTimestampMs": "1577804400000"
},
"timeoutMs": 10000,
"continuation": "___continuation___"
}
}
],
"actions": [
{
"addChatItemAction": {
"item": {
"liveChatMembershipItemRenderer": {
"id": "dummy_id",
"timestampUsec": 1570678496000000,
"authorExternalChannelId": "author_channel_id",
"headerSubtext": {
"runs": [
{
"text": "新規メンバー"
}
]
},
"authorName": {
"simpleText": "author_name"
},
"authorPhoto": {
"thumbnails": [
{
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
"width": 32,
"height": 32
},
{
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg",
"width": 64,
"height": 64
}
]
},
"authorBadges": [
{
"liveChatAuthorBadgeRenderer": {
"customThumbnail": {
"thumbnails": [
{
"url": "https://yt3.ggpht.com/X=s32-c-k"
},
{
"url": "https://yt3.ggpht.com/X=s64-c-k"
}
]
},
"tooltip": "新規メンバー",
"accessibility": {
"accessibilityData": {
"label": "新規メンバー"
}
}
}
}
],
"contextMenuEndpoint": {
"commandMetadata": {
"webCommandMetadata": {
"ignoreNavigation": true
}
},
"liveChatItemContextMenuEndpoint": {
"params": "___params___"
}
},
"contextMenuAccessibility": {
"accessibilityData": {
"label": "コメントの操作"
}
}
}
}
}
}
]
}
}
}
}

View File

@@ -0,0 +1,82 @@
{
"response": {
"responseContext": {
"webResponseContextExtensionData": ""
},
"continuationContents": {
"liveChatContinuation": {
"continuations": [
{
"invalidationContinuationData": {
"invalidationId": {
"objectSource": 1000,
"objectId": "___objectId___",
"topic": "chat~00000000000~0000000",
"subscribeToGcmTopics": true,
"protoCreationTimestampMs": "1577804400000"
},
"timeoutMs": 10000,
"continuation": "___continuation___"
}
}
],
"actions": [
{
"addChatItemAction": {
"item": {
"liveChatLegacyPaidMessageRenderer": {
"id": "dummy_id",
"timestampUsec": 1570678496000000,
"eventText": {
"runs": [
{
"text": "新規メンバー"
}
]
},
"detailText": {
"simpleText": "ようこそ、author_name"
},
"authorName": {
"simpleText": "author_name"
},
"authorPhoto": {
"thumbnails": [
{
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
"width": 32,
"height": 32
},
{
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg",
"width": 64,
"height": 64
}
]
},
"authorExternalChannelId": "author_channel_id",
"contextMenuEndpoint": {
"clickTrackingParams": "___clickTrackingParams___",
"commandMetadata": {
"webCommandMetadata": {
"ignoreNavigation": true
}
},
"liveChatItemContextMenuEndpoint": {
"params": "___params___"
}
},
"contextMenuAccessibility": {
"accessibilityData": {
"label": "コメントの操作"
}
}
}
}
}
}
]
}
}
}
}

View File

@@ -0,0 +1,112 @@
{
"response": {
"responseContext": {
"webResponseContextExtensionData": "data"
},
"continuationContents": {
"liveChatContinuation": {
"continuations": [
{
"liveChatReplayContinuationData": {
"invalidationId": {
"objectSource": 1000,
"objectId": "___objectId___",
"topic": "chat~00000000000~0000000",
"subscribeToGcmTopics": true,
"protoCreationTimestampMs": "1577804400000"
},
"timeoutMs": 10000,
"continuation": "___continuation___"
}
}
],
"actions": [
{
"replayChatItemAction": {
"actions": [
{
"addChatItemAction": {
"item": {
"liveChatTextMessageRenderer": {
"message": {
"runs": [
{
"text": "dummy_message"
}
]
},
"authorName": {
"simpleText": "author_name"
},
"authorPhoto": {
"thumbnails": [
{
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
"width": 32,
"height": 32
},
{
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg",
"width": 64,
"height": 64
}
]
},
"contextMenuEndpoint": {
"clickTrackingParams": "___clickTrackingParams___",
"commandMetadata": {
"webCommandMetadata": {
"ignoreNavigation": true
}
},
"liveChatItemContextMenuEndpoint": {
"params": "___params___"
}
},
"id": "dummy_id",
"timestampUsec": 1570678496000000,
"authorBadges": [
{
"liveChatAuthorBadgeRenderer": {
"customThumbnail": {
"thumbnails": [
{
"url": "https://yt3.ggpht.com/X=s16-c-k"
},
{
"url": "https://yt3.ggpht.com/X=s32-c-k"
}
]
},
"tooltip": "メンバー1 か月)",
"accessibility": {
"accessibilityData": {
"label": "メンバー1 か月)"
}
}
}
}
],
"authorExternalChannelId": "author_channel_id",
"contextMenuAccessibility": {
"accessibilityData": {
"label": "コメントの操作"
}
},
"timestampText": {
"simpleText": "1:23:45"
}
}
},
"clientId": "dummy_client_id"
}
}
],
"videoOffsetTimeMsec": "5025120"
}
}
]
}
}
}
}

184
tests/testdata/default/superchat.json vendored Normal file
View File

@@ -0,0 +1,184 @@
{
"response": {
"responseContext": {
"webResponseContextExtensionData": ""
},
"continuationContents": {
"liveChatContinuation": {
"continuations": [
{
"invalidationContinuationData": {
"invalidationId": {
"objectSource": 1000,
"objectId": "___objectId___",
"topic": "chat~00000000000~0000000",
"subscribeToGcmTopics": true,
"protoCreationTimestampMs": "1577804400000"
},
"timeoutMs": 10000,
"continuation": "___continuation___"
}
}
],
"actions": [
{
"addChatItemAction": {
"item": {
"liveChatPaidMessageRenderer": {
"id": "dummy_id",
"timestampUsec": 1570678496000000,
"authorName": {
"simpleText": "author_name"
},
"authorPhoto": {
"thumbnails": [
{
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
"width": 32,
"height": 32
},
{
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg",
"width": 64,
"height": 64
}
]
},
"purchaseAmountText": {
"simpleText": "¥800"
},
"message": {
"runs": [
{
"text": "dummy_message"
}
]
},
"headerBackgroundColor": 4278239141,
"headerTextColor": 4278190080,
"bodyBackgroundColor": 4280150454,
"bodyTextColor": 4278190080,
"authorExternalChannelId": "author_channel_id",
"authorNameTextColor": 2315255808,
"contextMenuEndpoint": {
"commandMetadata": {
"webCommandMetadata": {
"ignoreNavigation": true
}
},
"liveChatItemContextMenuEndpoint": {
"params": "___params___"
}
},
"timestampColor": 2147483648,
"contextMenuAccessibility": {
"accessibilityData": {
"label": "コメントの操作"
}
}
}
}
}
},
{
"addLiveChatTickerItemAction": {
"item": {
"liveChatTickerPaidMessageItemRenderer": {
"id": "dummy_id",
"amount": {
"simpleText": "¥846"
},
"amountTextColor": 4278190080,
"startBackgroundColor": 4280150454,
"endBackgroundColor": 4278239141,
"authorPhoto": {
"thumbnails": [
{
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
"width": 32,
"height": 32
},
{
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
"width": 64,
"height": 64
}
]
},
"durationSec": 120,
"showItemEndpoint": {
"commandMetadata": {
"webCommandMetadata": {
"ignoreNavigation": true
}
},
"showLiveChatItemEndpoint": {
"renderer": {
"liveChatPaidMessageRenderer": {
"id": "dummy_id",
"timestampUsec": 1570678496000000,
"authorName": {
"simpleText": "author_name"
},
"authorPhoto": {
"thumbnails": [
{
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
"width": 32,
"height": 32
},
{
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg",
"width": 64,
"height": 64
}
]
},
"purchaseAmountText": {
"simpleText": "¥846"
},
"message": {
"runs": [
{
"text": "dummy_message"
}
]
},
"headerBackgroundColor": 4278239141,
"headerTextColor": 4278190080,
"bodyBackgroundColor": 4280150454,
"bodyTextColor": 4278190080,
"authorExternalChannelId": "author_channel_id",
"authorNameTextColor": 2315255808,
"contextMenuEndpoint": {
"commandMetadata": {
"webCommandMetadata": {
"ignoreNavigation": true
}
},
"liveChatItemContextMenuEndpoint": {
"params": "___params___"
}
},
"timestampColor": 2147483648,
"contextMenuAccessibility": {
"accessibilityData": {
"label": "コメントの操作"
}
}
}
}
}
},
"authorExternalChannelId": "http://www.youtube.com/channel/author_channel_url",
"fullDurationSec": 120
}
},
"durationSec": "120"
}
}
]
}
}
}
}

View File

@@ -0,0 +1,99 @@
{
"response": {
"responseContext": {
"webResponseContextExtensionData": ""
},
"continuationContents": {
"liveChatContinuation": {
"continuations": [
{
"invalidationContinuationData": {
"invalidationId": {
"objectSource": 1000,
"objectId": "___objectId___",
"topic": "chat~00000000000~0000000",
"subscribeToGcmTopics": true,
"protoCreationTimestampMs": "1577804400000"
},
"timeoutMs": 10000,
"continuation": "___continuation___"
}
}
],
"actions": [
{
"addChatItemAction": {
"item": {
"liveChatPaidStickerRenderer": {
"id": "dummy_id",
"contextMenuEndpoint": {
"commandMetadata": {
"webCommandMetadata": {
"ignoreNavigation": true
}
},
"liveChatItemContextMenuEndpoint": {
"params": "___params___"
}
},
"contextMenuAccessibility": {
"accessibilityData": {
"label": "コメントの操作"
}
},
"timestampUsec": 1570678496000000,
"authorPhoto": {
"thumbnails": [
{
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
"width": 32,
"height": 32
},
{
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg",
"width": 64,
"height": 64
}
]
},
"authorName": {
"simpleText": "author_name"
},
"authorExternalChannelId": "author_channel_id",
"sticker": {
"thumbnails": [
{
"url": "//lh3.googleusercontent.com/param_s=s72-rp",
"width": 72,
"height": 72
},
{
"url": "//lh3.googleusercontent.com/param_s=s144-rp",
"width": 144,
"height": 144
}
],
"accessibility": {
"accessibilityData": {
"label": "___sticker_label___"
}
}
},
"moneyChipBackgroundColor": 4278248959,
"moneyChipTextColor": 4278190080,
"purchaseAmountText": {
"simpleText": "¥200"
},
"stickerDisplayWidth": 72,
"stickerDisplayHeight": 72,
"backgroundColor": 4278237396,
"authorNameTextColor": 3003121664
}
}
}
}
]
}
}
}
}

79
tests/testdata/default/textmessage.json vendored Normal file
View File

@@ -0,0 +1,79 @@
{
"response": {
"responseContext": {
"webResponseContextExtensionData": ""
},
"continuationContents": {
"liveChatContinuation": {
"continuations": [
{
"invalidationContinuationData": {
"invalidationId": {
"objectSource": 1000,
"objectId": "___objectId___",
"topic": "chat~00000000000~0000000",
"subscribeToGcmTopics": true,
"protoCreationTimestampMs": "1577804400000"
},
"timeoutMs": 10000,
"continuation": "___continuation___"
}
}
],
"actions": [
{
"addChatItemAction": {
"item": {
"liveChatTextMessageRenderer": {
"message": {
"runs": [
{
"text": "dummy_message"
}
]
},
"authorName": {
"simpleText": "author_name"
},
"authorPhoto": {
"thumbnails": [
{
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s32-x-x-xx-xx-xx-c0xffffff/photo.jpg",
"width": 32,
"height": 32
},
{
"url": "https://yt3.ggpht.com/------------/AAAAAAAAAAA/AAAAAAAAAAA/xxxxxxxxxxxx/s64-x-x-xx-xx-xx-c0xffffff/photo.jpg",
"width": 64,
"height": 64
}
]
},
"contextMenuEndpoint": {
"commandMetadata": {
"webCommandMetadata": {
"ignoreNavigation": true
}
},
"liveChatItemContextMenuEndpoint": {
"params": "___params___"
}
},
"id": "dummy_id",
"timestampUsec": 1570678496000000,
"authorExternalChannelId": "author_channel_id",
"contextMenuAccessibility": {
"accessibilityData": {
"label": "コメントの操作"
}
}
}
},
"clientId": "dummy_client_id"
}
}
]
}
}
}
}