diff --git a/DiscoveryPlusBridge.php b/DiscoveryPlusBridge.php
new file mode 100644
index 0000000..57ae929
--- /dev/null
+++ b/DiscoveryPlusBridge.php
@@ -0,0 +1,344 @@
+ [
+ 'name' => 'Show-ID',
+ 'type' => 'text',
+ 'title' => 'UUID der Serie (z.B. 91d2c345-d885-44b4-bb45-e696ea6c6f0d)',
+ 'required' => true,
+ 'exampleValue' => '91d2c345-d885-44b4-bb45-e696ea6c6f0d'
+ ],
+ 'season' => [
+ 'name' => 'Staffel',
+ 'type' => 'number',
+ 'title' => 'Staffelnummer (leer = höchste gefundene Staffel)',
+ 'required' => false,
+ 'defaultValue' => ''
+ ]
+ ]
+ ];
+
+ const TOKEN_URL = 'https://default.any-any.prd.api.discoveryplus.com/token?realm=bolt';
+ const API_BASE = 'https://default.any-any.prd.api.discoveryplus.com/cms/routes/show/';
+
+ private $showName = '';
+ private $token = null;
+
+ public function getName()
+ {
+ if (!empty($this->showName)) {
+ return $this->showName . ' - Discovery+';
+ }
+ return parent::getName();
+ }
+
+ /**
+ * Get an anonymous access token from Discovery+ API
+ */
+ private function getToken()
+ {
+ // Check cache first
+ $cachedToken = $this->loadCacheValue('discovery_token', 3600); // Cache for 1 hour
+ if ($cachedToken !== null && is_string($cachedToken) && !empty($cachedToken)) {
+ return $cachedToken;
+ }
+
+ // Generate a random device ID
+ $deviceId = sprintf(
+ '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
+ mt_rand(0, 0xffff),
+ mt_rand(0, 0xffff),
+ mt_rand(0, 0xffff),
+ mt_rand(0, 0x0fff) | 0x4000,
+ mt_rand(0, 0x3fff) | 0x8000,
+ mt_rand(0, 0xffff),
+ mt_rand(0, 0xffff),
+ mt_rand(0, 0xffff)
+ );
+
+ $headers = [
+ 'Content-Type: application/json',
+ 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
+ 'x-disco-client: WEB:NT 10.0:dplus:6.8.1',
+ 'x-disco-params: realm=bolt,bid=dplus,features=ar',
+ 'x-device-info: dplus/6.8.1 (desktop/desktop; Windows/NT 10.0; ' . $deviceId . '/unknown)',
+ 'Origin: https://play.discoveryplus.com',
+ 'Referer: https://play.discoveryplus.com/'
+ ];
+
+ // Use CURLOPT_HEADER to get response headers
+ $opts = [
+ CURLOPT_HEADER => true,
+ CURLOPT_RETURNTRANSFER => true
+ ];
+
+ try {
+ $fullResponse = getContents(self::TOKEN_URL, $headers, $opts);
+ } catch (Exception $e) {
+ returnServerError('Token request failed: ' . $e->getMessage());
+ }
+
+ if ($fullResponse === false || empty($fullResponse)) {
+ returnServerError('Empty response from Discovery+ token endpoint');
+ }
+
+ // Split headers and body
+ $parts = explode("\r\n\r\n", $fullResponse, 2);
+ if (count($parts) < 2) {
+ returnServerError('Invalid token response format - could not split headers and body');
+ }
+
+ $headerSection = $parts[0];
+
+ // Extract token from Set-Cookie header
+ if (preg_match('/Set-Cookie:.*?st=([^;]+)/i', $headerSection, $matches)) {
+ $token = $matches[1];
+ $this->saveCacheValue('discovery_token', $token);
+ return $token;
+ }
+
+ returnServerError('Could not find st token in response headers. Headers: ' . substr($headerSection, 0, 500));
+ }
+
+ /**
+ * Make an authenticated API request to Discovery+
+ */
+ private function makeApiRequest($url)
+ {
+ if ($this->token === null) {
+ $this->token = $this->getToken();
+ }
+
+ if (empty($this->token)) {
+ returnServerError('Token is empty or invalid');
+ }
+
+ $headers = [
+ 'Content-Type: application/json',
+ 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
+ 'x-disco-client: WEB:NT 10.0:dplus:6.8.1',
+ 'x-disco-params: realm=bolt,bid=dplus,features=ar',
+ 'x-device-info: dplus/6.8.1 (desktop/desktop; Windows/NT 10.0)',
+ 'Cookie: st=' . $this->token,
+ 'Origin: https://play.discoveryplus.com',
+ 'Referer: https://play.discoveryplus.com/'
+ ];
+
+ try {
+ $response = getContents($url, $headers);
+ } catch (Exception $e) {
+ // If we get a 400, try to get more details
+ $tokenInfo = is_string($this->token) ? 'Token length: ' . strlen($this->token) : 'Token type: ' . gettype($this->token);
+ returnServerError('API request failed: ' . $e->getMessage() . '. ' . $tokenInfo);
+ }
+
+ $data = json_decode($response, true);
+
+ if (!$data) {
+ returnServerError('Could not parse API response');
+ }
+
+ if (isset($data['errors'])) {
+ returnServerError('API Error: ' . json_encode($data['errors']));
+ }
+
+ return $data;
+ }
+
+ public function collectData()
+ {
+ $showId = $this->getInput('show_id');
+ if (empty($showId)) {
+ returnClientError('Show-ID ist erforderlich');
+ }
+
+ // Validate show ID format (should be a UUID)
+ if (!preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $showId)) {
+ returnClientError('Invalid Show-ID format. Expected UUID (e.g., 91d2c345-d885-44b4-bb45-e696ea6c6f0d)');
+ }
+
+ // Get desired season (null = highest available)
+ $desiredSeason = $this->getInput('season');
+ if ($desiredSeason !== null && $desiredSeason !== '') {
+ $desiredSeason = (int)$desiredSeason;
+ } else {
+ $desiredSeason = null;
+ }
+
+ // Step 1: Get show data with filters to find available seasons
+ $showUrl = self::API_BASE . $showId . '?include=default&decorators=viewingHistory,isFavorite,contentAction,badges&page[items.size]=10';
+ $showData = $this->makeApiRequest($showUrl);
+
+ if (!isset($showData['data'])) {
+ returnServerError('Keine Show-Daten gefunden');
+ }
+
+ // Extract show name
+ foreach ($showData['included'] ?? [] as $item) {
+ if ($item['type'] === 'show' && $item['id'] === $showId) {
+ $this->showName = $item['attributes']['name'] ?? '';
+ break;
+ }
+ }
+
+ // Step 2: Find collection with season filters
+ $collectionId = null;
+ $availableSeasons = [];
+
+ foreach ($showData['included'] ?? [] as $item) {
+ if ($item['type'] === 'collection' && isset($item['attributes']['component']['filters'])) {
+ foreach ($item['attributes']['component']['filters'] as $filter) {
+ if ($filter['id'] === 'seasonNumber' && isset($filter['options'])) {
+ // Found the collection with season filters!
+ $collectionId = $item['id'];
+ foreach ($filter['options'] as $option) {
+ $seasonNum = (int)$option['id'];
+ $availableSeasons[$seasonNum] = $option;
+ }
+ break 2; // Break out of both loops
+ }
+ }
+ }
+ }
+
+ if (empty($availableSeasons)) {
+ returnServerError('Keine Staffeln für diese Serie gefunden');
+ }
+
+ if ($collectionId === null) {
+ returnServerError('Konnte Collection-ID nicht finden');
+ }
+
+ // Determine target season
+ if ($desiredSeason === null) {
+ $targetSeason = max(array_keys($availableSeasons));
+ } else {
+ $targetSeason = $desiredSeason;
+ }
+
+ if (!isset($availableSeasons[$targetSeason])) {
+ returnServerError("Staffel $targetSeason nicht gefunden. Verfügbare Staffeln: " . implode(', ', array_keys($availableSeasons)));
+ }
+
+ // Step 3: Get episodes for the selected season
+ $episodesUrl = "https://default.any-any.prd.api.discoveryplus.com/cms/collections/{$collectionId}?include=default&decorators=viewingHistory,isFavorite,contentAction,badges&pf[show.id]={$showId}&pf[seasonNumber]={$targetSeason}";
+ $episodesData = $this->makeApiRequest($episodesUrl);
+
+ if (!isset($episodesData['included'])) {
+ returnServerError('Keine Episoden in der API-Antwort gefunden');
+ }
+
+ // Extract episodes and build image lookup table
+ $episodes = [];
+ $images = [];
+ foreach ($episodesData['included'] as $item) {
+ if ($item['type'] === 'video') {
+ $episodes[] = $item;
+ } elseif ($item['type'] === 'image') {
+ $images[$item['id']] = $item['attributes'];
+ }
+ }
+
+ if (empty($episodes)) {
+ returnServerError("Keine Episoden für Staffel $targetSeason gefunden");
+ }
+
+ // Sort by episode number (descending - newest first)
+ usort($episodes, function ($a, $b) {
+ $epA = $a['attributes']['episodeNumber'] ?? 0;
+ $epB = $b['attributes']['episodeNumber'] ?? 0;
+ return $epB - $epA;
+ });
+
+ // Convert episodes to RSS items
+ foreach ($episodes as $episode) {
+ $item = [];
+
+ $attrs = $episode['attributes'];
+
+ // Build title
+ $seasonNum = $attrs['seasonNumber'] ?? 1;
+ $episodeNum = $attrs['episodeNumber'] ?? 0;
+ $episodeName = $attrs['name'] ?? '';
+
+ $item['title'] = sprintf(
+ '%s S%02dE%02d - %s',
+ $this->showName,
+ $seasonNum,
+ $episodeNum,
+ $episodeName
+ );
+
+ // Description
+ $description = $attrs['description'] ?? '';
+ $item['content'] = $description;
+
+ // Build episode URL
+ $alternateId = $attrs['alternateId'] ?? $episode['id'];
+ $item['uri'] = sprintf(
+ 'https://www.discoveryplus.com/de/de/video/%s',
+ $alternateId
+ );
+
+ // Timestamp from air date
+ if (isset($attrs['airDate'])) {
+ $item['timestamp'] = strtotime($attrs['airDate']);
+ } elseif (isset($attrs['publishStart'])) {
+ $item['timestamp'] = strtotime($attrs['publishStart']);
+ }
+
+ // Author
+ $item['author'] = 'Discovery+';
+
+ // Extract image from relationships
+ $imageUrl = null;
+ if (isset($episode['relationships']['images']['data']) && is_array($episode['relationships']['images']['data'])) {
+ $imageRefs = $episode['relationships']['images']['data'];
+
+ // Try to find 'default' kind first
+ foreach ($imageRefs as $imageRef) {
+ $imageId = $imageRef['id'] ?? null;
+ if ($imageId && isset($images[$imageId])) {
+ $imageData = $images[$imageId];
+ if (isset($imageData['kind']) && $imageData['kind'] === 'default' && isset($imageData['src'])) {
+ $imageUrl = $imageData['src'];
+ break;
+ }
+ }
+ }
+
+ // If no 'default' image, take the first available
+ if (!$imageUrl && !empty($imageRefs)) {
+ $firstImageId = $imageRefs[0]['id'] ?? null;
+ if ($firstImageId && isset($images[$firstImageId]['src'])) {
+ $imageUrl = $images[$firstImageId]['src'];
+ }
+ }
+ }
+
+ if ($imageUrl) {
+ $item['enclosures'] = [$imageUrl];
+ $item['content'] = ' . ')
' . $description;
+ }
+
+ $this->items[] = $item;
+ }
+ }
+
+ public function getIcon()
+ {
+ return self::ICON;
+ }
+}
diff --git a/README.md b/README.md
index 44d5a5d..44df096 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,17 @@ Diese Sammlung enthält verschiedene Bridge-Implementierungen für RSS-Bridge, u
- **Parameter**:
- **Sender**: DMAX, TLC, HGTV
+### Discovery+ Bridge (Von Akamaru)
+- **Beschreibung**: RSS-Feed für Serien von Discovery+. Nutzt die offizielle Discovery+ API mit anonymer Authentifizierung.
+- **Parameter**:
+ - **Show-ID**: Die UUID der Serie (z.B. 91d2c345-d885-44b4-bb45-e696ea6c6f0d aus der URL /show/91d2c345-d885-44b4-bb45-e696ea6c6f0d)
+ - **Staffel** (optional): Staffelnummer (z.B. 12). Wenn leer, wird die höchste verfügbare Staffel verwendet
+- **Hinweise**:
+ - Feed-Items enthalten Episodenbeschreibung, Thumbnail (falls verfügbar) und direkten Link zur Episode
+ - Titel im Format "Serienname S01E01 - Episodentitel"
+ - Episoden sind nach Nummer sortiert (neueste zuerst)
+ - Autor ist auf "Discovery+" gesetzt
+
### Dubesor Bridge (Von Brawl, Claude)
- **Beschreibung**: First Impressions Blog von Dubesor zu LLM-Benchmarks