diff --git a/.env.example b/.env.example index 9d7fb34..c134133 100644 --- a/.env.example +++ b/.env.example @@ -5,16 +5,4 @@ VITE_API=http://localhost:3001 TMDB_API_KEY="..." TVDB_API_KEY="..." VIDEO_THUMBNAIL_TIME=10 -FANART_TV_API_KEY=".." - -# MongoDB -MONGO_HOST=mongo -MONGO_PORT=27017 -MONGO_DB=dupe -MONGO_AUTH_SOURCE=admin -MONGO_INITDB_ROOT_USERNAME=dupe -MONGO_INITDB_ROOT_PASSWORD=dupe -MONGO_USER=dupe -MONGO_PASS=dupe -# Opsiyonel özel bağlantı stringi (MONGO_HOST/PORT yerine geçer) -# MONGO_URI=mongodb://dupe:dupe@mongo:27017/dupe?authSource=admin +FANART_TV_API_KEY=".." \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8326525..db69546 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,6 @@ # Node modules */node_modules/ jspm_packages/ -/data -.kilocode/ -client/dist -claudedocs/ # Logs npm-debug.log* diff --git a/client/src/components/Sidebar.svelte b/client/src/components/Sidebar.svelte index 80cfdd7..4eb8fd7 100644 --- a/client/src/components/Sidebar.svelte +++ b/client/src/components/Sidebar.svelte @@ -16,9 +16,7 @@ let hasMusic = false; // Svelte store kullanarak reaktivite sağla import { writable } from 'svelte/store'; const diskSpaceStore = writable({ totalGB: '0', usedGB: '0', usedPercent: 0 }); - let diskSpace; - let hasMedia = false; - $: hasMedia = hasMovies || hasShows || hasMusic; + let diskSpace; // Store subscription'ı temizlemek için let unsubscribeDiskSpace; @@ -135,7 +133,7 @@ const unsubscribeMusic = musicCount.subscribe((count) => { - - diff --git a/client/src/routes/Files.svelte b/client/src/routes/Files.svelte index ba8870e..8c986da 100644 --- a/client/src/routes/Files.svelte +++ b/client/src/routes/Files.svelte @@ -4,7 +4,6 @@ import { cleanFileName, extractTitleAndYear } from "../utils/filename.js"; import { refreshMovieCount } from "../stores/movieStore.js"; import { refreshTvShowCount } from "../stores/tvStore.js"; - import { fetchTrashItems } from "../stores/trashStore.js"; import { activeSearchTerm, setSearchScope, @@ -1159,11 +1158,7 @@ } await loadFiles(); - await Promise.all([ - refreshMovieCount(), - refreshTvShowCount(), - fetchTrashItems() - ]); + await Promise.all([refreshMovieCount(), refreshTvShowCount()]); if (errors.length > 0) { alert("Silme hatası: " + errors[0]); @@ -1412,11 +1407,7 @@ } await loadFiles(); - await Promise.all([ - refreshMovieCount(), - refreshTvShowCount(), - fetchTrashItems() - ]); + await Promise.all([refreshMovieCount(), refreshTvShowCount()]); selectedItems = new Set( [...selectedItems].filter((name) => name !== item.name), ); diff --git a/client/src/routes/Trash.svelte b/client/src/routes/Trash.svelte index 6803ffa..eb69eb5 100644 --- a/client/src/routes/Trash.svelte +++ b/client/src/routes/Trash.svelte @@ -162,8 +162,7 @@ async function restoreItemFromMenu(item) { if (!item?.trashName) return; - // Menü hemen kapansın, API sonucu beklemesin - closeMenu(); + try { const result = await restoreItem(item.trashName); if (result.success) { @@ -175,6 +174,8 @@ console.error("Geri yükleme hatası:", err); alert("Geri yükleme sırasında bir hata oluştu."); } + + closeMenu(); } async function deleteItemFromMenu(item) { diff --git a/client/src/routes/TvShows.svelte b/client/src/routes/TvShows.svelte index dd60488..e7f836c 100644 --- a/client/src/routes/TvShows.svelte +++ b/client/src/routes/TvShows.svelte @@ -2,7 +2,7 @@ import { onMount, tick } from "svelte"; import { API, apiFetch } from "../utils/api.js"; import { cleanFileName } from "../utils/filename.js"; - import { tvShowRefreshVersion } from "../stores/tvStore.js"; + import { tvShowCount } from "../stores/tvStore.js"; import { activeSearchTerm, setSearchScope @@ -13,8 +13,6 @@ let refreshing = false; let rescanning = false; let error = null; - let mounted = false; - let unsubscribeVersion = null; let selectedShow = null; let selectedSeason = null; @@ -190,9 +188,11 @@ let filteredShows = []; if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const list = await resp.json(); shows = Array.isArray(list) ? list.map(normalizeShow) : []; + tvShowCount.set(shows.length); } catch (err) { error = err?.message || "TV dizileri alınamadı."; shows = []; + tvShowCount.set(0); } finally { loading = false; } @@ -251,21 +251,6 @@ let filteredShows = []; } } - onMount(() => { - mounted = true; - loadShows(); - unsubscribeVersion = tvShowRefreshVersion.subscribe((ver) => { - if (!mounted) return; - if (loading || refreshing || rescanning) return; - if (ver === null) return; - loadShows(); - }); - return () => { - mounted = false; - unsubscribeVersion && unsubscribeVersion(); - }; - }); - function openShow(show) { if (!show) return; selectedShow = show; diff --git a/client/src/stores/tvStore.js b/client/src/stores/tvStore.js index 37016c1..4b3fbf9 100644 --- a/client/src/stores/tvStore.js +++ b/client/src/stores/tvStore.js @@ -2,14 +2,11 @@ import { writable } from "svelte/store"; import { apiFetch } from "../utils/api.js"; export const tvShowCount = writable(0); -export const tvShowRefreshVersion = writable(0); - let requestSeq = 0; let lastValue = 0; let zeroTimer = null; export async function refreshTvShowCount() { - const prevValue = lastValue; const ticket = ++requestSeq; try { const resp = await apiFetch("/api/tvshows"); @@ -38,10 +35,6 @@ export async function refreshTvShowCount() { lastValue = 0; tvShowCount.set(0); } - // Sadece sayım gerçekten değiştiyse UI yenilemeyi tetikle - if (nextVal !== prevValue) { - tvShowRefreshVersion.update((v) => v + 1); - } } catch (err) { console.warn("⚠️ TV show count güncellenemedi:", err?.message || err); // Hata durumunda mevcut değeri koru, titreşimi önle diff --git a/client/src/styles/main.css b/client/src/styles/main.css index ec2219e..186b9a3 100644 --- a/client/src/styles/main.css +++ b/client/src/styles/main.css @@ -63,6 +63,7 @@ body, .sidebar .menu { padding-top: 6px; + flex: 1; } .sidebar .menu .item { @@ -617,11 +618,6 @@ img.thumb.loaded { border-top: 1px solid var(--border); } -/* Media menüsünün disk space'den önce olmasını sağla */ -.sidebar .media-menu { - margin-top: 10px; -} - .sidebar .disk-space-header { display: flex; align-items: center; diff --git a/docker-compose.yml b/docker-compose.yml index 8106b49..d0a7aa6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,25 +1,9 @@ version: "3.9" services: - mongo: - image: mongo:7 - container_name: mongo - restart: unless-stopped - environment: - MONGO_INITDB_ROOT_USERNAME: ${MONGO_INITDB_ROOT_USERNAME:-dupe} - MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD:-dupe} - MONGO_INITDB_DATABASE: ${MONGO_DB:-dupe} - volumes: - - ./data/mongo:/data/db - - ./mongo-init:/docker-entrypoint-initdb.d:ro - ports: - - "27017:27017" - dupe: build: . container_name: app - depends_on: - - mongo ports: - "3001:3001" volumes: @@ -34,9 +18,3 @@ services: TVDB_API_KEY: ${TVDB_API_KEY} FANART_TV_API_KEY: ${FANART_TV_API_KEY} VIDEO_THUMBNAIL_TIME: ${VIDEO_THUMBNAIL_TIME} - MONGO_HOST: ${MONGO_HOST:-mongo} - MONGO_PORT: ${MONGO_PORT:-27017} - MONGO_DB: ${MONGO_DB:-dupe} - MONGO_USER: ${MONGO_INITDB_ROOT_USERNAME:-dupe} - MONGO_PASS: ${MONGO_INITDB_ROOT_PASSWORD:-dupe} - MONGO_AUTH_SOURCE: ${MONGO_AUTH_SOURCE:-admin} diff --git a/mongo-init/init.js b/mongo-init/init.js deleted file mode 100644 index 998ef0a..0000000 --- a/mongo-init/init.js +++ /dev/null @@ -1,24 +0,0 @@ -// MongoDB başlangıç scripti: istenen veritabanını açar ve boş bir koleksiyonla başlatır -(async () => { - const dbName = - process.env.MONGO_DB || - process.env.MONGO_INITDB_DATABASE || - "dupe"; - - // Mevcut bağlantı, root kullanıcı ile admin DB üzerinden geliyor - const db = db.getSiblingDB(dbName); - - const marker = "dupe_init_marker"; - const collections = await db.getCollectionNames(); - - if (!collections.includes(marker)) { - db.createCollection(marker); - db[marker].insertOne({ - createdAt: new Date(), - note: "dupe init marker" - }); - print(`📦 '${dbName}' veritabanı için init marker oluşturuldu.`); - } else { - print(`ℹ️ '${dbName}' veritabanı zaten başlatılmış.`); - } -})(); diff --git a/server/modules/db.js b/server/modules/db.js deleted file mode 100644 index a9af71e..0000000 --- a/server/modules/db.js +++ /dev/null @@ -1,39 +0,0 @@ -import { MongoClient } from "mongodb"; - -const MONGO_HOST = process.env.MONGO_HOST || "mongo"; -const MONGO_PORT = process.env.MONGO_PORT || "27017"; -const MONGO_DB = process.env.MONGO_DB || "dupe"; -const MONGO_USER = process.env.MONGO_USER || "dupe"; -const MONGO_PASS = process.env.MONGO_PASS || "dupe"; -const MONGO_AUTH_SOURCE = process.env.MONGO_AUTH_SOURCE || "admin"; - -const DEFAULT_URI = `mongodb://${encodeURIComponent(MONGO_USER)}:${encodeURIComponent( - MONGO_PASS -)}@${MONGO_HOST}:${MONGO_PORT}/${encodeURIComponent(MONGO_DB)}?authSource=${encodeURIComponent( - MONGO_AUTH_SOURCE -)}`; - -const MONGO_URI = process.env.MONGO_URI || DEFAULT_URI; - -let client = null; -let db = null; - -export async function connectMongo() { - if (client && db) return { client, db }; - - const mongoClient = new MongoClient(MONGO_URI, { - maxPoolSize: 10, - serverSelectionTimeoutMS: 5000 - }); - - await mongoClient.connect(); - client = mongoClient; - db = mongoClient.db(MONGO_DB); - - console.log(`📦 MongoDB bağlantısı hazır (db: ${db.databaseName})`); - return { client, db }; -} - -export function getDb() { - return db; -} diff --git a/server/modules/tvDataStore.js b/server/modules/tvDataStore.js deleted file mode 100644 index c0d1729..0000000 --- a/server/modules/tvDataStore.js +++ /dev/null @@ -1,154 +0,0 @@ -import { connectMongo } from "./db.js"; - -const COLLECTION = "tv_data"; -const TVDB_KEY_PREFIX = "tvdb-"; - -function canonicalTvdbKey(tvdbId) { - if (tvdbId === null || tvdbId === undefined) return null; - return `${TVDB_KEY_PREFIX}${tvdbId}`; -} - -async function getCollection() { - const { db } = await connectMongo(); - const col = db.collection(COLLECTION); - await col.createIndex({ rootFolder: 1 }); - await col.createIndex({ rootFolders: 1 }); - await col.createIndex({ tvdbId: 1 }, { sparse: true }); - await col.createIndex({ updatedAt: -1 }); - return col; -} - -function buildDocument(key, rootFolder, seriesData) { - const tvdbId = seriesData?.id ?? seriesData?.tvdbId ?? null; - return { - _id: key, - key, - rootFolder, - rootFolders: rootFolder ? [rootFolder] : [], - tvdbId, - name: seriesData?.name || null, - data: seriesData || {}, - updatedAt: Date.now() - }; -} - -export async function upsertTvSeries(key, rootFolder, seriesData) { - const col = await getCollection(); - const tvdbId = seriesData?.id ?? seriesData?.tvdbId ?? null; - const canonicalKey = tvdbId !== null ? canonicalTvdbKey(tvdbId) : null; - const targetKey = canonicalKey || key; - - const existingByTvdb = - tvdbId !== null ? await col.findOne({ tvdbId }) : null; - const existing = - existingByTvdb || - (await col.findOne({ _id: targetKey })) || - (await col.findOne({ _id: key })); - - const desiredKey = canonicalKey || existingByTvdb?._id || targetKey; - const doc = buildDocument( - desiredKey, - rootFolder || existing?.rootFolder, - seriesData - ); - - const rootSet = new Set(existing?.rootFolders || []); - if (existing?.rootFolder) rootSet.add(existing.rootFolder); - if (rootFolder) rootSet.add(rootFolder); - doc.rootFolders = Array.from(rootSet); - doc.rootFolder = doc.rootFolder || doc.rootFolders[0] || null; - doc.tvdbId = tvdbId; - doc.key = canonicalKey || doc._id; - doc._id = doc.key; - - await col.updateOne({ _id: doc._id }, { $set: doc }, { upsert: true }); - - // Eğer eski bir anahtar farklıysa temizle - if (existing && existing._id !== doc._id) { - await col.deleteOne({ _id: existing._id }).catch(() => {}); - } - - return doc.data; -} - -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({ - $or: [{ rootFolder }, { rootFolders: rootFolder }] - }) - .toArray(); - return docs.map((doc) => ({ - key: doc.key, - rootFolder: doc.rootFolder, - rootFolders: doc.rootFolders || [], - 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, - rootFolders: doc.rootFolders || [], - data: doc.data - })); -} - -export async function listTvSeriesKeysForRoot(rootFolder) { - const col = await getCollection(); - const docs = await col - .find({ - $or: [{ rootFolder }, { rootFolders: 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(); - 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 }; diff --git a/server/package.json b/server/package.json index c220dc2..7b966be 100644 --- a/server/package.json +++ b/server/package.json @@ -8,7 +8,6 @@ "dependencies": { "cors": "^2.8.5", "express": "^4.19.2", - "mongodb": "^6.9.0", "jsonwebtoken": "^9.0.2", "mime-types": "^2.1.35", "multer": "^1.4.5-lts.1", diff --git a/server/server.js b/server/server.js index 927b13f..2da7f94 100644 --- a/server/server.js +++ b/server/server.js @@ -13,16 +13,6 @@ import { createAuth } from "./modules/auth.js"; 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 { - canonicalTvdbKey, - getTvSeriesByKey as loadTvSeriesByKey, - listAllTvSeries as listAllTvSeriesFromDb, - listTvSeriesKeysForRoot as listTvSeriesKeysForRootDb, - removeTvSeriesByKey, - removeTvSeriesByRoot, - upsertTvSeries -} from "./modules/tvDataStore.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -107,16 +97,6 @@ app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use("/downloads", express.static(DOWNLOAD_DIR)); -// MongoDB bağlantısını başlat -connectMongo() - .then(({ db }) => { - app.locals.db = db; - app.locals.getDb = getDb; - }) - .catch((err) => { - console.error("❌ MongoDB bağlantısı kurulamadı:", err.message); - }); - // --- En uygun video dosyasını seç --- function pickBestVideoFile(torrent) { const videos = torrent.files @@ -2748,49 +2728,41 @@ async function ensureSeriesData( const normalizedRoot = sanitizeRelative(rootFolder); const normalizedFile = normalizeTrashPath(relativeFilePath); - const candidateKeys = await listTvSeriesKeysForRoot(normalizedRoot); + const candidateKeys = listTvSeriesKeysForRoot(normalizedRoot); let seriesData = null; let existingPaths = null; for (const key of candidateKeys) { const candidatePaths = tvSeriesPathsByKey(key); - let data = null; + if (!fs.existsSync(candidatePaths.metadata)) continue; try { - data = await loadTvSeriesByKey(key); - } catch (err) { - console.warn(`⚠️ TV metadata okunamadı (db - ${key}): ${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) => + 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; + Object.values(season.episodes).some((episode) => episode?.file === normalizedFile) + ); + if (matchesEpisode) { + seriesData = data; + existingPaths = candidatePaths; + break; + } + } catch (err) { + console.warn( + `⚠️ series.json okunamadı (${candidatePaths.metadata}): ${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}` + ); } } @@ -2817,63 +2789,22 @@ async function ensureSeriesData( seriesData.tvdbId = seriesId; } - // Eğer aynı tvdbId için kanonik kayıt varsa onu yükle (bölümler birleşsin) - if (!existingPaths && seriesId) { - const canonicalKey = canonicalTvdbKey(seriesId); - try { - const existingCanonical = await loadTvSeriesByKey(canonicalKey); - if (existingCanonical) { - seriesData = existingCanonical; - existingPaths = tvSeriesPathsByKey(canonicalKey); - } - } catch (err) { - console.warn( - `⚠️ TV metadata kanonik kayıt okunamadı (${canonicalKey}): ${err.message}` - ); - } - } + let targetPaths = + existingPaths && existingPaths.key?.includes("__") ? existingPaths : null; - // Her zaman kanonik anahtarı hedefle; legacy path varsa taşı - const canonicalPaths = buildTvSeriesPaths( - normalizedRoot, - seriesId, - seriesInfo.title - ); - let targetPaths = canonicalPaths || existingPaths || null; - if (!targetPaths && existingPaths) { - targetPaths = existingPaths; + if (!targetPaths) { + targetPaths = buildTvSeriesPaths( + normalizedRoot, + seriesId, + seriesInfo.title + ); + if (!targetPaths && existingPaths) { + targetPaths = existingPaths; + } } if (!targetPaths) return null; - // Legacy klasör -> kanonik klasöre taşı - if ( - existingPaths && - canonicalPaths && - existingPaths.key !== canonicalPaths.key && - fs.existsSync(existingPaths.dir) - ) { - try { - if (!fs.existsSync(targetPaths.dir)) { - fs.mkdirSync(targetPaths.dir, { recursive: true }); - } - const entries = fs.readdirSync(existingPaths.dir, { withFileTypes: true }); - for (const entry of entries) { - const src = path.join(existingPaths.dir, entry.name); - const dest = path.join(targetPaths.dir, entry.name); - if (!fs.existsSync(dest)) { - fs.renameSync(src, dest); - } - } - fs.rmSync(existingPaths.dir, { recursive: true, force: true }); - console.log(`🔀 TV metadata klasörü taşındı: ${existingPaths.key} -> ${targetPaths.key}`); - } catch (err) { - console.warn( - `⚠️ TV metadata klasörü taşınamadı (${existingPaths.key} -> ${targetPaths.key}): ${err.message}` - ); - } - } - const showDir = targetPaths.dir; const seriesMetaPath = targetPaths.metadata; @@ -3212,7 +3143,6 @@ async function ensureSeriesData( still: fs.existsSync(stillPath) ? encodeTvDataPath(targetPaths.key, path.relative(showDir, stillPath)) : null, - folder: normalizedRoot, file: normalizedFile, mediaInfo: mediaInfo || null, tvdbEpisodeId: episodeTvdbId, @@ -3225,55 +3155,8 @@ async function ensureSeriesData( seasonContainer.episodeCount = Object.keys(seasonContainer.episodes).length; seasonContainer.updatedAt = Date.now(); - // Eski kayıt varsa sezon/bölüm verilerini koruyarak birleştir - try { - const existingDoc = await loadTvSeriesByKey(targetPaths.key); - const existingSeasons = - existingDoc && typeof existingDoc.seasons === "object" - ? existingDoc.seasons - : {}; - const incomingSeasons = - seriesData && typeof seriesData.seasons === "object" - ? seriesData.seasons - : {}; - - const mergedSeasons = { ...existingSeasons }; - - // Önce mevcut sezonları kopyala, sonra gelen verileri ekle/override et - for (const [seasonKey, incomingSeason] of Object.entries(incomingSeasons)) { - const prevSeason = mergedSeasons[seasonKey] || {}; - const prevEpisodes = - (prevSeason && typeof prevSeason.episodes === "object" - ? prevSeason.episodes - : {}) || {}; - const nextEpisodes = { ...prevEpisodes }; - - if (incomingSeason.episodes && typeof incomingSeason.episodes === "object") { - for (const [epKey, epVal] of Object.entries(incomingSeason.episodes)) { - nextEpisodes[epKey] = epVal; - } - } - - mergedSeasons[seasonKey] = { - ...prevSeason, - ...incomingSeason, - episodes: nextEpisodes - }; - } - - seriesData.seasons = mergedSeasons; - } catch (err) { - console.warn(`⚠️ TV metadata merge başarısız (db - ${targetPaths.key}): ${err.message}`); - } - ensureDirForFile(seriesMetaPath); - try { - await upsertTvSeries(targetPaths.key, normalizedRoot, seriesData); - } catch (err) { - console.warn( - `⚠️ TV metadata kaydedilemedi (db - ${targetPaths.key}): ${err.message}` - ); - } + fs.writeFileSync(seriesMetaPath, JSON.stringify(seriesData, null, 2), "utf-8"); if ( existingPaths && @@ -3319,16 +3202,12 @@ async function ensureSeriesData( } function tvSeriesKey(rootFolder, seriesId = null, fallbackTitle = null) { - const tvdbId = normalizeTvdbId(seriesId); - if (tvdbId !== null) { - return canonicalTvdbKey(tvdbId); - } - const normalizedRoot = rootFolder ? sanitizeRelative(rootFolder) : null; if (!normalizedRoot) return null; - let suffix = null; - if (fallbackTitle) { + if (seriesId) { + suffix = String(seriesId).toLowerCase(); + } else if (fallbackTitle) { const slug = String(fallbackTitle) .trim() .toLowerCase() @@ -3345,14 +3224,11 @@ function tvSeriesKey(rootFolder, seriesId = null, fallbackTitle = null) { function parseTvSeriesKey(key) { const normalized = sanitizeRelative(String(key || "")); - if (normalized.startsWith("tvdb-")) { - return { rootFolder: null, seriesId: normalized.slice(5), key: normalized, canonical: true }; - } if (!normalized.includes("__")) { - return { rootFolder: normalized, seriesId: null, key: normalized, canonical: false }; + return { rootFolder: normalized, seriesId: null, key: normalized }; } const [rootFolder, suffix] = normalized.split("__", 2); - return { rootFolder, seriesId: suffix || null, key: normalized, canonical: false }; + return { rootFolder, seriesId: suffix || null, key: normalized }; } function tvSeriesPathsByKey(key) { @@ -3371,11 +3247,10 @@ function tvSeriesPathsByKey(key) { } function tvSeriesPaths(rootFolderOrKey, seriesId = null, fallbackTitle = null) { - const rawKey = String(rootFolderOrKey || ""); if ( seriesId === null && fallbackTitle === null && - (rawKey.includes("__") || rawKey.startsWith("tvdb-")) + String(rootFolderOrKey || "").includes("__") ) { return tvSeriesPathsByKey(rootFolderOrKey); } @@ -3410,19 +3285,9 @@ function seasonAssetPaths(paths, seasonNumber) { }; } -async function listTvSeriesKeysForRoot(rootFolder) { +function listTvSeriesKeysForRoot(rootFolder) { const normalizedRoot = rootFolder ? sanitizeRelative(rootFolder) : null; if (!normalizedRoot) return []; - - // Önce DB'den oku (rootFolders desteğiyle) - try { - const dbKeys = await listTvSeriesKeysForRootDb(normalizedRoot); - if (dbKeys?.length) return dbKeys; - } catch (err) { - console.warn(`⚠️ TV metadata anahtarları DB'den alınamadı: ${err.message}`); - } - - // Legacy cache dizinlerini tara (geri uyumluluk) if (!fs.existsSync(TV_DATA_ROOT)) return []; const keys = []; try { @@ -3445,34 +3310,13 @@ async function listTvSeriesKeysForRoot(rootFolder) { return keys; } -async function removeSeriesData(rootFolder, seriesId = null) { +function removeSeriesData(rootFolder, seriesId = null) { const keys = seriesId ? [tvSeriesKey(rootFolder, seriesId)].filter(Boolean) - : await listTvSeriesKeysForRoot(rootFolder); - - if (!keys.length) return; - - if (seriesId === null) { - await removeTvSeriesByRoot(rootFolder); - } - + : listTvSeriesKeysForRoot(rootFolder); for (const key of keys) { - if (seriesId !== null) { - await removeTvSeriesByKey(key).catch((err) => - console.warn(`⚠️ TV metadata silinemedi (db - ${key}): ${err.message}`) - ); - } const dir = tvSeriesDir(key); - // 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)) { + if (dir && fs.existsSync(dir)) { try { fs.rmSync(dir, { recursive: true, force: true }); console.log(`🧹 TV metadata silindi: ${dir}`); @@ -3486,115 +3330,77 @@ async function removeSeriesData(rootFolder, seriesId = null) { function removeSeriesEpisode(rootFolder, relativeFilePath) { if (!rootFolder || !relativeFilePath) return; - (async () => { - const keys = await listTvSeriesKeysForRoot(rootFolder); - if (!keys.length) return; + const keys = listTvSeriesKeysForRoot(rootFolder); + if (!keys.length) return; - for (const key of keys) { - const paths = tvSeriesPathsByKey(key); - - 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; - - 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; - } - } - - if ( - seasonChanged && - (!season.episodes || Object.keys(season.episodes).length === 0) - ) { - delete seasons[seasonKey]; - } - } - - if (!removed) continue; - - if (!Object.keys(seasons).length) { - await removeSeriesData(seriesData._dupe?.folder || rootFolder, seriesData.id); - continue; - } - - seriesData.seasons = seasons; - seriesData.updatedAt = Date.now(); - - try { - await upsertTvSeries(key, 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}` - ) - ); -} - -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; + for (const key of keys) { const paths = tvSeriesPathsByKey(key); if (!fs.existsSync(paths.metadata)) continue; + let seriesData; try { - const data = JSON.parse(fs.readFileSync(paths.metadata, "utf-8")) || {}; - const parsed = parseTvSeriesKey(key); - const rootForDoc = data?._dupe?.folder || parsed.rootFolder || null; - await upsertTvSeries(key, rootForDoc, data); - imported += 1; - try { - fs.rmSync(paths.metadata, { force: true }); - } catch { - /* no-op */ + seriesData = JSON.parse(fs.readFileSync(paths.metadata, "utf-8")); + } catch (err) { + console.warn( + `⚠️ series.json okunamadı (${paths.metadata}): ${err.message}` + ); + continue; + } + + const seasons = seriesData?.seasons || {}; + let removed = false; + + 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; + } + } + + 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 { + 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); } } catch (err) { console.warn( - `⚠️ Legacy series.json içeri aktarılamadı (${paths.metadata}): ${err.message}` + `⚠️ TV metadata klasörü temizlenemedi (${paths.dir}): ${err.message}` ); } } - - return imported; } function purgeRootFolder(rootFolder) { @@ -3616,9 +3422,7 @@ function purgeRootFolder(rootFolder) { removeAllThumbnailsForRoot(safe); removeMovieData(safe); - removeSeriesData(safe).catch((err) => - console.warn(`⚠️ TV metadata silinemedi (purge - ${safe}): ${err?.message || err}`) - ); + removeSeriesData(safe); const infoPath = path.join(DOWNLOAD_DIR, safe, INFO_FILENAME); if (fs.existsSync(infoPath)) { @@ -3939,74 +3743,67 @@ function renameSeriesDataPaths(rootFolder, oldRel, newRel) { const newPrefix = normalizeTrashPath(newRel); if (!oldPrefix || oldPrefix === newPrefix) return; - (async () => { - const keys = await listTvSeriesKeysForRoot(rootFolder); - if (!keys.length) return; + const keys = listTvSeriesKeysForRoot(rootFolder); + for (const key of keys) { + const metadataPath = tvSeriesPathsByKey(key).metadata; + if (!fs.existsSync(metadataPath)) continue; - for (const key of keys) { - const paths = tvSeriesPathsByKey(key); - let seriesData; - try { - seriesData = await loadTvSeriesByKey(key); - } catch (err) { - console.warn(`⚠️ TV metadata okunamadı (db - ${key}): ${err.message}`); - continue; - } - if (!seriesData) continue; + 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; - }; + 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; + 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; - } + 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 (changed) { - try { - await upsertTvSeries(key, rootFolder, seriesData); - } catch (err) { - console.warn( - `⚠️ TV metadata güncellenemedi (db - ${key}): ${err.message}` - ); + if (episode.videoPath) { + const nextVideo = transform(episode.videoPath); + if (nextVideo !== episode.videoPath) { + episode.videoPath = nextVideo; + changed = true; + } } } } - })().catch((err) => - console.warn( - `⚠️ TV metadata yeniden adlandırma tamamlanamadı (${rootFolder}): ${err.message}` - ) - ); + + if (changed) { + try { + fs.writeFileSync(metadataPath, JSON.stringify(seriesData, null, 2), "utf-8"); + } catch (err) { + console.warn( + `⚠️ series.json güncellenemedi (${metadataPath}): ${err.message}` + ); + } + } + } } function removeThumbnailsForDirectory(rootFolder, relativeDir) { @@ -4128,37 +3925,14 @@ function broadcastSnapshot() { } let mediaRescanTask = null; -let pendingMediaRescan = { - movies: false, - tv: false, - tvRoots: new Set(), - clearCacheMovies: false, - clearCacheTv: false, - reason: "manual" -}; +let pendingMediaRescan = { movies: false, tv: false }; +let lastMediaRescanReason = "manual"; -function queueMediaRescan({ - movies = false, - tv = false, - reason = "manual", - roots = [], - clearCacheMovies = false, - clearCacheTv = false -} = {}) { +function queueMediaRescan({ movies = false, tv = false, reason = "manual" } = {}) { if (!movies && !tv) return; pendingMediaRescan.movies = pendingMediaRescan.movies || movies; pendingMediaRescan.tv = pendingMediaRescan.tv || tv; - pendingMediaRescan.clearCacheMovies = - pendingMediaRescan.clearCacheMovies || clearCacheMovies; - pendingMediaRescan.clearCacheTv = - pendingMediaRescan.clearCacheTv || clearCacheTv; - pendingMediaRescan.reason = reason; - if (tv && Array.isArray(roots)) { - roots.forEach((r) => { - const safe = sanitizeRelative(r); - if (safe) pendingMediaRescan.tvRoots.add(safe); - }); - } + lastMediaRescanReason = reason; if (!mediaRescanTask) { mediaRescanTask = runQueuedMediaRescan().finally(() => { mediaRescanTask = null; @@ -4168,29 +3942,16 @@ function queueMediaRescan({ async function runQueuedMediaRescan() { while (pendingMediaRescan.movies || pendingMediaRescan.tv) { - const targets = { - movies: pendingMediaRescan.movies, - tv: pendingMediaRescan.tv, - tvRoots: new Set(pendingMediaRescan.tvRoots), - clearCacheMovies: pendingMediaRescan.clearCacheMovies, - clearCacheTv: pendingMediaRescan.clearCacheTv, - reason: pendingMediaRescan.reason - }; - pendingMediaRescan = { - movies: false, - tv: false, - tvRoots: new Set(), - clearCacheMovies: false, - clearCacheTv: false, - reason: "manual" - }; + const targets = { ...pendingMediaRescan }; + pendingMediaRescan = { movies: false, tv: false }; + const reason = lastMediaRescanReason; console.log( - `🔁 Medya taraması tetiklendi (${targets.reason}) -> movies:${targets.movies} tv:${targets.tv} roots:${Array.from(targets.tvRoots).join(",") || "-"}` + `🔁 Medya taraması tetiklendi (${reason}) -> movies:${targets.movies} tv:${targets.tv}` ); try { if (targets.movies) { if (TMDB_API_KEY) { - await rebuildMovieMetadata({ clearCache: targets.clearCacheMovies }); + await rebuildMovieMetadata({ clearCache: true }); } else { console.warn("⚠️ TMDB anahtarı tanımsız olduğu için film taraması atlandı."); } @@ -4198,10 +3959,7 @@ async function runQueuedMediaRescan() { if (targets.tv) { if (TVDB_API_KEY) { - await rebuildTvMetadata({ - clearCache: targets.clearCacheTv, - roots: Array.from(targets.tvRoots) - }); + await rebuildTvMetadata({ clearCache: true }); } else { console.warn("⚠️ TVDB anahtarı tanımsız olduğu için dizi taraması atlandı."); } @@ -5164,10 +4922,7 @@ app.delete("/api/file", requireAuth, (req, res) => { queueMediaRescan({ movies: mediaFlags.movies, tv: mediaFlags.tv, - reason: "trash-add", - roots: [folderId], - clearCacheMovies: false, - clearCacheTv: false + reason: "trash-add" }); } @@ -5640,10 +5395,7 @@ app.post("/api/trash/restore", requireAuth, (req, res) => { queueMediaRescan({ movies: mediaFlags.movies, tv: mediaFlags.tv, - reason: "trash-restore", - roots: [rootFolder], - clearCacheMovies: false, - clearCacheTv: false + reason: "trash-restore" }); } @@ -5856,7 +5608,7 @@ async function rebuildMovieMetadata({ clearCache = false, resetSeriesData = fals if (!videoEntries.length) { removeMovieData(folder); if (resetSeriesData) { - await removeSeriesData(folder); + removeSeriesData(folder); } const update = { primaryVideoPath: null, @@ -5883,7 +5635,7 @@ async function rebuildMovieMetadata({ clearCache = false, resetSeriesData = fals removeMovieData(folder); if (resetSeriesData) { - await removeSeriesData(folder); + removeSeriesData(folder); } const matches = []; @@ -6055,19 +5807,16 @@ app.post("/api/youtube/download", requireAuth, async (req, res) => { }); // --- 📺 TV dizileri listesi --- -app.get("/api/tvshows", requireAuth, async (req, res) => { +app.get("/api/tvshows", requireAuth, (req, res) => { try { - let seriesDocs = await listAllTvSeriesFromDb(); - if (!seriesDocs || !seriesDocs.length) { - const migrated = await importLegacySeriesMetadata(); - if (migrated > 0) { - seriesDocs = await listAllTvSeriesFromDb(); - } - } - if (!seriesDocs || !seriesDocs.length) { + if (!fs.existsSync(TV_DATA_ROOT)) { return res.json([]); } + const dirEntries = fs + .readdirSync(TV_DATA_ROOT, { withFileTypes: true }) + .filter((d) => d.isDirectory()); + const aggregated = new Map(); const mergeEpisode = (existing, incoming) => { @@ -6087,71 +5836,57 @@ app.get("/api/tvshows", requireAuth, async (req, res) => { return merged; }; - for (const doc of seriesDocs) { - const key = sanitizeRelative(doc.key); + for (const dirent of dirEntries) { + const key = sanitizeRelative(dirent.name); if (!key) continue; const paths = tvSeriesPathsByKey(key); - const parsed = parseTvSeriesKey(key); + if (!paths || !fs.existsSync(paths.metadata)) continue; + const { rootFolder } = parseTvSeriesKey(key); + if (!rootFolder) continue; - 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 infoForFolder = readInfoForRoot(rootFolder) || {}; + const infoFiles = infoForFolder.files || {}; + const infoEpisodes = infoForFolder.seriesEpisodes || {}; const infoEpisodeIndex = new Map(); - const infoFilesByFolder = new Map(); - for (const folderName of folderSet) { - const infoForFolder = readInfoForRoot(folderName) || {}; - const infoFiles = infoForFolder.files || {}; - const infoEpisodes = infoForFolder.seriesEpisodes || {}; - infoFilesByFolder.set(folderName, infoFiles); + for (const [relPath, meta] of Object.entries(infoEpisodes)) { + if (!meta) continue; + const seasonNumber = toFiniteNumber( + meta.season ?? meta.seasonNumber ?? meta.seasonNum + ); + const episodeNumber = toFiniteNumber( + meta.episode ?? meta.episodeNumber ?? meta.episodeNum + ); + if (!Number.isFinite(seasonNumber) || !Number.isFinite(episodeNumber)) continue; - for (const [relPath, meta] of Object.entries(infoEpisodes)) { - if (!meta) continue; - const seasonNumber = toFiniteNumber( - meta.season ?? meta.seasonNumber ?? meta.seasonNum - ); - const episodeNumber = toFiniteNumber( - meta.episode ?? meta.episodeNumber ?? meta.episodeNum - ); - if (!Number.isFinite(seasonNumber) || !Number.isFinite(episodeNumber)) - continue; + const normalizedRel = normalizeTrashPath(relPath); + const ext = path.extname(normalizedRel).toLowerCase(); + if (!VIDEO_EXTS.includes(ext)) continue; - const normalizedRel = normalizeTrashPath(relPath); - const ext = path.extname(normalizedRel).toLowerCase(); - if (!VIDEO_EXTS.includes(ext)) continue; + const absVideo = normalizedRel + ? path.join(DOWNLOAD_DIR, rootFolder, normalizedRel) + : null; + if (!absVideo || !fs.existsSync(absVideo)) continue; - const absVideo = normalizedRel - ? path.join(DOWNLOAD_DIR, folderName, normalizedRel) - : null; - if (!absVideo || !fs.existsSync(absVideo)) continue; - - infoEpisodeIndex.set(`${folderName}:${seasonNumber}-${episodeNumber}`, { - relPath: normalizedRel, - meta, - folder: folderName - }); - } + infoEpisodeIndex.set(`${seasonNumber}-${episodeNumber}`, { + relPath: normalizedRel, + meta + }); } - let seriesData = doc.data; - if (!seriesData) { - await removeTvSeriesByKey(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; } const seasonsObj = seriesData?.seasons || {}; if (!Object.keys(seasonsObj).length) { - await removeSeriesData(rootFolder, seriesData.id ?? seriesData.tvdbId ?? null); + removeSeriesData(rootFolder, seriesData.id ?? seriesData.tvdbId ?? null); continue; } @@ -6186,14 +5921,13 @@ app.get("/api/tvshows", requireAuth, async (req, res) => { ), seasons: new Map(), primaryFolder: rootFolder, - folders: new Set([rootFolder, ...docFolders]) + folders: new Set([rootFolder]) }; aggregated.set(showKey, base); return base; })(); record.folders.add(rootFolder); - for (const extra of docFolders) record.folders.add(extra); if ( seriesData.overview && seriesData.overview.length > (record.overview?.length || 0) @@ -6227,17 +5961,16 @@ app.get("/api/tvshows", requireAuth, async (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)) { if (!rawEpisode || typeof rawEpisode !== "object") continue; - const episodeRootForCleanup = rawEpisode.folder || rootFolder; const relativeFile = (rawEpisode.file || "").replace(/\\/g, "/"); if (relativeFile) { const absEpisodePath = path.join( DOWNLOAD_DIR, - episodeRootForCleanup, + rootFolder, relativeFile ); if (!fs.existsSync(absEpisodePath)) { @@ -6306,16 +6039,13 @@ app.get("/api/tvshows", requireAuth, async (req, res) => { "0" )}E${String(episodeNumber).padStart(2, "0")}`; } - const episodeRoot = rawEpisode.folder || rootFolder; - const infoEpisode = infoEpisodeIndex.get(`${episodeRoot}:${seasonNumber}-${episodeNumber}`); + const infoEpisode = infoEpisodeIndex.get(`${seasonNumber}-${episodeNumber}`); if (infoEpisode?.relPath) { const normalizedRel = infoEpisode.relPath.replace(/^\/+/, ""); - const withRoot = `${episodeRoot}/${normalizedRel}`.replace(/^\/+/, ""); + const withRoot = `${rootFolder}/${normalizedRel}`.replace(/^\/+/, ""); normalizedEpisode.file = normalizedRel; normalizedEpisode.videoPath = withRoot; - const fileMeta = (infoFilesByFolder.get(episodeRoot) || {})[ - normalizedRel - ]; + const fileMeta = infoFiles[normalizedRel]; if (fileMeta?.mediaInfo && !normalizedEpisode.mediaInfo) { normalizedEpisode.mediaInfo = fileMeta.mediaInfo; } @@ -6329,20 +6059,20 @@ app.get("/api/tvshows", requireAuth, async (req, res) => { let videoPath = rawVideoPath.replace(/\\/g, "/").replace(/^\.\//, ""); if (videoPath) { const isExternal = /^https?:\/\//i.test(videoPath); - const needsFolderPrefix = - !isExternal && - !videoPath.startsWith(`${episodeRoot}/`) && - !videoPath.startsWith(`/${episodeRoot}/`); - if (needsFolderPrefix) { - videoPath = `${episodeRoot}/${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; } normalizedEpisode.videoPath = finalPath; } else if (relativeFile) { - normalizedEpisode.videoPath = `${episodeRoot}/${relativeFile}` + normalizedEpisode.videoPath = `${rootFolder}/${relativeFile}` .replace(/\\/g, "/") .replace(/^\/+/, ""); if (normalizedEpisode.videoPath !== rawVideoPath) { @@ -6371,7 +6101,7 @@ app.get("/api/tvshows", requireAuth, async (req, res) => { } } } - normalizedEpisode.folder = episodeRoot; + normalizedEpisode.folder = rootFolder; const existingEpisode = seasonRecord.episodes.get(episodeNumber); seasonRecord.episodes.set( @@ -6389,10 +6119,14 @@ app.get("/api/tvshows", requireAuth, async (req, res) => { try { seriesData.seasons = seasonsObj; seriesData.updatedAt = Date.now(); - await upsertTvSeries(key, rootFolder, seriesData); + fs.writeFileSync( + paths.metadata, + JSON.stringify(seriesData, null, 2), + "utf-8" + ); } catch (err) { console.warn( - `⚠️ TV metadata güncellenemedi (db - ${key}): ${err.message}` + `⚠️ series.json güncellenemedi (${paths.metadata}): ${err.message}` ); } } @@ -6532,19 +6266,12 @@ app.get("/api/music", requireAuth, (req, res) => { } }); -async function rebuildTvMetadata({ clearCache = false, roots = null } = {}) { +async function rebuildTvMetadata({ clearCache = false } = {}) { if (!TVDB_API_KEY) { throw new Error("TVDB API erişimi için gerekli anahtar tanımlı değil."); } - const limitedRoots = - Array.isArray(roots) && roots.length - ? roots - .map((r) => sanitizeRelative(r)) - .filter(Boolean) - : null; - - if (clearCache && !limitedRoots && fs.existsSync(TV_DATA_ROOT)) { + if (clearCache && fs.existsSync(TV_DATA_ROOT)) { try { fs.rmSync(TV_DATA_ROOT, { recursive: true, force: true }); console.log("🧹 TV cache temizlendi."); @@ -6563,15 +6290,10 @@ async function rebuildTvMetadata({ clearCache = false, roots = null } = {}) { tvdbEpisodeDetailCache.clear(); } - let dirEntries = fs + const dirEntries = fs .readdirSync(DOWNLOAD_DIR, { withFileTypes: true }) .filter((d) => d.isDirectory()); - if (limitedRoots) { - const allowed = new Set(limitedRoots); - dirEntries = dirEntries.filter((d) => allowed.has(d.name)); - } - const processed = []; for (const dirent of dirEntries) { @@ -6580,14 +6302,6 @@ async function rebuildTvMetadata({ clearCache = false, roots = null } = {}) { 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 || {}; @@ -7186,7 +6900,7 @@ app.post("/api/match/manual", requireAuth, async (req, res) => { // Mevcut movie_data ve TV verilerini temizle removeMovieData(rootFolder, relativeVideoPath); - await removeSeriesData(rootFolder); + removeSeriesData(rootFolder); // TMDB'den detaylı bilgi al const movieDetails = await tmdbFetch(`/movie/${movieId}`, { @@ -7268,7 +6982,7 @@ app.post("/api/match/manual", requireAuth, async (req, res) => { // Mevcut movie_data ve TV verilerini temizle removeMovieData(rootFolder); - await removeSeriesData(rootFolder); + removeSeriesData(rootFolder); // TVDB'den dizi bilgilerini al const extended = await fetchTvdbSeriesExtended(seriesId);