Files
mon-petit-cinema/api.php
T
2026-06-21 14:56:13 +02:00

437 lines
24 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
header("Content-Type: application/json; charset=UTF-8");
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 TMDB
try {
$pdo = new PDO("mysql:host=localhost;dbname=mon_cinema;charset=utf8mb4", "root", "", [
PDO::ATTR_ERRMODE => 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))");
$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 TMDB (accélère massivement les imports répétés)
$pdo->exec("CREATE TABLE IF NOT EXISTS cache_tmdb (
cache_key VARCHAR(100) PRIMARY KEY,
data TEXT NOT NULL,
created_at INT NOT NULL
)");
} catch (\PDOException $e) { echo json_encode(["error" => "Erreur BDD : " . $e->getMessage()]); exit; }
function makeStableId($title, $year) {
$key = strtolower(trim($title ?? '')) . '|' . trim($year ?? '');
return (abs(crc32($key)) % 2000000000) + 100000000;
}
function checkAuth($pdo) {
$stmtCheck = $pdo->query("SELECT COUNT(*) FROM users");
if ($stmtCheck->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);
$encrypted = openssl_encrypt($data, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
return base64_encode($encrypted . '::' . $iv);
}
function decryptData($encryptedStr) {
$decoded = base64_decode($encryptedStr);
if (strpos($decoded, '::') !== false) { list($encData, $iv) = explode('::', $decoded, 2); } else { return null; }
$key = hash('sha256', ENCRYPTION_KEY, true);
$iv = substr($iv, 0, 16);
return openssl_decrypt($encData, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
}
function getTmdbApiKey($pdo) {
$stmt = $pdo->prepare("SELECT key_value FROM config WHERE key_name = 'tmdb_api_key'");
$stmt->execute();
$row = $stmt->fetch();
if (!$row) return null;
return decryptData($row['key_value']);
}
// ── Récupération d'image via EAN (Open Library) ──
function fetchImageByEAN($ean) {
if (empty($ean) || strlen($ean) < 10) return null;
$url = "https://openlibrary.org/api/books?bibkeys=ISBN:{$ean}&jscmd=data&format=json";
$res = httpGet($url, 5);
if (!$res) return null;
$data = json_decode($res, true);
$key = "ISBN:{$ean}";
if (isset($data[$key])) {
if (!empty($data[$key]['cover']['large'])) return $data[$key]['cover']['large'];
if (!empty($data[$key]['cover']['medium'])) return $data[$key]['cover']['medium'];
}
return null;
}
// ── HTTP unifié (cURL ou file_get_contents) ──
function httpGet($url, $timeout = 6) {
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);
$res = curl_exec($ch);
curl_close($ch);
return $res ?: null;
}
$ctx = stream_context_create(['http' => ['timeout' => $timeout, 'user_agent' => 'MonCinema/2.0']]);
return @file_get_contents($url, false, $ctx);
}
// ── 🚀 Récupération TMDB avec CACHE + curl_multi (parallèle) ──
function fetchTmdbData($title, $year, $apiKey, $pdo = null) {
if (empty($apiKey) || empty($title)) return null;
$cleanTitle = preg_replace('/\s*\[.*?\]\s*/', '', $title);
$cleanTitle = trim($cleanTitle);
$cacheKey = md5(strtolower($cleanTitle) . '|' . $year);
// 1. Vérifier le cache BDD (évite 90% des appels après le 1er import)
if ($pdo) {
try {
$stmt = $pdo->prepare("SELECT data, created_at FROM cache_tmdb WHERE cache_key = ?");
$stmt->execute([$cacheKey]);
$row = $stmt->fetch();
if ($row && (time() - $row['created_at']) < TMDB_CACHE_TTL) {
return json_decode($row['data'], true);
}
} catch (\Exception $e) { /* ignore */ }
}
// 2. Préparation des 3 URLs TMDB
$searchUrl = "https://api.themoviedb.org/3/search/movie?api_key={$apiKey}&query=" . urlencode($cleanTitle) . "&year={$year}&language=fr-FR";
// 3. Exécution parallèle avec curl_multi (×3 plus rapide que séquentiel)
$results = [];
if (function_exists('curl_multi_init')) {
$mh = curl_multi_init();
$handles = [];
$ch1 = curl_init($searchUrl);
curl_setopt_array($ch1, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 6, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_USERAGENT => 'MonCinema/2.0']);
curl_multi_add_handle($mh, $ch1);
$handles['search'] = $ch1;
// On lance d'abord la recherche, puis on ajoutera les autres handles selon le résultat
$running = 0;
do { curl_multi_exec($mh, $running); curl_multi_select($mh); } while ($running > 0);
$searchRes = curl_multi_getcontent($ch1);
curl_multi_remove_handle($mh, $ch1);
curl_close($ch1);
if ($searchRes) {
$searchData = json_decode($searchRes, true);
if (!empty($searchData['results'])) {
$movie = $searchData['results'][0];
$movieId = $movie['id'];
$poster = !empty($movie['poster_path']) ? "https://image.tmdb.org/t/p/w500" . $movie['poster_path'] : '';
// Lancer credits + providers en parallèle
$creditsUrl = "https://api.themoviedb.org/3/movie/{$movieId}/credits?api_key={$apiKey}&language=fr-FR";
$watchUrl = "https://api.themoviedb.org/3/movie/{$movieId}/watch/providers?api_key={$apiKey}";
$ch2 = curl_init($creditsUrl);
$ch3 = curl_init($watchUrl);
curl_setopt_array($ch2, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 6, CURLOPT_SSL_VERIFYPEER => false]);
curl_setopt_array($ch3, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 6, CURLOPT_SSL_VERIFYPEER => false]);
curl_multi_add_handle($mh, $ch2);
curl_multi_add_handle($mh, $ch3);
$running = 0;
do { curl_multi_exec($mh, $running); curl_multi_select($mh); } while ($running > 0);
$creditsRes = curl_multi_getcontent($ch2);
$watchRes = curl_multi_getcontent($ch3);
curl_multi_remove_handle($mh, $ch2); curl_close($ch2);
curl_multi_remove_handle($mh, $ch3); curl_close($ch3);
curl_multi_close($mh);
// Parsing
$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 ($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));
}
$results = ['director' => $director, 'poster' => $poster, 'streaming' => $streaming];
}
}
} else {
// Fallback sans curl_multi (hébergement mutualisé basique)
$searchRes = httpGet($searchUrl, 6);
if ($searchRes) {
$searchData = json_decode($searchRes, true);
if (!empty($searchData['results'])) {
$movie = $searchData['results'][0];
$movieId = $movie['id'];
$poster = !empty($movie['poster_path']) ? "https://image.tmdb.org/t/p/w500" . $movie['poster_path'] : '';
$creditsRes = httpGet("https://api.themoviedb.org/3/movie/{$movieId}/credits?api_key={$apiKey}&language=fr-FR", 6);
$watchRes = httpGet("https://api.themoviedb.org/3/movie/{$movieId}/watch/providers?api_key={$apiKey}", 6);
$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 ($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));
}
$results = ['director' => $director, 'poster' => $poster, 'streaming' => $streaming];
}
}
}
// 4. Sauvegarder dans le cache BDD
if (!empty($results) && $pdo) {
try {
$stmt = $pdo->prepare("REPLACE INTO cache_tmdb (cache_key, data, created_at) VALUES (?, ?, ?)");
$stmt->execute([$cacheKey, json_encode($results), time()]);
} catch (\Exception $e) { /* ignore */ }
}
return !empty($results) ? $results : null;
}
function detectFormat($title) {
$t = strtoupper($title);
if (strpos($t, '4K') !== false || strpos($t, 'UHD') !== false) return 'Blu-ray 4K';
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 'DVD';
}
function extractYear($dateStr) {
if (preg_match('/(\d{4})/', $dateStr, $matches)) return $matches[1];
return '';
}
// ── ROUTEUR PRINCIPAL ──
$action = $_GET['action'] ?? '';
$data = json_decode(file_get_contents('php://input'), true) ?? [];
switch ($action) {
case 'check_security_status':
$stmt = $pdo->query("SELECT COUNT(*) FROM users");
echo json_encode(["is_blank" => ($stmt->fetchColumn() == 0)]);
break;
case 'login':
$stmt = $pdo->query("SELECT COUNT(*) FROM users");
if ($stmt->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 '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':
// 🚀 Cache HTTP 5 min pour éviter de recharger à chaque visite
$lastModified = filemtime(__FILE__);
$ifModified = $_SERVER['HTTP_IF_MODIFIED_SINCE'] ?? null;
if ($ifModified && strtotime($ifModified) >= $lastModified - 300) {
http_response_code(304);
exit;
}
header("Last-Modified: " . gmdate("D, d M Y H:i:s", $lastModified) . " GMT");
header("Cache-Control: public, max-age=300");
$crit = $pdo->query("SELECT *, 'critique' AS type FROM critiques ORDER BY id DESC")->fetchAll();
$video = $pdo->query("SELECT *, 'videotheque' AS type FROM videotheque ORDER BY id DESC")->fetchAll();
echo json_encode(array_merge($crit, $video));
break;
case 'save_film':
checkAuth($pdo);
$type = $data['type'] ?? 'critique';
$id = !empty($data['id']) ? $data['id'] : makeStableId($data['title'] ?? '', $data['year'] ?? '0000');
// 🚀 Skip TMDB si toutes les données sont déjà présentes
$needTmdb = empty($data['director']) || empty($data['poster']) || (empty($data['streaming']) && $type === 'critique');
if ($needTmdb) {
$tmdbData = fetchTmdbData($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['streaming']) && $type === 'critique') {
$data['streaming'] = !empty($tmdbData['streaming']) ? $tmdbData['streaming'] : 'Disponible en support physique ou Cinéma';
}
} elseif (empty($data['streaming']) && $type === 'critique') {
$data['streaming'] = 'Disponible en support physique ou Cinéma';
}
}
if ($type === 'critique') {
$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'] ?? '', $data['streaming'] ?? '']);
} else {
$sql = "INSERT INTO videotheque (id, title, year, director, poster, format, length, publisher, ean_isbn13, number_of_discs, aspect_ratio, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE title=VALUES(title), year=VALUES(year), director=VALUES(director), poster=VALUES(poster), format=VALUES(format), length=VALUES(length), publisher=VALUES(publisher), ean_isbn13=VALUES(ean_isbn13), number_of_discs=VALUES(number_of_discs), aspect_ratio=VALUES(aspect_ratio), description=VALUES(description)";
$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'] ?? '']);
}
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;
// ── IMPORT PAR LOTS OPTIMISÉ ──
case 'import_batch':
checkAuth($pdo);
$items = $data['items'] ?? [];
$type = $data['type'] ?? 'videotheque';
$tmdbApiKey = getTmdbApiKey($pdo);
$imported = 0;
$stats = ['tmdb_calls' => 0, 'cache_hits' => 0, 'ean_hits' => 0];
// 🚀 Transaction SQL pour accélérer les INSERT (×5 plus rapide)
$pdo->beginTransaction();
foreach ($items as $rowData) {
$title = $rowData['title'] ?? $rowData['Name'] ?? 'Sans titre';
$firstName = $rowData['first_name'] ?? '';
$lastName = $rowData['last_name'] ?? '';
$director = (!empty($firstName) && !empty($lastName)) ? trim("$firstName $lastName") : ($rowData['creators'] ?? $rowData['Director'] ?? '');
$publishDate = $rowData['publish_date'] ?? $rowData['Year'] ?? '';
$year = extractYear($publishDate);
$ean = $rowData['ean_isbn13'] ?? $rowData['EAN'] ?? '';
$description = $rowData['description'] ?? $rowData['Description'] ?? $rowData['Review'] ?? '';
$publisher = $rowData['publisher'] ?? $rowData['Publisher'] ?? '';
$length = $rowData['length'] ?? $rowData['Length'] ?? '';
$discs = $rowData['number_of_discs'] ?? 1;
$aspect = $rowData['aspect_ratio'] ?? '';
$format = $rowData['format'] ?? $rowData['Format'] ?? detectFormat($title);
// 🚀 Récupération image : priorité EAN (jaquette physique)
$poster = $rowData['poster'] ?? $rowData['Poster'] ?? '';
if (empty($poster) && !empty($ean)) {
$poster = fetchImageByEAN($ean);
if ($poster) $stats['ean_hits']++;
}
// 🚀 TMDB uniquement si données manquantes (et utilisation du cache)
$needTmdb = empty($poster) || empty($director);
$tmdbData = null;
if ($needTmdb && $tmdbApiKey) {
$tmdbData = fetchTmdbData($title, $year, $tmdbApiKey, $pdo);
$stats['tmdb_calls']++;
if ($tmdbData) {
if (empty($director)) $director = $tmdbData['director'];
if (empty($poster) && !empty($tmdbData['poster'])) $poster = $tmdbData['poster'];
}
}
$id = makeStableId($title, $year);
if ($type === 'critique') {
$rating = isset($rowData['rating']) && $rowData['rating'] !== '' ? (float)$rowData['rating'] : (isset($rowData['Rating']) ? (float)$rowData['Rating'] : 3.0);
$review = $rowData['review'] ?? $rowData['Review'] ?? $description;
$streaming = $rowData['streaming'] ?? $rowData['Streaming'] ?? (!empty($tmdbData['streaming']) ? $tmdbData['streaming'] : 'Disponible en support physique ou Cinéma');
$sql = "INSERT INTO critiques (id, title, year, director, poster, rating, review, streaming) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE 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]);
} else {
$sql = "INSERT INTO videotheque (id, title, year, director, poster, format, length, publisher, ean_isbn13, number_of_discs, aspect_ratio, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE 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)";
$stmt = $pdo->prepare($sql);
$stmt->execute([$id, $title, $year, $director, $poster, $format, $length, $publisher, $ean, $discs, $aspect, $description]);
}
$imported++;
}
$pdo->commit();
echo json_encode(["success" => true, "imported" => $imported, "stats" => $stats]);
break;
}