| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404 |
- # -*- 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()
|