refactor: Improve video handling in collectionController

This commit is contained in:
Peifan Li
2025-11-24 19:46:29 -05:00
parent 7be0cc112c
commit 22214f26cd
3 changed files with 342 additions and 47 deletions

View File

@@ -1,9 +1,9 @@
{
"loginEnabled": true,
"loginEnabled": false,
"defaultAutoPlay": false,
"defaultAutoLoop": false,
"maxConcurrentDownloads": 1,
"isPasswordSet": true,
"language": "en",
"password": ""
}
"password": "$2b$10$OC6wVmMg4p32ynOddLfALO8AqK/ByA.UjiLBldjCs3l/QhTm.qvvK"
}

View File

@@ -30,7 +30,7 @@ export const createCollection = (req: Request, res: Response): any => {
const newCollection: Collection = {
id: Date.now().toString(),
name,
videos: videoId ? [videoId] : [],
videos: [], // Initialize with empty videos
createdAt: new Date().toISOString(),
title: name, // Ensure title is also set as it's required by the interface
};
@@ -38,6 +38,14 @@ export const createCollection = (req: Request, res: Response): any => {
// Save the new collection
storageService.saveCollection(newCollection);
// If videoId is provided, add it to the collection (this handles file moving)
if (videoId) {
const updatedCollection = storageService.addVideoToCollection(newCollection.id, videoId);
if (updatedCollection) {
return res.status(201).json(updatedCollection);
}
}
res.status(201).json(newCollection);
} catch (error) {
console.error("Error creating collection:", error);
@@ -53,32 +61,30 @@ export const updateCollection = (req: Request, res: Response): any => {
const { id } = req.params;
const { name, videoId, action } = req.body;
// Update the collection atomically
const updatedCollection = storageService.atomicUpdateCollection(
id,
(collection) => {
// Update the collection
if (name) {
collection.name = name;
collection.title = name; // Update title as well
}
// Add or remove a video
if (videoId) {
if (action === "add") {
// Add the video if it's not already in the collection
if (!collection.videos.includes(videoId)) {
collection.videos.push(videoId);
}
} else if (action === "remove") {
// Remove the video
collection.videos = collection.videos.filter((v) => v !== videoId);
}
}
let updatedCollection: Collection | null | undefined;
// Handle name update first
if (name) {
updatedCollection = storageService.atomicUpdateCollection(id, (collection) => {
collection.name = name;
collection.title = name;
return collection;
});
}
// Handle video add/remove
if (videoId) {
if (action === "add") {
updatedCollection = storageService.addVideoToCollection(id, videoId);
} else if (action === "remove") {
updatedCollection = storageService.removeVideoFromCollection(id, videoId);
}
);
}
// If no changes requested but id exists, return current collection
if (!name && !videoId) {
updatedCollection = storageService.getCollectionById(id);
}
if (!updatedCollection) {
return res
@@ -101,18 +107,16 @@ export const deleteCollection = (req: Request, res: Response): any => {
const { id } = req.params;
const { deleteVideos } = req.query;
let success = false;
// If deleteVideos is true, delete all videos in the collection first
if (deleteVideos === 'true') {
const collection = storageService.getCollectionById(id);
if (collection && collection.videos && collection.videos.length > 0) {
collection.videos.forEach((videoId) => {
storageService.deleteVideo(videoId);
});
}
success = storageService.deleteCollectionAndVideos(id);
} else {
// Default: Move files back to root/other, then delete collection
success = storageService.deleteCollectionWithFiles(id);
}
const success = storageService.deleteCollection(id);
if (!success) {
return res
.status(404)

View File

@@ -25,6 +25,7 @@ export interface Collection {
title: string;
videos: string[];
updatedAt?: string;
name?: string; // Add name property
[key: string]: any;
}
@@ -240,23 +241,20 @@ export function deleteVideo(id: string): boolean {
return false;
}
// Remove the video file from the videos directory
// Remove the video file
if (videoToDelete.videoFilename) {
const videoFilePath = path.join(VIDEOS_DIR, videoToDelete.videoFilename);
if (fs.existsSync(videoFilePath)) {
fs.unlinkSync(videoFilePath);
const actualPath = findVideoFile(videoToDelete.videoFilename);
if (actualPath && fs.existsSync(actualPath)) {
fs.unlinkSync(actualPath);
}
}
// Remove the thumbnail file from the images directory
// Remove the thumbnail file
if (videoToDelete.thumbnailFilename) {
const thumbnailFilePath = path.join(
IMAGES_DIR,
videoToDelete.thumbnailFilename
);
if (fs.existsSync(thumbnailFilePath)) {
fs.unlinkSync(thumbnailFilePath);
}
const actualPath = findImageFile(videoToDelete.thumbnailFilename);
if (actualPath && fs.existsSync(actualPath)) {
fs.unlinkSync(actualPath);
}
}
// Filter out the deleted video from the videos array
@@ -342,3 +340,296 @@ export function deleteCollection(id: string): boolean {
);
return true;
}
// Helper to find where a video file currently resides
function findVideoFile(filename: string): string | null {
// Check root
const rootPath = path.join(VIDEOS_DIR, filename);
if (fs.existsSync(rootPath)) return rootPath;
// Check collections
const collections = getCollections();
for (const collection of collections) {
const collectionName = collection.name || collection.title;
if (collectionName) {
const collectionPath = path.join(VIDEOS_DIR, collectionName, filename);
if (fs.existsSync(collectionPath)) return collectionPath;
}
}
return null;
}
// Helper to find where an image file currently resides
function findImageFile(filename: string): string | null {
// Check root
const rootPath = path.join(IMAGES_DIR, filename);
if (fs.existsSync(rootPath)) return rootPath;
// Check collections
const collections = getCollections();
for (const collection of collections) {
const collectionName = collection.name || collection.title;
if (collectionName) {
const collectionPath = path.join(IMAGES_DIR, collectionName, filename);
if (fs.existsSync(collectionPath)) return collectionPath;
}
}
return null;
}
// Helper to move a file
function moveFile(sourcePath: string, destPath: string): void {
try {
if (fs.existsSync(sourcePath)) {
fs.ensureDirSync(path.dirname(destPath));
fs.moveSync(sourcePath, destPath, { overwrite: true });
console.log(`Moved file from ${sourcePath} to ${destPath}`);
}
} catch (error) {
console.error(`Error moving file from ${sourcePath} to ${destPath}:`, error);
}
}
// Add video to collection and move files
export function addVideoToCollection(collectionId: string, videoId: string): Collection | null {
const collection = atomicUpdateCollection(collectionId, (c) => {
if (!c.videos.includes(videoId)) {
c.videos.push(videoId);
}
return c;
});
if (collection) {
const video = getVideoById(videoId);
const collectionName = collection.name || collection.title;
if (video && collectionName) {
const updates: Partial<Video> = {};
let updated = false;
// Move video file
if (video.videoFilename) {
const currentVideoPath = findVideoFile(video.videoFilename);
const targetVideoPath = path.join(VIDEOS_DIR, collectionName, video.videoFilename);
if (currentVideoPath && currentVideoPath !== targetVideoPath) {
moveFile(currentVideoPath, targetVideoPath);
updates.videoPath = `/videos/${collectionName}/${video.videoFilename}`;
updated = true;
}
}
// Move image file
if (video.thumbnailFilename) {
const currentImagePath = findImageFile(video.thumbnailFilename);
const targetImagePath = path.join(IMAGES_DIR, collectionName, video.thumbnailFilename);
if (currentImagePath && currentImagePath !== targetImagePath) {
moveFile(currentImagePath, targetImagePath);
updates.thumbnailPath = `/images/${collectionName}/${video.thumbnailFilename}`;
updated = true;
}
}
if (updated) {
updateVideo(videoId, updates);
}
}
}
return collection;
}
// Remove video from collection and move files
export function removeVideoFromCollection(collectionId: string, videoId: string): Collection | null {
const collection = atomicUpdateCollection(collectionId, (c) => {
c.videos = c.videos.filter((v) => v !== videoId);
return c;
});
if (collection) {
const video = getVideoById(videoId);
if (video) {
// Check if video is in any other collection
const allCollections = getCollections();
const otherCollection = allCollections.find(c => c.videos.includes(videoId) && c.id !== collectionId);
let targetVideoDir = VIDEOS_DIR;
let targetImageDir = IMAGES_DIR;
let videoPathPrefix = '/videos';
let imagePathPrefix = '/images';
if (otherCollection) {
const otherName = otherCollection.name || otherCollection.title;
if (otherName) {
targetVideoDir = path.join(VIDEOS_DIR, otherName);
targetImageDir = path.join(IMAGES_DIR, otherName);
videoPathPrefix = `/videos/${otherName}`;
imagePathPrefix = `/images/${otherName}`;
}
}
const updates: Partial<Video> = {};
let updated = false;
// Move video file
if (video.videoFilename) {
const currentVideoPath = findVideoFile(video.videoFilename);
const targetVideoPath = path.join(targetVideoDir, video.videoFilename);
if (currentVideoPath && currentVideoPath !== targetVideoPath) {
moveFile(currentVideoPath, targetVideoPath);
updates.videoPath = `${videoPathPrefix}/${video.videoFilename}`;
updated = true;
}
}
// Move image file
if (video.thumbnailFilename) {
const currentImagePath = findImageFile(video.thumbnailFilename);
const targetImagePath = path.join(targetImageDir, video.thumbnailFilename);
if (currentImagePath && currentImagePath !== targetImagePath) {
moveFile(currentImagePath, targetImagePath);
updates.thumbnailPath = `${imagePathPrefix}/${video.thumbnailFilename}`;
updated = true;
}
}
if (updated) {
updateVideo(videoId, updates);
}
}
}
return collection;
}
// Delete collection and move files back to root (or other collection)
export function deleteCollectionWithFiles(collectionId: string): boolean {
const collection = getCollectionById(collectionId);
if (!collection) return false;
const collectionName = collection.name || collection.title;
// Move files for all videos in the collection
if (collection.videos && collection.videos.length > 0) {
collection.videos.forEach(videoId => {
const video = getVideoById(videoId);
if (video) {
// Check if video is in any OTHER collection (excluding the one being deleted)
const allCollections = getCollections();
const otherCollection = allCollections.find(c => c.videos.includes(videoId) && c.id !== collectionId);
let targetVideoDir = VIDEOS_DIR;
let targetImageDir = IMAGES_DIR;
let videoPathPrefix = '/videos';
let imagePathPrefix = '/images';
if (otherCollection) {
const otherName = otherCollection.name || otherCollection.title;
if (otherName) {
targetVideoDir = path.join(VIDEOS_DIR, otherName);
targetImageDir = path.join(IMAGES_DIR, otherName);
videoPathPrefix = `/videos/${otherName}`;
imagePathPrefix = `/images/${otherName}`;
}
}
const updates: Partial<Video> = {};
let updated = false;
// Move video file
if (video.videoFilename) {
// We know it should be in the collection folder being deleted, but use findVideoFile to be safe
const currentVideoPath = findVideoFile(video.videoFilename);
const targetVideoPath = path.join(targetVideoDir, video.videoFilename);
if (currentVideoPath && currentVideoPath !== targetVideoPath) {
moveFile(currentVideoPath, targetVideoPath);
updates.videoPath = `${videoPathPrefix}/${video.videoFilename}`;
updated = true;
}
}
// Move image file
if (video.thumbnailFilename) {
const currentImagePath = findImageFile(video.thumbnailFilename);
const targetImagePath = path.join(targetImageDir, video.thumbnailFilename);
if (currentImagePath && currentImagePath !== targetImagePath) {
moveFile(currentImagePath, targetImagePath);
updates.thumbnailPath = `${imagePathPrefix}/${video.thumbnailFilename}`;
updated = true;
}
}
if (updated) {
updateVideo(videoId, updates);
}
}
});
}
// Delete the collection from DB
const success = deleteCollection(collectionId);
// Remove the collection directories if empty
if (success && collectionName) {
try {
const videoCollectionDir = path.join(VIDEOS_DIR, collectionName);
const imageCollectionDir = path.join(IMAGES_DIR, collectionName);
if (fs.existsSync(videoCollectionDir) && fs.readdirSync(videoCollectionDir).length === 0) {
fs.rmdirSync(videoCollectionDir);
}
if (fs.existsSync(imageCollectionDir) && fs.readdirSync(imageCollectionDir).length === 0) {
fs.rmdirSync(imageCollectionDir);
}
} catch (error) {
console.error("Error removing collection directories:", error);
}
}
return success;
}
// Delete collection and all its videos
export function deleteCollectionAndVideos(collectionId: string): boolean {
const collection = getCollectionById(collectionId);
if (!collection) return false;
const collectionName = collection.name || collection.title;
// Delete all videos in the collection
if (collection.videos && collection.videos.length > 0) {
// Create a copy of the videos array to iterate over safely
const videosToDelete = [...collection.videos];
videosToDelete.forEach(videoId => {
deleteVideo(videoId);
});
}
// Delete the collection from DB
const success = deleteCollection(collectionId);
// Remove the collection directories
if (success && collectionName) {
try {
const videoCollectionDir = path.join(VIDEOS_DIR, collectionName);
const imageCollectionDir = path.join(IMAGES_DIR, collectionName);
if (fs.existsSync(videoCollectionDir)) {
fs.rmdirSync(videoCollectionDir);
}
if (fs.existsSync(imageCollectionDir)) {
fs.rmdirSync(imageCollectionDir);
}
} catch (error) {
console.error("Error removing collection directories:", error);
}
}
return success;
}