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]>
322 lines
10 KiB
JavaScript
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;
|