From ee4b696fc5a03e03f75643fbbbedd55272a5d4ed Mon Sep 17 00:00:00 2001 From: taizan-hokuto <55448286+taizan-hokuto@users.noreply.github.com> Date: Thu, 23 Jul 2020 16:20:12 +0900 Subject: [PATCH 1/6] Add colors attribute --- .../processors/default/renderer/legacypaid.py | 2 +- .../processors/default/renderer/paidmessage.py | 15 +++++++++++++++ .../processors/default/renderer/paidsticker.py | 17 +++++++++++++++-- 3 files changed, 31 insertions(+), 3 deletions(-) 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 From 6e6bb8e01951e909735d97361eee6b752fc4cc08 Mon Sep 17 00:00:00 2001 From: taizan-hokuto <55448286+taizan-hokuto@users.noreply.github.com> Date: Thu, 23 Jul 2020 16:20:38 +0900 Subject: [PATCH 2/6] Add tests --- tests/test_default_processor.py | 228 ++++++++++++++++++ .../testdata/default/newSponsor_current.json | 100 ++++++++ tests/testdata/default/newSponsor_lagacy.json | 82 +++++++ .../testdata/default/replay_member_text.json | 112 +++++++++ tests/testdata/default/superchat.json | 184 ++++++++++++++ tests/testdata/default/supersticker.json | 99 ++++++++ tests/testdata/default/textmessage.json | 79 ++++++ 7 files changed, 884 insertions(+) create mode 100644 tests/test_default_processor.py create mode 100644 tests/testdata/default/newSponsor_current.json create mode 100644 tests/testdata/default/newSponsor_lagacy.json create mode 100644 tests/testdata/default/replay_member_text.json create mode 100644 tests/testdata/default/superchat.json create mode 100644 tests/testdata/default/supersticker.json create mode 100644 tests/testdata/default/textmessage.json 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/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 From 0abf8dd9f0bae6ff3b0175265301672b583e27e1 Mon Sep 17 00:00:00 2001 From: taizan-hokuto <55448286+taizan-hokuto@users.noreply.github.com> Date: Fri, 24 Jul 2020 14:03:07 +0900 Subject: [PATCH 3/6] Make it possible to extract video id from url --- pytchat/cli/__init__.py | 18 ++++++++++++------ pytchat/cli/arguments.py | 11 +++++++---- pytchat/core_async/livechat.py | 3 ++- pytchat/core_multithread/livechat.py | 3 ++- pytchat/processors/html_archiver.py | 2 +- pytchat/tool/extract/extractor.py | 3 ++- pytchat/tool/videoinfo.py | 5 +++-- pytchat/util/extract_video_id.py | 25 +++++++++++++++++++++++++ 8 files changed, 54 insertions(+), 16 deletions(-) create mode 100644 pytchat/util/extract_video_id.py 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 2cc3ff8..17f91f2 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 7f99c55..f439026 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/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 From 174d9f27c0cbe978c632d7c70122be2a303cba36 Mon Sep 17 00:00:00 2001 From: taizan-hokuto <55448286+taizan-hokuto@users.noreply.github.com> Date: Fri, 24 Jul 2020 14:03:20 +0900 Subject: [PATCH 4/6] Add tests --- tests/test_extract_video_id.py | 55 ++++++++++++++++++++++++++++++++++ tests/test_livechat.py | 6 ++-- tests/test_livechat_2.py | 8 ++--- tests/test_videoinfo.py | 24 ++++++++------- 4 files changed, 76 insertions(+), 17 deletions(-) create mode 100644 tests/test_extract_video_id.py 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 - From d8656161cd718aa10e85d41f00dff19283f68232 Mon Sep 17 00:00:00 2001 From: taizan-hokuto <55448286+taizan-hokuto@users.noreply.github.com> Date: Fri, 24 Jul 2020 14:04:13 +0900 Subject: [PATCH 5/6] Update README --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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() From aa8a4fb592d1074d660686c074391f0f6c56945b Mon Sep 17 00:00:00 2001 From: taizan-hokuto <55448286+taizan-hokuto@users.noreply.github.com> Date: Fri, 24 Jul 2020 16:26:09 +0900 Subject: [PATCH 6/6] Increment version --- pytchat/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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'