Files
dupe/server/server.js

404 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
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();
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 });
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 videoExts = [".mp4", ".webm", ".mkv", ".mov", ".m4v"];
const videos = torrent.files
.map((f, i) => ({ i, f }))
.filter(({ f }) => videoExts.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;
}
// --- 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);
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, // 🆕 ilk tracker
added, // 🆕 eklenme zamanı
files: torrent.files.map((f, i) => ({
index: i,
name: f.name,
length: f.length
})),
selectedIndex,
thumbnail: hasThumb ? `/thumbnail/${torrent.infoHash}` : null
};
}
);
}
// --- 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
});
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
}))
});
});
// --- İndirme tamamlandığında thumbnail oluştur ---
torrent.on("done", () => {
const entry = torrents.get(torrent.infoHash);
if (!entry) return;
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(`⚠️ Thumbnail oluşturulamadı: ${err.message}`);
else console.log(`📸 Thumbnail oluşturuldu: ${thumbnailPath}`);
});
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// --- 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);
});
// --- 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;
torrent.destroy(() => {
torrents.delete(req.params.hash);
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);
}
}
res.json({ ok: true });
});
});
// --- 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);
}
});
// --- 📁 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(/^\./, "")
);
};
// --- 📁 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() === "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 url = `/media/${urlPath}`;
const isImage = String(type).startsWith("image/");
const isVideo = String(type).startsWith("video/");
const thumb = isImage
? url
: hasVideoThumb
? `/downloads/${rootHash}/thumbnail.jpg`
: null;
result.push({
name: rel,
size,
type,
url,
thumbnail: thumb
});
}
}
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"));
});
}
const wss = new WebSocketServer({ server });
wss.on("connection", (ws) => {
ws.send(JSON.stringify({ type: "progress", torrents: snapshot() }));
});
setInterval(() => {
const data = JSON.stringify({ type: "progress", torrents: snapshot() });
wss.clients.forEach((c) => c.readyState === 1 && c.send(data));
}, 1000);
client.on("error", (err) => {
if (!String(err).includes("uTP"))
console.error("WebTorrent error:", err.message);
});