From 167e642f47965d4ce8d43078a2d981817dd2d7e4 Mon Sep 17 00:00:00 2001 From: Akamaru Date: Fri, 26 Dec 2025 18:09:33 +0100 Subject: [PATCH] Neu: ZDF Mediathek Serien Bridge --- README.md | 11 ++ ZDFMediathekSeriesBridge.php | 295 +++++++++++++++++++++++++++++++++++ 2 files changed, 306 insertions(+) create mode 100644 ZDFMediathekSeriesBridge.php diff --git a/README.md b/README.md index 0ce4ecb..17fd1eb 100644 --- a/README.md +++ b/README.md @@ -348,6 +348,17 @@ Diese Sammlung enthält verschiedene Bridge-Implementierungen für RSS-Bridge, u - **Sprache**: Wähle zwischen Englisch (Standard), Spanisch, Deutsch, Französisch, Italienisch und Chinesisch - **Plattform**: Wähle zwischen Windows (Standard) oder Mac +### [ZDF Mediathek Serien Bridge](https://bridge.ponywave.de/#bridge-ZDFMediathekSeriesBridge) (Von Akamaru) +- **Beschreibung**: Gibt die neuesten Episoden einer Show aus der ZDF Mediathek zurück +- **Parameter**: + - **Show-Slug**: Der Show-Identifier aus der URL (z.B. "welke-und-pastewka-102" aus /shows/welke-und-pastewka-102) + - **Limit** (optional): Maximale Anzahl an Episoden (Standard: 20) +- **Hinweise**: + - Nutzt die offizielle ZDF Content API + - Unterstützt automatische URL-Erkennung für /shows/ und /video/shows/ URLs + - Feed-Items enthalten Titel, Thumbnail, Teaser-Beschreibung und Sendedatum + - Episoden sind nach Datum sortiert (neueste zuerst) + ## Installation 1. Kopiere die gewünschten Bridge-Dateien in das `bridges`-Verzeichnis deiner RSS-Bridge-Installation diff --git a/ZDFMediathekSeriesBridge.php b/ZDFMediathekSeriesBridge.php new file mode 100644 index 0000000..c4f3789 --- /dev/null +++ b/ZDFMediathekSeriesBridge.php @@ -0,0 +1,295 @@ + [ + 'show_slug' => [ + 'name' => 'Show-Slug (z.B. "welke-und-pastewka-102")', + 'type' => 'text', + 'required' => true, + 'exampleValue' => 'welke-und-pastewka-102', + 'title' => 'Den Show-Slug findest du in der URL: zdf.de/shows/SHOW-SLUG' + ], + 'limit' => [ + 'name' => 'Maximale Anzahl an Episoden', + 'type' => 'number', + 'required' => false, + 'defaultValue' => 20 + ] + ] + ]; + + private $showTitle = ''; + private $showDescription = ''; + + public function getIcon() + { + return 'https://www.google.com/s2/favicons?domain=www.zdf.de&sz=32'; + } + + public function getName() + { + if (!empty($this->showTitle)) { + return $this->showTitle . ' - ZDF Mediathek'; + } + return parent::getName(); + } + + public function getURI() + { + $showSlug = $this->getInput('show_slug'); + + if (!empty($showSlug)) { + return self::URI . '/shows/' . $showSlug; + } + + return self::URI; + } + + public function collectData() + { + $showSlug = $this->getInput('show_slug'); + $limit = $this->getInput('limit') ?? 20; + + if (empty($showSlug)) { + returnClientError('Show-Slug ist erforderlich.'); + } + + // Validate show slug format + if (!preg_match('/^[a-z0-9-]+-\d+$/i', $showSlug)) { + returnClientError('Ungültiger Show-Slug. Format: name-123'); + } + + // Fetch show data from ZDF API + $apiUrl = self::API_URI . $showSlug . '.json?profile=default'; + + // Set custom headers for ZDF API authentication + $curlOpts = [ + CURLOPT_HTTPHEADER => [ + 'Api-Auth: ' . self::API_AUTH + ] + ]; + + $jsonData = getContents($apiUrl, [], $curlOpts); + + if (!$jsonData) { + returnServerError('Konnte die Show-Daten nicht von der ZDF API laden.'); + } + + $data = json_decode($jsonData, true); + + if (!$data) { + returnServerError('Konnte die JSON-Antwort nicht parsen.'); + } + + // Extract show title + $this->showTitle = $data['title'] ?? 'Unbekannte Show'; + + // Extract episodes from modules + $episodes = []; + $modules = $data['module'] ?? []; + + foreach ($modules as $module) { + $moduleTitle = $module['title'] ?? ''; + + // Skip modules that don't look like episode containers + // Episode modules usually have titles like "Folge X mit..." + if (empty($moduleTitle) || stripos($moduleTitle, 'TV-Sendetermine') !== false || stripos($moduleTitle, 'Mehr') !== false) { + continue; + } + + $teasers = $module['teaser'] ?? []; + + foreach ($teasers as $teaser) { + $target = $teaser['http://zdf.de/rels/target'] ?? null; + + if (!$target || empty($target['id'])) { + continue; + } + + // Only include episodes (not other content types) + if (($target['contentType'] ?? '') !== 'episode') { + continue; + } + + $episodes[] = $this->parseEpisode($target); + } + } + + if (empty($episodes)) { + returnServerError('Keine Episoden gefunden. Die Show existiert möglicherweise nicht oder hat keine Episoden.'); + } + + // Sort by editorial date (newest first) + usort($episodes, function ($a, $b) { + $timeA = $a['timestamp'] ?? 0; + $timeB = $b['timestamp'] ?? 0; + return $timeB - $timeA; + }); + + // Limit episodes + $episodes = array_slice($episodes, 0, $limit); + + // Create RSS items + foreach ($episodes as $episode) { + $item = $this->createItemFromEpisode($episode); + $this->items[] = $item; + } + } + + /** + * Parse episode data from API response + */ + private function parseEpisode(array $target): array + { + $episode = []; + + // Title + $episode['title'] = $target['teaserHeadline'] ?? $target['title'] ?? 'Unbekannte Episode'; + + // URL + $episode['url'] = $target['webCanonical'] ?? $target['http://zdf.de/rels/sharing-url'] ?? ''; + + // Description + $episode['description'] = $target['teasertext'] ?? $target['description'] ?? ''; + + // Image + $imageLayouts = $target['teaserImageRef']['layouts'] ?? []; + if (!empty($imageLayouts)) { + // Prefer 1280x720 or 1920x1080 resolution + $episode['image'] = $imageLayouts['1280x720'] ?? $imageLayouts['1920x1080'] ?? $imageLayouts['768x432'] ?? reset($imageLayouts); + } else { + $episode['image'] = null; + } + + // Timestamp from editorialDate + $editorialDate = $target['editorialDate'] ?? null; + if ($editorialDate) { + $timestamp = strtotime($editorialDate); + $episode['timestamp'] = $timestamp !== false ? $timestamp : null; + } else { + $episode['timestamp'] = null; + } + + // Try to extract season/episode from title or metadata + $episode['season'] = null; + $episode['episode'] = null; + + // Check if there's season/episode info in the title + $title = $episode['title']; + if (preg_match('/S(\d+)\s*E(\d+)/i', $title, $matches)) { + $episode['season'] = (int)$matches[1]; + $episode['episode'] = (int)$matches[2]; + } elseif (preg_match('/Staffel\s*(\d+).*?Folge\s*(\d+)/i', $title, $matches)) { + $episode['season'] = (int)$matches[1]; + $episode['episode'] = (int)$matches[2]; + } elseif (preg_match('/Folge\s*(\d+)/i', $title, $matches)) { + // Just episode number without season + $episode['episode'] = (int)$matches[1]; + } + + // UID + $episode['uid'] = $target['id'] ?? md5($episode['url']); + + return $episode; + } + + /** + * Create RSS item from episode data + */ + private function createItemFromEpisode(array $episode): array + { + $item = []; + + // Build title with season/episode info if available + $title = $episode['title'] ?? 'Unbekannte Episode'; + + if (!empty($episode['season']) && !empty($episode['episode'])) { + $seasonEp = sprintf('S%02dE%02d', (int)$episode['season'], (int)$episode['episode']); + $item['title'] = $this->showTitle . ' ' . $seasonEp . ' - ' . $title; + } elseif (!empty($episode['episode'])) { + $item['title'] = $this->showTitle . ' E' . sprintf('%02d', (int)$episode['episode']) . ' - ' . $title; + } else { + $item['title'] = $this->showTitle . ' - ' . $title; + } + + // URL + $episodeUrl = $episode['url'] ?? ''; + if (!empty($episodeUrl)) { + // Handle both absolute and relative URLs + if (strpos($episodeUrl, 'http') === 0) { + $item['uri'] = $episodeUrl; + } else { + $item['uri'] = self::URI . (strpos($episodeUrl, '/') === 0 ? '' : '/') . $episodeUrl; + } + } else { + $item['uri'] = $this->getURI(); + } + + // Timestamp + if (!empty($episode['timestamp'])) { + $item['timestamp'] = $episode['timestamp']; + } + + // UID + $item['uid'] = $episode['uid']; + + // Author + $item['author'] = 'ZDF'; + + // Content: Image + Description + $content = ''; + + // Add image + if (!empty($episode['image'])) { + $imageUrl = $episode['image']; + $content .= '' . htmlspecialchars($title) . '
'; + $item['enclosures'] = [$imageUrl]; + } + + // Add description + if (!empty($episode['description'])) { + $content .= '

' . htmlspecialchars($episode['description']) . '

'; + } + + $item['content'] = $content; + + // Categories + if (!empty($episode['season'])) { + $item['categories'] = ['Staffel ' . $episode['season']]; + } + + return $item; + } + + public function detectParameters($url) + { + // Pattern 1: https://www.zdf.de/shows/welke-und-pastewka-102 + if (preg_match('#zdf\.de/shows/([a-z0-9-]+-\d+)#i', $url, $matches)) { + return [ + 'show_slug' => $matches[1] + ]; + } + + // Pattern 2: Episode URLs - extract show slug from episode URL + // https://www.zdf.de/video/shows/welke-und-pastewka-102/episode-slug + if (preg_match('#zdf\.de/video/shows/([a-z0-9-]+-\d+)/#i', $url, $matches)) { + return [ + 'show_slug' => $matches[1] + ]; + } + + return null; + } +}