Films and checklist loading #35
+76
-21
@@ -1,6 +1,7 @@
|
|||||||
window.onload = async function () {
|
window.onload = async function () {
|
||||||
const token = localStorage.getItem('auth_token');
|
const token = localStorage.getItem('auth_token');
|
||||||
const movieTableBody = document.getElementById('movie-table-body');
|
const movieTableBody = document.getElementById('movie-table-body');
|
||||||
|
const watchedRatioEl = document.getElementById('watched-ratio');
|
||||||
|
|
||||||
function showTableLoading() {
|
function showTableLoading() {
|
||||||
if (!movieTableBody) return;
|
if (!movieTableBody) return;
|
||||||
@@ -18,6 +19,18 @@ window.onload = async function () {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setInlineLoading(el, label = 'Chargement…') {
|
||||||
|
if (!el) return;
|
||||||
|
el.innerHTML = `
|
||||||
|
<span class="d-inline-flex align-items-center gap-2" aria-live="polite" aria-busy="true">
|
||||||
|
<span class="spinner-border spinner-border-sm text-secondary" role="status" aria-label="Chargement">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</span>
|
||||||
|
<span>${label}</span>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function showTableError(message) {
|
function showTableError(message) {
|
||||||
if (!movieTableBody) return;
|
if (!movieTableBody) return;
|
||||||
movieTableBody.innerHTML = `
|
movieTableBody.innerHTML = `
|
||||||
@@ -135,29 +148,52 @@ window.onload = async function () {
|
|||||||
return Number.isNaN(d.getTime()) ? null : d;
|
return Number.isNaN(d.getTime()) ? null : d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchMoviesSummary(year) {
|
||||||
|
const res = await fetch(`/api/movies/summary?year=${encodeURIComponent(String(year))}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to fetch movies summary');
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
const activeYear = await fetchActiveYear();
|
const activeYear = await fetchActiveYear();
|
||||||
document.title = `Pool Oscars ${activeYear} - Checklist`;
|
document.title = `Pool Oscars ${activeYear} - Checklist`;
|
||||||
const oscarYearEl = document.getElementById('oscar-year');
|
const oscarYearEl = document.getElementById('oscar-year');
|
||||||
if (oscarYearEl) oscarYearEl.textContent = String(activeYear);
|
if (oscarYearEl) oscarYearEl.textContent = String(activeYear);
|
||||||
|
|
||||||
const oscarEffectiveDate = await fetchOscarEffectiveDate(activeYear);
|
// Render countdown immediately with a deterministic fallback (then refine from settings).
|
||||||
const targetDate =
|
|
||||||
parseLocalNoonFromIsoDate(oscarEffectiveDate) ||
|
|
||||||
new Date(`March 15, ${activeYear} 12:00:00`);
|
|
||||||
|
|
||||||
const oscarDayMonthEl = document.getElementById('oscar-date-day-month');
|
const oscarDayMonthEl = document.getElementById('oscar-date-day-month');
|
||||||
|
const timeLeftEl = document.getElementById('time-left');
|
||||||
|
const fallbackDate = parseLocalNoonFromIsoDate(`${activeYear}-03-15`) || new Date(`March 15, ${activeYear} 12:00:00`);
|
||||||
if (oscarDayMonthEl) {
|
if (oscarDayMonthEl) {
|
||||||
try {
|
try {
|
||||||
oscarDayMonthEl.textContent = targetDate.toLocaleDateString('fr-FR', { day: 'numeric', month: 'long' });
|
oscarDayMonthEl.textContent = fallbackDate.toLocaleDateString('fr-FR', { day: 'numeric', month: 'long' });
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
oscarDayMonthEl.textContent = '15 mars';
|
oscarDayMonthEl.textContent = '15 mars';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentDate = new Date();
|
const currentDate = new Date();
|
||||||
const timeDifference = targetDate - currentDate;
|
let timeDifference = fallbackDate - currentDate;
|
||||||
const daysLeft = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
|
let daysLeft = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
|
||||||
document.getElementById('time-left').textContent = `${daysLeft} jours`;
|
if (timeLeftEl) timeLeftEl.textContent = `${daysLeft} jours`;
|
||||||
|
|
||||||
|
fetchOscarEffectiveDate(activeYear).then((oscarEffectiveDate) => {
|
||||||
|
const targetDate =
|
||||||
|
parseLocalNoonFromIsoDate(oscarEffectiveDate) ||
|
||||||
|
fallbackDate;
|
||||||
|
|
||||||
|
if (oscarDayMonthEl) {
|
||||||
|
try {
|
||||||
|
oscarDayMonthEl.textContent = targetDate.toLocaleDateString('fr-FR', { day: 'numeric', month: 'long' });
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
const now = new Date();
|
||||||
|
timeDifference = targetDate - now;
|
||||||
|
daysLeft = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
|
||||||
|
if (timeLeftEl) timeLeftEl.textContent = `${daysLeft} jours`;
|
||||||
|
}).catch(() => {});
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
try {
|
try {
|
||||||
@@ -186,22 +222,35 @@ window.onload = async function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showTableLoading();
|
showTableLoading();
|
||||||
|
setInlineLoading(watchedRatioEl);
|
||||||
|
const progressBarEl = document.getElementById('progress-bar');
|
||||||
|
if (progressBarEl) {
|
||||||
|
progressBarEl.classList.add('progress-bar-striped', 'progress-bar-animated');
|
||||||
|
}
|
||||||
|
|
||||||
let movies = [];
|
let movies = [];
|
||||||
let watchedMovies = [];
|
let watchedMovies = [];
|
||||||
let watchedMoviesInYear = [];
|
let watchedMoviesInYear = [];
|
||||||
let movieImdbIds = new Set();
|
let movieImdbIds = new Set();
|
||||||
try {
|
try {
|
||||||
// Fetch remaining data in parallel (Render latency is high).
|
// Start summary fetch immediately so the header can update without waiting for the full list.
|
||||||
const [completionModalContent, moviesRes, watchedRes] = await Promise.all([
|
const summaryPromise = fetchMoviesSummary(activeYear)
|
||||||
|
.then((summary) => {
|
||||||
|
const totalCount = Number(summary?.totalMoviesCount) || 0;
|
||||||
|
const watchedCount = Number(summary?.watchedMoviesCount) || 0;
|
||||||
|
const ratioPct = totalCount > 0 ? ((watchedCount / totalCount) * 100).toFixed(1) : '0.0';
|
||||||
|
if (watchedRatioEl) watchedRatioEl.innerText = `Vu: ${watchedCount} / ${totalCount} (${ratioPct}%)`;
|
||||||
|
updateProgressBar(watchedCount, totalCount, false);
|
||||||
|
return summary;
|
||||||
|
})
|
||||||
|
.catch(() => null);
|
||||||
|
|
||||||
|
// Fetch remaining data in parallel.
|
||||||
|
const [completionModalContent, moviesRes] = await Promise.all([
|
||||||
fetchCompletionModalContent(),
|
fetchCompletionModalContent(),
|
||||||
fetch(`/api/movies?year=${encodeURIComponent(String(activeYear))}&view=checklist`, {
|
fetch(`/api/movies?year=${encodeURIComponent(String(activeYear))}&view=checklist`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
}),
|
|
||||||
fetch('/api/movies/watchedMovies', {
|
|
||||||
method: 'GET',
|
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -218,8 +267,11 @@ window.onload = async function () {
|
|||||||
// Sort movies alphabetically by title
|
// Sort movies alphabetically by title
|
||||||
movies.sort((a, b) => a.title.localeCompare(b.title, 'fr', { sensitivity: 'base' }));
|
movies.sort((a, b) => a.title.localeCompare(b.title, 'fr', { sensitivity: 'base' }));
|
||||||
|
|
||||||
if (watchedRes.ok) {
|
const summary = await summaryPromise;
|
||||||
watchedMovies = await watchedRes.json();
|
watchedMovies = Array.isArray(summary?.watchedMovies) ? summary.watchedMovies : [];
|
||||||
|
|
||||||
|
if (progressBarEl) {
|
||||||
|
progressBarEl.classList.remove('progress-bar-striped', 'progress-bar-animated');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showTableError('Impossible de charger la checklist. Réessaie dans quelques instants.');
|
showTableError('Impossible de charger la checklist. Réessaie dans quelques instants.');
|
||||||
@@ -259,10 +311,12 @@ window.onload = async function () {
|
|||||||
frame();
|
frame();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateProgressBar(watchedCount, totalCount) {
|
function updateProgressBar(watchedCount, totalCount, celebrate = true) {
|
||||||
const progressBar = document.getElementById('progress-bar');
|
const progressBar = document.getElementById('progress-bar');
|
||||||
const percentage = (watchedCount / totalCount) * 100;
|
if (!progressBar) return;
|
||||||
console.log(percentage);
|
const safeTotal = Number(totalCount);
|
||||||
|
const safeWatched = Number(watchedCount);
|
||||||
|
const percentage = safeTotal > 0 && Number.isFinite(safeWatched) ? (safeWatched / safeTotal) * 100 : 0;
|
||||||
progressBar.style.width = `${percentage}%`;
|
progressBar.style.width = `${percentage}%`;
|
||||||
|
|
||||||
if (percentage <= 50) {
|
if (percentage <= 50) {
|
||||||
@@ -273,8 +327,8 @@ window.onload = async function () {
|
|||||||
|
|
||||||
progressBar.setAttribute('aria-valuenow', Math.round(percentage));
|
progressBar.setAttribute('aria-valuenow', Math.round(percentage));
|
||||||
|
|
||||||
|
if (celebrate) {
|
||||||
const videoModal = new bootstrap.Modal(document.getElementById("videoModal"));
|
const videoModal = new bootstrap.Modal(document.getElementById("videoModal"));
|
||||||
|
|
||||||
if (percentage === 100) {
|
if (percentage === 100) {
|
||||||
videoModal.show();
|
videoModal.show();
|
||||||
launchConfetti();
|
launchConfetti();
|
||||||
@@ -285,6 +339,7 @@ window.onload = async function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateProgressBar(watchedMoviesCount, totalMoviesCount);
|
updateProgressBar(watchedMoviesCount, totalMoviesCount);
|
||||||
|
|
||||||
|
|||||||
+89
-45
@@ -10,6 +10,18 @@ window.onload = async function () {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setInlineLoading(el, label = 'Chargement…') {
|
||||||
|
if (!el) return;
|
||||||
|
el.innerHTML = `
|
||||||
|
<span class="d-inline-flex align-items-center gap-2" aria-live="polite" aria-busy="true">
|
||||||
|
<span class="spinner-border spinner-border-sm text-secondary" role="status" aria-label="Chargement">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</span>
|
||||||
|
<span>${label}</span>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function setLoadError(el, message) {
|
function setLoadError(el, message) {
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
@@ -49,28 +61,37 @@ window.onload = async function () {
|
|||||||
return Number.isNaN(d.getTime()) ? null : d;
|
return Number.isNaN(d.getTime()) ? null : d;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchMoviesLastUpdated(year, token) {
|
async function fetchMoviesSummary(year, token) {
|
||||||
const el = document.getElementById('movies-last-updated');
|
const res = await fetch(`/api/movies/summary?year=${encodeURIComponent(String(year))}`, {
|
||||||
if (!el) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(`/api/movies/last-update?year=${encodeURIComponent(String(year))}`, {
|
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { 'Authorization': `Bearer ${token}` },
|
headers: { 'Authorization': `Bearer ${token}` },
|
||||||
});
|
});
|
||||||
if (!res.ok) return;
|
if (!res.ok) throw new Error('Failed to fetch movies summary');
|
||||||
|
return await res.json();
|
||||||
const data = await res.json();
|
|
||||||
const lastUpdatedIso = data?.lastUpdated;
|
|
||||||
if (!lastUpdatedIso) return;
|
|
||||||
|
|
||||||
const d = new Date(lastUpdatedIso);
|
|
||||||
if (Number.isNaN(d.getTime())) return;
|
|
||||||
|
|
||||||
el.textContent = d.toLocaleString('fr-FR', { dateStyle: 'medium', timeStyle: 'short' });
|
|
||||||
} catch (_) {
|
|
||||||
// Keep the default placeholder (—)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applySummaryToHeader(summary) {
|
||||||
|
const lastUpdatedEl = document.getElementById('movies-last-updated');
|
||||||
|
const ratioEl = document.getElementById('watched-ratio');
|
||||||
|
|
||||||
|
const lastUpdatedIso = summary?.lastUpdated;
|
||||||
|
if (lastUpdatedEl) {
|
||||||
|
if (lastUpdatedIso) {
|
||||||
|
const d = new Date(lastUpdatedIso);
|
||||||
|
if (!Number.isNaN(d.getTime())) {
|
||||||
|
lastUpdatedEl.textContent = d.toLocaleString('fr-FR', { dateStyle: 'medium', timeStyle: 'short' });
|
||||||
|
} else {
|
||||||
|
lastUpdatedEl.textContent = '—';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lastUpdatedEl.textContent = '—';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = Number(summary?.totalMoviesCount) || 0;
|
||||||
|
const watched = Number(summary?.watchedMoviesCount) || 0;
|
||||||
|
const ratioPct = total > 0 ? ((watched / total) * 100).toFixed(1) : '0.0';
|
||||||
|
if (ratioEl) ratioEl.innerText = `Vu: ${watched} / ${total} (${ratioPct}%)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeYear = await fetchActiveYear();
|
const activeYear = await fetchActiveYear();
|
||||||
@@ -78,24 +99,41 @@ window.onload = async function () {
|
|||||||
const oscarYearEl = document.getElementById('oscar-year');
|
const oscarYearEl = document.getElementById('oscar-year');
|
||||||
if (oscarYearEl) oscarYearEl.textContent = String(activeYear);
|
if (oscarYearEl) oscarYearEl.textContent = String(activeYear);
|
||||||
|
|
||||||
const oscarEffectiveDate = await fetchOscarEffectiveDate(activeYear);
|
// Render countdown immediately with a deterministic fallback (then refine from settings).
|
||||||
const targetDate =
|
|
||||||
parseLocalNoonFromIsoDate(oscarEffectiveDate) ||
|
|
||||||
new Date(`March 15, ${activeYear} 12:00:00`);
|
|
||||||
|
|
||||||
const oscarDayMonthEl = document.getElementById('oscar-date-day-month');
|
const oscarDayMonthEl = document.getElementById('oscar-date-day-month');
|
||||||
|
const timeLeftEl = document.getElementById('time-left');
|
||||||
|
const fallbackDate = parseLocalNoonFromIsoDate(`${activeYear}-03-15`) || new Date(`March 15, ${activeYear} 12:00:00`);
|
||||||
if (oscarDayMonthEl) {
|
if (oscarDayMonthEl) {
|
||||||
try {
|
try {
|
||||||
oscarDayMonthEl.textContent = targetDate.toLocaleDateString('fr-FR', { day: 'numeric', month: 'long' });
|
oscarDayMonthEl.textContent = fallbackDate.toLocaleDateString('fr-FR', { day: 'numeric', month: 'long' });
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
oscarDayMonthEl.textContent = '15 mars';
|
oscarDayMonthEl.textContent = '15 mars';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (timeLeftEl) {
|
||||||
|
const currentDate = new Date();
|
||||||
|
const timeDifference = fallbackDate - currentDate;
|
||||||
|
const daysLeft = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
|
||||||
|
timeLeftEl.textContent = `${daysLeft} jours`;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchOscarEffectiveDate(activeYear).then((oscarEffectiveDate) => {
|
||||||
|
const targetDate =
|
||||||
|
parseLocalNoonFromIsoDate(oscarEffectiveDate) ||
|
||||||
|
fallbackDate;
|
||||||
|
|
||||||
|
if (oscarDayMonthEl) {
|
||||||
|
try {
|
||||||
|
oscarDayMonthEl.textContent = targetDate.toLocaleDateString('fr-FR', { day: 'numeric', month: 'long' });
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
if (timeLeftEl) {
|
||||||
const currentDate = new Date();
|
const currentDate = new Date();
|
||||||
const timeDifference = targetDate - currentDate;
|
const timeDifference = targetDate - currentDate;
|
||||||
const daysLeft = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
|
const daysLeft = Math.floor(timeDifference / (1000 * 60 * 60 * 24));
|
||||||
document.getElementById('time-left').textContent = `${daysLeft} jours`;
|
timeLeftEl.textContent = `${daysLeft} jours`;
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
|
||||||
const token = localStorage.getItem('auth_token');
|
const token = localStorage.getItem('auth_token');
|
||||||
if (token) {
|
if (token) {
|
||||||
@@ -128,21 +166,33 @@ window.onload = async function () {
|
|||||||
const moviesList = document.getElementById('movies-list');
|
const moviesList = document.getElementById('movies-list');
|
||||||
setLoading(moviesList);
|
setLoading(moviesList);
|
||||||
|
|
||||||
// Populate "Dernière mise à jour des films" banner (per active year)
|
// Make header stats feel responsive (these used to wait for the heavy movies payload).
|
||||||
await fetchMoviesLastUpdated(activeYear, token);
|
setInlineLoading(document.getElementById('movies-last-updated'));
|
||||||
|
setInlineLoading(document.getElementById('watched-ratio'));
|
||||||
|
|
||||||
|
let watchedMoviesInYear = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch movies + watched list in parallel (Render latency is high).
|
// Start summary fetch immediately so header can update ASAP.
|
||||||
const [res, watchedRes] = await Promise.all([
|
const summaryPromise = fetchMoviesSummary(activeYear, token)
|
||||||
fetch(`/api/movies?year=${encodeURIComponent(String(activeYear))}`, {
|
.then((data) => {
|
||||||
method: 'GET',
|
watchedMoviesInYear = Array.isArray(data?.watchedMovies) ? data.watchedMovies : [];
|
||||||
headers: { 'Authorization': `Bearer ${token}` },
|
applySummaryToHeader(data);
|
||||||
}),
|
return data;
|
||||||
fetch('/api/movies/watchedMovies', {
|
|
||||||
method: 'GET',
|
|
||||||
headers: { 'Authorization': `Bearer ${token}` },
|
|
||||||
})
|
})
|
||||||
]);
|
.catch(() => {
|
||||||
|
// Leave the placeholders (—) if summary fails; movies list can still load.
|
||||||
|
const lastUpdatedEl = document.getElementById('movies-last-updated');
|
||||||
|
if (lastUpdatedEl) lastUpdatedEl.textContent = '—';
|
||||||
|
const ratioEl = document.getElementById('watched-ratio');
|
||||||
|
if (ratioEl) ratioEl.textContent = '—';
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await fetch(`/api/movies?year=${encodeURIComponent(String(activeYear))}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
localStorage.removeItem('auth_token');
|
localStorage.removeItem('auth_token');
|
||||||
@@ -151,18 +201,12 @@ window.onload = async function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const movies = await res.json();
|
const movies = await res.json();
|
||||||
|
// If summary is still in flight, wait for it before rendering watched banners.
|
||||||
|
await summaryPromise;
|
||||||
|
|
||||||
// Sort movies alphabetically by title (server also sorts, but keep client as a fallback)
|
// Sort movies alphabetically by title (server also sorts, but keep client as a fallback)
|
||||||
movies.sort((a, b) => a.title.localeCompare(b.title, 'fr', { sensitivity: 'base' }));
|
movies.sort((a, b) => a.title.localeCompare(b.title, 'fr', { sensitivity: 'base' }));
|
||||||
|
|
||||||
let watchedMovies = [];
|
|
||||||
if (watchedRes.ok) {
|
|
||||||
watchedMovies = await watchedRes.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
const movieImdbIds = new Set(movies.map((m) => m.imdb_id).filter(Boolean));
|
|
||||||
const watchedMoviesInYear = watchedMovies.filter((wm) => movieImdbIds.has(wm.imdb_id));
|
|
||||||
|
|
||||||
const totalMoviesCount = movies.length;
|
const totalMoviesCount = movies.length;
|
||||||
const watchedMoviesCount = watchedMoviesInYear.length;
|
const watchedMoviesCount = watchedMoviesInYear.length;
|
||||||
const ratioPct = totalMoviesCount > 0 ? ((watchedMoviesCount / totalMoviesCount) * 100).toFixed(1) : '0.0';
|
const ratioPct = totalMoviesCount > 0 ? ((watchedMoviesCount / totalMoviesCount) * 100).toFixed(1) : '0.0';
|
||||||
|
|||||||
@@ -195,6 +195,74 @@ router.get("/last-update", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get lightweight per-year summary for fast UI (counts, watched-in-year, last update)
|
||||||
|
router.get("/summary", async (req, res) => {
|
||||||
|
const token = req.headers.authorization?.split(" ")[1];
|
||||||
|
if (!token) return res.status(401).json({ message: "Token is required" });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||||
|
|
||||||
|
const year = parseOscarYear(req.query.year);
|
||||||
|
const filter = year ? { year } : {};
|
||||||
|
|
||||||
|
// Base (year-wide) data can be cached (not user-specific).
|
||||||
|
const cacheKey = `movies:summary-base:${year || "all"}`;
|
||||||
|
let base = cacheGet(cacheKey);
|
||||||
|
|
||||||
|
if (!base) {
|
||||||
|
const [idDocs, latestMovie] = await Promise.all([
|
||||||
|
Movie.find(filter).select("imdb_id").lean(),
|
||||||
|
Movie.findOne(filter)
|
||||||
|
.select("updatedAt createdAt")
|
||||||
|
.sort({ updatedAt: -1, createdAt: -1, _id: -1 })
|
||||||
|
.lean(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const imdbIds = idDocs.map((d) => d.imdb_id).filter(Boolean);
|
||||||
|
const imdbIdSet = new Set(imdbIds);
|
||||||
|
|
||||||
|
const lastUpdated =
|
||||||
|
latestMovie?.updatedAt ||
|
||||||
|
latestMovie?.createdAt ||
|
||||||
|
(typeof latestMovie?._id?.getTimestamp === "function" ? latestMovie._id.getTimestamp() : null);
|
||||||
|
|
||||||
|
base = {
|
||||||
|
year: year || null,
|
||||||
|
totalMoviesCount: imdbIds.length,
|
||||||
|
imdbIds,
|
||||||
|
// Store ISO string so it's JSON-safe + stable
|
||||||
|
lastUpdated: lastUpdated ? new Date(lastUpdated).toISOString() : null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Avoid storing a Set in cache (non-serializable and not needed elsewhere)
|
||||||
|
cacheSet(cacheKey, base);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await User.findById(decoded.id).select("watchedMovies").lean();
|
||||||
|
if (!user) return res.status(404).json({ message: "User not found" });
|
||||||
|
|
||||||
|
const imdbIdSet = new Set(base.imdbIds || []);
|
||||||
|
const watchedMoviesAll = Array.isArray(user.watchedMovies) ? user.watchedMovies : [];
|
||||||
|
const watchedMoviesInYear = watchedMoviesAll.filter((wm) => imdbIdSet.has(wm.imdb_id));
|
||||||
|
|
||||||
|
// Since this is user-specific, don't encourage shared caching.
|
||||||
|
res.set("Cache-Control", "private, max-age=5");
|
||||||
|
return res.status(200).json({
|
||||||
|
year: base.year,
|
||||||
|
totalMoviesCount: base.totalMoviesCount,
|
||||||
|
watchedMoviesCount: watchedMoviesInYear.length,
|
||||||
|
watchedMovies: watchedMoviesInYear,
|
||||||
|
lastUpdated: base.lastUpdated,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof jwt.JsonWebTokenError) {
|
||||||
|
return res.status(401).json({ message: "Invalid or expired token" });
|
||||||
|
}
|
||||||
|
return res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Get watched movies for the user
|
// Get watched movies for the user
|
||||||
router.get("/watchedMovies", async (req, res) => {
|
router.get("/watchedMovies", async (req, res) => {
|
||||||
const token = req.headers.authorization?.split(" ")[1]; // Get token from header
|
const token = req.headers.authorization?.split(" ")[1]; // Get token from header
|
||||||
|
|||||||
Reference in New Issue
Block a user