Video/resim thumbnail cache’ini kalıcı klasöre taşı ve Docker volume ile paylaştır.
This commit is contained in:
@@ -8,6 +8,7 @@ services:
|
||||
- "3001:3001"
|
||||
volumes:
|
||||
- ./downloads:/app/server/downloads
|
||||
- ./server/cache:/app/server/cache
|
||||
restart: unless-stopped
|
||||
# Login credentials for basic auth
|
||||
environment:
|
||||
|
||||
1
server/cache/.gitkeep
vendored
Normal file
1
server/cache/.gitkeep
vendored
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
361
server/server.js
361
server/server.js
@@ -17,6 +17,7 @@ const app = express();
|
||||
const upload = multer({ dest: path.join(__dirname, "uploads") });
|
||||
const client = new WebTorrent();
|
||||
const torrents = new Map();
|
||||
let wss;
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
// --- İndirilen dosyalar için klasör oluştur ---
|
||||
@@ -24,6 +25,20 @@ const DOWNLOAD_DIR = path.join(__dirname, "downloads");
|
||||
if (!fs.existsSync(DOWNLOAD_DIR))
|
||||
fs.mkdirSync(DOWNLOAD_DIR, { recursive: true });
|
||||
|
||||
// --- Thumbnail cache klasörü ---
|
||||
const CACHE_DIR = path.join(__dirname, "cache");
|
||||
const THUMBNAIL_DIR = path.join(CACHE_DIR, "thumbnails");
|
||||
const VIDEO_THUMB_ROOT = path.join(THUMBNAIL_DIR, "videos");
|
||||
const IMAGE_THUMB_ROOT = path.join(THUMBNAIL_DIR, "images");
|
||||
|
||||
for (const dir of [THUMBNAIL_DIR, VIDEO_THUMB_ROOT, IMAGE_THUMB_ROOT]) {
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
const VIDEO_THUMBNAIL_TIME = process.env.VIDEO_THUMBNAIL_TIME || "00:00:05";
|
||||
const VIDEO_EXTS = [".mp4", ".webm", ".mkv", ".mov", ".m4v"];
|
||||
const generatingThumbnails = new Set();
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
@@ -31,21 +46,186 @@ app.use("/downloads", express.static(DOWNLOAD_DIR));
|
||||
|
||||
// --- En uygun video dosyasını seç ---
|
||||
function pickBestVideoFile(torrent) {
|
||||
const videoExts = [".mp4", ".webm", ".mkv", ".mov", ".m4v"];
|
||||
const videos = torrent.files
|
||||
.map((f, i) => ({ i, f }))
|
||||
.filter(({ f }) => videoExts.includes(path.extname(f.name).toLowerCase()));
|
||||
.filter(({ f }) => VIDEO_EXTS.includes(path.extname(f.name).toLowerCase()));
|
||||
if (!videos.length) return 0;
|
||||
videos.sort((a, b) => b.f.length - a.f.length);
|
||||
return videos[0].i;
|
||||
}
|
||||
|
||||
function ensureDirForFile(filePath) {
|
||||
const dir = path.dirname(filePath);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
function sanitizeRelative(relPath) {
|
||||
return relPath.replace(/^[\\/]+/, "");
|
||||
}
|
||||
|
||||
function relPathToSegments(relPath) {
|
||||
return sanitizeRelative(relPath).split(/[\\/]/).filter(Boolean);
|
||||
}
|
||||
|
||||
function rootFromRelPath(relPath) {
|
||||
const segments = relPathToSegments(relPath);
|
||||
return segments[0] || null;
|
||||
}
|
||||
|
||||
function getVideoThumbnailPaths(relPath) {
|
||||
const parsed = path.parse(relPath);
|
||||
const relThumb = path.join("videos", parsed.dir, `${parsed.name}.jpg`);
|
||||
const absThumb = path.join(THUMBNAIL_DIR, relThumb);
|
||||
return { relThumb, absThumb };
|
||||
}
|
||||
|
||||
function getImageThumbnailPaths(relPath) {
|
||||
const parsed = path.parse(relPath);
|
||||
const relThumb = path.join(
|
||||
"images",
|
||||
parsed.dir,
|
||||
`${parsed.name}${parsed.ext || ".jpg"}`
|
||||
);
|
||||
const absThumb = path.join(THUMBNAIL_DIR, relThumb);
|
||||
return { relThumb, absThumb };
|
||||
}
|
||||
|
||||
function thumbnailUrl(relThumb) {
|
||||
const safe = relThumb
|
||||
.split(path.sep)
|
||||
.filter(Boolean)
|
||||
.map(encodeURIComponent)
|
||||
.join("/");
|
||||
return `/thumbnails/${safe}`;
|
||||
}
|
||||
|
||||
function markGenerating(absThumb, add) {
|
||||
if (add) generatingThumbnails.add(absThumb);
|
||||
else generatingThumbnails.delete(absThumb);
|
||||
}
|
||||
|
||||
function queueVideoThumbnail(fullPath, relPath) {
|
||||
const { relThumb, absThumb } = getVideoThumbnailPaths(relPath);
|
||||
if (fs.existsSync(absThumb) || generatingThumbnails.has(absThumb)) return;
|
||||
|
||||
ensureDirForFile(absThumb);
|
||||
markGenerating(absThumb, true);
|
||||
|
||||
const cmd = `ffmpeg -y -ss ${VIDEO_THUMBNAIL_TIME} -i "${fullPath}" -frames:v 1 -vf "scale=320:-1" -q:v 2 "${absThumb}"`;
|
||||
exec(cmd, (err) => {
|
||||
markGenerating(absThumb, false);
|
||||
if (err) {
|
||||
console.warn(`⚠️ Video thumbnail oluşturulamadı (${fullPath}): ${err.message}`);
|
||||
return;
|
||||
}
|
||||
console.log(`🎞️ Video thumbnail oluşturuldu: ${absThumb}`);
|
||||
const root = rootFromRelPath(relPath);
|
||||
if (root) broadcastFileUpdate(root);
|
||||
});
|
||||
}
|
||||
|
||||
function queueImageThumbnail(fullPath, relPath) {
|
||||
const { relThumb, absThumb } = getImageThumbnailPaths(relPath);
|
||||
if (fs.existsSync(absThumb) || generatingThumbnails.has(absThumb)) return;
|
||||
|
||||
ensureDirForFile(absThumb);
|
||||
markGenerating(absThumb, true);
|
||||
|
||||
const outputExt = path.extname(absThumb).toLowerCase();
|
||||
const needsQuality = outputExt === ".jpg" || outputExt === ".jpeg";
|
||||
const qualityArgs = needsQuality ? ' -q:v 5' : "";
|
||||
|
||||
const cmd = `ffmpeg -y -i "${fullPath}" -vf "scale=320:-1"${qualityArgs} "${absThumb}"`;
|
||||
exec(cmd, (err) => {
|
||||
markGenerating(absThumb, false);
|
||||
if (err) {
|
||||
console.warn(`⚠️ Resim thumbnail oluşturulamadı (${fullPath}): ${err.message}`);
|
||||
return;
|
||||
}
|
||||
console.log(`🖼️ Resim thumbnail oluşturuldu: ${absThumb}`);
|
||||
const root = rootFromRelPath(relPath);
|
||||
if (root) broadcastFileUpdate(root);
|
||||
});
|
||||
}
|
||||
|
||||
function removeThumbnailsForPath(relPath) {
|
||||
const normalized = sanitizeRelative(relPath);
|
||||
if (!normalized) return;
|
||||
|
||||
const directDirs = [
|
||||
path.join(VIDEO_THUMB_ROOT, normalized),
|
||||
path.join(IMAGE_THUMB_ROOT, normalized)
|
||||
];
|
||||
|
||||
for (const target of directDirs) {
|
||||
try {
|
||||
if (fs.existsSync(target) && fs.lstatSync(target).isDirectory()) {
|
||||
fs.rmSync(target, { recursive: true, force: true });
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ Thumbnail klasörü silinemedi (${target}): ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = path.parse(normalized);
|
||||
const candidates = [
|
||||
path.join(VIDEO_THUMB_ROOT, parsed.dir, `${parsed.name}.jpg`),
|
||||
path.join(IMAGE_THUMB_ROOT, parsed.dir, `${parsed.name}${parsed.ext}`)
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
if (fs.existsSync(candidate)) fs.rmSync(candidate, { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ Thumbnail silinemedi (${candidate}): ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resolveThumbnailAbsolute(relThumbPath) {
|
||||
const normalized = sanitizeRelative(relThumbPath);
|
||||
const resolved = path.resolve(THUMBNAIL_DIR, normalized);
|
||||
if (
|
||||
resolved !== THUMBNAIL_DIR &&
|
||||
!resolved.startsWith(THUMBNAIL_DIR + path.sep)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function broadcastFileUpdate(rootFolder) {
|
||||
if (!wss) return;
|
||||
const data = JSON.stringify({
|
||||
type: "fileUpdate",
|
||||
path: rootFolder
|
||||
});
|
||||
wss.clients.forEach((c) => c.readyState === 1 && c.send(data));
|
||||
}
|
||||
|
||||
function broadcastSnapshot() {
|
||||
if (!wss) return;
|
||||
const data = JSON.stringify({ type: "progress", torrents: snapshot() });
|
||||
wss.clients.forEach((c) => c.readyState === 1 && c.send(data));
|
||||
}
|
||||
|
||||
// --- Snapshot (thumbnail dahil, tracker + tarih eklendi) ---
|
||||
function snapshot() {
|
||||
return Array.from(torrents.values()).map(
|
||||
({ torrent, selectedIndex, savePath, added }) => {
|
||||
const thumbPath = path.join(savePath, "thumbnail.jpg");
|
||||
const hasThumb = fs.existsSync(thumbPath);
|
||||
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);
|
||||
}
|
||||
|
||||
return {
|
||||
infoHash: torrent.infoHash,
|
||||
name: torrent.name,
|
||||
@@ -63,31 +243,12 @@ function snapshot() {
|
||||
length: f.length
|
||||
})),
|
||||
selectedIndex,
|
||||
thumbnail: hasThumb ? `/thumbnail/${torrent.infoHash}` : null
|
||||
thumbnail
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createImageThumbnail(filePath, outputDir) {
|
||||
const fileName = path.basename(filePath);
|
||||
const thumbDir = path.join(outputDir, "thumbnail");
|
||||
const thumbPath = path.join(thumbDir, fileName);
|
||||
|
||||
if (!fs.existsSync(thumbDir)) fs.mkdirSync(thumbDir, { recursive: true });
|
||||
|
||||
// 320px genişlikte orantılı thumbnail oluştur
|
||||
const cmd = `ffmpeg -y -i "${filePath}" -vf "scale=320:-1" -q:v 5 "${thumbPath}"`;
|
||||
|
||||
exec(cmd, (err) => {
|
||||
if (err) {
|
||||
console.warn(`❌ Thumbnail oluşturulamadı: ${fileName}`, err.message);
|
||||
} else {
|
||||
console.log(`🖼️ Thumbnail oluşturuldu: ${thumbPath}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- Basit kimlik doğrulama sistemi ---
|
||||
const USERNAME = process.env.USERNAME;
|
||||
const PASSWORD = process.env.PASSWORD;
|
||||
@@ -156,11 +317,7 @@ app.post("/api/transfer", requireAuth, upload.single("torrent"), (req, res) => {
|
||||
length: f.length
|
||||
}))
|
||||
});
|
||||
const data = JSON.stringify({
|
||||
type: "progress",
|
||||
torrents: snapshot()
|
||||
});
|
||||
wss.clients.forEach((c) => c.readyState === 1 && c.send(data));
|
||||
broadcastSnapshot();
|
||||
});
|
||||
|
||||
// --- İndirme tamamlandığında thumbnail oluştur ---
|
||||
@@ -170,49 +327,32 @@ app.post("/api/transfer", requireAuth, upload.single("torrent"), (req, res) => {
|
||||
|
||||
console.log(`✅ Torrent tamamlandı: ${torrent.name}`);
|
||||
|
||||
// --- 1️⃣ Video için thumbnail oluştur ---
|
||||
const videoFile = torrent.files[entry.selectedIndex];
|
||||
const videoPath = path.join(entry.savePath, videoFile.path);
|
||||
const thumbnailPath = path.join(entry.savePath, "thumbnail.jpg");
|
||||
|
||||
const cmd = `ffmpeg -ss 00:00:30 -i "${videoPath}" -frames:v 1 -q:v 2 "${thumbnailPath}"`;
|
||||
exec(cmd, (err) => {
|
||||
if (err)
|
||||
console.warn(`⚠️ Video thumbnail oluşturulamadı: ${err.message}`);
|
||||
else {
|
||||
console.log(`🎞️ Video thumbnail oluşturuldu: ${thumbnailPath}`);
|
||||
const data = JSON.stringify({
|
||||
type: "fileUpdate",
|
||||
path: path.relative(DOWNLOAD_DIR, entry.savePath)
|
||||
});
|
||||
wss.clients.forEach((c) => c.readyState === 1 && c.send(data));
|
||||
}
|
||||
});
|
||||
|
||||
// --- 2️⃣ Resimler için thumbnail oluştur ---
|
||||
// Tüm resimleri tara, küçük hallerini kök klasör altındaki /thumbnail klasörüne oluştur
|
||||
const rootThumbDir = path.join(entry.savePath, "thumbnail");
|
||||
if (!fs.existsSync(rootThumbDir))
|
||||
fs.mkdirSync(rootThumbDir, { recursive: true });
|
||||
const rootFolder = path.basename(entry.savePath);
|
||||
|
||||
torrent.files.forEach((file) => {
|
||||
const filePath = path.join(entry.savePath, file.path);
|
||||
const mimeType = mime.lookup(filePath) || "";
|
||||
const fullPath = path.join(entry.savePath, file.path);
|
||||
const relPath = path.join(rootFolder, file.path);
|
||||
const mimeType = mime.lookup(fullPath) || "";
|
||||
|
||||
if (mimeType.startsWith("image/")) {
|
||||
const thumbPath = path.join(rootThumbDir, path.basename(filePath));
|
||||
|
||||
// 320px genişlikte, orantılı küçük versiyon oluştur
|
||||
const imgCmd = `ffmpeg -y -i "${filePath}" -vf "scale=320:-1" -q:v 5 "${thumbPath}"`;
|
||||
exec(imgCmd, (err) => {
|
||||
if (err)
|
||||
console.warn(
|
||||
`⚠️ Resim thumbnail oluşturulamadı (${file.name}): ${err.message}`
|
||||
);
|
||||
else console.log(`🖼️ Resim thumbnail oluşturuldu: ${thumbPath}`);
|
||||
});
|
||||
if (mimeType.startsWith("video/")) {
|
||||
queueVideoThumbnail(fullPath, relPath);
|
||||
} else if (mimeType.startsWith("image/")) {
|
||||
queueImageThumbnail(fullPath, relPath);
|
||||
}
|
||||
});
|
||||
|
||||
// Eski thumbnail yapısını temizle
|
||||
try {
|
||||
const legacyThumb = path.join(entry.savePath, "thumbnail.jpg");
|
||||
if (fs.existsSync(legacyThumb)) fs.rmSync(legacyThumb, { force: true });
|
||||
const legacyDir = path.join(entry.savePath, "thumbnail");
|
||||
if (fs.existsSync(legacyDir))
|
||||
fs.rmSync(legacyDir, { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
console.warn("⚠️ Eski thumbnail klasörü temizlenemedi:", err.message);
|
||||
}
|
||||
|
||||
broadcastSnapshot();
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
@@ -220,15 +360,12 @@ app.post("/api/transfer", requireAuth, upload.single("torrent"), (req, res) => {
|
||||
});
|
||||
|
||||
// --- Thumbnail endpoint ---
|
||||
app.get("/thumbnail/:hash", (req, res) => {
|
||||
const entry = torrents.get(req.params.hash);
|
||||
if (!entry) return res.status(404).end();
|
||||
|
||||
const thumbnailPath = path.join(entry.savePath, "thumbnail.jpg");
|
||||
if (!fs.existsSync(thumbnailPath))
|
||||
return res.status(404).send("Thumbnail yok");
|
||||
|
||||
res.sendFile(thumbnailPath);
|
||||
app.get("/thumbnails/:path(*)", requireAuth, (req, res) => {
|
||||
const relThumb = req.params.path || "";
|
||||
const fullPath = resolveThumbnailAbsolute(relThumb);
|
||||
if (!fullPath) return res.status(400).send("Geçersiz thumbnail yolu");
|
||||
if (!fs.existsSync(fullPath)) return res.status(404).send("Thumbnail yok");
|
||||
res.sendFile(fullPath);
|
||||
});
|
||||
|
||||
// --- Torrentleri listele ---
|
||||
@@ -252,6 +389,7 @@ app.delete("/api/torrents/:hash", requireAuth, (req, res) => {
|
||||
const { torrent, savePath } = entry;
|
||||
torrent.destroy(() => {
|
||||
torrents.delete(req.params.hash);
|
||||
const rootFolder = savePath ? path.basename(savePath) : null;
|
||||
if (savePath && fs.existsSync(savePath)) {
|
||||
try {
|
||||
fs.rmSync(savePath, { recursive: true, force: true });
|
||||
@@ -260,6 +398,11 @@ app.delete("/api/torrents/:hash", requireAuth, (req, res) => {
|
||||
console.warn(`⚠️ ${savePath} silinemedi:`, err.message);
|
||||
}
|
||||
}
|
||||
if (rootFolder) {
|
||||
removeThumbnailsForPath(rootFolder);
|
||||
broadcastFileUpdate(rootFolder);
|
||||
}
|
||||
broadcastSnapshot();
|
||||
res.json({ ok: true });
|
||||
});
|
||||
});
|
||||
@@ -314,6 +457,7 @@ app.delete("/api/file", requireAuth, (req, res) => {
|
||||
// 1) Dosya/klasörü sil
|
||||
fs.rmSync(fullPath, { recursive: true, force: true });
|
||||
console.log(`🗑️ Dosya/klasör silindi: ${fullPath}`);
|
||||
removeThumbnailsForPath(filePath);
|
||||
|
||||
// 2) İlk segment (klasör adı) => folderId (örn: "1730048432921")
|
||||
const folderId = (filePath.split(/[\\/]/)[0] || "").trim();
|
||||
@@ -334,27 +478,15 @@ app.delete("/api/file", requireAuth, (req, res) => {
|
||||
entry?.torrent?.destroy(() => {
|
||||
torrents.delete(matchedInfoHash);
|
||||
console.log(`🧹 Torrent kaydı da temizlendi: ${matchedInfoHash}`);
|
||||
// anında WebSocket güncellemesi (broadcastSnapshot global fonksiyonunu kullanıyorsan onu çağır)
|
||||
if (typeof broadcastSnapshot === "function") {
|
||||
broadcastSnapshot();
|
||||
} else if (wss) {
|
||||
const data = JSON.stringify({
|
||||
type: "progress",
|
||||
torrents: snapshot()
|
||||
});
|
||||
wss.clients.forEach((c) => c.readyState === 1 && c.send(data));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Torrent eşleşmediyse de listeyi tazele (ör. sade dosya silinmiştir)
|
||||
if (typeof broadcastSnapshot === "function") {
|
||||
broadcastSnapshot();
|
||||
} else if (wss) {
|
||||
const data = JSON.stringify({ type: "progress", torrents: snapshot() });
|
||||
wss.clients.forEach((c) => c.readyState === 1 && c.send(data));
|
||||
}
|
||||
}
|
||||
|
||||
if (folderId) broadcastFileUpdate(folderId);
|
||||
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
console.error("❌ Dosya silinemedi:", err.message);
|
||||
@@ -402,29 +534,20 @@ app.get("/api/files", requireAuth, (req, res) => {
|
||||
const full = path.join(dir, entry.name);
|
||||
const rel = path.relative(DOWNLOAD_DIR, full);
|
||||
|
||||
if (rel.toLowerCase().includes("/thumbnail")) continue;
|
||||
|
||||
// 🔥 Ignore kontrolü (hem dosya hem klasör için)
|
||||
if (isIgnored(entry.name) || isIgnored(rel)) continue;
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
result = result.concat(walk(full));
|
||||
} else {
|
||||
if (entry.name.toLowerCase() === "thumbnail.jpg") continue;
|
||||
|
||||
const size = fs.statSync(full).size;
|
||||
const type = mime.lookup(full) || "application/octet-stream";
|
||||
|
||||
const parts = rel.split(path.sep);
|
||||
const rootHash = parts[0];
|
||||
const videoThumbPath = path.join(
|
||||
DOWNLOAD_DIR,
|
||||
rootHash,
|
||||
"thumbnail.jpg"
|
||||
);
|
||||
const hasVideoThumb = fs.existsSync(videoThumbPath);
|
||||
|
||||
const urlPath = encodeURIComponent(rel).replace(/%2F/g, "/");
|
||||
const safeRel = sanitizeRelative(rel);
|
||||
const urlPath = safeRel
|
||||
.split(/[\\/]/)
|
||||
.map(encodeURIComponent)
|
||||
.join("/");
|
||||
const url = `/media/${urlPath}`;
|
||||
|
||||
const isImage = String(type).startsWith("image/");
|
||||
@@ -432,27 +555,20 @@ app.get("/api/files", requireAuth, (req, res) => {
|
||||
|
||||
let thumb = null;
|
||||
|
||||
// 🎬 Video thumbnail
|
||||
if (hasVideoThumb) {
|
||||
thumb = `/downloads/${rootHash}/thumbnail.jpg`;
|
||||
if (isVideo) {
|
||||
const { relThumb, absThumb } = getVideoThumbnailPaths(safeRel);
|
||||
if (fs.existsSync(absThumb)) thumb = thumbnailUrl(relThumb);
|
||||
else queueVideoThumbnail(full, safeRel);
|
||||
}
|
||||
|
||||
// 🖼️ Resim thumbnail (thumbnail klasöründe varsa)
|
||||
const imageThumbPath = path.join(
|
||||
DOWNLOAD_DIR,
|
||||
rootHash,
|
||||
"thumbnail",
|
||||
path.basename(rel)
|
||||
);
|
||||
|
||||
if (isImage && fs.existsSync(imageThumbPath)) {
|
||||
thumb = `/downloads/${rootHash}/thumbnail/${encodeURIComponent(
|
||||
path.basename(rel)
|
||||
)}`;
|
||||
if (isImage) {
|
||||
const { relThumb, absThumb } = getImageThumbnailPaths(safeRel);
|
||||
if (fs.existsSync(absThumb)) thumb = thumbnailUrl(relThumb);
|
||||
else queueImageThumbnail(full, safeRel);
|
||||
}
|
||||
|
||||
result.push({
|
||||
name: rel,
|
||||
name: safeRel,
|
||||
size,
|
||||
type,
|
||||
url,
|
||||
@@ -527,7 +643,7 @@ if (fs.existsSync(publicDir)) {
|
||||
});
|
||||
}
|
||||
|
||||
const wss = new WebSocketServer({ server });
|
||||
wss = new WebSocketServer({ server });
|
||||
wss.on("connection", (ws) => {
|
||||
ws.send(JSON.stringify({ type: "progress", torrents: snapshot() }));
|
||||
});
|
||||
@@ -535,8 +651,7 @@ wss.on("connection", (ws) => {
|
||||
// --- ⏱️ Her 2 saniyede bir aktif torrent durumu yayınla ---
|
||||
setInterval(() => {
|
||||
if (torrents.size > 0) {
|
||||
const data = JSON.stringify({ type: "progress", torrents: snapshot() });
|
||||
wss.clients.forEach((c) => c.readyState === 1 && c.send(data));
|
||||
broadcastSnapshot();
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user