Files
oscars-pool/routes/movies.js
T
Cursor Agentandmathieumoran1 e0ec4760ce feat: Add movie editing functionality and modal
This commit introduces the ability to edit movie details through a modal interface. It includes backend API endpoints for updating movie information and frontend logic to display and manage the edit modal. The changes also refine the year selection and movie loading processes in the admin panel.

Co-authored-by: mathieumoran1 <[email protected]>
2025-12-20 22:24:38 +00:00

322 lines
10 KiB
JavaScript

const express = require("express");
const Movie = require("../models/Movie");
const User = require("../models/User");
const axios = require("axios");
const jwt = require("jsonwebtoken");
const router = express.Router();
function parseOscarYear(raw) {
if (raw === undefined || raw === null || raw === "") return null;
const n = Number(raw);
if (!Number.isInteger(n)) return null;
if (n < 1900 || n > 3000) return null;
return n;
}
function isValidImdbId(value) {
return typeof value === "string" && /^tt\d{5,}$/.test(value.trim());
}
// Fetch movie details from OMDb API
async function fetchMovieDetailsFromOmdb(imdb_id) {
const apiKey = process.env.OMDB_API; // Replace with your actual OMDb API key
const omdbUrl = `https://www.omdbapi.com/?i=${encodeURIComponent(imdb_id)}&apikey=${apiKey}`;
try {
const response = await axios.get(omdbUrl);
if (response.data.Response === "True") {
return {
title: response.data.Title,
description: response.data.Plot,
rating: response.data.imdbRating,
poster: response.data.Poster
};
} else {
throw new Error('Movie not found on OMDb API');
}
} catch (error) {
throw new Error(`Error fetching movie details: ${error.message}`);
}
}
// Get available Oscar years (distinct years from movies)
router.get("/years", async (req, res) => {
try {
const years = await Movie.distinct("year");
const cleaned = years
.filter((y) => typeof y === "number" && Number.isFinite(y))
.sort((a, b) => b - a);
res.status(200).json(cleaned);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Get all movies
router.get("/", async (req, res) => {
try {
const year = parseOscarYear(req.query.year);
const filter = year ? { year } : {};
const movies = await Movie.find(filter);
res.status(200).json(movies);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Get latest movie update/add timestamp
router.get("/last-update", async (req, res) => {
const token = req.headers.authorization?.split(" ")[1];
if (!token) return res.status(401).json({ message: "Token is required" });
try {
jwt.verify(token, process.env.JWT_SECRET);
const latestMovie = await Movie.findOne().sort({ updatedAt: -1, createdAt: -1, _id: -1 });
if (!latestMovie) {
return res.status(200).json({ lastUpdated: null });
}
const lastUpdated =
latestMovie.updatedAt ||
latestMovie.createdAt ||
(typeof latestMovie._id?.getTimestamp === "function" ? latestMovie._id.getTimestamp() : null);
return res.status(200).json({ lastUpdated: lastUpdated ? new Date(lastUpdated).toISOString() : null });
} 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
router.get("/watchedMovies", async (req, res) => {
const token = req.headers.authorization?.split(" ")[1]; // Get token from header
if (!token) return res.status(401).json({ message: "Token is required" });
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET); // Verify token
const user = await User.findById(decoded.id); // Get user by ID
if (!user) return res.status(404).json({ message: "User not found" });
res.status(200).json(user.watchedMovies); // Return the list of watched movies
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Update watched movies for the user
router.patch("/users/updateWatchedMovies", async (req, res) => {
const token = req.headers.authorization?.split(" ")[1]; // Get token from header
if (!token) return res.status(401).json({ message: "Token is required" });
const { imdb_id, isChecked } = req.body;
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET); // Verify token
const user = await User.findById(decoded.id); // Get user by ID
if (!user) return res.status(404).json({ message: "User not found" });
if (isChecked) {
// Add movie to watchedMovies with the watched date if not already added
const movieIndex = user.watchedMovies.findIndex(movie => movie.imdb_id === imdb_id);
if (movieIndex === -1) {
user.watchedMovies.push({ imdb_id, watchedDate: new Date() });
}
} else {
// Remove movie from watchedMovies array
user.watchedMovies = user.watchedMovies.filter(movie => movie.imdb_id !== imdb_id);
}
await user.save(); // Save updated user
res.status(200).json({ message: "Watched movies updated successfully" });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Add movie
router.post("/add", async (req, res) => {
const { imdb_id, category, vod_link, year } = req.body;
try {
// Get the token from the Authorization header
const token = req.headers.authorization?.split(" ")[1];
if (!token) {
return res.status(401).json({ message: "Authentication token is required" });
}
// Verify and decode the token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Check if the user is an admin
if (!decoded.admin) {
return res.status(403).json({ message: "You do not have admin privileges" });
}
const parsedYear = parseOscarYear(year);
if (!parsedYear) {
return res.status(400).json({ message: "Année invalide (ex: 2026)" });
}
// Fetch movie details from OMDb API
const movieDetails = await fetchMovieDetailsFromOmdb(imdb_id);
// Proceed with adding the movie
const movie = new Movie({
imdb_id,
title: movieDetails.title,
description: movieDetails.description,
rating: movieDetails.rating,
poster: movieDetails.poster,
year: parsedYear,
category,
vod_link
});
await movie.save();
res.status(201).json({ message: "Movie added successfully!" });
} catch (err) {
if (err instanceof jwt.JsonWebTokenError) {
return res.status(401).json({ message: "Invalid or expired token" });
}
res.status(400).json({ error: err.message });
}
});
// Admin: delete one or more movies (by Mongo _id or imdb_id)
router.delete("/delete", async (req, res) => {
const token = req.headers.authorization?.split(" ")[1];
if (!token) return res.status(401).json({ message: "Authentication token is required" });
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
if (!decoded.admin) {
return res.status(403).json({ message: "You do not have admin privileges" });
}
const { ids, imdb_ids } = req.body || {};
const idList = Array.isArray(ids) ? ids.filter(Boolean) : [];
const imdbList = Array.isArray(imdb_ids) ? imdb_ids.filter(Boolean) : [];
if (idList.length === 0 && imdbList.length === 0) {
return res.status(400).json({ message: "Aucun film sélectionné." });
}
const filter = idList.length
? { _id: { $in: idList } }
: { imdb_id: { $in: imdbList } };
const moviesToDelete = await Movie.find(filter).select("imdb_id");
const deletedImdbIds = moviesToDelete.map((m) => m.imdb_id).filter(Boolean);
const result = await Movie.deleteMany(filter);
if (deletedImdbIds.length > 0) {
await User.updateMany(
{},
{ $pull: { watchedMovies: { imdb_id: { $in: deletedImdbIds } } } }
);
}
return res.status(200).json({
message: "Movies deleted successfully",
deletedCount: result.deletedCount || 0,
});
} catch (err) {
if (err instanceof jwt.JsonWebTokenError) {
return res.status(401).json({ message: "Invalid or expired token" });
}
return res.status(500).json({ error: err.message });
}
});
// Admin: update a movie (edit fields, optionally refresh from OMDb)
router.put("/:id", async (req, res) => {
const token = req.headers.authorization?.split(" ")[1];
if (!token) return res.status(401).json({ message: "Authentication token is required" });
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
if (!decoded.admin) {
return res.status(403).json({ message: "You do not have admin privileges" });
}
const { id } = req.params;
const {
imdb_id,
title,
description,
rating,
poster,
year,
category,
vod_link,
refreshOmdb,
} = req.body || {};
if (imdb_id !== undefined && imdb_id !== null && !isValidImdbId(imdb_id)) {
return res.status(400).json({ message: "IMDB ID invalide. Exemple attendu: tt1234567" });
}
if (year !== undefined && year !== null && year !== "") {
const parsedYear = parseOscarYear(year);
if (!parsedYear) {
return res.status(400).json({ message: "Année invalide (ex: 2026)" });
}
}
if (typeof category === "string" && category.trim() === "") {
return res.status(400).json({ message: "Catégorie invalide." });
}
if (typeof vod_link === "string" && vod_link.trim() === "") {
return res.status(400).json({ message: "Lien VOD invalide." });
}
const movie = await Movie.findById(id);
if (!movie) return res.status(404).json({ message: "Movie not found" });
if (imdb_id !== undefined) movie.imdb_id = String(imdb_id).trim();
if (category !== undefined) movie.category = String(category).trim();
if (vod_link !== undefined) movie.vod_link = String(vod_link).trim();
if (year !== undefined && year !== null && year !== "") {
movie.year = parseOscarYear(year);
} else if (year === null || year === "") {
// allow clearing the year explicitly
movie.year = null;
}
// If refreshOmdb is true, overwrite OMDb-derived fields from the imdb_id
if (refreshOmdb) {
const details = await fetchMovieDetailsFromOmdb(movie.imdb_id);
movie.title = details.title;
movie.description = details.description;
movie.rating = details.rating;
movie.poster = details.poster;
} else {
if (title !== undefined) movie.title = title;
if (description !== undefined) movie.description = description;
if (rating !== undefined) movie.rating = rating;
if (poster !== undefined) movie.poster = poster;
}
await movie.save();
return res.status(200).json(movie);
} catch (err) {
if (err instanceof jwt.JsonWebTokenError) {
return res.status(401).json({ message: "Invalid or expired token" });
}
return res.status(500).json({ error: err.message });
}
});
module.exports = router;