Neu: RTL+ Bridge
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/.claude
|
||||
@@ -109,6 +109,15 @@ Diese Sammlung enthält verschiedene Bridge-Implementierungen für RSS-Bridge, u
|
||||
- **Title Only Mode**: Behält nur Titel und Link (entfernt alles andere)
|
||||
- **Item Limit**: Maximale Anzahl der zurückgegebenen Items (Standard: 20)
|
||||
|
||||
### RTL+ Bridge (Von Akamaru)
|
||||
- **Beschreibung**: RSS-Feed für Serien von RTL+. Ruft automatisch alle Staffeln und Episoden einer Serie ab. Nutzt die offizielle RTL+ GraphQL API mit OAuth2-Authentifizierung (anonym).
|
||||
- **Parameter**:
|
||||
- **Serien-ID**: Die numerische ID der Serie (z.B. 948092 für "Die Nibelungen" aus der URL /video-tv/serien/die-nibelungen-kampf-der-koenigreiche-948092)
|
||||
- **Hinweise**:
|
||||
- Feed-Items enthalten Episodenbeschreibung, Thumbnail und direkten Link zur Episode
|
||||
- Titel im Format "Serienname S01E01 - Episodentitel"
|
||||
- Keine Datumsinformationen verfügbar (API liefert keine Veröffentlichungsdaten), aber jede Episode hat eine eindeutige ID
|
||||
|
||||
### Snowbreak News Bridge (Von Akamaru)
|
||||
- **Beschreibung**: Zeigt die neuesten Nachrichten von Snowbreak: Containment Zone
|
||||
|
||||
|
||||
264
RTLPlusBridge.php
Normal file
264
RTLPlusBridge.php
Normal file
@@ -0,0 +1,264 @@
|
||||
<?php
|
||||
|
||||
class RTLPlusBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'RTL+ Serien RSS';
|
||||
const URI = 'https://plus.rtl.de/';
|
||||
const DESCRIPTION = 'RSS-Feed für Serien von RTL+';
|
||||
const CACHE_TIMEOUT = 21600; // 6h
|
||||
const MAINTAINER = 'Akamaru';
|
||||
const ICON = 'https://plus.rtl.de/favicon.ico';
|
||||
const PARAMETERS = [
|
||||
[
|
||||
'series_id' => [
|
||||
'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'] = '<p>' . htmlspecialchars($description) . '</p>';
|
||||
|
||||
// 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'] .= '<p><img src="' . htmlspecialchars($imageUrl) . '" alt="' . htmlspecialchars($episodeTitle) . '"></p>';
|
||||
$item['enclosures'] = [$imageUrl];
|
||||
}
|
||||
|
||||
// URL konstruieren (zur Episode-Seite)
|
||||
if (isset($episode['urlData']['watchPath'])) {
|
||||
$item['uri'] = 'https://plus.rtl.de' . $episode['urlData']['watchPath'];
|
||||
} else {
|
||||
$item['uri'] = 'https://plus.rtl.de/video-tv/serien/s-' . $seriesId;
|
||||
}
|
||||
|
||||
// Datum
|
||||
if (isset($episode['recentBroadcastDate']) && !empty($episode['recentBroadcastDate'])) {
|
||||
$item['timestamp'] = strtotime($episode['recentBroadcastDate']);
|
||||
}
|
||||
|
||||
// UID
|
||||
$item['uid'] = $episode['id'] ?? uniqid();
|
||||
|
||||
// Autor
|
||||
$item['author'] = 'RTL+';
|
||||
|
||||
// Kategorien (Staffel)
|
||||
if ($seasonNumber) {
|
||||
$item['categories'] = ['Staffel ' . $seasonNumber];
|
||||
}
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
if (!is_null($this->feedName)) {
|
||||
return $this->feedName;
|
||||
}
|
||||
return parent::getName();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user