diff --git a/server/index.ts b/server/index.ts index 7e31a6a..08e049f 100644 --- a/server/index.ts +++ b/server/index.ts @@ -54,16 +54,17 @@ app.get('/api/settings', requireAuth, (_req, res) => { res.json(readSettings()) }) app.post('/api/settings', requireAuth, (req, res) => { - const { mediaPath, completePath, organizeMode, category, nfoSupport, autoImport, devMode, deleteTorrentOnMove } = req.body + const { mediaPath, completePath, organizeMode, category, nfoSupport, autoImport, devMode, deleteTorrentOnMove, autoUnimportMissing } = req.body const patch: Record = {} - if (mediaPath !== undefined) patch.mediaPath = mediaPath - if (completePath !== undefined) patch.completePath = completePath - if (organizeMode !== undefined) patch.organizeMode = organizeMode - if (category !== undefined) patch.category = category - if (nfoSupport !== undefined) patch.nfoSupport = nfoSupport - if (autoImport !== undefined) patch.autoImport = autoImport - if (devMode !== undefined) patch.devMode = devMode - if (deleteTorrentOnMove !== undefined) patch.deleteTorrentOnMove = deleteTorrentOnMove + if (mediaPath !== undefined) patch.mediaPath = mediaPath + if (completePath !== undefined) patch.completePath = completePath + if (organizeMode !== undefined) patch.organizeMode = organizeMode + if (category !== undefined) patch.category = category + if (nfoSupport !== undefined) patch.nfoSupport = nfoSupport + if (autoImport !== undefined) patch.autoImport = autoImport + if (devMode !== undefined) patch.devMode = devMode + if (deleteTorrentOnMove !== undefined) patch.deleteTorrentOnMove = deleteTorrentOnMove + if (autoUnimportMissing !== undefined) patch.autoUnimportMissing = autoUnimportMissing const updated = writeSettings(patch) logger.info('api', 'Paramètres mis à jour') res.json(updated) @@ -840,6 +841,43 @@ app.post('/api/rename-episode', requireAuth, async (req, res) => { } }) +// ── Désimport série complète ─────────────────────────────────── +app.delete('/api/organized/:serieId', requireAuth, async (req, res) => { + const serieId = String(req.params.serieId) + const deleteFile = req.query.deleteFile === 'true' + const ORGANIZED_PATH = path.join(DATA_DIR, 'organized.json') + let organized: Record> = {} + try { if (fs.existsSync(ORGANIZED_PATH)) organized = JSON.parse(fs.readFileSync(ORGANIZED_PATH, 'utf-8')) } catch {} + + const sd = await readSerieData(Number(serieId)) + if (!sd) { res.status(404).json({ error: 'Série introuvable' }); return } + + const episodeIds = new Set() + for (const season of sd.seasons ?? []) { + for (const ep of season.episodes ?? []) episodeIds.add(String(ep.id)) + } + + let removed = 0 + const errors: string[] = [] + + for (const [hash, episodes] of Object.entries(organized)) { + for (const epId of Object.keys(episodes)) { + if (!episodeIds.has(epId)) continue + const entry = episodes[epId] + if (deleteFile && entry?.dest_path && fs.existsSync(entry.dest_path)) { + try { fs.unlinkSync(entry.dest_path) } catch (err) { errors.push(entry.dest_path) } + } + delete organized[hash][epId] + removed++ + } + if (Object.keys(organized[hash]).length === 0) delete organized[hash] + } + + fs.writeFileSync(ORGANIZED_PATH, JSON.stringify(organized, null, 2), 'utf-8') + logger.info('api', `Désimport série ${serieId} — ${removed} épisode(s) retirés`) + res.json({ ok: true, removed, errors }) +}) + // ── Désimport épisode ────────────────────────────────────────── app.delete('/api/organized/:serieId/:episodeId', requireAuth, async (req, res) => { const serieId = String(req.params.serieId) @@ -921,6 +959,7 @@ app.get('/api/organized-summary', requireAuth, async (_req, res) => { } const needsRename = expectedName !== currentName + const fileExists = orgEntry.dest_path ? fs.existsSync(orgEntry.dest_path) : false episodes.push({ episode_id : ep.id, episode_number: ep.episode_number, @@ -931,6 +970,7 @@ app.get('/api/organized-summary', requireAuth, async (_req, res) => { dest_path : orgEntry.dest_path, torrent_hash : orgHash, needs_rename : needsRename, + file_exists : fileExists, }) } } diff --git a/server/organize.ts b/server/organize.ts index 4e4e862..648edd1 100644 --- a/server/organize.ts +++ b/server/organize.ts @@ -196,11 +196,28 @@ export async function scanMediaPath( if (Object.keys(organized[hash]).length === 0) delete organized[hash] } - if (result.added > 0 || removed > 0) { + // Désimport automatique des fichiers manquants sur le disque + const { autoUnimportMissing } = readSettings() as any + let autoRemoved = 0 + if (autoUnimportMissing) { + for (const [hash, episodes] of Object.entries(organized)) { + for (const [episodeId, entry] of Object.entries(episodes as Record)) { + const destPath = entry?.dest_path + if (destPath && !fs.existsSync(destPath)) { + delete (organized[hash] as any)[episodeId] + autoRemoved++ + logger.info('organize', `Désimport auto : fichier introuvable "${destPath}"`) + } + } + if (Object.keys(organized[hash]).length === 0) delete organized[hash] + } + } + + if (result.added > 0 || removed > 0 || autoRemoved > 0) { fs.writeFileSync(organizedPath, JSON.stringify(organized, null, 2), 'utf-8') } - logger.info('organize', `Scan terminé — ${result.found} fichiers, ${result.added} ajoutés, ${removed} orphelins supprimés${noMatch > 0 ? `, ${noMatch} non matchés` : ''}`) + logger.info('organize', `Scan terminé — ${result.found} fichiers, ${result.added} ajoutés, ${removed} orphelins supprimés${autoRemoved > 0 ? `, ${autoRemoved} désimportés auto (fichier manquant)` : ''}${noMatch > 0 ? `, ${noMatch} non matchés` : ''}`) return result } diff --git a/src/views/DownloadsView.vue b/src/views/DownloadsView.vue index ff0288b..d0f7717 100644 --- a/src/views/DownloadsView.vue +++ b/src/views/DownloadsView.vue @@ -7,10 +7,7 @@

Activité

- + {{ polling ? 'En direct' : 'Pause' }}
@@ -19,15 +16,12 @@ {{ importingAll ? '...' : 'Importer tout' }} - + @@ -39,39 +33,24 @@ - +
- +
- -
- -
+

Colonnes affichées

-