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..65cda863e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,41 @@ +## 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 +- 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 and various memory usage improvements + +### 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 diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 80d336e8a..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" @@ -1528,3 +1528,11 @@ msgstr "" msgctxt "#30805" msgid "Use adaptive streaming formats with external player" msgstr "" + +msgctxt "#30806" +msgid "Jump to page..." +msgstr "" + +msgctxt "#30807" +msgid "Use channel name as" +msgstr "" 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 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_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/__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/abstract_provider.py b/resources/lib/youtube_plugin/kodion/abstract_provider.py index 8e89dd1df..c5b2fae40 100644 --- a/resources/lib/youtube_plugin/kodion/abstract_provider.py +++ b/resources/lib/youtube_plugin/kodion/abstract_provider.py @@ -12,13 +12,13 @@ import re -from .constants import content, paths +from .constants import CHECK_SETTINGS, REROUTE, content, paths from .exceptions import KodionException from .items import ( DirectoryItem, NewSearchItem, + NextPageItem, SearchHistoryItem, - menu_items, ) from .utils import to_unicode @@ -32,61 +32,81 @@ def __init__(self): self._dict_path = {} # register some default paths - self.register_path(r'^/$', '_internal_root') + self.register_path(r''.join(( + '^', + '(?:', paths.HOME, ')?/?$' + )), self._internal_root) + + self.register_path(r''.join(( + '^', + paths.ROUTE, + '(?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, '/(?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 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): + 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) + + 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() ui = context.get_ui() - context.send_notification('check_settings', 'defer') + context.send_notification(CHECK_SETTINGS, 'defer') wizard_steps = self.get_wizard_steps(context) @@ -105,8 +125,8 @@ def run_wizard(self, context): else: step += 1 finally: - settings.set_bool(settings.SETUP_WIZARD, False) - context.send_notification('check_settings', 'process') + settings.setup_wizard_enabled(False) + context.send_notification(CHECK_SETTINGS, 'process') def get_wizard_steps(self, context): # can be overridden by the derived class @@ -114,25 +134,25 @@ def get_wizard_steps(self, context): def navigate(self, context): path = context.get_path() + for re_path, method in self._dict_path.items(): + re_match = re_path.search(path) + if not re_match: + 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, + } - for key in self._dict_path: - 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 + refresh = context.get_param('refresh') + if refresh is not None: + options[self.RESULT_UPDATE_LISTING] = bool(refresh) + + return result, options raise KodionException("Mapping for path '%s' not found" % path) @@ -164,6 +184,56 @@ 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): + current_path = context.get_path() + current_params = context.get_params() + if re_match: + path = re_match.group('path') + if params is None: + 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() + window_return = params.pop('window_return', True) + 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(REROUTE, path) + context.execute('ActivateWindow(Videos, {0}{1})'.format( + context.create_uri(path, params), + ', return' if window_return else '', + )) + return False + def on_bookmarks(self, context, re_match): raise NotImplementedError() @@ -211,8 +281,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) @@ -260,7 +331,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/constants/__init__.py b/resources/lib/youtube_plugin/kodion/constants/__init__.py index cdc7218d4..d9eca4807 100644 --- a/resources/lib/youtube_plugin/kodion/constants/__init__.py +++ b/resources/lib/youtube_plugin/kodion/constants/__init__.py @@ -32,23 +32,37 @@ 'true': True, } +ABORT_FLAG = 'abort_requested' BUSY_FLAG = 'busy' -SWITCH_PLAYER_FLAG = 'switch_player' +CHECK_SETTINGS = 'check_settings' +PLAYER_DATA = 'player_json' +PLAYLIST_PATH = 'playlist_path' PLAYLIST_POSITION = 'playlist_position' +REROUTE = 'reroute' +SLEEPING = 'sleeping' +SWITCH_PLAYER_FLAG = 'switch_player' WAIT_FLAG = 'builtin_running' +WAKEUP = 'wakeup' __all__ = ( + 'ABORT_FLAG', 'ADDON_ID', 'ADDON_PATH', 'BUSY_FLAG', + 'CHECK_SETTINGS', 'DATA_PATH', 'MEDIA_PATH', + 'PLAYER_DATA', + 'PLAYLIST_PATH', '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/constants/const_paths.py b/resources/lib/youtube_plugin/kodion/constants/const_paths.py index 669984b39..5791f26d3 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_paths.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_paths.py @@ -13,11 +13,14 @@ BOOKMARKS = '/kodion/bookmarks' EXTERNAL_SEARCH = '/search' +GOTO_PAGE = '/kodion/goto_page' +ROUTE = '/kodion/route' SEARCH = '/kodion/search' WATCH_LATER = '/kodion/watch_later' 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' diff --git a/resources/lib/youtube_plugin/kodion/constants/const_settings.py b/resources/lib/youtube_plugin/kodion/constants/const_settings.py index a49ebe94c..8b80d4a38 100644 --- a/resources/lib/youtube_plugin/kodion/constants/const_settings.py +++ b/resources/lib/youtube_plugin/kodion/constants/const_settings.py @@ -55,12 +55,16 @@ 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) 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 e7012c7fe..c3855547b 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): @@ -43,16 +43,21 @@ class AbstractContext(object): 'logged_in', 'play', 'prompt_for_subtitles', - 'refresh', 'resume', 'screensaver', 'strm', + 'window_return', } _INT_PARAMS = { + 'fanart_type', 'live', 'next_page_token', 'offset', 'page', + 'refresh', + } + _INT_BOOL_PARAMS = { + 'refresh', } _FLOAT_PARAMS = { 'seek', @@ -97,13 +102,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 @@ -112,14 +111,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 = 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) @@ -176,7 +174,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) @@ -226,11 +224,11 @@ 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: - uri = '/' + uri = '/' if params else '/?' uri = self._plugin_id.join(('plugin://', uri)) @@ -239,11 +237,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 @@ -261,7 +280,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: @@ -343,9 +368,9 @@ def get_id(self): return self._plugin_id def get_handle(self): - raise NotImplementedError() + return self._plugin_handle - def get_settings(self): + def get_settings(self, flush=False): raise NotImplementedError() def localize(self, text_id, default_text=None): @@ -400,3 +425,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 eb5b9c099..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 ( @@ -22,7 +22,7 @@ xbmcaddon, xbmcplugin, ) -from ...constants import 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 @@ -39,6 +39,7 @@ class XbmcContext(AbstractContext): _addon = None + _settings = None _KODI_UI_SUBTITLE_OPTIONS = None @@ -133,8 +134,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, @@ -275,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 = { @@ -292,57 +295,55 @@ 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) - if plugin_id and plugin_id != ADDON_ID: - self._addon = xbmcaddon.Addon(id=plugin_id) - - """ - 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._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) 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_id = plugin_id or ADDON_ID - 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')) - self._settings = XbmcPluginSettings(self._addon) + + 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 @@ -399,39 +400,43 @@ 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_handle(self): - return self._plugin_handle - def get_data_path(self): return self._data_path 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 @@ -526,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 @@ -585,8 +588,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(): @@ -661,7 +663,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): @@ -683,5 +685,27 @@ 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 + 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') + self.send_notification(WAKEUP, True) 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: 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/base_item.py b/resources/lib/youtube_plugin/kodion/items/base_item.py index c0eee5ff6..d0d69e0e7 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' @@ -68,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): @@ -195,14 +192,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/menu_items.py b/resources/lib/youtube_plugin/kodion/items/menu_items.py index 2ba6ce9b9..3a0dd7473 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, )) ) @@ -30,8 +32,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 +44,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 +56,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, }, @@ -71,11 +73,12 @@ def play_with(context): def refresh(context): + params = context.get_params() return ( context.localize('refresh'), - 'ActivateWindow(Videos, {0}, return)'.format(context.create_uri( - context.get_path(), - dict(context.get_params(), refresh=True), + 'RunPlugin({0})'.format(context.create_uri( + (paths.ROUTE, context.get_path(),), + 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, )) ) @@ -307,8 +312,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 +502,7 @@ def bookmarks_clear(context): )) ) + def search_remove(context, query): return ( context.localize('search.remove'), @@ -528,3 +534,41 @@ def search_clear(context): (paths.SEARCH, 'clear',), )) ) + + +def separator(): + return ( + '--------', + 'noop' + ) + + +def goto_home(context): + return ( + context.localize(10000), + 'RunPlugin({0})'.format(context.create_uri( + (paths.ROUTE, paths.HOME,), + { + 'window_return': False, + }, + )) + ) + + +def goto_quick_search(context): + return ( + context.localize('search.quick'), + 'RunPlugin({0})'.format(context.create_uri( + (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 e1c9c3d81..55886c9f7 100644 --- a/resources/lib/youtube_plugin/kodion/items/next_page_item.py +++ b/resources/lib/youtube_plugin/kodion/items/next_page_item.py @@ -10,26 +10,65 @@ from __future__ import absolute_import, division, unicode_literals +from . import menu_items from .directory_item import DirectoryItem 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'] + + page = params.get('page', 2) + items_per_page = params.get('items_per_page', 50) + if 'page_token' not in params: + params['page_token'] = self.create_page_token(page, items_per_page) + + super(NextPageItem, self).__init__( + 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) - self.next_page = True + 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(), + ] + self.set_context_menu(context_menu) + + @classmethod + def create_page_token(cls, page, items_per_page=50): + 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/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 0d1f6ed8e..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,9 +10,11 @@ from __future__ import absolute_import, division, unicode_literals -from .. import AudioItem, DirectoryItem, ImageItem, UriItem, VideoItem -from ...constants import SWITCH_PLAYER_FLAG +from json import dumps + +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 @@ -31,6 +33,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 +102,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) @@ -175,6 +186,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) @@ -183,26 +198,16 @@ 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}] - # 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" @@ -212,6 +217,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 @@ -248,8 +261,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 +300,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) @@ -323,7 +339,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) @@ -367,7 +383,12 @@ def video_playback_item(context, video_item, show_fanart=None): '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: @@ -400,7 +421,7 @@ def video_playback_item(context, video_item, show_fanart=None): 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', @@ -416,7 +437,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) @@ -434,7 +455,7 @@ def audio_listitem(context, audio_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 = audio_item.get_image() or 'DefaultAudio.png' list_item.setArt({ 'icon': image, @@ -448,6 +469,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 @@ -480,7 +503,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, @@ -525,7 +548,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, @@ -542,7 +565,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) @@ -613,7 +636,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', @@ -631,17 +654,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/json_store/access_manager.py b/resources/lib/youtube_plugin/kodion/json_store/access_manager.py index 702d6e9d8..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': { @@ -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,15 +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() - 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/monitors/player_monitor.py b/resources/lib/youtube_plugin/kodion/monitors/player_monitor.py index 98102c9e5..0604c10f7 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,75 +98,65 @@ 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 (not current_file.startswith(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': - # refresh client, tokens may need refreshing - self._provider.reset_client() client = self._provider.get_client(self._context) logged_in = self._provider.is_logged_in() @@ -175,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), @@ -186,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, @@ -200,15 +195,13 @@ 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() 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 @@ -309,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: @@ -364,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) @@ -375,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, @@ -404,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/kodion/monitors/service_monitor.py b/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py index bd0817701..e6441de2b 100644 --- a/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py +++ b/resources/lib/youtube_plugin/kodion/monitors/service_monitor.py @@ -12,8 +12,8 @@ import json import threading -from ..compatibility import xbmc, xbmcaddon -from ..constants import ADDON_ID +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 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}|' @@ -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)) @@ -73,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( @@ -165,9 +172,11 @@ 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 + + def tear_down(self): + self._settings.flush() 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/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 16c3feb6c..5cb82e1f0 100644 --- a/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py +++ b/resources/lib/youtube_plugin/kodion/plugin/xbmc/xbmc_plugin.py @@ -13,32 +13,52 @@ 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, + CHECK_SETTINGS, + PLAYLIST_PATH, + PLAYLIST_POSITION, + REROUTE, + SLEEPING, +) 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 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': @@ -51,12 +71,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(): @@ -69,13 +100,13 @@ 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 - max_wait_time = min(position, num_items) + num_items = playlist.add_items(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: @@ -87,83 +118,85 @@ 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 settings.is_setup_wizard_enabled(): + if ui.get_property(SLEEPING): + context.wakeup() + 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: + settings = context.get_settings() + + if settings.setup_wizard_enabled(): provider.run_wizard(context) try: - results = provider.navigate(context) + route = ui.get_property(REROUTE) + 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(REROUTE) + else: + 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.fanart_selection() + 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: @@ -172,23 +205,26 @@ def _set_resolved_url(self, context, base_item, show_fanart): ui.set_property(BUSY_FLAG, 'true') playlist = XbmcPlaylist('auto', context) position, _ = playlist.get_position() - ui.set_property(PLAYLIST_POSITION, str(position)) - - item = playback_item(context, base_item, show_fanart) + 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, + base_item, + show_fanart=context.get_settings().fanart_selection(), + 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 diff --git a/resources/lib/youtube_plugin/kodion/plugin_runner.py b/resources/lib/youtube_plugin/kodion/plugin_runner.py index 44509c2b5..4f3eabbf7 100644 --- a/resources/lib/youtube_plugin/kodion/plugin_runner.py +++ b/resources/lib/youtube_plugin/kodion/plugin_runner.py @@ -10,57 +10,57 @@ 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(context=_context, + plugin=_plugin, + provider=_provider, + profiler=_profiler): + 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/kodion/script_actions.py b/resources/lib/youtube_plugin/kodion/script_actions.py index f6db310f2..af32894cb 100644 --- a/resources/lib/youtube_plugin/kodion/script_actions.py +++ b/resources/lib/youtube_plugin/kodion/script_actions.py @@ -29,9 +29,9 @@ 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.set_bool('kodion.video.quality.isa', False) + settings.use_isa(False) elif action == 'inputstreamhelper': try: @@ -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/kodion/service_runner.py b/resources/lib/youtube_plugin/kodion/service_runner.py index 447be67a8..aadb2f17b 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, SLEEPING, TEMP_PATH, WAKEUP 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(), @@ -33,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) @@ -42,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(): @@ -53,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 @@ -64,7 +69,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) @@ -72,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/abstract_settings.py b/resources/lib/youtube_plugin/kodion/settings/abstract_settings.py index 7c825657a..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: @@ -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 @@ -150,10 +158,28 @@ 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, value=None): + default = 1 + 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] def safe_search(self): index = self.get_int(settings.SAFE_SEARCH, 0) @@ -380,9 +406,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() @@ -409,3 +441,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/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) 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..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,18 +48,39 @@ 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 """ 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__, + ) + 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() def get_result(self, func, *args, **kwargs): @@ -64,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) @@ -83,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 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..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 @@ -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 @@ -144,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/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..556ade575 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 @@ -360,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 diff --git a/resources/lib/youtube_plugin/youtube/client/__config__.py b/resources/lib/youtube_plugin/youtube/client/__config__.py index 9cc843198..618b005ab 100644 --- a/resources/lib/youtube_plugin/youtube/client/__config__.py +++ b/resources/lib/youtube_plugin/youtube/client/__config__.py @@ -12,7 +12,6 @@ from base64 import b64decode from ... import key_sets -from ...kodion.context import XbmcContext from ...kodion.json_store import APIKeyStore, AccessManager @@ -22,17 +21,9 @@ 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 - - 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', '') @@ -47,16 +38,17 @@ def _on_init(self): self._json_api['keys']['personal'] = {'api_key': stripped_key, 'client_id': stripped_id, 'client_secret': stripped_secret} self._api_jstore.save(self._json_api) - 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} @@ -69,9 +61,9 @@ def _on_init(self): 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() @@ -91,8 +83,14 @@ 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._context.execute('RunPlugin({0})'.format( + self._context.create_uri( + ('sign', 'out'), + { + 'confirmed': True, + } + ) + )) self._access_manager.set_last_key_hash(current_set_hash) @staticmethod @@ -103,11 +101,13 @@ def get_current_user(self): return self._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() @@ -116,30 +116,35 @@ def get_api_keys(self, 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 = self._json_api['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) @@ -151,7 +156,6 @@ def _get_key_set_hash(self, switch): 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()) stripped_id = ''.join(client_id.replace('.apps.googleusercontent.com', '').split()) stripped_secret = ''.join(client_secret.split()) @@ -191,12 +195,9 @@ def _strip_api_keys(self, api_key, client_id, 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') + 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/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 e6b5814e1..85b3890b6 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 @@ -144,30 +143,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 @@ -227,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 @@ -590,10 +579,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', @@ -1214,7 +1200,6 @@ def get_related_videos(self, 'browseId' )) - thumb_getter = itemgetter(0, -1) if retry == 1: related_videos = chain.from_iterable(related_videos) @@ -1238,10 +1223,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', @@ -1639,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: @@ -1656,34 +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': {'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 + _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: @@ -1725,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 @@ -1835,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/resource_manager.py b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py index 05ac7f873..f1e1a9afe 100644 --- a/resources/lib/youtube_plugin/youtube/helper/resource_manager.py +++ b/resources/lib/youtube_plugin/youtube/helper/resource_manager.py @@ -12,14 +12,15 @@ 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() - 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 @@ -30,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: @@ -41,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 @@ -68,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 @@ -99,12 +101,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', {}) @@ -135,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 @@ -207,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: @@ -216,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: @@ -279,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/helper/tv.py b/resources/lib/youtube_plugin/youtube/helper/tv.py index 55fe39805..bc101beb7 100644 --- a/resources/lib/youtube_plugin/youtube/helper/tv.py +++ b/resources/lib/youtube_plugin/youtube/helper/tv.py @@ -68,14 +68,14 @@ 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): - new_params = dict(context.get_params(), + 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, - 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 @@ -117,14 +117,14 @@ 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): - new_params = dict(context.get_params(), + 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, - 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 @@ -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: @@ -170,14 +170,14 @@ 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): - new_params = dict(context.get_params(), + 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, - 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/utils.py b/resources/lib/youtube_plugin/youtube/helper/utils.py index 92b9709b3..7aa6d30c1 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, @@ -173,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', @@ -228,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 ) ) @@ -269,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): @@ -390,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_size = settings.get_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() @@ -485,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( @@ -508,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') @@ -574,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(( @@ -585,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 @@ -646,10 +651,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: @@ -691,7 +692,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( @@ -752,6 +753,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( @@ -775,7 +780,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) @@ -795,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())) @@ -847,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 b71f37043..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.use_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': @@ -372,7 +384,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 @@ -383,33 +395,43 @@ 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: - client = provider.get_client(context) - yt_next_page_token = client.calculate_next_page_token( - page + 1, yt_results_per_page - ) + params = context.get_params() + 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 + 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 current_page * yt_results_per_page < yt_total_results: + new_params['items_per_page'] = yt_results_per_page + else: + 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 - new_params = dict(context.get_params(), - page_token=yt_next_page_token) - if yt_visitor_data: - new_params['visitor'] = yt_visitor_data + 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 - new_context = context.clone(new_params=new_params) - current_page = new_context.get_param('page', 1) - next_page_item = NextPageItem(new_context, current_page) - result.append(next_page_item) + + next_page_item = NextPageItem(context, new_params) + result.append(next_page_item) return result diff --git a/resources/lib/youtube_plugin/youtube/helper/video_info.py b/resources/lib/youtube_plugin/youtube/helper/video_info.py index 71fd3fdf1..89482d022 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: @@ -954,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, @@ -981,7 +983,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 = {} @@ -998,13 +1000,14 @@ 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 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') @@ -1065,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() @@ -1091,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: @@ -1155,7 +1181,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, @@ -1306,15 +1332,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, } @@ -1713,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, @@ -1746,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: @@ -1825,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: @@ -1834,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'] ) @@ -1842,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'] @@ -1896,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]: @@ -1987,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' @@ -2011,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' 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/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', ''), diff --git a/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py b/resources/lib/youtube_plugin/youtube/helper/yt_playlist.py index 7fcc71030..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 @@ -110,7 +111,11 @@ 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=params.get('refresh', 0) + 1), + ) context.get_ui().show_notification( message=context.localize('playlist.removed_from'), @@ -173,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: @@ -207,19 +213,19 @@ 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: 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', )) 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..a7301fc38 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': [ @@ -282,9 +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) - provider.reset_client() + settings.set_language(language_id) + settings.set_region(region_id) return step @@ -322,7 +321,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: @@ -444,6 +446,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 diff --git a/resources/lib/youtube_plugin/youtube/provider.py b/resources/lib/youtube_plugin/youtube/provider.py index c461e2e66..b26424989 100644 --- a/resources/lib/youtube_plugin/youtube/provider.py +++ b/resources/lib/youtube_plugin/youtube/provider.py @@ -13,8 +13,9 @@ import json import re from base64 import b64decode +from weakref import proxy -from .client import YouTube +from .client import APICheck, YouTube from .helper import ( ResourceManager, UrlResolver, @@ -50,10 +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): @@ -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: @@ -89,20 +91,29 @@ 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') - 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,147 +123,159 @@ 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 {} def reset_client(self): self._client = None + self._api_check = 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') + if not self._api_check: + self._api_check = APICheck(context) + configs = self._api_check.get_configs() 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): + 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_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(): - # 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 self._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 @@ -270,8 +293,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 @@ -719,7 +741,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' @@ -902,23 +924,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/?$') @@ -937,24 +958,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 = [] @@ -1010,7 +1031,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 +1434,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 +1498,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) @@ -1571,5 +1592,12 @@ def handle_exception(self, context, exception_to_handle): return True - def tear_down(self, context): - context.tear_down() + def tear_down(self): + 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 diff --git a/resources/media/bookmarks.png b/resources/media/bookmarks.png index e5604f1c1..6330eec57 100644 Binary files a/resources/media/bookmarks.png and b/resources/media/bookmarks.png differ diff --git a/resources/settings.xml b/resources/settings.xml index fad839fd0..07f5385e1 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -745,6 +745,21 @@ true + + 0 + cast + + + + + + , + + + true + true + + 0 ffadd8e6 @@ -795,29 +810,28 @@ 0 - 1 + 1 - - + + + - - 0 - true - - - + 0 - true - - - true - - - + 2 + + + + + + + + +