From 53090e6863898b6fe4198271dbbd6c25d18d712e Mon Sep 17 00:00:00 2001 From: Akamaru Date: Mon, 24 Nov 2025 00:00:16 +0100 Subject: [PATCH] Neu: TVMaze Series Bridge --- TVMazeSeriesBridge.php | 193 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 TVMazeSeriesBridge.php diff --git a/TVMazeSeriesBridge.php b/TVMazeSeriesBridge.php new file mode 100644 index 0000000..1a65f78 --- /dev/null +++ b/TVMazeSeriesBridge.php @@ -0,0 +1,193 @@ + [ + 'show_id' => [ + 'name' => 'TVMaze Show ID', + 'type' => 'number', + 'required' => true, + 'exampleValue' => '54421', + 'title' => 'Find the Show ID in the URL: tvmaze.com/shows/ID/name' + ], + 'limit' => [ + 'name' => 'Maximum number of episodes', + 'type' => 'number', + 'required' => false, + 'defaultValue' => 50 + ] + ] + ]; + + private $showTitle = ''; + private $showUrl = ''; + private $officialSite = ''; + private $webChannelName = ''; + + public function getIcon() + { + // Use streaming service favicon if available + if (!empty($this->officialSite)) { + $host = parse_url($this->officialSite, PHP_URL_HOST); + if ($host) { + return 'https://www.google.com/s2/favicons?domain=' . $host . '&sz=32'; + } + } + return 'https://www.google.com/s2/favicons?domain=www.tvmaze.com&sz=32'; + } + + public function getName() + { + if (!empty($this->showTitle)) { + $suffix = !empty($this->webChannelName) ? $this->webChannelName : 'TVMaze'; + return $this->showTitle . ' - ' . $suffix; + } + return parent::getName(); + } + + public function getURI() + { + // Prefer officialSite (e.g. Amazon Prime link) + if (!empty($this->officialSite)) { + return $this->officialSite; + } + + if (!empty($this->showUrl)) { + return $this->showUrl; + } + + $showId = $this->getInput('show_id'); + if (!empty($showId)) { + return self::URI . '/shows/' . $showId; + } + + return self::URI; + } + + public function collectData() + { + $showId = $this->getInput('show_id'); + $limit = $this->getInput('limit') ?? 50; + + if (empty($showId)) { + returnClientError('Show ID is required.'); + } + + // First fetch show information + $showUrl = 'https://api.tvmaze.com/shows/' . urlencode((string)$showId); + $showJson = getContents($showUrl); + $showData = json_decode($showJson, true); + + if (!$showData) { + returnServerError('Show not found. Please check the Show ID.'); + } + + $this->showTitle = $showData['name'] ?? 'Unknown Series'; + $this->showUrl = $showData['url'] ?? ''; + $this->officialSite = $showData['officialSite'] ?? ''; + $this->webChannelName = $showData['webChannel']['name'] ?? $showData['network']['name'] ?? ''; + + // Fetch episodes + $episodesUrl = 'https://api.tvmaze.com/shows/' . urlencode((string)$showId) . '/episodes'; + $episodesJson = getContents($episodesUrl); + $episodes = json_decode($episodesJson, true); + + if (!$episodes || !is_array($episodes)) { + returnServerError('No episodes found.'); + } + + // Sort by season and episode (newest first for feed) + usort($episodes, function ($a, $b) { + // First by season, then by episode (descending) + if ($a['season'] !== $b['season']) { + return $b['season'] - $a['season']; + } + return $b['number'] - $a['number']; + }); + + // Limit the number of episodes + $episodes = array_slice($episodes, 0, (int)$limit); + + foreach ($episodes as $episode) { + $item = []; + + // Title: "Series Title S01E01: Episode Title" + $seasonNum = $episode['season'] ?? 1; + $episodeNum = $episode['number'] ?? 0; + $episodeTitle = $episode['name'] ?? 'Unknown Episode'; + + $item['title'] = sprintf( + '%s S%02dE%02d: %s', + $this->showTitle, + $seasonNum, + $episodeNum, + $episodeTitle + ); + + // Episode URL (prefer streaming service officialSite) + if (!empty($this->officialSite)) { + $item['uri'] = $this->officialSite; + } else { + $item['uri'] = $episode['url'] ?? self::URI; + } + + // Author: Streaming service name + if (!empty($this->webChannelName)) { + $item['author'] = $this->webChannelName; + } + + // Unique ID + $item['uid'] = 'tvmaze-' . ($episode['id'] ?? $showId . '-' . $seasonNum . '-' . $episodeNum); + + // Timestamp from airdate + if (!empty($episode['airdate'])) { + $timestamp = strtotime($episode['airdate']); + if ($timestamp !== false) { + $item['timestamp'] = $timestamp; + } + } + + // Content: Image + Description + Details + $content = ''; + + // Thumbnail + $imageUrl = $episode['image']['original'] ?? $episode['image']['medium'] ?? null; + if ($imageUrl) { + $content .= '' . htmlspecialchars($episodeTitle) . '
'; + $item['enclosures'] = [$imageUrl]; + } + + // Description + $summary = $episode['summary'] ?? ''; + if (!empty($summary)) { + // TVMaze returns HTML, keep it for content + $content .= '

' . $summary . '

'; + } + + $item['content'] = $content; + + $this->items[] = $item; + } + } + + public function detectParameters($url) + { + // URL-Format: https://www.tvmaze.com/shows/54421/lol-last-one-laughing + if (preg_match('#tvmaze\.com/shows/(\d+)#i', $url, $matches)) { + return [ + 'show_id' => $matches[1] + ]; + } + + return null; + } +}