feat: Add support for deleting cookies

This commit is contained in:
Peifan Li
2025-12-05 15:40:00 -05:00
parent 61977c4ba3
commit c8aed79a0d
31 changed files with 486 additions and 111 deletions

View File

@@ -1,6 +1,6 @@
# MyTube
一个 YouTube/Bilibili/MissAV 视频下载和播放应用,支持频道订阅与自动下载,允许您将视频及其缩略图本地保存。将您的视频整理到收藏夹中,以便轻松访问和管理。现已支持[yt-dlp所有网址](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md##),包括微博,小红书,x.com等。
一个 YouTube/Bilibili/MissAV 视频下载和播放应用,支持频道订阅与自动下载,允许您将视频及其缩略图本地保存。将您的视频整理到收藏夹中,以便轻松访问和管理。现已支持[yt-dlp所有网址](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md##),包括微博,小红书,X.com等。
[![Discord](https://img.shields.io/badge/Discord-Join_Us-7289DA?logo=discord&logoColor=white)](https://discord.gg/tqucdh7a)
@@ -23,12 +23,10 @@
- **并发下载限制**:设置同时下载的数量限制以管理带宽。
- **本地库**:自动保存视频缩略图和元数据,提供丰富的浏览体验。
- **视频播放器**:自定义播放器,支持播放/暂停、循环、快进/快退、全屏和调光控制。
- **字幕**:自动下载 YouTube 默认语言字幕。
- **字幕**:自动下载 YouTube / Bilibili 默认语言字幕。
- **搜索功能**:支持在本地库中搜索视频,或在线搜索 YouTube 视频。
- **收藏夹**:创建自定义收藏夹以整理您的视频。
- **订阅功能**:订阅您喜爱的频道,并在新视频发布时自动下载。
- **现代化 UI**:响应式深色主题界面,包含“返回主页”功能和玻璃拟态效果。
- **主题支持**:支持在明亮和深色模式之间切换,支持平滑过渡。
- **登录保护**:通过密码登录页面保护您的应用。
- **国际化**:支持多种语言,包括英语、中文、西班牙语、法语、德语、日语、韩语、阿拉伯语和葡萄牙语。
- **分页功能**:支持分页浏览,高效管理大量视频。

View File

@@ -23,11 +23,9 @@ A YouTube/Bilibili/MissAV video downloader and player that supports channel subs
- **Concurrent Download Limit**: Set a limit on the number of simultaneous downloads to manage bandwidth.
- **Local Library**: Automatically save video thumbnails and metadata for a rich browsing experience.
- **Video Player**: Custom player with Play/Pause, Loop, Seek, Full-screen, and Dimming controls.
- **Auto Subtitles**: Automatically download YouTube default language subtitles.
- **Auto Subtitles**: Automatically download YouTube / Bilibili default language subtitles.
- **Search**: Search for videos locally in your library or online via YouTube.
- **Collections**: Organize videos into custom collections for easy access.
- **Modern UI**: Responsive, dark-themed interface with a "Back to Home" feature and glassmorphism effects.
- **Theme Support**: Toggle between Light and Dark modes with smooth transitions.
- **Login Protection**: Secure your application with a password login page.
- **Internationalization**: Support for multiple languages including English, Chinese, Spanish, French, German, Japanese, Korean, Arabic, and Portuguese.
- **Pagination**: Efficiently browse large libraries with pagination support.

View File

@@ -158,6 +158,21 @@ export const updateSettings = async (req: Request, res: Response) => {
}
};
export const getPasswordEnabled = async (_req: Request, res: Response) => {
try {
const settings = storageService.getSettings();
const mergedSettings = { ...defaultSettings, ...settings };
// Return true only if login is enabled AND a password is set
const isEnabled = mergedSettings.loginEnabled && !!mergedSettings.password;
res.json({ enabled: isEnabled });
} catch (error) {
console.error('Error checking password status:', error);
res.status(500).json({ error: 'Failed to check password status' });
}
};
export const verifyPassword = async (req: Request, res: Response) => {
try {
const { password } = req.body;
@@ -210,3 +225,32 @@ export const uploadCookies = async (req: Request, res: Response) => {
res.status(500).json({ error: 'Failed to upload cookies', details: error.message });
}
};
export const checkCookies = async (_req: Request, res: Response) => {
try {
const { DATA_DIR } = require('../config/paths');
const cookiesPath = path.join(DATA_DIR, 'cookies.txt');
const exists = fs.existsSync(cookiesPath);
res.json({ exists });
} catch (error) {
console.error('Error checking cookies:', error);
res.status(500).json({ error: 'Failed to check cookies' });
}
};
export const deleteCookies = async (_req: Request, res: Response) => {
try {
const { DATA_DIR } = require('../config/paths');
const cookiesPath = path.join(DATA_DIR, 'cookies.txt');
if (fs.existsSync(cookiesPath)) {
fs.unlinkSync(cookiesPath);
res.json({ success: true, message: 'Cookies deleted successfully' });
} else {
res.status(404).json({ error: 'Cookies file not found' });
}
} catch (error) {
console.error('Error deleting cookies:', error);
res.status(500).json({ error: 'Failed to delete cookies' });
}
};

View File

@@ -1,16 +1,19 @@
import express from 'express';
import multer from 'multer';
import os from 'os';
import { deleteLegacyData, getSettings, migrateData, updateSettings, uploadCookies, verifyPassword } from '../controllers/settingsController';
import { checkCookies, deleteCookies, deleteLegacyData, getPasswordEnabled, getSettings, migrateData, updateSettings, uploadCookies, verifyPassword } from '../controllers/settingsController';
const router = express.Router();
const upload = multer({ dest: os.tmpdir() });
router.get('/', getSettings);
router.post('/', updateSettings);
router.get('/password-enabled', getPasswordEnabled);
router.post('/verify-password', verifyPassword);
router.post('/migrate', migrateData);
router.post('/delete-legacy', deleteLegacyData);
router.post('/upload-cookies', upload.single('file'), uploadCookies);
router.post('/delete-cookies', deleteCookies);
router.get('/check-cookies', checkCookies);
export default router;

View File

@@ -1,4 +1,4 @@
import { Box, CssBaseline, ThemeProvider } from '@mui/material';
import { Box, CircularProgress, CssBaseline, ThemeProvider } from '@mui/material';
import { useEffect, useMemo, useState } from 'react';
import { Route, BrowserRouter as Router, Routes } from 'react-router-dom';
import './App.css';
@@ -72,7 +72,17 @@ function AppContent() {
<CssBaseline />
{!isAuthenticated && loginRequired ? (
checkingAuth ? (
<div className="loading">Loading...</div>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '100vh',
bgcolor: 'background.default'
}}
>
<CircularProgress size={48} />
</Box>
) : (
<LoginPage />
)

View File

@@ -15,17 +15,17 @@ import {
Typography
} from '@mui/material';
import React, { useState } from 'react';
import { useLanguage } from '../../contexts/LanguageContext';
import { Collection } from '../../types';
import { useLanguage } from '../contexts/LanguageContext';
import { Collection } from '../types';
interface CollectionModalProps {
open: boolean;
onClose: () => void;
videoCollections: Collection[];
collections: Collection[];
onAddToCollection: (collectionId: string) => Promise<void>;
onCreateCollection: (name: string) => Promise<void>;
onRemoveFromCollection: () => void;
videoCollections?: Collection[];
collections?: Collection[];
onAddToCollection?: (collectionId: string) => Promise<void>;
onCreateCollection?: (name: string) => Promise<void>;
onRemoveFromCollection?: () => void;
}
const CollectionModal: React.FC<CollectionModalProps> = ({
@@ -48,13 +48,13 @@ const CollectionModal: React.FC<CollectionModalProps> = ({
};
const handleCreate = async () => {
if (!newCollectionName.trim()) return;
if (!newCollectionName.trim() || !onCreateCollection) return;
await onCreateCollection(newCollectionName);
handleClose();
};
const handleAdd = async () => {
if (!selectedCollection) return;
if (!selectedCollection || !onAddToCollection) return;
await onAddToCollection(selectedCollection);
handleClose();
};
@@ -63,7 +63,7 @@ const CollectionModal: React.FC<CollectionModalProps> = ({
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
<DialogTitle>{t('addToCollection')}</DialogTitle>
<DialogContent dividers>
{videoCollections.length > 0 && (
{videoCollections && videoCollections.length > 0 && onRemoveFromCollection && (
<Alert severity="info" sx={{ mb: 3 }} action={
<Button color="error" size="small" onClick={() => {
onRemoveFromCollection();
@@ -79,7 +79,7 @@ const CollectionModal: React.FC<CollectionModalProps> = ({
</Alert>
)}
{collections && collections.length > 0 && (
{collections && collections.length > 0 && onAddToCollection && (
<Box sx={{ mb: 4 }}>
<Typography variant="subtitle2" gutterBottom>{t('addToExistingCollection')}</Typography>
<Stack direction="row" spacing={2}>
@@ -94,9 +94,9 @@ const CollectionModal: React.FC<CollectionModalProps> = ({
<MenuItem
key={collection.id}
value={collection.id}
disabled={videoCollections.length > 0 && videoCollections[0].id === collection.id}
disabled={videoCollections && videoCollections.length > 0 && videoCollections[0].id === collection.id}
>
{collection.name} {videoCollections.length > 0 && videoCollections[0].id === collection.id ? t('current') : ''}
{collection.name} {videoCollections && videoCollections.length > 0 && videoCollections[0].id === collection.id ? t('current') : ''}
</MenuItem>
))}
</Select>
@@ -112,6 +112,7 @@ const CollectionModal: React.FC<CollectionModalProps> = ({
</Box>
)}
{onCreateCollection && (
<Box>
<Typography variant="subtitle2" gutterBottom>{t('createNewCollection')}</Typography>
<Stack direction="row" spacing={2}>
@@ -132,6 +133,7 @@ const CollectionModal: React.FC<CollectionModalProps> = ({
</Button>
</Stack>
</Box>
)}
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="inherit">{t('cancel')}</Button>

View File

@@ -20,7 +20,7 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({ debugMode, onDebugM
return (
<Box>
<Typography variant="h6" gutterBottom>{t('debugMode')}</Typography>
<Typography variant="body2" color="text.secondary" paragraph>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{t('debugModeDescription')}
</Typography>
<FormControlLabel

View File

@@ -1,8 +1,10 @@
import { CloudUpload } from '@mui/icons-material';
import { Box, Button, Typography } from '@mui/material';
import { CheckCircle, CloudUpload, Delete, ErrorOutline } from '@mui/icons-material';
import { Alert, Box, Button, CircularProgress, Typography } from '@mui/material';
import { useMutation, useQuery } from '@tanstack/react-query';
import axios from 'axios';
import React from 'react';
import React, { useState } from 'react';
import { useLanguage } from '../../contexts/LanguageContext';
import ConfirmationModal from '../ConfirmationModal';
const API_URL = import.meta.env.VITE_API_URL;
@@ -32,20 +34,57 @@ const CookieSettings: React.FC<CookieSettingsProps> = ({ onSuccess, onError }) =
'Content-Type': 'multipart/form-data'
}
});
onSuccess(t('cookiesUploadedSuccess') || 'Cookies uploaded successfully');
handleSuccess(t('cookiesUploadedSuccess') || 'Cookies uploaded successfully');
} catch (error) {
console.error('Error uploading cookies:', error);
onError(t('cookiesUploadFailed') || 'Failed to upload cookies');
}
// Reset input
e.target.value = '';
};
const { data: cookieStatus, refetch: refetchCookieStatus, isLoading } = useQuery({
queryKey: ['cookieStatus'],
queryFn: async () => {
const response = await axios.get(`${API_URL}/settings/check-cookies`);
return response.data;
}
});
const handleSuccess = (msg: string) => {
onSuccess(msg);
refetchCookieStatus();
};
// Delete mutation
const deleteMutation = useMutation({
mutationFn: async () => {
const response = await axios.post(`${API_URL}/settings/delete-cookies`);
return response.data;
},
onSuccess: () => {
onSuccess(t('cookiesDeletedSuccess') || 'Cookies deleted successfully');
refetchCookieStatus();
},
onError: () => {
onError(t('cookiesDeleteFailed') || 'Failed to delete cookies');
}
});
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const handleDelete = () => {
setShowDeleteConfirm(true);
};
const confirmDelete = () => {
deleteMutation.mutate();
setShowDeleteConfirm(false);
};
return (
<Box>
<Typography variant="h6" gutterBottom>{t('cookieSettings') || 'Cookie Settings'}</Typography>
<Typography variant="body2" color="text.secondary" paragraph>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{t('cookieUploadDescription') || 'Upload cookies.txt to pass YouTube bot checks and enable Bilibili subtitle downloads. The file will be renamed to cookies.txt automatically. (Example: use "Get cookies.txt LOCALLY" extension to export cookies)'}
</Typography>
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
@@ -62,7 +101,44 @@ const CookieSettings: React.FC<CookieSettingsProps> = ({ onSuccess, onError }) =
onChange={handleFileUpload}
/>
</Button>
{cookieStatus?.exists && (
<Box>
<Button
variant="outlined"
color="error"
startIcon={<Delete />}
onClick={handleDelete}
disabled={deleteMutation.isPending}
>
{t('deleteCookies') || 'Delete Cookies'}
</Button>
</Box>
)}
{isLoading ? (
<CircularProgress size={24} />
) : cookieStatus?.exists ? (
<Alert icon={<CheckCircle fontSize="inherit" />} severity="success">
{t('cookiesFound') || 'cookies.txt found'}
</Alert>
) : (
<Alert icon={<ErrorOutline fontSize="inherit" />} severity="warning">
{t('cookiesNotFound') || 'cookies.txt not found'}
</Alert>
)}
</Box>
<ConfirmationModal
isOpen={showDeleteConfirm}
onClose={() => setShowDeleteConfirm(false)}
onConfirm={confirmDelete}
title={t('deleteCookies') || 'Delete Cookies'}
message={t('confirmDeleteCookies') || 'Are you sure you want to delete the cookies file? This may affect downloading capabilities.'}
confirmText={t('delete') || 'Delete'}
cancelText={t('cancel') || 'Cancel'}
isDanger={true}
/>
</Box>
);
};

View File

@@ -14,7 +14,7 @@ const DatabaseSettings: React.FC<DatabaseSettingsProps> = ({ onMigrate, onDelete
return (
<Box>
<Typography variant="h6" gutterBottom>{t('database')}</Typography>
<Typography variant="body2" color="text.secondary" paragraph>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{t('migrateDataDescription')}
</Typography>
<Button
@@ -28,7 +28,7 @@ const DatabaseSettings: React.FC<DatabaseSettingsProps> = ({ onMigrate, onDelete
<Box sx={{ mt: 3 }}>
<Typography variant="h6" gutterBottom>{t('removeLegacyData')}</Typography>
<Typography variant="body2" color="text.secondary" paragraph>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{t('removeLegacyDataDescription')}
</Typography>
<Button

View File

@@ -40,7 +40,7 @@ const DownloadSettings: React.FC<DownloadSettingsProps> = ({
<Box sx={{ mt: 3 }}>
<Typography variant="h6" gutterBottom>{t('cleanupTempFiles')}</Typography>
<Typography variant="body2" color="text.secondary" paragraph>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{t('cleanupTempFilesDescription')}
</Typography>
{activeDownloadsCount > 0 && (

View File

@@ -25,7 +25,7 @@ const TagsSettings: React.FC<TagsSettingsProps> = ({ tags, onTagsChange }) => {
return (
<Box>
<Typography variant="h6" gutterBottom>{t('tagsManagement') || 'Tags Management'}</Typography>
<Typography variant="body2" color="text.secondary" paragraph>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{t('tagsManagementNote') || 'Please remember to click "Save Settings" after adding or removing tags to apply changes.'}
</Typography>
<Box sx={{ display: 'flex', gap: 1, mb: 2, flexWrap: 'wrap' }}>

View File

@@ -1,4 +1,5 @@
import {
Add,
Delete,
Folder
} from '@mui/icons-material';
@@ -16,9 +17,11 @@ import {
} from '@mui/material';
import { useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useCollection } from '../contexts/CollectionContext';
import { useLanguage } from '../contexts/LanguageContext';
import { Collection, Video } from '../types';
import { formatDuration, parseDuration } from '../utils/formatUtils';
import CollectionModal from './CollectionModal';
import ConfirmationModal from './ConfirmationModal';
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL;
@@ -140,6 +143,33 @@ const VideoCard: React.FC<VideoCardProps> = ({
// Collections Logic (State and Handlers)
const { collections: allCollections, addToCollection, createCollection, removeFromCollection } = useCollection();
const [showCollectionModal, setShowCollectionModal] = useState(false);
const handleAddToCollection = async (collectionId: string) => {
if (!video.id) return;
await addToCollection(collectionId, video.id);
};
const handleCreateCollection = async (name: string) => {
if (!video.id) return;
await createCollection(name, video.id);
};
const handleRemoveFromCollection = async () => {
if (!video.id) return;
await removeFromCollection(video.id);
};
const handleAddClick = (e: React.MouseEvent) => {
e.stopPropagation();
setShowCollectionModal(true);
};
// Calculate collections that contain THIS video
const currentVideoCollections = allCollections.filter(c => c.videos.includes(video.id));
return (
<>
<Card
@@ -154,6 +184,9 @@ const VideoCard: React.FC<VideoCardProps> = ({
boxShadow: theme.shadows[8],
'& .delete-btn': {
opacity: 1
},
'& .add-btn': {
opacity: 1
}
},
border: isFirstInAnyCollection ? `1px solid ${theme.palette.primary.main}` : 'none'
@@ -263,6 +296,8 @@ const VideoCard: React.FC<VideoCardProps> = ({
sx={{ position: 'absolute', top: 8, left: 8 }}
/>
)}
</Box>
<CardContent sx={{ flexGrow: 1, p: 2 }}>
@@ -318,12 +353,36 @@ const VideoCard: React.FC<VideoCardProps> = ({
transition: 'opacity 0.2s',
'&:hover': {
bgcolor: 'error.main',
}
},
zIndex: 2
}}
>
<Delete fontSize="small" />
</IconButton>
)}
{!isMobile && (
<IconButton
className="add-btn"
onClick={handleAddClick}
size="small"
sx={{
position: 'absolute',
top: 8,
right: showDeleteButton ? 40 : 8, // Shift left if delete button is present
bgcolor: 'rgba(0,0,0,0.6)',
color: 'white',
opacity: 0, // Hidden by default, shown on hover
transition: 'opacity 0.2s',
'&:hover': {
bgcolor: 'primary.main',
},
zIndex: 2
}}
>
<Add fontSize="small" />
</IconButton>
)}
</Card>
<ConfirmationModal
@@ -336,6 +395,16 @@ const VideoCard: React.FC<VideoCardProps> = ({
cancelText={t('cancel')}
isDanger={true}
/>
<CollectionModal
open={showCollectionModal}
onClose={() => setShowCollectionModal(false)}
videoCollections={currentVideoCollections}
collections={allCollections}
onAddToCollection={handleAddToCollection}
onCreateCollection={handleCreateCollection}
onRemoveFromCollection={handleRemoveFromCollection}
/>
</>
);
};

View File

@@ -324,7 +324,7 @@ const VideoInfo: React.FC<VideoInfoProps> = ({
<Tooltip title={t('addToCollection')}>
<Button
variant="outlined"
onClick={onAddToCollection}
onClick={() => onAddToCollection()}
sx={{ minWidth: 'auto', p: 1 }}
>
<Add />

View File

@@ -1,5 +1,6 @@
import { Alert, AlertColor, Snackbar } from '@mui/material';
import React, { createContext, ReactNode, useContext, useState } from 'react';
import { createContext, ReactNode, useContext, useState } from 'react';
import { SNACKBAR_AUTO_HIDE_DURATION } from '../utils/constants';
interface SnackbarContextType {
showSnackbar: (message: string, severity?: AlertColor) => void;
@@ -42,7 +43,7 @@ export const SnackbarProvider: React.FC<SnackbarProviderProps> = ({ children })
{children}
<Snackbar
open={open}
autoHideDuration={3000}
autoHideDuration={SNACKBAR_AUTO_HIDE_DURATION}
onClose={handleClose}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>

View File

@@ -49,9 +49,18 @@ export const VideoProvider: React.FC<{ children: React.ReactNode }> = ({ childre
const { data: videos = [], isLoading: videosLoading, error: videosError, refetch: refetchVideos } = useQuery({
queryKey: ['videos'],
queryFn: async () => {
console.log('Fetching videos from:', `${API_URL}/videos`);
try {
const response = await axios.get(`${API_URL}/videos`);
console.log('Videos fetch success');
return response.data as Video[];
} catch (err) {
console.error('Videos fetch failed:', err);
throw err;
}
},
retry: 10,
retryDelay: 1000,
});
// Tags Query
@@ -61,6 +70,8 @@ export const VideoProvider: React.FC<{ children: React.ReactNode }> = ({ childre
const response = await axios.get(`${API_URL}/settings`);
return response.data.tags || [];
},
retry: 10,
retryDelay: 1000,
});
const [selectedTags, setSelectedTags] = useState<string[]>([]);

View File

@@ -239,8 +239,8 @@ const Home: React.FC = () => {
minWidth: 'auto',
p: 1,
display: { xs: 'none', md: 'inline-flex' },
color: 'text.primary',
borderColor: 'text.primary',
color: 'text.secondary',
borderColor: 'text.secondary',
}}
>
<ViewSidebar sx={{ transform: 'rotate(180deg)' }} />

View File

@@ -13,7 +13,7 @@ const InstructionPage: React.FC = () => {
<Typography variant="h5" gutterBottom sx={{ fontWeight: 'bold', color: 'primary.main' }}>
{t('instructionSection1Title')}
</Typography>
<Typography variant="body1" paragraph color="text.secondary">
<Typography variant="body1" sx={{ mb: 2 }} color="text.secondary">
{t('instructionSection1Desc')}
</Typography>
@@ -80,7 +80,7 @@ const InstructionPage: React.FC = () => {
<Typography variant="h5" gutterBottom sx={{ fontWeight: 'bold', color: 'primary.main' }}>
{t('instructionSection2Title')}
</Typography>
<Typography variant="body1" paragraph color="text.secondary">
<Typography variant="body1" sx={{ mb: 2 }} color="text.secondary">
{t('instructionSection2Desc')}
</Typography>
@@ -108,7 +108,7 @@ const InstructionPage: React.FC = () => {
<Typography variant="h5" gutterBottom sx={{ fontWeight: 'bold', color: 'primary.main' }}>
{t('instructionSection3Title')}
</Typography>
<Typography variant="body1" paragraph color="text.secondary">
<Typography variant="body1" sx={{ mb: 2 }} color="text.secondary">
{t('instructionSection3Desc')}
</Typography>

View File

@@ -1,18 +1,19 @@
import { LockOutlined } from '@mui/icons-material';
import { ErrorOutline, LockOutlined } from '@mui/icons-material';
import {
Alert,
Avatar,
Box,
Button,
CircularProgress,
Container,
CssBaseline,
TextField,
ThemeProvider,
Typography
} from '@mui/material';
import { useMutation } from '@tanstack/react-query';
import { useMutation, useQuery } from '@tanstack/react-query';
import axios from 'axios';
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { useAuth } from '../contexts/AuthContext';
import { useLanguage } from '../contexts/LanguageContext';
import getTheme from '../theme';
@@ -25,6 +26,24 @@ const LoginPage: React.FC = () => {
const { t } = useLanguage();
const { login } = useAuth();
// Check backend connection and password status
const { data: statusData, isLoading: isCheckingConnection, isError: isConnectionError, refetch: retryConnection } = useQuery({
queryKey: ['healthCheck'],
queryFn: async () => {
const response = await axios.get(`${API_URL}/settings/password-enabled`, { timeout: 5000 });
return response.data;
},
retry: 1,
retryDelay: 1000,
});
// Auto-login if password is not enabled
useEffect(() => {
if (statusData && statusData.enabled === false) {
login();
}
}, [statusData, login]);
// Use dark theme for login page to match app style
const theme = getTheme('dark');
@@ -68,6 +87,37 @@ const LoginPage: React.FC = () => {
alignItems: 'center',
}}
>
{isCheckingConnection ? (
// Loading state while checking backend connection
<>
<CircularProgress sx={{ mb: 2 }} />
<Typography variant="body1" color="text.secondary">
{t('checkingConnection') || 'Checking connection...'}
</Typography>
</>
) : isConnectionError ? (
// Backend connection error state
<>
<Avatar sx={{ m: 1, bgcolor: 'error.main', width: 56, height: 56 }}>
<ErrorOutline fontSize="large" />
</Avatar>
<Typography component="h1" variant="h5" sx={{ mt: 2, mb: 1 }}>
{t('connectionError') || 'Connection Error'}
</Typography>
<Alert severity="error" sx={{ mt: 2, mb: 2, width: '100%' }}>
{t('backendConnectionFailed') || 'Unable to connect to the server. Please check if the backend is running and port is open, then try again.'}
</Alert>
<Button
variant="contained"
onClick={() => retryConnection()}
sx={{ mt: 2 }}
>
{t('retry') || 'Retry'}
</Button>
</>
) : (
// Normal login form
<>
<Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
<LockOutlined />
</Avatar>
@@ -104,6 +154,8 @@ const LoginPage: React.FC = () => {
{loginMutation.isPending ? t('verifying') : t('signIn')}
</Button>
</Box>
</>
)}
</Box>
</Container>
</ThemeProvider>

View File

@@ -30,6 +30,7 @@ import { useDownload } from '../contexts/DownloadContext';
import { useLanguage } from '../contexts/LanguageContext';
import { Settings } from '../types';
import ConsoleManager from '../utils/consoleManager';
import { SNACKBAR_AUTO_HIDE_DURATION } from '../utils/constants';
import { Language } from '../utils/translations';
const API_URL = import.meta.env.VITE_API_URL;
@@ -363,7 +364,7 @@ const SettingsPage: React.FC = () => {
<Snackbar
open={!!message}
autoHideDuration={6000}
autoHideDuration={SNACKBAR_AUTO_HIDE_DURATION}
onClose={() => setMessage(null)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>

View File

@@ -10,8 +10,8 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import CollectionModal from '../components/CollectionModal';
import ConfirmationModal from '../components/ConfirmationModal';
import CollectionModal from '../components/VideoPlayer/CollectionModal';
import CommentsSection from '../components/VideoPlayer/CommentsSection';
import UpNextSidebar from '../components/VideoPlayer/UpNextSidebar';
import VideoControls from '../components/VideoPlayer/VideoControls';

View File

@@ -0,0 +1 @@
export const SNACKBAR_AUTO_HIDE_DURATION = 6000;

View File

@@ -95,6 +95,12 @@ export const ar = {
onlyTxtFilesAllowed: "يسمح فقط بملفات .txt",
cookiesUploadedSuccess: "تم تحميل ملفات تعريف الارتباط بنجاح",
cookiesUploadFailed: "فشل تحميل ملفات تعريف الارتباط",
cookiesFound: "تم العثور على cookies.txt",
cookiesNotFound: "لم يتم العثور على cookies.txt",
deleteCookies: "حذف ملفات تعريف الارتباط",
confirmDeleteCookies: "هل أنت متأكد أنك تريد حذف ملف تعريف الارتباط؟ سيؤثر هذا على قدرتك على تنزيل مقاطع الفيديو المقيدة بالعمر أو الخاصة بالأعضاء فقط.",
cookiesDeletedSuccess: "تم حذف ملفات تعريف الارتباط بنجاح",
cookiesDeleteFailed: "فشل حذف ملفات تعريف الارتباط",
// Cloud Drive
cloudDriveSettings: "التخزين السحابي (OpenList)",
@@ -168,6 +174,7 @@ export const ar = {
rateThisVideo: "قيم هذا الفيديو",
enterFullscreen: "ملء الشاشة",
exitFullscreen: "خروج من ملء الشاشة",
share: "مشاركة",
editTitle: "تعديل العنوان",
titleUpdated: "تم تحديث العنوان بنجاح",
titleUpdateFailed: "فشل تحديث العنوان",
@@ -189,6 +196,10 @@ export const ar = {
incorrectPassword: "كلمة المرور غير صحيحة",
loginFailed: "فشل التحقق من كلمة المرور",
defaultPasswordHint: "كلمة المرور الافتراضية: 123",
checkingConnection: "جارٍ التحقق من الاتصال...",
connectionError: "خطأ في الاتصال",
backendConnectionFailed: "تعذر الاتصال بالخادم. يرجى التحقق من تشغيل الخادم الخلفي ومن أن المنفذ مفتوح، ثم أعد المحاولة.",
retry: "إعادة المحاولة",
// Collection Page
loadingCollection: "جاري تحميل المجموعة...",

View File

@@ -54,6 +54,12 @@ export const de = {
onlyTxtFilesAllowed: "Nur .txt-Dateien erlaubt",
cookiesUploadedSuccess: "Cookies erfolgreich hochgeladen",
cookiesUploadFailed: "Fehler beim Hochladen der Cookies",
cookiesFound: "cookies.txt gefunden",
cookiesNotFound: "cookies.txt nicht gefunden",
deleteCookies: "Cookies löschen",
confirmDeleteCookies: "Sind Sie sicher, dass Sie die Cookie-Datei löschen möchten? Dies beeinträchtigt Ihre Fähigkeit, altersbeschränkte oder nur für Mitglieder zugängliche Videos herunterzuladen.",
cookiesDeletedSuccess: "Cookies erfolgreich gelöscht",
cookiesDeleteFailed: "Fehler beim Löschen der Cookies",
// Cloud Drive
cloudDriveSettings: "Cloud-Speicher (OpenList)",
@@ -85,7 +91,8 @@ export const de = {
create: "Erstellen", removeFromCollection: "Aus Sammlung Entfernen",
confirmRemoveFromCollection: "Sind Sie sicher, dass Sie dieses Video aus der Sammlung entfernen möchten?", remove: "Entfernen",
loadingVideo: "Video wird geladen...", current: "(Aktuell)", rateThisVideo: "Dieses Video bewerten",
enterFullscreen: "Vollbild", exitFullscreen: "Vollbild Verlassen", editTitle: "Titel Bearbeiten",
enterFullscreen: "Vollbild", exitFullscreen: "Vollbild Verlassen",
share: "Teilen", editTitle: "Titel Bearbeiten",
titleUpdated: "Titel erfolgreich aktualisiert", titleUpdateFailed: "Fehler beim Aktualisieren des Titels",
refreshThumbnail: "Vorschaubild aktualisieren",
thumbnailRefreshed: "Vorschaubild erfolgreich aktualisiert",
@@ -100,6 +107,10 @@ export const de = {
searchCancelled: "Suche abgebrochen",
signIn: "Anmelden", verifying: "Überprüfen...", incorrectPassword: "Falsches Passwort",
loginFailed: "Fehler beim Überprüfen des Passworts", defaultPasswordHint: "Standardpasswort: 123",
checkingConnection: "Verbindung wird überprüft...",
connectionError: "Verbindungsfehler",
backendConnectionFailed: "Verbindung zum Server nicht möglich. Bitte überprüfen Sie, ob das Backend läuft und der Port geöffnet ist, und versuchen Sie es erneut.",
retry: "Erneut versuchen",
loadingCollection: "Sammlung wird geladen...", collectionNotFound: "Sammlung nicht gefunden",
noVideosInCollection: "Keine Videos in dieser Sammlung.", back: "Zurück", loadVideosError: "Fehler beim Laden der Videos. Bitte versuchen Sie es später erneut.",
unknownAuthor: "Unbekannt", noVideosForAuthor: "Keine Videos für diesen Autor gefunden.",

View File

@@ -96,6 +96,12 @@ export const en = {
onlyTxtFilesAllowed: "Only .txt files are allowed",
cookiesUploadedSuccess: "Cookies uploaded successfully",
cookiesUploadFailed: "Failed to upload cookies",
cookiesFound: "cookies.txt found",
cookiesNotFound: "cookies.txt not found",
deleteCookies: "Delete Cookies",
confirmDeleteCookies: "Are you sure you want to delete the cookies file? This will affect your ability to download age-restricted or member-only videos.",
cookiesDeletedSuccess: "Cookies deleted successfully",
cookiesDeleteFailed: "Failed to delete cookies",
// Cloud Drive
cloudDriveSettings: "Cloud Drive (OpenList)",
@@ -191,6 +197,10 @@ export const en = {
incorrectPassword: "Incorrect password",
loginFailed: "Failed to verify password",
defaultPasswordHint: "Default password: 123",
checkingConnection: "Checking connection...",
connectionError: "Connection Error",
backendConnectionFailed: "Unable to connect to the server. Please check if the backend is running and port is open, then try again.",
retry: "Retry",
// Collection Page
loadingCollection: "Loading collection...",

View File

@@ -51,7 +51,13 @@ export const es = {
uploadCookies: "Subir Cookies",
onlyTxtFilesAllowed: "Solo se permiten archivos .txt",
cookiesUploadedSuccess: "Cookies subidas con éxito",
cookiesUploadFailed: "Error al subir cookies",
cookiesUploadFailed: "Error al cargar cookies",
cookiesFound: "cookies.txt encontrado",
cookiesNotFound: "cookies.txt no encontrado",
deleteCookies: "Eliminar Cookies",
confirmDeleteCookies: "¿Estás seguro de que deseas eliminar el archivo de cookies? Esto afectará tu capacidad para descargar videos con restricción de edad o solo para miembros.",
cookiesDeletedSuccess: "Cookies eliminadas con éxito",
cookiesDeleteFailed: "Error al eliminar cookies",
// Cloud Drive
cloudDriveSettings: "Almacenamiento en la Nube (OpenList)",
@@ -93,7 +99,8 @@ export const es = {
create: "Crear", removeFromCollection: "Eliminar de la Colección",
confirmRemoveFromCollection: "¿Está seguro de que desea eliminar este video de la colección?", remove: "Eliminar",
loadingVideo: "Cargando video...", current: "(Actual)", rateThisVideo: "Calificar este video",
enterFullscreen: "Pantalla Completa", exitFullscreen: "Salir de Pantalla Completa", editTitle: "Editar Título",
enterFullscreen: "Pantalla Completa", exitFullscreen: "Salir de Pantalla Completa",
share: "Compartir", editTitle: "Editar Título",
titleUpdated: "Título actualizado exitosamente", titleUpdateFailed: "Error al actualizar el título",
refreshThumbnail: "Actualizar miniatura",
thumbnailRefreshed: "Miniatura actualizada con éxito",
@@ -108,6 +115,10 @@ export const es = {
searchCancelled: "Búsqueda cancelada",
signIn: "Iniciar Sesión", verifying: "Verificando...", incorrectPassword: "Contraseña incorrecta",
loginFailed: "Error al verificar la contraseña", defaultPasswordHint: "Contraseña predeterminada: 123",
checkingConnection: "Comprobando conexión...",
connectionError: "Error de Conexión",
backendConnectionFailed: "No se puede conectar al servidor. Verifique que el backend esté en ejecución y que el puerto esté abierto, luego intente de nuevo.",
retry: "Reintentar",
loadingCollection: "Cargando colección...", collectionNotFound: "Colección no encontrada",
noVideosInCollection: "No hay videos en esta colección.", back: "Volver", loadVideosError: "Error al cargar los videos. Por favor, inténtelo más tarde.",
unknownAuthor: "Desconocido", noVideosForAuthor: "No se encontraron videos para este autor.",

View File

@@ -95,6 +95,12 @@ export const fr = {
onlyTxtFilesAllowed: "Seuls les fichiers .txt sont autorisés",
cookiesUploadedSuccess: "Cookies téléchargés avec succès",
cookiesUploadFailed: "Échec du téléchargement des cookies",
cookiesFound: "cookies.txt trouvé",
cookiesNotFound: "cookies.txt introuvable",
deleteCookies: "Supprimer les cookies",
confirmDeleteCookies: "Êtes-vous sûr de vouloir supprimer le fichier de cookies ? Cela affectera votre capacité à télécharger des vidéos soumises à une limite d'âge ou réservées aux membres.",
cookiesDeletedSuccess: "Cookies supprimés avec succès",
cookiesDeleteFailed: "Échec de la suppression des cookies",
// Cloud Drive
cloudDriveSettings: "Stockage Cloud (OpenList)",
@@ -177,6 +183,7 @@ export const fr = {
rateThisVideo: "Noter cette vidéo",
enterFullscreen: "Plein écran",
exitFullscreen: "Quitter le plein écran",
share: "Partager",
editTitle: "Modifier le titre",
titleUpdated: "Titre mis à jour avec succès",
titleUpdateFailed: "Échec de la mise à jour du titre",
@@ -197,6 +204,10 @@ export const fr = {
incorrectPassword: "Mot de passe incorrect",
loginFailed: "Échec de la vérification du mot de passe",
defaultPasswordHint: "Mot de passe par défaut : 123",
checkingConnection: "Vérification de la connexion...",
connectionError: "Erreur de Connexion",
backendConnectionFailed: "Impossible de se connecter au serveur. Veuillez vérifier que le backend est en cours d'exécution et que le port est ouvert, puis réessayez.",
retry: "Réessayer",
// Collection Page
loadingCollection: "Chargement de la collection...",

View File

@@ -94,7 +94,13 @@ export const ja = {
uploadCookies: "Cookieをアップロード",
onlyTxtFilesAllowed: ".txtファイルのみ許可されています",
cookiesUploadedSuccess: "Cookieが正常にアップロードされました",
cookiesUploadFailed: "Cookieのアップロードに失敗しました",
cookiesUploadFailed: "Cookiesのアップロードに失敗しました",
cookiesFound: "cookies.txtが見つかりました",
cookiesNotFound: "cookies.txtが見つかりません",
deleteCookies: "Cookiesを削除",
confirmDeleteCookies: "cookiesファイルを削除してもよろしいですか年齢制限のある動画やメンバー限定動画のダウンロードに影響する可能性があります。",
cookiesDeletedSuccess: "Cookiesが正常に削除されました",
cookiesDeleteFailed: "Cookiesの削除に失敗しました",
// Cloud Drive
cloudDriveSettings: "クラウドストレージ (OpenList)",
@@ -166,6 +172,7 @@ export const ja = {
rateThisVideo: "この動画を評価",
enterFullscreen: "全画面表示",
exitFullscreen: "全画面表示を終了",
share: "共有",
editTitle: "タイトルを編集",
titleUpdated: "タイトルが正常に更新されました",
titleUpdateFailed: "タイトルの更新に失敗しました",
@@ -187,6 +194,10 @@ export const ja = {
incorrectPassword: "パスワードが間違っています",
loginFailed: "パスワードの確認に失敗しました",
defaultPasswordHint: "デフォルトのパスワード: 123",
checkingConnection: "接続を確認中...",
connectionError: "接続エラー",
backendConnectionFailed: "サーバーに接続できません。バックエンドが実行されていること、およびポートが開いていることを確認してから、再試行してください。",
retry: "再試行",
// Collection Page
loadingCollection: "コレクションを読み込み中...",

View File

@@ -95,6 +95,12 @@ export const ko = {
onlyTxtFilesAllowed: ".txt 파일만 허용됩니다",
cookiesUploadedSuccess: "쿠키가 성공적으로 업로드되었습니다",
cookiesUploadFailed: "쿠키 업로드 실패",
cookiesFound: "cookies.txt를 찾았습니다",
cookiesNotFound: "cookies.txt를 찾을 수 없습니다",
deleteCookies: "쿠키 삭제",
confirmDeleteCookies: "쿠키 파일을 삭제하시겠습니까? 연령 제한 또는 회원 전용 비디오를 다운로드하는 데 영향을 줄 수 있습니다.",
cookiesDeletedSuccess: "쿠키가 성공적으로 삭제되었습니다",
cookiesDeleteFailed: "쿠키 삭제 실패",
// Cloud Drive
cloudDriveSettings: "클라우드 드라이브 (OpenList)",
@@ -168,6 +174,7 @@ export const ko = {
rateThisVideo: "이 동영상 평가",
enterFullscreen: "전체 화면",
exitFullscreen: "전체 화면 종료",
share: "공유",
editTitle: "제목 편집",
titleUpdated: "제목이 성공적으로 업데이트됨",
titleUpdateFailed: "제목 업데이트 실패",
@@ -189,6 +196,10 @@ export const ko = {
incorrectPassword: "비밀번호가 올바르지 않습니다",
loginFailed: "비밀번호 확인 실패",
defaultPasswordHint: "기본 비밀번호: 123",
checkingConnection: "연결 확인 중...",
connectionError: "연결 오류",
backendConnectionFailed: "서버에 연결할 수 없습니다. 백엔드가 실행 중이고 포트가 열려 있는지 확인한 후 다시 시도하세요.",
retry: "다시 시도",
// Collection Page
loadingCollection: "컬렉션 로드 중...",

View File

@@ -95,6 +95,12 @@ export const pt = {
onlyTxtFilesAllowed: "Apenas arquivos .txt são permitidos",
cookiesUploadedSuccess: "Cookies enviados com sucesso",
cookiesUploadFailed: "Falha ao enviar cookies",
cookiesFound: "cookies.txt encontrado",
cookiesNotFound: "cookies.txt não encontrado",
deleteCookies: "Excluir Cookies",
confirmDeleteCookies: "Tem certeza de que deseja excluir o arquivo de cookies? Isso afetará sua capacidade de baixar vídeos com restrição de idade ou exclusivos para membros.",
cookiesDeletedSuccess: "Cookies excluídos com sucesso",
cookiesDeleteFailed: "Falha ao excluir cookies",
// Cloud Drive
cloudDriveSettings: "Armazenamento em Nuvem (OpenList)",
@@ -168,6 +174,7 @@ export const pt = {
rateThisVideo: "Avaliar este vídeo",
enterFullscreen: "Tela Cheia",
exitFullscreen: "Sair da Tela Cheia",
share: "Compartilhar",
editTitle: "Editar Título",
titleUpdated: "Título atualizado com sucesso",
titleUpdateFailed: "Falha ao atualizar título",
@@ -197,6 +204,10 @@ export const pt = {
incorrectPassword: "Senha incorreta",
loginFailed: "Falha ao verificar senha",
defaultPasswordHint: "Senha padrão: 123",
checkingConnection: "Verificando conexão...",
connectionError: "Erro de Conexão",
backendConnectionFailed: "Não foi possível conectar ao servidor. Verifique se o backend está em execução e a porta está aberta, depois tente novamente.",
retry: "Tentar Novamente",
// Collection Page
loadingCollection: "Carregando coleção...",

View File

@@ -95,6 +95,12 @@ export const ru = {
onlyTxtFilesAllowed: "Разрешены только файлы .txt",
cookiesUploadedSuccess: "Cookie успешно загружены",
cookiesUploadFailed: "Не удалось загрузить cookie",
cookiesFound: "cookies.txt найден",
cookiesNotFound: "cookies.txt не найден",
deleteCookies: "Удалить Cookie",
confirmDeleteCookies: "Вы уверены, что хотите удалить файл cookie? Это повлияет на возможность скачивания видео с возрастными ограничениями или только для участников.",
cookiesDeletedSuccess: "Cookie успешно удалены",
cookiesDeleteFailed: "Не удалось удалить cookie",
// Cloud Drive
cloudDriveSettings: "Облачное хранилище (OpenList)",
@@ -168,6 +174,7 @@ export const ru = {
rateThisVideo: "Оценить это видео",
enterFullscreen: "На весь экран",
exitFullscreen: "Выйти из полноэкранного режима",
share: "Поделиться",
editTitle: "Редактировать название",
titleUpdated: "Название успешно обновлено",
titleUpdateFailed: "Не удалось обновить название",
@@ -189,6 +196,10 @@ export const ru = {
incorrectPassword: "Неверный пароль",
loginFailed: "Ошибка проверки пароля",
defaultPasswordHint: "Пароль по умолчанию: 123",
checkingConnection: "Проверка соединения...",
connectionError: "Ошибка соединения",
backendConnectionFailed: "Не удалось подключиться к серверу. Убедитесь, что сервер запущен и порт открыт, затем повторите попытку.",
retry: "Повторить",
// Collection Page
loadingCollection: "Загрузка коллекции...",

View File

@@ -95,7 +95,13 @@ export const zh = {
uploadCookies: "上传 Cookie",
onlyTxtFilesAllowed: "仅允许 .txt 文件",
cookiesUploadedSuccess: "Cookie 上传成功",
cookiesUploadFailed: "Cookie 上传失败",
cookiesUploadFailed: "上传 Cookies 失败",
cookiesFound: "已找到 cookies.txt",
cookiesNotFound: "未找到 cookies.txt",
deleteCookies: "删除 Cookies",
confirmDeleteCookies: "您确定要删除 cookies 文件吗?这将影响您下载有年龄限制或仅限会员视频的能力。",
cookiesDeletedSuccess: "Cookies 删除成功",
cookiesDeleteFailed: "删除 Cookies 失败",
// Cloud Drive
cloudDriveSettings: "云端存储 (OpenList)",
@@ -169,6 +175,7 @@ export const zh = {
rateThisVideo: "给视频评分",
enterFullscreen: "全屏",
exitFullscreen: "退出全屏",
share: "分享",
editTitle: "编辑标题",
titleUpdated: "标题更新成功",
titleUpdateFailed: "更新标题失败",
@@ -190,6 +197,10 @@ export const zh = {
incorrectPassword: "密码错误",
loginFailed: "验证密码失败",
defaultPasswordHint: "默认密码123",
checkingConnection: "正在检查连接...",
connectionError: "连接错误",
backendConnectionFailed: "无法连接到服务器。请检查后端是否正在运行并确保端口已开放,然后重试。",
retry: "重试",
// Collection Page
loadingCollection: "加载合集中...",