$url, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_CONNECTTIMEOUT => 8, CURLOPT_FOLLOWLOCATION => false, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_USERAGENT => 'KaubuntuRe/2.0', ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); if ($response === false || !empty($error) || $httpCode < 200 || $httpCode >= 300) { error_log('YouTube API error: ' . $error . ' (HTTP ' . $httpCode . ')'); return []; } return json_decode($response, true) ?: []; } function getYouTubeChannelId(): string { if (defined('YOUTUBE_CHANNEL_ID') && !empty(YOUTUBE_CHANNEL_ID)) { return YOUTUBE_CHANNEL_ID; } if (!defined('YOUTUBE_API_KEY') || empty(YOUTUBE_API_KEY)) return ''; if (!defined('YOUTUBE_CHANNEL_HANDLE') || empty(YOUTUBE_CHANNEL_HANDLE)) return ''; $handle = ltrim(YOUTUBE_CHANNEL_HANDLE, '@'); $cached = $GLOBALS['simple_api_cache']->get('yt_channel_id_' . $handle, []); if ($cached !== null) return (string) $cached; $data = callYouTubeApi('channels', ['part' => 'snippet', 'forHandle' => $handle]); $channelId = $data['items'][0]['id'] ?? ''; if ($channelId) { $GLOBALS['simple_api_cache']->set('yt_channel_id_' . $handle, [], $channelId, 86400); } return $channelId; } function isYouTubeShort(string $isoDuration): bool { preg_match('/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/', $isoDuration, $m); $seconds = (int)($m[1] ?? 0) * 3600 + (int)($m[2] ?? 0) * 60 + (int)($m[3] ?? 0); return $seconds > 0 && $seconds <= 60; } function getYouTubeLatestVideos(int $count = 6): array { // Priorité 1 : YouTube Data API v3 if (defined('YOUTUBE_API_KEY') && !empty(YOUTUBE_API_KEY)) { $channelId = getYouTubeChannelId(); if ($channelId) { $shortsTarget = min(3, $count - 1); $regularTarget = $count - $shortsTarget; $cacheKey = 'yt_videos2_' . $channelId . '_' . $count; $cached = $GLOBALS['simple_api_cache']->get($cacheKey, []); if ($cached !== null) return $cached; // On récupère 50 vidéos pour avoir suffisamment de chaque type $searchData = callYouTubeApi('search', [ 'part' => 'snippet', 'channelId' => $channelId, 'order' => 'date', 'type' => 'video', 'maxResults' => 50, ]); if (!empty($searchData['items'])) { $videoIds = array_map(fn($i) => $i['id']['videoId'], $searchData['items']); // videos.list coûte 1 unité quota (vs 100 pour search) $detailData = callYouTubeApi('videos', [ 'part' => 'contentDetails', 'id' => implode(',', $videoIds), ]); $durations = []; foreach ($detailData['items'] ?? [] as $item) { $durations[$item['id']] = $item['contentDetails']['duration'] ?? ''; } $shorts = []; $regular = []; foreach ($searchData['items'] as $item) { $videoId = $item['id']['videoId']; $isShort = isset($durations[$videoId]) ? isYouTubeShort($durations[$videoId]) : false; $video = [ 'id' => $videoId, 'title' => html_entity_decode($item['snippet']['title'], ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'thumbnail' => $item['snippet']['thumbnails']['high']['url'] ?? $item['snippet']['thumbnails']['default']['url'], 'publishedAt' => $item['snippet']['publishedAt'], 'url' => $isShort ? 'https://www.youtube.com/shorts/' . $videoId : 'https://www.youtube.com/watch?v=' . $videoId, 'isShort' => $isShort, ]; if ($isShort && count($shorts) < $shortsTarget) { $shorts[] = $video; } elseif (!$isShort && count($regular) < $regularTarget) { $regular[] = $video; } if (count($shorts) >= $shortsTarget && count($regular) >= $regularTarget) break; } // Shorts en premier, puis vidéos normales $videos = array_merge($shorts, $regular); if (!empty($videos)) { $ttl = defined('CACHE_DURATION') ? CACHE_DURATION : 900; $GLOBALS['simple_api_cache']->set($cacheKey, [], $videos, $ttl); return $videos; } } } } // Priorité 2 : flux RSS public YouTube (sans clé API, nécessite YOUTUBE_CHANNEL_ID) return getYouTubeVideosFromRSS($count); } function getYouTubeVideosFromRSS(int $count = 6): array { if (!defined('YOUTUBE_CHANNEL_ID') || empty(YOUTUBE_CHANNEL_ID)) return []; $cacheKey = 'yt_rss_' . YOUTUBE_CHANNEL_ID . '_' . $count; $cached = $GLOBALS['simple_api_cache']->get($cacheKey, []); if ($cached !== null) return $cached; $feedUrl = 'https://www.youtube.com/feeds/videos.xml?channel_id=' . urlencode(YOUTUBE_CHANNEL_ID); $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $feedUrl, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_FOLLOWLOCATION => false, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_USERAGENT => 'KaubuntuRe/2.0', ]); $xmlString = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if (!$xmlString || $httpCode !== 200) { error_log('YouTube RSS error: HTTP ' . $httpCode); return []; } $xml = @simplexml_load_string($xmlString); if (!$xml) return []; $videos = []; $ytNs = 'http://www.youtube.com/xml/schemas/2015'; foreach ($xml->entry as $entry) { if (count($videos) >= $count) break; $yt = $entry->children($ytNs); $videoId = (string) $yt->videoId; if (!$videoId) continue; $videos[] = [ 'id' => $videoId, 'title' => html_entity_decode((string) $entry->title, ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'thumbnail' => "https://i.ytimg.com/vi/{$videoId}/hqdefault.jpg", 'publishedAt' => (string) $entry->published, 'url' => "https://www.youtube.com/watch?v={$videoId}", 'isShort' => false, ]; } if (!empty($videos)) { $ttl = defined('CACHE_DURATION') ? CACHE_DURATION : 3600; $GLOBALS['simple_api_cache']->set($cacheKey, [], $videos, $ttl); } return $videos; }