qBittorrent Selected Size + Count

Show selected torrent count and total size in qBittorrent WebUI footer (old/new versions)

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         qBittorrent Selected Size + Count
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Show selected torrent count and total size in qBittorrent WebUI footer (old/new versions)
// @match http://localhost:8080/*
// @author       me + others
// @run-at       document-idle
// @grant        none
// @license      MIT

// ==/UserScript==

/*
  Notes:
  - !!! Make sure to adjust the @match above to your WebUI address if needed !!!
  - This version was adjusted by ChatGPT in October 2025 and is confirmed to work on qBittorrent WebUI v5.0.4 - v5.1.2
  - Adds selected torrent count alongside the total size
  - Updates original v0.5 logic to get full old + new WebUI compatibility (#statusBar + #desktopFooter)
  - Replaces 1s polling with debounced MutationObservers (lightweight, real-time)
  - Dynamically detects “Size” column instead of hard-coded index (future-proof)
  - Accurate byte-based math replaces mixed-unit summation
  - Enhanced lightweight multi-language label detection (one time at startup, no performance cost)
  - Keeps English size units (KiB, MiB, GiB, TiB) for reliable parsing across locales
  - Privacy checked to be safe and running 100% locally (no data collection, no external requests)
*/

(() => {
  'use strict';

  const FOOTER_ID = 'tmSelectedSizeTotal';
  const DEBOUNCE = 80;
  let footerEl = null, sizeColIndex = null, timer = null;

  // --- Lightweight language detection (only affects label text)
  const helpLang = {
    "Help": "en", "Ajutor": "ro", "Aide": "fr", "Ayuda": "es", "Справка": "ru",
    "Aiuto": "it", "帮助": "zh", "Hilfe": "de", "Βοήθεια": "el",
    "Ajuda": "pt", "日本語": "ja"
  };
  const labelMap = {
    en: 'Selected Torrents:', ro: 'Torrente selectate:', fr: 'Torrents sélectionnés:',
    es: 'Torrents seleccionados:', ru: 'Выбранные торренты:', it: 'Torrent selezionati:',
    zh: '选中的种子:', de: 'Ausgewählte Torrents:', el: 'Επιλεγμένα Torrents:',
    pt: 'Torrents Selecionados:', ja: '選択したトレント:'
  };
  const uiText = document.querySelector('#desktopNavbar a:last-child')?.innerText.trim() || 'Help';
  const LABEL = labelMap[helpLang[uiText] || 'en'];

  const waitFor = (sel, cb, tries = 0) => {
    const el = document.querySelector(sel);
    if (el) return cb(el);
    if (tries < 50) setTimeout(() => waitFor(sel, cb, tries + 1), 200);
  };

  const ensureFooter = (row) => {
    if (footerEl = document.getElementById(FOOTER_ID)) return;
    const td = document.createElement('td');
    td.id = FOOTER_ID;
    td.textContent = `${LABEL} 0 (0.00 MiB)`;
    const sep = document.createElement('td');
    sep.className = 'statusBarSeparator';
    row.insertBefore(td, row.firstElementChild);
    row.insertBefore(sep, td.nextElementSibling); // replaced comma operator
    footerEl = td;
  };

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

  const fmt = (b) => {
    const u = ['B','KiB','MiB','GiB','TiB'];
    let i = 0;
    while (b >= 1024 && i < u.length - 1) { b /= 1024; i++; } // replaced comma operator with braces
    return `${b.toFixed(2)} ${u[i]}`;
  };

  const findSizeCol = () => {
    const ths = document.querySelectorAll('#torrentsTableDiv thead th');
    for (const [i, th] of ths.entries()) {
      if (th.classList.contains('column_size') || /size/i.test(th.textContent)) return i;
    }
    return null;
  };

  const update = () => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      if (!footerEl) return;
      if (sizeColIndex == null) sizeColIndex = findSizeCol();
      const rows = document.querySelectorAll('#torrentsTableDiv tbody tr.selected');
      let total = 0;
      for (const r of rows) {
        const cell = r.children[sizeColIndex];
        if (cell) total += toBytes(cell.textContent);
      }
      footerEl.textContent = `${LABEL} ${rows.length} (${fmt(total)})`;
    }, DEBOUNCE);
  };

  const start = () => {
    const tableDiv = document.querySelector('#torrentsTableDiv');
    if (!tableDiv) return;
    const tb = tableDiv.querySelector('tbody');
    if (tb) new MutationObserver(update).observe(tb, { subtree: true, attributes: true, childList: true });
    new MutationObserver(() => { sizeColIndex = null; update(); })
      .observe(document.body, { subtree: true, childList: true });
    document.addEventListener('click', update, true);
    document.addEventListener('keyup', update, true);
    update();
  };

  waitFor('#desktopFooter tr, #statusBar table tr', ensureFooter);
  waitFor('#torrentsTableDiv', start);
})();