feat(webdav): dizi metadatasını taşıma desteği ekle

Diziler ve bölümler kökler arası taşınırken ilişkili metadata
dosyalarının (.tvmetadata, series.json) güncellenmesini sağlar.
collectSeriesIdsForPath ile etkilenen dizileri tespit eder,
moveSeriesDataBetweenRoots ile metadata klasörlerini taşır ve
updateSeriesJsonAfterRootMove ile içindeki yolları günceller.
This commit is contained in:
2026-02-01 17:35:53 +03:00
parent 66aa99f0f7
commit e20b3ad591

View File

@@ -5052,6 +5052,196 @@ function moveInfoDataBetweenRoots(oldRoot, newRoot, oldRel, newRel, isDirectory)
return true; 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 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) { function renameInfoPaths(rootFolder, oldRel, newRel) {
if (!rootFolder) return; if (!rootFolder) return;
const info = readInfoForRoot(rootFolder); const info = readInfoForRoot(rootFolder);
@@ -6601,6 +6791,13 @@ app.post("/api/file/move", requireAuth, (req, res) => {
.json({ error: "Kök klasör bu yöntemle taşınamaz" }); .json({ error: "Kök klasör bu yöntemle taşınamaz" });
} }
const preMoveInfo = sourceRoot ? readInfoForRoot(sourceRoot) : null;
const affectedSeriesIds = collectSeriesIdsForPath(
preMoveInfo,
sourceRelWithinRoot,
isDirectory
);
fs.renameSync(sourceFullPath, newFullPath); fs.renameSync(sourceFullPath, newFullPath);
const sameRoot = const sameRoot =
@@ -6627,6 +6824,15 @@ app.post("/api/file/move", requireAuth, (req, res) => {
destRelWithinRoot, destRelWithinRoot,
isDirectory isDirectory
); );
if (affectedSeriesIds.size) {
moveSeriesDataBetweenRoots(
sourceRoot,
destRoot,
sourceRelWithinRoot,
destRelWithinRoot,
affectedSeriesIds
);
}
if (isDirectory) { if (isDirectory) {
removeThumbnailsForDirectory(sourceRoot, sourceRelWithinRoot); removeThumbnailsForDirectory(sourceRoot, sourceRelWithinRoot);
} else { } else {