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

@@ -1,6 +1,6 @@
<script>
import { onMount, tick } from "svelte";
import { API, apiFetch, renameFolder } from "../utils/api.js";
import { API, apiFetch, moveEntry, renameFolder } from "../utils/api.js";
import { cleanFileName, extractTitleAndYear } from "../utils/filename.js";
import { refreshMovieCount } from "../stores/movieStore.js";
import { refreshTvShowCount } from "../stores/tvStore.js";
@@ -617,6 +617,23 @@
selectedItems = new Set();
}
function resolveEntryOriginalPath(entry) {
if (!entry) return "";
if (entry.isDirectory) {
const original =
entry.primaryOriginalPath ||
(Array.isArray(entry.originalPaths) ? entry.originalPaths[0] : null) ||
resolveOriginalPathForDisplay(entry.displayPath, currentOriginalPath);
return normalizePath(original);
}
return normalizePath(entry?.name);
}
function clearDragState() {
draggingItem = null;
dragOverItem = null;
}
function handleDragStart(entry, event) {
draggingItem = entry;
dragOverItem = null;
@@ -646,23 +663,30 @@
}
}
function handleDrop(entry, event) {
async function handleDrop(entry, event) {
if (!draggingItem) return;
if (normalizePath(currentPath) !== lastDragPath) {
draggingItem = null;
dragOverItem = null;
clearDragState();
return;
}
event.preventDefault();
event.stopPropagation();
reorderEntries(draggingItem, entry);
draggingItem = null;
dragOverItem = null;
const source = draggingItem;
clearDragState();
if (!source || !entry) return;
if (entry.isDirectory) {
if (source.name === entry.name) return;
await moveEntryToDirectory(source, entry);
return;
}
reorderEntries(source, entry);
}
function handleDragEnd() {
draggingItem = null;
dragOverItem = null;
clearDragState();
}
function handleContainerDragOver(event) {
@@ -674,8 +698,7 @@
function handleContainerDrop(event) {
if (!draggingItem) return;
if (normalizePath(currentPath) !== lastDragPath) {
draggingItem = null;
dragOverItem = null;
clearDragState();
return;
}
event.preventDefault();
@@ -687,8 +710,7 @@
filtered.push(draggingItem.name);
customOrder.set(key, filtered);
applyOrdering(currentPath);
draggingItem = null;
dragOverItem = null;
clearDragState();
}
function reorderEntries(source, target) {
@@ -707,6 +729,51 @@
applyOrdering(currentPath);
}
async function moveEntryToDirectory(source, target) {
const sourcePath = resolveEntryOriginalPath(source);
const targetPath = resolveEntryOriginalPath(target);
if (!sourcePath || !targetPath) return;
const normalizedSource = normalizePath(sourcePath);
const normalizedTarget = normalizePath(targetPath);
if (!normalizedSource || !normalizedTarget) return;
if (source?.isDirectory) {
if (
normalizedTarget === normalizedSource ||
normalizedTarget.startsWith(`${normalizedSource}/`)
) {
alert("Bir klasörü kendi içine taşıyamazsın.");
return;
}
}
const parentOfSource = normalizedSource.split("/").slice(0, -1).join("/");
if (parentOfSource === normalizedTarget) {
return;
}
try {
const result = await moveEntry(normalizedSource, normalizedTarget);
if (!result?.success) {
const message =
result?.error || "Öğe taşınırken bir hata oluştu.";
alert(message);
return;
}
if (!result?.unchanged) {
await loadFiles();
}
selectedItems = new Set();
} catch (err) {
console.error("❌ Taşıma hatası:", err);
alert("Öğe taşınamadı. Lütfen tekrar dene.");
}
}
function updateUrlPath(
path,
originalPath = currentOriginalPath,

View File

@@ -11,6 +11,7 @@
let movies = [];
let loading = true;
let refreshing = false;
let rescanning = false;
let error = null;
let selectedMovie = null;
let selectedRuntime = null;
@@ -342,7 +343,11 @@ async function loadMovies() {
async function refreshMovies() {
try {
refreshing = true;
await apiFetch("/api/movies/refresh", { method: "POST" });
const resp = await apiFetch("/api/movies/refresh", { method: "POST" });
if (!resp.ok) {
const data = await resp.json().catch(() => ({}));
throw new Error(data?.error || `HTTP ${resp.status}`);
}
await loadMovies();
} catch (err) {
console.error("Movies refresh error:", err);
@@ -352,6 +357,23 @@ async function loadMovies() {
}
}
async function rescanMovies() {
try {
rescanning = true;
const resp = await apiFetch("/api/movies/rescan", { method: "POST" });
if (!resp.ok) {
const data = await resp.json().catch(() => ({}));
throw new Error(data?.error || `HTTP ${resp.status}`);
}
await loadMovies();
} catch (err) {
console.error("Movies rescan error:", err);
error = err?.message || "Tam tarama sırasında bir sorun oluştu.";
} finally {
rescanning = false;
}
}
function openMovie(movie) {
selectedMovie = movie;
}
@@ -393,14 +415,23 @@ async function loadMovies() {
<div class="section-accent"></div>
<div class="movies-header">
<h2>Movies</h2>
<div class="header-actions">
<button
class="refresh-btn"
on:click={rescanMovies}
disabled={loading || refreshing || rescanning}
>
{rescanning ? "Rebuilding…" : "Full Rescan"}
</button>
<button
class="refresh-btn"
on:click={refreshMovies}
disabled={loading || refreshing}
disabled={loading || refreshing || rescanning}
>
{refreshing ? "Refreshing…" : "Refresh Metadata"}
</button>
</div>
</div>
{#if loading}
<div class="state-placeholder">Loading movies…</div>
@@ -655,6 +686,11 @@ async function loadMovies() {
justify-content: space-between;
}
.header-actions {
display: flex;
gap: 10px;
}
.section-accent {
height: 2px;
width: calc(100% + 52px);

View File

@@ -11,6 +11,7 @@
let shows = [];
let loading = true;
let refreshing = false;
let rescanning = false;
let error = null;
let selectedShow = null;
@@ -219,7 +220,11 @@ let filteredShows = [];
async function refreshShows() {
try {
refreshing = true;
await apiFetch("/api/tvshows/refresh", { method: "POST" });
const resp = await apiFetch("/api/tvshows/refresh", { method: "POST" });
if (!resp.ok) {
const data = await resp.json().catch(() => ({}));
throw new Error(data?.error || `HTTP ${resp.status}`);
}
await loadShows();
} catch (err) {
console.error("TV metadata refresh error:", err);
@@ -229,6 +234,23 @@ let filteredShows = [];
}
}
async function rescanShows() {
try {
rescanning = true;
const resp = await apiFetch("/api/tvshows/rescan", { method: "POST" });
if (!resp.ok) {
const data = await resp.json().catch(() => ({}));
throw new Error(data?.error || `HTTP ${resp.status}`);
}
await loadShows();
} catch (err) {
console.error("TV metadata rescan error:", err);
error = err?.message || "Tam tarama sırasında bir sorun oluştu.";
} finally {
rescanning = false;
}
}
function openShow(show) {
if (!show) return;
selectedShow = show;
@@ -611,14 +633,23 @@ async function openVideoAtIndex(index) {
<div class="section-accent"></div>
<div class="tv-header">
<h2>Tv Shows</h2>
<div class="header-actions">
<button
class="refresh-btn"
disabled={loading || refreshing}
disabled={loading || refreshing || rescanning}
on:click={rescanShows}
>
{rescanning ? "Rebuilding…" : "Full Rescan"}
</button>
<button
class="refresh-btn"
disabled={loading || refreshing || rescanning}
on:click={refreshShows}
>
{refreshing ? "Refreshing…" : "Refresh Metadata"}
</button>
</div>
</div>
{#if loading}
<div class="state-placeholder">Loading shows…</div>
@@ -1019,6 +1050,11 @@ async function openVideoAtIndex(index) {
justify-content: space-between;
}
.header-actions {
display: flex;
gap: 10px;
}
.tv-header h2 {
font-size: 26px;
margin: 0;

View File

@@ -44,6 +44,18 @@ export async function deleteFromTrash(trashName) {
return res.json();
}
export async function moveEntry(sourcePath, targetDirectory) {
const res = await apiFetch("/api/file/move", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
sourcePath,
targetDirectory
})
});
return res.json();
}
export async function renameFolder(path, newName) {
const res = await apiFetch("/api/folder", {
method: "PATCH",

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.");
}
if (clearCache && fs.existsSync(TV_DATA_ROOT)) {
try {
const folders = fs
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}`
);
}
}
fs.mkdirSync(TV_DATA_ROOT, { recursive: true });
if (clearCache) {
tvdbSeriesCache.clear();
tvdbEpisodeCache.clear();
tvdbEpisodeDetailCache.clear();
}
const dirEntries = fs
.readdirSync(DOWNLOAD_DIR, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name);
.filter((d) => d.isDirectory());
const processed = [];
for (const folder of folders) {
const safeFolder = sanitizeRelative(folder);
if (!safeFolder) continue;
const rootDir = path.join(DOWNLOAD_DIR, safeFolder);
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;
const info = readInfoForRoot(safeFolder) || {};
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);