/** * qBittorrent Driver */ import type { TorrentClientDriver, TorrentInfo } from './index.js' // Mapping état qBittorrent → état normalisé 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): Promise { 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) return { ok: true, message: 'Connexion réussie' } } catch (err) { return { ok: false, message: err instanceof Error ? err.message : 'Erreur inconnue' } } }, 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 } return { online: true, version: (await res.text()).trim() } } catch { 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}`) }, } export default QB