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:
2025-12-13 13:26:58 +03:00
parent 7ac71606e3
commit 485c3cfd94
3 changed files with 329 additions and 81 deletions

View File

@@ -13,6 +13,9 @@
let refreshing = false; let refreshing = false;
let rescanning = false; let rescanning = false;
let error = null; let error = null;
let mounted = false;
let lastLoadedCount = null;
let unsubscribeCount = null;
let selectedShow = null; let selectedShow = null;
let selectedSeason = null; let selectedSeason = null;
@@ -189,10 +192,12 @@ let filteredShows = [];
const list = await resp.json(); const list = await resp.json();
shows = Array.isArray(list) ? list.map(normalizeShow) : []; shows = Array.isArray(list) ? list.map(normalizeShow) : [];
tvShowCount.set(shows.length); tvShowCount.set(shows.length);
lastLoadedCount = shows.length;
} catch (err) { } catch (err) {
error = err?.message || "TV dizileri alınamadı."; error = err?.message || "TV dizileri alınamadı.";
shows = []; shows = [];
tvShowCount.set(0); tvShowCount.set(0);
lastLoadedCount = 0;
} finally { } finally {
loading = false; loading = false;
} }
@@ -251,6 +256,21 @@ let filteredShows = [];
} }
} }
onMount(() => {
mounted = true;
loadShows();
unsubscribeCount = tvShowCount.subscribe((val) => {
if (!mounted) return;
if (loading || refreshing || rescanning) return;
if (val === lastLoadedCount) return;
loadShows();
});
return () => {
mounted = false;
unsubscribeCount && unsubscribeCount();
};
});
function openShow(show) { function openShow(show) {
if (!show) return; if (!show) return;
selectedShow = show; selectedShow = show;

View File

@@ -1,12 +1,19 @@
import { connectMongo } from "./db.js"; import { connectMongo } from "./db.js";
const COLLECTION = "tv_data"; 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() { async function getCollection() {
const { db } = await connectMongo(); const { db } = await connectMongo();
const col = db.collection(COLLECTION); const col = db.collection(COLLECTION);
await col.createIndex({ rootFolder: 1 }); 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 }); await col.createIndex({ updatedAt: -1 });
return col; return col;
} }
@@ -17,6 +24,7 @@ function buildDocument(key, rootFolder, seriesData) {
_id: key, _id: key,
key, key,
rootFolder, rootFolder,
rootFolders: rootFolder ? [rootFolder] : [],
tvdbId, tvdbId,
name: seriesData?.name || null, name: seriesData?.name || null,
data: seriesData || {}, data: seriesData || {},
@@ -26,8 +34,40 @@ function buildDocument(key, rootFolder, seriesData) {
export async function upsertTvSeries(key, rootFolder, seriesData) { export async function upsertTvSeries(key, rootFolder, seriesData) {
const col = await getCollection(); const col = await getCollection();
const doc = buildDocument(key, rootFolder, seriesData); const tvdbId = seriesData?.id ?? seriesData?.tvdbId ?? null;
await col.updateOne({ _id: key }, { $set: doc }, { upsert: true }); 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; return doc.data;
} }
@@ -39,10 +79,15 @@ export async function getTvSeriesByKey(key) {
export async function getTvSeriesByRoot(rootFolder) { export async function getTvSeriesByRoot(rootFolder) {
const col = await getCollection(); 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) => ({ return docs.map((doc) => ({
key: doc.key, key: doc.key,
rootFolder: doc.rootFolder, rootFolder: doc.rootFolder,
rootFolders: doc.rootFolders || [],
data: doc.data data: doc.data
})); }));
} }
@@ -53,13 +98,19 @@ export async function listAllTvSeries() {
return docs.map((doc) => ({ return docs.map((doc) => ({
key: doc.key, key: doc.key,
rootFolder: doc.rootFolder, rootFolder: doc.rootFolder,
rootFolders: doc.rootFolders || [],
data: doc.data data: doc.data
})); }));
} }
export async function listTvSeriesKeysForRoot(rootFolder) { export async function listTvSeriesKeysForRoot(rootFolder) {
const col = await getCollection(); 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); return docs.map((d) => d.key).filter(Boolean);
} }
@@ -70,5 +121,34 @@ export async function removeTvSeriesByKey(key) {
export async function removeTvSeriesByRoot(rootFolder) { export async function removeTvSeriesByRoot(rootFolder) {
const col = await getCollection(); 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 };

View File

@@ -15,8 +15,10 @@ import { restoreTorrentsFromDisk } from "./modules/state.js";
import { createWebsocketServer, broadcastJson } from "./modules/websocket.js"; import { createWebsocketServer, broadcastJson } from "./modules/websocket.js";
import { connectMongo, getDb } from "./modules/db.js"; import { connectMongo, getDb } from "./modules/db.js";
import { import {
canonicalTvdbKey,
getTvSeriesByKey as loadTvSeriesByKey, getTvSeriesByKey as loadTvSeriesByKey,
listAllTvSeries as listAllTvSeriesFromDb, listAllTvSeries as listAllTvSeriesFromDb,
listTvSeriesKeysForRoot as listTvSeriesKeysForRootDb,
removeTvSeriesByKey, removeTvSeriesByKey,
removeTvSeriesByRoot, removeTvSeriesByRoot,
upsertTvSeries upsertTvSeries
@@ -2815,22 +2817,63 @@ async function ensureSeriesData(
seriesData.tvdbId = seriesId; seriesData.tvdbId = seriesId;
} }
let targetPaths = // Eğer aynı tvdbId için kanonik kayıt varsa onu yükle (bölümler birleşsin)
existingPaths && existingPaths.key?.includes("__") ? existingPaths : null; 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}`
);
}
}
if (!targetPaths) { // Her zaman kanonik anahtarı hedefle; legacy path varsa taşı
targetPaths = buildTvSeriesPaths( const canonicalPaths = buildTvSeriesPaths(
normalizedRoot, normalizedRoot,
seriesId, seriesId,
seriesInfo.title seriesInfo.title
); );
let targetPaths = canonicalPaths || existingPaths || null;
if (!targetPaths && existingPaths) { if (!targetPaths && existingPaths) {
targetPaths = existingPaths; targetPaths = existingPaths;
} }
}
if (!targetPaths) return null; 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 showDir = targetPaths.dir;
const seriesMetaPath = targetPaths.metadata; const seriesMetaPath = targetPaths.metadata;
@@ -3169,6 +3212,7 @@ async function ensureSeriesData(
still: fs.existsSync(stillPath) still: fs.existsSync(stillPath)
? encodeTvDataPath(targetPaths.key, path.relative(showDir, stillPath)) ? encodeTvDataPath(targetPaths.key, path.relative(showDir, stillPath))
: null, : null,
folder: normalizedRoot,
file: normalizedFile, file: normalizedFile,
mediaInfo: mediaInfo || null, mediaInfo: mediaInfo || null,
tvdbEpisodeId: episodeTvdbId, tvdbEpisodeId: episodeTvdbId,
@@ -3181,6 +3225,47 @@ async function ensureSeriesData(
seasonContainer.episodeCount = Object.keys(seasonContainer.episodes).length; seasonContainer.episodeCount = Object.keys(seasonContainer.episodes).length;
seasonContainer.updatedAt = Date.now(); 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); ensureDirForFile(seriesMetaPath);
try { try {
await upsertTvSeries(targetPaths.key, normalizedRoot, seriesData); await upsertTvSeries(targetPaths.key, normalizedRoot, seriesData);
@@ -3234,12 +3319,16 @@ async function ensureSeriesData(
} }
function tvSeriesKey(rootFolder, seriesId = null, fallbackTitle = null) { function tvSeriesKey(rootFolder, seriesId = null, fallbackTitle = null) {
const tvdbId = normalizeTvdbId(seriesId);
if (tvdbId !== null) {
return canonicalTvdbKey(tvdbId);
}
const normalizedRoot = rootFolder ? sanitizeRelative(rootFolder) : null; const normalizedRoot = rootFolder ? sanitizeRelative(rootFolder) : null;
if (!normalizedRoot) return null; if (!normalizedRoot) return null;
let suffix = null; let suffix = null;
if (seriesId) { if (fallbackTitle) {
suffix = String(seriesId).toLowerCase();
} else if (fallbackTitle) {
const slug = String(fallbackTitle) const slug = String(fallbackTitle)
.trim() .trim()
.toLowerCase() .toLowerCase()
@@ -3256,11 +3345,14 @@ function tvSeriesKey(rootFolder, seriesId = null, fallbackTitle = null) {
function parseTvSeriesKey(key) { function parseTvSeriesKey(key) {
const normalized = sanitizeRelative(String(key || "")); const normalized = sanitizeRelative(String(key || ""));
if (normalized.startsWith("tvdb-")) {
return { rootFolder: null, seriesId: normalized.slice(5), key: normalized, canonical: true };
}
if (!normalized.includes("__")) { 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); 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) { function tvSeriesPathsByKey(key) {
@@ -3279,10 +3371,11 @@ function tvSeriesPathsByKey(key) {
} }
function tvSeriesPaths(rootFolderOrKey, seriesId = null, fallbackTitle = null) { function tvSeriesPaths(rootFolderOrKey, seriesId = null, fallbackTitle = null) {
const rawKey = String(rootFolderOrKey || "");
if ( if (
seriesId === null && seriesId === null &&
fallbackTitle === null && fallbackTitle === null &&
String(rootFolderOrKey || "").includes("__") (rawKey.includes("__") || rawKey.startsWith("tvdb-"))
) { ) {
return tvSeriesPathsByKey(rootFolderOrKey); return tvSeriesPathsByKey(rootFolderOrKey);
} }
@@ -3317,9 +3410,19 @@ function seasonAssetPaths(paths, seasonNumber) {
}; };
} }
function listTvSeriesKeysForRoot(rootFolder) { async function listTvSeriesKeysForRoot(rootFolder) {
const normalizedRoot = rootFolder ? sanitizeRelative(rootFolder) : null; const normalizedRoot = rootFolder ? sanitizeRelative(rootFolder) : null;
if (!normalizedRoot) return []; 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 []; if (!fs.existsSync(TV_DATA_ROOT)) return [];
const keys = []; const keys = [];
try { try {
@@ -3342,16 +3445,34 @@ function listTvSeriesKeysForRoot(rootFolder) {
return keys; return keys;
} }
function removeSeriesData(rootFolder, seriesId = null) { async function removeSeriesData(rootFolder, seriesId = null) {
const keys = seriesId const keys = seriesId
? [tvSeriesKey(rootFolder, seriesId)].filter(Boolean) ? [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) { for (const key of keys) {
removeTvSeriesByKey(key).catch((err) => if (seriesId !== null) {
await removeTvSeriesByKey(key).catch((err) =>
console.warn(`⚠️ TV metadata silinemedi (db - ${key}): ${err.message}`) console.warn(`⚠️ TV metadata silinemedi (db - ${key}): ${err.message}`)
); );
}
const dir = tvSeriesDir(key); 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 { try {
fs.rmSync(dir, { recursive: true, force: true }); fs.rmSync(dir, { recursive: true, force: true });
console.log(`🧹 TV metadata silindi: ${dir}`); console.log(`🧹 TV metadata silindi: ${dir}`);
@@ -3365,10 +3486,10 @@ function removeSeriesData(rootFolder, seriesId = null) {
function removeSeriesEpisode(rootFolder, relativeFilePath) { function removeSeriesEpisode(rootFolder, relativeFilePath) {
if (!rootFolder || !relativeFilePath) return; if (!rootFolder || !relativeFilePath) return;
const keys = listTvSeriesKeysForRoot(rootFolder); (async () => {
const keys = await listTvSeriesKeysForRoot(rootFolder);
if (!keys.length) return; if (!keys.length) return;
(async () => {
for (const key of keys) { for (const key of keys) {
const paths = tvSeriesPathsByKey(key); const paths = tvSeriesPathsByKey(key);
@@ -3408,7 +3529,7 @@ function removeSeriesEpisode(rootFolder, relativeFilePath) {
if (!removed) continue; if (!removed) continue;
if (!Object.keys(seasons).length) { if (!Object.keys(seasons).length) {
removeSeriesData(seriesData._dupe?.folder || rootFolder, seriesData.id); await removeSeriesData(seriesData._dupe?.folder || rootFolder, seriesData.id);
continue; continue;
} }
@@ -3416,7 +3537,7 @@ function removeSeriesEpisode(rootFolder, relativeFilePath) {
seriesData.updatedAt = Date.now(); seriesData.updatedAt = Date.now();
try { try {
await upsertTvSeries(key, paths.rootFolder, seriesData); await upsertTvSeries(key, rootFolder, seriesData);
} catch (err) { } catch (err) {
console.warn( console.warn(
`⚠️ TV metadata güncellenemedi (db - ${key}): ${err.message}` `⚠️ TV metadata güncellenemedi (db - ${key}): ${err.message}`
@@ -3457,7 +3578,9 @@ async function importLegacySeriesMetadata() {
try { try {
const data = JSON.parse(fs.readFileSync(paths.metadata, "utf-8")) || {}; 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; imported += 1;
try { try {
fs.rmSync(paths.metadata, { force: true }); fs.rmSync(paths.metadata, { force: true });
@@ -3493,7 +3616,9 @@ function purgeRootFolder(rootFolder) {
removeAllThumbnailsForRoot(safe); removeAllThumbnailsForRoot(safe);
removeMovieData(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); const infoPath = path.join(DOWNLOAD_DIR, safe, INFO_FILENAME);
if (fs.existsSync(infoPath)) { if (fs.existsSync(infoPath)) {
@@ -3814,10 +3939,10 @@ function renameSeriesDataPaths(rootFolder, oldRel, newRel) {
const newPrefix = normalizeTrashPath(newRel); const newPrefix = normalizeTrashPath(newRel);
if (!oldPrefix || oldPrefix === newPrefix) return; if (!oldPrefix || oldPrefix === newPrefix) return;
const keys = listTvSeriesKeysForRoot(rootFolder); (async () => {
const keys = await listTvSeriesKeysForRoot(rootFolder);
if (!keys.length) return; if (!keys.length) return;
(async () => {
for (const key of keys) { for (const key of keys) {
const paths = tvSeriesPathsByKey(key); const paths = tvSeriesPathsByKey(key);
let seriesData; let seriesData;
@@ -3869,7 +3994,7 @@ function renameSeriesDataPaths(rootFolder, oldRel, newRel) {
if (changed) { if (changed) {
try { try {
await upsertTvSeries(key, paths.rootFolder, seriesData); await upsertTvSeries(key, rootFolder, seriesData);
} catch (err) { } catch (err) {
console.warn( console.warn(
`⚠️ TV metadata güncellenemedi (db - ${key}): ${err.message}` `⚠️ TV metadata güncellenemedi (db - ${key}): ${err.message}`
@@ -5686,7 +5811,7 @@ async function rebuildMovieMetadata({ clearCache = false, resetSeriesData = fals
if (!videoEntries.length) { if (!videoEntries.length) {
removeMovieData(folder); removeMovieData(folder);
if (resetSeriesData) { if (resetSeriesData) {
removeSeriesData(folder); await removeSeriesData(folder);
} }
const update = { const update = {
primaryVideoPath: null, primaryVideoPath: null,
@@ -5713,7 +5838,7 @@ async function rebuildMovieMetadata({ clearCache = false, resetSeriesData = fals
removeMovieData(folder); removeMovieData(folder);
if (resetSeriesData) { if (resetSeriesData) {
removeSeriesData(folder); await removeSeriesData(folder);
} }
const matches = []; const matches = [];
@@ -5922,13 +6047,28 @@ app.get("/api/tvshows", requireAuth, async (req, res) => {
if (!key) continue; if (!key) continue;
const paths = tvSeriesPathsByKey(key); const paths = tvSeriesPathsByKey(key);
const parsed = parseTvSeriesKey(key); const parsed = parseTvSeriesKey(key);
const rootFolder = parsed.rootFolder || doc.rootFolder;
if (!rootFolder) continue;
const infoForFolder = readInfoForRoot(rootFolder) || {}; 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 folderName of folderSet) {
const infoForFolder = readInfoForRoot(folderName) || {};
const infoFiles = infoForFolder.files || {}; const infoFiles = infoForFolder.files || {};
const infoEpisodes = infoForFolder.seriesEpisodes || {}; const infoEpisodes = infoForFolder.seriesEpisodes || {};
const infoEpisodeIndex = new Map(); infoFilesByFolder.set(folderName, infoFiles);
for (const [relPath, meta] of Object.entries(infoEpisodes)) { for (const [relPath, meta] of Object.entries(infoEpisodes)) {
if (!meta) continue; if (!meta) continue;
@@ -5938,22 +6078,25 @@ app.get("/api/tvshows", requireAuth, async (req, res) => {
const episodeNumber = toFiniteNumber( const episodeNumber = toFiniteNumber(
meta.episode ?? meta.episodeNumber ?? meta.episodeNum meta.episode ?? meta.episodeNumber ?? meta.episodeNum
); );
if (!Number.isFinite(seasonNumber) || !Number.isFinite(episodeNumber)) continue; if (!Number.isFinite(seasonNumber) || !Number.isFinite(episodeNumber))
continue;
const normalizedRel = normalizeTrashPath(relPath); const normalizedRel = normalizeTrashPath(relPath);
const ext = path.extname(normalizedRel).toLowerCase(); const ext = path.extname(normalizedRel).toLowerCase();
if (!VIDEO_EXTS.includes(ext)) continue; if (!VIDEO_EXTS.includes(ext)) continue;
const absVideo = normalizedRel const absVideo = normalizedRel
? path.join(DOWNLOAD_DIR, rootFolder, normalizedRel) ? path.join(DOWNLOAD_DIR, folderName, normalizedRel)
: null; : null;
if (!absVideo || !fs.existsSync(absVideo)) continue; if (!absVideo || !fs.existsSync(absVideo)) continue;
infoEpisodeIndex.set(`${seasonNumber}-${episodeNumber}`, { infoEpisodeIndex.set(`${folderName}:${seasonNumber}-${episodeNumber}`, {
relPath: normalizedRel, relPath: normalizedRel,
meta meta,
folder: folderName
}); });
} }
}
let seriesData = doc.data; let seriesData = doc.data;
if (!seriesData) { if (!seriesData) {
@@ -5963,7 +6106,7 @@ app.get("/api/tvshows", requireAuth, async (req, res) => {
const seasonsObj = seriesData?.seasons || {}; const seasonsObj = seriesData?.seasons || {};
if (!Object.keys(seasonsObj).length) { if (!Object.keys(seasonsObj).length) {
removeSeriesData(rootFolder, seriesData.id ?? seriesData.tvdbId ?? null); await removeSeriesData(rootFolder, seriesData.id ?? seriesData.tvdbId ?? null);
continue; continue;
} }
@@ -5998,13 +6141,14 @@ app.get("/api/tvshows", requireAuth, async (req, res) => {
), ),
seasons: new Map(), seasons: new Map(),
primaryFolder: rootFolder, primaryFolder: rootFolder,
folders: new Set([rootFolder]) folders: new Set([rootFolder, ...docFolders])
}; };
aggregated.set(showKey, base); aggregated.set(showKey, base);
return base; return base;
})(); })();
record.folders.add(rootFolder); record.folders.add(rootFolder);
for (const extra of docFolders) record.folders.add(extra);
if ( if (
seriesData.overview && seriesData.overview &&
seriesData.overview.length > (record.overview?.length || 0) seriesData.overview.length > (record.overview?.length || 0)
@@ -6043,11 +6187,12 @@ app.get("/api/tvshows", requireAuth, async (req, res) => {
const rawEpisodes = rawSeason.episodes || {}; const rawEpisodes = rawSeason.episodes || {};
for (const [episodeKey, rawEpisode] of Object.entries(rawEpisodes)) { for (const [episodeKey, rawEpisode] of Object.entries(rawEpisodes)) {
if (!rawEpisode || typeof rawEpisode !== "object") continue; if (!rawEpisode || typeof rawEpisode !== "object") continue;
const episodeRootForCleanup = rawEpisode.folder || rootFolder;
const relativeFile = (rawEpisode.file || "").replace(/\\/g, "/"); const relativeFile = (rawEpisode.file || "").replace(/\\/g, "/");
if (relativeFile) { if (relativeFile) {
const absEpisodePath = path.join( const absEpisodePath = path.join(
DOWNLOAD_DIR, DOWNLOAD_DIR,
rootFolder, episodeRootForCleanup,
relativeFile relativeFile
); );
if (!fs.existsSync(absEpisodePath)) { if (!fs.existsSync(absEpisodePath)) {
@@ -6116,13 +6261,16 @@ app.get("/api/tvshows", requireAuth, async (req, res) => {
"0" "0"
)}E${String(episodeNumber).padStart(2, "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) { if (infoEpisode?.relPath) {
const normalizedRel = infoEpisode.relPath.replace(/^\/+/, ""); const normalizedRel = infoEpisode.relPath.replace(/^\/+/, "");
const withRoot = `${rootFolder}/${normalizedRel}`.replace(/^\/+/, ""); const withRoot = `${episodeRoot}/${normalizedRel}`.replace(/^\/+/, "");
normalizedEpisode.file = normalizedRel; normalizedEpisode.file = normalizedRel;
normalizedEpisode.videoPath = withRoot; normalizedEpisode.videoPath = withRoot;
const fileMeta = infoFiles[normalizedRel]; const fileMeta = (infoFilesByFolder.get(episodeRoot) || {})[
normalizedRel
];
if (fileMeta?.mediaInfo && !normalizedEpisode.mediaInfo) { if (fileMeta?.mediaInfo && !normalizedEpisode.mediaInfo) {
normalizedEpisode.mediaInfo = fileMeta.mediaInfo; normalizedEpisode.mediaInfo = fileMeta.mediaInfo;
} }
@@ -6138,10 +6286,10 @@ app.get("/api/tvshows", requireAuth, async (req, res) => {
const isExternal = /^https?:\/\//i.test(videoPath); const isExternal = /^https?:\/\//i.test(videoPath);
const needsFolderPrefix = const needsFolderPrefix =
!isExternal && !isExternal &&
!videoPath.startsWith(`${rootFolder}/`) && !videoPath.startsWith(`${episodeRoot}/`) &&
!videoPath.startsWith(`/${rootFolder}/`); !videoPath.startsWith(`/${episodeRoot}/`);
if (needsFolderPrefix) { if (needsFolderPrefix) {
videoPath = `${rootFolder}/${videoPath}`.replace(/\\/g, "/"); videoPath = `${episodeRoot}/${videoPath}`.replace(/\\/g, "/");
} }
const finalPath = videoPath.replace(/^\/+/, ""); const finalPath = videoPath.replace(/^\/+/, "");
if (finalPath !== rawVideoPath) { if (finalPath !== rawVideoPath) {
@@ -6149,7 +6297,7 @@ app.get("/api/tvshows", requireAuth, async (req, res) => {
} }
normalizedEpisode.videoPath = finalPath; normalizedEpisode.videoPath = finalPath;
} else if (relativeFile) { } else if (relativeFile) {
normalizedEpisode.videoPath = `${rootFolder}/${relativeFile}` normalizedEpisode.videoPath = `${episodeRoot}/${relativeFile}`
.replace(/\\/g, "/") .replace(/\\/g, "/")
.replace(/^\/+/, ""); .replace(/^\/+/, "");
if (normalizedEpisode.videoPath !== rawVideoPath) { 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); const existingEpisode = seasonRecord.episodes.get(episodeNumber);
seasonRecord.episodes.set( seasonRecord.episodes.set(
@@ -6981,7 +7129,7 @@ app.post("/api/match/manual", requireAuth, async (req, res) => {
// Mevcut movie_data ve TV verilerini temizle // Mevcut movie_data ve TV verilerini temizle
removeMovieData(rootFolder, relativeVideoPath); removeMovieData(rootFolder, relativeVideoPath);
removeSeriesData(rootFolder); await removeSeriesData(rootFolder);
// TMDB'den detaylı bilgi al // TMDB'den detaylı bilgi al
const movieDetails = await tmdbFetch(`/movie/${movieId}`, { 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 // Mevcut movie_data ve TV verilerini temizle
removeMovieData(rootFolder); removeMovieData(rootFolder);
removeSeriesData(rootFolder); await removeSeriesData(rootFolder);
// TVDB'den dizi bilgilerini al // TVDB'den dizi bilgilerini al
const extended = await fetchTvdbSeriesExtended(seriesId); const extended = await fetchTvdbSeriesExtended(seriesId);