diff --git a/server/index.ts b/server/index.ts index aafc10b..d2e50ba 100644 --- a/server/index.ts +++ b/server/index.ts @@ -230,13 +230,69 @@ app.get('/api/downloads', requireAuth, async (_req, res) => { try { const { category } = readSettings() const torrents = await dispatchList(category ?? 'fankai') - res.json(torrents) + + // Enrichir avec l'état d'organisation + let organized: Record> = {} + try { + const orgPath = path.join(process.cwd(), 'data', 'organized.json') + if (fs.existsSync(orgPath)) + organized = JSON.parse(fs.readFileSync(orgPath, 'utf-8')) + } catch {} + + let torrentFinal: any[] = [] + try { + const tfPath = path.join(process.cwd(), 'data', 'torrent_final.json') + if (fs.existsSync(tfPath)) + torrentFinal = JSON.parse(fs.readFileSync(tfPath, 'utf-8')) + } catch {} + + const enriched = torrents.map((t: any) => { + const orgFiles = organized[t.hash] ?? {} + const meta = torrentFinal.find((m: any) => + m.infohash?.toLowerCase() === t.hash?.toLowerCase() + ) + const totalFiles = (meta?.torrent_files?.length ?? 0) || 1 + const doneFiles = Object.keys(orgFiles).length + let organizeState: 'none' | 'partial' | 'done' = 'none' + if (doneFiles >= totalFiles) organizeState = 'done' + else if (doneFiles > 0) organizeState = 'partial' + return { ...t, organizeState, organizeProgress: { done: doneFiles, total: totalFiles } } + }) + + res.json(enriched) } catch (err) { res.status(500).json({ error: err instanceof Error ? err.message : 'Erreur inconnue' }) } }) -// ── Organize ─────────────────────────────────────────────────── +// ── Résultats d'organisation récents (pour notifications frontend) ── +interface OrganizeNotif { + hash : string + name : string + done : number + skipped : number + errors : number + at : string +} +const recentOrganized: OrganizeNotif[] = [] +const MAX_NOTIFS = 20 + +function pushNotif(n: OrganizeNotif) { + recentOrganized.unshift(n) + if (recentOrganized.length > MAX_NOTIFS) recentOrganized.pop() +} + +app.get('/api/organize/recent', requireAuth, (_req, res) => { + res.json(recentOrganized) +}) + +// Vider les notifs lues +app.post('/api/organize/recent/clear', requireAuth, (_req, res) => { + recentOrganized.length = 0 + res.json({ ok: true }) +}) + + app.post('/api/organize', requireAuth, async (req, res) => { const { hash, name, save_path } = req.body if (!hash || !name || !save_path) { @@ -252,6 +308,12 @@ app.post('/api/organize', requireAuth, async (req, res) => { }) // ── Torrents status ──────────────────────────────────────────── +// ── System info ──────────────────────────────────────────────── +app.get('/api/system', requireAuth, (_req, res) => { + const isDocker = fs.existsSync('/.dockerenv') + res.json({ isDocker }) +}) + app.get('/api/torrents/status', requireAuth, (_req, res) => { try { if (!fs.existsSync(TORRENTS_PATH)) { res.json({ exists: false, count: 0, empty: true }); return } @@ -299,7 +361,14 @@ app.listen(PORT, () => { const autoOrganize = async () => { try { const { category } = readSettings() - await autoOrganizeAll(() => dispatchList(category ?? 'fankai')) + await autoOrganizeAll( + () => dispatchList(category ?? 'fankai'), + (result) => { + if (result.done > 0 || result.errors > 0) { + pushNotif({ ...result, at: new Date().toISOString() }) + } + } + ) } catch (err) { console.error('[organize] Erreur auto-organise:', err) } diff --git a/server/organize.ts b/server/organize.ts index bc918d6..402b821 100644 --- a/server/organize.ts +++ b/server/organize.ts @@ -24,7 +24,8 @@ export interface OrganizeResult { * puis les données sont passées au worker. */ export async function autoOrganizeAll( - listFn: () => Promise + listFn : () => Promise, + onResult?: (r: { hash: string; name: string; done: number; skipped: number; errors: number }) => void ): Promise { if (workerRunning) { console.log('[organize] Worker déjà en cours, skip') @@ -58,6 +59,13 @@ export async function autoOrganizeAll( else console.log(msg.msg) } else if (msg.type === 'result') { console.log(`[organize] ${msg.name} → ${msg.done} OK, ${msg.skipped} skippés, ${msg.errors.length} erreurs`) + onResult?.({ + hash : msg.hash, + name : msg.name, + done : msg.done, + skipped: msg.skipped, + errors : msg.errors.length, + }) } else if (msg.type === 'done') { console.log('[organize] Worker terminé') workerRunning = false diff --git a/src/views/CatalogView.vue b/src/views/CatalogView.vue index c9e38c6..f623705 100644 --- a/src/views/CatalogView.vue +++ b/src/views/CatalogView.vue @@ -8,14 +8,20 @@ Fankarr
+ + class="relative p-2 rounded-lg text-zinc-500 hover:text-zinc-100 hover:bg-white/5 transition" + > - + + + - + + class="absolute -top-0.5 -right-0.5 min-w-[16px] h-4 bg-orange-500 text-white text-[9px] font-bold rounded-full flex items-center justify-center px-1 leading-none"> + {{ activeDownloads }} + \ No newline at end of file diff --git a/src/views/DownloadsView.vue b/src/views/DownloadsView.vue index f739b4b..e12979b 100644 --- a/src/views/DownloadsView.vue +++ b/src/views/DownloadsView.vue @@ -103,16 +103,25 @@
- - + +
+ + + {{ t.organizeState === 'done' ? `ORGANISÉ (${t.organizeProgress?.done}/${t.organizeProgress?.total})` : + t.organizeState === 'partial' ? `EN COURS (${t.organizeProgress?.done}/${t.organizeProgress?.total})` : + 'NON ORGANISÉ' }} + + + +
@@ -130,7 +139,6 @@ const loading = ref(true) const polling = ref(true) const noClients = ref(false) const organizing = ref>({}) -const organized = ref>({}) let pollInterval: ReturnType | null = null diff --git a/src/views/SettingsView.vue b/src/views/SettingsView.vue index 8b1f9b6..56192cc 100644 --- a/src/views/SettingsView.vue +++ b/src/views/SettingsView.vue @@ -87,20 +87,34 @@
- - + + DOCKER +
+ -

Dossier de complétion de qBittorrent (monté dans Docker)

+

+ Défini par le volume Docker — non modifiable ici + Dossier de complétion de qBittorrent +

- - + + DOCKER +
+ -

Dossier racine de la bibliothèque Fankai dans Jellyfin

+

+ Défini par le volume Docker — non modifiable ici + Dossier racine de la bibliothèque Fankai dans Jellyfin +

@@ -215,8 +229,9 @@ const form = ref({ organizeMode: 'hardlink' as 'hardlink' | 'move', }) -const saving = ref(false) -const updating = ref(false) +const saving = ref(false) +const updating = ref(false) +const isDocker = ref(false) const torrentsStatus = ref({ exists: false, count: 0, empty: true }) const clients = ref([]) @@ -244,18 +259,20 @@ function clientLabel(type: string): string { // ── Init ─────────────────────────────────────────────────────── onMounted(async () => { - const [settingsRes, statusRes, clientsRes, availRes] = await Promise.all([ + const [settingsRes, statusRes, clientsRes, availRes, systemRes] = await Promise.all([ fetch('/api/settings', { credentials: 'include' }), fetch('/api/torrents/status', { credentials: 'include' }), fetch('/api/torrent-clients', { credentials: 'include' }), fetch('/api/torrent-clients/available', { credentials: 'include' }), + fetch('/api/system', { credentials: 'include' }), ]) if (settingsRes.ok) Object.assign(form.value, await settingsRes.json()) if (statusRes.ok) torrentsStatus.value = await statusRes.json() if (clientsRes.ok) clients.value = await clientsRes.json() if (availRes.ok) availableClients.value = await availRes.json() + if (systemRes.ok) isDocker.value = (await systemRes.json()).isDocker - for (const c of clients.value) refreshHealth(c.uuid) + for (const c of clients.value) await refreshHealth(c.uuid) }) // ── Healthcheck ──────────────────────────────────────────────── @@ -345,7 +362,7 @@ async function saveClient() { if (res.ok) { const client = await res.json() clients.value.push(client) - refreshHealth(client.uuid) + await refreshHealth(client.uuid) closeModal() toast('Client enregistré ✓', 'success') } else {