diff --git a/pytchat/processors/superchat/calculator.py b/pytchat/processors/superchat/calculator.py index 9d9e221..f62452f 100644 --- a/pytchat/processors/superchat/calculator.py +++ b/pytchat/processors/superchat/calculator.py @@ -14,6 +14,7 @@ items_sticker = [ 'item', 'liveChatPaidStickerRenderer' ] + class SuperchatCalculator(ChatProcessor): """ Calculate the amount of SuperChat by currency. diff --git a/pytchat/tool/extract/extractor.py b/pytchat/tool/extract/extractor.py index ba54394..58141db 100644 --- a/pytchat/tool/extract/extractor.py +++ b/pytchat/tool/extract/extractor.py @@ -24,7 +24,7 @@ class Extractor: def _get_duration_of_video(self, video_id): duration = 0 try: - duration = VideoInfo(video_id).duration + duration = VideoInfo(video_id).get_duration() except InvalidVideoIdException: raise return duration diff --git a/pytchat/tool/mining/superchat_miner.py b/pytchat/tool/mining/superchat_miner.py index 626c42e..8a5b3bd 100644 --- a/pytchat/tool/mining/superchat_miner.py +++ b/pytchat/tool/mining/superchat_miner.py @@ -54,7 +54,7 @@ class SuperChatMiner: def extract(video_id, div = 1, callback = None, processor = None): duration = 0 try: - duration = VideoInfo(video_id).duration + duration = VideoInfo(video_id).get_duration() except InvalidVideoIdException: raise if duration == 0: diff --git a/pytchat/tool/videoinfo.py b/pytchat/tool/videoinfo.py index c959220..c0a5099 100644 --- a/pytchat/tool/videoinfo.py +++ b/pytchat/tool/videoinfo.py @@ -4,8 +4,10 @@ import requests from .. import config from .. import util from ..exceptions import InvalidVideoIdException + headers = config.headers -pattern=re.compile(r"yt\.setConfig\({'PLAYER_CONFIG': ({.*})}\);") + +pattern = re.compile(r"yt\.setConfig\({'PLAYER_CONFIG': ({.*})}\);") item_channel_id =[ "videoDetails", @@ -36,7 +38,6 @@ item_author_image =[ "url" ] - item_thumbnail = [ "defaultThumbnail", "thumbnails", @@ -63,24 +64,27 @@ item_moving_thumbnail = [ ] class VideoInfo: - def __init__(self,video_id): + ''' + VideoInfo object retrieves YouTube video informations + from the video page. + + Parameter + --------- + video_id : str + + Exception + --------- + 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._parse(text) - self._get_attributes() - def _get_attributes(self): - self.duration = self._duration() - self.channel_id = self._channel_id() - self.channel_name = self._channel_name() - self.thumbnail = self._thumbnail() - self.author_image = self._author_image() - self.title = self._title() - self.moving_thumbnail = self._moving_thumbnail() - - def _get_page_text(self,video_id): + 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 @@ -91,8 +95,8 @@ class VideoInfo: if response is None: raise InvalidVideoIdException( f"Specified video_id [{self.video_id}] is invalid.") - self.renderer = self._get_item(json.loads(response), item_renderer) - if self.renderer is None: + self._renderer = self._get_item(json.loads(response), item_renderer) + if self._renderer is None: raise InvalidVideoIdException( f"No renderer found in video_id: [{self.video_id}].") @@ -111,29 +115,35 @@ class VideoInfo: return None return dict_body - def _duration(self): - return int(self.renderer.get("videoDurationSeconds") or 0) - - def _title(self): - if self.renderer.get("title"): - return [''.join(run["text"]) - for run in self.renderer["title"]["runs"]][0] + def get_duration(self): + duration_seconds = self._renderer.get("videoDurationSeconds") + if duration_seconds: + '''Fetched value is string, so cast to integer.''' + return int(duration_seconds) + '''When key is not found, explicitly returns None.''' return None - def _channel_id(self): - channel_url = self._get_item(self.renderer, item_channel_id) + def get_title(self): + print(self._renderer) + if self._renderer.get("title"): + return [''.join(run["text"]) + for run in self._renderer["title"]["runs"]][0] + return None + + def get_channel_id(self): + channel_url = self._get_item(self._renderer, item_channel_id) if channel_url: return channel_url[9:] return None - def _author_image(self): - return self._get_item(self.renderer, item_author_image) + def get_author_image(self): + return self._get_item(self._renderer, item_author_image) - def _thumbnail(self): - return self._get_item(self.renderer, item_thumbnail) + def get_thumbnail(self): + return self._get_item(self._renderer, item_thumbnail) - def _channel_name(self): - return self._get_item(self.renderer, item_channel_name) + def get_channel_name(self): + return self._get_item(self._renderer, item_channel_name) - def _moving_thumbnail(self): - return self._get_item(self.renderer, item_moving_thumbnail) \ No newline at end of file + def get_moving_thumbnail(self): + return self._get_item(self._renderer, item_moving_thumbnail) diff --git a/tests/test_calculator_get_item.py b/tests/test_calculator_get_item.py index 9034b21..afc48d0 100644 --- a/tests/test_calculator_get_item.py +++ b/tests/test_calculator_get_item.py @@ -71,6 +71,7 @@ items_test_list3 = [ 'root', 'node4' ] + items_test_list_nest = [ 'root', 'node5', @@ -114,7 +115,8 @@ def test_get_items_2(): assert get_item(dict_test, items_test_nest) == 'value2-0' def test_get_items_3(): - assert get_item(dict_test, items_test_list0) == {'node3-1-0' : 'value3-1-0'} + assert get_item( + dict_test, items_test_list0) == {'node3-1-0' : 'value3-1-0'} def test_get_items_4(): assert get_item(dict_test, items_test_list1) == 'value3-1-0' diff --git a/tests/test_videoinfo.py b/tests/test_videoinfo.py new file mode 100644 index 0000000..786977b --- /dev/null +++ b/tests/test_videoinfo.py @@ -0,0 +1,62 @@ +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: + return f.read() + +def _set_test_data(filepath, mocker): + _text = _open_file(filepath) + response_mock = mocker.Mock() + response_mock.status_code = 200 + 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') + actual_thumbnail_url = 'https://i.ytimg.com/vi/fzI9FNjXQ0o/hqdefault.jpg' + 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') + '''live page :duration = 0''' + assert info.get_duration() == 0 + 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' + assert info.get_title() == ( + 'Coffee Jazz Music - Chill Out Lounge Jazz Music Radio' + ' - 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') + 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' + 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/videoinfo/archived_page.txt b/tests/testdata/videoinfo/archived_page.txt new file mode 100644 index 0000000..37dd74f --- /dev/null +++ b/tests/testdata/videoinfo/archived_page.txt @@ -0,0 +1,15 @@ + + + + + GitHub Arctic Code Vault - YouTube + + + + + + +
+ \ No newline at end of file diff --git a/tests/testdata/videoinfo/invalid_video_id_page.txt b/tests/testdata/videoinfo/invalid_video_id_page.txt new file mode 100644 index 0000000..c146430 --- /dev/null +++ b/tests/testdata/videoinfo/invalid_video_id_page.txt @@ -0,0 +1,14 @@ + + + + + YouTube + + + + + +
+ \ No newline at end of file diff --git a/tests/testdata/videoinfo/live_page.txt b/tests/testdata/videoinfo/live_page.txt new file mode 100644 index 0000000..45d0b04 --- /dev/null +++ b/tests/testdata/videoinfo/live_page.txt @@ -0,0 +1,15 @@ + + + + + Coffee Jazz Music - Chill Out Lounge Jazz Music Radio - 24/7 Live Stream - Slow Jazz - YouTube + + + + + + +
+ \ No newline at end of file diff --git a/tests/testdata/videoinfo/no_info_page.txt b/tests/testdata/videoinfo/no_info_page.txt new file mode 100644 index 0000000..5e2c971 --- /dev/null +++ b/tests/testdata/videoinfo/no_info_page.txt @@ -0,0 +1,15 @@ + + + + + Cozy January Slow Jazz - Dreamy Jazz Cafe Music for Winter Evening - YouTube + + + + + + +
+ \ No newline at end of file