diff --git a/README.md b/README.md
index 50a26fe..9c2267b 100644
--- a/README.md
+++ b/README.md
@@ -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.
@@ -30,10 +30,9 @@ One-liner command.
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()
diff --git a/pytchat/__init__.py b/pytchat/__init__.py
index 7ea418c..aa979bb 100644
--- a/pytchat/__init__.py
+++ b/pytchat/__init__.py
@@ -2,7 +2,7 @@
pytchat is a lightweight python library to browse youtube livechat without Selenium or BeautifulSoup.
"""
__copyright__ = 'Copyright (C) 2019 taizan-hokuto'
-__version__ = '0.0.9.1'
+__version__ = '0.1.0'
__license__ = 'MIT'
__author__ = 'taizan-hokuto'
__author_email__ = '55448286+taizan-hokuto@users.noreply.github.com'
diff --git a/pytchat/cli/__init__.py b/pytchat/cli/__init__.py
index 696af45..62237ff 100644
--- a/pytchat/cli/__init__.py
+++ b/pytchat/cli/__init__.py
@@ -1,5 +1,6 @@
import argparse
from pathlib import Path
+from pytchat.util.extract_video_id import extract_video_id
from .arguments import Arguments
from .. exceptions import InvalidVideoIdException, NoContents
from .. processors.html_archiver import HTMLArchiver
@@ -19,16 +20,19 @@ https://github.com/PetterKraabol/Twitch-Chat-Downloader
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'
+ # 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='./')
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
@@ -43,14 +47,16 @@ def main():
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()}")
+ print(f" output path: {path.resolve()}")
Extractor(video_id,
processor=HTMLArchiver(
Arguments().output + video_id + '.html'),
callback=_disp_progress
).extract()
print("\nExtraction end.\n")
- except (InvalidVideoIdException, NoContents) as e:
+ except InvalidVideoIdException:
+ print("Invalid Video ID or URL:", video_id)
+ except (TypeError, NoContents) as e:
print(e)
return
parser.print_help()
diff --git a/pytchat/cli/arguments.py b/pytchat/cli/arguments.py
index d6fea2b..6f62548 100644
--- a/pytchat/cli/arguments.py
+++ b/pytchat/cli/arguments.py
@@ -16,8 +16,8 @@ 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):
@@ -35,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(',')]
+
+
+
diff --git a/pytchat/core_async/livechat.py b/pytchat/core_async/livechat.py
index fac41ad..ed11922 100644
--- a/pytchat/core_async/livechat.py
+++ b/pytchat/core_async/livechat.py
@@ -15,6 +15,7 @@ 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
@@ -86,7 +87,7 @@ class LiveChatAsync:
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)
diff --git a/pytchat/core_multithread/livechat.py b/pytchat/core_multithread/livechat.py
index f4a2513..d05ca3d 100644
--- a/pytchat/core_multithread/livechat.py
+++ b/pytchat/core_multithread/livechat.py
@@ -14,6 +14,7 @@ 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
@@ -84,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)
diff --git a/pytchat/processors/default/renderer/legacypaid.py b/pytchat/processors/default/renderer/legacypaid.py
index ee238cf..dc6b2e4 100644
--- a/pytchat/processors/default/renderer/legacypaid.py
+++ b/pytchat/processors/default/renderer/legacypaid.py
@@ -12,4 +12,4 @@ class LiveChatLegacyPaidMessageRenderer(BaseRenderer):
def get_message(self, renderer):
message = (renderer["eventText"]["runs"][0]["text"]
) + ' / ' + (renderer["detailText"]["simpleText"])
- return message
+ return message, [message]
diff --git a/pytchat/processors/default/renderer/paidmessage.py b/pytchat/processors/default/renderer/paidmessage.py
index 9e69ab4..70bd055 100644
--- a/pytchat/processors/default/renderer/paidmessage.py
+++ b/pytchat/processors/default/renderer/paidmessage.py
@@ -4,6 +4,10 @@ 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")
@@ -18,6 +22,7 @@ class LiveChatPaidMessageRenderer(BaseRenderer):
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):
amountDisplayString = renderer["purchaseAmountText"]["simpleText"]
@@ -29,3 +34,13 @@ class LiveChatPaidMessageRenderer(BaseRenderer):
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
diff --git a/pytchat/processors/default/renderer/paidsticker.py b/pytchat/processors/default/renderer/paidsticker.py
index b474e71..723eb88 100644
--- a/pytchat/processors/default/renderer/paidsticker.py
+++ b/pytchat/processors/default/renderer/paidsticker.py
@@ -4,6 +4,10 @@ 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")
@@ -18,8 +22,9 @@ class LiveChatPaidStickerRenderer(BaseRenderer):
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):
amountDisplayString = renderer["purchaseAmountText"]["simpleText"]
@@ -31,3 +36,11 @@ class LiveChatPaidStickerRenderer(BaseRenderer):
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
diff --git a/pytchat/processors/html_archiver.py b/pytchat/processors/html_archiver.py
index dedab39..3676770 100644
--- a/pytchat/processors/html_archiver.py
+++ b/pytchat/processors/html_archiver.py
@@ -47,7 +47,7 @@ class HTMLArchiver(ChatProcessor):
super().__init__()
self.save_path = self._checkpath(save_path)
self.processor = DefaultProcessor()
- self.emoji_table = {} # table for custom emojis. key: emoji_id, value: base64 encoded image binary.
+ self.emoji_table = {} # tuble for custom emojis. key: emoji_id, value: base64 encoded image binary.
self.header = [HEADER_HTML]
self.body = ['
\n', '\n', self._parse_table_header(fmt_headers)]
diff --git a/pytchat/tool/extract/extractor.py b/pytchat/tool/extract/extractor.py
index 2b421af..56bd8aa 100644
--- a/pytchat/tool/extract/extractor.py
+++ b/pytchat/tool/extract/extractor.py
@@ -3,6 +3,7 @@ from . import duplcheck
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
@@ -14,7 +15,7 @@ class Extractor:
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
diff --git a/pytchat/tool/videoinfo.py b/pytchat/tool/videoinfo.py
index 13712dc..a87e0c4 100644
--- a/pytchat/tool/videoinfo.py
+++ b/pytchat/tool/videoinfo.py
@@ -3,6 +3,7 @@ import re
import requests
from .. import config
from ..exceptions import InvalidVideoIdException
+from ..util.extract_video_id import extract_video_id
headers = config.headers
@@ -78,8 +79,8 @@ class VideoInfo:
'''
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):
diff --git a/pytchat/util/extract_video_id.py b/pytchat/util/extract_video_id.py
new file mode 100644
index 0000000..75385f8
--- /dev/null
+++ b/pytchat/util/extract_video_id.py
@@ -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
diff --git a/tests/test_default_processor.py b/tests/test_default_processor.py
new file mode 100644
index 0000000..1ea7a3d
--- /dev/null
+++ b/tests/test_default_processor.py
@@ -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()
diff --git a/tests/test_extract_video_id.py b/tests/test_extract_video_id.py
new file mode 100644
index 0000000..7d97851
--- /dev/null
+++ b/tests/test_extract_video_id.py
@@ -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
diff --git a/tests/test_livechat.py b/tests/test_livechat.py
index 6c0d38f..31c7677 100644
--- a/tests/test_livechat.py
+++ b/tests/test_livechat.py
@@ -11,13 +11,13 @@ def _open_file(path):
@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)
try:
- chat = LiveChatAsync(video_id='')
+ chat = LiveChatAsync(video_id='__test_id__')
assert chat.is_alive()
chat.terminate()
assert not chat.is_alive()
@@ -33,7 +33,7 @@ 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()
diff --git a/tests/test_livechat_2.py b/tests/test_livechat_2.py
index 0fbe42a..42e42c2 100644
--- a/tests/test_livechat_2.py
+++ b/tests/test_livechat_2.py
@@ -20,7 +20,7 @@ def test_async_live_stream(*mock):
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
@@ -60,7 +60,7 @@ def test_async_replay_stream(*mock):
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
@@ -93,7 +93,7 @@ def test_async_force_replay(*mock):
mock[0].get(pattern_replay, status=200, body=_text_replay)
# force replay
chat = LiveChatAsync(
- video_id='', processor=DummyProcessor(), force_replay=True)
+ video_id='__test_id__', processor=DummyProcessor(), force_replay=True)
chats = await chat.get()
rawdata = chats[0]["chatdata"]
# assert fetching replaychat data
@@ -119,7 +119,7 @@ def test_multithread_live_stream(mocker):
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
diff --git a/tests/test_videoinfo.py b/tests/test_videoinfo.py
index 786977b..8a33075 100644
--- a/tests/test_videoinfo.py
+++ b/tests/test_videoinfo.py
@@ -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
-
diff --git a/tests/testdata/default/newSponsor_current.json b/tests/testdata/default/newSponsor_current.json
new file mode 100644
index 0000000..346fba3
--- /dev/null
+++ b/tests/testdata/default/newSponsor_current.json
@@ -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": "コメントの操作"
+ }
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/testdata/default/newSponsor_lagacy.json b/tests/testdata/default/newSponsor_lagacy.json
new file mode 100644
index 0000000..aa9cc9b
--- /dev/null
+++ b/tests/testdata/default/newSponsor_lagacy.json
@@ -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": "コメントの操作"
+ }
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/testdata/default/replay_member_text.json b/tests/testdata/default/replay_member_text.json
new file mode 100644
index 0000000..eaaf4b2
--- /dev/null
+++ b/tests/testdata/default/replay_member_text.json
@@ -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"
+ }
+ }
+ ]
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/testdata/default/superchat.json b/tests/testdata/default/superchat.json
new file mode 100644
index 0000000..a5b41d1
--- /dev/null
+++ b/tests/testdata/default/superchat.json
@@ -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"
+ }
+ }
+ ]
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/testdata/default/supersticker.json b/tests/testdata/default/supersticker.json
new file mode 100644
index 0000000..fddf26d
--- /dev/null
+++ b/tests/testdata/default/supersticker.json
@@ -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
+ }
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/testdata/default/textmessage.json b/tests/testdata/default/textmessage.json
new file mode 100644
index 0000000..474b26b
--- /dev/null
+++ b/tests/testdata/default/textmessage.json
@@ -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"
+ }
+ }
+ ]
+ }
+ }
+ }
+}
\ No newline at end of file