diff --git a/server/server.js b/server/server.js index 4c1ee58..cfb3084 100644 --- a/server/server.js +++ b/server/server.js @@ -8,7 +8,7 @@ import mime from "mime-types"; import { fileURLToPath } from "url"; import { exec, spawn } from "child_process"; import crypto from "crypto"; // 🔒 basit token üretimi için -import { getSystemDiskInfo } from "./utils/diskSpace.js"; +import { getDiskSpace, getDownloadsSize } from "./utils/diskSpace.js"; import { createAuth } from "./modules/auth.js"; import { buildHealthReport, healthRouter } from "./modules/health.js"; import { restoreTorrentsFromDisk } from "./modules/state.js"; @@ -735,7 +735,7 @@ function startYoutubeDownload(url) { youtubeJobs.set(job.id, job); launchYoutubeJob(job); console.log(`▶️ YouTube indirmesi başlatıldı: ${job.url}`); - broadcastSnapshot(); + scheduleSnapshotBroadcast(); return job; } @@ -888,7 +888,7 @@ function launchYoutubeJob(job) { binary, args }); - broadcastSnapshot(); + scheduleSnapshotBroadcast(); }); } @@ -922,7 +922,7 @@ function startYoutubeStage(job, fileName) { }; job.currentStage = stage; job.stages.push(stage); - broadcastSnapshot(); + scheduleSnapshotBroadcast(); } function updateYoutubeProgress(job, match) { @@ -959,7 +959,7 @@ function updateYoutubeProgress(job, match) { if (Number.isFinite(speedValue) && speedUnit) { job.downloadSpeed = bytesFromHuman(speedValue, speedUnit); } - broadcastSnapshot(); + scheduleSnapshotBroadcast(); } async function finalizeYoutubeJob(job, exitCode) { @@ -979,7 +979,7 @@ async function finalizeYoutubeJob(job, exitCode) { args: job.debug?.args, lastLines: tail }); - broadcastSnapshot(); + scheduleSnapshotBroadcast(); return; } if (exitCode !== 0 && fallbackMedia) { @@ -1007,7 +1007,7 @@ async function finalizeYoutubeJob(job, exitCode) { savePath: job.savePath, lastLines: job.debug?.logs?.slice(-8) || [] }); - broadcastSnapshot(); + scheduleSnapshotBroadcast(); return; } @@ -1065,13 +1065,13 @@ async function finalizeYoutubeJob(job, exitCode) { primaryMediaInfo: mediaInfo }); broadcastFileUpdate(job.folderId); - broadcastSnapshot(); + scheduleSnapshotBroadcast(); broadcastDiskSpace(); console.log(`✅ YouTube indirmesi tamamlandı: ${job.title}`); } catch (err) { job.state = "error"; job.error = err?.message || "YouTube indirimi tamamlanamadı"; - broadcastSnapshot(); + scheduleSnapshotBroadcast(); } } @@ -1248,7 +1248,7 @@ function removeYoutubeJob(jobId, { removeFiles = true } = {}) { console.warn("YT cache silinemedi:", err.message); } } - broadcastSnapshot(); + scheduleSnapshotBroadcast(); if (filesRemoved) { broadcastFileUpdate(job.folderId); broadcastDiskSpace(); @@ -4104,25 +4104,108 @@ function broadcastFileUpdate(rootFolder) { wss.clients.forEach((c) => c.readyState === 1 && c.send(data)); } -function broadcastDiskSpace() { - if (!wss) return; - getSystemDiskInfo(DOWNLOAD_DIR).then(diskInfo => { - const data = JSON.stringify({ - type: "diskSpace", - data: diskInfo - }); - wss.clients.forEach((c) => c.readyState === 1 && c.send(data)); - }).catch(err => { - console.error("❌ Disk space broadcast error:", err.message); - }); +const DISK_SPACE_CACHE_TTL_MS = 30000; +const DOWNLOADS_SIZE_CACHE_TTL_MS = 5 * 60 * 1000; +let diskSpaceCache = { value: null, fetchedAt: 0 }; +let downloadsSizeCache = { value: null, fetchedAt: 0 }; +let diskInfoInFlight = null; +let lastDiskSpacePayload = null; + +async function getCachedDiskInfo({ force = false } = {}) { + const now = Date.now(); + const diskFresh = + !force && diskSpaceCache.value && now - diskSpaceCache.fetchedAt < DISK_SPACE_CACHE_TTL_MS; + const downloadsFresh = + !force && + downloadsSizeCache.value && + now - downloadsSizeCache.fetchedAt < DOWNLOADS_SIZE_CACHE_TTL_MS; + + if (diskFresh && downloadsFresh) { + return { + ...diskSpaceCache.value, + downloads: downloadsSizeCache.value, + timestamp: new Date().toISOString() + }; + } + + if (diskInfoInFlight) return diskInfoInFlight; + + diskInfoInFlight = (async () => { + const diskSpace = diskFresh + ? diskSpaceCache.value + : await getDiskSpace(DOWNLOAD_DIR); + if (!diskFresh) { + diskSpaceCache = { value: diskSpace, fetchedAt: now }; + } + + const downloadsSize = downloadsFresh + ? downloadsSizeCache.value + : await getDownloadsSize(DOWNLOAD_DIR); + if (!downloadsFresh) { + downloadsSizeCache = { value: downloadsSize, fetchedAt: now }; + } + + return { + ...diskSpace, + downloads: downloadsSize, + timestamp: new Date().toISOString() + }; + })(); + + try { + return await diskInfoInFlight; + } finally { + diskInfoInFlight = null; + } } +function broadcastDiskSpace() { + if (!wss || !hasActiveWsClients()) return; + getCachedDiskInfo() + .then((diskInfo) => { + const data = JSON.stringify({ + type: "diskSpace", + data: diskInfo + }); + if (data === lastDiskSpacePayload) return; + lastDiskSpacePayload = data; + wss.clients.forEach((c) => c.readyState === 1 && c.send(data)); + }) + .catch((err) => { + console.error("❌ Disk space broadcast error:", err.message); + }); +} + +let lastSnapshotPayload = null; +const SNAPSHOT_DEBOUNCE_MS = 1000; +let snapshotTimer = null; +let lastSnapshotAt = 0; + function broadcastSnapshot() { - if (!wss) return; + if (!wss || !hasActiveWsClients()) return; const data = JSON.stringify({ type: "progress", torrents: snapshot() }); + if (data === lastSnapshotPayload) return; + lastSnapshotPayload = data; wss.clients.forEach((c) => c.readyState === 1 && c.send(data)); } +function scheduleSnapshotBroadcast() { + if (!wss || !hasActiveWsClients()) return; + const now = Date.now(); + const remaining = SNAPSHOT_DEBOUNCE_MS - (now - lastSnapshotAt); + if (remaining <= 0) { + lastSnapshotAt = now; + broadcastSnapshot(); + return; + } + if (snapshotTimer) return; + snapshotTimer = setTimeout(() => { + snapshotTimer = null; + lastSnapshotAt = Date.now(); + broadcastSnapshot(); + }, remaining); +} + let mediaRescanTask = null; let pendingMediaRescan = { movies: false, tv: false }; let lastMediaRescanReason = "manual"; @@ -4252,22 +4335,73 @@ function inferMediaFlagsFromTrashEntry(entry) { return { movies: true, tv: false }; } +const THUMBNAIL_CHECK_INTERVAL_MS = 15000; + +function hasActiveWsClients() { + if (!wss) return false; + let hasActive = false; + wss.clients.forEach((c) => { + if (c.readyState === 1) hasActive = true; + }); + return hasActive; +} + +function ensureTorrentSnapshotCache(entry) { + if (!entry) return entry; + const { torrent, savePath } = entry; + if (!entry.rootFolder && savePath) entry.rootFolder = path.basename(savePath); + + if (!entry.filesSnapshot && Array.isArray(torrent?.files)) { + entry.filesSnapshot = torrent.files.map((f, i) => ({ + index: i, + name: f.name, + length: f.length + })); + } + + if ( + entry.bestVideoIndex === undefined || + entry.bestVideoIndex === null + ) { + entry.bestVideoIndex = pickBestVideoFile(torrent); + } + + const bestVideo = + torrent?.files?.[entry.bestVideoIndex] || torrent?.files?.[0]; + if (!bestVideo || !savePath || !entry.rootFolder) return entry; + + const now = Date.now(); + const shouldCheckThumbnail = + !entry.thumbnail || + !entry.thumbnailCheckedAt || + now - entry.thumbnailCheckedAt > THUMBNAIL_CHECK_INTERVAL_MS; + + if (shouldCheckThumbnail) { + const relPath = path.join(entry.rootFolder, bestVideo.path); + const { relThumb, absThumb } = getVideoThumbnailPaths(relPath); + entry.thumbnailCheckedAt = now; + entry.thumbnailRelPath = relThumb; + + if (fs.existsSync(absThumb)) { + entry.thumbnail = thumbnailUrl(relThumb); + entry.thumbnailQueued = false; + } else if (torrent?.progress === 1 || torrent?.done) { + if (!entry.thumbnailQueued) { + queueVideoThumbnail(path.join(savePath, bestVideo.path), relPath); + entry.thumbnailQueued = true; + } + } + } + + return entry; +} + // --- Snapshot (thumbnail dahil, tracker + tarih eklendi) --- function snapshot() { - const torrentEntries = Array.from(torrents.values()).map( - ({ torrent, selectedIndex, savePath, added, paused }) => { - const rootFolder = path.basename(savePath); - const bestVideoIndex = pickBestVideoFile(torrent); - const bestVideo = torrent.files[bestVideoIndex]; - let thumbnail = null; - - if (bestVideo) { - const relPath = path.join(rootFolder, bestVideo.path); - const { relThumb, absThumb } = getVideoThumbnailPaths(relPath); - if (fs.existsSync(absThumb)) thumbnail = thumbnailUrl(relThumb); - else if (torrent.progress === 1) - queueVideoThumbnail(path.join(savePath, bestVideo.path), relPath); - } + const torrentEntries = Array.from(torrents.values()).map((entry) => { + const { torrent, selectedIndex, savePath, added, paused } = entry; + ensureTorrentSnapshotCache(entry); + const rootFolder = entry?.rootFolder || path.basename(savePath); return { infoHash: torrent.infoHash, @@ -4282,13 +4416,9 @@ function snapshot() { added, savePath, // 🆕 BURASI! paused: paused || false, // Pause durumunu ekle - files: torrent.files.map((f, i) => ({ - index: i, - name: f.name, - length: f.length - })), + files: entry?.filesSnapshot || [], selectedIndex, - thumbnail + thumbnail: entry?.thumbnail || null }; } ); @@ -4308,9 +4438,24 @@ function wireTorrent(torrent, { savePath, added, respond, restored = false }) { selectedIndex: 0, savePath, added, - paused: false + paused: false, + filesSnapshot: null, + thumbnail: null, + thumbnailCheckedAt: 0, + thumbnailQueued: false, + bestVideoIndex: null, + rootFolder: savePath ? path.basename(savePath) : null }); + const scheduleTorrentSnapshot = () => scheduleSnapshotBroadcast(); + torrent.on("download", scheduleTorrentSnapshot); + torrent.on("upload", scheduleTorrentSnapshot); + torrent.on("wire", scheduleTorrentSnapshot); + torrent.on("noPeers", scheduleTorrentSnapshot); + torrent.on("metadata", scheduleTorrentSnapshot); + torrent.on("warning", scheduleTorrentSnapshot); + torrent.on("error", scheduleTorrentSnapshot); + torrent.on("ready", () => { onTorrentReady({ torrent, savePath, added, respond, restored }); }); @@ -4322,12 +4467,19 @@ function wireTorrent(torrent, { savePath, added, respond, restored = false }) { function onTorrentReady({ torrent, savePath, added, respond }) { const selectedIndex = pickBestVideoFile(torrent); + const existing = torrents.get(torrent.infoHash) || {}; torrents.set(torrent.infoHash, { torrent, selectedIndex, savePath, added, - paused: false + paused: false, + filesSnapshot: existing.filesSnapshot || null, + thumbnail: existing.thumbnail || null, + thumbnailCheckedAt: existing.thumbnailCheckedAt || 0, + thumbnailQueued: existing.thumbnailQueued || false, + bestVideoIndex: selectedIndex, + rootFolder: savePath ? path.basename(savePath) : existing.rootFolder || null }); const rootFolder = path.basename(savePath); upsertInfoFile(savePath, { @@ -4356,7 +4508,7 @@ function onTorrentReady({ torrent, savePath, added, respond }) { }; if (typeof respond === "function") respond(payload); - broadcastSnapshot(); + scheduleSnapshotBroadcast(); } async function onTorrentDone({ torrent }) { @@ -4594,7 +4746,7 @@ async function onTorrentDone({ torrent }) { infoUpdate.files[bestVideoPath].type || rootType; } - broadcastSnapshot(); + scheduleSnapshotBroadcast(); } // Auth router ve middleware createAuth ile yüklendi @@ -4741,7 +4893,7 @@ app.delete("/api/torrents/:hash", requireAuth, (req, res) => { `ℹ️ ${req.params.hash} torrent'i tamamlandığı için yalnızca Transfers listesinden kaldırıldı; dosyalar tutuldu.` ); } - broadcastSnapshot(); + scheduleSnapshotBroadcast(); res.json({ ok: true, filesRemoved: !isComplete @@ -4877,7 +5029,7 @@ app.post("/api/torrents/toggle-all", requireAuth, (req, res) => { global.pausedTorrents = pausedTorrents; - broadcastSnapshot(); + scheduleSnapshotBroadcast(); res.json({ ok: true, action, @@ -4935,7 +5087,7 @@ app.post("/api/torrents/:hash/toggle", requireAuth, (req, res) => { } global.pausedTorrents = pausedTorrents; - broadcastSnapshot(); + scheduleSnapshotBroadcast(); res.json({ ok: true, action, @@ -5106,16 +5258,16 @@ app.delete("/api/file", requireAuth, (req, res) => { entry?.torrent?.destroy(() => { torrents.delete(matchedInfoHash); console.log(`🧹 Torrent kaydı da temizlendi: ${matchedInfoHash}`); - broadcastSnapshot(); + scheduleSnapshotBroadcast(); // Torrent silindiğinde disk space bilgisini güncelle broadcastDiskSpace(); }); } else { - broadcastSnapshot(); + scheduleSnapshotBroadcast(); } } else { - broadcastSnapshot(); + scheduleSnapshotBroadcast(); } if ( @@ -6958,13 +7110,6 @@ wss.on("connection", (ws) => { broadcastDiskSpace(); }); -// --- ⏱️ Her 2 saniyede bir aktif torrent durumu yayınla --- -setInterval(() => { - if (torrents.size > 0) { - broadcastSnapshot(); - } -}, 2000); - // --- ⏱️ Her 30 saniyede bir disk space bilgisi yayınla --- setInterval(() => { broadcastDiskSpace(); @@ -6978,7 +7123,9 @@ app.get("/api/disk-space", requireAuth, async (req, res) => { fs.mkdirSync(DOWNLOAD_DIR, { recursive: true }); } - const diskInfo = await getSystemDiskInfo(DOWNLOAD_DIR); + const diskInfo = await getCachedDiskInfo({ + force: req.query?.fresh === "1" + }); res.json(diskInfo); } catch (err) { console.error("❌ Disk space error:", err.message); @@ -7568,7 +7715,7 @@ app.patch("/api/folder", requireAuth, (req, res) => { console.log(`📁 Kök klasör yeniden adlandırıldı: ${rootFolder} -> ${newRootFolder}`); broadcastFileUpdate(rootFolder); broadcastFileUpdate(newRootFolder); - broadcastSnapshot(); + scheduleSnapshotBroadcast(); return res.json({ success: true, message: "Klasör yeniden adlandırıldı",