diff --git a/BpbMediathekSeriesBridge.php b/BpbMediathekSeriesBridge.php
new file mode 100644
index 0000000..ece68cd
--- /dev/null
+++ b/BpbMediathekSeriesBridge.php
@@ -0,0 +1,281 @@
+ [
+ 'series_slug' => [
+ 'name' => 'Serien-Slug (z.B. "faketrain")',
+ 'type' => 'text',
+ 'required' => true,
+ 'exampleValue' => 'faketrain',
+ 'title' => 'Den Serien-Slug findest du in der URL: bpb.de/mediathek/reihen/SERIEN-SLUG/'
+ ],
+ 'limit' => [
+ 'name' => 'Maximale Anzahl an Episoden',
+ 'type' => 'number',
+ 'required' => false,
+ 'defaultValue' => 20
+ ],
+ 'fetch_details' => [
+ 'name' => 'Detaillierte Metadaten laden',
+ 'type' => 'checkbox',
+ 'required' => false,
+ 'defaultValue' => 'checked',
+ 'title' => 'Lädt JSON-LD Metadaten von Episodenseiten (Thumbnail, Beschreibung, Datum)'
+ ]
+ ]
+ ];
+
+ private $seriesTitle = '';
+
+ public function collectData()
+ {
+ $seriesSlug = $this->getInput('series_slug');
+ $limit = $this->getInput('limit') ?? 20;
+ $fetchDetails = $this->getInput('fetch_details') ?? false;
+
+ if (empty($seriesSlug)) {
+ returnClientError('Serien-Slug ist erforderlich.');
+ }
+
+ // Build series listing URL
+ $seriesUrl = self::URI . '/mediathek/reihen/' . $seriesSlug . '/';
+
+ // Fetch HTML
+ $html = getSimpleHTMLDOM($seriesUrl);
+ if (!$html) {
+ returnServerError('Konnte die Serien-Seite nicht laden: ' . $seriesUrl);
+ }
+
+ // Extract series title from page
+ $titleTag = $html->find('h1', 0);
+ if ($titleTag) {
+ $this->seriesTitle = trim($titleTag->plaintext);
+ } else {
+ $this->seriesTitle = 'Unbekannte Serie';
+ }
+
+ // Find all episode entries (h3 tags with links)
+ $episodeElements = $html->find('h3 a[href*="/mediathek/"]');
+
+ if (empty($episodeElements)) {
+ returnServerError('Keine Episoden gefunden.');
+ }
+
+ // Collect all episodes (no early limit for proper sorting)
+ $episodes = [];
+
+ foreach ($episodeElements as $episodeLink) {
+ $episode = [];
+
+ // Extract title
+ $episode['title'] = trim($episodeLink->plaintext);
+
+ // Extract URL
+ $episode['url'] = $this->normalizeUrl($episodeLink->href);
+
+ // If fetch_details is enabled, load individual episode page
+ if ($fetchDetails) {
+ $this->enrichEpisodeWithJsonLD($episode);
+ } else {
+ // Basic metadata only
+ $episode['timestamp'] = 0; // Will be sorted to bottom
+ $episode['thumbnail'] = null;
+ $episode['description'] = '';
+ }
+
+ $episodes[] = $episode;
+ }
+
+ // Sort episodes: newest first (by timestamp)
+ usort($episodes, function ($a, $b) {
+ $timeA = $a['timestamp'] ?? 0;
+ $timeB = $b['timestamp'] ?? 0;
+ return $timeB <=> $timeA; // Descending order
+ });
+
+ // Apply limit after sorting
+ $episodes = array_slice($episodes, 0, $limit);
+
+ // Create RSS items from episodes
+ foreach ($episodes as $episode) {
+ $item = [];
+
+ // Title: Episode title only
+ $item['title'] = $episode['title'];
+
+ // URL
+ $item['uri'] = $episode['url'];
+
+ // Unique ID
+ $item['uid'] = md5($episode['url']);
+
+ // Timestamp
+ if (!empty($episode['timestamp'])) {
+ $item['timestamp'] = $episode['timestamp'];
+ }
+
+ // Author
+ $item['author'] = 'bpb.de';
+
+ // Build content HTML (only image + description)
+ $content = '';
+
+ // Thumbnail
+ if (!empty($episode['thumbnail'])) {
+ $content .= '
';
+ $item['enclosures'] = [$episode['thumbnail']];
+ }
+
+ // Description
+ if (!empty($episode['description'])) {
+ $content .= '
' . $episode['description'] . '
'; + } + + $item['content'] = $content; + + $this->items[] = $item; + } + } + + /** + * Enriches episode data by fetching and parsing JSON-LD from episode page + */ + private function enrichEpisodeWithJsonLD(&$episode) + { + try { + $episodeHtml = getSimpleHTMLDOM($episode['url']); + if (!$episodeHtml) { + // Failed to load, use basic data + $episode['timestamp'] = 0; + $episode['thumbnail'] = null; + $episode['description'] = ''; + return; + } + + // Extract JSON-LD script tag + $jsonLdScript = $episodeHtml->find('script[type="application/ld+json"]', 0); + if (!$jsonLdScript) { + $episode['timestamp'] = 0; + $episode['thumbnail'] = null; + $episode['description'] = ''; + return; + } + + $jsonData = json_decode($jsonLdScript->innertext, true); + if (!$jsonData || json_last_error() !== JSON_ERROR_NONE) { + $episode['timestamp'] = 0; + $episode['thumbnail'] = null; + $episode['description'] = ''; + return; + } + + // Parse VideoObject schema + if (isset($jsonData['@type']) && $jsonData['@type'] === 'VideoObject') { + // Thumbnail/image + if (isset($jsonData['thumbnailUrl'])) { + $episode['thumbnail'] = is_array($jsonData['thumbnailUrl']) + ? $jsonData['thumbnailUrl'][0] + : $jsonData['thumbnailUrl']; + } else { + $episode['thumbnail'] = null; + } + + // Description + if (isset($jsonData['description'])) { + $episode['description'] = $jsonData['description']; + } else { + $episode['description'] = ''; + } + + // Upload/publish date + if (isset($jsonData['uploadDate'])) { + $timestamp = strtotime($jsonData['uploadDate']); + if ($timestamp !== false) { + $episode['timestamp'] = $timestamp; + } else { + $episode['timestamp'] = 0; + } + } elseif (isset($jsonData['datePublished'])) { + $timestamp = strtotime($jsonData['datePublished']); + if ($timestamp !== false) { + $episode['timestamp'] = $timestamp; + } else { + $episode['timestamp'] = 0; + } + } else { + $episode['timestamp'] = 0; + } + } else { + // Not a VideoObject, use defaults + $episode['timestamp'] = 0; + $episode['thumbnail'] = null; + $episode['description'] = ''; + } + } catch (Exception $e) { + // Graceful degradation: Silent fail, use basic data + $episode['timestamp'] = 0; + $episode['thumbnail'] = null; + $episode['description'] = ''; + } + } + + /** + * Normalizes relative URLs to absolute URLs + */ + private function normalizeUrl($url) + { + if (strpos($url, 'http') === 0) { + return $url; + } + + return strpos($url, '/') === 0 + ? self::URI . $url + : self::URI . '/' . $url; + } + + public function getName() + { + if (!empty($this->seriesTitle)) { + return $this->seriesTitle . ' - bpb.de Mediathek'; + } + return parent::getName(); + } + + public function getURI() + { + $seriesSlug = $this->getInput('series_slug'); + + if (!empty($seriesSlug)) { + return self::URI . '/mediathek/reihen/' . $seriesSlug . '/'; + } + + return self::URI; + } + + public function getIcon() + { + return 'https://www.google.com/s2/favicons?domain=www.bpb.de&sz=32'; + } + + public function detectParameters($url) + { + // Pattern: https://www.bpb.de/mediathek/reihen/SLUG/ + if (preg_match('#bpb\.de/mediathek/reihen/([a-z0-9-]+)/?#i', $url, $matches)) { + return [ + 'series_slug' => $matches[1] + ]; + } + + return null; + } +} diff --git a/README.md b/README.md index 9fd71df..272f09a 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,17 @@ Diese Sammlung enthält verschiedene Bridge-Implementierungen für RSS-Bridge, u - **Parameter**: - **Kategorie**: All, Announcements, Updates, Events +### [bpb.de Mediathek Serien Bridge](https://bridge.ponywave.de/#bridge-BpbMediathekSeriesBridge) (Von Akamaru) +- **Beschreibung**: Gibt die neuesten Episoden einer Serie aus der bpb.de Mediathek zurück +- **Parameter**: + - **Serien-Slug**: Der Serien-Slug aus der URL (z.B. "faketrain" aus bpb.de/mediathek/reihen/faketrain/) + - **Limit** (optional): Maximale Anzahl an Episoden (Standard: 20) + - **Detaillierte Metadaten laden** (optional): Checkbox zum Laden von JSON-LD Metadaten (Thumbnail, Beschreibung, Datum) +- **Hinweise**: + - Sortierung: Neueste Folge zuerst + - Extrahiert Thumbnails, Beschreibungen und Upload-Datum von individuellen Episodenseiten + - Unterstützt URL-Auto-Detection via detectParameters() + ### [Brown Dust 2 News Bridge](https://bridge.ponywave.de/#bridge-BrownDust2Bridge) (Von Akamaru) - **Beschreibung**: Zeigt die neuesten Nachrichten von Brown Dust 2 - **Parameter**: