Neu: ZDF Mediathek Serien Bridge
This commit is contained in:
11
README.md
11
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
|
||||
|
||||
295
ZDFMediathekSeriesBridge.php
Normal file
295
ZDFMediathekSeriesBridge.php
Normal file
@@ -0,0 +1,295 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class ZDFMediathekSeriesBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'ZDF Mediathek Serien Bridge';
|
||||
const URI = 'https://www.zdf.de';
|
||||
const API_URI = 'https://api.zdf.de/content/documents/';
|
||||
const API_AUTH = 'Bearer aa3noh4ohz9eeboo8shiesheec9ciequ9Quah7el';
|
||||
const DESCRIPTION = 'Gibt die neuesten Episoden einer Show aus der ZDF Mediathek zurück.';
|
||||
const MAINTAINER = 'Akamaru';
|
||||
const CACHE_TIMEOUT = 3600; // 1 Stunde
|
||||
|
||||
const PARAMETERS = [
|
||||
'Show' => [
|
||||
'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 .= '<img src="' . htmlspecialchars($imageUrl) . '" alt="' . htmlspecialchars($title) . '" /><br>';
|
||||
$item['enclosures'] = [$imageUrl];
|
||||
}
|
||||
|
||||
// Add description
|
||||
if (!empty($episode['description'])) {
|
||||
$content .= '<p>' . htmlspecialchars($episode['description']) . '</p>';
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user