Files
mon-petit-cinema/js/admin.js
T
2026-06-16 20:36:19 +02:00

303 lines
11 KiB
JavaScript

/**
* =========================================================================
* Mon Cinéma - Module d'Administration (admin.js)
* Gestion de l'état, de la persistance, des onglets et des imports/exports CSV
* =========================================================================
*/
// ── 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 = [];
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;
}
}
if (row.length > 1 || row[0] !== '') rows.push(row);
return rows;
}
// ── 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;
const reader = new FileReader();
reader.onload = async function (e) {
const text = e.target.result;
const rows = parseFlexibleCSV(text);
if (rows.length <= 1) {
alert("Le fichier semble vide ou invalide.");
input.value = '';
return;
}
// Normalisation des entêtes pour la correspondance des colonnes (insensible à la casse)
const headers = rows[0].map(h => h.trim().toLowerCase());
// 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 titleRaw = cols[idxTitle]?.trim();
if (!titleRaw) continue;
// 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.type === 'critique' && f.title.toLowerCase() === title.toLowerCase() && f.year === year
);
if (alreadyExists) {
duplicateCount++;
continue;
}
toImport.push({ title, year, director, rating, review, filePoster });
}
// Sortie anticipée si aucun élément n'est à importer
if (toImport.length === 0) {
alert(`Aucun nouveau film à ajouter.\n⚠️ ${duplicateCount} doublon(s) ignoré(s).`);
input.value = '';
return;
}
const hasTmdb = !!localStorage.getItem('tmdb-api-key');
showImportProgress(toImport.length);
let importedCount = 0;
// 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";
// 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, 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,
rating,
review,
poster,
streaming,
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, 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();
switchTab('critique');
alert(
`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 = `
<div class="import-card">
<h3>Importation du catalogue CSV</h3>
<div class="import-progress-bar"><div id="import-progress-fill"></div></div>
<p id="import-progress-text">Préparation...</p>
</div>
`;
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 || films.length === 0) {
alert("Aucune donnée à exporter.");
return;
}
// 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(';')];
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 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", `mon_cinema_export_${new Date().toISOString().slice(0,10)}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
// ── PERSISTANCE & SYNCHRONISATION LOCALSTORAGE ──
function persist() {
localStorage.setItem('mon-cinema-films', JSON.stringify(films));
if (typeof renderFilms === 'function') {
renderFilms();
}
}
// ── 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'));
const targetTab = document.getElementById(`tab-${tabName}`);
const targetBtn = document.querySelector(`[onclick="switchTab('${tabName}')"]`);
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);
}