refactor: Consolidate file moving operations into file manager
This commit is contained in:
473
backend/src/services/storageService/collectionFileManager.ts
Normal file
473
backend/src/services/storageService/collectionFileManager.ts
Normal file
@@ -0,0 +1,473 @@
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import {
|
||||
IMAGES_DIR,
|
||||
SUBTITLES_DIR,
|
||||
UPLOADS_DIR,
|
||||
VIDEOS_DIR,
|
||||
} from "../../config/paths";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { findImageFile, findVideoFile, moveFile } from "./fileHelpers";
|
||||
import { getSettings } from "./settings";
|
||||
import { Collection, Video } from "./types";
|
||||
|
||||
/**
|
||||
* File manager layer for collection-related file operations
|
||||
* This module handles all file system operations when videos are added/removed from collections
|
||||
*/
|
||||
|
||||
export interface FileMoveResult {
|
||||
updated: boolean;
|
||||
updates: Partial<Video>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move video files to a collection directory
|
||||
*/
|
||||
export function moveVideoToCollection(
|
||||
video: Video,
|
||||
collectionName: string,
|
||||
allCollections: Collection[]
|
||||
): FileMoveResult {
|
||||
const updates: Partial<Video> = {};
|
||||
let updated = false;
|
||||
|
||||
if (video.videoFilename) {
|
||||
const currentVideoPath = findVideoFile(video.videoFilename, allCollections);
|
||||
const targetVideoPath = path.join(
|
||||
VIDEOS_DIR,
|
||||
collectionName,
|
||||
video.videoFilename
|
||||
);
|
||||
|
||||
if (currentVideoPath && currentVideoPath !== targetVideoPath) {
|
||||
moveFile(currentVideoPath, targetVideoPath);
|
||||
updates.videoPath = `/videos/${collectionName}/${video.videoFilename}`;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
return { updated, updates };
|
||||
}
|
||||
|
||||
/**
|
||||
* Move video files from a collection directory (to root or another collection)
|
||||
*/
|
||||
export function moveVideoFromCollection(
|
||||
video: Video,
|
||||
targetVideoDir: string,
|
||||
videoPathPrefix: string,
|
||||
allCollections: Collection[]
|
||||
): FileMoveResult {
|
||||
const updates: Partial<Video> = {};
|
||||
let updated = false;
|
||||
|
||||
if (video.videoFilename) {
|
||||
const currentVideoPath = findVideoFile(video.videoFilename, allCollections);
|
||||
const targetVideoPath = path.join(targetVideoDir, video.videoFilename);
|
||||
|
||||
if (currentVideoPath && currentVideoPath !== targetVideoPath) {
|
||||
moveFile(currentVideoPath, targetVideoPath);
|
||||
updates.videoPath = `${videoPathPrefix}/${video.videoFilename}`;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
return { updated, updates };
|
||||
}
|
||||
|
||||
/**
|
||||
* Move thumbnail files to a collection directory
|
||||
*/
|
||||
export function moveThumbnailToCollection(
|
||||
video: Video,
|
||||
collectionName: string,
|
||||
allCollections: Collection[]
|
||||
): FileMoveResult {
|
||||
const updates: Partial<Video> = {};
|
||||
let updated = false;
|
||||
|
||||
if (video.thumbnailFilename) {
|
||||
// Find existing file using path from DB if possible, or fallback to search
|
||||
let currentImagePath = "";
|
||||
if (video.thumbnailPath) {
|
||||
if (video.thumbnailPath.startsWith("/videos/")) {
|
||||
currentImagePath = path.join(
|
||||
VIDEOS_DIR,
|
||||
video.thumbnailPath.replace(/^\/videos\//, "")
|
||||
);
|
||||
} else if (video.thumbnailPath.startsWith("/images/")) {
|
||||
currentImagePath = path.join(
|
||||
IMAGES_DIR,
|
||||
video.thumbnailPath.replace(/^\/images\//, "")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentImagePath || !fs.existsSync(currentImagePath)) {
|
||||
currentImagePath =
|
||||
findImageFile(video.thumbnailFilename, allCollections) || "";
|
||||
}
|
||||
|
||||
// Determine target
|
||||
const settings = getSettings();
|
||||
const moveWithVideo = settings.moveThumbnailsToVideoFolder;
|
||||
|
||||
let targetImagePath = "";
|
||||
let newWebPath = "";
|
||||
|
||||
if (moveWithVideo) {
|
||||
targetImagePath = path.join(
|
||||
VIDEOS_DIR,
|
||||
collectionName,
|
||||
video.thumbnailFilename
|
||||
);
|
||||
newWebPath = `/videos/${collectionName}/${video.thumbnailFilename}`;
|
||||
} else {
|
||||
targetImagePath = path.join(
|
||||
IMAGES_DIR,
|
||||
collectionName,
|
||||
video.thumbnailFilename
|
||||
);
|
||||
newWebPath = `/images/${collectionName}/${video.thumbnailFilename}`;
|
||||
}
|
||||
|
||||
if (currentImagePath && currentImagePath !== targetImagePath) {
|
||||
moveFile(currentImagePath, targetImagePath);
|
||||
updates.thumbnailPath = newWebPath;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
return { updated, updates };
|
||||
}
|
||||
|
||||
/**
|
||||
* Move thumbnail files from a collection directory (to root or another collection)
|
||||
*/
|
||||
export function moveThumbnailFromCollection(
|
||||
video: Video,
|
||||
targetVideoDir: string,
|
||||
targetImageDir: string,
|
||||
videoPathPrefix: string,
|
||||
imagePathPrefix: string,
|
||||
allCollections: Collection[]
|
||||
): FileMoveResult {
|
||||
const updates: Partial<Video> = {};
|
||||
let updated = false;
|
||||
|
||||
if (video.thumbnailFilename) {
|
||||
// Find existing file using path from DB if possible
|
||||
let currentImagePath = "";
|
||||
if (video.thumbnailPath) {
|
||||
if (video.thumbnailPath.startsWith("/videos/")) {
|
||||
currentImagePath = path.join(
|
||||
VIDEOS_DIR,
|
||||
video.thumbnailPath.replace(/^\/videos\//, "")
|
||||
);
|
||||
} else if (video.thumbnailPath.startsWith("/images/")) {
|
||||
currentImagePath = path.join(
|
||||
IMAGES_DIR,
|
||||
video.thumbnailPath.replace(/^\/images\//, "")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentImagePath || !fs.existsSync(currentImagePath)) {
|
||||
currentImagePath =
|
||||
findImageFile(video.thumbnailFilename, allCollections) || "";
|
||||
}
|
||||
|
||||
// Determine target
|
||||
const settings = getSettings();
|
||||
const moveWithVideo = settings.moveThumbnailsToVideoFolder;
|
||||
|
||||
let targetImagePath = "";
|
||||
let newWebPath = "";
|
||||
|
||||
if (moveWithVideo) {
|
||||
// Target is same as video target
|
||||
targetImagePath = path.join(targetVideoDir, video.thumbnailFilename);
|
||||
newWebPath = `${videoPathPrefix}/${video.thumbnailFilename}`;
|
||||
} else {
|
||||
// Target is image dir (root or other collection)
|
||||
targetImagePath = path.join(targetImageDir, video.thumbnailFilename);
|
||||
newWebPath = `${imagePathPrefix}/${video.thumbnailFilename}`;
|
||||
}
|
||||
|
||||
if (currentImagePath && currentImagePath !== targetImagePath) {
|
||||
moveFile(currentImagePath, targetImagePath);
|
||||
updates.thumbnailPath = newWebPath;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
return { updated, updates };
|
||||
}
|
||||
|
||||
/**
|
||||
* Move subtitle files to a collection directory
|
||||
*/
|
||||
export function moveSubtitlesToCollection(
|
||||
video: Video,
|
||||
collectionName: string
|
||||
): FileMoveResult {
|
||||
const updates: Partial<Video> = {};
|
||||
let updated = false;
|
||||
|
||||
if (video.subtitles && video.subtitles.length > 0) {
|
||||
const newSubtitles = [...video.subtitles];
|
||||
let subtitlesUpdated = false;
|
||||
|
||||
newSubtitles.forEach((sub, index) => {
|
||||
let currentSubPath = sub.path;
|
||||
// Determine existing absolute path
|
||||
let absoluteSourcePath = "";
|
||||
if (sub.path.startsWith("/videos/")) {
|
||||
absoluteSourcePath = path.join(
|
||||
VIDEOS_DIR,
|
||||
sub.path.replace("/videos/", "")
|
||||
);
|
||||
} else if (sub.path.startsWith("/subtitles/")) {
|
||||
absoluteSourcePath = path.join(path.dirname(SUBTITLES_DIR), sub.path); // SUBTITLES_DIR is uploads/subtitles
|
||||
}
|
||||
|
||||
let targetSubDir = "";
|
||||
let newWebPath = "";
|
||||
|
||||
// Logic:
|
||||
// If it's currently in VIDEOS_DIR (starts with /videos/), it should stay with video -> move to new video folder
|
||||
// If it's currently in SUBTITLES_DIR (starts with /subtitles/), it should move to new mirror folder in SUBTITLES_DIR
|
||||
|
||||
if (sub.path.startsWith("/videos/")) {
|
||||
targetSubDir = path.join(VIDEOS_DIR, collectionName);
|
||||
newWebPath = `/videos/${collectionName}/${path.basename(sub.path)}`;
|
||||
} else if (sub.path.startsWith("/subtitles/")) {
|
||||
targetSubDir = path.join(SUBTITLES_DIR, collectionName);
|
||||
newWebPath = `/subtitles/${collectionName}/${path.basename(sub.path)}`;
|
||||
}
|
||||
|
||||
if (absoluteSourcePath && targetSubDir && newWebPath) {
|
||||
const targetSubPath = path.join(targetSubDir, path.basename(sub.path));
|
||||
if (
|
||||
fs.existsSync(absoluteSourcePath) &&
|
||||
absoluteSourcePath !== targetSubPath
|
||||
) {
|
||||
moveFile(absoluteSourcePath, targetSubPath);
|
||||
newSubtitles[index] = {
|
||||
...sub,
|
||||
path: newWebPath,
|
||||
};
|
||||
subtitlesUpdated = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (subtitlesUpdated) {
|
||||
updates.subtitles = newSubtitles;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
return { updated, updates };
|
||||
}
|
||||
|
||||
/**
|
||||
* Move subtitle files from a collection directory (to root or another collection)
|
||||
*/
|
||||
export function moveSubtitlesFromCollection(
|
||||
video: Video,
|
||||
targetVideoDir: string,
|
||||
targetSubDir: string,
|
||||
videoPathPrefix: string,
|
||||
subtitlePathPrefix?: string
|
||||
): FileMoveResult {
|
||||
const updates: Partial<Video> = {};
|
||||
let updated = false;
|
||||
|
||||
if (video.subtitles && video.subtitles.length > 0) {
|
||||
const newSubtitles = [...video.subtitles];
|
||||
let subtitlesUpdated = false;
|
||||
|
||||
newSubtitles.forEach((sub, index) => {
|
||||
let absoluteSourcePath = "";
|
||||
// Construct absolute source path based on DB path
|
||||
if (sub.path.startsWith("/videos/")) {
|
||||
absoluteSourcePath = path.join(
|
||||
VIDEOS_DIR,
|
||||
sub.path.replace("/videos/", "")
|
||||
);
|
||||
} else if (sub.path.startsWith("/subtitles/")) {
|
||||
// sub.path is like /subtitles/Collection/file.vtt
|
||||
// SUBTITLES_DIR is uploads/subtitles
|
||||
absoluteSourcePath = path.join(
|
||||
UPLOADS_DIR,
|
||||
sub.path.replace(/^\//, "")
|
||||
); // path.join(headers...) -> uploads/subtitles/...
|
||||
}
|
||||
|
||||
let targetSubDirPath = "";
|
||||
let newWebPath = "";
|
||||
|
||||
if (sub.path.startsWith("/videos/")) {
|
||||
targetSubDirPath = targetVideoDir; // Calculated above (root or other collection)
|
||||
newWebPath = `${videoPathPrefix}/${path.basename(sub.path)}`;
|
||||
} else if (sub.path.startsWith("/subtitles/")) {
|
||||
// Should move to root subtitles or other collection subtitles
|
||||
targetSubDirPath = targetSubDir;
|
||||
newWebPath = subtitlePathPrefix
|
||||
? `${subtitlePathPrefix}/${path.basename(sub.path)}`
|
||||
: `/subtitles/${path.basename(sub.path)}`;
|
||||
}
|
||||
|
||||
if (absoluteSourcePath && targetSubDirPath && newWebPath) {
|
||||
const targetSubPath = path.join(
|
||||
targetSubDirPath,
|
||||
path.basename(sub.path)
|
||||
);
|
||||
|
||||
// Ensure correct paths for move
|
||||
// Need to handle potential double slashes or construction issues if any
|
||||
if (
|
||||
fs.existsSync(absoluteSourcePath) &&
|
||||
absoluteSourcePath !== targetSubPath
|
||||
) {
|
||||
moveFile(absoluteSourcePath, targetSubPath);
|
||||
newSubtitles[index] = {
|
||||
...sub,
|
||||
path: newWebPath,
|
||||
};
|
||||
subtitlesUpdated = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (subtitlesUpdated) {
|
||||
updates.subtitles = newSubtitles;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
return { updated, updates };
|
||||
}
|
||||
|
||||
/**
|
||||
* Move all video files (video, thumbnail, subtitles) to a collection
|
||||
*/
|
||||
export function moveAllFilesToCollection(
|
||||
video: Video,
|
||||
collectionName: string,
|
||||
allCollections: Collection[]
|
||||
): Partial<Video> {
|
||||
const allUpdates: Partial<Video> = {};
|
||||
|
||||
// Move video file
|
||||
const videoResult = moveVideoToCollection(
|
||||
video,
|
||||
collectionName,
|
||||
allCollections
|
||||
);
|
||||
if (videoResult.updated) {
|
||||
Object.assign(allUpdates, videoResult.updates);
|
||||
}
|
||||
|
||||
// Move thumbnail
|
||||
const thumbnailResult = moveThumbnailToCollection(
|
||||
video,
|
||||
collectionName,
|
||||
allCollections
|
||||
);
|
||||
if (thumbnailResult.updated) {
|
||||
Object.assign(allUpdates, thumbnailResult.updates);
|
||||
}
|
||||
|
||||
// Move subtitles
|
||||
const subtitlesResult = moveSubtitlesToCollection(video, collectionName);
|
||||
if (subtitlesResult.updated) {
|
||||
Object.assign(allUpdates, subtitlesResult.updates);
|
||||
}
|
||||
|
||||
return allUpdates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move all video files (video, thumbnail, subtitles) from a collection
|
||||
*/
|
||||
export function moveAllFilesFromCollection(
|
||||
video: Video,
|
||||
targetVideoDir: string,
|
||||
targetImageDir: string,
|
||||
targetSubDir: string,
|
||||
videoPathPrefix: string,
|
||||
imagePathPrefix: string,
|
||||
subtitlePathPrefix: string | undefined,
|
||||
allCollections: Collection[]
|
||||
): Partial<Video> {
|
||||
const allUpdates: Partial<Video> = {};
|
||||
|
||||
// Move video file
|
||||
const videoResult = moveVideoFromCollection(
|
||||
video,
|
||||
targetVideoDir,
|
||||
videoPathPrefix,
|
||||
allCollections
|
||||
);
|
||||
if (videoResult.updated) {
|
||||
Object.assign(allUpdates, videoResult.updates);
|
||||
}
|
||||
|
||||
// Move thumbnail
|
||||
const thumbnailResult = moveThumbnailFromCollection(
|
||||
video,
|
||||
targetVideoDir,
|
||||
targetImageDir,
|
||||
videoPathPrefix,
|
||||
imagePathPrefix,
|
||||
allCollections
|
||||
);
|
||||
if (thumbnailResult.updated) {
|
||||
Object.assign(allUpdates, thumbnailResult.updates);
|
||||
}
|
||||
|
||||
// Move subtitles
|
||||
const subtitlesResult = moveSubtitlesFromCollection(
|
||||
video,
|
||||
targetVideoDir,
|
||||
targetSubDir,
|
||||
videoPathPrefix,
|
||||
subtitlePathPrefix
|
||||
);
|
||||
if (subtitlesResult.updated) {
|
||||
Object.assign(allUpdates, subtitlesResult.updates);
|
||||
}
|
||||
|
||||
return allUpdates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up empty collection directories
|
||||
*/
|
||||
export function cleanupCollectionDirectories(collectionName: string): void {
|
||||
const collectionVideoDir = path.join(VIDEOS_DIR, collectionName);
|
||||
const collectionImageDir = path.join(IMAGES_DIR, collectionName);
|
||||
|
||||
try {
|
||||
if (
|
||||
fs.existsSync(collectionVideoDir) &&
|
||||
fs.readdirSync(collectionVideoDir).length === 0
|
||||
) {
|
||||
fs.rmdirSync(collectionVideoDir);
|
||||
}
|
||||
if (
|
||||
fs.existsSync(collectionImageDir) &&
|
||||
fs.readdirSync(collectionImageDir).length === 0
|
||||
) {
|
||||
fs.rmdirSync(collectionImageDir);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
"Error removing collection directories",
|
||||
e instanceof Error ? e : new Error(String(e))
|
||||
);
|
||||
}
|
||||
}
|
||||
253
backend/src/services/storageService/collectionRepository.ts
Normal file
253
backend/src/services/storageService/collectionRepository.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../../db";
|
||||
import { collections, collectionVideos, videos } from "../../db/schema";
|
||||
import { DatabaseError } from "../../errors/DownloadErrors";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { Collection } from "./types";
|
||||
|
||||
/**
|
||||
* Repository layer for collection database operations
|
||||
* This module handles all direct database interactions for collections
|
||||
*/
|
||||
|
||||
export function getCollections(): Collection[] {
|
||||
try {
|
||||
const rows = db
|
||||
.select({
|
||||
c: collections,
|
||||
cv: collectionVideos,
|
||||
})
|
||||
.from(collections)
|
||||
.leftJoin(
|
||||
collectionVideos,
|
||||
eq(collections.id, collectionVideos.collectionId)
|
||||
)
|
||||
.all();
|
||||
|
||||
const map = new Map<string, Collection>();
|
||||
for (const row of rows) {
|
||||
if (!map.has(row.c.id)) {
|
||||
map.set(row.c.id, {
|
||||
...row.c,
|
||||
title: row.c.title || row.c.name,
|
||||
updatedAt: row.c.updatedAt || undefined,
|
||||
videos: [],
|
||||
});
|
||||
}
|
||||
if (row.cv) {
|
||||
map.get(row.c.id)!.videos.push(row.cv.videoId);
|
||||
}
|
||||
}
|
||||
return Array.from(map.values());
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Error getting collections",
|
||||
error instanceof Error ? error : new Error(String(error))
|
||||
);
|
||||
// Return empty array for backward compatibility with frontend
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function getCollectionById(id: string): Collection | undefined {
|
||||
try {
|
||||
const rows = db
|
||||
.select({
|
||||
c: collections,
|
||||
cv: collectionVideos,
|
||||
})
|
||||
.from(collections)
|
||||
.leftJoin(
|
||||
collectionVideos,
|
||||
eq(collections.id, collectionVideos.collectionId)
|
||||
)
|
||||
.where(eq(collections.id, id))
|
||||
.all();
|
||||
|
||||
if (rows.length === 0) return undefined;
|
||||
|
||||
const collection: Collection = {
|
||||
...rows[0].c,
|
||||
title: rows[0].c.title || rows[0].c.name,
|
||||
updatedAt: rows[0].c.updatedAt || undefined,
|
||||
videos: [],
|
||||
};
|
||||
|
||||
for (const row of rows) {
|
||||
if (row.cv) {
|
||||
collection.videos.push(row.cv.videoId);
|
||||
}
|
||||
}
|
||||
|
||||
return collection;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Error getting collection by id",
|
||||
error instanceof Error ? error : new Error(String(error))
|
||||
);
|
||||
throw new DatabaseError(
|
||||
`Failed to get collection by id: ${id}`,
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
"getCollectionById"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a collection that contains a specific video
|
||||
*/
|
||||
export function getCollectionByVideoId(
|
||||
videoId: string
|
||||
): Collection | undefined {
|
||||
try {
|
||||
const rows = db
|
||||
.select({
|
||||
c: collections,
|
||||
cv: collectionVideos,
|
||||
})
|
||||
.from(collections)
|
||||
.innerJoin(
|
||||
collectionVideos,
|
||||
eq(collections.id, collectionVideos.collectionId)
|
||||
)
|
||||
.where(eq(collectionVideos.videoId, videoId))
|
||||
.all();
|
||||
|
||||
if (rows.length === 0) return undefined;
|
||||
|
||||
// Get the first collection that contains this video
|
||||
const collectionId = rows[0].c.id;
|
||||
return getCollectionById(collectionId);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Error getting collection by video id",
|
||||
error instanceof Error ? error : new Error(String(error))
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a collection by name or title
|
||||
*/
|
||||
export function getCollectionByName(name: string): Collection | undefined {
|
||||
try {
|
||||
const allCollections = getCollections();
|
||||
return allCollections.find((c) => c.name === name || c.title === name);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Error getting collection by name",
|
||||
error instanceof Error ? error : new Error(String(error))
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveCollection(collection: Collection): Collection {
|
||||
try {
|
||||
db.transaction(() => {
|
||||
// Insert collection
|
||||
db.insert(collections)
|
||||
.values({
|
||||
id: collection.id,
|
||||
name: collection.name || collection.title,
|
||||
title: collection.title,
|
||||
createdAt: collection.createdAt || new Date().toISOString(),
|
||||
updatedAt: collection.updatedAt,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: collections.id,
|
||||
set: {
|
||||
name: collection.name || collection.title,
|
||||
title: collection.title,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
.run();
|
||||
|
||||
// Sync videos
|
||||
// First delete existing links
|
||||
db.delete(collectionVideos)
|
||||
.where(eq(collectionVideos.collectionId, collection.id))
|
||||
.run();
|
||||
|
||||
// Then insert new links
|
||||
if (collection.videos && collection.videos.length > 0) {
|
||||
for (const videoId of collection.videos) {
|
||||
// Check if video exists to avoid FK error
|
||||
const videoExists = db
|
||||
.select({ id: videos.id })
|
||||
.from(videos)
|
||||
.where(eq(videos.id, videoId))
|
||||
.get();
|
||||
if (videoExists) {
|
||||
db.insert(collectionVideos)
|
||||
.values({
|
||||
collectionId: collection.id,
|
||||
videoId: videoId,
|
||||
})
|
||||
.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return collection;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Error saving collection",
|
||||
error instanceof Error ? error : new Error(String(error))
|
||||
);
|
||||
throw new DatabaseError(
|
||||
`Failed to save collection: ${collection.id}`,
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
"saveCollection"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function atomicUpdateCollection(
|
||||
id: string,
|
||||
updateFn: (collection: Collection) => Collection | null
|
||||
): Collection | null {
|
||||
try {
|
||||
const collection = getCollectionById(id);
|
||||
if (!collection) return null;
|
||||
|
||||
// Deep copy not strictly needed as we reconstruct, but good for safety if updateFn mutates
|
||||
const collectionCopy = JSON.parse(JSON.stringify(collection));
|
||||
const updatedCollection = updateFn(collectionCopy);
|
||||
|
||||
if (!updatedCollection) return null;
|
||||
|
||||
updatedCollection.updatedAt = new Date().toISOString();
|
||||
saveCollection(updatedCollection);
|
||||
return updatedCollection;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Error atomic updating collection",
|
||||
error instanceof Error ? error : new Error(String(error))
|
||||
);
|
||||
throw new DatabaseError(
|
||||
`Failed to atomically update collection: ${id}`,
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
"atomicUpdateCollection"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteCollection(id: string): boolean {
|
||||
try {
|
||||
const result = db.delete(collections).where(eq(collections.id, id)).run();
|
||||
return result.changes > 0;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Error deleting collection",
|
||||
error instanceof Error ? error : new Error(String(error))
|
||||
);
|
||||
throw new DatabaseError(
|
||||
`Failed to delete collection: ${id}`,
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
"deleteCollection"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,152 +1,45 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import {
|
||||
IMAGES_DIR,
|
||||
SUBTITLES_DIR,
|
||||
UPLOADS_DIR,
|
||||
VIDEOS_DIR,
|
||||
} from "../../config/paths";
|
||||
import { db } from "../../db";
|
||||
import { collections, collectionVideos, videos } from "../../db/schema";
|
||||
import { DatabaseError } from "../../errors/DownloadErrors";
|
||||
import { IMAGES_DIR, SUBTITLES_DIR, VIDEOS_DIR } from "../../config/paths";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { findImageFile, findVideoFile, moveFile } from "./fileHelpers";
|
||||
import { getSettings } from "./settings";
|
||||
import { Collection, Video } from "./types";
|
||||
import {
|
||||
cleanupCollectionDirectories,
|
||||
moveAllFilesFromCollection,
|
||||
moveAllFilesToCollection,
|
||||
} from "./collectionFileManager";
|
||||
import {
|
||||
atomicUpdateCollection as atomicUpdateCollectionRepo,
|
||||
deleteCollection as deleteCollectionRepo,
|
||||
getCollectionById as getCollectionByIdRepo,
|
||||
getCollectionByName as getCollectionByNameRepo,
|
||||
getCollectionByVideoId as getCollectionByVideoIdRepo,
|
||||
getCollections as getCollectionsRepo,
|
||||
saveCollection as saveCollectionRepo,
|
||||
} from "./collectionRepository";
|
||||
import { Collection } from "./types";
|
||||
import { deleteVideo, getVideoById, updateVideo } from "./videos";
|
||||
|
||||
export function getCollections(): Collection[] {
|
||||
try {
|
||||
const rows = db
|
||||
.select({
|
||||
c: collections,
|
||||
cv: collectionVideos,
|
||||
})
|
||||
.from(collections)
|
||||
.leftJoin(
|
||||
collectionVideos,
|
||||
eq(collections.id, collectionVideos.collectionId)
|
||||
)
|
||||
.all();
|
||||
|
||||
const map = new Map<string, Collection>();
|
||||
for (const row of rows) {
|
||||
if (!map.has(row.c.id)) {
|
||||
map.set(row.c.id, {
|
||||
...row.c,
|
||||
title: row.c.title || row.c.name,
|
||||
updatedAt: row.c.updatedAt || undefined,
|
||||
videos: [],
|
||||
});
|
||||
}
|
||||
if (row.cv) {
|
||||
map.get(row.c.id)!.videos.push(row.cv.videoId);
|
||||
}
|
||||
}
|
||||
return Array.from(map.values());
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Error getting collections",
|
||||
error instanceof Error ? error : new Error(String(error))
|
||||
);
|
||||
// Return empty array for backward compatibility with frontend
|
||||
return [];
|
||||
}
|
||||
return getCollectionsRepo();
|
||||
}
|
||||
|
||||
export function getCollectionById(id: string): Collection | undefined {
|
||||
try {
|
||||
const rows = db
|
||||
.select({
|
||||
c: collections,
|
||||
cv: collectionVideos,
|
||||
})
|
||||
.from(collections)
|
||||
.leftJoin(
|
||||
collectionVideos,
|
||||
eq(collections.id, collectionVideos.collectionId)
|
||||
)
|
||||
.where(eq(collections.id, id))
|
||||
.all();
|
||||
|
||||
if (rows.length === 0) return undefined;
|
||||
|
||||
const collection: Collection = {
|
||||
...rows[0].c,
|
||||
title: rows[0].c.title || rows[0].c.name,
|
||||
updatedAt: rows[0].c.updatedAt || undefined,
|
||||
videos: [],
|
||||
};
|
||||
|
||||
for (const row of rows) {
|
||||
if (row.cv) {
|
||||
collection.videos.push(row.cv.videoId);
|
||||
}
|
||||
}
|
||||
|
||||
return collection;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Error getting collection by id",
|
||||
error instanceof Error ? error : new Error(String(error))
|
||||
);
|
||||
throw new DatabaseError(
|
||||
`Failed to get collection by id: ${id}`,
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
"getCollectionById"
|
||||
);
|
||||
}
|
||||
return getCollectionByIdRepo(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a collection that contains a specific video
|
||||
*/
|
||||
export function getCollectionByVideoId(videoId: string): Collection | undefined {
|
||||
try {
|
||||
const rows = db
|
||||
.select({
|
||||
c: collections,
|
||||
cv: collectionVideos,
|
||||
})
|
||||
.from(collections)
|
||||
.innerJoin(
|
||||
collectionVideos,
|
||||
eq(collections.id, collectionVideos.collectionId)
|
||||
)
|
||||
.where(eq(collectionVideos.videoId, videoId))
|
||||
.all();
|
||||
|
||||
if (rows.length === 0) return undefined;
|
||||
|
||||
// Get the first collection that contains this video
|
||||
const collectionId = rows[0].c.id;
|
||||
return getCollectionById(collectionId);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Error getting collection by video id",
|
||||
error instanceof Error ? error : new Error(String(error))
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
export function getCollectionByVideoId(
|
||||
videoId: string
|
||||
): Collection | undefined {
|
||||
return getCollectionByVideoIdRepo(videoId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a collection by name or title
|
||||
*/
|
||||
export function getCollectionByName(name: string): Collection | undefined {
|
||||
try {
|
||||
const allCollections = getCollections();
|
||||
return allCollections.find(
|
||||
(c) => c.name === name || c.title === name
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Error getting collection by name",
|
||||
error instanceof Error ? error : new Error(String(error))
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
return getCollectionByNameRepo(name);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,7 +48,7 @@ export function getCollectionByName(name: string): Collection | undefined {
|
||||
* @returns A unique collection name
|
||||
*/
|
||||
export function generateUniqueCollectionName(baseName: string): string {
|
||||
const existingCollection = getCollectionByName(baseName);
|
||||
const existingCollection = getCollectionByNameRepo(baseName);
|
||||
if (!existingCollection) {
|
||||
return baseName;
|
||||
}
|
||||
@@ -164,7 +57,7 @@ export function generateUniqueCollectionName(baseName: string): string {
|
||||
let counter = 2;
|
||||
let uniqueName = `${baseName} (${counter})`;
|
||||
|
||||
while (getCollectionByName(uniqueName)) {
|
||||
while (getCollectionByNameRepo(uniqueName)) {
|
||||
counter++;
|
||||
uniqueName = `${baseName} (${counter})`;
|
||||
}
|
||||
@@ -176,112 +69,18 @@ export function generateUniqueCollectionName(baseName: string): string {
|
||||
}
|
||||
|
||||
export function saveCollection(collection: Collection): Collection {
|
||||
try {
|
||||
db.transaction(() => {
|
||||
// Insert collection
|
||||
db.insert(collections)
|
||||
.values({
|
||||
id: collection.id,
|
||||
name: collection.name || collection.title,
|
||||
title: collection.title,
|
||||
createdAt: collection.createdAt || new Date().toISOString(),
|
||||
updatedAt: collection.updatedAt,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: collections.id,
|
||||
set: {
|
||||
name: collection.name || collection.title,
|
||||
title: collection.title,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
.run();
|
||||
|
||||
// Sync videos
|
||||
// First delete existing links
|
||||
db.delete(collectionVideos)
|
||||
.where(eq(collectionVideos.collectionId, collection.id))
|
||||
.run();
|
||||
|
||||
// Then insert new links
|
||||
if (collection.videos && collection.videos.length > 0) {
|
||||
for (const videoId of collection.videos) {
|
||||
// Check if video exists to avoid FK error
|
||||
const videoExists = db
|
||||
.select({ id: videos.id })
|
||||
.from(videos)
|
||||
.where(eq(videos.id, videoId))
|
||||
.get();
|
||||
if (videoExists) {
|
||||
db.insert(collectionVideos)
|
||||
.values({
|
||||
collectionId: collection.id,
|
||||
videoId: videoId,
|
||||
})
|
||||
.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return collection;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Error saving collection",
|
||||
error instanceof Error ? error : new Error(String(error))
|
||||
);
|
||||
throw new DatabaseError(
|
||||
`Failed to save collection: ${collection.id}`,
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
"saveCollection"
|
||||
);
|
||||
}
|
||||
return saveCollectionRepo(collection);
|
||||
}
|
||||
|
||||
export function atomicUpdateCollection(
|
||||
id: string,
|
||||
updateFn: (collection: Collection) => Collection | null
|
||||
): Collection | null {
|
||||
try {
|
||||
const collection = getCollectionById(id);
|
||||
if (!collection) return null;
|
||||
|
||||
// Deep copy not strictly needed as we reconstruct, but good for safety if updateFn mutates
|
||||
const collectionCopy = JSON.parse(JSON.stringify(collection));
|
||||
const updatedCollection = updateFn(collectionCopy);
|
||||
|
||||
if (!updatedCollection) return null;
|
||||
|
||||
updatedCollection.updatedAt = new Date().toISOString();
|
||||
saveCollection(updatedCollection);
|
||||
return updatedCollection;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Error atomic updating collection",
|
||||
error instanceof Error ? error : new Error(String(error))
|
||||
);
|
||||
throw new DatabaseError(
|
||||
`Failed to atomically update collection: ${id}`,
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
"atomicUpdateCollection"
|
||||
);
|
||||
}
|
||||
return atomicUpdateCollectionRepo(id, updateFn);
|
||||
}
|
||||
|
||||
export function deleteCollection(id: string): boolean {
|
||||
try {
|
||||
const result = db.delete(collections).where(eq(collections.id, id)).run();
|
||||
return result.changes > 0;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Error deleting collection",
|
||||
error instanceof Error ? error : new Error(String(error))
|
||||
);
|
||||
throw new DatabaseError(
|
||||
`Failed to delete collection: ${id}`,
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
"deleteCollection"
|
||||
);
|
||||
}
|
||||
return deleteCollectionRepo(id);
|
||||
}
|
||||
|
||||
export function addVideoToCollection(
|
||||
@@ -302,149 +101,14 @@ export function addVideoToCollection(
|
||||
const allCollections = getCollections();
|
||||
|
||||
if (video && collectionName) {
|
||||
const updates: Partial<Video> = {};
|
||||
let updated = false;
|
||||
// Use file manager to move all files
|
||||
const updates = moveAllFilesToCollection(
|
||||
video,
|
||||
collectionName,
|
||||
allCollections
|
||||
);
|
||||
|
||||
if (video.videoFilename) {
|
||||
const currentVideoPath = findVideoFile(
|
||||
video.videoFilename,
|
||||
allCollections
|
||||
);
|
||||
const targetVideoPath = path.join(
|
||||
VIDEOS_DIR,
|
||||
collectionName,
|
||||
video.videoFilename
|
||||
);
|
||||
|
||||
if (currentVideoPath && currentVideoPath !== targetVideoPath) {
|
||||
moveFile(currentVideoPath, targetVideoPath);
|
||||
updates.videoPath = `/videos/${collectionName}/${video.videoFilename}`;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (video.thumbnailFilename) {
|
||||
// Find existing file using path from DB if possible, or fallback to search
|
||||
let currentImagePath = "";
|
||||
if (video.thumbnailPath) {
|
||||
if (video.thumbnailPath.startsWith("/videos/")) {
|
||||
currentImagePath = path.join(
|
||||
VIDEOS_DIR,
|
||||
video.thumbnailPath.replace(/^\/videos\//, "")
|
||||
);
|
||||
} else if (video.thumbnailPath.startsWith("/images/")) {
|
||||
currentImagePath = path.join(
|
||||
IMAGES_DIR,
|
||||
video.thumbnailPath.replace(/^\/images\//, "")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentImagePath || !fs.existsSync(currentImagePath)) {
|
||||
currentImagePath =
|
||||
findImageFile(video.thumbnailFilename, allCollections) || "";
|
||||
}
|
||||
|
||||
// Determine target
|
||||
const settings = getSettings();
|
||||
const moveWithVideo = settings.moveThumbnailsToVideoFolder;
|
||||
|
||||
let targetImagePath = "";
|
||||
let newWebPath = "";
|
||||
|
||||
if (moveWithVideo) {
|
||||
targetImagePath = path.join(
|
||||
VIDEOS_DIR,
|
||||
collectionName,
|
||||
video.thumbnailFilename
|
||||
);
|
||||
newWebPath = `/videos/${collectionName}/${video.thumbnailFilename}`;
|
||||
} else {
|
||||
targetImagePath = path.join(
|
||||
IMAGES_DIR,
|
||||
collectionName,
|
||||
video.thumbnailFilename
|
||||
);
|
||||
newWebPath = `/images/${collectionName}/${video.thumbnailFilename}`;
|
||||
}
|
||||
|
||||
if (currentImagePath && currentImagePath !== targetImagePath) {
|
||||
moveFile(currentImagePath, targetImagePath);
|
||||
updates.thumbnailPath = newWebPath;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle subtitles
|
||||
if (video.subtitles && video.subtitles.length > 0) {
|
||||
const newSubtitles = [...video.subtitles];
|
||||
let subtitlesUpdated = false;
|
||||
|
||||
newSubtitles.forEach((sub, index) => {
|
||||
let currentSubPath = sub.path;
|
||||
// Determine existing absolute path
|
||||
let absoluteSourcePath = "";
|
||||
if (sub.path.startsWith("/videos/")) {
|
||||
absoluteSourcePath = path.join(
|
||||
VIDEOS_DIR,
|
||||
sub.path.replace("/videos/", "")
|
||||
);
|
||||
} else if (sub.path.startsWith("/subtitles/")) {
|
||||
absoluteSourcePath = path.join(
|
||||
path.dirname(SUBTITLES_DIR),
|
||||
sub.path
|
||||
); // SUBTITLES_DIR is uploads/subtitles
|
||||
}
|
||||
|
||||
// If we can't determine source path easily from DB, try to find it
|
||||
if (!fs.existsSync(absoluteSourcePath)) {
|
||||
// Fallback: try finding in root or collection folders
|
||||
// But simpler to rely on path stored in DB if valid
|
||||
}
|
||||
|
||||
let targetSubDir = "";
|
||||
let newWebPath = "";
|
||||
|
||||
// Logic:
|
||||
// If it's currently in VIDEOS_DIR (starts with /videos/), it should stay with video -> move to new video folder
|
||||
// If it's currently in SUBTITLES_DIR (starts with /subtitles/), it should move to new mirror folder in SUBTITLES_DIR
|
||||
|
||||
if (sub.path.startsWith("/videos/")) {
|
||||
targetSubDir = path.join(VIDEOS_DIR, collectionName);
|
||||
newWebPath = `/videos/${collectionName}/${path.basename(sub.path)}`;
|
||||
} else if (sub.path.startsWith("/subtitles/")) {
|
||||
targetSubDir = path.join(SUBTITLES_DIR, collectionName);
|
||||
newWebPath = `/subtitles/${collectionName}/${path.basename(
|
||||
sub.path
|
||||
)}`;
|
||||
}
|
||||
|
||||
if (absoluteSourcePath && targetSubDir && newWebPath) {
|
||||
const targetSubPath = path.join(
|
||||
targetSubDir,
|
||||
path.basename(sub.path)
|
||||
);
|
||||
if (
|
||||
fs.existsSync(absoluteSourcePath) &&
|
||||
absoluteSourcePath !== targetSubPath
|
||||
) {
|
||||
moveFile(absoluteSourcePath, targetSubPath);
|
||||
newSubtitles[index] = {
|
||||
...sub,
|
||||
path: newWebPath,
|
||||
};
|
||||
subtitlesUpdated = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (subtitlesUpdated) {
|
||||
updates.subtitles = newSubtitles;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
if (Object.keys(updates).length > 0) {
|
||||
updateVideo(videoId, updates);
|
||||
}
|
||||
}
|
||||
@@ -474,156 +138,36 @@ export function removeVideoFromCollection(
|
||||
|
||||
let targetVideoDir = VIDEOS_DIR;
|
||||
let targetImageDir = IMAGES_DIR;
|
||||
let targetSubDir = SUBTITLES_DIR;
|
||||
let videoPathPrefix = "/videos";
|
||||
let imagePathPrefix = "/images";
|
||||
let subtitlePathPrefix: string | undefined = undefined;
|
||||
|
||||
if (otherCollection) {
|
||||
const otherName = otherCollection.name || otherCollection.title;
|
||||
if (otherName) {
|
||||
targetVideoDir = path.join(VIDEOS_DIR, otherName);
|
||||
targetImageDir = path.join(IMAGES_DIR, otherName);
|
||||
targetSubDir = path.join(SUBTITLES_DIR, otherName);
|
||||
videoPathPrefix = `/videos/${otherName}`;
|
||||
imagePathPrefix = `/images/${otherName}`;
|
||||
subtitlePathPrefix = `/subtitles/${otherName}`;
|
||||
}
|
||||
}
|
||||
|
||||
const updates: Partial<Video> = {};
|
||||
let updated = false;
|
||||
// Use file manager to move all files
|
||||
const updates = moveAllFilesFromCollection(
|
||||
video,
|
||||
targetVideoDir,
|
||||
targetImageDir,
|
||||
targetSubDir,
|
||||
videoPathPrefix,
|
||||
imagePathPrefix,
|
||||
subtitlePathPrefix,
|
||||
allCollections
|
||||
);
|
||||
|
||||
if (video.videoFilename) {
|
||||
const currentVideoPath = findVideoFile(
|
||||
video.videoFilename,
|
||||
allCollections
|
||||
);
|
||||
const targetVideoPath = path.join(targetVideoDir, video.videoFilename);
|
||||
|
||||
if (currentVideoPath && currentVideoPath !== targetVideoPath) {
|
||||
moveFile(currentVideoPath, targetVideoPath);
|
||||
updates.videoPath = `${videoPathPrefix}/${video.videoFilename}`;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (video.thumbnailFilename) {
|
||||
// Find existing file using path from DB if possible
|
||||
let currentImagePath = "";
|
||||
if (video.thumbnailPath) {
|
||||
if (video.thumbnailPath.startsWith("/videos/")) {
|
||||
currentImagePath = path.join(
|
||||
VIDEOS_DIR,
|
||||
video.thumbnailPath.replace(/^\/videos\//, "")
|
||||
);
|
||||
} else if (video.thumbnailPath.startsWith("/images/")) {
|
||||
currentImagePath = path.join(
|
||||
IMAGES_DIR,
|
||||
video.thumbnailPath.replace(/^\/images\//, "")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentImagePath || !fs.existsSync(currentImagePath)) {
|
||||
currentImagePath =
|
||||
findImageFile(video.thumbnailFilename, allCollections) || "";
|
||||
}
|
||||
|
||||
// Determine target
|
||||
const settings = getSettings();
|
||||
const moveWithVideo = settings.moveThumbnailsToVideoFolder;
|
||||
|
||||
let targetImagePath = "";
|
||||
let newWebPath = "";
|
||||
|
||||
if (moveWithVideo) {
|
||||
// Target is same as video target
|
||||
targetImagePath = path.join(targetVideoDir, video.thumbnailFilename);
|
||||
newWebPath = `${videoPathPrefix}/${video.thumbnailFilename}`;
|
||||
} else {
|
||||
// Target is image dir (root or other collection)
|
||||
targetImagePath = path.join(targetImageDir, video.thumbnailFilename);
|
||||
newWebPath = `${imagePathPrefix}/${video.thumbnailFilename}`;
|
||||
}
|
||||
|
||||
if (currentImagePath && currentImagePath !== targetImagePath) {
|
||||
moveFile(currentImagePath, targetImagePath);
|
||||
updates.thumbnailPath = newWebPath;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle subtitles
|
||||
if (video.subtitles && video.subtitles.length > 0) {
|
||||
const newSubtitles = [...video.subtitles];
|
||||
let subtitlesUpdated = false;
|
||||
|
||||
newSubtitles.forEach((sub, index) => {
|
||||
let absoluteSourcePath = "";
|
||||
// Construct absolute source path based on DB path
|
||||
if (sub.path.startsWith("/videos/")) {
|
||||
absoluteSourcePath = path.join(
|
||||
VIDEOS_DIR,
|
||||
sub.path.replace("/videos/", "")
|
||||
);
|
||||
} else if (sub.path.startsWith("/subtitles/")) {
|
||||
// sub.path is like /subtitles/Collection/file.vtt
|
||||
// SUBTITLES_DIR is uploads/subtitles
|
||||
absoluteSourcePath = path.join(
|
||||
UPLOADS_DIR,
|
||||
sub.path.replace(/^\//, "")
|
||||
); // path.join(headers...) -> uploads/subtitles/...
|
||||
}
|
||||
|
||||
let targetSubDir = "";
|
||||
let newWebPath = "";
|
||||
|
||||
if (sub.path.startsWith("/videos/")) {
|
||||
targetSubDir = targetVideoDir; // Calculated above (root or other collection)
|
||||
newWebPath = `${videoPathPrefix}/${path.basename(sub.path)}`;
|
||||
} else if (sub.path.startsWith("/subtitles/")) {
|
||||
// Should move to root subtitles or other collection subtitles
|
||||
if (otherCollection) {
|
||||
const otherName = otherCollection.name || otherCollection.title;
|
||||
if (otherName) {
|
||||
targetSubDir = path.join(SUBTITLES_DIR, otherName);
|
||||
newWebPath = `/subtitles/${otherName}/${path.basename(
|
||||
sub.path
|
||||
)}`;
|
||||
}
|
||||
} else {
|
||||
// Move to root subtitles dir
|
||||
targetSubDir = SUBTITLES_DIR;
|
||||
newWebPath = `/subtitles/${path.basename(sub.path)}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (absoluteSourcePath && targetSubDir && newWebPath) {
|
||||
const targetSubPath = path.join(
|
||||
targetSubDir,
|
||||
path.basename(sub.path)
|
||||
);
|
||||
|
||||
// Ensure correct paths for move
|
||||
// Need to handle potential double slashes or construction issues if any
|
||||
if (
|
||||
fs.existsSync(absoluteSourcePath) &&
|
||||
absoluteSourcePath !== targetSubPath
|
||||
) {
|
||||
moveFile(absoluteSourcePath, targetSubPath);
|
||||
newSubtitles[index] = {
|
||||
...sub,
|
||||
path: newWebPath,
|
||||
};
|
||||
subtitlesUpdated = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (subtitlesUpdated) {
|
||||
updates.subtitles = newSubtitles;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
if (Object.keys(updates).length > 0) {
|
||||
updateVideo(videoId, updates);
|
||||
}
|
||||
}
|
||||
@@ -643,100 +187,19 @@ export function deleteCollectionWithFiles(collectionId: string): boolean {
|
||||
collection.videos.forEach((videoId) => {
|
||||
const video = getVideoById(videoId);
|
||||
if (video) {
|
||||
// Move files back to root
|
||||
const updates: Partial<Video> = {};
|
||||
let updated = false;
|
||||
// Move files back to root (no collection)
|
||||
const updates = moveAllFilesFromCollection(
|
||||
video,
|
||||
VIDEOS_DIR,
|
||||
IMAGES_DIR,
|
||||
SUBTITLES_DIR,
|
||||
"/videos",
|
||||
"/images",
|
||||
undefined, // No subtitle prefix for root
|
||||
allCollections
|
||||
);
|
||||
|
||||
if (video.videoFilename) {
|
||||
const currentVideoPath = findVideoFile(
|
||||
video.videoFilename,
|
||||
allCollections
|
||||
);
|
||||
const targetVideoPath = path.join(VIDEOS_DIR, video.videoFilename);
|
||||
|
||||
if (currentVideoPath && currentVideoPath !== targetVideoPath) {
|
||||
moveFile(currentVideoPath, targetVideoPath);
|
||||
updates.videoPath = `/videos/${video.videoFilename}`;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (video.thumbnailFilename) {
|
||||
const currentImagePath = findImageFile(
|
||||
video.thumbnailFilename,
|
||||
allCollections
|
||||
);
|
||||
const targetImagePath = path.join(
|
||||
IMAGES_DIR,
|
||||
video.thumbnailFilename
|
||||
);
|
||||
|
||||
if (currentImagePath && currentImagePath !== targetImagePath) {
|
||||
moveFile(currentImagePath, targetImagePath);
|
||||
updates.thumbnailPath = `/images/${video.thumbnailFilename}`;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle subtitles
|
||||
if (video.subtitles && video.subtitles.length > 0) {
|
||||
const newSubtitles = [...video.subtitles];
|
||||
let subtitlesUpdated = false;
|
||||
|
||||
newSubtitles.forEach((sub, index) => {
|
||||
let absoluteSourcePath = "";
|
||||
// Construct absolute source path based on DB path
|
||||
if (sub.path.startsWith("/videos/")) {
|
||||
absoluteSourcePath = path.join(
|
||||
VIDEOS_DIR,
|
||||
sub.path.replace("/videos/", "")
|
||||
);
|
||||
} else if (sub.path.startsWith("/subtitles/")) {
|
||||
absoluteSourcePath = path.join(
|
||||
UPLOADS_DIR,
|
||||
sub.path.replace(/^\//, "")
|
||||
);
|
||||
}
|
||||
|
||||
let targetSubDir = "";
|
||||
let newWebPath = "";
|
||||
|
||||
if (sub.path.startsWith("/videos/")) {
|
||||
targetSubDir = VIDEOS_DIR;
|
||||
newWebPath = `/videos/${path.basename(sub.path)}`;
|
||||
} else if (sub.path.startsWith("/subtitles/")) {
|
||||
// Move to root subtitles dir
|
||||
targetSubDir = SUBTITLES_DIR;
|
||||
newWebPath = `/subtitles/${path.basename(sub.path)}`;
|
||||
}
|
||||
|
||||
if (absoluteSourcePath && targetSubDir && newWebPath) {
|
||||
const targetSubPath = path.join(
|
||||
targetSubDir,
|
||||
path.basename(sub.path)
|
||||
);
|
||||
|
||||
if (
|
||||
fs.existsSync(absoluteSourcePath) &&
|
||||
absoluteSourcePath !== targetSubPath
|
||||
) {
|
||||
moveFile(absoluteSourcePath, targetSubPath);
|
||||
newSubtitles[index] = {
|
||||
...sub,
|
||||
path: newWebPath,
|
||||
};
|
||||
subtitlesUpdated = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (subtitlesUpdated) {
|
||||
updates.subtitles = newSubtitles;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
if (Object.keys(updates).length > 0) {
|
||||
updateVideo(videoId, updates);
|
||||
}
|
||||
}
|
||||
@@ -745,28 +208,7 @@ export function deleteCollectionWithFiles(collectionId: string): boolean {
|
||||
|
||||
// Delete collection directory if exists and empty
|
||||
if (collectionName) {
|
||||
const collectionVideoDir = path.join(VIDEOS_DIR, collectionName);
|
||||
const collectionImageDir = path.join(IMAGES_DIR, collectionName);
|
||||
|
||||
try {
|
||||
if (
|
||||
fs.existsSync(collectionVideoDir) &&
|
||||
fs.readdirSync(collectionVideoDir).length === 0
|
||||
) {
|
||||
fs.rmdirSync(collectionVideoDir);
|
||||
}
|
||||
if (
|
||||
fs.existsSync(collectionImageDir) &&
|
||||
fs.readdirSync(collectionImageDir).length === 0
|
||||
) {
|
||||
fs.rmdirSync(collectionImageDir);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
"Error removing collection directories",
|
||||
e instanceof Error ? e : new Error(String(e))
|
||||
);
|
||||
}
|
||||
cleanupCollectionDirectories(collectionName);
|
||||
}
|
||||
|
||||
return deleteCollection(collectionId);
|
||||
@@ -787,22 +229,7 @@ export function deleteCollectionAndVideos(collectionId: string): boolean {
|
||||
|
||||
// Delete collection directory if exists
|
||||
if (collectionName) {
|
||||
const collectionVideoDir = path.join(VIDEOS_DIR, collectionName);
|
||||
const collectionImageDir = path.join(IMAGES_DIR, collectionName);
|
||||
|
||||
try {
|
||||
if (fs.existsSync(collectionVideoDir)) {
|
||||
fs.rmdirSync(collectionVideoDir);
|
||||
}
|
||||
if (fs.existsSync(collectionImageDir)) {
|
||||
fs.rmdirSync(collectionImageDir);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
"Error removing collection directories",
|
||||
e instanceof Error ? e : new Error(String(e))
|
||||
);
|
||||
}
|
||||
cleanupCollectionDirectories(collectionName);
|
||||
}
|
||||
|
||||
return deleteCollection(collectionId);
|
||||
|
||||
Reference in New Issue
Block a user