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 .= '
';
+ $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; + } +}