204 lines
7.4 KiB
PHP
204 lines
7.4 KiB
PHP
|
|
<?php
|
||
|
|
/**
|
||
|
|
* Intégration YouTube Data API v3
|
||
|
|
*/
|
||
|
|
|
||
|
|
function callYouTubeApi(string $endpoint, array $params = []): array {
|
||
|
|
if (!defined('YOUTUBE_API_KEY') || empty(YOUTUBE_API_KEY)) return [];
|
||
|
|
|
||
|
|
$params['key'] = YOUTUBE_API_KEY;
|
||
|
|
$url = 'https://www.googleapis.com/youtube/v3/' . $endpoint . '?' . http_build_query($params);
|
||
|
|
|
||
|
|
$ch = curl_init();
|
||
|
|
curl_setopt_array($ch, [
|
||
|
|
CURLOPT_URL => $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;
|
||
|
|
}
|