Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ac608a0b1 | |||
| e7925aa39f | |||
| e20b3ad591 | |||
| 66aa99f0f7 | |||
| b7014ee27e | |||
| 3b98df4348 | |||
| e937a67090 | |||
| 569a7975de | |||
| 5e6da2b445 |
12
.env.example
12
.env.example
@@ -31,3 +31,15 @@ AUTO_PAUSE_ON_COMPLETE=0
|
||||
# Medya işleme adımlarını (ffprobe/ffmpeg, thumbnail ve TMDB/TVDB metadata) devre dışı bırakır;
|
||||
# CPU ve disk kullanımını düşürür, ancak kapalıyken medya bilgileri eksik kalır.
|
||||
DISABLE_MEDIA_PROCESSING=0
|
||||
# WebDAV erişimi; Infuse gibi istemciler için salt-okuma paylaşımlar.
|
||||
WEBDAV_ENABLED=1
|
||||
# WebDAV Basic Auth kullanıcı adı.
|
||||
WEBDAV_USERNAME=dupe
|
||||
# WebDAV Basic Auth şifresi (güçlü bir parola kullanın).
|
||||
WEBDAV_PASSWORD=superpassword
|
||||
# WebDAV kök path'i (proxy üzerinden erişilecek).
|
||||
WEBDAV_PATH=/webdav
|
||||
# WebDAV salt-okuma modu.
|
||||
WEBDAV_READONLY=1
|
||||
# WebDAV index yeniden oluşturma süresi (ms).
|
||||
WEBDAV_INDEX_TTL=60000
|
||||
|
||||
1292
client/package-lock.json
generated
Normal file
1292
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import { onMount, tick } from "svelte";
|
||||
import { onDestroy, onMount, tick } from "svelte";
|
||||
import MatchModal from "../components/MatchModal.svelte";
|
||||
import { API, apiFetch, moveEntry, renameFolder, copyEntry } from "../utils/api.js";
|
||||
import { cleanFileName, extractTitleAndYear } from "../utils/filename.js";
|
||||
@@ -452,6 +452,7 @@
|
||||
const VIEW_KEY = "filesViewMode";
|
||||
let viewMode = "grid";
|
||||
let initialPath = "";
|
||||
let unpatchHistory = null;
|
||||
if (typeof window !== "undefined") {
|
||||
const storedView = window.localStorage.getItem(VIEW_KEY);
|
||||
if (storedView === "grid" || storedView === "list") {
|
||||
@@ -491,14 +492,16 @@
|
||||
let clipboardItem = null;
|
||||
let clipboardOperation = null; // 'cut' veya 'copy'
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
const syncFromLocation = ({ replace = false } = {}) => {
|
||||
if (typeof window === "undefined") return;
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const pathParam = params.get("path");
|
||||
let nextPath = "";
|
||||
if (pathParam) {
|
||||
try {
|
||||
initialPath = normalizePath(decodeURIComponent(pathParam));
|
||||
nextPath = normalizePath(decodeURIComponent(pathParam));
|
||||
} catch (err) {
|
||||
initialPath = normalizePath(pathParam);
|
||||
nextPath = normalizePath(pathParam);
|
||||
}
|
||||
}
|
||||
const playParam = params.get("play");
|
||||
@@ -509,16 +512,49 @@
|
||||
pendingPlayTarget = playParam;
|
||||
}
|
||||
params.delete("play");
|
||||
}
|
||||
const search = params.toString();
|
||||
const newUrl = `${window.location.pathname}${search ? `?${search}` : ""}${window.location.hash}`;
|
||||
window.history.replaceState(
|
||||
{ path: initialPath, originalPath: null },
|
||||
{ path: nextPath, originalPath: null },
|
||||
"",
|
||||
newUrl,
|
||||
);
|
||||
}
|
||||
currentPath = normalizePath(initialPath);
|
||||
|
||||
const state = window.history.state || {};
|
||||
const nextOriginal =
|
||||
typeof state.originalPath === "string"
|
||||
? normalizePath(state.originalPath)
|
||||
: resolveOriginalPathForDisplay(nextPath);
|
||||
|
||||
if (
|
||||
normalizePath(currentPath) === nextPath &&
|
||||
normalizePath(currentOriginalPath) === nextOriginal
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hadSearch = searchTerm.trim().length > 0;
|
||||
const nextSearchTerm = hadSearch ? "" : searchTerm;
|
||||
currentPath = nextPath;
|
||||
currentOriginalPath = nextOriginal;
|
||||
if (hadSearch) {
|
||||
clearSearch("files");
|
||||
}
|
||||
selectedItems = new Set();
|
||||
activeMenu = null;
|
||||
if (isCreatingFolder) cancelCreateFolder();
|
||||
updateVisibleState(files, nextPath);
|
||||
renderedEntries = filterEntriesBySearch(visibleEntries, nextSearchTerm);
|
||||
if (replace) {
|
||||
updateUrlPath(nextPath, nextOriginal, { replace: true });
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
syncFromLocation({ replace: true });
|
||||
initialPath = currentPath;
|
||||
}
|
||||
// 🎬 Player kontrolleri
|
||||
let videoEl;
|
||||
let isPlaying = false;
|
||||
@@ -1694,34 +1730,37 @@
|
||||
}, 50);
|
||||
}
|
||||
});
|
||||
const handlePopState = (event) => {
|
||||
if (typeof window === "undefined") return;
|
||||
const statePath =
|
||||
event?.state && typeof event.state.path === "string"
|
||||
? event.state.path
|
||||
: null;
|
||||
const stateOriginal =
|
||||
event?.state && typeof event.state.originalPath === "string"
|
||||
? event.state.originalPath
|
||||
: null;
|
||||
if (statePath !== null) {
|
||||
currentPath = normalizePath(statePath);
|
||||
} else {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const paramPath = params.get("path");
|
||||
currentPath = normalizePath(paramPath || "");
|
||||
}
|
||||
if (stateOriginal !== null) {
|
||||
currentOriginalPath = normalizePath(stateOriginal);
|
||||
} else {
|
||||
currentOriginalPath = resolveOriginalPathForDisplay(
|
||||
currentPath,
|
||||
currentOriginalPath,
|
||||
);
|
||||
}
|
||||
selectedItems = new Set();
|
||||
activeMenu = null;
|
||||
const handlePopState = () => {
|
||||
syncFromLocation();
|
||||
};
|
||||
|
||||
const handleLocationChange = () => {
|
||||
syncFromLocation();
|
||||
};
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
const originalPushState = window.history.pushState;
|
||||
const originalReplaceState = window.history.replaceState;
|
||||
const triggerChange = () => {
|
||||
window.dispatchEvent(new Event("locationchange"));
|
||||
};
|
||||
window.history.pushState = function (...args) {
|
||||
originalPushState.apply(this, args);
|
||||
triggerChange();
|
||||
};
|
||||
window.history.replaceState = function (...args) {
|
||||
originalReplaceState.apply(this, args);
|
||||
triggerChange();
|
||||
};
|
||||
window.addEventListener("locationchange", handleLocationChange);
|
||||
window.addEventListener("popstate", handlePopState);
|
||||
unpatchHistory = () => {
|
||||
window.history.pushState = originalPushState;
|
||||
window.history.replaceState = originalReplaceState;
|
||||
window.removeEventListener("locationchange", handleLocationChange);
|
||||
window.removeEventListener("popstate", handlePopState);
|
||||
};
|
||||
}
|
||||
ws.onmessage = async (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data);
|
||||
@@ -1949,6 +1988,7 @@
|
||||
|
||||
window.removeEventListener("click", handleClickOutside);
|
||||
window.removeEventListener("popstate", handlePopState);
|
||||
if (unpatchHistory) unpatchHistory();
|
||||
|
||||
// Resize observer'ı temizle
|
||||
if (breadcrumbContainer) {
|
||||
|
||||
@@ -19,3 +19,9 @@ services:
|
||||
DEBUG_CPU: ${DEBUG_CPU}
|
||||
AUTO_PAUSE_ON_COMPLETE: ${AUTO_PAUSE_ON_COMPLETE}
|
||||
DISABLE_MEDIA_PROCESSING: ${DISABLE_MEDIA_PROCESSING}
|
||||
WEBDAV_ENABLED: ${WEBDAV_ENABLED}
|
||||
WEBDAV_USERNAME: ${WEBDAV_USERNAME}
|
||||
WEBDAV_PASSWORD: ${WEBDAV_PASSWORD}
|
||||
WEBDAV_PATH: ${WEBDAV_PATH}
|
||||
WEBDAV_READONLY: ${WEBDAV_READONLY}
|
||||
WEBDAV_INDEX_TTL: ${WEBDAV_INDEX_TTL}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"mime-types": "^2.1.35",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"node-fetch": "^3.3.2",
|
||||
"webdav-server": "^2.6.2",
|
||||
"webtorrent": "^1.9.7",
|
||||
"ws": "^8.18.0"
|
||||
}
|
||||
|
||||
887
server/server.js
887
server/server.js
@@ -5,6 +5,7 @@ import WebTorrent from "webtorrent";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import mime from "mime-types";
|
||||
import { v2 as webdav } from "webdav-server";
|
||||
import { fileURLToPath } from "url";
|
||||
import { exec, spawn } from "child_process";
|
||||
import crypto from "crypto"; // 🔒 basit token üretimi için
|
||||
@@ -28,6 +29,16 @@ const PORT = process.env.PORT || 3001;
|
||||
const DEBUG_CPU = process.env.DEBUG_CPU === "1";
|
||||
const DISABLE_MEDIA_PROCESSING = process.env.DISABLE_MEDIA_PROCESSING === "1";
|
||||
const AUTO_PAUSE_ON_COMPLETE = process.env.AUTO_PAUSE_ON_COMPLETE === "1";
|
||||
const WEBDAV_ENABLED = ["1", "true", "yes", "on"].includes(
|
||||
String(process.env.WEBDAV_ENABLED || "").toLowerCase()
|
||||
);
|
||||
const WEBDAV_USERNAME = process.env.WEBDAV_USERNAME || "";
|
||||
const WEBDAV_PASSWORD = process.env.WEBDAV_PASSWORD || "";
|
||||
const WEBDAV_PATH = process.env.WEBDAV_PATH || "/webdav";
|
||||
const WEBDAV_READONLY = !["0", "false", "no", "off"].includes(
|
||||
String(process.env.WEBDAV_READONLY || "1").toLowerCase()
|
||||
);
|
||||
const WEBDAV_INDEX_TTL = Number(process.env.WEBDAV_INDEX_TTL || 60000);
|
||||
|
||||
// --- İndirilen dosyalar için klasör oluştur ---
|
||||
const DOWNLOAD_DIR = path.join(__dirname, "downloads");
|
||||
@@ -52,6 +63,7 @@ const MOVIE_DATA_ROOT = path.join(CACHE_DIR, "movie_data");
|
||||
const TV_DATA_ROOT = path.join(CACHE_DIR, "tv_data");
|
||||
const ANIME_DATA_ROOT = path.join(CACHE_DIR, "anime_data");
|
||||
const YT_DATA_ROOT = path.join(CACHE_DIR, "yt_data");
|
||||
const WEBDAV_ROOT = path.join(CACHE_DIR, "webdav");
|
||||
const ANIME_ROOT_FOLDER = "_anime";
|
||||
const ROOT_TRASH_REGISTRY = path.join(CACHE_DIR, "root-trash.json");
|
||||
const MUSIC_EXTENSIONS = new Set([
|
||||
@@ -73,7 +85,8 @@ for (const dir of [
|
||||
MOVIE_DATA_ROOT,
|
||||
TV_DATA_ROOT,
|
||||
ANIME_DATA_ROOT,
|
||||
YT_DATA_ROOT
|
||||
YT_DATA_ROOT,
|
||||
WEBDAV_ROOT
|
||||
]) {
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
@@ -2572,6 +2585,433 @@ function resolveTvDataAbsolute(relPath) {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function sanitizeWebdavSegment(value) {
|
||||
if (!value) return "Unknown";
|
||||
return String(value)
|
||||
.replace(/[\\/:*?"<>|]+/g, "_")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function makeUniqueWebdavName(base, used, suffix = "") {
|
||||
let name = base || "Unknown";
|
||||
if (!used.has(name)) {
|
||||
used.add(name);
|
||||
return name;
|
||||
}
|
||||
const fallback = suffix ? `${name} [${suffix}]` : `${name} [${used.size}]`;
|
||||
if (!used.has(fallback)) {
|
||||
used.add(fallback);
|
||||
return fallback;
|
||||
}
|
||||
let counter = 2;
|
||||
while (used.has(`${fallback} ${counter}`)) counter += 1;
|
||||
const finalName = `${fallback} ${counter}`;
|
||||
used.add(finalName);
|
||||
return finalName;
|
||||
}
|
||||
|
||||
function ensureDirSync(dirPath) {
|
||||
if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
function createSymlinkSafe(targetPath, linkPath) {
|
||||
try {
|
||||
if (fs.existsSync(linkPath)) return;
|
||||
ensureDirSync(path.dirname(linkPath));
|
||||
let stat = null;
|
||||
try {
|
||||
stat = fs.statSync(targetPath);
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
if (stat?.isFile?.()) {
|
||||
try {
|
||||
fs.linkSync(targetPath, linkPath);
|
||||
return;
|
||||
} catch (err) {
|
||||
if (err?.code !== "EXDEV") {
|
||||
// Hardlink başarısızsa symlink'e düş
|
||||
fs.symlinkSync(targetPath, linkPath);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
fs.symlinkSync(targetPath, linkPath);
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ WebDAV link oluşturulamadı (${linkPath}): ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveMovieVideoAbsPath(metadata) {
|
||||
const dupe = metadata?._dupe || {};
|
||||
const rootFolder = sanitizeRelative(dupe.folder || "") || null;
|
||||
let videoPath = dupe.videoPath || metadata?.videoPath || null;
|
||||
if (!videoPath) return null;
|
||||
videoPath = String(videoPath).replace(/\\/g, "/").replace(/^\/+/, "");
|
||||
const hasRoot = rootFolder && videoPath.startsWith(`${rootFolder}/`);
|
||||
const absPath = hasRoot
|
||||
? path.join(DOWNLOAD_DIR, videoPath)
|
||||
: rootFolder
|
||||
? path.join(DOWNLOAD_DIR, rootFolder, videoPath)
|
||||
: path.join(DOWNLOAD_DIR, videoPath);
|
||||
return fs.existsSync(absPath) ? absPath : null;
|
||||
}
|
||||
|
||||
function resolveEpisodeAbsPath(rootFolder, episode) {
|
||||
if (!episode) return null;
|
||||
let videoPath = episode.videoPath || episode.file || "";
|
||||
if (!videoPath) return null;
|
||||
videoPath = String(videoPath).replace(/\\/g, "/").replace(/^\/+/, "");
|
||||
const candidates = [];
|
||||
|
||||
if (rootFolder === ANIME_ROOT_FOLDER) {
|
||||
candidates.push(path.join(DOWNLOAD_DIR, videoPath));
|
||||
if (videoPath.includes("/")) {
|
||||
candidates.push(path.join(DOWNLOAD_DIR, path.basename(videoPath)));
|
||||
}
|
||||
} else {
|
||||
if (rootFolder && !videoPath.startsWith(`${rootFolder}/`)) {
|
||||
candidates.push(path.join(DOWNLOAD_DIR, rootFolder, videoPath));
|
||||
}
|
||||
candidates.push(path.join(DOWNLOAD_DIR, videoPath));
|
||||
}
|
||||
|
||||
if (episode.file && episode.file !== videoPath) {
|
||||
const fallbackFile = String(episode.file)
|
||||
.replace(/\\/g, "/")
|
||||
.replace(/^\/+/, "");
|
||||
candidates.push(path.join(DOWNLOAD_DIR, fallbackFile));
|
||||
if (rootFolder && !fallbackFile.startsWith(`${rootFolder}/`)) {
|
||||
candidates.push(path.join(DOWNLOAD_DIR, rootFolder, fallbackFile));
|
||||
}
|
||||
}
|
||||
|
||||
for (const absPath of candidates) {
|
||||
if (fs.existsSync(absPath)) return absPath;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function rebuildWebdavIndex() {
|
||||
try {
|
||||
if (fs.existsSync(WEBDAV_ROOT)) {
|
||||
fs.rmSync(WEBDAV_ROOT, { recursive: true, force: true });
|
||||
}
|
||||
fs.mkdirSync(WEBDAV_ROOT, { recursive: true });
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ WebDAV kökü temizlenemedi (${WEBDAV_ROOT}): ${err.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const categoryDefs = [
|
||||
{ label: "Movies" },
|
||||
{ label: "TV Shows" },
|
||||
{ label: "Anime" }
|
||||
];
|
||||
|
||||
for (const cat of categoryDefs) {
|
||||
const dir = path.join(WEBDAV_ROOT, cat.label);
|
||||
ensureDirSync(dir);
|
||||
}
|
||||
|
||||
const isVideoExt = (value) =>
|
||||
VIDEO_EXTS.includes(String(value || "").toLowerCase());
|
||||
|
||||
const shouldSkip = (relPath) => {
|
||||
if (!relPath) return true;
|
||||
const normalized = relPath.replace(/\\/g, "/").replace(/^\/+/, "");
|
||||
const segments = normalized.split("/").filter(Boolean);
|
||||
if (!segments.length) return true;
|
||||
return segments.some((seg) => seg.startsWith("yt_"));
|
||||
};
|
||||
|
||||
const makeEpisodeCode = (seasonNum, episodeNum) => {
|
||||
const season = Number(seasonNum);
|
||||
const episode = Number(episodeNum);
|
||||
if (!Number.isFinite(season) || !Number.isFinite(episode)) return null;
|
||||
return `S${String(season).padStart(2, "0")}E${String(episode).padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
const getEpisodeNumber = (episodeKey, episode) => {
|
||||
const direct =
|
||||
episode?.episodeNumber ??
|
||||
episode?.number ??
|
||||
episode?.episode ??
|
||||
null;
|
||||
if (Number.isFinite(Number(direct))) return Number(direct);
|
||||
const match = String(episodeKey || "").match(/(\d{1,4})/);
|
||||
return match ? Number(match[1]) : null;
|
||||
};
|
||||
|
||||
// Movies
|
||||
try {
|
||||
const movieDirs = fs.readdirSync(MOVIE_DATA_ROOT, { withFileTypes: true });
|
||||
const usedMovieNames = new Set();
|
||||
for (const dirent of movieDirs) {
|
||||
if (!dirent.isDirectory()) continue;
|
||||
const metaPath = path.join(MOVIE_DATA_ROOT, dirent.name, "metadata.json");
|
||||
if (!fs.existsSync(metaPath)) continue;
|
||||
let metadata = null;
|
||||
try {
|
||||
metadata = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
||||
} catch (err) {
|
||||
continue;
|
||||
}
|
||||
const absVideo = resolveMovieVideoAbsPath(metadata);
|
||||
if (!absVideo) continue;
|
||||
if (shouldSkip(absVideo.replace(DOWNLOAD_DIR, ""))) continue;
|
||||
const title =
|
||||
metadata?.title ||
|
||||
metadata?.matched_title ||
|
||||
metadata?._dupe?.displayName ||
|
||||
dirent.name;
|
||||
const year =
|
||||
metadata?.release_date?.slice?.(0, 4) ||
|
||||
metadata?.matched_year ||
|
||||
metadata?.year ||
|
||||
null;
|
||||
const baseName = sanitizeWebdavSegment(
|
||||
year ? `${title} (${year})` : title
|
||||
);
|
||||
const uniqueName = makeUniqueWebdavName(
|
||||
baseName,
|
||||
usedMovieNames,
|
||||
metadata?.id || dirent.name
|
||||
);
|
||||
const movieDir = path.join(WEBDAV_ROOT, "Movies", uniqueName);
|
||||
ensureDirSync(movieDir);
|
||||
const ext = path.extname(absVideo);
|
||||
const fileName = sanitizeWebdavSegment(`${uniqueName}${ext}`);
|
||||
const linkPath = path.join(movieDir, fileName);
|
||||
createSymlinkSafe(absVideo, linkPath);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ WebDAV movie index oluşturulamadı: ${err.message}`);
|
||||
}
|
||||
|
||||
const buildSeriesCategory = (
|
||||
dataRoot,
|
||||
categoryLabel,
|
||||
usedShowNames,
|
||||
coveredRoots
|
||||
) => {
|
||||
try {
|
||||
const dirs = fs.readdirSync(dataRoot, { withFileTypes: true });
|
||||
for (const dirent of dirs) {
|
||||
if (!dirent.isDirectory()) continue;
|
||||
const seriesDir = path.join(dataRoot, dirent.name);
|
||||
const seriesPath = path.join(seriesDir, "series.json");
|
||||
if (!fs.existsSync(seriesPath)) continue;
|
||||
let seriesData = null;
|
||||
try {
|
||||
seriesData = JSON.parse(fs.readFileSync(seriesPath, "utf-8"));
|
||||
} catch (err) {
|
||||
continue;
|
||||
}
|
||||
const { rootFolder } = parseTvSeriesKey(dirent.name);
|
||||
if (coveredRoots) coveredRoots.add(rootFolder);
|
||||
const showTitle = sanitizeWebdavSegment(
|
||||
seriesData?.name || seriesData?.title || dirent.name
|
||||
);
|
||||
const uniqueShow = makeUniqueWebdavName(
|
||||
showTitle,
|
||||
usedShowNames,
|
||||
seriesData?.id || dirent.name
|
||||
);
|
||||
const showDir = path.join(WEBDAV_ROOT, categoryLabel, uniqueShow);
|
||||
const seasons = seriesData?.seasons || {};
|
||||
let createdSeasonCount = 0;
|
||||
for (const seasonKey of Object.keys(seasons)) {
|
||||
const season = seasons[seasonKey];
|
||||
if (!season?.episodes) continue;
|
||||
const seasonNumber =
|
||||
season?.seasonNumber ?? Number(seasonKey) ?? null;
|
||||
const seasonLabel = seasonNumber
|
||||
? `Season ${String(seasonNumber).padStart(2, "0")}`
|
||||
: "Season";
|
||||
const seasonDir = path.join(showDir, seasonLabel);
|
||||
let createdEpisodeCount = 0;
|
||||
for (const episodeKey of Object.keys(season.episodes)) {
|
||||
const episode = season.episodes[episodeKey];
|
||||
const absVideo = resolveEpisodeAbsPath(rootFolder, episode);
|
||||
if (!absVideo) continue;
|
||||
if (shouldSkip(absVideo.replace(DOWNLOAD_DIR, ""))) continue;
|
||||
const ext = path.extname(absVideo);
|
||||
if (!isVideoExt(ext)) continue;
|
||||
if (createdEpisodeCount === 0) {
|
||||
ensureDirSync(seasonDir);
|
||||
ensureDirSync(showDir);
|
||||
createdSeasonCount += 1;
|
||||
}
|
||||
const episodeNumber = getEpisodeNumber(episodeKey, episode);
|
||||
const code = makeEpisodeCode(seasonNumber, episodeNumber);
|
||||
const safeCode =
|
||||
code ||
|
||||
`S${String(seasonNumber || 0).padStart(2, "0")}E${String(
|
||||
Number(episodeNumber) || 0
|
||||
).padStart(2, "0")}`;
|
||||
const fileName = sanitizeWebdavSegment(
|
||||
`${uniqueShow} - ${safeCode}${ext}`
|
||||
);
|
||||
const linkPath = path.join(seasonDir, fileName);
|
||||
createSymlinkSafe(absVideo, linkPath);
|
||||
createdEpisodeCount += 1;
|
||||
}
|
||||
}
|
||||
if (createdSeasonCount === 0 && fs.existsSync(showDir)) {
|
||||
fs.rmSync(showDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ WebDAV ${categoryLabel} index oluşturulamadı: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const tvUsedShowNames = new Set();
|
||||
const animeUsedShowNames = new Set();
|
||||
const coveredTvRoots = new Set();
|
||||
buildSeriesCategory(TV_DATA_ROOT, "TV Shows", tvUsedShowNames, coveredTvRoots);
|
||||
buildSeriesCategory(ANIME_DATA_ROOT, "Anime", animeUsedShowNames, null);
|
||||
|
||||
const buildSeriesFromInfoJson = (categoryLabel, usedShowNames, coveredRoots) => {
|
||||
try {
|
||||
const roots = fs.readdirSync(DOWNLOAD_DIR, { withFileTypes: true });
|
||||
for (const dirent of roots) {
|
||||
if (!dirent.isDirectory()) continue;
|
||||
const rootFolder = dirent.name;
|
||||
if (coveredRoots?.has(rootFolder)) continue;
|
||||
const infoPath = path.join(DOWNLOAD_DIR, rootFolder, "info.json");
|
||||
if (!fs.existsSync(infoPath)) continue;
|
||||
let info = null;
|
||||
try {
|
||||
info = JSON.parse(fs.readFileSync(infoPath, "utf-8"));
|
||||
} catch (err) {
|
||||
continue;
|
||||
}
|
||||
const episodes = info?.seriesEpisodes;
|
||||
if (!episodes || typeof episodes !== "object") continue;
|
||||
|
||||
const showBuckets = new Map();
|
||||
for (const [relPath, episode] of Object.entries(episodes)) {
|
||||
if (!episode) continue;
|
||||
if (shouldSkip(relPath)) continue;
|
||||
const absVideo = path.join(DOWNLOAD_DIR, rootFolder, relPath);
|
||||
if (!fs.existsSync(absVideo)) continue;
|
||||
const ext = path.extname(absVideo).toLowerCase();
|
||||
if (!isVideoExt(ext)) continue;
|
||||
|
||||
const showTitleRaw =
|
||||
episode.showTitle || info?.name || rootFolder || "Unknown";
|
||||
const showKey = `${episode.showId || ""}__${showTitleRaw}`;
|
||||
if (!showBuckets.has(showKey)) {
|
||||
showBuckets.set(showKey, {
|
||||
title: showTitleRaw,
|
||||
episodes: []
|
||||
});
|
||||
}
|
||||
showBuckets.get(showKey).episodes.push({
|
||||
absVideo,
|
||||
relPath,
|
||||
season: episode.season,
|
||||
episode: episode.episode,
|
||||
key: episode.key
|
||||
});
|
||||
}
|
||||
|
||||
for (const bucket of showBuckets.values()) {
|
||||
const showTitle = sanitizeWebdavSegment(bucket.title);
|
||||
const uniqueShow = makeUniqueWebdavName(
|
||||
showTitle,
|
||||
usedShowNames,
|
||||
bucket.title
|
||||
);
|
||||
const showDir = path.join(WEBDAV_ROOT, categoryLabel, uniqueShow);
|
||||
let createdSeasonCount = 0;
|
||||
for (const entry of bucket.episodes) {
|
||||
const seasonNumber = Number(entry.season);
|
||||
const episodeNumber = Number(entry.episode);
|
||||
const seasonLabel = Number.isFinite(seasonNumber)
|
||||
? `Season ${String(seasonNumber).padStart(2, "0")}`
|
||||
: "Season";
|
||||
const seasonDir = path.join(showDir, seasonLabel);
|
||||
if (createdSeasonCount === 0) {
|
||||
ensureDirSync(showDir);
|
||||
}
|
||||
if (!fs.existsSync(seasonDir)) {
|
||||
ensureDirSync(seasonDir);
|
||||
createdSeasonCount += 1;
|
||||
}
|
||||
const ext = path.extname(entry.absVideo);
|
||||
const code =
|
||||
entry.key ||
|
||||
makeEpisodeCode(seasonNumber, episodeNumber) ||
|
||||
`S${String(seasonNumber || 0).padStart(2, "0")}E${String(
|
||||
Number(episodeNumber) || 0
|
||||
).padStart(2, "0")}`;
|
||||
const fileName = sanitizeWebdavSegment(
|
||||
`${uniqueShow} - ${code}${ext}`
|
||||
);
|
||||
const linkPath = path.join(seasonDir, fileName);
|
||||
createSymlinkSafe(entry.absVideo, linkPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ WebDAV ${categoryLabel} info.json index oluşturulamadı: ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
buildSeriesFromInfoJson("TV Shows", tvUsedShowNames, coveredTvRoots);
|
||||
}
|
||||
|
||||
let webdavIndexLast = 0;
|
||||
let webdavIndexBuilding = false;
|
||||
async function ensureWebdavIndexFresh() {
|
||||
if (!WEBDAV_ENABLED) return;
|
||||
const now = Date.now();
|
||||
if (webdavIndexBuilding) return;
|
||||
if (now - webdavIndexLast < WEBDAV_INDEX_TTL) return;
|
||||
webdavIndexBuilding = true;
|
||||
try {
|
||||
rebuildWebdavIndex();
|
||||
webdavIndexLast = Date.now();
|
||||
} finally {
|
||||
webdavIndexBuilding = false;
|
||||
}
|
||||
}
|
||||
|
||||
function webdavAuthMiddleware(req, res, next) {
|
||||
if (!WEBDAV_ENABLED) return res.status(404).end();
|
||||
const authHeader = req.headers.authorization || "";
|
||||
if (!authHeader.startsWith("Basic ")) {
|
||||
res.setHeader("WWW-Authenticate", "Basic realm=\"Dupe WebDAV\"");
|
||||
return res.status(401).end();
|
||||
}
|
||||
const raw = Buffer.from(authHeader.slice(6), "base64")
|
||||
.toString("utf-8")
|
||||
.split(":");
|
||||
const user = raw.shift() || "";
|
||||
const pass = raw.join(":");
|
||||
if (!WEBDAV_USERNAME || !WEBDAV_PASSWORD) {
|
||||
return res.status(500).end();
|
||||
}
|
||||
if (user !== WEBDAV_USERNAME || pass !== WEBDAV_PASSWORD) {
|
||||
res.setHeader("WWW-Authenticate", "Basic realm=\"Dupe WebDAV\"");
|
||||
return res.status(401).end();
|
||||
}
|
||||
return next();
|
||||
}
|
||||
|
||||
function webdavReadonlyGuard(req, res, next) {
|
||||
if (!WEBDAV_READONLY) return next();
|
||||
const allowed = new Set(["GET", "HEAD", "OPTIONS", "PROPFIND"]);
|
||||
if (!allowed.has(req.method)) {
|
||||
return res.status(403).end();
|
||||
}
|
||||
return next();
|
||||
}
|
||||
|
||||
function serveCachedFile(req, res, filePath, { maxAgeSeconds = 86400 } = {}) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return res.status(404).send("Dosya bulunamadı");
|
||||
@@ -4612,6 +5052,316 @@ function moveInfoDataBetweenRoots(oldRoot, newRoot, oldRel, newRel, isDirectory)
|
||||
return true;
|
||||
}
|
||||
|
||||
function collectSeriesIdsForPath(info, oldRel, isDirectory) {
|
||||
const ids = new Set();
|
||||
if (!info || typeof info !== "object") return ids;
|
||||
const normalizedOldRel = normalizeTrashPath(oldRel);
|
||||
const shouldMatch = (key) => {
|
||||
if (!normalizedOldRel) return true;
|
||||
const normalizedKey = normalizeTrashPath(key);
|
||||
if (normalizedKey === normalizedOldRel) return true;
|
||||
return (
|
||||
isDirectory &&
|
||||
normalizedOldRel &&
|
||||
normalizedKey.startsWith(`${normalizedOldRel}/`)
|
||||
);
|
||||
};
|
||||
|
||||
const episodes = info.seriesEpisodes || {};
|
||||
for (const [key, value] of Object.entries(episodes)) {
|
||||
if (!shouldMatch(key)) continue;
|
||||
const id = value?.showId ?? value?.id ?? null;
|
||||
if (id) ids.add(id);
|
||||
}
|
||||
|
||||
const files = info.files || {};
|
||||
for (const [key, value] of Object.entries(files)) {
|
||||
if (!shouldMatch(key)) continue;
|
||||
const id = value?.seriesMatch?.id ?? null;
|
||||
if (id) ids.add(id);
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
function collectMovieRelPathsForMove(info, oldRel, isDirectory) {
|
||||
const relPaths = new Set();
|
||||
if (!info || typeof info !== "object") return relPaths;
|
||||
const normalizedOldRel = normalizeTrashPath(oldRel);
|
||||
const shouldMatch = (key) => {
|
||||
if (!normalizedOldRel) return true;
|
||||
const normalizedKey = normalizeTrashPath(key);
|
||||
if (normalizedKey === normalizedOldRel) return true;
|
||||
return (
|
||||
isDirectory &&
|
||||
normalizedOldRel &&
|
||||
normalizedKey.startsWith(`${normalizedOldRel}/`)
|
||||
);
|
||||
};
|
||||
|
||||
if (info.primaryVideoPath && shouldMatch(info.primaryVideoPath)) {
|
||||
relPaths.add(normalizeTrashPath(info.primaryVideoPath));
|
||||
}
|
||||
|
||||
const files = info.files || {};
|
||||
for (const [key, value] of Object.entries(files)) {
|
||||
if (!value?.movieMatch) continue;
|
||||
if (!shouldMatch(key)) continue;
|
||||
relPaths.add(normalizeTrashPath(key));
|
||||
}
|
||||
|
||||
return relPaths;
|
||||
}
|
||||
|
||||
function mapRelPathForMove(oldRel, newRel, relPath, isDirectory) {
|
||||
const normalizedOldRel = normalizeTrashPath(oldRel);
|
||||
const normalizedNewRel = normalizeTrashPath(newRel);
|
||||
const normalizedRel = normalizeTrashPath(relPath);
|
||||
if (!normalizedOldRel) return normalizedRel;
|
||||
if (normalizedRel === normalizedOldRel) return normalizedNewRel;
|
||||
if (
|
||||
isDirectory &&
|
||||
normalizedRel.startsWith(`${normalizedOldRel}/`)
|
||||
) {
|
||||
const suffix = normalizedRel.slice(normalizedOldRel.length).replace(/^\/+/, "");
|
||||
return normalizedNewRel
|
||||
? `${normalizedNewRel}${suffix ? `/${suffix}` : ""}`
|
||||
: suffix;
|
||||
}
|
||||
return normalizedRel;
|
||||
}
|
||||
|
||||
function moveMovieDataDir(oldKey, newKey, oldRoot, newRoot, newRelPath) {
|
||||
if (!oldKey || !newKey) return false;
|
||||
if (oldKey === newKey) return false;
|
||||
const oldDir = movieDataPathsByKey(oldKey).dir;
|
||||
const newDir = movieDataPathsByKey(newKey).dir;
|
||||
if (!fs.existsSync(oldDir)) return false;
|
||||
if (fs.existsSync(newDir)) {
|
||||
try {
|
||||
fs.rmSync(newDir, { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ Movie metadata hedefi temizlenemedi (${newDir}): ${err.message}`);
|
||||
}
|
||||
}
|
||||
try {
|
||||
fs.renameSync(oldDir, newDir);
|
||||
} catch (err) {
|
||||
try {
|
||||
fs.cpSync(oldDir, newDir, { recursive: true });
|
||||
fs.rmSync(oldDir, { recursive: true, force: true });
|
||||
} catch (copyErr) {
|
||||
console.warn(`⚠️ Movie metadata taşınamadı (${oldDir}): ${copyErr.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const metadataPath = path.join(newDir, "metadata.json");
|
||||
if (fs.existsSync(metadataPath)) {
|
||||
try {
|
||||
const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
|
||||
if (metadata?._dupe) {
|
||||
metadata._dupe.folder = newRoot;
|
||||
metadata._dupe.videoPath = newRelPath;
|
||||
metadata._dupe.cacheKey = newKey;
|
||||
}
|
||||
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ movie metadata güncellenemedi (${metadataPath}): ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function moveMovieDataBetweenRoots(oldRoot, newRoot, oldRel, newRel, relPaths, isDirectory) {
|
||||
if (!oldRoot || !newRoot) return false;
|
||||
if (!relPaths || relPaths.size === 0) return false;
|
||||
let movedAny = false;
|
||||
for (const relPath of relPaths) {
|
||||
const mappedRel = mapRelPathForMove(oldRel, newRel, relPath, isDirectory);
|
||||
const oldKey = movieDataKey(oldRoot, relPath);
|
||||
const newKey = movieDataKey(newRoot, mappedRel);
|
||||
if (moveMovieDataDir(oldKey, newKey, oldRoot, newRoot, mappedRel)) {
|
||||
movedAny = true;
|
||||
}
|
||||
}
|
||||
return movedAny;
|
||||
}
|
||||
|
||||
function moveMovieDataWithinRoot(rootFolder, oldRel, newRel, relPaths, isDirectory) {
|
||||
if (!rootFolder) return false;
|
||||
if (!relPaths || relPaths.size === 0) return false;
|
||||
let movedAny = false;
|
||||
for (const relPath of relPaths) {
|
||||
const mappedRel = mapRelPathForMove(oldRel, newRel, relPath, isDirectory);
|
||||
const oldKey = movieDataKey(rootFolder, relPath);
|
||||
const newKey = movieDataKey(rootFolder, mappedRel);
|
||||
if (moveMovieDataDir(oldKey, newKey, rootFolder, rootFolder, mappedRel)) {
|
||||
movedAny = true;
|
||||
}
|
||||
}
|
||||
return movedAny;
|
||||
}
|
||||
|
||||
function updateSeriesJsonAfterRootMove(seriesData, oldRoot, newRoot, oldRel, newRel) {
|
||||
if (!seriesData || typeof seriesData !== "object") return false;
|
||||
let changed = false;
|
||||
const oldKey = seriesData?._dupe?.key || null;
|
||||
if (seriesData._dupe) {
|
||||
seriesData._dupe.folder = newRoot;
|
||||
seriesData._dupe.key = tvSeriesKey(newRoot, seriesData._dupe.seriesId);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
const encodeKey = (key) =>
|
||||
String(key || "")
|
||||
.split(path.sep)
|
||||
.map(encodeURIComponent)
|
||||
.join("/");
|
||||
const oldKeyEncoded = oldKey ? encodeKey(oldKey) : null;
|
||||
const newKeyEncoded = seriesData?._dupe?.key
|
||||
? encodeKey(seriesData._dupe.key)
|
||||
: null;
|
||||
const oldPrefix = oldKeyEncoded ? `/tv-data/${oldKeyEncoded}/` : null;
|
||||
const newPrefix = newKeyEncoded ? `/tv-data/${newKeyEncoded}/` : null;
|
||||
const replaceTvDataPath = (value) => {
|
||||
if (!value || !oldPrefix || !newPrefix || typeof value !== "string") {
|
||||
return value;
|
||||
}
|
||||
if (value.includes(oldPrefix)) {
|
||||
changed = true;
|
||||
return value.replace(oldPrefix, newPrefix);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
if (seriesData.poster) seriesData.poster = replaceTvDataPath(seriesData.poster);
|
||||
if (seriesData.backdrop)
|
||||
seriesData.backdrop = replaceTvDataPath(seriesData.backdrop);
|
||||
|
||||
const oldRelNorm = normalizeTrashPath(oldRel);
|
||||
const newRelNorm = normalizeTrashPath(newRel);
|
||||
const shouldTransform = (value) => {
|
||||
const normalized = normalizeTrashPath(value);
|
||||
if (!oldRelNorm) return true;
|
||||
if (normalized === oldRelNorm) return true;
|
||||
return (
|
||||
oldRelNorm && normalized.startsWith(`${oldRelNorm}/`)
|
||||
);
|
||||
};
|
||||
const transformRel = (value) => {
|
||||
const normalized = normalizeTrashPath(value);
|
||||
if (!shouldTransform(normalized)) return value;
|
||||
const suffix = oldRelNorm
|
||||
? normalized.slice(oldRelNorm.length).replace(/^\/+/, "")
|
||||
: normalized;
|
||||
const next = newRelNorm ? `${newRelNorm}${suffix ? `/${suffix}` : ""}` : suffix;
|
||||
if (next !== value) changed = true;
|
||||
return next;
|
||||
};
|
||||
|
||||
const seasons = seriesData?.seasons || {};
|
||||
for (const season of Object.values(seasons)) {
|
||||
if (!season) continue;
|
||||
if (season.poster) season.poster = replaceTvDataPath(season.poster);
|
||||
if (!season.episodes) continue;
|
||||
for (const episode of Object.values(season.episodes)) {
|
||||
if (!episode) continue;
|
||||
if (episode.still) episode.still = replaceTvDataPath(episode.still);
|
||||
if (episode.file) {
|
||||
const nextFile = transformRel(episode.file);
|
||||
if (nextFile !== episode.file) {
|
||||
episode.file = nextFile;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (episode.videoPath) {
|
||||
const video = String(episode.videoPath).replace(/\\/g, "/");
|
||||
if (video.startsWith(`${oldRoot}/`)) {
|
||||
episode.videoPath = `${newRoot}/${video.slice(oldRoot.length + 1)}`;
|
||||
changed = true;
|
||||
} else {
|
||||
const nextVideo = transformRel(video);
|
||||
if (nextVideo !== video) {
|
||||
episode.videoPath = nextVideo;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
function moveSeriesDataBetweenRoots(oldRoot, newRoot, oldRel, newRel, seriesIds) {
|
||||
if (!oldRoot || !newRoot) return false;
|
||||
if (!seriesIds || !seriesIds.size) return false;
|
||||
|
||||
let movedAny = false;
|
||||
for (const seriesId of seriesIds) {
|
||||
if (!seriesId) continue;
|
||||
const oldPaths = tvSeriesPaths(oldRoot, seriesId);
|
||||
if (!oldPaths || !fs.existsSync(oldPaths.dir)) continue;
|
||||
const newKey = tvSeriesKey(newRoot, seriesId);
|
||||
if (!newKey) continue;
|
||||
const newPaths = tvSeriesPathsByKey(newKey);
|
||||
if (!newPaths) continue;
|
||||
|
||||
if (fs.existsSync(newPaths.dir)) {
|
||||
try {
|
||||
fs.rmSync(newPaths.dir, { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ TV metadata hedefi temizlenemedi (${newPaths.dir}): ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
fs.renameSync(oldPaths.dir, newPaths.dir);
|
||||
} catch (err) {
|
||||
try {
|
||||
fs.cpSync(oldPaths.dir, newPaths.dir, { recursive: true });
|
||||
fs.rmSync(oldPaths.dir, { recursive: true, force: true });
|
||||
} catch (copyErr) {
|
||||
console.warn(`⚠️ TV metadata taşınamadı (${oldPaths.dir}): ${copyErr.message}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const metadataPath = path.join(newPaths.dir, "series.json");
|
||||
if (fs.existsSync(metadataPath)) {
|
||||
try {
|
||||
const seriesData = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
|
||||
const changed = updateSeriesJsonAfterRootMove(
|
||||
seriesData,
|
||||
oldRoot,
|
||||
newRoot,
|
||||
oldRel,
|
||||
newRel
|
||||
);
|
||||
if (changed) {
|
||||
fs.writeFileSync(
|
||||
metadataPath,
|
||||
JSON.stringify(seriesData, null, 2),
|
||||
"utf-8"
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ series.json güncellenemedi (${metadataPath}): ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
movedAny = true;
|
||||
}
|
||||
|
||||
if (movedAny) {
|
||||
renameSeriesDataPaths(newRoot, oldRel, newRel);
|
||||
}
|
||||
|
||||
return movedAny;
|
||||
}
|
||||
|
||||
function renameInfoPaths(rootFolder, oldRel, newRel) {
|
||||
if (!rootFolder) return;
|
||||
const info = readInfoForRoot(rootFolder);
|
||||
@@ -6161,6 +6911,18 @@ app.post("/api/file/move", requireAuth, (req, res) => {
|
||||
.json({ error: "Kök klasör bu yöntemle taşınamaz" });
|
||||
}
|
||||
|
||||
const preMoveInfo = sourceRoot ? readInfoForRoot(sourceRoot) : null;
|
||||
const affectedSeriesIds = collectSeriesIdsForPath(
|
||||
preMoveInfo,
|
||||
sourceRelWithinRoot,
|
||||
isDirectory
|
||||
);
|
||||
const affectedMovieRelPaths = collectMovieRelPathsForMove(
|
||||
preMoveInfo,
|
||||
sourceRelWithinRoot,
|
||||
isDirectory
|
||||
);
|
||||
|
||||
fs.renameSync(sourceFullPath, newFullPath);
|
||||
|
||||
const sameRoot =
|
||||
@@ -6172,6 +6934,15 @@ app.post("/api/file/move", requireAuth, (req, res) => {
|
||||
renameInfoPaths(sourceRoot, sourceRelWithinRoot, destRelWithinRoot);
|
||||
renameSeriesDataPaths(sourceRoot, sourceRelWithinRoot, destRelWithinRoot);
|
||||
renameTrashEntries(sourceRoot, sourceRelWithinRoot, destRelWithinRoot);
|
||||
if (affectedMovieRelPaths.size) {
|
||||
moveMovieDataWithinRoot(
|
||||
sourceRoot,
|
||||
sourceRelWithinRoot,
|
||||
destRelWithinRoot,
|
||||
affectedMovieRelPaths,
|
||||
isDirectory
|
||||
);
|
||||
}
|
||||
if (isDirectory) {
|
||||
removeThumbnailsForDirectory(sourceRoot, sourceRelWithinRoot);
|
||||
} else {
|
||||
@@ -6187,6 +6958,25 @@ app.post("/api/file/move", requireAuth, (req, res) => {
|
||||
destRelWithinRoot,
|
||||
isDirectory
|
||||
);
|
||||
if (affectedSeriesIds.size) {
|
||||
moveSeriesDataBetweenRoots(
|
||||
sourceRoot,
|
||||
destRoot,
|
||||
sourceRelWithinRoot,
|
||||
destRelWithinRoot,
|
||||
affectedSeriesIds
|
||||
);
|
||||
}
|
||||
if (affectedMovieRelPaths.size) {
|
||||
moveMovieDataBetweenRoots(
|
||||
sourceRoot,
|
||||
destRoot,
|
||||
sourceRelWithinRoot,
|
||||
destRelWithinRoot,
|
||||
affectedMovieRelPaths,
|
||||
isDirectory
|
||||
);
|
||||
}
|
||||
if (isDirectory) {
|
||||
removeThumbnailsForDirectory(sourceRoot, sourceRelWithinRoot);
|
||||
} else {
|
||||
@@ -6775,7 +7565,7 @@ app.get("/api/movies", requireAuth, (req, res) => {
|
||||
.readdirSync(MOVIE_DATA_ROOT, { withFileTypes: true })
|
||||
.filter((d) => d.isDirectory());
|
||||
|
||||
const movies = entries
|
||||
const rawMovies = entries
|
||||
.map((dirent) => {
|
||||
const key = dirent.name;
|
||||
const paths = movieDataPathsByKey(key);
|
||||
@@ -6797,6 +7587,17 @@ app.get("/api/movies", requireAuth, (req, res) => {
|
||||
const dupe = metadata._dupe || {};
|
||||
const rootFolder = dupe.folder || key;
|
||||
const videoPath = dupe.videoPath || metadata.videoPath || null;
|
||||
const absVideo = resolveMovieVideoAbsPath(metadata);
|
||||
if (!absVideo) {
|
||||
try {
|
||||
fs.rmSync(paths.dir, { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`⚠️ Movie metadata temizlenemedi (${paths.dir}): ${err.message}`
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const encodedKey = key
|
||||
.split(path.sep)
|
||||
.map(encodeURIComponent)
|
||||
@@ -6815,6 +7616,14 @@ app.get("/api/movies", requireAuth, (req, res) => {
|
||||
: null);
|
||||
const cacheKey = paths.key;
|
||||
return {
|
||||
_absVideo: absVideo,
|
||||
_cacheDir: paths.dir,
|
||||
_cacheKey: cacheKey,
|
||||
_dedupeKey:
|
||||
typeof metadata.id === "number" && metadata.id
|
||||
? `id:${metadata.id}`
|
||||
: `title:${(metadata.title || metadata.matched_title || rootFolder)
|
||||
.toLowerCase()}-${year || "unknown"}`,
|
||||
folder: rootFolder,
|
||||
cacheKey,
|
||||
id:
|
||||
@@ -6849,6 +7658,51 @@ app.get("/api/movies", requireAuth, (req, res) => {
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const dedupedMap = new Map();
|
||||
for (const item of rawMovies) {
|
||||
const key = item._dedupeKey;
|
||||
if (!dedupedMap.has(key)) {
|
||||
dedupedMap.set(key, item);
|
||||
continue;
|
||||
}
|
||||
const existing = dedupedMap.get(key);
|
||||
const existingScore =
|
||||
existing?.metadata?._dupe?.fetchedAt ||
|
||||
existing?.metadata?._dupe?.matchedAt ||
|
||||
existing?.metadata?.matchedAt ||
|
||||
0;
|
||||
const nextScore =
|
||||
item?.metadata?._dupe?.fetchedAt ||
|
||||
item?.metadata?._dupe?.matchedAt ||
|
||||
item?.metadata?.matchedAt ||
|
||||
0;
|
||||
if (nextScore > existingScore) {
|
||||
if (existing?._cacheDir) {
|
||||
try {
|
||||
fs.rmSync(existing._cacheDir, { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`⚠️ Movie metadata temizlenemedi (${existing._cacheDir}): ${err.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
dedupedMap.set(key, item);
|
||||
} else if (item?._cacheDir) {
|
||||
try {
|
||||
fs.rmSync(item._cacheDir, { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`⚠️ Movie metadata temizlenemedi (${item._cacheDir}): ${err.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const movies = Array.from(dedupedMap.values()).map((item) => {
|
||||
const { _absVideo, _cacheDir, _cacheKey, _dedupeKey, ...rest } = item;
|
||||
return rest;
|
||||
});
|
||||
|
||||
movies.sort((a, b) => {
|
||||
const yearA = a.year || 0;
|
||||
const yearB = b.year || 0;
|
||||
@@ -8479,6 +9333,35 @@ if (restored.length) {
|
||||
console.log(`♻️ ${restored.length} torrent yeniden eklendi.`);
|
||||
}
|
||||
|
||||
// --- 📁 WebDAV (Infuse) ---
|
||||
if (WEBDAV_ENABLED) {
|
||||
const webdavServer = new webdav.WebDAVServer({
|
||||
strictMode: false
|
||||
});
|
||||
const webdavBasePath = WEBDAV_PATH.startsWith("/")
|
||||
? WEBDAV_PATH
|
||||
: `/${WEBDAV_PATH}`;
|
||||
const userManager = new webdav.SimpleUserManager();
|
||||
if (WEBDAV_USERNAME && WEBDAV_PASSWORD) {
|
||||
userManager.addUser(WEBDAV_USERNAME, WEBDAV_PASSWORD, false);
|
||||
}
|
||||
webdavServer.httpAuthentication = new webdav.HTTPBasicAuthentication(
|
||||
userManager,
|
||||
"Dupe WebDAV"
|
||||
);
|
||||
webdavServer.setFileSystem("/", new webdav.PhysicalFileSystem(WEBDAV_ROOT));
|
||||
|
||||
app.use(
|
||||
webdavBasePath,
|
||||
webdavAuthMiddleware,
|
||||
webdavReadonlyGuard,
|
||||
async (req, res) => {
|
||||
await ensureWebdavIndexFresh();
|
||||
webdavServer.executeRequest(req, res);
|
||||
}
|
||||
);
|
||||
console.log(`📁 WebDAV aktif: ${webdavBasePath}`);
|
||||
}
|
||||
|
||||
// --- ✅ Client build (frontend) dosyalarını sun ---
|
||||
const publicDir = path.join(__dirname, "public");
|
||||
|
||||
Reference in New Issue
Block a user