Joyn: Nutze GraphQL-API
This commit is contained in:
232
JoynBridge.php
232
JoynBridge.php
@@ -4,115 +4,203 @@ class JoynBridge extends BridgeAbstract {
|
|||||||
const URI = 'https://www.joyn.de/';
|
const URI = 'https://www.joyn.de/';
|
||||||
const DESCRIPTION = 'RSS-Feed für Serien von Joyn.de';
|
const DESCRIPTION = 'RSS-Feed für Serien von Joyn.de';
|
||||||
const CACHE_TIMEOUT = 21600; // 6h
|
const CACHE_TIMEOUT = 21600; // 6h
|
||||||
const MAINTAINER = 'Akamaru';
|
const MAINTAINER = 'Akamaru, Claude';
|
||||||
const ICON = 'https://www.joyn.de/favicon.ico';
|
const ICON = 'https://www.joyn.de/favicon.ico';
|
||||||
const PARAMETERS = [
|
const PARAMETERS = [
|
||||||
[
|
[
|
||||||
'series_id' => [
|
'series_id' => [
|
||||||
'name' => 'Serien-ID',
|
'name' => 'Serien-ID',
|
||||||
'type' => 'text',
|
'type' => 'text',
|
||||||
'title' => 'ID der Serie (z.B. match-in-paradise)',
|
'title' => 'ID der Serie (z.B. auf-streife)',
|
||||||
'required' => true
|
'required' => true
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private $feedName = null;
|
||||||
|
|
||||||
|
private function makeGraphQLRequest($query, $variables = []) {
|
||||||
|
$url = 'https://api.joyn.de/graphql';
|
||||||
|
$headers = [
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'x-api-key: 4f0fd9f18abbe3cf0e87fdb556bc39c8',
|
||||||
|
'Joyn-Platform: web'
|
||||||
|
];
|
||||||
|
|
||||||
|
$payload = json_encode([
|
||||||
|
'query' => $query,
|
||||||
|
'variables' => $variables
|
||||||
|
]);
|
||||||
|
|
||||||
|
$context = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'method' => 'POST',
|
||||||
|
'header' => implode("\r\n", $headers) . "\r\n",
|
||||||
|
'content' => $payload
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = file_get_contents($url, false, $context);
|
||||||
|
if ($response === false) {
|
||||||
|
returnServerError('GraphQL request failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($response, true);
|
||||||
|
if (!$data) {
|
||||||
|
returnServerError('Could not parse GraphQL response');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($data['errors'])) {
|
||||||
|
returnServerError('GraphQL errors: ' . json_encode($data['errors']));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data['data'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
public function collectData() {
|
public function collectData() {
|
||||||
$series_id = $this->getInput('series_id');
|
$series_id = $this->getInput('series_id');
|
||||||
if (empty($series_id)) {
|
if (empty($series_id)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$url = 'https://www.joyn.de/serien/' . $series_id;
|
$path = '/serien/' . $series_id;
|
||||||
$html = getSimpleHTMLDOM($url);
|
|
||||||
|
|
||||||
if (!$html) {
|
// First GraphQL request: Get number of seasons and episodes
|
||||||
returnServerError('Could not load page');
|
$seasonQuery = '
|
||||||
}
|
query ($path: String!) {
|
||||||
|
page(path: $path) {
|
||||||
$scriptTag = $html->find('script[id="__NEXT_DATA__"]', 0);
|
... on SeriesPage {
|
||||||
if (!$scriptTag) {
|
series {
|
||||||
returnServerError('Could not find NEXT_DATA script tag');
|
numberOfSeasons
|
||||||
}
|
seasons {
|
||||||
|
numberOfEpisodes
|
||||||
$jsonData = json_decode($scriptTag->innertext, true);
|
}
|
||||||
if (!$jsonData) {
|
|
||||||
returnServerError('Could not parse JSON data');
|
|
||||||
}
|
|
||||||
|
|
||||||
$series = $jsonData['props']['pageProps']['initialData']['page']['series'] ?? null;
|
|
||||||
if (!$series) {
|
|
||||||
returnServerError('Could not find series data');
|
|
||||||
}
|
|
||||||
|
|
||||||
$episodeMap = [];
|
|
||||||
// Zuerst freeSeasons (werden bevorzugt)
|
|
||||||
if (isset($series['freeSeasons'])) {
|
|
||||||
foreach ($series['freeSeasons'] as $season) {
|
|
||||||
if (isset($season['episodes'])) {
|
|
||||||
foreach ($season['episodes'] as $episode) {
|
|
||||||
$episode['seasonNumber'] = $season['number'];
|
|
||||||
$episode['__plus__'] = false;
|
|
||||||
$episodeMap[$episode['id']] = $episode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Dann svodSeasons, aber nur wenn die Episode noch nicht existiert
|
|
||||||
if (isset($series['svodSeasons'])) {
|
|
||||||
foreach ($series['svodSeasons'] as $season) {
|
|
||||||
if (isset($season['episodes'])) {
|
|
||||||
foreach ($season['episodes'] as $episode) {
|
|
||||||
if (!isset($episodeMap[$episode['id']])) {
|
|
||||||
$episode['seasonNumber'] = $season['number'];
|
|
||||||
$episode['__plus__'] = true;
|
|
||||||
$episodeMap[$episode['id']] = $episode;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
';
|
||||||
|
|
||||||
|
$seasonData = $this->makeGraphQLRequest($seasonQuery, ['path' => $path]);
|
||||||
|
if (!$seasonData || !isset($seasonData['page']['series'])) {
|
||||||
|
returnServerError('Could not fetch series information');
|
||||||
}
|
}
|
||||||
$allEpisodes = array_values($episodeMap);
|
|
||||||
// Sortiere nach Episodennummer absteigend (neueste zuerst)
|
$series = $seasonData['page']['series'];
|
||||||
usort($allEpisodes, function($a, $b) {
|
$numberOfSeasons = $series['numberOfSeasons'] ?? 0;
|
||||||
$seasonCompare = ($b['seasonNumber'] ?? 0) <=> ($a['seasonNumber'] ?? 0);
|
$seasons = $series['seasons'] ?? [];
|
||||||
if ($seasonCompare !== 0) {
|
|
||||||
return $seasonCompare;
|
if ($numberOfSeasons === 0 || empty($seasons)) {
|
||||||
|
returnServerError('No seasons found for this series');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the last season (index: numberOfSeasons - 1)
|
||||||
|
$lastSeasonIndex = $numberOfSeasons - 1;
|
||||||
|
$lastSeason = $seasons[$lastSeasonIndex] ?? null;
|
||||||
|
|
||||||
|
if (!$lastSeason) {
|
||||||
|
returnServerError('Could not find last season');
|
||||||
|
}
|
||||||
|
|
||||||
|
$numberOfEpisodesInLastSeason = $lastSeason['numberOfEpisodes'] ?? 0;
|
||||||
|
|
||||||
|
if ($numberOfEpisodesInLastSeason === 0) {
|
||||||
|
returnServerError('No episodes found in last season');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate offset to get last 10 episodes (or all if less than 10)
|
||||||
|
$episodeOffset = max(0, $numberOfEpisodesInLastSeason - 10);
|
||||||
|
|
||||||
|
// Second GraphQL request: Get the latest episodes
|
||||||
|
$episodeQuery = '
|
||||||
|
query ($path: String!) {
|
||||||
|
page(path: $path) {
|
||||||
|
... on SeriesPage {
|
||||||
|
series {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
description
|
||||||
|
numberOfSeasons
|
||||||
|
seasons(offset: ' . $lastSeasonIndex . ') {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
number
|
||||||
|
numberOfEpisodes
|
||||||
|
episodes(offset: ' . $episodeOffset . ') {
|
||||||
|
id
|
||||||
|
number
|
||||||
|
airdate
|
||||||
|
title
|
||||||
|
description
|
||||||
|
path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return ($b['number'] ?? 0) <=> ($a['number'] ?? 0);
|
';
|
||||||
});
|
|
||||||
$seriesTitle = $series['title'] ?? '';
|
$episodeData = $this->makeGraphQLRequest($episodeQuery, ['path' => $path]);
|
||||||
foreach ($allEpisodes as $episode) {
|
if (!$episodeData || !isset($episodeData['page']['series'])) {
|
||||||
$seasonNumber = $episode['seasonNumber'] ?? 1;
|
returnServerError('Could not fetch episode information');
|
||||||
|
}
|
||||||
|
|
||||||
|
$seriesData = $episodeData['page']['series'];
|
||||||
|
$seriesTitle = $seriesData['title'] ?? '';
|
||||||
|
|
||||||
|
// Set feed name
|
||||||
|
$this->feedName = $seriesTitle . ' | Joyn';
|
||||||
|
$seasons = $seriesData['seasons'] ?? [];
|
||||||
|
|
||||||
|
if (empty($seasons)) {
|
||||||
|
returnServerError('No season data found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$season = $seasons[0];
|
||||||
|
$seasonNumber = $season['number'] ?? 1;
|
||||||
|
$episodes = $season['episodes'] ?? [];
|
||||||
|
|
||||||
|
// Reverse episodes array to show newest first
|
||||||
|
$episodes = array_reverse($episodes);
|
||||||
|
|
||||||
|
foreach ($episodes as $episode) {
|
||||||
|
$episodeId = $episode['id'] ?? '';
|
||||||
$episodeNumber = $episode['number'] ?? 1;
|
$episodeNumber = $episode['number'] ?? 1;
|
||||||
$title = $episode['title'] ?? '';
|
$title = $episode['title'] ?? '';
|
||||||
|
$description = $episode['description'] ?? '';
|
||||||
$airdate = $episode['airdate'] ?? null;
|
$airdate = $episode['airdate'] ?? null;
|
||||||
$path = $episode['path'] ?? '';
|
$episodePath = $episode['path'] ?? '';
|
||||||
$primaryImage = $episode['primaryImage']['url'] ?? '';
|
|
||||||
$isPlus = !empty($episode['__plus__']);
|
|
||||||
$item = [];
|
$item = [];
|
||||||
|
|
||||||
$seasonNum = str_pad($seasonNumber, 2, '0', STR_PAD_LEFT);
|
$seasonNum = str_pad($seasonNumber, 2, '0', STR_PAD_LEFT);
|
||||||
$epNum = str_pad($episodeNumber, 2, '0', STR_PAD_LEFT);
|
$epNum = str_pad($episodeNumber, 2, '0', STR_PAD_LEFT);
|
||||||
$epCode = 'S' . $seasonNum . 'E' . $epNum;
|
$epCode = 'S' . $seasonNum . 'E' . $epNum;
|
||||||
$item['title'] = sprintf('%s%s %s "%s"', $isPlus ? '[Joyn PLUS+] ' : '', $seriesTitle, $epCode, $title);
|
|
||||||
$item['content'] = '<p>Neue Folge verfügbar';
|
$item['title'] = sprintf('%s %s "%s"', $seriesTitle, $epCode, $title);
|
||||||
if ($isPlus) {
|
$item['content'] = '<p>' . htmlspecialchars($description) . '</p>';
|
||||||
$item['content'] .= ' <span style="color:red;font-weight:bold">Joyn PLUS+ (Abo benötigt)</span>';
|
$item['uid'] = $episodeId;
|
||||||
}
|
$item['author'] = 'Joyn';
|
||||||
$item['content'] .= '</p>';
|
|
||||||
if ($primaryImage) {
|
|
||||||
$item['content'] .= '<p><img src="' . htmlspecialchars($primaryImage) . '" alt="' . htmlspecialchars($title) . '"></p>';
|
|
||||||
$item['enclosures'] = [$primaryImage];
|
|
||||||
}
|
|
||||||
if ($airdate) {
|
if ($airdate) {
|
||||||
$item['timestamp'] = $airdate;
|
$item['timestamp'] = $airdate;
|
||||||
}
|
}
|
||||||
if ($path) {
|
|
||||||
$item['uri'] = 'https://www.joyn.de' . $path;
|
if ($episodePath) {
|
||||||
|
$item['uri'] = 'https://www.joyn.de' . $episodePath;
|
||||||
} else {
|
} else {
|
||||||
$item['uri'] = $url;
|
$item['uri'] = 'https://www.joyn.de' . $path;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->items[] = $item;
|
$this->items[] = $item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getName() {
|
||||||
|
if (!is_null($this->feedName)) {
|
||||||
|
return $this->feedName;
|
||||||
|
}
|
||||||
|
return parent::getName();
|
||||||
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user