From 7e53bab0fccee5219d0b89f0ff6d0b898efcf3ef Mon Sep 17 00:00:00 2001 From: MasutaYunikon Date: Tue, 21 Apr 2026 15:53:39 +0200 Subject: [PATCH] feat: utilisation des bar de telechargement par fichier dans le torrent plutot que celui du torrent --- server/torrent-clients/index.ts | 6 +++ server/torrent-clients/qbittorrent.ts | 48 ++++++++++++++++-------- server/torrent-clients/rtorrent.ts | 24 ++++++++++-- server/torrent-clients/transmission.ts | 7 ++++ src/components/serie/SerieSeasonCard.vue | 24 +++++++++--- src/views/SerieView.vue | 4 +- 6 files changed, 87 insertions(+), 26 deletions(-) diff --git a/server/torrent-clients/index.ts b/server/torrent-clients/index.ts index 4627e4d..67c1bf4 100644 --- a/server/torrent-clients/index.ts +++ b/server/torrent-clients/index.ts @@ -34,6 +34,11 @@ export interface SavedClient { config: Record } +export interface TorrentFileProgress { + index : number + progress: number // 0–100 +} + export interface TorrentInfo { hash : string name : string @@ -48,6 +53,7 @@ export interface TorrentInfo { eta : number save_path : string category : string + files ?: TorrentFileProgress[] } export interface DownloadOptions { diff --git a/server/torrent-clients/qbittorrent.ts b/server/torrent-clients/qbittorrent.ts index c710936..f2f20b8 100644 --- a/server/torrent-clients/qbittorrent.ts +++ b/server/torrent-clients/qbittorrent.ts @@ -171,21 +171,39 @@ const QB: TorrentClientDriver = { }) 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, - uploaded : t.uploaded ?? 0, - ratio : Math.round((t.ratio ?? 0) * 100) / 100, - speed : t.dlspeed, - upspeed : t.upspeed ?? 0, - eta : t.eta ?? -1, - save_path : t.save_path, - category : t.category ?? '', - } satisfies TorrentInfo)) + + // Récupérer la progression par fichier pour chaque torrent (requêtes parallèles) + 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}`, { + headers: { Cookie: `SID=${sid}` }, + }) + if (r.ok) fileMap.set(t.hash, await r.json()) + } catch {} + })) + + return data.map(t => { + const fileInfo = fileMap.get(t.hash) ?? [] + return { + hash : t.hash, + name : t.name, + state : mapState(t.state), + progress : Math.round(t.progress * 100), + size : t.size, + downloaded: t.downloaded, + uploaded : t.uploaded ?? 0, + ratio : Math.round((t.ratio ?? 0) * 100) / 100, + speed : t.dlspeed, + upspeed : t.upspeed ?? 0, + eta : t.eta ?? -1, + save_path : t.save_path, + category : t.category ?? '', + files : fileInfo.length > 0 + ? fileInfo.map((f: any, i: number) => ({ index: i, progress: Math.round((f.progress ?? 0) * 100) })) + : undefined, + } satisfies TorrentInfo + }) }, async add(config, url, options?: DownloadOptions) { diff --git a/server/torrent-clients/rtorrent.ts b/server/torrent-clients/rtorrent.ts index 310024c..5341f2a 100644 --- a/server/torrent-clients/rtorrent.ts +++ b/server/torrent-clients/rtorrent.ts @@ -229,17 +229,29 @@ const RT: TorrentClientDriver = { const rows = await d_multicall(config, 'main', methods) + // Récupérer la progression par fichier pour chaque torrent (requêtes parallèles) + const fileMap = new Map() + await Promise.all(rows.map(async (r: any) => { + const hash = String(r[0]).toUpperCase() + try { + const files = await rpcCall(config, 'f.multicall', [hash, '', 'f.completed_length=', 'f.size_bytes=']) + if (Array.isArray(files) && files.length > 0) fileMap.set(hash.toLowerCase(), files) + } catch {} + })) + return rows .map(r => { const [hash, name, isOpen, isActive, isChecking, isComplete, size, downloaded, uploadedTotal, ratioRaw, dlSpeed, ulSpeed, leftBytes, directory, label] = r - const progress = size > 0 ? Math.min(100, Math.round(((size - leftBytes) / size) * 100)) : 0 - const eta = Number(dlSpeed) > 0 && Number(leftBytes) > 0 ? Math.round(Number(leftBytes) / Number(dlSpeed)) : -1 + const progress = size > 0 ? Math.min(100, Math.round(((size - leftBytes) / size) * 100)) : 0 + const eta = Number(dlSpeed) > 0 && Number(leftBytes) > 0 ? Math.round(Number(leftBytes) / Number(dlSpeed)) : -1 + const hashLower = String(hash).toLowerCase() + const fileInfo = fileMap.get(hashLower) ?? [] return { - hash : String(hash).toLowerCase(), + hash : hashLower, name : String(name), state : mapState(Number(isOpen), Number(isActive), Number(isChecking), Number(isComplete)), progress, @@ -252,6 +264,12 @@ const RT: TorrentClientDriver = { eta, save_path : String(directory), category : String(label ?? ''), + files : fileInfo.length > 0 + ? fileInfo.map((f: any, i: number) => ({ + index : i, + progress: Number(f[1]) > 0 ? Math.round((Number(f[0]) / Number(f[1])) * 100) : 0, + })) + : undefined, } satisfies TorrentInfo }) .filter(t => { diff --git a/server/torrent-clients/transmission.ts b/server/torrent-clients/transmission.ts index acb4ca0..09fdcd2 100644 --- a/server/torrent-clients/transmission.ts +++ b/server/torrent-clients/transmission.ts @@ -96,6 +96,7 @@ const TR: TorrentClientDriver = { 'hashString', 'name', 'status', 'percentDone', 'totalSize', 'downloadedEver', 'uploadedEver', 'uploadRatio', 'rateDownload', 'rateUpload', 'eta', 'downloadDir', 'labels', + 'fileStats', // progression par fichier (bytesCompleted + length) ] const data = await trRequest(config, 'torrent-get', { fields }) const torrents: any[] = data.arguments?.torrents ?? [] @@ -119,6 +120,12 @@ const TR: TorrentClientDriver = { eta : t.eta ?? -1, save_path : t.downloadDir, category : t.labels?.[0] ?? '', + files : Array.isArray(t.fileStats) && t.fileStats.length > 0 + ? t.fileStats.map((fs: any, i: number) => ({ + index : i, + progress: fs.length > 0 ? Math.round((fs.bytesCompleted / fs.length) * 100) : 0, + })) + : undefined, } satisfies TorrentInfo)) }, diff --git a/src/components/serie/SerieSeasonCard.vue b/src/components/serie/SerieSeasonCard.vue index 41e2382..a7fd602 100644 --- a/src/components/serie/SerieSeasonCard.vue +++ b/src/components/serie/SerieSeasonCard.vue @@ -158,7 +158,7 @@ import { ref, computed, h } from 'vue' import { ChevronUp, Download, Loader, Check, X } from 'lucide-vue-next' -interface ActiveTorrent { hash: string; progress: number; state: string } +interface ActiveTorrent { hash: string; progress: number; state: string; files?: { index: number; progress: number }[] } const props = defineProps<{ season : any @@ -232,13 +232,25 @@ const seasonBtnIcon = computed(() => { // ── État épisode ─────────────────────────────────────────────── function epProgress(ep: any): ActiveTorrent | null { + // Un épisode déjà dans la bibliothèque n'a jamais besoin d'une barre de progression. + if (ep.organized) return null + + const torrent = torrentProgress(extractHash(ep.torrent)) + if (!torrent) return null + if (ep.torrent?.file_index != null) { - // Épisode dans un pack : le hash est partagé par tous les épisodes du pack. - // On affiche la progression du pack UNIQUEMENT pour cet épisode s'il a été - // explicitement téléchargé dans cette session (isDownloaded). - return isDownloaded(`ep-${ep.id}`) ? torrentProgress(extractHash(ep.torrent)) : null + // Épisode dans un pack : on n'affiche que si téléchargé dans cette session. + if (!isDownloaded(`ep-${ep.id}`)) return null + // Si le client remonte la progression par fichier, on utilise celle du fichier précis + // plutôt que la progression globale du pack. + if (torrent.files && torrent.files.length > 0) { + const file = torrent.files.find(f => f.index === ep.torrent.file_index) + if (file != null) return { ...torrent, progress: file.progress } + } + return torrent } - return torrentProgress(extractHash(ep.torrent)) + + return torrent } function epState(ep: any): 'idle' | 'loading' | 'done' | 'unavailable' { diff --git a/src/views/SerieView.vue b/src/views/SerieView.vue index fba585f..0581edc 100644 --- a/src/views/SerieView.vue +++ b/src/views/SerieView.vue @@ -133,7 +133,7 @@ const epActionLoading = ref>({}) const downloadingAll = ref(false) const downloadingSeason = ref>({}) -interface ActiveTorrent { hash: string; progress: number; state: string } +export interface ActiveTorrent { hash: string; progress: number; state: string; files?: { index: number; progress: number }[] } const activeTorrents = ref([]) let pollTimer: ReturnType | null = null @@ -192,7 +192,7 @@ async function fetchActiveDownloads() { const res = await fetch('/api/downloads', { credentials: 'include' }) if (!res.ok) return const list: any[] = await res.json() - activeTorrents.value = list.map(t => ({ hash: t.hash, progress: t.progress ?? 0, state: t.state })) + activeTorrents.value = list.map(t => ({ hash: t.hash, progress: t.progress ?? 0, state: t.state, files: t.files })) } catch {} }