From 329150f1c4c968ed2e111113aab008f3d534a7b1 Mon Sep 17 00:00:00 2001 From: MasutaYunikon Date: Mon, 23 Mar 2026 04:52:33 +0100 Subject: [PATCH] fix(organize): migration vers paths { infohash, path } pour le matching fichiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: le format paths[] dans series/{id}.json passe de string[] à { infohash: string, path: string }[] — nécessite une mise à jour du scraper. fix(organize): buildFileMap matche par infohash au lieu d'index ou titre L'ancien système utilisait soit un index positionnel fragile, soit le titre du torrent comme clé de matching — les deux cassaient si l'utilisateur renommait le torrent dans qBittorrent ou si un pack intégrale ne couvrait pas tous les épisodes. Désormais chaque path est lié à un infohash précis. fix(organize): source fichier via save_path qBittorrent + fullPath relatif path.join(savePath, fullPath) utilise le save_path retourné par qBittorrent au lieu de deviner le nom du dossier depuis le titre du torrent. Résiste aux renommages de dossier côté client. fix(organize): scanMediaPath adapté au nouveau format paths[] L'index filename → infohash est maintenant construit directement depuis ep.paths[].infohash sans passer par la hiérarchie torrents série/saison/épisode. --- server/organize-worker.ts | 88 ++++++++++++++++++++------------------- server/organize.ts | 35 ++-------------- 2 files changed, 49 insertions(+), 74 deletions(-) diff --git a/server/organize-worker.ts b/server/organize-worker.ts index 75ceaf2..ed0d156 100644 --- a/server/organize-worker.ts +++ b/server/organize-worker.ts @@ -79,19 +79,28 @@ function findTorrentByHash(hash: string, seriesData: any[]): { torrent: any; ser return null } -// Construit la map filename → { season_number, original_filename } -// depuis les paths[] de la série en utilisant l'index du torrent -function buildFileMap(serieData: any, torrentIdx: number, seasonFilter?: number): Map { - const map = new Map() +// Construit la map filename → { season_number, original_filename, fullPath } +// en matchant par infohash dans ep.paths[] +function buildFileMap( + serieData : any, + hash : string, + seasonFilter?: number +): Map { + const map = new Map() + const h = hash.toLowerCase() + for (const season of serieData.seasons ?? []) { if (seasonFilter !== undefined && season.season_number !== seasonFilter) continue for (const ep of season.episodes ?? []) { - const filePath = (ep.paths ?? [])[torrentIdx] - if (!filePath) continue - const filename = filePath.split('/').pop() ?? filePath - // original_filename vient de l'API Fankai (passé dans ep directement si disponible) + const match = (ep.paths ?? []).find((p: any) => p.infohash?.toLowerCase() === h) + if (!match) continue + const filename = match.path.split('/').pop() ?? match.path const original = ep.original_filename ?? filename - map.set(filename, { season_number: season.season_number, original_filename: original }) + map.set(filename, { + season_number : season.season_number, + original_filename: original, + fullPath : match.path, + }) } } return map @@ -202,37 +211,21 @@ async function organizeTorrent(hash: string, name: string, savePath: string, ser } const { torrent, serieData } = found - const serieTitle = serieData.title ?? serieData.show_title ?? name - log(`[organize] ✓ Trouvé : ${torrent.title ?? name} — série: ${serieTitle}`) - - // Déterminer l'index du torrent dans sa hiérarchie - let torrentIdx = 0 - let seasonFilter: number | undefined - if (torrent._episode) { - // Torrent niveau épisode : paths[0] est le seul fichier - torrentIdx = (torrent._episode.torrents ?? []).findIndex((t: any) => t.infohash?.toLowerCase() === hash.toLowerCase()) - seasonFilter = torrent._season?.season_number - } else if (torrent._season) { - torrentIdx = (torrent._season.torrents ?? []).findIndex((t: any) => t.infohash?.toLowerCase() === hash.toLowerCase()) - seasonFilter = torrent._season.season_number - } else { - torrentIdx = (serieData.torrents ?? []).findIndex((t: any) => t.infohash?.toLowerCase() === hash.toLowerCase()) - } - - // Construire la map filename → { season_number, original_filename } - const fileMap = buildFileMap(serieData, torrentIdx, seasonFilter) + const serieTitle = serieData.title ?? serieData.show_title ?? name + const torrentTitle = torrent.title ?? name + log(`[organize] ✓ Trouvé : ${torrentTitle} — série: ${serieTitle}`) // ── NFO/images depuis GitLab ────────────────────────────── if (nfoSupport) { - const serieDestRoot = path.join(mediaPath, serieTitle) - await downloadGitlabFolder(serieTitle, serieDestRoot) + await downloadGitlabFolder(serieTitle, path.join(mediaPath, serieTitle)) } // ── Cas torrent épisode unique ──────────────────────────── if (torrent._episode) { - const ep = torrent._episode - const season = torrent._season - const filePath = (ep.paths ?? [])[torrentIdx] + const ep = torrent._episode + const season = torrent._season + const match = (ep.paths ?? []).find((p: any) => p.infohash?.toLowerCase() === hash.toLowerCase()) + const filePath = match?.path ?? null const filename = filePath?.split('/').pop() ?? name const original = ep.original_filename ?? filename const destDir = path.join(mediaPath, serieTitle, seasonFolder(season.season_number ?? 1)) @@ -240,12 +233,16 @@ async function organizeTorrent(hash: string, name: string, savePath: string, ser if (isOrganized(hash, filename)) return { ...result, total: 1, skipped: 1 } - const candidates = [ - path.join(savePath, name, filename), + const candidates = filePath ? [ + path.join(savePath, filePath), + path.join(completePath, filePath), + path.join(savePath, filename), + path.join(completePath, filename), + ] : [ path.join(savePath, filename), - path.join(completePath, name, filename), path.join(completePath, filename), ] + let src: string | null = null for (const c of candidates) { if (fs.existsSync(c)) { src = c; break } } @@ -268,7 +265,10 @@ async function organizeTorrent(hash: string, name: string, savePath: string, ser return { ...result, total: 1, done: 1 } } - // ── Pack saison / intégrale : itérer sur les fichiers ───── + // ── Pack saison / intégrale ─────────────────────────────── + const seasonFilter = torrent._season?.season_number + const fileMap = buildFileMap(serieData, hash, seasonFilter) + if (fileMap.size === 0) { warn(`[organize] Aucun fichier mappé pour ${name}`) return result @@ -276,13 +276,15 @@ async function organizeTorrent(hash: string, name: string, savePath: string, ser result.total = fileMap.size - for (const [filename, { season_number, original_filename }] of fileMap) { + for (const [filename, { season_number, original_filename, fullPath }] of fileMap) { if (isOrganized(hash, filename)) { result.skipped++; continue } + // Source : on utilise save_path + fullPath (chemin relatif exact dans le torrent) + // save_path vient de qBittorrent → résiste aux renommages de dossier const candidates = [ - path.join(savePath, name, filename), + path.join(savePath, fullPath), + path.join(completePath, fullPath), path.join(savePath, filename), - path.join(completePath, name, filename), path.join(completePath, filename), ] let src: string | null = null @@ -339,9 +341,9 @@ parentPort?.on('message', async (msg: any) => { } // Vérifier si déjà tout organisé - const { serieData } = found - const torrentIdx = (serieData.torrents ?? []).findIndex((st: any) => st.infohash?.toLowerCase() === t.hash.toLowerCase()) - const fileMap = buildFileMap(serieData, torrentIdx) + const { torrent, serieData } = found + const seasonFilter = torrent._season?.season_number + const fileMap = buildFileMap(serieData, t.hash, seasonFilter) const allDone = fileMap.size > 0 ? [...fileMap.keys()].every(f => organized[t.hash]?.[f]) : !!organized[t.hash]?.[t.name] diff --git a/server/organize.ts b/server/organize.ts index ebab360..b86bb9e 100644 --- a/server/organize.ts +++ b/server/organize.ts @@ -51,39 +51,12 @@ export async function scanMediaPath( // Construire index filename → infohash depuis les seriesData const filenameIndex = new Map() // filename → infohash for (const sd of seriesData) { - // Torrents niveau série - for (const [i, t] of (sd.torrents ?? []).entries()) { - const hash = t.infohash?.toLowerCase() - if (!hash) continue - for (const season of sd.seasons ?? []) { - for (const ep of season.episodes ?? []) { - const filePath = (ep.paths ?? [])[i] - if (!filePath) continue - const filename = filePath.split('/').pop() - if (filename) filenameIndex.set(filename, hash) - } - } - } - // Torrents niveau saison for (const season of sd.seasons ?? []) { - for (const [i, t] of (season.torrents ?? []).entries()) { - const hash = t.infohash?.toLowerCase() - if (!hash) continue - for (const ep of season.episodes ?? []) { - const filePath = (ep.paths ?? [])[i] - if (!filePath) continue - const filename = filePath.split('/').pop() - if (filename) filenameIndex.set(filename, hash) - } - } - // Torrents niveau épisode for (const ep of season.episodes ?? []) { - for (const [i, t] of (ep.torrents ?? []).entries()) { - const hash = t.infohash?.toLowerCase() - if (!hash) continue - const filePath = (ep.paths ?? [])[i] - if (!filePath) continue - const filename = filePath.split('/').pop() + for (const p of ep.paths ?? []) { + const hash = p.infohash?.toLowerCase() + if (!hash || !p.path) continue + const filename = p.path.split('/').pop() if (filename) filenameIndex.set(filename, hash) } }