feat: Add functionality to format, rename, and store video files

This commit is contained in:
Peifan Li
2025-12-20 15:24:17 -05:00
parent ec02a94318
commit 24078d5798
2 changed files with 121 additions and 20 deletions

View File

@@ -4,6 +4,7 @@ import fs from "fs-extra";
import path from "path";
import { IMAGES_DIR, VIDEOS_DIR } from "../config/paths";
import * as storageService from "../services/storageService";
import { formatVideoFilename } from "../utils/helpers";
import { logger } from "../utils/logger";
import { successResponse } from "../utils/response";
@@ -113,24 +114,53 @@ export const scanFiles = async (
const relativePath = path.relative(VIDEOS_DIR, filePath);
const webPath = `/videos/${relativePath.split(path.sep).join("/")}`;
// Check if exists in DB
// Check if exists in DB by original filename
if (existingFilenames.has(filename)) {
continue;
}
logger.info(`Found new video file: ${relativePath}`);
const stats = fs.statSync(filePath);
const createdDate = stats.birthtime;
const videoId = (Date.now() + Math.floor(Math.random() * 10000)).toString();
// Generate thumbnail
const thumbnailFilename = `${path.parse(filename).name}.jpg`;
const thumbnailPath = path.join(IMAGES_DIR, thumbnailFilename);
// Extract title from filename
const originalTitle = path.parse(filename).name;
const author = "Admin";
const dateString = createdDate
.toISOString()
.split("T")[0]
.replace(/-/g, "");
// Format filename using the same format as downloaded videos: Title-Author-Year.ext
// formatVideoFilename already handles sanitization (removes symbols, replaces spaces with dots)
const baseFilename = formatVideoFilename(originalTitle, author, dateString);
// Use original title for database (for display purposes)
// The title should be readable, not sanitized like filenames
const displayTitle = originalTitle || "Untitled Video";
const videoExtension = path.extname(filename);
const newVideoFilename = `${baseFilename}${videoExtension}`;
// Check if the new formatted filename already exists in DB (to avoid duplicates)
if (existingFilenames.has(newVideoFilename)) {
logger.info(
`Skipping file "${filename}" - formatted filename "${newVideoFilename}" already exists in database`
);
continue;
}
logger.info(`Found new video file: ${relativePath}`);
const videoId = (Date.now() + Math.floor(Math.random() * 10000)).toString();
const newThumbnailFilename = `${baseFilename}.jpg`;
// Generate thumbnail with temporary name first
const tempThumbnailPath = path.join(
IMAGES_DIR,
`${path.parse(filename).name}.jpg`
);
await new Promise<void>((resolve) => {
exec(
`ffmpeg -i "${filePath}" -ss 00:00:00 -vframes 1 "${thumbnailPath}"`,
`ffmpeg -i "${filePath}" -ss 00:00:00 -vframes 1 "${tempThumbnailPath}"`,
(error) => {
if (error) {
logger.error("Error generating thumbnail:", error);
@@ -167,26 +197,97 @@ export const scanFiles = async (
logger.error("Error getting duration:", err);
}
// Rename video file to the new format (preserve subfolder structure)
const fileDir = path.dirname(filePath);
const newVideoPath = path.join(fileDir, newVideoFilename);
let finalVideoFilename = filename;
let finalVideoPath = filePath;
let finalWebPath = webPath;
try {
// Check if the new filename already exists
if (fs.existsSync(newVideoPath) && newVideoPath !== filePath) {
logger.warn(
`Target filename already exists: ${newVideoFilename}, keeping original filename`
);
} else if (newVideoFilename !== filename) {
// Rename the video file (in the same directory)
fs.moveSync(filePath, newVideoPath);
finalVideoFilename = newVideoFilename;
finalVideoPath = newVideoPath;
// Update web path to reflect the new filename while preserving subfolder structure
const dirName = path.dirname(relativePath);
if (dirName !== ".") {
finalWebPath = `/videos/${dirName
.split(path.sep)
.join("/")}/${newVideoFilename}`;
} else {
finalWebPath = `/videos/${newVideoFilename}`;
}
logger.info(
`Renamed video file from "${filename}" to "${newVideoFilename}"`
);
}
} catch (renameError) {
logger.error(`Error renaming video file: ${renameError}`);
// Continue with original filename if rename fails
}
// Rename thumbnail file to match the new video filename
const finalThumbnailPath = path.join(IMAGES_DIR, newThumbnailFilename);
let finalThumbnailFilename = newThumbnailFilename;
try {
if (fs.existsSync(tempThumbnailPath)) {
if (
fs.existsSync(finalThumbnailPath) &&
tempThumbnailPath !== finalThumbnailPath
) {
// If target exists, remove the temp one
fs.removeSync(tempThumbnailPath);
logger.warn(
`Thumbnail filename already exists: ${newThumbnailFilename}, using existing`
);
} else if (tempThumbnailPath !== finalThumbnailPath) {
// Rename the thumbnail file
fs.moveSync(tempThumbnailPath, finalThumbnailPath);
logger.info(`Renamed thumbnail file to "${newThumbnailFilename}"`);
}
}
} catch (renameError) {
logger.error(`Error renaming thumbnail file: ${renameError}`);
// Use temp filename if rename fails
if (fs.existsSync(tempThumbnailPath)) {
finalThumbnailFilename = path.basename(tempThumbnailPath);
}
}
const newVideo = {
id: videoId,
title: path.parse(filename).name,
author: "Admin",
title: displayTitle,
author: author,
source: "local",
sourceUrl: "",
videoFilename: filename,
videoPath: webPath,
thumbnailFilename: fs.existsSync(thumbnailPath)
? thumbnailFilename
videoFilename: finalVideoFilename,
videoPath: finalWebPath,
thumbnailFilename: fs.existsSync(finalThumbnailPath)
? finalThumbnailFilename
: fs.existsSync(tempThumbnailPath)
? path.basename(tempThumbnailPath)
: undefined,
thumbnailPath: fs.existsSync(thumbnailPath)
? `/images/${thumbnailFilename}`
thumbnailPath: fs.existsSync(finalThumbnailPath)
? `/images/${finalThumbnailFilename}`
: fs.existsSync(tempThumbnailPath)
? `/images/${path.basename(tempThumbnailPath)}`
: undefined,
thumbnailUrl: fs.existsSync(thumbnailPath)
? `/images/${thumbnailFilename}`
thumbnailUrl: fs.existsSync(finalThumbnailPath)
? `/images/${finalThumbnailFilename}`
: fs.existsSync(tempThumbnailPath)
? `/images/${path.basename(tempThumbnailPath)}`
: undefined,
createdAt: createdDate.toISOString(),
addedAt: new Date().toISOString(),
date: createdDate.toISOString().split("T")[0].replace(/-/g, ""),
date: dateString,
duration: duration,
};

View File

@@ -336,7 +336,7 @@ const Header: React.FC<HeaderProps> = ({
<Slide direction="up" in={isMobile && isScrolled} mountOnEnter unmountOnExit>
<Fab
color="primary"
size="small"
size="medium"
aria-label="scroll to top"
onClick={() => {
window.scrollTo({ top: 0, behavior: 'smooth' });