'Musique', 'Films' => 'Films', 'Vehicles' => 'Véhicules', 'Art' => 'Art', 'Sports' => 'Sports', 'Travels' => 'Voyages', 'Gaming' => 'Jeux vidéo', 'People' => 'Personnes', 'Comedy' => 'Humour', 'Entertainment' => 'Divertissement', 'News & Politics' => 'Actualités & Politique', 'How To' => 'Tutoriels', 'Education' => 'Éducation', 'Activism' => 'Activisme', 'Science & Technology' => 'Science & Technologie', 'Animals' => 'Animaux', 'Kids' => 'Enfants', 'Food' => 'Cuisine', ]; // Si une constante PRIORITY_CATEGORIES est définie, utiliser ces traductions if (defined('PRIORITY_CATEGORIES')) { $priorityCategories = PRIORITY_CATEGORIES; foreach ($priorityCategories as $id => $name) { // Trouver la clé anglaise correspondant à l'ID $englishName = array_search($id, array_keys($categories)); if ($englishName !== false) { $translations[$englishName] = $name; } } } $result = []; foreach ($categories as $key => $name) { // Utiliser la traduction si disponible, sinon garder le nom original $translatedName = isset($translations[$name]) ? $translations[$name] : $name; $result[$key] = $translatedName; } return $result; } /** * Version originale pour appeler l'API PeerTube (sans cache) * Cette fonction est maintenant utilisée en interne par callPeerTubeApi * * @param string $endpoint Point de terminaison de l'API * @param array $params Paramètres optionnels pour la requête * @return array Données retournées par l'API */ function callPeerTubeApiOriginal($endpoint, $params = []) { // Validation de l'URL de base PeerTube pour prévenir SSRF if (!isValidPeerTubeUrl(PEERTUBE_URL)) { error_log('SECURITY: Invalid PeerTube URL detected: ' . PEERTUBE_URL); return []; } // Nettoyer et valider l'endpoint $endpoint = ltrim($endpoint, '/'); if (!isValidApiEndpoint($endpoint)) { error_log('SECURITY: Invalid API endpoint detected: ' . $endpoint); return []; } $url = PEERTUBE_URL . '/api/v1/' . $endpoint; // Ajouter les paramètres à l'URL if (!empty($params)) { $url .= '?' . http_build_query($params); } // Initialiser cURL avec options de sécurité $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 30); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); curl_setopt($ch, CURLOPT_MAXREDIRS, 0); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); curl_setopt($ch, CURLOPT_USERAGENT, 'fediverse-oki/1.0'); // Ajouter la clé API si définie if (defined('API_KEY') && !empty(API_KEY)) { curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: ApiKey ' . API_KEY ]); } // Exécuter la requête $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); // Traiter la réponse if ($response === false || !empty($error)) { error_log('PeerTube API error: ' . $error); return []; } if ($httpCode < 200 || $httpCode >= 300) { error_log('PeerTube API HTTP error: ' . $httpCode); return []; } // Décoder la réponse JSON $data = json_decode($response, true); return $data ?: []; } /** * Fonction utilitaire pour appeler l'API PeerTube avec cache * * @param string $endpoint Point de terminaison de l'API * @param array $params Paramètres optionnels pour la requête * @return array Données retournées par l'API */ function callPeerTubeApi($endpoint, $params = []) { // Utiliser la fonction cachée si disponible if (function_exists('callPeerTubeApiCached')) { return callPeerTubeApiCached($endpoint, $params); } // Fallback vers la version originale return callPeerTubeApiOriginal($endpoint, $params); } /** * Valide l'URL PeerTube pour prévenir les attaques SSRF * * @param string $url URL à valider * @return bool True si l'URL est valide et sûre */ function isValidPeerTubeUrl($url) { // Vérifier que l'URL est bien formée $parsed = parse_url($url); if (!$parsed || !isset($parsed['scheme']) || !isset($parsed['host'])) { return false; } // Autoriser uniquement HTTPS (ou HTTP en développement) if (!in_array($parsed['scheme'], ['https', 'http'])) { return false; } // Bloquer les adresses IP privées et locales $host = $parsed['host']; if (filter_var($host, FILTER_VALIDATE_IP)) { if (!filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { return false; } } // Bloquer localhost et autres domaines dangereux $blockedHosts = ['localhost', '127.0.0.1', '::1', '0.0.0.0', 'metadata.google.internal']; if (in_array(strtolower($host), $blockedHosts)) { return false; } return true; } /** * Valide l'endpoint API pour prévenir l'injection de chemins * * @param string $endpoint Endpoint à valider * @return bool True si l'endpoint est valide */ function isValidApiEndpoint($endpoint) { // Bloquer les tentatives de path traversal if (strpos($endpoint, '..') !== false || strpos($endpoint, '//') !== false) { return false; } // Autoriser uniquement les caractères alphanumériques, tirets, underscores et slashes if (!preg_match('/^[a-zA-Z0-9\/_-]+$/', $endpoint)) { return false; } // Liste blanche des endpoints autorisés $allowedEndpoints = [ 'videos', 'videos/categories', 'search/videos', 'videos/.*', // Pour les endpoints dynamiques comme videos/{id} 'videos/.*/comment-threads', // Pour les commentaires 'accounts', 'accounts/.*/videos' // Pour les vidéos d'un compte spécifique ]; foreach ($allowedEndpoints as $pattern) { // Remplacer les .* par des marqueurs temporaires $tempPattern = str_replace('.*', '__WILDCARD__', $pattern); // Échapper les caractères spéciaux regex $escapedPattern = preg_quote($tempPattern, '/'); // Remettre les wildcards en place $regexPattern = str_replace('__WILDCARD__', '.*', $escapedPattern); if (preg_match('/^' . $regexPattern . '$/', $endpoint)) { return true; } } return false; } /** * Récupère les catégories depuis l'API PeerTube * * @return array Liste des catégories */ function getCategories() { // Utiliser les catégories déjà récupérées $categories = PEERTUBE_CATEGORIES; $result = []; foreach ($categories as $key => $name) { $result[] = [ 'id' => $key, 'name' => $name ]; } return $result; } /** * Récupère les vidéos récentes depuis l'API PeerTube * * @param int $count Nombre de vidéos à récupérer * @return array Liste des vidéos récentes */ function getRecentVideos($count = RECENT_VIDEOS_COUNT) { // Récupérer les vidéos récentes $data = callPeerTubeApi('videos', [ 'sort' => '-publishedAt', 'count' => $count, 'isLocal' => true ]); return formatVideosData($data['data'] ?? []); } /** * Récupère les vidéos tendances depuis l'API PeerTube * * @param int $count Nombre de vidéos à récupérer * @return array Liste des vidéos tendances */ function getTrendingVideos($count = TRENDING_VIDEOS_COUNT) { // Récupérer les vidéos tendances $data = callPeerTubeApi('videos', [ 'sort' => '-trending', 'count' => $count, 'isLocal' => true ]); return formatVideosData($data['data'] ?? []); } /** * Récupère les vidéos avec un tag spécifique depuis l'API PeerTube * * @param string $tag Tag à filtrer * @param int $count Nombre de vidéos à récupérer * @return array Liste des vidéos */ function getVideosByTag($tag, $count) { // Récupérer les vidéos par tag $data = callPeerTubeApi('videos', [ 'tagsOneOf' => $tag, 'count' => $count, 'isLocal' => true ]); return formatVideosData($data['data'] ?? []); } /** * Récupère les shorts (vidéos courtes) depuis l'API PeerTube * Les shorts sont des vidéos locales de moins de 2 minutes * * @param int $count Nombre de shorts à récupérer * @return array Liste des shorts */ function getShorts($count = SHORTS_COUNT) { // Récupérer plus de vidéos que nécessaire pour pouvoir filtrer $data = callPeerTubeApi('videos', [ 'sort' => '-publishedAt', // Les plus récentes d'abord 'count' => SHORTS_COUNT_SEARCH, 'isLocal' => true ]); // Formater les données $allVideos = formatVideosData($data['data'] ?? []); // Filtrer pour ne garder que les vidéos de moins de 2 minutes (120 secondes) et en mode portrait $shortVideos = array_filter($allVideos, function($video) { // Vérifier la durée (moins de 2 minutes) $durationOk = $video['duration'] < SHORTS_MAX_DURATION; // Vérifier le ratio (mode portrait) $ratioOk = isset($video['aspectRatio']) && $video['aspectRatio'] <= 1; return $durationOk && $ratioOk; }); // Limiter au nombre demandé return array_slice($shortVideos, 0, $count); } /** * Récupère les vidéos sur l'indépendance depuis l'API PeerTube * * @param int $count Nombre de vidéos à récupérer * @return array Liste des vidéos sur l'indépendance */ function getIndependenceVideos($count = INDEPENDENCE_VIDEOS_COUNT) { // Récupérer les vidéos sur l'indépendance return getVideosByTag(TAG_INDEPENDENCE, $count); } /** * Vérifie s'il y a un direct en cours du compte LIVE_ACCOUNT_NAME sur l'instance PeerTube * * @return array|null Informations sur le direct en cours ou null si aucun direct */ function getLiveStream() { // Récupérer les lives du compte spécifié $accountName = LIVE_ACCOUNT_NAME; $data = callPeerTubeApi('accounts/' . $accountName . '/videos', [ 'count' => 1, 'isLocal' => true, 'isLive' => true, // Filtrer uniquement les lives 'sort' => '-publishedAt' // Les plus récents en premier ]); // Vérifier si on a des résultats if (empty($data['data']) || count($data['data']) === 0) { return null; } // Formater les données du live $liveData = formatVideosData($data['data']); // Filtrer pour ne garder que les lives en cours $activeLives = array_filter($liveData, function($video) { return isset($video['isLive']) && $video['isLive'] === true; }); // Retourner le premier live trouvé return !empty($activeLives) ? reset($activeLives) : null; } /** * Formate les données brutes des vidéos venant de l'API * * @param array $videosData Données brutes des vidéos * @return array Données formatées */ function formatVideosData($videosData) { $videos = []; foreach ($videosData as $video) { // Récupérer la vignette (thumbnail) $thumbnail = isset($video['previewPath']) ? PEERTUBE_URL . $video['previewPath'] : 'img/default-thumbnail.jpg'; // Récupérer l'avatar de la chaîne $channelAvatar = isset($video['channel']['avatars'][0]['path']) && isset($video['channel']['avatars'][0]['path']) ? PEERTUBE_URL . $video['channel']['avatars'][0]['path'] : 'img/default-avatar.png'; // Formater les données $videos[] = [ 'id' => $video['uuid'], 'title' => $video['name'], 'thumbnail' => $thumbnail, 'duration' => $video['duration'], 'channel' => $video['channel']['displayName'], 'channelAvatar' => $channelAvatar, 'views' => $video['views'], 'date' => $video['publishedAt'], 'aspectRatio' => $video['aspectRatio'], 'description' => $video['description'] ?? '', 'tags' => $video['tags'] ?? [], 'isLive' => isset($video['isLive']) ? $video['isLive'] : false ]; } return $videos; } // Fonctions utilitaires pour formater les données d'affichage function formatDuration($seconds) { $hours = floor($seconds / 3600); $minutes = floor(($seconds % 3600) / 60); $remainingSeconds = $seconds % 60; if ($hours > 0) { return sprintf('%d:%02d:%02d', $hours, $minutes, $remainingSeconds); } else { return sprintf('%d:%02d', $minutes, $remainingSeconds); } } function formatViewCount($views) { if ($views >= 1000000) { return round($views / 1000000, 1) . 'M'; } elseif ($views >= 1000) { return round($views / 1000, 1) . 'K'; } else { return $views; } } function formatDate($dateString) { $date = new DateTime($dateString); $now = new DateTime(); $interval = $now->diff($date); if ($interval->days == 0) { return 'Aujourd\'hui'; } elseif ($interval->days == 1) { return 'Hier'; } elseif ($interval->days < 7) { return 'Il y a ' . $interval->days . ' jours'; } elseif ($interval->days < 30) { $weeks = floor($interval->days / 7); return 'Il y a ' . $weeks . ' semaine' . ($weeks > 1 ? 's' : ''); } elseif ($interval->days < 365) { $months = floor($interval->days / 30); return 'Il y a ' . $months . ' mois'; } else { $years = floor($interval->days / 365); return 'Il y a ' . $years . ' an' . ($years > 1 ? 's' : ''); } } /** * Récupère les vidéos d'une catégorie spécifique depuis l'API PeerTube * * @param int $categoryId Identifiant de la catégorie * @param int $count Nombre de vidéos à récupérer * @return array Liste des vidéos de la catégorie */ function getVideosByCategory($categoryId, $count = CATEGORY_VIDEOS_COUNT) { // Récupérer les vidéos par catégorie $data = callPeerTubeApi('videos', [ 'categoryOneOf' => $categoryId, 'count' => $count, 'sort' => '-publishedAt', // Les plus récentes d'abord 'isLocal' => true ]); return formatVideosData($data['data'] ?? []); } /** * Récupère la liste des catégories à afficher (triées selon les priorités) * * @return array Liste des catégories avec id, name et videos */ function getDisplayCategories() { $categories = []; $priorityCategories = PRIORITY_CATEGORIES; $allCategories = PEERTUBE_CATEGORIES; // Ajouter uniquement les catégories prioritaires dans l'ordre défini foreach ($priorityCategories as $catId => $categoryName) { $videos = getVideosByCategory($catId); // N'ajouter que les catégories qui ont des vidéos if (!empty($videos)) { $categories[] = [ 'id' => $catId, 'name' => $categoryName, 'videos' => $videos ]; } } return $categories; } /** * Récupère les commentaires d'une vidéo * @param string $videoId ID de la vidéo * @return array Tableau des commentaires */ function getVideoComments($videoId) { $endpoint = "videos/{$videoId}/comment-threads"; $response = callPeerTubeApi($endpoint); if (!$response || !isset($response['data'])) { return []; } return $response['data']; } /** * Récupère les options de téléchargement pour une vidéo * @param string $videoId ID de la vidéo * @return array Options de téléchargement */ function getVideoDownloadOptions($videoId) { // Récupérer les informations complètes de la vidéo $videoData = callPeerTubeApi('videos/' . $videoId); $downloadOptions = []; // Ajouter les fichiers directs s'ils existent if (isset($videoData['files']) && !empty($videoData['files'])) { foreach ($videoData['files'] as $file) { if (isset($file['fileDownloadUrl']) && !empty($file['fileDownloadUrl'])) { $downloadOptions[] = [ 'type' => 'direct', 'url' => PEERTUBE_URL . $file['fileDownloadUrl'], 'resolution' => isset($file['resolution']['label']) ? $file['resolution']['label'] : 'Original', 'size' => isset($file['size']) ? formatFileSize($file['size']) : 'Inconnu' ]; } } } // Ajouter les playlists de streaming s'ils existent if (isset($videoData['streamingPlaylists']) && !empty($videoData['streamingPlaylists'])) { foreach ($videoData['streamingPlaylists'] as $playlist) { if (isset($playlist['files']) && !empty($playlist['files'])) { foreach ($playlist['files'] as $file) { if (isset($file['fileDownloadUrl']) && !empty($file['fileDownloadUrl'])) { $downloadOptions[] = [ 'type' => 'hls', 'url' => $file['fileDownloadUrl'], 'resolution' => isset($file['resolution']['label']) ? $file['resolution']['label'] : 'Original', 'size' => isset($file['size']) ? formatFileSize($file['size']) : 'Inconnu' ]; } } } } } return $downloadOptions; } /** * Formate la taille d'un fichier en format lisible * @param int $bytes Taille en octets * @return string Taille formatée */ function formatFileSize($bytes) { $units = ['B', 'KB', 'MB', 'GB', 'TB']; $bytes = max($bytes, 0); $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); $pow = min($pow, count($units) - 1); $bytes /= (1 << (10 * $pow)); return round($bytes, 2) . ' ' . $units[$pow]; } /** * Recherche des vidéos selon un critère * * @param string $query Terme de recherche * @param int $count Nombre de vidéos à récupérer * @param int $start Index de départ pour la pagination * @return array Liste des vidéos correspondant à la recherche */ function searchVideos($query, $count = COUNT_VIDEO_SEARCH, $start = 0) { if (empty($query)) { return []; } // Vérifier si la recherche concerne un tag (commence par #) $isTagSearch = false; if (substr($query, 0, 1) === '#') { $isTagSearch = true; $tag = substr($query, 1); // Enlever le # du début // Récupérer les vidéos avec ce tag via l'API $data = callPeerTubeApi('videos', [ 'tagsOneOf' => $tag, 'count' => $count, 'start' => $start, 'isLocal' => true, // Uniquement les vidéos locales 'sort' => '-publishedAt' // Les plus récentes d'abord ]); return formatVideosData($data['data'] ?? []); } // Recherche normale (pas un tag) $data = callPeerTubeApi('search/videos', [ 'search' => $query, 'count' => $count, 'start' => $start, 'isLocal' => true, // Uniquement les vidéos locales 'sort' => '-publishedAt' // Les plus récentes d'abord ]); return formatVideosData($data['data'] ?? []); } /** * Récupère les derniers épisodes d'un ou plusieurs podcasts Castopod via leurs feeds RSS * * @param string $castopodUrl URL de l'instance Castopod * @param array|string $podcastSlugs Slug(s) du/des podcast(s) - tableau ou chaîne unique * @param int $count Nombre d'épisodes à récupérer (total) * @return array Liste des épisodes formatés, triés par date */ function getCastopodEpisodes($castopodUrl = null, $podcastSlugs = null, $count = 5) { if (!defined('CASTOPOD_ENABLED') || !CASTOPOD_ENABLED) { return []; } $castopodUrl = $castopodUrl ?? CASTOPOD_URL; $podcastSlugs = $podcastSlugs ?? (defined('CASTOPOD_PODCAST_SLUGS') ? CASTOPOD_PODCAST_SLUGS : ['joukawouve']); $count = $count ?? CASTOPOD_EPISODES_COUNT; // Convertir en tableau si c'est une chaîne unique (rétrocompatibilité) if (is_string($podcastSlugs)) { $podcastSlugs = [$podcastSlugs]; } // Clé de cache unique pour la combinaison de podcasts $cacheKey = 'castopod_' . md5($castopodUrl . implode('_', $podcastSlugs) . '_' . $count); // Vérifier le cache if (defined('CACHE_ENABLED') && CACHE_ENABLED) { $cachedData = getFromCache($cacheKey); if ($cachedData !== null) { return $cachedData; } } $allEpisodes = []; // Itérer sur chaque podcast foreach ($podcastSlugs as $index => $podcastSlug) { try { // Ajouter un délai entre les requêtes pour éviter le rate limiting (sauf pour la première) // Note: Castopod a un rate limit strict, le cache est fortement recommandé if ($index > 0) { sleep(5); // 5 secondes de délai minimum pour Castopod } // Construire l'URL du feed RSS pour ce podcast $feedUrl = rtrim($castopodUrl, '/') . '/@' . $podcastSlug . '/feed'; // Récupérer le feed RSS $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $feedUrl); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); $xmlContent = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); 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 if (defined('CACHE_ENABLED') && CACHE_ENABLED) { saveToCache($cacheKey, $allEpisodes); } return $allEpisodes; } /** * Récupère des morceaux aléatoires depuis Funkwhale via l'API * * @param string $funkwhaleUrl URL de l'instance Funkwhale * @param int $count Nombre de morceaux à récupérer * @return array Liste des morceaux formatés */ function getFunkwhaleTracks($funkwhaleUrl = null, $count = null) { if (!defined('FUNKWHALE_ENABLED') || !FUNKWHALE_ENABLED) { return []; } $funkwhaleUrl = $funkwhaleUrl ?? FUNKWHALE_URL; $count = $count ?? FUNKWHALE_TRACKS_COUNT; // Clé de cache - on récupère un grand nombre de morceaux pour le cache $cacheKey = 'funkwhale_' . md5($funkwhaleUrl); $cacheFetchSize = 50; // Nombre de morceaux à mettre en cache // Vérifier le cache $allTracks = null; if (defined('CACHE_ENABLED') && CACHE_ENABLED) { $allTracks = getFromCache($cacheKey); } // Si pas de cache, récupérer depuis l'API if ($allTracks === null) { try { // Construire l'URL de l'API avec filtre pour morceaux locaux uniquement // Le paramètre scope=me récupère uniquement les morceaux de l'instance locale $apiUrl = rtrim($funkwhaleUrl, '/') . '/api/v1/tracks?ordering=random&page_size=' . $cacheFetchSize . '&scope=all'; // Récupérer les morceaux via cURL $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $apiUrl); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); $jsonContent = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode !== 200 || !$jsonContent) { return []; } // Parser le JSON $data = json_decode($jsonContent, true); if (!$data || !isset($data['results'])) { return []; } $allTracks = []; // Extraire le domaine de l'instance locale pour filtrer $localDomain = parse_url($funkwhaleUrl, PHP_URL_HOST); // Parcourir les morceaux et filtrer pour garder uniquement les morceaux locaux foreach ($data['results'] as $item) { // Vérifier si le morceau est local en comparant le domaine // Les morceaux fédérés ont généralement un domaine différent dans leur URL $isLocal = true; // Vérifier via l'URL de l'artiste ou de l'album if (isset($item['artist']['channel'])) { $artistDomain = parse_url($item['artist']['channel'], PHP_URL_HOST); if ($artistDomain && $artistDomain !== $localDomain) { $isLocal = false; } } // Ou vérifier via le champ is_local si disponible if (isset($item['is_local']) && !$item['is_local']) { $isLocal = false; } // Ne garder que les morceaux locaux if ($isLocal) { // Récupérer l'URL d'écoute depuis l'API (peut être relative) $listenUrl = $item['listen_url'] ?? ''; // Si l'URL est relative, la transformer en URL absolue if (!empty($listenUrl) && strpos($listenUrl, 'http') !== 0) { $listenUrl = rtrim($funkwhaleUrl, '/') . $listenUrl; } $track = [ 'title' => $item['title'] ?? '', 'artist' => $item['artist']['name'] ?? 'Artiste inconnu', 'album' => $item['album']['title'] ?? '', 'cover' => $item['album']['cover']['urls']['medium_square_crop'] ?? '', 'duration' => $item['uploads'][0]['duration'] ?? 0, 'link' => rtrim($funkwhaleUrl, '/') . '/library/tracks/' . ($item['id'] ?? ''), 'trackId' => $item['id'] ?? 0, 'audioUrl' => $listenUrl, ]; // Formater la durée if ($track['duration']) { $track['formattedDuration'] = formatDuration($track['duration']); } else { $track['formattedDuration'] = ''; } $allTracks[] = $track; } } // Mettre en cache tous les morceaux locaux if (defined('CACHE_ENABLED') && CACHE_ENABLED) { saveToCache($cacheKey, $allTracks); } } catch (Exception $e) { error_log('Erreur lors de la récupération des morceaux Funkwhale: ' . $e->getMessage()); return []; } } // Si pas assez de morceaux, retourner ce qu'on a if (empty($allTracks)) { return []; } // Sélectionner aléatoirement $count morceaux parmi tous les morceaux en cache $trackCount = count($allTracks); if ($trackCount <= $count) { return $allTracks; } // Mélanger et prendre les N premiers shuffle($allTracks); return array_slice($allTracks, 0, $count); } ?>