$podcastSlug) { try { // Construire l'URL du feed RSS pour ce podcast $feedUrl = rtrim($castopodUrl, '/') . '/@' . $podcastSlug . '/feed'; // Récupérer le feed RSS avec backoff uniquement sur rate limit (HTTP 429) $maxAttempts = 3; $attempt = 0; $xmlContent = false; $httpCode = 0; while ($attempt < $maxAttempts) { $response = httpGet($feedUrl, ['timeout' => 10]); $xmlContent = $response['body']; $httpCode = $response['code']; // Succès ou erreur définitive : on sort de la boucle if ($httpCode !== 429) { break; } // Rate limit : attendre avant de réessayer (backoff exponentiel simple) $attempt++; if ($attempt < $maxAttempts) { sleep(5 * $attempt); } } if ($httpCode !== 200 || !$xmlContent) { continue; // Passer au podcast suivant en cas d'erreur } $episodes = []; // Vérifier si SimpleXML est disponible if (function_exists('simplexml_load_string')) { // Parser avec SimpleXML (méthode recommandée) $xml = simplexml_load_string($xmlContent); if (!$xml) { continue; // Passer au podcast suivant } // Enregistrer les namespaces $namespaces = $xml->getNamespaces(true); // Extraire les informations du podcast $podcastTitle = (string) $xml->channel->title; $podcastImage = (string) $xml->channel->image->url; $podcastLink = (string) $xml->channel->link; // Parcourir les items (épisodes) - on récupère tous pour trier après foreach ($xml->channel->item as $item) { // Extraire les infos de l'épisode $itunesNs = $item->children($namespaces['itunes'] ?? 'http://www.itunes.com/dtds/podcast-1.0.dtd'); // Récupérer l'image de l'épisode $episodeImage = $podcastImage; if (isset($itunesNs->image)) { $imageAttrs = $itunesNs->image->attributes(); if (isset($imageAttrs['href'])) { $episodeImage = (string) $imageAttrs['href']; } } $episode = [ 'title' => (string) $item->title, 'link' => (string) $item->link, 'pubDate' => (string) $item->pubDate, 'description' => strip_tags((string) $item->description), 'duration' => (string) $itunesNs->duration ?? '', 'image' => $episodeImage, 'audioUrl' => (string) $item->enclosure['url'] ?? '', 'podcastTitle' => $podcastTitle, 'podcastLink' => $podcastLink, 'podcastSlug' => $podcastSlug, ]; // Formater la date if ($episode['pubDate']) { try { $date = new DateTime($episode['pubDate']); $episode['formattedDate'] = $date->format('d/m/Y'); $episode['timestamp'] = $date->getTimestamp(); } catch (Exception $e) { $episode['formattedDate'] = ''; $episode['timestamp'] = 0; } } // Formater la durée if ($episode['duration']) { $episode['formattedDuration'] = formatDuration($episode['duration']); } else { $episode['formattedDuration'] = ''; } // Limiter la description if (strlen($episode['description']) > 150) { $episode['description'] = substr($episode['description'], 0, 150) . '...'; } $episodes[] = $episode; } } else { // Fallback: Parser avec regex (moins fiable mais fonctionne sans SimpleXML) // Extraire les infos du podcast preg_match('/.*?(.*?)<\/title>/s', $xmlContent, $podcastTitleMatch); preg_match('/<channel>.*?<link>(.*?)<\/link>/s', $xmlContent, $podcastLinkMatch); preg_match('/<image>.*?<url>(.*?)<\/url>/s', $xmlContent, $podcastImageMatch); $podcastTitle = $podcastTitleMatch[1] ?? ''; $podcastLink = $podcastLinkMatch[1] ?? ''; $podcastImage = $podcastImageMatch[1] ?? ''; // Extraire tous les items preg_match_all('/<item>(.*?)<\/item>/s', $xmlContent, $items); foreach ($items[1] as $itemContent) { // Extraire les données de chaque item preg_match('/<title>(.*?)<\/title>/', $itemContent, $titleMatch); preg_match('/<link>(.*?)<\/link>/', $itemContent, $linkMatch); preg_match('/<pubDate>(.*?)<\/pubDate>/', $itemContent, $dateMatch); preg_match('/<description>(.*?)<\/description>/s', $itemContent, $descMatch); preg_match('/<itunes:duration>(.*?)<\/itunes:duration>/', $itemContent, $durationMatch); preg_match('/<itunes:image[^>]*href=["\']([^"\']*)["\']/', $itemContent, $imageMatch); preg_match('/<enclosure[^>]*url=["\']([^"\']*)["\']/', $itemContent, $audioMatch); $episode = [ 'title' => $titleMatch[1] ?? '', 'link' => $linkMatch[1] ?? '', 'pubDate' => $dateMatch[1] ?? '', 'description' => strip_tags($descMatch[1] ?? ''), 'duration' => $durationMatch[1] ?? '', 'image' => $imageMatch[1] ?? $podcastImage, 'audioUrl' => $audioMatch[1] ?? '', 'podcastTitle' => $podcastTitle, 'podcastLink' => $podcastLink, 'podcastSlug' => $podcastSlug, ]; // Formater la date if ($episode['pubDate']) { try { $date = new DateTime($episode['pubDate']); $episode['formattedDate'] = $date->format('d/m/Y'); $episode['timestamp'] = $date->getTimestamp(); } catch (Exception $e) { $episode['formattedDate'] = ''; $episode['timestamp'] = 0; } } // Formater la durée if ($episode['duration']) { $episode['formattedDuration'] = formatDuration($episode['duration']); } else { $episode['formattedDuration'] = ''; } // Limiter la description if (strlen($episode['description']) > 150) { $episode['description'] = substr($episode['description'], 0, 150) . '...'; } $episodes[] = $episode; } } // Ajouter les épisodes de ce podcast à la liste globale $allEpisodes = array_merge($allEpisodes, $episodes); } catch (Exception $e) { error_log('Erreur lors de la récupération des épisodes Castopod pour ' . $podcastSlug . ': ' . $e->getMessage()); // Continuer avec le podcast suivant } } // Trier tous les épisodes par date (du plus récent au plus ancien) usort($allEpisodes, function($a, $b) { return ($b['timestamp'] ?? 0) - ($a['timestamp'] ?? 0); }); // Limiter au nombre demandé $allEpisodes = array_slice($allEpisodes, 0, $count); // Mettre en cache (uniquement si non vide : une erreur temporaire // — rate limit HTTP 429, timeout… — ne doit pas être figée en cache) if (!empty($allEpisodes) && defined('CACHE_ENABLED') && CACHE_ENABLED) { saveToCache($cacheKey, $allEpisodes); } return $allEpisodes; }