From 609f522a9c1efe9161a591e36d55882b631b2b35 Mon Sep 17 00:00:00 2001 From: szbk Date: Fri, 12 Dec 2025 13:59:15 +0300 Subject: [PATCH] =?UTF-8?q?MongoDB=20ile=20TV=20dizileri=20i=C3=A7in=20CRU?= =?UTF-8?q?D=20i=C5=9Flemleri=20eklendi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/modules/tvDataStore.js | 74 +++++++ server/server.js | 396 ++++++++++++++++++++-------------- 2 files changed, 307 insertions(+), 163 deletions(-) create mode 100644 server/modules/tvDataStore.js diff --git a/server/modules/tvDataStore.js b/server/modules/tvDataStore.js new file mode 100644 index 0000000..b05314c --- /dev/null +++ b/server/modules/tvDataStore.js @@ -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 }); +} diff --git a/server/server.js b/server/server.js index 5b2c27d..2fba987 100644 --- a/server/server.js +++ b/server/server.js @@ -14,6 +14,13 @@ import { buildHealthReport, healthRouter } from "./modules/health.js"; import { restoreTorrentsFromDisk } from "./modules/state.js"; import { createWebsocketServer, broadcastJson } from "./modules/websocket.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 __dirname = path.dirname(__filename); @@ -2739,41 +2746,49 @@ async function ensureSeriesData( const normalizedRoot = sanitizeRelative(rootFolder); const normalizedFile = normalizeTrashPath(relativeFilePath); - const candidateKeys = listTvSeriesKeysForRoot(normalizedRoot); + const candidateKeys = await listTvSeriesKeysForRoot(normalizedRoot); let seriesData = null; let existingPaths = null; for (const key of candidateKeys) { const candidatePaths = tvSeriesPathsByKey(key); - if (!fs.existsSync(candidatePaths.metadata)) continue; + let data = null; try { - const data = JSON.parse(fs.readFileSync(candidatePaths.metadata, "utf-8")) || {}; - const seasons = data?.seasons || {}; - const matchesEpisode = Object.values(seasons).some((season) => - season?.episodes && - Object.values(season.episodes).some((episode) => episode?.file === normalizedFile) - ); - if (matchesEpisode) { - seriesData = data; - existingPaths = candidatePaths; - break; - } + data = await loadTvSeriesByKey(key); } catch (err) { - console.warn( - `⚠️ series.json okunamadı (${candidatePaths.metadata}): ${err.message}` - ); + console.warn(`⚠️ TV metadata okunamadı (db - ${key}): ${err.message}`); } - } - const legacyPaths = tvSeriesPaths(normalizedRoot); - if (!seriesData && fs.existsSync(legacyPaths.metadata)) { - try { - seriesData = JSON.parse(fs.readFileSync(legacyPaths.metadata, "utf-8")) || {}; - existingPaths = legacyPaths; - } catch (err) { - console.warn( - `⚠️ series.json okunamadı (${legacyPaths.metadata}): ${err.message}` - ); + // Fallback: eski dosya varsa bir kere oku ve Mongo'ya yaz + if (!data && fs.existsSync(candidatePaths.metadata)) { + try { + data = JSON.parse(fs.readFileSync(candidatePaths.metadata, "utf-8")) || {}; + await upsertTvSeries(key, candidatePaths.rootFolder, data); + try { + fs.rmSync(candidatePaths.metadata, { force: true }); + } catch { + /* no-op */ + } + } catch (err) { + console.warn( + `⚠️ series.json okunamadı (${candidatePaths.metadata}): ${err.message}` + ); + } + } + + if (!data) continue; + const seasons = data?.seasons || {}; + const matchesEpisode = Object.values(seasons).some( + (season) => + season?.episodes && + Object.values(season.episodes).some( + (episode) => episode?.file === normalizedFile + ) + ); + if (matchesEpisode) { + seriesData = data; + existingPaths = candidatePaths; + break; } } @@ -3167,7 +3182,13 @@ async function ensureSeriesData( seasonContainer.updatedAt = Date.now(); 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 ( existingPaths && @@ -3326,6 +3347,9 @@ function removeSeriesData(rootFolder, seriesId = null) { ? [tvSeriesKey(rootFolder, seriesId)].filter(Boolean) : listTvSeriesKeysForRoot(rootFolder); for (const key of keys) { + removeTvSeriesByKey(key).catch((err) => + console.warn(`⚠️ TV metadata silinemedi (db - ${key}): ${err.message}`) + ); const dir = tvSeriesDir(key); if (dir && fs.existsSync(dir)) { try { @@ -3344,74 +3368,110 @@ function removeSeriesEpisode(rootFolder, relativeFilePath) { const keys = listTvSeriesKeysForRoot(rootFolder); if (!keys.length) return; - for (const key of keys) { - const paths = tvSeriesPathsByKey(key); - if (!fs.existsSync(paths.metadata)) continue; + (async () => { + for (const key of keys) { + const paths = tvSeriesPathsByKey(key); - let seriesData; - try { - seriesData = JSON.parse(fs.readFileSync(paths.metadata, "utf-8")); - } catch (err) { - console.warn( - `⚠️ series.json okunamadı (${paths.metadata}): ${err.message}` - ); - continue; - } + let seriesData; + try { + seriesData = await loadTvSeriesByKey(key); + } catch (err) { + console.warn(`⚠️ TV metadata okunamadı (db - ${key}): ${err.message}`); + continue; + } + if (!seriesData) continue; - const seasons = seriesData?.seasons || {}; - let removed = false; + const seasons = seriesData?.seasons || {}; + let removed = false; - for (const [seasonKey, season] of Object.entries(seasons)) { - if (!season?.episodes) continue; + for (const [seasonKey, season] of Object.entries(seasons)) { + if (!season?.episodes) continue; - let seasonChanged = false; - for (const episodeKey of Object.keys(season.episodes)) { - const episode = season.episodes[episodeKey]; - if (episode?.file === relativeFilePath) { - delete season.episodes[episodeKey]; - seasonChanged = true; - removed = true; + let seasonChanged = false; + for (const episodeKey of Object.keys(season.episodes)) { + const episode = season.episodes[episodeKey]; + if (episode?.file === relativeFilePath) { + delete season.episodes[episodeKey]; + seasonChanged = true; + removed = true; + } + } + + if ( + seasonChanged && + (!season.episodes || Object.keys(season.episodes).length === 0) + ) { + delete seasons[seasonKey]; } } - if ( - seasonChanged && - (!season.episodes || Object.keys(season.episodes).length === 0) - ) { - delete seasons[seasonKey]; + if (!removed) continue; + + if (!Object.keys(seasons).length) { + removeSeriesData(seriesData._dupe?.folder || rootFolder, seriesData.id); + continue; + } + + seriesData.seasons = seasons; + seriesData.updatedAt = Date.now(); + + try { + await upsertTvSeries(key, paths.rootFolder, seriesData); + } catch (err) { + console.warn( + `⚠️ TV metadata güncellenemedi (db - ${key}): ${err.message}` + ); + } + + try { + const seasonDirs = [paths.episodesDir, paths.seasonsDir]; + for (const dir of seasonDirs) { + if (!dir || !fs.existsSync(dir)) continue; + cleanupEmptyDirs(dir); + } + } catch (err) { + console.warn( + `⚠️ TV metadata klasörü temizlenemedi (${paths.dir}): ${err.message}` + ); } } + })().catch((err) => + console.warn( + `⚠️ TV bölümü silme işlemi tamamlanamadı (${rootFolder}/${relativeFilePath}): ${err.message}` + ) + ); +} - if (!removed) continue; +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()); - if (!Object.keys(seasons).length) { - removeSeriesData(seriesData._dupe?.folder || rootFolder, seriesData.id); - continue; - } - - seriesData.seasons = seasons; - seriesData.updatedAt = Date.now(); + for (const dirent of dirEntries) { + const key = sanitizeRelative(dirent.name); + if (!key) continue; + const paths = tvSeriesPathsByKey(key); + if (!fs.existsSync(paths.metadata)) continue; try { - fs.writeFileSync(paths.metadata, JSON.stringify(seriesData, null, 2), "utf-8"); - } catch (err) { - console.warn( - `⚠️ series.json güncellenemedi (${paths.metadata}): ${err.message}` - ); - } - - try { - const seasonDirs = [paths.episodesDir, paths.seasonsDir]; - for (const dir of seasonDirs) { - if (!dir || !fs.existsSync(dir)) continue; - cleanupEmptyDirs(dir); + 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( - `⚠️ TV metadata klasörü temizlenemedi (${paths.dir}): ${err.message}` + `⚠️ Legacy series.json içeri aktarılamadı (${paths.metadata}): ${err.message}` ); } } + + return imported; } function purgeRootFolder(rootFolder) { @@ -3755,66 +3815,73 @@ function renameSeriesDataPaths(rootFolder, oldRel, newRel) { if (!oldPrefix || oldPrefix === newPrefix) return; const keys = listTvSeriesKeysForRoot(rootFolder); - for (const key of keys) { - const metadataPath = tvSeriesPathsByKey(key).metadata; - if (!fs.existsSync(metadataPath)) continue; + if (!keys.length) return; - let seriesData; - try { - seriesData = JSON.parse(fs.readFileSync(metadataPath, "utf-8")); - } catch (err) { - console.warn(`⚠️ series.json okunamadı (${metadataPath}): ${err.message}`); - continue; - } - - const transform = (value) => { - const normalized = normalizeTrashPath(value); - if ( - normalized === oldPrefix || - normalized.startsWith(`${oldPrefix}/`) - ) { - const suffix = normalized.slice(oldPrefix.length).replace(/^\/+/, ""); - return newPrefix - ? `${newPrefix}${suffix ? `/${suffix}` : ""}` - : suffix; - } - return value; - }; - - let changed = false; - - const seasons = seriesData?.seasons || {}; - for (const season of Object.values(seasons)) { - if (!season?.episodes) continue; - for (const episode of Object.values(season.episodes)) { - if (!episode || typeof episode !== "object") continue; - if (episode.file) { - const nextFile = transform(episode.file); - if (nextFile !== episode.file) { - episode.file = nextFile; - changed = true; - } - } - if (episode.videoPath) { - const nextVideo = transform(episode.videoPath); - if (nextVideo !== episode.videoPath) { - episode.videoPath = nextVideo; - changed = true; - } - } - } - } - - if (changed) { + (async () => { + for (const key of keys) { + const paths = tvSeriesPathsByKey(key); + let seriesData; try { - fs.writeFileSync(metadataPath, JSON.stringify(seriesData, null, 2), "utf-8"); + seriesData = await loadTvSeriesByKey(key); } catch (err) { - console.warn( - `⚠️ series.json güncellenemedi (${metadataPath}): ${err.message}` - ); + console.warn(`⚠️ TV metadata okunamadı (db - ${key}): ${err.message}`); + continue; + } + if (!seriesData) continue; + + const transform = (value) => { + const normalized = normalizeTrashPath(value); + if ( + normalized === oldPrefix || + normalized.startsWith(`${oldPrefix}/`) + ) { + const suffix = normalized.slice(oldPrefix.length).replace(/^\/+/, ""); + return newPrefix + ? `${newPrefix}${suffix ? `/${suffix}` : ""}` + : suffix; + } + return value; + }; + + let changed = false; + + const seasons = seriesData?.seasons || {}; + for (const season of Object.values(seasons)) { + if (!season?.episodes) continue; + for (const episode of Object.values(season.episodes)) { + if (!episode || typeof episode !== "object") continue; + if (episode.file) { + const nextFile = transform(episode.file); + if (nextFile !== episode.file) { + episode.file = nextFile; + changed = true; + } + } + if (episode.videoPath) { + const nextVideo = transform(episode.videoPath); + if (nextVideo !== episode.videoPath) { + episode.videoPath = nextVideo; + changed = true; + } + } + } + } + + if (changed) { + try { + await upsertTvSeries(key, paths.rootFolder, seriesData); + } catch (err) { + console.warn( + `⚠️ 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) { @@ -5818,16 +5885,19 @@ app.post("/api/youtube/download", requireAuth, async (req, res) => { }); // --- 📺 TV dizileri listesi --- -app.get("/api/tvshows", requireAuth, (req, res) => { +app.get("/api/tvshows", requireAuth, async (req, res) => { 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([]); } - const dirEntries = fs - .readdirSync(TV_DATA_ROOT, { withFileTypes: true }) - .filter((d) => d.isDirectory()); - const aggregated = new Map(); const mergeEpisode = (existing, incoming) => { @@ -5847,12 +5917,12 @@ app.get("/api/tvshows", requireAuth, (req, res) => { return merged; }; - for (const dirent of dirEntries) { - const key = sanitizeRelative(dirent.name); + for (const doc of seriesDocs) { + const key = sanitizeRelative(doc.key); if (!key) continue; const paths = tvSeriesPathsByKey(key); - if (!paths || !fs.existsSync(paths.metadata)) continue; - const { rootFolder } = parseTvSeriesKey(key); + const parsed = parseTvSeriesKey(key); + const rootFolder = parsed.rootFolder || doc.rootFolder; if (!rootFolder) continue; const infoForFolder = readInfoForRoot(rootFolder) || {}; @@ -5885,13 +5955,9 @@ app.get("/api/tvshows", requireAuth, (req, res) => { }); } - let seriesData; - try { - seriesData = JSON.parse(fs.readFileSync(paths.metadata, "utf-8")); - } catch (err) { - console.warn( - `⚠️ series.json okunamadı (${paths.metadata}): ${err.message}` - ); + let seriesData = doc.data; + if (!seriesData) { + await removeTvSeriesByKey(key); continue; } @@ -5972,7 +6038,7 @@ app.get("/api/tvshows", requireAuth, (req, res) => { ); if (!Number.isFinite(seasonNumber)) continue; - const seasonPaths = seasonAssetPaths(paths, seasonNumber); + const seasonPaths = seasonAssetPaths(paths, seasonNumber); const rawEpisodes = rawSeason.episodes || {}; for (const [episodeKey, rawEpisode] of Object.entries(rawEpisodes)) { @@ -6053,7 +6119,7 @@ app.get("/api/tvshows", requireAuth, (req, res) => { const infoEpisode = infoEpisodeIndex.get(`${seasonNumber}-${episodeNumber}`); if (infoEpisode?.relPath) { const normalizedRel = infoEpisode.relPath.replace(/^\/+/, ""); - const withRoot = `${rootFolder}/${normalizedRel}`.replace(/^\/+/, ""); + const withRoot = `${rootFolder}/${normalizedRel}`.replace(/^\/+/, ""); normalizedEpisode.file = normalizedRel; normalizedEpisode.videoPath = withRoot; const fileMeta = infoFiles[normalizedRel]; @@ -6070,13 +6136,13 @@ app.get("/api/tvshows", requireAuth, (req, res) => { let videoPath = rawVideoPath.replace(/\\/g, "/").replace(/^\.\//, ""); if (videoPath) { const isExternal = /^https?:\/\//i.test(videoPath); - const needsFolderPrefix = - !isExternal && - !videoPath.startsWith(`${rootFolder}/`) && - !videoPath.startsWith(`/${rootFolder}/`); - if (needsFolderPrefix) { - videoPath = `${rootFolder}/${videoPath}`.replace(/\\/g, "/"); - } + const needsFolderPrefix = + !isExternal && + !videoPath.startsWith(`${rootFolder}/`) && + !videoPath.startsWith(`/${rootFolder}/`); + if (needsFolderPrefix) { + videoPath = `${rootFolder}/${videoPath}`.replace(/\\/g, "/"); + } const finalPath = videoPath.replace(/^\/+/, ""); if (finalPath !== rawVideoPath) { dataChanged = true; @@ -6130,14 +6196,10 @@ app.get("/api/tvshows", requireAuth, (req, res) => { try { seriesData.seasons = seasonsObj; seriesData.updatedAt = Date.now(); - fs.writeFileSync( - paths.metadata, - JSON.stringify(seriesData, null, 2), - "utf-8" - ); + await upsertTvSeries(key, rootFolder, seriesData); } catch (err) { 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); if (!fs.existsSync(rootDir)) continue; + if (clearCache) { + try { + await removeTvSeriesByRoot(folder); + } catch (err) { + console.warn(`⚠️ TV metadata temizlenemedi (db - ${folder}): ${err.message}`); + } + } + try { const info = readInfoForRoot(folder) || {}; const infoFiles = info.files || {};