Files
dupe/server/server.js

806 lines
24 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import express from "express";
import cors from "cors";
import multer from "multer";
import WebTorrent from "webtorrent";
import fs from "fs";
import path from "path";
import mime from "mime-types";
import { WebSocketServer } from "ws";
import { fileURLToPath } from "url";
import { exec } from "child_process";
import crypto from "crypto"; // 🔒 basit token üretimi için
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
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 ---
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();
const INFO_FILENAME = "info.json";
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use("/downloads", express.static(DOWNLOAD_DIR));
// --- En uygun video dosyasını seç ---
function pickBestVideoFile(torrent) {
const videos = torrent.files
.map((f, i) => ({ i, f }))
.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 infoFilePath(savePath) {
return path.join(savePath, INFO_FILENAME);
}
function readInfoFile(savePath) {
const target = infoFilePath(savePath);
if (!fs.existsSync(target)) return null;
try {
return JSON.parse(fs.readFileSync(target, "utf-8"));
} catch (err) {
console.warn(`⚠️ info.json okunamadı (${target}): ${err.message}`);
return null;
}
}
function upsertInfoFile(savePath, partial) {
const target = infoFilePath(savePath);
try {
ensureDirForFile(target);
let current = {};
if (fs.existsSync(target)) {
try {
current = JSON.parse(fs.readFileSync(target, "utf-8")) || {};
} catch (err) {
console.warn(`⚠️ info.json parse edilemedi (${target}): ${err.message}`);
}
}
const timestamp = Date.now();
const next = {
...current,
...partial,
updatedAt: timestamp
};
if (!next.createdAt) {
next.createdAt =
current.createdAt ?? partial?.createdAt ?? timestamp;
}
if (!next.added && partial?.added) {
next.added = partial.added;
}
if (!next.folder) {
next.folder = path.basename(savePath);
}
fs.writeFileSync(target, JSON.stringify(next, null, 2), "utf-8");
return next;
} catch (err) {
console.warn(`⚠️ info.json yazılamadı (${target}): ${err.message}`);
return null;
}
}
function readInfoForRoot(rootFolder) {
const safe = sanitizeRelative(rootFolder);
if (!safe) return null;
const target = path.join(DOWNLOAD_DIR, safe, INFO_FILENAME);
if (!fs.existsSync(target)) return null;
try {
return JSON.parse(fs.readFileSync(target, "utf-8"));
} catch (err) {
console.warn(`⚠️ info.json okunamadı (${target}): ${err.message}`);
return null;
}
}
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 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}`);
}
}
const potentialDirs = [
path.join(VIDEO_THUMB_ROOT, parsed.dir),
path.join(IMAGE_THUMB_ROOT, parsed.dir)
];
for (const dirPath of potentialDirs) {
cleanupEmptyDirs(dirPath);
}
}
function cleanupEmptyDirs(startDir) {
let dir = startDir;
while (
dir &&
dir.startsWith(THUMBNAIL_DIR) &&
fs.existsSync(dir)
) {
try {
const stat = fs.lstatSync(dir);
if (!stat.isDirectory()) break;
const entries = fs.readdirSync(dir);
if (entries.length > 0) break;
fs.rmdirSync(dir);
} catch (err) {
console.warn(`⚠️ Thumbnail klasörü temizlenemedi (${dir}): ${err.message}`);
break;
}
const parent = path.dirname(dir);
if (
!parent ||
parent === dir ||
parent.length < THUMBNAIL_DIR.length ||
parent === THUMBNAIL_DIR
) {
break;
}
dir = parent;
}
}
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 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,
progress: torrent.progress,
downloaded: torrent.downloaded,
downloadSpeed: torrent.downloadSpeed,
uploadSpeed: torrent.uploadSpeed,
numPeers: torrent.numPeers,
tracker: torrent.announce?.[0] || null,
added,
savePath, // 🆕 BURASI!
files: torrent.files.map((f, i) => ({
index: i,
name: f.name,
length: f.length
})),
selectedIndex,
thumbnail
};
}
);
}
// --- Basit kimlik doğrulama sistemi ---
const USERNAME = process.env.USERNAME;
const PASSWORD = process.env.PASSWORD;
let activeTokens = new Set();
app.post("/api/login", (req, res) => {
const { username, password } = req.body;
if (username === USERNAME && password === PASSWORD) {
const token = crypto.randomBytes(24).toString("hex");
activeTokens.add(token);
return res.json({ token });
}
res.status(401).json({ error: "Invalid credentials" });
});
function requireAuth(req, res, next) {
const token = req.headers.authorization?.split(" ")[1] || req.query.token;
if (!token || !activeTokens.has(token))
return res.status(401).json({ error: "Unauthorized" });
next();
}
// --- Torrent veya magnet ekleme ---
app.post("/api/transfer", requireAuth, upload.single("torrent"), (req, res) => {
try {
let source = req.body.magnet;
if (req.file) source = fs.readFileSync(req.file.path);
if (!source)
return res.status(400).json({ error: "magnet veya .torrent gerekli" });
// Her torrent için ayrı klasör
const savePath = path.join(DOWNLOAD_DIR, Date.now().toString());
fs.mkdirSync(savePath, { recursive: true });
const torrent = client.add(source, { announce: [], path: savePath });
// 🆕 Torrent eklendiği anda tarih kaydedelim
const added = Date.now();
torrents.set(torrent.infoHash, {
torrent,
selectedIndex: 0,
savePath,
added
});
// --- Metadata geldiğinde ---
torrent.on("ready", () => {
const selectedIndex = pickBestVideoFile(torrent);
torrents.set(torrent.infoHash, {
torrent,
selectedIndex,
savePath,
added
});
const rootFolder = path.basename(savePath);
upsertInfoFile(savePath, {
infoHash: torrent.infoHash,
name: torrent.name,
tracker: torrent.announce?.[0] || null,
added,
createdAt: added,
folder: rootFolder
});
broadcastFileUpdate(rootFolder);
res.json({
ok: true,
infoHash: torrent.infoHash,
name: torrent.name,
selectedIndex,
tracker: torrent.announce?.[0] || null,
added,
files: torrent.files.map((f, i) => ({
index: i,
name: f.name,
length: f.length
}))
});
broadcastSnapshot();
});
// --- İndirme tamamlandığında thumbnail oluştur ---
torrent.on("done", () => {
const entry = torrents.get(torrent.infoHash);
if (!entry) return;
console.log(`✅ Torrent tamamlandı: ${torrent.name}`);
const rootFolder = path.basename(entry.savePath);
torrent.files.forEach((file) => {
const fullPath = path.join(entry.savePath, file.path);
const relPath = path.join(rootFolder, file.path);
const mimeType = mime.lookup(fullPath) || "";
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);
}
upsertInfoFile(entry.savePath, {
completedAt: Date.now(),
totalBytes: torrent.downloaded,
fileCount: torrent.files.length
});
broadcastFileUpdate(rootFolder);
broadcastSnapshot();
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// --- Thumbnail endpoint ---
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 ---
app.get("/api/torrents", requireAuth, (req, res) => {
res.json(snapshot());
});
// --- Seçili dosya değiştir ---
app.post("/api/torrents/:hash/select/:index", requireAuth, (req, res) => {
const entry = torrents.get(req.params.hash);
if (!entry) return res.status(404).json({ error: "torrent bulunamadı" });
entry.selectedIndex = Number(req.params.index) || 0;
res.json({ ok: true, selectedIndex: entry.selectedIndex });
});
// --- Torrent silme (disk dahil) ---
app.delete("/api/torrents/:hash", requireAuth, (req, res) => {
const entry = torrents.get(req.params.hash);
if (!entry) return res.status(404).json({ error: "torrent bulunamadı" });
const { torrent, savePath } = entry;
const isComplete = torrent?.done || (torrent?.progress ?? 0) >= 1;
const rootFolder = savePath ? path.basename(savePath) : null;
torrent.destroy(() => {
torrents.delete(req.params.hash);
if (!isComplete) {
if (savePath && fs.existsSync(savePath)) {
try {
fs.rmSync(savePath, { recursive: true, force: true });
console.log(`🗑️ ${savePath} klasörü silindi`);
} catch (err) {
console.warn(`⚠️ ${savePath} silinemedi:`, err.message);
}
}
if (rootFolder) {
removeThumbnailsForPath(rootFolder);
broadcastFileUpdate(rootFolder);
}
} else {
console.log(
` ${req.params.hash} torrent'i tamamlandığı için yalnızca Transfers listesinden kaldırıldı; dosyalar tutuldu.`
);
}
broadcastSnapshot();
res.json({
ok: true,
filesRemoved: !isComplete
});
});
});
// --- GENEL MEDYA SUNUMU (🆕 resimler + videolar) ---
app.get("/media/:path(*)", requireAuth, (req, res) => {
const relPath = req.params.path;
const fullPath = path.join(DOWNLOAD_DIR, relPath);
if (!fs.existsSync(fullPath)) return res.status(404).send("File not found");
const stat = fs.statSync(fullPath);
const fileSize = stat.size;
const type = mime.lookup(fullPath) || "application/octet-stream";
const isVideo = String(type).startsWith("video/");
const range = req.headers.range;
if (isVideo && range) {
const [startStr, endStr] = range.replace(/bytes=/, "").split("-");
const start = parseInt(startStr, 10);
const end = endStr ? parseInt(endStr, 10) : fileSize - 1;
const chunkSize = end - start + 1;
const file = fs.createReadStream(fullPath, { start, end });
const head = {
"Content-Range": `bytes ${start}-${end}/${fileSize}`,
"Accept-Ranges": "bytes",
"Content-Length": chunkSize,
"Content-Type": type
};
res.writeHead(206, head);
file.pipe(res);
} else {
const head = {
"Content-Length": fileSize,
"Content-Type": type,
"Accept-Ranges": isVideo ? "bytes" : "none"
};
res.writeHead(200, head);
fs.createReadStream(fullPath).pipe(res);
}
});
// --- 🗑️ Tekil dosya veya torrent klasörü silme ---
app.delete("/api/file", requireAuth, (req, res) => {
const filePath = req.query.path;
if (!filePath) return res.status(400).json({ error: "path gerekli" });
const fullPath = path.join(DOWNLOAD_DIR, filePath);
if (!fs.existsSync(fullPath))
return res.status(404).json({ error: "Dosya bulunamadı" });
try {
// 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();
// 3) torrents Mapinde, savePath'in son klasörü folderId olan entryyi bul
let matchedInfoHash = null;
for (const [infoHash, entry] of torrents.entries()) {
const lastDir = path.basename(entry.savePath);
if (lastDir === folderId) {
matchedInfoHash = infoHash;
break;
}
}
// 4) Eşleşen torrent varsa destroy + Mapten sil + snapshot yayınla
if (matchedInfoHash) {
const entry = torrents.get(matchedInfoHash);
entry?.torrent?.destroy(() => {
torrents.delete(matchedInfoHash);
console.log(`🧹 Torrent kaydı da temizlendi: ${matchedInfoHash}`);
broadcastSnapshot();
});
} else {
// Torrent eşleşmediyse de listeyi tazele (ör. sade dosya silinmiştir)
broadcastSnapshot();
}
if (folderId) broadcastFileUpdate(folderId);
res.json({ ok: true });
} catch (err) {
console.error("❌ Dosya silinemedi:", err.message);
res.status(500).json({ error: err.message });
}
});
// --- 📁 Dosya gezgini (🆕 type ve url alanları eklendi; resim thumb'ı) ---
app.get("/api/files", requireAuth, (req, res) => {
// --- 🧩 .ignoreFiles içeriğini oku ---
let ignoreList = [];
const ignorePath = path.join(__dirname, ".ignoreFiles");
if (fs.existsSync(ignorePath)) {
try {
const raw = fs.readFileSync(ignorePath, "utf-8");
ignoreList = raw
.split("\n")
.map((l) => l.trim().toLowerCase())
.filter((l) => l && !l.startsWith("#"));
} catch (err) {
console.warn("⚠️ .ignoreFiles okunamadı:", err.message);
}
}
// --- 🔍 Yardımcı fonksiyon: dosya ignoreListte mi? ---
const isIgnored = (name) => {
const lower = name.toLowerCase();
const ext = path.extname(lower).replace(".", "");
return ignoreList.some(
(ignored) =>
lower === ignored ||
lower.endsWith(ignored) ||
lower.endsWith(`.${ignored}`) ||
ext === ignored.replace(/^\./, "")
);
};
const infoCache = new Map();
const getInfo = (relPath) => {
const root = rootFromRelPath(relPath);
if (!root) return null;
if (!infoCache.has(root)) {
infoCache.set(root, readInfoForRoot(root));
}
return infoCache.get(root);
};
// --- 📁 Klasörleri dolaş ---
const walk = (dir) => {
let result = [];
const list = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of list) {
const full = path.join(dir, entry.name);
const rel = path.relative(DOWNLOAD_DIR, full);
// 🔥 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() === INFO_FILENAME) continue;
const size = fs.statSync(full).size;
const type = mime.lookup(full) || "application/octet-stream";
const safeRel = sanitizeRelative(rel);
const urlPath = safeRel
.split(/[\\/]/)
.map(encodeURIComponent)
.join("/");
const url = `/media/${urlPath}`;
const isImage = String(type).startsWith("image/");
const isVideo = String(type).startsWith("video/");
let thumb = null;
if (isVideo) {
const { relThumb, absThumb } = getVideoThumbnailPaths(safeRel);
if (fs.existsSync(absThumb)) thumb = thumbnailUrl(relThumb);
else queueVideoThumbnail(full, safeRel);
}
if (isImage) {
const { relThumb, absThumb } = getImageThumbnailPaths(safeRel);
if (fs.existsSync(absThumb)) thumb = thumbnailUrl(relThumb);
else queueImageThumbnail(full, safeRel);
}
const info = getInfo(safeRel) || {};
const rootFolder = rootFromRelPath(safeRel);
const added =
info.added ?? info.createdAt ?? null;
const completedAt = info.completedAt ?? null;
const tracker = info.tracker ?? null;
const torrentName = info.name ?? null;
const infoHash = info.infoHash ?? null;
result.push({
name: safeRel,
size,
type,
url,
thumbnail: thumb,
rootFolder,
added,
completedAt,
tracker,
torrentName,
infoHash
});
}
}
return result;
};
try {
const files = walk(DOWNLOAD_DIR);
res.json(files);
} catch (err) {
console.error("📁 Files API error:", err);
res.status(500).json({ error: err.message });
}
});
// --- Stream endpoint (torrent içinden) ---
app.get("/stream/:hash", requireAuth, (req, res) => {
const entry = torrents.get(req.params.hash);
if (!entry) return res.status(404).end();
const file =
entry.torrent.files[entry.selectedIndex] || entry.torrent.files[0];
const total = file.length;
const type = mime.lookup(file.name) || "video/mp4";
const range = req.headers.range;
if (!range) {
res.writeHead(200, {
"Content-Length": total,
"Content-Type": type,
"Accept-Ranges": "bytes"
});
return file.createReadStream().pipe(res);
}
const [s, e] = range.replace(/bytes=/, "").split("-");
const start = parseInt(s, 10);
const end = e ? parseInt(e, 10) : total - 1;
res.writeHead(206, {
"Content-Range": `bytes ${start}-${end}/${total}`,
"Accept-Ranges": "bytes",
"Content-Length": end - start + 1,
"Content-Type": type
});
const stream = file.createReadStream({ start, end });
stream.on("error", (err) => console.warn("Stream error:", err.message));
res.on("close", () => stream.destroy());
stream.pipe(res);
});
console.log("📂 Download path:", DOWNLOAD_DIR);
// --- WebSocket: anlık durum yayını ---
const server = app.listen(PORT, () =>
console.log(`✅ WebTorrent server ${PORT} portunda çalışıyor`)
);
// --- ✅ Client build (frontend) dosyalarını sun ---
const publicDir = path.join(__dirname, "public");
if (fs.existsSync(publicDir)) {
app.use(express.static(publicDir));
app.get("*", (req, res, next) => {
if (req.path.startsWith("/api")) return next();
res.sendFile(path.join(publicDir, "index.html"));
});
}
wss = new WebSocketServer({ server });
wss.on("connection", (ws) => {
ws.send(JSON.stringify({ type: "progress", torrents: snapshot() }));
});
// --- ⏱️ Her 2 saniyede bir aktif torrent durumu yayınla ---
setInterval(() => {
if (torrents.size > 0) {
broadcastSnapshot();
}
}, 2000);
client.on("error", (err) => {
if (!String(err).includes("uTP"))
console.error("WebTorrent error:", err.message);
});