JWT, server modüler hale getirildi, Torrent durumu kalıcı hale getirildi.
This commit is contained in:
567
server/server.js
567
server/server.js
@@ -5,11 +5,14 @@ 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
|
||||
import { getSystemDiskInfo } from "./utils/diskSpace.js";
|
||||
import { createAuth } from "./modules/auth.js";
|
||||
import { buildHealthReport, healthRouter } from "./modules/health.js";
|
||||
import { restoreTorrentsFromDisk } from "./modules/state.js";
|
||||
import { createWebsocketServer, broadcastJson } from "./modules/websocket.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -148,6 +151,37 @@ function ensureDirForFile(filePath) {
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
const AUTH_DATA_DIR = path.join(__dirname, "data");
|
||||
const USERS_FILE = path.join(AUTH_DATA_DIR, "users.json");
|
||||
const JWT_SECRET_FILE = path.join(CACHE_DIR, "jwt-secret");
|
||||
|
||||
let healthSnapshot = null;
|
||||
const auth = createAuth({ usersPath: USERS_FILE, secretPath: JWT_SECRET_FILE });
|
||||
const { router: authRouter, requireAuth, requireRole, issueMediaToken, verifyToken } = auth;
|
||||
|
||||
app.use(authRouter);
|
||||
|
||||
buildHealthReport({
|
||||
ffmpegPath: "ffmpeg",
|
||||
ffprobePath: FFPROBE_PATH,
|
||||
tmdbKey: TMDB_API_KEY,
|
||||
tvdbKey: TVDB_API_KEY,
|
||||
fanartKey: FANART_TV_API_KEY
|
||||
})
|
||||
.then((report) => {
|
||||
healthSnapshot = report;
|
||||
const missing = report.binaries.filter((b) => !b.ok);
|
||||
if (missing.length) {
|
||||
console.warn("⚠️ Eksik bağımlılıklar:", missing.map((m) => m.name).join(", "));
|
||||
}
|
||||
if (!TMDB_API_KEY || !TVDB_API_KEY) {
|
||||
console.warn("⚠️ TMDB/TVDB anahtarları eksik, metadata özellikleri sınırlı olacak.");
|
||||
}
|
||||
})
|
||||
.catch((err) => console.warn("⚠️ Sağlık kontrolü çalıştırılamadı:", err.message));
|
||||
|
||||
app.get("/api/health", requireAuth, healthRouter(() => healthSnapshot));
|
||||
|
||||
function tvdbImageUrl(pathSegment) {
|
||||
if (!pathSegment) return null;
|
||||
if (pathSegment.startsWith("http")) return pathSegment;
|
||||
@@ -3244,9 +3278,9 @@ function snapshot() {
|
||||
queueVideoThumbnail(path.join(savePath, bestVideo.path), relPath);
|
||||
}
|
||||
|
||||
return {
|
||||
infoHash: torrent.infoHash,
|
||||
name: torrent.name,
|
||||
return {
|
||||
infoHash: torrent.infoHash,
|
||||
name: torrent.name,
|
||||
progress: torrent.progress,
|
||||
downloaded: torrent.downloaded,
|
||||
downloadSpeed: paused ? 0 : torrent.downloadSpeed, // Pause durumunda hız 0
|
||||
@@ -3268,27 +3302,259 @@ function snapshot() {
|
||||
);
|
||||
}
|
||||
|
||||
// --- Basit kimlik doğrulama sistemi ---
|
||||
const USERNAME = process.env.USERNAME;
|
||||
const PASSWORD = process.env.PASSWORD;
|
||||
let activeTokens = new Set();
|
||||
function wireTorrent(torrent, { savePath, added, respond, restored = false }) {
|
||||
torrents.set(torrent.infoHash, {
|
||||
torrent,
|
||||
selectedIndex: 0,
|
||||
savePath,
|
||||
added,
|
||||
paused: false
|
||||
});
|
||||
|
||||
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" });
|
||||
});
|
||||
torrent.on("ready", () => {
|
||||
onTorrentReady({ torrent, savePath, added, respond, restored });
|
||||
});
|
||||
|
||||
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.on("done", () => {
|
||||
onTorrentDone({ torrent });
|
||||
});
|
||||
}
|
||||
|
||||
function onTorrentReady({ torrent, savePath, added, respond }) {
|
||||
const selectedIndex = pickBestVideoFile(torrent);
|
||||
torrents.set(torrent.infoHash, {
|
||||
torrent,
|
||||
selectedIndex,
|
||||
savePath,
|
||||
added,
|
||||
paused: false
|
||||
});
|
||||
const rootFolder = path.basename(savePath);
|
||||
upsertInfoFile(savePath, {
|
||||
infoHash: torrent.infoHash,
|
||||
name: torrent.name,
|
||||
tracker: torrent.announce?.[0] || null,
|
||||
added,
|
||||
magnetURI: torrent.magnetURI,
|
||||
createdAt: added,
|
||||
folder: rootFolder
|
||||
});
|
||||
broadcastFileUpdate(rootFolder);
|
||||
|
||||
const payload = {
|
||||
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
|
||||
}))
|
||||
};
|
||||
|
||||
if (typeof respond === "function") respond(payload);
|
||||
broadcastSnapshot();
|
||||
}
|
||||
|
||||
async function onTorrentDone({ torrent }) {
|
||||
const entry = torrents.get(torrent.infoHash);
|
||||
if (!entry) return;
|
||||
|
||||
console.log(`✅ Torrent tamamlandı: ${torrent.name}`);
|
||||
|
||||
const rootFolder = path.basename(entry.savePath);
|
||||
|
||||
const bestVideoIndex = pickBestVideoFile(torrent);
|
||||
const bestVideo =
|
||||
torrent.files[bestVideoIndex] || torrent.files[0] || null;
|
||||
const displayName = bestVideo?.name || torrent.name || rootFolder;
|
||||
const bestVideoPath = bestVideo?.path
|
||||
? bestVideo.path.replace(/\\/g, "/")
|
||||
: null;
|
||||
|
||||
const perFileMetadata = {};
|
||||
const seriesEpisodes = {};
|
||||
let primaryMediaInfo = null;
|
||||
|
||||
for (const file of torrent.files) {
|
||||
const fullPath = path.join(entry.savePath, file.path);
|
||||
const relPathWithRoot = path.join(rootFolder, file.path);
|
||||
const normalizedRelPath = file.path.replace(/\\/g, "/");
|
||||
const mimeType = mime.lookup(fullPath) || "";
|
||||
const ext = path.extname(file.name).replace(/^\./, "").toLowerCase();
|
||||
|
||||
if (mimeType.startsWith("video/")) {
|
||||
queueVideoThumbnail(fullPath, relPathWithRoot);
|
||||
} else if (mimeType.startsWith("image/")) {
|
||||
queueImageThumbnail(fullPath, relPathWithRoot);
|
||||
}
|
||||
|
||||
let metaInfo = null;
|
||||
if (
|
||||
mimeType.startsWith("video/") ||
|
||||
mimeType.startsWith("audio/") ||
|
||||
mimeType.startsWith("image/")
|
||||
) {
|
||||
metaInfo = await extractMediaInfo(fullPath);
|
||||
}
|
||||
|
||||
if (
|
||||
!primaryMediaInfo &&
|
||||
bestVideoPath &&
|
||||
normalizedRelPath === bestVideoPath &&
|
||||
metaInfo
|
||||
) {
|
||||
primaryMediaInfo = metaInfo;
|
||||
}
|
||||
|
||||
perFileMetadata[normalizedRelPath] = {
|
||||
size: file.length,
|
||||
extension: ext || null,
|
||||
mimeType,
|
||||
mediaInfo: metaInfo
|
||||
};
|
||||
|
||||
const seriesInfo = parseSeriesInfo(file.name);
|
||||
if (seriesInfo) {
|
||||
try {
|
||||
const ensured = await ensureSeriesData(
|
||||
rootFolder,
|
||||
normalizedRelPath,
|
||||
seriesInfo,
|
||||
metaInfo
|
||||
);
|
||||
if (ensured?.show && ensured?.episode) {
|
||||
seriesEpisodes[normalizedRelPath] = {
|
||||
season: seriesInfo.season,
|
||||
episode: seriesInfo.episode,
|
||||
key: seriesInfo.key,
|
||||
title: ensured.episode.title || seriesInfo.title,
|
||||
showId: ensured.show.id || null,
|
||||
showTitle: ensured.show.title || seriesInfo.title,
|
||||
seasonName:
|
||||
ensured.season?.name || `Season ${seriesInfo.season}`,
|
||||
seasonId: ensured.season?.tvdbSeasonId || null,
|
||||
seasonPoster: ensured.season?.poster || null,
|
||||
overview: ensured.episode.overview || "",
|
||||
aired: ensured.episode.aired || null,
|
||||
runtime: ensured.episode.runtime || null,
|
||||
still: ensured.episode.still || null,
|
||||
episodeId: ensured.episode.tvdbEpisodeId || null,
|
||||
slug: ensured.episode.slug || null
|
||||
};
|
||||
const fileEntry = perFileMetadata[normalizedRelPath] || {};
|
||||
perFileMetadata[normalizedRelPath] = {
|
||||
...fileEntry,
|
||||
seriesMatch: {
|
||||
id: ensured.show.id || null,
|
||||
title: ensured.show.title || seriesInfo.title,
|
||||
season: ensured.season?.seasonNumber ?? seriesInfo.season,
|
||||
episode: ensured.episode.episodeNumber ?? seriesInfo.episode,
|
||||
code: ensured.episode.code || seriesInfo.key,
|
||||
poster: ensured.show.poster || null,
|
||||
backdrop: ensured.show.backdrop || null,
|
||||
seasonPoster: ensured.season?.poster || null,
|
||||
aired: ensured.episode.aired || null,
|
||||
runtime: ensured.episode.runtime || null,
|
||||
tvdbEpisodeId: ensured.episode.tvdbEpisodeId || null,
|
||||
matchedAt: Date.now()
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`⚠️ TV metadata oluşturulamadı (${rootFolder} - ${file.name}): ${
|
||||
err?.message || err
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
const infoUpdate = {
|
||||
completedAt: Date.now(),
|
||||
totalBytes: torrent.downloaded,
|
||||
fileCount: torrent.files.length,
|
||||
files: perFileMetadata,
|
||||
magnetURI: torrent.magnetURI
|
||||
};
|
||||
if (bestVideoPath) infoUpdate.primaryVideoPath = bestVideoPath;
|
||||
if (Object.keys(seriesEpisodes).length) {
|
||||
infoUpdate.seriesEpisodes = seriesEpisodes;
|
||||
}
|
||||
|
||||
const ensuredMedia = await ensureMovieData(
|
||||
rootFolder,
|
||||
displayName,
|
||||
bestVideoPath,
|
||||
primaryMediaInfo
|
||||
);
|
||||
if (ensuredMedia?.mediaInfo) {
|
||||
infoUpdate.primaryMediaInfo = ensuredMedia.mediaInfo;
|
||||
if (!infoUpdate.files) infoUpdate.files = perFileMetadata;
|
||||
if (bestVideoPath) {
|
||||
const entry = infoUpdate.files[bestVideoPath] || {};
|
||||
infoUpdate.files[bestVideoPath] = {
|
||||
...entry,
|
||||
movieMatch: ensuredMedia.metadata
|
||||
? {
|
||||
id: ensuredMedia.metadata.id ?? null,
|
||||
title:
|
||||
ensuredMedia.metadata.title ||
|
||||
ensuredMedia.metadata.matched_title ||
|
||||
displayName,
|
||||
year: ensuredMedia.metadata.release_date
|
||||
? Number(
|
||||
ensuredMedia.metadata.release_date.slice(0, 4)
|
||||
)
|
||||
: ensuredMedia.metadata.matched_year || null,
|
||||
poster: ensuredMedia.metadata.poster_path || null,
|
||||
backdrop: ensuredMedia.metadata.backdrop_path || null,
|
||||
cacheKey: ensuredMedia.cacheKey || null,
|
||||
matchedAt: Date.now()
|
||||
}
|
||||
: entry.movieMatch
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
upsertInfoFile(entry.savePath, infoUpdate);
|
||||
broadcastFileUpdate(rootFolder);
|
||||
|
||||
// Torrent tamamlandığında disk space bilgisini güncelle
|
||||
broadcastDiskSpace();
|
||||
|
||||
// Medya tespiti tamamlandığında özel bildirim gönder
|
||||
if (Object.keys(seriesEpisodes).length > 0 || infoUpdate.primaryMediaInfo) {
|
||||
if (wss) {
|
||||
const data = JSON.stringify({
|
||||
type: "mediaDetected",
|
||||
rootFolder,
|
||||
hasSeriesEpisodes: Object.keys(seriesEpisodes).length > 0,
|
||||
hasMovieMatch: !!infoUpdate.primaryMediaInfo
|
||||
});
|
||||
wss.clients.forEach((c) => c.readyState === 1 && c.send(data));
|
||||
}
|
||||
}
|
||||
|
||||
broadcastSnapshot();
|
||||
}
|
||||
|
||||
// Auth router ve middleware createAuth ile yüklendi
|
||||
// --- Güvenli medya URL'i (TV için) ---
|
||||
// Dönen URL segmentleri ayrı ayrı encode eder, slash'ları korur ve tam hostlu URL döner
|
||||
app.get("/api/media-url", requireAuth, (req, res) => {
|
||||
@@ -3299,9 +3565,7 @@ app.get("/api/media-url", requireAuth, (req, res) => {
|
||||
const ttl = Math.min(Math.max(Number(req.query.ttl) || 3600, 60), 72 * 3600);
|
||||
|
||||
// Medya token oluştur
|
||||
const mediaToken = crypto.randomBytes(16).toString("hex");
|
||||
activeTokens.add(mediaToken);
|
||||
setTimeout(() => activeTokens.delete(mediaToken), ttl * 1000);
|
||||
const mediaToken = issueMediaToken(filePath, ttl);
|
||||
|
||||
// Her path segmentini ayrı encode et (slash korunur)
|
||||
const encodedPath = String(filePath)
|
||||
@@ -3336,243 +3600,10 @@ app.post("/api/transfer", requireAuth, upload.single("torrent"), (req, res) => {
|
||||
// 🆕 Torrent eklendiği anda tarih kaydedelim
|
||||
const added = Date.now();
|
||||
|
||||
torrents.set(torrent.infoHash, {
|
||||
torrent,
|
||||
selectedIndex: 0,
|
||||
wireTorrent(torrent, {
|
||||
savePath,
|
||||
added,
|
||||
paused: false
|
||||
});
|
||||
|
||||
// --- Metadata geldiğinde ---
|
||||
torrent.on("ready", () => {
|
||||
const selectedIndex = pickBestVideoFile(torrent);
|
||||
torrents.set(torrent.infoHash, {
|
||||
torrent,
|
||||
selectedIndex,
|
||||
savePath,
|
||||
added,
|
||||
paused: false
|
||||
});
|
||||
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", async () => {
|
||||
const entry = torrents.get(torrent.infoHash);
|
||||
if (!entry) return;
|
||||
|
||||
console.log(`✅ Torrent tamamlandı: ${torrent.name}`);
|
||||
|
||||
const rootFolder = path.basename(entry.savePath);
|
||||
|
||||
const bestVideoIndex = pickBestVideoFile(torrent);
|
||||
const bestVideo =
|
||||
torrent.files[bestVideoIndex] || torrent.files[0] || null;
|
||||
const displayName = bestVideo?.name || torrent.name || rootFolder;
|
||||
const bestVideoPath = bestVideo?.path
|
||||
? bestVideo.path.replace(/\\/g, "/")
|
||||
: null;
|
||||
|
||||
const perFileMetadata = {};
|
||||
const seriesEpisodes = {};
|
||||
let primaryMediaInfo = null;
|
||||
|
||||
for (const file of torrent.files) {
|
||||
const fullPath = path.join(entry.savePath, file.path);
|
||||
const relPathWithRoot = path.join(rootFolder, file.path);
|
||||
const normalizedRelPath = file.path.replace(/\\/g, "/");
|
||||
const mimeType = mime.lookup(fullPath) || "";
|
||||
const ext = path.extname(file.name).replace(/^\./, "").toLowerCase();
|
||||
|
||||
if (mimeType.startsWith("video/")) {
|
||||
queueVideoThumbnail(fullPath, relPathWithRoot);
|
||||
} else if (mimeType.startsWith("image/")) {
|
||||
queueImageThumbnail(fullPath, relPathWithRoot);
|
||||
}
|
||||
|
||||
let metaInfo = null;
|
||||
if (
|
||||
mimeType.startsWith("video/") ||
|
||||
mimeType.startsWith("audio/") ||
|
||||
mimeType.startsWith("image/")
|
||||
) {
|
||||
metaInfo = await extractMediaInfo(fullPath);
|
||||
}
|
||||
|
||||
if (
|
||||
!primaryMediaInfo &&
|
||||
bestVideoPath &&
|
||||
normalizedRelPath === bestVideoPath &&
|
||||
metaInfo
|
||||
) {
|
||||
primaryMediaInfo = metaInfo;
|
||||
}
|
||||
|
||||
perFileMetadata[normalizedRelPath] = {
|
||||
size: file.length,
|
||||
extension: ext || null,
|
||||
mimeType,
|
||||
mediaInfo: metaInfo
|
||||
};
|
||||
|
||||
const seriesInfo = parseSeriesInfo(file.name);
|
||||
if (seriesInfo) {
|
||||
try {
|
||||
const ensured = await ensureSeriesData(
|
||||
rootFolder,
|
||||
normalizedRelPath,
|
||||
seriesInfo,
|
||||
metaInfo
|
||||
);
|
||||
if (ensured?.show && ensured?.episode) {
|
||||
seriesEpisodes[normalizedRelPath] = {
|
||||
season: seriesInfo.season,
|
||||
episode: seriesInfo.episode,
|
||||
key: seriesInfo.key,
|
||||
title: ensured.episode.title || seriesInfo.title,
|
||||
showId: ensured.show.id || null,
|
||||
showTitle: ensured.show.title || seriesInfo.title,
|
||||
seasonName:
|
||||
ensured.season?.name || `Season ${seriesInfo.season}`,
|
||||
seasonId: ensured.season?.tvdbSeasonId || null,
|
||||
seasonPoster: ensured.season?.poster || null,
|
||||
overview: ensured.episode.overview || "",
|
||||
aired: ensured.episode.aired || null,
|
||||
runtime: ensured.episode.runtime || null,
|
||||
still: ensured.episode.still || null,
|
||||
episodeId: ensured.episode.tvdbEpisodeId || null,
|
||||
slug: ensured.episode.slug || null
|
||||
};
|
||||
const fileEntry = perFileMetadata[normalizedRelPath] || {};
|
||||
perFileMetadata[normalizedRelPath] = {
|
||||
...fileEntry,
|
||||
seriesMatch: {
|
||||
id: ensured.show.id || null,
|
||||
title: ensured.show.title || seriesInfo.title,
|
||||
season: ensured.season?.seasonNumber ?? seriesInfo.season,
|
||||
episode: ensured.episode.episodeNumber ?? seriesInfo.episode,
|
||||
code: ensured.episode.code || seriesInfo.key,
|
||||
poster: ensured.show.poster || null,
|
||||
backdrop: ensured.show.backdrop || null,
|
||||
seasonPoster: ensured.season?.poster || null,
|
||||
aired: ensured.episode.aired || null,
|
||||
runtime: ensured.episode.runtime || null,
|
||||
tvdbEpisodeId: ensured.episode.tvdbEpisodeId || null,
|
||||
matchedAt: Date.now()
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`⚠️ TV metadata oluşturulamadı (${rootFolder} - ${file.name}): ${
|
||||
err?.message || err
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
const infoUpdate = {
|
||||
completedAt: Date.now(),
|
||||
totalBytes: torrent.downloaded,
|
||||
fileCount: torrent.files.length,
|
||||
files: perFileMetadata
|
||||
};
|
||||
if (bestVideoPath) infoUpdate.primaryVideoPath = bestVideoPath;
|
||||
if (Object.keys(seriesEpisodes).length) {
|
||||
infoUpdate.seriesEpisodes = seriesEpisodes;
|
||||
}
|
||||
|
||||
const ensuredMedia = await ensureMovieData(
|
||||
rootFolder,
|
||||
displayName,
|
||||
bestVideoPath,
|
||||
primaryMediaInfo
|
||||
);
|
||||
if (ensuredMedia?.mediaInfo) {
|
||||
infoUpdate.primaryMediaInfo = ensuredMedia.mediaInfo;
|
||||
if (!infoUpdate.files) infoUpdate.files = perFileMetadata;
|
||||
if (bestVideoPath) {
|
||||
const entry = infoUpdate.files[bestVideoPath] || {};
|
||||
infoUpdate.files[bestVideoPath] = {
|
||||
...entry,
|
||||
movieMatch: ensuredMedia.metadata
|
||||
? {
|
||||
id: ensuredMedia.metadata.id ?? null,
|
||||
title:
|
||||
ensuredMedia.metadata.title ||
|
||||
ensuredMedia.metadata.matched_title ||
|
||||
displayName,
|
||||
year: ensuredMedia.metadata.release_date
|
||||
? Number(
|
||||
ensuredMedia.metadata.release_date.slice(0, 4)
|
||||
)
|
||||
: ensuredMedia.metadata.matched_year || null,
|
||||
poster: ensuredMedia.metadata.poster_path || null,
|
||||
backdrop: ensuredMedia.metadata.backdrop_path || null,
|
||||
cacheKey: ensuredMedia.cacheKey || null,
|
||||
matchedAt: Date.now()
|
||||
}
|
||||
: entry.movieMatch
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
upsertInfoFile(entry.savePath, infoUpdate);
|
||||
broadcastFileUpdate(rootFolder);
|
||||
|
||||
// Torrent tamamlandığında disk space bilgisini güncelle
|
||||
broadcastDiskSpace();
|
||||
|
||||
// Medya tespiti tamamlandığında özel bildirim gönder
|
||||
if (Object.keys(seriesEpisodes).length > 0 || infoUpdate.primaryMediaInfo) {
|
||||
if (wss) {
|
||||
const data = JSON.stringify({
|
||||
type: "mediaDetected",
|
||||
rootFolder,
|
||||
hasSeriesEpisodes: Object.keys(seriesEpisodes).length > 0,
|
||||
hasMovieMatch: !!infoUpdate.primaryMediaInfo
|
||||
});
|
||||
wss.clients.forEach((c) => c.readyState === 1 && c.send(data));
|
||||
}
|
||||
}
|
||||
|
||||
broadcastSnapshot();
|
||||
respond: (payload) => res.json(payload)
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
@@ -5525,6 +5556,16 @@ app.get("/stream/:hash", requireAuth, (req, res) => {
|
||||
|
||||
console.log("🗄️ Download path:", DOWNLOAD_DIR);
|
||||
|
||||
// Sunucu açılışında mevcut torrentleri yeniden ekle
|
||||
const restored = restoreTorrentsFromDisk({
|
||||
downloadDir: DOWNLOAD_DIR,
|
||||
client,
|
||||
register: (torrent, ctx) => wireTorrent(torrent, ctx)
|
||||
});
|
||||
if (restored.length) {
|
||||
console.log(`♻️ ${restored.length} torrent yeniden eklendi.`);
|
||||
}
|
||||
|
||||
|
||||
// --- ✅ Client build (frontend) dosyalarını sun ---
|
||||
const publicDir = path.join(__dirname, "public");
|
||||
@@ -5540,15 +5581,11 @@ const server = app.listen(PORT, () =>
|
||||
console.log(`🐔 du.pe server ${PORT} portunda çalışıyor`)
|
||||
);
|
||||
|
||||
wss = new WebSocketServer({ server });
|
||||
wss = createWebsocketServer(server, { verifyToken });
|
||||
wss.on("connection", (ws) => {
|
||||
ws.send(JSON.stringify({ type: "progress", torrents: snapshot() }));
|
||||
// Bağlantı kurulduğunda disk space bilgisi gönder
|
||||
broadcastDiskSpace();
|
||||
|
||||
ws.on("error", (error) => {
|
||||
console.error("🔌 WebSocket error:", error);
|
||||
});
|
||||
});
|
||||
|
||||
// --- ⏱️ Her 2 saniyede bir aktif torrent durumu yayınla ---
|
||||
|
||||
Reference in New Issue
Block a user