feat: Add functionality to move thumbnails to video folder
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
127
backend/src/services/thumbnailService.ts
Normal file
127
backend/src/services/thumbnailService.ts
Normal 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 };
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -75,4 +75,5 @@ export interface Settings {
|
|||||||
showYoutubeSearch?: boolean;
|
showYoutubeSearch?: boolean;
|
||||||
proxyOnlyYoutube?: boolean;
|
proxyOnlyYoutube?: boolean;
|
||||||
moveSubtitlesToVideoFolder?: boolean;
|
moveSubtitlesToVideoFolder?: boolean;
|
||||||
|
moveThumbnailsToVideoFolder?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.',
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user