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:
1 parent
ed66c2c0a2
commit
f19cc027ee
4 files changed
+206
-74
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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({
|
||||
|
||||
Reference in new issue
Block a user