Dosya taşıma ve "Full Rescan" özelliği eklendi.

This commit is contained in:
2025-11-02 20:48:39 +03:00
parent 3e07e2a270
commit e5c0e8626e
5 changed files with 665 additions and 99 deletions

View File

@@ -2370,6 +2370,125 @@ function pruneInfoForDirectory(rootFolder, relativeDir) {
}
}
function writeInfoForRoot(rootFolder, info) {
if (!rootFolder || !info) return;
const safe = sanitizeRelative(rootFolder);
if (!safe) return;
const target = path.join(DOWNLOAD_DIR, safe, INFO_FILENAME);
try {
info.updatedAt = Date.now();
fs.writeFileSync(target, JSON.stringify(info, null, 2), "utf-8");
} catch (err) {
console.warn(`⚠️ info.json güncellenemedi (${target}): ${err.message}`);
}
}
function moveInfoDataBetweenRoots(oldRoot, newRoot, oldRel, newRel, isDirectory) {
if (!oldRoot || !newRoot) return false;
const oldInfo = readInfoForRoot(oldRoot);
if (!oldInfo) return false;
let newInfo = readInfoForRoot(newRoot);
if (!newInfo) {
const rootDir = path.join(DOWNLOAD_DIR, sanitizeRelative(newRoot));
newInfo =
upsertInfoFile(rootDir, {
folder: newRoot,
createdAt: Date.now(),
updatedAt: Date.now()
}) || readInfoForRoot(newRoot) || { folder: newRoot };
}
const normalizedOldRel = normalizeTrashPath(oldRel);
const normalizedNewRel = normalizeTrashPath(newRel);
const shouldMove = (normalizedKey) => {
if (!normalizedOldRel) return true;
if (normalizedKey === normalizedOldRel) return true;
return (
isDirectory &&
normalizedOldRel &&
normalizedKey.startsWith(`${normalizedOldRel}/`)
);
};
const mapKey = (normalizedKey) => {
const suffix = normalizedOldRel
? normalizedKey.slice(normalizedOldRel.length).replace(/^\/+/, "")
: normalizedKey;
if (!normalizedNewRel) return suffix;
return `${normalizedNewRel}${suffix ? `/${suffix}` : ""}`;
};
let moved = false;
if (oldInfo.files && typeof oldInfo.files === "object") {
const remaining = {};
for (const [key, value] of Object.entries(oldInfo.files)) {
const normalizedKey = normalizeTrashPath(key);
if (!shouldMove(normalizedKey)) {
remaining[key] = value;
continue;
}
const nextKey = mapKey(normalizedKey);
if (!newInfo.files || typeof newInfo.files !== "object") {
newInfo.files = {};
}
newInfo.files[nextKey] = value;
moved = true;
}
if (Object.keys(remaining).length > 0) oldInfo.files = remaining;
else delete oldInfo.files;
}
if (oldInfo.seriesEpisodes && typeof oldInfo.seriesEpisodes === "object") {
const remainingEpisodes = {};
for (const [key, value] of Object.entries(oldInfo.seriesEpisodes)) {
const normalizedKey = normalizeTrashPath(key);
if (!shouldMove(normalizedKey)) {
remainingEpisodes[key] = value;
continue;
}
const nextKey = mapKey(normalizedKey);
if (
!newInfo.seriesEpisodes ||
typeof newInfo.seriesEpisodes !== "object"
) {
newInfo.seriesEpisodes = {};
}
newInfo.seriesEpisodes[nextKey] = value;
moved = true;
}
if (Object.keys(remainingEpisodes).length > 0) {
oldInfo.seriesEpisodes = remainingEpisodes;
} else {
delete oldInfo.seriesEpisodes;
}
}
if (oldInfo.primaryVideoPath) {
const normalizedPrimary = normalizeTrashPath(oldInfo.primaryVideoPath);
if (shouldMove(normalizedPrimary)) {
const nextPrimary = mapKey(normalizedPrimary);
newInfo.primaryVideoPath = nextPrimary;
if (oldInfo.primaryMediaInfo !== undefined) {
newInfo.primaryMediaInfo = oldInfo.primaryMediaInfo;
delete oldInfo.primaryMediaInfo;
}
if (oldInfo.movieMatch !== undefined) {
newInfo.movieMatch = oldInfo.movieMatch;
delete oldInfo.movieMatch;
}
delete oldInfo.primaryVideoPath;
moved = true;
}
}
if (!moved) return false;
writeInfoForRoot(oldRoot, oldInfo);
writeInfoForRoot(newRoot, newInfo);
return true;
}
function renameInfoPaths(rootFolder, oldRel, newRel) {
if (!rootFolder) return;
const info = readInfoForRoot(rootFolder);
@@ -3405,6 +3524,149 @@ app.delete("/api/file", requireAuth, (req, res) => {
}
});
// --- 🚚 Dosya veya klasörü hedef klasöre taşıma ---
app.post("/api/file/move", requireAuth, (req, res) => {
try {
const { sourcePath, targetDirectory } = req.body || {};
if (!sourcePath) {
return res.status(400).json({ error: "sourcePath gerekli" });
}
const normalizedSource = normalizeTrashPath(sourcePath);
if (!normalizedSource) {
return res.status(400).json({ error: "Geçersiz sourcePath" });
}
const sourceFullPath = path.join(DOWNLOAD_DIR, normalizedSource);
if (!fs.existsSync(sourceFullPath)) {
return res.status(404).json({ error: "Kaynak öğe bulunamadı" });
}
const sourceStats = fs.statSync(sourceFullPath);
const isDirectory = sourceStats.isDirectory();
const normalizedTargetDir = targetDirectory
? normalizeTrashPath(targetDirectory)
: "";
if (normalizedTargetDir) {
const targetDirFullPath = path.join(DOWNLOAD_DIR, normalizedTargetDir);
if (!fs.existsSync(targetDirFullPath)) {
return res.status(404).json({ error: "Hedef klasör bulunamadı" });
}
}
const posixPath = path.posix;
const sourceName = posixPath.basename(normalizedSource);
const newRelativePath = normalizedTargetDir
? posixPath.join(normalizedTargetDir, sourceName)
: sourceName;
if (newRelativePath === normalizedSource) {
return res.json({ success: true, unchanged: true });
}
if (
isDirectory &&
(newRelativePath === normalizedSource ||
newRelativePath.startsWith(`${normalizedSource}/`))
) {
return res
.status(400)
.json({ error: "Bir klasörü kendi içine taşıyamazsın." });
}
const newFullPath = path.join(DOWNLOAD_DIR, newRelativePath);
if (fs.existsSync(newFullPath)) {
return res
.status(409)
.json({ error: "Hedef konumda aynı isimde bir öğe zaten var" });
}
const destinationParent = path.dirname(newFullPath);
if (!fs.existsSync(destinationParent)) {
fs.mkdirSync(destinationParent, { recursive: true });
}
const sourceRoot = rootFromRelPath(normalizedSource);
const destRoot = rootFromRelPath(newRelativePath);
const sourceSegments = relPathToSegments(normalizedSource);
const destSegments = relPathToSegments(newRelativePath);
const sourceRelWithinRoot = sourceSegments.slice(1).join("/");
const destRelWithinRoot = destSegments.slice(1).join("/");
if (sourceRoot && !sourceRelWithinRoot) {
return res
.status(400)
.json({ error: "Kök klasör bu yöntemle taşınamaz" });
}
fs.renameSync(sourceFullPath, newFullPath);
const sameRoot =
sourceRoot && destRoot && sourceRoot === destRoot && sourceRoot !== null;
const movedAcrossRoots =
sourceRoot && destRoot && sourceRoot !== destRoot && sourceRoot !== null;
if (sameRoot) {
renameInfoPaths(sourceRoot, sourceRelWithinRoot, destRelWithinRoot);
renameSeriesDataPaths(sourceRoot, sourceRelWithinRoot, destRelWithinRoot);
renameTrashEntries(sourceRoot, sourceRelWithinRoot, destRelWithinRoot);
if (isDirectory) {
removeThumbnailsForDirectory(sourceRoot, sourceRelWithinRoot);
} else {
removeThumbnailsForPath(normalizedSource);
}
trashStateCache.delete(sourceRoot);
} else {
if (movedAcrossRoots) {
moveInfoDataBetweenRoots(
sourceRoot,
destRoot,
sourceRelWithinRoot,
destRelWithinRoot,
isDirectory
);
if (isDirectory) {
removeThumbnailsForDirectory(sourceRoot, sourceRelWithinRoot);
} else {
removeThumbnailsForPath(normalizedSource);
}
if (sourceRoot) trashStateCache.delete(sourceRoot);
if (destRoot) trashStateCache.delete(destRoot);
} else if (sourceRoot) {
if (isDirectory) {
pruneInfoForDirectory(sourceRoot, sourceRelWithinRoot);
removeThumbnailsForDirectory(sourceRoot, sourceRelWithinRoot);
} else {
pruneInfoEntry(sourceRoot, sourceRelWithinRoot);
removeThumbnailsForPath(normalizedSource);
}
trashStateCache.delete(sourceRoot);
}
}
if (sourceRoot) {
broadcastFileUpdate(sourceRoot);
}
if (destRoot && destRoot !== sourceRoot) {
broadcastFileUpdate(destRoot);
}
res.json({
success: true,
newPath: newRelativePath,
rootFolder: destRoot || null,
isDirectory,
movedAcrossRoots: Boolean(movedAcrossRoots)
});
} catch (err) {
console.error("❌ File move error:", err);
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 ---
@@ -3859,41 +4121,117 @@ app.get("/api/movies", requireAuth, (req, res) => {
}
});
async function rebuildMovieMetadata({ clearCache = false } = {}) {
if (!TMDB_API_KEY) {
throw new Error("TMDB API key tanımlı değil.");
}
if (clearCache && fs.existsSync(MOVIE_DATA_ROOT)) {
try {
fs.rmSync(MOVIE_DATA_ROOT, { recursive: true, force: true });
console.log("🧹 Movie cache temizlendi.");
} catch (err) {
console.warn(
`⚠️ Movie cache temizlenemedi (${MOVIE_DATA_ROOT}): ${err.message}`
);
}
}
fs.mkdirSync(MOVIE_DATA_ROOT, { recursive: true });
const dirEntries = fs
.readdirSync(DOWNLOAD_DIR, { withFileTypes: true })
.filter((d) => d.isDirectory());
const processed = [];
for (const dirent of dirEntries) {
const folder = sanitizeRelative(dirent.name);
if (!folder) continue;
const rootDir = path.join(DOWNLOAD_DIR, folder);
if (!fs.existsSync(rootDir)) continue;
try {
const info = readInfoForRoot(folder) || {};
const displayName = info?.name || dirent.name || folder;
const normalizePath = (value) =>
value ? String(value).replace(/\\/g, "/") : value;
let primaryVideo = normalizePath(info?.primaryVideoPath || null);
if (primaryVideo) {
const absPrimary = path.join(rootDir, primaryVideo);
if (!fs.existsSync(absPrimary)) {
primaryVideo = null;
}
}
if (!primaryVideo) {
primaryVideo = normalizePath(guessPrimaryVideo(folder));
}
if (!primaryVideo) {
removeMovieData(folder);
if (clearCache) {
upsertInfoFile(rootDir, {
primaryVideoPath: null,
primaryMediaInfo: null
});
}
console.log(
` Movie taraması atlandı (video bulunamadı): ${folder}`
);
processed.push(folder);
continue;
}
let mediaInfo =
info?.files?.[primaryVideo]?.mediaInfo || info?.primaryMediaInfo || null;
if (!mediaInfo) {
const absVideo = path.join(rootDir, primaryVideo);
if (fs.existsSync(absVideo)) {
try {
mediaInfo = await extractMediaInfo(absVideo);
} catch (err) {
console.warn(
`⚠️ Media info alınamadı (${absVideo}): ${err?.message || err}`
);
}
}
}
const ensured = await ensureMovieData(
folder,
displayName,
primaryVideo,
mediaInfo
);
const update = {
primaryVideoPath: primaryVideo,
primaryMediaInfo: ensured || mediaInfo || null
};
upsertInfoFile(rootDir, update);
processed.push(folder);
} catch (err) {
console.error(
`❌ Movie metadata yeniden oluşturulamadı (${folder}):`,
err?.message || err
);
}
}
return processed;
}
app.post("/api/movies/refresh", requireAuth, async (req, res) => {
if (!TMDB_API_KEY) {
return res.status(400).json({ error: "TMDB API key tanımlı değil." });
}
try {
const folders = fs
.readdirSync(DOWNLOAD_DIR, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name);
const processed = [];
for (const folder of folders) {
const info = readInfoForRoot(folder);
const displayName = info?.name || folder;
const primaryVideo = info?.primaryVideoPath || guessPrimaryVideo(folder);
const candidateMedia =
info?.files?.[primaryVideo]?.mediaInfo || info?.primaryMediaInfo || null;
const ensured = await ensureMovieData(
folder,
displayName,
primaryVideo,
candidateMedia
);
if (primaryVideo || ensured) {
const update = {};
if (primaryVideo) update.primaryVideoPath = primaryVideo;
if (ensured) update.primaryMediaInfo = ensured;
if (Object.keys(update).length) {
upsertInfoFile(path.join(DOWNLOAD_DIR, folder), update);
}
}
processed.push(folder);
}
const processed = await rebuildMovieMetadata();
res.json({ ok: true, processed });
} catch (err) {
console.error("🎬 Movies refresh error:", err);
@@ -3901,6 +4239,20 @@ app.post("/api/movies/refresh", requireAuth, async (req, res) => {
}
});
app.post("/api/movies/rescan", requireAuth, async (req, res) => {
if (!TMDB_API_KEY) {
return res.status(400).json({ error: "TMDB API key tanımlı değil." });
}
try {
const processed = await rebuildMovieMetadata({ clearCache: true });
res.json({ ok: true, processed });
} catch (err) {
console.error("🎬 Movies rescan error:", err);
res.status(500).json({ error: err.message });
}
});
// --- 📺 TV dizileri listesi ---
app.get("/api/tvshows", requireAuth, (req, res) => {
try {
@@ -4283,33 +4635,58 @@ app.get("/api/tvshows", requireAuth, (req, res) => {
}
});
app.post("/api/tvshows/refresh", requireAuth, async (req, res) => {
async function rebuildTvMetadata({ clearCache = false } = {}) {
if (!TVDB_API_KEY) {
return res
.status(400)
.json({ error: "TVDB API erişimi için gerekli anahtar tanımlı değil." });
throw new Error("TVDB API erişimi için gerekli anahtar tanımlı değil.");
}
try {
const folders = fs
.readdirSync(DOWNLOAD_DIR, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name);
if (clearCache && fs.existsSync(TV_DATA_ROOT)) {
try {
fs.rmSync(TV_DATA_ROOT, { recursive: true, force: true });
console.log("🧹 TV cache temizlendi.");
} catch (err) {
console.warn(
`⚠️ TV cache temizlenemedi (${TV_DATA_ROOT}): ${err.message}`
);
}
}
const processed = [];
fs.mkdirSync(TV_DATA_ROOT, { recursive: true });
for (const folder of folders) {
const safeFolder = sanitizeRelative(folder);
if (!safeFolder) continue;
const rootDir = path.join(DOWNLOAD_DIR, safeFolder);
if (!fs.existsSync(rootDir)) continue;
if (clearCache) {
tvdbSeriesCache.clear();
tvdbEpisodeCache.clear();
tvdbEpisodeDetailCache.clear();
}
const info = readInfoForRoot(safeFolder) || {};
const dirEntries = fs
.readdirSync(DOWNLOAD_DIR, { withFileTypes: true })
.filter((d) => d.isDirectory());
const processed = [];
for (const dirent of dirEntries) {
const folder = sanitizeRelative(dirent.name);
if (!folder) continue;
const rootDir = path.join(DOWNLOAD_DIR, folder);
if (!fs.existsSync(rootDir)) continue;
try {
const info = readInfoForRoot(folder) || {};
const infoFiles = info.files || {};
const detected = {};
const walkDir = async (currentDir, relativeBase = "") => {
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
let entries = [];
try {
entries = fs.readdirSync(currentDir, { withFileTypes: true });
} catch (err) {
console.warn(
`⚠️ Klasör okunamadı (${currentDir}): ${err.message}`
);
return;
}
for (const entry of entries) {
const relPath = relativeBase
? `${relativeBase}/${entry.name}`
@@ -4321,6 +4698,7 @@ app.post("/api/tvshows/refresh", requireAuth, async (req, res) => {
continue;
}
if (!entry.isFile()) continue;
if (entry.name.toLowerCase() === INFO_FILENAME) continue;
const ext = path.extname(entry.name).toLowerCase();
if (!VIDEO_EXTS.includes(ext)) continue;
@@ -4343,7 +4721,7 @@ app.post("/api/tvshows/refresh", requireAuth, async (req, res) => {
try {
const ensured = await ensureSeriesData(
safeFolder,
folder,
normalizedRel,
seriesInfo,
mediaInfo
@@ -4370,7 +4748,7 @@ app.post("/api/tvshows/refresh", requireAuth, async (req, res) => {
}
} catch (err) {
console.warn(
`⚠️ TV metadata yenilenemedi (${safeFolder} - ${entry.name}): ${
`⚠️ TV metadata yenilenemedi (${folder} - ${entry.name}): ${
err?.message || err
}`
);
@@ -4380,16 +4758,37 @@ app.post("/api/tvshows/refresh", requireAuth, async (req, res) => {
await walkDir(rootDir);
if (Object.keys(detected).length) {
const episodeCount = Object.keys(detected).length;
if (episodeCount > 0) {
upsertInfoFile(rootDir, { seriesEpisodes: detected });
} else if (clearCache) {
upsertInfoFile(rootDir, { seriesEpisodes: {} });
}
processed.push({
folder: safeFolder,
episodes: Object.keys(detected).length
folder,
episodes: episodeCount
});
} catch (err) {
console.error(
`❌ TV metadata yeniden oluşturulamadı (${folder}):`,
err?.message || err
);
}
}
return processed;
}
app.post("/api/tvshows/refresh", requireAuth, async (req, res) => {
if (!TVDB_API_KEY) {
return res
.status(400)
.json({ error: "TVDB API erişimi için gerekli anahtar tanımlı değil." });
}
try {
const processed = await rebuildTvMetadata();
res.json({ ok: true, processed });
} catch (err) {
console.error("📺 TvShows refresh error:", err);
@@ -4397,6 +4796,22 @@ app.post("/api/tvshows/refresh", requireAuth, async (req, res) => {
}
});
app.post("/api/tvshows/rescan", requireAuth, async (req, res) => {
if (!TVDB_API_KEY) {
return res
.status(400)
.json({ error: "TVDB API erişimi için gerekli anahtar tanımlı değil." });
}
try {
const processed = await rebuildTvMetadata({ clearCache: true });
res.json({ ok: true, processed });
} catch (err) {
console.error("📺 TvShows rescan 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);