feat: Add option to move subtitles to video folder
This commit is contained in:
10
README-zh.md
10
README-zh.md
@@ -86,7 +86,15 @@ MAX_FILE_SIZE=500000000
|
||||
|
||||
## 星标历史
|
||||
|
||||
[](https://www.star-history.com/#franklioxygen/MyTube&type=date&legend=bottom-right)
|
||||
## Star History
|
||||
|
||||
<a href="https://www.star-history.com/#franklioxygen/MyTube&type=date&legend=bottom-right">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=franklioxygen/MyTube&type=date&theme=dark&legend=bottom-right" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=franklioxygen/MyTube&type=date&legend=bottom-right" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=franklioxygen/MyTube&type=date&legend=bottom-right" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## 免责声明
|
||||
|
||||
|
||||
@@ -86,7 +86,13 @@ For detailed instructions on how to deploy MyTube using Docker, please refer to
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://www.star-history.com/#franklioxygen/MyTube&type=date&legend=bottom-right)
|
||||
<a href="https://www.star-history.com/#franklioxygen/MyTube&type=date&legend=bottom-right">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=franklioxygen/MyTube&type=date&theme=dark&legend=bottom-right" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=franklioxygen/MyTube&type=date&legend=bottom-right" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=franklioxygen/MyTube&type=date&legend=bottom-right" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Disclaimer
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
147
backend/src/services/subtitleService.ts
Normal file
147
backend/src/services/subtitleService.ts
Normal file
@@ -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 };
|
||||
};
|
||||
@@ -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<DatabaseSettingsProps> = ({ onMigrate, onDeleteLegacy, onFormatFilenames, isSaving }) => {
|
||||
const DatabaseSettings: React.FC<DatabaseSettingsProps> = ({
|
||||
onMigrate,
|
||||
onDeleteLegacy,
|
||||
onFormatFilenames,
|
||||
isSaving,
|
||||
moveSubtitlesToVideoFolder,
|
||||
onMoveSubtitlesToVideoFolderChange
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
return (
|
||||
@@ -56,6 +65,23 @@ const DatabaseSettings: React.FC<DatabaseSettingsProps> = ({ onMigrate, onDelete
|
||||
{t('deleteLegacyDataButton')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>{t('moveSubtitlesToVideoFolder')}</Typography>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={moveSubtitlesToVideoFolder}
|
||||
onChange={(e) => onMoveSubtitlesToVideoFolderChange(e.target.checked)}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
}
|
||||
label={moveSubtitlesToVideoFolder ? t('moveSubtitlesToVideoFolderOn') : t('moveSubtitlesToVideoFolderOff')}
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
||||
{t('moveSubtitlesToVideoFolderDescription')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
|
||||
@@ -74,4 +74,5 @@ export interface Settings {
|
||||
ytDlpConfig?: string;
|
||||
showYoutubeSearch?: boolean;
|
||||
proxyOnlyYoutube?: boolean;
|
||||
moveSubtitlesToVideoFolder?: boolean;
|
||||
}
|
||||
|
||||
@@ -251,6 +251,12 @@ export const ar = {
|
||||
collectionContains: "تحتوي هذه المجموعة على",
|
||||
deleteCollectionOnly: "حذف المجموعة فقط",
|
||||
|
||||
proxyOnlyApplyToYoutube: 'الوكيل ينطبق فقط على يوتيوب',
|
||||
moveSubtitlesToVideoFolder: 'موقع الترجمة',
|
||||
moveSubtitlesToVideoFolderOn: 'مع الفيديو',
|
||||
moveSubtitlesToVideoFolderOff: 'في مجلد الترجمات المعزول',
|
||||
moveSubtitlesToVideoFolderDescription: 'عند التمكن، سيتم نقل ملفات الترجمة إلى نفس المجلد الموجود به ملف الفيديو. عند التعطيل، سيتم نقلها إلى مجلد الترجمات المعزول.',
|
||||
|
||||
// Snackbar Messages
|
||||
videoDownloading: "جاري تنزيل الفيديو",
|
||||
downloadStartedSuccessfully: "بدأ التنزيل بنجاح",
|
||||
|
||||
@@ -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.',
|
||||
};
|
||||
|
||||
@@ -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.',
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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é.',
|
||||
};
|
||||
|
||||
@@ -453,5 +453,9 @@ export const ja = {
|
||||
hide: "隠す",
|
||||
reset: "リセット",
|
||||
more: "もっと見る",
|
||||
proxyOnlyApplyToYoutube: 'Proxy only apply to Youtube',
|
||||
proxyOnlyApplyToYoutube: 'プロキシはYoutubeにのみ適用されます',
|
||||
moveSubtitlesToVideoFolder: '字幕の場所',
|
||||
moveSubtitlesToVideoFolderOn: '動画と同じ場所',
|
||||
moveSubtitlesToVideoFolderOff: '独立した字幕フォルダー',
|
||||
moveSubtitlesToVideoFolderDescription: '有効にすると、字幕ファイルは動画ファイルと同じフォルダーに移動されます。無効にすると、独立した字幕フォルダーに移動されます。',
|
||||
};
|
||||
|
||||
@@ -447,5 +447,9 @@ export const ko = {
|
||||
hide: "숨기기",
|
||||
reset: "초기화",
|
||||
more: "더 보기",
|
||||
proxyOnlyApplyToYoutube: 'Proxy only apply to Youtube',
|
||||
proxyOnlyApplyToYoutube: '프록시는 Youtube에만 적용됩니다',
|
||||
moveSubtitlesToVideoFolder: '자막 위치',
|
||||
moveSubtitlesToVideoFolderOn: '동영상과 함께',
|
||||
moveSubtitlesToVideoFolderOff: '격리된 자막 폴더',
|
||||
moveSubtitlesToVideoFolderDescription: '활성화하면 자막 파일이 동영상 파일과 같은 폴더로 이동됩니다. 비활성화하면 격리된 자막 폴더로 이동됩니다.',
|
||||
};
|
||||
|
||||
@@ -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.',
|
||||
};
|
||||
|
||||
@@ -25,7 +25,11 @@ export const ru = {
|
||||
searchResultsFor: "Результаты поиска для",
|
||||
fromYourLibrary: "Из вашей библиотеки",
|
||||
noMatchingVideos: "В вашей библиотеке нет подходящих видео.",
|
||||
fromYouTube: "С YouTube",
|
||||
proxyOnlyApplyToYoutube: 'Прокси применяется только к Youtube',
|
||||
moveSubtitlesToVideoFolder: 'Расположение субтитров',
|
||||
moveSubtitlesToVideoFolderOn: 'Вместе с видео',
|
||||
moveSubtitlesToVideoFolderOff: 'В изолированной папке субтитров',
|
||||
moveSubtitlesToVideoFolderDescription: 'Если включено, файлы субтитров будут перемещены в ту же папку, что и видеофайл. Если отключено, они будут перемещены в изолированную папку субтитров.',
|
||||
loadingYouTubeResults: "Загрузка результатов YouTube...",
|
||||
noYouTubeResults: "Результаты YouTube не найдены",
|
||||
noVideosYet: "Видео пока нет. Отправьте URL видео, чтобы скачать первое!",
|
||||
|
||||
@@ -443,5 +443,9 @@ export const zh = {
|
||||
hide: "隐藏",
|
||||
reset: "重置",
|
||||
more: "更多",
|
||||
proxyOnlyApplyToYoutube: '代理仅应用于 Youtube',
|
||||
proxyOnlyApplyToYoutube: '代理仅应用于Youtube',
|
||||
moveSubtitlesToVideoFolder: '字幕位置',
|
||||
moveSubtitlesToVideoFolderOn: '与视频在同一文件夹',
|
||||
moveSubtitlesToVideoFolderOff: '在独立字幕文件夹',
|
||||
moveSubtitlesToVideoFolderDescription: '启用后,字幕文件将被移动到与视频文件相同的文件夹中。禁用后,它们将被移动到独立字幕文件夹。',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user