mirror of
https://github.com/hotheadhacker/seedbox-lite.git
synced 2025-09-02 00:51:36 +03:00
Add episode download functionality and enhance UI controls for Torrent Page
This commit is contained in:
@@ -274,6 +274,7 @@
|
||||
|
||||
.netflix-episode {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
background: rgba(42, 42, 42, 0.6);
|
||||
@@ -318,6 +319,32 @@
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.netflix-episode-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.netflix-episode-download {
|
||||
background: rgba(229, 9, 20, 0.9);
|
||||
color: #fff;
|
||||
border: none;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.netflix-episode-download:hover {
|
||||
background: #e50914;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.netflix-progress-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
@@ -339,6 +366,24 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.netflix-episode-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.netflix-episode-actions .netflix-file-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.netflix-episode-actions .netflix-file-icon:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.netflix-episode-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -396,13 +441,20 @@
|
||||
.netflix-file-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: rgba(109, 109, 110, 0.3);
|
||||
background: rgba(229, 9, 20, 0.8);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #b3b3b3;
|
||||
color: #ffffff;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.netflix-file-icon:hover {
|
||||
background: #e50914;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.netflix-file-info {
|
||||
|
||||
@@ -120,6 +120,16 @@ const TorrentPageNetflix = () => {
|
||||
return parseFloat((bytesPerSecond / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const handleDownload = (fileIndex) => {
|
||||
const downloadUrl = config.getDownloadUrl(torrentHash, fileIndex);
|
||||
const link = document.createElement('a');
|
||||
link.href = downloadUrl;
|
||||
link.download = files[fileIndex]?.name || 'download';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="netflix-page">
|
||||
@@ -245,6 +255,17 @@ const TorrentPageNetflix = () => {
|
||||
</button>
|
||||
)}
|
||||
|
||||
{mainVideoFile && (
|
||||
<button
|
||||
className="netflix-secondary-btn"
|
||||
onClick={() => handleDownload(mainVideoFile.index)}
|
||||
title="Download video"
|
||||
>
|
||||
<Download size={20} />
|
||||
Download
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button className="netflix-secondary-btn">
|
||||
<Plus size={20} />
|
||||
My List
|
||||
@@ -320,6 +341,16 @@ const TorrentPageNetflix = () => {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="netflix-episode-actions">
|
||||
<button
|
||||
className="netflix-episode-download"
|
||||
onClick={() => handleDownload(file.index)}
|
||||
title="Download episode"
|
||||
>
|
||||
<Download size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -333,7 +364,12 @@ const TorrentPageNetflix = () => {
|
||||
<div className="netflix-files">
|
||||
{otherFiles.map(file => (
|
||||
<div key={file.index} className="netflix-file">
|
||||
<div className="netflix-file-icon">
|
||||
<div
|
||||
className="netflix-file-icon"
|
||||
onClick={() => handleDownload(file.index)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
title="Download file"
|
||||
>
|
||||
<Download size={16} />
|
||||
</div>
|
||||
<div className="netflix-file-info">
|
||||
|
||||
@@ -298,7 +298,37 @@ const loadTorrentFromId = (torrentId) => {
|
||||
console.log(`🧲 Constructed magnet URI from hash: ${magnetUri}`);
|
||||
}
|
||||
|
||||
const torrent = client.add(magnetUri);
|
||||
let torrent;
|
||||
|
||||
try {
|
||||
torrent = client.add(magnetUri);
|
||||
} catch (addError) {
|
||||
// Handle duplicate torrent error from WebTorrent client
|
||||
if (addError.message && addError.message.includes('duplicate')) {
|
||||
console.log(`🔍 Duplicate torrent detected in WebTorrent client, finding existing`);
|
||||
|
||||
// Extract hash from the torrent ID
|
||||
let hash = torrentId;
|
||||
if (torrentId.startsWith('magnet:')) {
|
||||
const match = torrentId.match(/xt=urn:btih:([a-fA-F0-9]{40})/);
|
||||
if (match) hash = match[1];
|
||||
}
|
||||
|
||||
// Find the existing torrent in the client
|
||||
const existingTorrent = client.torrents.find(t =>
|
||||
t.infoHash.toLowerCase() === hash.toLowerCase()
|
||||
);
|
||||
|
||||
if (existingTorrent) {
|
||||
console.log(`✅ Found existing torrent in client: ${existingTorrent.name || existingTorrent.infoHash}`);
|
||||
resolve(existingTorrent);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
reject(addError);
|
||||
return;
|
||||
}
|
||||
|
||||
let resolved = false;
|
||||
|
||||
@@ -472,6 +502,7 @@ app.post('/api/torrents', async (req, res) => {
|
||||
try {
|
||||
const newTorrent = await loadTorrentFromId(torrentId);
|
||||
return res.json({
|
||||
success: true,
|
||||
infoHash: newTorrent.infoHash,
|
||||
name: newTorrent.name || 'Loading...',
|
||||
size: newTorrent.length || 0,
|
||||
@@ -499,13 +530,28 @@ app.post('/api/torrents', async (req, res) => {
|
||||
);
|
||||
|
||||
if (existingTorrent) {
|
||||
console.log(`✅ Found existing torrent: ${existingTorrent.name}`);
|
||||
return res.json({
|
||||
success: true,
|
||||
infoHash: existingTorrent.infoHash,
|
||||
name: existingTorrent.name || 'Loading...',
|
||||
size: existingTorrent.length || 0,
|
||||
status: 'existing'
|
||||
status: 'existing',
|
||||
message: 'Torrent already added'
|
||||
});
|
||||
}
|
||||
|
||||
// If we can't find the existing torrent, still return success
|
||||
// This handles edge cases where duplicate is detected but torrent isn't in our list yet
|
||||
console.log(`✅ Duplicate detected but not found in list, assuming success`);
|
||||
return res.json({
|
||||
success: true,
|
||||
infoHash: hash,
|
||||
name: 'Duplicate torrent',
|
||||
size: 0,
|
||||
status: 'duplicate',
|
||||
message: 'Torrent already exists in the system'
|
||||
});
|
||||
}
|
||||
|
||||
throw loadError;
|
||||
@@ -513,6 +559,7 @@ app.post('/api/torrents', async (req, res) => {
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
infoHash: torrent.infoHash,
|
||||
name: torrent.name || 'Loading...',
|
||||
size: torrent.length || 0,
|
||||
@@ -545,7 +592,36 @@ app.post('/api/torrents/upload', upload.single('torrentFile'), async (req, res)
|
||||
|
||||
// Load the torrent using the buffer
|
||||
const torrent = await new Promise((resolve, reject) => {
|
||||
const loadedTorrent = client.add(torrentBuffer);
|
||||
let loadedTorrent;
|
||||
|
||||
try {
|
||||
loadedTorrent = client.add(torrentBuffer);
|
||||
} catch (addError) {
|
||||
// Handle duplicate torrent in file upload
|
||||
if (addError.message && addError.message.includes('duplicate')) {
|
||||
console.log(`🔍 Duplicate torrent file detected, finding existing`);
|
||||
|
||||
// Parse the torrent buffer to get the info hash
|
||||
const parseTorrent = require('parse-torrent');
|
||||
try {
|
||||
const parsed = parseTorrent(torrentBuffer);
|
||||
const existingTorrent = client.torrents.find(t =>
|
||||
t.infoHash.toLowerCase() === parsed.infoHash.toLowerCase()
|
||||
);
|
||||
|
||||
if (existingTorrent) {
|
||||
console.log(`✅ Found existing torrent from file: ${existingTorrent.name || existingTorrent.infoHash}`);
|
||||
resolve(existingTorrent);
|
||||
return;
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.error(`❌ Error parsing torrent for duplicate check:`, parseError.message);
|
||||
}
|
||||
}
|
||||
|
||||
reject(addError);
|
||||
return;
|
||||
}
|
||||
|
||||
let resolved = false;
|
||||
|
||||
@@ -572,6 +648,29 @@ app.post('/api/torrents/upload', upload.single('torrentFile'), async (req, res)
|
||||
if (resolved) return;
|
||||
resolved = true;
|
||||
console.error(`❌ Error loading uploaded torrent:`, err.message);
|
||||
|
||||
// Handle duplicate error in event handler too
|
||||
if (err.message && err.message.includes('duplicate')) {
|
||||
console.log(`🔍 Duplicate torrent detected in error handler`);
|
||||
|
||||
// Try to find existing torrent and return it
|
||||
const parseTorrent = require('parse-torrent');
|
||||
try {
|
||||
const parsed = parseTorrent(torrentBuffer);
|
||||
const existingTorrent = client.torrents.find(t =>
|
||||
t.infoHash.toLowerCase() === parsed.infoHash.toLowerCase()
|
||||
);
|
||||
|
||||
if (existingTorrent) {
|
||||
console.log(`✅ Found existing torrent in error handler: ${existingTorrent.name}`);
|
||||
resolve(existingTorrent);
|
||||
return;
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.error(`❌ Error parsing in error handler:`, parseError.message);
|
||||
}
|
||||
}
|
||||
|
||||
reject(err);
|
||||
});
|
||||
|
||||
@@ -588,6 +687,7 @@ app.post('/api/torrents/upload', upload.single('torrentFile'), async (req, res)
|
||||
fs.unlinkSync(torrentPath);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
infoHash: torrent.infoHash,
|
||||
name: torrent.name,
|
||||
size: torrent.length,
|
||||
@@ -836,6 +936,69 @@ app.get('/api/torrents/:identifier/files/:fileIdx/stream', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// UNIVERSAL DOWNLOAD - Download files with proper headers
|
||||
app.get('/api/torrents/:identifier/files/:fileIdx/download', async (req, res) => {
|
||||
const { identifier, fileIdx } = req.params;
|
||||
console.log(`📥 UNIVERSAL DOWNLOAD: ${identifier}/${fileIdx}`);
|
||||
|
||||
try {
|
||||
const torrent = await universalTorrentResolver(identifier);
|
||||
|
||||
if (!torrent) {
|
||||
return res.status(404).json({ error: 'Torrent not found for download' });
|
||||
}
|
||||
|
||||
const file = torrent.files[fileIdx];
|
||||
if (!file) {
|
||||
return res.status(404).json({ error: 'File not found' });
|
||||
}
|
||||
|
||||
// Ensure torrent is active and file is selected
|
||||
torrent.resume();
|
||||
file.select();
|
||||
|
||||
console.log(`📥 Downloading: ${file.name} (${(file.length / 1024 / 1024).toFixed(1)} MB)`);
|
||||
|
||||
// Set download headers
|
||||
const filename = file.name.replace(/[^a-zA-Z0-9.-]/g, '_');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
||||
res.setHeader('Content-Type', 'application/octet-stream');
|
||||
res.setHeader('Content-Length', file.length);
|
||||
res.setHeader('Accept-Ranges', 'bytes');
|
||||
|
||||
// Handle range requests for resume capability
|
||||
const range = req.headers.range;
|
||||
if (range) {
|
||||
const parts = range.replace(/bytes=/, "").split("-");
|
||||
const start = parseInt(parts[0], 10);
|
||||
const end = parts[1] ? parseInt(parts[1], 10) : file.length - 1;
|
||||
const chunkSize = (end - start) + 1;
|
||||
|
||||
res.writeHead(206, {
|
||||
'Content-Range': `bytes ${start}-${end}/${file.length}`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': chunkSize,
|
||||
'Content-Disposition': `attachment; filename="${filename}"`,
|
||||
'Content-Type': 'application/octet-stream'
|
||||
});
|
||||
|
||||
const stream = file.createReadStream({ start, end });
|
||||
stream.pipe(res);
|
||||
} else {
|
||||
res.writeHead(200, {
|
||||
'Content-Length': file.length,
|
||||
'Content-Disposition': `attachment; filename="${filename}"`,
|
||||
'Content-Type': 'application/octet-stream'
|
||||
});
|
||||
file.createReadStream().pipe(res);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Universal download failed:`, error.message);
|
||||
res.status(500).json({ error: 'Download failed: ' + error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// UNIVERSAL REMOVE - Cleans everything
|
||||
app.delete('/api/torrents/:identifier', async (req, res) => {
|
||||
const identifier = req.params.identifier;
|
||||
|
||||
Reference in New Issue
Block a user