From a7a577fed56471bfad87788c6c28c3d70ffa9b1b Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sat, 27 Apr 2024 22:42:13 +0000 Subject: [PATCH 01/59] Translated using Weblate (Spanish (Spain) (es_es)) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (373 of 373 strings) Translated using Weblate (Spanish (Spain) (es_es)) Currently translated at 100.0% (373 of 373 strings) Co-authored-by: Hosted Weblate Co-authored-by: José Antonio Alvarado Co-authored-by: roliverosc Translate-URL: https://kodi.weblate.cloud/projects/kodi-add-ons-video/plugin-video-youtube/es_es/ Translation: Kodi add-ons: video/plugin.video.youtube --- .../resource.language.es_es/strings.po | 122 +++++++++--------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/resources/language/resource.language.es_es/strings.po b/resources/language/resource.language.es_es/strings.po index 647e703b6..78ea84b5c 100644 --- a/resources/language/resource.language.es_es/strings.po +++ b/resources/language/resource.language.es_es/strings.po @@ -7,8 +7,8 @@ msgstr "" "Project-Id-Version: XBMC-Addons\n" "Report-Msgid-Bugs-To: translations@kodi.tv\n" "POT-Creation-Date: 2015-09-21 11:01+0000\n" -"PO-Revision-Date: 2024-04-26 02:42+0000\n" -"Last-Translator: roliverosc \n" +"PO-Revision-Date: 2024-04-27 22:42+0000\n" +"Last-Translator: José Antonio Alvarado \n" "Language-Team: Spanish (Spain) \n" "Language: es_es\n" "MIME-Version: 1.0\n" @@ -55,7 +55,7 @@ msgstr "YouTube" # empty strings from id 30004 to 30006 msgctxt "#30007" msgid "Use InputStream Adaptive" -msgstr "Usar InputStream Adaptativo" +msgstr "Usar InputStream Adaptive" msgctxt "#30008" msgid "Configure InputStream Adaptive" @@ -678,7 +678,7 @@ msgstr "Alto (4:3)" msgctxt "#30594" msgid "Safe search" -msgstr "Filtrado SafeSearch" +msgstr "Activar Búsqueda segura (SafeSearch)" msgctxt "#30595" msgid "Moderate" @@ -834,7 +834,7 @@ msgstr "Activar página de configuración API" msgctxt "#30633" msgid "http://:/youtube/api (see Advanced > HTTP Server)" -msgstr "http://:/youtube/api (ver Servidor HTTP)" +msgstr "http://:/youtube/api (ver Avanzado > Servidor HTTP)" msgctxt "#30634" msgid "YouTube Add-on API Configuration" @@ -894,15 +894,15 @@ msgstr "Emisiones en directo recientes" msgctxt "#30648" msgid "API Key is incorrect. Settings - API - API Key" -msgstr "La clave API es incorrecta. Ajustes - API - API Key" +msgstr "La clave API es incorrecta. Ajustes > API > API Key" msgctxt "#30649" msgid "Client Id is incorrect. Settings - API - API Id" -msgstr "La ID de cliente es incorrecta. Ajustes - API - API ID" +msgstr "La ID de cliente es incorrecta. Ajustes > API > API ID" msgctxt "#30650" msgid "Client Secret is incorrect. Settings - API - API Secret" -msgstr "La API Secreta es incorrecta. Ajustes - API - API Secret" +msgstr "La API Secreta es incorrecta. Ajustes > API > API Secret" msgctxt "#30651" msgid "Location" @@ -950,7 +950,7 @@ msgstr "Añadir un usuario" msgctxt "#30662" msgid "Remove a user" -msgstr "Eliminar un usuario" +msgstr "Borrar un usuario" msgctxt "#30663" msgid "Rename a user" @@ -966,15 +966,15 @@ msgstr "¿Desea cambiar a '%s' ahora?" msgctxt "#30666" msgid "Removed '%s'" -msgstr "Eliminado '%s'" +msgstr "'%s' borrado" msgctxt "#30667" msgid "Renamed '%s' to '%s'" -msgstr "Renombrar '$s' a '%s'" +msgstr "'$s' renombrado a '%s'" msgctxt "#30668" msgid "Play count minimum percent" -msgstr "Porcentaje mínimo total para reproducir" +msgstr "Porcentaje mínimo para contar como reproducido" msgctxt "#30669" msgid "Mark unwatched" @@ -990,7 +990,7 @@ msgstr "Limpiar historial de reproducción" msgctxt "#30672" msgid "Delete playback history database" -msgstr "Borrar la base de datos del historial de reproducción" +msgstr "Eliminar base de datos del historial de reproducción" msgctxt "#30673" msgid "playback history" @@ -1002,7 +1002,7 @@ msgstr "Restablecer punto de reanudación" msgctxt "#30675" msgid "Use local playback history (watched, resume tracking)" -msgstr "Usar historial de reproducción local (vistos, puntos de reanudación)" +msgstr "Usar historial de reproducción local (vistos, punto de reanudación)" msgctxt "#30676" msgid "Just now" @@ -1054,15 +1054,15 @@ msgstr "caché de datos" msgctxt "#30688" msgid "Use MPEG-DASH for videos" -msgstr "Utilizar MPEG-DASH para los vídeos" +msgstr "Usar MPEG-DASH para vídeos" msgctxt "#30689" msgid "Use for live streams" -msgstr "Usar para streams en directo" +msgstr "Usar para trasmisiones en directo" msgctxt "#30690" msgid "InputStream Adaptive >= 2.0.12 is required for adaptive live streams" -msgstr "Se requiere InputStream Adaptive >=2.0.12 para los emisiones adaptativas en directo" +msgstr "Se requiere InputStream Adaptive >= 2.0.12 para retransmisiones en directo" msgctxt "#30691" msgid "Airing now" @@ -1118,7 +1118,7 @@ msgstr "¿Está seguro?" msgctxt "#30704" msgid "Use YouTube website urls with default player" -msgstr "Usar el reproductor por defecto para las url de YouTube" +msgstr "Usar URLs del sitio YouTube con reproductor por defecto" msgctxt "#30705" msgid "Download subtitles" @@ -1138,11 +1138,11 @@ msgstr "Reproducir sólo audio" msgctxt "#30709" msgid "Failed to retrieve Watch Later playlist id. To increase the chances of retrieval add 8-10 videos to Watch Later via the web/app and retry." -msgstr "Error al recuperar la id para la lista de reproducción Ver más tarde. Para aumentar las posibilidades de recuperación, agregue 8-10 vídeos a Ver más tarde a través de la web/aplicación y vuelva a intentarlo." +msgstr "No se ha podido recuperar la ID de la lista de reproducción Ver más tarde. Para aumentar las posibilidades de recuperación, agregue 8-10 vídeos a Ver más tarde a través de la web/aplicación y vuelva a intentarlo." msgctxt "#30710" msgid "Failed to retrieve Watch Later playlist id. Try again tomorrow without removing any of the videos." -msgstr "Error al recuperar la id para la lista de reproducción Ver más tarde. Inténtalo de nuevo mañana sin quitar ninguno de los vídeos." +msgstr "No se ha podido recuperar la ID de la lista de reproducción Ver más tarde. Inténtalo de nuevo mañana sin quitar ninguno de los vídeos." msgctxt "#30711" msgid "Searching for Watch Later... Page %s" @@ -1150,7 +1150,7 @@ msgstr "Buscando para Ver más tarde... Página %s" msgctxt "#30712" msgid "Rate videos in playlists" -msgstr "Calificar vídeos en listas de reproducción" +msgstr "Valorar vídeos en listas de reproducción" msgctxt "#30713" msgid "Added to Watch Later" @@ -1162,7 +1162,7 @@ msgstr "Añadido a lista de reproducción" msgctxt "#30715" msgid "Removed from playlist" -msgstr "Eliminado de la lista de reproducción" +msgstr "Borrado de la lista de reproducción" msgctxt "#30716" msgid "Liked video" @@ -1190,15 +1190,15 @@ msgstr "Establecer por defecto WEBM adaptativo (4K)" msgctxt "#30722" msgid "Enable HDR video" -msgstr "Habilitar vídeo HDR" +msgstr "Activar vídeo HDR" msgctxt "#30723" msgid "Proxy is required for MPEG-DASH VODs (see Advanced > HTTP Server)[CR]HDR and >1080p video requires InputStream Adaptive >= 2.3.14" -msgstr "Se necesita proxy para los VODs de MPEG-DASH (consulte Avanzado > Servidor HTTP)[CR]Los vídeos HDR y >1080p requieren InputStream Adaptive >= 2.3.14" +msgstr "Se necesita proxy para los VODs MPEG-DASH (ver Avanzado > Servidor HTTP)[CR]Los vídeos HDR y >1080p requieren InputStream Adaptive >= 2.3.14" msgctxt "#30724" msgid "Enable high framerate video" -msgstr "Habilitar vídeo con tasas altas de fotogramas" +msgstr "Activar vídeo con altas tasas de fotogramas" msgctxt "#30725" msgid "1440p (QHD)" @@ -1210,11 +1210,11 @@ msgstr "Subidas" msgctxt "#30727" msgid "Enable H.264 video" -msgstr "Habilitar vídeo H.264" +msgstr "Activar vídeo H.264" msgctxt "#30728" msgid "Enable VP9 video" -msgstr "Habilitar vídeo VP9" +msgstr "Activar vídeo VP9" msgctxt "#30729" msgid "" @@ -1222,11 +1222,11 @@ msgstr "Búsqueda remota" msgctxt "#30730" msgid "Play (Ask for quality)" -msgstr "Reproducir (Seleccionar calidad)" +msgstr "Reproducir (Solicitar calidad)" msgctxt "#30731" msgid "The YouTube add-on now requires that you use your own API keys.[CR]For more information see the wiki: [B]https://ytaddon.page.link/keys[/B][CR][CR]Sorry for the inconvenience." -msgstr "El complemento YouTube ahora requiere que use sus propias claves API.[CR]Para obtener más información, consulte la wiki: [B]https://ytaddon.page.link/keys[/B][CR][CR]Lo siento por las molestias." +msgstr "El complemento YouTube ahora requiere que use sus propias claves API.[CR]Para obtener más información, consulte la wiki: [B]https://ytaddon.page.link/keys[/B][CR][CR]Lo siento por los inconvenientes ocasionados." msgctxt "#30732" msgid "Comments" @@ -1250,7 +1250,7 @@ msgstr "Ocultar vídeos cortos (1 minuto o menos)" msgctxt "#30737" msgid "Use alternate client details" -msgstr "Utilizar datos alternativos del cliente" +msgstr "Usar detalles de cliente alternativo" msgctxt "#30738" msgid "Alternate #1" @@ -1266,11 +1266,11 @@ msgstr "HLS" msgctxt "#30741" msgid "Multi-stream HLS" -msgstr "HLS Multi-emisión" +msgstr "HLS Multistream" msgctxt "#30742" msgid "Adaptive HLS" -msgstr "HLS Adaptativo" +msgstr "Adaptative HLS" msgctxt "#30743" msgid "MPEG-DASH" @@ -1298,23 +1298,23 @@ msgstr "Funcionalidades de emisión" msgctxt "#30749" msgid "Enable AV1 video" -msgstr "Habilitar vídeo AV1" +msgstr "Activar vídeo AV1" msgctxt "#30750" msgid "Enable Vorbis audio" -msgstr "Habilitar audio Vorbis" +msgstr "Activar audio Vorbis" msgctxt "#30751" msgid "Enable Opus audio" -msgstr "Habilitar audio Opus" +msgstr "Activar audio Opus" msgctxt "#30752" msgid "Enable AAC audio" -msgstr "Habilitar audio AAC" +msgstr "Activar audio AAC" msgctxt "#30753" msgid "Enable surround sound audio" -msgstr "Habilitar audio Surround" +msgstr "Activar audio Surround" msgctxt "#30754" msgid "Enable AC-3 audio" @@ -1322,19 +1322,19 @@ msgstr "Habilitar audio AC-3" msgctxt "#30755" msgid "Enable EAC-3 audio" -msgstr "Habilitar audio EAC-3" +msgstr "Activar audio EAC-3" msgctxt "#30756" msgid "Enable DTS audio" -msgstr "Habilitar audio DTS" +msgstr "Activar audio DTS" msgctxt "#30757" msgid "Remove similar/duplicate streams" -msgstr "Eliminar emisiones similares/duplicadas" +msgstr "Quitar transmisiones similares/duplicadas" msgctxt "#30758" msgid "Stream selection" -msgstr "Selección de emisión" +msgstr "Selección de transmisión" msgctxt "#30759" msgid "Quality selection" @@ -1358,11 +1358,11 @@ msgstr "Multiaudio" msgctxt "#30764" msgid "Requests connect timeout" -msgstr "Tiempo de espera de la solicitud de conexión" +msgstr "Tiempo de espera para solicitudes de conexión" msgctxt "#30765" msgid "Requests read timeout" -msgstr "Tiempo de espera de solicitud de lectura" +msgstr "Tiempo de espera para solicitudes de lectura" msgctxt "#30766" msgid "Premieres" @@ -1378,27 +1378,27 @@ msgstr "Desactiva la alta velocidad de fotogramas con la máxima calidad de víd msgctxt "#30769" msgid "Clear Watch Later list" -msgstr "Borrar lista de Seguimiento" +msgstr "Limpiar lista Ver más tarde" msgctxt "#30770" msgid "Are you sure you want to clear your Watch Later list?" -msgstr "¿Estás seguro de que quieres borrar tu lista de Seguimiento?" +msgstr "¿Estás seguro de que quieres limpiar tu lista Ver más tarde?" msgctxt "#30771" msgid "Disable fractional framerate hinting" -msgstr "Desactivar la sugerencia de framerate fraccional" +msgstr "Desactivar sugerencia de tasa de fotogramas mínima" msgctxt "#30772" msgid "Disable all framerate hinting" -msgstr "Desactivar todas las sugerencias de velocidad de fotogramas" +msgstr "Desactivar todas las sugerencias de tasa de fotogramas" msgctxt "#30773" msgid "Show video details in video lists" -msgstr "Mostrar detalles del vídeo en las listas de vídeos" +msgstr "Mostrar detalles del vídeo en listas de vídeos" msgctxt "#30774" msgid "All available" -msgstr "Todos disponibles" +msgstr "Todo disponible" msgctxt "#30775" msgid "%s (translation)" @@ -1418,15 +1418,15 @@ msgstr "¿Importar el antiguo historial de reproducción?" msgctxt "#30779" msgid "Import old search history?" -msgstr "¿Importar el antiguo historial de búsqueda?" +msgstr "¿Importar antiguo historial de búsquedas?" msgctxt "#30780" msgid "Clear local watch later list" -msgstr "Borrar la lista de seguimiento local" +msgstr "Limpiar lista de seguimiento local" msgctxt "#30781" msgid "Delete watch later database" -msgstr "Borrar la base de datos de seguimiento" +msgstr "Eliminar base de datos de seguimiento" msgctxt "#30782" msgid "local watch later list" @@ -1434,7 +1434,7 @@ msgstr "lista de seguimiento local" msgctxt "#30783" msgid "settings to recommended values" -msgstr "ajustes a los valores recomendados" +msgstr "ajustes para valores recomendados" msgctxt "#30784" msgid "listings to show minimal details" @@ -1474,15 +1474,15 @@ msgstr "8K/60 fps, HDR, usando AV1 | Dispositivo moderno o PC con todas las capa msgctxt "#30793" msgid "Views count display colour" -msgstr "Color de la pantalla de recuento de visitas" +msgstr "Color de visualización del recuento de visitas" msgctxt "#30794" msgid "Likes count display colour" -msgstr "Color de la pantalla de recuento de likes" +msgstr "Color de visualización del recuento de Me gusta" msgctxt "#30795" msgid "Comments count display colour" -msgstr "Color de la pantalla de recuento de comentarios" +msgstr "Color de visualización del recuento de comentarios" msgctxt "#30796" msgid "1080p/60 fps | Raspberry Pi 4, or similar" @@ -1498,7 +1498,7 @@ msgstr "Limpiar lista de marcadores" msgctxt "#30799" msgid "Delete bookmarks database" -msgstr "Borrar base de datos de marcadores" +msgstr "Eliminar base de datos de marcadores" msgctxt "#30800" msgid "bookmarks list" @@ -1506,11 +1506,11 @@ msgstr "lista de marcadores" msgctxt "#30801" msgid "Clear Bookmarks list" -msgstr "Limpiar lista de Marcadores" +msgstr "Limpiar lista de marcadores" msgctxt "#30802" msgid "Are you sure you want to clear your Bookmarks list?" -msgstr "¿Seguro que quieres borrar tu lista de Marcadores?" +msgstr "¿Estás seguro de que quieres limpiar tu lista de Marcadores?" msgctxt "#30803" msgid "Bookmark %s" @@ -1518,11 +1518,11 @@ msgstr "Marcador %s" msgctxt "#30804" msgid "Use YouTube website urls with external player" -msgstr "Usar reproductor externo para urls de YouTube" +msgstr "Usar reproductor externo para URLs de YouTube" msgctxt "#30805" msgid "Use adaptive streaming formats with external player" -msgstr "Usar reproductor externo para formatos de video Adaptativo" +msgstr "Usar reproductor externo para formatos de transmisiones Adaptativas" # Kodion Common # empty strings from id 30039 to 30099 From b392a33cd2c47757135e38f89717ef63d1dc1854 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:20:02 +1000 Subject: [PATCH 02/59] Create NextPageItem instances without cloning context --- .../kodion/items/next_page_item.py | 24 +++++++-------- .../lib/youtube_plugin/youtube/helper/tv.py | 30 +++++++++---------- .../lib/youtube_plugin/youtube/helper/v3.py | 5 ++-- 3 files changed, 27 insertions(+), 32 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/items/next_page_item.py b/resources/lib/youtube_plugin/kodion/items/next_page_item.py index e1c9c3d81..9945574e0 100644 --- a/resources/lib/youtube_plugin/kodion/items/next_page_item.py +++ b/resources/lib/youtube_plugin/kodion/items/next_page_item.py @@ -14,20 +14,16 @@ class NextPageItem(DirectoryItem): - def __init__(self, context, current_page=1, image=None, fanart=None): - next_page = current_page + 1 - new_params = dict(context.get_params(), page=next_page) - if 'refresh' in new_params: - del new_params['refresh'] - name = context.localize('next_page') % next_page - - super(NextPageItem, self).__init__(name, - context.create_uri( - context.get_path(), - new_params - ), - image=image, - category_label='__inherit__') + def __init__(self, context, params, image=None, fanart=None): + if 'refresh' in params: + del params['refresh'] + + super(NextPageItem, self).__init__( + context.localize('next_page') % params.get('page', 2), + context.create_uri(context.get_path(), params), + image=image, + category_label='__inherit__', + ) if fanart: self.set_fanart(fanart) diff --git a/resources/lib/youtube_plugin/youtube/helper/tv.py b/resources/lib/youtube_plugin/youtube/helper/tv.py index 55fe39805..f220115ed 100644 --- a/resources/lib/youtube_plugin/youtube/helper/tv.py +++ b/resources/lib/youtube_plugin/youtube/helper/tv.py @@ -70,12 +70,12 @@ def my_subscriptions_to_items(provider, context, json_data, do_filter=False): # next page next_page_token = json_data.get('next_page_token', 0) if next_page_token or json_data.get('continue', False): - new_params = dict(context.get_params(), + params = context.get_params() + new_params = dict(params, next_page_token=next_page_token, - offset=json_data.get('offset', 0)) - new_context = context.clone(new_params=new_params) - current_page = new_context.get_param('page', 1) - next_page_item = NextPageItem(new_context, current_page) + offset=json_data.get('offset', 0), + page=params.get('page', 1) + 1) + next_page_item = NextPageItem(context, new_params) result.append(next_page_item) return result @@ -119,12 +119,12 @@ def tv_videos_to_items(provider, context, json_data): # next page next_page_token = json_data.get('next_page_token', 0) if next_page_token or json_data.get('continue', False): - new_params = dict(context.get_params(), + params = context.get_params() + new_params = dict(params, next_page_token=next_page_token, - offset=json_data.get('offset', 0)) - new_context = context.clone(new_params=new_params) - current_page = new_context.get_param('page', 1) - next_page_item = NextPageItem(new_context, current_page) + offset=json_data.get('offset', 0), + page=params.get('page', 1) + 1) + next_page_item = NextPageItem(context, new_params) result.append(next_page_item) return result @@ -172,12 +172,12 @@ def saved_playlists_to_items(provider, context, json_data): # next page next_page_token = json_data.get('next_page_token', 0) if next_page_token or json_data.get('continue', False): - new_params = dict(context.get_params(), + params = context.get_params() + new_params = dict(params, next_page_token=next_page_token, - offset=json_data.get('offset', 0)) - new_context = context.clone(new_params=new_params) - current_page = new_context.get_param('page', 1) - next_page_item = NextPageItem(new_context, current_page) + offset=json_data.get('offset', 0), + page=params.get('page', 1) + 1) + next_page_item = NextPageItem(context, new_params) result.append(next_page_item) return result diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index b71f37043..7583b0168 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -406,9 +406,8 @@ def response_to_items(provider, 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) + + next_page_item = NextPageItem(context, new_params) result.append(next_page_item) return result From bf27bb6e1b557d9561c65518f2d191f501d2dfe3 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:22:35 +1000 Subject: [PATCH 03/59] Create new menu_items.separator function to remove duplicate code --- resources/lib/youtube_plugin/kodion/items/menu_items.py | 7 +++++++ resources/lib/youtube_plugin/youtube/helper/utils.py | 2 +- resources/lib/youtube_plugin/youtube/provider.py | 6 +++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/items/menu_items.py b/resources/lib/youtube_plugin/kodion/items/menu_items.py index 2ba6ce9b9..4c45bb4d3 100644 --- a/resources/lib/youtube_plugin/kodion/items/menu_items.py +++ b/resources/lib/youtube_plugin/kodion/items/menu_items.py @@ -528,3 +528,10 @@ def search_clear(context): (paths.SEARCH, 'clear',), )) ) + + +def separator(): + return ( + '--------', + 'noop' + ) diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 92b9709b3..1ae398e77 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -775,7 +775,7 @@ def update_video_infos(provider, context, video_id_dict, if context_menu: context_menu.append( - ('--------', 'noop') + menu_items.separator(), ) video_item.set_context_menu(context_menu) diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index c461e2e66..2553ddcb7 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -1010,7 +1010,7 @@ def on_playback_history(self, context, re_match): menu_items.history_clear( context ), - ('--------', 'noop'), + menu_items.separator(), ] video_item.add_context_menu(context_menu) @@ -1413,7 +1413,7 @@ def on_bookmarks(self, context, re_match): menu_items.bookmarks_clear( context ), - ('--------', 'noop'), + menu_items.separator(), ] item.add_context_menu(context_menu) bookmarks.append(item) @@ -1477,7 +1477,7 @@ def on_watch_later(self, context, re_match): menu_items.watch_later_local_clear( context ), - ('--------', 'noop'), + menu_items.separator(), ] video_item.add_context_menu(context_menu) From ab3592436a0505c8794f280608af82d865673052 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:28:20 +1000 Subject: [PATCH 04/59] Move Play with (external/remote player) lower down in the context menu - Now located along with the other Play context menu items --- resources/lib/youtube_plugin/youtube/helper/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 1ae398e77..a48a85f99 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -646,10 +646,6 @@ def update_video_infos(provider, context, video_id_dict, ) )) - # 'play with...' (external player) - if alternate_player: - context_menu.append(menu_items.play_with(context)) - # add 'Watch Later' only if we are not in my 'Watch Later' list if watch_later_id: if not playlist_id or watch_later_id != playlist_id: @@ -752,6 +748,10 @@ def update_video_infos(provider, context, video_id_dict, ) ) + # 'play with...' (external player) + if alternate_player: + context_menu.append(menu_items.play_with(context)) + if not subtitles_prompt: context_menu.append( menu_items.play_with_subtitles( From 73c59b97a8ea8be3852611f5b1a03b3f02cdd368 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:30:25 +1000 Subject: [PATCH 05/59] Add dedicated route for main plugin menu (/home) --- resources/lib/youtube_plugin/kodion/abstract_provider.py | 5 ++++- resources/lib/youtube_plugin/kodion/constants/const_paths.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index 8e89dd1df..95c061671 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -32,7 +32,10 @@ def __init__(self): self._dict_path = {} # register some default paths - self.register_path(r'^/$', '_internal_root') + self.register_path(r''.join(( + '^', + '(?:', paths.HOME, ')?/?$' + )), '_internal_root') self.register_path(r''.join(( '^', diff --git a/resources/lib/youtube_plugin/kodion/constants/const_paths.py b/resources/lib/youtube_plugin/kodion/constants/const_paths.py index 669984b39..af6ed0a70 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_paths.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_paths.py @@ -18,6 +18,7 @@ HISTORY = '/kodion/playback_history' DISLIKED_VIDEOS = '/special/disliked_videos' +HOME = '/home' LIKED_VIDEOS = '/channel/mine/playlist/LL' MY_PLAYLISTS = '/channel/mine/playlists' MY_SUBSCRIPTIONS = '/special/new_uploaded_videos' From 065029439acbb163e3cdf63264579688aefcc853 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:31:47 +1000 Subject: [PATCH 06/59] Ensure that an empty plugin path can route to home - Kodi will not accept an empty plugin path when opening a window --- resources/lib/youtube_plugin/kodion/context/abstract_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index e7012c7fe..9297fc455 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -230,7 +230,7 @@ def create_uri(self, path=None, params=None): elif path: uri = path else: - uri = '/' + uri = '/' if params else '/?' uri = self._plugin_id.join(('plugin://', uri)) From d7dc79d8528a00d5f7e3cb923266b0f55e8864fc Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:33:04 +1000 Subject: [PATCH 07/59] Misc tidy ups --- .../kodion/abstract_provider.py | 1 - .../kodion/ui/xbmc/xbmc_context_ui.py | 4 ++- .../lib/youtube_plugin/youtube/helper/tv.py | 12 ++++---- .../lib/youtube_plugin/youtube/helper/v3.py | 29 ++++++++++++------- .../youtube/helper/video_info.py | 2 +- 5 files changed, 28 insertions(+), 20 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index 95c061671..b16482771 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -18,7 +18,6 @@ DirectoryItem, NewSearchItem, SearchHistoryItem, - menu_items, ) from .utils import to_unicode 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 561292798..92a41ef7f 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 @@ -35,7 +35,9 @@ def create_progress_dialog(self, heading, text=None, background=False): def on_keyboard_input(self, title, default='', hidden=False): # Starting with Gotham (13.X > ...) dialog = xbmcgui.Dialog() - result = dialog.input(title, to_unicode(default), type=xbmcgui.INPUT_ALPHANUM) + result = dialog.input(title, + to_unicode(default), + type=xbmcgui.INPUT_ALPHANUM) if result: text = to_unicode(result) return True, text diff --git a/resources/lib/youtube_plugin/youtube/helper/tv.py b/resources/lib/youtube_plugin/youtube/helper/tv.py index f220115ed..88047702a 100644 --- a/resources/lib/youtube_plugin/youtube/helper/tv.py +++ b/resources/lib/youtube_plugin/youtube/helper/tv.py @@ -68,8 +68,8 @@ def my_subscriptions_to_items(provider, context, json_data, do_filter=False): result = utils.filter_short_videos(result) # next page - next_page_token = json_data.get('next_page_token', 0) - if next_page_token or json_data.get('continue', False): + next_page_token = json_data.get('next_page_token') + if next_page_token or json_data.get('continue'): params = context.get_params() new_params = dict(params, next_page_token=next_page_token, @@ -117,8 +117,8 @@ def tv_videos_to_items(provider, context, json_data): result = utils.filter_short_videos(result) # next page - next_page_token = json_data.get('next_page_token', 0) - if next_page_token or json_data.get('continue', False): + next_page_token = json_data.get('next_page_token') + if next_page_token or json_data.get('continue'): params = context.get_params() new_params = dict(params, next_page_token=next_page_token, @@ -170,8 +170,8 @@ def saved_playlists_to_items(provider, context, json_data): utils.update_fanarts(provider, context, channel_items_dict) # next page - next_page_token = json_data.get('next_page_token', 0) - if next_page_token or json_data.get('continue', False): + next_page_token = json_data.get('next_page_token') + if next_page_token or json_data.get('continue'): params = context.get_params() new_params = dict(params, next_page_token=next_page_token, diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index 7583b0168..748170a1e 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -383,27 +383,34 @@ def response_to_items(provider, We implemented our own calculation for the token into the YouTube client This should work for up to ~2000 entries. """ - page_info = json_data.get('pageInfo', {}) - yt_total_results = int(page_info.get('totalResults', 0)) - yt_results_per_page = int(page_info.get('resultsPerPage', 0)) page = context.get_param('page', 1) - offset = 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', '') - if yt_next_page_token or (page * yt_results_per_page < yt_total_results): - if not yt_next_page_token: + yt_next_page_token = json_data.get('nextPageToken') + if not yt_next_page_token: + page_info = json_data.get('pageInfo', {}) + yt_total_results = int(page_info.get('totalResults', 0)) + yt_results_per_page = int(page_info.get('resultsPerPage', 0)) + + if page * yt_results_per_page < yt_total_results: client = provider.get_client(context) yt_next_page_token = client.calculate_next_page_token( page + 1, yt_results_per_page ) - new_params = dict(context.get_params(), - page_token=yt_next_page_token) + if yt_next_page_token: + params = context.get_params() + new_params = dict(params, + page_token=yt_next_page_token, + page=page + 1) + + yt_visitor_data = json_data.get('visitorData') if yt_visitor_data: new_params['visitor'] = yt_visitor_data + + yt_click_tracking = json_data.get('clickTracking') if yt_click_tracking: new_params['click_tracking'] = yt_click_tracking + + offset = json_data.get('offset') if offset: new_params['offset'] = offset diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 71fd3fdf1..76c21fae7 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -1155,7 +1155,7 @@ def _get_video_info(self): for client_name in self._prioritised_clients: if status and status != 'OK': self._context.log_warning( - 'Failed to retrieved video info - ' + 'Failed to retrieve video info - ' 'video_id: {0}, client: {1}, auth: {2},\n' 'status: {3}, reason: {4}'.format( video_id, From eba51150c49d00f94e2bf58b800be7e21ace691b Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:37:47 +1000 Subject: [PATCH 08/59] Add Home and Quick search to context menu of Next page items --- .../kodion/abstract_provider.py | 5 +++-- .../youtube_plugin/kodion/items/menu_items.py | 21 +++++++++++++++++++ .../kodion/items/next_page_item.py | 8 +++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index b16482771..0b807cae5 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -213,8 +213,9 @@ def _internal_search(self, context, re_match): query = None # came from page 1 of search query by '..'/back # user doesn't want to input on this path - if (folder_path.startswith('plugin://%s' % context.get_id()) and - re.match('.+/(?:query|input)/.*', folder_path)): + if (not params.get('refresh') + and folder_path.startswith('plugin://%s' % context.get_id()) + and re.match('.+/(?:query|input)/.*', folder_path)): cached = data_cache.get_item('search_query', data_cache.ONE_DAY) if cached: query = to_unicode(cached) diff --git a/resources/lib/youtube_plugin/kodion/items/menu_items.py b/resources/lib/youtube_plugin/kodion/items/menu_items.py index 4c45bb4d3..7f566223d 100644 --- a/resources/lib/youtube_plugin/kodion/items/menu_items.py +++ b/resources/lib/youtube_plugin/kodion/items/menu_items.py @@ -535,3 +535,24 @@ def separator(): '--------', 'noop' ) + + +def goto_home(context): + return ( + context.localize(10000), + 'ActivateWindow(Videos, {0}, return)'.format(context.create_uri( + (paths.HOME,), + )) + ) + + +def goto_quick_search(context): + return ( + context.localize('search.quick'), + 'ActivateWindow(Videos, {0}, return)'.format(context.create_uri( + (paths.SEARCH, 'input',), + { + 'refresh': True, + }, + )) + ) diff --git a/resources/lib/youtube_plugin/kodion/items/next_page_item.py b/resources/lib/youtube_plugin/kodion/items/next_page_item.py index 9945574e0..8de11a6db 100644 --- a/resources/lib/youtube_plugin/kodion/items/next_page_item.py +++ b/resources/lib/youtube_plugin/kodion/items/next_page_item.py @@ -10,6 +10,7 @@ from __future__ import absolute_import, division, unicode_literals +from . import menu_items from .directory_item import DirectoryItem @@ -29,3 +30,10 @@ def __init__(self, context, params, image=None, fanart=None): self.set_fanart(fanart) self.next_page = True + + context_menu = [ + menu_items.goto_home(context), + menu_items.goto_quick_search(context), + menu_items.separator(), + ] + self.set_context_menu(context_menu) From 3e0ddb9188f7d319038f6db551519e91591847f9 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Wed, 24 Apr 2024 10:42:17 +1000 Subject: [PATCH 09/59] Make calculate_next_page_token a class method of NextPageItem - Calculate automatically if page_token is not provided --- .../youtube_plugin/kodion/items/base_item.py | 10 ---- .../kodion/items/directory_item.py | 9 ++++ .../kodion/items/next_page_item.py | 38 +++++++++++++-- .../youtube_plugin/youtube/client/youtube.py | 24 ---------- .../lib/youtube_plugin/youtube/helper/v3.py | 46 +++++++++---------- 5 files changed, 66 insertions(+), 61 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/items/base_item.py b/resources/lib/youtube_plugin/kodion/items/base_item.py index c0eee5ff6..6576c7543 100644 --- a/resources/lib/youtube_plugin/kodion/items/base_item.py +++ b/resources/lib/youtube_plugin/kodion/items/base_item.py @@ -44,8 +44,6 @@ def __init__(self, name, uri, image='', fanart=''): self._dateadded = None self._short_details = None - self._next_page = False - def __str__(self): return ('------------------------------\n' 'Name: |{0}|\n' @@ -195,14 +193,6 @@ def set_bookmark_timestamp(self, timestamp): def get_bookmark_timestamp(self): return self._bookmark_timestamp - @property - def next_page(self): - return self._next_page - - @next_page.setter - def next_page(self, value): - self._next_page = bool(value) - @property def playable(self): return self._playable diff --git a/resources/lib/youtube_plugin/kodion/items/directory_item.py b/resources/lib/youtube_plugin/kodion/items/directory_item.py index 5587872e3..5f928672a 100644 --- a/resources/lib/youtube_plugin/kodion/items/directory_item.py +++ b/resources/lib/youtube_plugin/kodion/items/directory_item.py @@ -34,6 +34,7 @@ def __init__(self, self._channel_id = channel_id self._playlist_id = playlist_id self._subscription_id = subscription_id + self._next_page = False def set_name(self, name, category_label=None): name = super(DirectoryItem, self).set_name(name) @@ -96,3 +97,11 @@ def set_playlist_id(self, value): def get_playlist_id(self): return self._playlist_id + + @property + def next_page(self): + return self._next_page + + @next_page.setter + def next_page(self, value): + self._next_page = value diff --git a/resources/lib/youtube_plugin/kodion/items/next_page_item.py b/resources/lib/youtube_plugin/kodion/items/next_page_item.py index 8de11a6db..856cd28bc 100644 --- a/resources/lib/youtube_plugin/kodion/items/next_page_item.py +++ b/resources/lib/youtube_plugin/kodion/items/next_page_item.py @@ -19,8 +19,15 @@ def __init__(self, context, params, image=None, fanart=None): if 'refresh' in params: del params['refresh'] + self.next_page = params.get('page', 2) + self.items_per_page = params.pop('items_per_page', 50) + if 'page_token' not in params: + params['page_token'] = self.calculate_next_page_token( + self.next_page, self.items_per_page + ) + super(NextPageItem, self).__init__( - context.localize('next_page') % params.get('page', 2), + context.localize('next_page') % self.next_page, context.create_uri(context.get_path(), params), image=image, category_label='__inherit__', @@ -29,11 +36,36 @@ def __init__(self, context, params, image=None, fanart=None): if fanart: self.set_fanart(fanart) - self.next_page = True - context_menu = [ menu_items.goto_home(context), menu_items.goto_quick_search(context), menu_items.separator(), ] self.set_context_menu(context_menu) + + @classmethod + def calculate_next_page_token(cls, page, items_per_page): + low = 'AEIMQUYcgkosw048' + high = 'ABCDEFGHIJKLMNOP' + len_low = len(low) + len_high = len(high) + + position = (page - 1) * items_per_page + + overflow_token = 'Q' + if position >= 128: + overflow_token_iteration = position // 128 + overflow_token = '%sE' % high[overflow_token_iteration] + low_iteration = position % len_low + + # at this position the iteration starts with 'I' again (after 'P') + if position >= 256: + multiplier = (position // 128) - 1 + position -= 128 * multiplier + high_iteration = (position // len_low) % len_high + + return 'C{high_token}{low_token}{overflow_token}AA'.format( + high_token=high[high_iteration], + low_token=low[low_iteration], + overflow_token=overflow_token + ) diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index e6b5814e1..21e26abe0 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -144,30 +144,6 @@ def get_language(self): def get_region(self): return self._region - @staticmethod - def calculate_next_page_token(page, max_result): - page -= 1 - low = 'AEIMQUYcgkosw048' - high = 'ABCDEFGHIJKLMNOP' - len_low = len(low) - len_high = len(high) - - position = page * max_result - - overflow_token = 'Q' - if position >= 128: - overflow_token_iteration = position // 128 - overflow_token = '%sE' % high[overflow_token_iteration] - low_iteration = position % len_low - - # at this position the iteration starts with 'I' again (after 'P') - if position >= 256: - multiplier = (position // 128) - 1 - position -= 128 * multiplier - high_iteration = (position // len_low) % len_high - - return 'C%s%s%sAA' % (high[high_iteration], low[low_iteration], overflow_token) - def update_watch_history(self, context, video_id, url, status=None): if status is None: cmt = st = et = state = None diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index 748170a1e..c0f3a3052 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -383,39 +383,37 @@ def response_to_items(provider, We implemented our own calculation for the token into the YouTube client This should work for up to ~2000 entries. """ - page = context.get_param('page', 1) + params = context.get_params() + page = params.get('page', 1) + new_params = dict(params, page=page + 1) + yt_next_page_token = json_data.get('nextPageToken') - if not yt_next_page_token: + if yt_next_page_token: + new_params['page_token'] = yt_next_page_token + else: page_info = json_data.get('pageInfo', {}) yt_total_results = int(page_info.get('totalResults', 0)) - yt_results_per_page = int(page_info.get('resultsPerPage', 0)) + yt_results_per_page = int(page_info.get('resultsPerPage', 50)) if page * yt_results_per_page < yt_total_results: - client = provider.get_client(context) - yt_next_page_token = client.calculate_next_page_token( - page + 1, yt_results_per_page - ) - - if yt_next_page_token: - params = context.get_params() - new_params = dict(params, - page_token=yt_next_page_token, - page=page + 1) + new_params['items_per_page'] = yt_results_per_page + else: + return result - yt_visitor_data = json_data.get('visitorData') - if yt_visitor_data: - new_params['visitor'] = yt_visitor_data + yt_visitor_data = json_data.get('visitorData') + if yt_visitor_data: + new_params['visitor'] = yt_visitor_data - yt_click_tracking = json_data.get('clickTracking') - if yt_click_tracking: - new_params['click_tracking'] = yt_click_tracking + yt_click_tracking = json_data.get('clickTracking') + if yt_click_tracking: + new_params['click_tracking'] = yt_click_tracking - offset = json_data.get('offset') - if offset: - new_params['offset'] = offset + offset = json_data.get('offset') + if offset: + new_params['offset'] = offset - next_page_item = NextPageItem(context, new_params) - result.append(next_page_item) + next_page_item = NextPageItem(context, new_params) + result.append(next_page_item) return result From a38f673e580340867704fdd00c18113fe07a1bd1 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Wed, 24 Apr 2024 10:50:49 +1000 Subject: [PATCH 10/59] Consolidate duplicate code and code paths when generating plugin content --- .../youtube_plugin/kodion/items/__init__.py | 2 - .../kodion/items/xbmc/xbmc_items.py | 26 +--- .../kodion/plugin/xbmc/xbmc_plugin.py | 122 ++++++++---------- 3 files changed, 64 insertions(+), 86 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/items/__init__.py b/resources/lib/youtube_plugin/kodion/items/__init__.py index 8dc4a97ad..8faec4265 100644 --- a/resources/lib/youtube_plugin/kodion/items/__init__.py +++ b/resources/lib/youtube_plugin/kodion/items/__init__.py @@ -27,7 +27,6 @@ audio_listitem, directory_listitem, image_listitem, - playback_item, uri_listitem, video_listitem, video_playback_item, @@ -51,7 +50,6 @@ 'audio_listitem', 'directory_listitem', 'image_listitem', - 'playback_item', 'uri_listitem', 'video_listitem', 'video_playback_item', diff --git a/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py index 0d1f6ed8e..97dcd3db4 100644 --- a/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py @@ -10,9 +10,9 @@ from __future__ import absolute_import, division, unicode_literals -from .. import AudioItem, DirectoryItem, ImageItem, UriItem, VideoItem -from ...constants import SWITCH_PLAYER_FLAG +from .. import AudioItem, DirectoryItem, ImageItem, VideoItem from ...compatibility import xbmc, xbmcgui +from ...constants import SWITCH_PLAYER_FLAG from ...utils import current_system_version, datetime_parser @@ -323,7 +323,7 @@ def set_info(list_item, item, properties): info_tag.setYear(value) -def video_playback_item(context, video_item, show_fanart=None): +def video_playback_item(context, video_item, show_fanart=None, **_kwargs): uri = video_item.get_uri() context.log_debug('Converting VideoItem |%s|' % uri) @@ -416,7 +416,7 @@ def video_playback_item(context, video_item, show_fanart=None): return list_item -def audio_listitem(context, audio_item, show_fanart=None): +def audio_listitem(context, audio_item, show_fanart=None, for_playback=False): uri = audio_item.get_uri() context.log_debug('Converting AudioItem |%s|' % uri) @@ -448,6 +448,8 @@ def audio_listitem(context, audio_item, show_fanart=None): if context_menu: list_item.addContextMenuItems(context_menu) + if for_playback: + return list_item return uri, list_item, False @@ -542,7 +544,7 @@ def image_listitem(context, image_item, show_fanart=None): return uri, list_item, False -def uri_listitem(context, uri_item): +def uri_listitem(context, uri_item, **_kwargs): uri = uri_item.get_uri() context.log_debug('Converting UriItem |%s|' % uri) @@ -631,17 +633,3 @@ def video_listitem(context, video_item, show_fanart=None): list_item.addContextMenuItems(context_menu) return uri, list_item, False - - -def playback_item(context, base_item, show_fanart=None): - if isinstance(base_item, UriItem): - return uri_listitem(context, base_item) - - if isinstance(base_item, AudioItem): - _, item, _ = audio_listitem(context, base_item, show_fanart) - return item - - if isinstance(base_item, VideoItem): - return video_playback_item(context, base_item, show_fanart) - - return None diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py index 16c3feb6c..63774d63f 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py @@ -13,25 +13,40 @@ from traceback import format_stack from ..abstract_plugin import AbstractPlugin -from ...constants import BUSY_FLAG, PLAYLIST_POSITION from ...compatibility import xbmcplugin +from ...constants import BUSY_FLAG, PLAYLIST_POSITION from ...exceptions import KodionException from ...items import ( - AudioItem, DirectoryItem, - ImageItem, - UriItem, - VideoItem, audio_listitem, directory_listitem, image_listitem, - playback_item, + uri_listitem, video_listitem, + video_playback_item, ) from ...player import XbmcPlaylist class XbmcPlugin(AbstractPlugin): + _LIST_ITEM_MAP = { + 'AudioItem': audio_listitem, + 'DirectoryItem': directory_listitem, + 'ImageItem': image_listitem, + 'SearchItem': directory_listitem, + 'SearchHistoryItem': directory_listitem, + 'NewSearchItem': directory_listitem, + 'NextPageItem': directory_listitem, + 'VideoItem': video_listitem, + 'WatchLaterItem': directory_listitem, + } + + _PLAY_ITEM_MAP = { + 'AudioItem': audio_listitem, + 'UriItem': uri_listitem, + 'VideoItem': video_playback_item, + } + def __init__(self): super(XbmcPlugin, self).__init__() self.handle = None @@ -97,73 +112,50 @@ def run(self, provider, context): provider.run_wizard(context) try: - results = provider.navigate(context) + result, options = provider.navigate(context) except KodionException as exc: + result = options = None if provider.handle_exception(context, exc): context.log_error('XbmcRunner.run - {exc}:\n{details}'.format( exc=exc, details=''.join(format_stack()) )) ui.on_ok("Error in ContentProvider", exc.__str__()) - xbmcplugin.endOfDirectory( - self.handle, - succeeded=False, - updateListing=True, - ) - return False - - result, options = results - if result is None: - result = False - if isinstance(result, bool): - xbmcplugin.endOfDirectory( - self.handle, - succeeded=result, - updateListing=True, - ) - return result - - show_fanart = settings.show_fanart() - if isinstance(result, (VideoItem, AudioItem, UriItem)): - return self._set_resolved_url(context, result, show_fanart) - - if isinstance(result, DirectoryItem): - item_count = 1 - items = [directory_listitem(context, result, show_fanart)] - elif isinstance(result, (list, tuple)): - item_count = len(result) - items = [ - directory_listitem(context, item, show_fanart) - if isinstance(item, DirectoryItem) - else video_listitem(context, item, show_fanart) - if isinstance(item, VideoItem) - else audio_listitem(context, item, show_fanart) - if isinstance(item, AudioItem) - else image_listitem(context, item, show_fanart) - if isinstance(item, ImageItem) - else None + item_count = 0 + if isinstance(result, (list, tuple)): + show_fanart = settings.show_fanart() + result = [ + self._LIST_ITEM_MAP[item.__class__.__name__]( + context, item, show_fanart=show_fanart + ) for item in result + if item.__class__.__name__ in self._LIST_ITEM_MAP ] - else: - xbmcplugin.endOfDirectory( - self.handle, - succeeded=False, - updateListing=True, + item_count = len(result) + elif result.__class__.__name__ in self._PLAY_ITEM_MAP: + result = self._set_resolved_url(context, result) + + if item_count: + succeeded = xbmcplugin.addDirectoryItems( + self.handle, result, item_count ) - return False + cache_to_disc = options.get(provider.RESULT_CACHE_TO_DISC, True) + update_listing = options.get(provider.RESULT_UPDATE_LISTING, False) + else: + succeeded = bool(result) + cache_to_disc = False + update_listing = True - succeeded = xbmcplugin.addDirectoryItems( - self.handle, items, item_count - ) xbmcplugin.endOfDirectory( self.handle, succeeded=succeeded, - updateListing=options.get(provider.RESULT_UPDATE_LISTING, False), - cacheToDisc=options.get(provider.RESULT_CACHE_TO_DISC, True) + updateListing=update_listing, + cacheToDisc=cache_to_disc, ) return succeeded - def _set_resolved_url(self, context, base_item, show_fanart): + def _set_resolved_url(self, context, base_item): + resolved = False uri = base_item.get_uri() if base_item.playable: @@ -174,21 +166,21 @@ def _set_resolved_url(self, context, base_item, show_fanart): position, _ = playlist.get_position() ui.set_property(PLAYLIST_POSITION, str(position)) - item = playback_item(context, base_item, show_fanart) + item = self._PLAY_ITEM_MAP[base_item.__class__.__name__]( + context, + base_item, + show_fanart=context.get_settings().show_fanart(), + for_playback=True, + ) xbmcplugin.setResolvedUrl(self.handle, succeeded=True, listitem=item) - return True - - if context.is_plugin_path(uri): + resolved = True + elif context.is_plugin_path(uri): context.log_debug('Redirecting to: |{0}|'.format(uri)) context.execute('RunPlugin({0})'.format(uri)) else: context.log_debug('Running script: |{0}|'.format(uri)) context.execute('RunScript({0})'.format(uri)) - xbmcplugin.endOfDirectory(self.handle, - succeeded=False, - updateListing=True, - cacheToDisc=False) - return False + return resolved From 5c490f936976d89415fe8cbb4d95aee4382c2c76 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 25 Apr 2024 09:08:47 +1000 Subject: [PATCH 11/59] Simplify md5 hash creation --- .../lib/youtube_plugin/kodion/items/base_item.py | 3 +-- .../kodion/json_store/access_manager.py | 10 +--------- .../youtube_plugin/kodion/sql_store/function_cache.py | 11 +++++++---- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/items/base_item.py b/resources/lib/youtube_plugin/kodion/items/base_item.py index 6576c7543..d0d69e0e7 100644 --- a/resources/lib/youtube_plugin/kodion/items/base_item.py +++ b/resources/lib/youtube_plugin/kodion/items/base_item.py @@ -66,8 +66,7 @@ def get_id(self): :return: unique id of the item. """ md5_hash = md5() - md5_hash.update(self._name.encode('utf-8')) - md5_hash.update(self._uri.encode('utf-8')) + md5_hash.update(''.join((self._name, self._uri)).encode('utf-8')) return md5_hash.hexdigest() def set_name(self, name): diff --git a/resources/lib/youtube_plugin/kodion/json_store/access_manager.py b/resources/lib/youtube_plugin/kodion/json_store/access_manager.py index 702d6e9d8..e1827b142 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/access_manager.py +++ b/resources/lib/youtube_plugin/kodion/json_store/access_manager.py @@ -590,13 +590,5 @@ def dev_keys_changed(self, addon_id, api_key, client_id, client_secret): @staticmethod def calc_key_hash(key, id, secret): md5_hash = md5() - try: - md5_hash.update(key.encode('utf-8')) - md5_hash.update(id.encode('utf-8')) - md5_hash.update(secret.encode('utf-8')) - except: - md5_hash.update(key) - md5_hash.update(id) - md5_hash.update(secret) - + md5_hash.update(''.join((key, id, secret)).encode('utf-8')) return md5_hash.hexdigest() diff --git a/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py b/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py index 19850a727..ece79618f 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py @@ -51,10 +51,13 @@ def _create_id_from_func(partial_func): :return: id for the given function """ md5_hash = md5() - md5_hash.update(partial_func.func.__module__.encode('utf-8')) - md5_hash.update(partial_func.func.__name__.encode('utf-8')) - md5_hash.update(str(partial_func.args).encode('utf-8')) - md5_hash.update(str(partial_func.keywords).encode('utf-8')) + signature = ( + partial_func.func.__module__, + partial_func.func.__name__, + partial_func.args, + partial_func.keywords.items() + ) + md5_hash.update(','.join(map(str, signature)).encode('utf-8')) return md5_hash.hexdigest() def get_result(self, func, *args, **kwargs): From 199b4f4b03d0787c5f6294f92da192e210883918 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 25 Apr 2024 09:38:40 +1000 Subject: [PATCH 12/59] Update FunctionCache - Add oneshot function to clear previously cached value once retrieved - Add option to cache ignoring parameter values for all or builtin types --- .../kodion/sql_store/function_cache.py | 56 +++++++++++++++---- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py b/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py index ece79618f..06297fe42 100644 --- a/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py +++ b/resources/lib/youtube_plugin/kodion/sql_store/function_cache.py @@ -12,6 +12,7 @@ from functools import partial from hashlib import md5 +from itertools import chain from .storage import Storage @@ -22,11 +23,15 @@ class FunctionCache(Storage): _table_updated = False _sql = {} + _BUILTIN = str.__module__ + PARAMS_NONE = 0 + PARAMS_BUILTINS = 1 + PARAMS_ALL = 2 + def __init__(self, filepath, max_file_size_mb=5): max_file_size_kb = max_file_size_mb * 1024 super(FunctionCache, self).__init__(filepath, max_file_size_kb=max_file_size_kb) - self._enabled = True def enabled(self): @@ -43,10 +48,10 @@ def disable(self): """ self._enabled = False - @staticmethod - def _create_id_from_func(partial_func): + @classmethod + def _create_id_from_func(cls, partial_func, hash_params=PARAMS_ALL): """ - Creats an id from the given function + Creates an id from the given function :param partial_func: :return: id for the given function """ @@ -54,9 +59,27 @@ def _create_id_from_func(partial_func): signature = ( partial_func.func.__module__, partial_func.func.__name__, - partial_func.args, - partial_func.keywords.items() ) + if hash_params == cls.PARAMS_BUILTINS: + signature = chain( + signature, + (( + arg + if type(arg).__module__ == cls._BUILTIN else + type(arg) + ) for arg in partial_func.args), + (( + (key, arg) + if type(arg).__module__ == cls._BUILTIN else + (key, type(arg)) + ) for key, arg in partial_func.keywords.items()), + ) + elif hash_params == cls.PARAMS_ALL: + signature = chain( + signature, + partial_func.args, + partial_func.keywords.items(), + ) md5_hash.update(','.join(map(str, signature)).encode('utf-8')) return md5_hash.hexdigest() @@ -67,18 +90,27 @@ def get_result(self, func, *args, **kwargs): if not self._enabled: return partial_func() - # only return before cached data + # only return previously cached data cache_id = self._create_id_from_func(partial_func) return self._get(cache_id) def run(self, func, seconds, *args, **kwargs): """ Returns the cached data of the given function. - :param func, function to cache - :param seconds: time to live in - :param _refresh: bool, updates cache with new func result + :param function func: function to call and cache if not already cached + :param int|None seconds: max allowable age of cached result + :param tuple args: positional arguments passed to the function + :param dict kwargs: keyword arguments passed to the function + :keyword _cacheparams: (int) cache result for function and parameters. + 0: function only, + 1: include value of builtin type parameters + 2: include value of all parameters, default 2 + :keyword _oneshot: (bool) remove previously cached result, default False + :keyword _refresh: (bool) updates cache with new result, default False :return: """ + cache_params = kwargs.pop('_cacheparams', self.PARAMS_ALL) + oneshot = kwargs.pop('_oneshot', False) refresh = kwargs.pop('_refresh', False) partial_func = partial(func, *args, **kwargs) @@ -86,11 +118,13 @@ def run(self, func, seconds, *args, **kwargs): if not self._enabled: return partial_func() - cache_id = self._create_id_from_func(partial_func) + cache_id = self._create_id_from_func(partial_func, cache_params) data = None if refresh else self._get(cache_id, seconds=seconds) if data is None: data = partial_func() self._set(cache_id, data) + elif oneshot: + self._remove(cache_id) return data From 4ae90e95a0b2eaff450acf38177fb24cab42234b Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 25 Apr 2024 10:49:24 +1000 Subject: [PATCH 13/59] Simplify AbstractProvider.navigate --- .../kodion/abstract_provider.py | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index 0b807cae5..4e5853ce3 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -116,25 +116,29 @@ def get_wizard_steps(self, context): def navigate(self, context): path = context.get_path() - - for key in self._dict_path: + for key, method_name in self._dict_path.items(): re_match = re.search(key, path, re.UNICODE) - if re_match is not None: - method_name = self._dict_path.get(key, '') - method = getattr(self, method_name, None) - if method is not None: - result = method(context, re_match) - refresh = context.get_param('refresh', False) - if not isinstance(result, tuple): - options = { - self.RESULT_CACHE_TO_DISC: True, - self.RESULT_UPDATE_LISTING: refresh, - } - else: - result, options = result - if refresh: - options[self.RESULT_UPDATE_LISTING] = refresh - return result, options + if not re_match: + continue + + method = getattr(self, method_name, None) + if not method: + continue + + result = method(context, re_match) + if isinstance(result, tuple): + result, options = result + else: + options = { + self.RESULT_CACHE_TO_DISC: True, + self.RESULT_UPDATE_LISTING: False, + } + + refresh = context.get_param('refresh') + if refresh: + options[self.RESULT_UPDATE_LISTING] = refresh + + return result, options raise KodionException("Mapping for path '%s' not found" % path) From ada3851ec2802c8bc4f5257f4d1894b46c50af9d Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 25 Apr 2024 10:50:58 +1000 Subject: [PATCH 14/59] Add force parameter to AbstractContext.set_path - Can be used if proper path has already been created --- .../kodion/context/abstract_context.py | 33 +++++++++++++++---- .../youtube_plugin/kodion/utils/__init__.py | 2 -- .../youtube_plugin/kodion/utils/methods.py | 21 +----------- .../youtube_plugin/youtube/helper/utils.py | 3 +- 4 files changed, 29 insertions(+), 30 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index 9297fc455..5d185dc35 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -13,7 +13,7 @@ import os from .. import logger -from ..compatibility import to_str, urlencode +from ..compatibility import quote, to_str, urlencode from ..constants import VALUE_FROM_STR from ..json_store import AccessManager from ..sql_store import ( @@ -24,7 +24,7 @@ SearchHistory, WatchLaterList, ) -from ..utils import create_path, current_system_version +from ..utils import current_system_version class AbstractContext(object): @@ -115,7 +115,7 @@ def __init__(self, path='/', params=None, plugin_name='', plugin_id=''): self._plugin_name = plugin_name self._version = 'UNKNOWN' self._plugin_id = plugin_id - self._path = create_path(path) + self._path = self.create_path(path) self._params = params self._utils = None @@ -226,7 +226,7 @@ def get_system_version(): def create_uri(self, path=None, params=None): if isinstance(path, (list, tuple)): - uri = create_path(*path, is_uri=True) + uri = self.create_path(*path, is_uri=True) elif path: uri = path else: @@ -239,11 +239,32 @@ def create_uri(self, path=None, params=None): return uri + @staticmethod + def create_path(*args, **kwargs): + path = '/'.join([ + part + for part in [ + str(arg).strip('/').replace('\\', '/').replace('//', '/') + for arg in args + ] if part + ]) + if path: + path = path.join(('/', '/')) + else: + return '/' + + if kwargs.get('is_uri'): + return quote(path) + return path + def get_path(self): return self._path - def set_path(self, *path): - self._path = create_path(*path) + def set_path(self, *path, **kwargs): + if kwargs.get('force'): + self._path = path[0] + else: + self._path = self.create_path(*path) def get_params(self): return self._params diff --git a/resources/lib/youtube_plugin/kodion/utils/__init__.py b/resources/lib/youtube_plugin/kodion/utils/__init__.py index f4567a779..4e9d5a2cf 100644 --- a/resources/lib/youtube_plugin/kodion/utils/__init__.py +++ b/resources/lib/youtube_plugin/kodion/utils/__init__.py @@ -12,7 +12,6 @@ from . import datetime_parser from .methods import ( - create_path, duration_to_seconds, find_best_fit, find_video_id, @@ -35,7 +34,6 @@ __all__ = ( - 'create_path', 'current_system_version', 'datetime_parser', 'duration_to_seconds', diff --git a/resources/lib/youtube_plugin/kodion/utils/methods.py b/resources/lib/youtube_plugin/kodion/utils/methods.py index 1efa8b658..95da428a0 100644 --- a/resources/lib/youtube_plugin/kodion/utils/methods.py +++ b/resources/lib/youtube_plugin/kodion/utils/methods.py @@ -18,12 +18,11 @@ from datetime import timedelta from math import floor, log -from ..compatibility import byte_string_type, quote, string_type, xbmc, xbmcvfs +from ..compatibility import byte_string_type, string_type, xbmc, xbmcvfs from ..logger import log_error __all__ = ( - 'create_path', 'duration_to_seconds', 'find_best_fit', 'find_video_id', @@ -176,24 +175,6 @@ def _find_best_fit_video(_stream_data): return selected_stream_data -def create_path(*args, **kwargs): - path = '/'.join([ - part - for part in [ - str(arg).strip('/').replace('\\', '/').replace('//', '/') - for arg in args - ] if part - ]) - if path: - path = path.join(('/', '/')) - else: - return '/' - - if kwargs.get('is_uri', False): - return quote(path) - return path - - def strip_html_from_text(text): """ Removes html tags diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index a48a85f99..a2e9d7e9c 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -17,7 +17,6 @@ from ...kodion.constants import content, paths from ...kodion.items import DirectoryItem, menu_items from ...kodion.utils import ( - create_path, datetime_parser, friendly_number, strip_html_from_text, @@ -687,7 +686,7 @@ def update_video_infos(provider, context, video_id_dict, # got to [CHANNEL] only if we are not directly in the channel if (channel_id and channel_name and - create_path('channel', channel_id) != path): + context.create_path('channel', channel_id) != path): video_item.set_channel_id(channel_id) context_menu.append( menu_items.go_to_channel( From 1bddcaef4d50f07fd93ab664cee2dc7a1e8e3938 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 25 Apr 2024 10:56:50 +1000 Subject: [PATCH 15/59] Add internal rerouting to workaround issues with ActivateWindow - ActivateWindow opens new window before path is navigated to, even if navigation fails, leading to loss of window history --- .../kodion/abstract_provider.py | 33 ++++++++++++++++++- .../kodion/constants/__init__.py | 4 ++- .../kodion/constants/const_paths.py | 1 + .../kodion/plugin/xbmc/xbmc_plugin.py | 16 +++++++-- 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index 4e5853ce3..81a0b905f 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -12,7 +12,7 @@ import re -from .constants import content, paths +from .constants import content, paths, ROUTE_FLAG from .exceptions import KodionException from .items import ( DirectoryItem, @@ -36,6 +36,12 @@ def __init__(self): '(?:', paths.HOME, ')?/?$' )), '_internal_root') + self.register_path(r''.join(( + '^', + paths.ROUTE, + '(?P/[^?]+?)(?:/*[?].+|/*)$' + )), 'reroute') + self.register_path(r''.join(( '^', paths.WATCH_LATER, @@ -170,6 +176,31 @@ def on_root(self, context, re_match): def _internal_root(self, context, re_match): return self.on_root(context, re_match) + def reroute(self, context, re_match=None, path=None, params=None): + if re_match: + path = re_match.group('path') + if params is None: + params = context.get_params() + if path: + result = None + function_cache = context.get_function_cache() + try: + result, options = function_cache.run( + self.navigate, + seconds=None, + _cacheparams=function_cache.PARAMS_NONE, + _refresh=True, + context=context.clone(path, params), + ) + finally: + if not result: + return False + context.get_ui().set_property(ROUTE_FLAG, path) + context.execute('ActivateWindow(Videos, {0}, return)'.format( + context.create_uri(path, params) + )) + return False + def on_bookmarks(self, context, re_match): raise NotImplementedError() diff --git a/resources/lib/youtube_plugin/kodion/constants/__init__.py b/resources/lib/youtube_plugin/kodion/constants/__init__.py index cdc7218d4..f63107730 100644 --- a/resources/lib/youtube_plugin/kodion/constants/__init__.py +++ b/resources/lib/youtube_plugin/kodion/constants/__init__.py @@ -33,8 +33,9 @@ } BUSY_FLAG = 'busy' -SWITCH_PLAYER_FLAG = 'switch_player' PLAYLIST_POSITION = 'playlist_position' +ROUTE_FLAG = 'route' +SWITCH_PLAYER_FLAG = 'switch_player' WAIT_FLAG = 'builtin_running' __all__ = ( @@ -45,6 +46,7 @@ 'MEDIA_PATH', 'PLAYLIST_POSITION', 'RESOURCE_PATH', + 'ROUTE_FLAG', 'SWITCH_PLAYER_FLAG', 'TEMP_PATH', 'VALUE_FROM_STR', diff --git a/resources/lib/youtube_plugin/kodion/constants/const_paths.py b/resources/lib/youtube_plugin/kodion/constants/const_paths.py index af6ed0a70..96dec3bdc 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_paths.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_paths.py @@ -13,6 +13,7 @@ BOOKMARKS = '/kodion/bookmarks' EXTERNAL_SEARCH = '/search' +ROUTE = '/kodion/route' SEARCH = '/kodion/search' WATCH_LATER = '/kodion/watch_later' HISTORY = '/kodion/playback_history' diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py index 63774d63f..4207c154f 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py @@ -14,7 +14,7 @@ from ..abstract_plugin import AbstractPlugin from ...compatibility import xbmcplugin -from ...constants import BUSY_FLAG, PLAYLIST_POSITION +from ...constants import BUSY_FLAG, PLAYLIST_POSITION, ROUTE_FLAG from ...exceptions import KodionException from ...items import ( DirectoryItem, @@ -112,7 +112,19 @@ def run(self, provider, context): provider.run_wizard(context) try: - result, options = provider.navigate(context) + route = ui.get_property(ROUTE_FLAG) + if route: + function_cache = context.get_function_cache() + result, options = function_cache.run( + provider.navigate, + seconds=None, + _cacheparams=function_cache.PARAMS_NONE, + _oneshot=True, + context=context.clone(route), + ) + ui.clear_property(ROUTE_FLAG) + else: + result, options = provider.navigate(context) except KodionException as exc: result = options = None if provider.handle_exception(context, exc): From 713cd876607cec7e2f9738c659a1ef2dec3dad76 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 25 Apr 2024 11:08:47 +1000 Subject: [PATCH 16/59] Use internal rerouting instead of ActivateWindow --- .../youtube_plugin/kodion/items/menu_items.py | 32 +++++++++---------- .../kodion/ui/xbmc/xbmc_context_ui.py | 9 ------ .../youtube/helper/yt_playlist.py | 4 ++- 3 files changed, 18 insertions(+), 27 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/items/menu_items.py b/resources/lib/youtube_plugin/kodion/items/menu_items.py index 7f566223d..85851dac9 100644 --- a/resources/lib/youtube_plugin/kodion/items/menu_items.py +++ b/resources/lib/youtube_plugin/kodion/items/menu_items.py @@ -30,8 +30,8 @@ def more_for_video(context, video_id, logged_in=False, refresh=False): def related_videos(context, video_id): return ( context.localize('related_videos'), - 'ActivateWindow(Videos, {0}, return)'.format(context.create_uri( - ('special', 'related_videos',), + 'RunPlugin({0})'.format(context.create_uri( + (paths.ROUTE, 'special', 'related_videos',), { 'video_id': video_id, }, @@ -42,8 +42,8 @@ def related_videos(context, video_id): def video_comments(context, video_id): return ( context.localize('video.comments'), - 'ActivateWindow(Videos, {0}, return)'.format(context.create_uri( - ('special', 'parent_comments',), + 'RunPlugin({0})'.format(context.create_uri( + (paths.ROUTE, 'special', 'parent_comments',), { 'video_id': video_id, }, @@ -54,8 +54,8 @@ def video_comments(context, video_id): def content_from_description(context, video_id): return ( context.localize('video.description.links'), - 'ActivateWindow(Videos, {0}, return)'.format(context.create_uri( - ('special', 'description_links',), + 'RunPlugin({0})'.format(context.create_uri( + (paths.ROUTE, 'special', 'description_links',), { 'video_id': video_id, }, @@ -73,8 +73,8 @@ def play_with(context): def refresh(context): return ( context.localize('refresh'), - 'ActivateWindow(Videos, {0}, return)'.format(context.create_uri( - context.get_path(), + 'RunPlugin({0})'.format(context.create_uri( + (paths.ROUTE, context.get_path(),), dict(context.get_params(), refresh=True), )) ) @@ -307,8 +307,8 @@ def watch_later_local_clear(context): def go_to_channel(context, channel_id, channel_name): return ( context.localize('go_to_channel') % context.get_ui().bold(channel_name), - 'ActivateWindow(Videos, {0}, return)'.format(context.create_uri( - ('channel', channel_id,), + 'RunPlugin({0})'.format(context.create_uri( + (paths.ROUTE, 'channel', channel_id,), )) ) @@ -497,6 +497,7 @@ def bookmarks_clear(context): )) ) + def search_remove(context, query): return ( context.localize('search.remove'), @@ -540,8 +541,8 @@ def separator(): def goto_home(context): return ( context.localize(10000), - 'ActivateWindow(Videos, {0}, return)'.format(context.create_uri( - (paths.HOME,), + 'RunPlugin({0})'.format(context.create_uri( + (paths.ROUTE, paths.HOME,), )) ) @@ -549,10 +550,7 @@ def goto_home(context): def goto_quick_search(context): return ( context.localize('search.quick'), - 'ActivateWindow(Videos, {0}, return)'.format(context.create_uri( - (paths.SEARCH, 'input',), - { - 'refresh': True, - }, + 'RunPlugin({0})'.format(context.create_uri( + (paths.ROUTE, paths.SEARCH, 'input',), )) ) 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 92a41ef7f..93c8c6ebe 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 @@ -146,15 +146,6 @@ def refresh_container(): addon_id=ADDON_ID )) - def reload_container(self, path=None): - context = self._context - if path in (True, None): - path = context.get_path() - params = dict(context.get_params(), refresh=True) - xbmc.executebuiltin('ActivateWindow(Videos, {0}, return)'.format( - context.create_uri(path, params) - )) - @staticmethod def set_property(property_id, value): property_id = '-'.join((ADDON_ID, property_id)) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py index 7fcc71030..2e343c849 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py @@ -110,7 +110,9 @@ def _process_remove_video(provider, context): if keymap_action: context.get_ui().set_focus_next_item() elif path is not False: - context.get_ui().reload_container(path) + provider.reroute(context, + path=path, + params=dict(params, refresh=True)) context.get_ui().show_notification( message=context.localize('playlist.removed_from'), From 99aaf489dc984c9eec78541d00a92d5feec30667 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 25 Apr 2024 22:33:48 +1000 Subject: [PATCH 17/59] Allow for refreshing a listing that has previously been refreshed - Kodi will not reload a listing if the path has not changed - As a workaround make refresh parameter an integer (was bool) and increment it --- .../kodion/abstract_provider.py | 4 +-- .../kodion/context/abstract_context.py | 13 ++++++++-- .../youtube_plugin/kodion/items/menu_items.py | 25 +++++++++++-------- .../youtube_plugin/kodion/script_actions.py | 2 +- .../youtube/helper/yt_playlist.py | 8 +++--- 5 files changed, 34 insertions(+), 18 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index 81a0b905f..dae85ddc1 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -141,8 +141,8 @@ def navigate(self, context): } refresh = context.get_param('refresh') - if refresh: - options[self.RESULT_UPDATE_LISTING] = refresh + if refresh is not None: + options[self.RESULT_UPDATE_LISTING] = bool(refresh) return result, options diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index 5d185dc35..a9b72fd04 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -43,7 +43,6 @@ class AbstractContext(object): 'logged_in', 'play', 'prompt_for_subtitles', - 'refresh', 'resume', 'screensaver', 'strm', @@ -53,6 +52,10 @@ class AbstractContext(object): 'next_page_token', 'offset', 'page', + 'refresh', + } + _INT_BOOL_PARAMS = { + 'refresh', } _FLOAT_PARAMS = { 'seek', @@ -282,7 +285,13 @@ def parse_params(self, params=None): if param in self._BOOL_PARAMS: parsed_value = VALUE_FROM_STR.get(str(value).lower(), False) elif param in self._INT_PARAMS: - parsed_value = int(value) + parsed_value = None + if param in self._INT_BOOL_PARAMS: + parsed_value = VALUE_FROM_STR.get(str(value).lower()) + if parsed_value is None: + parsed_value = int(value) + else: + parsed_value = int(parsed_value) elif param in self._FLOAT_PARAMS: parsed_value = float(value) elif param in self._LIST_PARAMS: diff --git a/resources/lib/youtube_plugin/kodion/items/menu_items.py b/resources/lib/youtube_plugin/kodion/items/menu_items.py index 85851dac9..0318235a8 100644 --- a/resources/lib/youtube_plugin/kodion/items/menu_items.py +++ b/resources/lib/youtube_plugin/kodion/items/menu_items.py @@ -14,15 +14,17 @@ def more_for_video(context, video_id, logged_in=False, refresh=False): + params = { + 'video_id': video_id, + 'logged_in': logged_in, + } + if refresh: + params['refresh'] = context.get_param('refresh', 0) + 1 return ( context.localize('video.more'), 'RunPlugin({0})'.format(context.create_uri( ('video', 'more',), - { - 'video_id': video_id, - 'logged_in': logged_in, - 'refresh': refresh, - }, + params, )) ) @@ -71,11 +73,12 @@ def play_with(context): def refresh(context): + params = context.get_params() return ( context.localize('refresh'), 'RunPlugin({0})'.format(context.create_uri( (paths.ROUTE, context.get_path(),), - dict(context.get_params(), refresh=True), + dict(params, refresh=params.get('refresh', 0) + 1), )) ) @@ -245,14 +248,16 @@ def add_my_subscriptions_filter(context, channel_name): def rate_video(context, video_id, refresh=False): + params = { + 'video_id': video_id, + } + if refresh: + params['refresh'] = context.get_param('refresh', 0) + 1 return ( context.localize('video.rate'), 'RunPlugin({0})'.format(context.create_uri( ('video', 'rate',), - { - 'video_id': video_id, - 'refresh': refresh, - }, + params, )) ) diff --git a/resources/lib/youtube_plugin/kodion/script_actions.py b/resources/lib/youtube_plugin/kodion/script_actions.py index f6db310f2..4fb9f16ae 100644 --- a/resources/lib/youtube_plugin/kodion/script_actions.py +++ b/resources/lib/youtube_plugin/kodion/script_actions.py @@ -239,7 +239,7 @@ def switch_to_user(user): localize('user.changed') % access_manager.get_username(user), localize('user.switch') ) - if context.get_param('refresh') is not False: + if context.get_param('refresh') != 0: ui.refresh_container() if action == 'switch': diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py index 2e343c849..6d49d1c50 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py @@ -110,9 +110,11 @@ def _process_remove_video(provider, context): if keymap_action: context.get_ui().set_focus_next_item() elif path is not False: - provider.reroute(context, - path=path, - params=dict(params, refresh=True)) + provider.reroute( + context, + path=path, + params=dict(params, refresh=params.get('refresh', 0) + 1), + ) context.get_ui().show_notification( message=context.localize('playlist.removed_from'), From ddfd5c821c15e75eef1860b7b78e2354c0e15328 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 25 Apr 2024 22:36:42 +1000 Subject: [PATCH 18/59] Add refresh to Next page context menu --- resources/lib/youtube_plugin/kodion/items/next_page_item.py | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/lib/youtube_plugin/kodion/items/next_page_item.py b/resources/lib/youtube_plugin/kodion/items/next_page_item.py index 856cd28bc..c1f0fb260 100644 --- a/resources/lib/youtube_plugin/kodion/items/next_page_item.py +++ b/resources/lib/youtube_plugin/kodion/items/next_page_item.py @@ -37,6 +37,7 @@ def __init__(self, context, params, image=None, fanart=None): self.set_fanart(fanart) context_menu = [ + menu_items.refresh(context), menu_items.goto_home(context), menu_items.goto_quick_search(context), menu_items.separator(), From 44cd99bbc126bdcb8c8ca26354121ee93083d6b2 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 25 Apr 2024 23:03:40 +1000 Subject: [PATCH 19/59] Add Next page item to last page of paginated listings, to go back to first page --- .../lib/youtube_plugin/youtube/helper/v3.py | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index c0f3a3052..733a0afbc 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -372,7 +372,7 @@ def response_to_items(provider, result = filter_short_videos(result) # no processing of next page item - if not process_next_page: + if not result or not process_next_page: return result # next page @@ -384,33 +384,39 @@ def response_to_items(provider, This should work for up to ~2000 entries. """ params = context.get_params() - page = params.get('page', 1) - new_params = dict(params, page=page + 1) + current_page = params.get('page', 1) + next_page = current_page + 1 + new_params = dict(params, page=next_page) yt_next_page_token = json_data.get('nextPageToken') if yt_next_page_token: new_params['page_token'] = yt_next_page_token - else: + elif 'page_token' in new_params: + del new_params['page_token'] page_info = json_data.get('pageInfo', {}) yt_total_results = int(page_info.get('totalResults', 0)) yt_results_per_page = int(page_info.get('resultsPerPage', 50)) - if page * yt_results_per_page < yt_total_results: + if current_page * yt_results_per_page < yt_total_results: new_params['items_per_page'] = yt_results_per_page else: - return result + next_page = 1 + new_params['page'] = 1 + else: + return result yt_visitor_data = json_data.get('visitorData') if yt_visitor_data: new_params['visitor'] = yt_visitor_data - yt_click_tracking = json_data.get('clickTracking') - if yt_click_tracking: - new_params['click_tracking'] = yt_click_tracking + if next_page > 1: + yt_click_tracking = json_data.get('clickTracking') + if yt_click_tracking: + new_params['click_tracking'] = yt_click_tracking - offset = json_data.get('offset') - if offset: - new_params['offset'] = offset + offset = json_data.get('offset') + if offset: + new_params['offset'] = offset next_page_item = NextPageItem(context, new_params) result.append(next_page_item) From 176631a63b15824b40ab27efbc82f4dd134be74b Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 26 Apr 2024 07:30:25 +1000 Subject: [PATCH 20/59] Minor optimisation of registering and checking paths for routing --- .../kodion/abstract_provider.py | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index dae85ddc1..72414fef3 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -34,61 +34,65 @@ def __init__(self): self.register_path(r''.join(( '^', '(?:', paths.HOME, ')?/?$' - )), '_internal_root') + )), self._internal_root) self.register_path(r''.join(( '^', paths.ROUTE, '(?P/[^?]+?)(?:/*[?].+|/*)$' - )), 'reroute') + )), self.reroute) self.register_path(r''.join(( '^', paths.WATCH_LATER, '/(?Padd|clear|list|remove)/?$' - )), 'on_watch_later') + )), self.on_watch_later) self.register_path(r''.join(( '^', paths.BOOKMARKS, '/(?Padd|clear|list|remove)/?$' - )), 'on_bookmarks') + )), self.on_bookmarks) self.register_path(r''.join(( '^', '(', paths.SEARCH, '|', paths.EXTERNAL_SEARCH, ')', '/(?Pinput|query|list|remove|clear|rename)?/?$' - )), '_internal_search') + )), self._internal_search) self.register_path(r''.join(( '^', paths.HISTORY, '/?$' - )), 'on_playback_history') + )), self.on_playback_history) self.register_path(r'(?P.*\/)extrafanart\/([\?#].+)?$', - '_internal_on_extra_fanart') + self._internal_on_extra_fanart) """ - Test each method of this class for the appended attribute '_re_match' by the - decorator (RegisterProviderPath). - The '_re_match' attributes describes the path which must match for the decorated method. + Test each method of this class for the attribute 'kodion_re_path' added + by the decorator @RegisterProviderPath. + The 'kodion_re_path' attribute is a regular expression that must match + the current path in order for the decorated method to run. """ + for attribute_name in dir(self): + if attribute_name.startswith('__'): + continue + attribute = getattr(self, attribute_name, None) + if not attribute or not callable(attribute): + continue + re_path = getattr(attribute, 'kodion_re_path', None) + if re_path: + self.register_path(re_path, attribute) - for method_name in dir(self): - method = getattr(self, method_name, None) - path = method and getattr(method, 'kodion_re_path', None) - if path: - self.register_path(path, method_name) - - def register_path(self, re_path, method_name): + def register_path(self, re_path, method): """ - Registers a new method by name (string) for the given regular expression + Registers a new method for the given regular expression :param re_path: regular expression of the path - :param method_name: name of the method + :param method: method to be registered :return: """ - self._dict_path[re_path] = method_name + self._dict_path[re.compile(re_path, re.UNICODE)] = method def run_wizard(self, context): settings = context.get_settings() @@ -122,15 +126,11 @@ def get_wizard_steps(self, context): def navigate(self, context): path = context.get_path() - for key, method_name in self._dict_path.items(): - re_match = re.search(key, path, re.UNICODE) + for re_path, method in self._dict_path.items(): + re_match = re_path.search(path) if not re_match: continue - method = getattr(self, method_name, None) - if not method: - continue - result = method(context, re_match) if isinstance(result, tuple): result, options = result From 9a964aa0727ca03c9f89d430620e9d1284faf108 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 26 Apr 2024 07:36:09 +1000 Subject: [PATCH 21/59] Add Jump to page... context menu item to Next page item #715 - Will prompt for page of listing to jump to - Can also be used to jump directly to a page of a listing #317 - plugin://plugin.video.youtube/goto_page// --- .../resource.language.en_gb/strings.po | 4 +++ .../kodion/abstract_provider.py | 27 +++++++++++++++++++ .../kodion/constants/const_paths.py | 1 + .../kodion/context/xbmc/xbmc_context.py | 3 ++- .../youtube_plugin/kodion/items/menu_items.py | 10 +++++++ .../kodion/items/next_page_item.py | 16 ++++++----- .../youtube/helper/yt_playlist.py | 2 +- 7 files changed, 54 insertions(+), 9 deletions(-) diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 80d336e8a..75b69e577 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1528,3 +1528,7 @@ msgstr "" msgctxt "#30805" msgid "Use adaptive streaming formats with external player" msgstr "" + +msgctxt "#30806" +msgid "Jump to page..." +msgstr "" diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index 72414fef3..af9269b53 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -16,6 +16,7 @@ from .exceptions import KodionException from .items import ( DirectoryItem, + NextPageItem, NewSearchItem, SearchHistoryItem, ) @@ -42,6 +43,13 @@ def __init__(self): '(?P/[^?]+?)(?:/*[?].+|/*)$' )), self.reroute) + self.register_path(r''.join(( + '^', + paths.GOTO_PAGE, + '(?P/[0-9]+)?' + '(?P/[^?]+?)(?:/*[?].+|/*)$' + )), self._internal_goto_page) + self.register_path(r''.join(( '^', paths.WATCH_LATER, @@ -176,6 +184,25 @@ def on_root(self, context, re_match): def _internal_root(self, context, re_match): return self.on_root(context, re_match) + def _internal_goto_page(self, context, re_match): + page = re_match.group('page') + if page: + page = int(page.lstrip('/')) + else: + result, page = context.get_ui().on_numeric_input( + context.localize('page.choose'), 1 + ) + if not result: + return False + + path = re_match.group('path') + params = context.get_params() + page_token = NextPageItem.create_page_token( + page, params.get('items_per_page', 50) + ) + params = dict(params, page=page, page_token=page_token) + return self.reroute(context, path=path, params=params) + def reroute(self, context, re_match=None, path=None, params=None): if re_match: path = re_match.group('path') diff --git a/resources/lib/youtube_plugin/kodion/constants/const_paths.py b/resources/lib/youtube_plugin/kodion/constants/const_paths.py index 96dec3bdc..5791f26d3 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_paths.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_paths.py @@ -13,6 +13,7 @@ BOOKMARKS = '/kodion/bookmarks' EXTERNAL_SEARCH = '/search' +GOTO_PAGE = '/kodion/goto_page' ROUTE = '/kodion/route' SEARCH = '/kodion/search' WATCH_LATER = '/kodion/watch_later' 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 eb5b9c099..8fbc11783 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -133,8 +133,9 @@ class XbmcContext(AbstractContext): 'my_subscriptions.filter.remove': 30588, 'my_subscriptions.filter.removed': 30590, 'my_subscriptions.filtered': 30584, - 'next_page': 30106, 'none': 30561, + 'page.next': 30106, + 'page.choose': 30806, 'playlist.added_to': 30714, 'playlist.create': 30522, 'playlist.play.all': 30531, diff --git a/resources/lib/youtube_plugin/kodion/items/menu_items.py b/resources/lib/youtube_plugin/kodion/items/menu_items.py index 0318235a8..0a1ec5638 100644 --- a/resources/lib/youtube_plugin/kodion/items/menu_items.py +++ b/resources/lib/youtube_plugin/kodion/items/menu_items.py @@ -559,3 +559,13 @@ def goto_quick_search(context): (paths.ROUTE, paths.SEARCH, 'input',), )) ) + + +def goto_page(context): + return ( + context.localize('page.choose'), + 'RunPlugin({0})'.format(context.create_uri( + (paths.GOTO_PAGE, context.get_path(),), + context.get_params(), + )) + ) diff --git a/resources/lib/youtube_plugin/kodion/items/next_page_item.py b/resources/lib/youtube_plugin/kodion/items/next_page_item.py index c1f0fb260..55886c9f7 100644 --- a/resources/lib/youtube_plugin/kodion/items/next_page_item.py +++ b/resources/lib/youtube_plugin/kodion/items/next_page_item.py @@ -19,25 +19,27 @@ def __init__(self, context, params, image=None, fanart=None): if 'refresh' in params: del params['refresh'] - self.next_page = params.get('page', 2) - self.items_per_page = params.pop('items_per_page', 50) + page = params.get('page', 2) + items_per_page = params.get('items_per_page', 50) if 'page_token' not in params: - params['page_token'] = self.calculate_next_page_token( - self.next_page, self.items_per_page - ) + params['page_token'] = self.create_page_token(page, items_per_page) super(NextPageItem, self).__init__( - context.localize('next_page') % self.next_page, + context.localize('page.next') % page, context.create_uri(context.get_path(), params), image=image, category_label='__inherit__', ) + self.next_page = page + self.items_per_page = items_per_page + if fanart: self.set_fanart(fanart) context_menu = [ menu_items.refresh(context), + menu_items.goto_page(context), menu_items.goto_home(context), menu_items.goto_quick_search(context), menu_items.separator(), @@ -45,7 +47,7 @@ def __init__(self, context, params, image=None, fanart=None): self.set_context_menu(context_menu) @classmethod - def calculate_next_page_token(cls, page, items_per_page): + def create_page_token(cls, page, items_per_page=50): low = 'AEIMQUYcgkosw048' high = 'ABCDEFGHIJKLMNOP' len_low = len(low) diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py index 6d49d1c50..eb22e0d18 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py @@ -223,7 +223,7 @@ def _process_select_playlist(provider, context): if page_token: next_page = current_page + 1 items.append(( - ui.bold(context.localize('next_page') % next_page), '', + ui.bold(context.localize('page.next') % next_page), '', 'playlist.next', 'DefaultFolder.png', )) From 39489d69675b52c1812f760ff0eb612d126f35ff Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 26 Apr 2024 13:53:37 +1000 Subject: [PATCH 22/59] Add setting to use channel name as studio #717 - `Settings > Advanced > Views > Use channel name as` - Can be used as cast or as studio - By default channel name is used as artist for sorting purposes --- .../resource.language.en_gb/strings.po | 4 ++++ .../kodion/constants/const_settings.py | 1 + .../youtube_plugin/kodion/items/video_item.py | 6 ++--- .../kodion/items/xbmc/xbmc_items.py | 22 +++++++++++++++---- .../kodion/settings/abstract_settings.py | 3 +++ .../youtube_plugin/youtube/helper/utils.py | 22 ++++++++++++------- resources/settings.xml | 15 +++++++++++++ 7 files changed, 58 insertions(+), 15 deletions(-) diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 75b69e577..1acd905a7 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1532,3 +1532,7 @@ msgstr "" msgctxt "#30806" msgid "Jump to page..." msgstr "" + +msgctxt "#30807" +msgid "Use channel name as" +msgstr "" diff --git a/resources/lib/youtube_plugin/kodion/constants/const_settings.py b/resources/lib/youtube_plugin/kodion/constants/const_settings.py index a49ebe94c..22913ee98 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_settings.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_settings.py @@ -55,6 +55,7 @@ SEARCH_SIZE = 'kodion.search.size' # (int) CACHE_SIZE = 'kodion.cache.size' # (int) +CHANNEL_NAME_ALIASES = 'youtube.view.channel_name.aliases' # (list[string]) DETAILED_DESCRIPTION = 'youtube.view.description.details' # (bool) DETAILED_LABELS = 'youtube.view.label.details' # (bool) LABEL_COLOR = 'youtube.view.label.color' # (string) diff --git a/resources/lib/youtube_plugin/kodion/items/video_item.py b/resources/lib/youtube_plugin/kodion/items/video_item.py index 931b4a3f8..cb2cadc4d 100644 --- a/resources/lib/youtube_plugin/kodion/items/video_item.py +++ b/resources/lib/youtube_plugin/kodion/items/video_item.py @@ -163,12 +163,12 @@ def get_directors(self): def set_directors(self, directors): self._directors = list(directors) - def add_cast(self, member, role=None, order=None, thumbnail=None): + def add_cast(self, name, role=None, order=None, thumbnail=None): if self._cast is None: self._cast = [] - if member: + if name: self._cast.append({ - 'member': to_str(member), + 'name': to_str(name), 'role': to_str(role) if role else '', 'order': int(order) if order else len(self._cast) + 1, 'thumbnail': to_str(thumbnail) if thumbnail else '', diff --git a/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py index 97dcd3db4..a92e59af9 100644 --- a/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py @@ -31,6 +31,11 @@ def set_info(list_item, item, properties): if value is not None: info_labels['artist'] = value + value = item.get_cast() + if value is not None: + info_labels['castandrole'] = [(member['name'], member['role']) + for member in value] + value = item.get_code() if value is not None: info_labels['code'] = value @@ -95,6 +100,10 @@ def set_info(list_item, item, properties): if value is not None: info_labels['year'] = value + value = item.get_studios() + if value is not None: + info_labels['studio'] = value + if info_labels: list_item.setInfo('video', info_labels) @@ -201,8 +210,10 @@ def set_info(list_item, item, properties): # cast: list[xbmc.Actor] # From list[{member: str, role: str, order: int, thumbnail: str}] - # Currently unused - # info_tag.setCast(xbmc.Actor(**member) for member in item.get_cast()) + # Used as alias for channel name if enabled + value = item.get_cast() + if value is not None: + info_tag.setCast([xbmc.Actor(**member) for member in value]) # code: str # eg. "466K | 3.9K | 312" @@ -248,8 +259,10 @@ def set_info(list_item, item, properties): info_tag.setSeason(value) # studio: list[str] - # Currently unused - # info_tag.setStudios(item.get_studios()) + # Used as alias for channel name if enabled + value = item.get_studios() + if value is not None: + info_tag.setStudios(value) elif isinstance(item, DirectoryItem): info_tag = list_item.getVideoInfoTag() @@ -285,6 +298,7 @@ def set_info(list_item, item, properties): # artist: list[str] # eg. ["Angerfist"] + # Used as alias for channel name value = item.get_artists() if value is not None: info_tag.setArtists(value) diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index 7c825657a..df1a98d5f 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -409,3 +409,6 @@ def get_label_color(self, label_part): def get_label_color(self, label_part): return self._COLOR_MAP.get(label_part, 'white') + + def get_channel_name_aliases(self): + return frozenset(self.get_string_list(settings.CHANNEL_NAME_ALIASES)) diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index a2e9d7e9c..a6d3a07b9 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -389,18 +389,21 @@ def update_video_infos(provider, context, video_id_dict, else: watch_later_id = None + localize = context.localize settings = context.get_settings() alternate_player = settings.support_alternative_player() default_web_urls = settings.default_player_web_urls() ask_quality = not default_web_urls and settings.ask_for_video_quality() audio_only = settings.audio_only() + channel_name_aliases = settings.get_channel_name_aliases() hide_shorts = settings.hide_short_videos() show_details = settings.show_detailed_description() subtitles_prompt = settings.get_subtitle_selection() == 1 thumb_size = settings.use_thumbnail_size() thumb_stamp = get_thumb_timestamp() - untitled = context.localize('untitled') + channel_role = localize(19029) + untitled = localize('untitled') path = context.get_path() ui = context.get_ui() @@ -484,8 +487,7 @@ def update_video_infos(provider, context, video_id_dict, video_item.set_aired_from_datetime(local_datetime) video_item.set_premiered_from_datetime(local_datetime) video_item.set_date_from_datetime(local_datetime) - type_label = context.localize('live' if video_item.live - else 'upcoming') + type_label = localize('live' if video_item.live else 'upcoming') start_at = '{type_label} {start_at}'.format( type_label=type_label, start_at=datetime_parser.get_scheduled_start( @@ -507,7 +509,7 @@ def update_video_infos(provider, context, video_id_dict, continue color = settings.get_label_color(stat) - label = context.localize(label) + label = localize(label) if value == 1: label = label.rstrip('s') @@ -573,8 +575,15 @@ def update_video_infos(provider, context, video_id_dict, if season and episode: break - # plot + # channel name channel_name = snippet.get('channelTitle', '') + video_item.add_artist(channel_name) + if 'cast' in channel_name_aliases: + video_item.add_cast(channel_name, role=channel_role) + if 'studio' in channel_name_aliases: + video_item.add_studio(channel_name) + + # plot description = strip_html_from_text(snippet['description']) if show_details: description = ''.join(( @@ -584,9 +593,6 @@ def update_video_infos(provider, context, video_id_dict, else ui.new_line(start_at, cr_after=1)) if start_at else '', description, )) - # video_item.add_studio(channel_name) - # video_item.add_cast(channel_name) - video_item.add_artist(channel_name) video_item.set_plot(description) # date time diff --git a/resources/settings.xml b/resources/settings.xml index fad839fd0..2b1f77a92 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -745,6 +745,21 @@ true + + 0 + cast + + + + + + , + + + true + true + + 0 ffadd8e6 From 76c131d81e22ef0058422272e91a4571db2999d0 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sat, 27 Apr 2024 18:12:03 +1000 Subject: [PATCH 23/59] Don't attempt to reroute to same path with same params - Kodi doesn't support this --- resources/lib/youtube_plugin/kodion/abstract_provider.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index af9269b53..ca5f48ee8 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -204,11 +204,15 @@ def _internal_goto_page(self, context, re_match): return self.reroute(context, path=path, params=params) def reroute(self, context, re_match=None, path=None, params=None): + current_path = context.get_path() + current_params = context.get_params() if re_match: path = re_match.group('path') if params is None: - params = context.get_params() - if path: + params = current_params + if (path and path != current_path + or 'refresh' in params + or params != current_params): result = None function_cache = context.get_function_cache() try: From 3018ee61684fe88b35eef09d73700a72fe06f358 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 28 Apr 2024 18:01:46 +1000 Subject: [PATCH 24/59] Don't get/set settings directly, use specific settings methods instead --- .../youtube_plugin/kodion/abstract_provider.py | 2 +- .../kodion/context/abstract_context.py | 2 +- .../kodion/plugin/xbmc/xbmc_plugin.py | 2 +- .../youtube_plugin/kodion/script_actions.py | 2 +- .../kodion/settings/abstract_settings.py | 18 ++++++++++++++++-- .../youtube/helper/yt_setup_wizard.py | 4 ++-- .../lib/youtube_plugin/youtube/provider.py | 12 ++++++------ 7 files changed, 28 insertions(+), 14 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index ca5f48ee8..90900bc6f 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -125,7 +125,7 @@ def run_wizard(self, context): else: step += 1 finally: - settings.set_bool(settings.SETUP_WIZARD, False) + settings.setup_wizard_enabled(False) context.send_notification('check_settings', 'process') def get_wizard_steps(self, context): diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index a9b72fd04..a17c9e7a3 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -179,7 +179,7 @@ def get_function_cache(self): def get_search_history(self): if not self._search_history: settings = self.get_settings() - search_size = settings.get_int(settings.SEARCH_SIZE, 50) + search_size = settings.get_search_history_size() uuid = self.get_access_manager().get_current_user_id() filename = 'search.sqlite' filepath = os.path.join(self.get_data_path(), uuid, filename) diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py index 4207c154f..f94a06aeb 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py @@ -108,7 +108,7 @@ def run(self, provider, context): ui.clear_property(BUSY_FLAG) ui.clear_property(PLAYLIST_POSITION) - if settings.is_setup_wizard_enabled(): + if settings.setup_wizard_enabled(): provider.run_wizard(context) try: diff --git a/resources/lib/youtube_plugin/kodion/script_actions.py b/resources/lib/youtube_plugin/kodion/script_actions.py index 4fb9f16ae..c23e5a416 100644 --- a/resources/lib/youtube_plugin/kodion/script_actions.py +++ b/resources/lib/youtube_plugin/kodion/script_actions.py @@ -31,7 +31,7 @@ def _config_actions(context, action, *_args): if context.use_inputstream_adaptive(): xbmcaddon.Addon(id='inputstream.adaptive').openSettings() else: - settings.set_bool('kodion.video.quality.isa', False) + settings.use_isa(False) elif action == 'inputstreamhelper': try: diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index df1a98d5f..20fb3368b 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -94,10 +94,18 @@ def cache_size(self, value=None): def get_search_history_size(self): return self.get_int(settings.SEARCH_SIZE, 10) - def is_setup_wizard_enabled(self): + def setup_wizard_enabled(self, value=None): # Increment min_required on new release to enable oneshot on first run min_required = 3 - forced_runs = self.get_int(settings.SETUP_WIZARD_RUNS, min_required - 1) + + if value is False: + self.set_int(settings.SETUP_WIZARD_RUNS, min_required) + return self.set_bool(settings.SETUP_WIZARD, False) + if value is True: + self.set_int(settings.SETUP_WIZARD_RUNS, 0) + return self.set_bool(settings.SETUP_WIZARD, True) + + forced_runs = self.get_int(settings.SETUP_WIZARD_RUNS, 0) if forced_runs < min_required: self.set_int(settings.SETUP_WIZARD_RUNS, min_required) return True @@ -380,9 +388,15 @@ def show_detailed_labels(self, value=None): def get_language(self): return self.get_string(settings.LANGUAGE, 'en_US').replace('_', '-') + def set_language(self, language_id): + return self.set_string(settings.LANGUAGE, language_id) + def get_region(self): return self.get_string(settings.REGION, 'US') + def set_region(self, region_id): + return self.set_string(settings.REGION, region_id) + def get_watch_later_playlist(self): return self.get_string(settings.WATCH_LATER_PLAYLIST, '').strip() 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 7aa362f3e..63831e944 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py @@ -282,8 +282,8 @@ def _get_selected_region(item): # set new language id and region id settings = context.get_settings() - settings.set_string(settings.LANGUAGE, language_id) - settings.set_string(settings.REGION, region_id) + settings.set_language(language_id) + settings.set_region(region_id) provider.reset_client() return step diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 2553ddcb7..76cd9b7dd 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -937,24 +937,24 @@ def api_key_update(self, context, re_match): log_list = [] if api_key: - settings.set_string('youtube.api.key', api_key) + settings.api_key(api_key) updated_list.append(localize('api.key')) log_list.append('Key') if client_id: - settings.set_string('youtube.api.id', client_id) + settings.api_id(client_id) updated_list.append(localize('api.id')) log_list.append('Id') if client_secret: - settings.set_string('youtube.api.secret', client_secret) + settings.api_secret(client_secret) updated_list.append(localize('api.secret')) log_list.append('Secret') if updated_list: ui.show_notification(localize('updated_') % ', '.join(updated_list)) context.log_debug('Updated API keys: %s' % ', '.join(log_list)) - client_id = settings.get_string('youtube.api.id', '') - client_secret = settings.get_string('youtube.api.secret', '') - api_key = settings.get_string('youtube.api.key', '') + client_id = settings.api_id() + client_secret = settings.api_secret() + api_key = settings.api_key missing_list = [] log_list = [] From 1092d360564dec8e267838dde1dee4bf83171b11 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Sun, 28 Apr 2024 23:00:58 +1000 Subject: [PATCH 25/59] Update thumbnail quality setting and selection - Add best available option - Selection will work correctly for both V3 and V1 responses --- .../kodion/settings/abstract_settings.py | 25 +++- .../youtube_plugin/youtube/client/youtube.py | 28 +---- .../lib/youtube_plugin/youtube/helper/tv.py | 2 +- .../youtube_plugin/youtube/helper/utils.py | 113 +++++++++++++++--- .../lib/youtube_plugin/youtube/helper/v3.py | 2 +- .../youtube/helper/video_info.py | 21 ++-- .../youtube/helper/yt_playlist.py | 6 +- resources/settings.xml | 7 +- 8 files changed, 139 insertions(+), 65 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index 20fb3368b..14b6ea699 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -158,10 +158,27 @@ def set_subtitle_selection(self, value): def set_subtitle_download(self, value): return self.set_bool(settings.SUBTITLE_DOWNLOAD, value) - def use_thumbnail_size(self): - size = self.get_int(settings.THUMB_SIZE, 0) - sizes = {0: 'medium', 1: 'high'} - return sizes[size] + _THUMB_SIZES = { + 0: { # Medium (16:9) + 'size': 320 * 180, + 'ratio': 320 / 180, + }, + 1: { # High (4:3) + 'size': 480 * 360, + 'ratio': 480 / 360, + }, + 2: { # Best available + 'size': 0, + 'ratio': 0, + }, + } + + def get_thumbnail_size(self): + default = 1 + value = self.get_int(settings.THUMB_SIZE, default) + if value in self._THUMB_SIZES: + return self._THUMB_SIZES[value] + return self._THUMB_SIZES[default] def safe_search(self): index = self.get_int(settings.SAFE_SEARCH, 0) diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 21e26abe0..a614494e4 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -14,7 +14,6 @@ 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 @@ -566,10 +565,7 @@ def get_recommended_for_home(self, ('title', 'runs', 0, 'text'), ('headline', 'simpleText'), )), - 'thumbnails': dict(zip( - ('default', 'high'), - video['thumbnail']['thumbnails'], - )), + 'thumbnails': video['thumbnail']['thumbnails'], 'channelId': self.json_traverse(video, ( ('longBylineText', 'shortBylineText'), 'runs', @@ -1190,7 +1186,6 @@ def get_related_videos(self, 'browseId' )) - thumb_getter = itemgetter(0, -1) if retry == 1: related_videos = chain.from_iterable(related_videos) @@ -1214,10 +1209,7 @@ def get_related_videos(self, ), ) )), - 'thumbnails': dict(zip( - ('default', 'high'), - thumb_getter(video['thumbnail']['thumbnails']), - )), + 'thumbnails': video['thumbnail']['thumbnails'], 'channelId': self.json_traverse(video, path=( ('longBylineText', 'shortBylineText'), 'runs', @@ -1637,20 +1629,8 @@ def _perform(_playlist_idx, _page_token, _offset, _result): 'channel': _item.get('shortBylineText', {}).get('runs', [{}])[0].get('text', ''), 'channel_id': _item.get('shortBylineText', {}).get('runs', [{}])[0] .get('navigationEndpoint', {}).get('browseEndpoint', {}).get('browseId', ''), - 'thumbnails': {'default': {'url': ''}, 'medium': {'url': ''}, 'high': {'url': ''}}} - - _thumbs = _item.get('thumbnail', {}).get('thumbnails', [{}]) - - for _thumb in _thumbs: - _thumb_url = _thumb.get('url', '') - if _thumb_url.startswith('//'): - _thumb_url = 'https:' + _thumb_url - if _thumb_url.endswith('/default.jpg'): - _video_item['thumbnails']['default']['url'] = _thumb_url - elif _thumb_url.endswith('/mqdefault.jpg'): - _video_item['thumbnails']['medium']['url'] = _thumb_url - elif _thumb_url.endswith('/hqdefault.jpg'): - _video_item['thumbnails']['high']['url'] = _thumb_url + 'thumbnails': (_item.get('thumbnail', {}) + .get('thumbnails', [{}]))} _result['items'].append(_video_item) diff --git a/resources/lib/youtube_plugin/youtube/helper/tv.py b/resources/lib/youtube_plugin/youtube/helper/tv.py index 88047702a..bc101beb7 100644 --- a/resources/lib/youtube_plugin/youtube/helper/tv.py +++ b/resources/lib/youtube_plugin/youtube/helper/tv.py @@ -134,7 +134,7 @@ def saved_playlists_to_items(provider, context, json_data): result = [] playlist_id_dict = {} - thumb_size = context.get_settings().use_thumbnail_size() + thumb_size = context.get_settings().get_thumbnail_size() incognito = context.get_param('incognito', False) item_params = {} if incognito: diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index a6d3a07b9..d4cf1d0b3 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -172,7 +172,7 @@ def update_channel_infos(provider, context, channel_id_dict, in_bookmarks_list = False in_subscription_list = False - thumb_size = settings.use_thumbnail_size + thumb_size = settings.get_thumbnail_size() banners = [ 'bannerTvMediumImageUrl', 'bannerTvLowImageUrl', @@ -268,7 +268,7 @@ def update_playlist_infos(provider, context, playlist_id_dict, custom_history_id = access_manager.get_watch_history_id() logged_in = provider.is_logged_in() path = context.get_path() - thumb_size = context.get_settings().use_thumbnail_size() + thumb_size = context.get_settings().get_thumbnail_size() # if the path directs to a playlist of our own, set channel id to 'mine' if path.startswith(paths.MY_PLAYLISTS): @@ -399,7 +399,7 @@ def update_video_infos(provider, context, video_id_dict, hide_shorts = settings.hide_short_videos() show_details = settings.show_detailed_description() subtitles_prompt = settings.get_subtitle_selection() == 1 - thumb_size = settings.use_thumbnail_size() + thumb_size = settings.get_thumbnail_size() thumb_stamp = get_thumb_timestamp() channel_role = localize(19029) @@ -800,8 +800,8 @@ def update_play_info(provider, context, video_id, video_item, video_stream, if meta_data: video_item.live = meta_data.get('status', {}).get('live', False) video_item.set_subtitles(meta_data.get('subtitles', None)) - image = get_thumbnail(settings.use_thumbnail_size(), - meta_data.get('images', {})) + image = get_thumbnail(settings.get_thumbnail_size(), + meta_data.get('thumbnails', {})) if image: if video_item.live: image = ''.join((image, '?ct=', get_thumb_timestamp())) @@ -852,21 +852,96 @@ def update_fanarts(provider, context, channel_items_dict, data=None): channel_item.set_fanart(fanart) +THUMB_TYPES = { + 'default': { + 'url': 'https://i.ytimg.com/vi/{0}/default{1}.jpg', + 'width': 120, + 'height': 90, + 'size': 120 * 90, + 'ratio': 120 / 90, # 4:3 + }, + 'medium': { + 'url': 'https://i.ytimg.com/vi/{0}/mqdefault{1}.jpg', + 'width': 320, + 'height': 180, + 'size': 320 * 180, + 'ratio': 320 / 180, # 16:9 + }, + 'high': { + 'url': 'https://i.ytimg.com/vi/{0}/hqdefault{1}.jpg', + 'width': 480, + 'height': 360, + 'size': 480 * 360, + 'ratio': 480 / 360, # 4:3 + }, + 'standard': { + 'url': 'https://i.ytimg.com/vi/{0}/sddefault{1}.jpg', + 'width': 640, + 'height': 480, + 'size': 640 * 480, + 'ratio': 640 / 480, # 4:3 + }, + '720': { + 'url': 'https://i.ytimg.com/vi/{0}/hq720{1}.jpg', + 'width': 1280, + 'height': 720, + 'size': 1280 * 720, + 'ratio': 1280 / 720, # 16:9 + }, + 'oar': { + 'url': 'https://i.ytimg.com/vi/{0}/oardefault{1}.jpg', + 'size': 0, + 'ratio': 0, + }, + 'maxres': { + 'url': 'https://i.ytimg.com/vi/{0}/maxresdefault{1}.jpg', + 'width': 1920, + 'height': 1080, + 'size': 1920 * 1080, + 'ratio': 1920 / 1080, # 16:9 + }, +} + + def get_thumbnail(thumb_size, thumbnails): - if thumb_size == 'high': - thumbnail_sizes = ['high', 'medium', 'default'] - else: - thumbnail_sizes = ['medium', 'high', 'default'] - - image = '' - for thumbnail_size in thumbnail_sizes: - try: - image = thumbnails.get(thumbnail_size, {}).get('url', '') - except AttributeError: - image = thumbnails.get(thumbnail_size, '') - if image: - break - return image + if not thumbnails: + return None + is_dict = isinstance(thumbnails, dict) + size_limit = thumb_size['size'] + ratio_limit = thumb_size['ratio'] + + def _sort_ratio_size(thumb): + if is_dict: + thumb_type, thumb = thumb + else: + thumb_type = None + + if 'size' in thumb: + size = thumb['size'] + ratio = thumb['ratio'] + elif 'width' in thumb: + width = thumb['width'] + height = thumb['height'] + size = width * height + ratio = width / height + elif thumb_type in THUMB_TYPES: + thumb = THUMB_TYPES[thumb_type] + size = thumb['size'] + ratio = thumb['ratio'] + else: + return False, False + return ( + ratio_limit and ratio_limit * 0.9 <= ratio <= ratio_limit * 1.1, + size <= size_limit and size if size_limit else size + ) + + thumbnail = sorted(thumbnails.items() if is_dict else thumbnails, + key=_sort_ratio_size, + reverse=True)[0] + url = (thumbnail[1] if is_dict else thumbnail).get('url') + if url and url.startswith('//'): + url = 'https:' + url + return url def get_shelf_index_by_title(context, json_data, shelf_title): diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index 733a0afbc..26425b949 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -49,7 +49,7 @@ def _process_list_response(provider, context, json_data): item_params['addon_id'] = addon_id settings = context.get_settings() - thumb_size = settings.use_thumbnail_size() + thumb_size = settings.get_thumbnail_size() use_play_data = not incognito and settings.use_local_history() for yt_item in yt_items: diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 76c21fae7..d8eda7c77 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -19,6 +19,7 @@ from .ratebypass import ratebypass from .signature.cipher import Cipher from .subtitles import Subtitles +from .utils import THUMB_TYPES from ..client.request_client import YouTubeRequestClient from ..youtube_exceptions import InvalidJSON, YouTubeException from ...kodion.compatibility import ( @@ -917,7 +918,7 @@ def _load_hls_manifest(self, url, live_type=None, meta_info=None, if meta_info is None: meta_info = {'video': {}, 'channel': {}, - 'images': {}, + 'thumbnails': {}, 'subtitles': []} if playback_stats is None: @@ -981,7 +982,7 @@ def _create_stream_list(self, if meta_info is None: meta_info = {'video': {}, 'channel': {}, - 'images': {}, + 'thumbnails': {}, 'subtitles': []} if playback_stats is None: playback_stats = {} @@ -1306,15 +1307,13 @@ def _get_video_info(self): .encode('raw_unicode_escape') .decode('raw_unicode_escape')), }, - 'images': { - 'high': ('https://i.ytimg.com/vi/{0}/hqdefault{1}.jpg' - .format(video_id, thumb_suffix)), - 'medium': ('https://i.ytimg.com/vi/{0}/mqdefault{1}.jpg' - .format(video_id, thumb_suffix)), - 'standard': ('https://i.ytimg.com/vi/{0}/sddefault{1}.jpg' - .format(video_id, thumb_suffix)), - 'default': ('https://i.ytimg.com/vi/{0}/default{1}.jpg' - .format(video_id, thumb_suffix)), + 'thumbnails': { + thumb_type: { + 'url': thumb['url'].format(video_id, thumb_suffix), + 'size': thumb['size'], + 'ratio': thumb['ratio'], + } + for thumb_type, thumb in THUMB_TYPES.items() }, 'subtitles': None, } diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py index eb22e0d18..8dc657abc 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py @@ -10,6 +10,7 @@ from __future__ import absolute_import, division, unicode_literals +from .utils import get_thumbnail from ...kodion import KodionException from ...kodion.utils import find_video_id @@ -177,6 +178,7 @@ def _process_select_playlist(provider, context): else: watch_later_id = None + thumb_size = context.get_settings().get_thumb_size() default_thumb = context.create_resource_path('media', 'playlist.png') while True: @@ -211,13 +213,13 @@ def _process_select_playlist(provider, context): snippet = playlist.get('snippet', {}) title = snippet.get('title', '') description = snippet.get('description', '') - thumbnail = snippet.get('thumbnails', {}).get('default', {}) + thumbnail = get_thumbnail(thumb_size, snippet.get('thumbnails', {})) playlist_id = playlist.get('id', '') if title and playlist_id: items.append(( title, description, playlist_id, - thumbnail.get('url') or default_thumb + thumbnail or default_thumb )) if page_token: diff --git a/resources/settings.xml b/resources/settings.xml index 2b1f77a92..07844febf 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -810,11 +810,12 @@ 0 - 1 + 1 - - + + + From 75e2bf3bcdd2a736a44664c97edf0a21c01fa1d9 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 29 Apr 2024 04:26:05 +1000 Subject: [PATCH 26/59] Add option to use video thumbnail as fanart #716 - Also added plugin url query parameter fanart_type to override settings - Can be used to set fanart for specific widgets only - Allowable values are the same as the setting: - 0: No fanart - 1: Default - 2: Channel - 3: Thumbnail --- .../resource.language.en_gb/strings.po | 2 +- .../kodion/constants/const_settings.py | 5 +++- .../kodion/context/abstract_context.py | 1 + .../kodion/items/xbmc/xbmc_items.py | 10 +++---- .../kodion/plugin/xbmc/xbmc_plugin.py | 4 +-- .../kodion/settings/abstract_settings.py | 9 ++++--- .../youtube/helper/resource_manager.py | 17 +++++++----- .../lib/youtube_plugin/youtube/helper/v3.py | 26 ++++++++++++++----- resources/settings.xml | 24 ++++++++--------- 9 files changed, 59 insertions(+), 39 deletions(-) diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 1acd905a7..6ba6eac82 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -318,7 +318,7 @@ msgid "Go to %s" msgstr "" msgctxt "#30503" -msgid "Show channel fanart" +msgid "Channel fanart" msgstr "" msgctxt "#30504" diff --git a/resources/lib/youtube_plugin/kodion/constants/const_settings.py b/resources/lib/youtube_plugin/kodion/constants/const_settings.py index 22913ee98..8b80d4a38 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_settings.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_settings.py @@ -61,7 +61,10 @@ LABEL_COLOR = 'youtube.view.label.color' # (string) THUMB_SIZE = 'kodion.thumbnail.size' # (int) -SHOW_FANART = 'kodion.fanart.show' # (bool) +THUMB_SIZE_BEST = 2 +FANART_SELECTION = 'kodion.fanart.selection' # (int) +FANART_CHANNEL = 2 +FANART_THUMBNAIL = 3 LANGUAGE = 'youtube.language' # (str) REGION = 'youtube.region' # (str) diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index a17c9e7a3..3ee03495a 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -48,6 +48,7 @@ class AbstractContext(object): 'strm', } _INT_PARAMS = { + 'fanart_type', 'live', 'next_page_token', 'offset', diff --git a/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py index a92e59af9..5a1a92b4b 100644 --- a/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py @@ -414,7 +414,7 @@ def video_playback_item(context, video_item, show_fanart=None, **_kwargs): return list_item if show_fanart is None: - show_fanart = settings.show_fanart() + show_fanart = settings.fanart_selection() image = video_item.get_image() list_item.setArt({ 'icon': image or 'DefaultVideo.png', @@ -448,7 +448,7 @@ def audio_listitem(context, audio_item, show_fanart=None, for_playback=False): list_item = xbmcgui.ListItem(**kwargs) if show_fanart is None: - show_fanart = context.get_settings().show_fanart() + show_fanart = context.get_settings().fanart_selection() image = audio_item.get_image() or 'DefaultAudio.png' list_item.setArt({ 'icon': image, @@ -496,7 +496,7 @@ def directory_listitem(context, directory_item, show_fanart=None): props['specialSort'] = 'top' if show_fanart is None: - show_fanart = context.get_settings().show_fanart() + show_fanart = context.get_settings().fanart_selection() image = directory_item.get_image() or 'DefaultFolder.png' list_item.setArt({ 'icon': image, @@ -541,7 +541,7 @@ def image_listitem(context, image_item, show_fanart=None): list_item = xbmcgui.ListItem(**kwargs) if show_fanart is None: - show_fanart = context.get_settings().show_fanart() + show_fanart = context.get_settings().fanart_selection() image = image_item.get_image() or 'DefaultPicture.png' list_item.setArt({ 'icon': image, @@ -629,7 +629,7 @@ def video_listitem(context, video_item, show_fanart=None): props['playlist_item_id'] = prop_value if show_fanart is None: - show_fanart = context.get_settings().show_fanart() + show_fanart = context.get_settings().fanart_selection() image = video_item.get_image() list_item.setArt({ 'icon': image or 'DefaultVideo.png', diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py index f94a06aeb..b84b5f7fe 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py @@ -135,7 +135,7 @@ def run(self, provider, context): item_count = 0 if isinstance(result, (list, tuple)): - show_fanart = settings.show_fanart() + show_fanart = settings.fanart_selection() result = [ self._LIST_ITEM_MAP[item.__class__.__name__]( context, item, show_fanart=show_fanart @@ -181,7 +181,7 @@ def _set_resolved_url(self, context, base_item): item = self._PLAY_ITEM_MAP[base_item.__class__.__name__]( context, base_item, - show_fanart=context.get_settings().show_fanart(), + show_fanart=context.get_settings().fanart_selection(), for_playback=True, ) xbmcplugin.setResolvedUrl(self.handle, diff --git a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index 14b6ea699..94017f517 100644 --- a/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py @@ -83,8 +83,8 @@ def ask_for_video_quality(self): return (self.get_bool(settings.VIDEO_QUALITY_ASK, False) or self.get_int(settings.MPD_STREAM_SELECT) == 4) - def show_fanart(self): - return self.get_bool(settings.SHOW_FANART, True) + def fanart_selection(self): + return self.get_int(settings.FANART_SELECTION, 2) def cache_size(self, value=None): if value is not None: @@ -173,9 +173,10 @@ def set_subtitle_download(self, value): }, } - def get_thumbnail_size(self): + def get_thumbnail_size(self, value=None): default = 1 - value = self.get_int(settings.THUMB_SIZE, default) + if value is None: + value = self.get_int(settings.THUMB_SIZE, default) if value in self._THUMB_SIZES: return self._THUMB_SIZES[value] return self._THUMB_SIZES[default] diff --git a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py index 05ac7f873..954804042 100644 --- a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py +++ b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py @@ -17,9 +17,10 @@ def __init__(self, context, client): self._client = client self._data_cache = context.get_data_cache() self._function_cache = context.get_function_cache() - self._show_fanart = context.get_settings().get_bool( - 'youtube.channel.fanart.show', True - ) + fanart_type = context.get_param('fanart_type') + if fanart_type is None: + fanart_type = context.get_settings().fanart_selection() + self._fanart_type = fanart_type self.new_data = {} @staticmethod @@ -99,12 +100,16 @@ def get_channels(self, ids, defer_cache=False): return result def get_fanarts(self, channel_ids, defer_cache=False): - if not self._show_fanart: + if self._fanart_type != self._context.get_settings().FANART_CHANNEL: return {} result = self.get_channels(channel_ids, defer_cache=defer_cache) - banners = ['bannerTvMediumImageUrl', 'bannerTvLowImageUrl', - 'bannerTvImageUrl', 'bannerExternalUrl'] + banners = ( + 'bannerTvMediumImageUrl', + 'bannerTvLowImageUrl', + 'bannerTvImageUrl', + 'bannerExternalUrl', + ) # transform for key, item in result.items(): images = item.get('brandingSettings', {}).get('image', {}) diff --git a/resources/lib/youtube_plugin/youtube/helper/v3.py b/resources/lib/youtube_plugin/youtube/helper/v3.py index 26425b949..22a92e827 100644 --- a/resources/lib/youtube_plugin/youtube/helper/v3.py +++ b/resources/lib/youtube_plugin/youtube/helper/v3.py @@ -41,17 +41,26 @@ def _process_list_response(provider, context, json_data): result = [] item_params = {} - incognito = context.get_param('incognito', False) + params = context.get_params() + incognito = params.get('incognito', False) if incognito: item_params['incognito'] = incognito - addon_id = context.get_param('addon_id', '') + addon_id = params.get('addon_id', '') if addon_id: item_params['addon_id'] = addon_id settings = context.get_settings() - thumb_size = settings.get_thumbnail_size() use_play_data = not incognito and settings.use_local_history() + thumb_size = settings.get_thumbnail_size() + fanart_type = params.get('fanart_type') + if fanart_type is None: + fanart_type = settings.fanart_selection() + if fanart_type == settings.FANART_THUMBNAIL: + fanart_type = settings.get_thumbnail_size(settings.THUMB_SIZE_BEST) + else: + fanart_type = False + for yt_item in yt_items: is_youtube, kind = _parse_kind(yt_item) if not is_youtube or not kind: @@ -61,7 +70,10 @@ def _process_list_response(provider, context, json_data): item_id = yt_item.get('id') snippet = yt_item.get('snippet', {}) title = snippet.get('title', context.localize('untitled')) - image = get_thumbnail(thumb_size, snippet.get('thumbnails', {})) + + thumbnails = snippet.get('thumbnails', {}) + image = get_thumbnail(thumb_size, thumbnails) + fanart = get_thumbnail(fanart_type, thumbnails) if fanart_type else None if kind == 'searchresult': _, kind = _parse_kind(item_id) @@ -77,7 +89,7 @@ def _process_list_response(provider, context, json_data): ('play',), dict(item_params, video_id=item_id), ) - item = VideoItem(title, item_uri, image=image) + item = VideoItem(title, item_uri, image=image, fanart=fanart) video_id_dict[item_id] = item elif kind == 'channel': @@ -141,7 +153,7 @@ def _process_list_response(provider, context, json_data): ('play',), dict(item_params, video_id=item_id), ) - item = VideoItem(title, item_uri, image=image) + item = VideoItem(title, item_uri, image=image, fanart=fanart) video_id_dict[item_id] = item elif kind == 'activity': @@ -158,7 +170,7 @@ def _process_list_response(provider, context, json_data): ('play',), dict(item_params, video_id=item_id), ) - item = VideoItem(title, item_uri, image=image) + item = VideoItem(title, item_uri, image=image, fanart=fanart) video_id_dict[item_id] = item elif kind == 'commentthread': diff --git a/resources/settings.xml b/resources/settings.xml index 07844febf..07f5385e1 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -820,20 +820,18 @@ - + 0 - true - - - - 0 - true - - - true - - - + 2 + + + + + + + + + From e21c32106d227f90649bbd9db8b91f7071aaea13 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 29 Apr 2024 09:41:57 +1000 Subject: [PATCH 27/59] Only enable MPEG-DASH for live streams in Kodi v21+ - Follow up to 71cc472 --- .../lib/youtube_plugin/youtube/helper/yt_setup_wizard.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 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 63831e944..aea42bc4f 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py @@ -16,8 +16,8 @@ from ...kodion.constants import ADDON_ID, DATA_PATH, WAIT_FLAG from ...kodion.network import Locator, httpd_status from ...kodion.sql_store import PlaybackHistory, SearchHistory +from ...kodion.utils import current_system_version, to_unicode from ...kodion.utils.datetime_parser import strptime -from ...kodion.utils.methods import to_unicode DEFAULT_LANGUAGES = {'items': [ @@ -322,7 +322,10 @@ def process_default_settings(_provider, context, step, steps): settings.use_mpd_videos(True) settings.stream_select(4 if settings.ask_for_video_quality() else 3) settings.set_subtitle_download(False) - settings.live_stream_type(3) + if current_system_version.compatible(21, 0): + settings.live_stream_type(3) + else: + settings.live_stream_type(2) if not xbmcvfs.exists('special://profile/playercorefactory.xml'): settings.default_player_web_urls(False) if settings.cache_size() < 20: From a42f9caf71c2987caf4f3af772ceb5446b8f848a Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 30 Apr 2024 10:31:01 +1000 Subject: [PATCH 28/59] Fix JSONRPC local notifications not working --- .../lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py | 3 +-- 1 file changed, 1 insertion(+), 2 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 8fbc11783..9aeddfec6 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -586,8 +586,7 @@ def send_notification(self, method, data): jsonrpc(method='JSONRPC.NotifyAll', params={'sender': ADDON_ID, 'message': method, - 'data': data}, - no_response=True) + 'data': data}) def use_inputstream_adaptive(self): if self._settings.use_isa(): From 56ee84addf1ccb4a5646dacf8b7db52b5305dcaf Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 30 Apr 2024 11:10:17 +1000 Subject: [PATCH 29/59] Use constants for window property and notification event names --- resources/lib/youtube_plugin/kodion/abstract_provider.py | 8 ++++---- resources/lib/youtube_plugin/kodion/constants/__init__.py | 8 ++++++-- .../youtube_plugin/kodion/context/xbmc/xbmc_context.py | 4 ++-- .../lib/youtube_plugin/kodion/monitors/service_monitor.py | 6 +++--- .../lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py | 6 +++--- resources/lib/youtube_plugin/kodion/service_runner.py | 7 ++++--- 6 files changed, 22 insertions(+), 17 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index 90900bc6f..4328f2b83 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -12,7 +12,7 @@ import re -from .constants import content, paths, ROUTE_FLAG +from .constants import CHECK_SETTINGS, REROUTE, content, paths from .exceptions import KodionException from .items import ( DirectoryItem, @@ -106,7 +106,7 @@ def run_wizard(self, context): settings = context.get_settings() ui = context.get_ui() - context.send_notification('check_settings', 'defer') + context.send_notification(CHECK_SETTINGS, 'defer') wizard_steps = self.get_wizard_steps(context) @@ -126,7 +126,7 @@ def run_wizard(self, context): step += 1 finally: settings.setup_wizard_enabled(False) - context.send_notification('check_settings', 'process') + context.send_notification(CHECK_SETTINGS, 'process') def get_wizard_steps(self, context): # can be overridden by the derived class @@ -226,7 +226,7 @@ def reroute(self, context, re_match=None, path=None, params=None): finally: if not result: return False - context.get_ui().set_property(ROUTE_FLAG, path) + context.get_ui().set_property(REROUTE, path) context.execute('ActivateWindow(Videos, {0}, return)'.format( context.create_uri(path, params) )) diff --git a/resources/lib/youtube_plugin/kodion/constants/__init__.py b/resources/lib/youtube_plugin/kodion/constants/__init__.py index f63107730..5a60e4483 100644 --- a/resources/lib/youtube_plugin/kodion/constants/__init__.py +++ b/resources/lib/youtube_plugin/kodion/constants/__init__.py @@ -32,21 +32,25 @@ 'true': True, } +ABORT_FLAG = 'abort_requested' BUSY_FLAG = 'busy' +CHECK_SETTINGS = 'check_settings' PLAYLIST_POSITION = 'playlist_position' -ROUTE_FLAG = 'route' +REROUTE = 'reroute' SWITCH_PLAYER_FLAG = 'switch_player' WAIT_FLAG = 'builtin_running' __all__ = ( + 'ABORT_FLAG', 'ADDON_ID', 'ADDON_PATH', 'BUSY_FLAG', + 'CHECK_SETTINGS', 'DATA_PATH', 'MEDIA_PATH', 'PLAYLIST_POSITION', 'RESOURCE_PATH', - 'ROUTE_FLAG', + 'REROUTE', 'SWITCH_PLAYER_FLAG', 'TEMP_PATH', 'VALUE_FROM_STR', 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 9aeddfec6..e9a497e9b 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -22,7 +22,7 @@ xbmcaddon, xbmcplugin, ) -from ...constants import ADDON_ID, content, sort +from ...constants import ABORT_FLAG, ADDON_ID, content, sort from ...player import XbmcPlayer, XbmcPlaylist from ...settings import XbmcPluginSettings from ...ui import XbmcContextUI @@ -661,7 +661,7 @@ def inputstream_adaptive_auto_stream_selection(): return False def abort_requested(self): - return self.get_ui().get_property('abort_requested').lower() == 'true' + return self.get_ui().get_property(ABORT_FLAG).lower() == 'true' @staticmethod def get_infobool(name): diff --git a/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py b/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py index bd0817701..5b8ada925 100644 --- a/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py +++ b/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py @@ -13,7 +13,7 @@ import threading from ..compatibility import xbmc, xbmcaddon -from ..constants import ADDON_ID +from ..constants import ADDON_ID, CHECK_SETTINGS from ..logger import log_debug from ..network import get_connect_address, get_http_server, httpd_status from ..settings import XbmcPluginSettings @@ -45,8 +45,8 @@ def __init__(self): def onNotification(self, sender, method, data): if sender != ADDON_ID: return - - if method.endswith('.check_settings'): + group, separator, event = method.partition('.') + if event == CHECK_SETTINGS: if not isinstance(data, dict): data = json.loads(data) log_debug('onNotification: |check_settings| -> |{data}|' diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py index b84b5f7fe..dbcbdbcfe 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py @@ -14,7 +14,7 @@ from ..abstract_plugin import AbstractPlugin from ...compatibility import xbmcplugin -from ...constants import BUSY_FLAG, PLAYLIST_POSITION, ROUTE_FLAG +from ...constants import BUSY_FLAG, PLAYLIST_POSITION, REROUTE from ...exceptions import KodionException from ...items import ( DirectoryItem, @@ -112,7 +112,7 @@ def run(self, provider, context): provider.run_wizard(context) try: - route = ui.get_property(ROUTE_FLAG) + route = ui.get_property(REROUTE) if route: function_cache = context.get_function_cache() result, options = function_cache.run( @@ -122,7 +122,7 @@ def run(self, provider, context): _oneshot=True, context=context.clone(route), ) - ui.clear_property(ROUTE_FLAG) + ui.clear_property(REROUTE) else: result, options = provider.navigate(context) except KodionException as exc: diff --git a/resources/lib/youtube_plugin/kodion/service_runner.py b/resources/lib/youtube_plugin/kodion/service_runner.py index 447be67a8..4a8d2653b 100644 --- a/resources/lib/youtube_plugin/kodion/service_runner.py +++ b/resources/lib/youtube_plugin/kodion/service_runner.py @@ -10,7 +10,7 @@ from __future__ import absolute_import, division, unicode_literals -from .constants import ADDON_ID, TEMP_PATH +from .constants import ABORT_FLAG, ADDON_ID, TEMP_PATH from .context import XbmcContext from .monitors import PlayerMonitor, ServiceMonitor from .utils import rm_dir @@ -23,7 +23,8 @@ def run(): context = XbmcContext() context.log_debug('YouTube service initialization...') - context.get_ui().clear_property('abort_requested') + ui = context.get_ui() + ui.clear_property(ABORT_FLAG) monitor = ServiceMonitor() player = PlayerMonitor(provider=Provider(), @@ -64,7 +65,7 @@ def run(): break waited += wait_interval - context.get_ui().set_property('abort_requested', 'true') + ui.set_property(ABORT_FLAG, 'true') # clean up any/all playback monitoring threads player.cleanup_threads(only_ended=False) From 8ac77074a534f90a11de16e39d340273a6431565 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 30 Apr 2024 11:18:52 +1000 Subject: [PATCH 30/59] Improve idle shutdown of http server - Don't try to restart automatically once manually shutdown except - if not idle - wake up notification is received - Fix possible issue where remote commands are received while Kodi thinks it (the GUI) is idle --- .../youtube_plugin/kodion/constants/__init__.py | 4 ++++ .../kodion/context/abstract_context.py | 3 +++ .../kodion/context/xbmc/xbmc_context.py | 6 +++++- .../kodion/monitors/service_monitor.py | 5 ++++- .../kodion/plugin/xbmc/xbmc_plugin.py | 6 +++++- .../lib/youtube_plugin/kodion/service_runner.py | 14 +++++++++----- 6 files changed, 30 insertions(+), 8 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/constants/__init__.py b/resources/lib/youtube_plugin/kodion/constants/__init__.py index 5a60e4483..9dd42ad9c 100644 --- a/resources/lib/youtube_plugin/kodion/constants/__init__.py +++ b/resources/lib/youtube_plugin/kodion/constants/__init__.py @@ -37,8 +37,10 @@ CHECK_SETTINGS = 'check_settings' PLAYLIST_POSITION = 'playlist_position' REROUTE = 'reroute' +SLEEPING = 'sleeping' SWITCH_PLAYER_FLAG = 'switch_player' WAIT_FLAG = 'builtin_running' +WAKEUP = 'wakeup' __all__ = ( 'ABORT_FLAG', @@ -51,10 +53,12 @@ 'PLAYLIST_POSITION', 'RESOURCE_PATH', 'REROUTE', + 'SLEEPING', 'SWITCH_PLAYER_FLAG', 'TEMP_PATH', 'VALUE_FROM_STR', 'WAIT_FLAG', + 'WAKEUP', 'content', 'paths', 'settings', diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index 3ee03495a..86abf3af0 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -431,3 +431,6 @@ def get_listitem_detail(detail_name, attr=False): def tear_down(self): pass + + def wakeup(self): + 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 e9a497e9b..9523c681a 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -22,7 +22,7 @@ xbmcaddon, xbmcplugin, ) -from ...constants import ABORT_FLAG, ADDON_ID, content, sort +from ...constants import ABORT_FLAG, ADDON_ID, WAKEUP, content, sort from ...player import XbmcPlayer, XbmcPlaylist from ...settings import XbmcPluginSettings from ...ui import XbmcContextUI @@ -685,3 +685,7 @@ def tear_down(self): del self._addon except AttributeError: pass + + def wakeup(self): + self.get_ui().set_property(WAKEUP, 'true') + self.send_notification(WAKEUP, True) diff --git a/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py b/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py index 5b8ada925..eabb98f26 100644 --- a/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py +++ b/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py @@ -13,7 +13,7 @@ import threading from ..compatibility import xbmc, xbmcaddon -from ..constants import ADDON_ID, CHECK_SETTINGS +from ..constants import ADDON_ID, CHECK_SETTINGS, WAKEUP from ..logger import log_debug from ..network import get_connect_address, get_http_server, httpd_status from ..settings import XbmcPluginSettings @@ -60,6 +60,9 @@ def onNotification(self, sender, method, data): self.onSettingsChanged() self._settings_state = None return + elif event == WAKEUP: + if not self.httpd and self.httpd_required(): + self.start_httpd() else: log_debug('onNotification: |unhandled method| -> |{method}|' .format(method=method)) diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py index dbcbdbcfe..941edc47d 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py @@ -14,7 +14,7 @@ from ..abstract_plugin import AbstractPlugin from ...compatibility import xbmcplugin -from ...constants import BUSY_FLAG, PLAYLIST_POSITION, REROUTE +from ...constants import BUSY_FLAG, PLAYLIST_POSITION, REROUTE, SLEEPING from ...exceptions import KodionException from ...items import ( DirectoryItem, @@ -108,6 +108,10 @@ def run(self, provider, context): ui.clear_property(BUSY_FLAG) ui.clear_property(PLAYLIST_POSITION) + if ui.get_property(SLEEPING): + context.wakeup() + ui.clear_property(SLEEPING) + if settings.setup_wizard_enabled(): provider.run_wizard(context) diff --git a/resources/lib/youtube_plugin/kodion/service_runner.py b/resources/lib/youtube_plugin/kodion/service_runner.py index 4a8d2653b..eea39fe9e 100644 --- a/resources/lib/youtube_plugin/kodion/service_runner.py +++ b/resources/lib/youtube_plugin/kodion/service_runner.py @@ -10,7 +10,7 @@ from __future__ import absolute_import, division, unicode_literals -from .constants import ABORT_FLAG, ADDON_ID, TEMP_PATH +from .constants import ABORT_FLAG, ADDON_ID, SLEEPING, TEMP_PATH, WAKEUP from .context import XbmcContext from .monitors import PlayerMonitor, ServiceMonitor from .utils import rm_dir @@ -34,7 +34,6 @@ def run(): # wipe add-on temp folder on updates/restarts (subtitles, and mpd files) rm_dir(TEMP_PATH) - wait_interval = 10 ping_period = waited = 60 restart_attempts = 0 plugin_url = 'plugin://{0}/'.format(ADDON_ID) @@ -43,8 +42,14 @@ def run(): if (monitor.httpd_required() and not context.get_infobool('System.IdleTime(10)')): monitor.start_httpd() - elif context.get_infobool('System.IdleTime(30)'): - monitor.shutdown_httpd() + waited = 0 + elif context.get_infobool('System.IdleTime(10)'): + if ui.get_property(WAKEUP): + ui.clear_property(WAKEUP) + waited = 0 + if waited >= 30: + monitor.shutdown_httpd() + ui.set_property(SLEEPING, 'true') elif waited >= ping_period: waited = 0 if monitor.ping_httpd(): @@ -54,7 +59,6 @@ def run(): restart_attempts += 1 else: monitor.shutdown_httpd() - restart_attempts = 0 if context.get_infolabel('Container.FolderPath').startswith(plugin_url): wait_interval = 1 From 723d183fb12bdd02ce383c2898dcb81632620d74 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 30 Apr 2024 11:19:33 +1000 Subject: [PATCH 31/59] Don't try to ping the http server if we already know it is not running --- .../lib/youtube_plugin/kodion/monitors/service_monitor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py b/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py index eabb98f26..88de8cc64 100644 --- a/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py +++ b/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py @@ -168,9 +168,8 @@ def restart_httpd(self): self.shutdown_httpd() self.start_httpd() - @staticmethod - def ping_httpd(): - return httpd_status() + def ping_httpd(self): + return self.httpd and httpd_status() def httpd_required(self): return self._use_httpd From 505c0f64ffeb3e535a3121bf96f88c03d4cb11cc Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 30 Apr 2024 11:20:49 +1000 Subject: [PATCH 32/59] Misc tidy ups --- .../kodion/abstract_provider.py | 2 +- .../kodion/items/xbmc/xbmc_items.py | 24 +++++++++---------- .../youtube_plugin/kodion/network/requests.py | 2 +- .../youtube_plugin/kodion/script_actions.py | 2 +- .../youtube_plugin/kodion/utils/methods.py | 2 +- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index 4328f2b83..ba94433d3 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -16,8 +16,8 @@ from .exceptions import KodionException from .items import ( DirectoryItem, - NextPageItem, NewSearchItem, + NextPageItem, SearchHistoryItem, ) from .utils import to_unicode diff --git a/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py index 5a1a92b4b..8411ead6c 100644 --- a/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py @@ -184,6 +184,10 @@ def set_info(list_item, item, properties): is_video = True info_tag = list_item.getVideoInfoTag() + value = item.get_aired(as_info_label=True) + if value is not None: + info_tag.setFirstAired(value) + value = item.get_dateadded(as_info_label=True) if value is not None: info_tag.setDateAdded(value) @@ -192,22 +196,10 @@ def set_info(list_item, item, properties): if value is not None: info_tag.setLastPlayed(value) - value = item.get_aired(as_info_label=True) - if value is not None: - info_tag.setFirstAired(value) - value = item.get_premiered(as_info_label=True) if value is not None: info_tag.setPremiered(value) - # count: int - # eg. 12 - # Can be used to store an id for later, or for sorting purposes - # Used for Youtube video view count - value = item.get_count() - if value is not None: - list_item.setInfo('video', {'count': value}) - # cast: list[xbmc.Actor] # From list[{member: str, role: str, order: int, thumbnail: str}] # Used as alias for channel name if enabled @@ -223,6 +215,14 @@ def set_info(list_item, item, properties): if value is not None: info_tag.setProductionCode(value) + # count: int + # eg. 12 + # Can be used to store an id for later, or for sorting purposes + # Used for Youtube video view count + value = item.get_count() + if value is not None: + list_item.setInfo('video', {'count': value}) + # director: list[str] # eg. "Steven Spielberg" # Currently unused diff --git a/resources/lib/youtube_plugin/kodion/network/requests.py b/resources/lib/youtube_plugin/kodion/network/requests.py index 7b5c14e25..4241edf10 100644 --- a/resources/lib/youtube_plugin/kodion/network/requests.py +++ b/resources/lib/youtube_plugin/kodion/network/requests.py @@ -27,7 +27,7 @@ 'InvalidJSONError' ) -_settings = XbmcPluginSettings(xbmcaddon.Addon(id=ADDON_ID)) +_settings = XbmcPluginSettings(xbmcaddon.Addon(ADDON_ID)) class BaseRequestsClass(object): diff --git a/resources/lib/youtube_plugin/kodion/script_actions.py b/resources/lib/youtube_plugin/kodion/script_actions.py index c23e5a416..af32894cb 100644 --- a/resources/lib/youtube_plugin/kodion/script_actions.py +++ b/resources/lib/youtube_plugin/kodion/script_actions.py @@ -29,7 +29,7 @@ def _config_actions(context, action, *_args): elif action == 'isa': if context.use_inputstream_adaptive(): - xbmcaddon.Addon(id='inputstream.adaptive').openSettings() + xbmcaddon.Addon('inputstream.adaptive').openSettings() else: settings.use_isa(False) diff --git a/resources/lib/youtube_plugin/kodion/utils/methods.py b/resources/lib/youtube_plugin/kodion/utils/methods.py index 95da428a0..556ade575 100644 --- a/resources/lib/youtube_plugin/kodion/utils/methods.py +++ b/resources/lib/youtube_plugin/kodion/utils/methods.py @@ -341,7 +341,7 @@ def jsonrpc(batch=None, **kwargs): return None do_response = False - for request_id, kwargs in enumerate(batch or (kwargs, )): + for request_id, kwargs in enumerate(batch or (kwargs,)): do_response = (not kwargs.pop('no_response', False)) or do_response if do_response and 'id' not in kwargs: kwargs['id'] = request_id From 4545a04cda949cef4cc392c5aa19966d905e7887 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 30 Apr 2024 15:25:34 +1000 Subject: [PATCH 33/59] Further fixes for busy dialog crash workaround --- .../kodion/constants/__init__.py | 2 ++ .../kodion/player/xbmc/xbmc_playlist.py | 4 +-- .../kodion/plugin/xbmc/xbmc_plugin.py | 34 ++++++++++++++----- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/constants/__init__.py b/resources/lib/youtube_plugin/kodion/constants/__init__.py index 9dd42ad9c..58bdc8a5f 100644 --- a/resources/lib/youtube_plugin/kodion/constants/__init__.py +++ b/resources/lib/youtube_plugin/kodion/constants/__init__.py @@ -35,6 +35,7 @@ ABORT_FLAG = 'abort_requested' BUSY_FLAG = 'busy' CHECK_SETTINGS = 'check_settings' +PLAYLIST_PATH = 'playlist_path' PLAYLIST_POSITION = 'playlist_position' REROUTE = 'reroute' SLEEPING = 'sleeping' @@ -50,6 +51,7 @@ 'CHECK_SETTINGS', 'DATA_PATH', 'MEDIA_PATH', + 'PLAYLIST_PATH', 'PLAYLIST_POSITION', 'RESOURCE_PATH', 'REROUTE', diff --git a/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist.py b/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist.py index f35d7369e..5b8ee0ea6 100644 --- a/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist.py +++ b/resources/lib/youtube_plugin/kodion/player/xbmc/xbmc_playlist.py @@ -133,7 +133,7 @@ def get_items(self, properties=None, dumps=False): self._context.log_error('XbmcPlaylist.get_items error - |{0}: {1}|' .format(error.get('code', 'unknown'), error.get('message', 'unknown'))) - return '[]' if dumps else [] + return '' if dumps else [] def add_items(self, items, loads=False): if loads: @@ -206,7 +206,7 @@ def get_position(self, offset=0): position += (offset + 1) # A playlist with only one element has no next item - if playlist_size > 1 and position <= playlist_size: + if playlist_size >= 1 and position <= playlist_size: self._context.log_debug('playlistid: {0}, position - {1}/{2}' .format(playlistid, position, diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py index 941edc47d..1b0ae43db 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py @@ -14,7 +14,13 @@ from ..abstract_plugin import AbstractPlugin from ...compatibility import xbmcplugin -from ...constants import BUSY_FLAG, PLAYLIST_POSITION, REROUTE, SLEEPING +from ...constants import ( + BUSY_FLAG, + PLAYLIST_PATH, + PLAYLIST_POSITION, + REROUTE, + SLEEPING, +) from ...exceptions import KodionException from ...items import ( DirectoryItem, @@ -66,12 +72,23 @@ def run(self, provider, context): playlist = XbmcPlaylist('auto', context, retry=3) position, remaining = playlist.get_position() - items = playlist.get_items() if remaining else None + items = playlist.get_items() playlist.clear() context.log_warning('Multiple busy dialogs active - ' 'playlist cleared to avoid Kodi crash') + if position and items: + path = items[position - 1]['file'] + old_path = ui.get_property(PLAYLIST_PATH) + old_position = ui.get_property(PLAYLIST_POSITION) + if (old_position and position == int(old_position) + and old_path and path == old_path): + if remaining: + position += 1 + else: + items = None + if items: max_wait_time = 30 while ui.busy_dialog_active(): @@ -84,12 +101,8 @@ def run(self, provider, context): context.log_warning('Multiple busy dialogs active - ' 'reloading playlist') - num_items = playlist.add_items(items) - - old_position = ui.get_property(PLAYLIST_POSITION) - if old_position and position == int(old_position): - position += 1 + num_items = playlist.add_items(items) max_wait_time = min(position, num_items) while ui.busy_dialog_active() or playlist.size() < position: max_wait_time -= 1 @@ -102,10 +115,12 @@ def run(self, provider, context): playlist.play_playlist_item(position) ui.clear_property(BUSY_FLAG) + ui.clear_property(PLAYLIST_PATH) ui.clear_property(PLAYLIST_POSITION) return False ui.clear_property(BUSY_FLAG) + ui.clear_property(PLAYLIST_PATH) ui.clear_property(PLAYLIST_POSITION) if ui.get_property(SLEEPING): @@ -180,7 +195,10 @@ def _set_resolved_url(self, context, base_item): ui.set_property(BUSY_FLAG, 'true') playlist = XbmcPlaylist('auto', context) position, _ = playlist.get_position() - ui.set_property(PLAYLIST_POSITION, str(position)) + items = playlist.get_items() + if position and items: + ui.set_property(PLAYLIST_PATH, items[position - 1]['file']) + ui.set_property(PLAYLIST_POSITION, str(position)) item = self._PLAY_ITEM_MAP[base_item.__class__.__name__]( context, From 5b3ef25290e4cc423f5840913e507107df65cf00 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 30 Apr 2024 19:15:32 +1000 Subject: [PATCH 34/59] Workaround for new settings interface not updating correctly --- .../kodion/context/abstract_context.py | 2 +- .../kodion/context/xbmc/xbmc_context.py | 29 +++++++++++++++---- .../kodion/monitors/service_monitor.py | 10 +++++-- .../kodion/plugin/xbmc/xbmc_plugin.py | 8 ++++- .../youtube/helper/yt_setup_wizard.py | 1 + 5 files changed, 39 insertions(+), 11 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index 86abf3af0..f411ccc62 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -376,7 +376,7 @@ def get_id(self): def get_handle(self): raise NotImplementedError() - def get_settings(self): + def get_settings(self, flush=False): raise NotImplementedError() def localize(self, text_id, default_text=None): 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 9523c681a..9a285aae0 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -39,6 +39,7 @@ class XbmcContext(AbstractContext): _addon = None + _settings = None _KODI_UI_SUBTITLE_OPTIONS = None @@ -276,7 +277,8 @@ class XbmcContext(AbstractContext): def __new__(cls, *args, **kwargs): if not cls._addon: - cls._addon = xbmcaddon.Addon(id=ADDON_ID) + cls._addon = xbmcaddon.Addon(ADDON_ID) + cls._settings = XbmcPluginSettings(cls._addon) if not cls._KODI_UI_SUBTITLE_OPTIONS: cls._KODI_UI_SUBTITLE_OPTIONS = { @@ -298,8 +300,10 @@ def __init__(self, override=True): super(XbmcContext, self).__init__(path, params, plugin_name, plugin_id) - if plugin_id and plugin_id != ADDON_ID: - self._addon = xbmcaddon.Addon(id=plugin_id) + self._plugin_id = plugin_id or ADDON_ID + if self._plugin_id != ADDON_ID: + self._addon = xbmcaddon.Addon(self._plugin_id) + self._settings = XbmcPluginSettings(self._addon) """ I don't know what xbmc/kodi is doing with a simple uri, but we have to extract the information from the @@ -338,12 +342,10 @@ def __init__(self, self._video_player = None self._audio_player = None self._plugin_handle = int(sys.argv[1]) if is_plugin_invocation else -1 - self._plugin_id = plugin_id or ADDON_ID self._plugin_name = plugin_name or self._addon.getAddonInfo('name') self._version = self._addon.getAddonInfo('version') self._addon_path = make_dirs(self._addon.getAddonInfo('path')) self._data_path = make_dirs(self._addon.getAddonInfo('profile')) - self._settings = XbmcPluginSettings(self._addon) def get_region(self): pass # implement from abstract @@ -432,7 +434,14 @@ def get_data_path(self): def get_addon_path(self): return self._addon_path - def get_settings(self): + def get_settings(self, flush=False): + if flush or not self._settings: + if self._plugin_id != ADDON_ID: + self._addon = xbmcaddon.Addon(self._plugin_id) + self._settings = XbmcPluginSettings(self._addon) + else: + self.__class__._addon = xbmcaddon.Addon(ADDON_ID) + self.__class__._settings = XbmcPluginSettings(self._addon) return self._settings @classmethod @@ -683,6 +692,14 @@ def tear_down(self): self._settings.flush() try: del self._addon + del self._settings + except AttributeError: + pass + try: + del self.__class__._addon + self.__class__._addon = None + del self.__class__._settings + self.__class__._settings = None except AttributeError: pass diff --git a/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py b/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py index 88de8cc64..c41b2a1eb 100644 --- a/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py +++ b/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py @@ -12,7 +12,7 @@ import json import threading -from ..compatibility import xbmc, xbmcaddon +from ..compatibility import xbmc, xbmcaddon, xbmcgui from ..constants import ADDON_ID, CHECK_SETTINGS, WAKEUP from ..logger import log_debug from ..network import get_connect_address, get_http_server, httpd_status @@ -76,12 +76,16 @@ def onSettingsChanged(self): self.waitForAbort(1) if changes != self._settings_changes: return - if changes > 1: - log_debug('onSettingsChanged: {0} changes'.format(changes)) + log_debug('onSettingsChanged: {0} change(s)'.format(changes)) self._settings_changes = 0 settings = self._settings settings.flush(xbmcaddon.Addon(ADDON_ID)) + + xbmcgui.Window(10000).setProperty( + '-'.join((ADDON_ID, CHECK_SETTINGS)), 'true' + ) + if (not xbmc.getCondVisibility('Container.IsUpdating') and not xbmc.getCondVisibility('System.HasActiveModalDialog') and xbmc.getInfoLabel('Container.FolderPath').startswith( diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py index 1b0ae43db..f7e833af5 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py @@ -16,6 +16,7 @@ from ...compatibility import xbmcplugin from ...constants import ( BUSY_FLAG, + CHECK_SETTINGS, PLAYLIST_PATH, PLAYLIST_POSITION, REROUTE, @@ -59,7 +60,6 @@ def __init__(self): def run(self, provider, context): self.handle = context.get_handle() - settings = context.get_settings() ui = context.get_ui() if ui.get_property(BUSY_FLAG).lower() == 'true': @@ -127,6 +127,12 @@ def run(self, provider, context): context.wakeup() ui.clear_property(SLEEPING) + if ui.get_property(CHECK_SETTINGS): + settings = context.get_settings(flush=True) + ui.clear_property(CHECK_SETTINGS) + else: + settings = context.get_settings() + if settings.setup_wizard_enabled(): provider.run_wizard(context) 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 aea42bc4f..9054b19bd 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py @@ -447,6 +447,7 @@ def process_subtitles(_provider, context, step, steps): context.execute('RunScript({addon_id},config/subtitles)'.format( addon_id=ADDON_ID ), wait_for=WAIT_FLAG) + context.get_settings(flush=True) return step From 83b59c6f797e28e4891349b333d11d6cd032ef56 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 30 Apr 2024 21:40:39 +1000 Subject: [PATCH 35/59] Improve logging of unknown itags --- resources/lib/youtube_plugin/youtube/helper/video_info.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index d8eda7c77..682eb7a16 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -955,7 +955,8 @@ def _load_hls_manifest(self, url, live_type=None, meta_info=None, yt_format = self.FORMAT.get(itag) if not yt_format: - self._context.log_debug('Unknown itag: {0}'.format(itag)) + self._context.log_debug('Unknown itag: {itag}\n{stream}' + .format(itag=itag, stream=match[0])) continue stream = {'url': playlist_url, @@ -1005,7 +1006,8 @@ def _create_stream_list(self, stream_map['itag'] = itag yt_format = self.FORMAT.get(itag) if not yt_format: - self._context.log_debug('Unknown itag: {0}'.format(itag)) + self._context.log_debug('Unknown itag: {itag}\n{stream}' + .format(itag=itag, stream=stream_map)) continue if (yt_format.get('discontinued') or yt_format.get('unsupported') or (yt_format.get('dash/video') From d3b557597dbda1e76bd57c0e5100985471ed0165 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 30 Apr 2024 22:34:42 +1000 Subject: [PATCH 36/59] Version bump v7.0.7+beta.1 --- addon.xml | 2 +- changelog.txt | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/addon.xml b/addon.xml index 9c119eca5..7f0abffd2 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/changelog.txt b/changelog.txt index 4c331c6a7..9931f2365 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,37 @@ +## v7.0.7+beta.1 +### Fixed +- Fixed not being able to re-refresh a directory listing that has already been refreshed +- Fixed various window and history nagivation issues +- Fixed http server idle shutdown not restarting if plugin is run but GUI is still idle +- Additional improvements to busy dialog crash workaround +- Workaround for new settings interface not updated correctly + +### Changed +- Removed Settings > Advanced > Views > Show channel fanart + - Now included as option in Settings > Advanced > Views > Show fanart +- MPEG-DASH for live streams only enabled by default in Kodi v21 + +### New +- Improvements to plugin page navigation #715 + - Refresh added to context menu of Next page item + - Jump to page added to context menu of Next page item + - Can also be used in plugin url: plugin://plugin.video.youtube/goto_page// #317 + - Home added to context menu of Next page item + - Quick search added to context menu of Next page item + - Next page item added to last page of directory list to go back to first page +- Add option to use channel name as studio and/or cast #717 + - Settings > Advanced > Views > Use channel name as +- Add option to use best available thumbnail quality + - Settings > Advanced > Views > Thumbnail size +- Added option to use video thumbnail as fanart in Settings > Advanced > Views > Show fanart #716 + - Also added plugin url query parameter fanart_type to override settings + - Can be used to set fanart for specific widgets only: plugin://plugin.video.youtube/?fanart_type=<0/1/2/3> + - Allowable values are the same as the setting: + - 0: No fanart + - 1: Default + - 2: Channel + - 3: Thumbnail + ## v7.0.6.3 ### Fixed - Improve updating containers and (re)loading windows #681 From 4ee8aed249e547812c51a207912ad6eb0460b357 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Wed, 1 May 2024 14:36:45 +1000 Subject: [PATCH 37/59] Fix bookmark icon background colour --- resources/media/bookmarks.png | Bin 3744 -> 3198 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/resources/media/bookmarks.png b/resources/media/bookmarks.png index e5604f1c11f62da2366641da9df3bea805985f46..6330eec57b6fb2c45466f7f11fb93373f3d217b1 100644 GIT binary patch delta 1799 zcmV+i2l)7)9sU@QKs*4+P)t-sC@3g$a&nE0jggU&{QUg={r&&{|LyJV%gf7&iHYv+ z?)CNc+S=Os`ueA*r)z6#>+9>n!orczDu1=LwZXx`;^N}X&CT!c@6621-rnB+{{Ftc zzIl0hY;0`fUw&5qobo|XJ@Ugt=rq%qN1YV;o)RtWV*V# zTwGj?jEv>w<;TazcXxMSU|`P9&TVaN?Ck9FW9uvc000SaNLh0L01m_e01m_fm7}`> z1wsZxNklga+u1wcDCq%GR#Cbk}bC|39jd&XIj2%SwQx zvrgYf9}>xm$$4}lO9_w)lMx3NlOP8Nf8gfqO^DBI*#hP?%pU)Qku#}@eM7Sa%xUN8JX)S#&@p=1TiOh0xj0`QJv`$FF$D`a@i8+r+Wt0O1Q+LSqf~MR%tU0iSp2P_ zp8=s9S|pb&pgO0j$42FEO>w)yxOAM;If@ogUCP*ObpF z((;H;P`ZHXR7BSmM=k?SfBE6%3#iVhDa1vYnFGY7uy_IP{m)*p3}*m8(4jzpxg7TJ zBI05R-y_l>pn5E0q+%GzfXDnGMumXt3AGDMr0F@pU;aEg1XNE&j8xJYP(S4dv?vi! zE$B#xl+l%E+`U7Kfa*%dX-g^tuDG*6jez!@Fi&C*;En-`1au#Ve^7}tApH@ECIL|n zNEx`Mk^ayafA&FZEf3XZ#l1m{y0+#lOjQ4Z*sX<3mrVk_5nu!Kh&TQ&UF3X_+aziP*kurh8g2ajyXNLdVkx9XfL#U=y(kxK zCi%-UY6RGJ8)f{qx8u?u<+?zR05`_zx4jHA$`7syvuadcgNO)CkCAf5$?vfLj9rbV>-|Dg;14R38@*K+g^WxVjA> zfKCYkT!jD#i0b140_fR609UsG1kfoVfU6Jy0a1NiKma{E2;k~AP%Pm2i|2eVhh70+ zeA!ID;` z%JuSpFtK{V?Md_rc;$v)vdVommpTF0H2eYk)9?9c9~toUw0HqOOsPlA>NR)hAp@>g z*CYS(^>qSXi|n6IsyB1KZNi3kO>lqf(> zesljl!f#~~v|lbPP$S@{{_qnr34Z1_fffP5@W16oVoxMN|AMGPz#E>sEy3^;A_=Cy zaGOVkfM2QEGM~KTMu|v*_<|?}2vH>o_>CGJ?c^8nyLcH)|KLW46d1EI}N|%oitFKfX$aa8N|z=FadWTeq!|d{}>i9zv0p+gHM7&1ngk5 pKJNWLlMx3NlOP8N8Ae4_eE{S8l6>hJjOPFV002ovPDHLkV1f>SP3-^x delta 2340 zcmV+<3ETGm7@!@HKz{*WP)t-s4Gj%)a&nE0jggU&{QUg={r&&{|LyJV9UUDuHa0>+ zLLwp}5)u;2%gYZB4-^y>4h{|l1qB2I1PKWVI5;?oiHQ>v6YlQr_4V~kOiW2hN!r@l z`uh4LBqSpvBd4dQ7#J99Yikh^5fBg%>+9<-E-u2t!X6$T7ZDd192^`WAt5FvCObPj zQ&Ursd@6q@Cnq2vASfs(wY9ZSP*B0a!Q$fL&CSj4@9)ga%ui2GA0Hpy-roNH{=UAx zd3kwkY;5D>M$@cH#avvK0ZZ7MS6OAqobov zPEKcMXBin8t*xywF)`cQ+oGbPL_|d4;o)RtWV$fAx?EgbjEsy@Qc~sR1m zq_~S!*~Nu|fUt@jf_NcRdsw%JZEdx!t=1m)vUmIce>%sTWM-0`T`btl>-U)-vNH*R z_sN^&oxO;YQ4AE5UjO5fDAweAOnyA6vKZa)C?FIJu)`-azDmt>0#{1=!o8BSOShl^+qF_ z=)*Bu_9kl7qoaD4p$Qm-S7_-60`xA!5YWh=0WTagfWn7>fef%d1zr??0ncInUu^-; zq4Enuz#-QD$N;BbEsz2FuR{hP177e9IL!Hf5CRT!{vU{dL3p;(7XqAqwSWNq*FgYv z{XhUM{Xl@;B?M3x0wBQYR|^Qxe;ou+*AE2H(hp++$4BXb7sdikoTLX{_zIY8wkGMJ z555AXlK2!o^uboZM0+}aX-`nc16u(z$#fjQN*xbu164?Lo47S;B=f!ixfCb-QMH^8jX3Quh&+Mfdwoq>I_(1pn;y40?fWzSvdw4 za7G+`nE~+`8t923!0M|`e-}S>6Iv2E_e;CIp!Pr%&CAF{pr( zax0kuN!)Kj;0DY>z+_7ve3bxkzX<_L0aI!#kpa>{p9uj+0TXR?_{B9q&~HM(QNWDe zYMM?0|Ac^}05uJu3=m8Ign*#{F%3|tYk9V{TT-;hJoBs!t0W&QlGT9{QQc%nQTTW|6?j>LW(dd@$rpTz$rD$q4 z=(e0KJx>f-K-AebW0TRPONl=NwmW(sK?{f`FX;gy4LuKk(`^}-(n(_20-}XABf7jP zd>OEol?H(eh@uTWWTT?=Tmv@fhXuh4h)$XDrA^X11I(ug!WR&&wDhP+D#C4&)(ZWg zf`Nc&`LY?KZh{^eaCw=2Ov6DyGa!eV2dAnAa88Zz5)hrzYg0@avX#F=!b!l65v^!$2JFygHk<@(>Pj!gy#{R3W;TojjEgl!yrMbC zXTSd-yt))NNa8)()P#=!H5)Qfg%HV5t+W*4 zBVbdmG2?wry=st?#AT%r8v)}g=$zJ#WAz&?Q;DFY6k;P_SFXsTUGKSJ@SZmY;?Wl1T-Mu04XDAd-Cx*XA;8&v0kLaZq%h1dv?WspW> zzastMWM`es#AT%r7Xj+6VKPdU{fhjPqvAZEwZ&41i-0{9pSbp`>{sLhPD?+G1q=oR=v_hpbs+!(oPM={ z0R7iN0Cf)n2++HP0O~>j1UUU_0Rj52g8=Ft1Q4Ki2?5lF00?mU)nZ=)ZoYn#9{FH@ zE8zSaZzj#Rj!>^ROa;_8nn{wx!_8YX&=Z~l-oD+9rGPkYzEk=R;hZ#J`aVS#0_yLM z$bTTE1c)Sf&w0(zCKJHt+vHZ>@2aiR4@`8g_`XNS5TFbMFJR%0I{YTxkq>BP38sJ# zjlN!6HTnzq=&nBe7Q@YtOTR%zVhU(~t}O&EVE&V)-omAO?>=qfVhV`6cR#K6KVR=7 z;In&1_P1-FKWNiqnFM#rzfQ>#5D%yM6QY2|7slal*T%%qaxGWT-75WBIa@$RfC9cW zFMjRx{|G-clVGUihR8+0qpz&P|ColO=&_juU-y2WVBiAoTZjK4jcSIBBp4}wyjz%s zfW`y5c1zkdxA?tfBti2V=L-Z<0rDT2>xYVv1k}I%P7h1kwI?)eVkANLo;^b}1ne22 z0us>qUJoj=i=V>}#%a*}(YzsM0wnn*GZ+E&pYG~0NxQa6!v(Ax;?GZMRA32+en~eA zMZno#^@yU^;n!F<#NVhpgW5@0gO#m~XKAy$0;?@|!pDSbRqWaUmypaP!Z z@N0}~p(aQHeM=t?<_%F2qJU>U{2Dt6!V@rH>Eq#vX&^WOgO@(N31I&3k_y;Y>EogD z^M4l-(B5C^ Date: Thu, 2 May 2024 09:28:30 +1000 Subject: [PATCH 38/59] Enable all base profiler methods in proxy object --- resources/lib/youtube_plugin/kodion/debug.py | 22 +++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/resources/lib/youtube_plugin/kodion/debug.py b/resources/lib/youtube_plugin/kodion/debug.py index 72712c0df..8736296b4 100644 --- a/resources/lib/youtube_plugin/kodion/debug.py +++ b/resources/lib/youtube_plugin/kodion/debug.py @@ -75,6 +75,26 @@ def __exit__(self, *args, **kwargs): *args, **kwargs ) + def disable(self, *args, **kwargs): + return super(Profiler.Proxy, self).__call__().disable( + *args, **kwargs + ) + + def enable(self, *args, **kwargs): + return super(Profiler.Proxy, self).__call__().enable( + *args, **kwargs + ) + + def get_stats(self, *args, **kwargs): + return super(Profiler.Proxy, self).__call__().get_stats( + *args, **kwargs + ) + + def print_stats(self, *args, **kwargs): + return super(Profiler.Proxy, self).__call__().print_stats( + *args, **kwargs + ) + _instances = set() def __new__(cls, *args, **kwargs): @@ -205,7 +225,7 @@ def get_stats(self, flush=True, reuse=False): self._Stats( self._profiler, stream=output_stream - ).strip_dirs().sort_stats('cumulative', 'time').print_stats(50) + ).strip_dirs().sort_stats('cumulative', 'time').print_stats(20) output = output_stream.getvalue() # Occurs when no stats were able to be generated from profiler except TypeError: From ffc8fb1847f189b23238c456f5d0267b83c61a13 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 2 May 2024 09:43:20 +1000 Subject: [PATCH 39/59] Fix adding channel items directly to bookmarks --- resources/lib/youtube_plugin/youtube/helper/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index d4cf1d0b3..7aa6d30c1 100644 --- a/resources/lib/youtube_plugin/youtube/helper/utils.py +++ b/resources/lib/youtube_plugin/youtube/helper/utils.py @@ -227,8 +227,8 @@ def update_channel_infos(provider, context, channel_id_dict, if not in_bookmarks_list: context_menu.append( - menu_items.bookmarks_add( - context, channel_item + menu_items.bookmarks_add_channel( + context, channel_id ) ) From 5eeed1a0d4b302d0daaf94bb2ffa1e95fd6f2577 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 2 May 2024 12:49:12 +1000 Subject: [PATCH 40/59] Make better use of reuselanguageinvoker --- resources/lib/plugin.py | 3 +- .../kodion/abstract_provider.py | 2 +- .../kodion/context/abstract_context.py | 21 ++--- .../kodion/context/xbmc/xbmc_context.py | 79 +++++++++---------- .../kodion/plugin/xbmc/xbmc_plugin.py | 1 + .../youtube_plugin/kodion/plugin_runner.py | 59 +++++++------- .../lib/youtube_plugin/youtube/provider.py | 4 +- 7 files changed, 76 insertions(+), 93 deletions(-) diff --git a/resources/lib/plugin.py b/resources/lib/plugin.py index ae5348210..290bd2f30 100644 --- a/resources/lib/plugin.py +++ b/resources/lib/plugin.py @@ -10,8 +10,7 @@ from __future__ import absolute_import, division, unicode_literals -from youtube_plugin import youtube from youtube_plugin.kodion import plugin_runner -plugin_runner.run(youtube.Provider()) +plugin_runner.run() diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index ba94433d3..2224f4d8d 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -329,7 +329,7 @@ def _internal_search(self, context, re_match): def handle_exception(self, context, exception_to_handle): return True - def tear_down(self, context): + def tear_down(self): pass diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index f411ccc62..761dcda63 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -101,13 +101,7 @@ class AbstractContext(object): 'reload_path', } - def __init__(self, path='/', params=None, plugin_name='', plugin_id=''): - if not params: - params = {} - - self._cache_path = None - self._debug_path = None - + def __init__(self, path='/', params=None, plugin_id=''): self._function_cache = None self._data_cache = None self._search_history = None @@ -116,14 +110,13 @@ def __init__(self, path='/', params=None, plugin_name='', plugin_id=''): self._watch_later_list = None self._access_manager = None - self._plugin_name = plugin_name - self._version = 'UNKNOWN' + self._plugin_handle = -1 self._plugin_id = plugin_id - self._path = self.create_path(path) - self._params = params - self._utils = None + self._plugin_name = None + self._version = 'UNKNOWN' - # create valid uri + self._path = self.create_path(path) + self._params = params or {} self.parse_params() self._uri = self.create_uri(self._path, self._params) @@ -374,7 +367,7 @@ def get_id(self): return self._plugin_id def get_handle(self): - raise NotImplementedError() + return self._plugin_handle def get_settings(self, flush=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 9a285aae0..da238350c 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -295,58 +295,56 @@ def __new__(cls, *args, **kwargs): def __init__(self, path='/', params=None, - plugin_name='', - plugin_id='', - override=True): - super(XbmcContext, self).__init__(path, params, plugin_name, plugin_id) + plugin_id=''): + super(XbmcContext, self).__init__(path, params, plugin_id) self._plugin_id = plugin_id or ADDON_ID if self._plugin_id != ADDON_ID: self._addon = xbmcaddon.Addon(self._plugin_id) self._settings = XbmcPluginSettings(self._addon) - """ - I don't know what xbmc/kodi is doing with a simple uri, but we have to extract the information from the - sys parameters and re-build our clean uri. - Also we extract the path and parameters - man, that would be so simple with the normal url-parsing routines. - """ - num_args = len(sys.argv) - if override and num_args: - uri = sys.argv[0] - is_plugin_invocation = uri.startswith('plugin://') - if is_plugin_invocation: - # first the path of the uri - parsed_url = urlsplit(uri) - self._path = unquote(parsed_url.path) - - # after that try to get the params - if num_args > 2: - params = sys.argv[2][1:] - if params: - self.parse_params(dict(parse_qsl(params))) - - # then Kodi resume status - if num_args > 3 and sys.argv[3].lower() == 'resume:true': - self._params['resume'] = True - - self._uri = self.create_uri(self._path, self._params) - elif num_args: - uri = sys.argv[0] - is_plugin_invocation = uri.startswith('plugin://') - else: - is_plugin_invocation = False - self._ui = None self._video_playlist = None self._audio_playlist = None self._video_player = None self._audio_player = None - self._plugin_handle = int(sys.argv[1]) if is_plugin_invocation else -1 - self._plugin_name = plugin_name or self._addon.getAddonInfo('name') + + self._plugin_name = self._addon.getAddonInfo('name') self._version = self._addon.getAddonInfo('version') + self._addon_path = make_dirs(self._addon.getAddonInfo('path')) self._data_path = make_dirs(self._addon.getAddonInfo('profile')) + def init(self): + num_args = len(sys.argv) + if num_args: + uri = sys.argv[0] + if uri.startswith('plugin://'): + self._plugin_handle = int(sys.argv[1]) + else: + self._plugin_handle = -1 + return + else: + self._plugin_handle = -1 + return + + # first the path of the uri + parsed_url = urlsplit(uri) + self._path = unquote(parsed_url.path) + + # after that try to get the params + self._params = {} + if num_args > 2: + params = sys.argv[2][1:] + if params: + self.parse_params(dict(parse_qsl(params))) + + # then Kodi resume status + if num_args > 3 and sys.argv[3].lower() == 'resume:true': + self._params['resume'] = True + + self._uri = self.create_uri(self._path, self._params) + def get_region(self): pass # implement from abstract @@ -425,9 +423,6 @@ def get_ui(self): self._ui = XbmcContextUI(self._addon, weakref.proxy(self)) return self._ui - def get_handle(self): - return self._plugin_handle - def get_data_path(self): return self._data_path @@ -536,9 +531,7 @@ def clone(self, new_path=None, new_params=None): new_context = XbmcContext(path=new_path, params=new_params, - plugin_name=self._plugin_name, - plugin_id=self._plugin_id, - override=False) + plugin_id=self._plugin_id) new_context._function_cache = self._function_cache new_context._search_history = self._search_history new_context._bookmarks_list = self._bookmarks_list diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py index f7e833af5..4f55c426e 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py @@ -128,6 +128,7 @@ def run(self, provider, context): ui.clear_property(SLEEPING) if ui.get_property(CHECK_SETTINGS): + provider.reset_client() settings = context.get_settings(flush=True) ui.clear_property(CHECK_SETTINGS) else: diff --git a/resources/lib/youtube_plugin/kodion/plugin_runner.py b/resources/lib/youtube_plugin/kodion/plugin_runner.py index 44509c2b5..c8105b964 100644 --- a/resources/lib/youtube_plugin/kodion/plugin_runner.py +++ b/resources/lib/youtube_plugin/kodion/plugin_runner.py @@ -10,57 +10,54 @@ from __future__ import absolute_import, division, unicode_literals +import atexit +from copy import deepcopy +from platform import python_version -__all__ = ('run',) +from .plugin import XbmcPlugin +from .context import XbmcContext +from ..youtube import Provider -def run(provider, context=None): - if not context: - from .context import XbmcContext +__all__ = ('run',) - context = XbmcContext() +context = XbmcContext() +plugin = XbmcPlugin() +provider = Provider() - profiler = context.get_infobool('System.GetBool(debug.showloginfo)') - if profiler: - from .debug import Profiler +profiler = context.get_infobool('System.GetBool(debug.showloginfo)') +if profiler: + from .debug import Profiler - profiler = Profiler(enabled=True, lazy=False) + profiler = Profiler(enabled=False) - from copy import deepcopy - from platform import python_version +atexit.register(provider.tear_down) +atexit.register(context.tear_down) - from .plugin import XbmcPlugin - plugin = XbmcPlugin() +def run(): + if profiler: + profiler.enable(flush=True) context.log_debug('Starting Kodion framework by bromix...') + context.init() - addon_version = context.get_version() - python_version = 'Python {0}'.format(python_version()) - - redacted = '' params = deepcopy(context.get_params()) - if 'api_key' in params: - params['api_key'] = redacted - if 'client_id' in params: - params['client_id'] = redacted - if 'client_secret' in params: - params['client_secret'] = redacted + for key in ('api_key', 'client_id', 'client_secret'): + if key in params: + params[key] = '' context.log_notice('Running: {plugin} ({version}) on {kodi} with {python}\n' 'Path: {path}\n' 'Params: {params}' .format(plugin=context.get_name(), - version=addon_version, + version=context.get_version(), kodi=context.get_system_version(), - python=python_version, + python='Python {0}'.format(python_version()), path=context.get_path(), params=params)) - try: - plugin.run(provider, context) - finally: - if profiler: - profiler.print_stats() + plugin.run(provider, context) - provider.tear_down(context) + if profiler: + profiler.print_stats() diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 76cd9b7dd..5ff8aff74 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -1571,5 +1571,5 @@ def handle_exception(self, context, exception_to_handle): return True - def tear_down(self, context): - context.tear_down() + def tear_down(self): + pass From 02fee00336c77bed59357a5c337686c58e52f26e Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 2 May 2024 13:19:19 +1000 Subject: [PATCH 41/59] Misc tidy ups --- .../youtube_plugin/youtube/client/youtube.py | 119 ++++++++++++------ 1 file changed, 81 insertions(+), 38 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index a614494e4..010f918ba 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -202,37 +202,51 @@ def get_video_streams(self, context, video_id): # update title for video_stream in video_streams: - title = '%s (%s)' % (context.get_ui().bold(video_stream['title']), video_stream['container']) + title = '%s (%s)' % ( + context.get_ui().bold(video_stream['title']), + video_stream['container'] + ) if 'audio' in video_stream and 'video' in video_stream: - if video_stream['audio']['bitrate'] > 0 and video_stream['video']['encoding'] and \ - video_stream['audio']['encoding']: - title = '%s (%s; %s / %s@%d)' % (context.get_ui().bold(video_stream['title']), - video_stream['container'], - video_stream['video']['encoding'], - video_stream['audio']['encoding'], - video_stream['audio']['bitrate']) - - elif video_stream['video']['encoding'] and video_stream['audio']['encoding']: - title = '%s (%s; %s / %s)' % (context.get_ui().bold(video_stream['title']), - video_stream['container'], - video_stream['video']['encoding'], - video_stream['audio']['encoding']) + if (video_stream['audio']['bitrate'] > 0 + and video_stream['video']['encoding'] + and video_stream['audio']['encoding']): + title = '%s (%s; %s / %s@%d)' % ( + context.get_ui().bold(video_stream['title']), + video_stream['container'], + video_stream['video']['encoding'], + video_stream['audio']['encoding'], + video_stream['audio']['bitrate'] + ) + + elif (video_stream['video']['encoding'] + and video_stream['audio']['encoding']): + title = '%s (%s; %s / %s)' % ( + context.get_ui().bold(video_stream['title']), + video_stream['container'], + video_stream['video']['encoding'], + video_stream['audio']['encoding'] + ) elif 'audio' in video_stream and 'video' not in video_stream: - if video_stream['audio']['encoding'] and video_stream['audio']['bitrate'] > 0: - title = '%s (%s; %s@%d)' % (context.get_ui().bold(video_stream['title']), - video_stream['container'], - video_stream['audio']['encoding'], - video_stream['audio']['bitrate']) + if (video_stream['audio']['encoding'] + and video_stream['audio']['bitrate'] > 0): + title = '%s (%s; %s@%d)' % ( + context.get_ui().bold(video_stream['title']), + video_stream['container'], + video_stream['audio']['encoding'], + video_stream['audio']['bitrate'] + ) elif 'audio' in video_stream or 'video' in video_stream: encoding = video_stream.get('audio', {}).get('encoding') if not encoding: encoding = video_stream.get('video', {}).get('encoding') if encoding: - title = '%s (%s; %s)' % (context.get_ui().bold(video_stream['title']), - video_stream['container'], - encoding) + title = '%s (%s; %s)' % ( + context.get_ui().bold(video_stream['title']), + video_stream['container'], + encoding + ) video_stream['title'] = title @@ -1607,10 +1621,15 @@ def _perform(_playlist_idx, _page_token, _offset, _result): post_data=_post_data) _data = {} if 'continuationContents' in _json_data: - _data = _json_data.get('continuationContents', {}).get('horizontalListContinuation', {}) + _data = (_json_data.get('continuationContents', {}) + .get('horizontalListContinuation', {})) elif 'contents' in _json_data: - _data = _json_data.get('contents', {}).get('sectionListRenderer', {}).get('contents', [{}])[_playlist_idx].get( - 'shelfRenderer', {}).get('content', {}).get('horizontalListRenderer', {}) + _data = (_json_data.get('contents', {}) + .get('sectionListRenderer', {}) + .get('contents', [{}])[_playlist_idx] + .get('shelfRenderer', {}) + .get('content', {}) + .get('horizontalListRenderer', {})) _items = _data.get('items', []) if not _result: @@ -1624,22 +1643,36 @@ def _perform(_playlist_idx, _page_token, _offset, _result): for _item in _items: _item = _item.get('gridPlaylistRenderer', {}) if _item: - _video_item = {'id': _item['playlistId'], - 'title': _item.get('title', {}).get('runs', [{}])[0].get('text', ''), - 'channel': _item.get('shortBylineText', {}).get('runs', [{}])[0].get('text', ''), - 'channel_id': _item.get('shortBylineText', {}).get('runs', [{}])[0] - .get('navigationEndpoint', {}).get('browseEndpoint', {}).get('browseId', ''), - 'thumbnails': (_item.get('thumbnail', {}) - .get('thumbnails', [{}]))} + _video_item = { + 'id': _item['playlistId'], + 'title': (_item.get('title', {}) + .get('runs', [{}])[0] + .get('text', '')), + 'channel': (_item.get('shortBylineText', {}) + .get('runs', [{}])[0] + .get('text', '')), + 'channel_id': (_item.get('shortBylineText', {}) + .get('runs', [{}])[0] + .get('navigationEndpoint', {}) + .get('browseEndpoint', {}) + .get('browseId', '')), + 'thumbnails': (_item.get('thumbnail', {}) + .get('thumbnails', [{}])), + } _result['items'].append(_video_item) - _continuations = _data.get('continuations', [{}])[0].get('nextContinuationData', {}).get('continuation', '') + _continuations = (_data.get('continuations', [{}])[0] + .get('nextContinuationData', {}) + .get('continuation', '')) if _continuations and len(_result['items']) <= self._max_results: _result['next_page_token'] = _continuations if len(_result['items']) < self._max_results: - _result = _perform(_playlist_idx=playlist_index, _page_token=_continuations, _offset=0, _result=_result) + _result = _perform(_playlist_idx=playlist_index, + _page_token=_continuations, + _offset=0, + _result=_result) # trim result if len(_result['items']) > self._max_results: @@ -1681,18 +1714,28 @@ def _perform(_playlist_idx, _page_token, _offset, _result): method='POST', path='browse', post_data=_en_post_data) - contents = json_data.get('contents', {}).get('sectionListRenderer', {}).get('contents', [{}]) + contents = (json_data.get('contents', {}) + .get('sectionListRenderer', {}) + .get('contents', [{}])) for idx, shelf in enumerate(contents): - title = shelf.get('shelfRenderer', {}).get('title', {}).get('runs', [{}])[0].get('text', '') + title = (shelf.get('shelfRenderer', {}) + .get('title', {}) + .get('runs', [{}])[0] + .get('text', '')) if title.lower() == 'saved playlists': playlist_index = idx break if playlist_index is not None: - contents = json_data.get('contents', {}).get('sectionListRenderer', {}).get('contents', [{}]) + contents = (json_data.get('contents', {}) + .get('sectionListRenderer', {}) + .get('contents', [{}])) if 0 <= playlist_index < len(contents): - result = _perform(_playlist_idx=playlist_index, _page_token=page_token, _offset=offset, _result=result) + result = _perform(_playlist_idx=playlist_index, + _page_token=page_token, + _offset=offset, + _result=result) return result From 733e8d20addf4bb752a3d8593615a073cb0549fe Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Thu, 2 May 2024 13:23:09 +1000 Subject: [PATCH 42/59] Update changelog --- changelog.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/changelog.txt b/changelog.txt index 9931f2365..3ae2d2ea6 100644 --- a/changelog.txt +++ b/changelog.txt @@ -5,11 +5,14 @@ - Fixed http server idle shutdown not restarting if plugin is run but GUI is still idle - Additional improvements to busy dialog crash workaround - Workaround for new settings interface not updated correctly +- Fix bookmarks icon background colour +- Fix adding channel items directly to bookmarks ### Changed - Removed Settings > Advanced > Views > Show channel fanart - Now included as option in Settings > Advanced > Views > Show fanart - MPEG-DASH for live streams only enabled by default in Kodi v21 +- Make better use of reuselanguageinvoker ### New - Improvements to plugin page navigation #715 From 94a5a896064e7973239e1622ab1e7e93f44c1f85 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 3 May 2024 10:56:25 +1000 Subject: [PATCH 43/59] Add support for fallback video url - Note that multiple BaseUrl elements are not currently supported by InputStream.Adaptive --- .../youtube/helper/video_info.py | 61 ++++++++++++++----- 1 file changed, 47 insertions(+), 14 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 682eb7a16..ef99948a8 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -1000,7 +1000,7 @@ def _create_stream_list(self, if not url: continue - url = self._process_url_params(url) + url, _ = self._process_url_params(url) itag = str(stream_map['itag']) stream_map['itag'] = itag @@ -1068,12 +1068,12 @@ def _process_signature_cipher(self, stream_map): def _process_url_params(self, url): if not url: - return url + return url, None parts = urlsplit(url) query = parse_qs(parts.query) new_query = {} - update_url = False + update_url = {} if self._calculate_n and 'n' in query: self._player_js = self._player_js or self._get_player_js() @@ -1094,12 +1094,35 @@ def _process_url_params(self, url): content_length = query.get('clen', [''])[0] new_query['range'] = '0-{0}'.format(content_length) + if 'mn' in query and 'fvip' in query: + fvip = query['fvip'][0] + primary, _, secondary = query['mn'][0].partition(',') + prefix, separator, server = parts.netloc.partition('---') + if primary and secondary: + update_url = { + 'netloc': separator.join(( + re.sub(r'\d+', fvip, prefix), + server.replace(primary, secondary), + )), + } + if new_query: query.update(new_query) - elif not update_url: - return url + query = urlencode(query, doseq=True) + elif update_url: + query = parts.query + else: + return url, None - return parts._replace(query=urlencode(query, doseq=True)).geturl() + if update_url: + return ( + parts._replace(query=query).geturl(), + parts._replace(query=query, **update_url).geturl(), + ) + return ( + parts._replace(query=query).geturl(), + None, + ) def _get_error_details(self, playability_status, details=None): if not playability_status: @@ -1714,15 +1737,15 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): data[quality_group] = {} url = unquote(url) - url = self._process_url_params(url) - url = (url.replace("&", "&") - .replace('"', """) - .replace("<", "<") - .replace(">", ">")) + primary_url, secondary_url = self._process_url_params(url) + primary_url = (primary_url.replace("&", "&") + .replace('"', """) + .replace("<", "<") + .replace(">", ">")) details = { 'mimeType': mime_type, - 'baseUrl': url, + 'baseUrl': primary_url, 'mediaType': media_type, 'container': container, 'codecs': codecs, @@ -1747,6 +1770,12 @@ def _process_stream_data(self, stream_data, default_lang_code='und'): 'sampleRate': sample_rate, 'channels': channels, } + if secondary_url: + secondary_url = (secondary_url.replace("&", "&") + .replace('"', """) + .replace("<", "<") + .replace(">", ">")) + details['baseUrlSecondary'] = secondary_url data[mime_group][itag] = data[quality_group][itag] = details if not video_data: @@ -1988,7 +2017,9 @@ def _filter_group(previous_group, previous_stream, item): '/>\n' # Representation Label element is not used by ISA '\t\t\t\t\n' - '\t\t\t\t{baseUrl}\n' + '\t\t\t\t{baseUrl}\n' + + ('\t\t\t\t{baseUrlSecondary}\n' + if 'baseUrlSecondary' in stream else '') + '\t\t\t\t\n' '\t\t\t\t\t\n' '\t\t\t\t\n' @@ -2012,7 +2043,9 @@ def _filter_group(previous_group, previous_stream, item): '>\n' # Representation Label element is not used by ISA '\t\t\t\t\n' - '\t\t\t\t{baseUrl}\n' + '\t\t\t\t{baseUrl}\n' + + ('\t\t\t\t{baseUrlSecondary}\n' + if 'baseUrlSecondary' in stream else '') + '\t\t\t\t\n' '\t\t\t\t\t\n' '\t\t\t\t\n' From 3fa9ed16edc311faf78f62b4c3a48f180f63fe2c Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 3 May 2024 11:29:06 +1000 Subject: [PATCH 44/59] Attempt to fix possible memory leaks --- .../lib/youtube_plugin/kodion/monitors/service_monitor.py | 3 +++ resources/lib/youtube_plugin/kodion/service_runner.py | 1 + .../kodion/settings/xbmc/xbmc_plugin_settings.py | 5 +++++ 3 files changed, 9 insertions(+) diff --git a/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py b/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py index c41b2a1eb..e6441de2b 100644 --- a/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py +++ b/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py @@ -177,3 +177,6 @@ def ping_httpd(self): def httpd_required(self): return self._use_httpd + + def tear_down(self): + self._settings.flush() diff --git a/resources/lib/youtube_plugin/kodion/service_runner.py b/resources/lib/youtube_plugin/kodion/service_runner.py index eea39fe9e..aadb2f17b 100644 --- a/resources/lib/youtube_plugin/kodion/service_runner.py +++ b/resources/lib/youtube_plugin/kodion/service_runner.py @@ -77,4 +77,5 @@ def run(): if monitor.httpd: monitor.shutdown_httpd() # shutdown http server + monitor.tear_down() context.tear_down() diff --git a/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py b/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py index d22706bd0..f188bbca0 100644 --- a/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py +++ b/resources/lib/youtube_plugin/kodion/settings/xbmc/xbmc_plugin_settings.py @@ -10,6 +10,8 @@ from __future__ import absolute_import, division, unicode_literals +import atexit + from ..abstract_settings import AbstractSettings from ...compatibility import xbmcaddon from ...constants import VALUE_FROM_STR @@ -270,3 +272,6 @@ def set_string_list(self, setting, value, echo=None): status=error if error else 'success' )) return not error + + +atexit.register(XbmcPluginSettings.flush) From d41957b9608669c6f5d30a50493a936383b497ce Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 3 May 2024 11:30:22 +1000 Subject: [PATCH 45/59] Use local lookup of module level globals in plugin_runner --- .../youtube_plugin/kodion/plugin_runner.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/plugin_runner.py b/resources/lib/youtube_plugin/kodion/plugin_runner.py index c8105b964..4f3eabbf7 100644 --- a/resources/lib/youtube_plugin/kodion/plugin_runner.py +++ b/resources/lib/youtube_plugin/kodion/plugin_runner.py @@ -21,21 +21,24 @@ __all__ = ('run',) -context = XbmcContext() -plugin = XbmcPlugin() -provider = Provider() +_context = XbmcContext() +_plugin = XbmcPlugin() +_provider = Provider() -profiler = context.get_infobool('System.GetBool(debug.showloginfo)') -if profiler: +_profiler = _context.get_infobool('System.GetBool(debug.showloginfo)') +if _profiler: from .debug import Profiler - profiler = Profiler(enabled=False) + _profiler = Profiler(enabled=False) -atexit.register(provider.tear_down) -atexit.register(context.tear_down) +atexit.register(_provider.tear_down) +atexit.register(_context.tear_down) -def run(): +def run(context=_context, + plugin=_plugin, + provider=_provider, + profiler=_profiler): if profiler: profiler.enable(flush=True) From de3ed12a74c868da27afea7e7b7c9065b2a7d7e6 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Fri, 3 May 2024 15:38:13 +1000 Subject: [PATCH 46/59] Fix possibly using expired access tokens now that client is not being reloaded every plugin invocation --- resources/lib/youtube_plugin/__init__.py | 12 +- .../kodion/json_store/access_manager.py | 72 +++--- .../youtube/client/__config__.py | 146 ++++++------ .../youtube_plugin/youtube/client/__init__.py | 6 +- .../youtube/client/login_client.py | 67 ++---- .../youtube_plugin/youtube/client/youtube.py | 7 +- .../youtube_plugin/youtube/helper/yt_login.py | 120 ++++++---- .../lib/youtube_plugin/youtube/provider.py | 213 +++++++++--------- 8 files changed, 324 insertions(+), 319 deletions(-) diff --git a/resources/lib/youtube_plugin/__init__.py b/resources/lib/youtube_plugin/__init__.py index a7a23e288..a2d997b5f 100644 --- a/resources/lib/youtube_plugin/__init__.py +++ b/resources/lib/youtube_plugin/__init__.py @@ -13,15 +13,15 @@ key_sets = { 'youtube-tv': { - 'id': 'ODYxNTU2NzA4NDU0LWQ2ZGxtM2xoMDVpZGQ4bnBlazE4azZiZThiYTNvYzY4', - 'key': 'QUl6YVN5QzZmdlpTSkhBN1Z6NWo4akNpS1J0N3RVSU9xakUyTjNn', - 'secret': 'U2JvVmhvRzlzMHJOYWZpeENTR0dLWEFU' + 'client_id': 'ODYxNTU2NzA4NDU0LWQ2ZGxtM2xoMDVpZGQ4bnBlazE4azZiZThiYTNvYzY4', + 'api_key': 'QUl6YVN5QzZmdlpTSkhBN1Z6NWo4akNpS1J0N3RVSU9xakUyTjNn', + 'client_secret': 'U2JvVmhvRzlzMHJOYWZpeENTR0dLWEFU' }, 'provided': { '0': { - 'id': '', - 'key': '', - 'secret': '' + 'client_id': '', + 'api_key': '', + 'client_secret': '' } } } diff --git a/resources/lib/youtube_plugin/kodion/json_store/access_manager.py b/resources/lib/youtube_plugin/kodion/json_store/access_manager.py index e1827b142..ae808fc72 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/access_manager.py +++ b/resources/lib/youtube_plugin/kodion/json_store/access_manager.py @@ -378,17 +378,16 @@ def get_access_token(self): Returns the access token for some API :return: access_token """ - return self.get_current_user_details().get('access_token', '') + token = self.get_current_user_details().get('access_token', '') + return token.split('|') def get_refresh_token(self): """ Returns the refresh token :return: refresh token """ - return self.get_current_user_details().get('refresh_token', '') - - def has_refresh_token(self): - return self.get_refresh_token() != '' + token = self.get_current_user_details().get('refresh_token', '') + return token.split('|') def is_access_token_expired(self): """ @@ -401,19 +400,12 @@ def is_access_token_expired(self): access_token = current_user.get('access_token', '') expires = int(current_user.get('token_expires', -1)) - # with no access_token it must be expired - if not access_token: + if access_token and expires <= int(time.time()): return True - - # in this case no expiration date was set - if expires == -1: - return False - - now = int(time.time()) - return expires <= now + return False def update_access_token(self, - access_token, + access_token=None, unix_timestamp=None, refresh_token=None): """ @@ -424,14 +416,24 @@ def update_access_token(self, :return: """ current_user = { - 'access_token': access_token, + 'access_token': ( + '|'.join(access_token) + if isinstance(access_token, (list, tuple)) else + access_token + if access_token else + '' + ) } if unix_timestamp is not None: current_user['token_expires'] = int(unix_timestamp) if refresh_token is not None: - current_user['refresh_token'] = refresh_token + current_user['refresh_token'] = ( + '|'.join(refresh_token) + if isinstance(refresh_token, (list, tuple)) else + refresh_token + ) data = { 'access_manager': { @@ -492,17 +494,14 @@ def get_dev_access_token(self, addon_id): :param addon_id: addon id :return: access_token """ - return self.get_developer(addon_id).get('access_token', '') + return self.get_developer(addon_id).get('access_token', '').split('|') def get_dev_refresh_token(self, addon_id): """ Returns the refresh token :return: refresh token """ - return self.get_developer(addon_id).get('refresh_token', '') - - def developer_has_refresh_token(self, addon_id): - return self.get_dev_refresh_token(addon_id) != '' + return self.get_developer(addon_id).get('refresh_token', '').split('|') def is_dev_access_token_expired(self, addon_id): """ @@ -515,20 +514,13 @@ def is_dev_access_token_expired(self, addon_id): access_token = developer.get('access_token', '') expires = int(developer.get('token_expires', -1)) - # with no access_token it must be expired - if not access_token: + if access_token and expires <= int(time.time()): return True - - # in this case no expiration date was set - if expires == -1: - return False - - now = int(time.time()) - return expires <= now + return False def update_dev_access_token(self, addon_id, - access_token, + access_token=None, unix_timestamp=None, refresh_token=None): """ @@ -540,14 +532,24 @@ def update_dev_access_token(self, :return: """ developer = { - 'access_token': access_token + 'access_token': ( + '|'.join(access_token) + if isinstance(access_token, (list, tuple)) else + access_token + if access_token else + '' + ) } if unix_timestamp is not None: developer['token_expires'] = int(unix_timestamp) if refresh_token is not None: - developer['refresh_token'] = refresh_token + developer['refresh_token'] = ( + '|'.join(refresh_token) + if isinstance(refresh_token, (list, tuple)) else + refresh_token + ) data = { 'access_manager': { @@ -588,7 +590,7 @@ def dev_keys_changed(self, addon_id, api_key, client_id, client_secret): return False @staticmethod - def calc_key_hash(key, id, secret): + def calc_key_hash(key, id, secret, **_kwargs): md5_hash = md5() md5_hash.update(''.join((key, id, secret)).encode('utf-8')) return md5_hash.hexdigest() diff --git a/resources/lib/youtube_plugin/youtube/client/__config__.py b/resources/lib/youtube_plugin/youtube/client/__config__.py index 9cc843198..81903dc04 100644 --- a/resources/lib/youtube_plugin/youtube/client/__config__.py +++ b/resources/lib/youtube_plugin/youtube/client/__config__.py @@ -12,8 +12,7 @@ from base64 import b64decode from ... import key_sets -from ...kodion.context import XbmcContext -from ...kodion.json_store import APIKeyStore, AccessManager +from ...kodion.json_store import APIKeyStore DEFAULT_SWITCH = 1 @@ -22,21 +21,13 @@ class APICheck(object): def __init__(self, context): self._context = context - self._settings = context.get_settings() - self._ui = context.get_ui() self._api_jstore = APIKeyStore() - self._json_api = self._api_jstore.get_data() - self._access_manager = AccessManager(context) - self.changed = False + json_data = self._api_jstore.get_data() + access_manager = context.get_access_manager() - self._on_init() - - def _on_init(self): - self._json_api = self._api_jstore.get_data() - - j_key = self._json_api['keys']['personal'].get('api_key', '') - j_id = self._json_api['keys']['personal'].get('client_id', '') - j_secret = self._json_api['keys']['personal'].get('client_secret', '') + j_key = json_data['keys']['personal'].get('api_key', '') + j_id = json_data['keys']['personal'].get('client_id', '') + j_secret = json_data['keys']['personal'].get('client_secret', '') if j_key and j_id and j_secret: # users are now pasting keys into api_keys.json @@ -44,37 +35,38 @@ def _on_init(self): stripped_key, stripped_id, stripped_secret = self._strip_api_keys(j_key, j_id, j_secret) if (stripped_key and stripped_id and stripped_secret and (j_key != stripped_key or j_id != stripped_id or j_secret != stripped_secret)): - self._json_api['keys']['personal'] = {'api_key': stripped_key, 'client_id': stripped_id, 'client_secret': stripped_secret} - self._api_jstore.save(self._json_api) + json_data['keys']['personal'] = {'api_key': stripped_key, 'client_id': stripped_id, 'client_secret': stripped_secret} + self._api_jstore.save(json_data) - original_key = self._settings.api_key() - original_id = self._settings.api_id() - original_secret = self._settings.api_secret() + settings = self._context.get_settings() + original_key = settings.api_key() + original_id = settings.api_id() + original_secret = settings.api_secret() if original_key and original_id and original_secret: own_key, own_id, own_secret = self._strip_api_keys(original_key, original_id, original_secret) if own_key and own_id and own_secret: if (original_key != own_key) or (original_id != own_id) or (original_secret != own_secret): - self._settings.api_key(own_key) - self._settings.api_id(own_id) - self._settings.api_secret(own_secret) + settings.api_key(own_key) + settings.api_id(own_id) + settings.api_secret(own_secret) if (j_key != own_key) or (j_id != own_id) or (j_secret != own_secret): - self._json_api['keys']['personal'] = {'api_key': own_key, 'client_id': own_id, 'client_secret': own_secret} - self._api_jstore.save(self._json_api) + json_data['keys']['personal'] = {'api_key': own_key, 'client_id': own_id, 'client_secret': own_secret} + self._api_jstore.save(json_data) - self._json_api = self._api_jstore.get_data() - j_key = self._json_api['keys']['personal'].get('api_key', '') - j_id = self._json_api['keys']['personal'].get('client_id', '') - j_secret = self._json_api['keys']['personal'].get('client_secret', '') + json_data = self._api_jstore.get_data() + j_key = json_data['keys']['personal'].get('api_key', '') + j_id = json_data['keys']['personal'].get('client_id', '') + j_secret = json_data['keys']['personal'].get('client_secret', '') if (not original_key or not original_id or not original_secret and j_key and j_secret and j_id): - self._settings.api_key(j_key) - self._settings.api_id(j_id) - self._settings.api_secret(j_secret) + settings.api_key(j_key) + settings.api_id(j_id) + settings.api_secret(j_secret) switch = self.get_current_switch() - user_details = self._access_manager.get_current_user_details() + user_details = access_manager.get_current_user_details() last_hash = user_details.get('last_key_hash', '') current_set_hash = self._get_key_set_hash(switch) @@ -82,7 +74,7 @@ def _on_init(self): if changed and switch == 'own': changed = self._get_key_set_hash('own_old') != last_hash if not changed: - self._access_manager.set_last_key_hash(current_set_hash) + access_manager.set_last_key_hash(current_set_hash) self.changed = changed self._context.log_debug('User: |{user}|, ' @@ -91,55 +83,68 @@ def _on_init(self): switch=switch)) if changed: self._context.log_debug('API key set changed: Signing out') - self._context.execute('RunPlugin(plugin://plugin.video.youtube/' - 'sign/out/?confirmed=true)') - self._access_manager.set_last_key_hash(current_set_hash) + self._context.execute('RunPlugin({0})'.format( + self._context.create_uri( + ('sign', 'out'), + { + 'confirmed': True, + } + ) + )) + access_manager.set_last_key_hash(current_set_hash) @staticmethod def get_current_switch(): return 'own' def get_current_user(self): - return self._access_manager.get_current_user() + return self._context.get_access_manager().get_current_user() def has_own_api_keys(self): - self._json_api = self._api_jstore.get_data() - own_key = self._json_api['keys']['personal']['api_key'] - own_id = self._json_api['keys']['personal']['client_id'] - own_secret = self._json_api['keys']['personal']['client_secret'] - return own_key and own_id and own_secret + json_data = self._api_jstore.get_data() + try: + return (json_data['keys']['personal']['api_key'] + and json_data['keys']['personal']['client_id'] + and json_data['keys']['personal']['client_secret']) + except KeyError: + return False def get_api_keys(self, switch): - self._json_api = self._api_jstore.get_data() + json_data = self._api_jstore.get_data() if switch == 'developer': - return self._json_api['keys'][switch] + return json_data['keys'][switch] decode = True if switch == 'youtube-tv': - api_key = key_sets[switch]['key'] - client_id = key_sets[switch]['id'] - client_secret = key_sets[switch]['secret'] + system = 'YouTube TV' + key_set_details = key_sets[switch] elif switch.startswith('own'): decode = False - api_key = self._json_api['keys']['personal']['api_key'] - client_id = self._json_api['keys']['personal']['client_id'] - client_secret = self._json_api['keys']['personal']['client_secret'] + system = 'All' + key_set_details = json_data['keys']['personal'] else: - api_key = key_sets['provided'][switch]['key'] - client_id = key_sets['provided'][switch]['id'] - client_secret = key_sets['provided'][switch]['secret'] - - if decode: - api_key = b64decode(api_key).decode('utf-8') - client_id = b64decode(client_id).decode('utf-8') - client_secret = b64decode(client_secret).decode('utf-8') - - client_id += '.apps.googleusercontent.com' - return {'key': api_key, - 'id': client_id, - 'secret': client_secret} + system = 'All' + if switch not in key_sets['provided']: + switch = 0 + key_set_details = key_sets['provided'][switch] + + key_set = { + 'system': system, + 'id': '', + 'key': '', + 'secret': '' + } + for key, value in key_set_details.items(): + if decode: + value = b64decode(value).decode('utf-8') + key = key.partition('_')[-1] + if key and key in key_set: + key_set[key] = value + if not key_set['id'].endswith('.apps.googleusercontent.com'): + key_set['id'] += '.apps.googleusercontent.com' + return key_set def _get_key_set_hash(self, switch): key_set = self.get_api_keys(switch) @@ -148,10 +153,9 @@ def _get_key_set_hash(self, switch): if switch == 'own_old': client_id += '.apps.googleusercontent.com' key_set['id'] = client_id - return self._access_manager.calc_key_hash(**key_set) + return self._context.get_access_manager().calc_key_hash(**key_set) def _strip_api_keys(self, api_key, client_id, client_secret): - stripped_key = ''.join(api_key.split()) stripped_id = ''.join(client_id.replace('.apps.googleusercontent.com', '').split()) stripped_secret = ''.join(client_secret.split()) @@ -190,13 +194,3 @@ def _strip_api_keys(self, api_key, client_id, client_secret): return_secret = client_secret return return_key, return_id, return_secret - - -_api_check = APICheck(XbmcContext()) - -keys_changed = _api_check.changed -current_user = _api_check.get_current_user() - -api = _api_check.get_api_keys(_api_check.get_current_switch()) -youtube_tv = _api_check.get_api_keys('youtube-tv') -developer_keys = _api_check.get_api_keys('developer') diff --git a/resources/lib/youtube_plugin/youtube/client/__init__.py b/resources/lib/youtube_plugin/youtube/client/__init__.py index 8a0c95b2d..bd3a6a397 100644 --- a/resources/lib/youtube_plugin/youtube/client/__init__.py +++ b/resources/lib/youtube_plugin/youtube/client/__init__.py @@ -10,7 +10,11 @@ from __future__ import absolute_import, division, unicode_literals +from .__config__ import APICheck from .youtube import YouTube -__all__ = ('YouTube',) +__all__ = ( + 'APICheck', + 'YouTube', +) diff --git a/resources/lib/youtube_plugin/youtube/client/login_client.py b/resources/lib/youtube_plugin/youtube/client/login_client.py index 3dc6d79a8..7a17a6c15 100644 --- a/resources/lib/youtube_plugin/youtube/client/login_client.py +++ b/resources/lib/youtube_plugin/youtube/client/login_client.py @@ -12,12 +12,6 @@ import time -from .__config__ import ( - api, - developer_keys, - keys_changed, - youtube_tv, -) from .request_client import YouTubeRequestClient from ..youtube_exceptions import ( InvalidGrant, @@ -29,8 +23,6 @@ class LoginClient(YouTubeRequestClient): - api_keys_changed = keys_changed - ANDROID_CLIENT_AUTH_URL = 'https://android.clients.google.com/auth' DEVICE_CODE_URL = 'https://accounts.google.com/o/oauth2/device/code' REVOKE_URL = 'https://accounts.google.com/o/oauth2/revoke' @@ -46,29 +38,15 @@ class LoginClient(YouTubeRequestClient): )) TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token' - CONFIGS = { - 'youtube-tv': { - 'system': 'YouTube TV', - 'key': youtube_tv['key'], - 'id': youtube_tv['id'], - 'secret': youtube_tv['secret'] - }, - 'main': { - 'system': 'All', - 'key': api['key'], - 'id': api['id'], - 'secret': api['secret'] - }, - 'developer': developer_keys - } - def __init__(self, - config=None, + configs=None, access_token='', access_token_tv='', **kwargs): - self._config = self.CONFIGS['main'] if config is None else config - self._config_tv = self.CONFIGS['youtube-tv'] + if not configs: + configs = {} + self._config = configs.get('main') or {} + self._config_tv = configs.get('youtube-tv') or {} self._access_token = access_token self._access_token_tv = access_token_tv @@ -132,8 +110,8 @@ def revoke(self, refresh_token): raise_exc=True) def refresh_token_tv(self, refresh_token): - client_id = str(self.CONFIGS['youtube-tv']['id']) - client_secret = str(self.CONFIGS['youtube-tv']['secret']) + client_id = self._config_tv.get('id', '') + client_secret = self._config_tv.get('secret', '') return self.refresh_token(refresh_token, client_id=client_id, client_secret=client_secret) @@ -146,8 +124,8 @@ def refresh_token(self, refresh_token, client_id='', client_secret=''): ' Chrome/61.0.3163.100 Safari/537.36', 'Content-Type': 'application/x-www-form-urlencoded'} - client_id = client_id or self._config['id'] - client_secret = client_secret or self._config['secret'] + client_id = client_id or self._config.get('id', '') + client_secret = client_secret or self._config.get('secret', '') post_data = {'client_id': client_id, 'client_secret': client_secret, 'refresh_token': refresh_token, @@ -181,8 +159,8 @@ def refresh_token(self, refresh_token, client_id='', client_secret=''): return '', '' def request_access_token_tv(self, code, client_id='', client_secret=''): - client_id = client_id or self.CONFIGS['youtube-tv']['id'] - client_secret = client_secret or self.CONFIGS['youtube-tv']['secret'] + client_id = client_id or self._config_tv.get('id', '') + client_secret = client_secret or self._config_tv.get('secret', '') return self.request_access_token(code, client_id=client_id, client_secret=client_secret) @@ -195,8 +173,8 @@ def request_access_token(self, code, client_id='', client_secret=''): ' Chrome/61.0.3163.100 Safari/537.36', 'Content-Type': 'application/x-www-form-urlencoded'} - client_id = client_id or self._config['id'] - client_secret = client_secret or self._config['secret'] + client_id = client_id or self._config.get('id', '') + client_secret = client_secret or self._config.get('secret', '') post_data = {'client_id': client_id, 'client_secret': client_secret, 'code': code, @@ -225,7 +203,7 @@ def request_access_token(self, code, client_id='', client_secret=''): return json_data def request_device_and_user_code_tv(self): - client_id = str(self.CONFIGS['youtube-tv']['id']) + client_id = self._config_tv.get('id', '') return self.request_device_and_user_code(client_id=client_id) def request_device_and_user_code(self, client_id=''): @@ -236,7 +214,7 @@ def request_device_and_user_code(self, client_id=''): ' Chrome/61.0.3163.100 Safari/537.36', 'Content-Type': 'application/x-www-form-urlencoded'} - client_id = client_id or self._config['id'] + client_id = client_id or self._config.get('id', '') post_data = {'client_id': client_id, 'scope': 'https://www.googleapis.com/auth/youtube'} @@ -261,9 +239,6 @@ def request_device_and_user_code(self, client_id=''): raise_exc=True) return json_data - def get_access_token(self): - return self._access_token - def authenticate(self, username, password): headers = {'device': '38c6ee9a82b8b10a', 'app': 'com.google.android.youtube', @@ -310,16 +285,16 @@ def authenticate(self, username, password): def _get_config_type(self, client_id, client_secret=None): """used for logging""" if client_secret is None: - using_conf_tv = client_id == self.CONFIGS['youtube-tv'].get('id') - using_conf_main = client_id == self.CONFIGS['main'].get('id') + using_conf_tv = client_id == self._config_tv.get('id', '') + using_conf_main = client_id == self._config.get('id', '') else: using_conf_tv = ( - client_secret == self.CONFIGS['youtube-tv'].get('secret') - and client_id == self.CONFIGS['youtube-tv'].get('id') + client_secret == self._config_tv.get('secret', '') + and client_id == self._config_tv.get('id', '') ) using_conf_main = ( - client_secret == self.CONFIGS['main'].get('secret') - and client_id == self.CONFIGS['main'].get('id') + client_secret == self._config.get('secret', '') + and client_id == self._config.get('id', '') ) if not using_conf_main and not using_conf_tv: return 'None' diff --git a/resources/lib/youtube_plugin/youtube/client/youtube.py b/resources/lib/youtube_plugin/youtube/client/youtube.py index 010f918ba..85b3890b6 100644 --- a/resources/lib/youtube_plugin/youtube/client/youtube.py +++ b/resources/lib/youtube_plugin/youtube/client/youtube.py @@ -1834,8 +1834,11 @@ def api_request(self, client = self.build_client(version, client_data) if 'key' in client['params'] and not client['params']['key']: - client['params']['key'] = (self._config.get('key') - or self._config_tv['key']) + key = self._config.get('key') or self._config_tv.get('key') + if key: + client['params']['key'] = key + else: + client['params']['key'] if method != 'POST' and 'json' in client: del client['json'] diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_login.py b/resources/lib/youtube_plugin/youtube/helper/yt_login.py index 45d9d4eb0..799f93178 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_login.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_login.py @@ -18,27 +18,36 @@ def process(mode, provider, context, sign_out_refresh=True): addon_id = context.get_param('addon_id', None) + access_manager = context.get_access_manager() + localize = context.localize + ui = context.get_ui() def _do_logout(): - signout_access_manager = context.get_access_manager() if addon_id: - if signout_access_manager.developer_has_refresh_token(addon_id): - refresh_tokens = signout_access_manager.get_dev_refresh_token(addon_id).split('|') - refresh_tokens = list(set(refresh_tokens)) - for _refresh_token in refresh_tokens: - provider.get_client(context).revoke(_refresh_token) - elif signout_access_manager.has_refresh_token(): - refresh_tokens = signout_access_manager.get_refresh_token().split('|') - refresh_tokens = list(set(refresh_tokens)) - for _refresh_token in refresh_tokens: - provider.get_client(context).revoke(_refresh_token) - - provider.reset_client() - - if addon_id: - signout_access_manager.update_dev_access_token(addon_id, access_token='', refresh_token='') + refresh_tokens = access_manager.get_dev_refresh_token(addon_id) + client = provider.get_client(context) + if refresh_tokens: + for _refresh_token in set(refresh_tokens): + try: + client.revoke(_refresh_token) + except LoginException: + pass + access_manager.update_dev_access_token( + addon_id, access_token='', refresh_token='' + ) else: - signout_access_manager.update_access_token(access_token='', refresh_token='') + refresh_tokens = access_manager.get_refresh_token() + client = provider.get_client(context) + if refresh_tokens: + for _refresh_token in set(refresh_tokens): + try: + client.revoke(_refresh_token) + except LoginException: + pass + access_manager.update_access_token( + access_token='', refresh_token='' + ) + provider.reset_client() def _do_login(_for_tv=False): _client = provider.get_client(context) @@ -57,15 +66,19 @@ def _do_login(_for_tv=False): interval = 5 device_code = json_data['device_code'] user_code = json_data['user_code'] - verification_url = json_data.get('verification_url', 'youtube.com/activate').lstrip('https://www.') + verification_url = json_data.get('verification_url') + if verification_url: + verification_url = verification_url.lstrip('https://www.') + else: + verification_url = 'youtube.com/activate' - text = [context.localize('sign.go_to') % context.get_ui().bold(verification_url), - '[CR]%s %s' % (context.localize('sign.enter_code'), - context.get_ui().bold(user_code))] + text = [localize('sign.go_to') % ui.bold(verification_url), + '[CR]%s %s' % (localize('sign.enter_code'), + ui.bold(user_code))] text = ''.join(text) - with context.get_ui().create_progress_dialog( - heading=context.localize('sign.in'), text=text, background=False + with ui.create_progress_dialog( + heading=localize('sign.in'), text=text, background=False ) as dialog: steps = ((10 * 60) // interval) # 10 Minutes dialog.set_total(steps) @@ -102,7 +115,7 @@ def _do_login(_for_tv=False): if json_data['error'] != 'authorization_pending': message = json_data['error'] title = '%s: %s' % (context.get_name(), message) - context.get_ui().show_notification(message, title) + ui.show_notification(message, title) context.log_error('Error requesting access token: |error|' .format(error=message)) @@ -115,47 +128,58 @@ def _do_login(_for_tv=False): if mode == 'out': _do_logout() if sign_out_refresh: - context.get_ui().refresh_container() + ui.refresh_container() elif mode == 'in': - context.get_ui().on_ok(context.localize('sign.twice.title'), - context.localize('sign.twice.text')) + ui.on_ok(localize('sign.twice.title'), localize('sign.twice.text')) - access_token_tv, expires_in_tv, refresh_token_tv = _do_login(_for_tv=True) + tv_token = _do_login(_for_tv=True) + access_token, expires_in, refresh_token = tv_token # abort tv login - context.log_debug('YouTube-TV Login: Access Token |%s| Refresh Token |%s| Expires |%s|' % - (access_token_tv != '', refresh_token_tv != '', expires_in_tv)) - if not access_token_tv and not refresh_token_tv: + context.log_debug('YouTube-TV Login:' + ' Access Token |{0}|,' + ' Refresh Token |{1}|,' + ' Expires |{2}|' + .format(access_token != '', + refresh_token != '', + expires_in)) + if not access_token and not refresh_token: provider.reset_client() if addon_id: - context.get_access_manager().update_dev_access_token(addon_id, '') + access_manager.update_dev_access_token(addon_id) else: - context.get_access_manager().update_access_token('') - context.get_ui().refresh_container() + access_manager.update_access_token('') + ui.refresh_container() return - access_token_kodi, expires_in_kodi, refresh_token_kodi = _do_login(_for_tv=False) + kodi_token = _do_login(_for_tv=False) + access_token, expires_in, refresh_token = kodi_token # abort kodi login - context.log_debug('YouTube-Kodi Login: Access Token |%s| Refresh Token |%s| Expires |%s|' % - (access_token_kodi != '', refresh_token_kodi != '', expires_in_kodi)) - if not access_token_kodi and not refresh_token_kodi: + context.log_debug('YouTube-Kodi Login:' + ' Access Token |{0}|,' + ' Refresh Token |{1}|,' + ' Expires |{2}|' + .format(access_token != '', + refresh_token != '', + expires_in)) + if not access_token and not refresh_token: provider.reset_client() if addon_id: - context.get_access_manager().update_dev_access_token(addon_id, '') + access_manager.update_dev_access_token(addon_id) else: - context.get_access_manager().update_access_token('') - context.get_ui().refresh_container() + access_manager.update_access_token('') + ui.refresh_container() return - access_token = '%s|%s' % (access_token_tv, access_token_kodi) - refresh_token = '%s|%s' % (refresh_token_tv, refresh_token_kodi) - expires_in = min(expires_in_tv, expires_in_kodi) - provider.reset_client() if addon_id: - context.get_access_manager().update_dev_access_token(addon_id, access_token, expires_in, refresh_token) + access_manager.update_dev_access_token( + addon_id, *list(zip(tv_token, kodi_token)) + ) else: - context.get_access_manager().update_access_token(access_token, expires_in, refresh_token) + access_manager.update_access_token( + *list(zip(tv_token, kodi_token)) + ) - context.get_ui().refresh_container() + ui.refresh_container() diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 5ff8aff74..2a05cb020 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -14,7 +14,7 @@ import re from base64 import b64decode -from .client import YouTube +from .client import APICheck, YouTube from .helper import ( ResourceManager, UrlResolver, @@ -89,7 +89,7 @@ def get_dev_config(context, addon_id, dev_configs): if dev_config and not context.get_settings().allow_dev_keys(): context.log_debug('Developer config ignored') - return None + return {} if dev_config: if (not dev_config.get('main') @@ -121,138 +121,142 @@ def reset_client(self): self._client = None def get_client(self, context): - if self._client is not None: - return self._client - # set the items per page (later) - settings = context.get_settings() access_manager = context.get_access_manager() - items_per_page = settings.items_per_page() - - plugin_lang = settings.get_language() - plugin_region = settings.get_region() - - api_last_origin = access_manager.get_last_origin() - - youtube_config = YouTube.CONFIGS.get('main') + api_check = APICheck(context) + configs = { + 'youtube-tv': api_check.get_api_keys('youtube-tv'), + 'main': api_check.get_api_keys(api_check.get_current_switch()), + 'developer': api_check.get_api_keys('developer') + } dev_id = context.get_param('addon_id') - dev_configs = YouTube.CONFIGS.get('developer') - dev_config = self.get_dev_config(context, dev_id, dev_configs) - dev_keys = dev_config.get('main') if dev_config else None - - refresh_tokens = [] - - if dev_id: - origin = dev_config.get('origin') if dev_config.get('origin') else dev_id - else: + if not dev_id or dev_id == ADDON_ID: + dev_id = dev_keys = None origin = ADDON_ID + else: + dev_config = self.get_dev_config( + context, dev_id, configs['developer'] + ) + origin = dev_config.get('origin') or dev_id + dev_keys = dev_config.get('main') + api_last_origin = access_manager.get_last_origin() if api_last_origin != origin: context.log_debug('API key origin changed: |{old}| to |{new}|' .format(old=api_last_origin, new=origin)) access_manager.set_last_origin(origin) + self.reset_client() if dev_id: - access_tokens = access_manager.get_dev_access_token(dev_id).split('|') - if len(access_tokens) != 2 or access_manager.is_dev_access_token_expired(dev_id): - # reset access_token - access_manager.update_dev_access_token(dev_id, '') - access_tokens = [] - else: - access_tokens = access_manager.get_access_token().split('|') - if len(access_tokens) != 2 or access_manager.is_access_token_expired(): + access_tokens = access_manager.get_dev_access_token(dev_id) + if access_manager.is_dev_access_token_expired(dev_id): # reset access_token - access_manager.update_access_token('') access_tokens = [] + access_manager.update_dev_access_token(dev_id, access_tokens) + elif self._client: + return self._client - if dev_id: if dev_keys: - context.log_debug('Selecting YouTube developer config "%s"' % dev_id) + context.log_debug('Selecting YouTube developer config "{0}"' + .format(dev_id)) + configs['main'] = dev_keys else: - context.log_debug('Selecting YouTube config "%s" w/ developer access tokens' % youtube_config['system']) - - if access_manager.developer_has_refresh_token(dev_id): - if dev_keys: - keys_changed = access_manager.dev_keys_changed(dev_id, dev_keys['key'], dev_keys['id'], dev_keys['secret']) - else: - keys_changed = access_manager.dev_keys_changed(dev_id, youtube_config['key'], youtube_config['id'], youtube_config['secret']) - + dev_keys = configs['main'] + context.log_debug('Selecting YouTube config "{0}"' + ' w/ developer access tokens' + .format(dev_keys['system'])) + + refresh_tokens = access_manager.get_dev_refresh_token(dev_id) + if refresh_tokens: + keys_changed = access_manager.dev_keys_changed( + dev_id, dev_keys['key'], dev_keys['id'], dev_keys['secret'] + ) if keys_changed: - context.log_warning('API key set changed: Resetting client and updating access token') + context.log_warning('API key set changed: Resetting client' + ' and updating access token') self.reset_client() - access_manager.update_dev_access_token(dev_id, access_token='', refresh_token='') - - access_tokens = access_manager.get_dev_access_token(dev_id) - if access_tokens: - access_tokens = access_tokens.split('|') - else: access_tokens = [] - - refresh_tokens = access_manager.get_dev_refresh_token(dev_id) - if refresh_tokens: - refresh_tokens = refresh_tokens.split('|') - else: refresh_tokens = [] + access_manager.update_dev_access_token( + dev_id, access_tokens, -1, refresh_tokens + ) - context.log_debug('Access token count: |%d| Refresh token count: |%d|' % (len(access_tokens), len(refresh_tokens))) + context.log_debug( + 'Access token count: |{0}|, refresh token count: |{1}|' + .format(len(access_tokens), len(refresh_tokens)) + ) else: - context.log_debug('Selecting YouTube config "%s"' % youtube_config['system']) - - if access_manager.has_refresh_token(): - if YouTube.api_keys_changed: - context.log_warning('API key set changed: Resetting client and updating access token') + access_tokens = access_manager.get_access_token() + if access_manager.is_access_token_expired(): + # reset access_token + access_tokens = [] + access_manager.update_access_token(access_tokens) + elif self._client: + return self._client + + context.log_debug('Selecting YouTube config "{0}"' + .format(configs['main']['system'])) + + refresh_tokens = access_manager.get_refresh_token() + if refresh_tokens: + if api_check.changed: + context.log_warning('API key set changed: Resetting client' + ' and updating access token') self.reset_client() - access_manager.update_access_token(access_token='', refresh_token='') - - access_tokens = access_manager.get_access_token() - if access_tokens: - access_tokens = access_tokens.split('|') - else: access_tokens = [] - - refresh_tokens = access_manager.get_refresh_token() - if refresh_tokens: - refresh_tokens = refresh_tokens.split('|') - else: refresh_tokens = [] + access_manager.update_access_token( + access_tokens, -1, refresh_tokens + ) - context.log_debug('Access token count: |%d| Refresh token count: |%d|' % (len(access_tokens), len(refresh_tokens))) + context.log_debug( + 'Access token count: |{0}|, refresh token count: |{1}|' + .format(len(access_tokens), len(refresh_tokens)) + ) + settings = context.get_settings() client = YouTube(context=context, - language=plugin_lang, - region=plugin_region, - items_per_page=items_per_page, - config=dev_keys if dev_keys else youtube_config) + language=settings.get_language(), + region=settings.get_region(), + items_per_page=settings.items_per_page(), + configs=configs) with client: - if not refresh_tokens or not refresh_tokens[0]: + if not refresh_tokens: self._client = client # create new access tokens elif len(access_tokens) != 2 and len(refresh_tokens) == 2: try: - access_token_kodi, expires_in_kodi = client.refresh_token(refresh_tokens[1]) - access_token_tv, expires_in_tv = client.refresh_token_tv(refresh_tokens[0]) - access_tokens = [access_token_tv, access_token_kodi] - access_token = '%s|%s' % (access_token_tv, access_token_kodi) - expires_in = min(expires_in_tv, expires_in_kodi) + kodi_token = client.refresh_token(refresh_tokens[1]) + tv_token = client.refresh_token_tv(refresh_tokens[0]) + access_tokens = (tv_token[0], kodi_token[0]) + expires_in = min(tv_token[1], kodi_token[1]) if dev_id: - access_manager.update_dev_access_token(dev_id, access_token, expires_in) + access_manager.update_dev_access_token( + dev_id, access_tokens, expires_in + ) else: - access_manager.update_access_token(access_token, expires_in) + access_manager.update_access_token( + access_tokens, expires_in + ) except (InvalidGrant, LoginException) as exc: self.handle_exception(context, exc) # reset access_token if isinstance(exc, InvalidGrant): if dev_id: - access_manager.update_dev_access_token(dev_id, access_token='', refresh_token='') + access_manager.update_dev_access_token( + dev_id, access_token='', refresh_token='' + ) else: - access_manager.update_access_token(access_token='', refresh_token='') + access_manager.update_access_token( + access_token='', refresh_token='' + ) elif dev_id: - access_manager.update_dev_access_token(dev_id, '') + access_manager.update_dev_access_token(dev_id) else: - access_manager.update_access_token('') + access_manager.update_access_token() # in debug log the login status self._logged_in = len(access_tokens) == 2 @@ -902,23 +906,22 @@ def maintenance_actions(self, context, re_match): if target == 'access_manager' and ui.on_yes_no_input( context.get_name(), localize('reset.access_manager.confirm') ): - try: - access_manager = context.get_access_manager() - client = self.get_client(context) - if access_manager.has_refresh_token(): - refresh_tokens = access_manager.get_refresh_token() - for refresh_token in set(refresh_tokens.split('|')): - try: - client.revoke(refresh_token) - except: - pass - self.reset_client() - access_manager.update_access_token(access_token='', - refresh_token='') - ui.refresh_container() - ui.show_notification(localize('succeeded')) - except: - ui.show_notification(localize('failed')) + access_manager = context.get_access_manager() + client = self.get_client(context) + refresh_tokens = access_manager.get_refresh_token() + success = True + if refresh_tokens: + for refresh_token in set(refresh_tokens): + try: + client.revoke(refresh_token) + except LoginException: + success = False + self.reset_client() + access_manager.update_access_token( + access_token='', refresh_token='' + ) + ui.refresh_container() + ui.show_notification(localize('succeeded' if success else 'failed')) # noinspection PyUnusedLocal @RegisterProviderPath('^/api/update/?$') From 322cb5e81245bbc93248a90900f8307a88256a45 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 6 May 2024 10:48:48 +1000 Subject: [PATCH 47/59] Don't store settings instances - Instances may be stale --- .../youtube_plugin/kodion/json_store/access_manager.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/json_store/access_manager.py b/resources/lib/youtube_plugin/kodion/json_store/access_manager.py index ae808fc72..c49efd55b 100644 --- a/resources/lib/youtube_plugin/kodion/json_store/access_manager.py +++ b/resources/lib/youtube_plugin/kodion/json_store/access_manager.py @@ -32,7 +32,7 @@ class AccessManager(JSONStore): def __init__(self, context): super(AccessManager, self).__init__('access_manager.json') - self._settings = context.get_settings() + self._context = context access_manager_data = self._data['access_manager'] self._user = access_manager_data.get('current_user', 0) self._last_origin = access_manager_data.get('last_origin', ADDON_ID) @@ -283,7 +283,7 @@ def get_watch_later_id(self): """ current_user = self.get_current_user_details() current_id = current_user.get('watch_later', 'WL') - settings_id = self._settings.get_watch_later_playlist() + settings_id = self._context.get_settings().get_watch_later_playlist() if settings_id and current_id != settings_id: current_id = self.set_watch_later_id(settings_id) @@ -301,7 +301,7 @@ def set_watch_later_id(self, playlist_id): if playlist_id.lower().strip() == 'wl': playlist_id = '' - self._settings.set_watch_later_playlist('') + self._context.get_settings().set_watch_later_playlist('') data = { 'access_manager': { 'users': { @@ -321,7 +321,7 @@ def get_watch_history_id(self): """ current_user = self.get_current_user_details() current_id = current_user.get('watch_history', 'HL') - settings_id = self._settings.get_history_playlist() + settings_id = self._context.get_settings().get_history_playlist() if settings_id and current_id != settings_id: current_id = self.set_watch_history_id(settings_id) @@ -339,7 +339,7 @@ def set_watch_history_id(self, playlist_id): if playlist_id.lower().strip() == 'hl': playlist_id = '' - self._settings.set_history_playlist('') + self._context.get_settings().set_history_playlist('') data = { 'access_manager': { 'users': { From 482e3bedff47e2644df0941db0e82b382952624e Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 6 May 2024 14:31:43 +1000 Subject: [PATCH 48/59] Misc tidy ups --- resources/lib/youtube_authentication.py | 9 ++-- .../kodion/plugin/xbmc/xbmc_plugin.py | 1 - .../youtube/helper/video_info.py | 8 ++-- .../lib/youtube_plugin/youtube/provider.py | 47 ++++++++++++++----- 4 files changed, 43 insertions(+), 22 deletions(-) diff --git a/resources/lib/youtube_authentication.py b/resources/lib/youtube_authentication.py index 50d73d803..5156eee9a 100644 --- a/resources/lib/youtube_authentication.py +++ b/resources/lib/youtube_authentication.py @@ -162,9 +162,10 @@ def reset_access_tokens(addon_id): """ if not addon_id or addon_id == ADDON_ID: context = XbmcContext() - context.log_error('Developer reset access tokens: |%s| Invalid addon_id' % addon_id) + context.log_error('Reset addon access tokens - invalid addon_id: |{0}|' + .format(addon_id)) return context = XbmcContext(params={'addon_id': addon_id}) - - access_manager = context.get_access_manager() - access_manager.update_dev_access_token(addon_id, access_token='', refresh_token='') + context.get_access_manager().update_dev_access_token( + addon_id, access_token='', refresh_token='' + ) diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py index 4f55c426e..c65c2449f 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py @@ -24,7 +24,6 @@ ) from ...exceptions import KodionException from ...items import ( - DirectoryItem, audio_listitem, directory_listitem, image_listitem, diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index ef99948a8..89482d022 100644 --- a/resources/lib/youtube_plugin/youtube/helper/video_info.py +++ b/resources/lib/youtube_plugin/youtube/helper/video_info.py @@ -1855,7 +1855,7 @@ def _filter_group(previous_group, previous_stream, item): skip_group = ( new_stream['height'] <= previous_stream['height'] - ) if media_type == 'video' else ( + if media_type == 'video' else new_stream['channels'] <= previous_stream['channels'] ) else: @@ -1864,7 +1864,7 @@ def _filter_group(previous_group, previous_stream, item): skip_group = ( new_stream['height'] == previous_stream['height'] - ) if media_type == 'video' else ( + if media_type == 'video' else 2 == new_stream['channels'] == previous_stream['channels'] ) @@ -1872,7 +1872,7 @@ def _filter_group(previous_group, previous_stream, item): skip_group and new_stream['fps'] == previous_stream['fps'] and new_stream['hdr'] == previous_stream['hdr'] - ) if media_type == 'video' else ( + if media_type == 'video' else skip_group and new_stream['langCode'] == previous_stream['langCode'] and new_stream['role'] == previous_stream['role'] @@ -1926,7 +1926,7 @@ def _filter_group(previous_group, previous_stream, item): if group.startswith(mime_type) and 'auto' in stream_select: label = '{0} [{1}]'.format( stream['langName'] - or self._context.localize('stream.automatic'), + or self._context.localize('stream.automatic'), stream['label'] ) if stream == main_stream[media_type]: diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 2a05cb020..83ba0ad72 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -79,7 +79,9 @@ def get_dev_config(context, addon_id, dev_configs): dev_config = {} if _dev_config: - context.log_debug('Using window property for developer keys is deprecated, instead use the youtube_registration module.') + context.log_warning('Using window property for developer keys is' + ' deprecated. Please use the' + ' youtube_registration module instead') try: dev_config = json.loads(_dev_config) except ValueError: @@ -92,17 +94,26 @@ def get_dev_config(context, addon_id, dev_configs): return {} if dev_config: - if (not dev_config.get('main') - or not dev_config['main'].get('key') - or not dev_config['main'].get('system') - or not dev_config.get('origin') - or not dev_config['main'].get('id') - or not dev_config['main'].get('secret')): - context.log_error('Error loading developer config: |invalid structure| ' - 'expected: |{"origin": ADDON_ID, "main": {"system": SYSTEM_NAME, "key": API_KEY, "id": CLIENT_ID, "secret": CLIENT_SECRET}}|') + dev_main = dev_origin = None + if {'main', 'origin'}.issubset(dev_config): + dev_main = dev_config['main'] + dev_origin = dev_config['origin'] + + if not {'system', 'key', 'id', 'secret'}.issubset(dev_main): + dev_main = None + + if not dev_main: + context.log_error('Invalid developer config: |{dev_config}|\n' + 'expected: |{{' + ' "origin": ADDON_ID,' + ' "main": {{' + ' "system": SYSTEM_NAME,' + ' "key": API_KEY,' + ' "id": CLIENT_ID,' + ' "secret": CLIENT_SECRET' + '}}}}|'.format(dev_config=dev_config)) return {} - dev_origin = dev_config['origin'] - dev_main = dev_config['main'] + dev_system = dev_main['system'] if dev_system == 'JSONStore': dev_key = b64decode(dev_main['key']) @@ -112,8 +123,18 @@ def get_dev_config(context, addon_id, dev_configs): dev_key = dev_main['key'] dev_id = dev_main['id'] dev_secret = dev_main['secret'] - context.log_debug('Using developer config: origin: |{0}| system |{1}|'.format(dev_origin, dev_system)) - return {'origin': dev_origin, 'main': {'id': dev_id, 'secret': dev_secret, 'key': dev_key, 'system': dev_system}} + context.log_debug('Using developer config: ' + '|origin: {origin}, system: {system}|' + .format(origin=dev_origin, system=dev_system)) + return { + 'origin': dev_origin, + 'main': { + 'system': dev_system, + 'id': dev_id, + 'secret': dev_secret, + 'key': dev_key, + } + } return {} From add121c53b693323e67b89a1aeacbdbdb19d4e4f Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 6 May 2024 14:35:02 +1000 Subject: [PATCH 49/59] Fix multiple busy dialog workaround trying to reload/play when playlist is not currently active --- .../lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py index c65c2449f..5cb82e1f0 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py @@ -102,7 +102,11 @@ def run(self, provider, context): 'reloading playlist') num_items = playlist.add_items(items) - max_wait_time = min(position, num_items) + if position: + max_wait_time = min(position, num_items) + else: + position = 1 + max_wait_time = num_items while ui.busy_dialog_active() or playlist.size() < position: max_wait_time -= 1 if max_wait_time < 0: From 4156976cc9079a986ffbfd979622a6e7257aad48 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 6 May 2024 14:46:56 +1000 Subject: [PATCH 50/59] Avoid creating new AccessManager instance when not required --- .../youtube/client/__config__.py | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/client/__config__.py b/resources/lib/youtube_plugin/youtube/client/__config__.py index 81903dc04..db5d5976c 100644 --- a/resources/lib/youtube_plugin/youtube/client/__config__.py +++ b/resources/lib/youtube_plugin/youtube/client/__config__.py @@ -12,7 +12,7 @@ from base64 import b64decode from ... import key_sets -from ...kodion.json_store import APIKeyStore +from ...kodion.json_store import APIKeyStore, AccessManager DEFAULT_SWITCH = 1 @@ -22,12 +22,12 @@ class APICheck(object): def __init__(self, context): self._context = context self._api_jstore = APIKeyStore() - json_data = self._api_jstore.get_data() - access_manager = context.get_access_manager() + self._json_api = self._api_jstore.get_data() + self._access_manager = AccessManager(context) - j_key = json_data['keys']['personal'].get('api_key', '') - j_id = json_data['keys']['personal'].get('client_id', '') - j_secret = json_data['keys']['personal'].get('client_secret', '') + j_key = self._json_api['keys']['personal'].get('api_key', '') + j_id = self._json_api['keys']['personal'].get('client_id', '') + j_secret = self._json_api['keys']['personal'].get('client_secret', '') if j_key and j_id and j_secret: # users are now pasting keys into api_keys.json @@ -35,8 +35,8 @@ def __init__(self, context): stripped_key, stripped_id, stripped_secret = self._strip_api_keys(j_key, j_id, j_secret) if (stripped_key and stripped_id and stripped_secret and (j_key != stripped_key or j_id != stripped_id or j_secret != stripped_secret)): - json_data['keys']['personal'] = {'api_key': stripped_key, 'client_id': stripped_id, 'client_secret': stripped_secret} - self._api_jstore.save(json_data) + self._json_api['keys']['personal'] = {'api_key': stripped_key, 'client_id': stripped_id, 'client_secret': stripped_secret} + self._api_jstore.save(self._json_api) settings = self._context.get_settings() original_key = settings.api_key() @@ -51,13 +51,13 @@ def __init__(self, context): settings.api_secret(own_secret) if (j_key != own_key) or (j_id != own_id) or (j_secret != own_secret): - json_data['keys']['personal'] = {'api_key': own_key, 'client_id': own_id, 'client_secret': own_secret} - self._api_jstore.save(json_data) + self._json_api['keys']['personal'] = {'api_key': own_key, 'client_id': own_id, 'client_secret': own_secret} + self._api_jstore.save(self._json_api) - json_data = self._api_jstore.get_data() - j_key = json_data['keys']['personal'].get('api_key', '') - j_id = json_data['keys']['personal'].get('client_id', '') - j_secret = json_data['keys']['personal'].get('client_secret', '') + self._json_api = self._api_jstore.get_data() + j_key = self._json_api['keys']['personal'].get('api_key', '') + j_id = self._json_api['keys']['personal'].get('client_id', '') + j_secret = self._json_api['keys']['personal'].get('client_secret', '') if (not original_key or not original_id or not original_secret and j_key and j_secret and j_id): @@ -66,7 +66,7 @@ def __init__(self, context): settings.api_secret(j_secret) switch = self.get_current_switch() - user_details = access_manager.get_current_user_details() + user_details = self._access_manager.get_current_user_details() last_hash = user_details.get('last_key_hash', '') current_set_hash = self._get_key_set_hash(switch) @@ -74,7 +74,7 @@ def __init__(self, context): if changed and switch == 'own': changed = self._get_key_set_hash('own_old') != last_hash if not changed: - access_manager.set_last_key_hash(current_set_hash) + self._access_manager.set_last_key_hash(current_set_hash) self.changed = changed self._context.log_debug('User: |{user}|, ' @@ -91,14 +91,14 @@ def __init__(self, context): } ) )) - access_manager.set_last_key_hash(current_set_hash) + self._access_manager.set_last_key_hash(current_set_hash) @staticmethod def get_current_switch(): return 'own' def get_current_user(self): - return self._context.get_access_manager().get_current_user() + return self._access_manager.get_current_user() def has_own_api_keys(self): json_data = self._api_jstore.get_data() @@ -110,9 +110,9 @@ def has_own_api_keys(self): return False def get_api_keys(self, switch): - json_data = self._api_jstore.get_data() + self._json_api = self._api_jstore.get_data() if switch == 'developer': - return json_data['keys'][switch] + return self._json_api['keys'][switch] decode = True if switch == 'youtube-tv': @@ -122,7 +122,7 @@ def get_api_keys(self, switch): elif switch.startswith('own'): decode = False system = 'All' - key_set_details = json_data['keys']['personal'] + key_set_details = self._json_api['keys']['personal'] else: system = 'All' @@ -153,7 +153,7 @@ def _get_key_set_hash(self, switch): if switch == 'own_old': client_id += '.apps.googleusercontent.com' key_set['id'] = client_id - return self._context.get_access_manager().calc_key_hash(**key_set) + return self._access_manager.calc_key_hash(**key_set) def _strip_api_keys(self, api_key, client_id, client_secret): stripped_key = ''.join(api_key.split()) From 98af6b2771ccd7ef5eadcf04a4834d4837e30652 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 6 May 2024 16:56:55 +1000 Subject: [PATCH 51/59] Don't create new instances of APICheck unnecessarily --- .../youtube_plugin/youtube/client/__config__.py | 7 +++++++ resources/lib/youtube_plugin/youtube/provider.py | 15 +++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/client/__config__.py b/resources/lib/youtube_plugin/youtube/client/__config__.py index db5d5976c..618b005ab 100644 --- a/resources/lib/youtube_plugin/youtube/client/__config__.py +++ b/resources/lib/youtube_plugin/youtube/client/__config__.py @@ -194,3 +194,10 @@ def _strip_api_keys(self, api_key, client_id, client_secret): return_secret = client_secret return return_key, return_id, return_secret + + def get_configs(self): + return { + 'youtube-tv': self.get_api_keys('youtube-tv'), + 'main': self.get_api_keys(self.get_current_switch()), + 'developer': self.get_api_keys('developer') + } diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 83ba0ad72..020c326de 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -52,6 +52,7 @@ def __init__(self): self._resource_manager = None self._client = None + self._api_check = None self._logged_in = False self.yt_video = yt_video @@ -140,16 +141,14 @@ def get_dev_config(context, addon_id, dev_configs): def reset_client(self): self._client = None + self._api_check = None def get_client(self, context): access_manager = context.get_access_manager() - api_check = APICheck(context) - configs = { - 'youtube-tv': api_check.get_api_keys('youtube-tv'), - 'main': api_check.get_api_keys(api_check.get_current_switch()), - 'developer': api_check.get_api_keys('developer') - } + if not self._api_check: + self._api_check = APICheck(context) + configs = self._api_check.get_configs() dev_id = context.get_param('addon_id') if not dev_id or dev_id == ADDON_ID: @@ -221,7 +220,7 @@ def get_client(self, context): refresh_tokens = access_manager.get_refresh_token() if refresh_tokens: - if api_check.changed: + if self._api_check.changed: context.log_warning('API key set changed: Resetting client' ' and updating access token') self.reset_client() @@ -744,7 +743,7 @@ def _on_users(self, _context, re_match): def _on_sign(self, context, re_match): sign_out_confirmed = context.get_param('confirmed') mode = re_match.group('mode') - if (mode == 'in') and context.get_access_manager().has_refresh_token(): + if (mode == 'in') and context.get_access_manager().get_refresh_token(): yt_login.process('out', self, context, sign_out_refresh=False) if (not sign_out_confirmed and mode == 'out' From ecfd8bacc59f1ee62d39d67790d06830bb32c928 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 6 May 2024 17:39:17 +1000 Subject: [PATCH 52/59] Reset window history when jumping to home --- resources/lib/youtube_plugin/kodion/abstract_provider.py | 6 ++++-- .../lib/youtube_plugin/kodion/context/abstract_context.py | 1 + resources/lib/youtube_plugin/kodion/items/menu_items.py | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index 2224f4d8d..c5b2fae40 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -215,6 +215,7 @@ def reroute(self, context, re_match=None, path=None, params=None): or params != current_params): result = None function_cache = context.get_function_cache() + window_return = params.pop('window_return', True) try: result, options = function_cache.run( self.navigate, @@ -227,8 +228,9 @@ def reroute(self, context, re_match=None, path=None, params=None): if not result: return False context.get_ui().set_property(REROUTE, path) - context.execute('ActivateWindow(Videos, {0}, return)'.format( - context.create_uri(path, params) + context.execute('ActivateWindow(Videos, {0}{1})'.format( + context.create_uri(path, params), + ', return' if window_return else '', )) return False diff --git a/resources/lib/youtube_plugin/kodion/context/abstract_context.py b/resources/lib/youtube_plugin/kodion/context/abstract_context.py index 761dcda63..c3855547b 100644 --- a/resources/lib/youtube_plugin/kodion/context/abstract_context.py +++ b/resources/lib/youtube_plugin/kodion/context/abstract_context.py @@ -46,6 +46,7 @@ class AbstractContext(object): 'resume', 'screensaver', 'strm', + 'window_return', } _INT_PARAMS = { 'fanart_type', diff --git a/resources/lib/youtube_plugin/kodion/items/menu_items.py b/resources/lib/youtube_plugin/kodion/items/menu_items.py index 0a1ec5638..3a0dd7473 100644 --- a/resources/lib/youtube_plugin/kodion/items/menu_items.py +++ b/resources/lib/youtube_plugin/kodion/items/menu_items.py @@ -548,6 +548,9 @@ def goto_home(context): context.localize(10000), 'RunPlugin({0})'.format(context.create_uri( (paths.ROUTE, paths.HOME,), + { + 'window_return': False, + }, )) ) From eb1b091243e9586360d5e7a7010d4a55be08dfea Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 6 May 2024 18:21:05 +1000 Subject: [PATCH 53/59] Don't store client instances - Instances may use incorrect settings and tokens --- .../youtube/helper/resource_manager.py | 24 +++++++++++-------- .../lib/youtube_plugin/youtube/provider.py | 4 ++-- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py index 954804042..f1e1a9afe 100644 --- a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py +++ b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py @@ -12,9 +12,9 @@ class ResourceManager(object): - def __init__(self, context, client): + def __init__(self, provider, context): self._context = context - self._client = client + self._provider = provider self._data_cache = context.get_data_cache() self._function_cache = context.get_function_cache() fanart_type = context.get_param('fanart_type') @@ -31,6 +31,7 @@ def _list_batch(input_list, n=50): yield input_list[i:i + n] def get_channels(self, ids, defer_cache=False): + client = self._provider.get_client(self._context) refresh = self._context.get_param('refresh') updated = [] for channel_id in ids: @@ -42,7 +43,7 @@ def get_channels(self, ids, defer_cache=False): continue data = self._function_cache.run( - self._client.get_channel_by_username, + client.get_channel_by_username, self._function_cache.ONE_DAY, _refresh=refresh, username=channel_id @@ -69,7 +70,7 @@ def get_channels(self, ids, defer_cache=False): .format(ids=list(result))) if to_update: - new_data = [self._client.get_channels(list_of_50) + new_data = [client.get_channels(list_of_50) for list_of_50 in self._list_batch(to_update, n=50)] if not any(new_data): new_data = None @@ -140,7 +141,8 @@ def get_playlists(self, ids, defer_cache=False): .format(ids=list(result))) if to_update: - new_data = [self._client.get_playlists(list_of_50) + client = self._provider.get_client(self._context) + new_data = [client.get_playlists(list_of_50) for list_of_50 in self._list_batch(to_update, n=50)] if not any(new_data): new_data = None @@ -212,6 +214,7 @@ def get_playlist_items(self, ids=None, batch_id=None, defer_cache=False): self._context.log_debug('Found cached items for playlists:\n|{ids}|' .format(ids=list(result))) + client = self._provider.get_client(self._context) new_data = {} insert_point = 0 for playlist_id, page_token in to_update: @@ -221,7 +224,7 @@ def get_playlist_items(self, ids=None, batch_id=None, defer_cache=False): while 1: batch_id = (playlist_id, page_token) new_batch_ids.append(batch_id) - batch = self._client.get_playlist_items(*batch_id) + batch = client.get_playlist_items(*batch_id) new_data[batch_id] = batch page_token = batch.get('nextPageToken') if fetch_next else None if page_token is None: @@ -284,10 +287,11 @@ def get_videos(self, if to_update: notify_and_raise = not suppress_errors - new_data = [self._client.get_videos(list_of_50, - live_details, - notify=notify_and_raise, - raise_exc=notify_and_raise) + client = self._provider.get_client(self._context) + new_data = [client.get_videos(list_of_50, + live_details, + notify=notify_and_raise, + raise_exc=notify_and_raise) for list_of_50 in self._list_batch(to_update, n=50)] if not any(new_data): new_data = None diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 020c326de..40a080248 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -13,6 +13,7 @@ import json import re from base64 import b64decode +from weakref import proxy from .client import APICheck, YouTube from .helper import ( @@ -294,8 +295,7 @@ def get_client(self, context): def get_resource_manager(self, context): if not self._resource_manager: - # self._resource_manager = ResourceManager(weakref.proxy(context), weakref.proxy(self.get_client(context))) - self._resource_manager = ResourceManager(context, self.get_client(context)) + self._resource_manager = ResourceManager(proxy(self), context) return self._resource_manager # noinspection PyUnusedLocal From 02413ba5c6bf9a63c1a230dc758b5d19fab5e532 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Mon, 6 May 2024 18:22:20 +1000 Subject: [PATCH 54/59] Extend tear down methods to include all stored instances --- .../kodion/context/xbmc/xbmc_context.py | 22 ++++++++++++++----- .../lib/youtube_plugin/youtube/provider.py | 11 +++++++--- 2 files changed, 24 insertions(+), 9 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 da238350c..eb12c24c1 100644 --- a/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py +++ b/resources/lib/youtube_plugin/kodion/context/xbmc/xbmc_context.py @@ -11,7 +11,7 @@ from __future__ import absolute_import, division, unicode_literals import sys -import weakref +from weakref import proxy from ..abstract_context import AbstractContext from ...compatibility import ( @@ -400,27 +400,27 @@ def get_subtitle_language(self): def get_video_playlist(self): if not self._video_playlist: - self._video_playlist = XbmcPlaylist('video', weakref.proxy(self)) + self._video_playlist = XbmcPlaylist('video', proxy(self)) return self._video_playlist def get_audio_playlist(self): if not self._audio_playlist: - self._audio_playlist = XbmcPlaylist('audio', weakref.proxy(self)) + self._audio_playlist = XbmcPlaylist('audio', proxy(self)) return self._audio_playlist def get_video_player(self): if not self._video_player: - self._video_player = XbmcPlayer('video', weakref.proxy(self)) + self._video_player = XbmcPlayer('video', proxy(self)) return self._video_player def get_audio_player(self): if not self._audio_player: - self._audio_player = XbmcPlayer('audio', weakref.proxy(self)) + self._audio_player = XbmcPlayer('audio', proxy(self)) return self._audio_player def get_ui(self): if not self._ui: - self._ui = XbmcContextUI(self._addon, weakref.proxy(self)) + self._ui = XbmcContextUI(self._addon, proxy(self)) return self._ui def get_data_path(self): @@ -695,6 +695,16 @@ def tear_down(self): self.__class__._settings = None except AttributeError: pass + del self._ui + self._ui = None + del self._video_playlist + self._video_playlist = None + del self._audio_playlist + self._audio_playlist = None + del self._video_player + self._video_player = None + del self._audio_player + self._audio_player = None def wakeup(self): self.get_ui().set_property(WAKEUP, 'true') diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index 40a080248..b26424989 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -51,11 +51,9 @@ class Provider(AbstractProvider): def __init__(self): super(Provider, self).__init__() self._resource_manager = None - self._client = None self._api_check = None self._logged_in = False - self.yt_video = yt_video def get_wizard_steps(self, context): @@ -1595,4 +1593,11 @@ def handle_exception(self, context, exception_to_handle): return True def tear_down(self): - pass + del self._resource_manager + self._resource_manager = None + del self._client + self._client = None + del self._api_check + self._api_check = None + del self.yt_video + self.yt_video = None From bbd16db94fd9df6c2d3aebefec56ea15f8e9c72c Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 7 May 2024 05:00:31 +1000 Subject: [PATCH 55/59] Remove unnecessary client resets --- .../lib/youtube_plugin/kodion/monitors/player_monitor.py | 4 ---- .../lib/youtube_plugin/youtube/helper/yt_setup_wizard.py | 1 - 2 files changed, 5 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/monitors/player_monitor.py b/resources/lib/youtube_plugin/kodion/monitors/player_monitor.py index 98102c9e5..0c1bddaf5 100644 --- a/resources/lib/youtube_plugin/kodion/monitors/player_monitor.py +++ b/resources/lib/youtube_plugin/kodion/monitors/player_monitor.py @@ -165,8 +165,6 @@ def run(self): # only report state='paused' once if state == 'playing' or last_state == 'playing': - # refresh client, tokens may need refreshing - self._provider.reset_client() client = self._provider.get_client(self._context) logged_in = self._provider.is_logged_in() @@ -200,9 +198,7 @@ def run(self): total=self.total_time, percent=self.progress)) - # refresh client, tokens may need refreshing if logged_in: - self._provider.reset_client() client = self._provider.get_client(self._context) logged_in = self._provider.is_logged_in() 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 9054b19bd..a7301fc38 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_setup_wizard.py @@ -284,7 +284,6 @@ def _get_selected_region(item): settings = context.get_settings() settings.set_language(language_id) settings.set_region(region_id) - provider.reset_client() return step From 2e0abfca9c497423bb29dc1d93c7e82eaa289c6d Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 7 May 2024 05:26:42 +1000 Subject: [PATCH 56/59] Add support for timeshift_bufferlimit in inputstream.adaptive.manifest_config property - Currently hardcoded to default value of 4 hours i.e property does nothing - TODO: Make configurable or auto adjusting --- .../lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py b/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py index 8411ead6c..b05649603 100644 --- a/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py +++ b/resources/lib/youtube_plugin/kodion/items/xbmc/xbmc_items.py @@ -10,6 +10,8 @@ from __future__ import absolute_import, division, unicode_literals +from json import dumps + from .. import AudioItem, DirectoryItem, ImageItem, VideoItem from ...compatibility import xbmc, xbmcgui from ...constants import SWITCH_PLAYER_FLAG @@ -381,7 +383,12 @@ def video_playback_item(context, video_item, show_fanart=None, **_kwargs): 'inputstreamaddon') props[inputstream_property] = 'inputstream.adaptive' - if not current_system_version.compatible(21, 0): + if current_system_version.compatible(21, 0): + if video_item.live: + props['inputstream.adaptive.manifest_config'] = dumps({ + 'timeshift_bufferlimit': 4 * 60 * 60, + }) + else: props['inputstream.adaptive.manifest_type'] = manifest_type if headers: From 654812d76b745e25d2b99de9681fc7e2fcf44037 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 7 May 2024 10:15:43 +1000 Subject: [PATCH 57/59] Improve player monitoring when seeking #746 --- .../kodion/constants/__init__.py | 2 + .../kodion/monitors/player_monitor.py | 86 ++++++++++--------- .../youtube_plugin/youtube/helper/yt_play.py | 5 +- 3 files changed, 49 insertions(+), 44 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/constants/__init__.py b/resources/lib/youtube_plugin/kodion/constants/__init__.py index 58bdc8a5f..d9eca4807 100644 --- a/resources/lib/youtube_plugin/kodion/constants/__init__.py +++ b/resources/lib/youtube_plugin/kodion/constants/__init__.py @@ -35,6 +35,7 @@ ABORT_FLAG = 'abort_requested' BUSY_FLAG = 'busy' CHECK_SETTINGS = 'check_settings' +PLAYER_DATA = 'player_json' PLAYLIST_PATH = 'playlist_path' PLAYLIST_POSITION = 'playlist_position' REROUTE = 'reroute' @@ -51,6 +52,7 @@ 'CHECK_SETTINGS', 'DATA_PATH', 'MEDIA_PATH', + 'PLAYER_DATA', 'PLAYLIST_PATH', 'PLAYLIST_POSITION', 'RESOURCE_PATH', diff --git a/resources/lib/youtube_plugin/kodion/monitors/player_monitor.py b/resources/lib/youtube_plugin/kodion/monitors/player_monitor.py index 0c1bddaf5..3bdbda931 100644 --- a/resources/lib/youtube_plugin/kodion/monitors/player_monitor.py +++ b/resources/lib/youtube_plugin/kodion/monitors/player_monitor.py @@ -14,7 +14,7 @@ import threading from ..compatibility import xbmc -from ..constants import BUSY_FLAG, SWITCH_PLAYER_FLAG +from ..constants import BUSY_FLAG, PLAYER_DATA, SWITCH_PLAYER_FLAG class PlayerMonitorThread(threading.Thread): @@ -34,8 +34,8 @@ def __init__(self, player, provider, context, monitor, playback_json): self.channel_id = self.playback_json.get('channel_id') self.video_status = self.playback_json.get('video_status') - self.total_time = 0.0 self.current_time = 0.0 + self.total_time = 0.0 self.progress = 0 self.daemon = True @@ -62,7 +62,7 @@ def run(self): timeout_period = 5 waited = 0 - wait_interval = 0.2 + wait_interval = 0.5 while not player.isPlaying(): if self._context.abort_requested(): break @@ -98,70 +98,62 @@ def run(self): video_id_param = 'video_id=%s' % self.video_id report_url = use_remote_history and playback_stats.get('watchtime_url') - segment_start = 0 - played_time = -1.0 - wait_interval = 0.5 + segment_start = 0.0 + report_time = -1.0 + wait_interval = 1 report_period = waited = 10 while not self.abort_now(): try: current_file = player.getPlayingFile() - self.current_time = player.getTime() - self.total_time = player.getTotalTime() + played_time = player.getTime() + total_time = player.getTotalTime() + player.current_time = played_time + player.total_time = total_time except RuntimeError: self.stop() break - if (current_file != playing_file and not ( + if ((current_file != playing_file and not ( self._context.is_plugin_path(current_file, 'play/') - and video_id_param in current_file)): + and video_id_param in current_file + )) or total_time <= 0): self.stop() break - if self.current_time < 0: - self.current_time = 0.0 - - if self.total_time <= 0: - self.stop() - break - self.progress = int(100 * self.current_time / self.total_time) + _seek_time = player.start_time or player.seek_time + if waited and _seek_time and played_time < _seek_time: + waited = 0 + player.seekTime(_seek_time) + continue - if player.start_time or player.seek_time: - _seek_time = player.start_time or player.seek_time - if self.current_time < _seek_time: - player.seekTime(_seek_time) - try: - self.current_time = player.getTime() - except RuntimeError: - self.stop() - break - - if player.end_time and self.current_time >= player.end_time: - if clip and player.start_time: + if player.end_time and played_time >= player.end_time: + if waited and clip and player.start_time: + waited = 0 player.seekTime(player.start_time) - else: - player.stop() + continue + player.stop() if waited >= report_period: waited = 0 last_state = state - if self.current_time == played_time: + if played_time == report_time: state = 'paused' else: state = 'playing' - played_time = self.current_time + report_time = played_time if logged_in and report_url: if state == 'playing': - segment_end = self.current_time + segment_end = played_time else: segment_end = segment_start if segment_start > segment_end: segment_end = segment_start + report_period - if segment_end > self.total_time: - segment_end = self.total_time + if segment_end > total_time: + segment_end = total_time # only report state='paused' once if state == 'playing' or last_state == 'playing': @@ -173,7 +165,7 @@ def run(self): self._context, self.video_id, report_url, - status=(self.current_time, + status=(played_time, segment_start, segment_end, state), @@ -184,6 +176,11 @@ def run(self): self._monitor.waitForAbort(wait_interval) waited += wait_interval + self.current_time = player.current_time + self.total_time = player.total_time + if self.total_time > 0: + self.progress = int(100 * self.current_time / self.total_time) + state = 'stopped' self._context.send_notification('PlaybackStopped', { 'video_id': self.video_id, @@ -204,7 +201,7 @@ def run(self): if self.progress >= settings.get_play_count_min_percent(): play_count += 1 - self.current_time = 0.0 + self.current_time = 0 segment_end = self.total_time else: segment_end = self.current_time @@ -305,6 +302,8 @@ def __init__(self, provider, context, monitor): self.seek_time = None self.start_time = None self.end_time = None + self.current_time = None + self.total_time = None def stop_threads(self): for thread in self.threads: @@ -360,10 +359,10 @@ def onAVStarted(self): if not self._ui.busy_dialog_active(): self._ui.clear_property(BUSY_FLAG) - playback_json = self._ui.get_property('playback_json') + playback_json = self._ui.get_property(PLAYER_DATA) if not playback_json: return - self._ui.clear_property('playback_json') + self._ui.clear_property(PLAYER_DATA) self.cleanup_threads() playback_json = json.loads(playback_json) @@ -371,10 +370,14 @@ def onAVStarted(self): self.seek_time = float(playback_json.get('seek_time')) self.start_time = float(playback_json.get('start_time')) self.end_time = float(playback_json.get('end_time')) - except (ValueError, TypeError): + self.current_time = max(0.0, self.getTime()) + self.total_time = max(0.0, self.getTotalTime()) + except (ValueError, TypeError, RuntimeError): self.seek_time = None self.start_time = None self.end_time = None + self.current_time = 0.0 + self.total_time = 0.0 self.threads.append(PlayerMonitorThread(self, self._provider, @@ -400,6 +403,7 @@ def onPlayBackError(self): def onPlayBackSeek(self, time, seekOffset): time_s = time / 1000 + self.current_time = time_s self.seek_time = None if ((self.end_time and time_s > self.end_time + 1) or (self.start_time and time_s < self.start_time - 1)): diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_play.py b/resources/lib/youtube_plugin/youtube/helper/yt_play.py index f7fe89fa3..d09675bee 100644 --- a/resources/lib/youtube_plugin/youtube/helper/yt_play.py +++ b/resources/lib/youtube_plugin/youtube/helper/yt_play.py @@ -17,7 +17,7 @@ from ..helper import utils, v3 from ..youtube_exceptions import YouTubeException from ...kodion.compatibility import urlencode, urlunsplit -from ...kodion.constants import SWITCH_PLAYER_FLAG, paths +from ...kodion.constants import PLAYER_DATA, SWITCH_PLAYER_FLAG, paths from ...kodion.items import VideoItem from ...kodion.network import get_connect_address from ...kodion.utils import select_stream @@ -147,8 +147,7 @@ def play_video(provider, context): 'refresh_only': screensaver } - ui.set_property('playback_json', json.dumps(playback_json, - ensure_ascii=False)) + ui.set_property(PLAYER_DATA, json.dumps(playback_json, ensure_ascii=False)) context.send_notification('PlaybackInit', { 'video_id': video_id, 'channel_id': playback_json.get('channel_id', ''), From c0458d5a0c3ee75e24da34b6fcea8176ab409e6f Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Tue, 7 May 2024 22:04:07 +1000 Subject: [PATCH 58/59] Fix not detecting headers appended to curl request url #746 --- .../lib/youtube_plugin/kodion/monitors/player_monitor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/lib/youtube_plugin/kodion/monitors/player_monitor.py b/resources/lib/youtube_plugin/kodion/monitors/player_monitor.py index 3bdbda931..0604c10f7 100644 --- a/resources/lib/youtube_plugin/kodion/monitors/player_monitor.py +++ b/resources/lib/youtube_plugin/kodion/monitors/player_monitor.py @@ -113,10 +113,10 @@ def run(self): self.stop() break - if ((current_file != playing_file and not ( + if (not current_file.startswith(playing_file) and not ( self._context.is_plugin_path(current_file, 'play/') and video_id_param in current_file - )) or total_time <= 0): + )) or total_time <= 0: self.stop() break From 7e20b40255f41681d1d31e5026c484293d394998 Mon Sep 17 00:00:00 2001 From: MoojMidge <56883549+MoojMidge@users.noreply.github.com> Date: Wed, 8 May 2024 07:04:35 +1000 Subject: [PATCH 59/59] Update changelog --- changelog.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 3ae2d2ea6..65cda863e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -7,12 +7,13 @@ - Workaround for new settings interface not updated correctly - Fix bookmarks icon background colour - Fix adding channel items directly to bookmarks +- Fixes for player monitoring preventing item being marked as watched #746 ### Changed - Removed Settings > Advanced > Views > Show channel fanart - Now included as option in Settings > Advanced > Views > Show fanart - MPEG-DASH for live streams only enabled by default in Kodi v21 -- Make better use of reuselanguageinvoker +- Make better use of reuselanguageinvoker and various memory usage improvements ### New - Improvements to plugin page navigation #715