[ 'name' => 'Serien-ID', 'type' => 'text', '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; } $path = '/serien/' . $series_id; // First GraphQL request: Get all seasons to find highest available number $checkQuery = ' query ($path: String!) { page(path: $path) { ... on SeriesPage { series { numberOfSeasons seasons { number numberOfEpisodes episodes(first: 1) { number } } } } } } '; $checkData = $this->makeGraphQLRequest($checkQuery, ['path' => $path]); if (!$checkData || !isset($checkData['page']['series'])) { returnServerError('Could not fetch series information'); } $series = $checkData['page']['series']; $numberOfSeasons = $series['numberOfSeasons'] ?? 0; $seasons = $series['seasons'] ?? []; if ($numberOfSeasons === 0 || empty($seasons)) { returnServerError('No seasons found for this series'); } // Find the highest available season number (delisted seasons create gaps) $seasonNumbers = array_map(function($season) { return $season['number'] ?? 0; }, $seasons); $highestSeasonNumber = max($seasonNumbers); // Find first and last seasons by their actual position in the array $firstSeason = $seasons[0] ?? null; $lastSeason = $seasons[count($seasons) - 1] ?? null; if (!$firstSeason || !$lastSeason) { returnServerError('Could not find seasons'); } $firstSeasonNumber = $firstSeason['number'] ?? 1; $lastSeasonNumber = $lastSeason['number'] ?? 1; $firstEpisodes = $firstSeason['episodes'] ?? []; $firstEpisodeNumber = !empty($firstEpisodes) ? ($firstEpisodes[0]['number'] ?? 1) : 1; // Check if series is chronologically sorted (newest first) // If first season has higher number than last season, it's sorted newest first $isChronological = $firstSeasonNumber > $lastSeasonNumber; $seasonOffset = 0; $episodeOffset = 0; $seasonLimit = 1; $episodeLimit = 10; if (!$isChronological) { // Series is sorted oldest first, use offset to get latest (last season in array) $seasonOffset = count($seasons) - 1; $numberOfEpisodesInLastSeason = $lastSeason['numberOfEpisodes'] ?? 0; $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(' . ($seasonOffset > 0 ? 'offset: ' . $seasonOffset : 'first: ' . $seasonLimit) . ') { id title number numberOfEpisodes episodes(' . ($episodeOffset > 0 ? 'offset: ' . $episodeOffset : 'first: ' . $episodeLimit) . ') { id number airdate title description path } } } } } } '; $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 only if series is sorted oldest first if (!$isChronological) { $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; $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"', $seriesTitle, $epCode, $title); $item['content'] = '
' . htmlspecialchars($description) . '
'; $item['uid'] = $episodeId; $item['author'] = 'Joyn'; if ($airdate) { $item['timestamp'] = $airdate; } if ($episodePath) { $item['uri'] = 'https://www.joyn.de' . $episodePath; } else { $item['uri'] = 'https://www.joyn.de' . $path; } $this->items[] = $item; } } public function getName() { if (!is_null($this->feedName)) { return $this->feedName; } return parent::getName(); } }