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:
@@ -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;
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
244
server/server.js
244
server/server.js
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user