diff --git a/client/src/App.svelte b/client/src/App.svelte
index c386df3..3317a54 100644
--- a/client/src/App.svelte
+++ b/client/src/App.svelte
@@ -12,6 +12,7 @@
import { API } from "./utils/api.js";
import { refreshMovieCount } from "./stores/movieStore.js";
import { refreshTvShowCount } from "./stores/tvStore.js";
+ import { fetchTrashItems } from "./stores/trashStore.js";
const token = localStorage.getItem("token");
@@ -24,7 +25,7 @@
refreshTimer = setTimeout(async () => {
refreshTimer = null;
try {
- await Promise.all([refreshMovieCount(), refreshTvShowCount()]);
+ await Promise.all([refreshMovieCount(), refreshTvShowCount(), fetchTrashItems()]);
} catch (err) {
console.warn("Medya sayacı yenileme başarısız:", err);
}
@@ -45,6 +46,7 @@
if (token) {
refreshMovieCount();
refreshTvShowCount();
+ fetchTrashItems();
const authToken = localStorage.getItem("token");
if (authToken) {
const wsUrl = `${API.replace("http", "ws")}?token=${authToken}`;
diff --git a/client/src/components/Sidebar.svelte b/client/src/components/Sidebar.svelte
index 7753b09..6381239 100644
--- a/client/src/components/Sidebar.svelte
+++ b/client/src/components/Sidebar.svelte
@@ -1,14 +1,16 @@
+
+
+
+
+ {#if isLoading}
+
+ {:else if filteredTrashItems.length === 0}
+
+
+
+ {#if hasSearch}
+ Aramanla eşleşen öğe bulunamadı
+ {:else}
+ Çöp boş
+ {/if}
+
+
+ {:else}
+
+ {#each filteredTrashItems as item (item.trashName)}
+
+ {/each}
+
+ {/if}
+
+
+{#if activeMenu}
+
+{/if}
+
+
diff --git a/client/src/routes/TvShows.svelte b/client/src/routes/TvShows.svelte
index 0044ef7..bcc9999 100644
--- a/client/src/routes/TvShows.svelte
+++ b/client/src/routes/TvShows.svelte
@@ -280,8 +280,8 @@ let filteredShows = [];
const ext = name.split(".").pop()?.toLowerCase() || "";
const inferredType = ext ? `video/${ext}` : "video/mp4";
const size =
+ Number(episode.fileSize) ||
Number(episode.mediaInfo?.format?.size) ||
- Number(episode.mediaInfo?.format?.bit_rate) ||
null;
return {
name,
@@ -300,9 +300,19 @@ let filteredShows = [];
)
.filter(Boolean);
+ const encodePathSegments = (value) =>
+ value
+ ? value
+ .split(/[\\/]/)
+ .map((segment) => encodeURIComponent(segment))
+ .join("/")
+ : "";
+
$: selectedName = selectedVideo?.name ?? "";
- $: encName = selectedName ? encodeURIComponent(selectedName) : "";
- $: downloadHref = encName ? `${API}/downloads/${encName}` : "#";
+ $: encName = encodePathSegments(selectedName);
+ $: downloadHref = encName
+ ? `${API}/downloads/${encName}?token=${localStorage.getItem("token") || ""}`
+ : "#";
$: selectedLabel = selectedVideo?.episode
? `${selectedVideo.show.title} · ${formatEpisodeCode(
selectedVideo.episode
@@ -414,8 +424,9 @@ async function openVideoAtIndex(index) {
function getVideoURL() {
if (!selectedName) return "";
const token = localStorage.getItem("token");
- // selectedName zaten encode edilmiş, tekrar encode etme
- return `${API}/media/${selectedName}?token=${token}`;
+ const encoded = encodePathSegments(selectedName);
+ if (!encoded) return "";
+ return `${API}/media/${encoded}?token=${token}`;
}
function playEpisodeFromCard(episode) {
diff --git a/client/src/stores/trashStore.js b/client/src/stores/trashStore.js
new file mode 100644
index 0000000..24747bc
--- /dev/null
+++ b/client/src/stores/trashStore.js
@@ -0,0 +1,67 @@
+import { writable } from 'svelte/store';
+import { getTrashItems, restoreFromTrash, deleteFromTrash } from '../utils/api';
+
+export const trashItems = writable([]);
+export const trashCount = writable(0);
+
+// Çöp öğelerini API'den al
+export async function fetchTrashItems() {
+ try {
+ const items = await getTrashItems();
+
+ const processedItems = Array.isArray(items)
+ ? items.map((item) => {
+ const segments = String(item.name || "")
+ .split(/[\\/]/)
+ .filter(Boolean);
+ const displayName =
+ segments.length > 0 ? segments[segments.length - 1] : item.name;
+ const parentPath =
+ segments.length > 1 ? segments.slice(0, -1).join("/") : "";
+ return {
+ ...item,
+ displayName,
+ parentPath
+ };
+ })
+ : [];
+
+ trashItems.set(processedItems);
+ trashCount.set(processedItems.length);
+ return processedItems;
+ } catch (error) {
+ console.error('Çöp öğeleri alınırken hata:', error);
+ trashCount.set(0);
+ }
+ return [];
+}
+
+// Çöpten geri yükle
+export async function restoreItem(trashName) {
+ try {
+ const result = await restoreFromTrash(trashName);
+ if (result.success) {
+ // Listeyi yenile
+ await fetchTrashItems();
+ }
+ return result;
+ } catch (error) {
+ console.error('Öğe geri yüklenirken hata:', error);
+ throw error;
+ }
+}
+
+// Çöpten tamamen sil
+export async function deleteItemPermanently(trashName) {
+ try {
+ const result = await deleteFromTrash(trashName);
+ if (result.success) {
+ // Listeyi yenile
+ await fetchTrashItems();
+ }
+ return result;
+ } catch (error) {
+ console.error('Öğe silinirken hata:', error);
+ throw error;
+ }
+}
diff --git a/client/src/utils/api.js b/client/src/utils/api.js
index ca63aaf..3543d97 100644
--- a/client/src/utils/api.js
+++ b/client/src/utils/api.js
@@ -19,3 +19,36 @@ export async function apiFetch(path, options = {}) {
}
return res;
}
+
+// 🗑️ Çöp API'leri
+export async function getTrashItems() {
+ const res = await apiFetch("/api/trash");
+ return res.json();
+}
+
+export async function restoreFromTrash(trashName) {
+ const res = await apiFetch("/api/trash/restore", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ trashName })
+ });
+ return res.json();
+}
+
+export async function deleteFromTrash(trashName) {
+ const res = await apiFetch(`/api/trash`, {
+ method: "DELETE",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ trashName })
+ });
+ return res.json();
+}
+
+export async function renameFolder(path, newName) {
+ const res = await apiFetch("/api/folder", {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ path, newName })
+ });
+ return res.json();
+}
diff --git a/server/server.js b/server/server.js
index 70311da..c8acd16 100644
--- a/server/server.js
+++ b/server/server.js
@@ -26,6 +26,11 @@ const DOWNLOAD_DIR = path.join(__dirname, "downloads");
if (!fs.existsSync(DOWNLOAD_DIR))
fs.mkdirSync(DOWNLOAD_DIR, { recursive: true });
+// --- Çöp klasörü oluştur ---
+const TRASH_DIR = path.join(__dirname, "trash");
+if (!fs.existsSync(TRASH_DIR))
+ fs.mkdirSync(TRASH_DIR, { recursive: true });
+
// --- Thumbnail cache klasörü ---
const CACHE_DIR = path.join(__dirname, "cache");
const THUMBNAIL_DIR = path.join(CACHE_DIR, "thumbnails");
@@ -627,6 +632,228 @@ function cleanupEmptyDirs(startDir) {
}
}
+// --- 🗑️ .trash yardımcı fonksiyonları ---
+const trashStateCache = new Map();
+
+function normalizeTrashPath(value) {
+ if (value === null || value === undefined) return "";
+ return String(value).replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
+}
+
+function trashFlagPathFor(rootFolder) {
+ const safeRoot = sanitizeRelative(rootFolder);
+ if (!safeRoot) return null;
+ return path.join(DOWNLOAD_DIR, safeRoot, ".trash");
+}
+
+function readTrashRegistry(rootFolder) {
+ const flagPath = trashFlagPathFor(rootFolder);
+ if (!flagPath || !fs.existsSync(flagPath)) return null;
+ try {
+ const raw = JSON.parse(fs.readFileSync(flagPath, "utf-8"));
+ if (!raw || typeof raw !== "object") return null;
+ if (!Array.isArray(raw.items)) raw.items = [];
+ raw.items = raw.items
+ .map((item) => {
+ if (!item || typeof item !== "object") return null;
+ const normalizedPath = normalizeTrashPath(item.path);
+ return {
+ ...item,
+ path: normalizedPath,
+ originalPath: item.originalPath || normalizedPath,
+ deletedAt: Number(item.deletedAt) || Date.now(),
+ isDirectory: Boolean(item.isDirectory),
+ type: item.type || (item.isDirectory ? "inode/directory" : null)
+ };
+ })
+ .filter(Boolean);
+ return raw;
+ } catch (err) {
+ console.warn(`⚠️ .trash dosyası okunamadı (${flagPath}): ${err.message}`);
+ return null;
+ }
+}
+
+function writeTrashRegistry(rootFolder, registry) {
+ const flagPath = trashFlagPathFor(rootFolder);
+ if (!flagPath) return;
+ const items = Array.isArray(registry?.items)
+ ? registry.items.filter(Boolean)
+ : [];
+ if (!items.length) {
+ try {
+ if (fs.existsSync(flagPath)) fs.rmSync(flagPath, { force: true });
+ } catch (err) {
+ console.warn(`⚠️ .trash kaldırılırken hata (${flagPath}): ${err.message}`);
+ }
+ trashStateCache.delete(rootFolder);
+ return;
+ }
+ const payload = {
+ updatedAt: Date.now(),
+ items: items.map((item) => ({
+ ...item,
+ path: normalizeTrashPath(item.path),
+ originalPath: item.originalPath || normalizeTrashPath(item.path),
+ deletedAt: Number(item.deletedAt) || Date.now(),
+ isDirectory: Boolean(item.isDirectory),
+ type: item.type || (item.isDirectory ? "inode/directory" : null)
+ }))
+ };
+ try {
+ fs.writeFileSync(flagPath, JSON.stringify(payload, null, 2), "utf-8");
+ } catch (err) {
+ console.warn(`⚠️ .trash yazılamadı (${flagPath}): ${err.message}`);
+ }
+ trashStateCache.delete(rootFolder);
+}
+
+function addTrashEntry(rootFolder, entry) {
+ if (!rootFolder || !entry) return null;
+ const safeRoot = sanitizeRelative(rootFolder);
+ if (!safeRoot) return null;
+
+ const registry = readTrashRegistry(safeRoot) || { items: [] };
+ const normalizedPath = normalizeTrashPath(entry.path);
+ const isDirectory = Boolean(entry.isDirectory);
+ const timestamp = Number(entry.deletedAt) || Date.now();
+ const type =
+ entry.type ||
+ (isDirectory
+ ? "inode/directory"
+ : mime.lookup(entry.originalPath || normalizedPath) ||
+ "application/octet-stream");
+
+ let items = registry.items.filter((item) => {
+ const itemPath = normalizeTrashPath(item.path);
+ if (isDirectory) {
+ if (!itemPath) return false;
+ if (itemPath === normalizedPath) return false;
+ if (itemPath.startsWith(`${normalizedPath}/`)) return false;
+ return true;
+ }
+
+ // üst klasör çöpteyse tekrar eklemeye gerek yok
+ if (item.isDirectory) {
+ const normalizedItemPath = itemPath;
+ if (
+ !normalizedPath ||
+ normalizedPath === normalizedItemPath ||
+ normalizedPath.startsWith(`${normalizedItemPath}/`)
+ ) {
+ return true;
+ }
+ }
+ return itemPath !== normalizedPath;
+ });
+
+ if (!isDirectory) {
+ const ancestor = items.find(
+ (item) =>
+ item.isDirectory &&
+ (normalizeTrashPath(item.path) === "" ||
+ normalizedPath === normalizeTrashPath(item.path) ||
+ normalizedPath.startsWith(`${normalizeTrashPath(item.path)}/`))
+ );
+ if (ancestor) {
+ trashStateCache.delete(safeRoot);
+ return ancestor;
+ }
+ }
+
+ const newEntry = {
+ ...entry,
+ path: normalizedPath,
+ originalPath:
+ entry.originalPath ||
+ (normalizedPath ? `${safeRoot}/${normalizedPath}` : safeRoot),
+ deletedAt: timestamp,
+ isDirectory,
+ type
+ };
+ items.push(newEntry);
+ writeTrashRegistry(safeRoot, { ...registry, items });
+ return newEntry;
+}
+
+function removeTrashEntry(rootFolder, relPath) {
+ if (!rootFolder) return null;
+ const safeRoot = sanitizeRelative(rootFolder);
+ if (!safeRoot) return null;
+ const registry = readTrashRegistry(safeRoot);
+ if (!registry || !Array.isArray(registry.items)) return null;
+
+ const normalized = normalizeTrashPath(relPath);
+ const removed = [];
+ const kept = [];
+
+ for (const item of registry.items) {
+ const itemPath = normalizeTrashPath(item.path);
+ if (
+ itemPath === normalized ||
+ (item.isDirectory &&
+ normalized &&
+ normalized.startsWith(`${itemPath}/`))
+ ) {
+ removed.push(item);
+ continue;
+ }
+ kept.push(item);
+ }
+
+ if (!removed.length) return null;
+ writeTrashRegistry(safeRoot, { ...registry, items: kept });
+ return removed[0];
+}
+
+function getTrashStateForRoot(rootFolder) {
+ if (!rootFolder) return null;
+ if (trashStateCache.has(rootFolder)) return trashStateCache.get(rootFolder);
+
+ const registry = readTrashRegistry(rootFolder);
+ if (!registry || !Array.isArray(registry.items) || !registry.items.length) {
+ trashStateCache.set(rootFolder, null);
+ return null;
+ }
+
+ const directories = [];
+ const files = new Set();
+
+ for (const item of registry.items) {
+ const normalizedPath = normalizeTrashPath(item.path);
+ if (item.isDirectory) {
+ directories.push(normalizedPath);
+ } else {
+ files.add(normalizedPath);
+ }
+ }
+
+ directories.sort((a, b) => a.length - b.length);
+
+ const result = { registry, directories, files };
+ trashStateCache.set(rootFolder, result);
+ return result;
+}
+
+function isPathTrashed(rootFolder, relPath, isDirectory = false) {
+ if (!rootFolder) return false;
+ const state = getTrashStateForRoot(rootFolder);
+ if (!state) return false;
+
+ const normalized = normalizeTrashPath(relPath);
+ for (const dirPath of state.directories) {
+ if (!dirPath) return true;
+ if (normalized === dirPath) return true;
+ if (normalized.startsWith(`${dirPath}/`)) return true;
+ }
+
+ if (!isDirectory && state.files.has(normalized)) {
+ return true;
+ }
+
+ return false;
+}
+
function resolveThumbnailAbsolute(relThumbPath) {
const normalized = sanitizeRelative(relThumbPath);
const resolved = path.resolve(THUMBNAIL_DIR, normalized);
@@ -2074,6 +2301,301 @@ function pruneInfoEntry(rootFolder, relativePath) {
}
}
+function pruneInfoForDirectory(rootFolder, relativeDir) {
+ if (!rootFolder) return;
+ const info = readInfoForRoot(rootFolder);
+ if (!info) return;
+
+ const normalizedDir = normalizeTrashPath(relativeDir);
+ const prefix = normalizedDir ? `${normalizedDir}/` : "";
+ const removedEpisodePaths = [];
+ let changed = false;
+
+ if (info.files && typeof info.files === "object") {
+ for (const key of Object.keys(info.files)) {
+ if (key === normalizedDir || (prefix && key.startsWith(prefix))) {
+ delete info.files[key];
+ changed = true;
+ }
+ }
+ if (Object.keys(info.files).length === 0) delete info.files;
+ }
+
+ if (info.seriesEpisodes && typeof info.seriesEpisodes === "object") {
+ for (const key of Object.keys(info.seriesEpisodes)) {
+ if (key === normalizedDir || (prefix && key.startsWith(prefix))) {
+ removedEpisodePaths.push(key);
+ delete info.seriesEpisodes[key];
+ changed = true;
+ }
+ }
+ if (Object.keys(info.seriesEpisodes).length === 0) {
+ delete info.seriesEpisodes;
+ }
+ }
+
+ if (info.primaryVideoPath) {
+ if (
+ info.primaryVideoPath === normalizedDir ||
+ (prefix && info.primaryVideoPath.startsWith(prefix))
+ ) {
+ delete info.primaryVideoPath;
+ delete info.primaryMediaInfo;
+ delete info.movieMatch;
+ changed = true;
+ }
+ }
+
+ if (changed) {
+ 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}`);
+ }
+ }
+
+ // TV metadata dosyalarından da temizle
+ for (const relPath of removedEpisodePaths) {
+ try {
+ removeSeriesEpisode(rootFolder, relPath);
+ } catch (err) {
+ console.warn(
+ `⚠️ Serie metadata temizlenemedi (${rootFolder}/${relPath}): ${err.message}`
+ );
+ }
+ }
+}
+
+function renameInfoPaths(rootFolder, oldRel, newRel) {
+ if (!rootFolder) return;
+ const info = readInfoForRoot(rootFolder);
+ if (!info) return;
+
+ const oldPrefix = normalizeTrashPath(oldRel);
+ const newPrefix = normalizeTrashPath(newRel);
+ if (!oldPrefix || oldPrefix === newPrefix) return;
+
+ const transformKey = (key) => {
+ const normalizedKey = normalizeTrashPath(key);
+ if (
+ normalizedKey === oldPrefix ||
+ normalizedKey.startsWith(`${oldPrefix}/`)
+ ) {
+ const suffix = normalizedKey.slice(oldPrefix.length).replace(/^\/+/, "");
+ return newPrefix
+ ? `${newPrefix}${suffix ? `/${suffix}` : ""}`
+ : suffix;
+ }
+ return normalizedKey;
+ };
+
+ let changed = false;
+
+ if (info.files && typeof info.files === "object") {
+ const nextFiles = {};
+ for (const [key, value] of Object.entries(info.files)) {
+ const nextKey = transformKey(key);
+ if (nextKey !== key) changed = true;
+ nextFiles[nextKey] = value;
+ }
+ info.files = nextFiles;
+ }
+
+ if (info.seriesEpisodes && typeof info.seriesEpisodes === "object") {
+ const nextEpisodes = {};
+ for (const [key, value] of Object.entries(info.seriesEpisodes)) {
+ const nextKey = transformKey(key);
+ if (nextKey !== key) changed = true;
+ nextEpisodes[nextKey] = value;
+ }
+ info.seriesEpisodes = nextEpisodes;
+ }
+
+ if (
+ info.primaryVideoPath &&
+ (info.primaryVideoPath === oldPrefix ||
+ info.primaryVideoPath.startsWith(`${oldPrefix}/`))
+ ) {
+ const suffix = info.primaryVideoPath.slice(oldPrefix.length).replace(/^\/+/, "");
+ info.primaryVideoPath = newPrefix
+ ? `${newPrefix}${suffix ? `/${suffix}` : ""}`
+ : suffix;
+ changed = true;
+ }
+
+ if (changed) {
+ 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 renameSeriesDataPaths(rootFolder, oldRel, newRel) {
+ if (!rootFolder) return;
+ const oldPrefix = normalizeTrashPath(oldRel);
+ const newPrefix = normalizeTrashPath(newRel);
+ if (!oldPrefix || oldPrefix === newPrefix) return;
+
+ const metadataPath = tvSeriesPaths(rootFolder).metadata;
+ if (!fs.existsSync(metadataPath)) return;
+
+ let seriesData;
+ try {
+ seriesData = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
+ } catch (err) {
+ console.warn(`⚠️ series.json okunamadı (${metadataPath}): ${err.message}`);
+ return;
+ }
+
+ const transform = (value) => {
+ const normalized = normalizeTrashPath(value);
+ if (
+ normalized === oldPrefix ||
+ normalized.startsWith(`${oldPrefix}/`)
+ ) {
+ const suffix = normalized.slice(oldPrefix.length).replace(/^\/+/, "");
+ return newPrefix
+ ? `${newPrefix}${suffix ? `/${suffix}` : ""}`
+ : suffix;
+ }
+ return value;
+ };
+
+ let changed = false;
+
+ const seasons = seriesData?.seasons || {};
+ for (const season of Object.values(seasons)) {
+ if (!season?.episodes) continue;
+ for (const episode of Object.values(season.episodes)) {
+ if (!episode || typeof episode !== "object") continue;
+ if (episode.file) {
+ const nextFile = transform(episode.file);
+ if (nextFile !== episode.file) {
+ episode.file = nextFile;
+ changed = true;
+ }
+ }
+ if (episode.videoPath) {
+ const nextVideo = transform(episode.videoPath);
+ if (nextVideo !== episode.videoPath) {
+ episode.videoPath = nextVideo;
+ changed = true;
+ }
+ }
+ }
+ }
+
+ if (changed) {
+ try {
+ fs.writeFileSync(metadataPath, JSON.stringify(seriesData, null, 2), "utf-8");
+ } catch (err) {
+ console.warn(
+ `⚠️ series.json güncellenemedi (${metadataPath}): ${err.message}`
+ );
+ }
+ }
+}
+
+function removeThumbnailsForDirectory(rootFolder, relativeDir) {
+ const normalizedRoot = sanitizeRelative(rootFolder);
+ if (!normalizedRoot) return;
+ const normalizedDir = normalizeTrashPath(relativeDir);
+ const segments = normalizedDir ? normalizedDir.split("/") : [];
+
+ const videoThumbDir = path.join(
+ VIDEO_THUMB_ROOT,
+ normalizedRoot,
+ ...segments
+ );
+ const imageThumbDir = path.join(
+ IMAGE_THUMB_ROOT,
+ normalizedRoot,
+ ...segments
+ );
+
+ for (const dir of [videoThumbDir, imageThumbDir]) {
+ if (!dir.startsWith(THUMBNAIL_DIR)) continue;
+ if (fs.existsSync(dir)) {
+ try {
+ fs.rmSync(dir, { recursive: true, force: true });
+ } catch (err) {
+ console.warn(`⚠️ Thumbnail klasörü silinemedi (${dir}): ${err.message}`);
+ }
+ }
+ cleanupEmptyDirs(path.dirname(dir));
+ }
+}
+
+function renameTrashEntries(rootFolder, oldRel, newRel) {
+ if (!rootFolder) return;
+ const registry = readTrashRegistry(rootFolder);
+ if (!registry || !Array.isArray(registry.items)) return;
+
+ const oldPrefix = normalizeTrashPath(oldRel);
+ const newPrefix = normalizeTrashPath(newRel);
+ if (!oldPrefix || oldPrefix === newPrefix) return;
+
+ let changed = false;
+ const updatedItems = registry.items.map((item) => {
+ const itemPath = normalizeTrashPath(item.path);
+ if (
+ itemPath === oldPrefix ||
+ itemPath.startsWith(`${oldPrefix}/`)
+ ) {
+ const suffix = itemPath.slice(oldPrefix.length).replace(/^\/+/, "");
+ const nextPath = newPrefix
+ ? `${newPrefix}${suffix ? `/${suffix}` : ""}`
+ : suffix;
+ changed = true;
+ return {
+ ...item,
+ path: nextPath,
+ originalPath: nextPath
+ ? `${rootFolder}/${nextPath}`
+ : rootFolder
+ };
+ }
+ return item;
+ });
+
+ if (changed) {
+ writeTrashRegistry(rootFolder, { ...registry, items: updatedItems });
+ }
+}
+
+function renameRootCaches(oldRoot, newRoot) {
+ const pairs = [
+ VIDEO_THUMB_ROOT,
+ IMAGE_THUMB_ROOT,
+ MOVIE_DATA_ROOT,
+ TV_DATA_ROOT
+ ];
+
+ for (const base of pairs) {
+ const from = path.join(base, oldRoot);
+ if (!fs.existsSync(from)) continue;
+ const to = path.join(base, newRoot);
+ try {
+ fs.mkdirSync(path.dirname(to), { recursive: true });
+ fs.renameSync(from, to);
+ } catch (err) {
+ console.warn(
+ `⚠️ Kök cache klasörü yeniden adlandırılamadı (${from} -> ${to}): ${err.message}`
+ );
+ }
+ }
+}
+
function broadcastFileUpdate(rootFolder) {
if (!wss) return;
const data = JSON.stringify({
@@ -2389,6 +2911,19 @@ app.post("/api/transfer", requireAuth, upload.single("torrent"), (req, res) => {
// 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();
});
} catch (err) {
@@ -2735,8 +3270,6 @@ app.get("/media/:path(*)", requireAuth, (req, res) => {
const isVideo = String(type).startsWith("video/");
const range = req.headers.range;
- console.log("Media info:", { fileSize, type, isVideo, range }); // Debug için log ekle
-
// CORS headers ekle
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
@@ -2767,7 +3300,7 @@ app.get("/media/:path(*)", requireAuth, (req, res) => {
}
});
-// --- 🗑️ Tekil dosya veya torrent klasörü silme ---
+// --- 🗑️ Tekil dosya veya torrent klasörüne .trash flag'i ekleme ---
app.delete("/api/file", requireAuth, (req, res) => {
const filePath = req.query.path;
if (!filePath) return res.status(400).json({ error: "path gerekli" });
@@ -2782,7 +3315,7 @@ app.delete("/api/file", requireAuth, (req, res) => {
stats = fs.statSync(fullPath);
} catch (err) {
const message = err?.message || String(err);
- console.warn(`⚠️ Silme sırasında stat alınamadı (${fullPath}): ${message}`);
+ console.warn(`⚠️ Silme işlemi sırasında stat alınamadı (${fullPath}): ${message}`);
}
if (!stats || !fs.existsSync(fullPath)) {
@@ -2795,60 +3328,47 @@ app.delete("/api/file", requireAuth, (req, res) => {
}
try {
- fs.rmSync(fullPath, { recursive: true, force: true });
- console.log(`🗑️ Dosya/klasör silindi: ${fullPath}`);
- removeThumbnailsForPath(safePath);
+ const isDirectory = stats.isDirectory();
+ const relWithinRoot = safePath.split(/[\\/]/).slice(1).join("/");
+ let trashEntry = null;
- if (folderId) {
- const relWithinRoot = safePath.split(/[\/]/).slice(1).join("/");
- const rootExists = rootDir && fs.existsSync(rootDir);
+ if (folderId && rootDir) {
+ trashEntry = addTrashEntry(folderId, {
+ path: relWithinRoot,
+ originalPath: safePath,
+ isDirectory,
+ deletedAt: Date.now(),
+ type: isDirectory
+ ? "inode/directory"
+ : mime.lookup(fullPath) || "application/octet-stream"
+ });
- if (!relWithinRoot || !rootExists) {
- purgeRootFolder(folderId);
+ if (isDirectory) {
+ pruneInfoForDirectory(folderId, relWithinRoot);
} else {
- const remaining = fs.readdirSync(rootDir);
- const meaningful = remaining.filter((name) => {
- if (!name) return false;
- if (name === INFO_FILENAME) return false;
- if (name.startsWith(".")) return false;
- return true;
- });
-
pruneInfoEntry(folderId, relWithinRoot);
removeSeriesEpisode(folderId, relWithinRoot);
-
- if (meaningful.length === 0) {
- removeAllThumbnailsForRoot(folderId);
- removeMovieData(folderId);
- removeSeriesData(folderId);
- const infoPath = path.join(rootDir, INFO_FILENAME);
- if (fs.existsSync(infoPath)) {
- try {
- fs.rmSync(infoPath, { force: true });
- } catch (err) {
- console.warn(`⚠️ info.json kaldırılamadı (${infoPath}): ${err.message}`);
- }
- }
- } else {
- const infoAfter = readInfoForRoot(folderId);
- const displayName = infoAfter?.name || folderId;
- const primaryVideo = infoAfter?.primaryVideoPath || guessPrimaryVideo(folderId);
- if (primaryVideo) {
- const candidateMedia =
- infoAfter?.files?.[primaryVideo]?.mediaInfo ||
- infoAfter?.primaryMediaInfo ||
- null;
- ensureMovieData(folderId, displayName, primaryVideo, candidateMedia).catch(
- (err) =>
- console.warn(
- `⚠️ Movie metadata yenilenemedi (${folderId}): ${err?.message || err}`
- )
- );
- }
- }
}
+ }
+ if (isDirectory) {
+ console.log(`🗑️ Klasör çöpe taşındı (işaretlendi): ${safePath}`);
+ } else {
+ console.log(`🗑️ Dosya çöpe taşındı (işaretlendi): ${fullPath}`);
+ removeThumbnailsForPath(safePath);
+ }
+
+ if (!folderId) {
+ // Kök klasöre ait olmayan dosyaları doğrudan sil
+ if (fs.existsSync(fullPath)) {
+ fs.rmSync(fullPath, { recursive: true, force: true });
+ }
+ removeThumbnailsForPath(safePath);
+ }
+
+ if (folderId) {
broadcastFileUpdate(folderId);
+ trashStateCache.delete(folderId);
}
if (folderId) {
@@ -2903,7 +3423,7 @@ app.get("/api/files", requireAuth, (req, res) => {
}
}
- // --- 🔍 Yardımcı fonksiyon: dosya ignoreList’te mi? ---
+ // --- 🔍 Yardımcı fonksiyon: dosya ignoreList'te mi? ---
const isIgnored = (name) => {
const lower = name.toLowerCase();
const ext = path.extname(lower).replace(".", "");
@@ -2942,8 +3462,13 @@ app.get("/api/files", requireAuth, (req, res) => {
const safeRel = sanitizeRelative(rel);
if (!safeRel) continue;
- const dirInfo = getInfo(safeRel) || {};
const rootFolder = rootFromRelPath(safeRel);
+ const relWithinRoot = safeRel.split(/[\\/]/).slice(1).join("/");
+
+ // 🗑️ Çöpte işaretli klasörleri atla
+ if (isPathTrashed(rootFolder, relWithinRoot, true)) continue;
+
+ const dirInfo = getInfo(safeRel) || {};
const added = dirInfo.added ?? dirInfo.createdAt ?? null;
const completedAt = dirInfo.completedAt ?? null;
const tracker = dirInfo.tracker ?? null;
@@ -2974,10 +3499,16 @@ app.get("/api/files", requireAuth, (req, res) => {
} else {
if (entry.name.toLowerCase() === INFO_FILENAME) continue;
+ const safeRel = sanitizeRelative(rel);
+ const rootFolder = rootFromRelPath(safeRel);
+ const relWithinRoot = safeRel.split(/[\\/]/).slice(1).join("/");
+
+ // 🗑️ Çöpte işaretli dosyaları atla
+ if (isPathTrashed(rootFolder, relWithinRoot, false)) continue;
+
const size = fs.statSync(full).size;
const type = mime.lookup(full) || "application/octet-stream";
- const safeRel = sanitizeRelative(rel);
const urlPath = safeRel
.split(/[\\/]/)
.map(encodeURIComponent)
@@ -3002,13 +3533,11 @@ app.get("/api/files", requireAuth, (req, res) => {
}
const info = getInfo(safeRel) || {};
- const rootFolder = rootFromRelPath(safeRel);
const added = info.added ?? info.createdAt ?? null;
const completedAt = info.completedAt ?? null;
const tracker = info.tracker ?? null;
const torrentName = info.name ?? null;
const infoHash = info.infoHash ?? null;
- const relWithinRoot = safeRel.split(/[\\/]/).slice(1).join("/");
const fileMeta = relWithinRoot
? info.files?.[relWithinRoot] || null
: null;
@@ -3054,6 +3583,194 @@ app.get("/api/files", requireAuth, (req, res) => {
}
});
+// --- 🗑️ Çöp listesi API (.trash flag sistemi) ---
+app.get("/api/trash", requireAuth, (req, res) => {
+ try {
+ const result = [];
+ const roots = fs
+ .readdirSync(DOWNLOAD_DIR, { withFileTypes: true })
+ .filter((dirent) => dirent.isDirectory());
+
+ for (const dirent of roots) {
+ const rootFolder = sanitizeRelative(dirent.name);
+ if (!rootFolder) continue;
+
+ const state = getTrashStateForRoot(rootFolder);
+ if (!state || !Array.isArray(state.registry?.items)) continue;
+
+ const info = readInfoForRoot(rootFolder) || {};
+
+ for (const item of state.registry.items) {
+ const relWithinRoot = normalizeTrashPath(item.path);
+ const displayPath = relWithinRoot
+ ? `${rootFolder}/${relWithinRoot}`
+ : rootFolder;
+ const fullPath = path.join(DOWNLOAD_DIR, displayPath);
+
+ if (!fs.existsSync(fullPath)) {
+ removeTrashEntry(rootFolder, relWithinRoot);
+ continue;
+ }
+
+ let stat = null;
+ try {
+ stat = fs.statSync(fullPath);
+ } catch (err) {
+ console.warn(
+ `⚠️ Çöp öğesi stat okunamadı (${fullPath}): ${err.message}`
+ );
+ }
+
+ const isDirectory = item.isDirectory || stat?.isDirectory() || false;
+ const type = isDirectory
+ ? "inode/directory"
+ : mime.lookup(fullPath) || item.type || "application/octet-stream";
+ const size = stat?.size ?? 0;
+
+ let thumbnail = null;
+ let mediaInfo = null;
+
+ if (!isDirectory) {
+ const isVideo = String(type).startsWith("video/");
+ const isImage = String(type).startsWith("image/");
+
+ if (isVideo) {
+ const { relThumb, absThumb } = getVideoThumbnailPaths(displayPath);
+ if (fs.existsSync(absThumb)) {
+ thumbnail = thumbnailUrl(relThumb);
+ }
+ } else if (isImage) {
+ const { relThumb, absThumb } = getImageThumbnailPaths(displayPath);
+ if (fs.existsSync(absThumb)) {
+ thumbnail = thumbnailUrl(relThumb);
+ }
+ }
+
+ const metaKey = relWithinRoot || null;
+ if (metaKey && info.files && info.files[metaKey]) {
+ mediaInfo = info.files[metaKey].mediaInfo || null;
+ }
+ }
+
+ result.push({
+ name: displayPath,
+ trashName: displayPath,
+ size,
+ type,
+ isDirectory,
+ thumbnail,
+ mediaInfo,
+ movedAt: Number(item.deletedAt) || Date.now(),
+ originalPath: displayPath,
+ folderId: rootFolder
+ });
+ }
+ }
+
+ result.sort((a, b) => (b.movedAt || 0) - (a.movedAt || 0));
+ res.json(result);
+ } catch (err) {
+ console.error("🗑️ Trash API error:", err);
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// --- 🗑️ Çöpten geri yükleme API (.trash flag sistemi) ---
+app.post("/api/trash/restore", requireAuth, (req, res) => {
+ try {
+ const { trashName } = req.body;
+
+ if (!trashName) {
+ return res.status(400).json({ error: "trashName gerekli" });
+ }
+
+ const safeName = sanitizeRelative(trashName);
+ const segments = safeName.split(/[\\/]/).filter(Boolean);
+ if (!segments.length) {
+ return res.status(400).json({ error: "Geçersiz trashName" });
+ }
+
+ const rootFolder = segments[0];
+ const relWithinRoot = segments.slice(1).join("/");
+
+ const removed = removeTrashEntry(rootFolder, relWithinRoot);
+ if (!removed) {
+ return res.status(404).json({ error: "Çöp öğesi bulunamadı" });
+ }
+
+ console.log(`♻️ Öğe geri yüklendi: ${safeName}`);
+
+ broadcastFileUpdate(rootFolder);
+
+ res.json({
+ success: true,
+ message: "Öğe başarıyla geri yüklendi",
+ folderId: rootFolder
+ });
+ } catch (err) {
+ console.error("❌ Restore error:", err);
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// --- 🗑️ Çöpü tamamen silme API (.trash flag sistemi) ---
+app.delete("/api/trash", requireAuth, (req, res) => {
+ try {
+ const trashName =
+ req.body?.trashName || req.query?.trashName || req.params?.trashName;
+
+ if (!trashName) {
+ return res.status(400).json({ error: "trashName gerekli" });
+ }
+
+ const safeName = sanitizeRelative(trashName);
+ const segments = safeName.split(/[\\/]/).filter(Boolean);
+ if (!segments.length) {
+ return res.status(400).json({ error: "Geçersiz trashName" });
+ }
+
+ const rootFolder = segments[0];
+ const relWithinRoot = segments.slice(1).join("/");
+
+ const removed = removeTrashEntry(rootFolder, relWithinRoot);
+ if (!removed) {
+ return res.status(404).json({ error: "Çöp öğesi bulunamadı" });
+ }
+
+ const fullPath = path.join(DOWNLOAD_DIR, safeName);
+ if (fs.existsSync(fullPath)) {
+ try {
+ fs.rmSync(fullPath, { recursive: true, force: true });
+ } catch (err) {
+ console.warn(`⚠️ Çöp öğesi silinemedi (${fullPath}): ${err.message}`);
+ }
+ }
+
+ if (!relWithinRoot) {
+ purgeRootFolder(rootFolder);
+ } else if (removed.isDirectory) {
+ pruneInfoForDirectory(rootFolder, relWithinRoot);
+ } else {
+ pruneInfoEntry(rootFolder, relWithinRoot);
+ removeThumbnailsForPath(safeName);
+ removeSeriesEpisode(rootFolder, relWithinRoot);
+ }
+
+ console.log(`🗑️ Öğe kalıcı olarak silindi: ${safeName}`);
+
+ broadcastFileUpdate(rootFolder);
+ broadcastDiskSpace();
+
+ res.json({
+ success: true,
+ message: "Öğe tamamen silindi"
+ });
+ } catch (err) {
+ console.error("❌ Delete trash error:", err);
+ res.status(500).json({ error: err.message });
+ }
+});
+
// --- 🎬 Film listesi ---
app.get("/api/movies", requireAuth, (req, res) => {
try {
@@ -3220,6 +3937,36 @@ app.get("/api/tvshows", requireAuth, (req, res) => {
const paths = tvSeriesPaths(folder);
if (!fs.existsSync(paths.metadata)) continue;
+ const infoForFolder = readInfoForRoot(folder) || {};
+ const infoFiles = infoForFolder.files || {};
+ const infoEpisodes = infoForFolder.seriesEpisodes || {};
+ const infoEpisodeIndex = new Map();
+
+ for (const [relPath, meta] of Object.entries(infoEpisodes)) {
+ if (!meta) continue;
+ const seasonNumber = toFiniteNumber(
+ meta.season ?? meta.seasonNumber ?? meta.seasonNum
+ );
+ const episodeNumber = toFiniteNumber(
+ meta.episode ?? meta.episodeNumber ?? meta.episodeNum
+ );
+ if (!Number.isFinite(seasonNumber) || !Number.isFinite(episodeNumber)) continue;
+
+ const normalizedRel = normalizeTrashPath(relPath);
+ const ext = path.extname(normalizedRel).toLowerCase();
+ if (!VIDEO_EXTS.includes(ext)) continue;
+
+ const absVideo = normalizedRel
+ ? path.join(DOWNLOAD_DIR, folder, normalizedRel)
+ : null;
+ if (!absVideo || !fs.existsSync(absVideo)) continue;
+
+ infoEpisodeIndex.set(`${seasonNumber}-${episodeNumber}`, {
+ relPath: normalizedRel,
+ meta
+ });
+ }
+
let seriesData;
try {
seriesData = JSON.parse(fs.readFileSync(paths.metadata, "utf-8"));
@@ -3390,8 +4137,22 @@ app.get("/api/tvshows", requireAuth, (req, res) => {
if (!normalizedEpisode.code) {
normalizedEpisode.code = `S${String(seasonNumber).padStart(
2,
- "0"
- )}E${String(episodeNumber).padStart(2, "0")}`;
+ "0"
+ )}E${String(episodeNumber).padStart(2, "0")}`;
+ }
+ const infoEpisode = infoEpisodeIndex.get(`${seasonNumber}-${episodeNumber}`);
+ if (infoEpisode?.relPath) {
+ const normalizedRel = infoEpisode.relPath.replace(/^\/+/, "");
+ const withRoot = `${folder}/${normalizedRel}`.replace(/^\/+/, "");
+ normalizedEpisode.file = normalizedRel;
+ normalizedEpisode.videoPath = withRoot;
+ const fileMeta = infoFiles[normalizedRel];
+ if (fileMeta?.mediaInfo && !normalizedEpisode.mediaInfo) {
+ normalizedEpisode.mediaInfo = fileMeta.mediaInfo;
+ }
+ if (fileMeta?.size) {
+ normalizedEpisode.fileSize = Number(fileMeta.size);
+ }
}
const relativeFile =
normalizedEpisode.file || normalizedEpisode.videoPath || "";
@@ -3419,6 +4180,28 @@ app.get("/api/tvshows", requireAuth, (req, res) => {
dataChanged = true;
}
}
+ if (normalizedEpisode.videoPath && !/^https?:\/\//i.test(normalizedEpisode.videoPath)) {
+ const ext = path.extname(normalizedEpisode.videoPath).toLowerCase();
+ if (!VIDEO_EXTS.includes(ext)) {
+ const absVideo = path.join(DOWNLOAD_DIR, normalizedEpisode.videoPath);
+ if (
+ !fs.existsSync(absVideo) ||
+ !VIDEO_EXTS.includes(path.extname(absVideo).toLowerCase())
+ ) {
+ normalizedEpisode.videoPath = null;
+ }
+ }
+ if (normalizedEpisode.videoPath) {
+ const absVideo = path.join(DOWNLOAD_DIR, normalizedEpisode.videoPath);
+ if (!fs.existsSync(absVideo)) {
+ normalizedEpisode.videoPath = null;
+ }
+ if (fs.existsSync(absVideo)) {
+ const stats = fs.statSync(absVideo);
+ normalizedEpisode.fileSize = Number(stats.size);
+ }
+ }
+ }
normalizedEpisode.folder = folder;
const existingEpisode = seasonRecord.episodes.get(episodeNumber);
@@ -3454,9 +4237,16 @@ app.get("/api/tvshows", requireAuth, (req, res) => {
.map((record) => {
const seasons = Array.from(record.seasons.values())
.map((season) => {
- const episodes = Array.from(season.episodes.values()).sort(
- (a, b) => a.episodeNumber - b.episodeNumber
- );
+ const episodes = Array.from(season.episodes.values())
+ .filter((episode) => {
+ if (!episode?.videoPath) return false;
+ if (/^https?:\/\//i.test(episode.videoPath)) return true;
+ const ext = path.extname(episode.videoPath).toLowerCase();
+ if (!VIDEO_EXTS.includes(ext)) return false;
+ const absVideo = path.join(DOWNLOAD_DIR, episode.videoPath);
+ return fs.existsSync(absVideo);
+ })
+ .sort((a, b) => a.episodeNumber - b.episodeNumber);
return {
seasonNumber: season.seasonNumber,
name: season.name || `Season ${season.seasonNumber}`,
@@ -3644,7 +4434,7 @@ app.get("/stream/:hash", requireAuth, (req, res) => {
stream.pipe(res);
});
-console.log("🗄️ Download path:", DOWNLOAD_DIR);
+console.log("🗄️ Download path:", DOWNLOAD_DIR);
// --- ✅ Client build (frontend) dosyalarını sun ---
@@ -4172,6 +4962,135 @@ app.post("/api/folder", requireAuth, async (req, res) => {
}
});
+app.patch("/api/folder", requireAuth, (req, res) => {
+ try {
+ const { path: targetPath, newName } = req.body || {};
+ if (!targetPath || !newName) {
+ return res.status(400).json({ error: "path ve newName gerekli" });
+ }
+
+ const safePath = sanitizeRelative(targetPath);
+ if (!safePath) {
+ return res.status(400).json({ error: "Geçersiz hedef yol" });
+ }
+
+ const segments = safePath.split(/[\\/]/).filter(Boolean);
+ if (!segments.length) {
+ return res.status(400).json({ error: "Geçersiz hedef yol" });
+ }
+
+ const trimmedName = String(newName).trim();
+ if (!trimmedName || /[\\/]/.test(trimmedName)) {
+ return res.status(400).json({ error: "Geçersiz yeni isim" });
+ }
+
+ const parentSegments = segments.slice(0, -1);
+ const newSegments = [...parentSegments, trimmedName];
+ const newRelativePath = newSegments.join("/");
+ const safeNewRelativePath = sanitizeRelative(newRelativePath);
+ if (!safeNewRelativePath) {
+ return res.status(400).json({ error: "Geçersiz yeni yol" });
+ }
+
+ const oldFullPath = path.join(DOWNLOAD_DIR, safePath);
+ const newFullPath = path.join(DOWNLOAD_DIR, safeNewRelativePath);
+
+ if (!fs.existsSync(oldFullPath)) {
+ return res.status(404).json({ error: "Yeniden adlandırılacak klasör bulunamadı" });
+ }
+
+ if (fs.existsSync(newFullPath)) {
+ return res.status(409).json({ error: "Yeni isimde bir klasör zaten var" });
+ }
+
+ // Yeniden adlandırmayı gerçekleştir
+ fs.renameSync(oldFullPath, newFullPath);
+
+ const rootFolder = segments[0];
+ const newRootFolder = newSegments[0];
+ const oldRelWithinRoot = segments.slice(1).join("/");
+ const newRelWithinRoot = newSegments.slice(1).join("/");
+
+ if (!oldRelWithinRoot) {
+ // Kök klasör yeniden adlandırıldı
+ renameRootCaches(rootFolder, newRootFolder);
+ trashStateCache.delete(rootFolder);
+ trashStateCache.delete(newRootFolder);
+
+ // Torrent kayıtlarını güncelle
+ for (const entry of torrents.values()) {
+ if (!entry?.savePath) continue;
+ const baseName = path.basename(entry.savePath);
+ if (baseName === rootFolder) {
+ entry.savePath = path.join(path.dirname(entry.savePath), newRootFolder);
+ }
+ }
+
+ console.log(`📁 Kök klasör yeniden adlandırıldı: ${rootFolder} -> ${newRootFolder}`);
+ broadcastFileUpdate(rootFolder);
+ broadcastFileUpdate(newRootFolder);
+ broadcastSnapshot();
+ return res.json({
+ success: true,
+ message: "Klasör yeniden adlandırıldı",
+ oldPath: safePath,
+ newPath: safeNewRelativePath
+ });
+ }
+
+ // Alt klasör yeniden adlandırıldı
+ renameInfoPaths(rootFolder, oldRelWithinRoot, newRelWithinRoot);
+ renameSeriesDataPaths(rootFolder, oldRelWithinRoot, newRelWithinRoot);
+ renameTrashEntries(rootFolder, oldRelWithinRoot, newRelWithinRoot);
+ removeThumbnailsForDirectory(rootFolder, oldRelWithinRoot);
+ trashStateCache.delete(rootFolder);
+
+ console.log(
+ `📁 Klasör yeniden adlandırıldı: ${safePath} -> ${safeNewRelativePath}`
+ );
+
+ broadcastFileUpdate(rootFolder);
+
+ res.json({
+ success: true,
+ message: "Klasör yeniden adlandırıldı",
+ oldPath: safePath,
+ newPath: safeNewRelativePath
+ });
+ } catch (err) {
+ console.error("❌ Folder rename error:", err);
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// Recursive klasör kopyalama fonksiyonu
+function copyFolderRecursiveSync(source, target) {
+ // Hedef klasörü oluştur
+ if (!fs.existsSync(target)) {
+ fs.mkdirSync(target, { recursive: true });
+ }
+
+ // Kaynak klasördeki tüm öğeleri oku
+ const files = fs.readdirSync(source);
+
+ // Her öğeyi işle
+ files.forEach(file => {
+ const sourcePath = path.join(source, file);
+ const targetPath = path.join(target, file);
+
+ // Dosya istatistiklerini al
+ const stats = fs.statSync(sourcePath);
+
+ if (stats.isDirectory()) {
+ // Alt klasörse recursive olarak kopyala
+ copyFolderRecursiveSync(sourcePath, targetPath);
+ } else {
+ // Dosyaysa kopyala
+ fs.copyFileSync(sourcePath, targetPath);
+ }
+ });
+}
+
client.on("error", (err) => {
if (!String(err).includes("uTP"))
console.error("WebTorrent error:", err.message);