channels.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. # -*- coding: utf-8 -*-
  2. """
  3. Sender-Definitionen und Alias-Mapper fuer das TV-Programm-Plugin.
  4. Enthaelt alle unterstuetzten deutschen TV-Sender mit Aliasnamen fuer die
  5. Spracherkennung sowie einen Mapper zur Aufloesung gesprochener Sendernamen.
  6. """
  7. from __future__ import annotations
  8. from trixy_core.utils.debug import pdebug
  9. # Alle unterstuetzten TV-Sender mit Display-Name, Sprach-Aliase,
  10. # Sendergruppe und XMLTV-IDs fuer EPG-Zuordnung
  11. CHANNELS: dict[str, dict] = {
  12. "ard": {
  13. "name": "Das Erste",
  14. "aliases": ["ARD", "Das Erste", "Erstes", "Erste"],
  15. "group": "ard",
  16. "xmltv_ids": ["DasErste.de", "ARD.de"],
  17. },
  18. "zdf": {
  19. "name": "ZDF",
  20. "aliases": ["ZDF", "Zweites"],
  21. "group": "zdf",
  22. "xmltv_ids": ["ZDF.de"],
  23. },
  24. "rtl": {
  25. "name": "RTL",
  26. "aliases": ["RTL", "RTL Television"],
  27. "group": "rtl",
  28. "xmltv_ids": ["RTL.de"],
  29. },
  30. "rtl2": {
  31. "name": "RTLZWEI",
  32. "aliases": ["RTL2", "RTL Zwei", "RTLZWEI", "RTL II", "RTL 2"],
  33. "group": "rtl",
  34. "xmltv_ids": ["RTL2.de", "RTLZWEI.de"],
  35. },
  36. "vox": {
  37. "name": "VOX",
  38. "aliases": ["VOX", "Vox"],
  39. "group": "rtl",
  40. "xmltv_ids": ["VOX.de"],
  41. },
  42. "ntv": {
  43. "name": "ntv",
  44. "aliases": ["NTV", "ntv", "N-TV"],
  45. "group": "rtl",
  46. "xmltv_ids": ["ntv.de"],
  47. },
  48. "nitro": {
  49. "name": "NITRO",
  50. "aliases": ["Nitro", "RTL Nitro"],
  51. "group": "rtl",
  52. "xmltv_ids": ["NITRO.de", "RTLNitro.de"],
  53. },
  54. "rtlplus": {
  55. "name": "RTLplus",
  56. "aliases": ["RTL Plus", "RTLplus", "RTL+"],
  57. "group": "rtl",
  58. "xmltv_ids": ["RTLplus.de"],
  59. },
  60. "super_rtl": {
  61. "name": "SUPER RTL",
  62. "aliases": ["Super RTL", "SUPER RTL", "SuperRTL", "Toggo"],
  63. "group": "rtl",
  64. "xmltv_ids": ["SuperRTL.de"],
  65. },
  66. "sat1": {
  67. "name": "SAT.1",
  68. "aliases": ["SAT1", "SAT 1", "Sat eins", "Sat.1", "Sat 1"],
  69. "group": "prosieben",
  70. "xmltv_ids": ["Sat1.de", "SAT1.de"],
  71. },
  72. "pro7": {
  73. "name": "ProSieben",
  74. "aliases": ["Pro7", "ProSieben", "Pro Sieben", "Pro 7", "Prosieben"],
  75. "group": "prosieben",
  76. "xmltv_ids": ["ProSieben.de"],
  77. },
  78. "pro7_maxx": {
  79. "name": "ProSieben MAXX",
  80. "aliases": [
  81. "Pro7 Maxx",
  82. "ProSieben Maxx",
  83. "Pro7 MAXX",
  84. "Prosieben Maxx",
  85. ],
  86. "group": "prosieben",
  87. "xmltv_ids": ["ProSiebenMaxx.de", "ProSiebenMAXX.de"],
  88. },
  89. "kabel1": {
  90. "name": "kabel eins",
  91. "aliases": ["Kabel1", "Kabel eins", "Kabel 1", "Kabeleins"],
  92. "group": "prosieben",
  93. "xmltv_ids": ["kabel1.de", "kabeleins.de"],
  94. },
  95. "sixx": {
  96. "name": "sixx",
  97. "aliases": ["Sixx", "SIXX", "6"],
  98. "group": "prosieben",
  99. "xmltv_ids": ["sixx.de"],
  100. },
  101. "sat1_gold": {
  102. "name": "SAT.1 Gold",
  103. "aliases": ["Sat1 Gold", "SAT 1 Gold"],
  104. "group": "prosieben",
  105. "xmltv_ids": ["Sat1Gold.de"],
  106. },
  107. "arte": {
  108. "name": "ARTE",
  109. "aliases": ["ARTE", "Arte"],
  110. "group": "public",
  111. "xmltv_ids": ["arte.de"],
  112. },
  113. "3sat": {
  114. "name": "3sat",
  115. "aliases": ["3sat", "Drei Sat", "3 Sat", "Dreisat"],
  116. "group": "public",
  117. "xmltv_ids": ["3sat.de"],
  118. },
  119. "phoenix": {
  120. "name": "phoenix",
  121. "aliases": ["Phoenix", "PHOENIX"],
  122. "group": "public",
  123. "xmltv_ids": ["phoenix.de"],
  124. },
  125. "zdfneo": {
  126. "name": "ZDFneo",
  127. "aliases": ["ZDF Neo", "ZDFneo"],
  128. "group": "zdf",
  129. "xmltv_ids": ["ZDFneo.de"],
  130. },
  131. "zdfinfo": {
  132. "name": "ZDFinfo",
  133. "aliases": ["ZDF Info", "ZDFinfo"],
  134. "group": "zdf",
  135. "xmltv_ids": ["ZDFinfo.de"],
  136. },
  137. "one": {
  138. "name": "ONE",
  139. "aliases": ["ONE", "One", "Einsfestival"],
  140. "group": "ard",
  141. "xmltv_ids": ["ONE.de"],
  142. },
  143. "tagesschau24": {
  144. "name": "tagesschau24",
  145. "aliases": ["Tagesschau 24", "tagesschau24"],
  146. "group": "ard",
  147. "xmltv_ids": ["tagesschau24.de"],
  148. },
  149. "kika": {
  150. "name": "KiKA",
  151. "aliases": ["KiKA", "Kika", "Kinderkanal"],
  152. "group": "ard",
  153. "xmltv_ids": ["KiKA.de"],
  154. },
  155. "wdr": {
  156. "name": "WDR",
  157. "aliases": ["WDR", "Westdeutscher Rundfunk"],
  158. "group": "ard",
  159. "xmltv_ids": ["WDR.de"],
  160. },
  161. "ndr": {
  162. "name": "NDR",
  163. "aliases": ["NDR", "Norddeutscher Rundfunk"],
  164. "group": "ard",
  165. "xmltv_ids": ["NDR.de"],
  166. },
  167. "br": {
  168. "name": "BR",
  169. "aliases": ["BR", "Bayerischer Rundfunk", "Bayerisches Fernsehen"],
  170. "group": "ard",
  171. "xmltv_ids": ["BR.de"],
  172. },
  173. "hr": {
  174. "name": "hr",
  175. "aliases": ["HR", "Hessischer Rundfunk"],
  176. "group": "ard",
  177. "xmltv_ids": ["hr.de"],
  178. },
  179. "swr": {
  180. "name": "SWR",
  181. "aliases": ["SWR", "Suedwestrundfunk"],
  182. "group": "ard",
  183. "xmltv_ids": ["SWR.de"],
  184. },
  185. "mdr": {
  186. "name": "MDR",
  187. "aliases": ["MDR", "Mitteldeutscher Rundfunk"],
  188. "group": "ard",
  189. "xmltv_ids": ["MDR.de"],
  190. },
  191. "rbb": {
  192. "name": "rbb",
  193. "aliases": ["RBB", "rbb"],
  194. "group": "ard",
  195. "xmltv_ids": ["rbb.de"],
  196. },
  197. "dmax": {
  198. "name": "DMAX",
  199. "aliases": ["DMAX", "DMax"],
  200. "group": "other",
  201. "xmltv_ids": ["DMAX.de"],
  202. },
  203. "tele5": {
  204. "name": "TELE 5",
  205. "aliases": ["Tele 5", "Tele5", "TELE5"],
  206. "group": "other",
  207. "xmltv_ids": ["Tele5.de", "TELE5.de"],
  208. },
  209. "disney": {
  210. "name": "Disney Channel",
  211. "aliases": ["Disney Channel", "Disney"],
  212. "group": "other",
  213. "xmltv_ids": ["DisneyChannel.de"],
  214. },
  215. "comedy_central": {
  216. "name": "Comedy Central",
  217. "aliases": ["Comedy Central"],
  218. "group": "other",
  219. "xmltv_ids": ["ComedyCentral.de"],
  220. },
  221. "nick": {
  222. "name": "Nickelodeon",
  223. "aliases": ["Nick", "Nickelodeon"],
  224. "group": "other",
  225. "xmltv_ids": ["Nickelodeon.de"],
  226. },
  227. "welt": {
  228. "name": "WELT",
  229. "aliases": ["Welt", "WELT", "N24"],
  230. "group": "other",
  231. "xmltv_ids": ["WELT.de", "N24.de"],
  232. },
  233. }
  234. class ChannelMapper:
  235. """
  236. Mapper fuer Sendernamen aus Spracheingabe auf interne Sender-IDs.
  237. Baut beim Initialisieren einen normalisierten Alias-Index auf.
  238. Unterstuetzt exakte und Teilstring-Suche sowie XMLTV-ID-Aufloesung.
  239. Filter-Logik:
  240. - whitelist leer + blacklist leer → alle Sender aktiv
  241. - whitelist gesetzt → NUR diese Sender (Blacklist wird ignoriert)
  242. - blacklist gesetzt → alle AUSSER diese Sender
  243. """
  244. def __init__(
  245. self,
  246. channel_filter: list[str] | None = None,
  247. blacklist: list[str] | None = None,
  248. custom_aliases: dict[str, str] | None = None,
  249. ) -> None:
  250. """
  251. Args:
  252. channel_filter: Whitelist — wenn gesetzt, werden NUR diese Sender einbezogen.
  253. blacklist: Sender die ausgeschlossen werden (z.B. Shopping-/Gluecksspiel-Sender).
  254. Wird ignoriert wenn channel_filter (Whitelist) gesetzt ist.
  255. custom_aliases: Benutzerdefinierte Aliase, z.B. {"Mein Sender": "rtl2"}.
  256. """
  257. # Alias -> channel_id (normalisiert: lowercase, gestrippt)
  258. self._alias_map: dict[str, str] = {}
  259. # XMLTV-ID -> channel_id
  260. self._xmltv_map: dict[str, str] = {}
  261. # Aktive Sender-IDs
  262. self._active_channels: list[str] = []
  263. self._build_maps(channel_filter, blacklist, custom_aliases)
  264. def _build_maps(
  265. self,
  266. channel_filter: list[str] | None,
  267. blacklist: list[str] | None,
  268. custom_aliases: dict[str, str] | None,
  269. ) -> None:
  270. """Baut die internen Lookup-Maps auf."""
  271. # Blacklist normalisieren (lowercase fuer Vergleich)
  272. blackset = {b.lower() for b in (blacklist or [])}
  273. for channel_id, info in CHANNELS.items():
  274. # Whitelist: nur angegebene Sender einbeziehen
  275. if channel_filter and channel_id not in channel_filter:
  276. continue
  277. # Blacklist: Sender ausschliessen (nur wenn keine Whitelist)
  278. if not channel_filter and blackset and channel_id.lower() in blackset:
  279. pdebug(f"ChannelMapper: Sender '{channel_id}' via Blacklist ausgeschlossen")
  280. continue
  281. self._active_channels.append(channel_id)
  282. # Alias-Map aufbauen (alle Aliase + Sender-ID + Display-Name)
  283. all_names = [channel_id, info["name"]] + info.get("aliases", [])
  284. for alias in all_names:
  285. normalized = self._normalize(alias)
  286. if normalized:
  287. self._alias_map[normalized] = channel_id
  288. # XMLTV-ID-Map aufbauen
  289. for xmltv_id in info.get("xmltv_ids", []):
  290. self._xmltv_map[xmltv_id] = channel_id
  291. # Benutzerdefinierte Aliase hinzufuegen (ueberschreiben bei Konflikt)
  292. if custom_aliases:
  293. for alias, channel_id in custom_aliases.items():
  294. if channel_id in CHANNELS:
  295. normalized = self._normalize(alias)
  296. if normalized:
  297. self._alias_map[normalized] = channel_id
  298. pdebug(f"ChannelMapper: Benutzerdefinierter Alias '{alias}' -> '{channel_id}'")
  299. # Sender ggf. auch aktivieren
  300. if channel_id not in self._active_channels:
  301. self._active_channels.append(channel_id)
  302. pdebug(
  303. f"ChannelMapper: {len(self._active_channels)} Sender, "
  304. f"{len(self._alias_map)} Aliase, "
  305. f"{len(self._xmltv_map)} XMLTV-IDs geladen"
  306. )
  307. def resolve(self, spoken_name: str) -> str | None:
  308. """
  309. Loest einen gesprochenen Sendernamen auf eine interne Sender-ID auf.
  310. Versucht zuerst exakte Uebereinstimmung (normalisiert), dann
  311. Teilstring-Suche ueber alle Aliase.
  312. Args:
  313. spoken_name: Der vom Nutzer gesprochene Sendername.
  314. Returns:
  315. Die interne Sender-ID oder None, wenn nicht gefunden.
  316. """
  317. normalized = self._normalize(spoken_name)
  318. if not normalized:
  319. return None
  320. # Exakter Match
  321. if normalized in self._alias_map:
  322. return self._alias_map[normalized]
  323. # Teilstring-Match: laengster passender Alias gewinnt
  324. best_match: str | None = None
  325. best_length = 0
  326. for alias, channel_id in self._alias_map.items():
  327. if alias in normalized or normalized in alias:
  328. if len(alias) > best_length:
  329. best_match = channel_id
  330. best_length = len(alias)
  331. if best_match:
  332. pdebug(f"ChannelMapper: Teilstring-Match '{spoken_name}' -> '{best_match}'")
  333. return best_match
  334. def resolve_xmltv_id(self, xmltv_channel_id: str) -> str | None:
  335. """
  336. Mappt eine XMLTV-Sender-ID auf die interne Sender-ID.
  337. Args:
  338. xmltv_channel_id: Die Sender-ID aus dem XMLTV-Datenformat.
  339. Returns:
  340. Die interne Sender-ID oder None.
  341. """
  342. return self._xmltv_map.get(xmltv_channel_id)
  343. def get_display_name(self, channel_id: str) -> str:
  344. """
  345. Gibt den lesbaren Anzeigenamen eines Senders zurueck.
  346. Geeignet fuer TTS-Ausgabe.
  347. Args:
  348. channel_id: Die interne Sender-ID.
  349. Returns:
  350. Der Anzeigename oder die Sender-ID als Fallback.
  351. """
  352. info = CHANNELS.get(channel_id)
  353. if info:
  354. return info["name"]
  355. return channel_id
  356. def get_all_channel_ids(self) -> list[str]:
  357. """
  358. Gibt alle aktiven Sender-IDs zurueck.
  359. Returns:
  360. Liste aller Sender-IDs, die im Mapper registriert sind.
  361. """
  362. return list(self._active_channels)
  363. @staticmethod
  364. def _normalize(name: str) -> str:
  365. """Normalisiert einen Sendernamen: lowercase, Whitespace gestrippt."""
  366. return name.strip().lower()