Compare commits

...
8 Commits
Author SHA1 Message Date
MasutaYunikon d8e284b0de Merge pull request #59 from Masutayunikon/release-please--branches--main
chore(main): release 2.4.0
2026-03-24 21:55:31 +01:00
MasutaYunikon 0b98269695 chore(main): release 2.4.0 2026-03-24 19:19:44 +01:00
MasutaYunikon 402946b462 Merge remote-tracking branch 'origin/main' 2026-03-24 19:19:03 +01:00
MasutaYunikon c374ceb0a7 fix(organize): sanitisation des caractères spéciaux dans les noms de dossiers
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É".
2026-03-24 19:18:51 +01:00
MasutaYunikon 6f21b0a5cd Merge pull request #58 from Masutayunikon/release-please--branches--main
chore(main): release 2.3.3
2026-03-24 03:41:25 +01:00
MasutaYunikon 1c36bb7688 chore(main): release 2.3.3 2026-03-24 03:39:35 +01:00
MasutaYunikon 82a9d51731 Merge remote-tracking branch 'origin/main' 2026-03-24 03:39:03 +01:00
MasutaYunikon ac03aa3c11 fix: recuperation des episodes avec un delai 2026-03-24 03:38:54 +01:00
5 changed files with 118 additions and 23 deletions

No files matched your search

+21
View File
@@ -1,5 +1,26 @@
# Changelog
## [2.4.0](https://github.com/Masutayunikon/FanKarr/compare/v2.3.3...v2.4.0) (2026-03-24)
### Features
* **settings:** option titres optimisés Plex (title_for_plex) ([54dfe6b](https://github.com/Masutayunikon/FanKarr/commit/54dfe6b595c65a77b50dc3b4853182b594df0d9b))
### Bug Fixes
* **catalogue:** activeTorrents filtre désormais uniquement state=downloading ([54dfe6b](https://github.com/Masutayunikon/FanKarr/commit/54dfe6b595c65a77b50dc3b4853182b594df0d9b))
* **organize:** retry sur les erreurs réseau dans enrichSeriesDataWithOriginalFilenames ([54dfe6b](https://github.com/Masutayunikon/FanKarr/commit/54dfe6b595c65a77b50dc3b4853182b594df0d9b))
* **organize:** sanitisation des caractères spéciaux dans les noms de dossiers ([54dfe6b](https://github.com/Masutayunikon/FanKarr/commit/54dfe6b595c65a77b50dc3b4853182b594df0d9b))
## [2.3.3](https://github.com/Masutayunikon/FanKarr/compare/v2.3.2...v2.3.3) (2026-03-24)
### Bug Fixes
* recuperation des episodes avec un delai ([dfc6c13](https://github.com/Masutayunikon/FanKarr/commit/dfc6c1300e3dd8323e422ce3e71465bac77f94cf))
## [2.3.2](https://github.com/Masutayunikon/FanKarr/compare/v2.3.1...v2.3.2) (2026-03-24)
+43 -14
View File
@@ -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 ────────────────────────────────────────────
@@ -279,16 +279,43 @@ function normalizeEpisode(ep: any): any {
// Fetche /seasons/{id}/episodes pour chaque saison et injecte original_filename
// dans chaque épisode du serieData, indexé par episode_id
async function enrichSeriesDataWithOriginalFilenames(seriesData: any[]): Promise<any[]> {
return Promise.all(seriesData.map(async (sd) => {
if (!sd) return sd
const enrichedSeasons = await Promise.all((sd.seasons ?? []).map(async (season: any) => {
const results: any[] = []
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[] = []
try {
const res = await fankaiGet(`/seasons/${season.id}/episodes`)
epsData = Array.isArray(res) ? res : (res.episodes ?? [])
} catch { return season }
// 2 tentatives en cas d'erreur réseau ponctuelle
let lastErr: any
for (let attempt = 0; attempt < 2; attempt++) {
try {
const res = await fankaiGet(`/seasons/${season.id}/episodes`)
epsData = Array.isArray(res) ? res : (res.episodes ?? [])
break
} catch (e) {
lastErr = e
if (attempt === 0) await new Promise(r => setTimeout(r, 500))
}
}
if (epsData.length === 0 && lastErr) throw lastErr
} catch {
enrichedSeasons.push(season)
continue
}
// Petit délai pour ne pas saturer l'API
await new Promise(r => setTimeout(r, 50))
// Index episode_id → original_filename + formatted_name
const origMap = new Map<number, { original_filename: string; formatted_name: string }>()
for (const ep of epsData) {
if (ep.id) origMap.set(ep.id, {
@@ -297,17 +324,19 @@ async function enrichSeriesDataWithOriginalFilenames(seriesData: any[]): Promise
})
}
// Injecter dans les épisodes du serieData
const enrichedEpisodes = (season.episodes ?? []).map((ep: any) => ({
...ep,
original_filename: origMap.get(ep.id)?.original_filename ?? null,
formatted_name : origMap.get(ep.id)?.formatted_name ?? null,
}))
return { ...season, episodes: enrichedEpisodes }
}))
return { ...sd, seasons: enrichedSeasons }
}))
enrichedSeasons.push({ ...season, episodes: enrichedEpisodes })
}
results.push({ ...sd, title_for_plex, seasons: enrichedSeasons })
}
return results
}
// Helper réutilisable : charge + enrichit toutes les seriesData du cache
+31 -8
View File
@@ -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)
+3 -1
View File
@@ -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 {
+20
View File
@@ -154,6 +154,25 @@
</p>
</div>
<!-- Titres Plex -->
<div>
<label class="text-[10px] text-[#5a7a94] tracking-widest block mb-2">NOMS DE DOSSIERS</label>
<div class="flex items-center gap-3">
<button @click="form.usePlexTitles = !form.usePlexTitles"
class="relative shrink-0 w-11 h-6 rounded-full transition-colors cursor-pointer"
:class="form.usePlexTitles ? 'bg-[#e8513a]' : 'bg-[#1e2d3d]'">
<span class="absolute top-1 left-1 w-4 h-4 rounded-full bg-white transition-transform duration-200"
:class="form.usePlexTitles ? 'translate-x-5' : 'translate-x-0'" />
</button>
<span class="text-xs text-[#c8d8e8]">Utiliser les titres optimisés Plex</span>
</div>
<p class="text-[10px] text-[#5a7a94] mt-1.5">
Utilise <span class="text-white">title_for_plex</span> pour les noms de dossiers —
évite les caractères spéciaux (ex: <span class="text-white">Kaguya-sama - Love is War Henshū</span>
au lieu de <span class="text-white">Kaguya-sama : Love is War Henshū</span>)
</p>
</div>
<!-- Support NFO -->
<div>
<label class="text-[10px] text-[#5a7a94] tracking-widest block mb-2">NFO / MÉTADONNÉES</label>
@@ -277,6 +296,7 @@ const form = ref({
organizeMode: 'hardlink' as 'hardlink' | 'move',
nfoSupport : false,
autoImport : true,
usePlexTitles: false,
})
const saving = ref(false)