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ū)
+