From dc503e93a726b18a051f19a8f5f3158c2e505fbe Mon Sep 17 00:00:00 2001 From: Cedric Date: Tue, 16 Jun 2026 20:36:19 +0200 Subject: [PATCH] Actualiser js/admin.js --- js/admin.js | 948 +++++++++++----------------------------------------- 1 file changed, 204 insertions(+), 744 deletions(-) diff --git a/js/admin.js b/js/admin.js index 1457015..f49d2dc 100644 --- a/js/admin.js +++ b/js/admin.js @@ -1,518 +1,59 @@ -const ADMIN_PASSWORD = 'cinema2024'; -const STORAGE_KEY = 'mon-cinema-films'; +/** + * ========================================================================= + * Mon Cinéma - Module d'Administration (admin.js) + * Gestion de l'état, de la persistance, des onglets et des imports/exports CSV + * ========================================================================= + */ -let films = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); -let currentRating = 0; -let editingFilmId = null; -let currentTab = 'critique'; - -function persist() { - localStorage.setItem(STORAGE_KEY, JSON.stringify(films)); -} - -if (location.pathname.includes('dashboard.html') && sessionStorage.getItem('admin-auth') !== '1') { - window.location.href = 'login.html'; -} - -function doLogin() { - const val = document.getElementById('login-pwd').value; - if (val === ADMIN_PASSWORD) { - sessionStorage.setItem('admin-auth', '1'); - window.location.href = 'dashboard.html'; - } else { - document.getElementById('login-err').style.display = 'block'; - } -} - -function switchTab(tabName) { - currentTab = tabName; - - document.getElementById('tab-critiques').classList.toggle('active', tabName === 'critique'); - document.getElementById('tab-videotheque').classList.toggle('active', tabName === 'videotheque'); - - const thDynamic = document.getElementById('th-dynamic-col'); - if (thDynamic) { - thDynamic.textContent = tabName === 'critique' ? 'Note' : 'Format / Éditeur'; - } - - const btnCritiqueExport = document.getElementById('actions-critiques-export'); - const btnCritiqueImport = document.getElementById('actions-critiques-import'); - const btnVideoExport = document.getElementById('actions-video-export'); - const btnVideoImport = document.getElementById('actions-video-import'); - - if (tabName === 'videotheque') { - if (btnCritiqueExport) btnCritiqueExport.style.display = 'none'; - if (btnCritiqueImport) btnCritiqueImport.style.display = 'none'; - if (btnVideoExport) btnVideoExport.style.display = 'inline-flex'; - if (btnVideoImport) btnVideoImport.style.display = 'inline-flex'; - } else { - if (btnCritiqueExport) btnCritiqueExport.style.display = 'inline-flex'; - if (btnCritiqueImport) btnCritiqueImport.style.display = 'inline-flex'; - if (btnVideoExport) btnVideoExport.style.display = 'none'; - if (btnVideoImport) btnVideoImport.style.display = 'none'; - } - - renderAdminTable(); -} - -function handleTypeChange(typeValue) { - const videoFields = document.getElementById('form-group-videotheque-fields'); - const critiqueFields = document.getElementById('form-group-critique-fields'); - - if (typeValue === 'videotheque') { - if (videoFields) videoFields.style.display = 'block'; - if (critiqueFields) critiqueFields.style.display = 'none'; - } else { - if (videoFields) videoFields.style.display = 'none'; - if (critiqueFields) critiqueFields.style.display = 'block'; - } -} - -function setRating(n) { - currentRating = n; - const buttons = document.querySelectorAll('#star-select button'); - buttons.forEach((btn, idx) => { - btn.style.color = idx < n ? 'var(--gold)' : 'var(--muted)'; - }); -} - -function resetStars() { - currentRating = 0; - const buttons = document.querySelectorAll('#star-select button'); - buttons.forEach(btn => btn.style.color = 'var(--muted)'); -} - -function saveTmdbKey() { - const key = document.getElementById('tmdb-key-input').value.trim(); - localStorage.setItem('tmdb-api-key', key); - alert('Clé TMDB enregistrée avec succès !'); -} - -async function searchTMDB() { - const query = document.getElementById('search-input').value.trim(); - const key = localStorage.getItem('tmdb-api-key') || document.getElementById('tmdb-key-input').value.trim(); - const resultsWrap = document.getElementById('search-results-wrap'); - - if (!key) { - alert("Veuillez configurer votre clé API TMDB d'abord."); - return; - } - if (!query) return; - - resultsWrap.innerHTML = '
Recherche en cours...
'; - - try { - const res = await fetch(`https://api.themoviedb.org/3/search/movie?api_key=${key}&query=${encodeURIComponent(query)}&language=fr-FR`); - const data = await res.json(); - - if (!data.results || data.results.length === 0) { - resultsWrap.innerHTML = '
Aucun résultat trouvé.
'; - return; - } - - resultsWrap.innerHTML = ` -
- ${data.results.slice(0, 5).map(movie => { - const posterUrl = movie.poster_path ? `https://image.tmdb.org/t/p/w300${movie.poster_path}` : ''; - const year = movie.release_date ? movie.release_date.substring(0, 4) : ''; - const safeTitle = movie.title.replace(/'/g, "\\'").replace(/"/g, """); - return ` -
- ${posterUrl ? `` : `
`} - ${movie.title} (${year}) -
- `; - }).join('')} -
- `; - } catch (err) { - resultsWrap.innerHTML = '
Erreur lors de la recherche.
'; - } -} - -function selectTmdbMovie(title, year, poster) { - document.getElementById('f-title').value = title; - document.getElementById('f-year').value = year; - if (poster) { - document.getElementById('f-poster').value = poster; - updatePosterPreview(poster); - } - document.getElementById('search-results-wrap').innerHTML = ''; -} - -function updatePosterPreview(url) { - const img = document.getElementById('poster-preview-img'); - const none = document.getElementById('poster-preview-none'); - const info = document.getElementById('poster-preview-info'); - - if (url) { - img.src = url; - img.style.display = 'block'; - none.style.display = 'none'; - info.innerHTML = "Affiche sélectionnée"; - } else { - img.style.display = 'none'; - none.style.display = 'flex'; - info.innerHTML = "Aucune affiche sélectionnée.
Faites une recherche ou collez une URL."; - } -} - -function openAddModal() { - editingFilmId = null; - document.getElementById('modal-title').textContent = "Nouvelle entrée"; - - document.getElementById('f-type').value = currentTab; - handleTypeChange(currentTab); - - resetStars(); - document.getElementById('f-title').value = ''; - document.getElementById('f-year').value = ''; - document.getElementById('f-director').value = ''; - document.getElementById('f-poster').value = ''; - document.getElementById('f-review').value = ''; - document.getElementById('f-format').value = 'dvd'; - document.getElementById('f-length').value = ''; - document.getElementById('f-publisher').value = ''; - document.getElementById('f-ean').value = ''; - document.getElementById('f-discs').value = '1'; - document.getElementById('f-aspect').value = ''; - document.getElementById('f-description').value = ''; - document.getElementById('search-input').value = ''; - document.getElementById('search-results-wrap').innerHTML = ''; - updatePosterPreview(''); - - document.getElementById('form-overlay').classList.add('open'); -} - -function openEditModal(id) { - const f = films.find(x => x.id === id); - if (!f) return; - - editingFilmId = id; - document.getElementById('modal-title').textContent = "Modifier l'entrée"; - - const type = f.type || 'critique'; - document.getElementById('f-type').value = type; - handleTypeChange(type); - - document.getElementById('f-title').value = f.title || ''; - document.getElementById('f-year').value = f.year || ''; - document.getElementById('f-director').value = f.director || ''; - document.getElementById('f-poster').value = f.poster || ''; - document.getElementById('f-review').value = f.review || ''; - document.getElementById('f-format').value = f.format || 'dvd'; - document.getElementById('f-length').value = f.length || ''; - document.getElementById('f-publisher').value = f.publisher || ''; - document.getElementById('f-ean').value = f.ean_isbn13 || ''; - document.getElementById('f-discs').value = f.number_of_discs || '1'; - document.getElementById('f-aspect').value = f.aspect_ratio || ''; - document.getElementById('f-description').value = f.description || ''; - - document.getElementById('search-input').value = ''; - document.getElementById('search-results-wrap').innerHTML = ''; - - setRating(f.rating || 0); - updatePosterPreview(f.poster || ''); - - document.getElementById('form-overlay').classList.add('open'); -} - -function closeModal() { - document.getElementById('form-overlay').classList.remove('open'); -} - -function saveFilm() { - const title = document.getElementById('f-title').value.trim(); - if (!title) { - alert("Le titre du film est obligatoire !"); - return; - } - const type = document.getElementById('f-type').value; - - const metadataVideotheque = type === 'videotheque' ? { - format: document.getElementById('f-format').value, - length: document.getElementById('f-length').value.trim(), - publisher: document.getElementById('f-publisher').value.trim(), - ean_isbn13: document.getElementById('f-ean').value.trim(), - number_of_discs: document.getElementById('f-discs').value.trim(), - aspect_ratio: document.getElementById('f-aspect').value.trim(), - description: document.getElementById('f-description').value.trim() - } : { - format: '', length: '', publisher: '', ean_isbn13: '', number_of_discs: '', aspect_ratio: '', description: '' - }; - - if (editingFilmId !== null) { - const index = films.findIndex(f => f.id === editingFilmId); - if (index !== -1) { - films[index] = { - ...films[index], - title: title, - type: type, - year: document.getElementById('f-year').value.trim(), - director: document.getElementById('f-director').value.trim(), - poster: document.getElementById('f-poster').value.trim(), - rating: type === 'critique' ? (currentRating || 1) : 0, - review: type === 'critique' ? document.getElementById('f-review').value.trim() : '', - streaming: films[index].streaming || "Disponible au cinéma ou support physique", - ...metadataVideotheque - }; - } - } else { - const film = { - id: Date.now(), - title: title, - type: type, - year: document.getElementById('f-year').value.trim(), - director: document.getElementById('f-director').value.trim(), - poster: document.getElementById('f-poster').value.trim(), - rating: type === 'critique' ? (currentRating || 1) : 0, - review: type === 'critique' ? document.getElementById('f-review').value.trim() : '', - streaming: "Disponible au cinéma ou support physique", - ...metadataVideotheque - }; - films.unshift(film); - } - persist(); - renderAdminTable(); - closeModal(); -} - -function deleteFilm(id) { - if (confirm("Voulez-vous vraiment supprimer cet élément ?")) { - films = films.filter(f => f.id !== id); - persist(); - renderAdminTable(); - } -} - -function renderAdminTable() { - const tbody = document.getElementById('admin-tbody'); - const emptyState = document.getElementById('admin-empty'); - const emptyMessage = document.getElementById('empty-message'); - - if (!tbody) return; - - const selectAll = document.getElementById('th-select-all'); - if (selectAll) selectAll.checked = false; - const btnBulkReset = document.getElementById('btn-bulk-delete'); - if (btnBulkReset) btnBulkReset.style.display = 'none'; - const countSpanReset = document.getElementById('bulk-select-count'); - if (countSpanReset) countSpanReset.textContent = '0'; - - const filteredFilms = films.filter(f => { - const fType = f.type || 'critique'; - return fType === currentTab; - }); - - if (filteredFilms.length === 0) { - tbody.innerHTML = ''; - if (emptyState) { - emptyMessage.textContent = currentTab === 'critique' - ? "Aucun film pour l'instant. Ajoutez votre première critique !" - : "Votre vidéothèque est vide. Ajoutez vos premiers DVD ou Blu-ray !"; - emptyState.style.display = 'block'; - } - return; - } - - if (emptyState) emptyState.style.display = 'none'; - - const countEl = document.getElementById('films-count'); - if (countEl) countEl.textContent = filteredFilms.length; - - tbody.innerHTML = filteredFilms.map(f => { - const posterHtml = f.poster - ? `` - : '
'; - - let dynamicCellHtml = ''; - if (currentTab === 'critique') { - dynamicCellHtml = `${'★'.repeat(f.rating || 1)}${'☆'.repeat(5 - (f.rating || 1))}`; - } else { - let formatLabel = (f.format || 'DVD').toUpperCase().replace('_4K', ' 4K'); - let publisherLabel = f.publisher ? `
${f.publisher}
` : ''; - dynamicCellHtml = `${formatLabel}${publisherLabel}`; - } - - return ` - - - - - ${posterHtml} - ${f.title} - ${f.year || '—'} - ${f.director || '—'} - ${dynamicCellHtml} - - - - - - `; - }).join(''); -} - -function toggleSelectAll(masterCheckbox) { - const checkboxes = document.querySelectorAll('.row-select'); - checkboxes.forEach(cb => cb.checked = masterCheckbox.checked); - updateBulkDeleteButton(); -} - -function updateBulkDeleteButton() { - const checkedBoxes = document.querySelectorAll('.row-select:checked'); - const btnBulk = document.getElementById('btn-bulk-delete'); - const countSpan = document.getElementById('bulk-select-count'); - - if (btnBulk && countSpan) { - if (checkedBoxes && checkedBoxes.length > 0) { - countSpan.textContent = checkedBoxes.length; - btnBulk.style.display = 'inline-flex'; - } else { - btnBulk.style.display = 'none'; - } - } -} - -function deleteSelectedFilms() { - const checkedBoxes = document.querySelectorAll('.row-select:checked'); - if (!checkedBoxes || checkedBoxes.length === 0) return; - - if (confirm(`Êtes-vous sûr de vouloir supprimer définitivement ces ${checkedBoxes.length} éléments ?`)) { - const idsToDelete = Array.from(checkedBoxes).map(cb => parseInt(cb.getAttribute('data-id'))); - films = films.filter(f => !idsToDelete.includes(f.id)); - persist(); - renderAdminTable(); - } -} - -function parseLetterboxdCSV(text) { +// ── PARSEUR CSV UNIVERSEL (Robuste & Hybride) ── +/** + * Analyse une chaîne de caractères CSV de manière flexible. + * Détecte automatiquement si le séparateur principal est une virgule ou un point-virgule. + * Gère correctement les sauts de ligne internes et le double-échappement des guillemets. + */ +function parseFlexibleCSV(text) { const rows = []; - const lines = text.split(/\r\n|\n/); - for (const line of lines) { - if (!line.trim()) continue; - const cols = []; - let cur = '', inQ = false; - for (let i = 0; i < line.length; i++) { - const c = line[i]; - if (c === '"') { - if (inQ && line[i+1] === '"') { cur += '"'; i++; } - else inQ = !inQ; - } else if (c === ',' && !inQ) { - cols.push(cur); cur = ''; - } else { - cur += c; + let row = [""]; + let inQuotes = false; + + // Détection dynamique du séparateur principal (, ou ;) basé sur la première ligne + const firstLine = text.split(/\r?\n/)[0]; + const semiColonCount = (firstLine.match(/;/g) || []).length; + const commaCount = (firstLine.match(/,/g) || []).length; + const separator = semiColonCount > commaCount ? ';' : ','; + + for (let i = 0; i < text.length; i++) { + let c = text[i]; + let next = text[i + 1]; + + if (c === '"') { + if (inQuotes && next === '"') { + // Gestion des doubles guillemets internes (ex: ""citation"") + row[row.length - 1] += '"'; + i++; + } else { + // Bascule de l'état de lecture entre intérieur/extérieur des guillemets + inQuotes = !inQuotes; } + } else if (c === separator && !inQuotes) { + row.push(''); + } else if ((c === '\r' || c === '\n') && !inQuotes) { + if (c === '\r' && next === '\n') { i++; } + rows.push(row); + row = ['']; + } else { + row[row.length - 1] += c; } - cols.push(cur); - rows.push(cols); } + if (row.length > 1 || row[0] !== '') rows.push(row); return rows; } -function showImportProgress(total) { - let overlay = document.getElementById('import-progress-overlay'); - if (!overlay) { - overlay = document.createElement('div'); - overlay.id = 'import-progress-overlay'; - overlay.style.cssText = ` - position:fixed;inset:0;background:rgba(0,0,0,0.85);z-index:999; - display:flex;align-items:center;justify-content:center; - `; - overlay.innerHTML = ` -
-
- Import -
-
- Initialisation… -
-
-
-
-
0 / ${total}
-
- `; - document.body.appendChild(overlay); - } - return overlay; -} - -function updateImportProgress(done, total, label) { - const bar = document.getElementById('import-progress-bar'); - const lbl = document.getElementById('import-progress-label'); - const cnt = document.getElementById('import-progress-count'); - if (bar) bar.style.width = Math.round((done / total) * 100) + '%'; - if (lbl) lbl.textContent = label; - if (cnt) cnt.textContent = `${done} / ${total}`; -} - -function hideImportProgress() { - const el = document.getElementById('import-progress-overlay'); - if (el) el.remove(); -} - -async function fetchTmdbPoster(title, year) { - const key = localStorage.getItem('tmdb-api-key'); - if (!key) return ''; - try { - const yearParam = year ? `&year=${year}` : ''; - const res = await fetch( - `https://api.themoviedb.org/3/search/movie?api_key=${key}&query=${encodeURIComponent(title)}&language=fr-FR${yearParam}` - ); - const data = await res.json(); - if (data.results && data.results.length > 0 && data.results[0].poster_path) { - return `https://image.tmdb.org/t/p/w500${data.results[0].poster_path}`; - } - if (year) { - const res2 = await fetch( - `https://api.themoviedb.org/3/search/movie?api_key=${key}&query=${encodeURIComponent(title)}&language=fr-FR` - ); - const data2 = await res2.json(); - if (data2.results && data2.results.length > 0 && data2.results[0].poster_path) { - return `https://image.tmdb.org/t/p/w500${data2.results[0].poster_path}`; - } - } - } catch (e) {} - return ''; -} - -async function fetchDvdCover(ean, title, year) { - if (ean) { - try { - const olRes = await fetch( - `https://openlibrary.org/api/books?bibkeys=ISBN:${encodeURIComponent(ean)}&format=json&jscmd=data` - ); - const olData = await olRes.json(); - const key = `ISBN:${ean}`; - if (olData[key]) { - const book = olData[key]; - if (book.cover) { - const coverUrl = book.cover.large || book.cover.medium || book.cover.small || ''; - if (coverUrl) return coverUrl; - } - } - } catch (e) {} - - try { - const testUrl = `https://covers.openlibrary.org/b/isbn/${ean}-L.jpg?default=false`; - const testRes = await fetch(testUrl); - if (testRes.ok && testRes.headers.get('content-type')?.includes('image')) { - return testUrl; - } - } catch (e) {} - } - return await fetchTmdbPoster(title, year); -} - +// ── FONCTION PRINCIPALE D'IMPORTATION CSV ── +/** + * Gère l'importation de fichiers CSV (Format d'export Mon Cinéma, importcinetest.csv ou Letterboxd). + * Déduplique les entrées existantes et récupère les métadonnées TMDB si nécessaire. + */ function importFromCSV(input) { const file = input.files[0]; if (!file) return; @@ -520,7 +61,7 @@ function importFromCSV(input) { const reader = new FileReader(); reader.onload = async function (e) { const text = e.target.result; - const rows = parseLetterboxdCSV(text); + const rows = parseFlexibleCSV(text); if (rows.length <= 1) { alert("Le fichier semble vide ou invalide."); @@ -528,39 +69,79 @@ function importFromCSV(input) { return; } + // Normalisation des entêtes pour la correspondance des colonnes (insensible à la casse) const headers = rows[0].map(h => h.trim().toLowerCase()); - const isReviews = headers.includes('review'); - const iCol = { - name: headers.indexOf('name'), - year: headers.indexOf('year'), - rating: headers.indexOf('rating'), - review: isReviews ? headers.indexOf('review') : -1, - watchedDate: headers.indexOf('watched date'), - }; + + // Mappage adaptatif des indices de colonnes (Français vs Anglais / Letterboxd) + const idxTitle = headers.includes('titre') ? headers.indexOf('titre') : (headers.includes('name') ? headers.indexOf('name') : headers.indexOf('title')); + const idxYear = headers.includes('annee') ? headers.indexOf('annee') : (headers.includes('year') ? headers.indexOf('year') : headers.indexOf('publish_date')); + const idxDirector = headers.includes('realisateur') ? headers.indexOf('realisateur') : headers.indexOf('creators'); + const idxRating = headers.indexOf('note') !== -1 ? headers.indexOf('note') : headers.indexOf('rating'); + const idxReview = headers.indexOf('critique') !== -1 ? headers.indexOf('critique') : headers.indexOf('review'); + const idxPoster = headers.indexOf('url_affiche') !== -1 ? headers.indexOf('url_affiche') : headers.indexOf('url'); + + if (idxTitle === -1) { + alert("Erreur : Impossible de localiser la colonne contenant les titres des films."); + input.value = ''; + return; + } const toImport = []; let duplicateCount = 0; + // Phase 1 : Extraction et nettoyage des données brutes for (let i = 1; i < rows.length; i++) { const cols = rows[i]; - const title = iCol.name >= 0 ? cols[iCol.name]?.trim() : ''; - if (!title) continue; + const titleRaw = cols[idxTitle]?.trim(); + if (!titleRaw) continue; - const year = iCol.year >= 0 ? (cols[iCol.year]?.trim() || '') : ''; - const ratingRaw = iCol.rating >= 0 ? parseFloat(cols[iCol.rating]) : 0; - const rating = Math.round(Math.min(5, Math.max(1, ratingRaw))); - const review = (isReviews && iCol.review >= 0) ? (cols[iCol.review]?.trim() || '') : ''; + // Nettoyage des suffixes d'éditions spéciales souvent présents dans les inventaires de DVD + let title = titleRaw.replace(/\s*\[.*?\]/g, "").replace(/\s*- Édition.*/gi, ""); + // Extraction de l'année (Format AAAAmj ou juste AAAA) + let year = ''; + if (idxYear >= 0 && cols[idxYear]) { + year = cols[idxYear].trim().substring(0, 4); + } + + // Extraction du réalisateur (Prend le premier si liste séparée par des virgules) + let director = ''; + if (idxDirector >= 0 && cols[idxDirector]) { + director = cols[idxDirector].split(',')[0].trim(); + } + + // Extraction et normalisation de la note (Borne entre 1 et 5) + let rating = 0; + if (idxRating >= 0 && cols[idxRating]) { + const ratingRaw = parseFloat(cols[idxRating]); + if (!isNaN(ratingRaw) && ratingRaw > 0) { + rating = Math.round(Math.min(5, Math.max(1, ratingRaw))); + } + } + if (rating === 0) rating = 3; // Note par défaut si non spécifiée + + // Extraction de la critique + let review = idxReview >= 0 && cols[idxReview] ? cols[idxReview].trim() : ''; + + // Extraction de l'URL de l'affiche si elle existe déjà + let filePoster = idxPoster >= 0 && cols[idxPoster] ? cols[idxPoster].trim() : ''; + + // Vérification des doublons (Même titre et même année) const alreadyExists = films.some( - f => f.title.toLowerCase() === title.toLowerCase() && f.year === year + f => f.type === 'critique' && f.title.toLowerCase() === title.toLowerCase() && f.year === year ); - if (alreadyExists) { duplicateCount++; continue; } + + if (alreadyExists) { + duplicateCount++; + continue; + } - toImport.push({ title, year, rating, review }); + toImport.push({ title, year, director, rating, review, filePoster }); } + // Sortie anticipée si aucun élément n'est à importer if (toImport.length === 0) { - alert(`Aucun nouvel élément à importer.\n⚠️ ${duplicateCount} doublon(s) ignoré(s).`); + alert(`Aucun nouveau film à ajouter.\n⚠️ ${duplicateCount} doublon(s) ignoré(s).`); input.value = ''; return; } @@ -569,275 +150,154 @@ function importFromCSV(input) { showImportProgress(toImport.length); let importedCount = 0; - for (let i = 0; i < toImport.length; i++) { - const { title, year, rating, review } = toImport[i]; - updateImportProgress(i, toImport.length, `🎬 ${title}`); - let poster = ''; + // Phase 2 : Traitement séquentiel et enrichissement TMDB + for (let i = 0; i < toImport.length; i++) { + const { title, year, director, rating, review, filePoster } = toImport[i]; + updateImportProgress(i, toImport.length, `🎬 Traitement : ${title}`); + + let poster = filePoster; let streaming = "Disponible au cinéma ou support physique"; - if (hasTmdb) { - // Double appel unifié (Affiche + Streaming) + // Si aucune affiche n'est fournie mais que l'API TMDB est dispo -> On cherche l'affiche + if (!poster && hasTmdb) { const details = await fetchTmdbDetails(title, year); poster = details.poster; streaming = details.streaming; - await new Promise(r => setTimeout(r, 350)); + await new Promise(r => setTimeout(r, 200)); // Temporisation pour l'API Rate-Limit + } else if (hasTmdb && poster) { + // Si l'affiche est fournie mais qu'on veut rafraîchir les plateformes de streaming uniquement + const details = await fetchTmdbDetails(title, year); + streaming = details.streaming; } + // Injection dans la base globale de l'application films.push({ id: Date.now() + Math.floor(Math.random() * 100000) + i, title, type: 'critique', year, - director: '', + director, rating, review, poster, streaming, - format: '', - length: '', publisher: '', ean_isbn13: '', number_of_discs: '1', aspect_ratio: '', description: '' + format: '', length: '', publisher: '', ean_isbn13: '', number_of_discs: '1', aspect_ratio: '', description: '' }); importedCount++; } + // Finalisation de l'affichage de progression updateImportProgress(toImport.length, toImport.length, '✅ Terminé !'); - await new Promise(r => setTimeout(r, 600)); + await new Promise(r => setTimeout(r, 400)); hideImportProgress(); + // Tri de la collection par nouveautés (IDs les plus récents en premier) films.sort((a, b) => b.id - a.id); persist(); - renderAdminTable(); + switchTab('critique'); - const typeLabel = isReviews ? 'reviews.csv (avec critiques)' : 'ratings.csv (notes seules)'; - const posterInfo = hasTmdb ? ' · Affiches et plateformes récupérées via TMDB' : ' · ⚠️ Clé TMDB absente'; alert( - `Import Letterboxd — ${typeLabel}\n` + - `✅ ${importedCount} film(s) importé(s)${posterInfo}\n` + - `⚠️ ${duplicateCount} doublon(s) ignoré(s).` + `Importation réussie avec succès !\n` + + `✅ ${importedCount} critique(s) ajoutée(s) depuis "${file.name}".\n` + + `⚠️ ${duplicateCount} doublon(s) évité(s).` ); input.value = ''; }; + reader.readAsText(file, 'UTF-8'); } +// ── FONCTIONS UTILITAIRES DE PROGRESSION DE L'IMPORT ── +function showImportProgress(total) { + let overlay = document.getElementById('import-overlay'); + if (!overlay) { + overlay = document.createElement('div'); + overlay.id = 'import-overlay'; + overlay.innerHTML = ` +
+

Importation du catalogue CSV

+
+

Préparation...

+
+ `; + document.body.appendChild(overlay); + } + overlay.style.display = 'flex'; +} + +function updateImportProgress(current, total, message) { + const fill = document.getElementById('import-progress-fill'); + const text = document.getElementById('import-progress-text'); + const percent = Math.round((current / total) * 100); + if (fill) fill.style.width = `${percent}%`; + if (text) text.innerText = `${message} (${percent}%)`; +} + +function hideImportProgress() { + const overlay = document.getElementById('import-overlay'); + if (overlay) overlay.style.display = 'none'; +} + +// ── FONCTION D'EXPORTATION DU CATALOGUE (CSV) ── +/** + * Génère et déclenche le téléchargement d'un fichier CSV contenant l'ensemble de la bibliothèque actuelle. + */ function exportToCSV() { - if (films.length === 0) { alert("Aucune donnée à exporter."); return; } - let csvContent = "ID;Titre;Annee;Realisateur;Note;Critique;URL_Affiche;Type;Format\r\n"; - films.filter(f => (f.type || 'critique') === 'critique').forEach(f => { - const cleanTitle = (f.title || '').replace(/"/g, '""'); - const cleanDirector = (f.director || '').replace(/"/g, '""'); - const cleanReview = (f.review || '').replace(/\r?\n|\r/g, ' ').replace(/"/g, '""'); - const cleanPoster = (f.poster || '').replace(/"/g, '""'); - csvContent += `${f.id};"${cleanTitle}";${f.year || ''};"${cleanDirector}";${f.rating || 0};"${cleanReview}";"${cleanPoster}";"critique";""\r\n`; - }); - const blob = new Blob([new Uint8Array([0xEF, 0xBB, 0xBF]), csvContent], { type: 'text/csv;charset=utf-8;' }); - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.setAttribute("href", url); - link.setAttribute("download", `export_critiques_${Date.now()}.csv`); - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); -} - -function importVideothequeFromInput(input) { - const file = input.files[0]; - if (!file) return; - - const reader = new FileReader(); - reader.onload = async function(e) { - await importVideothequeCSV(e.target.result); - input.value = ''; - }; - reader.readAsText(file, 'UTF-8'); -} - -async function importVideothequeCSV(text) { - const lines = []; - let row = [""]; - let inQuotes = false; - for (let i = 0; i < text.length; i++) { - let c = text[i], next = text[i+1]; - if (c === '"') { - if (inQuotes && next === '"') { row[row.length - 1] += '"'; i++; } - else { inQuotes = !inQuotes; } - } else if (c === ',' && !inQuotes) { - row.push(''); - } else if ((c === '\r' || c === '\n') && !inQuotes) { - if (c === '\r' && next === '\n') { i++; } - lines.push(row); row = ['']; - } else { - row[row.length - 1] += c; - } - } - if (row.length > 1 || row[0] !== '') lines.push(row); - if (lines.length <= 1) return; - - const toImport = []; - let duplicateCount = 0; - - for (let i = 1; i < lines.length; i++) { - const data = lines[i]; - if (data.length < 2 || !data[1]) continue; - - const title = data[1].trim(); - const creators = data[2] ? data[2].trim() : ''; - const ean = data[6] ? data[6].trim() : ''; - const description = data[8] ? data[8].trim() : ''; - const publisher = data[9] ? data[9].trim() : ''; - const publishDate = data[10] ? data[10].trim() : ''; - const length = data[15] ? data[15].trim() : ''; - const discs = data[16] ? data[16].trim() : '1'; - const aspect = data[20] ? data[20].trim() : ''; - - let year = publishDate ? publishDate.split('-')[0] : ''; - - const lowerTitle = title.toLowerCase(); - const lowerDesc = description.toLowerCase(); - let format = 'dvd'; - if (lowerTitle.includes('blu-ray') || lowerTitle.includes('bluray') || - lowerDesc.includes('blu-ray') || lowerTitle.includes('4k')) { - format = (lowerTitle.includes('4k') || lowerDesc.includes('4k')) ? 'bluray_4k' : 'bluray'; - } - - const duplicate = films.some( - f => f.type === 'videotheque' && - f.title.toLowerCase() === title.toLowerCase() && - f.year === year - ); - if (duplicate) { duplicateCount++; continue; } - - toImport.push({ title, creators, ean, description, publisher, year, length, discs, aspect, format }); - } - - if (toImport.length === 0) { - alert(`Aucun nouvel élément à importer.\n⚠️ ${duplicateCount} doublon(s) ignoré(s).`); + if (!films || films.length === 0) { + alert("Aucune donnée à exporter."); return; } - showImportProgress(toImport.length); - let importedCount = 0; - let coversFound = 0; + // Définition des entêtes conformes à la structure globale de l'application Mon Cinéma + const headers = ['ID', 'Titre', 'Annee', 'Realisateur', 'Note', 'Critique', 'URL_Affiche']; + + const csvRows = [headers.join(';')]; - for (let i = 0; i < toImport.length; i++) { - const { title, creators, ean, description, publisher, year, length, discs, aspect, format } = toImport[i]; - - const sourceLabel = ean ? `EAN ${ean}` : title; - updateImportProgress(i, toImport.length, `📀 ${sourceLabel}`); - - let poster = ''; - try { - poster = await fetchDvdCover(ean, title, year); - if (poster) coversFound++; - } catch(e) {} - - await new Promise(r => setTimeout(r, 300)); - - films.push({ - id: Date.now() + Math.floor(Math.random() * 100000) + i, - title, type: 'videotheque', - director: creators, year, poster, format, length, - publisher, ean_isbn13: ean, - number_of_discs: discs, - aspect_ratio: aspect, - description, rating: 0, review: '' - }); - importedCount++; - } - - updateImportProgress(toImport.length, toImport.length, '✅ Terminé !'); - await new Promise(r => setTimeout(r, 600)); - hideImportProgress(); - - persist(); - renderAdminTable(); - alert( - `Import Vidéothèque\n` + - `✅ ${importedCount} film(s) importé(s)\n` + - `🖼️ ${coversFound} jaquette(s) trouvée(s) (Open Library + TMDB)\n` + - `⚠️ ${duplicateCount} doublon(s) ignoré(s).` - ); -} - -function exportVideothequeToCSV() { - const filmsVideo = films.filter(f => (f.type || 'critique') === 'videotheque'); - if (filmsVideo.length === 0) { alert("Aucune donnée de vidéothèque à exporter."); return; } - - let csvContent = "item_type,title,creators,ean_isbn13,description,publisher,publish_date,length,number_of_discs,aspect_ratio\r\n"; - filmsVideo.forEach(f => { - const cleanTitle = (f.title || '').replace(/"/g, '""'); - const cleanDirector = (f.director || '').replace(/"/g, '""'); - const cleanEan = (f.ean_isbn13 || ''); - const cleanDesc = (f.description || '').replace(/\r?\n|\r/g, ' ').replace(/"/g, '""'); - const cleanPublisher = (f.publisher || '').replace(/"/g, '""'); - const fakePublishDate = f.year ? `${f.year}-01-01` : ''; - csvContent += `movie,"${cleanTitle}","${cleanDirector}","${cleanEan}","${cleanDesc}","${cleanPublisher}","${fakePublishDate}","${f.length || ''}","${f.number_of_discs || '1'}","${f.aspect_ratio || ''}"\r\n`; + films.forEach(f => { + const row = [ + f.id, + `"${(f.title || '').replace(/"/g, '""')}"`, + f.year || '', + `"${(f.director || '').replace(/"/g, '""')}"`, + f.rating || 0, + `"${(f.review || '').replace(/"/g, '""')}"`, + `"${(f.poster || '').replace(/"/g, '""')}"` + ]; + csvRows.push(row.join(';')); }); - const blob = new Blob([new Uint8Array([0xEF, 0xBB, 0xBF]), csvContent], { type: 'text/csv;charset=utf-8;' }); + const csvContent = "\uFEFF" + csvRows.join("\n"); // Ajout du BOM UTF-8 pour Excel + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); + const link = document.createElement("a"); link.setAttribute("href", url); - link.setAttribute("download", `export_videotheque_${Date.now()}.csv`); + link.setAttribute("download", `mon_cinema_export_${new Date().toISOString().slice(0,10)}.csv`); document.body.appendChild(link); link.click(); document.body.removeChild(link); } -function goPublic() { window.location.href = '../index.html'; } - -document.addEventListener('DOMContentLoaded', () => { - const savedKey = localStorage.getItem('tmdb-api-key'); - if (savedKey && document.getElementById('tmdb-key-input')) { - document.getElementById('tmdb-key-input').value = savedKey; +// ── PERSISTANCE & SYNCHRONISATION LOCALSTORAGE ── +function persist() { + localStorage.setItem('mon-cinema-films', JSON.stringify(films)); + if (typeof renderFilms === 'function') { + renderFilms(); } - renderAdminTable(); -}); +} -// Récupère l'affiche ET les plateformes de streaming en France -async function fetchTmdbDetails(title, year) { - const key = localStorage.getItem('tmdb-api-key'); - let streamingDefault = "Disponible au cinéma ou support physique"; - if (!key) return { poster: '', streaming: streamingDefault }; +// ── GESTION DE LA NAVIGATION PAR ONGLETS ── +function switchTab(tabName) { + document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active')); + document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active')); - try { - const yearParam = year ? `&year=${year}` : ''; - const res = await fetch( - `https://api.themoviedb.org/3/search/movie?api_key=${key}&query=${encodeURIComponent(title)}&language=fr-FR${yearParam}` - ); - const data = await res.json(); - - if (data.results && data.results.length > 0) { - const movie = data.results[0]; - const movieId = movie.id; - const posterPath = movie.poster_path ? `https://image.tmdb.org/t/p/w500${movie.poster_path}` : ''; - let streamingInfo = streamingDefault; + const targetTab = document.getElementById(`tab-${tabName}`); + const targetBtn = document.querySelector(`[onclick="switchTab('${tabName}')"]`); - try { - const watchRes = await fetch( - `https://api.themoviedb.org/3/movie/${movieId}/watch/providers?api_key=${key}` - ); - const watchData = await watchRes.json(); - - if (watchData.results && watchData.results.FR) { - const frProviders = watchData.results.FR; - const providers = frProviders.flatrate || frProviders.buy || frProviders.rent || []; - - if (providers.length > 0) { - const names = providers.map(p => p.provider_name); - const uniqueNames = [...new Set(names)].slice(0, 3); - streamingInfo = `Disponible sur : ${uniqueNames.join(', ')}`; - } - } - } catch (watchErr) { - console.error("Erreur watch providers TMDB:", watchErr); - } - - return { poster: posterPath, streaming: streamingInfo }; - } - } catch (e) { - console.error("Erreur globale fetchTmdbDetails:", e); - } - - return { poster: '', streaming: streamingDefault }; + if (targetTab) targetTab.classList.add('active'); + if (targetBtn) targetBtn.classList.add('active'); + + // Sauvegarde de l'onglet actif pour reconexion + localStorage.setItem('mon-cinema-active-tab', tabName); } \ No newline at end of file