This commit is contained in:
2025-12-27 21:21:28 +03:00
3 changed files with 1650 additions and 195 deletions

View File

@@ -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";
@@ -760,7 +760,7 @@ function startYoutubeDownload(url) {
youtubeJobs.set(job.id, job);
launchYoutubeJob(job);
console.log(`▶️ YouTube indirmesi başlatıldı: ${job.url}`);
broadcastSnapshot();
scheduleSnapshotBroadcast();
return job;
}
@@ -1033,7 +1033,7 @@ function launchPornhubJob(job) {
binary,
args
});
broadcastSnapshot();
scheduleSnapshotBroadcast();
});
}
@@ -1067,7 +1067,7 @@ function startYoutubeStage(job, fileName) {
};
job.currentStage = stage;
job.stages.push(stage);
broadcastSnapshot();
scheduleSnapshotBroadcast();
}
function updateYoutubeProgress(job, match) {
@@ -1104,7 +1104,7 @@ function updateYoutubeProgress(job, match) {
if (Number.isFinite(speedValue) && speedUnit) {
job.downloadSpeed = bytesFromHuman(speedValue, speedUnit);
}
broadcastSnapshot();
scheduleSnapshotBroadcast();
}
async function finalizeYoutubeJob(job, exitCode) {
@@ -1124,7 +1124,7 @@ async function finalizeYoutubeJob(job, exitCode) {
args: job.debug?.args,
lastLines: tailLines
});
broadcastSnapshot();
scheduleSnapshotBroadcast();
return;
}
if (exitCode !== 0 && fallbackMedia) {
@@ -1153,7 +1153,7 @@ async function finalizeYoutubeJob(job, exitCode) {
savePath: job.savePath,
lastLines: job.debug?.logs?.slice(-8) || []
});
broadcastSnapshot();
scheduleSnapshotBroadcast();
return;
}
@@ -1262,13 +1262,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();
}
}
@@ -1504,7 +1504,7 @@ function removeYoutubeJob(jobId, { removeFiles = true } = {}) {
console.warn("YT cache silinemedi:", err.message);
}
}
broadcastSnapshot();
scheduleSnapshotBroadcast();
if (filesRemoved) {
broadcastFileUpdate(job.folderId);
broadcastDiskSpace();
@@ -4372,25 +4372,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";
@@ -4520,22 +4603,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,
@@ -4550,13 +4684,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
};
}
);
@@ -4576,9 +4706,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 });
});
@@ -4590,12 +4735,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, {
@@ -4624,7 +4776,7 @@ function onTorrentReady({ torrent, savePath, added, respond }) {
};
if (typeof respond === "function") respond(payload);
broadcastSnapshot();
scheduleSnapshotBroadcast();
}
async function onTorrentDone({ torrent }) {
@@ -4862,7 +5014,7 @@ async function onTorrentDone({ torrent }) {
infoUpdate.files[bestVideoPath].type || rootType;
}
broadcastSnapshot();
scheduleSnapshotBroadcast();
}
// Auth router ve middleware createAuth ile yüklendi
@@ -5016,7 +5168,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
@@ -5152,7 +5304,7 @@ app.post("/api/torrents/toggle-all", requireAuth, (req, res) => {
global.pausedTorrents = pausedTorrents;
broadcastSnapshot();
scheduleSnapshotBroadcast();
res.json({
ok: true,
action,
@@ -5210,7 +5362,7 @@ app.post("/api/torrents/:hash/toggle", requireAuth, (req, res) => {
}
global.pausedTorrents = pausedTorrents;
broadcastSnapshot();
scheduleSnapshotBroadcast();
res.json({
ok: true,
action,
@@ -5381,16 +5533,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 (
@@ -6947,7 +7099,8 @@ function collectMusicEntries() {
? `https://www.youtube.com/watch?v=${fileMeta.youtube.videoId}`
: null,
thumbnail,
categories: metadata?.categories || fileMeta?.categories || null
categories: metadata?.categories || fileMeta?.categories || null,
mediaInfo: fileMeta?.mediaInfo || null
});
}
entries.sort((a, b) => (b.added || 0) - (a.added || 0));
@@ -7335,13 +7488,6 @@ wss.on("connection", (ws) => {
}
});
// --- ⏱️ 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();
@@ -7355,7 +7501,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);
@@ -7945,7 +8093,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ı",