Files
mon-petit-cinema/js/admin.js
T

862 lines
33 KiB
JavaScript
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.
const ADMIN_PASSWORD = 'cinema2024';
const STORAGE_KEY = 'mon-cinema-films';
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 = '<div style="color:var(--muted); font-size:0.85rem; padding:0.5rem 0;">Recherche en cours...</div>';
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 = '<div style="color:var(--muted); font-size:0.85rem; padding:0.5rem 0;">Aucun résultat trouvé.</div>';
return;
}
resultsWrap.innerHTML = `
<div style="display:flex; flex-direction:column; gap:0.5rem; margin-top:0.5rem; max-height:200px; overflow-y:auto; background:rgba(255,255,255,0.05); padding:0.5rem; border-radius:4px;">
${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, "&quot;");
return `
<div onclick="selectTmdbMovie('${safeTitle}', '${year}', '${posterUrl}')"
style="display:flex; align-items:center; gap:0.7rem; padding:0.4rem; cursor:pointer; border-radius:3px; transition:background 0.2s;"
onmouseover="this.style.background='rgba(255,255,255,0.08)'"
onmouseout="this.style.background='transparent'">
${posterUrl ? `<img src="${posterUrl}" style="width:30px; height:42px; object-fit:cover; border-radius:2px;">` : `<div style="width:30px; height:42px; background:#111; display:flex; align-items:center; justify-content:center; font-size:0.5rem; color:var(--muted);">—</div>`}
<span style="font-size:0.85rem; color:var(--text);">${movie.title} <span style="color:var(--muted)">(${year})</span></span>
</div>
`;
}).join('')}
</div>
`;
} catch (err) {
resultsWrap.innerHTML = '<div style="color:var(--red); font-size:0.85rem; padding:0.5rem 0;">Erreur lors de la recherche.</div>';
}
}
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 = "<strong>Affiche sélectionnée</strong>";
} else {
img.style.display = 'none';
none.style.display = 'flex';
info.innerHTML = "Aucune affiche sélectionnée.<br>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", // Conserve ou ajoute par défaut
...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", // Valeur par défaut à la création
...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;
// Reset select-all + bulk-delete button BEFORE rebuilding DOM
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';
// Update film count display
const countEl = document.getElementById('films-count');
if (countEl) countEl.textContent = filteredFilms.length;
tbody.innerHTML = filteredFilms.map(f => {
const posterHtml = f.poster
? `<img class="thumb" src="${f.poster}" alt="" style="width:40px; height:55px; object-fit:cover; border-radius:3px;">`
: '<div style="color:var(--muted); text-align:center;">—</div>';
let dynamicCellHtml = '';
if (currentTab === 'critique') {
dynamicCellHtml = `<span style="color:var(--gold); letter-spacing:0.05em;">${'★'.repeat(f.rating || 1)}${'☆'.repeat(5 - (f.rating || 1))}</span>`;
} else {
let formatLabel = (f.format || 'DVD').toUpperCase().replace('_4K', ' 4K');
let publisherLabel = f.publisher ? `<div style="font-size:0.75rem; color:var(--muted); margin-top:2px;">${f.publisher}</div>` : '';
dynamicCellHtml = `<span class="badge-format" style="background:rgba(255,255,255,0.1); padding:0.2rem 0.5rem; border-radius:3px; font-size:0.8rem; font-weight:bold;">${formatLabel}</span>${publisherLabel}`;
}
return `
<tr>
<td style="text-align:center;">
<input type="checkbox" class="row-select" data-id="${f.id}" onclick="updateBulkDeleteButton()" style="cursor:pointer; transform: scale(1.2);">
</td>
<td>${posterHtml}</td>
<td style="font-weight:500; color:var(--text);">${f.title}</td>
<td>${f.year || '—'}</td>
<td>${f.director || '—'}</td>
<td>${dynamicCellHtml}</td>
<td style="display: flex; gap: 0.4rem; border: none;">
<button onclick="openEditModal(${f.id})" style="background: var(--gold); color: #000; border: none; padding: 0.4rem 0.6rem; border-radius: 3px; cursor: pointer; font-size: 0.8rem; font-weight: 500; display: inline-flex; align-items: center; gap: 0.2rem;">
Modifier
</button>
<button onclick="deleteFilm(${f.id})" style="background:var(--red); color:#fff; border:none; padding:0.4rem 0.6rem; border-radius:3px; cursor:pointer; font-size:0.8rem; display: inline-flex; align-items: center; gap: 0.2rem;">
Supprimer
</button>
</td>
</tr>
`;
}).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();
}
}
// ── Letterboxd CSV parser (handles quoted commas correctly) ──────────────
function parseLetterboxdCSV(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;
}
}
cols.push(cur);
rows.push(cols);
}
return rows;
}
// ── Show import progress overlay ─────────────────────────────────────────
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 = `
<div style="background:var(--surface);border:1px solid var(--border);border-radius:8px;
padding:2rem 2.5rem;min-width:320px;text-align:center;">
<div style="font-family:'Playfair Display',serif;color:var(--gold);font-size:1.2rem;margin-bottom:1.2rem;">
Import
</div>
<div id="import-progress-label" style="font-size:0.82rem;color:var(--muted);margin-bottom:0.8rem;">
Initialisation…
</div>
<div style="background:var(--surface2);border-radius:99px;height:6px;overflow:hidden;margin-bottom:0.8rem;">
<div id="import-progress-bar" style="height:100%;background:var(--gold);width:0%;transition:width 0.3s;border-radius:99px;"></div>
</div>
<div id="import-progress-count" style="font-size:0.75rem;color:var(--muted);">0 / ${total}</div>
</div>
`;
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();
}
// ── TMDB poster lookup (single film) ─────────────────────────────────────
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}`;
}
// Retry without year if no result
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 '';
}
// ── DVD/Blu-ray cover: Open Library (EAN) → TMDB fallback ────────────────
async function fetchDvdCover(ean, title, year) {
// Step 1 — Open Library ISBN/EAN lookup (no key needed)
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];
// Prefer large cover, fall back to medium/small
if (book.cover) {
const coverUrl = book.cover.large || book.cover.medium || book.cover.small || '';
if (coverUrl) return coverUrl;
}
}
} catch (e) {}
// Step 1b — Open Library cover API directly by ISBN (faster path)
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) {}
}
// Step 2 — TMDB fallback (uses existing key)
return await fetchTmdbPoster(title, year);
}
// ── Main Letterboxd import (reviews.csv OR ratings.csv) ──────────────────
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 = parseLetterboxdCSV(text);
if (rows.length <= 1) {
alert("Le fichier semble vide ou invalide.");
input.value = '';
return;
}
const headers = rows[0].map(h => h.trim().toLowerCase());
// Detect file type: reviews.csv has a "Review" column, ratings.csv doesn't
const isReviews = headers.includes('review');
const iCol = { // column indices by header name
name: headers.indexOf('name'),
year: headers.indexOf('year'),
rating: headers.indexOf('rating'),
review: isReviews ? headers.indexOf('review') : -1,
watchedDate: headers.indexOf('watched date'),
};
// Collect rows to import (skip duplicates first pass)
const toImport = [];
let duplicateCount = 0;
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 year = iCol.year >= 0 ? (cols[iCol.year]?.trim() || '') : '';
const ratingRaw = iCol.rating >= 0 ? parseFloat(cols[iCol.rating]) : 0;
// Letterboxd uses 0.55 scale; convert to our 15 integer scale
const rating = Math.round(Math.min(5, Math.max(1, ratingRaw)));
const review = (isReviews && iCol.review >= 0) ? (cols[iCol.review]?.trim() || '') : '';
const alreadyExists = films.some(
f => f.title.toLowerCase() === title.toLowerCase() && f.year === year
);
if (alreadyExists) { duplicateCount++; continue; }
toImport.push({ title, year, rating, review });
}
if (toImport.length === 0) {
alert(`Aucun nouvel élément à importer.\n⚠️ ${duplicateCount} doublon(s) ignoré(s).`);
input.value = '';
return;
}
const hasTmdb = !!localStorage.getItem('tmdb-api-key');
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 = '';
let streaming = "Disponible au cinéma ou support physique";
if (hasTmdb) {
// On récupère les deux infos en un seul traitement
const details = await fetchTmdbDetails(title, year);
poster = details.poster;
streaming = details.streaming;
// Augmentation légère du délai pour respecter les limites de l'API avec le double appel
await new Promise(r => setTimeout(r, 350));
}
films.push({
id: Date.now() + Math.floor(Math.random() * 100000) + i,
title,
type: 'critique',
year,
director: '',
rating,
review,
poster,
streaming, // Ajout du nouveau champ
format: '', length: '', publisher: '', ean_isbn13: '', number_of_discs: '1', aspect_ratio: '', description: ''
});
importedCount++;
}
updateImportProgress(toImport.length, toImport.length, '✅ Terminé !');
await new Promise(r => setTimeout(r, 600));
hideImportProgress();
films.sort((a, b) => b.id - a.id);
persist();
renderAdminTable();
const typeLabel = isReviews ? 'reviews.csv (avec critiques)' : 'ratings.csv (notes seules)';
const posterInfo = hasTmdb ? ' · Affiches récupérées via TMDB' : ' · ⚠️ Clé TMDB absente, affiches non récupérées';
alert(
`Import Letterboxd — ${typeLabel}\n` +
`${importedCount} film(s) importé(s)${posterInfo}\n` +
`⚠️ ${duplicateCount} doublon(s) ignoré(s).`
);
input.value = '';
};
reader.readAsText(file, 'UTF-8');
}
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) {
// ── Parse CSV (handles quoted commas) ──
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;
// ── Collect rows to import ──
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).`);
return;
}
// ── Progress bar ──
showImportProgress(toImport.length);
let importedCount = 0;
let coversFound = 0;
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}`);
// ── Fetch cover: Open Library (EAN) → TMDB ──
let poster = '';
try {
poster = await fetchDvdCover(ean, title, year);
if (poster) coversFound++;
} catch(e) {}
// Respectful delay
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`;
});
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_videotheque_${Date.now()}.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;
}
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 };
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();
let movie = data.results && data.results.length > 0 ? data.results[0] : null;
if (!movie && 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) movie = data2.results[0];
}
if (movie) {
const poster = movie.poster_path ? `https://image.tmdb.org/t/p/w500${movie.poster_path}` : '';
let streaming = streamingDefault;
try {
const providerRes = await fetch(`https://api.themoviedb.org/3/movie/${movie.id}/watch/providers?api_key=${key}`);
const providerData = await providerRes.json();
// On cherche les plateformes d'abonnement (flatrate) disponibles en France (FR)
if (providerData.results && providerData.results.FR && providerData.results.FR.flatrate) {
const providers = providerData.results.FR.flatrate.map(p => p.provider_name);
if (providers.length > 0) {
streaming = providers.join(', ');
}
}
} catch (e) {
console.error("Erreur fournisseurs streaming:", e);
}
return { poster, streaming };
}
} catch (e) {
console.error("Erreur recherche TMDB:", e);
}
return { poster: '', streaming: streamingDefault };
}