feat(settings): ajout du toggle import automatique (activé par défaut)

Nouveau champ autoImport dans Settings — quand désactivé, le worker ne se
lance jamais automatiquement. Activé par défaut pour conserver le comportement
existant. Le toggle est exposé dans SettingsView avec une description claire.

feat(organize): nommage des fichiers selon original_filename ou formatted_name

Selon l'option NFO :
- nfoSupport activé → original_filename (correspond aux .nfo GitLab)
- nfoSupport désactivé → formatted_name de l'API Fankai (format Jellyfin/Plex
  standardisé, ex: Serie.S01E01.MULTI.1080p.x264-FANKAI.mkv)

formatted_name est maintenant injecté dans enrichSeriesDataWithOriginalFilenames
en même temps que original_filename.

feat(catalogue): tri du catalogue avec 4 options

- A→Z (défaut, localeCompare français)
- Z→A
- Récents (par année décroissante)
- Importés (fichiers importés en premier)

feat(catalogue): badge IMPORTÉ et refresh automatique post-import

- Renommage ORGANISÉ → IMPORTÉ sur les affiches et dans les toasts
- Le catalogue se rafraîchit automatiquement quand fetchOrganizeNotifs
  détecte de nouvelles notifications d'import

feat(settings): textes génériques sans mention de logiciels spécifiques

- "Dossier Complétion" → "Dossier Téléchargements"
- "Dossier Jellyfin" → "Médiathèque Fankai"
- "Mode Organisation" → "Mode Import"
- "Organisation" → "Import" partout dans l'interface

feat(downloads): bouton Organiser tout renommé Importer tout

Itère sur tous les torrents seeding non importés et les envoie
séquentiellement à /api/organize avec feedback toast de progression.
This commit is contained in:
MasutaYunikon committed 2026-03-24 02:32:47 +01:00
1 parent 74662de2b6
commit e1e430a968
7 files changed
+241 -246

No files matched your search

+10 -6
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 } = req.body
res.json(writeSettings({ mediaPath, completePath, organizeMode, category, nfoSupport }))
const { mediaPath, completePath, organizeMode, category, nfoSupport, autoImport } = req.body
res.json(writeSettings({ mediaPath, completePath, organizeMode, category, nfoSupport, autoImport }))
})
// ── Torrent clients ────────────────────────────────────────────
@@ -288,16 +288,20 @@ async function enrichSeriesDataWithOriginalFilenames(seriesData: any[]): Promise
epsData = Array.isArray(res) ? res : (res.episodes ?? [])
} catch { return season }
// Index episode_id → original_filename
const origMap = new Map<number, string>()
// 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 && ep.original_filename) origMap.set(ep.id, ep.original_filename)
if (ep.id) origMap.set(ep.id, {
original_filename: ep.original_filename ?? '',
formatted_name : ep.formatted_name ?? '',
})
}
// Injecter dans les épisodes du serieData
const enrichedEpisodes = (season.episodes ?? []).map((ep: any) => ({
...ep,
original_filename: origMap.get(ep.id) ?? null,
original_filename: origMap.get(ep.id)?.original_filename ?? null,
formatted_name : origMap.get(ep.id)?.formatted_name ?? null,
}))
return { ...season, episodes: enrichedEpisodes }
+22 -7
View File
@@ -89,9 +89,11 @@ function findTorrentByHash(hash: string, seriesData: any[]): { torrent: any; ser
// Construit la map filename → { season_number, original_filename, fullPath }
// Supporte les deux formats : { infohash, path } et string (legacy)
// destFilename : original_filename si nfoSupport, formatted_name sinon
function buildFileMap(
serieData : any,
hash : string,
nfoSupport : boolean,
seasonFilter?: number
): Map<string, { season_number: number; original_filename: string; fullPath: string }> {
const map = new Map<string, { season_number: number; original_filename: string; fullPath: string }>()
@@ -114,10 +116,18 @@ function buildFileMap(
if (!matchedPath) continue
const filename = matchedPath.split('/').pop() ?? matchedPath
const original = ep.original_filename ?? filename
// nfoSupport → original_filename (correspond aux .nfo GitLab)
// sinon → formatted_name de l'API (nommage Jellyfin/Plex standardisé)
const destName = nfoSupport
? (ep.original_filename ?? filename)
: (ep.formatted_name
? ep.formatted_name.replace(/[<>:"/\\|?*]/g, '') + '.mkv'
: (ep.original_filename ?? filename))
map.set(filename, {
season_number : season.season_number,
original_filename: original,
original_filename: destName,
fullPath : matchedPath,
})
}
@@ -268,9 +278,13 @@ 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 original = ep.original_filename ?? filename
const destName = nfoSupport
? (ep.original_filename ?? filename)
: (ep.formatted_name
? ep.formatted_name.replace(/[<>:"/\\|?*]/g, '') + '.mkv'
: (ep.original_filename ?? filename))
const destDir = path.join(mediaPath, serieTitle, seasonFolder(season.season_number ?? 1))
const dest = path.join(destDir, original)
const dest = path.join(destDir, destName)
if (isOrganized(hash, filename)) return { ...result, total: 1, skipped: 1 }
@@ -302,13 +316,13 @@ async function organizeTorrent(hash: string, name: string, savePath: string, ser
}
}
markOrganized(hash, filename)
log(`[organize] ✓ ${original} → Saison ${season.season_number}`)
log(`[organize] ✓ ${destName} → Saison ${season.season_number}`)
return { ...result, total: 1, done: 1 }
}
// ── Pack saison / intégrale ───────────────────────────────
const seasonFilter = torrent._season?.season_number
const fileMap = buildFileMap(serieData, hash, seasonFilter)
const fileMap = buildFileMap(serieData, hash, nfoSupport, seasonFilter)
if (fileMap.size === 0) {
warn(`[organize] Aucun fichier mappé pour ${name}`)
@@ -371,6 +385,7 @@ parentPort?.on('message', async (msg: any) => {
const torrents: any[] = msg.torrents
const seriesData: any[] = msg.seriesData ?? []
const organized = loadOrganized()
const { nfoSupport } = readSettings()
for (const t of torrents) {
if (t.state !== 'seeding') continue
@@ -384,7 +399,7 @@ parentPort?.on('message', async (msg: any) => {
// Vérifier si déjà tout organisé
const { torrent, serieData } = found
const seasonFilter = torrent._season?.season_number
const fileMap = buildFileMap(serieData, t.hash, seasonFilter)
const fileMap = buildFileMap(serieData, t.hash, nfoSupport, seasonFilter)
const allDone = fileMap.size > 0
? [...fileMap.keys()].every(f => organized[t.hash]?.[f])
: !!organized[t.hash]?.[t.name]
+5 -1
View File
@@ -122,13 +122,17 @@ export async function scanMediaPath(
}
/**
* Auto-organisation — appelée depuis index.ts avec les seriesData du cache GitHub.
* Auto-import — appelée depuis index.ts avec les seriesData du cache GitHub.
*/
export async function autoOrganizeAll(
listFn : () => Promise<any[]>,
seriesData : any[],
onResult ?: (r: { hash: string; name: string; done: number; skipped: number; errors: number; errorFiles: { file: string; error: string }[] }) => void
): Promise<void> {
// Vérifier si l'import auto est activé
const { autoImport } = readSettings()
if (!autoImport) return
if (workerRunning) {
console.log('[organize] Worker déjà en cours, skip')
return
+3 -1
View File
@@ -9,7 +9,8 @@ export interface Settings {
completePath : string
organizeMode : 'hardlink' | 'move'
category : string
nfoSupport : boolean // téléchargement NFO/images depuis GitLab Fankai Pack
nfoSupport : boolean
autoImport : boolean // import automatique quand un torrent passe en seeding
}
const defaults: Settings = {
@@ -18,6 +19,7 @@ const defaults: Settings = {
organizeMode : 'hardlink',
category : 'fankai',
nfoSupport : false,
autoImport : true,
}
export function readSettings(): Settings {