From c374ceb0a700d9d8da0c6c2e6c2ca7fcdba92cf9 Mon Sep 17 00:00:00 2001 From: MasutaYunikon Date: Tue, 24 Mar 2026 19:18:51 +0100 Subject: [PATCH] =?UTF-8?q?fix(organize):=20sanitisation=20des=20caract?= =?UTF-8?q?=C3=A8res=20sp=C3=A9ciaux=20dans=20les=20noms=20de=20dossiers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le titre "Kaguya-sama : Love is War Henshū" contenait un ":" qui causait une troncation du nom de dossier sur certains systèmes de fichiers. sanitizeDirName() remplace ":" par " -" et supprime les autres caractères interdits (<>"/\|?*). feat(settings): option titres optimisés Plex (title_for_plex) Nouveau toggle "Noms de dossiers" dans les paramètres. Quand activé, utilise le champ title_for_plex de l'API Fankai pour les noms de dossiers au lieu du titre brut — évite les caractères spéciaux incompatibles avec Plex et les systèmes de fichiers Windows/NTFS. enrichSeriesDataWithOriginalFilenames fetche maintenant /series/{id} pour injecter title_for_plex dans chaque serieData avant de passer au worker. fix(organize): retry sur les erreurs réseau dans enrichSeriesDataWithOriginalFilenames 2 tentatives avec 500ms d'attente entre les deux pour les erreurs réseau ponctuelles sur /seasons/{id}/episodes — évite que formatted_name soit null à cause d'un timeout isolé au démarrage. fix(catalogue): activeTorrents filtre désormais uniquement state=downloading Les torrents en seeding étaient inclus dans activeTorrents, forçant computeSerieDownloadState à retourner "downloading" même pour les séries entièrement importées. Les badges affichaient "EN COURS" au lieu de "IMPORTÉ". --- server/index.ts | 13 ++++++++++--- server/organize-worker.ts | 39 ++++++++++++++++++++++++++++++-------- server/settings.ts | 4 +++- src/views/SettingsView.vue | 20 +++++++++++++++++++ 4 files changed, 64 insertions(+), 12 deletions(-) diff --git a/server/index.ts b/server/index.ts index 8f0e3e3..7a0c3c9 100644 --- a/server/index.ts +++ b/server/index.ts @@ -42,8 +42,8 @@ app.get('/api/settings', requireAuth, (_req, res) => { res.json(readSettings()) }) app.post('/api/settings', requireAuth, (req, res) => { - const { mediaPath, completePath, organizeMode, category, nfoSupport, autoImport } = req.body - res.json(writeSettings({ mediaPath, completePath, organizeMode, category, nfoSupport, autoImport })) + const { mediaPath, completePath, organizeMode, category, nfoSupport, autoImport, usePlexTitles } = req.body + res.json(writeSettings({ mediaPath, completePath, organizeMode, category, nfoSupport, autoImport, usePlexTitles })) }) // ── Torrent clients ──────────────────────────────────────────── @@ -284,6 +284,13 @@ async function enrichSeriesDataWithOriginalFilenames(seriesData: any[]): Promise for (const sd of seriesData) { if (!sd) { results.push(sd); continue } + // Fetcher title_for_plex depuis l'API série + let title_for_plex: string | null = null + try { + const serieApi = await fankaiGet(`/series/${sd.id}`) + title_for_plex = serieApi.title_for_plex ?? null + } catch {} + const enrichedSeasons: any[] = [] for (const season of sd.seasons ?? []) { let epsData: any[] = [] @@ -326,7 +333,7 @@ async function enrichSeriesDataWithOriginalFilenames(seriesData: any[]): Promise enrichedSeasons.push({ ...season, episodes: enrichedEpisodes }) } - results.push({ ...sd, seasons: enrichedSeasons }) + results.push({ ...sd, title_for_plex, seasons: enrichedSeasons }) } return results diff --git a/server/organize-worker.ts b/server/organize-worker.ts index eb756d3..119cbfa 100644 --- a/server/organize-worker.ts +++ b/server/organize-worker.ts @@ -118,12 +118,18 @@ function buildFileMap( const filename = matchedPath.split('/').pop() ?? matchedPath // nfoSupport → original_filename (correspond aux .nfo GitLab) - // sinon → formatted_name de l'API (nommage Jellyfin/Plex standardisé) + // sinon → formatted_name de l'API, ou fallback original_filename + const fmtName = ep.formatted_name?.trim() + ? ep.formatted_name.replace(/[<>:"/\\|?*]/g, '') + '.mkv' + : null + + if (!nfoSupport && !fmtName) { + warn(`[organize] formatted_name manquant pour ep ${ep.id} (ep ${ep.episode_number}) — fallback original_filename`) + } + const destName = nfoSupport ? (ep.original_filename ?? filename) - : (ep.formatted_name - ? ep.formatted_name.replace(/[<>:"/\\|?*]/g, '') + '.mkv' - : (ep.original_filename ?? filename)) + : (fmtName ?? ep.original_filename ?? filename) map.set(filename, { season_number : season.season_number, @@ -243,6 +249,16 @@ function tryHardlink(src: string, dest: string): boolean { } } +// ─── Sanitise un nom de dossier ────────────────────────────── +// Remplace les caractères interdits sur les systèmes de fichiers communs +function sanitizeDirName(name: string): string { + return name + .replace(/:/g, ' -') // "Kaguya-sama : Love" → "Kaguya-sama - Love" + .replace(/[<>"/\\|?*]/g, '') // autres caractères interdits + .replace(/\s+/g, ' ') // espaces multiples + .trim() +} + // ─── Organise un torrent ────────────────────────────────────── async function organizeTorrent(hash: string, name: string, savePath: string, seriesData: any[]) { const { mediaPath, completePath, organizeMode, nfoSupport } = readSettings() @@ -258,7 +274,13 @@ async function organizeTorrent(hash: string, name: string, savePath: string, ser } const { torrent, serieData } = found - const serieTitle = serieData.title ?? serieData.show_title ?? name + const { usePlexTitles } = readSettings() + const rawTitle = serieData.title ?? serieData.show_title ?? name + const serieTitle = sanitizeDirName( + usePlexTitles && serieData.title_for_plex + ? serieData.title_for_plex + : rawTitle + ) const torrentTitle = torrent.title ?? name log(`[organize] ✓ Trouvé : ${torrentTitle} — série: ${serieTitle}`) @@ -278,11 +300,12 @@ async function organizeTorrent(hash: string, name: string, savePath: string, ser else if (p?.infohash?.toLowerCase() === hash.toLowerCase()) { filePath = p.path.replace(/\\/g, '/'); break } } const filename = filePath?.split('/').pop() ?? name + const fmtName = ep.formatted_name?.trim() + ? ep.formatted_name.replace(/[<>:"/\\|?*]/g, '') + '.mkv' + : null const destName = nfoSupport ? (ep.original_filename ?? filename) - : (ep.formatted_name - ? ep.formatted_name.replace(/[<>:"/\\|?*]/g, '') + '.mkv' - : (ep.original_filename ?? filename)) + : (fmtName ?? ep.original_filename ?? filename) const destDir = path.join(mediaPath, serieTitle, seasonFolder(season.season_number ?? 1)) const dest = path.join(destDir, destName) diff --git a/server/settings.ts b/server/settings.ts index 0a60edd..8e80fe9 100644 --- a/server/settings.ts +++ b/server/settings.ts @@ -10,7 +10,8 @@ export interface Settings { organizeMode : 'hardlink' | 'move' category : string nfoSupport : boolean - autoImport : boolean // import automatique quand un torrent passe en seeding + autoImport : boolean + usePlexTitles : boolean // utilise title_for_plex pour les noms de dossiers } const defaults: Settings = { @@ -20,6 +21,7 @@ const defaults: Settings = { category : 'fankai', nfoSupport : false, autoImport : true, + usePlexTitles: false, } export function readSettings(): Settings { diff --git a/src/views/SettingsView.vue b/src/views/SettingsView.vue index 6a7b725..6c82e39 100644 --- a/src/views/SettingsView.vue +++ b/src/views/SettingsView.vue @@ -154,6 +154,25 @@

+ +
+ +
+ + Utiliser les titres optimisés Plex +
+

+ Utilise title_for_plex pour les noms de dossiers — + évite les caractères spéciaux (ex: Kaguya-sama - Love is War Henshū + au lieu de Kaguya-sama : Love is War Henshū) +

+
+
@@ -277,6 +296,7 @@ const form = ref({ organizeMode: 'hardlink' as 'hardlink' | 'move', nfoSupport : false, autoImport : true, + usePlexTitles: false, }) const saving = ref(false)