Add low-quality server video support

Include low-quality video file paths in movie settings, expose them through the admin UI, and add a player toggle that streams /api/video with quality=low when available. Extend both Node and Python video endpoints to serve the low-quality file.

Test: not run (mark for testing)
This commit is contained in:
Mathieu Moran
2026-01-24 09:26:23 -05:00
parent b00280e9e4
commit f9d8722b4e
8 changed files with 197 additions and 25 deletions
+9 -2
View File
@@ -148,7 +148,7 @@ router.get("/", async (req, res) => {
const projection = isChecklist
? "imdb_id title"
: "imdb_id title description rating poster cam year category vod_link player_mode video_src embed_src video_file updatedAt createdAt";
: "imdb_id title description rating poster cam year category vod_link player_mode video_src embed_src video_file video_file_low updatedAt createdAt";
const cacheKey = `movies:list:${year || "all"}:${isChecklist ? "checklist" : "films"}`;
const cached = cacheGet(cacheKey);
@@ -333,7 +333,7 @@ router.patch("/users/updateWatchedMovies", async (req, res) => {
// Add movie
router.post("/add", async (req, res) => {
const { imdb_id, category, vod_link, year, player_mode, video_src, embed_src, video_file, cam } = req.body;
const { imdb_id, category, vod_link, year, player_mode, video_src, embed_src, video_file, video_file_low, cam } = req.body;
try {
// Get the token from the Authorization header
@@ -360,6 +360,7 @@ router.post("/add", async (req, res) => {
const normalizedVideo = normalizeOptionalString(video_src, 4096);
const normalizedEmbed = normalizeOptionalString(embed_src, 20000);
const normalizedFile = normalizeVideoFile(video_file);
const normalizedLowFile = normalizeVideoFile(video_file_low);
const normalizedMode = normalizePlayerMode(player_mode);
if (normalizedMode === null) {
return res.status(400).json({ message: "player_mode invalide (auto|video|embed)" });
@@ -398,6 +399,7 @@ router.post("/add", async (req, res) => {
video_src: normalizedVideo || undefined,
embed_src: normalizedEmbed || undefined,
video_file: normalizedFile || undefined,
video_file_low: normalizedLowFile || undefined,
});
await movie.save();
@@ -491,6 +493,7 @@ router.put("/:id", async (req, res) => {
video_src,
embed_src,
video_file,
video_file_low,
refreshOmdb,
cam,
} = req.body || {};
@@ -550,6 +553,10 @@ router.put("/:id", async (req, res) => {
movie.video_src = `/api/video/${movie._id}`;
}
}
if (video_file_low !== undefined) {
const v = normalizeVideoFile(video_file_low);
movie.video_file_low = v ? v : null;
}
if (year !== undefined && year !== null && year !== "") {
movie.year = parseOscarYear(year);
+11 -5
View File
@@ -119,17 +119,23 @@ async function streamMovieFile(req, res) {
return res.status(400).json({ message: "Invalid movie id" });
}
const movie = await Movie.findById(id).select("video_file").lean();
const qualityRaw = typeof req.query?.quality === "string" ? req.query.quality.trim().toLowerCase() : "";
const wantsLow = qualityRaw === "low";
const movie = await Movie.findById(id).select("video_file video_file_low").lean();
const videoFile = typeof movie?.video_file === "string" ? movie.video_file.trim() : "";
if (!videoFile) {
return res.status(404).json({ message: "No server video file configured for this movie" });
const videoFileLow = typeof movie?.video_file_low === "string" ? movie.video_file_low.trim() : "";
const chosen = wantsLow ? videoFileLow : videoFile;
if (!chosen) {
return res.status(404).json({ message: wantsLow ? "No low-quality server video file configured for this movie" : "No server video file configured for this movie" });
}
if (!isSafeRelativeFile(videoFile)) {
if (!isSafeRelativeFile(chosen)) {
return res.status(400).json({ message: "Invalid video_file path" });
}
const baseDir = getVideoBaseDir();
const fullPath = path.resolve(baseDir, videoFile);
const fullPath = path.resolve(baseDir, chosen);
if (!fullPath.startsWith(baseDir + path.sep) && fullPath !== baseDir) {
return res.status(400).json({ message: "Invalid video_file path" });
}