fix: les torrents sont stopé lors de la selection de fichier pour eviter qu'il commence le telechargement des autres fichiers avant de set les indexs

This commit is contained in:
MasutaYunikon committed 2026-04-21 15:22:06 +02:00
1 parent ed66c2c0a2
commit f19cc027ee
4 files changed
+206 -74

No files matched your search

+95 -23
View File
@@ -8,7 +8,7 @@ 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 (['pausedDL', 'pausedUP', 'stoppedDL', 'stoppedUP'].includes(state)) return 'paused'
if (['checkingDL', 'checkingUP', 'checkingResumeData'].includes(state)) return 'checking'
if (state === 'error' || state === 'missingFiles') return 'error'
return 'unknown'
@@ -34,9 +34,32 @@ async function qbLogin(config: Record<string, string | number>): Promise<string>
return sid
}
async function qbSetFilePriority(config: Record<string, string | number>, hash: string, fileIndex: number): Promise<void> {
for (let attempt = 0; attempt < 15; attempt++) {
await new Promise(r => setTimeout(r, 2000))
async function qbTorrentExists(config: Record<string, string | number>, sid: string, hash: string): Promise<boolean> {
try {
const res = await fetch(`${config.url}/api/v2/torrents/info?hashes=${hash}`, {
headers: { Cookie: `SID=${sid}` },
})
if (!res.ok) return false
const list = await res.json()
return Array.isArray(list) && list.length > 0
} catch {
return false
}
}
/**
* 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
*/
async function qbApplyFilePriority(
config : Record<string, string | number>,
hash : string,
fileIndex: number,
resume : boolean,
): Promise<void> {
for (let attempt = 0; attempt < 30; attempt++) {
await new Promise(r => setTimeout(r, 1000))
try {
const sid = await qbLogin(config)
const res = await fetch(`${config.url}/api/v2/torrents/files?hash=${hash}`, {
@@ -45,6 +68,8 @@ async function qbSetFilePriority(config: Record<string, string | number>, hash:
if (!res.ok) continue
const files: any[] = await res.json()
if (!Array.isArray(files) || files.length === 0) continue
// Désactiver tous les autres fichiers
const unwanted = files.map((_, i) => i).filter(i => i !== fileIndex)
if (unwanted.length > 0) {
const form = new FormData()
@@ -53,15 +78,36 @@ async function qbSetFilePriority(config: Record<string, string | number>, hash:
form.append('priority', '0')
await fetch(`${config.url}/api/v2/torrents/filePrio`, { method: 'POST', body: form, headers: { Cookie: `SID=${sid}` } })
}
// Activer le fichier cible
const form2 = new FormData()
form2.append('hash', hash)
form2.append('id', String(fileIndex))
form2.append('priority', '1')
await fetch(`${config.url}/api/v2/torrents/filePrio`, { method: 'POST', body: form2, headers: { Cookie: `SID=${sid}` } })
logger.info('qbittorrent', `Priorité fichier ${fileIndex} définie pour ${hash.slice(0, 8)}…`)
// Reprendre si le torrent était en pause
if (resume) {
const resumeForm = new FormData()
resumeForm.append('hashes', hash)
await fetch(`${config.url}/api/v2/torrents/resume`, { method: 'POST', body: resumeForm, headers: { Cookie: `SID=${sid}` } })
}
logger.info('qbittorrent', `Fichier ${fileIndex} sélectionné${resume ? ' + reprise' : ''} pour ${hash.slice(0, 8)}…`)
return
} catch {}
}
// Timeout : reprendre quand même pour ne pas laisser le torrent bloqué en pause
if (resume) {
try {
const sid = await qbLogin(config)
const resumeForm = new FormData()
resumeForm.append('hashes', hash)
await fetch(`${config.url}/api/v2/torrents/resume`, { method: 'POST', body: resumeForm, headers: { Cookie: `SID=${sid}` } })
} catch {}
}
throw new Error(`Timeout : métadonnées non disponibles pour ${hash.slice(0, 8)}…`)
}
@@ -135,30 +181,56 @@ const QB: TorrentClientDriver = {
},
async add(config, url, options?: DownloadOptions) {
const sid = await qbLogin(config)
const sid = await qbLogin(config)
const hashMatch = url.match(/xt=urn:btih:([a-fA-F0-9]{40,})/i)
const hash = hashMatch?.[1]?.toLowerCase() ?? null
if (options?.file_index != null && hash) {
const exists = await qbTorrentExists(config, sid, hash)
if (exists) {
// Torrent déjà présent → mettre à jour la priorité directement (async OK)
logger.info('qbittorrent', `Torrent ${hash.slice(0, 8)}… déjà présent, mise à jour priorité fichier ${options.file_index}`)
qbApplyFilePriority(config, hash, options.file_index, false).catch(err =>
logger.warn('qbittorrent', `Priorité fichier non appliquée : ${err instanceof Error ? err.message : err}`)
)
return
}
// Nouveau torrent → ajout en pause pour éviter tout téléchargement avant la sélection
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
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é en pause (sélection fichier ${options.file_index} en attente de métadonnées)`)
qbApplyFilePriority(config, hash, options.file_index, true).catch(err =>
logger.warn('qbittorrent', `Priorité fichier non appliquée : ${err instanceof Error ? err.message : err}`)
)
return
}
// Pas de sélection de fichier → ajout normal
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 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'}${config.savePath ? `, dossier: ${config.savePath}` : ''})`)
if (options?.file_index != null) {
const hashMatch = url.match(/xt=urn:btih:([a-fA-F0-9]{40,})/i)
const hash = hashMatch?.[1]?.toLowerCase() ?? null
if (hash) {
qbSetFilePriority(config, hash, options.file_index).catch(err =>
logger.warn('qbittorrent', `Priorité fichier non appliquée : ${err instanceof Error ? err.message : err}`)
)
} else {
logger.warn('qbittorrent', `file_index ignoré : hash non extractible depuis l'URL`)
}
}
logger.info('qbittorrent', `Torrent ajouté (catégorie: ${config.category ?? 'aucune'}${config.savePath ? `, dossier: ${config.savePath}` : ''})`)
},
async remove(config, hash, deleteFiles = false) {
@@ -176,4 +248,4 @@ const QB: TorrentClientDriver = {
},
}
export default QB
export default QB
+80 -34
View File
@@ -118,6 +118,55 @@ async function d_multicall(
return Array.isArray(result) ? result : []
}
async function rtTorrentExists(config: Record<string, string | number>, hash: string): Promise<boolean> {
try {
const rows = await d_multicall(config, 'main', ['d.hash'])
return rows.some(r => String(r[0]).toLowerCase() === hash.toLowerCase())
} catch {
return false
}
}
/**
* 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
*/
async function rtApplyFilePriority(
config : Record<string, string | number>,
hash : string,
fileIndex: number,
start : boolean,
): Promise<void> {
for (let attempt = 0; attempt < 30; attempt++) {
await new Promise(r => setTimeout(r, 1000))
try {
const files: any[] = await rpcCall(config, 'f.multicall', [hash, '', 'f.size_bytes='])
const fileCount = Array.isArray(files) ? files.length : 0
if (fileCount === 0) continue
for (let i = 0; i < fileCount; i++) {
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)}…`)
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)}…`)
}
}
// ─── State mapping ────────────────────────────────────────────
// rTorrent state : is_active (0/1) + is_hash_checking (0/1) + get_complete (0/1)
// + is_open (0/1)
@@ -221,43 +270,40 @@ const RT: TorrentClientDriver = {
},
async add(config, url, options?: DownloadOptions) {
// load_start charge et démarre le torrent
const method = url.startsWith('magnet:') ? 'load.start' : 'load.start'
const args: any[] = ['', url]
const hashMatch = url.match(/xt=urn:btih:([a-fA-F0-9]{40,})/i)
const hash = hashMatch?.[1]?.toUpperCase() ?? null
if (config.savePath) {
args.push(`d.directory.set="${config.savePath}"`)
}
if (options?.file_index != null && hash) {
const exists = await rtTorrentExists(config, hash)
if (config.category) {
args.push(`d.custom1.set=${config.category}`)
}
await rpcCall(config, method, args)
if (options?.file_index != null) {
// Wait for torrent metadata then set file priorities
const hashMatch = url.match(/xt=urn:btih:([a-fA-F0-9]{40,})/i)
const hash = hashMatch?.[1]?.toUpperCase() ?? null
if (hash) {
;(async () => {
for (let attempt = 0; attempt < 15; attempt++) {
await new Promise(r => setTimeout(r, 2000))
try {
const fileCount: number = await rpcCall(config, 'f.multicall', [hash, '', 'f.size_bytes='])
.then((r: any) => Array.isArray(r) ? r.length : 0)
if (fileCount === 0) continue
for (let i = 0; i < fileCount; i++) {
const priority = i === options.file_index ? 1 : 0
await rpcCall(config, 'f.priority.set', [hash, i, priority])
}
logger.info('rtorrent', `Priorité fichier ${options.file_index} définie pour ${hash.slice(0, 8)}…`)
return
} catch {}
}
logger.warn('rtorrent', `Priorité fichier non appliquée : timeout pour ${hash.slice(0, 8)}…`)
})()
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 =>
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
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)
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.warn('rtorrent', `Priorité fichier non appliquée : ${err instanceof Error ? err.message : err}`)
)
return
}
// Pas de sélection de fichier → ajout et démarrage normal
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.start', args)
logger.info('rtorrent', `Torrent ajouté avec succès${config.savePath ? ` (dossier: ${config.savePath})` : ''}`)
},
@@ -274,4 +320,4 @@ const RT: TorrentClientDriver = {
},
}
export default RT
export default RT
+24 -10
View File
@@ -125,20 +125,34 @@ const TR: TorrentClientDriver = {
async add(config, url, options?: DownloadOptions) {
const args: Record<string, unknown> = { filename: url }
if (config.savePath) {
args['download-dir'] = String(config.savePath)
}
if (config.category) {
args['labels'] = [String(config.category)]
}
if (config.savePath) args['download-dir'] = String(config.savePath)
if (config.category) args['labels'] = [String(config.category)]
// Transmission applique files-wanted après récupération des métadonnées,
// sans démarrer les autres fichiers entre-temps — natif et fiable.
if (options?.file_index != null) {
args['files-wanted'] = [options.file_index]
}
await trRequest(config, 'torrent-add', args)
logger.info('transmission', `Torrent ajouté avec succès (catégorie: ${config.category ?? 'aucune'}${config.savePath ? `, dossier: ${config.savePath}` : ''})`)
const res = await trRequest(config, 'torrent-add', args)
// Torrent déjà présent → Transmission renvoie torrent-duplicate
// On s'assure quand même que le fichier voulu est activé
if (options?.file_index != null && res.arguments?.['torrent-duplicate']) {
const dup = res.arguments['torrent-duplicate']
try {
await trRequest(config, 'torrent-set', {
ids : [dup.id],
'files-wanted': [options.file_index],
})
logger.info('transmission', `Torrent ${dup.hashString?.slice(0, 8)}… déjà présent, fichier ${options.file_index} activé`)
} catch (err) {
logger.warn('transmission', `Activation fichier sur torrent existant échouée : ${err instanceof Error ? err.message : err}`)
}
return
}
logger.info('transmission', `Torrent ajouté (catégorie: ${config.category ?? 'aucune'}${config.savePath ? `, dossier: ${config.savePath}` : ''})`)
},
async remove(config, hash, deleteFiles = false) {
@@ -152,4 +166,4 @@ const TR: TorrentClientDriver = {
},
}
export default TR
export default TR
+7 -7
View File
@@ -78,8 +78,8 @@
</table>
</div>
<p class="text-[11px] text-muted mt-2">
<span class="text-green-400">✓ Natif</span> — appliqué à l'ajout ·
<span class="text-yellow-500">Async</span> — appliqué après récupération des métadonnées ·
<span class="text-green-400">✓ Natif</span> — appliqué à l'ajout, aucun fichier superflu téléchargé ·
<span class="text-yellow-500">Async (pause)</span> — ajout en pause, sélection après métadonnées, puis reprise ·
<span class="text-muted">✗</span> — non supporté, torrent complet téléchargé
</p>
</div>
@@ -163,11 +163,11 @@ const availableClients = ref<any[]>([])
const healthStatus = ref<Record<string, boolean | null>>({})
const clientMatrix = [
{ 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 },
{ 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 },
]
const modal = ref({