diff --git a/api.php b/api.php index 2667a26..c0f198c 100644 --- a/api.php +++ b/api.php @@ -4,12 +4,11 @@ header("Access-Control-Allow-Origin: *"); header("Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS"); header("Access-Control-Allow-Headers: Content-Type, Authorization"); header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0"); -header("Pragma: no-cache"); if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; } define('ENCRYPTION_KEY', 'MaCleSecreteSuperRobuste123!'); -define('TMDB_CACHE_TTL', 86400); // 24h de cache +define('TMDB_CACHE_TTL', 604800); // 7 jours de cache try { $pdo = new PDO("mysql:host=localhost;dbname=mon_cinema;charset=utf8mb4", "root", "", [ @@ -21,9 +20,15 @@ try { $pdo->exec("CREATE TABLE IF NOT EXISTS critiques (id BIGINT PRIMARY KEY, title VARCHAR(255) NOT NULL, year VARCHAR(10), director VARCHAR(255), poster TEXT, rating DECIMAL(3,1) DEFAULT 3.0, review TEXT, streaming VARCHAR(255))"); $pdo->exec("ALTER TABLE critiques MODIFY COLUMN rating DECIMAL(3,1) DEFAULT 3.0;"); $pdo->exec("CREATE TABLE IF NOT EXISTS videotheque (id BIGINT PRIMARY KEY, title VARCHAR(255) NOT NULL, year VARCHAR(10), director VARCHAR(255), poster TEXT, format VARCHAR(50), length VARCHAR(50), publisher VARCHAR(255), ean_isbn13 VARCHAR(50), number_of_discs INT DEFAULT 1, aspect_ratio VARCHAR(50), description TEXT)"); - // 🆕 Table de cache pour les images (évite les appels répétés) - $pdo->exec("CREATE TABLE IF NOT EXISTS cache_images ( - cache_key VARCHAR(120) PRIMARY KEY, + + // 🆕 Tables de cache + $pdo->exec("CREATE TABLE IF NOT EXISTS cache_tmdb ( + cache_key VARCHAR(100) PRIMARY KEY, + data TEXT NOT NULL, + created_at INT NOT NULL + )"); + $pdo->exec("CREATE TABLE IF NOT EXISTS cache_ean ( + ean VARCHAR(20) PRIMARY KEY, image_url TEXT, source VARCHAR(20), created_at INT NOT NULL @@ -69,100 +74,114 @@ function getTmdbApiKey($pdo) { return decryptData($row['key_value']); } -// ─ HTTP unifié ── +// ── HTTP unifié avec cURL ── function httpGet($url, $timeout = 8) { if (function_exists('curl_init')) { $ch = curl_init($url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_USERAGENT, 'MonCinema/2.0'); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => $timeout, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_USERAGENT => 'MonCinema/3.0', + CURLOPT_FOLLOWLOCATION => true + ]); $res = curl_exec($ch); curl_close($ch); return $res ?: null; } - $ctx = stream_context_create(['http' => ['timeout' => $timeout, 'user_agent' => 'MonCinema/2.0']]); + $ctx = stream_context_create(['http' => ['timeout' => $timeout, 'user_agent' => 'MonCinema/3.0']]); return @file_get_contents($url, false, $ctx); } -// ── 🎬 RÉCUPÉRATION IMAGE VIA EAN (Open Library API) ─ +// ── NETTOYAGE TITRE (crucial pour TMDB) ── +function cleanTitleForTmdb($title) { + // Enlever tout ce qui est entre crochets/parenthèses + $clean = preg_replace('/\s*[\[\(].*?[\]\)]\s*/', '', $title); + // Enlever les mentions d'édition + $clean = preg_replace('/\s*-\s*(Édition|Edition|Collector|Simple|Spéciale|Speciale|Digibook|Ultimate|Intégrale|Integrale).*$/i', '', $clean); + // Enlever "Combo Blu-ray + DVD" + $clean = preg_replace('/\s*\[Combo.*?\]\s*/i', '', $clean); + // Enlever "Blu-ray", "DVD", "4K", "UHD" à la fin + $clean = preg_replace('/\s*(Blu-ray|DVD|4K|UHD|VHS)\s*$/i', '', $clean); + // Enlever " - Édition X DVD" + $clean = preg_replace('/\s*-\s*Édition\s+\d+\s*DVD\s*$/i', '', $clean); + // Nettoyer les espaces multiples et tirets en trop + $clean = preg_replace('/\s*-\s*$/', '', $clean); + $clean = preg_replace('/\s{2,}/', ' ', $clean); + return trim($clean); +} + +// ── RÉCUPÉRATION IMAGE VIA EAN (UPCitemdb - spécialisé DVD/Blu-ray) ── function fetchImageByEAN($ean, $pdo = null) { if (empty($ean) || strlen($ean) < 10) return null; // Vérifier le cache if ($pdo) { try { - $stmt = $pdo->prepare("SELECT image_url FROM cache_images WHERE cache_key = ? AND source = 'ean' AND created_at > ?"); - $stmt->execute(['ean_' . $ean, time() - TMDB_CACHE_TTL]); + $stmt = $pdo->prepare("SELECT image_url FROM cache_ean WHERE ean = ? AND created_at > ?"); + $stmt->execute([$ean, time() - TMDB_CACHE_TTL]); $row = $stmt->fetch(); if ($row && !empty($row['image_url'])) return $row['image_url']; } catch (\Exception $e) { /* ignore */ } } - // Open Library API (gratuit, sans clé, spécialisé dans les livres/DVD) - $url = "https://openlibrary.org/api/books?bibkeys=ISBN:{$ean}&jscmd=data&format=json"; + // UPCitemdb API (gratuit, spécialisé DVD/Blu-ray/CD) + $url = "https://api.upcitemdb.com/prod/trial/lookup?upc={$ean}"; $res = httpGet($url, 6); + if ($res) { + $data = json_decode($res, true); + if (!empty($data['items'][0]['images'][0])) { + $imageUrl = $data['items'][0]['images'][0]; + // Sauvegarder dans le cache + if ($pdo) { + try { + $stmt = $pdo->prepare("REPLACE INTO cache_ean (ean, image_url, source, created_at) VALUES (?, ?, 'upcitemdb', ?)"); + $stmt->execute([$ean, $imageUrl, time()]); + } catch (\Exception $e) { /* ignore */ } + } + return $imageUrl; + } + } + + // Fallback : Open Library (pour les livres/CD) + $url = "https://openlibrary.org/api/books?bibkeys=ISBN:{$ean}&jscmd=data&format=json"; + $res = httpGet($url, 5); if ($res) { $data = json_decode($res, true); $key = "ISBN:{$ean}"; if (isset($data[$key])) { $cover = $data[$key]['cover'] ?? []; $imageUrl = $cover['large'] ?? $cover['medium'] ?? $cover['small'] ?? null; - - // Sauvegarder dans le cache - if ($imageUrl && $pdo) { - try { - $stmt = $pdo->prepare("REPLACE INTO cache_images (cache_key, image_url, source, created_at) VALUES (?, ?, 'ean', ?)"); - $stmt->execute(['ean_' . $ean, $imageUrl, time()]); - } catch (\Exception $e) { /* ignore */ } + if ($imageUrl) { + if ($pdo) { + try { + $stmt = $pdo->prepare("REPLACE INTO cache_ean (ean, image_url, source, created_at) VALUES (?, ?, 'openlibrary', ?)"); + $stmt->execute([$ean, $imageUrl, time()]); + } catch (\Exception $e) { /* ignore */ } + } + return $imageUrl; } - return $imageUrl; - } - } - - // Fallback : Google Books API - $url = "https://www.googleapis.com/books/v1/volumes?q=isbn:{$ean}&maxResults=1"; - $res = httpGet($url, 6); - if ($res) { - $data = json_decode($res, true); - if (!empty($data['items'][0]['volumeInfo']['imageLinks']['thumbnail'])) { - $imageUrl = str_replace('http:', 'https:', $data['items'][0]['volumeInfo']['imageLinks']['thumbnail']); - if ($pdo) { - try { - $stmt = $pdo->prepare("REPLACE INTO cache_images (cache_key, image_url, source, created_at) VALUES (?, ?, 'google', ?)"); - $stmt->execute(['ean_' . $ean, $imageUrl, time()]); - } catch (\Exception $e) { /* ignore */ } - } - return $imageUrl; } } return null; } -// ── RÉCUPÉRATION TMDB (avec cache) ── +// ── TMDB AVEC CACHE + curl_multi ── function fetchTmdbData($title, $year, $apiKey, $pdo = null) { if (empty($apiKey) || empty($title)) return null; - $cleanTitle = preg_replace('/\s*\[.*?\]\s*/', '', $title); - $cleanTitle = trim($cleanTitle); + $cleanTitle = cleanTitleForTmdb($title); $cacheKey = md5(strtolower($cleanTitle) . '|' . $year); // Vérifier le cache if ($pdo) { try { - $stmt = $pdo->prepare("SELECT image_url FROM cache_images WHERE cache_key = ? AND source = 'tmdb' AND created_at > ?"); - $stmt->execute(['tmdb_' . $cacheKey, time() - TMDB_CACHE_TTL]); + $stmt = $pdo->prepare("SELECT data FROM cache_tmdb WHERE cache_key = ? AND created_at > ?"); + $stmt->execute([$cacheKey, time() - TMDB_CACHE_TTL]); $row = $stmt->fetch(); - if ($row && !empty($row['image_url'])) { - // Récupérer aussi le directeur et streaming depuis le cache JSON - $stmt2 = $pdo->prepare("SELECT image_url FROM cache_images WHERE cache_key = ?"); - $stmt2->execute(['tmdb_full_' . $cacheKey]); - $row2 = $stmt2->fetch(); - if ($row2) return json_decode($row2['image_url'], true); - } + if ($row) return json_decode($row['data'], true); } catch (\Exception $e) { /* ignore */ } } @@ -171,39 +190,69 @@ function fetchTmdbData($title, $year, $apiKey, $pdo = null) { if (!$searchRes) return null; $searchData = json_decode($searchRes, true); + if (empty($searchData['results'])) { + // 2ème tentative sans l'année + $searchUrl2 = "https://api.themoviedb.org/3/search/movie?api_key={$apiKey}&query=" . urlencode($cleanTitle) . "&language=fr-FR"; + $searchRes2 = httpGet($searchUrl2, 8); + if ($searchRes2) { + $searchData2 = json_decode($searchRes2, true); + if (!empty($searchData2['results'])) $searchData = $searchData2; + } + } + if (empty($searchData['results'])) return null; $movie = $searchData['results'][0]; $movieId = $movie['id']; $poster = !empty($movie['poster_path']) ? "https://image.tmdb.org/t/p/w500" . $movie['poster_path'] : ''; - // Récupération Réalisateur - $creditsUrl = "https://api.themoviedb.org/3/movie/{$movieId}/credits?api_key={$apiKey}&language=fr-FR"; - $creditsRes = httpGet($creditsUrl, 8); + // Appels parallèles avec curl_multi $director = ''; - if ($creditsRes) { - $creditsData = json_decode($creditsRes, true); - if (!empty($creditsData['crew'])) { - foreach ($creditsData['crew'] as $crew) { - if ($crew['job'] === 'Director') { $director = $crew['name']; break; } + $streaming = ''; + + if (function_exists('curl_multi_init')) { + $mh = curl_multi_init(); + $handles = []; + + $ch1 = curl_init("https://api.themoviedb.org/3/movie/{$movieId}/credits?api_key={$apiKey}&language=fr-FR"); + $ch2 = curl_init("https://api.themoviedb.org/3/movie/{$movieId}/watch/providers?api_key={$apiKey}"); + + curl_setopt_array($ch1, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 6, CURLOPT_SSL_VERIFYPEER => false]); + curl_setopt_array($ch2, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 6, CURLOPT_SSL_VERIFYPEER => false]); + + curl_multi_add_handle($mh, $ch1); + curl_multi_add_handle($mh, $ch2); + + $running = 0; + do { curl_multi_exec($mh, $running); curl_multi_select($mh); } while ($running > 0); + + $creditsRes = curl_multi_getcontent($ch1); + $watchRes = curl_multi_getcontent($ch2); + + curl_multi_remove_handle($mh, $ch1); curl_close($ch1); + curl_multi_remove_handle($mh, $ch2); curl_close($ch2); + curl_multi_close($mh); + + if ($creditsRes) { + $creditsData = json_decode($creditsRes, true); + if (!empty($creditsData['crew'])) { + foreach ($creditsData['crew'] as $crew) { + if ($crew['job'] === 'Director') { $director = $crew['name']; break; } + } } } - } - - // Récupération Streaming (France) - $streaming = ''; - $watchUrl = "https://api.themoviedb.org/3/movie/{$movieId}/watch/providers?api_key={$apiKey}"; - $watchRes = httpGet($watchUrl, 8); - if ($watchRes) { - $watchData = json_decode($watchRes, true); - $frProviders = $watchData['results']['FR'] ?? []; - $platforms = []; - if (!empty($frProviders['flatrate'])) { foreach ($frProviders['flatrate'] as $p) $platforms[] = $p['provider_name']; } - if (empty($platforms)) { - if (!empty($frProviders['rent'])) { foreach ($frProviders['rent'] as $p) $platforms[] = $p['provider_name'] . ' (loc.)'; } - if (!empty($frProviders['buy'])) { foreach ($frProviders['buy'] as $p) $platforms[] = $p['provider_name'] . ' (achat)'; } + + if ($watchRes) { + $watchData = json_decode($watchRes, true); + $frProviders = $watchData['results']['FR'] ?? []; + $platforms = []; + if (!empty($frProviders['flatrate'])) { foreach ($frProviders['flatrate'] as $p) $platforms[] = $p['provider_name']; } + if (empty($platforms)) { + if (!empty($frProviders['rent'])) { foreach ($frProviders['rent'] as $p) $platforms[] = $p['provider_name'] . ' (loc.)'; } + if (!empty($frProviders['buy'])) { foreach ($frProviders['buy'] as $p) $platforms[] = $p['provider_name'] . ' (achat)'; } + } + if (!empty($platforms)) $streaming = implode(', ', array_unique($platforms)); } - if (!empty($platforms)) $streaming = implode(', ', array_unique($platforms)); } $result = ['director' => $director, 'poster' => $poster, 'streaming' => $streaming]; @@ -211,24 +260,21 @@ function fetchTmdbData($title, $year, $apiKey, $pdo = null) { // Sauvegarder dans le cache if ($pdo) { try { - $stmt = $pdo->prepare("REPLACE INTO cache_images (cache_key, image_url, source, created_at) VALUES (?, ?, 'tmdb', ?)"); - $stmt->execute(['tmdb_' . $cacheKey, $poster, time()]); - $stmt2 = $pdo->prepare("REPLACE INTO cache_images (cache_key, image_url, source, created_at) VALUES (?, ?, 'tmdb_full', ?)"); - $stmt2->execute(['tmdb_full_' . $cacheKey, json_encode($result), time()]); + $stmt = $pdo->prepare("REPLACE INTO cache_tmdb (cache_key, data, created_at) VALUES (?, ?, ?)"); + $stmt->execute([$cacheKey, json_encode($result), time()]); } catch (\Exception $e) { /* ignore */ } } return $result; } -// ── Détection format ── function detectFormat($title, $description = '') { $t = strtoupper($title . ' ' . $description); if (strpos($t, '4K') !== false || strpos($t, 'UHD') !== false) return 'Blu-ray 4K'; - if (strpos($t, 'BLU-RAY') !== false || strpos($t, 'BLURAY') !== false || strpos($t, 'BLU-RAY') !== false) return 'Blu-ray'; + if (strpos($t, 'BLU-RAY') !== false || strpos($t, 'BLURAY') !== false) return 'Blu-ray'; if (strpos($t, 'DVD') !== false) return 'DVD'; if (strpos($t, 'VHS') !== false) return 'VHS'; - if (strpos($t, 'COFFRET') !== false || strpos($t, 'TRILOGIE') !== false || strpos($t, 'INTEGRALE') !== false) return 'Coffret'; + if (strpos($t, 'COFFRET') !== false || strpos($t, 'TRILOGIE') !== false) return 'Coffret'; return 'DVD'; } @@ -321,25 +367,22 @@ switch ($action) { else { http_response_code(400); echo json_encode(["success" => false, "error" => "Aucun élément sélectionné."]); } break; - // ── IMPORT PAR LOTS AVEC RÉCUPÉRATION JAQUETTES ─ case 'import_batch': checkAuth($pdo); $items = $data['items'] ?? []; $type = $data['type'] ?? 'videotheque'; $tmdbApiKey = getTmdbApiKey($pdo); $imported = 0; - $stats = ['ean_hits' => 0, 'tmdb_hits' => 0, 'no_image' => 0]; + $stats = ['ean_hits' => 0, 'tmdb_hits' => 0, 'no_image' => 0, 'ean_miss' => [], 'tmdb_miss' => []]; $pdo->beginTransaction(); foreach ($items as $rowData) { - // ── MAPPING EXACT DES COLONNES DE VOTRE CSV ── $title = $rowData['title'] ?? $rowData['Name'] ?? 'Sans titre'; $firstName = $rowData['first_name'] ?? ''; $lastName = $rowData['last_name'] ?? ''; $creators = $rowData['creators'] ?? ''; - // Réalisateur : priorité first_name + last_name, sinon creators $director = ''; if (!empty($firstName) && !empty($lastName)) { $director = trim("$firstName $lastName"); @@ -347,7 +390,6 @@ switch ($action) { $director = $creators; } - // Année depuis publish_date $publishDate = $rowData['publish_date'] ?? $rowData['Year'] ?? $rowData['year'] ?? ''; $year = extractYear($publishDate); @@ -355,33 +397,36 @@ switch ($action) { $description = $rowData['description'] ?? $rowData['Description'] ?? ''; $publisher = $rowData['publisher'] ?? $rowData['Publisher'] ?? ''; $length = $rowData['length'] ?? $rowData['Length'] ?? ''; - $discs = $rowData['number_of_discs'] ?? $rowData['Number of Discs'] ?? 1; - $aspect = $rowData['aspect_ratio'] ?? $rowData['Aspect Ratio'] ?? ''; + $discs = $rowData['number_of_discs'] ?? 1; + $aspect = $rowData['aspect_ratio'] ?? ''; $format = $rowData['format'] ?? $rowData['Format'] ?? detectFormat($title, $description); - // ── RÉCUPÉRATION IMAGE : PRIORITÉ EAN (jaquette physique) ── $poster = $rowData['poster'] ?? $rowData['Poster'] ?? $rowData['image'] ?? ''; - $imageSource = 'none'; + // 1. Priorité : EAN via UPCitemdb if (empty($poster) && !empty($ean)) { $eanImage = fetchImageByEAN($ean, $pdo); if ($eanImage) { $poster = $eanImage; - $imageSource = 'ean'; $stats['ean_hits']++; + } else { + $stats['ean_miss'][] = $ean; } } - // ── FALLBACK TMDB (affiche du film) ── + // 2. Fallback : TMDB avec titre nettoyé if (empty($poster) && $tmdbApiKey) { $tmdbData = fetchTmdbData($title, $year, $tmdbApiKey, $pdo); if ($tmdbData) { if (empty($director)) $director = $tmdbData['director']; if (!empty($tmdbData['poster'])) { $poster = $tmdbData['poster']; - $imageSource = 'tmdb'; $stats['tmdb_hits']++; + } else { + $stats['tmdb_miss'][] = $title; } + } else { + $stats['tmdb_miss'][] = $title; } }