1
0

Neu: RTL+ Bridge

This commit is contained in:
Akamaru
2025-11-09 20:31:08 +01:00
parent 4ab0c2be22
commit d8751fb514
3 changed files with 274 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/.claude

View File

@@ -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
View 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();
}
}