diff --git a/client/src/routes/Transfers.svelte b/client/src/routes/Transfers.svelte index 8f831e7..30a989c 100644 --- a/client/src/routes/Transfers.svelte +++ b/client/src/routes/Transfers.svelte @@ -4,6 +4,7 @@ let torrents = []; let ws; + let isAllPaused = false; // Modal / player state let showModal = false; @@ -26,7 +27,11 @@ ws = new WebSocket(url); ws.onmessage = (e) => { const d = JSON.parse(e.data); - if (d.type === "progress") torrents = d.torrents || []; + if (d.type === "progress") { + torrents = d.torrents || []; + // Tüm torrentlerin pause durumunu kontrol et + updateAllPausedState(); + } }; } @@ -34,6 +39,7 @@ const r = await apiFetch("/api/torrents"); // ✅ fetch yerine apiFetch if (!r.ok) return; torrents = await r.json(); + updateAllPausedState(); } async function upload(e) { @@ -67,6 +73,79 @@ await list(); } + async function removeAllTorrents() { + if (!confirm("Tüm torrent listesini silmek istediğinizden emin misiniz?")) return; + + // Tüm torrentleri API üzerinden sil + for (const torrent of torrents) { + await apiFetch(`/api/torrents/${torrent.infoHash}`, { method: "DELETE" }); + } + + // Listeyi güncelle + await list(); + } + + async function toggleAllTorrents() { + const action = isAllPaused ? "resume" : "pause"; + + try { + const r = await apiFetch("/api/torrents/toggle-all", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action }) + }); + + if (!r.ok) return; + + const result = await r.json(); + console.log(`${action} işlemi: ${result.updatedCount}/${result.totalCount} torrent güncellendi`); + + // Durumu güncelle + isAllPaused = !isAllPaused; + + // Listeyi yenile + await list(); + } catch (err) { + console.error("Toggle all torrents error:", err); + } + } + + function updateAllPausedState() { + if (torrents.length === 0) { + isAllPaused = false; + return; + } + + // Eğer tüm torrentler paused ise, global durumu paused yap + const allPaused = torrents.every(t => t.paused === true); + isAllPaused = allPaused; + } + + async function toggleSingleTorrent(hash) { + const torrent = torrents.find(t => t.infoHash === hash); + if (!torrent) return; + + const action = torrent.paused ? "resume" : "pause"; + + try { + const r = await apiFetch(`/api/torrents/${hash}/toggle`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action }) + }); + + if (!r.ok) return; + + const result = await r.json(); + console.log(`Single torrent ${action}:`, result); + + // Listeyi güncelle + await list(); + } catch (err) { + console.error("Toggle single torrent error:", err); + } + } + function streamURL(hash, index = 0) { const token = localStorage.getItem("token"); return `${API}/stream/${hash}?index=${index}&token=${token}`; @@ -303,19 +382,41 @@

Transfers

-
- - +
+
+ + +
+
+ + +
{#if torrents.length === 0} @@ -358,11 +459,24 @@
{t.name}
- +
+ + +
@@ -583,6 +697,22 @@ word-break: break-word; } + .toggle-btn { + background: transparent; + border: none; + font-size: 18px; + cursor: pointer; + transition: transform 0.15s; + color: #4caf50; + padding: 2px; + border-radius: 4px; + } + + .toggle-btn:hover { + transform: scale(1.2); + background: rgba(76, 175, 80, 0.1); + } + .remove-btn { background: transparent; border: none; @@ -800,4 +930,79 @@ font-size: 10px; } } + + /* --- Toggle All Torrents Button --- */ + .btn-toggle-all { + background: transparent; + border: 1px solid #ddd; + color: #666; + padding: 10px 14px; + border-radius: 6px; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + height: 36px; + width: 36px; + transition: all 0.2s ease; + font-size: 14px; + } + + .btn-toggle-all:hover { + background: var(--yellow); + border-color: var(--yellow-dark); + color: #222; + transform: scale(1.05); + } + + .btn-toggle-all:active { + transform: scale(0.95); + } + + /* --- Remove All Torrent List Button --- */ + .btn-remove-all { + background: transparent; + border: 1px solid #ddd; + color: #666; + padding: 10px 14px; + border-radius: 6px; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + height: 36px; + width: 36px; + transition: all 0.2s ease; + font-size: 14px; + } + + .btn-remove-all:hover { + background: #ff4444; + border-color: #cc0000; + color: white; + transform: scale(1.05); + } + + .btn-remove-all:active { + transform: scale(0.95); + } + + /* Responsive adjustments for toggle and remove buttons */ + @media (max-width: 768px) { + .btn-toggle-all, + .btn-remove-all { + height: 36px; + width: 36px; + padding: 8px; + } + } + + @media (max-width: 480px) { + .btn-toggle-all, + .btn-remove-all { + height: 34px; + width: 34px; + font-size: 12px; + } + } diff --git a/server/server.js b/server/server.js index b92dff4..adb57d2 100644 --- a/server/server.js +++ b/server/server.js @@ -2091,7 +2091,7 @@ function broadcastSnapshot() { // --- Snapshot (thumbnail dahil, tracker + tarih eklendi) --- function snapshot() { return Array.from(torrents.values()).map( - ({ torrent, selectedIndex, savePath, added }) => { + ({ torrent, selectedIndex, savePath, added, paused }) => { const rootFolder = path.basename(savePath); const bestVideoIndex = pickBestVideoFile(torrent); const bestVideo = torrent.files[bestVideoIndex]; @@ -2110,12 +2110,13 @@ function snapshot() { name: torrent.name, progress: torrent.progress, downloaded: torrent.downloaded, - downloadSpeed: torrent.downloadSpeed, - uploadSpeed: torrent.uploadSpeed, - numPeers: torrent.numPeers, + downloadSpeed: paused ? 0 : torrent.downloadSpeed, // Pause durumunda hız 0 + uploadSpeed: paused ? 0 : torrent.uploadSpeed, // Pause durumunda hız 0 + numPeers: paused ? 0 : torrent.numPeers, // Pause durumunda peer sayısı 0 tracker: torrent.announce?.[0] || null, added, savePath, // 🆕 BURASI! + paused: paused || false, // Pause durumunu ekle files: torrent.files.map((f, i) => ({ index: i, name: f.name, @@ -2171,7 +2172,8 @@ app.post("/api/transfer", requireAuth, upload.single("torrent"), (req, res) => { torrent, selectedIndex: 0, savePath, - added + added, + paused: false }); // --- Metadata geldiğinde --- @@ -2181,7 +2183,8 @@ app.post("/api/transfer", requireAuth, upload.single("torrent"), (req, res) => { torrent, selectedIndex, savePath, - added + added, + paused: false }); const rootFolder = path.basename(savePath); upsertInfoFile(savePath, { @@ -2422,6 +2425,200 @@ app.delete("/api/torrents/:hash", requireAuth, (req, res) => { }); }); +function getPieceCount(torrent) { + if (!torrent) return 0; + if (Array.isArray(torrent.pieces)) return torrent.pieces.length; + const pieces = torrent.pieces; + if (pieces && typeof pieces.length === "number") return pieces.length; + return 0; +} + +function pauseTorrentEntry(entry) { + const torrent = entry?.torrent; + if (!torrent || torrent._destroyed || entry.paused) return false; + + entry.previousSelection = entry.selectedIndex; + + const pieceCount = getPieceCount(torrent); + if (pieceCount > 0 && typeof torrent.deselect === "function") { + try { + torrent.deselect(0, pieceCount - 1, 0); + } catch (err) { + console.warn("Torrent deselect failed during pause:", err.message); + } + } + + if (Array.isArray(torrent.files)) { + for (const file of torrent.files) { + if (file && typeof file.deselect === "function") { + try { + file.deselect(); + } catch (err) { + console.warn( + `File deselect failed during pause (${torrent.infoHash}):`, + err.message + ); + } + } + } + } + + if (typeof torrent.pause === "function") { + try { + torrent.pause(); + } catch (err) { + console.warn("Torrent pause method failed:", err.message); + } + } + + entry.paused = true; + entry.pausedAt = Date.now(); + return true; +} + +function resumeTorrentEntry(entry) { + const torrent = entry?.torrent; + if (!torrent || torrent._destroyed || !entry.paused) return false; + + const pieceCount = getPieceCount(torrent); + if (pieceCount > 0 && typeof torrent.select === "function") { + try { + torrent.select(0, pieceCount - 1, 0); + } catch (err) { + console.warn("Torrent select failed during resume:", err.message); + } + } + + if (Array.isArray(torrent.files)) { + const preferredIndex = + entry.previousSelection !== undefined + ? entry.previousSelection + : entry.selectedIndex ?? 0; + const targetFile = + torrent.files[preferredIndex] || torrent.files[0] || null; + if (targetFile && typeof targetFile.select === "function") { + try { + targetFile.select(); + } catch (err) { + console.warn( + `File select failed during resume (${torrent.infoHash}):`, + err.message + ); + } + } + } + + if (typeof torrent.resume === "function") { + try { + torrent.resume(); + } catch (err) { + console.warn("Torrent resume method failed:", err.message); + } + } + + entry.paused = false; + delete entry.pausedAt; + return true; +} + +// --- Tüm torrentleri durdur/devam ettir --- +app.post("/api/torrents/toggle-all", requireAuth, (req, res) => { + try { + const { action } = req.body; // 'pause' veya 'resume' + + if (!action || (action !== 'pause' && action !== 'resume')) { + return res.status(400).json({ error: "action 'pause' veya 'resume' olmalı" }); + } + + let updatedCount = 0; + const pausedTorrents = new Set(); + + for (const [infoHash, entry] of torrents.entries()) { + if (!entry?.torrent || entry.torrent._destroyed) continue; + + try { + const changed = + action === "pause" + ? pauseTorrentEntry(entry) + : resumeTorrentEntry(entry); + if (changed) updatedCount++; + if (entry.paused) pausedTorrents.add(infoHash); + } catch (err) { + console.warn( + `⚠️ Torrent ${infoHash} ${action} işleminde hata:`, + err.message + ); + } + } + + global.pausedTorrents = pausedTorrents; + + broadcastSnapshot(); + res.json({ + ok: true, + action, + updatedCount, + totalCount: torrents.size + }); + } catch (err) { + console.error("❌ Toggle all torrents error:", err.message); + res.status(500).json({ error: err.message }); + } +}); + +// --- Tek torrent'i durdur/devam ettir --- +app.post("/api/torrents/:hash/toggle", requireAuth, (req, res) => { + try { + const { action } = req.body; // 'pause' veya 'resume' + const infoHash = req.params.hash; + + if (!action || (action !== 'pause' && action !== 'resume')) { + return res.status(400).json({ error: "action 'pause' veya 'resume' olmalı" }); + } + + const entry = torrents.get(infoHash); + if (!entry || !entry.torrent || entry.torrent._destroyed) { + return res.status(404).json({ error: "torrent bulunamadı" }); + } + + const changed = + action === "pause" + ? pauseTorrentEntry(entry) + : resumeTorrentEntry(entry); + + if (!changed) { + const message = + action === "pause" + ? "Torrent zaten durdurulmuş" + : "Torrent zaten devam ediyor"; + return res.json({ + ok: true, + action, + infoHash, + paused: entry.paused, + message + }); + } + + const pausedTorrents = new Set(); + for (const [hash, item] of torrents.entries()) { + if (item?.paused) pausedTorrents.add(hash); + } + global.pausedTorrents = pausedTorrents; + + broadcastSnapshot(); + res.json({ + ok: true, + action, + infoHash, + paused: entry.paused + }); + } catch (err) { + console.error("❌ Toggle single torrent error:", err.message); + res.status(500).json({ error: err.message }); + } +}); + // --- GENEL MEDYA SUNUMU (🆕 resimler + videolar) --- app.get("/media/:path(*)", requireAuth, (req, res) => { const relPath = req.params.path;