From 37d23fb396128299d256d7c62e8c6d470e5c0f5a Mon Sep 17 00:00:00 2001 From: Fridrich Strba Date: Fri, 21 Aug 2026 12:18:04 +0200 Subject: [PATCH 1/8] Backport Live TV folder structure and caching to SRF Play TV --- .../resource.language.de_de/strings.po | 8 + .../resource.language.en_gb/strings.po | 8 + .../resource.language.fr_fr/strings.po | 8 + .../resource.language.it_it/strings.po | 8 + resources/lib/srfplaytv.py | 210 ++++++++++++++++++ 5 files changed, 242 insertions(+) diff --git a/resources/language/resource.language.de_de/strings.po b/resources/language/resource.language.de_de/strings.po index ed0cfc2..555e252 100644 --- a/resources/language/resource.language.de_de/strings.po +++ b/resources/language/resource.language.de_de/strings.po @@ -106,3 +106,11 @@ msgstr "SRF auf YouTube" msgctxt "#30085" msgid "Search" msgstr "Suche" + +msgctxt "#30101" +msgid "Sport Live" +msgstr "Sport Live" + +msgctxt "#30102" +msgid "Andere Live-Streams" +msgstr "Andere Live-Streams" diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 56d5518..f4679e3 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -106,3 +106,11 @@ msgstr "" msgctxt "#30085" msgid "Search" msgstr "" + +msgctxt "#30101" +msgid "Sports Live" +msgstr "" + +msgctxt "#30102" +msgid "Other Live Streams" +msgstr "" diff --git a/resources/language/resource.language.fr_fr/strings.po b/resources/language/resource.language.fr_fr/strings.po index 35c4146..72e33c4 100644 --- a/resources/language/resource.language.fr_fr/strings.po +++ b/resources/language/resource.language.fr_fr/strings.po @@ -106,3 +106,11 @@ msgstr "SRF sur YouTube" msgctxt "#30085" msgid "Search" msgstr "Chercher" + +msgctxt "#30101" +msgid "Sports en direct" +msgstr "Sports en direct" + +msgctxt "#30102" +msgid "Les autres directs" +msgstr "Les autres directs" diff --git a/resources/language/resource.language.it_it/strings.po b/resources/language/resource.language.it_it/strings.po index b2a0840..ab2a19b 100644 --- a/resources/language/resource.language.it_it/strings.po +++ b/resources/language/resource.language.it_it/strings.po @@ -106,3 +106,11 @@ msgstr "SRF su YouTube" msgctxt "#30085" msgid "Search" msgstr "Ricerca" + +msgctxt "#30101" +msgid "Sport Live" +msgstr "" + +msgctxt "#30102" +msgid "Altri flussi in diretta" +msgstr "" diff --git a/resources/lib/srfplaytv.py b/resources/lib/srfplaytv.py index b5798df..5f2c6a8 100644 --- a/resources/lib/srfplaytv.py +++ b/resources/lib/srfplaytv.py @@ -24,6 +24,10 @@ from urllib.parse import parse_qsl import xbmc +import urllib.request +import json +import datetime +import xbmcgui import xbmcaddon import xbmcplugin @@ -42,6 +46,209 @@ def __init__(self): super(SRFPlayTV, self).__init__(int(sys.argv[1]), bu="srf", addon_id=ADDON_ID) + + def build_livetv_menu(self, sub_menu=None): + """Fetches 24/7 channels and scheduled event livestreams. + + If sub_menu is None, renders 24/7 channels and folder links for sub-menus. + If sub_menu is "sports" or "others", renders only that category of events. + """ + headers = {'User-Agent': 'Mozilla/5.0'} + + # Case 1: Build the main root "Direct TV" page + if sub_menu is None: + # 1. Fetch available 24/7 livestreams + livestreams_url = "https://www.srf.ch/play/v3/api/srf/production/tv-livestreams" + try: + req = urllib.request.Request(livestreams_url, headers=headers) + with urllib.request.urlopen(req) as response: + livestreams_data = json.loads(response.read().decode('utf-8')) + except Exception as e: + log(f"Failed to fetch live TV channels: {e}", xbmc.LOGERROR) + return + + channels = livestreams_data.get("data", []) + if not channels: + return + + # 2. Fetch program guide for EPG data (enriched fallback) + guide_url = "https://www.srf.ch/play/v3/api/srf/production/tv-program-guide" + guide_by_channel = {} + try: + req = urllib.request.Request(guide_url, headers=headers) + with urllib.request.urlopen(req) as response: + guide_data = json.loads(response.read().decode('utf-8')) + for item in guide_data.get("data", []): + ch_id = item.get("channel", {}).get("id") + if ch_id: + guide_by_channel[ch_id] = item.get("programList", []) + except Exception as e: + log(f"Failed to fetch live TV program guide (falling back to channels-only): {e}", xbmc.LOGWARNING) + + now_utc = datetime.datetime.now(datetime.timezone.utc) + + # Add 24/7 linear channels + for channel in channels: + title = channel.get("title") + urn = channel.get("livestreamUrn") + img_url = channel.get("imageUrl") + channel_id = channel.get("channelId") + + if title and urn: + current_show = "" + next_show = "" + + program_list = guide_by_channel.get(channel_id, []) + for prog in program_list: + try: + start = datetime.datetime.fromisoformat(prog["startTime"].replace('Z', '+00:00')) + end = datetime.datetime.fromisoformat(prog["endTime"].replace('Z', '+00:00')) + if start <= now_utc <= end: + current_show = prog["title"] + break + elif start > now_utc: + if not next_show: + formatted_start = start.astimezone().strftime("%H:%M") + next_show = f"À suivre : {prog['title']} ({formatted_start})" + except Exception: + pass + + status = current_show if current_show else next_show + display_name = f"{title} - {status}" if status else title + + list_item = xbmcgui.ListItem(label=display_name) + list_item.setProperty("IsPlayable", "true") + if img_url: + list_item.setArt({"thumb": img_url}) + + plugin_url = self.build_url(mode=50, name=urn) + xbmcplugin.addDirectoryItem(self.handle, plugin_url, list_item, isFolder=False) + + # Add folder items for the two sub-directories + sport_folder_url = self.build_url(mode=90, name="sports") + sport_item = xbmcgui.ListItem(label=self.plugin_language(30101)) + sport_item.setArt({"icon": self.icon}) + xbmcplugin.addDirectoryItem(self.handle, sport_folder_url, sport_item, isFolder=True) + + others_folder_url = self.build_url(mode=90, name="others") + others_item = xbmcgui.ListItem(label=self.plugin_language(30102)) + others_item.setArt({"icon": self.icon}) + xbmcplugin.addDirectoryItem(self.handle, others_folder_url, others_item, isFolder=True) + + # Case 2: Build a specific subdirectory ("sports" or "others") and its date folders/events + else: + sub_category, sep, date_filter = sub_menu.partition('_') + + # Fetch scheduled event livestreams with caching + scheduled_url = ( + "https://il.srgssr.ch/integrationlayer/2.0/srf/mediaList/video/" + "scheduledLivestreams?vector=portalplay&pageSize=100" + ) + scheduled_events = [] + try: + # Use SRGSSR's caching open_url to avoid redundant requests + response_text = self.open_url(scheduled_url) + if response_text: + scheduled_data = json.loads(response_text) + scheduled_events = scheduled_data.get("mediaList") or scheduled_data.get("data") or [] + except Exception as e: + log(f"Failed to fetch scheduled livestreams: {e}", xbmc.LOGWARNING) + + now_utc = datetime.datetime.now(datetime.timezone.utc) + local_now = now_utc.astimezone() + + # Filter out past events + active_and_upcoming = [] + for item in scheduled_events: + try: + valid_to = datetime.datetime.fromisoformat(item["validTo"].replace('Z', '+00:00')) + if valid_to >= now_utc: + active_and_upcoming.append(item) + except Exception: + pass + + # Filter by sub-category using the Integration Layer's "creatorUser" metadata attribute + filtered_events = [] + for item in active_and_upcoming: + is_sport = item.get("creatorUser") == "MMSport" + + if (sub_category == "sports" and is_sport) or (sub_category == "others" and not is_sport): + filtered_events.append(item) + + # Group filtered events by local date + grouped_events = {} + for item in filtered_events: + try: + valid_from = datetime.datetime.fromisoformat(item["validFrom"].replace('Z', '+00:00')) + local_start = valid_from.astimezone() + local_date = local_start.date() + if local_date not in grouped_events: + grouped_events[local_date] = [] + grouped_events[local_date].append((local_start, item)) + except Exception: + pass + + sorted_dates = sorted(grouped_events.keys()) + + def folder_name(dato): + weekdays = ( + self.language(30060), # Monday + self.language(30061), # Tuesday + self.language(30062), # Wednesday + self.language(30063), # Thursday + self.language(30064), # Friday + self.language(30065), # Saturday + self.language(30066), # Sunday + ) + today = local_now.date() + if dato == today: + return self.language(30058) # Today + elif dato == today - datetime.timedelta(days=1): + return self.language(30059) # Yesterday + return "%s, %s" % (weekdays[dato.weekday()], dato.strftime("%d.%m.%Y")) + + if not date_filter: + # Build Date Folders + for local_date in sorted_dates: + folder_label = folder_name(local_date) + folder_item = xbmcgui.ListItem(label=folder_label) + folder_item.setArt({"icon": self.icon}) + + target_name = f"{sub_category}_{local_date.isoformat()}" + folder_url = self.build_url(mode=90, name=target_name) + xbmcplugin.addDirectoryItem(self.handle, folder_url, folder_item, isFolder=True) + else: + # Render events chronologically for the selected date + selected_date = datetime.date.fromisoformat(date_filter) + events_on_day = grouped_events.get(selected_date, []) + events_on_day.sort(key=lambda x: x[0]) + + for start_time, item in events_on_day: + title = item.get("title") + urn = item.get("urn") + img_url = item.get("imageUrl") + + if title and urn: + try: + valid_to = datetime.datetime.fromisoformat(item["validTo"].replace('Z', '+00:00')) + if start_time <= now_utc <= valid_to: + prefix = "[COLOR red][LIVE] [/COLOR]" + else: + prefix = f"[{start_time.strftime('%H:%M')}] " + except Exception: + prefix = "" + + display_name = f"{prefix}{title}" + list_item = xbmcgui.ListItem(label=display_name) + list_item.setProperty("IsPlayable", "true") + if img_url: + list_item.setArt({"thumb": img_url}) + + plugin_url = self.build_url(mode=50, name=urn) + xbmcplugin.addDirectoryItem(self.handle, plugin_url, list_item, isFolder=False) + + + def log(msg, level=xbmc.LOGDEBUG): """ Logs a message using Kodi's logging interface. @@ -144,6 +351,9 @@ def run(): SRFPlayTV().menu_builder.build_menu_by_urn(name) elif mode == 200: SRFPlayTV().menu_builder.build_homepage_menu() + + elif mode == 90: + SRFPlayTV().build_livetv_menu(name) elif mode == 1000: SRFPlayTV().menu_builder.build_menu_apiv3(name, mode, page, page_hash) From 419e47c1b24789010afd1dffccbcf965524e4596 Mon Sep 17 00:00:00 2001 From: Fridrich Strba Date: Fri, 21 Aug 2026 13:55:31 +0200 Subject: [PATCH 2/8] Fix empty English translation strings for Live TV labels --- resources/language/resource.language.en_gb/strings.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index f4679e3..d63bbbe 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -109,8 +109,8 @@ msgstr "" msgctxt "#30101" msgid "Sports Live" -msgstr "" +msgstr "Sports Live" msgctxt "#30102" msgid "Other Live Streams" -msgstr "" +msgstr "Other Live Streams" From 46480b4a430b18a86864fc03da2ae276c5bfe3e1 Mon Sep 17 00:00:00 2001 From: Fridrich Strba Date: Fri, 21 Aug 2026 14:07:30 +0200 Subject: [PATCH 3/8] Fix empty Italian strings for Live TV labels --- resources/language/resource.language.it_it/strings.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/language/resource.language.it_it/strings.po b/resources/language/resource.language.it_it/strings.po index ab2a19b..bae578e 100644 --- a/resources/language/resource.language.it_it/strings.po +++ b/resources/language/resource.language.it_it/strings.po @@ -109,8 +109,8 @@ msgstr "Ricerca" msgctxt "#30101" msgid "Sport Live" -msgstr "" +msgstr "Sport Live" msgctxt "#30102" msgid "Altri flussi in diretta" -msgstr "" +msgstr "Altri flussi in diretta" From e5e1495d2abf4a6f3eb34020c8d8aa96d4a97096 Mon Sep 17 00:00:00 2001 From: Fridrich Strba Date: Fri, 21 Aug 2026 14:28:32 +0200 Subject: [PATCH 4/8] Add missing Live TV main menu item and centralize sub-menu labels --- resources/lib/srfplaytv.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/resources/lib/srfplaytv.py b/resources/lib/srfplaytv.py index 5f2c6a8..5b107f3 100644 --- a/resources/lib/srfplaytv.py +++ b/resources/lib/srfplaytv.py @@ -126,12 +126,12 @@ def build_livetv_menu(self, sub_menu=None): # Add folder items for the two sub-directories sport_folder_url = self.build_url(mode=90, name="sports") - sport_item = xbmcgui.ListItem(label=self.plugin_language(30101)) + sport_item = xbmcgui.ListItem(label=self.language(30101) or self.plugin_language(30101) or "Sports Live") sport_item.setArt({"icon": self.icon}) xbmcplugin.addDirectoryItem(self.handle, sport_folder_url, sport_item, isFolder=True) others_folder_url = self.build_url(mode=90, name="others") - others_item = xbmcgui.ListItem(label=self.plugin_language(30102)) + others_item = xbmcgui.ListItem(label=self.language(30102) or self.plugin_language(30102) or "Other Live Streams") others_item.setArt({"icon": self.icon}) xbmcplugin.addDirectoryItem(self.handle, others_folder_url, others_item, isFolder=True) @@ -310,7 +310,14 @@ def run(): "Search", "SRF_YouTube", ] - SRFPlayTV().menu_builder.build_main_menu(identifiers) + srf = SRFPlayTV() + srf.menu_builder.build_main_menu(identifiers) + + # Append Direct TV to the main menu + tv_list_item = xbmcgui.ListItem(label=srf.plugin_language(30072)) + tv_list_item.setArt({"icon": srf.icon}) + tv_url = srf.build_url(mode=90) + xbmcplugin.addDirectoryItem(int(sys.argv[1]), tv_url, tv_list_item, isFolder=True) elif mode == 10: SRFPlayTV().menu_builder.build_all_shows_menu() elif mode == 11: From a5cea7b6617e8704b36e191f9836855058565720 Mon Sep 17 00:00:00 2001 From: Fridrich Strba Date: Fri, 21 Aug 2026 19:11:27 +0200 Subject: [PATCH 5/8] Fix flake8 errors E501 and E303 --- resources/lib/srfplaytv.py | 130 +++++++++++++++++++++++++++---------- 1 file changed, 97 insertions(+), 33 deletions(-) diff --git a/resources/lib/srfplaytv.py b/resources/lib/srfplaytv.py index 5b107f3..f2102a8 100644 --- a/resources/lib/srfplaytv.py +++ b/resources/lib/srfplaytv.py @@ -43,26 +43,32 @@ class SRFPlayTV(srgssr.SRGSSR): def __init__(self): - super(SRFPlayTV, self).__init__(int(sys.argv[1]), bu="srf", addon_id=ADDON_ID) - - + super(SRFPlayTV, self).__init__( + int(sys.argv[1]), bu="srf", addon_id=ADDON_ID + ) def build_livetv_menu(self, sub_menu=None): """Fetches 24/7 channels and scheduled event livestreams. - If sub_menu is None, renders 24/7 channels and folder links for sub-menus. - If sub_menu is "sports" or "others", renders only that category of events. + If sub_menu is None, renders 24/7 channels and folder links for + sub-menus. + If sub_menu is "sports" or "others", renders only that category of + events. """ headers = {'User-Agent': 'Mozilla/5.0'} # Case 1: Build the main root "Direct TV" page if sub_menu is None: # 1. Fetch available 24/7 livestreams - livestreams_url = "https://www.srf.ch/play/v3/api/srf/production/tv-livestreams" + livestreams_url = ( + "https://www.srf.ch/play/v3/api/srf/production/tv-livestreams" + ) try: req = urllib.request.Request(livestreams_url, headers=headers) with urllib.request.urlopen(req) as response: - livestreams_data = json.loads(response.read().decode('utf-8')) + livestreams_data = json.loads( + response.read().decode('utf-8') + ) except Exception as e: log(f"Failed to fetch live TV channels: {e}", xbmc.LOGERROR) return @@ -72,7 +78,10 @@ def build_livetv_menu(self, sub_menu=None): return # 2. Fetch program guide for EPG data (enriched fallback) - guide_url = "https://www.srf.ch/play/v3/api/srf/production/tv-program-guide" + guide_url = ( + "https://www.srf.ch/play/v3/api/srf/production/" + "tv-program-guide" + ) guide_by_channel = {} try: req = urllib.request.Request(guide_url, headers=headers) @@ -81,9 +90,15 @@ def build_livetv_menu(self, sub_menu=None): for item in guide_data.get("data", []): ch_id = item.get("channel", {}).get("id") if ch_id: - guide_by_channel[ch_id] = item.get("programList", []) + guide_by_channel[ch_id] = item.get( + "programList", [] + ) except Exception as e: - log(f"Failed to fetch live TV program guide (falling back to channels-only): {e}", xbmc.LOGWARNING) + log( + "Failed to fetch live TV program guide " + f"(falling back to channels-only): {e}", + xbmc.LOGWARNING, + ) now_utc = datetime.datetime.now(datetime.timezone.utc) @@ -101,15 +116,24 @@ def build_livetv_menu(self, sub_menu=None): program_list = guide_by_channel.get(channel_id, []) for prog in program_list: try: - start = datetime.datetime.fromisoformat(prog["startTime"].replace('Z', '+00:00')) - end = datetime.datetime.fromisoformat(prog["endTime"].replace('Z', '+00:00')) + start = datetime.datetime.fromisoformat( + prog["startTime"].replace('Z', '+00:00') + ) + end = datetime.datetime.fromisoformat( + prog["endTime"].replace('Z', '+00:00') + ) if start <= now_utc <= end: current_show = prog["title"] break elif start > now_utc: if not next_show: - formatted_start = start.astimezone().strftime("%H:%M") - next_show = f"À suivre : {prog['title']} ({formatted_start})" + formatted_start = ( + start.astimezone().strftime("%H:%M") + ) + next_show = ( + "À suivre : " + f"{prog['title']} ({formatted_start})" + ) except Exception: pass @@ -122,26 +146,42 @@ def build_livetv_menu(self, sub_menu=None): list_item.setArt({"thumb": img_url}) plugin_url = self.build_url(mode=50, name=urn) - xbmcplugin.addDirectoryItem(self.handle, plugin_url, list_item, isFolder=False) + xbmcplugin.addDirectoryItem( + self.handle, plugin_url, list_item, isFolder=False + ) # Add folder items for the two sub-directories sport_folder_url = self.build_url(mode=90, name="sports") - sport_item = xbmcgui.ListItem(label=self.language(30101) or self.plugin_language(30101) or "Sports Live") + sport_item = xbmcgui.ListItem( + label=self.language(30101) + or self.plugin_language(30101) + or "Sports Live" + ) sport_item.setArt({"icon": self.icon}) - xbmcplugin.addDirectoryItem(self.handle, sport_folder_url, sport_item, isFolder=True) + xbmcplugin.addDirectoryItem( + self.handle, sport_folder_url, sport_item, isFolder=True + ) others_folder_url = self.build_url(mode=90, name="others") - others_item = xbmcgui.ListItem(label=self.language(30102) or self.plugin_language(30102) or "Other Live Streams") + others_item = xbmcgui.ListItem( + label=self.language(30102) + or self.plugin_language(30102) + or "Other Live Streams" + ) others_item.setArt({"icon": self.icon}) - xbmcplugin.addDirectoryItem(self.handle, others_folder_url, others_item, isFolder=True) + xbmcplugin.addDirectoryItem( + self.handle, others_folder_url, others_item, isFolder=True + ) - # Case 2: Build a specific subdirectory ("sports" or "others") and its date folders/events + # Case 2: Build a specific subdirectory ("sports" or "others") and + # its date folders/events else: sub_category, sep, date_filter = sub_menu.partition('_') # Fetch scheduled event livestreams with caching scheduled_url = ( - "https://il.srgssr.ch/integrationlayer/2.0/srf/mediaList/video/" + "https://il.srgssr.ch/integrationlayer/2.0/srf/" + "mediaList/video/" "scheduledLivestreams?vector=portalplay&pageSize=100" ) scheduled_events = [] @@ -150,9 +190,16 @@ def build_livetv_menu(self, sub_menu=None): response_text = self.open_url(scheduled_url) if response_text: scheduled_data = json.loads(response_text) - scheduled_events = scheduled_data.get("mediaList") or scheduled_data.get("data") or [] + scheduled_events = ( + scheduled_data.get("mediaList") + or scheduled_data.get("data") + or [] + ) except Exception as e: - log(f"Failed to fetch scheduled livestreams: {e}", xbmc.LOGWARNING) + log( + f"Failed to fetch scheduled livestreams: {e}", + xbmc.LOGWARNING, + ) now_utc = datetime.datetime.now(datetime.timezone.utc) local_now = now_utc.astimezone() @@ -161,25 +208,32 @@ def build_livetv_menu(self, sub_menu=None): active_and_upcoming = [] for item in scheduled_events: try: - valid_to = datetime.datetime.fromisoformat(item["validTo"].replace('Z', '+00:00')) + valid_to = datetime.datetime.fromisoformat( + item["validTo"].replace('Z', '+00:00') + ) if valid_to >= now_utc: active_and_upcoming.append(item) except Exception: pass - # Filter by sub-category using the Integration Layer's "creatorUser" metadata attribute + # Filter by sub-category using the Integration Layer's + # "creatorUser" metadata attribute filtered_events = [] for item in active_and_upcoming: is_sport = item.get("creatorUser") == "MMSport" - if (sub_category == "sports" and is_sport) or (sub_category == "others" and not is_sport): + if (sub_category == "sports" and is_sport) or ( + sub_category == "others" and not is_sport + ): filtered_events.append(item) # Group filtered events by local date grouped_events = {} for item in filtered_events: try: - valid_from = datetime.datetime.fromisoformat(item["validFrom"].replace('Z', '+00:00')) + valid_from = datetime.datetime.fromisoformat( + item["validFrom"].replace('Z', '+00:00') + ) local_start = valid_from.astimezone() local_date = local_start.date() if local_date not in grouped_events: @@ -205,7 +259,10 @@ def folder_name(dato): return self.language(30058) # Today elif dato == today - datetime.timedelta(days=1): return self.language(30059) # Yesterday - return "%s, %s" % (weekdays[dato.weekday()], dato.strftime("%d.%m.%Y")) + return "%s, %s" % ( + weekdays[dato.weekday()], + dato.strftime("%d.%m.%Y"), + ) if not date_filter: # Build Date Folders @@ -216,7 +273,9 @@ def folder_name(dato): target_name = f"{sub_category}_{local_date.isoformat()}" folder_url = self.build_url(mode=90, name=target_name) - xbmcplugin.addDirectoryItem(self.handle, folder_url, folder_item, isFolder=True) + xbmcplugin.addDirectoryItem( + self.handle, folder_url, folder_item, isFolder=True + ) else: # Render events chronologically for the selected date selected_date = datetime.date.fromisoformat(date_filter) @@ -230,7 +289,9 @@ def folder_name(dato): if title and urn: try: - valid_to = datetime.datetime.fromisoformat(item["validTo"].replace('Z', '+00:00')) + valid_to = datetime.datetime.fromisoformat( + item["validTo"].replace('Z', '+00:00') + ) if start_time <= now_utc <= valid_to: prefix = "[COLOR red][LIVE] [/COLOR]" else: @@ -245,8 +306,9 @@ def folder_name(dato): list_item.setArt({"thumb": img_url}) plugin_url = self.build_url(mode=50, name=urn) - xbmcplugin.addDirectoryItem(self.handle, plugin_url, list_item, isFolder=False) - + xbmcplugin.addDirectoryItem( + self.handle, plugin_url, list_item, isFolder=False + ) def log(msg, level=xbmc.LOGDEBUG): @@ -317,7 +379,9 @@ def run(): tv_list_item = xbmcgui.ListItem(label=srf.plugin_language(30072)) tv_list_item.setArt({"icon": srf.icon}) tv_url = srf.build_url(mode=90) - xbmcplugin.addDirectoryItem(int(sys.argv[1]), tv_url, tv_list_item, isFolder=True) + xbmcplugin.addDirectoryItem( + int(sys.argv[1]), tv_url, tv_list_item, isFolder=True + ) elif mode == 10: SRFPlayTV().menu_builder.build_all_shows_menu() elif mode == 11: From 18a8fc30321642dcdc56c04561496acecb1e9885 Mon Sep 17 00:00:00 2001 From: Fridrich Strba Date: Fri, 21 Aug 2026 19:11:32 +0200 Subject: [PATCH 6/8] Resumaziuns e descripziuns en tut las linguas naziunalas --- addon.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/addon.xml b/addon.xml index 2c9fa8b..1981f4a 100644 --- a/addon.xml +++ b/addon.xml @@ -12,10 +12,12 @@ SRF Play television video library SRF Play Videobibliothek Videoteca SRF Play + Bibliotega da videos SRF Play Avec SRF Play TV vous pouvez regarder les vidéos de la vidéothèque de la Radio télévision suisse (SRF). SRF Play TV allows you to watch the content from the video library of Swiss Radio and Television (SRF). Mit SRF Play TV können Sie den Inhalt der SRF Play Videobibliothek des Schweizer Fernsehens anschauen. SRF Play TV ti permette di accedere alla videoteca della Radio e della Televisione Svizzera. + Cun SRF Play TV pudais Vus guardar ils videos ord la bibliotega da videos da la Radio e Televisiun Svizra (SRF). Certains contenus sont géo-bloqués et disponibles uniquement en Suisse. Some content is geo-blocked and only available in Switzerland. Einige Inhalte sind nur in der Schweiz verfügbar. From f96a3ebb5151094186105deafba3423b748fe68a Mon Sep 17 00:00:00 2001 From: Fridrich Strba Date: Sat, 22 Aug 2026 18:45:57 +0200 Subject: [PATCH 7/8] Get livestream title from the query and pass it to the player --- resources/lib/srfplaytv.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/resources/lib/srfplaytv.py b/resources/lib/srfplaytv.py index f2102a8..d75feeb 100644 --- a/resources/lib/srfplaytv.py +++ b/resources/lib/srfplaytv.py @@ -187,7 +187,12 @@ def build_livetv_menu(self, sub_menu=None): scheduled_events = [] try: # Use SRGSSR's caching open_url to avoid redundant requests - response_text = self.open_url(scheduled_url) + # Disable cache for root menu to get fresh data, + # but use cache when navigating date sub-folders + should_cache = ('_' in sub_menu) if sub_menu else False + response_text = self.open_url( + scheduled_url, use_cache=should_cache + ) if response_text: scheduled_data = json.loads(response_text) scheduled_events = ( @@ -305,7 +310,9 @@ def folder_name(dato): if img_url: list_item.setArt({"thumb": img_url}) - plugin_url = self.build_url(mode=50, name=urn) + plugin_url = self.build_url( + mode=50, name=urn, title=title + ) xbmcplugin.addDirectoryItem( self.handle, plugin_url, list_item, isFolder=False ) @@ -354,6 +361,10 @@ def run(): page = unquote_plus(params["page"]) except Exception: page = None + try: + title = unquote_plus(params["title"]) + except Exception: + title = None log("Mode: " + str(mode)) log("URL : " + str(url)) @@ -417,7 +428,7 @@ def run(): name, mode, page=page, page_token=page_hash ) elif mode == 50: - SRFPlayTV().player.play_video(name) + SRFPlayTV().player.play_video(name, title=title) elif mode == 100: SRFPlayTV().menu_builder.build_menu_by_urn(name) elif mode == 200: From 919c5ddc9ebb98ea678749f2fd4350ef5813a6ff Mon Sep 17 00:00:00 2001 From: Fridrich Strba Date: Sat, 22 Aug 2026 20:44:42 +0200 Subject: [PATCH 8/8] Prefer cesimId over event uuid when building scheduled-livestream URNs --- resources/lib/srfplaytv.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/resources/lib/srfplaytv.py b/resources/lib/srfplaytv.py index d75feeb..d4051f1 100644 --- a/resources/lib/srfplaytv.py +++ b/resources/lib/srfplaytv.py @@ -292,6 +292,13 @@ def folder_name(dato): urn = item.get("urn") img_url = item.get("imageUrl") + # cesimId, when present, is the real swisstxt asset id + # and takes priority over the event's own uuid (which + # doesn't resolve on its own for these events). + cesim_id = item.get("cesimId") + if cesim_id: + urn = f"urn:swisstxt:video:srf:{cesim_id}" + if title and urn: try: valid_to = datetime.datetime.fromisoformat(