From e1e430a9685342e847bbd15c105bfa38f7064101 Mon Sep 17 00:00:00 2001 From: MasutaYunikon Date: Tue, 24 Mar 2026 02:32:47 +0100 Subject: [PATCH] =?UTF-8?q?feat(settings):=20ajout=20du=20toggle=20import?= =?UTF-8?q?=20automatique=20(activ=C3=A9=20par=20d=C3=A9faut)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- server/index.ts | 16 ++-- server/organize-worker.ts | 29 ++++-- server/organize.ts | 6 +- server/settings.ts | 4 +- src/views/CatalogView.vue | 184 ++++++++++++++++++------------------ src/views/DownloadsView.vue | 136 +++++++++++--------------- src/views/SettingsView.vue | 112 +++++++++++----------- 7 files changed, 241 insertions(+), 246 deletions(-) diff --git a/server/index.ts b/server/index.ts index d1b1d0f..2a64f27 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 } = 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() + // Index episode_id → original_filename + formatted_name + const origMap = new Map() 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 } diff --git a/server/organize-worker.ts b/server/organize-worker.ts index caed5ba..eb756d3 100644 --- a/server/organize-worker.ts +++ b/server/organize-worker.ts @@ -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 { const map = new Map() @@ -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] diff --git a/server/organize.ts b/server/organize.ts index f2036cf..3d710fc 100644 --- a/server/organize.ts +++ b/server/organize.ts @@ -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, seriesData : any[], onResult ?: (r: { hash: string; name: string; done: number; skipped: number; errors: number; errorFiles: { file: string; error: string }[] }) => void ): Promise { + // 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 diff --git a/server/settings.ts b/server/settings.ts index 6789c59..0a60edd 100644 --- a/server/settings.ts +++ b/server/settings.ts @@ -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 { diff --git a/src/views/CatalogView.vue b/src/views/CatalogView.vue index dbbc4be..4d0b14d 100644 --- a/src/views/CatalogView.vue +++ b/src/views/CatalogView.vue @@ -8,10 +8,8 @@ Fankarr
- + class="p-2 rounded-lg text-zinc-500 hover:text-zinc-100 hover:bg-white/5 transition"> @@ -21,32 +19,25 @@ + class="relative p-2 rounded-lg text-zinc-500 hover:text-zinc-100 hover:bg-white/5 transition"> - {{ activeDownloads }} - + -
- +
- +
-
- - - +
+ + - - +
- {{ filtered.length }} série{{ filtered.length > 1 ? 's' : '' }} + + +
+ +
+ + {{ filtered.length }} série{{ filtered.length > 1 ? 's' : '' }}
-

Chargement du catalogue…

-
@@ -117,12 +105,10 @@
-

Aucune série trouvée

-
-
- +
@@ -147,16 +127,12 @@
- - - - +
- @@ -168,32 +144,26 @@ - {{ serie.download_state === 'complete' ? 'ORGANISÉ' : serie.download_state === 'downloading' ? 'EN COURS' : 'PARTIEL' }} + {{ serie.download_state === 'complete' ? 'IMPORTÉ' : serie.download_state === 'downloading' ? 'EN COURS' : 'PARTIEL' }}
-
Voir
-

{{ serie.title }}

{{ serie.year }} - {{ serie.status }} + {{ serie.status }}
-
@@ -205,36 +175,66 @@ import { useSeriesStore } from '@/stores/series' import { useAuthStore } from '@/stores/auth' import { useToast } from '@/composables/useToast' -const store = useSeriesStore() -const auth = useAuthStore() +const store = useSeriesStore() +const auth = useAuthStore() const router = useRouter() const { add: toast } = useToast() -const search = ref('') +const search = ref('') const activeFilter = ref('all') +const activeSort = ref('alpha') const activeDownloads = ref(0) const filtersGroupe1 = [ - { label: 'Toutes', value: 'all' }, - { label: 'Disponibles', value: 'available' }, - { label: 'Sans torrent', value: 'unavailable' }, + { label: 'Toutes', value: 'all' }, + { label: 'Disponibles', value: 'available' }, + { label: 'Sans torrent', value: 'unavailable' }, ] const filtersGroupe2 = [ - { label: 'DL complet', value: 'complete' }, - { label: 'DL partiel', value: 'downloading' }, + { label: 'Importés', value: 'complete' }, + { label: 'En cours', value: 'downloading' }, +] + +const sortOptions = [ + { label: 'A→Z', value: 'alpha' }, + { label: 'Z→A', value: 'alpha-desc' }, + { label: 'Récents', value: 'recent' }, + { label: 'Importés', value: 'imported' }, ] const filtered = computed(() => { - let list = store.series - if (activeFilter.value === 'available') list = list.filter(s => s.has_torrents) - if (activeFilter.value === 'unavailable') list = list.filter(s => !s.has_torrents) - if (activeFilter.value === 'complete') list = list.filter(s => s.download_state === 'complete') - if (activeFilter.value === 'downloading') list = list.filter(s => s.download_state === 'downloading' || s.download_state === 'partial') + let list = [...store.series] + + // Filtres + if (activeFilter.value === 'available') list = list.filter(s => s.has_torrents) + if (activeFilter.value === 'unavailable') list = list.filter(s => !s.has_torrents) + if (activeFilter.value === 'complete') list = list.filter(s => s.download_state === 'complete') + if (activeFilter.value === 'downloading') list = list.filter(s => s.download_state === 'downloading' || s.download_state === 'partial') + if (search.value.trim()) { const q = search.value.toLowerCase() list = list.filter(s => s.title.toLowerCase().includes(q)) } + + // Tri + switch (activeSort.value) { + case 'alpha': + list.sort((a, b) => a.title.localeCompare(b.title, 'fr')) + break + case 'alpha-desc': + list.sort((a, b) => b.title.localeCompare(a.title, 'fr')) + break + case 'recent': + list.sort((a, b) => (b.year ?? 0) - (a.year ?? 0)) + break + case 'imported': + // Importés en premier, puis partiel, puis reste + const order: Record = { complete: 0, partial: 1, downloading: 2, none: 3 } + list.sort((a, b) => (order[a.download_state] ?? 3) - (order[b.download_state] ?? 3)) + break + } + return list }) @@ -243,7 +243,7 @@ async function handleLogout() { router.push('/auth') } -// ── Polling downloads actifs + notifications organisation ────── +// ── Polling downloads + notifs import ───────────────────────── let dlInterval: ReturnType | null = null const seenNotifs = new Set() @@ -261,23 +261,24 @@ async function fetchOrganizeNotifs() { const res = await fetch('/api/organize/recent', { credentials: 'include' }) if (!res.ok) return const notifs: any[] = await res.json() + let hasNew = false for (const n of notifs) { const key = `${n.hash}-${n.at}` if (seenNotifs.has(key)) continue seenNotifs.add(key) + hasNew = true if (n.done > 0) { const msg = n.errors > 0 - ? `${n.name} — ${n.done} fichier(s) organisé(s), ${n.errors} erreur(s)` - : `${n.name} — ${n.done} fichier(s) organisé(s) ✓` + ? `${n.name} — ${n.done} fichier(s) importé(s), ${n.errors} erreur(s)` + : `${n.name} — ${n.done} fichier(s) importé(s) ✓` toast(msg, n.errors > 0 ? 'error' : 'success') } } + // Rafraîchir le catalogue si de nouveaux imports ont eu lieu + if (hasNew) await store.fetchSeries() } catch {} } -// ── Cache images ─────────────────────────────────────────────── -// Géré directement dans le store series.ts - onMounted(async () => { if (store.series.length === 0) await store.fetchSeries() fetchActiveDownloads() @@ -288,7 +289,6 @@ onMounted(async () => { }, 10000) }) -// Refresh au retour sur le catalogue (après update ou scan) onActivated(async () => { await store.fetchSeries() fetchActiveDownloads() diff --git a/src/views/DownloadsView.vue b/src/views/DownloadsView.vue index 53a7973..62bef40 100644 --- a/src/views/DownloadsView.vue +++ b/src/views/DownloadsView.vue @@ -10,11 +10,9 @@

Téléchargements

-
@@ -38,49 +36,39 @@
- -
-
- - MASQUER ORGANISÉS + MASQUER IMPORTÉS

Aucun client torrent configuré

- + CONFIGURER →
@@ -99,37 +87,30 @@

{{ activeTab === 'active' ? 'Les torrents avec la catégorie « fankai » apparaîtront ici' - : hideOrganized ? 'Tous les torrents terminés sont organisés' : '' }} + : hideImported ? 'Tous les torrents terminés sont importés' : '' }}

-
+
-

{{ t.name }}

- + {{ stateBadge(t.state).label }} {{ t.client_name }}
-

{{ t.progress }}%

-

- {{ formatSize(t.downloaded) }} / {{ formatSize(t.size) }} -

+

{{ formatSize(t.downloaded) }} / {{ formatSize(t.size) }}

-
-
↓ {{ formatSpeed(t.speed) }} @@ -146,7 +126,6 @@ ✓ Complété
-
@@ -164,19 +143,19 @@
- {{ t.organizeState === 'done' ? `ORGANISÉ (${t.organizeProgress?.done}/${t.organizeProgress?.total})` : + :class="t.organizeState === 'done' ? 'border-green-500/40 text-green-500' : + t.organizeState === 'partial' ? 'border-yellow-500/40 text-yellow-500' : + 'border-red-500/40 text-red-400'"> + {{ t.organizeState === 'done' ? `IMPORTÉ (${t.organizeProgress?.done}/${t.organizeProgress?.total})` : t.organizeState === 'partial' ? `EN COURS (${t.organizeProgress?.done}/${t.organizeProgress?.total})` : - 'NON ORGANISÉ' }} + 'NON IMPORTÉ' }}
@@ -191,18 +170,17 @@ import { useToast } from '@/composables/useToast' const { add: toast } = useToast() -const torrents = ref([]) -const loading = ref(true) -const polling = ref(true) -const noClients = ref(false) -const organizing = ref>({}) -const organizingAll = ref(false) -const activeTab = ref<'active' | 'done'>('active') -const hideOrganized = ref(false) +const torrents = ref([]) +const loading = ref(true) +const polling = ref(true) +const noClients = ref(false) +const importing = ref>({}) +const importingAll = ref(false) +const activeTab = ref<'active' | 'done'>('active') +const hideImported = ref(false) let pollInterval: ReturnType | null = null -// ── Filtres onglets ──────────────────────────────────────────── const activeTorrents = computed(() => torrents.value.filter(t => ['downloading', 'paused', 'checking', 'error', 'unknown'].includes(t.state)) ) @@ -213,11 +191,10 @@ const doneTorrents = computed(() => const visibleTorrents = computed(() => { if (activeTab.value === 'active') return activeTorrents.value - if (hideOrganized.value) return doneTorrents.value.filter(t => t.organizeState !== 'done') + if (hideImported.value) return doneTorrents.value.filter(t => t.organizeState !== 'done') return doneTorrents.value }) -// ── Stats ────────────────────────────────────────────────────── const stats = computed(() => [ { label: 'EN COURS', value: torrents.value.filter(t => t.state === 'downloading').length }, { label: 'COMPLÉTÉS', value: torrents.value.filter(t => t.state === 'seeding').length }, @@ -225,7 +202,6 @@ const stats = computed(() => [ { label: 'ERREURS', value: torrents.value.filter(t => t.state === 'error').length }, ]) -// ── Fetch ────────────────────────────────────────────────────── async function fetchTorrents() { try { const res = await fetch('/api/downloads', { credentials: 'include' }) @@ -234,7 +210,6 @@ async function fetchTorrents() { torrents.value = await res.json() noClients.value = false } catch { - // silencieux en polling } finally { loading.value = false } @@ -258,46 +233,44 @@ function togglePolling() { onMounted(startPolling) onUnmounted(stopPolling) -// ── Organiser un torrent ─────────────────────────────────────── -async function organize(torrent: any) { - organizing.value[torrent.hash] = true +async function importTorrent(torrent: any) { + importing.value[torrent.hash] = true try { const res = await fetch('/api/organize', { - method : 'POST', + method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ hash: torrent.hash, save_path: torrent.save_path, name: torrent.name }) }) if (res.ok) { - toast(`${torrent.name} organisé ✓`, 'success') + toast(`${torrent.name} importé ✓`, 'success') await fetchTorrents() } else { const { error } = await res.json() - toast(error ?? 'Erreur lors de l\'organisation', 'error') + toast(error ?? "Erreur lors de l'import", 'error') } } catch { toast('Impossible de contacter le serveur', 'error') } finally { - organizing.value[torrent.hash] = false + importing.value[torrent.hash] = false } } -// ── Organiser tout ───────────────────────────────────────────── -async function organizeAll() { - const toOrganize = doneTorrents.value.filter(t => t.organizeState !== 'done') - if (toOrganize.length === 0) { - toast('Tous les torrents sont déjà organisés', 'success') +async function importAll() { + const toImport = doneTorrents.value.filter(t => t.organizeState !== 'done') + if (toImport.length === 0) { + toast('Tous les torrents sont déjà importés', 'success') return } - organizingAll.value = true + importingAll.value = true let done = 0 let errors = 0 - for (const t of toOrganize) { + for (const t of toImport) { try { const res = await fetch('/api/organize', { - method : 'POST', + method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ hash: t.hash, save_path: t.save_path, name: t.name }) @@ -309,14 +282,13 @@ async function organizeAll() { } } - organizingAll.value = false + importingAll.value = false await fetchTorrents() - if (errors === 0) toast(`${done} torrent(s) organisé(s) ✓`, 'success') + if (errors === 0) toast(`${done} torrent(s) importé(s) ✓`, 'success') else toast(`${done} OK, ${errors} erreur(s)`, 'error') } -// ── Helpers affichage ────────────────────────────────────────── function stateBadge(state: string) { const map: Record = { downloading: { label: 'TÉLÉCHARGEMENT', class: 'border-[#e8513a]/40 text-[#e8513a]' }, @@ -336,9 +308,7 @@ function formatSize(bytes: number): string { return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}` } -function formatSpeed(bps: number): string { - return `${formatSize(bps)}/s` -} +function formatSpeed(bps: number): string { return `${formatSize(bps)}/s` } function formatEta(seconds: number): string { if (seconds < 0 || seconds > 86400 * 7) return '∞' diff --git a/src/views/SettingsView.vue b/src/views/SettingsView.vue index 63af5c3..6a7b725 100644 --- a/src/views/SettingsView.vue +++ b/src/views/SettingsView.vue @@ -15,7 +15,7 @@
DONNÉES MANQUANTES

- {{ torrentsStatus.exists ? 'Le fichier de torrents est vide.' : 'Aucun fichier de torrents trouvé.' }} + {{ torrentsStatus.exists ? 'Le catalogue est vide.' : 'Aucun catalogue trouvé.' }} Téléchargez les données pour utiliser Fankarr.

@@ -28,9 +28,9 @@
DONNÉES
-

Torrents Fankai

+

Catalogue Fankai

- {{ torrentsStatus.empty ? 'Aucune donnée chargée' : `${torrentsStatus.count} torrents chargés` }} + {{ torrentsStatus.empty ? 'Aucune donnée chargée' : `${torrentsStatus.count} séries disponibles` }}

- +
MÉDIAS
-

Organisation

+

Import

- + DOCKER
-

Dossier de complétion de qBittorrent

+

Dossier où sont déposés les fichiers téléchargés

- +
- + DOCKER
-

Dossier racine de la bibliothèque Fankai dans Jellyfin

+

Dossier racine de votre médiathèque Fankai (Jellyfin, Kodi, Plex…)

- +
- +

- Hardlink recommandé — le fichier reste dans qBittorrent pour le ratio + Hardlink recommandé — le fichier reste dans le client torrent pour le ratio +

+
+ + +
+ +
+ + Importer automatiquement dès qu'un téléchargement est terminé +
+

+ Vérifie toutes les 5 minutes si de nouveaux fichiers sont prêts à être importés

@@ -151,7 +168,7 @@

Pour les lecteurs sans agent Fankai (Kodi, Infuse, etc.) — télécharge les métadonnées depuis - fankai_pack au moment de l'organisation + fankai_pack lors de l'import

@@ -162,7 +179,7 @@ @@ -200,7 +217,7 @@
-
@@ -259,11 +276,12 @@ const form = ref({ completePath: '', organizeMode: 'hardlink' as 'hardlink' | 'move', nfoSupport : false, + autoImport : true, }) -const saving = ref(false) -const updating = ref(false) -const scanning = ref(false) +const saving = ref(false) +const updating = ref(false) +const scanning = ref(false) const scanResult = ref<{ found: number; added: number } | null>(null) // ── Folder picker ────────────────────────────────────────────── @@ -272,23 +290,19 @@ const picker = ref<{ open: boolean; field: 'completePath' | 'mediaPath'; current }) function openPicker(field: 'completePath' | 'mediaPath') { - picker.value = { - open : true, - field, - currentPath: (form.value as any)[field] || '/', - } + picker.value = { open: true, field, currentPath: (form.value as any)[field] || '/' } } function onPickerSelect(path: string) { (form.value as any)[picker.value.field] = path picker.value.open = false } -const isDocker = ref(false) -const torrentsStatus = ref({ exists: false, count: 0, empty: true }) -const clients = ref([]) +const isDocker = ref(false) +const torrentsStatus = ref({ exists: false, count: 0, empty: true }) +const clients = ref([]) const availableClients = ref([]) -const healthStatus = ref>({}) +const healthStatus = ref>({}) const modal = ref({ open : false, @@ -333,9 +347,7 @@ async function refreshHealth(uuid: string) { try { const res = await fetch(`/api/torrent-clients/${uuid}/healthcheck`, { credentials: 'include' }) healthStatus.value[uuid] = res.ok ? (await res.json()).online : false - } catch { - healthStatus.value[uuid] = false - } + } catch { healthStatus.value[uuid] = false } } // ── Test / delete clients ────────────────────────────────────── @@ -362,9 +374,7 @@ function openAddModal() { modal.value = { open: true, name: '', type: '', config: {}, testing: false, saving: false, tested: false } } -function closeModal() { - modal.value.open = false -} +function closeModal() { modal.value.open = false } function onTypeChange() { modal.value.config = {} @@ -382,9 +392,7 @@ async function testNewClient() { modal.value.tested = false try { const res = await fetch('/api/torrent-clients/test-config', { - method : 'POST', - headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ type: modal.value.type, config: modal.value.config }) }) const { ok, message } = await res.json() @@ -402,14 +410,8 @@ async function saveClient() { modal.value.saving = true try { const res = await fetch('/api/torrent-clients', { - method : 'POST', - headers: { 'Content-Type': 'application/json' }, - credentials: 'include', - body: JSON.stringify({ - name : modal.value.name || clientLabel(modal.value.type), - type : modal.value.type, - config: modal.value.config - }) + method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', + body: JSON.stringify({ name: modal.value.name || clientLabel(modal.value.type), type: modal.value.type, config: modal.value.config }) }) if (res.ok) { const client = await res.json() @@ -426,7 +428,7 @@ async function saveClient() { } } -// ── Données torrents ─────────────────────────────────────────── +// ── Données ──────────────────────────────────────────────────── async function update() { updating.value = true try { @@ -434,7 +436,7 @@ async function update() { if (res.ok) { const { count } = await res.json() torrentsStatus.value = { exists: true, count, empty: count === 0 } - toast(`${count} torrents téléchargés ✓`, 'success') + toast(`${count} séries chargées ✓`, 'success') } else { const { error } = await res.json() toast(error ?? 'Erreur lors de la mise à jour', 'error') @@ -446,7 +448,7 @@ async function update() { } } -// ── Scan bibliothèque ────────────────────────────────────────── +// ── Scan ─────────────────────────────────────────────────────── async function scan() { scanning.value = true scanResult.value = null @@ -457,12 +459,12 @@ async function scan() { scanResult.value = { found: data.found, added: data.added } toast( data.added > 0 - ? `${data.found} fichiers scannés — ${data.added} ajoutés` - : `${data.found} fichiers scannés — rien de nouveau`, + ? `${data.found} fichiers analysés — ${data.added} ajoutés` + : `${data.found} fichiers analysés — rien de nouveau`, 'success' ) } else { - toast('Erreur lors du scan', 'error') + toast('Erreur lors de l\'analyse', 'error') } } catch { toast('Impossible de contacter le serveur', 'error') @@ -471,14 +473,12 @@ async function scan() { } } -// ── Sauvegarder settings ─────────────────────────────────────── +// ── Sauvegarder ──────────────────────────────────────────────── async function save() { saving.value = true try { const res = await fetch('/api/settings', { - method : 'POST', - headers: { 'Content-Type': 'application/json' }, - credentials: 'include', + method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(form.value) }) if (res.ok) toast('Configuration sauvegardée', 'success')