PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC ]); $pdo->exec("CREATE TABLE IF NOT EXISTS users (id INT PRIMARY KEY, username VARCHAR(50) NOT NULL, password_hash VARCHAR(255) NOT NULL)"); $pdo->exec("CREATE TABLE IF NOT EXISTS config (key_name VARCHAR(50) PRIMARY KEY, key_value TEXT NOT NULL)"); $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))"); try { $pdo->exec("ALTER TABLE critiques MODIFY COLUMN rating DECIMAL(3,1) DEFAULT 3.0"); } catch (\Exception $e) {} $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, actors TEXT)"); try { $pdo->exec("ALTER TABLE videotheque ADD COLUMN actors TEXT AFTER description"); } catch (\Exception $e) {} try { $pdo->exec("DROP TABLE IF EXISTS cache_api"); } catch (\Exception $e) {} } catch (\PDOException $e) { echo json_encode(["error" => "Erreur BDD : " . $e->getMessage()]); exit; } // ── FONCTIONS UTILITAIRES ── function makeStableId($type, $title, $year) { return (abs(crc32(strtolower(trim($type ?? '')) . '|' . strtolower(trim($title ?? '')) . '|' . trim($year ?? ''))) % 2000000000) + 100000000; } function checkAuth($pdo) { if ($pdo->query("SELECT COUNT(*) FROM users")->fetchColumn() == 0) return true; $token = $_SERVER['HTTP_AUTHORIZATION'] ?? ''; if (empty($token) && function_exists('apache_request_headers')) { $headers = apache_request_headers(); $token = $headers['Authorization'] ?? $headers['authorization'] ?? ''; } if ($token !== md5(ENCRYPTION_KEY . 'session')) { http_response_code(403); echo json_encode(["error" => "Accès interdit."]); exit; } } function encryptData($data) { $iv = openssl_random_pseudo_bytes(16); $key = hash('sha256', ENCRYPTION_KEY, true); return base64_encode(openssl_encrypt($data, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv) . '::' . $iv); } function decryptData($str) { $decoded = base64_decode($str); if (strpos($decoded, '::') === false) return null; list($enc, $iv) = explode('::', $decoded, 2); return openssl_decrypt($enc, 'AES-256-CBC', hash('sha256', ENCRYPTION_KEY, true), OPENSSL_RAW_DATA, substr($iv, 0, 16)); } function getTmdbApiKey($pdo) { $stmt = $pdo->prepare("SELECT key_value FROM config WHERE key_name = 'tmdb_api_key'"); $stmt->execute(); $row = $stmt->fetch(); return $row ? decryptData($row['key_value']) : null; } function httpGet($url, $timeout = 3) { if (!function_exists('curl_init')) { $ctx = stream_context_create(['http' => ['timeout' => $timeout, 'user_agent' => 'MonCinema/5.0']]); return @file_get_contents($url, false, $ctx); } $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => $timeout, CURLOPT_CONNECTTIMEOUT => 2, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) MonCinema/5.0', CURLOPT_FOLLOWLOCATION => true ]); $res = curl_exec($ch); curl_close($ch); return $res ?: null; } function cleanTitle($title) { $clean = preg_replace('/\s*[\[\(].*?[\]\)]\s*/', '', $title); $clean = preg_replace('/\s*-\s*(Édition|Edition|Collector|Simple|Spéciale|Digibook|Ultimate|Intégrale|Combo|SteelBook|Boîtier).*$/i', '', $clean); $clean = preg_replace('/(blu-ray|bluray|dvd|4k|ultra hd|combo|vhs|bdrip).*$/i', '', $clean); return trim(preg_replace('/\s{2,}/', ' ', $clean)); } function detectFormat($title, $desc = '') { $t = strtoupper($title . ' ' . $desc); if (strpos($t, '4K') !== false || strpos($t, 'UHD') !== false) return '4K Ultra HD'; 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) return 'Coffret'; return 'Blu-ray'; } function extractYear($dateStr) { if (preg_match('/(\d{4})/', $dateStr, $m)) return $m[1]; return ''; } // ── API DVDFr (SANS CACHE - Scraping HTML) ── function fetchDVDFr($ean, $pdo) { if (empty($ean) || strlen($ean) < 8) return null; // 🔥 SUPPRESSION DES APPELS À getCache() QUI N'EXISTE PLUS $ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'; // Étape 1 : Recherche via le site DVDfr (page HTML) $searchUrl = "https://www.dvdfr.com/search/?q=" . urlencode($ean); $ch = curl_init($searchUrl); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 8, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_USERAGENT => $ua, CURLOPT_FOLLOWLOCATION => true, CURLOPT_HTTPHEADER => [ 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language: fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7', ], ]); $html = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if (!$html || $httpCode !== 200) { error_log("DVDFr: Échec recherche HTML - HTTP $httpCode"); return null; } // Étape 2 : Extraire le lien vers la fiche du film $dvdUrl = null; if (preg_match('/]+href=["\']([^"\']+\.html)["\'][^>]*class=["\'][^"\']*result[^"\']*["\'][^>]*>/i', $html, $matches)) { $dvdUrl = $matches[1]; if (strpos($dvdUrl, 'http') !== 0) { $dvdUrl = 'https://www.dvdfr.com' . $dvdUrl; } } if (!$dvdUrl && preg_match('/href=["\']([^"\']*' . preg_quote($ean, '/') . '[^"\']*\.html)["\']/i', $html, $matches)) { $dvdUrl = $matches[1]; if (strpos($dvdUrl, 'http') !== 0) { $dvdUrl = 'https://www.dvdfr.com' . $dvdUrl; } } if (!$dvdUrl && preg_match('/]+href=["\'](https:\/\/www\.dvdfr\.com\/(?:dvd|blu-ray)\/[^"\']+\.html)["\']/i', $html, $matches)) { $dvdUrl = $matches[1]; } if (!$dvdUrl) { error_log("DVDFr: Aucune fiche trouvée pour EAN $ean"); return null; } error_log("DVDFr: Fiche trouvée - $dvdUrl"); // Étape 3 : Récupérer la fiche complète $ch2 = curl_init($dvdUrl); curl_setopt_array($ch2, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 8, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_USERAGENT => $ua, CURLOPT_FOLLOWLOCATION => true, CURLOPT_HTTPHEADER => [ 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language: fr-FR,fr;q=0.9', ], ]); $ficheHtml = curl_exec($ch2); curl_close($ch2); if (!$ficheHtml) { error_log("DVDFr: Impossible de charger la fiche"); return null; } // Étape 4 : Extraire les données depuis le HTML $result = [ 'poster' => '', 'publisher' => '', 'format' => '', 'length' => '', 'aspect' => '', 'discs' => '', ]; // 🔥 EXTRACTION ROBUSTE DE L'AFFICHE (plusieurs méthodes fallback) // Méthode 1 : Chercher toutes les images et filtrer par taille/URL preg_match_all('/]+src=["\']([^"\']+)["\'][^>]*>/i', $ficheHtml, $allImages); if (!empty($allImages[1])) { foreach ($allImages[1] as $imgUrl) { // Filtrer les images qui ressemblent à une jaquette if (preg_match('/(cover|jaquette|pochette|affiche|poster|front)/i', $imgUrl) || preg_match('/\.(jpg|jpeg|png|webp)$/i', $imgUrl)) { // Vérifier que c'est bien une URL complète if (strpos($imgUrl, 'http') === 0) { $result['poster'] = $imgUrl; error_log("DVDFr: Affiche trouvée (méthode 1) - $imgUrl"); break; } elseif (strpos($imgUrl, '//') === 0) { $result['poster'] = 'https:' . $imgUrl; error_log("DVDFr: Affiche trouvée (méthode 1b) - " . $result['poster']); break; } elseif (strpos($imgUrl, '/') === 0) { $result['poster'] = 'https://www.dvdfr.com' . $imgUrl; error_log("DVDFr: Affiche trouvée (méthode 1c) - " . $result['poster']); break; } } } } // Méthode 2 : Chercher dans les balises meta (Open Graph) if (empty($result['poster']) && preg_match('/]+property=["\']og:image["\'][^>]+content=["\']([^"\']+)["\']/i', $ficheHtml, $matches)) { $result['poster'] = $matches[1]; error_log("DVDFr: Affiche trouvée (méthode 2 - og:image) - " . $result['poster']); } // Méthode 3 : Chercher dans les balises link (rel="image_src") if (empty($result['poster']) && preg_match('/]+rel=["\']image_src["\'][^>]+href=["\']([^"\']+)["\']/i', $ficheHtml, $matches)) { $result['poster'] = $matches[1]; error_log("DVDFr: Affiche trouvée (méthode 3 - link image_src) - " . $result['poster']); } // Méthode 4 : Chercher dans les données JSON-LD (structured data) if (empty($result['poster']) && preg_match('/]+type=["\']application\/ld\+json["\'][^>]*>([^<]+)<\/script>/i', $ficheHtml, $matches)) { $jsonData = json_decode($matches[1], true); if ($jsonData && isset($jsonData['image'])) { $result['poster'] = is_array($jsonData['image']) ? $jsonData['image'][0] : $jsonData['image']; error_log("DVDFr: Affiche trouvée (méthode 4 - JSON-LD) - " . $result['poster']); } } // Extraction de l'éditeur (plusieurs méthodes) if (preg_match('/(?:éditeur|distributeur|studio)\s*[:<\/]>\s*([^<]+)/i', $ficheHtml, $matches)) { $result['publisher'] = trim(strip_tags($matches[1])); } elseif (preg_match('/]+class=["\'][^"\']*publisher[^"\']*["\'][^>]*>([^<]+)<\/span>/i', $ficheHtml, $matches)) { $result['publisher'] = trim(strip_tags($matches[1])); } // Extraction du format if (preg_match('/(4k\s*ultra\s*hd|ultra\s*hd|blu[\s-]?ray|dvd|coffret)/i', $ficheHtml, $matches)) { $format = strtoupper(trim($matches[1])); if (strpos($format, '4K') !== false || strpos($format, 'ULTRA') !== false) { $result['format'] = '4K Ultra HD'; } elseif (strpos($format, 'BLU') !== false) { $result['format'] = 'Blu-ray'; } elseif (strpos($format, 'DVD') !== false) { $result['format'] = 'DVD'; } elseif (strpos($format, 'COFFRET') !== false) { $result['format'] = 'Coffret'; } } // Extraction de la durée if (preg_match('/(?:durée|duree|duration)\s*[:<\/]>\s*(\d+)\s*(?:min|mn|h)/i', $ficheHtml, $matches)) { $result['length'] = trim($matches[1]) . ' min'; } // Extraction de l'aspect ratio if (preg_match('/(?:format\s*image|aspect\s*ratio|ratio)\s*[:<\/]>\s*([0-9.]+\s*[:\.]\s*[0-9.]+)/i', $ficheHtml, $matches)) { $result['aspect'] = trim($matches[1]); } // Extraction du nombre de disques if (preg_match('/(?:nombre\s*de\s*disques?|disques?|nb\s*disques?)\s*[:<\/]>\s*(\d+)/i', $ficheHtml, $matches)) { $result['discs'] = trim($matches[1]); } // Nettoyage des données $result = array_map(function($val) { return trim(strip_tags(html_entity_decode($val, ENT_QUOTES | ENT_HTML5, 'UTF-8'))); }, $result); // 🔥 LOG DE DÉBOGAGE error_log("DVDFr: Données finales - " . json_encode($result)); return (!empty($result['poster']) || !empty($result['publisher'])) ? $result : null; } // ── 2. API TMDB (SANS CACHE - TITRE FRANÇAIS) ── function fetchTMDBFull($title, $year, $apiKey, $pdo) { if (empty($apiKey) || empty($title)) return null; $cleanTitle = cleanTitle($title); // 🔥 SUPPRESSION DES APPELS À getCache() QUI N'EXISTE PLUS $searchUrl = "https://api.themoviedb.org/3/search/movie?api_key={$apiKey}&query=" . urlencode($cleanTitle) . "&year={$year}&language=fr-FR"; $searchRes = httpGet($searchUrl, 5); $searchData = $searchRes ? json_decode($searchRes, true) : []; if (empty($searchData['results'])) { $searchUrl = "https://api.themoviedb.org/3/search/movie?api_key={$apiKey}&query=" . urlencode($cleanTitle) . "&language=fr-FR"; $searchRes = httpGet($searchUrl, 5); $searchData = $searchRes ? json_decode($searchRes, true) : []; } if (empty($searchData['results'])) return null; $movieId = $searchData['results'][0]['id']; $detailsUrl = "https://api.themoviedb.org/3/movie/{$movieId}?api_key={$apiKey}&append_to_response=credits,watch/providers,translations&language=fr-FR"; $detailsRes = httpGet($detailsUrl, 5); if (!$detailsRes) return null; $details = json_decode($detailsRes, true); $frenchTitle = $details['title'] ?? ''; if (!empty($details['translations']['translations'])) { foreach ($details['translations']['translations'] as $translation) { if ($translation['iso_3166_1'] === 'FR' && !empty($translation['data']['title'])) { $frenchTitle = $translation['data']['title']; break; } } } $director = ''; if (!empty($details['credits']['crew'])) { $directorsList = []; foreach ($details['credits']['crew'] as $crew) { if ($crew['job'] === 'Director') $directorsList[] = $crew['name']; } $director = implode(', ', $directorsList); } $cast = []; if (!empty($details['credits']['cast'])) { $topCast = array_slice($details['credits']['cast'], 0, 4); foreach ($topCast as $actor) $cast[] = $actor['name']; } $overview = $details['overview'] ?? ''; $streaming = ''; $frProviders = $details['watch/providers']['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)); $result = [ 'title' => $frenchTitle, 'year' => !empty($details['release_date']) ? substr($details['release_date'], 0, 4) : '', 'director' => $director, 'poster' => !empty($details['poster_path']) ? "https://image.tmdb.org/t/p/w500" . $details['poster_path'] : '', 'length' => !empty($details['runtime']) ? $details['runtime'] . ' min' : '', 'streaming' => $streaming, 'overview' => $overview, 'cast' => $cast ]; // 🔥 SUPPRESSION DE L'APPEL À setCache() QUI N'EXISTE PLUS return $result; } // ── ROUTEUR PRINCIPAL ── $action = $_GET['action'] ?? ''; $data = json_decode(file_get_contents('php://input'), true) ?? []; switch ($action) { case 'check_security_status': echo json_encode(["is_blank" => ($pdo->query("SELECT COUNT(*) FROM users")->fetchColumn() == 0)]); break; case 'login': if ($pdo->query("SELECT COUNT(*) FROM users")->fetchColumn() == 0) { echo json_encode(["success" => true, "token" => md5(ENCRYPTION_KEY . 'session'), "blank" => true]); } else { $stmt = $pdo->prepare("SELECT password_hash FROM users WHERE username = 'admin'"); $stmt->execute(); $user = $stmt->fetch(); if ($user && password_verify($data['password'] ?? '', $user['password_hash'])) { echo json_encode(["success" => true, "token" => md5(ENCRYPTION_KEY . 'session'), "blank" => false]); } else { http_response_code(401); echo json_encode(["error" => "Mot de passe incorrect."]); } } break; case 'setup_admin': case 'update_password': checkAuth($pdo); $pwd = $data['password'] ?? $data['new_password'] ?? ''; $stmt = $pdo->prepare("REPLACE INTO users (id, username, password_hash) VALUES (1, 'admin', :pass)"); $stmt->execute([':pass' => password_hash($pwd, PASSWORD_BCRYPT)]); echo json_encode(["success" => true]); break; case 'get_config_keys': checkAuth($pdo); $stmt = $pdo->prepare("SELECT key_value FROM config WHERE key_name = 'tmdb_api_key'"); $stmt->execute(); $row = $stmt->fetch(); echo json_encode(['tmdb_api_key' => $row ? '••••••••' : '']); break; case 'save_config': checkAuth($pdo); $keyName = $data['key_name'] ?? ''; $keyValue = $data['key_value'] ?? ''; if ($keyName === 'tmdb_api_key' && !empty($keyValue)) { $stmt = $pdo->prepare("REPLACE INTO config (key_name, key_value) VALUES (?, ?)"); $stmt->execute([$keyName, encryptData($keyValue)]); echo json_encode(["success" => true]); } else { http_response_code(400); echo json_encode(["error" => "Données invalides."]); } break; case 'get_films': $sql = " SELECT id, title, year, director, poster, rating, review, NULL AS description, streaming, 'critique' AS type FROM critiques UNION ALL SELECT id, title, year, director, poster, NULL AS rating, NULL AS review, description, NULL AS streaming, 'videotheque' AS type FROM videotheque ORDER BY id DESC "; $result = $pdo->query($sql)->fetchAll(); echo json_encode($result); break; case 'search_ean_full': $ean = $_GET['ean'] ?? ''; if (!$ean) { echo json_encode(['error' => 'EAN manquant']); exit; } $result = [ 'ean' => $ean, 'title' => '', 'director' => '', 'year' => '', 'poster' => '', 'publisher' => '', 'format' => '', 'length' => '', 'number_of_discs' => 1, 'aspect_ratio' => '', 'actors' => '' ]; $dvdfrData = fetchDVDFr($ean, $pdo); if (!empty($dvdfrData)) { if (!empty($dvdfrData['poster'])) $result['poster'] = $dvdfrData['poster']; if (!empty($dvdfrData['publisher'])) $result['publisher'] = $dvdfrData['publisher']; if (!empty($dvdfrData['format'])) $result['format'] = $dvdfrData['format']; if (!empty($dvdfrData['length'])) $result['length'] = $dvdfrData['length']; if (!empty($dvdfrData['aspect'])) $result['aspect_ratio'] = $dvdfrData['aspect']; if (!empty($dvdfrData['discs'])) $result['number_of_discs'] = (int)$dvdfrData['discs']; } $tmdbKey = getTmdbApiKey($pdo); if ($tmdbKey && !empty($result['publisher'])) { $tmdbData = fetchTMDBFull($result['publisher'], '', $tmdbKey, $pdo); if ($tmdbData) { if (!empty($tmdbData['title'])) $result['title'] = $tmdbData['title']; if (!empty($tmdbData['year'])) $result['year'] = $tmdbData['year']; if (!empty($tmdbData['director'])) $result['director'] = $tmdbData['director']; if (!empty($tmdbData['cast'])) $result['actors'] = implode(', ', $tmdbData['cast']); } } echo json_encode(['success' => true, 'data' => $result]); break; case 'save_film': checkAuth($pdo); $type = $data['type'] ?? 'critique'; $id = !empty($data['id']) ? $data['id'] : makeStableId($type, $data['title'] ?? '', $data['year'] ?? '0000'); if (empty($data['director']) || empty($data['poster'])) { $tmdbData = fetchTMDBFull($data['title'] ?? '', $data['year'] ?? '', getTmdbApiKey($pdo), $pdo); if ($tmdbData) { if (empty($data['director'])) $data['director'] = $tmdbData['director']; if (empty($data['poster'])) $data['poster'] = $tmdbData['poster']; if (empty($data['length']) && !empty($tmdbData['length'])) $data['length'] = $tmdbData['length']; } } if ($type === 'critique') { $streaming = $data['streaming'] ?? ''; if (empty($streaming)) $streaming = 'Support physique / Cinéma'; $sql = "INSERT INTO critiques (id, title, year, director, poster, rating, review, streaming) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE title=VALUES(title), year=VALUES(year), director=VALUES(director), poster=VALUES(poster), rating=VALUES(rating), review=VALUES(review), streaming=VALUES(streaming)"; $stmt = $pdo->prepare($sql); $stmt->execute([$id, $data['title'] ?? '', $data['year'] ?? '', $data['director'] ?? '', $data['poster'] ?? '', $data['rating'] ?? 3.0, $data['review'] ?? '', $streaming]); } else { $sql = "INSERT INTO videotheque (id, title, year, director, poster, format, length, publisher, ean_isbn13, number_of_discs, aspect_ratio, description, actors) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE title=VALUES(title), year=VALUES(year), director=IF(VALUES(director)!='', VALUES(director), director), poster=IF(VALUES(poster)!='', VALUES(poster), poster), format=IF(VALUES(format)!='', VALUES(format), format), length=IF(VALUES(length)!='', VALUES(length), length), publisher=IF(VALUES(publisher)!='', VALUES(publisher), publisher), ean_isbn13=IF(VALUES(ean_isbn13)!='', VALUES(ean_isbn13), ean_isbn13), number_of_discs=IF(VALUES(number_of_discs)!=1, VALUES(number_of_discs), number_of_discs), aspect_ratio=IF(VALUES(aspect_ratio)!='', VALUES(aspect_ratio), aspect_ratio), description=IF(VALUES(description)!='', VALUES(description), description), actors=IF(VALUES(actors)!='', VALUES(actors), actors)"; $stmt = $pdo->prepare($sql); $stmt->execute([$id, $data['title'] ?? '', $data['year'] ?? '', $data['director'] ?? '', $data['poster'] ?? '', $data['format'] ?? '', $data['length'] ?? '', $data['publisher'] ?? '', $data['ean_isbn13'] ?? '', $data['number_of_discs'] ?? 1, $data['aspect_ratio'] ?? '', $data['description'] ?? '', $data['actors'] ?? '']); } echo json_encode(["success" => true]); break; case 'delete_film': checkAuth($pdo); $type = $_GET['type'] ?? 'critique'; $table = ($type === 'videotheque') ? 'videotheque' : 'critiques'; $id = $_GET['id'] ?? null; if (!$id) { http_response_code(400); echo json_encode(["error" => "ID manquant."]); break; } $stmt = $pdo->prepare("DELETE FROM $table WHERE id = ?"); $stmt->execute([$id]); echo json_encode(["success" => true]); break; case 'bulk_delete': checkAuth($pdo); $ids = $data['ids'] ?? []; $type = $data['type'] ?? 'critique'; $table = ($type === 'videotheque') ? 'videotheque' : 'critiques'; if (!empty($ids)) { $placeholders = implode(',', array_fill(0, count($ids), '?')); $stmt = $pdo->prepare("DELETE FROM $table WHERE id IN ($placeholders)"); $stmt->execute($ids); echo json_encode(["success" => true]); } else { http_response_code(400); echo json_encode(["success" => false, "error" => "Aucun élément sélectionné."]); } break; case 'import_batch': checkAuth($pdo); set_time_limit(0); $items = $data['items'] ?? []; $type = $data['type'] ?? 'videotheque'; $tmdbApiKey = getTmdbApiKey($pdo); $imported = 0; $debugLog = []; $pdo->beginTransaction(); try { foreach ($items as $rowData) { $title = $rowData['title'] ?? $rowData['Name'] ?? $rowData['Title'] ?? 'Sans titre'; $publishDate = $rowData['publish_date'] ?? $rowData['Year'] ?? $rowData['year'] ?? $rowData['Date'] ?? ''; $year = extractYear($publishDate); $id = makeStableId($type, $title, $year); if ($type === 'videotheque') { $csvActors = $rowData['ensemble'] ?? $rowData['creators'] ?? ''; $actors = ''; if (!empty($csvActors)) { $actorsArray = array_map('trim', explode(',', $csvActors)); $actors = implode(', ', array_slice($actorsArray, 0, 4)); } $ean = $rowData['ean_isbn13'] ?? $rowData['EAN'] ?? ''; if (!empty($ean)) { $eanFloat = floatval($ean); if ($eanFloat > 0) $ean = (string) round($eanFloat); $ean = preg_replace('/[^0-9]/', '', $ean); } $lengthRaw = $rowData['length'] ?? ''; $length = ''; if ($lengthRaw !== '' && $lengthRaw !== null) { $lengthVal = floatval($lengthRaw); if ($lengthVal > 0) $length = (string) round($lengthVal); } $discsRaw = $rowData['number_of_discs'] ?? ''; $discs = (is_numeric($discsRaw) && floatval($discsRaw) > 0) ? (int) round(floatval($discsRaw)) : 1; $description = $rowData['description'] ?? $rowData['Description'] ?? ''; $publisher = $rowData['publisher'] ?? ''; $aspect = $rowData['aspect_ratio'] ?? ''; $format = $rowData['format'] ?? detectFormat($title, $description); $poster = $rowData['poster'] ?? ''; $director = ''; if (!empty($ean)) { $dvdfrData = fetchDVDFr($ean, $pdo); if (!empty($dvdfrData)) { if (!empty($dvdfrData['poster'])) $poster = $dvdfrData['poster']; if (!empty($dvdfrData['publisher'])) $publisher = $dvdfrData['publisher']; if (!empty($dvdfrData['format'])) $format = $dvdfrData['format']; if (!empty($dvdfrData['length']) && empty($length)) $length = $dvdfrData['length']; if (!empty($dvdfrData['aspect']) && empty($aspect)) $aspect = $dvdfrData['aspect']; if (!empty($dvdfrData['discs']) && $discs === 1) $discs = (int)$dvdfrData['discs']; } } if ($tmdbApiKey && !empty($title)) { $tmdbTitle = $title; $tmdbTitle = preg_replace('/\s*[\[\(].*?[\]\)]\s*/', '', $tmdbTitle); $tmdbTitle = preg_replace('/\s*-\s*(Édition|Edition|Collector|Simple|Spéciale|Digibook|Ultimate|Intégrale|Combo|SteelBook|Boîtier|Coffret).*$/i', '', $tmdbTitle); $tmdbTitle = preg_replace('/\s*(Blu-ray|Bluray|DVD|4K|Ultra HD|Combo|VHS|BDRip|\[.*\]).*$/i', '', $tmdbTitle); $tmdbTitle = preg_replace('/\s*(Coffret|Trilogie|Quadrilogie|Collection|Anthologie).*$/i', '', $tmdbTitle); $tmdbTitle = preg_split('/\s*(\/|\+|:)\s*/', $tmdbTitle)[0]; $tmdbTitle = explode(' - ', $tmdbTitle)[0]; $tmdbTitle = trim($tmdbTitle); $tmdbData = fetchTMDBFull($tmdbTitle, $year, $tmdbApiKey, $pdo); if (!$tmdbData && $tmdbTitle !== $title) { $tmdbData = fetchTMDBFull($title, $year, $tmdbApiKey, $pdo); } if ($tmdbData) { if (!empty($tmdbData['title'])) $title = $tmdbData['title']; if (empty($director)) $director = $tmdbData['director'] ?? ''; if (empty($year) && !empty($tmdbData['year'])) $year = $tmdbData['year']; if (empty($length) && !empty($tmdbData['length'])) $length = $tmdbData['length']; if (!empty($tmdbData['overview'])) $description = $tmdbData['overview']; if (!empty($tmdbData['cast'])) $actors = implode(', ', $tmdbData['cast']); } } $sql = "INSERT INTO videotheque (id, title, year, director, poster, format, length, publisher, ean_isbn13, number_of_discs, aspect_ratio, description, actors) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE title=VALUES(title), year=VALUES(year), director=IF(VALUES(director)!='',VALUES(director),director), poster=IF(VALUES(poster)!='',VALUES(poster),poster), format=IF(VALUES(format)!='',VALUES(format),format), length=IF(VALUES(length)!='',VALUES(length),length), publisher=IF(VALUES(publisher)!='',VALUES(publisher),publisher), ean_isbn13=IF(VALUES(ean_isbn13)!='',VALUES(ean_isbn13),ean_isbn13), number_of_discs=IF(VALUES(number_of_discs)!=1,VALUES(number_of_discs),number_of_discs), aspect_ratio=IF(VALUES(aspect_ratio)!='',VALUES(aspect_ratio),aspect_ratio), description=IF(VALUES(description)!='',VALUES(description),description), actors=IF(VALUES(actors)!='',VALUES(actors),actors)"; $stmt = $pdo->prepare($sql); $stmt->execute([$id, $title, $year, $director, $poster, $format, $length, $publisher, $ean, $discs, $aspect, $description, $actors]); } else { $ratingRaw = $rowData['Rating'] ?? $rowData['rating'] ?? ''; $rating = ($ratingRaw !== '' && $ratingRaw !== null) ? (float)$ratingRaw : null; $review = $rowData['Review'] ?? $rowData['review'] ?? ''; $director = ''; $poster = ''; $streaming = ''; if ($tmdbApiKey && !empty($title)) { $tmdbData = fetchTMDBFull($title, $year, $tmdbApiKey, $pdo); if ($tmdbData) { $director = $tmdbData['director']; $poster = $tmdbData['poster']; $streaming = $tmdbData['streaming']; if (empty($year)) $year = $tmdbData['year']; if (!empty($tmdbData['title'])) $title = $tmdbData['title']; } } if (empty($streaming)) $streaming = 'Support physique / Cinéma'; $sql = "INSERT INTO critiques (id, title, year, director, poster, rating, review, streaming) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE title=VALUES(title), year=VALUES(year), rating=VALUES(rating), review=IF(VALUES(review)!='',VALUES(review),review), director=IF(VALUES(director)!='',VALUES(director),director), poster=IF(VALUES(poster)!='',VALUES(poster),poster), streaming=IF(VALUES(streaming)!='',VALUES(streaming),streaming)"; $stmt = $pdo->prepare($sql); $stmt->execute([$id, $title, $year, $director, $poster, $rating, $review, $streaming]); } $imported++; } $pdo->commit(); echo json_encode(["success" => true, "imported" => $imported, "debug" => $debugLog]); } catch (\Throwable $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } http_response_code(500); echo json_encode(["success" => false, "error" => "Erreur serveur : " . $e->getMessage(), "debug" => $debugLog]); } break; }