diff --git a/JoynBridge.php b/JoynBridge.php index 0c7d22e..720fdaf 100644 --- a/JoynBridge.php +++ b/JoynBridge.php @@ -4,115 +4,203 @@ class JoynBridge extends BridgeAbstract { const URI = 'https://www.joyn.de/'; const DESCRIPTION = 'RSS-Feed für Serien von Joyn.de'; const CACHE_TIMEOUT = 21600; // 6h - const MAINTAINER = 'Akamaru'; + const MAINTAINER = 'Akamaru, Claude'; const ICON = 'https://www.joyn.de/favicon.ico'; const PARAMETERS = [ [ 'series_id' => [ 'name' => 'Serien-ID', 'type' => 'text', - 'title' => 'ID der Serie (z.B. match-in-paradise)', + 'title' => 'ID der Serie (z.B. auf-streife)', '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() { $series_id = $this->getInput('series_id'); if (empty($series_id)) { return; } - $url = 'https://www.joyn.de/serien/' . $series_id; - $html = getSimpleHTMLDOM($url); + $path = '/serien/' . $series_id; - if (!$html) { - returnServerError('Could not load page'); - } - - $scriptTag = $html->find('script[id="__NEXT_DATA__"]', 0); - if (!$scriptTag) { - returnServerError('Could not find NEXT_DATA script tag'); - } - - $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; + // First GraphQL request: Get number of seasons and episodes + $seasonQuery = ' + query ($path: String!) { + page(path: $path) { + ... on SeriesPage { + series { + numberOfSeasons + seasons { + numberOfEpisodes + } } } } } + '; + + $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) - usort($allEpisodes, function($a, $b) { - $seasonCompare = ($b['seasonNumber'] ?? 0) <=> ($a['seasonNumber'] ?? 0); - if ($seasonCompare !== 0) { - return $seasonCompare; + + $series = $seasonData['page']['series']; + $numberOfSeasons = $series['numberOfSeasons'] ?? 0; + $seasons = $series['seasons'] ?? []; + + 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'] ?? ''; - foreach ($allEpisodes as $episode) { - $seasonNumber = $episode['seasonNumber'] ?? 1; + '; + + $episodeData = $this->makeGraphQLRequest($episodeQuery, ['path' => $path]); + if (!$episodeData || !isset($episodeData['page']['series'])) { + 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; $title = $episode['title'] ?? ''; + $description = $episode['description'] ?? ''; $airdate = $episode['airdate'] ?? null; - $path = $episode['path'] ?? ''; - $primaryImage = $episode['primaryImage']['url'] ?? ''; - $isPlus = !empty($episode['__plus__']); + $episodePath = $episode['path'] ?? ''; + $item = []; + $seasonNum = str_pad($seasonNumber, 2, '0', STR_PAD_LEFT); $epNum = str_pad($episodeNumber, 2, '0', STR_PAD_LEFT); $epCode = 'S' . $seasonNum . 'E' . $epNum; - $item['title'] = sprintf('%s%s %s "%s"', $isPlus ? '[Joyn PLUS+] ' : '', $seriesTitle, $epCode, $title); - $item['content'] = '

Neue Folge verfügbar'; - if ($isPlus) { - $item['content'] .= ' Joyn PLUS+ (Abo benötigt)'; - } - $item['content'] .= '

'; - if ($primaryImage) { - $item['content'] .= '

' . htmlspecialchars($title) . '

'; - $item['enclosures'] = [$primaryImage]; - } + + $item['title'] = sprintf('%s %s "%s"', $seriesTitle, $epCode, $title); + $item['content'] = '

' . htmlspecialchars($description) . '

'; + $item['uid'] = $episodeId; + $item['author'] = 'Joyn'; + if ($airdate) { $item['timestamp'] = $airdate; } - if ($path) { - $item['uri'] = 'https://www.joyn.de' . $path; + + if ($episodePath) { + $item['uri'] = 'https://www.joyn.de' . $episodePath; } else { - $item['uri'] = $url; + $item['uri'] = 'https://www.joyn.de' . $path; } + $this->items[] = $item; } } + + public function getName() { + if (!is_null($this->feedName)) { + return $this->feedName; + } + return parent::getName(); + } } \ No newline at end of file