From 4f86d980839ffc4feabcf59f562ed1996ef0aeb2 Mon Sep 17 00:00:00 2001 From: thomas-ernest Date: Sat, 28 Oct 2023 10:58:27 +0200 Subject: [PATCH] Issue #31 Restore multilanguage support Temporary commit / PR. Done : - multilanguage support from context menu - driven by renamed parameter (old nqme: show more streams, new multilanguage) - play lang stream from menu, context menu and playlist (series) - improve cache usage to build home page --- addon.py | 27 ++--- .../resource.language.de_de/strings.po | 4 +- .../resource.language.en_gb/strings.po | 2 +- .../resource.language.fr_fr/strings.po | 4 +- .../resource.language.it_it/strings.po | 4 +- .../resource.language.pl_pl/strings.po | 4 +- resources/lib/mapper/artecollection.py | 2 +- resources/lib/mapper/arteitem.py | 108 ++++++++++++++++-- resources/lib/mapper/artezone.py | 15 ++- resources/lib/mapper/live.py | 4 +- resources/lib/mapper/mapper.py | 71 +----------- resources/lib/settings.py | 4 +- resources/lib/user.py | 5 + resources/lib/view.py | 27 ++--- resources/settings.xml | 2 +- 15 files changed, 161 insertions(+), 122 deletions(-) diff --git a/addon.py b/addon.py index e9134f11..0a87656e 100644 --- a/addon.py +++ b/addon.py @@ -36,16 +36,17 @@ from resources.lib.settings import Settings # global declarations -# plugin stuff -plugin = Plugin() +CACHE_TTL = 2880 +plugin = Plugin() settings = Settings(plugin) @plugin.route('/', name='index') def index(): """Display home menu""" - return view.build_home_page(plugin, settings, plugin.get_storage('cached_categories', TTL=60)) + return view.build_home_page( + plugin, settings, plugin.get_storage('cached_categories', TTL=CACHE_TTL)) @plugin.route('/api_category/', name='api_category') @@ -58,13 +59,13 @@ def api_category(category_code): def cached_category(zone_id): """Display the menu for a category that is stored in cache from previous api call like home page""" - return view.get_cached_category(zone_id, plugin.get_storage('cached_categories', TTL=60)) + return view.get_cached_category(zone_id, plugin.get_storage('cached_categories', TTL=CACHE_TTL)) @plugin.route('/category_page///', name='category_page') def category_page(zone_id, page, page_id): """Display the menu for a category that needs an api call""" - return ArteZone(plugin, settings, plugin.get_storage('cached_categories', TTL=60)) \ + return ArteZone(plugin, settings, plugin.get_storage('cached_categories', TTL=CACHE_TTL)) \ .build_menu(zone_id, page, page_id) @@ -145,13 +146,7 @@ def purge_last_viewed(): def display_collection(kind, program_id): """Display menu for collection of content""" plugin.set_content('tvshows') - return plugin.finish(view.build_mixed_collection(plugin, kind, program_id, settings)) - - -@plugin.route('/streams/', name='streams') -def streams(program_id): - """Play a multi language content.""" - return plugin.finish(view.build_video_streams(plugin, settings, program_id)) + return plugin.finish(view.build_collection_menu_tree(plugin, settings, kind, program_id)) @plugin.route('/play_live/', name='play_live') @@ -179,16 +174,22 @@ def play(kind, program_id, audio_slot='1', from_playlist='0'): synched_player = Player(user.get_cached_token(plugin, settings.username, True), program_id) # try to seek parent collection, when out of the context of playlist creation sibling_playlist = None + if from_playlist == '0': sibling_playlist = view.build_sibling_playlist(plugin, settings, program_id) + item = None if sibling_playlist is not None and len(sibling_playlist['collection']) > 1: # Empty playlist, otherwise requested video is present twice in the playlist xbmc.PlayList(xbmc.PLAYLIST_VIDEO).clear() # Start playing with the first playlist item - result = plugin.set_resolved_url(plugin.add_to_playlist(sibling_playlist['collection'])[0]) + item = plugin.add_to_playlist(sibling_playlist['collection'])[0] + result = plugin.set_resolved_url(item) else: item = view.build_stream_url(plugin, kind, program_id, int(audio_slot), settings) result = plugin.set_resolved_url(item) + # Needed to play from context menu for multilanguage support + plugin.play_video(item) + # Needed to synch with Arte tv account synch_during_playback(synched_player) del synched_player return result diff --git a/resources/language/resource.language.de_de/strings.po b/resources/language/resource.language.de_de/strings.po index 9e7ef4d7..580f13a3 100644 --- a/resources/language/resource.language.de_de/strings.po +++ b/resources/language/resource.language.de_de/strings.po @@ -27,8 +27,8 @@ msgid "Stream video quality" msgstr "Qualität des Videostreams" msgctxt "#30053" -msgid "Show video stream options" -msgstr "Show video stream options" +msgid "Show multi-language streams in context menu" +msgstr "Mehrsprachige Streams im Kontextmenü anzeigen" msgctxt "#30054" msgid "Email" diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index feccd0e2..7b5a0d00 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -27,7 +27,7 @@ msgid "Stream video quality" msgstr "" msgctxt "#30053" -msgid "Show video stream options" +msgid "Show multi-language streams in context menu" msgstr "" msgctxt "#30054" diff --git a/resources/language/resource.language.fr_fr/strings.po b/resources/language/resource.language.fr_fr/strings.po index 2c63a3b9..7512ffc1 100644 --- a/resources/language/resource.language.fr_fr/strings.po +++ b/resources/language/resource.language.fr_fr/strings.po @@ -27,8 +27,8 @@ msgid "Stream video quality" msgstr "Qualité vidéo préferée" msgctxt "#30053" -msgid "Show video stream options" -msgstr "Afficher les options de flux vidéo" +msgid "Show multi-language streams in context menu" +msgstr "Afficher les flux multi-langue dans le menu contextuel" msgctxt "#30054" msgid "Email" diff --git a/resources/language/resource.language.it_it/strings.po b/resources/language/resource.language.it_it/strings.po index f3c0f593..47f07521 100644 --- a/resources/language/resource.language.it_it/strings.po +++ b/resources/language/resource.language.it_it/strings.po @@ -27,8 +27,8 @@ msgid "Stream video quality" msgstr "Qualità video" msgctxt "#30053" -msgid "Show video stream options" -msgstr "Show video stream options" +msgid "Show multi-language streams in context menu" +msgstr "Mostra flussi multilingue nel menu contestuale" msgctxt "#30054" msgid "Email" diff --git a/resources/language/resource.language.pl_pl/strings.po b/resources/language/resource.language.pl_pl/strings.po index 4404f858..72c24be3 100644 --- a/resources/language/resource.language.pl_pl/strings.po +++ b/resources/language/resource.language.pl_pl/strings.po @@ -27,8 +27,8 @@ msgid "Stream video quality" msgstr "Jakość wideo" msgctxt "#30053" -msgid "Show video stream options" -msgstr "Pokaz opcje wideo" +msgid "Show multi-language streams in context menu" +msgstr "Pokaż strumienie wielojęzyczne w menu kontekstowym" msgctxt "#30054" msgid "Email" diff --git a/resources/lib/mapper/artecollection.py b/resources/lib/mapper/artecollection.py index 092ceab8..9b6e0d1e 100644 --- a/resources/lib/mapper/artecollection.py +++ b/resources/lib/mapper/artecollection.py @@ -30,7 +30,7 @@ def _build_menu(self, json_dict, collection_type, **nav_arg): # Abstract class should NOT be instantiated # pylint: disable=assignment-from-none meta = self._get_page_meta(json_dict) - items = [ArteTvVideoItem(self.plugin, item).map_artetv_item() for item in pages] + items = [ArteTvVideoItem(self.plugin, self.settings, item).map_item() for item in pages] if meta and meta.get('pages', False): total_pages = meta.get('pages') current_page = meta.get('page') diff --git a/resources/lib/mapper/arteitem.py b/resources/lib/mapper/arteitem.py index a6065145..b94648a5 100644 --- a/resources/lib/mapper/arteitem.py +++ b/resources/lib/mapper/arteitem.py @@ -10,6 +10,8 @@ from xbmcswift2 import xbmc # pylint: disable=import-error from xbmcswift2 import actions +from resources.lib import api +from resources.lib import user # pylint: disable=too-few-public-methods @@ -43,19 +45,25 @@ class ArteVideoItem(ArteItem): It aims at being mapped into XBMC ListItem. """ + def __init__(self, plugin, settings, json_dict): + ArteItem.__init__(self, plugin, json_dict) + self.settings = settings + def build_item(self, path, is_playable): """Identify what is the type of current item and build the most detailled item possible""" if self.is_hbbtv(): - return ArteHbbTvVideoItem(self.plugin, self.json_dict).build_item(path, is_playable) - return ArteTvVideoItem(self.plugin, self.json_dict).build_item(path, is_playable) + return ArteHbbTvVideoItem(self.plugin, self.settings, self.json_dict).build_item( + path, is_playable) + return ArteTvVideoItem(self.plugin, self.settings, self.json_dict).build_item( + path, is_playable) def _build_item(self, path, is_playable): """ Build ListItem common to HBB TV and Arte TV API. """ item = self.json_dict - program_id = item.get('programId') label = self.format_title_and_subtitle() + return { 'label': label, 'path': path, @@ -73,7 +81,35 @@ def _build_item(self, path, is_playable): 'fanart_image': self._get_image_url(), 'TotalTime': str(self._get_duration()), }, - 'context_menu': [ + 'context_menu': self._build_context_menu(item), + } + + def _build_context_menu(self, item): + """ + Return an ordered list of tuple label-action to be used as context menu. + List contains tuples to manage favorites and mark as watched, is a user is logged in. + List contains tuples in multiple-languages, if setting is enabled. + List might be empty, but never None + """ + program_id = item.get('programId') + label = self.format_title_and_subtitle() + context_menu = [] + + # multi-language streams are available in context menu if enabled + if self.settings.multilanguage: + kind = item.get('kind') + if isinstance(kind, dict) and kind.get('code', False): + kind = kind.get('code') + # support multi-language for videos items and not collections e.g. TV_SERIES + if kind == 'SHOW': + streams = api.streams(kind, program_id, self.settings.language) + context_menu.extend( + self._map_streams(program_id, kind, streams, self.settings.quality)) + + # favorites management and mark as watched in Arte TV + # are available in context menu, if user is logged-in + if user.is_logged_in(self.plugin, self.settings): + context_menu.extend([ (self.plugin.addon.getLocalizedString(30023), actions.background(self.plugin.url_for( 'add_favorite', program_id=program_id, label=label))), @@ -83,8 +119,50 @@ def _build_item(self, path, is_playable): (self.plugin.addon.getLocalizedString(30035), actions.background(self.plugin.url_for( 'mark_as_watched', program_id=program_id, label=label))), - ], - } + ]) + + return context_menu + + def _map_streams(self, program_id, kind, streams, quality): + """Map JSON item and list of audio streams into a menu.""" + sorted_filtered_streams = self._sort_and_filter_streams(streams, quality) + + return [self._map_to_ctxt_menu(program_id, kind, stream) + for stream in sorted_filtered_streams] + + def _sort_and_filter_streams(self, streams, quality): + """ + Return a list of streams matching quality provided as parameter + and order by their numerical audio slot from Arte API + """ + if len(streams) <= 0: + return [] + + filtered_streams = None + for qlt in [quality] + [i for i in ['SQ', 'EQ', 'HQ', 'MQ'] if i is not quality]: + filtered_streams = [s for s in streams if s.get('quality') == qlt] + if len(filtered_streams) > 0: + break + + if filtered_streams is None or len(filtered_streams) == 0: + raise RuntimeError('Could not resolve stream...') + + return sorted( + filtered_streams, key=lambda s: s.get('audioSlot')) + + def _map_to_ctxt_menu(self, program_id, kind, stream): + """ + Map an Arte HBBTV API stream to a context menu item, + which enables to play the specific stream + """ + audio_slot = stream.get('audioSlot') + audio_label = stream.get('audioLabel') + return ( + audio_label, + actions.background(self.plugin.url_for( + 'play_siblings', kind=kind, program_id=program_id, + audio_slot=str(audio_slot), from_playlist='1')) + ) def _get_duration(self): """ @@ -154,7 +232,7 @@ class ArteTvVideoItem(ArteVideoItem): from Arte TV API data """ - def map_artetv_item(self): + def map_item(self): """ Return video menu item to show content from Arte TV API. Manage specificities of various types : playlist, menu or video items @@ -287,6 +365,13 @@ class ArteHbbTvVideoItem(ArteVideoItem): from Arte HBB TV API data """ + def map_item(self): + """Create a playable video menu item from a json returned by Arte HBBTV API""" + program_id = self.json_dict.get('programId') + kind = self.json_dict.get('kind') + path = self.plugin.url_for('play', kind=kind, program_id=program_id) + return self.build_item(path, True) + def build_item(self, path, is_playable): basic_item = super()._build_item(path, is_playable) if basic_item is None: @@ -355,3 +440,12 @@ def map_collection_as_menu_item(self): 'plotoutline': item.get('teaserText') } } + + def build_collection_or_hbbtv_item(self, settings): + """Return entry menu for video or playlist""" + item = self.json_dict + if ArteVideoItem(self.plugin, settings, item).is_playlist(): + item = ArteCollectionItem(self.plugin, item).map_collection_as_menu_item() + else: + item = ArteHbbTvVideoItem(self.plugin, settings, item).map_item() + return item diff --git a/resources/lib/mapper/artezone.py b/resources/lib/mapper/artezone.py index cc853ef1..3165d385 100644 --- a/resources/lib/mapper/artezone.py +++ b/resources/lib/mapper/artezone.py @@ -23,14 +23,27 @@ def build_item(self, zone): a zone in the HOME page or SEARH page result. """ zone_id = zone.get('id') + + # try to get category from cache + if isinstance(self.cached_categories, dict): + cached_category = self.cached_categories.get(zone_id, None) + if self._is_valid_menu(cached_category): + return { + 'label': zone.get('title'), + 'path': self.plugin.url_for('cached_category', zone_id=zone_id) + } + + # otherwise try to build the category and save it in cqche cached_category = self._build_menu( zone.get('content'), 'category_page', zone_id=zone_id, page_id='HOME') if self._is_valid_menu(cached_category): - self.cached_categories[zone_id] = cached_category + if isinstance(self.cached_categories, dict): + self.cached_categories[zone_id] = cached_category return { 'label': zone.get('title'), 'path': self.plugin.url_for('cached_category', zone_id=zone_id) } + return None def _is_valid_menu(self, cached_category): diff --git a/resources/lib/mapper/live.py b/resources/lib/mapper/live.py index 28136dd8..42740f22 100644 --- a/resources/lib/mapper/live.py +++ b/resources/lib/mapper/live.py @@ -1,6 +1,6 @@ """ Module for ArteLiveItem depends on ArteTvVideoItem and mapper module -for map_playable and match_hbbtv +for map_playable and match_artetv """ import html @@ -58,7 +58,7 @@ def build_item_live(self, quality, audio_slot): # while it starts the video like the live tv, with the above # 'path': plugin.url_for('play', kind='SHOW', program_id=programId.replace('_fr', '')), 'thumbnail': thumbnail_url, - 'is_playable': True, # not show_video_streams + 'is_playable': True, 'info_type': 'video', 'info': { 'title': meta.get('title'), diff --git a/resources/lib/mapper/mapper.py b/resources/lib/mapper/mapper.py index e9a6d497..c526e740 100644 --- a/resources/lib/mapper/mapper.py +++ b/resources/lib/mapper/mapper.py @@ -5,8 +5,6 @@ from resources.lib import utils from resources.lib.mapper.arteitem import ArteVideoItem from resources.lib.mapper.arteitem import ArteTvVideoItem -from resources.lib.mapper.arteitem import ArteHbbTvVideoItem -from resources.lib.mapper.arteitem import ArteCollectionItem from resources.lib.mapper.artezone import ArteZone from resources.lib.mapper.artefavorites import ArteFavorites from resources.lib.mapper.artehistory import ArteHistory @@ -26,18 +24,7 @@ def map_category_item(plugin, item, category_code): } -def map_generic_item(plugin, item, show_video_streams): - """Return entry menu for video or playlist""" - if ArteVideoItem(plugin, item).is_playlist(): - item = ArteCollectionItem(plugin, item).map_collection_as_menu_item() - elif show_video_streams is True: - item = map_video_streams_as_menu(plugin, item) - else: - item = map_video_as_item(plugin, item) - return item - - -def map_collection_as_playlist(plugin, arte_collection, req_start_program_id=None): +def map_collection_as_playlist(plugin, settings, arte_collection, req_start_program_id=None): """ Map a collection from arte API to a list of items ready to build a playlist. Playlist item will be in the same order as arte_collection, if start_program_id @@ -51,7 +38,7 @@ def map_collection_as_playlist(plugin, arte_collection, req_start_program_id=Non # assume arte_collection[0] will be mapped successfully with map_video_as_playlist_item start_program_id = arte_collection[0].get('programId') for arte_item in arte_collection or []: - xbmc_item = map_video_as_playlist_item(plugin, arte_item) + xbmc_item = map_video_as_playlist_item(plugin, settings, arte_item) if xbmc_item is None: break @@ -59,7 +46,7 @@ def map_collection_as_playlist(plugin, arte_collection, req_start_program_id=Non if before_start: if req_start_program_id is None: # start from the first element not fully viewed - if ArteTvVideoItem(plugin, arte_item).get_progress() < 0.95: + if ArteTvVideoItem(plugin, settings, arte_item).get_progress() < 0.95: before_start = False start_program_id = arte_item.get('programId') else: @@ -78,7 +65,7 @@ def map_collection_as_playlist(plugin, arte_collection, req_start_program_id=Non } -def map_video_as_playlist_item(plugin, item): +def map_video_as_playlist_item(plugin, settings, item): """ Create a video menu item without recursiveness to fetch parent collection from a json returned by Arte HBBTV or ArteTV API @@ -90,58 +77,10 @@ def map_video_as_playlist_item(plugin, item): path = plugin.url_for( 'play_siblings', kind=kind, program_id=program_id, audio_slot='1', from_playlist='1') - result = ArteVideoItem(plugin, item).build_item(path, True) + result = ArteVideoItem(plugin, settings, item).build_item(path, True) return result -def map_video_streams_as_menu(plugin, item): - """Create a menu item for video streams from a json returned by Arte HBBTV API""" - program_id = item.get('programId') - path = plugin.url_for('streams', program_id=program_id) - return ArteHbbTvVideoItem(plugin, item).build_item(path, False) - - -def map_video_as_item(plugin, item): - """Create a playable video menu item from a json returned by Arte HBBTV API""" - program_id = item.get('programId') - kind = item.get('kind') - path = plugin.url_for('play', kind=kind, program_id=program_id) - return ArteHbbTvVideoItem(plugin, item).build_item(path, True) - - -def map_streams(plugin, item, streams, quality): - """Map JSON item and list of audio streams into a menu.""" - program_id = item.get('programId') - kind = item.get('kind') - - video_item = map_video_as_item(plugin, item) - - filtered_streams = None - for qlt in [quality] + [i for i in ['SQ', 'EQ', 'HQ', 'MQ'] if i is not quality]: - filtered_streams = [s for s in streams if s.get('quality') == qlt] - if len(filtered_streams) > 0: - break - - if filtered_streams is None or len(filtered_streams) == 0: - raise RuntimeError('Could not resolve stream...') - - sorted_filtered_streams = sorted( - filtered_streams, key=lambda s: s.get('audioSlot')) - - def map_stream(video_item, stream): - audio_slot = stream.get('audioSlot') - audio_label = stream.get('audioLabel') - - video_item['label'] = audio_label - video_item['is_playable'] = True - video_item['path'] = plugin.url_for( - 'play_specific', kind=kind, program_id=program_id, audio_slot=str(audio_slot)) - - return video_item - - return [map_stream(dict(video_item), stream) for stream in sorted_filtered_streams] - - def map_zone_to_item(plugin, settings, zone, cached_categories): """Arte TV API page is split into zones. Map a 'zone' to menu item(s). Populate cached_categories for zones with videos available in child 'content'""" diff --git a/resources/lib/settings.py b/resources/lib/settings.py index e2d3cc22..05bcd0cc 100644 --- a/resources/lib/settings.py +++ b/resources/lib/settings.py @@ -21,8 +21,8 @@ def __init__(self, plugin): 'quality', choices=qualities) or qualities[0] # Should the plugin display all available streams for videos? # defaults to False - self.show_video_streams = plugin.get_setting( - 'show_video_streams', bool) or False + self.multilanguage = plugin.get_setting( + 'multilanguage', bool) or False # Arte TV user name # defaults to empty string to return false with if not str self.username = plugin.get_setting( diff --git a/resources/lib/user.py b/resources/lib/user.py index 90c90682..6098f7c0 100644 --- a/resources/lib/user.py +++ b/resources/lib/user.py @@ -119,6 +119,11 @@ def is_logged_in_as(plugin): return usr +def is_logged_in(plugin, settings): + """Return True if a token exists for the configured user, False otherwise""" + return get_cached_token(plugin, settings.username, True) is not None + + def get_cached_token(plugin, token_idx, silent=False): """ Return cached token for identified user or None. diff --git a/resources/lib/view.py b/resources/lib/view.py index 2f528bfe..c65acb0e 100644 --- a/resources/lib/view.py +++ b/resources/lib/view.py @@ -2,9 +2,10 @@ # pylint: disable=import-error from xbmcswift2 import xbmc +from resources.lib.mapper.arteitem import ArteCollectionItem from resources.lib.mapper.arteitem import ArteItem -from resources.lib.mapper.live import ArteLiveItem from resources.lib.mapper.artesearch import ArteSearch +from resources.lib.mapper.live import ArteLiveItem from resources.lib import api from resources.lib import hof from resources.lib.mapper import mapper @@ -19,7 +20,7 @@ def build_home_page(plugin, settings, cached_categories): ] try: addon_menu.append( - ArteLiveItem(plugin, api.player_video(settings.language, 'LIVE')) + ArteLiveItem(plugin, settings, api.player_video(settings.language, 'LIVE')) .build_item_live(settings.quality, '1')) # pylint: disable=broad-exception-caught # Could be improve. possible exceptions are limited to auth. errors @@ -72,26 +73,12 @@ def mark_as_watched(plugin, usr, program_id, label): plugin.notify(msg=msg, image='error') -def build_mixed_collection(plugin, kind, collection_id, settings): +def build_collection_menu_tree(plugin, settings, kind, collection_id): """Build menu of content available in collection collection_id thanks to HBB TV API""" - return [mapper.map_generic_item(plugin, item, settings.show_video_streams) for item in + return [ArteCollectionItem(plugin, item).build_collection_or_hbbtv_item(settings) for item in api.collection(kind, collection_id, settings.language)] -def build_video_streams(plugin, settings, program_id): - """Build the menu with the audio streams available for content program_id""" - item = api.video(program_id, settings.language) - - if item is None: - raise RuntimeError('Video not found...') - - program_id = item.get('programId') - kind = item.get('kind') - - return mapper.map_streams( - plugin, item, api.streams(kind, program_id, settings.language), settings.quality) - - def build_sibling_playlist(plugin, settings, program_id): """ Return a pair with videos belonging to the same parent as program id @@ -112,7 +99,7 @@ def build_sibling_playlist(plugin, settings, program_id): sibling_arte_items = api.collection_with_last_viewed( settings.language, user.get_cached_token(plugin, settings.username, True), parent_program.get('kind'), parent_program.get('programId')) - return mapper.map_collection_as_playlist(plugin, sibling_arte_items, program_id) + return mapper.map_collection_as_playlist(plugin, settings, sibling_arte_items, program_id) return None @@ -121,7 +108,7 @@ def build_collection_playlist(plugin, settings, kind, collection_id): Return a pair with collection with collection_id and program id of the first element in the collection """ - return mapper.map_collection_as_playlist(plugin, api.collection_with_last_viewed( + return mapper.map_collection_as_playlist(plugin, settings, api.collection_with_last_viewed( settings.language, user.get_cached_token(plugin, settings.username, True), kind, collection_id)) diff --git a/resources/settings.xml b/resources/settings.xml index d675b81b..d0b4827a 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -15,7 +15,7 @@ values="SQ (High)|EQ (Medium)|HQ (Low)" default="0"/>