From a7d5579209749c7dfc0835d32d2e6ee039707646 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 1 Feb 2024 15:31:47 +1100 Subject: [PATCH 01/21] Expose preselect and useDetails params with select dialog --- .../kodion/ui/abstract_context_ui.py | 2 +- .../kodion/ui/xbmc/xbmc_context_ui.py | 50 ++++++++++--------- .../lib/youtube_plugin/script_actions.py | 9 +++- 3 files changed, 34 insertions(+), 27 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/ui/abstract_context_ui.py b/resources/lib/youtube_plugin/kodion/ui/abstract_context_ui.py index 73e8da332..e9a2fa780 100644 --- a/resources/lib/youtube_plugin/kodion/ui/abstract_context_ui.py +++ b/resources/lib/youtube_plugin/kodion/ui/abstract_context_ui.py @@ -33,7 +33,7 @@ def on_ok(self, title, text): def on_remove_content(self, content_name): raise NotImplementedError() - def on_select(self, title, items=None): + def on_select(self, title, items=None, preselect=-1, use_details=False): raise NotImplementedError() def open_settings(self): diff --git a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py index b6fc6a7e2..0180834f8 100644 --- a/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py +++ b/resources/lib/youtube_plugin/kodion/ui/xbmc/xbmc_context_ui.py @@ -66,38 +66,40 @@ def on_delete_content(self, content_name): text = self._context.localize('content.delete') % to_unicode(content_name) return self.on_yes_no_input(self._context.localize('content.delete.confirm'), text) - def on_select(self, title, items=None): + def on_select(self, title, items=None, preselect=-1, use_details=False): if items is None: items = [] - use_details = (isinstance(items[0], tuple) and len(items[0]) == 4) - - _dict = {} - _items = [] - i = 0 - for item in items: + result_map = {} + dialog_items = [] + for idx, item in enumerate(items): if isinstance(item, tuple): - if use_details: - new_item = xbmcgui.ListItem(label=item[0], label2=item[1]) - new_item.setArt({'icon': item[3], 'thumb': item[3]}) - _items.append(new_item) - _dict[i] = item[2] + num_details = len(item) + if num_details > 2: + list_item = xbmcgui.ListItem(label=item[0], + label2=item[1], + offscreen=True) + if num_details > 3: + use_details = True + icon = item[3] + list_item.setArt({'icon': icon, 'thumb': icon}) + if num_details > 4 and item[4]: + preselect = idx + result_map[idx] = item[2] + dialog_items.append(list_item) else: - _dict[i] = item[1] - _items.append(item[0]) + result_map[idx] = item[1] + dialog_items.append(item[0]) else: - _dict[i] = i - _items.append(item) - - i += 1 + result_map[idx] = idx + dialog_items.append(item) dialog = xbmcgui.Dialog() - if use_details: - result = dialog.select(title, _items, useDetails=use_details) - else: - result = dialog.select(title, _items) - - return _dict.get(result, -1) + result = dialog.select(title, + dialog_items, + preselect=preselect, + useDetails=use_details) + return result_map.get(result, -1) def show_notification(self, message, diff --git a/resources/lib/youtube_plugin/script_actions.py b/resources/lib/youtube_plugin/script_actions.py index 2bd4560b5..8e0401a33 100644 --- a/resources/lib/youtube_plugin/script_actions.py +++ b/resources/lib/youtube_plugin/script_actions.py @@ -58,7 +58,9 @@ def _config_actions(action, *_args): ] sub_opts[sub_setting] = ui.bold(sub_opts[sub_setting]) - result = ui.on_select(localize('subtitles.language'), sub_opts) + result = ui.on_select(localize('subtitles.language'), + sub_opts, + preselect=sub_setting) if result > -1: settings.set_subtitle_languages(result) @@ -182,7 +184,10 @@ def select_user(reason, new_user=False): usernames.append(username) if new_user: usernames.append(ui.italic(localize('user.new'))) - return ui.on_select(reason, usernames), sorted(current_users.keys()) + return ( + ui.on_select(reason, usernames, preselect=current_user), + sorted(current_users.keys()), + ) def add_user(): results = ui.on_keyboard_input(localize('user.enter_name')) From 721571a3116be35ed8d82690a5359a5b6c1c4d7f Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 1 Feb 2024 15:33:39 +1100 Subject: [PATCH 02/21] Enable XbmcContext.get_language --- .../kodion/context/xbmc/xbmc_context.py | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index 271709431..124c5fd71 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -327,24 +327,10 @@ def format_time(time_obj, str_format=None): return time_obj.strftime(str_format) def get_language(self): - """ - The xbmc.getLanguage() method is fucked up!!! We always return 'en-US' for now - """ - - """ - if self.get_system_version().get_release_name() == 'Frodo': - return 'en-US' - - try: - language = xbmc.getLanguage(0, region=True) - language = language.split('-') - language = '%s-%s' % (language[0].lower(), language[1].upper()) - return language - except Exception as exc: - self.log_error('Failed to get system language (%s)', exc.__str__()) - return 'en-US' - """ - + kodi_language = xbmc.getLanguage(format=xbmc.ISO_639_1, region=True) + lang_code, seperator, region = kodi_language.partition('-') + if region: + return seperator.join((lang_code.lower(), region.upper())) return 'en-US' def get_language_name(self, lang_id=None): From 9763d83703ff23f3182034df90945fd0d3b1caa6 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 1 Feb 2024 15:46:11 +1100 Subject: [PATCH 03/21] Tidy up setup wizard --- .../youtube/helper/yt_setup_wizard.py | 272 +++++++++++++----- 1 file changed, 198 insertions(+), 74 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py b/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py index 39f0695f9..3e1c34cb7 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py @@ -13,91 +13,213 @@ from ...kodion.network import Locator -DEFAULT_LANGUAGES = {'items': [{'snippet': {'name': 'Afrikaans', 'hl': 'af'}, 'id': 'af'}, {'snippet': {'name': 'Azerbaijani', 'hl': 'az'}, 'id': 'az'}, {'snippet': {'name': 'Indonesian', 'hl': 'id'}, 'id': 'id'}, {'snippet': {'name': 'Malay', 'hl': 'ms'}, 'id': 'ms'}, - {'snippet': {'name': 'Catalan', 'hl': 'ca'}, 'id': 'ca'}, {'snippet': {'name': 'Czech', 'hl': 'cs'}, 'id': 'cs'}, {'snippet': {'name': 'Danish', 'hl': 'da'}, 'id': 'da'}, {'snippet': {'name': 'German', 'hl': 'de'}, 'id': 'de'}, - {'snippet': {'name': 'Estonian', 'hl': 'et'}, 'id': 'et'}, {'snippet': {'name': 'English (United Kingdom)', 'hl': 'en-GB'}, 'id': 'en-GB'}, {'snippet': {'name': 'English', 'hl': 'en'}, 'id': 'en'}, - {'snippet': {'name': 'Spanish (Spain)', 'hl': 'es'}, 'id': 'es'}, {'snippet': {'name': 'Spanish (Latin America)', 'hl': 'es-419'}, 'id': 'es-419'}, {'snippet': {'name': 'Basque', 'hl': 'eu'}, 'id': 'eu'}, - {'snippet': {'name': 'Filipino', 'hl': 'fil'}, 'id': 'fil'}, {'snippet': {'name': 'French', 'hl': 'fr'}, 'id': 'fr'}, {'snippet': {'name': 'French (Canada)', 'hl': 'fr-CA'}, 'id': 'fr-CA'}, {'snippet': {'name': 'Galician', 'hl': 'gl'}, 'id': 'gl'}, - {'snippet': {'name': 'Croatian', 'hl': 'hr'}, 'id': 'hr'}, {'snippet': {'name': 'Zulu', 'hl': 'zu'}, 'id': 'zu'}, {'snippet': {'name': 'Icelandic', 'hl': 'is'}, 'id': 'is'}, {'snippet': {'name': 'Italian', 'hl': 'it'}, 'id': 'it'}, - {'snippet': {'name': 'Swahili', 'hl': 'sw'}, 'id': 'sw'}, {'snippet': {'name': 'Latvian', 'hl': 'lv'}, 'id': 'lv'}, {'snippet': {'name': 'Lithuanian', 'hl': 'lt'}, 'id': 'lt'}, {'snippet': {'name': 'Hungarian', 'hl': 'hu'}, 'id': 'hu'}, - {'snippet': {'name': 'Dutch', 'hl': 'nl'}, 'id': 'nl'}, {'snippet': {'name': 'Norwegian', 'hl': 'no'}, 'id': 'no'}, {'snippet': {'name': 'Uzbek', 'hl': 'uz'}, 'id': 'uz'}, {'snippet': {'name': 'Polish', 'hl': 'pl'}, 'id': 'pl'}, - {'snippet': {'name': 'Portuguese (Portugal)', 'hl': 'pt-PT'}, 'id': 'pt-PT'}, {'snippet': {'name': 'Portuguese (Brazil)', 'hl': 'pt'}, 'id': 'pt'}, {'snippet': {'name': 'Romanian', 'hl': 'ro'}, 'id': 'ro'}, - {'snippet': {'name': 'Albanian', 'hl': 'sq'}, 'id': 'sq'}, {'snippet': {'name': 'Slovak', 'hl': 'sk'}, 'id': 'sk'}, {'snippet': {'name': 'Slovenian', 'hl': 'sl'}, 'id': 'sl'}, {'snippet': {'name': 'Finnish', 'hl': 'fi'}, 'id': 'fi'}, - {'snippet': {'name': 'Swedish', 'hl': 'sv'}, 'id': 'sv'}, {'snippet': {'name': 'Vietnamese', 'hl': 'vi'}, 'id': 'vi'}, {'snippet': {'name': 'Turkish', 'hl': 'tr'}, 'id': 'tr'}, {'snippet': {'name': 'Bulgarian', 'hl': 'bg'}, 'id': 'bg'}, - {'snippet': {'name': 'Kyrgyz', 'hl': 'ky'}, 'id': 'ky'}, {'snippet': {'name': 'Kazakh', 'hl': 'kk'}, 'id': 'kk'}, {'snippet': {'name': 'Macedonian', 'hl': 'mk'}, 'id': 'mk'}, {'snippet': {'name': 'Mongolian', 'hl': 'mn'}, 'id': 'mn'}, - {'snippet': {'name': 'Russian', 'hl': 'ru'}, 'id': 'ru'}, {'snippet': {'name': 'Serbian', 'hl': 'sr'}, 'id': 'sr'}, {'snippet': {'name': 'Ukrainian', 'hl': 'uk'}, 'id': 'uk'}, {'snippet': {'name': 'Greek', 'hl': 'el'}, 'id': 'el'}, - {'snippet': {'name': 'Armenian', 'hl': 'hy'}, 'id': 'hy'}, {'snippet': {'name': 'Hebrew', 'hl': 'iw'}, 'id': 'iw'}, {'snippet': {'name': 'Urdu', 'hl': 'ur'}, 'id': 'ur'}, {'snippet': {'name': 'Arabic', 'hl': 'ar'}, 'id': 'ar'}, - {'snippet': {'name': 'Persian', 'hl': 'fa'}, 'id': 'fa'}, {'snippet': {'name': 'Nepali', 'hl': 'ne'}, 'id': 'ne'}, {'snippet': {'name': 'Marathi', 'hl': 'mr'}, 'id': 'mr'}, {'snippet': {'name': 'Hindi', 'hl': 'hi'}, 'id': 'hi'}, - {'snippet': {'name': 'Bengali', 'hl': 'bn'}, 'id': 'bn'}, {'snippet': {'name': 'Punjabi', 'hl': 'pa'}, 'id': 'pa'}, {'snippet': {'name': 'Gujarati', 'hl': 'gu'}, 'id': 'gu'}, {'snippet': {'name': 'Tamil', 'hl': 'ta'}, 'id': 'ta'}, - {'snippet': {'name': 'Telugu', 'hl': 'te'}, 'id': 'te'}, {'snippet': {'name': 'Kannada', 'hl': 'kn'}, 'id': 'kn'}, {'snippet': {'name': 'Malayalam', 'hl': 'ml'}, 'id': 'ml'}, {'snippet': {'name': 'Sinhala', 'hl': 'si'}, 'id': 'si'}, - {'snippet': {'name': 'Thai', 'hl': 'th'}, 'id': 'th'}, {'snippet': {'name': 'Lao', 'hl': 'lo'}, 'id': 'lo'}, {'snippet': {'name': 'Myanmar (Burmese)', 'hl': 'my'}, 'id': 'my'}, {'snippet': {'name': 'Georgian', 'hl': 'ka'}, 'id': 'ka'}, - {'snippet': {'name': 'Amharic', 'hl': 'am'}, 'id': 'am'}, {'snippet': {'name': 'Khmer', 'hl': 'km'}, 'id': 'km'}, {'snippet': {'name': 'Chinese', 'hl': 'zh-CN'}, 'id': 'zh-CN'}, {'snippet': {'name': 'Chinese (Taiwan)', 'hl': 'zh-TW'}, 'id': 'zh-TW'}, - {'snippet': {'name': 'Chinese (Hong Kong)', 'hl': 'zh-HK'}, 'id': 'zh-HK'}, {'snippet': {'name': 'Japanese', 'hl': 'ja'}, 'id': 'ja'}, {'snippet': {'name': 'Korean', 'hl': 'ko'}, 'id': 'ko'}]} -DEFAULT_REGIONS = {'items': [{'snippet': {'gl': 'DZ', 'name': 'Algeria'}, 'id': 'DZ'}, {'snippet': {'gl': 'AR', 'name': 'Argentina'}, 'id': 'AR'}, {'snippet': {'gl': 'AU', 'name': 'Australia'}, 'id': 'AU'}, {'snippet': {'gl': 'AT', 'name': 'Austria'}, 'id': 'AT'}, - {'snippet': {'gl': 'AZ', 'name': 'Azerbaijan'}, 'id': 'AZ'}, {'snippet': {'gl': 'BH', 'name': 'Bahrain'}, 'id': 'BH'}, {'snippet': {'gl': 'BY', 'name': 'Belarus'}, 'id': 'BY'}, {'snippet': {'gl': 'BE', 'name': 'Belgium'}, 'id': 'BE'}, - {'snippet': {'gl': 'BA', 'name': 'Bosnia and Herzegovina'}, 'id': 'BA'}, {'snippet': {'gl': 'BR', 'name': 'Brazil'}, 'id': 'BR'}, {'snippet': {'gl': 'BG', 'name': 'Bulgaria'}, 'id': 'BG'}, {'snippet': {'gl': 'CA', 'name': 'Canada'}, 'id': 'CA'}, - {'snippet': {'gl': 'CL', 'name': 'Chile'}, 'id': 'CL'}, {'snippet': {'gl': 'CO', 'name': 'Colombia'}, 'id': 'CO'}, {'snippet': {'gl': 'HR', 'name': 'Croatia'}, 'id': 'HR'}, {'snippet': {'gl': 'CZ', 'name': 'Czech Republic'}, 'id': 'CZ'}, - {'snippet': {'gl': 'DK', 'name': 'Denmark'}, 'id': 'DK'}, {'snippet': {'gl': 'EG', 'name': 'Egypt'}, 'id': 'EG'}, {'snippet': {'gl': 'EE', 'name': 'Estonia'}, 'id': 'EE'}, {'snippet': {'gl': 'FI', 'name': 'Finland'}, 'id': 'FI'}, - {'snippet': {'gl': 'FR', 'name': 'France'}, 'id': 'FR'}, {'snippet': {'gl': 'GE', 'name': 'Georgia'}, 'id': 'GE'}, {'snippet': {'gl': 'DE', 'name': 'Germany'}, 'id': 'DE'}, {'snippet': {'gl': 'GH', 'name': 'Ghana'}, 'id': 'GH'}, - {'snippet': {'gl': 'GR', 'name': 'Greece'}, 'id': 'GR'}, {'snippet': {'gl': 'HK', 'name': 'Hong Kong'}, 'id': 'HK'}, {'snippet': {'gl': 'HU', 'name': 'Hungary'}, 'id': 'HU'}, {'snippet': {'gl': 'IS', 'name': 'Iceland'}, 'id': 'IS'}, - {'snippet': {'gl': 'IN', 'name': 'India'}, 'id': 'IN'}, {'snippet': {'gl': 'ID', 'name': 'Indonesia'}, 'id': 'ID'}, {'snippet': {'gl': 'IQ', 'name': 'Iraq'}, 'id': 'IQ'}, {'snippet': {'gl': 'IE', 'name': 'Ireland'}, 'id': 'IE'}, - {'snippet': {'gl': 'IL', 'name': 'Israel'}, 'id': 'IL'}, {'snippet': {'gl': 'IT', 'name': 'Italy'}, 'id': 'IT'}, {'snippet': {'gl': 'JM', 'name': 'Jamaica'}, 'id': 'JM'}, {'snippet': {'gl': 'JP', 'name': 'Japan'}, 'id': 'JP'}, - {'snippet': {'gl': 'JO', 'name': 'Jordan'}, 'id': 'JO'}, {'snippet': {'gl': 'KZ', 'name': 'Kazakhstan'}, 'id': 'KZ'}, {'snippet': {'gl': 'KE', 'name': 'Kenya'}, 'id': 'KE'}, {'snippet': {'gl': 'KW', 'name': 'Kuwait'}, 'id': 'KW'}, - {'snippet': {'gl': 'LV', 'name': 'Latvia'}, 'id': 'LV'}, {'snippet': {'gl': 'LB', 'name': 'Lebanon'}, 'id': 'LB'}, {'snippet': {'gl': 'LY', 'name': 'Libya'}, 'id': 'LY'}, {'snippet': {'gl': 'LT', 'name': 'Lithuania'}, 'id': 'LT'}, - {'snippet': {'gl': 'LU', 'name': 'Luxembourg'}, 'id': 'LU'}, {'snippet': {'gl': 'MK', 'name': 'Macedonia'}, 'id': 'MK'}, {'snippet': {'gl': 'MY', 'name': 'Malaysia'}, 'id': 'MY'}, {'snippet': {'gl': 'MX', 'name': 'Mexico'}, 'id': 'MX'}, - {'snippet': {'gl': 'ME', 'name': 'Montenegro'}, 'id': 'ME'}, {'snippet': {'gl': 'MA', 'name': 'Morocco'}, 'id': 'MA'}, {'snippet': {'gl': 'NP', 'name': 'Nepal'}, 'id': 'NP'}, {'snippet': {'gl': 'NL', 'name': 'Netherlands'}, 'id': 'NL'}, - {'snippet': {'gl': 'NZ', 'name': 'New Zealand'}, 'id': 'NZ'}, {'snippet': {'gl': 'NG', 'name': 'Nigeria'}, 'id': 'NG'}, {'snippet': {'gl': 'NO', 'name': 'Norway'}, 'id': 'NO'}, {'snippet': {'gl': 'OM', 'name': 'Oman'}, 'id': 'OM'}, - {'snippet': {'gl': 'PK', 'name': 'Pakistan'}, 'id': 'PK'}, {'snippet': {'gl': 'PE', 'name': 'Peru'}, 'id': 'PE'}, {'snippet': {'gl': 'PH', 'name': 'Philippines'}, 'id': 'PH'}, {'snippet': {'gl': 'PL', 'name': 'Poland'}, 'id': 'PL'}, - {'snippet': {'gl': 'PT', 'name': 'Portugal'}, 'id': 'PT'}, {'snippet': {'gl': 'PR', 'name': 'Puerto Rico'}, 'id': 'PR'}, {'snippet': {'gl': 'QA', 'name': 'Qatar'}, 'id': 'QA'}, {'snippet': {'gl': 'RO', 'name': 'Romania'}, 'id': 'RO'}, - {'snippet': {'gl': 'RU', 'name': 'Russia'}, 'id': 'RU'}, {'snippet': {'gl': 'SA', 'name': 'Saudi Arabia'}, 'id': 'SA'}, {'snippet': {'gl': 'SN', 'name': 'Senegal'}, 'id': 'SN'}, {'snippet': {'gl': 'RS', 'name': 'Serbia'}, 'id': 'RS'}, - {'snippet': {'gl': 'SG', 'name': 'Singapore'}, 'id': 'SG'}, {'snippet': {'gl': 'SK', 'name': 'Slovakia'}, 'id': 'SK'}, {'snippet': {'gl': 'SI', 'name': 'Slovenia'}, 'id': 'SI'}, {'snippet': {'gl': 'ZA', 'name': 'South Africa'}, 'id': 'ZA'}, - {'snippet': {'gl': 'KR', 'name': 'South Korea'}, 'id': 'KR'}, {'snippet': {'gl': 'ES', 'name': 'Spain'}, 'id': 'ES'}, {'snippet': {'gl': 'LK', 'name': 'Sri Lanka'}, 'id': 'LK'}, {'snippet': {'gl': 'SE', 'name': 'Sweden'}, 'id': 'SE'}, - {'snippet': {'gl': 'CH', 'name': 'Switzerland'}, 'id': 'CH'}, {'snippet': {'gl': 'TW', 'name': 'Taiwan'}, 'id': 'TW'}, {'snippet': {'gl': 'TZ', 'name': 'Tanzania'}, 'id': 'TZ'}, {'snippet': {'gl': 'TH', 'name': 'Thailand'}, 'id': 'TH'}, - {'snippet': {'gl': 'TN', 'name': 'Tunisia'}, 'id': 'TN'}, {'snippet': {'gl': 'TR', 'name': 'Turkey'}, 'id': 'TR'}, {'snippet': {'gl': 'UG', 'name': 'Uganda'}, 'id': 'UG'}, {'snippet': {'gl': 'UA', 'name': 'Ukraine'}, 'id': 'UA'}, - {'snippet': {'gl': 'AE', 'name': 'United Arab Emirates'}, 'id': 'AE'}, {'snippet': {'gl': 'GB', 'name': 'United Kingdom'}, 'id': 'GB'}, {'snippet': {'gl': 'US', 'name': 'United States'}, 'id': 'US'}, {'snippet': {'gl': 'VN', 'name': 'Vietnam'}, 'id': 'VN'}, - {'snippet': {'gl': 'YE', 'name': 'Yemen'}, 'id': 'YE'}, {'snippet': {'gl': 'ZW', 'name': 'Zimbabwe'}, 'id': 'ZW'}]} +DEFAULT_LANGUAGES = {'items': [ + {'snippet': {'name': 'Afrikaans', 'hl': 'af'}, 'id': 'af'}, + {'snippet': {'name': 'Azerbaijani', 'hl': 'az'}, 'id': 'az'}, + {'snippet': {'name': 'Indonesian', 'hl': 'id'}, 'id': 'id'}, + {'snippet': {'name': 'Malay', 'hl': 'ms'}, 'id': 'ms'}, + {'snippet': {'name': 'Catalan', 'hl': 'ca'}, 'id': 'ca'}, + {'snippet': {'name': 'Czech', 'hl': 'cs'}, 'id': 'cs'}, + {'snippet': {'name': 'Danish', 'hl': 'da'}, 'id': 'da'}, + {'snippet': {'name': 'German', 'hl': 'de'}, 'id': 'de'}, + {'snippet': {'name': 'Estonian', 'hl': 'et'}, 'id': 'et'}, + {'snippet': {'name': 'English (United Kingdom)', 'hl': 'en-GB'}, 'id': 'en-GB'}, + {'snippet': {'name': 'English', 'hl': 'en'}, 'id': 'en'}, + {'snippet': {'name': 'Spanish (Spain)', 'hl': 'es'}, 'id': 'es'}, + {'snippet': {'name': 'Spanish (Latin America)', 'hl': 'es-419'}, 'id': 'es-419'}, + {'snippet': {'name': 'Basque', 'hl': 'eu'}, 'id': 'eu'}, + {'snippet': {'name': 'Filipino', 'hl': 'fil'}, 'id': 'fil'}, + {'snippet': {'name': 'French', 'hl': 'fr'}, 'id': 'fr'}, + {'snippet': {'name': 'French (Canada)', 'hl': 'fr-CA'}, 'id': 'fr-CA'}, + {'snippet': {'name': 'Galician', 'hl': 'gl'}, 'id': 'gl'}, + {'snippet': {'name': 'Croatian', 'hl': 'hr'}, 'id': 'hr'}, + {'snippet': {'name': 'Zulu', 'hl': 'zu'}, 'id': 'zu'}, + {'snippet': {'name': 'Icelandic', 'hl': 'is'}, 'id': 'is'}, + {'snippet': {'name': 'Italian', 'hl': 'it'}, 'id': 'it'}, + {'snippet': {'name': 'Swahili', 'hl': 'sw'}, 'id': 'sw'}, + {'snippet': {'name': 'Latvian', 'hl': 'lv'}, 'id': 'lv'}, + {'snippet': {'name': 'Lithuanian', 'hl': 'lt'}, 'id': 'lt'}, + {'snippet': {'name': 'Hungarian', 'hl': 'hu'}, 'id': 'hu'}, + {'snippet': {'name': 'Dutch', 'hl': 'nl'}, 'id': 'nl'}, + {'snippet': {'name': 'Norwegian', 'hl': 'no'}, 'id': 'no'}, + {'snippet': {'name': 'Uzbek', 'hl': 'uz'}, 'id': 'uz'}, + {'snippet': {'name': 'Polish', 'hl': 'pl'}, 'id': 'pl'}, + {'snippet': {'name': 'Portuguese (Portugal)', 'hl': 'pt-PT'}, 'id': 'pt-PT'}, + {'snippet': {'name': 'Portuguese (Brazil)', 'hl': 'pt'}, 'id': 'pt'}, + {'snippet': {'name': 'Romanian', 'hl': 'ro'}, 'id': 'ro'}, + {'snippet': {'name': 'Albanian', 'hl': 'sq'}, 'id': 'sq'}, + {'snippet': {'name': 'Slovak', 'hl': 'sk'}, 'id': 'sk'}, + {'snippet': {'name': 'Slovenian', 'hl': 'sl'}, 'id': 'sl'}, + {'snippet': {'name': 'Finnish', 'hl': 'fi'}, 'id': 'fi'}, + {'snippet': {'name': 'Swedish', 'hl': 'sv'}, 'id': 'sv'}, + {'snippet': {'name': 'Vietnamese', 'hl': 'vi'}, 'id': 'vi'}, + {'snippet': {'name': 'Turkish', 'hl': 'tr'}, 'id': 'tr'}, + {'snippet': {'name': 'Bulgarian', 'hl': 'bg'}, 'id': 'bg'}, + {'snippet': {'name': 'Kyrgyz', 'hl': 'ky'}, 'id': 'ky'}, + {'snippet': {'name': 'Kazakh', 'hl': 'kk'}, 'id': 'kk'}, + {'snippet': {'name': 'Macedonian', 'hl': 'mk'}, 'id': 'mk'}, + {'snippet': {'name': 'Mongolian', 'hl': 'mn'}, 'id': 'mn'}, + {'snippet': {'name': 'Russian', 'hl': 'ru'}, 'id': 'ru'}, + {'snippet': {'name': 'Serbian', 'hl': 'sr'}, 'id': 'sr'}, + {'snippet': {'name': 'Ukrainian', 'hl': 'uk'}, 'id': 'uk'}, + {'snippet': {'name': 'Greek', 'hl': 'el'}, 'id': 'el'}, + {'snippet': {'name': 'Armenian', 'hl': 'hy'}, 'id': 'hy'}, + {'snippet': {'name': 'Hebrew', 'hl': 'iw'}, 'id': 'iw'}, + {'snippet': {'name': 'Urdu', 'hl': 'ur'}, 'id': 'ur'}, + {'snippet': {'name': 'Arabic', 'hl': 'ar'}, 'id': 'ar'}, + {'snippet': {'name': 'Persian', 'hl': 'fa'}, 'id': 'fa'}, + {'snippet': {'name': 'Nepali', 'hl': 'ne'}, 'id': 'ne'}, + {'snippet': {'name': 'Marathi', 'hl': 'mr'}, 'id': 'mr'}, + {'snippet': {'name': 'Hindi', 'hl': 'hi'}, 'id': 'hi'}, + {'snippet': {'name': 'Bengali', 'hl': 'bn'}, 'id': 'bn'}, + {'snippet': {'name': 'Punjabi', 'hl': 'pa'}, 'id': 'pa'}, + {'snippet': {'name': 'Gujarati', 'hl': 'gu'}, 'id': 'gu'}, + {'snippet': {'name': 'Tamil', 'hl': 'ta'}, 'id': 'ta'}, + {'snippet': {'name': 'Telugu', 'hl': 'te'}, 'id': 'te'}, + {'snippet': {'name': 'Kannada', 'hl': 'kn'}, 'id': 'kn'}, + {'snippet': {'name': 'Malayalam', 'hl': 'ml'}, 'id': 'ml'}, + {'snippet': {'name': 'Sinhala', 'hl': 'si'}, 'id': 'si'}, + {'snippet': {'name': 'Thai', 'hl': 'th'}, 'id': 'th'}, + {'snippet': {'name': 'Lao', 'hl': 'lo'}, 'id': 'lo'}, + {'snippet': {'name': 'Myanmar (Burmese)', 'hl': 'my'}, 'id': 'my'}, + {'snippet': {'name': 'Georgian', 'hl': 'ka'}, 'id': 'ka'}, + {'snippet': {'name': 'Amharic', 'hl': 'am'}, 'id': 'am'}, + {'snippet': {'name': 'Khmer', 'hl': 'km'}, 'id': 'km'}, + {'snippet': {'name': 'Chinese', 'hl': 'zh-CN'}, 'id': 'zh-CN'}, + {'snippet': {'name': 'Chinese (Taiwan)', 'hl': 'zh-TW'}, 'id': 'zh-TW'}, + {'snippet': {'name': 'Chinese (Hong Kong)', 'hl': 'zh-HK'}, 'id': 'zh-HK'}, + {'snippet': {'name': 'Japanese', 'hl': 'ja'}, 'id': 'ja'}, + {'snippet': {'name': 'Korean', 'hl': 'ko'}, 'id': 'ko'}, +]} +DEFAULT_REGIONS = {'items': [ + {'snippet': {'gl': 'DZ', 'name': 'Algeria'}, 'id': 'DZ'}, + {'snippet': {'gl': 'AR', 'name': 'Argentina'}, 'id': 'AR'}, + {'snippet': {'gl': 'AU', 'name': 'Australia'}, 'id': 'AU'}, + {'snippet': {'gl': 'AT', 'name': 'Austria'}, 'id': 'AT'}, + {'snippet': {'gl': 'AZ', 'name': 'Azerbaijan'}, 'id': 'AZ'}, + {'snippet': {'gl': 'BH', 'name': 'Bahrain'}, 'id': 'BH'}, + {'snippet': {'gl': 'BY', 'name': 'Belarus'}, 'id': 'BY'}, + {'snippet': {'gl': 'BE', 'name': 'Belgium'}, 'id': 'BE'}, + {'snippet': {'gl': 'BA', 'name': 'Bosnia and Herzegovina'}, 'id': 'BA'}, + {'snippet': {'gl': 'BR', 'name': 'Brazil'}, 'id': 'BR'}, + {'snippet': {'gl': 'BG', 'name': 'Bulgaria'}, 'id': 'BG'}, + {'snippet': {'gl': 'CA', 'name': 'Canada'}, 'id': 'CA'}, + {'snippet': {'gl': 'CL', 'name': 'Chile'}, 'id': 'CL'}, + {'snippet': {'gl': 'CO', 'name': 'Colombia'}, 'id': 'CO'}, + {'snippet': {'gl': 'HR', 'name': 'Croatia'}, 'id': 'HR'}, + {'snippet': {'gl': 'CZ', 'name': 'Czech Republic'}, 'id': 'CZ'}, + {'snippet': {'gl': 'DK', 'name': 'Denmark'}, 'id': 'DK'}, + {'snippet': {'gl': 'EG', 'name': 'Egypt'}, 'id': 'EG'}, + {'snippet': {'gl': 'EE', 'name': 'Estonia'}, 'id': 'EE'}, + {'snippet': {'gl': 'FI', 'name': 'Finland'}, 'id': 'FI'}, + {'snippet': {'gl': 'FR', 'name': 'France'}, 'id': 'FR'}, + {'snippet': {'gl': 'GE', 'name': 'Georgia'}, 'id': 'GE'}, + {'snippet': {'gl': 'DE', 'name': 'Germany'}, 'id': 'DE'}, + {'snippet': {'gl': 'GH', 'name': 'Ghana'}, 'id': 'GH'}, + {'snippet': {'gl': 'GR', 'name': 'Greece'}, 'id': 'GR'}, + {'snippet': {'gl': 'HK', 'name': 'Hong Kong'}, 'id': 'HK'}, + {'snippet': {'gl': 'HU', 'name': 'Hungary'}, 'id': 'HU'}, + {'snippet': {'gl': 'IS', 'name': 'Iceland'}, 'id': 'IS'}, + {'snippet': {'gl': 'IN', 'name': 'India'}, 'id': 'IN'}, + {'snippet': {'gl': 'ID', 'name': 'Indonesia'}, 'id': 'ID'}, + {'snippet': {'gl': 'IQ', 'name': 'Iraq'}, 'id': 'IQ'}, + {'snippet': {'gl': 'IE', 'name': 'Ireland'}, 'id': 'IE'}, + {'snippet': {'gl': 'IL', 'name': 'Israel'}, 'id': 'IL'}, + {'snippet': {'gl': 'IT', 'name': 'Italy'}, 'id': 'IT'}, + {'snippet': {'gl': 'JM', 'name': 'Jamaica'}, 'id': 'JM'}, + {'snippet': {'gl': 'JP', 'name': 'Japan'}, 'id': 'JP'}, + {'snippet': {'gl': 'JO', 'name': 'Jordan'}, 'id': 'JO'}, + {'snippet': {'gl': 'KZ', 'name': 'Kazakhstan'}, 'id': 'KZ'}, + {'snippet': {'gl': 'KE', 'name': 'Kenya'}, 'id': 'KE'}, + {'snippet': {'gl': 'KW', 'name': 'Kuwait'}, 'id': 'KW'}, + {'snippet': {'gl': 'LV', 'name': 'Latvia'}, 'id': 'LV'}, + {'snippet': {'gl': 'LB', 'name': 'Lebanon'}, 'id': 'LB'}, + {'snippet': {'gl': 'LY', 'name': 'Libya'}, 'id': 'LY'}, + {'snippet': {'gl': 'LT', 'name': 'Lithuania'}, 'id': 'LT'}, + {'snippet': {'gl': 'LU', 'name': 'Luxembourg'}, 'id': 'LU'}, + {'snippet': {'gl': 'MK', 'name': 'Macedonia'}, 'id': 'MK'}, + {'snippet': {'gl': 'MY', 'name': 'Malaysia'}, 'id': 'MY'}, + {'snippet': {'gl': 'MX', 'name': 'Mexico'}, 'id': 'MX'}, + {'snippet': {'gl': 'ME', 'name': 'Montenegro'}, 'id': 'ME'}, + {'snippet': {'gl': 'MA', 'name': 'Morocco'}, 'id': 'MA'}, + {'snippet': {'gl': 'NP', 'name': 'Nepal'}, 'id': 'NP'}, + {'snippet': {'gl': 'NL', 'name': 'Netherlands'}, 'id': 'NL'}, + {'snippet': {'gl': 'NZ', 'name': 'New Zealand'}, 'id': 'NZ'}, + {'snippet': {'gl': 'NG', 'name': 'Nigeria'}, 'id': 'NG'}, + {'snippet': {'gl': 'NO', 'name': 'Norway'}, 'id': 'NO'}, + {'snippet': {'gl': 'OM', 'name': 'Oman'}, 'id': 'OM'}, + {'snippet': {'gl': 'PK', 'name': 'Pakistan'}, 'id': 'PK'}, + {'snippet': {'gl': 'PE', 'name': 'Peru'}, 'id': 'PE'}, + {'snippet': {'gl': 'PH', 'name': 'Philippines'}, 'id': 'PH'}, + {'snippet': {'gl': 'PL', 'name': 'Poland'}, 'id': 'PL'}, + {'snippet': {'gl': 'PT', 'name': 'Portugal'}, 'id': 'PT'}, + {'snippet': {'gl': 'PR', 'name': 'Puerto Rico'}, 'id': 'PR'}, + {'snippet': {'gl': 'QA', 'name': 'Qatar'}, 'id': 'QA'}, + {'snippet': {'gl': 'RO', 'name': 'Romania'}, 'id': 'RO'}, + {'snippet': {'gl': 'RU', 'name': 'Russia'}, 'id': 'RU'}, + {'snippet': {'gl': 'SA', 'name': 'Saudi Arabia'}, 'id': 'SA'}, + {'snippet': {'gl': 'SN', 'name': 'Senegal'}, 'id': 'SN'}, + {'snippet': {'gl': 'RS', 'name': 'Serbia'}, 'id': 'RS'}, + {'snippet': {'gl': 'SG', 'name': 'Singapore'}, 'id': 'SG'}, + {'snippet': {'gl': 'SK', 'name': 'Slovakia'}, 'id': 'SK'}, + {'snippet': {'gl': 'SI', 'name': 'Slovenia'}, 'id': 'SI'}, + {'snippet': {'gl': 'ZA', 'name': 'South Africa'}, 'id': 'ZA'}, + {'snippet': {'gl': 'KR', 'name': 'South Korea'}, 'id': 'KR'}, + {'snippet': {'gl': 'ES', 'name': 'Spain'}, 'id': 'ES'}, + {'snippet': {'gl': 'LK', 'name': 'Sri Lanka'}, 'id': 'LK'}, + {'snippet': {'gl': 'SE', 'name': 'Sweden'}, 'id': 'SE'}, + {'snippet': {'gl': 'CH', 'name': 'Switzerland'}, 'id': 'CH'}, + {'snippet': {'gl': 'TW', 'name': 'Taiwan'}, 'id': 'TW'}, + {'snippet': {'gl': 'TZ', 'name': 'Tanzania'}, 'id': 'TZ'}, + {'snippet': {'gl': 'TH', 'name': 'Thailand'}, 'id': 'TH'}, + {'snippet': {'gl': 'TN', 'name': 'Tunisia'}, 'id': 'TN'}, + {'snippet': {'gl': 'TR', 'name': 'Turkey'}, 'id': 'TR'}, + {'snippet': {'gl': 'UG', 'name': 'Uganda'}, 'id': 'UG'}, + {'snippet': {'gl': 'UA', 'name': 'Ukraine'}, 'id': 'UA'}, + {'snippet': {'gl': 'AE', 'name': 'United Arab Emirates'}, 'id': 'AE'}, + {'snippet': {'gl': 'GB', 'name': 'United Kingdom'}, 'id': 'GB'}, + {'snippet': {'gl': 'US', 'name': 'United States'}, 'id': 'US'}, + {'snippet': {'gl': 'VN', 'name': 'Vietnam'}, 'id': 'VN'}, + {'snippet': {'gl': 'YE', 'name': 'Yemen'}, 'id': 'YE'}, + {'snippet': {'gl': 'ZW', 'name': 'Zimbabwe'}, 'id': 'ZW'}, +]} def _process_language(provider, context): - if not context.get_ui().on_yes_no_input(context.localize('setup_wizard.adjust'), - context.localize('setup_wizard.adjust.language_and_region')): + if not context.get_ui().on_yes_no_input( + context.localize('setup_wizard.adjust'), + context.localize('setup_wizard.adjust.language_and_region') + ): return client = provider.get_client(context) kodi_language = context.get_language() json_data = client.get_supported_languages(kodi_language) - if 'items' not in json_data: - items = DEFAULT_LANGUAGES['items'] - else: - items = json_data['items'] - language_list = [] + items = json_data.get('items') or DEFAULT_LANGUAGES['items'] invalid_ids = ['es-419'] # causes hl not a valid language error. Issue #418 - for item in items: - if item['id'] in invalid_ids: - continue - language_name = item['snippet']['name'] - hl = item['snippet']['hl'] - language_list.append((language_name, hl)) - language_list = sorted(language_list, key=lambda x: x[0]) + language_list = sorted([ + (item['snippet']['name'], item['snippet']['hl']) + for item in items + if item['id'] not in invalid_ids + ]) language_id = context.get_ui().on_select( - context.localize('setup_wizard.select_language'), language_list) + context.localize('setup_wizard.select_language'), + language_list, + ) if language_id == -1: return json_data = client.get_supported_regions(language=language_id) - if 'items' not in json_data: - items = DEFAULT_REGIONS['items'] - else: - items = json_data['items'] - region_list = [] - for item in items: - region_name = item['snippet']['name'] - gl = item['snippet']['gl'] - region_list.append((region_name, gl)) - region_list = sorted(region_list, key=lambda x: x[0]) - region_id = context.get_ui().on_select(context.localize('setup_wizard.select_region'), - region_list) + items = json_data.get('items') or DEFAULT_REGIONS['items'] + region_list = sorted([ + (item['snippet']['name'], item['snippet']['gl']) + for item in items + ]) + region_id = context.get_ui().on_select( + context.localize('setup_wizard.select_region'), + region_list, + ) if region_id == -1: return @@ -108,14 +230,16 @@ def _process_language(provider, context): def _process_geo_location(context): - if not context.get_ui().on_yes_no_input(context.get_name(), context.localize('perform_geolocation')): + if not context.get_ui().on_yes_no_input( + context.get_name(), context.localize('perform_geolocation') + ): return locator = Locator() locator.locate_requester() - coordinates = locator.coordinates() - if coordinates: - context.get_settings().set_location('{0[lat]},{0[lon]}'.format(coordinates)) + coords = locator.coordinates() + if coords: + context.get_settings().set_location('{0[lat]},{0[lon]}'.format(coords)) def process(provider, context): From 95ee8958d3b3616f5c315164976a8c58a51c9cc1 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 1 Feb 2024 15:47:53 +1100 Subject: [PATCH 04/21] Fix incorrect default return type for Youtube.get_related_videos --- resources/lib/youtube_plugin/youtube/client/youtube.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 21958d354..ffc99fa88 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -1039,7 +1039,7 @@ def get_related_videos(self, post_data=post_data, no_login=True) if not result: - return [] + return {} related_videos = self.json_traverse( result, @@ -1073,7 +1073,7 @@ def get_related_videos(self, ) ) if not related_videos: - return [] + return {} channel_id = self.json_traverse( result, From 3716d53ea6760a6ca83291bc82def27613d51f8d Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 2 Feb 2024 08:59:16 +1100 Subject: [PATCH 05/21] Fix removing item from playlist #570 --- .../lib/youtube_plugin/youtube/client/youtube.py | 14 ++++++++------ .../lib/youtube_plugin/youtube/helper/utils.py | 5 ++++- .../youtube_plugin/youtube/helper/yt_playlist.py | 8 +++++--- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index ffc99fa88..60a00ee79 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -309,6 +309,7 @@ def remove_video_from_playlist(self, return self.api_request(method='DELETE', path='playlistItems', params=params, + no_content=True, **kwargs) def unsubscribe(self, subscription_id, **kwargs): @@ -1581,6 +1582,8 @@ def _response_hook(self, **kwargs): response = kwargs['response'] self._context.log_debug('API response: |{0.status_code}|\n' 'headers: |{0.headers}|'.format(response)) + if response.status_code == 204 and 'no_content' in kwargs: + return True try: json_data = response.json() if 'error' in json_data: @@ -1707,9 +1710,8 @@ def api_request(self, params=log_params, data=client.get('json'), headers=log_headers)) - - json_data = self.request(response_hook=self._response_hook, - response_hook_kwargs=kwargs, - error_hook=self._error_hook, - **client) - return json_data + response = self.request(response_hook=self._response_hook, + response_hook_kwargs=kwargs, + error_hook=self._error_hook, + **client) + return response diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index c956915c6..bfd781a36 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -617,7 +617,10 @@ def update_video_infos(provider, context, video_id_dict, video_item.set_playlist_item_id(playlist_item_id) context_menu.append( menu_items.remove_video_from_playlist( - context, playlist_id, video_id, video_item.get_name() + context, + playlist_id=playlist_id, + video_id=playlist_item_id, + video_name=video_item.get_name(), ) ) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py index cbd880c40..452363605 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py @@ -96,9 +96,11 @@ def _process_remove_video(provider, context): if playlist_id.strip().lower() not in ('wl', 'hl'): if context.get_ui().on_remove_content(video_name): - json_data = provider.get_client(context).remove_video_from_playlist(playlist_id=playlist_id, - playlist_item_id=video_id) - if not json_data: + success = provider.get_client(context).remove_video_from_playlist( + playlist_id=playlist_id, + playlist_item_id=video_id, + ) + if not success: return False context.get_ui().refresh_container() From bd13ff433eed268e2fadcc8cd0ce2c8f02cdd7ba Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 2 Feb 2024 09:08:51 +1100 Subject: [PATCH 06/21] Respect pagination limits for related videos - Also partially fixes Youtube bug where related video details are no longer returned in first request to next endpoint - Fix #572 --- .../youtube_plugin/youtube/client/youtube.py | 92 ++++++++++++------- .../lib/youtube_plugin/youtube/helper/v3.py | 3 + .../youtube/helper/yt_specials.py | 1 + 3 files changed, 65 insertions(+), 31 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 60a00ee79..f6b4ee244 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -694,9 +694,10 @@ def index_items(items, index, # Fetch related videos. Use threads for faster execution. def threaded_get_related(video_id, func, *args, **kwargs): - related_videos = self.get_related_videos(video_id).get('items') - if related_videos: - func(related_videos[:items_per_page], *args, **kwargs) + related = self.get_related_videos(video_id, + max_results=items_per_page) + if related and 'items' in related: + func(related['items'][:items_per_page], *args, **kwargs) running = 0 threads = [] @@ -1024,6 +1025,7 @@ def get_related_videos(self, video_id, page_token='', max_results=0, + offset=0, **kwargs): # TODO: Improve handling of InnerTube requests, including automatic # continuation processing to retrieve max_results number of results @@ -1058,7 +1060,7 @@ def get_related_videos(self, 'results', ) ) + ( - slice(None), + slice(offset, None, None), ( ( 'compactVideoRenderer', @@ -1097,40 +1099,68 @@ def get_related_videos(self, ) ) + items = [{ + 'kind': "youtube#video", + 'id': video['videoId'], + 'related_video_id': video_id, + 'related_channel_id': channel_id, + 'partial': True, + 'snippet': { + 'title': video['title']['simpleText'], + 'thumbnails': dict(zip( + ('default', 'high'), + video['thumbnail']['thumbnails'], + )), + 'channelId': self.json_traverse(video, ( + ('longBylineText', 'shortBylineText'), + 'runs', + 0, + 'navigationEndpoint', + 'browseEndpoint', + 'browseId', + )), + } + } for video in related_videos if video and 'videoId' in video] + v3_response = { 'kind': 'youtube#videoListResponse', - 'items': [ - { - 'kind': "youtube#video", - 'id': video['videoId'], - 'related_video_id': video_id, - 'related_channel_id': channel_id, - 'partial': True, - 'snippet': { - 'title': video['title']['simpleText'], - 'thumbnails': dict(zip( - ('default', 'high'), - video['thumbnail']['thumbnails'], - )), - 'channelId': self.json_traverse(video, ( - ('longBylineText', 'shortBylineText'), - 'runs', - 0, - 'navigationEndpoint', - 'browseEndpoint', - 'browseId', - )), - } - } - for video in related_videos - if video and 'videoId' in video - ] + 'items': [], } last_item = related_videos[-1] if last_item and 'token' in last_item: - v3_response['nextPageToken'] = last_item['token'] + page_token = last_item['token'] + + while 1: + remaining = max_results - len(items) + if remaining < 0: + items = items[:max_results] + if page_token: + v3_response['nextPageToken'] = page_token + v3_response['offset'] = remaining + break + + if not page_token: + break + + if not remaining: + v3_response['nextPageToken'] = page_token + break + + continuation = self.get_related_videos( + video_id, + page_token=page_token, + max_results=remaining, + **kwargs + ) + if 'nextPageToken' in continuation: + page_token = continuation['nextPageToken'] + else: + page_token = '' + if 'items' in continuation: + items.extend(continuation['items']) + v3_response['items'] = items return v3_response def get_parent_comments(self, diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index 80b849c37..19bec0d7e 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -382,6 +382,7 @@ def response_to_items(provider, yt_total_results = int(page_info.get('totalResults', 0)) yt_results_per_page = int(page_info.get('resultsPerPage', 0)) page = int(context.get_param('page', 1)) + offset = int(json_data.get('offset', 0)) yt_visitor_data = json_data.get('visitorData', '') yt_next_page_token = json_data.get('nextPageToken', '') yt_click_tracking = json_data.get('clickTracking', '') @@ -398,6 +399,8 @@ def response_to_items(provider, new_params['visitor'] = yt_visitor_data if yt_click_tracking: new_params['click_tracking'] = yt_click_tracking + if offset: + new_params['offset'] = offset new_context = context.clone(new_params=new_params) current_page = new_context.get_param('page', 1) next_page_item = NextPageItem(new_context, current_page) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py index f388a5d84..f63ad1fa5 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_specials.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_specials.py @@ -30,6 +30,7 @@ def _process_related_videos(provider, context): _refresh=params.get('refresh'), video_id=video_id, page_token=params.get('page_token', ''), + offset=params.get('offset', 0), ) else: json_data = function_cache.run( From f0f2d86b26b224bebc06cea3ba64fab375db27a6 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 2 Feb 2024 09:10:40 +1100 Subject: [PATCH 07/21] Limit number of related videos fetched when autoplay suggested video is enabled --- resources/lib/youtube_plugin/youtube/helper/utils.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index bfd781a36..159dab252 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -807,8 +807,13 @@ def add_related_video_to_playlist(provider, context, client, v3, video_id): result_items = [] try: - json_data = client.get_related_videos(video_id, page_token=page_token, max_results=17) - result_items = v3.response_to_items(provider, context, json_data, process_next_page=False) + json_data = client.get_related_videos(video_id, + page_token=page_token, + max_results=5) + result_items = v3.response_to_items(provider, + context, + json_data, + process_next_page=False) page_token = json_data.get('nextPageToken', '') except: context.get_ui().show_notification('Failed to add a suggested video.', time_ms=5000) From 0b3a99e8b5178d70c38210e245b549cac68dfbff Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 2 Feb 2024 09:12:06 +1100 Subject: [PATCH 08/21] Fix not correctly including visitor data in continuation requests --- resources/lib/youtube_plugin/youtube/helper/v3.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index 19bec0d7e..5dd9b80b2 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -395,7 +395,7 @@ def response_to_items(provider, new_params = dict(context.get_params(), page_token=yt_next_page_token) - if yt_click_tracking: + if yt_visitor_data: new_params['visitor'] = yt_visitor_data if yt_click_tracking: new_params['click_tracking'] = yt_click_tracking From 6635d981576b8c639077a5a447440d8315dd78fd Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 2 Feb 2024 09:18:39 +1100 Subject: [PATCH 09/21] Fix invalid error raised for request responses with no content - Fix #568 --- resources/lib/youtube_plugin/youtube/client/youtube.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index f6b4ee244..9687570b4 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -210,6 +210,7 @@ def remove_playlist(self, playlist_id, **kwargs): return self.api_request(method='DELETE', path='playlists', params=params, + no_content=True, **kwargs) def get_supported_languages(self, language=None, **kwargs): @@ -285,6 +286,7 @@ def rate_video(self, video_id, rating='like', **kwargs): return self.api_request(method='POST', path='videos/rate', params=params, + no_content=True, **kwargs) def add_video_to_playlist(self, playlist_id, video_id, **kwargs): @@ -317,6 +319,7 @@ def unsubscribe(self, subscription_id, **kwargs): return self.api_request(method='DELETE', path='subscriptions', params=params, + no_content=True, **kwargs) def subscribe(self, channel_id, **kwargs): From 11fa0ef4bf2f5d2230d86ff9f82248dfd26ca8bf Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sat, 3 Feb 2024 11:04:51 +1100 Subject: [PATCH 10/21] Additional fixes for missing related video info - Fix #572 - TODO: Improve behaviour of using ActivateWindow/ReplaceWindow with no listing --- .../youtube/client/request_client.py | 14 +- .../youtube_plugin/youtube/client/youtube.py | 215 +++++++++++++----- 2 files changed, 162 insertions(+), 67 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/client/request_client.py b/resources/lib/youtube_plugin/youtube/client/request_client.py index 13c2d5f53..6e682e831 100644 --- a/resources/lib/youtube_plugin/youtube/client/request_client.py +++ b/resources/lib/youtube_plugin/youtube/client/request_client.py @@ -275,18 +275,18 @@ def __init__(self, language=None, region=None, exc_type=None, **_kwargs): super(YouTubeRequestClient, self).__init__(exc_type=exc_type) @classmethod - def json_traverse(cls, json_data, path): + def json_traverse(cls, json_data, path, default=None): if not json_data or not path: - return None + return default result = json_data for idx, keys in enumerate(path): if not isinstance(result, (dict, list, tuple)): - return None + return default if isinstance(keys, slice): return [ - cls.json_traverse(part, path[idx + 1:]) + cls.json_traverse(part, path[idx + 1:], default=default) for part in result[keys] if part ] @@ -296,7 +296,7 @@ def json_traverse(cls, json_data, path): for key in keys: if isinstance(key, (list, tuple)): - new_result = cls.json_traverse(result, key) + new_result = cls.json_traverse(result, key, default=default) if new_result: result = new_result break @@ -308,10 +308,10 @@ def json_traverse(cls, json_data, path): continue break else: - return None + return default if result == json_data: - return None + return default return result @classmethod diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 9687570b4..a1297649b 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -14,6 +14,7 @@ import xml.etree.ElementTree as ET from copy import deepcopy from itertools import chain, islice +from operator import itemgetter from random import randint from .login_client import LoginClient @@ -50,6 +51,42 @@ class YouTube(LoginClient): 'Host': 'www.googleapis.com', }, }, + 'tv': { + 'url': 'https://www.youtube.com/youtubei/v1/{_endpoint}', + 'method': None, + 'json': { + 'context': { + 'client': { + 'clientName': 'TVHTML5', + 'clientVersion': '5.20150304', + }, + }, + }, + 'headers': { + 'Host': 'www.youtube.com', + }, + 'params': { + 'key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8', + }, + }, + 'tv_embed': { + 'url': 'https://www.youtube.com/youtubei/v1/{_endpoint}', + 'method': None, + 'json': { + 'context': { + 'client': { + 'clientName': 'TVHTML5_SIMPLY_EMBEDDED_PLAYER', + 'clientVersion': '2.0', + }, + }, + }, + 'headers': { + 'Host': 'www.youtube.com', + }, + 'params': { + 'key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8', + }, + }, '_common': { '_access_token': None, 'json': { @@ -1029,17 +1066,16 @@ def get_related_videos(self, page_token='', max_results=0, offset=0, + retry=0, **kwargs): - # TODO: Improve handling of InnerTube requests, including automatic - # continuation processing to retrieve max_results number of results - # See Youtube.get_saved_playlists for existing implementation max_results = self._max_results if max_results <= 0 else max_results post_data = {'videoId': video_id} if page_token: post_data['continuation'] = page_token - result = self.api_request(version=1, + result = self.api_request(version=('tv' if retry == 1 else + 'tv_embed' if retry == 2 else 1), method='POST', path='next', post_data=post_data, @@ -1047,60 +1083,106 @@ def get_related_videos(self, if not result: return {} - related_videos = self.json_traverse( - result, - path=( - ( - 'onResponseReceivedEndpoints', - 0, - 'appendContinuationItemsAction', - 'continuationItems', - ) if page_token else ( - 'contents', - 'twoColumnWatchNextResults', - 'secondaryResults', - 'secondaryResults', - 'results', - ) - ) + ( - slice(offset, None, None), - ( - ( - 'compactVideoRenderer', - # 'videoId', - ), - ( - 'continuationItemRenderer', - 'continuationEndpoint', - 'continuationCommand', - # 'token', - ), - ), - ) - ) - if not related_videos: - return {} - - channel_id = self.json_traverse( - result, - path=( + related_videos = self.json_traverse(result, path=( + ( + 'onResponseReceivedEndpoints', + 0, + 'appendContinuationItemsAction', + 'continuationItems', + ) if page_token else ( + 'contents', + 'singleColumnWatchNextResults', + 'pivot', + 'pivot', + 'contents', + slice(0, None, None), + 'pivotShelfRenderer', + 'content', + 'pivotHorizontalListRenderer', + 'items', + ) if retry == 1 else ( + 'contents', + 'singleColumnWatchNextResults', + 'results', + 'results', + 'contents', + 2, + 'shelfRenderer', + 'content', + 'horizontalListRenderer', + 'items', + ) if retry == 2 else ( 'contents', 'twoColumnWatchNextResults', + 'secondaryResults', + 'secondaryResults', 'results', - 'results', - 'contents', - 1, - 'videoSecondaryInfoRenderer', - 'owner', - 'videoOwnerRenderer', - 'title', - 'runs', - 0, - 'navigationEndpoint', - 'browseEndpoint', - 'browseId' ) - ) + ) + ( + slice(offset, None, None), + ( + 'pivotVideoRenderer', + # 'videoId', + ) if retry == 1 else ( + 'compactVideoRenderer', + # 'videoId', + ) if retry == 2 else ( + ( + 'compactVideoRenderer', + # 'videoId', + ), + ( + 'continuationItemRenderer', + 'continuationEndpoint', + 'continuationCommand', + # 'token', + ), + ), + ), default=[]) + if not related_videos or not any(related_videos): + return {} if retry > 1 else self.get_related_videos( + video_id, + page_token=page_token, + max_results=max_results, + retry=(retry + 1), + **kwargs + ) + + channel_id = self.json_traverse(result, path=( + 'contents', + 'singleColumnWatchNextResults', + 'results', + 'results', + 'contents', + 1, + 'itemSectionRenderer', + 'contents', + 0, + 'videoOwnerRenderer', + 'navigationEndpoint', + 'browseEndpoint', + 'browseId' + ) if retry else ( + 'contents', + 'twoColumnWatchNextResults', + 'results', + 'results', + 'contents', + 1, + 'videoSecondaryInfoRenderer', + 'owner', + 'videoOwnerRenderer', + 'title', + 'runs', + 0, + 'navigationEndpoint', + 'browseEndpoint', + 'browseId' + )) + + thumb_getter = itemgetter(0, -1) + if retry == 1: + related_videos = chain.from_iterable(related_videos) items = [{ 'kind': "youtube#video", @@ -1109,12 +1191,24 @@ def get_related_videos(self, 'related_channel_id': channel_id, 'partial': True, 'snippet': { - 'title': video['title']['simpleText'], + 'title': self.json_traverse(video, path=( + 'title', + ( + ( + 'simpleText', + ), + ( + 'runs', + 0, + 'text' + ), + ) + )), 'thumbnails': dict(zip( ('default', 'high'), - video['thumbnail']['thumbnails'], + thumb_getter(video['thumbnail']['thumbnails']), )), - 'channelId': self.json_traverse(video, ( + 'channelId': self.json_traverse(video, path=( ('longBylineText', 'shortBylineText'), 'runs', 0, @@ -1130,9 +1224,10 @@ def get_related_videos(self, 'items': [], } - last_item = related_videos[-1] - if last_item and 'token' in last_item: - page_token = last_item['token'] + if not retry: + last_item = related_videos[-1] + if last_item and 'token' in last_item: + page_token = last_item['token'] while 1: remaining = max_results - len(items) From d4dcd996b8ca41604933ce9be9f1751c743eed55 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 4 Feb 2024 09:39:18 +1100 Subject: [PATCH 11/21] Fix for possible database locks during setup --- .../kodion/sql_store/storage.py | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/sql_store/storage.py b/resources/lib/youtube_plugin/kodion/sql_store/storage.py index ce76b3bcb..d990b5753 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/storage.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/storage.py @@ -173,15 +173,21 @@ def _open(self): self.__class__._table_created = False self.__class__._table_updated = True - try: - db = sqlite3.connect(self._filepath, - check_same_thread=False, - timeout=1, - isolation_level=None) - except sqlite3.OperationalError as exc: - log_error('SQLStorage._execute - {exc}:\n{details}'.format( - exc=exc, details=''.join(format_stack()) - )) + for _ in range(3): + try: + db = sqlite3.connect(self._filepath, + check_same_thread=False, + timeout=1, + isolation_level=None) + break + except (sqlite3.Error, sqlite3.OperationalError) as exc: + log_error('SQLStorage._open - {exc}:\n{details}'.format( + exc=exc, details=''.join(format_stack()) + )) + if isinstance(exc, sqlite3.Error): + return False + time.sleep(0.1) + else: return False cursor = db.cursor() @@ -207,7 +213,7 @@ def _open(self): ) if not self._table_updated: - for result in cursor.execute(self._sql['has_old_table']): + for result in self._execute(cursor, self._sql['has_old_table']): if result[0] == 1: statements.extend(( 'PRAGMA writable_schema = 1;', @@ -220,7 +226,7 @@ def _open(self): transaction_begin = len(sql_script) + 1 sql_script.extend(('BEGIN;', 'COMMIT;', 'VACUUM;')) sql_script[transaction_begin:transaction_begin] = statements - cursor.executescript('\n'.join(sql_script)) + self._execute(cursor, '\n'.join(sql_script), script=True) self.__class__._table_created = True self.__class__._table_updated = True @@ -239,7 +245,7 @@ def _close(self): self._db = None @staticmethod - def _execute(cursor, query, values=None, many=False): + def _execute(cursor, query, values=None, many=False, script=False): if values is None: values = () """ @@ -251,17 +257,16 @@ def _execute(cursor, query, values=None, many=False): try: if many: return cursor.executemany(query, values) + if script: + return cursor.executescript(query) return cursor.execute(query, values) - except sqlite3.OperationalError as exc: + except (sqlite3.Error, sqlite3.OperationalError) as exc: log_error('SQLStorage._execute - {exc}:\n{details}'.format( exc=exc, details=''.join(format_stack()) )) + if isinstance(exc, sqlite3.Error): + return [] time.sleep(0.1) - except sqlite3.Error as exc: - log_error('SQLStorage._execute - {exc}:\n{details}'.format( - exc=exc, details=''.join(format_stack()) - )) - return [] return [] def _optimize_file_size(self, defer=False): From 4a81220bc1079e0ed6041e619d38caa72ceed9b4 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 4 Feb 2024 10:40:21 +1100 Subject: [PATCH 12/21] Don't show subscribe context menu item in My Subscriptions - Partially address #568 --- .../youtube_plugin/kodion/constants/const_paths.py | 1 + resources/lib/youtube_plugin/youtube/helper/utils.py | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/constants/const_paths.py b/resources/lib/youtube_plugin/kodion/constants/const_paths.py index cd14cf4bb..cc5534148 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_paths.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_paths.py @@ -15,6 +15,7 @@ FAVORITES = 'kodion/favorites' WATCH_LATER = 'kodion/watch_later' HISTORY = 'kodion/playback_history' +MY_SUBSCRIPTIONS = '/special/new_uploaded_videos' API = '/youtube/api' API_SUBMIT = '/youtube/api/submit' diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 159dab252..00eb7118a 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -14,7 +14,7 @@ import time from math import log10 -from ...kodion.constants import content +from ...kodion.constants import content, paths from ...kodion.items import DirectoryItem, menu_items from ...kodion.utils import ( create_path, @@ -377,6 +377,13 @@ def update_video_infos(provider, context, video_id_dict, path = context.get_path() ui = context.get_ui() + if path.startswith(paths.MY_SUBSCRIPTIONS): + in_my_subscriptions_list = True + playlist_match = False + else: + in_my_subscriptions_list = False + playlist_match = __RE_PLAYLIST_MATCH.match(path) + for video_id, yt_item in data.items(): video_item = video_id_dict[video_id] @@ -572,7 +579,6 @@ def update_video_infos(provider, context, video_id_dict, /channel/[CHANNEL_ID]/playlist/[PLAYLIST_ID]/ /playlist/[PLAYLIST_ID]/ """ - playlist_match = __RE_PLAYLIST_MATCH.match(path) playlist_id = playlist_channel_id = '' if playlist_match: replace_context_menu = True @@ -634,7 +640,7 @@ def update_video_infos(provider, context, video_id_dict, ) ) - if logged_in: + if logged_in and not in_my_subscriptions_list: # subscribe to the channel of the video context_menu.append( menu_items.subscribe_to_channel( From e50cfbe3c51fd6b6004a39148dee895c0ee67609 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 4 Feb 2024 11:19:09 +1100 Subject: [PATCH 13/21] Use constants.paths rather than hardcoded paths for comparisons --- .../lib/youtube_plugin/kodion/abstract_provider.py | 10 +++++----- .../youtube_plugin/kodion/constants/const_paths.py | 13 +++++++++---- .../kodion/context/abstract_context.py | 4 ++-- .../lib/youtube_plugin/kodion/utils/methods.py | 4 ++-- .../lib/youtube_plugin/youtube/helper/utils.py | 8 ++++---- resources/lib/youtube_plugin/youtube/helper/v3.py | 3 ++- 6 files changed, 24 insertions(+), 18 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index c7f85f70b..1998c20b6 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -38,25 +38,25 @@ def __init__(self): self.register_path(r'^/$', '_internal_root') self.register_path(r''.join(( - '^/', + '^', paths.WATCH_LATER, '/(?Padd|clear|list|remove)/?$' )), '_internal_watch_later') self.register_path(r''.join(( - '^/', + '^', paths.FAVORITES, '/(?Padd|clear|list|remove)/?$' )), '_internal_favorite') self.register_path(r''.join(( - '^/', + '^', paths.SEARCH, '/(?Pinput|query|list|remove|clear|rename)/?$' )), '_internal_search') self.register_path(r''.join(( - '^/', + '^', paths.HISTORY, '/$' )), 'on_playback_history') @@ -320,7 +320,7 @@ def _internal_search(self, context, re_match): search_history.update(query) except: pass - context.set_path('/kodion/search/query/') + context.set_path(paths.SEARCH, 'query') if isinstance(query, bytes): query = query.decode('utf-8') return self.on_search(query, context, re_match) diff --git a/resources/lib/youtube_plugin/kodion/constants/const_paths.py b/resources/lib/youtube_plugin/kodion/constants/const_paths.py index cc5534148..2fd760fb8 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_paths.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_paths.py @@ -11,11 +11,16 @@ from __future__ import absolute_import, division, unicode_literals -SEARCH = 'kodion/search' -FAVORITES = 'kodion/favorites' -WATCH_LATER = 'kodion/watch_later' -HISTORY = 'kodion/playback_history' +SEARCH = '/kodion/search' +FAVORITES = '/kodion/favorites' +WATCH_LATER = '/kodion/watch_later' +HISTORY = '/kodion/playback_history' + +DISLIKED_VIDEOS = '/special/disliked_videos' +LIKED_VIDEOS = '/channel/mine/playlist/LL' +MY_PLAYLISTS = '/channel/mine/playlists' MY_SUBSCRIPTIONS = '/special/new_uploaded_videos' +SUBSCRIPTIONS = '/subscriptions/list' API = '/youtube/api' API_SUBMIT = '/youtube/api/submit' diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index 540a09839..d8065e45d 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -239,8 +239,8 @@ def create_uri(self, path='/', params=None): def get_path(self): return self._path - def set_path(self, value): - self._path = value + def set_path(self, path): + self._path = create_path(path) def get_params(self): return self._params diff --git a/resources/lib/youtube_plugin/kodion/utils/methods.py b/resources/lib/youtube_plugin/kodion/utils/methods.py index 41c040515..a96d827b7 100644 --- a/resources/lib/youtube_plugin/kodion/utils/methods.py +++ b/resources/lib/youtube_plugin/kodion/utils/methods.py @@ -184,7 +184,7 @@ def create_path(*args): if isinstance(arg, (list, tuple)): return create_path(*arg) - comps.append(str(arg.strip('/').replace('\\', '/').replace('//', '/'))) + comps.append(str(arg).strip('/').replace('\\', '/').replace('//', '/')) uri_path = '/'.join(comps) if uri_path: @@ -199,7 +199,7 @@ def create_uri_path(*args): if isinstance(arg, (list, tuple)): return create_uri_path(*arg) - comps.append(str(arg.strip('/').replace('\\', '/').replace('//', '/'))) + comps.append(str(arg).strip('/').replace('\\', '/').replace('//', '/')) uri_path = '/'.join(comps) if uri_path: diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 00eb7118a..d9e01280f 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -167,7 +167,7 @@ def update_channel_infos(provider, context, channel_id_dict, path = context.get_path() filter_list = None - if path == '/subscriptions/list/': + if path.startswith(paths.SUBSCRIPTIONS): in_subscription_list = True if settings.get_bool('youtube.folder.my_subscriptions_filtered.show', False): @@ -282,7 +282,7 @@ def update_playlist_infos(provider, context, playlist_id_dict, channel_id = snippet['channelId'] # if the path directs to a playlist of our own, set channel id to 'mine' - if path == '/channel/mine/playlists/': + if path.startswith(paths.MY_PLAYLISTS): channel_id = 'mine' channel_name = snippet.get('channelTitle', '') @@ -667,8 +667,8 @@ def update_video_infos(provider, context, video_id_dict, ) # more... - refresh_container = (path.startswith('/channel/mine/playlist/LL') - or path == '/special/disliked_videos/') + refresh_container = path.startswith((paths.LIKED_VIDEOS, + paths.DISLIKED_VIDEOS)) context_menu.extend(( menu_items.more_for_video( context, diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index 5dd9b80b2..43d0afae8 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -21,6 +21,7 @@ update_playlist_infos, update_video_infos, ) +from ...kodion.constants import paths from ...kodion import KodionException from ...kodion.items import DirectoryItem, NextPageItem, VideoItem, menu_items @@ -118,7 +119,7 @@ def _process_list_response(provider, context, json_data): elif kind == 'playlist': # set channel id to 'mine' if the path is for a playlist of our own - if context.get_path() == '/channel/mine/playlists/': + if context.get_path().startswith(paths.MY_PLAYLISTS): channel_id = 'mine' else: channel_id = snippet['channelId'] From 2427431ff2ac992d8bc2cf4ce8e8872df9b2af8c Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 5 Feb 2024 10:30:08 +1100 Subject: [PATCH 14/21] Add XbmcContext.get_listitem_detail helper function --- .../youtube_plugin/kodion/context/abstract_context.py | 4 ++++ .../youtube_plugin/kodion/context/xbmc/xbmc_context.py | 8 ++++++++ .../lib/youtube_plugin/youtube/helper/yt_playlist.py | 10 +++++----- .../youtube_plugin/youtube/helper/yt_subscriptions.py | 4 ++-- .../lib/youtube_plugin/youtube/helper/yt_video.py | 2 +- resources/lib/youtube_plugin/youtube/provider.py | 6 ++---- 6 files changed, 22 insertions(+), 12 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index d8065e45d..d049e3cfa 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -370,3 +370,7 @@ def sleep(milli_seconds): @staticmethod def get_infolabel(name): raise NotImplementedError() + + @staticmethod + def get_listitem_detail(detail_name, attr=False): + raise NotImplementedError() diff --git a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py index 124c5fd71..0e51d6a02 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -592,3 +592,11 @@ def abort_requested(self): @staticmethod def get_infolabel(name): return xbmc.getInfoLabel(name) + + @staticmethod + def get_listitem_detail(detail_name, attr=False): + return xbmc.getInfoLabel( + 'Container.ListItem(0).{0}'.format(detail_name) + if attr else + 'Container.ListItem(0).Property({0})'.format(detail_name) + ) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py index 452363605..14a6c7bd2 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py @@ -15,7 +15,7 @@ def _process_add_video(provider, context, keymap_action=False): - path = context.get_infolabel('Container.ListItem(0).FileNameAndPath') + path = context.get_listitem_detail('FileNameAndPath', attr=True) client = provider.get_client(context) logged_in = provider.is_logged_in() @@ -64,9 +64,9 @@ def _process_add_video(provider, context, keymap_action=False): def _process_remove_video(provider, context): - listitem_playlist_id = context.get_infolabel('Container.ListItem(0).Property(playlist_id)') - listitem_playlist_item_id = context.get_infolabel('Container.ListItem(0).Property(playlist_item_id)') - listitem_title = context.get_infolabel('Container.ListItem(0).Title') + listitem_playlist_id = context.get_listitem_detail('playlist_id') + listitem_playlist_item_id = context.get_listitem_detail('playlist_item_id') + listitem_title = context.get_listitem_detail('Title', attr=True) keymap_action = False playlist_id = context.get_param('playlist_id', '') @@ -141,7 +141,7 @@ def _process_remove_playlist(provider, context): def _process_select_playlist(provider, context): # Get listitem path asap, relies on listitems focus - path = context.get_infolabel('Container.ListItem(0).FileNameAndPath') + path = context.get_listitem_detail('FileNameAndPath', attr=True) params = context.get_params() ui = context.get_ui() diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py b/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py index 2d40944b4..ec4cec9d0 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py @@ -29,7 +29,7 @@ def _process_list(provider, context): def _process_add(provider, context): - listitem_subscription_id = context.get_infolabel('Container.ListItem(0).Property(subscription_id)') + listitem_subscription_id = context.get_listitem_detail('subscription_id') subscription_id = context.get_param('subscription_id', '') if (not subscription_id and listitem_subscription_id @@ -53,7 +53,7 @@ def _process_add(provider, context): def _process_remove(provider, context): - listitem_subscription_id = context.get_infolabel('Container.ListItem(0).Property(channel_subscription_id)') + listitem_subscription_id = context.get_listitem_detail('channel_subscription_id') subscription_id = context.get_param('subscription_id', '') if not subscription_id and listitem_subscription_id: diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_video.py b/resources/lib/youtube_plugin/youtube/helper/yt_video.py index c1e3bd527..2085d11e3 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_video.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_video.py @@ -16,7 +16,7 @@ def _process_rate_video(provider, context, re_match): - listitem_path = context.get_infolabel('Container.ListItem(0).FileNameAndPath') + listitem_path = context.get_listitem_detail('FileNameAndPath', attr=True) ratings = ['like', 'dislike', 'none'] rating_param = context.get_param('rating', '') diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 52e3f1ce9..d083d1ccd 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -385,9 +385,7 @@ def _on_channel_live(self, context, re_match): @RegisterProviderPath('^/(?P(channel|user))/(?P[^/]+)/$') def _on_channel(self, context, re_match): - listitem_channel_id = context.get_infolabel( - 'Container.ListItem(0).Property(channel_id)' - ) + listitem_channel_id = context.get_listitem_detail('channel_id') client = self.get_client(context) localize = context.localize @@ -591,7 +589,7 @@ def on_play(self, context, re_match): if ({'channel_id', 'live', 'playlist_id', 'playlist_ids', 'video_id'} .isdisjoint(params.keys())): - path = context.get_infolabel('Container.ListItem(0).FileNameAndPath') + path = context.get_listitem_detail('FileNameAndPath', attr=True) if context.is_plugin_path(path, 'play/'): video_id = find_video_id(path) if video_id: From f340599d865082e1dd2cc49d18da9efeb0fc5c5e Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 5 Feb 2024 10:34:31 +1100 Subject: [PATCH 15/21] Add ability to unsubscribe from My Subscriptions - Close #240 - Close #568 - TODO: Refresh cached My Subscriptions results on unsubscribe --- .../youtube_plugin/kodion/items/menu_items.py | 10 ++++-- .../youtube_plugin/youtube/client/youtube.py | 8 +++++ .../youtube_plugin/youtube/helper/utils.py | 10 ++++-- .../youtube/helper/yt_subscriptions.py | 36 +++++++++++-------- 4 files changed, 44 insertions(+), 20 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/items/menu_items.py b/resources/lib/youtube_plugin/kodion/items/menu_items.py index fd707a9b3..cef59ff03 100644 --- a/resources/lib/youtube_plugin/kodion/items/menu_items.py +++ b/resources/lib/youtube_plugin/kodion/items/menu_items.py @@ -333,13 +333,19 @@ def subscribe_to_channel(context, channel_id, channel_name=''): ) -def unsubscribe_from_channel(context, channel_id): +def unsubscribe_from_channel(context, channel_id=None, subscription_id=None): return ( context.localize('unsubscribe'), 'RunPlugin({0})'.format(context.create_uri( ('subscriptions', 'remove',), { - 'subscription_id': channel_id, + 'subscription_id': subscription_id, + }, + )) if subscription_id else + 'RunPlugin({0})'.format(context.create_uri( + ('subscriptions', 'remove',), + { + 'channel_id': channel_id, }, )) ) diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index a1297649b..0ea54d265 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -359,6 +359,14 @@ def unsubscribe(self, subscription_id, **kwargs): no_content=True, **kwargs) + def unsubscribe_channel(self, channel_id, **kwargs): + post_data = {'channelIds': [channel_id]} + return self.api_request(version=1, + method='POST', + path='subscription/unsubscribe', + post_data=post_data, + **kwargs) + def subscribe(self, channel_id, **kwargs): params = {'part': 'snippet'} post_data = {'kind': 'youtube#subscription', diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index d9e01280f..8f7b52b0b 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -209,7 +209,7 @@ def update_channel_infos(provider, context, channel_id_dict, channel_item.set_channel_subscription_id(subscription_id) context_menu.append( menu_items.unsubscribe_from_channel( - context, subscription_id + context, subscription_id=subscription_id ) ) @@ -640,9 +640,13 @@ def update_video_infos(provider, context, video_id_dict, ) ) - if logged_in and not in_my_subscriptions_list: - # subscribe to the channel of the video + if logged_in: context_menu.append( + # unsubscribe from the channel of the video + menu_items.unsubscribe_from_channel( + context, channel_id=channel_id + ) if in_my_subscriptions_list else + # subscribe to the channel of the video menu_items.subscribe_to_channel( context, channel_id, channel_name ) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py b/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py index ec4cec9d0..348ab5217 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_subscriptions.py @@ -54,27 +54,33 @@ def _process_add(provider, context): def _process_remove(provider, context): listitem_subscription_id = context.get_listitem_detail('channel_subscription_id') + listitem_channel_id = context.get_listitem_detail('channel_id') subscription_id = context.get_param('subscription_id', '') if not subscription_id and listitem_subscription_id: subscription_id = listitem_subscription_id - if subscription_id: - json_data = provider.get_client(context).unsubscribe(subscription_id) - if not json_data: - return False - - context.get_ui().refresh_container() - - context.get_ui().show_notification( - context.localize('unsubscribed.from.channel'), - time_ms=2500, - audible=False - ) - - return True + channel_id = context.get_param('channel_id', '') + if not channel_id and listitem_channel_id: + channel_id = listitem_channel_id - return False + if subscription_id: + success = provider.get_client(context).unsubscribe(subscription_id) + elif channel_id: + success = provider.get_client(context).unsubscribe_channel(channel_id) + else: + success = False + + if not success: + return False + + context.get_ui().refresh_container() + context.get_ui().show_notification( + context.localize('unsubscribed.from.channel'), + time_ms=2500, + audible=False + ) + return True def process(method, provider, context): From cf7564f220acdbe4362d56580ba4d654674123fd Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 5 Feb 2024 20:50:46 +1100 Subject: [PATCH 16/21] Fix typo in AbstractContext.set_path - missing unpack operator for path args --- .../lib/youtube_plugin/kodion/context/abstract_context.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index d049e3cfa..26f8187a5 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -239,8 +239,8 @@ def create_uri(self, path='/', params=None): def get_path(self): return self._path - def set_path(self, path): - self._path = create_path(path) + def set_path(self, *path): + self._path = create_path(*path) def get_params(self): return self._params From c376d5b50c9785234538d208f142041cb8f8f7a3 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 6 Feb 2024 16:32:29 +1100 Subject: [PATCH 17/21] Fix missing tzinfo in parsed datetime objects after a018257 - Fix #574 --- .../lib/youtube_plugin/kodion/utils/datetime_parser.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py b/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py index 7676698e0..999c8ebd1 100644 --- a/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py +++ b/resources/lib/youtube_plugin/kodion/utils/datetime_parser.py @@ -82,6 +82,8 @@ def parse(datetime_string): for group, value in match.groupdict().items() if value } + if timezone: + match['tzinfo'] = timezone.utc return datetime.combine( date=date.today(), time=dt_time(**match) @@ -95,6 +97,8 @@ def parse(datetime_string): for group, value in match.groupdict().items() if value } + if timezone: + match['tzinfo'] = timezone.utc return datetime(**match) # full date time @@ -105,6 +109,8 @@ def parse(datetime_string): for group, value in match.groupdict().items() if value } + if timezone: + match['tzinfo'] = timezone.utc return datetime(**match) # period - at the moment we support only hours, minutes and seconds @@ -129,6 +135,8 @@ def parse(datetime_string): for group, value in match.groupdict().items() if value } + if timezone: + match['tzinfo'] = timezone.utc return datetime(**match) raise KodionException('Could not parse |{datetime}| as ISO 8601' From 13f177366043a9fa54172509ea6dcaafcd61d2e1 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Wed, 7 Feb 2024 12:46:30 +1100 Subject: [PATCH 18/21] Make MPEG-DASH frame rate details configurable - Fix #336 --- .../resource.language.en_au/strings.po | 8 +++++++ .../resource.language.en_gb/strings.po | 8 +++++++ .../resource.language.en_nz/strings.po | 8 +++++++ .../resource.language.en_us/strings.po | 8 +++++++ .../youtube/helper/video_info.py | 21 +++++++++++++++---- resources/settings.xml | 4 +++- 6 files changed, 52 insertions(+), 5 deletions(-) diff --git a/resources/language/resource.language.en_au/strings.po b/resources/language/resource.language.en_au/strings.po index ecf6f327a..d2d6863c5 100644 --- a/resources/language/resource.language.en_au/strings.po +++ b/resources/language/resource.language.en_au/strings.po @@ -1384,3 +1384,11 @@ msgstr "" msgctxt "#30770" msgid "Are you sure you want to clear your Watch Later list?" msgstr "" + +msgctxt "#30771" +msgid "Enable framerate hinting" +msgstr "" + +msgctxt "#30772" +msgid "Enable fractional framerate hinting" +msgstr "" diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 9c994c905..5dc311e00 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1384,3 +1384,11 @@ msgstr "" msgctxt "#30770" msgid "Are you sure you want to clear your Watch Later list?" msgstr "" + +msgctxt "#30771" +msgid "Enable framerate hinting" +msgstr "" + +msgctxt "#30772" +msgid "Enable fractional framerate hinting" +msgstr "" diff --git a/resources/language/resource.language.en_nz/strings.po b/resources/language/resource.language.en_nz/strings.po index 872d3f7fe..f5bbdc3bb 100644 --- a/resources/language/resource.language.en_nz/strings.po +++ b/resources/language/resource.language.en_nz/strings.po @@ -1380,3 +1380,11 @@ msgstr "" msgctxt "#30770" msgid "Are you sure you want to clear your Watch Later list?" msgstr "" + +msgctxt "#30771" +msgid "Enable framerate hinting" +msgstr "" + +msgctxt "#30772" +msgid "Enable fractional framerate hinting" +msgstr "" diff --git a/resources/language/resource.language.en_us/strings.po b/resources/language/resource.language.en_us/strings.po index e60a50d0a..402ae1d1b 100644 --- a/resources/language/resource.language.en_us/strings.po +++ b/resources/language/resource.language.en_us/strings.po @@ -1385,3 +1385,11 @@ msgstr "" msgctxt "#30770" msgid "Are you sure you want to clear your Watch Later list?" msgstr "" + +msgctxt "#30771" +msgid "Enable framerate hinting" +msgstr "" + +msgctxt "#30772" +msgid "Enable fractional framerate hinting" +msgstr "" diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index d031dfc0d..2d2483c9f 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -1344,13 +1344,25 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): allow_hfr = 'hfr' in stream_features disable_hfr_max = 'no_hfr_max' in stream_features allow_ssa = 'ssa' in stream_features + fractional_frame_rate_hint = 'frac_hint' in stream_features stream_select = _settings.stream_select() fps_scale_map = { 0: '{0}000/1000', # --.00 fps - 24: '24000/1001', # 23.97 fps - 30: '30000/1001', # 29.97 fps - 60: '60000/1001', # 59.97 fps + 24: '24000/1001', # 23.976 fps + 25: '25000/1000', # 25.00 fps + 30: '30000/1001', # 29.976 fps + 48: '48000/1000', # 48.00 fps + 50: '50000/1000', # 50.00 fps + 60: '60000/1001', # 59.976 fps + } if fractional_frame_rate_hint else { + 0: '{0}000/1000', # --.00 fps + 24: '24000/1000', # 24.00 fps + 25: '25000/1000', # 25.00 fps + 30: '30000/1000', # 30.00 fps + 48: '48000/1000', # 48.00 fps + 50: '50000/1000', # 50.00 fps + 60: '60000/1000', # 60.00 fps } quality_factor_map = { @@ -1672,6 +1684,7 @@ def _filter_group(previous_group, previous_stream, item): _settings = self._context.get_settings() stream_features = _settings.stream_features() do_filter = 'filter' in stream_features + frame_rate_hint = 'fr_hint' in stream_features stream_select = _settings.stream_select() main_stream = { @@ -1823,7 +1836,7 @@ def _filter_group(previous_group, previous_stream, item): ' bandwidth="{bitrate}"' ' width="{width}"' ' height="{height}"' - ' frameRate="{frameRate}"' + ' frameRate="{frameRate}"' if frame_rate_hint else '' # quality and priority attributes are not used by ISA ' qualityRanking="{quality}"' ' selectionPriority="{priority}"' diff --git a/resources/settings.xml b/resources/settings.xml index fbe786f5c..188b2772f 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -263,7 +263,7 @@ 0 - avc1,vp9,av01,hdr,hfr,vorbis,mp4a,ssa,ac-3,ec-3,dts,filter + avc1,vp9,av01,hdr,hfr,fr_hint,frac_hint,vorbis,mp4a,ssa,ac-3,ec-3,dts,filter @@ -272,6 +272,8 @@ + + From 5ecc11753a72896b3f1c7aed75a8ef490148f424 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Wed, 7 Feb 2024 13:51:57 +1100 Subject: [PATCH 19/21] Change frame rate hint options to be disable toggles - Fix error in 13f1773 - Options are now disable toggles, default unselected, select to disable - Was previously an enable toggle, default selected, unselect to disable - Previous options were more intuitive, but the stream features setting already exists and adding new default selected options will not update the existing setting value i.e. the default values would not take effect until the user resets to default - Fix #336 --- .../resource.language.en_au/strings.po | 4 ++-- .../resource.language.en_gb/strings.po | 4 ++-- .../resource.language.en_nz/strings.po | 4 ++-- .../resource.language.en_us/strings.po | 4 ++-- .../kodion/settings/abstract_settings.py | 2 +- .../youtube/helper/video_info.py | 22 +++++++++---------- resources/settings.xml | 13 ++++++++--- 7 files changed, 30 insertions(+), 23 deletions(-) diff --git a/resources/language/resource.language.en_au/strings.po b/resources/language/resource.language.en_au/strings.po index d2d6863c5..e6835655f 100644 --- a/resources/language/resource.language.en_au/strings.po +++ b/resources/language/resource.language.en_au/strings.po @@ -1386,9 +1386,9 @@ msgid "Are you sure you want to clear your Watch Later list?" msgstr "" msgctxt "#30771" -msgid "Enable framerate hinting" +msgid "Disable fractional framerate hinting" msgstr "" msgctxt "#30772" -msgid "Enable fractional framerate hinting" +msgid "Disable all framerate hinting" msgstr "" diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 5dc311e00..9a3788533 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1386,9 +1386,9 @@ msgid "Are you sure you want to clear your Watch Later list?" msgstr "" msgctxt "#30771" -msgid "Enable framerate hinting" +msgid "Disable fractional framerate hinting" msgstr "" msgctxt "#30772" -msgid "Enable fractional framerate hinting" +msgid "Disable all framerate hinting" msgstr "" diff --git a/resources/language/resource.language.en_nz/strings.po b/resources/language/resource.language.en_nz/strings.po index f5bbdc3bb..9bbf340c8 100644 --- a/resources/language/resource.language.en_nz/strings.po +++ b/resources/language/resource.language.en_nz/strings.po @@ -1382,9 +1382,9 @@ msgid "Are you sure you want to clear your Watch Later list?" msgstr "" msgctxt "#30771" -msgid "Enable framerate hinting" +msgid "Disable fractional framerate hinting" msgstr "" msgctxt "#30772" -msgid "Enable fractional framerate hinting" +msgid "Disable all framerate hinting" msgstr "" diff --git a/resources/language/resource.language.en_us/strings.po b/resources/language/resource.language.en_us/strings.po index 402ae1d1b..77d46b799 100644 --- a/resources/language/resource.language.en_us/strings.po +++ b/resources/language/resource.language.en_us/strings.po @@ -1387,9 +1387,9 @@ msgid "Are you sure you want to clear your Watch Later list?" msgstr "" msgctxt "#30771" -msgid "Enable framerate hinting" +msgid "Disable fractional framerate hinting" msgstr "" msgctxt "#30772" -msgid "Enable fractional framerate hinting" +msgid "Disable all framerate hinting" msgstr "" diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index 2582a9bb3..25082c10a 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -292,7 +292,7 @@ def get_mpd_video_qualities(self): if selected >= key] def stream_features(self): - return self.get_string_list(settings.MPD_STREAM_FEATURES) + return frozenset(self.get_string_list(settings.MPD_STREAM_FEATURES)) _STREAM_SELECT = { 1: 'auto', diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 2d2483c9f..9683c1f95 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -1344,25 +1344,25 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): allow_hfr = 'hfr' in stream_features disable_hfr_max = 'no_hfr_max' in stream_features allow_ssa = 'ssa' in stream_features - fractional_frame_rate_hint = 'frac_hint' in stream_features + integer_frame_rate_hint = 'no_frac_fr_hint' in stream_features stream_select = _settings.stream_select() fps_scale_map = { 0: '{0}000/1000', # --.00 fps - 24: '24000/1001', # 23.976 fps + 24: '24000/1000', # 24.00 fps 25: '25000/1000', # 25.00 fps - 30: '30000/1001', # 29.976 fps + 30: '30000/1000', # 30.00 fps 48: '48000/1000', # 48.00 fps 50: '50000/1000', # 50.00 fps - 60: '60000/1001', # 59.976 fps - } if fractional_frame_rate_hint else { + 60: '60000/1000', # 60.00 fps + } if integer_frame_rate_hint else { 0: '{0}000/1000', # --.00 fps - 24: '24000/1000', # 24.00 fps + 24: '24000/1001', # 23.976 fps 25: '25000/1000', # 25.00 fps - 30: '30000/1000', # 30.00 fps + 30: '30000/1001', # 29.976 fps 48: '48000/1000', # 48.00 fps 50: '50000/1000', # 50.00 fps - 60: '60000/1000', # 60.00 fps + 60: '60000/1001', # 59.976 fps } quality_factor_map = { @@ -1684,7 +1684,7 @@ def _filter_group(previous_group, previous_stream, item): _settings = self._context.get_settings() stream_features = _settings.stream_features() do_filter = 'filter' in stream_features - frame_rate_hint = 'fr_hint' in stream_features + frame_rate_hint = 'no_fr_hint' not in stream_features stream_select = _settings.stream_select() main_stream = { @@ -1835,8 +1835,8 @@ def _filter_group(previous_group, previous_stream, item): ' mimeType="{mimeType}"' ' bandwidth="{bitrate}"' ' width="{width}"' - ' height="{height}"' - ' frameRate="{frameRate}"' if frame_rate_hint else '' + ' height="{height}"' + + (' frameRate="{frameRate}"' if frame_rate_hint else '') + # quality and priority attributes are not used by ISA ' qualityRanking="{quality}"' ' selectionPriority="{priority}"' diff --git a/resources/settings.xml b/resources/settings.xml index 188b2772f..45e7a332d 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -263,7 +263,14 @@ 0 - avc1,vp9,av01,hdr,hfr,fr_hint,frac_hint,vorbis,mp4a,ssa,ac-3,ec-3,dts,filter + + avc1,vp9,av01,hdr,hfr,vorbis,mp4a,ssa,ac-3,ec-3,dts,filter @@ -272,8 +279,8 @@ - - + + From 017945c9cad84edbb3394c54677317e79e7d5b14 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 8 Feb 2024 05:31:49 +1100 Subject: [PATCH 20/21] Misc tidy up --- .../lib/youtube_plugin/kodion/items/menu_items.py | 14 +++----------- .../youtube/client/request_client.py | 1 - .../youtube_plugin/youtube/helper/video_info.py | 4 ++-- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/items/menu_items.py b/resources/lib/youtube_plugin/kodion/items/menu_items.py index cef59ff03..a6238b1fe 100644 --- a/resources/lib/youtube_plugin/kodion/items/menu_items.py +++ b/resources/lib/youtube_plugin/kodion/items/menu_items.py @@ -312,18 +312,10 @@ def go_to_channel(context, channel_id, channel_name): def subscribe_to_channel(context, channel_id, channel_name=''): - if not channel_name: - return ( - context.localize('subscribe'), - 'RunPlugin({0})'.format(context.create_uri( - ('subscriptions', 'add',), - { - 'subscription_id': channel_id, - }, - )) - ) return ( - context.localize('subscribe_to') % context.get_ui().bold(channel_name), + context.localize('subscribe_to') % context.get_ui().bold(channel_name) + if channel_name else + context.localize('subscribe'), 'RunPlugin({0})'.format(context.create_uri( ('subscriptions', 'add',), { diff --git a/resources/lib/youtube_plugin/youtube/client/request_client.py b/resources/lib/youtube_plugin/youtube/client/request_client.py index 6e682e831..e21972ce5 100644 --- a/resources/lib/youtube_plugin/youtube/client/request_client.py +++ b/resources/lib/youtube_plugin/youtube/client/request_client.py @@ -174,7 +174,6 @@ class YouTubeRequestClient(BaseRequestsClass): 'smarttv_embedded': { '_id': 85, 'json': { - 'params': '2AMBCgIQBg', 'context': { 'client': { 'clientName': 'TVHTML5_SIMPLY_EMBEDDED_PLAYER', diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 9683c1f95..7647b2823 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -1359,10 +1359,10 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): 0: '{0}000/1000', # --.00 fps 24: '24000/1001', # 23.976 fps 25: '25000/1000', # 25.00 fps - 30: '30000/1001', # 29.976 fps + 30: '30000/1001', # 29.97 fps 48: '48000/1000', # 48.00 fps 50: '50000/1000', # 50.00 fps - 60: '60000/1001', # 59.976 fps + 60: '60000/1001', # 59.94 fps } quality_factor_map = { From d8389be86554df21e0a9a115974d0f4130f6893a Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 8 Feb 2024 06:48:44 +1100 Subject: [PATCH 21/21] Version bump - v7.0.3+beta.3 --- addon.xml | 2 +- changelog.txt | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/addon.xml b/addon.xml index 29f3ec62d..6eb7a9b81 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/changelog.txt b/changelog.txt index 19626a5da..ae9fa26df 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,19 @@ +## v7.0.3+beta.3 +### Fixed +Fix invalid error when removing subscriptions #568 +Fix removing item from playlist #570 +Fix related videos and respect pagination limits #572 +Fix not correctly including visitor data in continuation requests +Fix for possible database locks during setup +Fix incorrect timezone details for premiered time #574 + +### Changed +Don't show subscribe context menu item in My Subscriptions #568 + +### New +Add ability to unsubscribe from My Subscriptions #240, #568 +Make MPEG-DASH frame rate details configurable #336 + ## v7.0.3+beta.2 ### Changed - Function and data cache are now created per user