// 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')?1024**2:(u==='GIB')?1024**3:1024**4; return v * mult; }
function fmt(bytes) { if (bytes < 1024) return `${bytes.toFixed(2)} B`; if (bytes < 1024**2) 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();
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});
I just revised the script so it works with the latest Qbitorrent version.
// ==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')?1024**2:(u==='GIB')?1024**3:1024**4;
return v * mult;
}
function fmt(bytes) {
if (bytes < 1024) return `${bytes.toFixed(2)} B`;
if (bytes < 1024**2) 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(); });
})();