# -*- coding: utf-8 -*- """ Sender-Definitionen und Alias-Mapper fuer das TV-Programm-Plugin. Enthaelt alle unterstuetzten deutschen TV-Sender mit Aliasnamen fuer die Spracherkennung sowie einen Mapper zur Aufloesung gesprochener Sendernamen. """ from __future__ import annotations from trixy_core.utils.debug import pdebug # Alle unterstuetzten TV-Sender mit Display-Name, Sprach-Aliase, # Sendergruppe und XMLTV-IDs fuer EPG-Zuordnung CHANNELS: dict[str, dict] = { "ard": { "name": "Das Erste", "aliases": ["ARD", "Das Erste", "Erstes", "Erste"], "group": "ard", "xmltv_ids": ["DasErste.de", "ARD.de"], }, "zdf": { "name": "ZDF", "aliases": ["ZDF", "Zweites"], "group": "zdf", "xmltv_ids": ["ZDF.de"], }, "rtl": { "name": "RTL", "aliases": ["RTL", "RTL Television"], "group": "rtl", "xmltv_ids": ["RTL.de"], }, "rtl2": { "name": "RTLZWEI", "aliases": ["RTL2", "RTL Zwei", "RTLZWEI", "RTL II", "RTL 2"], "group": "rtl", "xmltv_ids": ["RTL2.de", "RTLZWEI.de"], }, "vox": { "name": "VOX", "aliases": ["VOX", "Vox"], "group": "rtl", "xmltv_ids": ["VOX.de"], }, "ntv": { "name": "ntv", "aliases": ["NTV", "ntv", "N-TV"], "group": "rtl", "xmltv_ids": ["ntv.de"], }, "nitro": { "name": "NITRO", "aliases": ["Nitro", "RTL Nitro"], "group": "rtl", "xmltv_ids": ["NITRO.de", "RTLNitro.de"], }, "rtlplus": { "name": "RTLplus", "aliases": ["RTL Plus", "RTLplus", "RTL+"], "group": "rtl", "xmltv_ids": ["RTLplus.de"], }, "super_rtl": { "name": "SUPER RTL", "aliases": ["Super RTL", "SUPER RTL", "SuperRTL", "Toggo"], "group": "rtl", "xmltv_ids": ["SuperRTL.de"], }, "sat1": { "name": "SAT.1", "aliases": ["SAT1", "SAT 1", "Sat eins", "Sat.1", "Sat 1"], "group": "prosieben", "xmltv_ids": ["Sat1.de", "SAT1.de"], }, "pro7": { "name": "ProSieben", "aliases": ["Pro7", "ProSieben", "Pro Sieben", "Pro 7", "Prosieben"], "group": "prosieben", "xmltv_ids": ["ProSieben.de"], }, "pro7_maxx": { "name": "ProSieben MAXX", "aliases": [ "Pro7 Maxx", "ProSieben Maxx", "Pro7 MAXX", "Prosieben Maxx", ], "group": "prosieben", "xmltv_ids": ["ProSiebenMaxx.de", "ProSiebenMAXX.de"], }, "kabel1": { "name": "kabel eins", "aliases": ["Kabel1", "Kabel eins", "Kabel 1", "Kabeleins"], "group": "prosieben", "xmltv_ids": ["kabel1.de", "kabeleins.de"], }, "sixx": { "name": "sixx", "aliases": ["Sixx", "SIXX", "6"], "group": "prosieben", "xmltv_ids": ["sixx.de"], }, "sat1_gold": { "name": "SAT.1 Gold", "aliases": ["Sat1 Gold", "SAT 1 Gold"], "group": "prosieben", "xmltv_ids": ["Sat1Gold.de"], }, "arte": { "name": "ARTE", "aliases": ["ARTE", "Arte"], "group": "public", "xmltv_ids": ["arte.de"], }, "3sat": { "name": "3sat", "aliases": ["3sat", "Drei Sat", "3 Sat", "Dreisat"], "group": "public", "xmltv_ids": ["3sat.de"], }, "phoenix": { "name": "phoenix", "aliases": ["Phoenix", "PHOENIX"], "group": "public", "xmltv_ids": ["phoenix.de"], }, "zdfneo": { "name": "ZDFneo", "aliases": ["ZDF Neo", "ZDFneo"], "group": "zdf", "xmltv_ids": ["ZDFneo.de"], }, "zdfinfo": { "name": "ZDFinfo", "aliases": ["ZDF Info", "ZDFinfo"], "group": "zdf", "xmltv_ids": ["ZDFinfo.de"], }, "one": { "name": "ONE", "aliases": ["ONE", "One", "Einsfestival"], "group": "ard", "xmltv_ids": ["ONE.de"], }, "tagesschau24": { "name": "tagesschau24", "aliases": ["Tagesschau 24", "tagesschau24"], "group": "ard", "xmltv_ids": ["tagesschau24.de"], }, "kika": { "name": "KiKA", "aliases": ["KiKA", "Kika", "Kinderkanal"], "group": "ard", "xmltv_ids": ["KiKA.de"], }, "wdr": { "name": "WDR", "aliases": ["WDR", "Westdeutscher Rundfunk"], "group": "ard", "xmltv_ids": ["WDR.de"], }, "ndr": { "name": "NDR", "aliases": ["NDR", "Norddeutscher Rundfunk"], "group": "ard", "xmltv_ids": ["NDR.de"], }, "br": { "name": "BR", "aliases": ["BR", "Bayerischer Rundfunk", "Bayerisches Fernsehen"], "group": "ard", "xmltv_ids": ["BR.de"], }, "hr": { "name": "hr", "aliases": ["HR", "Hessischer Rundfunk"], "group": "ard", "xmltv_ids": ["hr.de"], }, "swr": { "name": "SWR", "aliases": ["SWR", "Suedwestrundfunk"], "group": "ard", "xmltv_ids": ["SWR.de"], }, "mdr": { "name": "MDR", "aliases": ["MDR", "Mitteldeutscher Rundfunk"], "group": "ard", "xmltv_ids": ["MDR.de"], }, "rbb": { "name": "rbb", "aliases": ["RBB", "rbb"], "group": "ard", "xmltv_ids": ["rbb.de"], }, "dmax": { "name": "DMAX", "aliases": ["DMAX", "DMax"], "group": "other", "xmltv_ids": ["DMAX.de"], }, "tele5": { "name": "TELE 5", "aliases": ["Tele 5", "Tele5", "TELE5"], "group": "other", "xmltv_ids": ["Tele5.de", "TELE5.de"], }, "disney": { "name": "Disney Channel", "aliases": ["Disney Channel", "Disney"], "group": "other", "xmltv_ids": ["DisneyChannel.de"], }, "comedy_central": { "name": "Comedy Central", "aliases": ["Comedy Central"], "group": "other", "xmltv_ids": ["ComedyCentral.de"], }, "nick": { "name": "Nickelodeon", "aliases": ["Nick", "Nickelodeon"], "group": "other", "xmltv_ids": ["Nickelodeon.de"], }, "welt": { "name": "WELT", "aliases": ["Welt", "WELT", "N24"], "group": "other", "xmltv_ids": ["WELT.de", "N24.de"], }, } class ChannelMapper: """ Mapper fuer Sendernamen aus Spracheingabe auf interne Sender-IDs. Baut beim Initialisieren einen normalisierten Alias-Index auf. Unterstuetzt exakte und Teilstring-Suche sowie XMLTV-ID-Aufloesung. Filter-Logik: - whitelist leer + blacklist leer → alle Sender aktiv - whitelist gesetzt → NUR diese Sender (Blacklist wird ignoriert) - blacklist gesetzt → alle AUSSER diese Sender """ def __init__( self, channel_filter: list[str] | None = None, blacklist: list[str] | None = None, custom_aliases: dict[str, str] | None = None, ) -> None: """ Args: channel_filter: Whitelist — wenn gesetzt, werden NUR diese Sender einbezogen. blacklist: Sender die ausgeschlossen werden (z.B. Shopping-/Gluecksspiel-Sender). Wird ignoriert wenn channel_filter (Whitelist) gesetzt ist. custom_aliases: Benutzerdefinierte Aliase, z.B. {"Mein Sender": "rtl2"}. """ # Alias -> channel_id (normalisiert: lowercase, gestrippt) self._alias_map: dict[str, str] = {} # XMLTV-ID -> channel_id self._xmltv_map: dict[str, str] = {} # Aktive Sender-IDs self._active_channels: list[str] = [] self._build_maps(channel_filter, blacklist, custom_aliases) def _build_maps( self, channel_filter: list[str] | None, blacklist: list[str] | None, custom_aliases: dict[str, str] | None, ) -> None: """Baut die internen Lookup-Maps auf.""" # Blacklist normalisieren (lowercase fuer Vergleich) blackset = {b.lower() for b in (blacklist or [])} for channel_id, info in CHANNELS.items(): # Whitelist: nur angegebene Sender einbeziehen if channel_filter and channel_id not in channel_filter: continue # Blacklist: Sender ausschliessen (nur wenn keine Whitelist) if not channel_filter and blackset and channel_id.lower() in blackset: pdebug(f"ChannelMapper: Sender '{channel_id}' via Blacklist ausgeschlossen") continue self._active_channels.append(channel_id) # Alias-Map aufbauen (alle Aliase + Sender-ID + Display-Name) all_names = [channel_id, info["name"]] + info.get("aliases", []) for alias in all_names: normalized = self._normalize(alias) if normalized: self._alias_map[normalized] = channel_id # XMLTV-ID-Map aufbauen for xmltv_id in info.get("xmltv_ids", []): self._xmltv_map[xmltv_id] = channel_id # Benutzerdefinierte Aliase hinzufuegen (ueberschreiben bei Konflikt) if custom_aliases: for alias, channel_id in custom_aliases.items(): if channel_id in CHANNELS: normalized = self._normalize(alias) if normalized: self._alias_map[normalized] = channel_id pdebug(f"ChannelMapper: Benutzerdefinierter Alias '{alias}' -> '{channel_id}'") # Sender ggf. auch aktivieren if channel_id not in self._active_channels: self._active_channels.append(channel_id) pdebug( f"ChannelMapper: {len(self._active_channels)} Sender, " f"{len(self._alias_map)} Aliase, " f"{len(self._xmltv_map)} XMLTV-IDs geladen" ) def resolve(self, spoken_name: str) -> str | None: """ Loest einen gesprochenen Sendernamen auf eine interne Sender-ID auf. Versucht zuerst exakte Uebereinstimmung (normalisiert), dann Teilstring-Suche ueber alle Aliase. Args: spoken_name: Der vom Nutzer gesprochene Sendername. Returns: Die interne Sender-ID oder None, wenn nicht gefunden. """ normalized = self._normalize(spoken_name) if not normalized: return None # Exakter Match if normalized in self._alias_map: return self._alias_map[normalized] # Teilstring-Match: laengster passender Alias gewinnt best_match: str | None = None best_length = 0 for alias, channel_id in self._alias_map.items(): if alias in normalized or normalized in alias: if len(alias) > best_length: best_match = channel_id best_length = len(alias) if best_match: pdebug(f"ChannelMapper: Teilstring-Match '{spoken_name}' -> '{best_match}'") return best_match def resolve_xmltv_id(self, xmltv_channel_id: str) -> str | None: """ Mappt eine XMLTV-Sender-ID auf die interne Sender-ID. Args: xmltv_channel_id: Die Sender-ID aus dem XMLTV-Datenformat. Returns: Die interne Sender-ID oder None. """ return self._xmltv_map.get(xmltv_channel_id) def get_display_name(self, channel_id: str) -> str: """ Gibt den lesbaren Anzeigenamen eines Senders zurueck. Geeignet fuer TTS-Ausgabe. Args: channel_id: Die interne Sender-ID. Returns: Der Anzeigename oder die Sender-ID als Fallback. """ info = CHANNELS.get(channel_id) if info: return info["name"] return channel_id def get_all_channel_ids(self) -> list[str]: """ Gibt alle aktiven Sender-IDs zurueck. Returns: Liste aller Sender-IDs, die im Mapper registriert sind. """ return list(self._active_channels) @staticmethod def _normalize(name: str) -> str: """Normalisiert einen Sendernamen: lowercase, Whitespace gestrippt.""" return name.strip().lower()