Dosya taşıma ve "Full Rescan" özelliği eklendi.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
509
server/server.js
509
server/server.js
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user