diff --git a/server/server.js b/server/server.js index 450c787..e2ee173 100644 --- a/server/server.js +++ b/server/server.js @@ -119,11 +119,13 @@ function enumerateVideoFiles(rootFolder) { const absPath = path.join(currentDir, name); if (entry.isDirectory()) { + if (isPathTrashed(safe, relPath, true)) continue; stack.push(relPath); continue; } if (!entry.isFile()) continue; + if (isPathTrashed(safe, relPath, false)) continue; const ext = path.extname(name).toLowerCase(); if (!VIDEO_EXTS.includes(ext)) continue; @@ -3261,6 +3263,135 @@ function broadcastSnapshot() { wss.clients.forEach((c) => c.readyState === 1 && c.send(data)); } +let mediaRescanTask = null; +let pendingMediaRescan = { movies: false, tv: false }; +let lastMediaRescanReason = "manual"; + +function queueMediaRescan({ movies = false, tv = false, reason = "manual" } = {}) { + if (!movies && !tv) return; + pendingMediaRescan.movies = pendingMediaRescan.movies || movies; + pendingMediaRescan.tv = pendingMediaRescan.tv || tv; + lastMediaRescanReason = reason; + if (!mediaRescanTask) { + mediaRescanTask = runQueuedMediaRescan().finally(() => { + mediaRescanTask = null; + }); + } +} + +async function runQueuedMediaRescan() { + while (pendingMediaRescan.movies || pendingMediaRescan.tv) { + const targets = { ...pendingMediaRescan }; + pendingMediaRescan = { movies: false, tv: false }; + const reason = lastMediaRescanReason; + console.log( + `🔁 Medya taraması tetiklendi (${reason}) -> movies:${targets.movies} tv:${targets.tv}` + ); + try { + if (targets.movies) { + if (TMDB_API_KEY) { + await rebuildMovieMetadata({ clearCache: true }); + } else { + console.warn("⚠️ TMDB anahtarı tanımsız olduğu için film taraması atlandı."); + } + } + + if (targets.tv) { + if (TVDB_API_KEY) { + await rebuildTvMetadata({ clearCache: true }); + } else { + console.warn("⚠️ TVDB anahtarı tanımsız olduğu için dizi taraması atlandı."); + } + } + + if (targets.movies || targets.tv) { + broadcastFileUpdate("media-library"); + } + } catch (err) { + console.error("❌ Medya kütüphanesi taraması başarısız:", err?.message || err); + } + } +} + +function detectMediaFlagsForPath(info, relWithinRoot, isDirectory) { + const flags = { movies: false, tv: false }; + if (!info || typeof info !== "object") return flags; + + const normalized = normalizeTrashPath(relWithinRoot); + const matchesPath = (candidate) => { + const normalizedCandidate = normalizeTrashPath(candidate); + if (!normalized) return true; + if (isDirectory) { + return ( + normalizedCandidate === normalized || + normalizedCandidate.startsWith(`${normalized}/`) + ); + } + return normalizedCandidate === normalized; + }; + + const files = info.files || {}; + for (const [key, meta] of Object.entries(files)) { + if (!meta) continue; + if (!matchesPath(key)) continue; + if (meta.movieMatch) flags.movies = true; + if (meta.seriesMatch) flags.tv = true; + } + + const episodes = info.seriesEpisodes || {}; + for (const [key] of Object.entries(episodes)) { + if (!matchesPath(key)) continue; + flags.tv = true; + break; + } + + if (!normalized || isDirectory) { + if (info.movieMatch || info.primaryVideoPath) { + flags.movies = true; + } + if ( + info.seriesEpisodes && + Object.keys(info.seriesEpisodes).length && + !flags.tv + ) { + flags.tv = true; + } + } + + return flags; +} + +function inferMediaFlagsFromTrashEntry(entry) { + if (!entry) return { movies: false, tv: false }; + if ( + entry.mediaFlags && + typeof entry.mediaFlags === "object" && + ("movies" in entry.mediaFlags || "tv" in entry.mediaFlags) + ) { + return { + movies: Boolean(entry.mediaFlags.movies), + tv: Boolean(entry.mediaFlags.tv) + }; + } + + const normalized = normalizeTrashPath(entry.path || entry.originalPath || ""); + if (!normalized || entry.isDirectory) { + return { movies: true, tv: true }; + } + + const ext = path.extname(normalized).toLowerCase(); + if (!VIDEO_EXTS.includes(ext)) { + return { movies: false, tv: false }; + } + + const base = path.basename(normalized); + const seriesCandidate = parseSeriesInfo(base); + if (seriesCandidate) { + return { movies: false, tv: true }; + } + return { movies: true, tv: false }; +} + // --- Snapshot (thumbnail dahil, tracker + tarih eklendi) --- function snapshot() { return Array.from(torrents.values()).map( @@ -3988,6 +4119,7 @@ app.delete("/api/file", requireAuth, (req, res) => { const fullPath = path.join(DOWNLOAD_DIR, safePath); const folderId = (safePath.split(/[\/]/)[0] || "").trim(); const rootDir = folderId ? path.join(DOWNLOAD_DIR, folderId) : null; + let mediaFlags = { movies: false, tv: false }; let stats = null; try { @@ -4010,6 +4142,16 @@ app.delete("/api/file", requireAuth, (req, res) => { const isDirectory = stats.isDirectory(); const relWithinRoot = safePath.split(/[\\/]/).slice(1).join("/"); let trashEntry = null; + if (folderId && rootDir) { + const infoBeforeDelete = readInfoForRoot(folderId); + mediaFlags = detectMediaFlagsForPath( + infoBeforeDelete, + relWithinRoot, + isDirectory + ); + } else { + mediaFlags = { movies: false, tv: false }; + } if (folderId && rootDir) { trashEntry = addTrashEntry(folderId, { @@ -4019,7 +4161,8 @@ app.delete("/api/file", requireAuth, (req, res) => { deletedAt: Date.now(), type: isDirectory ? "inode/directory" - : mime.lookup(fullPath) || "application/octet-stream" + : mime.lookup(fullPath) || "application/octet-stream", + mediaFlags: { ...mediaFlags } }); if (isDirectory) { @@ -4077,6 +4220,17 @@ app.delete("/api/file", requireAuth, (req, res) => { broadcastSnapshot(); } + if ( + folderId && + (mediaFlags.movies || mediaFlags.tv) + ) { + queueMediaRescan({ + movies: mediaFlags.movies, + tv: mediaFlags.tv, + reason: "trash-add" + }); + } + res.json({ ok: true, filesRemoved: true }); } catch (err) { console.error("❌ Dosya silinemedi:", err.message); @@ -4519,10 +4673,18 @@ app.post("/api/trash/restore", requireAuth, (req, res) => { if (!removed) { return res.status(404).json({ error: "Çöp öğesi bulunamadı" }); } + const mediaFlags = inferMediaFlagsFromTrashEntry(removed); console.log(`♻️ Öğe geri yüklendi: ${safeName}`); broadcastFileUpdate(rootFolder); + if (mediaFlags.movies || mediaFlags.tv) { + queueMediaRescan({ + movies: mediaFlags.movies, + tv: mediaFlags.tv, + reason: "trash-restore" + }); + } res.json({ success: true, @@ -4692,7 +4854,7 @@ app.get("/api/movies", requireAuth, (req, res) => { } }); -async function rebuildMovieMetadata({ clearCache = false } = {}) { +async function rebuildMovieMetadata({ clearCache = false, resetSeriesData = false } = {}) { if (!TMDB_API_KEY) { throw new Error("TMDB API key tanımlı değil."); } @@ -4732,7 +4894,9 @@ async function rebuildMovieMetadata({ clearCache = false } = {}) { if (!videoEntries.length) { removeMovieData(folder); - removeSeriesData(folder); + if (resetSeriesData) { + removeSeriesData(folder); + } const update = { primaryVideoPath: null, primaryMediaInfo: null, @@ -4757,7 +4921,9 @@ async function rebuildMovieMetadata({ clearCache = false } = {}) { } removeMovieData(folder); - removeSeriesData(folder); + if (resetSeriesData) { + removeSeriesData(folder); + } const matches = []; @@ -5346,11 +5512,13 @@ async function rebuildTvMetadata({ clearCache = false } = {}) { const absPath = path.join(currentDir, entry.name); if (entry.isDirectory()) { + if (isPathTrashed(folder, relPath, true)) continue; await walkDir(absPath, relPath); continue; } if (!entry.isFile()) continue; + if (isPathTrashed(folder, relPath, false)) continue; if (entry.name.toLowerCase() === INFO_FILENAME) continue; const ext = path.extname(entry.name).toLowerCase(); if (!VIDEO_EXTS.includes(ext)) continue;