From 1c97dfc45df12e3e91f98979388097fc01f621b5 Mon Sep 17 00:00:00 2001 From: mpeter50 <83356418+mpeter50@users.noreply.github.com> Date: Thu, 4 Nov 2021 23:53:30 +0100 Subject: [PATCH 01/13] add live chat extraction to separate branch --- yt_dlp/extractor/twitch.py | 80 +++++++++++++++++++++++++++++++++----- 1 file changed, 70 insertions(+), 10 deletions(-) diff --git a/yt_dlp/extractor/twitch.py b/yt_dlp/extractor/twitch.py index c8ee52014..57888d86e 100644 --- a/yt_dlp/extractor/twitch.py +++ b/yt_dlp/extractor/twitch.py @@ -525,6 +525,73 @@ class TwitchVodIE(TwitchBaseIE): } for path in images], } + def _download_chat(self, vod_id): + live_chat = list() + + request_url = f'https://api.twitch.tv/v5/videos/{vod_id}/comments' + query_params = { + 'client_id': self._CLIENT_ID + } + + self.to_screen('Downloading chat fragment JSONs') + + # TODO: question: is it OK to use this config value for this purpose? + max_retries = self.get_param('extractor_retries') + retries = 0 + pagenum = 1 + while True: + response_json = self._download_json( + request_url, + vod_id, + fatal=False, + note='Downloading chat fragment JSON page %d' % pagenum, + errnote='Live chat fragment download failed.', + query=query_params) + + if response_json is False: + self.report_warning(f'Unable to fetch next chat history fragment. {retries}. try of {max_retries}') + + if retries < max_retries: + retries += 1 + continue + else: + self.report_warning('Chat history download failed: retry limit reached') + # TODO: when this happens, should I forget a partial chat history, or is it better to keep it too? + # I think if I keep it, it might be better to persist a warning that it is incomplete + # live_chat.clear() + break + + live_chat.extend(response_json.get('comments') or []) + next_fragment_cursor = str_or_none(response_json.get('_next')) + + if next_fragment_cursor is None: + break + + query_params['cursor'] = next_fragment_cursor + pagenum += 1 + + chat_history_length = len(live_chat) + + self.to_screen('Extracted %d chat messages' % chat_history_length) + if chat_history_length == 0: + return None + + return self._extract_chat(live_chat, request_url) + + def _extract_chat(self, chat_history, request_url): + return { + 'live_chat': [ # subtitle tag + { # JSON subformat as URL + 'url': request_url, + 'ext': 'json' + }, + { # JSON subformat as data + 'data': json.dumps(chat_history), + 'ext': 'json' + } + ] + } + def _real_extract(self, url): vod_id = self._match_id(url) @@ -556,16 +623,9 @@ class TwitchVodIE(TwitchBaseIE): if 't' in query: info['start_time'] = parse_duration(query['t'][0]) - if info.get('timestamp') is not None: - info['subtitles'] = { - 'rechat': [{ - 'url': update_url_query( - 'https://api.twitch.tv/v5/videos/%s/comments' % vod_id, { - 'client_id': self._CLIENT_ID, - }), - 'ext': 'json', - }], - } + if ('live_chat' in self.get_param('subtitleslangs', [])) \ + and info.get('timestamp') is not None: + info['subtitles'] = self._download_chat(vod_id) return info From 20258169732233c7ae572d4397317be3a18a0425 Mon Sep 17 00:00:00 2001 From: mpeter50 <83356418+mpeter50@users.noreply.github.com> Date: Fri, 26 Nov 2021 20:35:34 +0100 Subject: [PATCH 02/13] fix import list formatting --- yt_dlp/extractor/twitch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/yt_dlp/extractor/twitch.py b/yt_dlp/extractor/twitch.py index 57888d86e..e66199200 100644 --- a/yt_dlp/extractor/twitch.py +++ b/yt_dlp/extractor/twitch.py @@ -299,6 +299,7 @@ class TwitchVodIE(TwitchBaseIE): 'view_count': int, }, 'params': { + 'subtitleslangs': ['live_chat'], 'skip_download': True }, }, { From 7adae468018a60a2f3ccb4cd1c851a7002217c19 Mon Sep 17 00:00:00 2001 From: mpeter50 <83356418+mpeter50@users.noreply.github.com> Date: Wed, 28 Dec 2022 18:50:42 +0100 Subject: [PATCH 03/13] fix twitch vod chat download chat download now uses the GraphQL API, instead of the old one that doesn't work anymore --- yt_dlp/extractor/twitch.py | 87 ++++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 40 deletions(-) diff --git a/yt_dlp/extractor/twitch.py b/yt_dlp/extractor/twitch.py index e66199200..d6f16f953 100644 --- a/yt_dlp/extractor/twitch.py +++ b/yt_dlp/extractor/twitch.py @@ -3,6 +3,7 @@ import itertools import json import random import re +import time from .common import InfoExtractor from ..compat import ( @@ -55,6 +56,7 @@ class TwitchBaseIE(InfoExtractor): 'VideoMetadata': '49b5b8f268cdeb259d75b58dcb0c1a748e3b575003448a2333dc5cdafd49adad', 'VideoPlayer_ChapterSelectButtonVideo': '8d2793384aac3773beab5e59bd5d6f585aedb923d292800119e03d40cd0f9b41', 'VideoPlayer_VODSeekbarPreviewVideo': '07e99e4d56c5a7c67117a154777b0baf85a5ffefa393b213f4bc712ccaf85dd6', + 'VideoCommentsByOffsetOrCursor': 'b70a3591ff0f4e0313d126c6a1502d79a1c02baebb288227c582044aa76adf6a', } @property @@ -526,67 +528,72 @@ class TwitchVodIE(TwitchBaseIE): } for path in images], } - def _download_chat(self, vod_id): - live_chat = list() + def _extract_chat(self, vod_id): + chat_history = [] + has_more_pages = True + retry_sleep = 5 + max_retries = 3 + retries = 0 + pagenum = 1 + gql_ops = [ + { + 'operationName': 'VideoCommentsByOffsetOrCursor', + 'variables': { + 'videoID': vod_id, + # 'cursor': + } + } + ] - request_url = f'https://api.twitch.tv/v5/videos/{vod_id}/comments' - query_params = { - 'client_id': self._CLIENT_ID - } + self.to_screen('Downloading chat fragment pages') - self.to_screen('Downloading chat fragment JSONs') + while has_more_pages: + response = self._download_gql(vod_id, gql_ops, 'Downloading chat fragment page %d' % pagenum, fatal=False) - # TODO: question: is it OK to use this config value for this purpose? - max_retries = self.get_param('extractor_retries') - retries = 0 - pagenum = 1 - while True: - response_json = self._download_json( - request_url, - vod_id, - fatal=False, - note='Downloading chat fragment JSON page %d' % pagenum, - errnote='Live chat fragment download failed.', - query=query_params) - - if response_json is False: - self.report_warning(f'Unable to fetch next chat history fragment. {retries}. try of {max_retries}') + if response is False: + self.report_warning(f'Unable to fetch next chat history fragment. {retries + 1}. try of {max_retries}') if retries < max_retries: retries += 1 + time.sleep(retry_sleep) continue else: self.report_warning('Chat history download failed: retry limit reached') - # TODO: when this happens, should I forget a partial chat history, or is it better to keep it too? + # TODO: when this happens, should I forget a partial chat history, or is it better to keep it? # I think if I keep it, it might be better to persist a warning that it is incomplete - # live_chat.clear() + # chat_history.clear() break - live_chat.extend(response_json.get('comments') or []) - next_fragment_cursor = str_or_none(response_json.get('_next')) + comments_obj = traverse_obj(response, (0, 'data', 'video', 'comments')) + chat_history.extend(traverse_obj(comments_obj, ('edges', slice, 'node'))) - if next_fragment_cursor is None: - break + has_more_pages = traverse_obj(comments_obj, ('pageInfo', 'hasNextPage')) + + if has_more_pages: + cursor = traverse_obj(comments_obj, ('edges', 0, 'cursor')) + if cursor is None: + self.report_warning("Cannot continue downloading chat history: cursor is missing. There are additional chat pages to download.") + break + + pagenum += 1 + gql_ops[0]['variables']['cursor'] = cursor - query_params['cursor'] = next_fragment_cursor - pagenum += 1 + if has_more_pages is None: + cursor = traverse_obj(comments_obj, ('edges', 0, 'cursor')) - chat_history_length = len(live_chat) + if cursor is not None: + self.report_warning("Next page indication is missing, but found cursor. Continuing chat history download.") + else: # In this case maintenance might be needed. Purpose is to prevent silent errors. + self.report_warning("Next page indication is missing, and cursor not found.") + chat_history_length = len(chat_history) self.to_screen('Extracted %d chat messages' % chat_history_length) if chat_history_length == 0: return None - return self._extract_chat(live_chat, request_url) - - def _extract_chat(self, chat_history, request_url): return { 'live_chat': [ # subtitle tag - { # JSON subformat as URL - 'url': request_url, - 'ext': 'json' - }, - { # JSON subformat as data + { 'data': json.dumps(chat_history), 'ext': 'json' } @@ -626,7 +633,7 @@ class TwitchVodIE(TwitchBaseIE): if ('live_chat' in self.get_param('subtitleslangs', [])) \ and info.get('timestamp') is not None: - info['subtitles'] = self._download_chat(vod_id) + info['subtitles'] = self._extract_chat(vod_id) return info From 0c9f2dfb294f810ec1e1bdda4ec8e175915a899a Mon Sep 17 00:00:00 2001 From: mpeter50 <83356418+mpeter50@users.noreply.github.com> Date: Wed, 28 Dec 2022 18:52:25 +0100 Subject: [PATCH 04/13] add version indication to the subtitles.live_chat.[].ext field --- yt_dlp/extractor/twitch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yt_dlp/extractor/twitch.py b/yt_dlp/extractor/twitch.py index d6f16f953..7eb594a9b 100644 --- a/yt_dlp/extractor/twitch.py +++ b/yt_dlp/extractor/twitch.py @@ -595,7 +595,7 @@ class TwitchVodIE(TwitchBaseIE): 'live_chat': [ # subtitle tag { 'data': json.dumps(chat_history), - 'ext': 'json' + 'ext': 'twitch-gql-20221228.json' } ] } From 5587ecffdf56f56713267b9ea60f87faf6d66120 Mon Sep 17 00:00:00 2001 From: mpeter50 <83356418+mpeter50@users.noreply.github.com> Date: Mon, 15 May 2023 13:13:47 +0200 Subject: [PATCH 05/13] print twitch gql api errors when extracting chat --- yt_dlp/extractor/twitch.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/yt_dlp/extractor/twitch.py b/yt_dlp/extractor/twitch.py index 7eb594a9b..860f17c9f 100644 --- a/yt_dlp/extractor/twitch.py +++ b/yt_dlp/extractor/twitch.py @@ -564,6 +564,11 @@ class TwitchVodIE(TwitchBaseIE): # chat_history.clear() break + response_errors = traverse_obj(response, (slice, 'errors')) + + if response_errors is not None and len(response_errors) > 0: + self.report_warning(f"Error response recevied for fetching next chat history fragment: {response_errors}") + comments_obj = traverse_obj(response, (0, 'data', 'video', 'comments')) chat_history.extend(traverse_obj(comments_obj, ('edges', slice, 'node'))) From 1781d316c979b8e5bf5827185e4621a442cb6024 Mon Sep 17 00:00:00 2001 From: mpeter50 <83356418+mpeter50@users.noreply.github.com> Date: Thu, 13 Jul 2023 20:43:01 +0200 Subject: [PATCH 06/13] twitch: fix traversing objects --- yt_dlp/extractor/twitch.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/yt_dlp/extractor/twitch.py b/yt_dlp/extractor/twitch.py index 860f17c9f..9a5ec3783 100644 --- a/yt_dlp/extractor/twitch.py +++ b/yt_dlp/extractor/twitch.py @@ -564,13 +564,12 @@ class TwitchVodIE(TwitchBaseIE): # chat_history.clear() break - response_errors = traverse_obj(response, (slice, 'errors')) - + response_errors = traverse_obj(response, (..., 'errors')) if response_errors is not None and len(response_errors) > 0: self.report_warning(f"Error response recevied for fetching next chat history fragment: {response_errors}") comments_obj = traverse_obj(response, (0, 'data', 'video', 'comments')) - chat_history.extend(traverse_obj(comments_obj, ('edges', slice, 'node'))) + chat_history.extend(traverse_obj(comments_obj, ('edges', ..., 'node'))) has_more_pages = traverse_obj(comments_obj, ('pageInfo', 'hasNextPage')) From 9d94a95e0ec43fa177227ae9fc7a3baa9b5e5111 Mon Sep 17 00:00:00 2001 From: mpeter50 <83356418+mpeter50@users.noreply.github.com> Date: Fri, 14 Jul 2023 01:03:27 +0200 Subject: [PATCH 07/13] twitch chat: review changes part 1 --- yt_dlp/extractor/twitch.py | 71 +++++++++++++++----------------------- 1 file changed, 28 insertions(+), 43 deletions(-) diff --git a/yt_dlp/extractor/twitch.py b/yt_dlp/extractor/twitch.py index 9a5ec3783..6af27ef6b 100644 --- a/yt_dlp/extractor/twitch.py +++ b/yt_dlp/extractor/twitch.py @@ -3,7 +3,6 @@ import itertools import json import random import re -import time from .common import InfoExtractor from ..compat import ( @@ -25,6 +24,7 @@ from ..utils import ( parse_iso8601, parse_qs, qualities, + RetryManager, str_or_none, traverse_obj, try_get, @@ -528,44 +528,34 @@ class TwitchVodIE(TwitchBaseIE): } for path in images], } - def _extract_chat(self, vod_id): + def _get_subtitles(self, vod_id): chat_history = [] has_more_pages = True - retry_sleep = 5 - max_retries = 3 - retries = 0 pagenum = 1 - gql_ops = [ - { - 'operationName': 'VideoCommentsByOffsetOrCursor', - 'variables': { - 'videoID': vod_id, - # 'cursor': - } - } - ] - - self.to_screen('Downloading chat fragment pages') + gql_ops = [{ + 'operationName': 'VideoCommentsByOffsetOrCursor', + 'variables': { 'videoID': vod_id } + # 'variables.cursor': + }] while has_more_pages: - response = self._download_gql(vod_id, gql_ops, 'Downloading chat fragment page %d' % pagenum, fatal=False) + response = None - if response is False: - self.report_warning(f'Unable to fetch next chat history fragment. {retries + 1}. try of {max_retries}') + for retry in self.RetryManager(): + response = self._download_gql(vod_id, gql_ops, 'Downloading chat fragment page %d' % pagenum, fatal=False) + # response = False + # TODO: delete the direct False, uncomment _download_gql + + if response is False: + retry.error = ExtractorError("f'Unable to fetch next chat history fragment.'", video_id=vod_id, ie=self) - if retries < max_retries: - retries += 1 - time.sleep(retry_sleep) - continue - else: - self.report_warning('Chat history download failed: retry limit reached') # TODO: when this happens, should I forget a partial chat history, or is it better to keep it? # I think if I keep it, it might be better to persist a warning that it is incomplete - # chat_history.clear() - break + + # time.sleep(5) response_errors = traverse_obj(response, (..., 'errors')) - if response_errors is not None and len(response_errors) > 0: + if response_errors: self.report_warning(f"Error response recevied for fetching next chat history fragment: {response_errors}") comments_obj = traverse_obj(response, (0, 'data', 'video', 'comments')) @@ -590,19 +580,15 @@ class TwitchVodIE(TwitchBaseIE): else: # In this case maintenance might be needed. Purpose is to prevent silent errors. self.report_warning("Next page indication is missing, and cursor not found.") - chat_history_length = len(chat_history) - self.to_screen('Extracted %d chat messages' % chat_history_length) - if chat_history_length == 0: - return None + if not chat_history: + return - return { - 'live_chat': [ # subtitle tag - { - 'data': json.dumps(chat_history), - 'ext': 'twitch-gql-20221228.json' - } - ] - } + self.to_screen('Extracted %d chat messages' % len(chat_history)) + + return { 'rechat': [{ + 'data': json.dumps(chat_history), + 'ext': 'twitch-gql-20221228.json' + }]} def _real_extract(self, url): vod_id = self._match_id(url) @@ -635,9 +621,8 @@ class TwitchVodIE(TwitchBaseIE): if 't' in query: info['start_time'] = parse_duration(query['t'][0]) - if ('live_chat' in self.get_param('subtitleslangs', [])) \ - and info.get('timestamp') is not None: - info['subtitles'] = self._extract_chat(vod_id) + if info.get('timestamp'): + info['subtitles'] = self.extract_subtitles(vod_id) return info From 548c359b0865c27eeefee047739a26379cb049ff Mon Sep 17 00:00:00 2001 From: mpeter50 <83356418+mpeter50@users.noreply.github.com> Date: Fri, 14 Jul 2023 13:38:50 +0200 Subject: [PATCH 08/13] twitch chat: review changes part 2 --- yt_dlp/extractor/twitch.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/yt_dlp/extractor/twitch.py b/yt_dlp/extractor/twitch.py index 6af27ef6b..e44154919 100644 --- a/yt_dlp/extractor/twitch.py +++ b/yt_dlp/extractor/twitch.py @@ -24,7 +24,6 @@ from ..utils import ( parse_iso8601, parse_qs, qualities, - RetryManager, str_or_none, traverse_obj, try_get, @@ -534,7 +533,7 @@ class TwitchVodIE(TwitchBaseIE): pagenum = 1 gql_ops = [{ 'operationName': 'VideoCommentsByOffsetOrCursor', - 'variables': { 'videoID': vod_id } + 'variables': {'videoID': vod_id} # 'variables.cursor': }] @@ -543,8 +542,6 @@ class TwitchVodIE(TwitchBaseIE): for retry in self.RetryManager(): response = self._download_gql(vod_id, gql_ops, 'Downloading chat fragment page %d' % pagenum, fatal=False) - # response = False - # TODO: delete the direct False, uncomment _download_gql if response is False: retry.error = ExtractorError("f'Unable to fetch next chat history fragment.'", video_id=vod_id, ie=self) @@ -552,8 +549,6 @@ class TwitchVodIE(TwitchBaseIE): # TODO: when this happens, should I forget a partial chat history, or is it better to keep it? # I think if I keep it, it might be better to persist a warning that it is incomplete - # time.sleep(5) - response_errors = traverse_obj(response, (..., 'errors')) if response_errors: self.report_warning(f"Error response recevied for fetching next chat history fragment: {response_errors}") @@ -582,10 +577,10 @@ class TwitchVodIE(TwitchBaseIE): if not chat_history: return + else: + self.write_debug(f'Extracted {len(chat_history)} chat messages') - self.to_screen('Extracted %d chat messages' % len(chat_history)) - - return { 'rechat': [{ + return {'rechat': [{ 'data': json.dumps(chat_history), 'ext': 'twitch-gql-20221228.json' }]} @@ -622,7 +617,12 @@ class TwitchVodIE(TwitchBaseIE): info['start_time'] = parse_duration(query['t'][0]) if info.get('timestamp'): - info['subtitles'] = self.extract_subtitles(vod_id) + info['subtitles'] = {'rechat': [{ + 'url': update_url_query(f'https://api.twitch.tv/v5/videos/{vod_id}/comments', + {'client_id': self._CLIENT_ID}), + 'ext': 'json', + }]}, + info['__post_extractor'] = lambda: {'subtitles': self.extract_subtitles(vod_id)} return info From 91b714934d94d5eafae71b07c10e585f05cd93f2 Mon Sep 17 00:00:00 2001 From: mpeter50 <83356418+mpeter50@users.noreply.github.com> Date: Fri, 14 Jul 2023 14:47:51 +0200 Subject: [PATCH 09/13] fix setting the subtitle in the infodict --- yt_dlp/extractor/twitch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yt_dlp/extractor/twitch.py b/yt_dlp/extractor/twitch.py index e44154919..8781a038d 100644 --- a/yt_dlp/extractor/twitch.py +++ b/yt_dlp/extractor/twitch.py @@ -621,7 +621,7 @@ class TwitchVodIE(TwitchBaseIE): 'url': update_url_query(f'https://api.twitch.tv/v5/videos/{vod_id}/comments', {'client_id': self._CLIENT_ID}), 'ext': 'json', - }]}, + }]} info['__post_extractor'] = lambda: {'subtitles': self.extract_subtitles(vod_id)} return info From 7cdb94e892e95ba115018a93eccf87c494ab7540 Mon Sep 17 00:00:00 2001 From: mpeter50 <83356418+mpeter50@users.noreply.github.com> Date: Fri, 14 Jul 2023 15:43:04 +0200 Subject: [PATCH 10/13] add extractor args for setting the IDs from args --- yt_dlp/extractor/twitch.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/yt_dlp/extractor/twitch.py b/yt_dlp/extractor/twitch.py index 8781a038d..d3c3583b9 100644 --- a/yt_dlp/extractor/twitch.py +++ b/yt_dlp/extractor/twitch.py @@ -63,6 +63,14 @@ class TwitchBaseIE(InfoExtractor): return self._configuration_arg( 'client_id', ['ue6666qo983tsx6so1t0vnawi233wa'], ie_key='Twitch', casesense=True)[0] + @property + def _DEVICE_ID(self): + return self._configuration_arg('device_id', [None], ie_key='Twitch')[0] + + @property + def _CLIENT_INTEGRITY(self): + return self._configuration_arg('client_integrity', [None], ie_key='Twitch', casesense=True)[0] + def _perform_login(self, username, password): def fail(message): raise ExtractorError( @@ -147,6 +155,14 @@ class TwitchBaseIE(InfoExtractor): gql_auth = self._get_cookies('https://gql.twitch.tv').get('auth-token') if gql_auth: headers['Authorization'] = 'OAuth ' + gql_auth.value + + # TODO: remove existence checks when the values will be generated + if self._DEVICE_ID: + headers["X-Device-Id"] = self._DEVICE_ID + + if self._CLIENT_INTEGRITY: + headers["Client-Integrity"] = self._CLIENT_INTEGRITY + return self._download_json( 'https://gql.twitch.tv/gql', video_id, note, data=json.dumps(ops).encode(), From e4a56bd921d3fb79ae9bbcaf44d16596fb12adb6 Mon Sep 17 00:00:00 2001 From: mpeter50 <83356418+mpeter50@users.noreply.github.com> Date: Sat, 9 Sep 2023 21:52:57 +0200 Subject: [PATCH 11/13] read device_id extractor arg case sensitively the Device ID usually contains lowercase and uppercase letters, and twitch cares about it --- yt_dlp/extractor/twitch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yt_dlp/extractor/twitch.py b/yt_dlp/extractor/twitch.py index d3c3583b9..feb9b7394 100644 --- a/yt_dlp/extractor/twitch.py +++ b/yt_dlp/extractor/twitch.py @@ -65,7 +65,7 @@ class TwitchBaseIE(InfoExtractor): @property def _DEVICE_ID(self): - return self._configuration_arg('device_id', [None], ie_key='Twitch')[0] + return self._configuration_arg('device_id', [None], ie_key='Twitch', casesense=True)[0] @property def _CLIENT_INTEGRITY(self): From 09760b227d3051e516926308639256e6afcb37b5 Mon Sep 17 00:00:00 2001 From: mpeter50 <83356418+mpeter50@users.noreply.github.com> Date: Sat, 9 Sep 2023 21:56:36 +0200 Subject: [PATCH 12/13] revert moving comment extraction to late running function --- yt_dlp/extractor/twitch.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/yt_dlp/extractor/twitch.py b/yt_dlp/extractor/twitch.py index feb9b7394..1a4bee023 100644 --- a/yt_dlp/extractor/twitch.py +++ b/yt_dlp/extractor/twitch.py @@ -633,12 +633,7 @@ class TwitchVodIE(TwitchBaseIE): info['start_time'] = parse_duration(query['t'][0]) if info.get('timestamp'): - info['subtitles'] = {'rechat': [{ - 'url': update_url_query(f'https://api.twitch.tv/v5/videos/{vod_id}/comments', - {'client_id': self._CLIENT_ID}), - 'ext': 'json', - }]} - info['__post_extractor'] = lambda: {'subtitles': self.extract_subtitles(vod_id)} + info['subtitles'] = self.extract_subtitles(vod_id) return info From 0500dbd9056dd7102daf75254169c72ef2727656 Mon Sep 17 00:00:00 2001 From: mpeter50 <83356418+mpeter50@users.noreply.github.com> Date: Sat, 9 Sep 2023 22:43:48 +0200 Subject: [PATCH 13/13] redo moving chat extraction to __post_exctractor --- yt_dlp/extractor/twitch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yt_dlp/extractor/twitch.py b/yt_dlp/extractor/twitch.py index 1a4bee023..ae2d0a32a 100644 --- a/yt_dlp/extractor/twitch.py +++ b/yt_dlp/extractor/twitch.py @@ -633,7 +633,7 @@ class TwitchVodIE(TwitchBaseIE): info['start_time'] = parse_duration(query['t'][0]) if info.get('timestamp'): - info['subtitles'] = self.extract_subtitles(vod_id) + info['__post_extractor'] = lambda: {'requested_subtitles': {'rechat': traverse_obj(self.extract_subtitles(vod_id), ['rechat', 0])}} return info