diff --git a/README-zh.md b/README-zh.md index abeba7b..0b05689 100644 --- a/README-zh.md +++ b/README-zh.md @@ -86,7 +86,15 @@ MAX_FILE_SIZE=500000000 ## 星标历史 -[![Star History Chart](https://api.star-history.com/svg?repos=franklioxygen/MyTube&type=date&legend=bottom-right)](https://www.star-history.com/#franklioxygen/MyTube&type=date&legend=bottom-right) +## Star History + + + + + + Star History Chart + + ## 免责声明 diff --git a/README.md b/README.md index ccfe567..0c2afd9 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,13 @@ For detailed instructions on how to deploy MyTube using Docker, please refer to ## Star History -[![Star History Chart](https://api.star-history.com/svg?repos=franklioxygen/MyTube&type=date&legend=bottom-right)](https://www.star-history.com/#franklioxygen/MyTube&type=date&legend=bottom-right) + + + + + Star History Chart + + ## Disclaimer diff --git a/backend/src/controllers/settingsController.ts b/backend/src/controllers/settingsController.ts index 73fc36a..2e8db21 100644 --- a/backend/src/controllers/settingsController.ts +++ b/backend/src/controllers/settingsController.ts @@ -29,6 +29,7 @@ interface Settings { ytDlpConfig?: string; showYoutubeSearch?: boolean; proxyOnlyYoutube?: boolean; + moveSubtitlesToVideoFolder?: boolean; } const defaultSettings: Settings = { @@ -189,6 +190,16 @@ export const updateSettings = async (req: Request, res: Response) => { storageService.saveSettings(newSettings); + // Check for moveSubtitlesToVideoFolder change + if (newSettings.moveSubtitlesToVideoFolder !== existingSettings.moveSubtitlesToVideoFolder) { + if (newSettings.moveSubtitlesToVideoFolder !== undefined) { + // Run asynchronously + const { moveAllSubtitles } = await import("../services/subtitleService"); + moveAllSubtitles(newSettings.moveSubtitlesToVideoFolder) + .catch(err => console.error("Error moving subtitles in background:", err)); + } + } + // Apply settings immediately where possible downloadManager.setMaxConcurrentDownloads( newSettings.maxConcurrentDownloads diff --git a/backend/src/services/storageService.ts b/backend/src/services/storageService.ts index 2429ee2..2336f9a 100644 --- a/backend/src/services/storageService.ts +++ b/backend/src/services/storageService.ts @@ -2,22 +2,22 @@ import { and, desc, eq, lt } from "drizzle-orm"; import fs from "fs-extra"; import path from "path"; import { - DATA_DIR, - IMAGES_DIR, - STATUS_DATA_PATH, - SUBTITLES_DIR, - UPLOADS_DIR, - VIDEOS_DIR, + DATA_DIR, + IMAGES_DIR, + STATUS_DATA_PATH, + SUBTITLES_DIR, + UPLOADS_DIR, + VIDEOS_DIR, } from "../config/paths"; import { db, sqlite } from "../db"; import { - collections, - collectionVideos, - downloadHistory, - downloads, - settings, - videoDownloads, - videos, + collections, + collectionVideos, + downloadHistory, + downloads, + settings, + videoDownloads, + videos, } from "../db/schema"; import { formatVideoFilename } from "../utils/helpers"; @@ -1506,6 +1506,61 @@ export function addVideoToCollection( } } + // 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) { updateVideo(videoId, updates); } @@ -1577,6 +1632,65 @@ export function removeVideoFromCollection( } } + // 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) { updateVideo(videoId, updates); } @@ -1625,6 +1739,52 @@ export function deleteCollectionWithFiles(collectionId: string): boolean { } } + // 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) { updateVideo(videoId, updates); } diff --git a/backend/src/services/subtitleService.ts b/backend/src/services/subtitleService.ts new file mode 100644 index 0000000..f1a862a --- /dev/null +++ b/backend/src/services/subtitleService.ts @@ -0,0 +1,147 @@ + +import fs from "fs-extra"; +import path from "path"; +import { SUBTITLES_DIR, VIDEOS_DIR } from "../config/paths"; +import * as storageService from "./storageService"; + +export const moveAllSubtitles = async (toVideoFolder: boolean) => { + console.log(`Starting to move all subtitles. Target: ${toVideoFolder ? 'Video Folders' : 'Central Subtitles Folder'}`); + const allVideos = storageService.getVideos(); + let movedCount = 0; + let errorCount = 0; + + for (const video of allVideos) { + if (!video.subtitles || video.subtitles.length === 0) continue; + + const newSubtitles = []; + let videoChanged = false; + + // Determine where the video file is located + let videoDir = VIDEOS_DIR; + let relativeVideoDir = ""; // Relative to VIDEOS_DIR + + if (video.videoFilename) { + // We need to find the actual location of the video file to know its folder + // storageService.findVideoFile is private, but we can replicate the logic or use the path stored in DB if reliable. + // However, video.videoPath usually starts with /videos/... + // Let's rely on finding the file to be sure. + + // Heuristic: check if it's in a collection folder + // We can iterate collections, or check the file system. + // Let's look at the videoPath property if available, it usually reflects the web path + // e.g. /videos/MyCollection/video.mp4 or /videos/video.mp4 + 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; + } + } + } + } + } + + for (const sub of video.subtitles) { + try { + // Determine current absolute path + // sub.path is like /subtitles/filename.vtt (if central) or /videos/Folder/filename.vtt (if local) + // BUT we should rely on where the file ACTUALLY is right now. + // We can try to resolve it based on the stored path. + + let currentAbsPath = ""; + if (sub.path.startsWith("/videos/")) { + currentAbsPath = path.join(VIDEOS_DIR, sub.path.replace(/^\/videos\//, "")); + } else if (sub.path.startsWith("/subtitles/")) { + currentAbsPath = path.join(SUBTITLES_DIR, sub.path.replace(/^\/subtitles\//, "")); + } else { + // Fallback to filename search in both locations + const centralPath = path.join(SUBTITLES_DIR, sub.filename); + if (fs.existsSync(centralPath)) { + currentAbsPath = centralPath; + } else { + const localPath = path.join(videoDir, sub.filename); + if (fs.existsSync(localPath)) { + currentAbsPath = localPath; + } + } + } + + if (!fs.existsSync(currentAbsPath)) { + console.warn(`Subtitle file not found: ${sub.path} or ${currentAbsPath}`); + newSubtitles.push(sub); // Keep the record even if file missing? Or maybe better to keep it to avoid data loss. + continue; + } + + let targetAbsPath = ""; + let newWebPath = ""; + + if (toVideoFolder) { + // Move TO video folder + targetAbsPath = path.join(videoDir, sub.filename); + if (relativeVideoDir) { + newWebPath = `/videos/${relativeVideoDir}/${sub.filename}`; + } else { + newWebPath = `/videos/${sub.filename}`; + } + } else { + // Move TO central subtitles folder + // Mirror the folder structure + if (relativeVideoDir) { + const targetDir = path.join(SUBTITLES_DIR, relativeVideoDir); + fs.ensureDirSync(targetDir); + targetAbsPath = path.join(targetDir, sub.filename); + newWebPath = `/subtitles/${relativeVideoDir}/${sub.filename}`; + } else { + targetAbsPath = path.join(SUBTITLES_DIR, sub.filename); + newWebPath = `/subtitles/${sub.filename}`; + } + } + + if (currentAbsPath !== targetAbsPath) { + fs.moveSync(currentAbsPath, targetAbsPath, { overwrite: true }); + newSubtitles.push({ + ...sub, + path: newWebPath + }); + videoChanged = true; + movedCount++; + } else { + // Already in the right place, but ensure path is correct + if (sub.path !== newWebPath) { + newSubtitles.push({ + ...sub, + path: newWebPath + }); + videoChanged = true; + } else { + newSubtitles.push(sub); + } + } + + } catch (err) { + console.error(`Failed to move subtitle ${sub.filename}:`, err); + newSubtitles.push(sub); // Keep original on error + errorCount++; + } + } + + if (videoChanged) { + storageService.updateVideo(video.id, { subtitles: newSubtitles }); + } + } + + console.log(`Finished moving subtitles. Moved: ${movedCount}, Errors: ${errorCount}`); + return { movedCount, errorCount }; +}; diff --git a/frontend/src/components/Settings/DatabaseSettings.tsx b/frontend/src/components/Settings/DatabaseSettings.tsx index 2821757..1f01163 100644 --- a/frontend/src/components/Settings/DatabaseSettings.tsx +++ b/frontend/src/components/Settings/DatabaseSettings.tsx @@ -1,4 +1,4 @@ -import { Box, Button, Typography } from '@mui/material'; +import { Box, Button, FormControlLabel, Switch, Typography } from '@mui/material'; import React from 'react'; import { useLanguage } from '../../contexts/LanguageContext'; @@ -7,9 +7,18 @@ interface DatabaseSettingsProps { onDeleteLegacy: () => void; onFormatFilenames: () => void; isSaving: boolean; + moveSubtitlesToVideoFolder: boolean; + onMoveSubtitlesToVideoFolderChange: (checked: boolean) => void; } -const DatabaseSettings: React.FC = ({ onMigrate, onDeleteLegacy, onFormatFilenames, isSaving }) => { +const DatabaseSettings: React.FC = ({ + onMigrate, + onDeleteLegacy, + onFormatFilenames, + isSaving, + moveSubtitlesToVideoFolder, + onMoveSubtitlesToVideoFolderChange +}) => { const { t } = useLanguage(); return ( @@ -56,6 +65,23 @@ const DatabaseSettings: React.FC = ({ onMigrate, onDelete {t('deleteLegacyDataButton')} + + + {t('moveSubtitlesToVideoFolder')} + onMoveSubtitlesToVideoFolderChange(e.target.checked)} + disabled={isSaving} + /> + } + label={moveSubtitlesToVideoFolder ? t('moveSubtitlesToVideoFolderOn') : t('moveSubtitlesToVideoFolderOff')} + /> + + {t('moveSubtitlesToVideoFolderDescription')} + + ); }; diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index 4fe8790..36782c1 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -53,7 +53,8 @@ const SettingsPage: React.FC = () => { itemsPerPage: 12, ytDlpConfig: '', showYoutubeSearch: true, - proxyOnlyYoutube: false + proxyOnlyYoutube: false, + moveSubtitlesToVideoFolder: false }); const [message, setMessage] = useState<{ text: string; type: 'success' | 'error' | 'warning' | 'info' } | null>(null); const debouncedSettings = useDebounce(settings, 1000); @@ -405,6 +406,8 @@ const SettingsPage: React.FC = () => { onDeleteLegacy={() => setShowDeleteLegacyModal(true)} onFormatFilenames={() => setShowFormatConfirmModal(true)} isSaving={isSaving} + moveSubtitlesToVideoFolder={settings.moveSubtitlesToVideoFolder || false} + onMoveSubtitlesToVideoFolderChange={(checked) => handleChange('moveSubtitlesToVideoFolder', checked)} /> diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 7915e35..00cbe1e 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -74,4 +74,5 @@ export interface Settings { ytDlpConfig?: string; showYoutubeSearch?: boolean; proxyOnlyYoutube?: boolean; + moveSubtitlesToVideoFolder?: boolean; } diff --git a/frontend/src/utils/locales/ar.ts b/frontend/src/utils/locales/ar.ts index 79e7911..46b0fc2 100644 --- a/frontend/src/utils/locales/ar.ts +++ b/frontend/src/utils/locales/ar.ts @@ -251,6 +251,12 @@ export const ar = { collectionContains: "تحتوي هذه المجموعة على", deleteCollectionOnly: "حذف المجموعة فقط", + proxyOnlyApplyToYoutube: 'الوكيل ينطبق فقط على يوتيوب', + moveSubtitlesToVideoFolder: 'موقع الترجمة', + moveSubtitlesToVideoFolderOn: 'مع الفيديو', + moveSubtitlesToVideoFolderOff: 'في مجلد الترجمات المعزول', + moveSubtitlesToVideoFolderDescription: 'عند التمكن، سيتم نقل ملفات الترجمة إلى نفس المجلد الموجود به ملف الفيديو. عند التعطيل، سيتم نقلها إلى مجلد الترجمات المعزول.', + // Snackbar Messages videoDownloading: "جاري تنزيل الفيديو", downloadStartedSuccessfully: "بدأ التنزيل بنجاح", diff --git a/frontend/src/utils/locales/de.ts b/frontend/src/utils/locales/de.ts index f3b6eb0..bae06e6 100644 --- a/frontend/src/utils/locales/de.ts +++ b/frontend/src/utils/locales/de.ts @@ -426,4 +426,9 @@ export const de = { hide: "Ausblenden", reset: "Zurücksetzen", more: "Mehr", + proxyOnlyApplyToYoutube: 'Proxy nur auf Youtube anwenden', + moveSubtitlesToVideoFolder: 'Untertitel-Speicherort', + moveSubtitlesToVideoFolderOn: 'Zusammen mit Video', + moveSubtitlesToVideoFolderOff: 'Im isolierten Untertitel-Ordner', + moveSubtitlesToVideoFolderDescription: 'Wenn aktiviert, werden Untertiteldateien in denselben Ordner wie die Videodatei verschoben. Wenn deaktiviert, werden sie in den isolierten Untertitelordner verschoben.', }; diff --git a/frontend/src/utils/locales/en.ts b/frontend/src/utils/locales/en.ts index d84a091..395b37c 100644 --- a/frontend/src/utils/locales/en.ts +++ b/frontend/src/utils/locales/en.ts @@ -455,4 +455,8 @@ export const en = { reset: "Reset", more: "More", proxyOnlyApplyToYoutube: 'Proxy only apply to Youtube', + moveSubtitlesToVideoFolder: 'Subtitles Location', + moveSubtitlesToVideoFolderOn: 'With video together', + 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.', }; diff --git a/frontend/src/utils/locales/es.ts b/frontend/src/utils/locales/es.ts index ab88342..81f55b3 100644 --- a/frontend/src/utils/locales/es.ts +++ b/frontend/src/utils/locales/es.ts @@ -22,6 +22,11 @@ export const es = { searchResultsFor: "Resultados de búsqueda para", fromYourLibrary: "De tu Biblioteca", noMatchingVideos: "No hay videos coincidentes en tu biblioteca.", + proxyOnlyApplyToYoutube: 'Proxy solo se aplica a Youtube', + moveSubtitlesToVideoFolder: 'Ubicación de subtítulos', + moveSubtitlesToVideoFolderOn: 'Junto al video', + moveSubtitlesToVideoFolderOff: 'En carpeta de subtítulos aislada', + moveSubtitlesToVideoFolderDescription: 'Cuando está habilitado, los archivos de subtítulos se moverán a la misma carpeta que el archivo de video. Cuando está deshabilitado, se moverán a la carpeta de subtítulos aislada.', fromYouTube: "De YouTube", loadingYouTubeResults: "Cargando resultados de YouTube...", noYouTubeResults: "No se encontraron resultados de YouTube", diff --git a/frontend/src/utils/locales/fr.ts b/frontend/src/utils/locales/fr.ts index 213d102..72fb7df 100644 --- a/frontend/src/utils/locales/fr.ts +++ b/frontend/src/utils/locales/fr.ts @@ -464,4 +464,9 @@ export const fr = { hide: "Masquer", reset: "Réinitialiser", more: "Plus", + proxyOnlyApplyToYoutube: 'Proxy s\'applique uniquement à Youtube', + moveSubtitlesToVideoFolder: 'Emplacement des sous-titres', + moveSubtitlesToVideoFolderOn: 'Avec la vidéo', + moveSubtitlesToVideoFolderOff: 'Dans le dossier de sous-titres isolé', + moveSubtitlesToVideoFolderDescription: 'Si activé, les fichiers de sous-titres seront déplacés dans le même dossier que le fichier vidéo. Si désactivé, ils seront déplacés vers le dossier de sous-titres isolé.', }; diff --git a/frontend/src/utils/locales/ja.ts b/frontend/src/utils/locales/ja.ts index 089ff9c..e5384a8 100644 --- a/frontend/src/utils/locales/ja.ts +++ b/frontend/src/utils/locales/ja.ts @@ -453,5 +453,9 @@ export const ja = { hide: "隠す", reset: "リセット", more: "もっと見る", - proxyOnlyApplyToYoutube: 'Proxy only apply to Youtube', + proxyOnlyApplyToYoutube: 'プロキシはYoutubeにのみ適用されます', + moveSubtitlesToVideoFolder: '字幕の場所', + moveSubtitlesToVideoFolderOn: '動画と同じ場所', + moveSubtitlesToVideoFolderOff: '独立した字幕フォルダー', + moveSubtitlesToVideoFolderDescription: '有効にすると、字幕ファイルは動画ファイルと同じフォルダーに移動されます。無効にすると、独立した字幕フォルダーに移動されます。', }; diff --git a/frontend/src/utils/locales/ko.ts b/frontend/src/utils/locales/ko.ts index bad66c3..8e2fbde 100644 --- a/frontend/src/utils/locales/ko.ts +++ b/frontend/src/utils/locales/ko.ts @@ -447,5 +447,9 @@ export const ko = { hide: "숨기기", reset: "초기화", more: "더 보기", - proxyOnlyApplyToYoutube: 'Proxy only apply to Youtube', + proxyOnlyApplyToYoutube: '프록시는 Youtube에만 적용됩니다', + moveSubtitlesToVideoFolder: '자막 위치', + moveSubtitlesToVideoFolderOn: '동영상과 함께', + moveSubtitlesToVideoFolderOff: '격리된 자막 폴더', + moveSubtitlesToVideoFolderDescription: '활성화하면 자막 파일이 동영상 파일과 같은 폴더로 이동됩니다. 비활성화하면 격리된 자막 폴더로 이동됩니다.', }; diff --git a/frontend/src/utils/locales/pt.ts b/frontend/src/utils/locales/pt.ts index 3ecda17..e29c1b8 100644 --- a/frontend/src/utils/locales/pt.ts +++ b/frontend/src/utils/locales/pt.ts @@ -458,4 +458,9 @@ export const pt = { hide: "Ocultar", reset: "Redefinir", more: "Mais", + proxyOnlyApplyToYoutube: 'Proxy aplica-se apenas ao Youtube', + moveSubtitlesToVideoFolder: 'Localização das legendas', + moveSubtitlesToVideoFolderOn: 'Junto com o vídeo', + moveSubtitlesToVideoFolderOff: 'Na pasta de legendas isolada', + moveSubtitlesToVideoFolderDescription: 'Quando ativado, os arquivos de legenda serão movidos para a mesma pasta do arquivo de vídeo. Quando desativado, eles serão movidos para a pasta de legendas isolada.', }; diff --git a/frontend/src/utils/locales/ru.ts b/frontend/src/utils/locales/ru.ts index 96c035f..6546d20 100644 --- a/frontend/src/utils/locales/ru.ts +++ b/frontend/src/utils/locales/ru.ts @@ -25,7 +25,11 @@ export const ru = { searchResultsFor: "Результаты поиска для", fromYourLibrary: "Из вашей библиотеки", noMatchingVideos: "В вашей библиотеке нет подходящих видео.", - fromYouTube: "С YouTube", + proxyOnlyApplyToYoutube: 'Прокси применяется только к Youtube', + moveSubtitlesToVideoFolder: 'Расположение субтитров', + moveSubtitlesToVideoFolderOn: 'Вместе с видео', + moveSubtitlesToVideoFolderOff: 'В изолированной папке субтитров', + moveSubtitlesToVideoFolderDescription: 'Если включено, файлы субтитров будут перемещены в ту же папку, что и видеофайл. Если отключено, они будут перемещены в изолированную папку субтитров.', loadingYouTubeResults: "Загрузка результатов YouTube...", noYouTubeResults: "Результаты YouTube не найдены", noVideosYet: "Видео пока нет. Отправьте URL видео, чтобы скачать первое!", diff --git a/frontend/src/utils/locales/zh.ts b/frontend/src/utils/locales/zh.ts index decdba1..0d692af 100644 --- a/frontend/src/utils/locales/zh.ts +++ b/frontend/src/utils/locales/zh.ts @@ -443,5 +443,9 @@ export const zh = { hide: "隐藏", reset: "重置", more: "更多", - proxyOnlyApplyToYoutube: '代理仅应用于 Youtube', + proxyOnlyApplyToYoutube: '代理仅应用于Youtube', + moveSubtitlesToVideoFolder: '字幕位置', + moveSubtitlesToVideoFolderOn: '与视频在同一文件夹', + moveSubtitlesToVideoFolderOff: '在独立字幕文件夹', + moveSubtitlesToVideoFolderDescription: '启用后,字幕文件将被移动到与视频文件相同的文件夹中。禁用后,它们将被移动到独立字幕文件夹。', };