feat: Add functionality to move thumbnails to video folder

This commit is contained in:
Peifan Li
2025-12-14 20:00:40 -05:00
parent 5406b30eca
commit dd94d80311
7 changed files with 229 additions and 15 deletions

View File

@@ -30,6 +30,7 @@ interface Settings {
showYoutubeSearch?: boolean; showYoutubeSearch?: boolean;
proxyOnlyYoutube?: boolean; proxyOnlyYoutube?: boolean;
moveSubtitlesToVideoFolder?: boolean; moveSubtitlesToVideoFolder?: boolean;
moveThumbnailsToVideoFolder?: boolean;
} }
const defaultSettings: Settings = { const defaultSettings: Settings = {
@@ -200,6 +201,16 @@ export const updateSettings = async (req: Request, res: Response) => {
} }
} }
// Check for moveThumbnailsToVideoFolder change
if (newSettings.moveThumbnailsToVideoFolder !== existingSettings.moveThumbnailsToVideoFolder) {
if (newSettings.moveThumbnailsToVideoFolder !== undefined) {
// Run asynchronously
const { moveAllThumbnails } = await import("../services/thumbnailService");
moveAllThumbnails(newSettings.moveThumbnailsToVideoFolder)
.catch(err => console.error("Error moving thumbnails in background:", err));
}
}
// Apply settings immediately where possible // Apply settings immediately where possible
downloadManager.setMaxConcurrentDownloads( downloadManager.setMaxConcurrentDownloads(
newSettings.maxConcurrentDownloads newSettings.maxConcurrentDownloads

View File

@@ -1492,16 +1492,38 @@ export function addVideoToCollection(
} }
if (video.thumbnailFilename) { if (video.thumbnailFilename) {
const currentImagePath = findImageFile(video.thumbnailFilename); // Find existing file using path from DB if possible, or fallback to search
const targetImagePath = path.join( let currentImagePath = "";
IMAGES_DIR, if (video.thumbnailPath) {
collectionName, if (video.thumbnailPath.startsWith("/videos/")) {
video.thumbnailFilename 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) || "";
}
// 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) { if (currentImagePath && currentImagePath !== targetImagePath) {
moveFile(currentImagePath, targetImagePath); moveFile(currentImagePath, targetImagePath);
updates.thumbnailPath = `/images/${collectionName}/${video.thumbnailFilename}`; updates.thumbnailPath = newWebPath;
updated = true; updated = true;
} }
} }
@@ -1619,15 +1641,40 @@ export function removeVideoFromCollection(
} }
if (video.thumbnailFilename) { if (video.thumbnailFilename) {
const currentImagePath = findImageFile(video.thumbnailFilename); // Find existing file using path from DB if possible
const targetImagePath = path.join( let currentImagePath = "";
targetImageDir, if (video.thumbnailPath) {
video.thumbnailFilename 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) || "";
}
// 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) { if (currentImagePath && currentImagePath !== targetImagePath) {
moveFile(currentImagePath, targetImagePath); moveFile(currentImagePath, targetImagePath);
updates.thumbnailPath = `${imagePathPrefix}/${video.thumbnailFilename}`; updates.thumbnailPath = newWebPath;
updated = true; updated = true;
} }
} }

View File

@@ -0,0 +1,127 @@
import fs from "fs-extra";
import path from "path";
import { IMAGES_DIR, VIDEOS_DIR } from "../config/paths";
import * as storageService from "./storageService";
export const moveAllThumbnails = async (toVideoFolder: boolean) => {
console.log(`Starting to move all thumbnails. Target: ${toVideoFolder ? 'Video Folders' : 'Central Images Folder'}`);
const allVideos = storageService.getVideos();
let movedCount = 0;
let errorCount = 0;
for (const video of allVideos) {
if (!video.thumbnailFilename) continue;
let videoChanged = false;
// Determine where the video file is located
let videoDir = VIDEOS_DIR;
let relativeVideoDir = ""; // Relative to VIDEOS_DIR
if (video.videoFilename) {
// Logic similar to subtitleService to find the video directory
if (video.videoPath) {
const cleanPath = video.videoPath.replace(/^\/videos\//, '');
const dirName = path.dirname(cleanPath);
if (dirName && dirName !== '.') {
videoDir = path.join(VIDEOS_DIR, dirName);
relativeVideoDir = dirName;
}
} else {
// Fallback: check collections
const collections = storageService.getCollections();
for (const col of collections) {
if (col.videos.includes(video.id)) {
const colName = col.name || col.title;
if (colName) {
videoDir = path.join(VIDEOS_DIR, colName);
relativeVideoDir = colName;
break;
}
}
}
}
}
try {
// Determine current absolute path of the thumbnail
let currentAbsPath = "";
// Check based on current path property if available
if (video.thumbnailPath) {
if (video.thumbnailPath.startsWith("/videos/")) {
currentAbsPath = path.join(VIDEOS_DIR, video.thumbnailPath.replace(/^\/videos\//, ""));
} else if (video.thumbnailPath.startsWith("/images/")) {
currentAbsPath = path.join(IMAGES_DIR, video.thumbnailPath.replace(/^\/images\//, ""));
}
}
// Fallback search if path is invalid or file doesn't exist at path
if (!currentAbsPath || !fs.existsSync(currentAbsPath)) {
const centralPath = path.join(IMAGES_DIR, video.thumbnailFilename);
if (fs.existsSync(centralPath)) {
currentAbsPath = centralPath;
} else {
const localPath = path.join(videoDir, video.thumbnailFilename);
if (fs.existsSync(localPath)) {
currentAbsPath = localPath;
}
}
}
if (!fs.existsSync(currentAbsPath)) {
// console.warn(`Thumbnail file not found: ${video.thumbnailFilename}`);
continue;
}
let targetAbsPath = "";
let newWebPath = "";
if (toVideoFolder) {
// Move TO video folder
targetAbsPath = path.join(videoDir, video.thumbnailFilename);
if (relativeVideoDir) {
newWebPath = `/videos/${relativeVideoDir}/${video.thumbnailFilename}`;
} else {
newWebPath = `/videos/${video.thumbnailFilename}`;
}
} else {
// Move TO central images folder
if (relativeVideoDir) {
const targetDir = path.join(IMAGES_DIR, relativeVideoDir);
fs.ensureDirSync(targetDir);
targetAbsPath = path.join(targetDir, video.thumbnailFilename);
newWebPath = `/images/${relativeVideoDir}/${video.thumbnailFilename}`;
} else {
targetAbsPath = path.join(IMAGES_DIR, video.thumbnailFilename);
newWebPath = `/images/${video.thumbnailFilename}`;
}
}
if (currentAbsPath !== targetAbsPath) {
fs.moveSync(currentAbsPath, targetAbsPath, { overwrite: true });
// Update video record
storageService.updateVideo(video.id, {
thumbnailPath: newWebPath
});
movedCount++;
} else {
// Already in the right place, but ensure path is correct in DB
if (video.thumbnailPath !== newWebPath) {
storageService.updateVideo(video.id, {
thumbnailPath: newWebPath
});
}
}
} catch (err) {
console.error(`Failed to move thumbnail ${video.thumbnailFilename}:`, err);
errorCount++;
}
}
console.log(`Finished moving thumbnails. Moved: ${movedCount}, Errors: ${errorCount}`);
return { movedCount, errorCount };
};

View File

@@ -9,6 +9,8 @@ interface DatabaseSettingsProps {
isSaving: boolean; isSaving: boolean;
moveSubtitlesToVideoFolder: boolean; moveSubtitlesToVideoFolder: boolean;
onMoveSubtitlesToVideoFolderChange: (checked: boolean) => void; onMoveSubtitlesToVideoFolderChange: (checked: boolean) => void;
moveThumbnailsToVideoFolder: boolean;
onMoveThumbnailsToVideoFolderChange: (checked: boolean) => void;
} }
const DatabaseSettings: React.FC<DatabaseSettingsProps> = ({ const DatabaseSettings: React.FC<DatabaseSettingsProps> = ({
@@ -17,7 +19,9 @@ const DatabaseSettings: React.FC<DatabaseSettingsProps> = ({
onFormatFilenames, onFormatFilenames,
isSaving, isSaving,
moveSubtitlesToVideoFolder, moveSubtitlesToVideoFolder,
onMoveSubtitlesToVideoFolderChange onMoveSubtitlesToVideoFolderChange,
moveThumbnailsToVideoFolder,
onMoveThumbnailsToVideoFolderChange
}) => { }) => {
const { t } = useLanguage(); const { t } = useLanguage();
@@ -82,6 +86,23 @@ const DatabaseSettings: React.FC<DatabaseSettingsProps> = ({
{t('moveSubtitlesToVideoFolderDescription')} {t('moveSubtitlesToVideoFolderDescription')}
</Typography> </Typography>
</Box> </Box>
<Box sx={{ mt: 3 }}>
<Typography variant="h6" gutterBottom>{t('moveThumbnailsToVideoFolder')}</Typography>
<FormControlLabel
control={
<Switch
checked={moveThumbnailsToVideoFolder}
onChange={(e) => onMoveThumbnailsToVideoFolderChange(e.target.checked)}
disabled={isSaving}
/>
}
label={moveThumbnailsToVideoFolder ? t('moveThumbnailsToVideoFolderOn') : t('moveThumbnailsToVideoFolderOff')}
/>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
{t('moveThumbnailsToVideoFolderDescription')}
</Typography>
</Box>
</Box> </Box>
); );
}; };

View File

@@ -54,7 +54,8 @@ const SettingsPage: React.FC = () => {
ytDlpConfig: '', ytDlpConfig: '',
showYoutubeSearch: true, showYoutubeSearch: true,
proxyOnlyYoutube: false, proxyOnlyYoutube: false,
moveSubtitlesToVideoFolder: false moveSubtitlesToVideoFolder: false,
moveThumbnailsToVideoFolder: false
}); });
const [message, setMessage] = useState<{ text: string; type: 'success' | 'error' | 'warning' | 'info' } | null>(null); const [message, setMessage] = useState<{ text: string; type: 'success' | 'error' | 'warning' | 'info' } | null>(null);
@@ -409,6 +410,8 @@ const SettingsPage: React.FC = () => {
isSaving={isSaving} isSaving={isSaving}
moveSubtitlesToVideoFolder={settings.moveSubtitlesToVideoFolder || false} moveSubtitlesToVideoFolder={settings.moveSubtitlesToVideoFolder || false}
onMoveSubtitlesToVideoFolderChange={(checked) => handleChange('moveSubtitlesToVideoFolder', checked)} onMoveSubtitlesToVideoFolderChange={(checked) => handleChange('moveSubtitlesToVideoFolder', checked)}
moveThumbnailsToVideoFolder={settings.moveThumbnailsToVideoFolder || false}
onMoveThumbnailsToVideoFolderChange={(checked) => handleChange('moveThumbnailsToVideoFolder', checked)}
/> />
</Grid> </Grid>

View File

@@ -75,4 +75,5 @@ export interface Settings {
showYoutubeSearch?: boolean; showYoutubeSearch?: boolean;
proxyOnlyYoutube?: boolean; proxyOnlyYoutube?: boolean;
moveSubtitlesToVideoFolder?: boolean; moveSubtitlesToVideoFolder?: boolean;
moveThumbnailsToVideoFolder?: boolean;
} }

View File

@@ -459,4 +459,8 @@ export const en = {
moveSubtitlesToVideoFolderOn: 'With video together', moveSubtitlesToVideoFolderOn: 'With video together',
moveSubtitlesToVideoFolderOff: 'In isolated subtitle folder', moveSubtitlesToVideoFolderOff: 'In isolated subtitle folder',
moveSubtitlesToVideoFolderDescription: 'When enabled, subtitle files will be moved to the same folder as the video file. When disabled, they will be moved to the isolated subtitle folder.', moveSubtitlesToVideoFolderDescription: 'When enabled, subtitle files will be moved to the same folder as the video file. When disabled, they will be moved to the isolated subtitle folder.',
moveThumbnailsToVideoFolder: 'Thumbnail Location',
moveThumbnailsToVideoFolderOn: 'With video together',
moveThumbnailsToVideoFolderOff: 'In isolated images folder',
moveThumbnailsToVideoFolderDescription: 'When enabled, thumbnail files will be moved to the same folder as the video file. When disabled, they will be moved to the isolated images folder.',
}; };