feat(tv): Kanonik TVDB anahtarları ve çoklu kök klasör desteği eklendi
Birden fazla kök klasör arasında veri birleştirmeyi sağlamak için TVDB kimliklerini kullanan TV dizileri için kanonik anahtar sistemi uygulandı. Kullanıcı arayüzünde reaktif yükleme eklendi ve eski yollardan otomatik geçişle meta veri yönetimi geliştirildi. Önemli Değişiklikler: - TV dizisi veri yapısı artık dizi başına birden fazla kök klasörü destekliyor - Eski klasör anahtarları otomatik olarak kanonik TVDB anahtarlarına taşınıyor - Veritabanı şeması, rootFolders dizisi için yeni indekslerle güncellendi
This commit is contained in:
@@ -1,12 +1,19 @@
|
||||
import { connectMongo } from "./db.js";
|
||||
|
||||
const COLLECTION = "tv_data";
|
||||
const TVDB_KEY_PREFIX = "tvdb-";
|
||||
|
||||
function canonicalTvdbKey(tvdbId) {
|
||||
if (tvdbId === null || tvdbId === undefined) return null;
|
||||
return `${TVDB_KEY_PREFIX}${tvdbId}`;
|
||||
}
|
||||
|
||||
async function getCollection() {
|
||||
const { db } = await connectMongo();
|
||||
const col = db.collection(COLLECTION);
|
||||
await col.createIndex({ rootFolder: 1 });
|
||||
await col.createIndex({ tvdbId: 1 });
|
||||
await col.createIndex({ rootFolders: 1 });
|
||||
await col.createIndex({ tvdbId: 1 }, { sparse: true });
|
||||
await col.createIndex({ updatedAt: -1 });
|
||||
return col;
|
||||
}
|
||||
@@ -17,6 +24,7 @@ function buildDocument(key, rootFolder, seriesData) {
|
||||
_id: key,
|
||||
key,
|
||||
rootFolder,
|
||||
rootFolders: rootFolder ? [rootFolder] : [],
|
||||
tvdbId,
|
||||
name: seriesData?.name || null,
|
||||
data: seriesData || {},
|
||||
@@ -26,8 +34,40 @@ function buildDocument(key, rootFolder, seriesData) {
|
||||
|
||||
export async function upsertTvSeries(key, rootFolder, seriesData) {
|
||||
const col = await getCollection();
|
||||
const doc = buildDocument(key, rootFolder, seriesData);
|
||||
await col.updateOne({ _id: key }, { $set: doc }, { upsert: true });
|
||||
const tvdbId = seriesData?.id ?? seriesData?.tvdbId ?? null;
|
||||
const canonicalKey = tvdbId !== null ? canonicalTvdbKey(tvdbId) : null;
|
||||
const targetKey = canonicalKey || key;
|
||||
|
||||
const existingByTvdb =
|
||||
tvdbId !== null ? await col.findOne({ tvdbId }) : null;
|
||||
const existing =
|
||||
existingByTvdb ||
|
||||
(await col.findOne({ _id: targetKey })) ||
|
||||
(await col.findOne({ _id: key }));
|
||||
|
||||
const desiredKey = canonicalKey || existingByTvdb?._id || targetKey;
|
||||
const doc = buildDocument(
|
||||
desiredKey,
|
||||
rootFolder || existing?.rootFolder,
|
||||
seriesData
|
||||
);
|
||||
|
||||
const rootSet = new Set(existing?.rootFolders || []);
|
||||
if (existing?.rootFolder) rootSet.add(existing.rootFolder);
|
||||
if (rootFolder) rootSet.add(rootFolder);
|
||||
doc.rootFolders = Array.from(rootSet);
|
||||
doc.rootFolder = doc.rootFolder || doc.rootFolders[0] || null;
|
||||
doc.tvdbId = tvdbId;
|
||||
doc.key = canonicalKey || doc._id;
|
||||
doc._id = doc.key;
|
||||
|
||||
await col.updateOne({ _id: doc._id }, { $set: doc }, { upsert: true });
|
||||
|
||||
// Eğer eski bir anahtar farklıysa temizle
|
||||
if (existing && existing._id !== doc._id) {
|
||||
await col.deleteOne({ _id: existing._id }).catch(() => {});
|
||||
}
|
||||
|
||||
return doc.data;
|
||||
}
|
||||
|
||||
@@ -39,10 +79,15 @@ export async function getTvSeriesByKey(key) {
|
||||
|
||||
export async function getTvSeriesByRoot(rootFolder) {
|
||||
const col = await getCollection();
|
||||
const docs = await col.find({ rootFolder }).toArray();
|
||||
const docs = await col
|
||||
.find({
|
||||
$or: [{ rootFolder }, { rootFolders: rootFolder }]
|
||||
})
|
||||
.toArray();
|
||||
return docs.map((doc) => ({
|
||||
key: doc.key,
|
||||
rootFolder: doc.rootFolder,
|
||||
rootFolders: doc.rootFolders || [],
|
||||
data: doc.data
|
||||
}));
|
||||
}
|
||||
@@ -53,13 +98,19 @@ export async function listAllTvSeries() {
|
||||
return docs.map((doc) => ({
|
||||
key: doc.key,
|
||||
rootFolder: doc.rootFolder,
|
||||
rootFolders: doc.rootFolders || [],
|
||||
data: doc.data
|
||||
}));
|
||||
}
|
||||
|
||||
export async function listTvSeriesKeysForRoot(rootFolder) {
|
||||
const col = await getCollection();
|
||||
const docs = await col.find({ rootFolder }).project({ key: 1 }).toArray();
|
||||
const docs = await col
|
||||
.find({
|
||||
$or: [{ rootFolder }, { rootFolders: rootFolder }]
|
||||
})
|
||||
.project({ key: 1 })
|
||||
.toArray();
|
||||
return docs.map((d) => d.key).filter(Boolean);
|
||||
}
|
||||
|
||||
@@ -70,5 +121,34 @@ export async function removeTvSeriesByKey(key) {
|
||||
|
||||
export async function removeTvSeriesByRoot(rootFolder) {
|
||||
const col = await getCollection();
|
||||
await col.deleteMany({ rootFolder });
|
||||
const cursor = col.find({
|
||||
$or: [{ rootFolder }, { rootFolders: rootFolder }]
|
||||
});
|
||||
|
||||
// Silmek yerine root'u listeden çıkar; boş kalırsa kaydı kaldır
|
||||
// Not: cursor.forEach async callback desteklemez, manual loop
|
||||
while (await cursor.hasNext()) {
|
||||
const doc = await cursor.next();
|
||||
const roots = new Set(doc.rootFolders || []);
|
||||
if (doc.rootFolder) roots.add(doc.rootFolder);
|
||||
roots.delete(rootFolder);
|
||||
|
||||
if (roots.size === 0) {
|
||||
await col.deleteOne({ _id: doc._id });
|
||||
} else {
|
||||
const nextRootFolder = Array.from(roots)[0];
|
||||
await col.updateOne(
|
||||
{ _id: doc._id },
|
||||
{
|
||||
$set: {
|
||||
rootFolder: nextRootFolder,
|
||||
rootFolders: Array.from(roots),
|
||||
updatedAt: Date.now()
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { canonicalTvdbKey };
|
||||
|
||||
298
server/server.js
298
server/server.js
@@ -15,8 +15,10 @@ import { restoreTorrentsFromDisk } from "./modules/state.js";
|
||||
import { createWebsocketServer, broadcastJson } from "./modules/websocket.js";
|
||||
import { connectMongo, getDb } from "./modules/db.js";
|
||||
import {
|
||||
canonicalTvdbKey,
|
||||
getTvSeriesByKey as loadTvSeriesByKey,
|
||||
listAllTvSeries as listAllTvSeriesFromDb,
|
||||
listTvSeriesKeysForRoot as listTvSeriesKeysForRootDb,
|
||||
removeTvSeriesByKey,
|
||||
removeTvSeriesByRoot,
|
||||
upsertTvSeries
|
||||
@@ -2815,22 +2817,63 @@ async function ensureSeriesData(
|
||||
seriesData.tvdbId = seriesId;
|
||||
}
|
||||
|
||||
let targetPaths =
|
||||
existingPaths && existingPaths.key?.includes("__") ? existingPaths : null;
|
||||
|
||||
if (!targetPaths) {
|
||||
targetPaths = buildTvSeriesPaths(
|
||||
normalizedRoot,
|
||||
seriesId,
|
||||
seriesInfo.title
|
||||
);
|
||||
if (!targetPaths && existingPaths) {
|
||||
targetPaths = existingPaths;
|
||||
// Eğer aynı tvdbId için kanonik kayıt varsa onu yükle (bölümler birleşsin)
|
||||
if (!existingPaths && seriesId) {
|
||||
const canonicalKey = canonicalTvdbKey(seriesId);
|
||||
try {
|
||||
const existingCanonical = await loadTvSeriesByKey(canonicalKey);
|
||||
if (existingCanonical) {
|
||||
seriesData = existingCanonical;
|
||||
existingPaths = tvSeriesPathsByKey(canonicalKey);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`⚠️ TV metadata kanonik kayıt okunamadı (${canonicalKey}): ${err.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Her zaman kanonik anahtarı hedefle; legacy path varsa taşı
|
||||
const canonicalPaths = buildTvSeriesPaths(
|
||||
normalizedRoot,
|
||||
seriesId,
|
||||
seriesInfo.title
|
||||
);
|
||||
let targetPaths = canonicalPaths || existingPaths || null;
|
||||
if (!targetPaths && existingPaths) {
|
||||
targetPaths = existingPaths;
|
||||
}
|
||||
|
||||
if (!targetPaths) return null;
|
||||
|
||||
// Legacy klasör -> kanonik klasöre taşı
|
||||
if (
|
||||
existingPaths &&
|
||||
canonicalPaths &&
|
||||
existingPaths.key !== canonicalPaths.key &&
|
||||
fs.existsSync(existingPaths.dir)
|
||||
) {
|
||||
try {
|
||||
if (!fs.existsSync(targetPaths.dir)) {
|
||||
fs.mkdirSync(targetPaths.dir, { recursive: true });
|
||||
}
|
||||
const entries = fs.readdirSync(existingPaths.dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const src = path.join(existingPaths.dir, entry.name);
|
||||
const dest = path.join(targetPaths.dir, entry.name);
|
||||
if (!fs.existsSync(dest)) {
|
||||
fs.renameSync(src, dest);
|
||||
}
|
||||
}
|
||||
fs.rmSync(existingPaths.dir, { recursive: true, force: true });
|
||||
console.log(`🔀 TV metadata klasörü taşındı: ${existingPaths.key} -> ${targetPaths.key}`);
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`⚠️ TV metadata klasörü taşınamadı (${existingPaths.key} -> ${targetPaths.key}): ${err.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const showDir = targetPaths.dir;
|
||||
const seriesMetaPath = targetPaths.metadata;
|
||||
|
||||
@@ -3169,6 +3212,7 @@ async function ensureSeriesData(
|
||||
still: fs.existsSync(stillPath)
|
||||
? encodeTvDataPath(targetPaths.key, path.relative(showDir, stillPath))
|
||||
: null,
|
||||
folder: normalizedRoot,
|
||||
file: normalizedFile,
|
||||
mediaInfo: mediaInfo || null,
|
||||
tvdbEpisodeId: episodeTvdbId,
|
||||
@@ -3181,6 +3225,47 @@ async function ensureSeriesData(
|
||||
seasonContainer.episodeCount = Object.keys(seasonContainer.episodes).length;
|
||||
seasonContainer.updatedAt = Date.now();
|
||||
|
||||
// Eski kayıt varsa sezon/bölüm verilerini koruyarak birleştir
|
||||
try {
|
||||
const existingDoc = await loadTvSeriesByKey(targetPaths.key);
|
||||
const existingSeasons =
|
||||
existingDoc && typeof existingDoc.seasons === "object"
|
||||
? existingDoc.seasons
|
||||
: {};
|
||||
const incomingSeasons =
|
||||
seriesData && typeof seriesData.seasons === "object"
|
||||
? seriesData.seasons
|
||||
: {};
|
||||
|
||||
const mergedSeasons = { ...existingSeasons };
|
||||
|
||||
// Önce mevcut sezonları kopyala, sonra gelen verileri ekle/override et
|
||||
for (const [seasonKey, incomingSeason] of Object.entries(incomingSeasons)) {
|
||||
const prevSeason = mergedSeasons[seasonKey] || {};
|
||||
const prevEpisodes =
|
||||
(prevSeason && typeof prevSeason.episodes === "object"
|
||||
? prevSeason.episodes
|
||||
: {}) || {};
|
||||
const nextEpisodes = { ...prevEpisodes };
|
||||
|
||||
if (incomingSeason.episodes && typeof incomingSeason.episodes === "object") {
|
||||
for (const [epKey, epVal] of Object.entries(incomingSeason.episodes)) {
|
||||
nextEpisodes[epKey] = epVal;
|
||||
}
|
||||
}
|
||||
|
||||
mergedSeasons[seasonKey] = {
|
||||
...prevSeason,
|
||||
...incomingSeason,
|
||||
episodes: nextEpisodes
|
||||
};
|
||||
}
|
||||
|
||||
seriesData.seasons = mergedSeasons;
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ TV metadata merge başarısız (db - ${targetPaths.key}): ${err.message}`);
|
||||
}
|
||||
|
||||
ensureDirForFile(seriesMetaPath);
|
||||
try {
|
||||
await upsertTvSeries(targetPaths.key, normalizedRoot, seriesData);
|
||||
@@ -3234,12 +3319,16 @@ async function ensureSeriesData(
|
||||
}
|
||||
|
||||
function tvSeriesKey(rootFolder, seriesId = null, fallbackTitle = null) {
|
||||
const tvdbId = normalizeTvdbId(seriesId);
|
||||
if (tvdbId !== null) {
|
||||
return canonicalTvdbKey(tvdbId);
|
||||
}
|
||||
|
||||
const normalizedRoot = rootFolder ? sanitizeRelative(rootFolder) : null;
|
||||
if (!normalizedRoot) return null;
|
||||
|
||||
let suffix = null;
|
||||
if (seriesId) {
|
||||
suffix = String(seriesId).toLowerCase();
|
||||
} else if (fallbackTitle) {
|
||||
if (fallbackTitle) {
|
||||
const slug = String(fallbackTitle)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
@@ -3256,11 +3345,14 @@ function tvSeriesKey(rootFolder, seriesId = null, fallbackTitle = null) {
|
||||
|
||||
function parseTvSeriesKey(key) {
|
||||
const normalized = sanitizeRelative(String(key || ""));
|
||||
if (normalized.startsWith("tvdb-")) {
|
||||
return { rootFolder: null, seriesId: normalized.slice(5), key: normalized, canonical: true };
|
||||
}
|
||||
if (!normalized.includes("__")) {
|
||||
return { rootFolder: normalized, seriesId: null, key: normalized };
|
||||
return { rootFolder: normalized, seriesId: null, key: normalized, canonical: false };
|
||||
}
|
||||
const [rootFolder, suffix] = normalized.split("__", 2);
|
||||
return { rootFolder, seriesId: suffix || null, key: normalized };
|
||||
return { rootFolder, seriesId: suffix || null, key: normalized, canonical: false };
|
||||
}
|
||||
|
||||
function tvSeriesPathsByKey(key) {
|
||||
@@ -3279,10 +3371,11 @@ function tvSeriesPathsByKey(key) {
|
||||
}
|
||||
|
||||
function tvSeriesPaths(rootFolderOrKey, seriesId = null, fallbackTitle = null) {
|
||||
const rawKey = String(rootFolderOrKey || "");
|
||||
if (
|
||||
seriesId === null &&
|
||||
fallbackTitle === null &&
|
||||
String(rootFolderOrKey || "").includes("__")
|
||||
(rawKey.includes("__") || rawKey.startsWith("tvdb-"))
|
||||
) {
|
||||
return tvSeriesPathsByKey(rootFolderOrKey);
|
||||
}
|
||||
@@ -3317,9 +3410,19 @@ function seasonAssetPaths(paths, seasonNumber) {
|
||||
};
|
||||
}
|
||||
|
||||
function listTvSeriesKeysForRoot(rootFolder) {
|
||||
async function listTvSeriesKeysForRoot(rootFolder) {
|
||||
const normalizedRoot = rootFolder ? sanitizeRelative(rootFolder) : null;
|
||||
if (!normalizedRoot) return [];
|
||||
|
||||
// Önce DB'den oku (rootFolders desteğiyle)
|
||||
try {
|
||||
const dbKeys = await listTvSeriesKeysForRootDb(normalizedRoot);
|
||||
if (dbKeys?.length) return dbKeys;
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ TV metadata anahtarları DB'den alınamadı: ${err.message}`);
|
||||
}
|
||||
|
||||
// Legacy cache dizinlerini tara (geri uyumluluk)
|
||||
if (!fs.existsSync(TV_DATA_ROOT)) return [];
|
||||
const keys = [];
|
||||
try {
|
||||
@@ -3342,16 +3445,34 @@ function listTvSeriesKeysForRoot(rootFolder) {
|
||||
return keys;
|
||||
}
|
||||
|
||||
function removeSeriesData(rootFolder, seriesId = null) {
|
||||
async function removeSeriesData(rootFolder, seriesId = null) {
|
||||
const keys = seriesId
|
||||
? [tvSeriesKey(rootFolder, seriesId)].filter(Boolean)
|
||||
: listTvSeriesKeysForRoot(rootFolder);
|
||||
: await listTvSeriesKeysForRoot(rootFolder);
|
||||
|
||||
if (!keys.length) return;
|
||||
|
||||
if (seriesId === null) {
|
||||
await removeTvSeriesByRoot(rootFolder);
|
||||
}
|
||||
|
||||
for (const key of keys) {
|
||||
removeTvSeriesByKey(key).catch((err) =>
|
||||
console.warn(`⚠️ TV metadata silinemedi (db - ${key}): ${err.message}`)
|
||||
);
|
||||
if (seriesId !== null) {
|
||||
await removeTvSeriesByKey(key).catch((err) =>
|
||||
console.warn(`⚠️ TV metadata silinemedi (db - ${key}): ${err.message}`)
|
||||
);
|
||||
}
|
||||
const dir = tvSeriesDir(key);
|
||||
if (dir && fs.existsSync(dir)) {
|
||||
// Dizini ancak DB kaydı kalmadıysa sil
|
||||
let keepDir = false;
|
||||
try {
|
||||
const stillExists = await loadTvSeriesByKey(key);
|
||||
keepDir = Boolean(stillExists);
|
||||
} catch {
|
||||
/* no-op */
|
||||
}
|
||||
|
||||
if (!keepDir && dir && fs.existsSync(dir)) {
|
||||
try {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
console.log(`🧹 TV metadata silindi: ${dir}`);
|
||||
@@ -3365,10 +3486,10 @@ function removeSeriesData(rootFolder, seriesId = null) {
|
||||
function removeSeriesEpisode(rootFolder, relativeFilePath) {
|
||||
if (!rootFolder || !relativeFilePath) return;
|
||||
|
||||
const keys = listTvSeriesKeysForRoot(rootFolder);
|
||||
if (!keys.length) return;
|
||||
|
||||
(async () => {
|
||||
const keys = await listTvSeriesKeysForRoot(rootFolder);
|
||||
if (!keys.length) return;
|
||||
|
||||
for (const key of keys) {
|
||||
const paths = tvSeriesPathsByKey(key);
|
||||
|
||||
@@ -3408,7 +3529,7 @@ function removeSeriesEpisode(rootFolder, relativeFilePath) {
|
||||
if (!removed) continue;
|
||||
|
||||
if (!Object.keys(seasons).length) {
|
||||
removeSeriesData(seriesData._dupe?.folder || rootFolder, seriesData.id);
|
||||
await removeSeriesData(seriesData._dupe?.folder || rootFolder, seriesData.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -3416,7 +3537,7 @@ function removeSeriesEpisode(rootFolder, relativeFilePath) {
|
||||
seriesData.updatedAt = Date.now();
|
||||
|
||||
try {
|
||||
await upsertTvSeries(key, paths.rootFolder, seriesData);
|
||||
await upsertTvSeries(key, rootFolder, seriesData);
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`⚠️ TV metadata güncellenemedi (db - ${key}): ${err.message}`
|
||||
@@ -3457,7 +3578,9 @@ async function importLegacySeriesMetadata() {
|
||||
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(paths.metadata, "utf-8")) || {};
|
||||
await upsertTvSeries(key, paths.rootFolder, data);
|
||||
const parsed = parseTvSeriesKey(key);
|
||||
const rootForDoc = data?._dupe?.folder || parsed.rootFolder || null;
|
||||
await upsertTvSeries(key, rootForDoc, data);
|
||||
imported += 1;
|
||||
try {
|
||||
fs.rmSync(paths.metadata, { force: true });
|
||||
@@ -3493,7 +3616,9 @@ function purgeRootFolder(rootFolder) {
|
||||
|
||||
removeAllThumbnailsForRoot(safe);
|
||||
removeMovieData(safe);
|
||||
removeSeriesData(safe);
|
||||
removeSeriesData(safe).catch((err) =>
|
||||
console.warn(`⚠️ TV metadata silinemedi (purge - ${safe}): ${err?.message || err}`)
|
||||
);
|
||||
|
||||
const infoPath = path.join(DOWNLOAD_DIR, safe, INFO_FILENAME);
|
||||
if (fs.existsSync(infoPath)) {
|
||||
@@ -3814,10 +3939,10 @@ function renameSeriesDataPaths(rootFolder, oldRel, newRel) {
|
||||
const newPrefix = normalizeTrashPath(newRel);
|
||||
if (!oldPrefix || oldPrefix === newPrefix) return;
|
||||
|
||||
const keys = listTvSeriesKeysForRoot(rootFolder);
|
||||
if (!keys.length) return;
|
||||
|
||||
(async () => {
|
||||
const keys = await listTvSeriesKeysForRoot(rootFolder);
|
||||
if (!keys.length) return;
|
||||
|
||||
for (const key of keys) {
|
||||
const paths = tvSeriesPathsByKey(key);
|
||||
let seriesData;
|
||||
@@ -3869,7 +3994,7 @@ function renameSeriesDataPaths(rootFolder, oldRel, newRel) {
|
||||
|
||||
if (changed) {
|
||||
try {
|
||||
await upsertTvSeries(key, paths.rootFolder, seriesData);
|
||||
await upsertTvSeries(key, rootFolder, seriesData);
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`⚠️ TV metadata güncellenemedi (db - ${key}): ${err.message}`
|
||||
@@ -5686,7 +5811,7 @@ async function rebuildMovieMetadata({ clearCache = false, resetSeriesData = fals
|
||||
if (!videoEntries.length) {
|
||||
removeMovieData(folder);
|
||||
if (resetSeriesData) {
|
||||
removeSeriesData(folder);
|
||||
await removeSeriesData(folder);
|
||||
}
|
||||
const update = {
|
||||
primaryVideoPath: null,
|
||||
@@ -5713,7 +5838,7 @@ async function rebuildMovieMetadata({ clearCache = false, resetSeriesData = fals
|
||||
|
||||
removeMovieData(folder);
|
||||
if (resetSeriesData) {
|
||||
removeSeriesData(folder);
|
||||
await removeSeriesData(folder);
|
||||
}
|
||||
|
||||
const matches = [];
|
||||
@@ -5922,37 +6047,55 @@ app.get("/api/tvshows", requireAuth, async (req, res) => {
|
||||
if (!key) continue;
|
||||
const paths = tvSeriesPathsByKey(key);
|
||||
const parsed = parseTvSeriesKey(key);
|
||||
const rootFolder = parsed.rootFolder || doc.rootFolder;
|
||||
if (!rootFolder) continue;
|
||||
|
||||
const infoForFolder = readInfoForRoot(rootFolder) || {};
|
||||
const infoFiles = infoForFolder.files || {};
|
||||
const infoEpisodes = infoForFolder.seriesEpisodes || {};
|
||||
const docFolders = Array.isArray(doc.rootFolders)
|
||||
? doc.rootFolders.filter(Boolean)
|
||||
: [];
|
||||
const primaryFolder =
|
||||
doc.rootFolder ||
|
||||
parsed.rootFolder ||
|
||||
docFolders[0] ||
|
||||
null;
|
||||
if (!primaryFolder) continue;
|
||||
|
||||
const rootFolder = primaryFolder;
|
||||
|
||||
const folderSet = new Set([rootFolder, ...docFolders]);
|
||||
const infoEpisodeIndex = new Map();
|
||||
const infoFilesByFolder = 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;
|
||||
for (const folderName of folderSet) {
|
||||
const infoForFolder = readInfoForRoot(folderName) || {};
|
||||
const infoFiles = infoForFolder.files || {};
|
||||
const infoEpisodes = infoForFolder.seriesEpisodes || {};
|
||||
infoFilesByFolder.set(folderName, infoFiles);
|
||||
|
||||
const normalizedRel = normalizeTrashPath(relPath);
|
||||
const ext = path.extname(normalizedRel).toLowerCase();
|
||||
if (!VIDEO_EXTS.includes(ext)) continue;
|
||||
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 absVideo = normalizedRel
|
||||
? path.join(DOWNLOAD_DIR, rootFolder, normalizedRel)
|
||||
: null;
|
||||
if (!absVideo || !fs.existsSync(absVideo)) continue;
|
||||
const normalizedRel = normalizeTrashPath(relPath);
|
||||
const ext = path.extname(normalizedRel).toLowerCase();
|
||||
if (!VIDEO_EXTS.includes(ext)) continue;
|
||||
|
||||
infoEpisodeIndex.set(`${seasonNumber}-${episodeNumber}`, {
|
||||
relPath: normalizedRel,
|
||||
meta
|
||||
});
|
||||
const absVideo = normalizedRel
|
||||
? path.join(DOWNLOAD_DIR, folderName, normalizedRel)
|
||||
: null;
|
||||
if (!absVideo || !fs.existsSync(absVideo)) continue;
|
||||
|
||||
infoEpisodeIndex.set(`${folderName}:${seasonNumber}-${episodeNumber}`, {
|
||||
relPath: normalizedRel,
|
||||
meta,
|
||||
folder: folderName
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let seriesData = doc.data;
|
||||
@@ -5963,7 +6106,7 @@ app.get("/api/tvshows", requireAuth, async (req, res) => {
|
||||
|
||||
const seasonsObj = seriesData?.seasons || {};
|
||||
if (!Object.keys(seasonsObj).length) {
|
||||
removeSeriesData(rootFolder, seriesData.id ?? seriesData.tvdbId ?? null);
|
||||
await removeSeriesData(rootFolder, seriesData.id ?? seriesData.tvdbId ?? null);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -5998,13 +6141,14 @@ app.get("/api/tvshows", requireAuth, async (req, res) => {
|
||||
),
|
||||
seasons: new Map(),
|
||||
primaryFolder: rootFolder,
|
||||
folders: new Set([rootFolder])
|
||||
folders: new Set([rootFolder, ...docFolders])
|
||||
};
|
||||
aggregated.set(showKey, base);
|
||||
return base;
|
||||
})();
|
||||
|
||||
record.folders.add(rootFolder);
|
||||
for (const extra of docFolders) record.folders.add(extra);
|
||||
if (
|
||||
seriesData.overview &&
|
||||
seriesData.overview.length > (record.overview?.length || 0)
|
||||
@@ -6043,11 +6187,12 @@ app.get("/api/tvshows", requireAuth, async (req, res) => {
|
||||
const rawEpisodes = rawSeason.episodes || {};
|
||||
for (const [episodeKey, rawEpisode] of Object.entries(rawEpisodes)) {
|
||||
if (!rawEpisode || typeof rawEpisode !== "object") continue;
|
||||
const episodeRootForCleanup = rawEpisode.folder || rootFolder;
|
||||
const relativeFile = (rawEpisode.file || "").replace(/\\/g, "/");
|
||||
if (relativeFile) {
|
||||
const absEpisodePath = path.join(
|
||||
DOWNLOAD_DIR,
|
||||
rootFolder,
|
||||
episodeRootForCleanup,
|
||||
relativeFile
|
||||
);
|
||||
if (!fs.existsSync(absEpisodePath)) {
|
||||
@@ -6116,13 +6261,16 @@ app.get("/api/tvshows", requireAuth, async (req, res) => {
|
||||
"0"
|
||||
)}E${String(episodeNumber).padStart(2, "0")}`;
|
||||
}
|
||||
const infoEpisode = infoEpisodeIndex.get(`${seasonNumber}-${episodeNumber}`);
|
||||
const episodeRoot = rawEpisode.folder || rootFolder;
|
||||
const infoEpisode = infoEpisodeIndex.get(`${episodeRoot}:${seasonNumber}-${episodeNumber}`);
|
||||
if (infoEpisode?.relPath) {
|
||||
const normalizedRel = infoEpisode.relPath.replace(/^\/+/, "");
|
||||
const withRoot = `${rootFolder}/${normalizedRel}`.replace(/^\/+/, "");
|
||||
const withRoot = `${episodeRoot}/${normalizedRel}`.replace(/^\/+/, "");
|
||||
normalizedEpisode.file = normalizedRel;
|
||||
normalizedEpisode.videoPath = withRoot;
|
||||
const fileMeta = infoFiles[normalizedRel];
|
||||
const fileMeta = (infoFilesByFolder.get(episodeRoot) || {})[
|
||||
normalizedRel
|
||||
];
|
||||
if (fileMeta?.mediaInfo && !normalizedEpisode.mediaInfo) {
|
||||
normalizedEpisode.mediaInfo = fileMeta.mediaInfo;
|
||||
}
|
||||
@@ -6138,10 +6286,10 @@ app.get("/api/tvshows", requireAuth, async (req, res) => {
|
||||
const isExternal = /^https?:\/\//i.test(videoPath);
|
||||
const needsFolderPrefix =
|
||||
!isExternal &&
|
||||
!videoPath.startsWith(`${rootFolder}/`) &&
|
||||
!videoPath.startsWith(`/${rootFolder}/`);
|
||||
!videoPath.startsWith(`${episodeRoot}/`) &&
|
||||
!videoPath.startsWith(`/${episodeRoot}/`);
|
||||
if (needsFolderPrefix) {
|
||||
videoPath = `${rootFolder}/${videoPath}`.replace(/\\/g, "/");
|
||||
videoPath = `${episodeRoot}/${videoPath}`.replace(/\\/g, "/");
|
||||
}
|
||||
const finalPath = videoPath.replace(/^\/+/, "");
|
||||
if (finalPath !== rawVideoPath) {
|
||||
@@ -6149,7 +6297,7 @@ app.get("/api/tvshows", requireAuth, async (req, res) => {
|
||||
}
|
||||
normalizedEpisode.videoPath = finalPath;
|
||||
} else if (relativeFile) {
|
||||
normalizedEpisode.videoPath = `${rootFolder}/${relativeFile}`
|
||||
normalizedEpisode.videoPath = `${episodeRoot}/${relativeFile}`
|
||||
.replace(/\\/g, "/")
|
||||
.replace(/^\/+/, "");
|
||||
if (normalizedEpisode.videoPath !== rawVideoPath) {
|
||||
@@ -6178,7 +6326,7 @@ app.get("/api/tvshows", requireAuth, async (req, res) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
normalizedEpisode.folder = rootFolder;
|
||||
normalizedEpisode.folder = episodeRoot;
|
||||
|
||||
const existingEpisode = seasonRecord.episodes.get(episodeNumber);
|
||||
seasonRecord.episodes.set(
|
||||
@@ -6981,7 +7129,7 @@ app.post("/api/match/manual", requireAuth, async (req, res) => {
|
||||
|
||||
// Mevcut movie_data ve TV verilerini temizle
|
||||
removeMovieData(rootFolder, relativeVideoPath);
|
||||
removeSeriesData(rootFolder);
|
||||
await removeSeriesData(rootFolder);
|
||||
|
||||
// TMDB'den detaylı bilgi al
|
||||
const movieDetails = await tmdbFetch(`/movie/${movieId}`, {
|
||||
@@ -7063,7 +7211,7 @@ app.post("/api/match/manual", requireAuth, async (req, res) => {
|
||||
|
||||
// Mevcut movie_data ve TV verilerini temizle
|
||||
removeMovieData(rootFolder);
|
||||
removeSeriesData(rootFolder);
|
||||
await removeSeriesData(rootFolder);
|
||||
|
||||
// TVDB'den dizi bilgilerini al
|
||||
const extended = await fetchTvdbSeriesExtended(seriesId);
|
||||
|
||||
Reference in New Issue
Block a user