Skip to content

Instantly share code, notes, and snippets.

@FoxxMD
Last active August 6, 2025 19:24
Show Gist options
  • Save FoxxMD/f3045b4e2c0afe39631c9550bd131133 to your computer and use it in GitHub Desktop.
Save FoxxMD/f3045b4e2c0afe39631c9550bd131133 to your computer and use it in GitHub Desktop.
Qbittorrent get total size, downloaded, uploaded, and overall ratio (WebUI only)
// Tested on qBittorrent 4.6.1
//
// 1. open qbittorent web interface
// 1a. (optionally) filter to just the torrents that should be totalled
// 1b. MAY need to ensure any/all columns to be totalled are enabled -> Size, Downloaded, Uploaded
// 2. open developer tools
// 3. copy and paste the *entirety* of the below code into the developer tools console and hit enter
//
// Will print totals in MiB/GiB/TiB for:
// * Size + Min/Max/Avg
// * Downloaded + % of Size + Min/Max/Avg
// * Uploaded + % of Size + Min/Max/Avg
// and Ratio + Min/Max/Avg
(function() {
const sizeIndex = Array.from(document.querySelector('tr.dynamicTableHeader').childNodes).findIndex(x => x.title === 'Size');
const downloadedIndex = Array.from(document.querySelector('tr.dynamicTableHeader').childNodes).findIndex(x => x.title === 'Downloaded');
const uploadedIndex = Array.from(document.querySelector('tr.dynamicTableHeader').childNodes).findIndex(x => x.title === 'Uploaded');
const ratioIndex = Array.from(document.querySelector('tr.dynamicTableHeader').childNodes).findIndex(x => x.title === 'Ratio');
const tibThreshold = 1048576;
if(sizeIndex === -1 && downloadedIndex === -1 && uploadedIndex === -1) {
console.warn('Must have at least one of the following columns enabled to compute some statistics: Size, Downloaded, Uploaded');
return;
}
const getMibVal = (valueStr, unit) => {
const val = parseFloat(valueStr);
if(val === 0) {
return 0;
}
switch (unit.toLocaleLowerCase()) {
case 'tib':
return val * 1024 * 1024;
case 'gib':
return val * 1024;
case 'mib':
return val;
case 'kib':
return val / 1024;
case 'B':
return val / 1024 / 1024;
}
}
const getTotalBreakdown = (val) => {
return [
`${((val / 1024) / 1024).toLocaleString('en', {maximumFractionDigits: 2})} TiB`,
`${(val / 1024).toLocaleString('en', {maximumFractionDigits: 2})} GiB`,
`${val.toLocaleString('en', {maximumFractionDigits: 2})} MiB`,
].join(' == ');
}
const getHumanSize = (valMib) => {
if(valMib === 0) {
return '0 B';
}
if(valMib < 1) {
return `${(valMib / 1024).toLocaleString('en', {maximumFractionDigits: 2})} KiB`
}
if(valMib >= tibThreshold) {
return `${(valMib / 1024 / 1024).toLocaleString('en', {maximumFractionDigits: 2})} TiB`
}
if(valMib >= 1024) {
return `${(valMib / 1024).toLocaleString('en', {maximumFractionDigits: 2})} GiB`
}
return `${(valMib).toLocaleString('en', {maximumFractionDigits: 2})} MiB`
}
let mibSize = sizeIndex !== -1 ? 0 : null;
let sizeMinMax = [Infinity,0];
let ratio = ratioIndex !== -1 ? 0 : null;
let ratioMinMax = [Infinity,0];
let mibDl = downloadedIndex !== -1 ? 0 : null;
let dlMinMax = [Infinity,0];
let mibUl = uploadedIndex !== -1 ? 0 : null;
let ulMinMax = [Infinity,0];
const elms = document.querySelectorAll('div#torrentsTableDiv tr.torrentsTableContextMenuTarget');
const torNum = elms.length;
elms.forEach((row) => {
if(mibSize !== null) {
const [str, unit] = row.childNodes[sizeIndex].title.split(' ');
const val = getMibVal(str, unit);
mibSize += val;
sizeMinMax = [Math.min(val, sizeMinMax[0]), Math.max(val, sizeMinMax[1])];
}
if(mibDl !== null) {
const [str, unit] = row.childNodes[downloadedIndex].title.split(' ');
const val = getMibVal(str, unit);
mibDl += val;
dlMinMax = [Math.min(val, dlMinMax[0]), Math.max(val, dlMinMax[1])];
}
if(mibDl !== null) {
const [str, unit] = row.childNodes[uploadedIndex].title.split(' ');
const val = getMibVal(str, unit);
mibUl += val;
ulMinMax = [Math.min(val, ulMinMax[0]), Math.max(val, ulMinMax[1])];
}
if(ratio !== null) {
const val = parseFloat(row.childNodes[ratioIndex].title);
ratio += val
ratioMinMax = [Math.min(val, ratioMinMax[0]), Math.max(val, ratioMinMax[1])];
}
});
if(mibSize !== null) {
console.log(`${'Size'.padEnd(11,' ')}: ${getTotalBreakdown(mibSize)} | Min ${getHumanSize(sizeMinMax[0])} | Max ${getHumanSize(sizeMinMax[1])} | Avg ${getHumanSize(mibSize / torNum)}`);
}
if(mibDl !== null) {
const parts = [`${'Downloaded'.padEnd(11,' ')}: ${getTotalBreakdown(mibDl)}`];
if(mibSize !== null) {
parts.push(`${((mibDl / mibSize) * 100).toLocaleString('en', {maximumFractionDigits: 2})}% of Size`);
}
parts.push(`Min ${getHumanSize(dlMinMax[0])} | Max ${getHumanSize(dlMinMax[1])} | Avg ${getHumanSize(mibDl / torNum)}`);
console.log(parts.join(' | '));
}
if(mibUl !== null) {
const parts = [`${'Uploaded'.padEnd(11,' ')}: ${getTotalBreakdown(mibUl)}`];
if(mibSize !== null) {
parts.push(`${((mibUl / mibSize) * 100).toLocaleString('en', {maximumFractionDigits: 2})}% of Size`);
}
parts.push(`Min ${getHumanSize(ulMinMax[0])} | Max ${getHumanSize(ulMinMax[1])} | Avg ${getHumanSize(mibUl / torNum)}`);
console.log(parts.join(' | '));
}
if(mibDl !== null && mibUl !== null) {
const parts = [
`${'Ratio'.padEnd(11,' ')}: ${((mibUl / mibDl)).toLocaleString('en', {maximumFractionDigits: 2})}`,
`Min ${ratioMinMax[0]} | Max ${ratioMinMax[1]} | Avg ${(ratio / torNum).toLocaleString('en', {maximumFractionDigits: 2})}`
];
console.log(parts.join(' | '));
}
})();
@FoxxMD
Copy link
Author

FoxxMD commented Jan 4, 2024

An alternative that displays on the interface (not written by me) here: https://greasyfork.org/en/scripts/483775-calculate-qbittorrent-selected-torrents-size

@kunearn
Copy link

kunearn commented Aug 6, 2025

Hello! I just revised the script from https://greasyfork.org/en/scripts/483775-calculate-qbittorrent-selected-torrents-size to work with Qbitorrent latest version (5.x.x). If you access the webui remotely, make sure to adjust the @match to your address. Hope this helps someone.

// ==UserScript==
// @name qBittorrent Selected Size (torrentsTableDiv fix)
// @namespace http://tampermonkey.net/
// @Version 1.0
// @description Show total size of selected torrents in the footer
// @match http://localhost:8080/*
// @run-at document-idle
// @grant none
// ==/UserScript==

(function () {
'use strict';

const FOOTER_ID = 'tmSelectedSizeTotal';
const LABEL = 'Selected Torrents Total Size:';
let sizeColIndex = null;
let lastSig = '';

function ensureFooter() {
const row = document.querySelector('#desktopFooter > table > tbody > tr');
if (!row) { setTimeout(ensureFooter, 500); return; }
if (!document.getElementById(FOOTER_ID)) {
const td = document.createElement('td');
td.id = FOOTER_ID;
td.textContent = ${LABEL} 0.00 MiB;
const sep = document.createElement('td');
sep.className = 'statusBarSeparator';
row.insertBefore(td, row.firstElementChild);
row.insertBefore(sep, td.nextSibling);
}
}

// Find the "Size" column index using the header th.column_size
function findSizeColIndex() {
// Header and body are in separate tables; just grab any "th.column_size"
const th = document.querySelector('th.column_size');
if (!th) return null;
const tr = th.closest('tr');
if (!tr) return null;
const idx = Array.from(tr.children).indexOf(th);
return (idx >= 0) ? idx : null;
}

function toBytes(text) {
const t = text.replace(/\u00A0/g,' ').replace(/,/g,'').trim(); // NBSP/commas
const m = t.match(/^([\d.]+)\s*(B|KiB|MiB|GiB|TiB)$/i);
if (!m) return null;
const v = parseFloat(m[1]);
const u = m[2].toUpperCase();
const mult = (u==='B')?1:(u==='KIB')?1024:(u==='MIB')?10242:(u==='GIB')?10243:1024**4;
return v * mult;
}

function fmt(bytes) {
if (bytes < 1024) return ${bytes.toFixed(2)} B;
if (bytes < 10242) return ${(bytes/1024).toFixed(2)} KiB;
if (bytes < 1024
3) return ${(bytes/1024**2).toFixed(2)} MiB;
if (bytes < 1024**4) return ${(bytes/1024**3).toFixed(2)} GiB;
return ${(bytes/1024**4).toFixed(2)} TiB;
}

function update() {
ensureFooter();

// Resolve Size column index once (or when header changes)
if (sizeColIndex == null) sizeColIndex = findSizeColIndex();

const rows = Array.from(document.querySelectorAll('#torrentsTableDiv tbody tr.selected'));
const sig = rows.map(r => r.getAttribute('data-row-id') || r.innerText.slice(0,50)).join('|') + `|idx:${sizeColIndex}`;
if (sig === lastSig) return;
lastSig = sig;

const el = document.getElementById(FOOTER_ID);
if (!el) return;

if (!rows.length || sizeColIndex == null) {
  el.textContent = `${LABEL} 0.00 MiB`;
  return;
}

let total = 0;
for (const r of rows) {
  const cell = r.children[sizeColIndex] || r.querySelector('td:nth-child('+(sizeColIndex+1)+')');
  if (!cell) continue;
  const b = toBytes(cell.textContent);
  if (b != null) total += b;
}
el.textContent = `${LABEL} ${fmt(total)}`;

}

function startObservers() {
const tableDiv = document.getElementById('torrentsTableDiv');
if (!tableDiv) { setTimeout(startObservers, 500); return; }

// Watch the body (selection/content changes)
const tb = tableDiv.querySelector('tbody') || tableDiv;
new MutationObserver(update).observe(tb, {subtree: true, attributes: true, childList: true});

// Watch headers — column moves/visibility can change index
const header = document.querySelector('th.column_size')?.closest('table') || document;
new MutationObserver(() => { sizeColIndex = null; update(); })
  .observe(document.body, {subtree: true, attributes: true, childList: true});

document.addEventListener('click', update, true);
document.addEventListener('keyup', update, true);

update();
// Fallback timer in case nothing fires
setInterval(update, 1000);

}

window.addEventListener('load', () => { ensureFooter(); startObservers(); });
})();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment