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