MongoDB ile TV dizileri için CRUD işlemleri eklendi

This commit is contained in:
2025-12-12 13:59:15 +03:00
parent 15611b1dc7
commit 609f522a9c
2 changed files with 307 additions and 163 deletions

View File

@@ -0,0 +1,74 @@
import { connectMongo } from "./db.js";
const COLLECTION = "tv_data";
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({ updatedAt: -1 });
return col;
}
function buildDocument(key, rootFolder, seriesData) {
const tvdbId = seriesData?.id ?? seriesData?.tvdbId ?? null;
return {
_id: key,
key,
rootFolder,
tvdbId,
name: seriesData?.name || null,
data: seriesData || {},
updatedAt: Date.now()
};
}
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 });
return doc.data;
}
export async function getTvSeriesByKey(key) {
const col = await getCollection();
const doc = await col.findOne({ _id: key });
return doc?.data || null;
}
export async function getTvSeriesByRoot(rootFolder) {
const col = await getCollection();
const docs = await col.find({ rootFolder }).toArray();
return docs.map((doc) => ({
key: doc.key,
rootFolder: doc.rootFolder,
data: doc.data
}));
}
export async function listAllTvSeries() {
const col = await getCollection();
const docs = await col.find({}).toArray();
return docs.map((doc) => ({
key: doc.key,
rootFolder: doc.rootFolder,
data: doc.data
}));
}
export async function listTvSeriesKeysForRoot(rootFolder) {
const col = await getCollection();
const docs = await col.find({ rootFolder }).project({ key: 1 }).toArray();
return docs.map((d) => d.key).filter(Boolean);
}
export async function removeTvSeriesByKey(key) {
const col = await getCollection();
await col.deleteOne({ _id: key });
}
export async function removeTvSeriesByRoot(rootFolder) {
const col = await getCollection();
await col.deleteMany({ rootFolder });
}

View File

@@ -14,6 +14,13 @@ import { buildHealthReport, healthRouter } from "./modules/health.js";
import { restoreTorrentsFromDisk } from "./modules/state.js"; 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 {
getTvSeriesByKey as loadTvSeriesByKey,
listAllTvSeries as listAllTvSeriesFromDb,
removeTvSeriesByKey,
removeTvSeriesByRoot,
upsertTvSeries
} from "./modules/tvDataStore.js";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@@ -2739,24 +2746,28 @@ async function ensureSeriesData(
const normalizedRoot = sanitizeRelative(rootFolder); const normalizedRoot = sanitizeRelative(rootFolder);
const normalizedFile = normalizeTrashPath(relativeFilePath); const normalizedFile = normalizeTrashPath(relativeFilePath);
const candidateKeys = listTvSeriesKeysForRoot(normalizedRoot); const candidateKeys = await listTvSeriesKeysForRoot(normalizedRoot);
let seriesData = null; let seriesData = null;
let existingPaths = null; let existingPaths = null;
for (const key of candidateKeys) { for (const key of candidateKeys) {
const candidatePaths = tvSeriesPathsByKey(key); const candidatePaths = tvSeriesPathsByKey(key);
if (!fs.existsSync(candidatePaths.metadata)) continue; let data = null;
try { try {
const data = JSON.parse(fs.readFileSync(candidatePaths.metadata, "utf-8")) || {}; data = await loadTvSeriesByKey(key);
const seasons = data?.seasons || {}; } catch (err) {
const matchesEpisode = Object.values(seasons).some((season) => console.warn(`⚠️ TV metadata okunamadı (db - ${key}): ${err.message}`);
season?.episodes && }
Object.values(season.episodes).some((episode) => episode?.file === normalizedFile)
); // Fallback: eski dosya varsa bir kere oku ve Mongo'ya yaz
if (matchesEpisode) { if (!data && fs.existsSync(candidatePaths.metadata)) {
seriesData = data; try {
existingPaths = candidatePaths; data = JSON.parse(fs.readFileSync(candidatePaths.metadata, "utf-8")) || {};
break; await upsertTvSeries(key, candidatePaths.rootFolder, data);
try {
fs.rmSync(candidatePaths.metadata, { force: true });
} catch {
/* no-op */
} }
} catch (err) { } catch (err) {
console.warn( console.warn(
@@ -2765,15 +2776,19 @@ async function ensureSeriesData(
} }
} }
const legacyPaths = tvSeriesPaths(normalizedRoot); if (!data) continue;
if (!seriesData && fs.existsSync(legacyPaths.metadata)) { const seasons = data?.seasons || {};
try { const matchesEpisode = Object.values(seasons).some(
seriesData = JSON.parse(fs.readFileSync(legacyPaths.metadata, "utf-8")) || {}; (season) =>
existingPaths = legacyPaths; season?.episodes &&
} catch (err) { Object.values(season.episodes).some(
console.warn( (episode) => episode?.file === normalizedFile
`⚠️ series.json okunamadı (${legacyPaths.metadata}): ${err.message}` )
); );
if (matchesEpisode) {
seriesData = data;
existingPaths = candidatePaths;
break;
} }
} }
@@ -3167,7 +3182,13 @@ async function ensureSeriesData(
seasonContainer.updatedAt = Date.now(); seasonContainer.updatedAt = Date.now();
ensureDirForFile(seriesMetaPath); ensureDirForFile(seriesMetaPath);
fs.writeFileSync(seriesMetaPath, JSON.stringify(seriesData, null, 2), "utf-8"); try {
await upsertTvSeries(targetPaths.key, normalizedRoot, seriesData);
} catch (err) {
console.warn(
`⚠️ TV metadata kaydedilemedi (db - ${targetPaths.key}): ${err.message}`
);
}
if ( if (
existingPaths && existingPaths &&
@@ -3326,6 +3347,9 @@ function removeSeriesData(rootFolder, seriesId = null) {
? [tvSeriesKey(rootFolder, seriesId)].filter(Boolean) ? [tvSeriesKey(rootFolder, seriesId)].filter(Boolean)
: listTvSeriesKeysForRoot(rootFolder); : listTvSeriesKeysForRoot(rootFolder);
for (const key of keys) { for (const key of keys) {
removeTvSeriesByKey(key).catch((err) =>
console.warn(`⚠️ TV metadata silinemedi (db - ${key}): ${err.message}`)
);
const dir = tvSeriesDir(key); const dir = tvSeriesDir(key);
if (dir && fs.existsSync(dir)) { if (dir && fs.existsSync(dir)) {
try { try {
@@ -3344,19 +3368,18 @@ function removeSeriesEpisode(rootFolder, relativeFilePath) {
const keys = listTvSeriesKeysForRoot(rootFolder); const keys = 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);
if (!fs.existsSync(paths.metadata)) continue;
let seriesData; let seriesData;
try { try {
seriesData = JSON.parse(fs.readFileSync(paths.metadata, "utf-8")); seriesData = await loadTvSeriesByKey(key);
} catch (err) { } catch (err) {
console.warn( console.warn(`⚠️ TV metadata okunamadı (db - ${key}): ${err.message}`);
`⚠️ series.json okunamadı (${paths.metadata}): ${err.message}`
);
continue; continue;
} }
if (!seriesData) continue;
const seasons = seriesData?.seasons || {}; const seasons = seriesData?.seasons || {};
let removed = false; let removed = false;
@@ -3393,10 +3416,10 @@ function removeSeriesEpisode(rootFolder, relativeFilePath) {
seriesData.updatedAt = Date.now(); seriesData.updatedAt = Date.now();
try { try {
fs.writeFileSync(paths.metadata, JSON.stringify(seriesData, null, 2), "utf-8"); await upsertTvSeries(key, paths.rootFolder, seriesData);
} catch (err) { } catch (err) {
console.warn( console.warn(
`⚠️ series.json güncellenemedi (${paths.metadata}): ${err.message}` `⚠️ TV metadata güncellenemedi (db - ${key}): ${err.message}`
); );
} }
@@ -3412,6 +3435,43 @@ function removeSeriesEpisode(rootFolder, relativeFilePath) {
); );
} }
} }
})().catch((err) =>
console.warn(
`⚠️ TV bölümü silme işlemi tamamlanamadı (${rootFolder}/${relativeFilePath}): ${err.message}`
)
);
}
async function importLegacySeriesMetadata() {
if (!fs.existsSync(TV_DATA_ROOT)) return 0;
let imported = 0;
const dirEntries = fs
.readdirSync(TV_DATA_ROOT, { withFileTypes: true })
.filter((d) => d.isDirectory());
for (const dirent of dirEntries) {
const key = sanitizeRelative(dirent.name);
if (!key) continue;
const paths = tvSeriesPathsByKey(key);
if (!fs.existsSync(paths.metadata)) continue;
try {
const data = JSON.parse(fs.readFileSync(paths.metadata, "utf-8")) || {};
await upsertTvSeries(key, paths.rootFolder, data);
imported += 1;
try {
fs.rmSync(paths.metadata, { force: true });
} catch {
/* no-op */
}
} catch (err) {
console.warn(
`⚠️ Legacy series.json içeri aktarılamadı (${paths.metadata}): ${err.message}`
);
}
}
return imported;
} }
function purgeRootFolder(rootFolder) { function purgeRootFolder(rootFolder) {
@@ -3755,17 +3815,19 @@ function renameSeriesDataPaths(rootFolder, oldRel, newRel) {
if (!oldPrefix || oldPrefix === newPrefix) return; if (!oldPrefix || oldPrefix === newPrefix) return;
const keys = listTvSeriesKeysForRoot(rootFolder); const keys = listTvSeriesKeysForRoot(rootFolder);
for (const key of keys) { if (!keys.length) return;
const metadataPath = tvSeriesPathsByKey(key).metadata;
if (!fs.existsSync(metadataPath)) continue;
(async () => {
for (const key of keys) {
const paths = tvSeriesPathsByKey(key);
let seriesData; let seriesData;
try { try {
seriesData = JSON.parse(fs.readFileSync(metadataPath, "utf-8")); seriesData = await loadTvSeriesByKey(key);
} catch (err) { } catch (err) {
console.warn(`⚠️ series.json okunamadı (${metadataPath}): ${err.message}`); console.warn(`⚠️ TV metadata okunamadı (db - ${key}): ${err.message}`);
continue; continue;
} }
if (!seriesData) continue;
const transform = (value) => { const transform = (value) => {
const normalized = normalizeTrashPath(value); const normalized = normalizeTrashPath(value);
@@ -3807,14 +3869,19 @@ function renameSeriesDataPaths(rootFolder, oldRel, newRel) {
if (changed) { if (changed) {
try { try {
fs.writeFileSync(metadataPath, JSON.stringify(seriesData, null, 2), "utf-8"); await upsertTvSeries(key, paths.rootFolder, seriesData);
} catch (err) { } catch (err) {
console.warn( console.warn(
`⚠️ series.json güncellenemedi (${metadataPath}): ${err.message}` `⚠️ TV metadata güncellenemedi (db - ${key}): ${err.message}`
); );
} }
} }
} }
})().catch((err) =>
console.warn(
`⚠️ TV metadata yeniden adlandırma tamamlanamadı (${rootFolder}): ${err.message}`
)
);
} }
function removeThumbnailsForDirectory(rootFolder, relativeDir) { function removeThumbnailsForDirectory(rootFolder, relativeDir) {
@@ -5818,16 +5885,19 @@ app.post("/api/youtube/download", requireAuth, async (req, res) => {
}); });
// --- 📺 TV dizileri listesi --- // --- 📺 TV dizileri listesi ---
app.get("/api/tvshows", requireAuth, (req, res) => { app.get("/api/tvshows", requireAuth, async (req, res) => {
try { try {
if (!fs.existsSync(TV_DATA_ROOT)) { let seriesDocs = await listAllTvSeriesFromDb();
if (!seriesDocs || !seriesDocs.length) {
const migrated = await importLegacySeriesMetadata();
if (migrated > 0) {
seriesDocs = await listAllTvSeriesFromDb();
}
}
if (!seriesDocs || !seriesDocs.length) {
return res.json([]); return res.json([]);
} }
const dirEntries = fs
.readdirSync(TV_DATA_ROOT, { withFileTypes: true })
.filter((d) => d.isDirectory());
const aggregated = new Map(); const aggregated = new Map();
const mergeEpisode = (existing, incoming) => { const mergeEpisode = (existing, incoming) => {
@@ -5847,12 +5917,12 @@ app.get("/api/tvshows", requireAuth, (req, res) => {
return merged; return merged;
}; };
for (const dirent of dirEntries) { for (const doc of seriesDocs) {
const key = sanitizeRelative(dirent.name); const key = sanitizeRelative(doc.key);
if (!key) continue; if (!key) continue;
const paths = tvSeriesPathsByKey(key); const paths = tvSeriesPathsByKey(key);
if (!paths || !fs.existsSync(paths.metadata)) continue; const parsed = parseTvSeriesKey(key);
const { rootFolder } = parseTvSeriesKey(key); const rootFolder = parsed.rootFolder || doc.rootFolder;
if (!rootFolder) continue; if (!rootFolder) continue;
const infoForFolder = readInfoForRoot(rootFolder) || {}; const infoForFolder = readInfoForRoot(rootFolder) || {};
@@ -5885,13 +5955,9 @@ app.get("/api/tvshows", requireAuth, (req, res) => {
}); });
} }
let seriesData; let seriesData = doc.data;
try { if (!seriesData) {
seriesData = JSON.parse(fs.readFileSync(paths.metadata, "utf-8")); await removeTvSeriesByKey(key);
} catch (err) {
console.warn(
`⚠️ series.json okunamadı (${paths.metadata}): ${err.message}`
);
continue; continue;
} }
@@ -6130,14 +6196,10 @@ app.get("/api/tvshows", requireAuth, (req, res) => {
try { try {
seriesData.seasons = seasonsObj; seriesData.seasons = seasonsObj;
seriesData.updatedAt = Date.now(); seriesData.updatedAt = Date.now();
fs.writeFileSync( await upsertTvSeries(key, rootFolder, seriesData);
paths.metadata,
JSON.stringify(seriesData, null, 2),
"utf-8"
);
} catch (err) { } catch (err) {
console.warn( console.warn(
`⚠️ series.json güncellenemedi (${paths.metadata}): ${err.message}` `⚠️ TV metadata güncellenemedi (db - ${key}): ${err.message}`
); );
} }
} }
@@ -6313,6 +6375,14 @@ async function rebuildTvMetadata({ clearCache = false } = {}) {
const rootDir = path.join(DOWNLOAD_DIR, folder); const rootDir = path.join(DOWNLOAD_DIR, folder);
if (!fs.existsSync(rootDir)) continue; if (!fs.existsSync(rootDir)) continue;
if (clearCache) {
try {
await removeTvSeriesByRoot(folder);
} catch (err) {
console.warn(`⚠️ TV metadata temizlenemedi (db - ${folder}): ${err.message}`);
}
}
try { try {
const info = readInfoForRoot(folder) || {}; const info = readInfoForRoot(folder) || {};
const infoFiles = info.files || {}; const infoFiles = info.files || {};