diff --git a/server/torrent-clients/qbittorrent.ts b/server/torrent-clients/qbittorrent.ts index 441f6b5..7c8f414 100644 --- a/server/torrent-clients/qbittorrent.ts +++ b/server/torrent-clients/qbittorrent.ts @@ -48,9 +48,9 @@ async function qbTorrentExists(config: Record, sid: str } /** - * Attend les métadonnées, applique les priorités de fichiers, puis reprend le torrent si `resume=true`. - * - resume=false : torrent déjà actif, on met juste à jour la priorité - * - resume=true : torrent ajouté en pause, on le reprend après avoir défini les priorités + * Attend que les métadonnées soient disponibles, applique les priorités, puis reprend le torrent. + * Utilisé après un ajout avec stopCondition=MetadataReceived (qBit actif → auto-pause après metadata) + * ou pour un torrent déjà présent (resume=false, juste mise à jour de priorité). */ async function qbApplyFilePriority( config : Record, @@ -58,8 +58,9 @@ async function qbApplyFilePriority( fileIndex: number, resume : boolean, ): Promise { - for (let attempt = 0; attempt < 30; attempt++) { - await new Promise(r => setTimeout(r, 1000)) + // 60 tentatives × 500 ms = 30 s max + for (let attempt = 0; attempt < 60; attempt++) { + await new Promise(r => setTimeout(r, 500)) try { const sid = await qbLogin(config) const res = await fetch(`${config.url}/api/v2/torrents/files?hash=${hash}`, { @@ -86,7 +87,7 @@ async function qbApplyFilePriority( form2.append('priority', '1') await fetch(`${config.url}/api/v2/torrents/filePrio`, { method: 'POST', body: form2, headers: { Cookie: `SID=${sid}` } }) - // Reprendre si le torrent était en pause + // Reprendre (torrent auto-mis en pause par stopCondition=MetadataReceived) if (resume) { const resumeForm = new FormData() resumeForm.append('hashes', hash) @@ -98,7 +99,7 @@ async function qbApplyFilePriority( } catch {} } - // Timeout : reprendre quand même pour ne pas laisser le torrent bloqué en pause + // Timeout : reprendre quand même pour ne pas laisser le torrent bloqué if (resume) { try { const sid = await qbLogin(config) @@ -198,13 +199,15 @@ const QB: TorrentClientDriver = { return } - // Nouveau torrent → ajout en pause pour éviter tout téléchargement avant la sélection + // Nouveau torrent → ajout actif avec stopCondition=MetadataReceived + // qBit fetch les métadonnées normalement via DHT/peers, + // puis se met en pause automatiquement dès qu'elles sont disponibles. + // On set les priorités sur la pause, puis on reprend. Zéro data indésirable. 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)) - form.append('paused', 'true') // qBit < 5.0 - form.append('stopped', 'true') // qBit ≥ 5.0 + form.append('stopCondition', 'MetadataReceived') // qBit ≥ 4.6 const res = await fetch(`${config.url}/api/v2/torrents/add`, { method: 'POST', body: form, headers: { Cookie: `SID=${sid}` }, @@ -212,7 +215,7 @@ const QB: TorrentClientDriver = { const text = await res.text() if (text !== 'Ok.') throw new Error(`Ajout échoué : ${text}`) - logger.info('qbittorrent', `Torrent ajouté en pause (sélection fichier ${options.file_index} en attente de métadonnées)`) + logger.info('qbittorrent', `Torrent ajouté (stopCondition=MetadataReceived, sélection fichier ${options.file_index} en attente)`) qbApplyFilePriority(config, hash, options.file_index, true).catch(err => logger.warn('qbittorrent', `Priorité fichier non appliquée : ${err instanceof Error ? err.message : err}`) ) diff --git a/server/torrent-clients/rtorrent.ts b/server/torrent-clients/rtorrent.ts index 8547b7e..310024c 100644 --- a/server/torrent-clients/rtorrent.ts +++ b/server/torrent-clients/rtorrent.ts @@ -128,18 +128,19 @@ async function rtTorrentExists(config: Record, hash: st } /** - * Attend les métadonnées, applique les priorités, puis démarre le torrent si `start=true`. - * - start=false : torrent déjà actif, on met juste à jour la priorité - * - start=true : torrent chargé sans démarrer (load.normal), on le démarre après les priorités + * Attend les métadonnées via polling rapide (300 ms), applique les priorités. + * Le torrent est ajouté avec load.start — il fetch les métadonnées activement via DHT/peers. + * Pendant le fetch (état "métadonnées en cours"), aucun byte de fichier n'est téléchargé. + * On set les priorités dès que les fichiers sont disponibles, avant que le download réel commence. */ async function rtApplyFilePriority( config : Record, hash : string, fileIndex: number, - start : boolean, ): Promise { - for (let attempt = 0; attempt < 30; attempt++) { - await new Promise(r => setTimeout(r, 1000)) + // 100 tentatives × 300 ms = 30 s max + for (let attempt = 0; attempt < 100; attempt++) { + await new Promise(r => setTimeout(r, 300)) try { const files: any[] = await rpcCall(config, 'f.multicall', [hash, '', 'f.size_bytes=']) const fileCount = Array.isArray(files) ? files.length : 0 @@ -149,22 +150,12 @@ async function rtApplyFilePriority( await rpcCall(config, 'f.priority.set', [hash, i, i === fileIndex ? 1 : 0]) } - if (start) { - await rpcCall(config, 'd.start', [hash]) - } - - logger.info('rtorrent', `Fichier ${fileIndex} sélectionné${start ? ' + démarrage' : ''} pour ${hash.slice(0, 8)}…`) + logger.info('rtorrent', `Fichier ${fileIndex} sélectionné pour ${hash.slice(0, 8)}…`) return } catch {} } - // Timeout : démarrer quand même pour ne pas laisser le torrent bloqué - if (start) { - try { await rpcCall(config, 'd.start', [hash]) } catch {} - logger.warn('rtorrent', `Timeout : démarré sans sélection de fichier pour ${hash.slice(0, 8)}…`) - } else { - throw new Error(`Timeout : métadonnées non disponibles pour ${hash.slice(0, 8)}…`) - } + throw new Error(`Timeout : métadonnées non disponibles pour ${hash.slice(0, 8)}…`) } // ─── State mapping ──────────────────────────────────────────── @@ -279,20 +270,22 @@ const RT: TorrentClientDriver = { if (exists) { // Torrent déjà présent → mettre à jour la priorité directement (async OK) logger.info('rtorrent', `Torrent ${hash.slice(0, 8)}… déjà présent, mise à jour priorité fichier ${options.file_index}`) - rtApplyFilePriority(config, hash, options.file_index, false).catch(err => + rtApplyFilePriority(config, hash, options.file_index).catch(err => logger.warn('rtorrent', `Priorité fichier non appliquée : ${err instanceof Error ? err.message : err}`) ) return } - // Nouveau torrent → charger sans démarrer pour éviter tout téléchargement avant la sélection + // Nouveau torrent → load.start (fetch métadonnées via DHT/peers actif) + // Pendant le fetch des métadonnées, aucun byte de fichier n'est téléchargé. + // On poll à 300 ms et on set les priorités dès que les fichiers sont disponibles. const args: any[] = ['', url] if (config.savePath) args.push(`d.directory.set="${config.savePath}"`) if (config.category) args.push(`d.custom1.set=${config.category}`) - await rpcCall(config, 'load.normal', args) + await rpcCall(config, 'load.start', args) - logger.info('rtorrent', `Torrent chargé sans démarrage (sélection fichier ${options.file_index} en attente de métadonnées)`) - rtApplyFilePriority(config, hash, options.file_index, true).catch(err => + logger.info('rtorrent', `Torrent ajouté (sélection fichier ${options.file_index} en attente de métadonnées)`) + rtApplyFilePriority(config, hash, options.file_index).catch(err => logger.warn('rtorrent', `Priorité fichier non appliquée : ${err instanceof Error ? err.message : err}`) ) return diff --git a/src/views/settings/DownloadClientView.vue b/src/views/settings/DownloadClientView.vue index 9177678..4e49256 100644 --- a/src/views/settings/DownloadClientView.vue +++ b/src/views/settings/DownloadClientView.vue @@ -78,8 +78,8 @@

- ✓ Natif — appliqué à l'ajout, aucun fichier superflu téléchargé · - Async (pause) — ajout en pause, sélection après métadonnées, puis reprise · + ✓ Natif — sélection à l'ajout, aucun fichier superflu · + Async ✓ — sélection dès réception des métadonnées, avant tout téléchargement · ✗ — non supporté, torrent complet téléchargé

@@ -163,11 +163,11 @@ const availableClients = ref([]) const healthStatus = ref>({}) const clientMatrix = [ - { id: 'qbittorrent', label: 'qBittorrent', fileSelect: 'Async (pause)', categories: true }, - { id: 'transmission', label: 'Transmission', fileSelect: '✓ Natif', categories: true }, - { id: 'rtorrent', label: 'rTorrent / ruTorrent', fileSelect: 'Async (pause)', categories: true }, - { id: 'utorrent', label: 'uTorrent', fileSelect: '✗', categories: true }, - { id: 'synology-ds', label: 'Synology Download Station', fileSelect: '✗', categories: false }, + { id: 'qbittorrent', label: 'qBittorrent', fileSelect: 'Async ✓', categories: true }, + { id: 'transmission', label: 'Transmission', fileSelect: '✓ Natif', categories: true }, + { id: 'rtorrent', label: 'rTorrent / ruTorrent', fileSelect: 'Async ✓', categories: true }, + { id: 'utorrent', label: 'uTorrent', fileSelect: '✗', categories: true }, + { id: 'synology-ds', label: 'Synology Download Station', fileSelect: '✗', categories: false }, ] const modal = ref({