mirror of
https://github.com/hotheadhacker/seedbox-lite.git
synced 2025-09-02 00:51:36 +03:00
Implement authentication flow with login screen and context management
This commit is contained in:
@@ -899,3 +899,9 @@ body {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* Loading animation for authentication */
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@@ -1,14 +1,51 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
import { AuthProvider, useAuth } from './context/AuthContext';
|
||||
import Layout from './components/Layout';
|
||||
import HomePage from './components/HomePage';
|
||||
import TorrentPageNetflix from './components/TorrentPageNetflix';
|
||||
import RecentPage from './components/RecentPage';
|
||||
import SettingsPage from './components/SettingsPage';
|
||||
import CacheManagementPage from './components/CacheManagementPage';
|
||||
import LoginScreen from './components/LoginScreen';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
const AuthenticatedApp = () => {
|
||||
const { isAuthenticated, isLoading, authenticate } = useAuth();
|
||||
|
||||
// Show loading spinner while checking authentication
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100vh',
|
||||
background: 'linear-gradient(135deg, #0f0f23 0%, #1a1a2e 50%, #16213e 100%)',
|
||||
color: '#ffffff'
|
||||
}}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
border: '4px solid rgba(255, 255, 255, 0.2)',
|
||||
borderTop: '4px solid #e50914',
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 1s linear infinite',
|
||||
margin: '0 auto 16px'
|
||||
}}></div>
|
||||
<p>Loading Seedbox...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show login screen if not authenticated
|
||||
if (!isAuthenticated) {
|
||||
return <LoginScreen onAuthSuccess={authenticate} />;
|
||||
}
|
||||
|
||||
// Show main app if authenticated
|
||||
return (
|
||||
<Router>
|
||||
<Routes>
|
||||
@@ -22,6 +59,14 @@ function App() {
|
||||
</Routes>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<AuthenticatedApp />
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -34,7 +34,7 @@ const Layout = () => {
|
||||
const diskData = await diskResponse.json().catch(() => ({ percentage: 0, total: 0 }));
|
||||
|
||||
// Calculate cache usage percentage relative to total disk space
|
||||
const cacheSize = stats.cacheSize || 0;
|
||||
const cacheSize = stats.downloadedBytes || 0;
|
||||
const totalDisk = diskData.total || 1;
|
||||
const cacheUsagePercentage = totalDisk > 0 ? (cacheSize / totalDisk) * 100 : 0;
|
||||
|
||||
|
||||
268
client/src/components/LoginScreen.css
Normal file
268
client/src/components/LoginScreen.css
Normal file
@@ -0,0 +1,268 @@
|
||||
.login-screen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-background {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #0f0f23 0%, #1a1a2e 50%, #16213e 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-background::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(circle at 30% 20%, rgba(255, 0, 150, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 70% 80%, rgba(0, 255, 255, 0.1) 0%, transparent 50%);
|
||||
animation: floatingGradient 6s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes floatingGradient {
|
||||
0% { transform: translateY(0px) rotate(0deg); }
|
||||
100% { transform: translateY(-10px) rotate(2deg); }
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background: rgba(20, 20, 35, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 24px;
|
||||
padding: 48px 40px;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
box-shadow:
|
||||
0 25px 50px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
animation: slideUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.login-icon {
|
||||
color: #e50914;
|
||||
margin-bottom: 16px;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
color: #ffffff;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.password-input-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.password-input {
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 0 50px 0 48px;
|
||||
font-size: 16px;
|
||||
color: #ffffff;
|
||||
transition: all 0.3s ease;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.password-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.password-input:focus {
|
||||
border-color: #e50914;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 0 0 4px rgba(229, 9, 20, 0.1);
|
||||
}
|
||||
|
||||
.password-input:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.toggle-password {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.toggle-password:hover:not(:disabled) {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.toggle-password:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: rgba(229, 9, 20, 0.1);
|
||||
border: 1px solid rgba(229, 9, 20, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
color: #ff6b6b;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
animation: shake 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-4px); }
|
||||
75% { transform: translateX(4px); }
|
||||
}
|
||||
|
||||
.login-button {
|
||||
height: 56px;
|
||||
background: linear-gradient(135deg, #e50914 0%, #b8070f 100%);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 16px rgba(229, 9, 20, 0.3);
|
||||
}
|
||||
|
||||
.login-button:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #f31e2a 0%, #c7080f 100%);
|
||||
box-shadow: 0 6px 20px rgba(229, 9, 20, 0.4);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.login-button:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 8px rgba(229, 9, 20, 0.3);
|
||||
}
|
||||
|
||||
.login-button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: 0 4px 16px rgba(229, 9, 20, 0.2);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-top: 2px solid #ffffff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
margin-top: 32px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.login-footer p {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Mobile responsiveness */
|
||||
@media (max-width: 480px) {
|
||||
.login-container {
|
||||
margin: 16px;
|
||||
padding: 32px 24px;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.password-input,
|
||||
.login-button {
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark theme compatibility */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.login-background {
|
||||
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 50%, #0f0f0f 100%);
|
||||
}
|
||||
}
|
||||
115
client/src/components/LoginScreen.jsx
Normal file
115
client/src/components/LoginScreen.jsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Lock, Eye, EyeOff, Shield } from 'lucide-react';
|
||||
import { config } from '../config/environment';
|
||||
import './LoginScreen.css';
|
||||
|
||||
const LoginScreen = ({ onAuthSuccess }) => {
|
||||
const [password, setPassword] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!password.trim()) {
|
||||
setError('Please enter a password');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const response = await fetch(config.getApiUrl('/api/auth/login'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ password }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Store authentication in localStorage
|
||||
localStorage.setItem('seedbox_authenticated', 'true');
|
||||
localStorage.setItem('seedbox_auth_timestamp', Date.now().toString());
|
||||
|
||||
console.log('✅ Authentication successful, stored in localStorage');
|
||||
onAuthSuccess();
|
||||
} else {
|
||||
setError(data.error || 'Authentication failed');
|
||||
setPassword(''); // Clear password on failure
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Authentication error:', err);
|
||||
setError('Connection error. Please check if the server is running.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="login-screen">
|
||||
<div className="login-background">
|
||||
<div className="login-container">
|
||||
<div className="login-header">
|
||||
<Shield size={48} className="login-icon" />
|
||||
<h1>Seedbox Access</h1>
|
||||
<p>Enter your password to access the torrent dashboard</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="login-form">
|
||||
<div className="password-input-container">
|
||||
<Lock size={20} className="input-icon" />
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Enter password"
|
||||
className="password-input"
|
||||
disabled={loading}
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="toggle-password"
|
||||
disabled={loading}
|
||||
>
|
||||
{showPassword ? <EyeOff size={20} /> : <Eye size={20} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="error-message">
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="login-button"
|
||||
disabled={loading || !password.trim()}
|
||||
>
|
||||
{loading ? (
|
||||
<div className="loading-spinner" />
|
||||
) : (
|
||||
<>
|
||||
<Lock size={18} />
|
||||
Access Dashboard
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="login-footer">
|
||||
<p>🔒 Your session will be remembered on this device</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginScreen;
|
||||
@@ -513,3 +513,22 @@ input:disabled + .slider:hover {
|
||||
transform: translateX(16px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Security Section */
|
||||
.security-section {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.security-info {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.security-info p {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Settings, Trash2, Download, Globe, Shield, HardDrive, ExternalLink } from 'lucide-react';
|
||||
import { Settings, Trash2, Download, Globe, Shield, HardDrive, ExternalLink, LogOut } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { config } from '../config/environment';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import progressService from '../services/progressService';
|
||||
import './SettingsPage.css';
|
||||
|
||||
const SettingsPage = () => {
|
||||
const { logout } = useAuth();
|
||||
const [settings, setSettings] = useState({
|
||||
downloadPath: '/tmp/seedbox-downloads',
|
||||
maxConnections: 50,
|
||||
@@ -144,6 +146,12 @@ const SettingsPage = () => {
|
||||
event.target.value = ''; // Reset file input
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
if (window.confirm('Are you sure you want to logout? You will need to enter the password again to access the dashboard.')) {
|
||||
logout();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="settings-page">
|
||||
<div className="page-header">
|
||||
@@ -327,6 +335,22 @@ const SettingsPage = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Security */}
|
||||
<div className="settings-section">
|
||||
<h2>🔐 Security</h2>
|
||||
<div className="security-section">
|
||||
<div className="security-info">
|
||||
<p>Your authentication is stored locally on this device for convenience. You can logout to require password entry on next access.</p>
|
||||
</div>
|
||||
<div className="action-buttons">
|
||||
<button onClick={handleLogout} className="action-button danger">
|
||||
<LogOut size={16} />
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* About */}
|
||||
<div className="settings-section">
|
||||
<h2>ℹ️ About</h2>
|
||||
|
||||
84
client/src/context/AuthContext.jsx
Normal file
84
client/src/context/AuthContext.jsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import React, { createContext, useState, useContext, useEffect, useCallback } from 'react';
|
||||
|
||||
const AuthContext = createContext();
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const AuthProvider = ({ children }) => {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const clearAuth = useCallback(() => {
|
||||
localStorage.removeItem('seedbox_authenticated');
|
||||
localStorage.removeItem('seedbox_auth_timestamp');
|
||||
setIsAuthenticated(false);
|
||||
console.log('🚪 Authentication cleared');
|
||||
}, []);
|
||||
|
||||
const checkAuthStatus = useCallback(() => {
|
||||
try {
|
||||
const authStatus = localStorage.getItem('seedbox_authenticated');
|
||||
const authTimestamp = localStorage.getItem('seedbox_auth_timestamp');
|
||||
|
||||
if (authStatus === 'true' && authTimestamp) {
|
||||
// Check if authentication is still valid (optional: add expiration logic here)
|
||||
const timestamp = parseInt(authTimestamp);
|
||||
const now = Date.now();
|
||||
|
||||
// Authentication expires after 30 days (optional)
|
||||
const EXPIRY_TIME = 30 * 24 * 60 * 60 * 1000; // 30 days in milliseconds
|
||||
|
||||
if (now - timestamp < EXPIRY_TIME) {
|
||||
setIsAuthenticated(true);
|
||||
console.log('✅ Found valid authentication in localStorage');
|
||||
} else {
|
||||
// Clear expired authentication
|
||||
clearAuth();
|
||||
console.log('⏰ Authentication expired, cleared localStorage');
|
||||
}
|
||||
} else {
|
||||
console.log('❌ No valid authentication found in localStorage');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking auth status:', error);
|
||||
clearAuth();
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [clearAuth]);
|
||||
|
||||
useEffect(() => {
|
||||
checkAuthStatus();
|
||||
}, [checkAuthStatus]);
|
||||
|
||||
const authenticate = () => {
|
||||
setIsAuthenticated(true);
|
||||
console.log('<27> User authenticated successfully');
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
clearAuth();
|
||||
// Optionally redirect to login or refresh page
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const value = {
|
||||
isAuthenticated,
|
||||
isLoading,
|
||||
authenticate,
|
||||
logout,
|
||||
clearAuth
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -650,6 +650,35 @@ app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Authentication endpoint
|
||||
app.post('/api/auth/login', (req, res) => {
|
||||
const { password } = req.body;
|
||||
const correctPassword = process.env.ACCESS_PASSWORD || 'seedbox123';
|
||||
|
||||
console.log(`🔐 Login attempt with password: ${password ? '[PROVIDED]' : '[MISSING]'}`);
|
||||
|
||||
if (!password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Password is required'
|
||||
});
|
||||
}
|
||||
|
||||
if (password === correctPassword) {
|
||||
console.log('✅ Authentication successful');
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'Authentication successful'
|
||||
});
|
||||
} else {
|
||||
console.log('❌ Authentication failed - incorrect password');
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'Invalid password'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// UNIVERSAL ADD TORRENT - Always succeeds
|
||||
app.post('/api/torrents', async (req, res) => {
|
||||
const { torrentId } = req.body;
|
||||
@@ -1254,40 +1283,19 @@ app.delete('/api/torrents', (req, res) => {
|
||||
// Cache stats
|
||||
app.get('/api/cache/stats', async (req, res) => {
|
||||
try {
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const activeTorrents = client.torrents.length;
|
||||
|
||||
const activeTorrents = Object.keys(torrents).length;
|
||||
|
||||
// Function to get directory size
|
||||
const getDirectorySize = async (dirPath) => {
|
||||
let size = 0;
|
||||
try {
|
||||
const items = await fs.readdir(dirPath);
|
||||
for (const item of items) {
|
||||
const itemPath = path.join(dirPath, item);
|
||||
const stats = await fs.stat(itemPath);
|
||||
if (stats.isDirectory()) {
|
||||
size += await getDirectorySize(itemPath);
|
||||
} else {
|
||||
size += stats.size;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Error calculating directory size:', error.message);
|
||||
}
|
||||
return size;
|
||||
};
|
||||
|
||||
// Calculate actual cache size from download directory
|
||||
const downloadDir = path.join(__dirname, 'downloads');
|
||||
// Calculate actual cache size from WebTorrent client data
|
||||
let cacheSize = 0;
|
||||
try {
|
||||
cacheSize = await getDirectorySize(downloadDir);
|
||||
} catch (error) {
|
||||
console.warn('Could not calculate cache size:', error.message);
|
||||
}
|
||||
let downloadedBytes = 0;
|
||||
|
||||
client.torrents.forEach(torrent => {
|
||||
// Add total size of each torrent
|
||||
cacheSize += torrent.length || 0;
|
||||
// Add downloaded bytes
|
||||
downloadedBytes += torrent.downloaded || 0;
|
||||
});
|
||||
|
||||
const formatBytes = (bytes) => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
@@ -1295,14 +1303,17 @@ app.get('/api/cache/stats', async (req, res) => {
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
|
||||
const stats = {
|
||||
totalSizeFormatted: formatBytes(cacheSize),
|
||||
totalSizeFormatted: formatBytes(downloadedBytes), // Use actual downloaded data
|
||||
totalSize: downloadedBytes,
|
||||
activeTorrents,
|
||||
cacheSize: cacheSize
|
||||
cacheSize: downloadedBytes, // Use downloaded bytes for cache calculation
|
||||
totalTorrentSize: cacheSize, // Total size of all torrents
|
||||
totalTorrentSizeFormatted: formatBytes(cacheSize)
|
||||
};
|
||||
|
||||
console.log(`📊 Cache stats: ${formatBytes(cacheSize)} (${activeTorrents} torrents)`);
|
||||
|
||||
console.log(`📊 Cache stats: ${formatBytes(downloadedBytes)} downloaded (${activeTorrents} torrents, ${formatBytes(cacheSize)} total)`);
|
||||
res.json(stats);
|
||||
} catch (error) {
|
||||
console.error('Error getting cache stats:', error);
|
||||
|
||||
Reference in New Issue
Block a user