BREAKING CHANGE: nouvelle architecture frontend avec layout sidebar, système de thèmes et routes restructurées ### Frontend - nouveau layout global `AppLayout.vue` avec sidebar de navigation inspirée Radarr/Sonarr - sidebar configurable via un objet `NavItem[]` avec support parents/enfants cliquables - 3 thèmes CSS (Indigo Électrique, Crimson Night, Teal Froid) persistés via localStorage - switcher de thème en bas de sidebar avec pastilles colorées - logo FanKarr cliquable, bouton déconnexion discret dans le header sidebar - badge animé sur "Activité" affichant le nombre de téléchargements en cours - migration complète vers Tailwind v4 avec variables CSS `@theme` - classes utilitaires globales : `btn-primary`, `btn-secondary`, `settings-card`, `settings-input` ### Vues - `SeriesView` : barre colorée en bas des affiches (vert/bleu/orange), dropdown filtres, boutons taille S/M/L/XL persistés - `SerieView` : bouton état unique par épisode (↓/⟳/✓/✗), badges simplifiés, toast via `useToast` global - `ActivityView` : onglets pill, stats, toggle "masquer importés", polling silencieux - `DashboardView` : stats catalogue/importées/en cours/erreurs, torrents actifs, derniers imports - `SettingsView` découpée en 3 vues indépendantes : `DownloadClientView`, `MediaManagementView`, `CatalogueView` - `LogsView` déplacée dans `src/views/settings/` - `AuthView` : migré au système de thème ### Composants - `SidebarNavItem` : support badge, chevron masqué si badge présent - `SettingsToggle` : composant réutilisable `v-model` - `FolderPicker` : migré au thème, `Teleport` intégré - `ToastContainer` : déplacé en bas droite, groupement des toasts similaires avec badge count ### Router - `AppLayout` comme layout racine pour toutes les routes protégées - `/auth` reste hors layout - redirections `/` → `/series`, `/downloads` → `/activity` - `SettingsLayout` avec titre dynamique par route - scroll géré sur `<main id="main-scroll">` dans `AppLayout` ### Stores & composables - nouveau store `downloads` partagé pour le badge sidebar - `usePosterSize` : taille des affiches persistée en localStorage - `useToast` : groupement des toasts du même type, timer reset au lieu d'empilement ### Serveur - logs structurés via `logger` dans `auth.ts`, `organize.ts`, `organize-worker.ts`, `torrent-clients/index.ts`, `qbittorrent.ts`, `index.ts` - suppression des `console.log/error/warn` en faveur de `logger.info/warn/error/debug` - messages worker routés vers `logger` via `organize.ts` - logs `debug` pour les événements verbeux (healthcheck, skip, worker terminé)
117 lines
4.8 KiB
TypeScript
117 lines
4.8 KiB
TypeScript
/**
|
|
* qBittorrent Driver
|
|
*/
|
|
|
|
import type { TorrentClientDriver, TorrentInfo } from './index.js'
|
|
import { logger } from '../logger.js'
|
|
|
|
function mapState(state: string): TorrentInfo['state'] {
|
|
if (['downloading', 'metaDL', 'queuedDL', 'stalledDL', 'forcedDL'].includes(state)) return 'downloading'
|
|
if (['uploading', 'queuedUP', 'stalledUP', 'forcedUP'].includes(state)) return 'seeding'
|
|
if (['pausedDL', 'pausedUP'].includes(state)) return 'paused'
|
|
if (['checkingDL', 'checkingUP', 'checkingResumeData'].includes(state)) return 'checking'
|
|
if (state === 'error' || state === 'missingFiles') return 'error'
|
|
return 'unknown'
|
|
}
|
|
|
|
async function qbLogin(config: Record<string, string | number>): Promise<string> {
|
|
const form = new URLSearchParams()
|
|
form.append('username', String(config.username ?? ''))
|
|
form.append('password', String(config.password ?? ''))
|
|
|
|
const res = await fetch(`${config.url}/api/v2/auth/login`, {
|
|
method : 'POST',
|
|
body : form,
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
})
|
|
|
|
const text = await res.text()
|
|
if (text !== 'Ok.') throw new Error(`Login échoué : ${text}`)
|
|
|
|
const cookie = res.headers.get('set-cookie') ?? ''
|
|
const sid = cookie.match(/SID=([^;]+)/)?.[1]
|
|
if (!sid) throw new Error('Cookie SID introuvable')
|
|
return sid
|
|
}
|
|
|
|
const QB: TorrentClientDriver = {
|
|
definition: {
|
|
id : 'qbittorrent',
|
|
label : 'qBittorrent',
|
|
fields: [
|
|
{ key: 'url', label: 'URL WebUI', type: 'url', placeholder: 'http://localhost:8080', required: true },
|
|
{ key: 'username', label: 'Identifiant', type: 'text', placeholder: 'admin', required: true },
|
|
{ key: 'password', label: 'Mot de passe', type: 'password', placeholder: '••••••••', required: true },
|
|
{ key: 'category', label: 'Catégorie', type: 'text', placeholder: 'fankai', required: false, default: 'fankai' },
|
|
],
|
|
},
|
|
|
|
async test(config) {
|
|
try {
|
|
await qbLogin(config)
|
|
logger.info('qbittorrent', `Test de connexion réussi sur ${config.url}`)
|
|
return { ok: true, message: 'Connexion réussie' }
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : 'Erreur inconnue'
|
|
logger.warn('qbittorrent', `Test de connexion échoué sur ${config.url} : ${msg}`)
|
|
return { ok: false, message: msg }
|
|
}
|
|
},
|
|
|
|
async healthcheck(config) {
|
|
try {
|
|
const sid = await qbLogin(config)
|
|
const res = await fetch(`${config.url}/api/v2/app/version`, {
|
|
headers: { Cookie: `SID=${sid}` },
|
|
})
|
|
if (!res.ok) return { online: false }
|
|
const version = (await res.text()).trim()
|
|
logger.debug('qbittorrent', `Healthcheck OK — version ${version}`)
|
|
return { online: true, version }
|
|
} catch (err) {
|
|
logger.debug('qbittorrent', `Healthcheck échoué : ${err instanceof Error ? err.message : err}`)
|
|
return { online: false }
|
|
}
|
|
},
|
|
|
|
async list(config, category) {
|
|
const sid = await qbLogin(config)
|
|
const params = new URLSearchParams()
|
|
if (category) params.set('category', category)
|
|
const res = await fetch(`${config.url}/api/v2/torrents/info?${params}`, {
|
|
headers: { Cookie: `SID=${sid}` },
|
|
})
|
|
if (!res.ok) throw new Error(`qB list échoué : ${res.status}`)
|
|
const data: any[] = await res.json()
|
|
return data.map(t => ({
|
|
hash : t.hash,
|
|
name : t.name,
|
|
state : mapState(t.state),
|
|
progress : Math.round(t.progress * 100),
|
|
size : t.size,
|
|
downloaded: t.downloaded,
|
|
speed : t.dlspeed,
|
|
eta : t.eta ?? -1,
|
|
save_path : t.save_path,
|
|
category : t.category ?? '',
|
|
} satisfies TorrentInfo))
|
|
},
|
|
|
|
async add(config, url) {
|
|
const sid = await qbLogin(config)
|
|
const form = new FormData()
|
|
form.append('urls', url)
|
|
if (config.category) form.append('category', String(config.category))
|
|
if (config.savePath) form.append('savepath', String(config.savePath))
|
|
const res = await fetch(`${config.url}/api/v2/torrents/add`, {
|
|
method : 'POST',
|
|
body : form,
|
|
headers: { Cookie: `SID=${sid}` },
|
|
})
|
|
const text = await res.text()
|
|
if (text !== 'Ok.') throw new Error(`Ajout échoué : ${text}`)
|
|
logger.info('qbittorrent', `Torrent ajouté avec succès (catégorie: ${config.category ?? 'aucune'})`)
|
|
},
|
|
}
|
|
|
|
export default QB |