feat: ajout du ratio, upload speed, etc dans les activités

This commit is contained in:
MasutaYunikon committed 2026-04-10 17:50:00 +02:00
1 parent 9b4dc4f782
commit 71961eb998
7 files changed
+146 -53

No files matched your search

+3
View File
@@ -41,7 +41,10 @@ export interface TorrentInfo {
progress : number
size : number
downloaded: number
uploaded : number
ratio : number
speed : number
upspeed : number
eta : number
save_path : string
category : string
+3
View File
@@ -93,7 +93,10 @@ const QB: TorrentClientDriver = {
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 ?? '',
+11 -5
View File
@@ -178,11 +178,13 @@ const RT: TorrentClientDriver = {
'd.complete',
'd.size_bytes',
'd.bytes_done',
'd.up.total', // total uploadé
'd.ratio', // ratio x1000
'd.down.rate',
'd.up.rate',
'd.left_bytes',
'd.down.rate',
'd.directory',
'd.custom1', // label rTorrent
'd.custom1',
]
const rows = await d_multicall(config, 'main', methods)
@@ -190,10 +192,11 @@ const RT: TorrentClientDriver = {
return rows
.map(r => {
const [hash, name, isOpen, isActive, isChecking, isComplete,
size, downloaded, speed, leftBytes, , directory, label] = r
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 = speed > 0 && leftBytes > 0 ? Math.round(leftBytes / speed) : -1
const eta = Number(dlSpeed) > 0 && Number(leftBytes) > 0 ? Math.round(Number(leftBytes) / Number(dlSpeed)) : -1
return {
hash : String(hash).toLowerCase(),
@@ -202,7 +205,10 @@ const RT: TorrentClientDriver = {
progress,
size : Number(size),
downloaded: Number(downloaded),
speed : Number(speed),
uploaded : Number(uploadedTotal ?? 0),
ratio : Math.round((Number(ratioRaw ?? 0) / 1000) * 100) / 100,
speed : Number(dlSpeed),
upspeed : Number(ulSpeed ?? 0),
eta,
save_path : String(directory),
category : String(label ?? ''),
+4 -6
View File
@@ -108,11 +108,6 @@ const DS: TorrentClientDriver = {
const tasks: any[] = data?.tasks ?? []
return tasks
.filter(t => {
if (!category) return true
// Synology n'a pas de labels natifs — on filtre par destination si elle contient la catégorie
return (t.additional?.detail?.destination as string)?.includes(String(category))
})
.map(t => {
const transfer = t.additional?.transfer ?? {}
const size = t.size ?? 0
@@ -124,10 +119,13 @@ const DS: TorrentClientDriver = {
progress : size > 0 ? Math.min(100, Math.round((dl / size) * 100)) : 0,
size,
downloaded: dl,
uploaded : transfer.size_uploaded ?? 0,
ratio : size > 0 ? Math.round(((transfer.size_uploaded ?? 0) / size) * 100) / 100 : 0,
speed : transfer.speed_download ?? 0,
upspeed : transfer.speed_upload ?? 0,
eta : -1,
save_path : t.additional?.detail?.destination ?? '',
category : '',
category : category ?? '',
} satisfies TorrentInfo
})
},
+5 -1
View File
@@ -94,7 +94,8 @@ const TR: TorrentClientDriver = {
async list(config, category) {
const fields = [
'hashString', 'name', 'status', 'percentDone', 'totalSize',
'downloadedEver', 'rateDownload', 'eta', 'downloadDir', 'labels',
'downloadedEver', 'uploadedEver', 'uploadRatio',
'rateDownload', 'rateUpload', 'eta', 'downloadDir', 'labels',
]
const data = await trRequest(config, 'torrent-get', { fields })
const torrents: any[] = data.arguments?.torrents ?? []
@@ -111,7 +112,10 @@ const TR: TorrentClientDriver = {
progress : Math.round(t.percentDone * 100),
size : t.totalSize,
downloaded: t.downloadedEver,
uploaded : t.uploadedEver ?? 0,
ratio : Math.round((t.uploadRatio ?? 0) * 100) / 100,
speed : t.rateDownload,
upspeed : t.rateUpload ?? 0,
eta : t.eta ?? -1,
save_path : t.downloadDir,
category : t.labels?.[0] ?? '',
+5 -2
View File
@@ -62,7 +62,7 @@ function mapTorrent(t: any[]): TorrentInfo {
// [hash, status, name, size, progress, downloaded, uploaded, ratio,
// ul_speed, dl_speed, eta, label, peers_connected, peers_swarm,
// seeds_connected, seeds_swarm, availability, queue_order, remaining]
const [hash, status, name, size, progress, downloaded, , , , dlSpeed, eta, label] = t
const [hash, status, name, size, progress, downloaded, uploaded, ratio, ulSpeed, dlSpeed, eta, label] = t
return {
hash : hash.toLowerCase(),
name,
@@ -70,9 +70,12 @@ function mapTorrent(t: any[]): TorrentInfo {
progress : Math.min(100, Math.round(progress / 10)),
size,
downloaded,
uploaded : uploaded ?? 0,
ratio : Math.round(((ratio ?? 0) / 1000) * 100) / 100, // uTorrent ratio est x1000
speed : dlSpeed ?? 0,
upspeed : ulSpeed ?? 0,
eta : eta ?? -1,
save_path : '', // récupéré séparément via torrent properties si besoin
save_path : '',
category : label ?? '',
}
}
+115 -39
View File
@@ -39,7 +39,7 @@
</div>
</div>
<!-- Toolbar : recherche + tri -->
<!-- Toolbar : recherche + tri + colonnes -->
<div class="flex items-center gap-2 mb-4">
<div class="relative flex-1 max-w-xs">
<Search class="absolute left-3 top-1/2 -translate-y-1/2 text-muted pointer-events-none" :size="14" />
@@ -51,6 +51,7 @@
/>
</div>
<!-- Tri -->
<div class="relative shrink-0" ref="sortRef">
<button
@click="sortOpen = !sortOpen"
@@ -61,7 +62,6 @@
<span class="hidden md:inline text-xs">Trier</span>
<span v-if="activeSort !== 'none'" class="absolute -top-1 -right-1 w-2 h-2 rounded-full bg-accent" />
</button>
<div
v-if="sortOpen"
class="absolute top-full right-0 mt-1 bg-card border border-border rounded-xl p-2 z-20 w-48 flex flex-col gap-0.5 shadow-xl"
@@ -78,17 +78,42 @@
</button>
</div>
</div>
<!-- Colonnes -->
<div class="relative shrink-0" ref="colRef">
<button
@click="colOpen = !colOpen"
class="btn-secondary flex items-center gap-1.5 py-1.5 px-2.5"
:class="colOpen ? 'border-accent text-accent' : ''"
>
<svg width="14" height="14" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" fill="none">
<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>
<span class="hidden md:inline text-xs">Colonnes</span>
</button>
<div
v-if="colOpen"
class="absolute top-full right-0 mt-1 bg-card border border-border rounded-xl p-2 z-20 w-52 flex flex-col gap-0.5 shadow-xl"
>
<p class="text-[10px] text-muted px-3 py-1">Colonnes affichées</p>
<label
v-for="col in columnOptions" :key="col.key"
class="flex items-center gap-2.5 px-3 py-2 rounded-lg text-xs cursor-pointer hover:bg-hover transition-colors"
>
<input type="checkbox" v-model="columns[col.key]" class="accent-accent" />
{{ col.label }}
</label>
</div>
</div>
</div>
<!-- Onglets + filtre -->
<!-- Onglets + filtres -->
<div class="flex items-center justify-between mb-4">
<div class="flex gap-1 bg-shell rounded-lg p-1 border border-border">
<button
@click="activeTab = 'active'"
class="px-4 py-1.5 text-xs rounded-md transition-colors"
:class="activeTab === 'active'
? 'bg-active text-primary'
: 'text-muted hover:text-primary'"
:class="activeTab === 'active' ? 'bg-active text-primary' : 'text-muted hover:text-primary'"
>
En cours
<span v-if="activeTorrents.length > 0" class="ml-1.5 text-[10px] text-accent">
@@ -98,9 +123,7 @@
<button
@click="activeTab = 'done'"
class="px-4 py-1.5 text-xs rounded-md transition-colors"
:class="activeTab === 'done'
? 'bg-active text-primary'
: 'text-muted hover:text-primary'"
:class="activeTab === 'done' ? 'bg-active text-primary' : 'text-muted hover:text-primary'"
>
Terminés
<span v-if="doneTorrents.length > 0" class="ml-1.5 text-[10px] text-accent">
@@ -109,18 +132,35 @@
</button>
</div>
<div v-if="activeTab === 'done'" class="flex items-center gap-2">
<span class="text-xs text-muted">Masquer importés</span>
<button
@click="hideImported = !hideImported"
class="relative shrink-0 w-10 h-[22px] rounded-full transition-colors duration-200"
:class="hideImported ? 'bg-accent' : 'bg-border'"
>
<span
class="absolute top-[3px] left-[3px] w-4 h-4 rounded-full bg-white transition-transform duration-200"
:class="hideImported ? 'translate-x-[18px]' : 'translate-x-0'"
/>
</button>
<div v-if="activeTab === 'done'" class="flex items-center gap-3">
<!-- Filtre seeds uniquement -->
<label class="flex items-center gap-1.5 cursor-pointer">
<span class="text-xs text-muted">Seeds uniquement</span>
<button
@click="seedingOnly = !seedingOnly"
class="relative shrink-0 w-10 h-[22px] rounded-full transition-colors duration-200"
:class="seedingOnly ? 'bg-green-500' : 'bg-border'"
>
<span
class="absolute top-[3px] left-[3px] w-4 h-4 rounded-full bg-white transition-transform duration-200"
:class="seedingOnly ? 'translate-x-[18px]' : 'translate-x-0'"
/>
</button>
</label>
<!-- Masquer importés -->
<label class="flex items-center gap-1.5 cursor-pointer">
<span class="text-xs text-muted">Masquer importés</span>
<button
@click="hideImported = !hideImported"
class="relative shrink-0 w-10 h-[22px] rounded-full transition-colors duration-200"
:class="hideImported ? 'bg-accent' : 'bg-border'"
>
<span
class="absolute top-[3px] left-[3px] w-4 h-4 rounded-full bg-white transition-transform duration-200"
:class="hideImported ? 'translate-x-[18px]' : 'translate-x-0'"
/>
</button>
</label>
</div>
</div>
@@ -162,12 +202,12 @@
<span class="text-[10px] px-2 py-0.5 rounded border" :class="stateBadge(t.state).class">
{{ stateBadge(t.state).label }}
</span>
<span class="text-xs text-muted">{{ t.client_name }}</span>
<span v-if="columns.client" class="text-xs text-muted">{{ t.client_name }}</span>
</div>
</div>
<div class="text-right shrink-0">
<p class="text-sm font-semibold text-primary">{{ clampedProgress(t) }}%</p>
<p class="text-xs text-muted mt-0.5">{{ formatSize(t.downloaded) }} / {{ formatSize(t.size) }}</p>
<p v-if="columns.size" class="text-xs text-muted mt-0.5">{{ formatSize(t.downloaded) }} / {{ formatSize(t.size) }}</p>
</div>
</div>
@@ -185,14 +225,29 @@
<!-- Infos bas + actions -->
<div class="flex items-center justify-between">
<div class="flex items-center gap-3 text-xs text-muted">
<!-- Downloading -->
<span v-if="t.state === 'downloading'">↓ {{ formatSpeed(t.speed) }}</span>
<span v-if="t.state === 'downloading' && t.eta > 0">ETA {{ formatEta(t.eta) }}</span>
<span v-if="t.state === 'seeding'" class="text-green-500">Complété</span>
<!-- Seeding : ratio + upload -->
<template v-if="t.state === 'seeding'">
<span class="text-green-500">Complété</span>
<span v-if="columns.ratio" class="text-xs font-mono"
:class="t.ratio >= 1 ? 'text-green-400' : t.ratio >= 0.5 ? 'text-yellow-500' : 'text-muted'">
R {{ t.ratio?.toFixed(2) ?? '0.00' }}
</span>
<span v-if="columns.uploaded" class="text-xs text-muted">
↑ {{ formatSize(t.uploaded ?? 0) }}
</span>
<span v-if="columns.upspeed && t.upspeed > 0" class="text-xs text-muted">
{{ formatSpeed(t.upspeed) }}
</span>
</template>
<span v-if="t.state === 'unknown'" class="text-yellow-500">État non remonté par le client</span>
</div>
<div v-if="t.state === 'seeding' || t.state === 'unknown'" class="flex items-center gap-2">
<!-- Erreurs fichiers -->
<div v-if="t.errorFiles?.length > 0" class="relative group/err">
<span class="text-xs px-2 py-0.5 rounded border border-red-500/40 text-red-400 cursor-default">
@@ -256,19 +311,41 @@ const importing = ref<Record<string, boolean>>({})
const importingAll = ref(false)
const activeTab = ref<'active' | 'done'>('active')
const hideImported = ref(false)
const search = ref('')
const sortOpen = ref(false)
const activeSort = ref<'none' | 'name' | 'size' | 'progress' | 'state'>('none')
const sortDir = ref<'asc' | 'desc'>('asc')
const sortRef = ref<HTMLElement | null>(null)
const seedingOnly = ref(false)
const search = ref('')
const sortOpen = ref(false)
const colOpen = ref(false)
const activeSort = ref<'none' | 'name' | 'size' | 'progress' | 'state' | 'ratio'>('none')
const sortDir = ref<'asc' | 'desc'>('asc')
const sortRef = ref<HTMLElement | null>(null)
const colRef = ref<HTMLElement | null>(null)
// Colonnes visibles
const columns = ref({
client : true,
size : true,
ratio : true,
uploaded: true,
upspeed : true,
})
const columnOptions = [
{ key: 'client', label: 'Client' },
{ key: 'size', label: 'Taille / Téléchargé' },
{ key: 'ratio', label: 'Ratio' },
{ key: 'uploaded', label: 'Uploadé (total)' },
{ key: 'upspeed', label: 'Vitesse upload' },
]
onClickOutside(sortRef, () => { sortOpen.value = false })
onClickOutside(colRef, () => { colOpen.value = false })
const sortOptions = [
{ label: 'Nom', value: 'name' },
{ label: 'Taille', value: 'size' },
{ label: 'Progression', value: 'progress' },
{ label: 'État', value: 'state' },
{ label: 'Ratio', value: 'ratio' },
]
function setSort(val: typeof activeSort.value) {
@@ -283,7 +360,6 @@ function setSort(val: typeof activeSort.value) {
let pollInterval: ReturnType<typeof setInterval> | null = null
// FIX : unknown retiré de activeTorrents, ajouté dans doneTorrents
const activeTorrents = computed(() =>
torrents.value.filter(t => ['downloading', 'paused', 'checking', 'error'].includes(t.state))
)
@@ -295,17 +371,18 @@ const doneTorrents = computed(() =>
const visibleTorrents = computed(() => {
let list = activeTab.value === 'active'
? activeTorrents.value
: hideImported.value
? doneTorrents.value.filter(t => t.organizeState !== 'done')
: doneTorrents.value
: doneTorrents.value
if (activeTab.value === 'done') {
if (seedingOnly.value) list = list.filter(t => t.state === 'seeding')
if (hideImported.value) list = list.filter(t => t.organizeState !== 'done')
}
// Recherche
if (search.value.trim()) {
const q = search.value.toLowerCase()
list = list.filter(t => t.name.toLowerCase().includes(q))
}
// Tri
if (activeSort.value !== 'none') {
const dir = sortDir.value === 'asc' ? 1 : -1
list = [...list].sort((a, b) => {
@@ -314,6 +391,7 @@ const visibleTorrents = computed(() => {
case 'size' : return (a.size - b.size) * dir
case 'progress': return (a.progress - b.progress) * dir
case 'state' : return a.state.localeCompare(b.state) * dir
case 'ratio' : return ((a.ratio ?? 0) - (b.ratio ?? 0)) * dir
default : return 0
}
})
@@ -329,7 +407,6 @@ const stats = computed(() => [
{ label: 'Erreurs', value: torrents.value.filter(t => t.state === 'error').length },
])
// FIX : progress cappé à 100 pour éviter "278.2 GB / 278.1 GB" → 100%
function clampedProgress(t: any): number {
return Math.min(100, Math.max(0, t.progress ?? 0))
}
@@ -420,14 +497,13 @@ function stateBadge(state: string) {
paused : { label: 'En pause', class: 'border-border text-muted' },
checking : { label: 'Vérification', class: 'border-yellow-500/40 text-yellow-500' },
error : { label: 'Erreur', class: 'border-red-500/40 text-red-400' },
// FIX : badge jaune pour signaler état dégradé sans alarmer
unknown : { label: 'État inconnu', class: 'border-yellow-500/40 text-yellow-500' },
}
return map[state] ?? map.unknown
}
function formatSize(bytes: number): string {
if (bytes === 0) return '0 B'
if (!bytes || bytes === 0) return '0 B'
const units = ['B', 'KB', 'MB', 'GB', 'TB']
const i = Math.floor(Math.log(bytes) / Math.log(1024))
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`