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)
|
- **Title Only Mode**: Behält nur Titel und Link (entfernt alles andere)
|
||||||
- **Item Limit**: Maximale Anzahl der zurückgegebenen Items (Standard: 20)
|
- **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)
|
### Snowbreak News Bridge (Von Akamaru)
|
||||||
- **Beschreibung**: Zeigt die neuesten Nachrichten von Snowbreak: Containment Zone
|
- **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