[ 'name' => 'Serien-ID', 'type' => 'text', 'title' => 'ID der Serie (z.B. 948092 für Die Nibelungen)', 'required' => true, 'exampleValue' => '948092' ] ] ]; private $feedName = null; private $authToken = null; private function getAuthToken() { if ($this->authToken !== null) { return $this->authToken; } // Hole anonymen/Guest Token von der Auth-API $authUrl = 'https://auth.rtl.de/auth/realms/rtlplus/protocol/openid-connect/token'; $postData = http_build_query([ 'grant_type' => 'client_credentials', 'client_id' => 'anonymous-user', 'client_secret' => '4bfeb73f-1c4a-4e9f-a7fa-96aa1ad3d94c' ]); $headers = array('Content-Type: application/x-www-form-urlencoded'); $opts = array( CURLOPT_POST => 1, CURLOPT_POSTFIELDS => $postData ); $response = getContents($authUrl, $headers, $opts); if ($response === false || empty($response)) { returnServerError('Could not get authentication token from RTL+'); } $data = json_decode($response, true); if (!$data || !isset($data['access_token'])) { returnServerError('Invalid authentication response: ' . substr($response, 0, 200)); } $this->authToken = $data['access_token']; return $this->authToken; } private function makeGraphQLRequest($operation, $hash, $variables) { $token = $this->getAuthToken(); $url = 'https://cdn.gateway.now-plus-prod.aws-cbc.cloud/graphql?' . 'operationName=' . urlencode($operation) . '&variables=' . urlencode(json_encode($variables)) . '&extensions=' . urlencode(json_encode([ 'persistedQuery' => [ 'version' => 1, 'sha256Hash' => $hash ] ])); $headers = array( 'Authorization: Bearer ' . $token, 'rtlplus-client-id: rci:rtlplus:web', 'rtlplus-client-version: 2025.11.6.1', 'Accept: application/json' ); $response = getContents($url, $headers); if ($response === false || empty($response)) { returnServerError('GraphQL request failed: ' . $operation); } $data = json_decode($response, true); if (!$data) { returnServerError('Invalid JSON response from RTL+ API: ' . substr($response, 0, 200)); } if (isset($data['errors'])) { $errorMsg = isset($data['errors'][0]['message']) ? $data['errors'][0]['message'] : 'Unknown GraphQL error'; returnServerError('RTL+ API error: ' . $errorMsg); } return $data['data'] ?? null; } public function collectData() { $seriesId = $this->getInput('series_id'); if (empty($seriesId)) { return; } // Hole Format-Daten (Serie mit Staffeln) $formatData = $this->makeGraphQLRequest( 'Format', 'd112638c0184ab5698af7b69532dfe2f12973f7af9cb137b9f70278130b1eafa', ['id' => 'rrn:watch:videohub:format:' . $seriesId] ); if (!$formatData || !isset($formatData['format'])) { returnServerError('Could not load series data'); } $format = $formatData['format']; $seriesName = $format['title'] ?? 'Unknown Series'; $seasons = $format['seasons'] ?? []; if (empty($seasons)) { returnServerError('No seasons found for this series'); } // Setze Feed-Namen $this->feedName = $seriesName . ' | RTL+'; // Sammle alle Episoden von allen Staffeln $allEpisodes = []; foreach ($seasons as $season) { $seasonId = $season['id'] ?? null; if (!$seasonId) { continue; } // Hole Episoden dieser Staffel (mit limit 50) $seasonData = $this->makeGraphQLRequest( 'SeasonWithFormatAndEpisodes', 'cc0fbbe17143f549a35efa6f8665ceb9b1cfae44b590f0b2381a9a304304c584', [ 'seasonId' => $seasonId, 'offset' => 0, 'limit' => 50 ] ); if (!$seasonData || !isset($seasonData['season']['episodes'])) { continue; } $episodes = $seasonData['season']['episodes']; $seasonNumber = $seasonData['season']['ordinal'] ?? null; foreach ($episodes as $episode) { $episode['_seasonNumber'] = $seasonNumber; $allEpisodes[] = $episode; } } if (empty($allEpisodes)) { returnServerError('No episodes found'); } // Sortiere Episoden nach Broadcast-Datum (neueste zuerst), oder nach Episodennummer usort($allEpisodes, function ($a, $b) { $dateA = isset($a['recentBroadcastDate']) ? strtotime($a['recentBroadcastDate']) : 0; $dateB = isset($b['recentBroadcastDate']) ? strtotime($b['recentBroadcastDate']) : 0; // Wenn beide Daten vorhanden sind, sortiere nach Datum if ($dateA > 0 && $dateB > 0) { return $dateB - $dateA; } // Sonst nach Staffel und Episode sortieren $seasonA = $a['_seasonNumber'] ?? 0; $seasonB = $b['_seasonNumber'] ?? 0; $epA = $a['number'] ?? 0; $epB = $b['number'] ?? 0; if ($seasonA != $seasonB) { return $seasonB - $seasonA; } return $epB - $epA; }); // Erstelle RSS Items foreach ($allEpisodes as $episode) { $item = []; // Titel $episodeTitle = $episode['title'] ?? ''; $episodeNumber = $episode['number'] ?? null; $seasonNumber = $episode['_seasonNumber'] ?? null; // Formatiere Titel mit S00E00 wenn möglich if (is_numeric($seasonNumber) && is_numeric($episodeNumber)) { $seasonNum = str_pad($seasonNumber, 2, '0', STR_PAD_LEFT); $epNum = str_pad($episodeNumber, 2, '0', STR_PAD_LEFT); $item['title'] = sprintf('%s S%sE%s - %s', $seriesName, $seasonNum, $epNum, $episodeTitle); } else { $item['title'] = sprintf('%s - %s', $seriesName, $episodeTitle); if ($seasonNumber) { $item['title'] = sprintf('%s [Staffel %s] - %s', $seriesName, $seasonNumber, $episodeTitle); } } // Beschreibung $description = $episode['descriptionV2'] ?? ''; $item['content'] = '
' . htmlspecialchars($description) . '
'; // Thumbnail hinzufügen $imageUrl = null; if (isset($episode['watchImages']['default']['absoluteUri'])) { $imageUrl = $episode['watchImages']['default']['absoluteUri']; } elseif (isset($episode['watchImages']['portrait']['absoluteUri'])) { $imageUrl = $episode['watchImages']['portrait']['absoluteUri']; } if ($imageUrl) { $item['content'] .= '