From b9a6b5056af59c8193f8e6bd8451c86b66f375ce Mon Sep 17 00:00:00 2001 From: mani Date: Tue, 6 Jan 2026 00:32:44 +0100 Subject: [PATCH] Add language-based track selection and context menu Context Menu Integration: - Add 'Play with track selection' option to video context menus - Available for movies and episodes via long-press/context menu - Forces manual track selection dialog even when auto-selection is configured Language Selection Improvements: - Add dropdown selectors for preferred audio/subtitle languages - Support German, English, French, Spanish, Italian, Japanese, Russian - Add comprehensive language matching (e.g., 'ger' matches 'deu', 'deutsch', 'german') - Add setting to auto-select 'No subtitles' when no matching subtitle found Track Selection Scoring System: - Audio tracks: Language match (+100), default flag (+50), channel count (+5-40), codec quality (+10-30), exclude commentary (-100) - Subtitle tracks: Language match (+100), forced flag (+50), SRT preference (+30), default flag (+10) Implementation Details: - Add force_track_selection parameter throughout playback chain - Preserve server/remote control track selections when not forcing manual selection - Add extensive debug logging for track selection decisions - Respect user preferences while allowing manual override via context menu --- .../language/resource.language.de/strings.po | 44 ++++++++++ .../resource.language.en_gb/strings.po | 44 ++++++++++ resources/lib/functions.py | 4 + resources/lib/item_functions.py | 14 +++ resources/lib/play_utils.py | 86 ++++++++++++++++--- resources/settings.xml | 5 +- 6 files changed, 182 insertions(+), 15 deletions(-) diff --git a/resources/language/resource.language.de/strings.po b/resources/language/resource.language.de/strings.po index 61f6323..4543f78 100644 --- a/resources/language/resource.language.de/strings.po +++ b/resources/language/resource.language.de/strings.po @@ -1281,3 +1281,47 @@ msgstr "Forced-Untertitel bevorzugen" msgctxt "#30690" msgid "SRT vor PGS/Bild-Untertiteln bevorzugen" msgstr "SRT vor PGS/Bild-Untertiteln bevorzugen" + +msgctxt "#30691" +msgid "Deutsch" +msgstr "Deutsch" + +msgctxt "#30692" +msgid "Englisch" +msgstr "Englisch" + +msgctxt "#30693" +msgid "Französisch" +msgstr "Französisch" + +msgctxt "#30694" +msgid "Spanisch" +msgstr "Spanisch" + +msgctxt "#30695" +msgid "Italienisch" +msgstr "Italienisch" + +msgctxt "#30696" +msgid "Japanisch" +msgstr "Japanisch" + +msgctxt "#30697" +msgid "Russisch" +msgstr "Russisch" + +msgctxt "#30698" +msgid "Andere" +msgstr "Andere" + +msgctxt "#30699" +msgid "Keine (nur Audio-Sprache)" +msgstr "Keine (nur Audio-Sprache)" + +msgctxt "#30700" +msgid "Bei keinem Treffer automatisch 'Keine Untertitel' wählen" +msgstr "Bei keinem Treffer automatisch 'Keine Untertitel' wählen" + +msgctxt "#30701" +msgid "Abspielen mit Track-Auswahl" +msgstr "Abspielen mit Track-Auswahl" diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 2e6494e..9f4a57b 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -1272,3 +1272,47 @@ msgstr "Prefer forced subtitles" msgctxt "#30690" msgid "Prefer SRT over PGS/image subtitles" msgstr "Prefer SRT over PGS/image subtitles" + +msgctxt "#30691" +msgid "German" +msgstr "German" + +msgctxt "#30692" +msgid "English" +msgstr "English" + +msgctxt "#30693" +msgid "French" +msgstr "French" + +msgctxt "#30694" +msgid "Spanish" +msgstr "Spanish" + +msgctxt "#30695" +msgid "Italian" +msgstr "Italian" + +msgctxt "#30696" +msgid "Japanese" +msgstr "Japanese" + +msgctxt "#30697" +msgid "Russian" +msgstr "Russian" + +msgctxt "#30698" +msgid "Other" +msgstr "Other" + +msgctxt "#30699" +msgid "None (audio language only)" +msgstr "None (audio language only)" + +msgctxt "#30700" +msgid "Auto-select 'No subtitles' if no match found" +msgstr "Auto-select 'No subtitles' if no match found" + +msgctxt "#30701" +msgid "Play with track selection" +msgstr "Play with track selection" diff --git a/resources/lib/functions.py b/resources/lib/functions.py index a630104..fa618f2 100644 --- a/resources/lib/functions.py +++ b/resources/lib/functions.py @@ -923,6 +923,9 @@ def play_action(params): audio_stream_index = params.get("audio_stream_index") log.debug("audio_stream_index: {0}".format(audio_stream_index)) + force_track_selection = params.get("force_track_selection", "false") == "true" + log.debug("force_track_selection: {0}".format(force_track_selection)) + action = params.get("action", "play") # set the current playing item id @@ -939,6 +942,7 @@ def play_action(params): play_info["media_source_id"] = media_source_id play_info["subtitle_stream_index"] = subtitle_stream_index play_info["audio_stream_index"] = audio_stream_index + play_info["force_track_selection"] = force_track_selection log.info("Sending jellycon_play_action : {0}".format(play_info)) play_file(play_info) diff --git a/resources/lib/item_functions.py b/resources/lib/item_functions.py index 45dd2e1..da3ec4b 100644 --- a/resources/lib/item_functions.py +++ b/resources/lib/item_functions.py @@ -595,6 +595,20 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F list_item.setProperties(item_properties) + # Add context menu for playable video items + if not folder and is_video and item_details.item_type.lower() in ['movie', 'episode']: + from . import translation + context_menu = [] + + # Add "Play with track selection" option + play_with_selection_url = ( + 'RunPlugin(plugin://plugin.video.jellycon/' + '?mode=PLAY&item_id={}&force_track_selection=true)'.format(item_details.id) + ) + context_menu.append((translation.translate_string(30701), play_with_selection_url)) + + list_item.addContextMenuItems(context_menu) + return u, list_item, folder diff --git a/resources/lib/play_utils.py b/resources/lib/play_utils.py index 955fd7a..37cb6a9 100644 --- a/resources/lib/play_utils.py +++ b/resources/lib/play_utils.py @@ -242,8 +242,10 @@ def play_file(play_info): media_source_id = play_info.get("media_source_id", "") subtitle_stream_index = play_info.get("subtitle_stream_index", None) audio_stream_index = play_info.get("audio_stream_index", None) + force_track_selection = play_info.get("force_track_selection", False) - log.debug("playFile id({0}) resume({1}) force_transcode({2})".format(item_id, auto_resume, force_transcode)) + log.debug("playFile id({0}) resume({1}) force_transcode({2}) force_track_selection({3})".format( + item_id, auto_resume, force_transcode, force_track_selection)) addon_path = settings.getAddonInfo('path') force_auto_resume = settings.getSetting('forceAutoResume') == 'true' @@ -461,7 +463,7 @@ def play_file(play_info): if playback_type == "2": # if transcoding then prompt for audio and subtitle playurl = audio_subs_pref(playurl, list_item, selected_media_source, item_id, audio_stream_index, - subtitle_stream_index) + subtitle_stream_index, force_track_selection) log.debug("New playurl for transcoding: {0}".format(playurl)) elif playback_type == "1": # for direct stream add any streamable subtitles @@ -778,7 +780,7 @@ def set_list_item_props(item_id, list_item, result, server, extra_props, title): # Present the list of audio and subtitles to select from # for external streamable subtitles add the URL to the Kodi item and let Kodi handle it # else ask for the subtitles to be burnt in when transcoding -def audio_subs_pref(url, list_item, media_source, item_id, audio_stream_index, subtitle_stream_index): +def audio_subs_pref(url, list_item, media_source, item_id, audio_stream_index, subtitle_stream_index, force_track_selection=False): dialog = xbmcgui.Dialog() audio_streams_list = {} audio_streams = [] @@ -795,11 +797,51 @@ def audio_subs_pref(url, list_item, media_source, item_id, audio_stream_index, s source_id = media_source["Id"] # Read user preferences - preferred_audio_lang = settings.getSetting("preferred_audio_language").lower() + # Map select index to language code + lang_index_map = ['ger', 'eng', 'fra', 'spa', 'ita', 'jpn', 'rus', ''] # '' = other/custom + sub_lang_index_map = ['', 'ger', 'eng', 'fra', 'spa', 'ita', 'jpn', 'rus', ''] # First is 'none' + + audio_lang_index = int(settings.getSetting("preferred_audio_language") or "0") + preferred_audio_lang = lang_index_map[audio_lang_index] if audio_lang_index < len(lang_index_map) else '' + auto_select_default_audio = settings.getSetting("auto_select_default_audio") == "true" - preferred_sub_lang = settings.getSetting("preferred_subtitle_language").lower() + + sub_lang_index = int(settings.getSetting("preferred_subtitle_language") or "1") + preferred_sub_lang = sub_lang_index_map[sub_lang_index] if sub_lang_index < len(sub_lang_index_map) else '' + prefer_forced = settings.getSetting("prefer_forced_subtitles") == "true" prefer_srt = settings.getSetting("prefer_srt_over_pgs") == "true" + auto_no_subs = settings.getSetting("auto_no_subtitles_if_no_match") == "true" + + # Language code mapping for better matching + language_map = { + 'ger': ['ger', 'deu', 'de', 'german', 'deutsch'], + 'eng': ['eng', 'en', 'english'], + 'fra': ['fra', 'fr', 'fre', 'french', 'français'], + 'spa': ['spa', 'es', 'spanish', 'español'], + 'ita': ['ita', 'it', 'italian', 'italiano'], + 'jpn': ['jpn', 'ja', 'japanese', '日本語'], + 'rus': ['rus', 'ru', 'russian'], + } + + # Helper function to check if language matches + def language_matches(stream_lang, preferred_lang): + stream_lang = stream_lang.lower() + preferred_lang = preferred_lang.lower() + + # Direct match + if preferred_lang in stream_lang or stream_lang in preferred_lang: + return True + + # Check against language map + for key, variations in language_map.items(): + if preferred_lang in variations: + # Check if stream language matches any variation + for variant in variations: + if variant in stream_lang or stream_lang.startswith(variant[:2]): + return True + + return False media_streams = media_source['MediaStreams'] @@ -856,7 +898,8 @@ def audio_subs_pref(url, list_item, media_source, item_id, audio_stream_index, s subtitle_streams_data.append(stream) # Auto-select audio track based on preferences - if select_audio_index is None and len(audio_streams_data) > 0: + # Only auto-select if not already set by server/remote control AND not forcing manual selection + if not force_track_selection and select_audio_index is None and len(audio_streams_data) > 0 and (preferred_audio_lang or auto_select_default_audio): auto_selected = None best_score = -1 @@ -864,10 +907,10 @@ def audio_subs_pref(url, list_item, media_source, item_id, audio_stream_index, s if preferred_audio_lang: for stream in audio_streams_data: score = 0 - stream_lang = stream.get('Language', '').lower() + stream_lang = stream.get('Language', '') - # Match against common variations (ger, deu, de, german) - if preferred_audio_lang in stream_lang or stream_lang.startswith(preferred_audio_lang[:3]): + # Match against common variations using language map + if language_matches(stream_lang, preferred_audio_lang): score += 100 # Language match # Bonus for default track @@ -911,19 +954,29 @@ def audio_subs_pref(url, list_item, media_source, item_id, audio_stream_index, s log.debug("Auto-selected audio (index {0}, score {1})".format(auto_selected, best_score)) # Auto-select subtitle track based on preferences - if select_subs_index is None and len(subtitle_streams_data) > 0: + # Only auto-select if not already set by server/remote control AND not forcing manual selection + if force_track_selection: + log.debug("Forcing manual track selection (from context menu)") + elif select_subs_index is not None: + log.debug("Using subtitle index from server/remote: {0}".format(select_subs_index)) + elif len(subtitle_streams_data) == 0: + log.debug("No subtitle streams available") + elif len(subtitle_streams_data) > 0 and preferred_sub_lang: # Only if user configured a language preference auto_selected = None best_score = -1 + log.debug("Auto-selecting subtitle: preferred_lang={0}, prefer_forced={1}, prefer_srt={2}".format( + preferred_sub_lang, prefer_forced, prefer_srt)) + for stream in subtitle_streams_data: score = 0 - stream_lang = stream.get('Language', '').lower() + stream_lang = stream.get('Language', '') codec = stream.get('Codec', '').lower() is_forced = stream.get('IsForced', False) is_default = stream.get('IsDefault', False) # Score based on language match - if preferred_sub_lang and (preferred_sub_lang in stream_lang or stream_lang.startswith(preferred_sub_lang[:3])): + if preferred_sub_lang and language_matches(stream_lang, preferred_sub_lang): score += 100 # Bonus for forced if preferred @@ -950,6 +1003,9 @@ def audio_subs_pref(url, list_item, media_source, item_id, audio_stream_index, s if auto_selected is not None and best_score >= 100: # Only auto-select if language matched select_subs_index = auto_selected log.debug("Auto-selected subtitle (index {0}, score {1})".format(auto_selected, best_score)) + elif auto_no_subs and preferred_sub_lang: # No match found but user wants auto "no subs" + select_subs_index = -1 # Special value to indicate "no subtitles" + log.debug("No matching subtitle found - auto-selected 'No subtitles'") # set audio index if select_audio_index is not None: @@ -967,8 +1023,12 @@ def audio_subs_pref(url, list_item, media_source, item_id, audio_stream_index, s # set subtitle index if select_subs_index is not None: + # Handle special "no subtitles" value + if select_subs_index == -1: + # User wants no subtitles - do nothing + pass # Load subtitles in the listitem if downloadable - if select_subs_index in downloadable_streams: + elif select_subs_index in downloadable_streams: subtitle_url = "%s/Videos/%s/%s/Subtitles/%s/Stream.srt" subtitle_url = subtitle_url % (settings.getSetting('server_address'), item_id, source_id, select_subs_index) log.debug("Streaming subtitles url: {0} {1}".format(select_subs_index, subtitle_url)) diff --git a/resources/settings.xml b/resources/settings.xml index de1bc06..85958c2 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -50,11 +50,12 @@ - + - + +