diff --git a/server/torrent-clients/deluge.ts b/server/torrent-clients/deluge.ts index 6d11e79..c79a200 100644 --- a/server/torrent-clients/deluge.ts +++ b/server/torrent-clients/deluge.ts @@ -8,7 +8,8 @@ * Progress : float 0–100 (déjà en pourcentage, pas 0–1) */ -import type { TorrentClientDriver, TorrentInfo, TorrentFileProgress, DownloadOptions } from './index.js' +import type { TorrentClientDriver, TorrentInfo, TorrentFileProgress, DownloadOptions, ClientConfig } from './index.js' +import { clientFetch } from './index.js' import { logger } from '../logger.js' // ─── Session cache ───────────────────────────────────────────────────────────── @@ -18,13 +19,13 @@ import { logger } from '../logger.js' interface SessionEntry { cookie: string; expires: number } const _sessions = new Map() -function sessionKey(config: Record): string { +function sessionKey(config: ClientConfig): string { return `${config.url}::${config.password}` } // ─── Helpers HTTP ────────────────────────────────────────────────────────────── -function delugeUrl(config: Record): string { +function delugeUrl(config: ClientConfig): string { return String(config.url ?? '').replace(/\/+$/, '') + '/json' } @@ -34,12 +35,13 @@ function parseCookie(raw: string): string { } async function delugeRPC( + config : ClientConfig, url : string, method : string, params : unknown[], cookie : string, ): Promise<{ result: any; newCookie: string | null }> { - const res = await fetch(url, { + const res = await clientFetch(config, url, { method : 'POST', headers: { 'Content-Type': 'application/json', @@ -68,9 +70,9 @@ async function delugeRPC( // ─── Login ───────────────────────────────────────────────────────────────────── -async function delugeLogin(config: Record): Promise { +async function delugeLogin(config: ClientConfig): Promise { const url = delugeUrl(config) - const res = await fetch(url, { + const res = await clientFetch(config, url, { method : 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body : JSON.stringify({ method: 'auth.login', params: [String(config.password ?? '')], id: 1 }), @@ -94,7 +96,7 @@ async function delugeLogin(config: Record): Promise, + config : ClientConfig, method : string, params : unknown[] = [], ): Promise { @@ -110,7 +112,7 @@ async function delugeCall( } try { - const { result, newCookie } = await delugeRPC(url, method, params, entry.cookie) + const { result, newCookie } = await delugeRPC(config, url, method, params, entry.cookie) // Mise à jour du cookie si Deluge en renvoie un nouveau if (newCookie) { entry.cookie = newCookie @@ -125,7 +127,7 @@ async function delugeCall( const cookie = await delugeLogin(config) const fresh = { cookie, expires: Date.now() + 30 * 60_000 } _sessions.set(key, fresh) - const { result } = await delugeRPC(url, method, params, cookie) + const { result } = await delugeRPC(config, url, method, params, cookie) return result } throw err @@ -178,7 +180,7 @@ function flattenDelugeFiles( // les priorités : fichier voulu = 1 (normal), tous les autres = 0 (skip). async function applyFilePriority( - config : Record, + config : ClientConfig, hash : string, fileIndex: number, ): Promise { @@ -211,6 +213,7 @@ const delugeDriver: TorrentClientDriver = { { key: 'savePath', label: 'Dossier cible', type: 'text', placeholder: '/downloads/fankai', required: false }, { key: 'remotePath', label: 'Chemin distant (client)', type: 'text', placeholder: '/downloads', required: false }, { key: 'localPath', label: 'Chemin local (FanKarr)', type: 'text', placeholder: '/mnt/nas/downloads', required: false }, + { key: 'ignoreCertificateErrors', label: 'Ignorer les erreurs de certificat SSL', type: 'boolean', required: false }, ], }, diff --git a/server/torrent-clients/index.ts b/server/torrent-clients/index.ts index c03ccc6..83dac7c 100644 --- a/server/torrent-clients/index.ts +++ b/server/torrent-clients/index.ts @@ -7,18 +7,22 @@ import { randomUUID } from 'crypto' import fs from 'fs' import path from 'path' +import { Agent, fetch as undiciFetch } from 'undici' import { DATA_DIR } from '../config.js' import { logger } from '../logger.js' // ─── Types ──────────────────────────────────────────────────────────────────── +/** Config d'un client torrent (les champs `boolean` sont stockés en booléens). */ +export type ClientConfig = Record + export interface FieldDef { key : string label : string - type : 'text' | 'password' | 'number' | 'url' + type : 'text' | 'password' | 'number' | 'url' | 'boolean' placeholder?: string required : boolean - default? : string | number + default? : string | number | boolean } export interface TorrentClientDefinition { @@ -34,7 +38,7 @@ export interface SavedClient { uuid : string name : string type : string - config: Record + config: ClientConfig } export interface TorrentFileProgress { @@ -70,12 +74,50 @@ export interface DownloadOptions { export interface TorrentClientDriver { definition : TorrentClientDefinition - test : (config: Record) => Promise<{ ok: boolean; message: string }> - healthcheck : (config: Record) => Promise<{ online: boolean; version?: string }> - add : (config: Record, url: string, options?: DownloadOptions) => Promise - list : (config: Record, category?: string) => Promise - remove : (config: Record, hash: string, deleteFiles?: boolean) => Promise - getFiles ?: (config: Record, hash: string) => Promise + test : (config: ClientConfig) => Promise<{ ok: boolean; message: string }> + healthcheck : (config: ClientConfig) => Promise<{ online: boolean; version?: string }> + add : (config: ClientConfig, url: string, options?: DownloadOptions) => Promise + list : (config: ClientConfig, category?: string) => Promise + remove : (config: ClientConfig, hash: string, deleteFiles?: boolean) => Promise + getFiles ?: (config: ClientConfig, hash: string) => Promise +} + +// ─── Certificats TLS ────────────────────────────────────────────────────────── +// Option "ignoreCertificateErrors" (par client) : accepte les certificats +// auto-signés/invalides. Node → Agent undici (cf. plex.ts) ; Bun → option +// `tls` de son fetch natif (l'Agent undici n'y est pas honoré). + +let _insecureAgent: Agent | null = null + +function insecureAgent(): Agent { + if (!_insecureAgent) _insecureAgent = new Agent({ connect: { rejectUnauthorized: false } }) + return _insecureAgent +} + +export function ignoreCertificateErrors(config: ClientConfig): boolean { + const v = config.ignoreCertificateErrors + return v === true || v === 'true' || v === 1 +} + +// Option `tls` propre au fetch de Bun, absente des types standards. +interface BunRequestInit extends RequestInit { + tls?: { rejectUnauthorized?: boolean } +} + +const isBun = 'bun' in process.versions + +/** fetch du client : accepte les certificats invalides si l'option est active. */ +export function clientFetch( + config : ClientConfig, + url : string, + init : RequestInit = {}, +): Promise { + if (!ignoreCertificateErrors(config)) return fetch(url, init) + if (isBun) { + const bunInit: BunRequestInit = { ...init, tls: { rejectUnauthorized: false } } + return fetch(url, bunInit) + } + return undiciFetch(url, { ...init, dispatcher: insecureAgent() } as Parameters[1]) as unknown as Promise } // ─── Registry ───────────────────────────────────────────────────────────────── @@ -174,7 +216,7 @@ export function listClients(): SavedClient[] { return loadClients() } -export function addClient(name: string, type: string, config: Record): SavedClient { +export function addClient(name: string, type: string, config: ClientConfig): SavedClient { const clients = loadClients() const client: SavedClient = { uuid: randomUUID(), name, type, config } clients.push(client) @@ -198,7 +240,7 @@ export function getClient(uuid: string): SavedClient | undefined { return loadClients().find(c => c.uuid === uuid) } -export function updateClient(uuid: string, name: string, type: string, config: Record): SavedClient | null { +export function updateClient(uuid: string, name: string, type: string, config: ClientConfig): SavedClient | null { const clients = loadClients() const index = clients.findIndex(c => c.uuid === uuid) if (index === -1) return null diff --git a/server/torrent-clients/qbittorrent.ts b/server/torrent-clients/qbittorrent.ts index 33960ff..fb9ccd0 100644 --- a/server/torrent-clients/qbittorrent.ts +++ b/server/torrent-clients/qbittorrent.ts @@ -3,7 +3,8 @@ * Supports qBittorrent 4.x and 5.x (including 5.2.0+ API key authentication) */ -import type { TorrentClientDriver, TorrentInfo, DownloadOptions } from './index.js' +import type { TorrentClientDriver, TorrentInfo, DownloadOptions, ClientConfig } from './index.js' +import { clientFetch } from './index.js' import { logger } from '../logger.js' // qBittorrent répond normalement "Ok." mais certains setups (reverse proxy, etc.) @@ -27,7 +28,7 @@ function mapState(state: string): TorrentInfo['state'] { } // Retourne les headers d'authentification : Bearer (≥5.2.0) ou cookie SID -async function qbAuth(config: Record): Promise> { +async function qbAuth(config: ClientConfig): Promise> { if (config.apiKey) { return { Authorization: `Bearer ${config.apiKey}` } } @@ -35,12 +36,12 @@ async function qbAuth(config: Record): Promise): Promise { +async function qbLogin(config: ClientConfig): 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`, { + const res = await clientFetch(config, `${config.url}/api/v2/auth/login`, { method : 'POST', body : form, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, @@ -55,9 +56,9 @@ async function qbLogin(config: Record): Promise return cookie } -async function qbTorrentExists(config: Record, authHeaders: Record, hash: string): Promise { +async function qbTorrentExists(config: ClientConfig, authHeaders: Record, hash: string): Promise { try { - const res = await fetch(`${config.url}/api/v2/torrents/info?hashes=${hash}`, { + const res = await clientFetch(config, `${config.url}/api/v2/torrents/info?hashes=${hash}`, { headers: authHeaders, }) if (!res.ok) return false @@ -73,7 +74,7 @@ async function qbTorrentExists(config: Record, authHead * puis applique les priorités (désactive tout sauf la cible). */ async function qbApplyFilePriority( - config : Record, + config : ClientConfig, hash : string, fileIndex: number, ): Promise { @@ -82,7 +83,7 @@ async function qbApplyFilePriority( await new Promise(r => setTimeout(r, attempt === 0 ? 100 : 500)) try { const authH = await qbAuth(config) - const res = await fetch(`${config.url}/api/v2/torrents/files?hash=${hash}`, { + const res = await clientFetch(config, `${config.url}/api/v2/torrents/files?hash=${hash}`, { headers: authH, }) if (!res.ok) continue @@ -105,13 +106,13 @@ async function qbApplyFilePriority( const unwanted = files.map((_, i) => i).filter(i => i !== fileIndex) if (unwanted.length > 0) { const body = new URLSearchParams({ hash, id: unwanted.join('|'), priority: '0' }) - const r = await fetch(`${config.url}/api/v2/torrents/filePrio`, { method: 'POST', body, headers }) + const r = await clientFetch(config, `${config.url}/api/v2/torrents/filePrio`, { method: 'POST', body, headers }) if (!r.ok) { logger.warn('qbittorrent', `filePrio désactivation échouée (${r.status}) — retry`); continue } } } // Activer le fichier cible (dans tous les cas) - const r2 = await fetch(`${config.url}/api/v2/torrents/filePrio`, { + const r2 = await clientFetch(config, `${config.url}/api/v2/torrents/filePrio`, { method: 'POST', body : new URLSearchParams({ hash, id: String(fileIndex), priority: '1' }), headers, @@ -119,7 +120,7 @@ async function qbApplyFilePriority( if (!r2.ok) { logger.warn('qbittorrent', `filePrio activation échouée (${r2.status}) — retry`); continue } // Vérification : le fichier cible est bien à priorité > 0 - const verif = await fetch(`${config.url}/api/v2/torrents/files?hash=${hash}`, { headers: authH }) + const verif = await clientFetch(config, `${config.url}/api/v2/torrents/files?hash=${hash}`, { headers: authH }) if (verif.ok) { const verifiedFiles: any[] = await verif.json() const target = verifiedFiles[fileIndex] @@ -152,13 +153,14 @@ const QB: TorrentClientDriver = { { key: 'savePath', label: 'Dossier cible', type: 'text', placeholder: '/downloads/fankai', required: false }, { key: 'remotePath', label: 'Chemin distant (client)', type: 'text', placeholder: '/downloads', required: false }, { key: 'localPath', label: 'Chemin local (FanKarr)', type: 'text', placeholder: '/mnt/nas/downloads', required: false }, + { key: 'ignoreCertificateErrors', label: 'Ignorer les erreurs de certificat SSL', type: 'boolean', required: false }, ], }, async test(config) { try { const authH = await qbAuth(config) - const res = await fetch(`${config.url}/api/v2/app/version`, { headers: authH }) + const res = await clientFetch(config, `${config.url}/api/v2/app/version`, { headers: authH }) if (!res.ok) throw new Error(`HTTP ${res.status}`) logger.info('qbittorrent', `Test de connexion réussi sur ${config.url}`) return { ok: true, message: 'Connexion réussie' } @@ -172,7 +174,7 @@ const QB: TorrentClientDriver = { async healthcheck(config) { try { const authH = await qbAuth(config) - const res = await fetch(`${config.url}/api/v2/app/version`, { + const res = await clientFetch(config, `${config.url}/api/v2/app/version`, { headers: authH, }) if (!res.ok) return { online: false } @@ -190,7 +192,7 @@ const QB: TorrentClientDriver = { const authH = await qbAuth(config) const params = new URLSearchParams() if (category) params.set('category', category) - const res = await fetch(`${config.url}/api/v2/torrents/info?${params}`, { + const res = await clientFetch(config, `${config.url}/api/v2/torrents/info?${params}`, { headers: authH, }) if (!res.ok) throw new Error(`qB list échoué : ${res.status}`) @@ -200,7 +202,7 @@ const QB: TorrentClientDriver = { const fileMap = new Map() await Promise.all(data.map(async (t: any) => { try { - const r = await fetch(`${config.url}/api/v2/torrents/files?hash=${t.hash}`, { + const r = await clientFetch(config, `${config.url}/api/v2/torrents/files?hash=${t.hash}`, { headers: authH, }) if (r.ok) fileMap.set(t.hash, await r.json()) @@ -237,7 +239,7 @@ const QB: TorrentClientDriver = { async getFiles(config, hash) { const authH = await qbAuth(config) - const res = await fetch(`${config.url}/api/v2/torrents/files?hash=${hash}`, { + const res = await clientFetch(config, `${config.url}/api/v2/torrents/files?hash=${hash}`, { headers: authH, }) if (!res.ok) return [] @@ -275,7 +277,7 @@ const QB: TorrentClientDriver = { let knownHashes: Set = new Set() if (!hash) { try { - const lr = await fetch(`${config.url}/api/v2/torrents/info`, { headers: authH }) + const lr = await clientFetch(config, `${config.url}/api/v2/torrents/info`, { headers: authH }) if (lr.ok) knownHashes = new Set((await lr.json()).map((t: any) => String(t.hash).toLowerCase())) } catch {} } @@ -292,7 +294,7 @@ const QB: TorrentClientDriver = { params.set('urls', url) if (config.category) params.set('category', String(config.category)) if (config.savePath) params.set('savepath', String(config.savePath)) - res = await fetch(`${config.url}/api/v2/torrents/add`, { + res = await clientFetch(config, `${config.url}/api/v2/torrents/add`, { method: 'POST', body: params, headers: { ...authH, 'Content-Type': 'application/x-www-form-urlencoded' }, }) @@ -306,7 +308,7 @@ const QB: TorrentClientDriver = { if (config.category) form.append('category', String(config.category)) if (config.savePath) form.append('savepath', String(config.savePath)) form.append('torrents', new Blob([torrentBytes], { type: 'application/x-bittorrent' }), 'torrent.torrent') - res = await fetch(`${config.url}/api/v2/torrents/add`, { method: 'POST', body: form, headers: authH }) + res = await clientFetch(config, `${config.url}/api/v2/torrents/add`, { method: 'POST', body: form, headers: authH }) } const text = await res.text() if (!isQbAddSuccess(text)) throw new Error(`Ajout échoué : ${text}`) @@ -325,7 +327,7 @@ const QB: TorrentClientDriver = { await new Promise(r => setTimeout(r, 500)) try { const ah = await qbAuth(config) - const lr = await fetch(`${config.url}/api/v2/torrents/info`, { headers: ah }) + const lr = await clientFetch(config, `${config.url}/api/v2/torrents/info`, { headers: ah }) if (!lr.ok) continue const newT = (await lr.json()).find((t: any) => !knownHashes.has(String(t.hash).toLowerCase())) if (!newT) continue @@ -347,7 +349,7 @@ const QB: TorrentClientDriver = { if (config.category) params.set('category', String(config.category)) if (config.savePath) params.set('savepath', String(config.savePath)) - const res = await fetch(`${config.url}/api/v2/torrents/add`, { + const res = await clientFetch(config, `${config.url}/api/v2/torrents/add`, { method: 'POST', body: params, headers: { ...authH, 'Content-Type': 'application/x-www-form-urlencoded' }, }) @@ -359,7 +361,7 @@ const QB: TorrentClientDriver = { async remove(config, hash, deleteFiles = false) { const authH = await qbAuth(config) const params = new URLSearchParams({ hashes: hash, deleteFiles: deleteFiles ? 'true' : 'false' }) - const res = await fetch(`${config.url}/api/v2/torrents/delete`, { + const res = await clientFetch(config, `${config.url}/api/v2/torrents/delete`, { method : 'POST', body : params, headers: { ...authH, 'Content-Type': 'application/x-www-form-urlencoded' }, diff --git a/server/torrent-clients/real-debrid.ts b/server/torrent-clients/real-debrid.ts index a21cf2c..458b097 100644 --- a/server/torrent-clients/real-debrid.ts +++ b/server/torrent-clients/real-debrid.ts @@ -13,12 +13,12 @@ * (dossier où l'utilisateur télécharge ses fichiers RD via FUSE, WebDAV ou autre). */ -import type { TorrentClientDriver, TorrentInfo, TorrentFileProgress, DownloadOptions } from './index.js' +import type { TorrentClientDriver, TorrentInfo, TorrentFileProgress, DownloadOptions, ClientConfig } from './index.js' import { logger } from '../logger.js' const RD_BASE = 'https://api.real-debrid.com/rest/1.0' -function rdHeaders(config: Record) { +function rdHeaders(config: ClientConfig) { return { Authorization: `Bearer ${config.apiKey}`, 'Content-Type': 'application/x-www-form-urlencoded', @@ -26,7 +26,7 @@ function rdHeaders(config: Record) { } async function rdFetch( - config : Record, + config : ClientConfig, method : 'GET' | 'POST' | 'DELETE', path : string, body ?: URLSearchParams, diff --git a/server/torrent-clients/rtorrent.ts b/server/torrent-clients/rtorrent.ts index d3cc694..5a98137 100644 --- a/server/torrent-clients/rtorrent.ts +++ b/server/torrent-clients/rtorrent.ts @@ -5,7 +5,8 @@ * Compatible ruTorrent (via /RPC2) et rTorrent standalone (via /RPC2 ou /XMLRPC). */ -import type { TorrentClientDriver, TorrentInfo, DownloadOptions } from './index.js' +import type { TorrentClientDriver, TorrentInfo, DownloadOptions, ClientConfig } from './index.js' +import { clientFetch } from './index.js' import { logger } from '../logger.js' // ─── XML-RPC helpers ────────────────────────────────────────── @@ -55,7 +56,7 @@ function parseXmlValue(node: Element): any { } async function rpcCall( - config : Record, + config : ClientConfig, method : string, params : any[] = [], ): Promise { @@ -69,7 +70,7 @@ async function rpcCall( const rpcPath = String(config.rpcPath || '/RPC2') const url = `${String(config.url).replace(/\/+$/, '')}${rpcPath}` - const res = await fetch(url, { method: 'POST', headers, body }) + const res = await clientFetch(config, url, { method: 'POST', headers, body }) if (!res.ok) throw new Error(`HTTP ${res.status}`) const text = await res.text() @@ -109,7 +110,7 @@ function parseInnerValue(inner: string): any { // multicall pour récupérer plusieurs champs en une requête async function d_multicall( - config : Record, + config : ClientConfig, view : string, methods: string[], ): Promise { @@ -118,7 +119,7 @@ async function d_multicall( return Array.isArray(result) ? result : [] } -async function rtTorrentExists(config: Record, hash: string): Promise { +async function rtTorrentExists(config: ClientConfig, hash: string): Promise { try { const rows = await d_multicall(config, 'main', ['d.hash']) return rows.some(r => String(r[0]).toLowerCase() === hash.toLowerCase()) @@ -134,7 +135,7 @@ async function rtTorrentExists(config: Record, hash: st * On set les priorités dès que les fichiers sont disponibles, avant que le download réel commence. */ async function rtApplyFilePriority( - config : Record, + config : ClientConfig, hash : string, fileIndex: number, ): Promise { @@ -192,6 +193,7 @@ const RT: TorrentClientDriver = { { key: 'savePath', label: 'Dossier cible', type: 'text', placeholder: '/downloads/fankai', required: false }, { key: 'remotePath', label: 'Chemin distant (client)', type: 'text', placeholder: '/downloads', required: false }, { key: 'localPath', label: 'Chemin local (FanKarr)', type: 'text', placeholder: '/mnt/nas/downloads', required: false }, + { key: 'ignoreCertificateErrors', label: 'Ignorer les erreurs de certificat SSL', type: 'boolean', required: false }, ], }, diff --git a/server/torrent-clients/synology-ds.ts b/server/torrent-clients/synology-ds.ts index 9aec56d..3a3f785 100644 --- a/server/torrent-clients/synology-ds.ts +++ b/server/torrent-clients/synology-ds.ts @@ -4,7 +4,8 @@ import fs from 'fs' import path from 'path' -import type { TorrentClientDriver, TorrentInfo, DownloadOptions } from './index.js' +import type { TorrentClientDriver, TorrentInfo, DownloadOptions, ClientConfig } from './index.js' +import { clientFetch } from './index.js' import { logger } from '../logger.js' import { DATA_DIR } from '../config.js' @@ -83,7 +84,7 @@ function mapState(status: string): TorrentInfo['state'] { // ─── Auth ────────────────────────────────────────────────────────────────────── -async function dsLogin(config: Record): Promise { +async function dsLogin(config: ClientConfig): Promise { const params = new URLSearchParams({ api : 'SYNO.API.Auth', version: '3', @@ -93,7 +94,7 @@ async function dsLogin(config: Record): Promise session: 'DownloadStation', format : 'sid', }) - const res = await fetch(`${config.url}/webapi/auth.cgi?${params}`) + const res = await clientFetch(config, `${config.url}/webapi/auth.cgi?${params}`) if (!res.ok) throw new Error(`HTTP ${res.status}`) const data = await res.json() if (!data.success) throw new Error(`Auth échouée : ${JSON.stringify(data.error)}`) @@ -101,7 +102,7 @@ async function dsLogin(config: Record): Promise } async function dsRequest( - config : Record, + config : ClientConfig, api : string, method : string, version: string, @@ -109,7 +110,7 @@ async function dsRequest( sid : string, ): Promise { const params = new URLSearchParams({ api, version, method, _sid: sid, ...extra }) - const res = await fetch(`${config.url}/webapi/DownloadStation/task.cgi?${params}`) + const res = await clientFetch(config, `${config.url}/webapi/DownloadStation/task.cgi?${params}`) if (!res.ok) throw new Error(`HTTP ${res.status}`) const data = await res.json() if (!data.success) throw new Error(`DS erreur : ${JSON.stringify(data.error)}`) @@ -131,6 +132,7 @@ const DS: TorrentClientDriver = { { key: 'savePath', label: 'Dossier cible', type: 'text', placeholder: '/volume1/downloads/fankai', required: false }, { key: 'remotePath', label: 'Chemin distant (client)', type: 'text', placeholder: '/volume1/downloads', required: false }, { key: 'localPath', label: 'Chemin local (FanKarr)', type: 'text', placeholder: '/mnt/nas/downloads', required: false }, + { key: 'ignoreCertificateErrors', label: 'Ignorer les erreurs de certificat SSL', type: 'boolean', required: false }, ], }, @@ -155,7 +157,7 @@ const DS: TorrentClientDriver = { method : 'getinfo', _sid : sid, }) - const res = await fetch(`${config.url}/webapi/DownloadStation/info.cgi?${params}`) + const res = await clientFetch(config, `${config.url}/webapi/DownloadStation/info.cgi?${params}`) if (!res.ok) return { online: false } const data = await res.json() if (!data.success) return { online: false } @@ -262,7 +264,7 @@ const DS: TorrentClientDriver = { uri : url, }) if (withDestination && config.savePath) params.append('destination', String(config.savePath)) - const res = await fetch(`${config.url}/webapi/DownloadStation/task.cgi?${params}`) + const res = await clientFetch(config, `${config.url}/webapi/DownloadStation/task.cgi?${params}`) if (!res.ok) throw new Error(`HTTP ${res.status}`) return res.json() } @@ -355,7 +357,7 @@ const DS: TorrentClientDriver = { force_complete: 'false', _sid : sid, }) - const res = await fetch(`${config.url}/webapi/DownloadStation/task.cgi?${params}`) + const res = await clientFetch(config, `${config.url}/webapi/DownloadStation/task.cgi?${params}`) if (!res.ok) throw new Error(`HTTP ${res.status}`) logger.info('synology-ds', `Torrent ${found.id} supprimé`) }, diff --git a/server/torrent-clients/transmission.ts b/server/torrent-clients/transmission.ts index 4588abe..1aa2e28 100644 --- a/server/torrent-clients/transmission.ts +++ b/server/torrent-clients/transmission.ts @@ -2,7 +2,8 @@ * Transmission Driver */ -import type { TorrentClientDriver, TorrentInfo, DownloadOptions } from './index.js' +import type { TorrentClientDriver, TorrentInfo, DownloadOptions, ClientConfig } from './index.js' +import { clientFetch } from './index.js' import { logger } from '../logger.js' function mapState(status: number): TorrentInfo['state'] { @@ -17,7 +18,7 @@ function mapState(status: number): TorrentInfo['state'] { } async function trRequest( - config: Record, + config: ClientConfig, method: string, args: Record = {}, sessionId = '' @@ -31,7 +32,7 @@ async function trRequest( headers['Authorization'] = 'Basic ' + btoa(`${config.username}:${config.password}`) } - const res = await fetch(`${config.url}/transmission/rpc`, { + const res = await clientFetch(config, `${config.url}/transmission/rpc`, { method : 'POST', headers, body : JSON.stringify({ method, arguments: args }), @@ -52,7 +53,7 @@ async function trRequest( return { ...data, sessionId } } -async function trGetIdByHash(config: Record, hash: string): Promise { +async function trGetIdByHash(config: ClientConfig, hash: string): Promise { const data = await trRequest(config, 'torrent-get', { fields: ['hashString', 'id'] }) const torrents = data.arguments?.torrents ?? [] return torrents.find((t: any) => t.hashString?.toLowerCase() === hash.toLowerCase())?.id ?? null @@ -63,7 +64,7 @@ async function trGetIdByHash(config: Record, hash: stri * puis applique les priorités : fichier cible voulu, tous les autres ignorés. */ async function trApplyFilePriority( - config : Record, + config : ClientConfig, hash : string, fileIndex: number, ): Promise { @@ -102,6 +103,7 @@ const TR: TorrentClientDriver = { { key: 'savePath', label: 'Dossier cible', type: 'text', placeholder: '/downloads/fankai', required: false }, { key: 'remotePath', label: 'Chemin distant (client)', type: 'text', placeholder: '/downloads', required: false }, { key: 'localPath', label: 'Chemin local (FanKarr)', type: 'text', placeholder: '/mnt/nas/downloads', required: false }, + { key: 'ignoreCertificateErrors', label: 'Ignorer les erreurs de certificat SSL', type: 'boolean', required: false }, ], }, diff --git a/server/torrent-clients/utorrent.ts b/server/torrent-clients/utorrent.ts index 0fb628c..2e9fc44 100644 --- a/server/torrent-clients/utorrent.ts +++ b/server/torrent-clients/utorrent.ts @@ -2,7 +2,8 @@ * uTorrent Driver */ -import type { TorrentClientDriver, TorrentInfo, DownloadOptions } from './index.js' +import type { TorrentClientDriver, TorrentInfo, DownloadOptions, ClientConfig } from './index.js' +import { clientFetch } from './index.js' import { logger } from '../logger.js' // uTorrent status flags (bitmask) : @@ -21,7 +22,7 @@ function mapState(status: number): TorrentInfo['state'] { // uTorrent nécessite un token CSRF + cookie à chaque session interface UTSession { token: string; cookie: string } -async function utTorrentExists(config: Record, hash: string): Promise { +async function utTorrentExists(config: ClientConfig, hash: string): Promise { try { const session = await utGetSession(config) const data = await utRequest(config, { action: 'getfiles', hash: hash.toUpperCase() }, session) @@ -35,7 +36,7 @@ async function utTorrentExists(config: Record, hash: st * tous les fichiers à 0 (skip), sauf le fichier cible à 2 (normal). */ async function utApplyFilePriority( - config : Record, + config : ClientConfig, hash : string, fileIndex: number, ): Promise { @@ -54,13 +55,13 @@ async function utApplyFilePriority( // Passer tous les fichiers à priorité 0 (skip) const skipQs = new URLSearchParams({ token: session.token, action: 'setprio', hash: HASH, p: '0' }) for (let i = 0; i < files.length; i++) skipQs.append('f', String(i)) - await fetch(`${config.url}/gui/?${skipQs}`, { + await clientFetch(config, `${config.url}/gui/?${skipQs}`, { headers: { Authorization: `Basic ${auth}`, Cookie: session.cookie }, }) // Activer le fichier cible (priorité 2 = normal) const selectQs = new URLSearchParams({ token: session.token, action: 'setprio', hash: HASH, p: '2', f: String(fileIndex) }) - await fetch(`${config.url}/gui/?${selectQs}`, { + await clientFetch(config, `${config.url}/gui/?${selectQs}`, { headers: { Authorization: `Basic ${auth}`, Cookie: session.cookie }, }) @@ -71,13 +72,13 @@ async function utApplyFilePriority( throw new Error(`Timeout : fichiers non disponibles pour ${hash.slice(0, 8)}…`) } -async function utGetSession(config: Record): Promise { +async function utGetSession(config: ClientConfig): Promise { const auth = btoa(`${config.username ?? ''}:${config.password ?? ''}`) const headers : Record = { 'Authorization': `Basic ${auth}`, } - const res = await fetch(`${config.url}/gui/token.html`, { headers }) + const res = await clientFetch(config, `${config.url}/gui/token.html`, { headers }) if (!res.ok) throw new Error(`HTTP ${res.status} — token introuvable`) const text = await res.text() @@ -90,14 +91,14 @@ async function utGetSession(config: Record): Promise, + config : ClientConfig, params : Record, session: UTSession, ): Promise { const auth = btoa(`${config.username ?? ''}:${config.password ?? ''}`) const qs = new URLSearchParams({ token: session.token, ...params }) - const res = await fetch(`${config.url}/gui/?${qs}`, { + const res = await clientFetch(config, `${config.url}/gui/?${qs}`, { headers: { 'Authorization': `Basic ${auth}`, 'Cookie' : session.cookie, @@ -142,6 +143,7 @@ const UT: TorrentClientDriver = { { key: 'savePath', label: 'Dossier cible', type: 'text', placeholder: '/downloads/fankai', required: false }, { key: 'remotePath', label: 'Chemin distant (client)', type: 'text', placeholder: '/downloads', required: false }, { key: 'localPath', label: 'Chemin local (FanKarr)', type: 'text', placeholder: '/mnt/nas/downloads', required: false }, + { key: 'ignoreCertificateErrors', label: 'Ignorer les erreurs de certificat SSL', type: 'boolean', required: false }, ], }, @@ -213,7 +215,7 @@ const UT: TorrentClientDriver = { const qs = new URLSearchParams({ token: session.token, action: 'add-url', s: url }) if (config.savePath) qs.set('path', String(config.savePath)) - const res = await fetch(`${config.url}/gui/?${qs}`, { + const res = await clientFetch(config, `${config.url}/gui/?${qs}`, { method : 'GET', headers: { Authorization: `Basic ${auth}`, Cookie: session.cookie }, }) diff --git a/src/views/settings/DownloadClientView.vue b/src/views/settings/DownloadClientView.vue index cc71ad7..b6e6602 100644 --- a/src/views/settings/DownloadClientView.vue +++ b/src/views/settings/DownloadClientView.vue @@ -138,16 +138,25 @@