From 6d4189730fadd4b1d6bc8383d2bda327c457a817 Mon Sep 17 00:00:00 2001 From: Gogs Date: Tue, 7 Jul 2026 15:54:11 +0200 Subject: [PATCH] Metadata --- app/bin/metadata-test.php | 13 + app/bin/rytec-analyze-subtitles.php | 49 ++ app/bin/rytec-list-sources.php | 31 + app/bin/update-epg.php | 63 ++ app/{tools => bin}/xmltv-analyze.php | 0 app/config/channels.json | 937 ++++++++++++++++++++++++- app/config/config.json | 6 +- app/public/assets/css/modal.css | 7 + app/public/assets/js/modules/modal.js | 41 +- app/src/Metadata/MetadataParser.php | 24 + app/src/Metadata/ParsedMetadata.php | 32 + app/src/Metadata/SubtitleParser.php | 66 ++ app/src/Services/ProgrammeService.php | 3 + app/src/Services/XmlTvCacheService.php | 33 +- app/src/bootstrap.php | 1 + app/templates/day.php | 2 + docker-compose.yaml | 1 + 17 files changed, 1293 insertions(+), 16 deletions(-) create mode 100644 app/bin/metadata-test.php create mode 100644 app/bin/rytec-analyze-subtitles.php create mode 100644 app/bin/rytec-list-sources.php create mode 100644 app/bin/update-epg.php rename app/{tools => bin}/xmltv-analyze.php (100%) create mode 100644 app/src/Metadata/MetadataParser.php create mode 100644 app/src/Metadata/ParsedMetadata.php create mode 100644 app/src/Metadata/SubtitleParser.php diff --git a/app/bin/metadata-test.php b/app/bin/metadata-test.php new file mode 100644 index 0000000..713b305 --- /dev/null +++ b/app/bin/metadata-test.php @@ -0,0 +1,13 @@ +parse([ + 'subtitle' => $subtitle, +]); + +print_r($result->toArray()); diff --git a/app/bin/rytec-analyze-subtitles.php b/app/bin/rytec-analyze-subtitles.php new file mode 100644 index 0000000..7a07265 --- /dev/null +++ b/app/bin/rytec-analyze-subtitles.php @@ -0,0 +1,49 @@ + $count) { + echo str_pad($pattern, 12) . str_pad((string)$count, 8, ' ', STR_PAD_LEFT) . "\n"; + echo " Beispiel: " . $samples[$pattern] . "\n\n"; +} diff --git a/app/bin/rytec-list-sources.php b/app/bin/rytec-list-sources.php new file mode 100644 index 0000000..6acc58f --- /dev/null +++ b/app/bin/rytec-list-sources.php @@ -0,0 +1,31 @@ +xpath('//sourcecat') as $cat) { + $catName = (string)$cat['sourcecatname']; + echo "\n=== {$catName} ===\n"; + + foreach ($cat->source as $source) { + $description = trim((string)$source->description); + echo "- {$description}\n"; + + foreach ($source->url as $sourceUrl) { + echo " " . trim((string)$sourceUrl) . "\n"; + } + } +} diff --git a/app/bin/update-epg.php b/app/bin/update-epg.php new file mode 100644 index 0000000..4ca621a --- /dev/null +++ b/app/bin/update-epg.php @@ -0,0 +1,63 @@ +get('epg', []); +$url = $epg['url'] ?? null; + +if (!$url) { + fwrite(STDERR, "Keine epg.url in config.json gesetzt.\n"); + exit(1); +} + +$target = '/var/www/epg/guide.xml'; +$xzFile = $target . '.xz'; +$tmpXml = $target . '.tmp'; + +echo "Lade EPG: {$url}\n"; + +$data = file_get_contents($url); + +if ($data === false || trim($data) === '') { + fwrite(STDERR, "Download fehlgeschlagen oder leer.\n"); + exit(1); +} + +file_put_contents($xzFile, $data); + +echo "Entpacke XZ...\n"; + +$command = 'xz -dc ' . escapeshellarg($xzFile) . ' > ' . escapeshellarg($tmpXml); +exec($command, $output, $code); + +if ($code !== 0 || !file_exists($tmpXml) || filesize($tmpXml) === 0) { + fwrite(STDERR, "XZ konnte nicht entpackt werden.\n"); + exit(1); +} + +$xml = @simplexml_load_file($tmpXml); + +if (!$xml) { + unlink($tmpXml); + fwrite(STDERR, "XML ungültig.\n"); + exit(1); +} + +rename($tmpXml, $target); + +echo "guide.xml aktualisiert.\n"; + +$cache = new XmlTvCacheService( + $target, + $config->get('timezone', 'Europe/Vienna'), + '/var/www/cache/guide.json' +); + +$cache->rebuildCache(); + +echo "guide.json und guide.version aktualisiert.\n"; diff --git a/app/tools/xmltv-analyze.php b/app/bin/xmltv-analyze.php similarity index 100% rename from app/tools/xmltv-analyze.php rename to app/bin/xmltv-analyze.php diff --git a/app/config/channels.json b/app/config/channels.json index 5eb7dbe..230da44 100644 --- a/app/config/channels.json +++ b/app/config/channels.json @@ -6,7 +6,7 @@ "country": "", "language": "", "enabled": true, - "favorite": false, + "favorite": true, "number": 1, "group": "Unknown" }, @@ -20,5 +20,940 @@ "favorite": true, "number": 2, "group": "Unknown" + }, + "Waidwerk.de": { + "uuid": "7c39fb8df69e25922a78508df7688dd7", + "name": "Waidwerk", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 3, + "group": "Unknown" + }, + "SonnenKlar.de": { + "uuid": "b45be206f058275fec3e81acf235ce0e", + "name": "sonnenklar.TV", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 4, + "group": "Unknown" + }, + "LustPur.de": { + "uuid": "9e711dfc39bb6024e6e19ba1d5cc0856", + "name": "Lust Pur", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 5, + "group": "Unknown" + }, + "NickJr.de": { + "uuid": "c0858303a1917b7fc83f82bc9176e27e", + "name": "Nick Jr.", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 6, + "group": "Unknown" + }, + "Jukebox.de": { + "uuid": "d63e9168e7c4e8b5bd6dc247793d84ba", + "name": "Jukebox", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 7, + "group": "Unknown" + }, + "Kinowelt.de": { + "uuid": "149398af7ab7876a749e79dd0ffce374", + "name": "KinoweltTV", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 8, + "group": "Unknown" + }, + "Heimatkanal.de": { + "uuid": "9b508da77f8fb3c9f50b3f943a9eac58", + "name": "Heimatkanal", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 9, + "group": "Unknown" + }, + "GoldstarTV.de": { + "uuid": "4c50c28383c4b4247d12d2aef810d9ce", + "name": "Goldstar TV", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 10, + "group": "Unknown" + }, + "MotorVision.de": { + "uuid": "509dc630b672a5046d29a2fe63941c86", + "name": "Motorvision", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 11, + "group": "Unknown" + }, + "MorethanSportsTV.de": { + "uuid": "6edfbb95f08e5929884c618d1598db89", + "name": "More than Sports TV", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 12, + "group": "Unknown" + }, + "Silverline.de": { + "uuid": "3b166f29d9750f3cb5f92a85092dc9d2", + "name": "SILVERLINE", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 13, + "group": "Unknown" + }, + "Sat1Comedy.de": { + "uuid": "cd70d139bd14027bd6e825ac65fdf2b8", + "name": "Sat1 Emotions", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 14, + "group": "Unknown" + }, + "ProSiebenFun.de": { + "uuid": "afbc420b3fb37b54e9984c56a0eb2b54", + "name": "ProSieben Fun", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 15, + "group": "Unknown" + }, + "GuteLauneTV.de": { + "uuid": "dd55594d98eae9de065f6c1569d9df92", + "name": "Gute Laune TV", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 16, + "group": "Unknown" + }, + "QVC.de": { + "uuid": "79b3789d98b63e98c1627cddef7658e2", + "name": "QVC", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 17, + "group": "Unknown" + }, + "CartoonNetwork.de": { + "uuid": "2893599f10648e2bdc86388aa0de6011", + "name": "Cartoon Network", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 18, + "group": "Unknown" + }, + "WeltDerWunder.de": { + "uuid": "f13b77009a3024471431b5ed33918547", + "name": "Welt der wunder", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 19, + "group": "Unknown" + }, + "Cartoonito.de": { + "uuid": "7dea097d5a482fd3800f35119b80013c", + "name": "Cartoonito", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 20, + "group": "Unknown" + }, + "SciFi.de": { + "uuid": "a8e96f3bc0ee92ac123bfa5fc33e53be", + "name": "Syfy HD", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 21, + "group": "Unknown" + }, + "RTLLiving.de": { + "uuid": "9d71db690ea9f4fbb74b8f1a269acfb8", + "name": "RTL Living", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 22, + "group": "Unknown" + }, + "RTLCrime.de": { + "uuid": "2984dcc3df9469a1e5bd048c1117dd6a", + "name": "RTL Crime", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 23, + "group": "Unknown" + }, + "Romance.de": { + "uuid": "0a18f79a9e692529d668ef408ec7af57", + "name": "Romance TV", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 24, + "group": "Unknown" + }, + "RiC.de": { + "uuid": "ad6424c7586e2e6b5a8e17d217d22b7b", + "name": "RiC", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 25, + "group": "Unknown" + }, + "HopeChannel.de": { + "uuid": "9259ed1824685eefcc1fc53dc4b7ad75", + "name": "HOPE Channel (Deutschland)", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 26, + "group": "Unknown" + }, + "13thStreet.de": { + "uuid": "06c4cc8de2b115181c4167e2dd2d85d4", + "name": "13th Street", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 27, + "group": "Unknown" + }, + "MarcoPoloTV.de": { + "uuid": "2490efe228d2a3f775330490d306532b", + "name": "Marco Polo TV", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 28, + "group": "Unknown" + }, + "FolxTV.de": { + "uuid": "abadddc0d1187015d8a5fcc81347ff73", + "name": "Folx.TV", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 29, + "group": "Unknown" + }, + "MelodieTV.at": { + "uuid": "0cf3bc1da3bc797da85b76d89f77ad36", + "name": "Melodie TV", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 30, + "group": "Unknown" + }, + "GEO.de": { + "uuid": "e8bb524597bb77e601ab62181e56b585", + "name": "GEO", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 31, + "group": "Unknown" + }, + "TeleclubZoom.de": { + "uuid": "9a39880a3ae50b8a7c61ffc1151726b8", + "name": "Teleclub Zoom D", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 32, + "group": "Unknown" + }, + "Universal.de": { + "uuid": "67652bba6c03b1cb8070efba8cc4d636", + "name": "Universal Channel", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 33, + "group": "Unknown" + }, + "StingrayDJAZZ.de": { + "uuid": "14632348cb1a6d0634edf3e3b2b1e15a", + "name": "Stingray DJAZZ", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 34, + "group": "Unknown" + }, + "Doxx.de": { + "uuid": "1c007cf898c6925962004f8cb7450ad7", + "name": "Doxx", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 35, + "group": "Unknown" + }, + "CuriosityChannel.de": { + "uuid": "8b6182ad1b00e2ef9442336f373eae02", + "name": "Curiosity Channel", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 36, + "group": "Unknown" + }, + "SpiegelGeschichte.de": { + "uuid": "c077c96c5fe8680d1cf95e1e417904ac", + "name": "Spiegel Geschichte", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 37, + "group": "Unknown" + }, + "AXNBlack.de": { + "uuid": "d0073a7cd57d51497f7573cdeed72efb", + "name": "AXN Black", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 38, + "group": "Unknown" + }, + "RTLPassion.de": { + "uuid": "b443978f10a1fb09ee536cef80879281", + "name": "Passion", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 39, + "group": "Unknown" + }, + "NatGeoWild.de": { + "uuid": "6d22aadc8379c796624b0c8938be63e2", + "name": "Nat Geo Wild", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 40, + "group": "Unknown" + }, + "NatGeoHD.de": { + "uuid": "ec42504124fd80a2cd4dc123771c114a", + "name": "National Geographic", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 41, + "group": "Unknown" + }, + "KTV.de": { + "uuid": "55130afe1583a4fcb9840725f9f953a1", + "name": "K-TV", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 42, + "group": "Unknown" + }, + "Dokusat.de": { + "uuid": "3043fa847b0bc74afcfb1068cf6811e3", + "name": "DOKUSAT", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 43, + "group": "Unknown" + }, + "HSE24.de": { + "uuid": "1848bf7daf38a9cc847ecaad5dd58c70", + "name": "HSE24", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 44, + "group": "Unknown" + }, + "StimmungsgartenTV.at": { + "uuid": "8f93e6f411fa3e856ea55b07a791636d", + "name": "Stimmungsgarten TV", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 45, + "group": "Unknown" + }, + "OneTerra.de": { + "uuid": "fb876f337af62d6054ec3d0c31aef0eb", + "name": "One Terra", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 46, + "group": "Unknown" + }, + "JustCooking.de": { + "uuid": "0865f4e098485fb552b85a8917f8ed0e", + "name": "Just Cooking", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 47, + "group": "Unknown" + }, + "CrimeTime.de": { + "uuid": "6b2268cb1fa86039d8333ffd62ff3164", + "name": "Crime Time", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 48, + "group": "Unknown" + }, + "XPLORE.de": { + "uuid": "cb92b8860cc95170eba1c49d180a0032", + "name": "XPLORE", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 49, + "group": "Unknown" + }, + "HistoryChannel.de": { + "uuid": "d1ea08829889f004d3c8c7635d050b8b", + "name": "History HD", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 50, + "group": "Unknown" + }, + "AnimalPlanet.de": { + "uuid": "9047c69ea877e2a36bedaed4c4bec1d6", + "name": "Animal Planet", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 51, + "group": "Unknown" + }, + "SchlagerparadiesTV.de": { + "uuid": "9b101e6c0e7a3b60e8d2bb7f140720d6", + "name": "Schlagerparadies.TV", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 52, + "group": "Unknown" + }, + "SkyNature.de": { + "uuid": "07530a17e31267a0d46886d813ce57c6", + "name": "Sky Nature", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 53, + "group": "Unknown" + }, + "SkyDocumentaries.de": { + "uuid": "c3d64ebf6ac0e1fad0b640b219bf749b", + "name": "Sky Documentaries", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 54, + "group": "Unknown" + }, + "Sky1.de": { + "uuid": "60accaa7994c650e83d596c47598ebf2", + "name": "Sky 1", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 55, + "group": "Unknown" + }, + "FixFoxi.de": { + "uuid": "097b79ba00395357b152b438b881a496", + "name": "Fix & Foxi", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 56, + "group": "Unknown" + }, + "ZweiMusicTelevision.de": { + "uuid": "4e1b5e6d6b8c96108dcb1d1fff880e58", + "name": "Zwei Music Television", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 57, + "group": "Unknown" + }, + "OneMusicTelevision.de": { + "uuid": "30d07ad39d093e6e7f1616a3945c2e32", + "name": "One Music Television", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 58, + "group": "Unknown" + }, + "Volksmusik.TV.de": { + "uuid": "9bc0cf4731e8af6dd0881d9cbec176c3", + "name": "Volksmusik.TV", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 59, + "group": "Unknown" + }, + "AutoMotorSport.de": { + "uuid": "8d1e71830f0220cf123bdbf0aa36511e", + "name": "Auto Motor Sport Ch.", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 60, + "group": "Unknown" + }, + "Nicktoons.de": { + "uuid": "8540f542d08471a7fec72e161c79a8db", + "name": "NickToons", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 61, + "group": "Unknown" + }, + "BonGusto.de": { + "uuid": "81b814ed603dd7b42994977832a33430", + "name": "Bon Gusto HDTV (Germany)", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 62, + "group": "Unknown" + }, + "sportdigital.de": { + "uuid": "316a2675287a8de8392d588086d39e60", + "name": "sportdigital", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 63, + "group": "Unknown" + }, + "eSports1.de": { + "uuid": "f5579fce130d62fecced97ed593ecf6f", + "name": "eSports1", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 64, + "group": "Unknown" + }, + "DMAX.ch": { + "uuid": "e80354ae9085014416542ad27bdb1e6d", + "name": "DMAX", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 65, + "group": "Unknown" + }, + "CrimeInvestigation.de": { + "uuid": "72f53fb9f7e907c5432b51cac2ab45b2", + "name": "Crime Investigation", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 66, + "group": "Unknown" + }, + "DiscoveryChannel.de": { + "uuid": "ce6e2a690ef5d6bdeed95c2c2bd6e694", + "name": "Discovery Channel", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 67, + "group": "Unknown" + }, + "AXNWhite.de": { + "uuid": "0172bb282868afdf299d8db189baa207", + "name": "AXN White", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 68, + "group": "Unknown" + }, + "TraceSportStars.de": { + "uuid": "7d5e897274edb5de649f8057c394f55e", + "name": "Trace Sport", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 69, + "group": "Unknown" + }, + "SPIEGEL_TV_Konflikt.de": { + "uuid": "55fc3f2e61d4d8999f200b2166a63150", + "name": "SPIEGEL TV Konflikt", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 70, + "group": "Unknown" + }, + "TopTrueCrime.de": { + "uuid": "3bb27c9f5c3327f8fcd20156a8bed70e", + "name": "Top True Crime", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 71, + "group": "Unknown" + }, + "StarkeFrauen.de": { + "uuid": "a22a0f06a47d6cc65a850f0f1f70cd22", + "name": "Starke Frauen", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 72, + "group": "Unknown" + }, + "HSETrend.de": { + "uuid": "21322f41712e26cdc61a4d3e2d9b50c2", + "name": "HSE24 Trend", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 73, + "group": "Unknown" + }, + "JuweloTV.de": { + "uuid": "5291c2ab746747b19d99b516720c9d03", + "name": "Juwelo", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 74, + "group": "Unknown" + }, + "QwestTV.de": { + "uuid": "c05cd0e974aa19f276fab0d152190f52", + "name": "Qwest TV", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 75, + "group": "Unknown" + }, + "Tierwelt.de": { + "uuid": "6f8c5e8ac916c268920bc1b2d1ea7d59", + "name": "Tierwelt", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 76, + "group": "Unknown" + }, + "JustFishing.de": { + "uuid": "5a8bee394b422be897e5a909929197c4", + "name": "Just Fishing", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 77, + "group": "Unknown" + }, + "Bronco.de": { + "uuid": "f3559edc34412c7b822aed677ad240b0", + "name": "Bronco", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 78, + "group": "Unknown" + }, + "Hohenrausch.de": { + "uuid": "775cc4a03c27343cfae0bbf87b8266d5", + "name": "Höhenrausch", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 79, + "group": "Unknown" + }, + "Rockland.de": { + "uuid": "ca59aa7e0f56361e7c86543081c1df33", + "name": "Rockland", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 80, + "group": "Unknown" + }, + "DuckTV.day": { + "uuid": "a839cf26e3299413ebc6fbdb25ca4e46", + "name": "Duck TV", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 81, + "group": "Unknown" + }, + "EWTN.de": { + "uuid": "d5af830b7b04d003a3755eeb645296ff", + "name": "EWTN", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 82, + "group": "Unknown" + }, + "DeluxeLounge.de": { + "uuid": "6cbd39ed99d952cca1f3a1cdac3105d9", + "name": "Deluxe Lounge", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 83, + "group": "Unknown" + }, + "DeluxeRock.de": { + "uuid": "04752424234e4e712ffd40f87a02baf2", + "name": "Deluxe Rock", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 84, + "group": "Unknown" + }, + "DeluxeDance.de": { + "uuid": "7ee9e8a42840a750efa6b4dac490a1e7", + "name": "Deluxe Dance", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 85, + "group": "Unknown" + }, + "DeluxeFlashback.de": { + "uuid": "51aebb6b4505d334954ed8ac9a070add", + "name": "Deluxe Flashback", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 86, + "group": "Unknown" + }, + "DeluxeRap.de": { + "uuid": "10f3637dfebdc5fed88313e21f10945e", + "name": "Deluxe Rap", + "icon": "", + "country": "", + "language": "", + "enabled": true, + "favorite": false, + "number": 87, + "group": "Unknown" } } \ No newline at end of file diff --git a/app/config/config.json b/app/config/config.json index ed71cdc..05742ad 100644 --- a/app/config/config.json +++ b/app/config/config.json @@ -4,5 +4,9 @@ "language": "de", "theme": "dark", "days": 7, - "default_view": "today" + "default_view": "today", + "epg": { + "provider": "rytec", + "url": "http://www.xmltvepg.nl/rytecDE_Common.xz" + } } diff --git a/app/public/assets/css/modal.css b/app/public/assets/css/modal.css index 439dd72..ea66143 100644 --- a/app/public/assets/css/modal.css +++ b/app/public/assets/css/modal.css @@ -76,3 +76,10 @@ line-height: 1.55; margin: 0; } + +.programme-modal-meta { + margin: .5rem 0 1rem; + color: var(--muted); + font-size: .92rem; + line-height: 1.4; +} diff --git a/app/public/assets/js/modules/modal.js b/app/public/assets/js/modules/modal.js index ce68720..be1e960 100644 --- a/app/public/assets/js/modules/modal.js +++ b/app/public/assets/js/modules/modal.js @@ -10,6 +10,7 @@ export function initProgrammeModal() { const description = document.getElementById("modalDescription"); const category = document.getElementById("modalCategory"); const time = document.getElementById("modalTime"); + const meta = document.getElementById("modalMeta"); const openModal = (programmeElement) => { const id = programmeElement.dataset.programmeId; @@ -19,10 +20,47 @@ export function initProgrammeModal() { return; } + const metadata = programme.metadata || {}; + title.textContent = programme.title || ""; - subtitle.textContent = programme.subtitle || ""; + subtitle.textContent = + metadata.episode_title || + programme.subtitle || + ""; description.textContent = programme.description || "Keine Beschreibung vorhanden."; category.textContent = programme.category || ""; + + const metaItems = []; + + if (metadata.season && metadata.episode) { + let text = `Staffel ${metadata.season} · Episode ${metadata.episode}`; + + if (metadata.episode_total) { + text += `/${metadata.episode_total}`; + } + + metaItems.push(text); + } + + if (metadata.country) { + metaItems.push(metadata.country); + } + + if (metadata.year) { + metaItems.push(metadata.year); + } + + if (metadata.age_rating) { + metaItems.push(`FSK ${metadata.age_rating}`); + } + + if (Array.isArray(metadata.genres) && metadata.genres.length) { + metaItems.push(metadata.genres.join(", ")); + } + + meta.textContent = metaItems.join(" • "); + meta.hidden = metaItems.length === 0; + time.textContent = `${programme.channel || ""} · ${programme.start || ""} – ${programme.stop || ""}`; modal.hidden = false; @@ -31,6 +69,7 @@ export function initProgrammeModal() { const closeModal = () => { modal.hidden = true; + meta.textContent = ""; document.body.classList.remove("modal-open"); }; diff --git a/app/src/Metadata/MetadataParser.php b/app/src/Metadata/MetadataParser.php new file mode 100644 index 0000000..36aa920 --- /dev/null +++ b/app/src/Metadata/MetadataParser.php @@ -0,0 +1,24 @@ +subtitleParser = new SubtitleParser(); + } + + public function parse(array $programme): ParsedMetadata + { + $metadata = new ParsedMetadata(); + + $subtitle = trim($programme['subtitle'] ?? ''); + + if ($subtitle !== '') { + $this->subtitleParser->parse($subtitle, $metadata); + } + + return $metadata; + } +} diff --git a/app/src/Metadata/ParsedMetadata.php b/app/src/Metadata/ParsedMetadata.php new file mode 100644 index 0000000..55cb21c --- /dev/null +++ b/app/src/Metadata/ParsedMetadata.php @@ -0,0 +1,32 @@ + $this->season, + 'episode' => $this->episode, + 'episode_total' => $this->episodeTotal, + 'episode_title' => $this->episodeTitle, + 'original_title' => $this->originalTitle, + 'genres' => $this->genres, + 'country' => $this->country, + 'year' => $this->year, + 'age_rating' => $this->ageRating, + ]; + } +} diff --git a/app/src/Metadata/SubtitleParser.php b/app/src/Metadata/SubtitleParser.php new file mode 100644 index 0000000..894260d --- /dev/null +++ b/app/src/Metadata/SubtitleParser.php @@ -0,0 +1,66 @@ + $part !== '' + )); + + $count = count($parts); + + if ($count >= 6) { + $this->parseEpisodeCode($parts[0], $metadata); + $metadata->episodeTitle = $parts[1] ?? null; + $metadata->originalTitle = $parts[2] ?? null; + $metadata->genres = $this->parseGenres($parts[3] ?? ''); + $metadata->country = $this->normalizeCountry($parts[4] ?? ''); + $metadata->year = $this->parseYear($parts[5] ?? ''); + $metadata->ageRating = $this->parseAgeRating($parts[6] ?? ''); + return; + } + } + + private function parseEpisodeCode(string $value, ParsedMetadata $metadata): void + { + if (preg_match('/^S(\d+)E(\d+)(?:\/(\d+))?$/i', trim($value), $m)) { + $metadata->season = (int)$m[1]; + $metadata->episode = (int)$m[2]; + $metadata->episodeTotal = isset($m[3]) ? (int)$m[3] : null; + } + } + + private function parseGenres(string $value): array + { + return array_values(array_filter( + array_map('trim', explode(',', $value)), + fn ($genre) => $genre !== '' + )); + } + + private function normalizeCountry(string $value): ?string + { + $value = trim($value); + return $value !== '' ? $value : null; + } + + private function parseYear(string $value): ?int + { + if (preg_match('/\b(19|20)\d{2}\b/', $value, $m)) { + return (int)$m[0]; + } + + return null; + } + + private function parseAgeRating(string $value): ?int + { + if (preg_match('/(\d+)\+?/', $value, $m)) { + return (int)$m[1]; + } + + return null; + } +} diff --git a/app/src/Services/ProgrammeService.php b/app/src/Services/ProgrammeService.php index d00f047..d36816b 100644 --- a/app/src/Services/ProgrammeService.php +++ b/app/src/Services/ProgrammeService.php @@ -35,6 +35,7 @@ class ProgrammeService 'title' => $p['title'] ?? '', 'desc' => $p['description'] ?? '', 'category' => $p['category'] ?? '', + 'metadata' => $p['metadata'] ?? [], 'start' => (new DateTime())->setTimestamp((int)$p['start'])->setTimezone(new DateTimeZone($this->timezone)), 'stop' => (new DateTime())->setTimestamp((int)$p['stop'])->setTimezone(new DateTimeZone($this->timezone)), ]; @@ -80,6 +81,7 @@ class ProgrammeService 'subtitle' => $p['subtitle'] ?? '', 'description' => $p['description'] ?? '', 'category' => $p['category'] ?? '', + 'metadata' => $p['metadata'] ?? [], 'start' => $start, 'stop' => $stop, 'percent' => $percent, @@ -151,6 +153,7 @@ class ProgrammeService 'subtitle' => $p['subtitle'] ?? '', 'description' => $p['description'] ?? '', 'category' => $p['category'] ?? '', + 'metadata' => $p['metadata'] ?? [], 'start' => $start, 'stop' => $stop, 'visible_start' => $visibleStart, diff --git a/app/src/Services/XmlTvCacheService.php b/app/src/Services/XmlTvCacheService.php index 541aa96..851d3b2 100644 --- a/app/src/Services/XmlTvCacheService.php +++ b/app/src/Services/XmlTvCacheService.php @@ -52,6 +52,9 @@ class XmlTvCacheService } $xml = simplexml_load_file($this->file); + $metadataParser = new MetadataParser(); + + foreach ($xml->channel as $channel) { $id = (string)$channel['id']; @@ -78,20 +81,24 @@ class XmlTvCacheService continue; } - $data['channels'][$channelId]['programmes'][] = [ - 'title' => (string)$programme->title, - 'subtitle' => (string)$programme->{'sub-title'}, - 'description' => (string)$programme->desc, - 'category' => (string)$programme->category, - 'start' => $start->getTimestamp(), - 'stop' => $stop->getTimestamp(), - 'duration' => $stop->getTimestamp() - $start->getTimestamp(), - 'year' => (string)$programme->date, - 'rating' => isset($programme->rating->value) ? (string)$programme->rating->value : '', - 'episode' => isset($programme->{'episode-num'}) ? (string)$programme->{'episode-num'} : '', - 'icon' => isset($programme->icon) ? (string)$programme->icon['src'] : '', - 'country' => (string)$programme->country, + $item = [ + 'title' => (string)$programme->title, + 'subtitle' => (string)$programme->{'sub-title'}, + 'description' => (string)$programme->desc, + 'category' => (string)$programme->category, + 'start' => $start->getTimestamp(), + 'stop' => $stop->getTimestamp(), + 'duration' => $stop->getTimestamp() - $start->getTimestamp(), + 'year' => (string)$programme->date, + 'rating' => isset($programme->rating->value) ? (string)$programme->rating->value : '', + 'episode' => isset($programme->{'episode-num'}) ? (string)$programme->{'episode-num'} : '', + 'icon' => isset($programme->icon) ? (string)$programme->icon['src'] : '', + 'country' => (string)$programme->country, ]; + + $item['metadata'] = $metadataParser->parse($item)->toArray(); + + $data['channels'][$channelId]['programmes'][] = $item; } foreach ($data['channels'] as &$channel) { diff --git a/app/src/bootstrap.php b/app/src/bootstrap.php index c78295a..b523727 100644 --- a/app/src/bootstrap.php +++ b/app/src/bootstrap.php @@ -6,6 +6,7 @@ spl_autoload_register(function (string $class): void { '/var/www/src/Controllers/' . $class . '.php', '/var/www/src/Services/' . $class . '.php', '/var/www/src/Http/' . $class . '.php', + '/var/www/src/Metadata/' . $class . '.php', ]; foreach ($paths as $file) { diff --git a/app/templates/day.php b/app/templates/day.php index 50bd15d..4a5879a 100644 --- a/app/templates/day.php +++ b/app/templates/day.php @@ -48,6 +48,7 @@ foreach ($dayProgrammes as $channel) { 'subtitle' => $programme['subtitle'], 'description' => $programme['description'], 'category' => $programme['category'], + 'metadata' => $programme['metadata'] ?? [], 'start' => date('H:i', $programme['start']), 'stop' => date('H:i', $programme['stop']), ]; @@ -236,6 +237,7 @@ foreach ($dayProgrammes as $channel) {
+

diff --git a/docker-compose.yaml b/docker-compose.yaml index cc9d16d..d998725 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -11,6 +11,7 @@ services: - ./app/templates:/var/www/templates - ./app/config:/var/www/config - ./app/cache:/var/www/cache + - ./app/bin:/var/www/bin - ./vendor:/var/www/vendor - ./epg:/var/www/epg - ./logos:/var/www/html/logos