futz / Parallel Download Manager

// ==UserScript==
// @name         Parallel Download Manager
// @namespace    https://github.com/paurakh/userscripts
// @version      0.3.0
// @description  IDM-inspired fast, verified parallel downloads for direct file links.
// @author       Px
// @license      MIT
// @match        http://*/*
// @match        https://*/*
// @noframes
// @grant        GM_xmlhttpRequest
// @connect      github.com
// @connect      objects.githubusercontent.com
// @connect      release-assets.githubusercontent.com
// @connect      *
// @run-at       document-start
// ==/UserScript==

(function () {
  'use strict';

  // This script intentionally has broad permissions. It only requests a URL after
  // a direct user click on a high-confidence download anchor.
  // IDM-inspired interface and segmented-download workflow; this is an independent userscript.
  const SCRIPT_PREFIX = 'pdm-';
  const CONNECTIONS = Math.min(8, Math.max(4, Number(navigator.hardwareConcurrency) || 6));
  const MAX_CONNECTIONS = 8;
  const CHUNK_SIZE = 16 * 1024 * 1024;
  const MIN_SEGMENTED_SIZE = 8 * 1024 * 1024;
  const MEMORY_WARNING_SIZE = 256 * 1024 * 1024;
  const MEMORY_LIMIT = 512 * 1024 * 1024;
  const MAX_RETRIES = 5;
  const REQUEST_TIMEOUT = 60_000;
  const DOWNLOAD_EXTENSIONS = /\.(?:7z|apk|appx|avi|bin|bz2|csv|deb|dmg|docx?|epub|exe|flac|gif|gz|iso|jar|jpeg?|m4a|mkv|mov|mp3|mp4|msi|odp|ods|odt|ogg|pdf|png|pptx?|rar|rpm|tar|tgz|torrent|txt|wav|webm|webp|xlsx?|xml|zip)(?:$|[?#])/i;
  const DOWNLOAD_HINT = /\b(download|export|get[ _-]?file|save[ _-]?file)\b/i;

  const bypassedAnchors = new WeakSet();
  const scriptAnchors = new WeakSet();
  let activeSession = null;
  let ui = null;

  function isEligibleClick(event) {
    if (!event.isTrusted || event.button !== 0 || event.defaultPrevented) return null;
    if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return null;

    const anchor = event.target instanceof Element ? event.target.closest('a[href]') : null;
    if (!anchor) return oneDriveDownloadCandidate(event.target);
    if (bypassedAnchors.has(anchor) || scriptAnchors.has(anchor)) return null;
    if (anchor.target && anchor.target.toLowerCase() === '_blank') return null;

    let url;
    try { url = new URL(anchor.href, document.baseURI); } catch { return null; }
    if (!/^https?:$/.test(url.protocol) || url.hash && url.pathname === location.pathname && url.search === location.search) return null;

    const hasDownloadAttribute = anchor.hasAttribute('download');
    const pathLooksDownloadable = DOWNLOAD_EXTENSIONS.test(url.pathname + url.search);
    const hintText = `${anchor.className || ''} ${anchor.id || ''} ${anchor.getAttribute('rel') || ''} ${anchor.textContent || ''}`;
    const hintedDownload = DOWNLOAD_HINT.test(hintText);
    if (!hasDownloadAttribute && !pathLooksDownloadable && !hintedDownload) return null;
    // Extra safety: if the link has no download attribute and no file extension, require strong hint
    if (!hasDownloadAttribute && !pathLooksDownloadable) {
      const strongHint = /\b(download|export)\b/i.test(hintText) && (anchor.getAttribute('download') !== null || /\bdownload\b/i.test(anchor.className) || /\bdownload\b/i.test(anchor.id));
      if (!strongHint) return null;
    }

    return { anchor, url };
  }

  function oneDriveDownloadCandidate(target) {
    if (!(target instanceof Element) || !isOneDriveHost(location.hostname)) return null;
    const control = target.closest('button,[role="button"],[role="menuitem"]');
    if (!control || bypassedAnchors.has(control)) return null;

    const label = `${control.getAttribute('aria-label') || ''} ${control.getAttribute('title') || ''} ${control.textContent || ''}`.replace(/\s+/g, ' ').trim();
    if (!/^(?:download|download file|download selected items?)$/i.test(label)) return null;

    const url = new URL(location.href);
    url.searchParams.set('download', '1');
    return { control, url, oneDrive: true };
  }

  function isOneDriveHost(hostname) {
    return /(^|\.)(?:1drv\.ms|onedrive\.com|onedrive\.live\.com|sharepoint\.com)$/i.test(hostname);
  }

  function snapshotLink(anchor, url, nativeControl = null) {
    return {
      url: url.href,
      hostname: url.hostname,
      download: anchor?.getAttribute('download') || '',
      target: anchor?.getAttribute('target') || '',
      rel: anchor?.getAttribute('rel') || '',
      referrerPolicy: anchor?.getAttribute('referrerpolicy') || '',
      type: anchor?.getAttribute('type') || '',
      nativeControl,
      filename: sanitizeFilename(anchor?.getAttribute('download') || filenameFromOneDrivePage() || filenameFromUrl(url) || 'download.bin'),
    };
  }

  function normalDownload(link) {
    closeUi();
    if (link.nativeControl?.isConnected) {
      bypassedAnchors.add(link.nativeControl);
      link.nativeControl.click();
      queueMicrotask(() => bypassedAnchors.delete(link.nativeControl));
      return;
    }
    const anchor = document.createElement('a');
    anchor.href = link.url;
    if (link.download) anchor.download = link.download;
    if (link.target) anchor.target = link.target;
    if (link.rel) anchor.rel = link.rel;
    if (link.referrerPolicy) anchor.referrerPolicy = link.referrerPolicy;
    if (link.type) anchor.type = link.type;
    bypassedAnchors.add(anchor);
    scriptAnchors.add(anchor);
    document.documentElement.appendChild(anchor);
    anchor.click();
    anchor.remove();
  }

  function ensureUi() {
    if (ui) return ui;
    const host = document.createElement('div');
    host.id = `${SCRIPT_PREFIX}host`;
    host.style.cssText = 'position:fixed;inset:0;z-index:2147483647;pointer-events:none;';
    const root = host.attachShadow({ mode: 'closed' });
    root.innerHTML = `
      <style>
        :host { color-scheme: dark; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; -webkit-font-smoothing: antialiased; }
        *, *::before, *::after { box-sizing: border-box; }
        .backdrop { position:fixed; inset:0; display:grid; place-items:center; padding:20px; background:rgba(0,0,0,.68); pointer-events:auto; opacity:0; transition:opacity 140ms ease; }
        .backdrop.visible { opacity:1; }
        .card { width:min(100%, 650px); overflow:hidden; border:1px solid #46515f; border-radius:4px; background:#20252b; color:#e8edf2; box-shadow:0 18px 50px rgba(0,0,0,.65); transform:translateY(6px); transition:transform 140ms ease; }
        .visible .card { transform:translateY(0) scale(1); }
        header { display:flex; gap:12px; align-items:center; padding:12px 16px 10px; background:#292f36; border-bottom:1px solid #4a535d; }
        .icon { display:grid; flex:none; place-items:center; width:32px; height:32px; border:1px solid #5f6d7b; border-radius:3px; background:#343c45; color:#7fc7ff; }
        .icon svg { width:18px; height:18px; }
        h1 { margin:0 0 2px; font-size:15px; line-height:1.25; font-weight:600; text-wrap:balance; }
        .subtitle, .detail { margin:0; color:#aeb8c3; font-size:12px; line-height:1.45; text-wrap:pretty; }
        .body { padding:12px 16px 14px; }
        .file { padding:9px 10px; border:1px solid #46515c; border-radius:2px; background:#171b1f; }
        .filename { display:block; overflow:hidden; color:#f0f4f7; font-size:12px; font-weight:600; text-overflow:ellipsis; white-space:nowrap; }
        .host { display:block; overflow:hidden; margin-top:3px; color:#8e9ba8; font-size:11px; text-overflow:ellipsis; white-space:nowrap; }
        .info-grid { display:grid; grid-template-columns:120px 1fr 120px 1fr; gap:5px 10px; margin:12px 0 10px; color:#c5ced7; font-size:12px; }
        .info-grid b { color:#f0f4f7; font-weight:500; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
        .progress-wrap { display:none; margin-top:10px; }
        .progress-wrap.show { display:block; }
        .bar { height:13px; overflow:hidden; border:1px solid #56616c; border-radius:1px; background:#101317; }
        .bar > span { display:block; width:0%; height:100%; background:#2aa84a; transition:width 150ms linear; }
        .stats { display:flex; justify-content:space-between; gap:12px; margin-top:6px; color:#b8c2cc; font-size:11px; font-variant-numeric:tabular-nums; }
        .connections { display:grid; grid-template-columns:repeat(var(--connection-count, 1), minmax(0, 1fr)); gap:3px; height:18px; margin-top:10px; padding:2px; border:1px solid #596572; background:#11151a; }
        .connections i { position:relative; display:block; overflow:hidden; background:#53616e; }
        .connections i[hidden] { display:none; }
        .connections i::after { content:""; position:absolute; inset:0; width:var(--progress, 0%); background:#d39b36; transition:width 100ms linear,background-color 100ms linear; }
        .error { display:none; margin-top:12px; padding:9px 10px; border:1px solid #75414b; border-radius:2px; background:#3a2026; color:#ffc5ce; font-size:12px; line-height:1.45; }
        .error.show { display:block; }
        footer { display:flex; flex-wrap:wrap; justify-content:flex-end; gap:8px; padding:10px 16px 12px; border-top:1px solid #4a535d; background:#292f36; }
        button { min-height:30px; padding:6px 14px; border:1px solid #5b6773; border-radius:2px; color:inherit; font:500 12px/1 inherit; cursor:pointer; transition:background-color 120ms,transform 120ms,opacity 120ms; }
        button:focus-visible { outline:2px solid #75bdf0; outline-offset:1px; }
        button:active:not(:disabled) { transform:scale(.98); }
        button:disabled { cursor:wait; opacity:.55; }
        .secondary { background:#3a424b; }
        .secondary:hover:not(:disabled) { background:#46515c; }
        .primary { background:#246da5; border-color:#6ea9d2; }
        .primary:hover:not(:disabled) { background:#2d82bd; }
        .cancel { margin-right:auto; background:#343b43; color:#c4cdd6; }
        .cancel:hover:not(:disabled) { background:#424c56; color:#fff; }
        @media (max-width:560px) { .info-grid { grid-template-columns:100px 1fr; } .info-grid b:nth-of-type(2) { grid-column:2; } }
        @media (prefers-reduced-motion: reduce) { .backdrop,.card,.bar,button { transition:none; } }
      </style>
      <div class="backdrop" role="presentation">
        <section class="card" role="dialog" aria-modal="true" aria-labelledby="pdm-title" aria-describedby="pdm-detail">
          <header><div class="icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v12m0 0 4-4m-4 4-4-4M5 21h14" stroke-linecap="round" stroke-linejoin="round"/></svg></div><div><h1 id="pdm-title">Parallel Download Manager</h1><p class="subtitle" id="pdm-subtitle">IDM-inspired segmented downloading.</p></div></header>
          <div class="body"><div class="file"><span class="filename" id="pdm-filename"></span><span class="host" id="pdm-host"></span></div><div class="info-grid"><span>File size</span><b id="pdm-size">—</b><span>Downloaded</span><b id="pdm-downloaded">—</b><span>Transfer rate</span><b id="pdm-rate">—</b><span>Time left</span><b id="pdm-eta">—</b></div><p class="detail" id="pdm-detail"></p><div class="progress-wrap" id="pdm-progress-wrap"><div class="bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"><span id="pdm-bar"></span></div><div class="connections" id="pdm-connections" aria-label="Segment progress"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></div></div><div class="error" id="pdm-error" role="alert"></div></div>
          <footer><button class="cancel" id="pdm-cancel" type="button">Cancel</button><button class="secondary" id="pdm-normal" type="button">Normal download</button><button class="primary" id="pdm-segmented" type="button">Segmented download</button></footer>
        </section>
      </div>`;
    document.documentElement.appendChild(host);
    ui = {
      host, root, backdrop: root.querySelector('.backdrop'), card: root.querySelector('.card'), title: root.querySelector('#pdm-title'), subtitle: root.querySelector('#pdm-subtitle'), filename: root.querySelector('#pdm-filename'), hostName: root.querySelector('#pdm-host'), detail: root.querySelector('#pdm-detail'), size: root.querySelector('#pdm-size'), downloaded: root.querySelector('#pdm-downloaded'), rate: root.querySelector('#pdm-rate'), eta: root.querySelector('#pdm-eta'), connectionStrip: root.querySelector('#pdm-connections'), connections: [...root.querySelectorAll('#pdm-connections i')], progressWrap: root.querySelector('#pdm-progress-wrap'), bar: root.querySelector('.bar'), barFill: root.querySelector('#pdm-bar'), error: root.querySelector('#pdm-error'), cancel: root.querySelector('#pdm-cancel'), normal: root.querySelector('#pdm-normal'), segmented: root.querySelector('#pdm-segmented'), previousFocus: null,
    };
    ui.cancel.addEventListener('click', () => activeSession ? activeSession.cancel() : closeUi());
    ui.backdrop.addEventListener('click', (event) => { if (event.target === ui.backdrop && !activeSession) closeUi(); });
    return ui;
  }

  function showChoice(link) {
    const view = ensureUi();
    if (activeSession) return;
    view.previousFocus = document.activeElement;
    view.filename.textContent = link.filename;
    view.hostName.textContent = link.hostname;
    view.size.textContent = 'Checking…';
    view.downloaded.textContent = '0 B';
    view.rate.textContent = '—';
    view.eta.textContent = '—';
    view.connections.forEach((connection) => connection.classList.remove('on'));
    view.connections.forEach((connection) => connection.classList.remove('complete'));
    view.connections.forEach((connection) => { connection.hidden = true; connection.style.setProperty('--progress', '0%'); connection.setAttribute('aria-valuenow', '0'); });
    view.connectionStrip.style.setProperty('--connection-count', '1');
    view.title.textContent = 'Choose download method';
    view.subtitle.textContent = `IDM-inspired fast mode · up to ${CONNECTIONS} verified connections.`;
    view.detail.textContent = 'Your userscript manager may request one-time permission for the file host. Choose “Always allow domain” to avoid repeated prompts.';
    view.error.textContent = '';
    view.error.classList.remove('show');
    view.progressWrap.classList.remove('show');
    view.barFill.style.width = '0%';
    view.bar.setAttribute('aria-valuenow', '0');
    setButtons({ cancel: true, normal: true, segmented: true });
    view.cancel.textContent = 'Cancel';
    view.normal.textContent = 'Normal download';
    view.segmented.textContent = 'Segmented download';
    view.normal.onclick = () => normalDownload(link);
    view.segmented.onclick = () => startSegmentedDownload(link);
    view.backdrop.classList.add('visible');
    requestAnimationFrame(() => view.segmented.focus());
  }

  function closeUi() {
    if (!ui) return;
    ui.backdrop.classList.remove('visible');
    const previousFocus = ui.previousFocus;
    ui.previousFocus = null;
    if (previousFocus instanceof HTMLElement && previousFocus.isConnected) previousFocus.focus({ preventScroll: true });
  }

  function setButtons({ cancel, normal, segmented }) {
    const view = ensureUi();
    view.cancel.disabled = !cancel;
    view.normal.disabled = !normal;
    view.segmented.disabled = !segmented;
  }

  function showError(message, allowNormal, link) {
    const view = ensureUi();
    view.title.textContent = 'Segmented download unavailable';
    view.subtitle.textContent = 'This link cannot be safely downloaded in parallel.';
    view.detail.textContent = 'You can still use the browser’s normal download.';
    view.error.textContent = message;
    view.error.classList.add('show');
    view.progressWrap.classList.remove('show');
    view.connections.forEach((connection) => { connection.hidden = true; connection.classList.remove('on', 'complete'); connection.style.setProperty('--progress', '0%'); connection.setAttribute('aria-valuenow', '0'); });
    view.cancel.textContent = 'Close';
    view.normal.textContent = 'Continue normally';
    view.normal.onclick = () => normalDownload(link);
    setButtons({ cancel: true, normal: allowNormal, segmented: false });
  }

  function setProgress(downloaded, total, startedAt, complete, retries) {
    const view = ensureUi();
    const now = Date.now();
    const elapsedSeconds = Math.max((now - startedAt) / 1000, 0.001);
    const bytesPerSecond = downloaded / elapsedSeconds;
    const percent = total ? Math.min(100, (downloaded / total) * 100) : 0;
    view.barFill.style.width = `${percent}%`;
    view.bar.setAttribute('aria-valuenow', String(Math.floor(percent)));
    view.size.textContent = formatBytes(total);
    view.downloaded.textContent = `${formatBytes(downloaded)} (${Math.floor(percent)}%)`;
    view.rate.textContent = `${formatBytes(bytesPerSecond)}/s`;
    view.eta.textContent = total > downloaded && bytesPerSecond > 0 ? formatTime((total - downloaded) / bytesPerSecond) : complete ? 'Complete' : '—';
  }

  async function startSegmentedDownload(link) {
    if (activeSession) return;
    const view = ensureUi();
    let sink = null;
    let session = null;
    try {
      // The picker must run synchronously in the button's activation handler.
      if (typeof window.showSaveFilePicker === 'function' && window.isSecureContext) {
        try {
          const handle = await window.showSaveFilePicker({ suggestedName: link.filename });
          sink = new FileSink(handle);
        } catch (error) {
          if (error && error.name === 'AbortError') return;
          // showSaveFilePicker is unavailable (not HTTPS, not Chromium, etc.).
          // Fall through — memory-backed download will be used if the file fits.
        }
      }
      // Only treat missing-picker as an error when the file is too large for memory (checked later).
      session = new DownloadSession(link, sink);
      activeSession = session;
      setButtons({ cancel: true, normal: false, segmented: false });
      view.cancel.textContent = 'Cancel download';
      view.title.textContent = 'Checking segmented download';
      view.subtitle.textContent = sink ? 'Saving directly to the selected file.' : 'No save picker is available. The file will be assembled in memory if it is small enough.';
      view.detail.textContent = 'Verifying byte-range support…';
      view.progressWrap.classList.add('show');
      view.connections.forEach((connection) => { connection.hidden = true; connection.classList.remove('on', 'complete'); });
      view.error.classList.remove('show');
      await session.start();
    } catch (error) {
      if (session?.cancelled) return;
      try { await session?.sink?.abort(); } catch {}
      showError(error.message || 'The download could not be started.', true, link);
    } finally {
      if (activeSession === session) activeSession = null;
    }
  }

  class DownloadSession {
    constructor(link, sink) {
      this.link = link;
      this.sink = sink;
      this.cancelled = false;
      this.requests = new Set();
      this.retryTimers = new Map();
      this.rateLimitUntil = 0;
      this.rateLimitStrikes = 0;
      this.serializeRequests = false;
      this.requestGate = Promise.resolve();
      this.downloaded = 0;
      this.retries = 0;
      this.startedAt = 0;
      this.lastUiUpdate = 0;
      this.connectionTotals = [];
      this.connectionDownloaded = [];
    }

    async start() {
      const metadata = await requestRange(this.link.url, 0, 0, this);
      if (metadata.status !== 206) throw new Error(metadata.status === 200 ? 'The server ignored byte ranges.' : `Server returned HTTP ${metadata.status}.`);
      const range = parseContentRange(metadata.headers['content-range']);
      if (!range || range.start !== 0 || range.end !== 0 || range.total <= 0 || metadata.bytes.byteLength !== 1) throw new Error('The server returned an invalid byte-range response.');

      this.total = range.total;
      this.url = metadata.finalUrl || this.link.url;
      this.validator = strongEtag(metadata.headers.etag) || metadata.headers['last-modified'] || '';
      this.type = metadata.headers['content-type'] || 'application/octet-stream';
      this.filename = sanitizeFilename(filenameFromDisposition(metadata.headers['content-disposition']) || this.link.filename);

      if (!this.sink && this.total > MEMORY_LIMIT) throw new Error(`This ${formatBytes(this.total)} file needs direct disk access. Use a Chromium browser on HTTPS, or choose Normal download.`);
      if (!this.sink) this.sink = new BlobSink(this.total, this.type, this.filename);
      this.sink.total = this.total;
      if (this.sink instanceof BlobSink && this.total > MEMORY_WARNING_SIZE) {
        ensureUi().detail.textContent = `This file will temporarily use up to ${formatBytes(this.total)} of browser memory.`;
      }

      await this.sink.open();
      this.segments = buildSegments(this.total, this.total < MIN_SEGMENTED_SIZE ? this.total : CHUNK_SIZE);
      const workerCount = Math.min(CONNECTIONS, MAX_CONNECTIONS, this.segments.length);
      const view = ensureUi();
      view.connectionStrip.style.setProperty('--connection-count', String(workerCount));
      view.connections.forEach((connection, index) => { connection.hidden = index >= workerCount; });
      this.startedAt = Date.now();
      view.title.textContent = this.segments.length === 1 ? 'Downloading file' : 'Downloading in segments';
      view.subtitle.textContent = `${Math.min(CONNECTIONS, this.segments.length, MAX_CONNECTIONS)} connection${Math.min(CONNECTIONS, this.segments.length, MAX_CONNECTIONS) === 1 ? '' : 's'} · ${this.sink instanceof FileSink ? 'saving directly to disk' : 'memory assembly'}`;
      view.detail.textContent = `${formatBytes(this.total)} · ${this.filename}`;
      setProgress(0, this.total, this.startedAt, false, 0);

      await this.downloadSegments();
      this.throwIfCancelled();
      await this.sink.close();
      setProgress(this.total, this.total, this.startedAt, true, this.retries);
      view.title.textContent = 'Download complete';
      view.subtitle.textContent = 'Every received segment passed range validation.';
      view.detail.textContent = this.filename;
      view.cancel.textContent = 'Close';
      setButtons({ cancel: true, normal: false, segmented: false });
      activeSession = null;
    }

    async downloadSegments() {
      const workerCount = Math.min(CONNECTIONS, MAX_CONNECTIONS, this.segments.length);
      const queues = Array.from({ length: workerCount }, () => []);
      this.segments.forEach((segment, index) => queues[index % workerCount].push(segment));
      this.connectionTotals = queues.map((queue) => queue.reduce((total, segment) => total + segment.end - segment.start + 1, 0));
      this.connectionDownloaded = queues.map(() => 0);
      const worker = async (workerIndex) => {
        for (const segment of queues[workerIndex]) {
          this.throwIfCancelled();
          await this.downloadSegment(segment, workerIndex);
        }
        this.updateConnectionProgress(workerIndex, 100);
      };
      await Promise.all(Array.from({ length: workerCount }, (_, index) => worker(index)));
      this.throwIfCancelled();
    }

    async downloadSegment(segment, workerIndex) {
      let lastError = null;
      const segmentSize = segment.end - segment.start + 1;
      const connection = ensureUi().connections[workerIndex];
      if (connection) {
        connection.hidden = false;
        connection.classList.add('on');
      }
      for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
        this.throwIfCancelled();
        let releaseRequestSlot = null;
        try {
          releaseRequestSlot = await this.acquireRequestSlot();
          const response = await requestRange(this.url, segment.start, segment.end, this, this.validator, (loaded) => {
            const laneTotal = this.connectionTotals[workerIndex] || segmentSize;
            const laneDownloaded = this.connectionDownloaded[workerIndex] || 0;
            this.updateConnectionProgress(workerIndex, ((laneDownloaded + loaded) / laneTotal) * 100);
          });
          validateSegmentResponse(response, segment, this.total, this.validator);
          this.throwIfCancelled();
          await this.sink.write(segment.start, response.bytes);
          this.connectionDownloaded[workerIndex] += response.bytes.byteLength;
          this.updateConnectionProgress(workerIndex, (this.connectionDownloaded[workerIndex] / this.connectionTotals[workerIndex]) * 100);
          this.downloaded += response.bytes.byteLength;
          this.updateUi();
          return;
        } catch (error) {
          lastError = error;
          if (error.status === 429) {
            this.rateLimitStrikes++;
            this.serializeRequests = true;
            const adaptiveBackoff = Math.min(120_000, 10_000 * (2 ** (this.rateLimitStrikes - 1)));
            const backoffMs = Math.max(error.backoffMs || 0, adaptiveBackoff);
            this.rateLimitUntil = Math.max(this.rateLimitUntil, Date.now() + backoffMs);
            const view = ensureUi();
            view.subtitle.textContent = 'Server rate limit detected · continuing with 1 connection';
            view.detail.textContent = `Cooling down for ${formatTime(backoffMs / 1000)} before retrying…`;
          }
          if (!isRetryable(error) || attempt === MAX_RETRIES - 1) break;
          this.retries++;
          this.updateUi();
          await this.waitForRetry(500 * (2 ** attempt) + Math.floor(Math.random() * 250));
        } finally {
          releaseRequestSlot?.();
        }
      }
      throw lastError || new Error('A segment failed.');
    }

    updateConnectionProgress(workerIndex, percent) {
      const connection = ensureUi().connections[workerIndex];
      if (!connection) return;
      connection.style.setProperty('--progress', `${Math.max(0, Math.min(100, percent))}%`);
      connection.setAttribute('aria-valuenow', String(Math.floor(percent)));
      connection.setAttribute('aria-label', `Connection ${workerIndex + 1}: ${Math.floor(percent)}%`);
    }

    async acquireRequestSlot() {
      if (!this.serializeRequests) {
        await this.waitForRateLimit();
        return () => {};
      }

      const previous = this.requestGate;
      let release;
      this.requestGate = new Promise((resolve) => { release = resolve; });
      await previous;
      try {
        await this.waitForRateLimit();
        this.throwIfCancelled();
        return release;
      } catch (error) {
        release();
        throw error;
      }
    }

    async waitForRateLimit() {
      const remaining = this.rateLimitUntil - Date.now();
      if (remaining > 0) await this.waitForRetry(remaining);
      this.throwIfCancelled();
    }

    updateUi() {
      const now = Date.now();
      if (now - this.lastUiUpdate < 150 && this.downloaded !== this.total) return;
      this.lastUiUpdate = now;
      setProgress(this.downloaded, this.total, this.startedAt, false, this.retries);
    }

    waitForRetry(ms) {
      return new Promise((resolve) => {
        const timer = setTimeout(() => { this.retryTimers.delete(timer); resolve(); }, ms);
        this.retryTimers.set(timer, resolve);
      });
    }

    throwIfCancelled() {
      if (this.cancelled) throw new Error('Download cancelled.');
    }

    async cancel() {
      if (this.cancelled) return;
      this.cancelled = true;
      for (const request of this.requests) request.abort();
      this.requests.clear();
      for (const [timer, resolve] of this.retryTimers) {
        clearTimeout(timer);
        resolve();
      }
      this.retryTimers.clear();
      try { await this.sink?.abort(); } catch {}
      const view = ensureUi();
      view.title.textContent = 'Download cancelled';
      view.subtitle.textContent = 'Active requests were stopped.';
      view.detail.textContent = 'No completed file was reported.';
      view.progressWrap.classList.remove('show');
      view.cancel.textContent = 'Close';
      setButtons({ cancel: true, normal: false, segmented: false });
      activeSession = null;
    }
  }

  class FileSink {
    constructor(handle) { this.handle = handle; this.writable = null; this.writeChain = Promise.resolve(); }
    async open() { this.writable = await this.handle.createWritable(); }
    async write(position, bytes) {
      this.writeChain = this.writeChain.then(() => this.writable.write({ type: 'write', position, data: bytes }));
      return this.writeChain;
    }
    async close() { await this.writeChain; await this.writable.truncate(this.total); await this.writable.close(); }
    async abort() { await this.writeChain.catch(() => {}); if (this.writable) await this.writable.abort(); }
  }

  class BlobSink {
    constructor(total, type, filename) { this.total = total; this.type = type; this.filename = filename; this.parts = new Map(); }
    async open() {}
    async write(position, bytes) { this.parts.set(position, bytes); }
    async close() {
      const parts = [...this.parts.entries()].sort((a, b) => a[0] - b[0]);
      let expected = 0;
      for (const [position, bytes] of parts) {
        if (position !== expected) throw new Error('A downloaded segment is missing.');
        expected += bytes.byteLength;
      }
      if (expected !== this.total) throw new Error('Downloaded bytes do not match the expected file size.');
      const blob = new Blob(parts.map(([, bytes]) => bytes), { type: this.type });
      const objectUrl = URL.createObjectURL(blob);
      const anchor = document.createElement('a');
      anchor.href = objectUrl;
      anchor.download = this.filename;
      bypassedAnchors.add(anchor);
      scriptAnchors.add(anchor);
      document.documentElement.appendChild(anchor);
      anchor.click();
      anchor.remove();
      setTimeout(() => URL.revokeObjectURL(objectUrl), 60_000);
      this.parts.clear();
    }
    async abort() { this.parts.clear(); }
  }

  function requestRange(url, start, end, session, validator = '', onProgress = null) {
    return new Promise((resolve, reject) => {
      let settled = false;
      const headers = { Range: `bytes=${start}-${end}`, Accept: '*/*' };
      if (validator) headers['If-Range'] = validator;
      const request = GM_xmlhttpRequest({
        method: 'GET', url, headers, responseType: 'arraybuffer', timeout: REQUEST_TIMEOUT,
        onprogress: (event) => { if (onProgress && Number.isFinite(event.loaded)) onProgress(event.loaded); },
        onload: (response) => finish(resolve, { status: response.status, headers: parseHeaders(response.responseHeaders), bytes: response.response || new ArrayBuffer(0), finalUrl: response.finalUrl || url }),
        onerror: () => fail(new Error('Network request failed.')),
        ontimeout: () => fail(Object.assign(new Error('Request timed out.'), { retryable: true })),
        onabort: () => fail(new Error('Download cancelled.')),
      });
      session.requests.add(request);
      function finish(callback, value) {
        if (settled) return;
        settled = true;
        session.requests.delete(request);
        callback(value);
      }
      function fail(error) { finish(reject, error); }
    });
  }

  function validateSegmentResponse(response, segment, total, validator) {
    if (response.status !== 206) {
      let retryable = response.status === 408 || response.status === 429 || response.status >= 500;
      let backoffMs = 0;
      if (response.status === 429) {
        backoffMs = parseRetryAfter(response.headers['retry-after']);
      }
      throw Object.assign(new Error(response.status === 200 ? 'The server stopped honoring byte ranges.' : `Segment request returned HTTP ${response.status}.`), { retryable, backoffMs, status: response.status });
    }
    const range = parseContentRange(response.headers['content-range']);
    const expectedLength = segment.end - segment.start + 1;
    if (!range || range.start !== segment.start || range.end !== segment.end || range.total !== total) throw new Error('The server returned a mismatched byte range.');
    if (response.bytes.byteLength !== expectedLength) throw Object.assign(new Error('The server returned a truncated segment.'), { retryable: true });
    if (validator && response.headers.etag && strongEtag(response.headers.etag) && strongEtag(response.headers.etag) !== validator) throw new Error('The file changed while downloading.');
  }

  function buildSegments(total, chunkSize) {
    const segments = [];
    for (let start = 0; start < total; start += chunkSize) segments.push({ start, end: Math.min(total - 1, start + chunkSize - 1) });
    return segments;
  }

  function parseHeaders(raw) {
    const headers = {};
    for (const line of String(raw || '').trim().split(/\r?\n/)) {
      const index = line.indexOf(':');
      if (index > 0) headers[line.slice(0, index).trim().toLowerCase()] = line.slice(index + 1).trim();
    }
    return headers;
  }

  function parseContentRange(value) {
    const match = String(value || '').match(/^bytes\s+(\d+)-(\d+)\/(\d+)$/i);
    if (!match) return null;
    const start = Number(match[1]); const end = Number(match[2]); const total = Number(match[3]);
    return Number.isSafeInteger(start) && Number.isSafeInteger(end) && Number.isSafeInteger(total) && start <= end && end < total ? { start, end, total } : null;
  }

  function parseRetryAfter(value) {
    const retryAfter = String(value || '').trim();
    if (!retryAfter) return 0;
    if (/^\d+$/.test(retryAfter)) return Number(retryAfter) * 1000;
    const retryAt = Date.parse(retryAfter);
    return Number.isFinite(retryAt) ? Math.max(0, retryAt - Date.now()) : 0;
  }

  function strongEtag(value) { return value && !/^W\//i.test(value) ? value : ''; }
  function isRetryable(error) { return Boolean(error?.retryable) || /Network request failed|Request timed out|truncated segment/i.test(error?.message || ''); }
  function filenameFromUrl(url) { try { return decodeURIComponent(url.pathname.split('/').pop() || ''); } catch { return ''; } }
  function filenameFromOneDrivePage() {
    if (!isOneDriveHost(location.hostname)) return '';
    const title = document.title.replace(/\s*[-–|]\s*(?:Microsoft\s+)?OneDrive.*$/i, '').trim();
    return title && !/^OneDrive$/i.test(title) ? title : '';
  }
  function filenameFromDisposition(value) {
    const utf8 = String(value || '').match(/filename\*\s*=\s*UTF-8''([^;]+)/i);
    if (utf8) { try { return decodeURIComponent(utf8[1]); } catch {} }
    const plain = String(value || '').match(/filename\s*=\s*(?:"([^"]+)"|([^;\s]+))/i);
    return plain ? plain[1] || plain[2] : '';
  }
  function sanitizeFilename(name) {
    const cleaned = String(name || 'download.bin').replace(/[\u0000-\u001f\\/:*?"<>|]+/g, '_').replace(/^\.+/, '').replace(/[.\s]+$/, '').slice(0, 180);
    return cleaned || 'download.bin';
  }
  function formatBytes(bytes) {
    if (!Number.isFinite(bytes) || bytes < 1024) return `${Math.max(0, Math.round(bytes || 0))} B`;
    const units = ['KB', 'MB', 'GB', 'TB']; let value = bytes / 1024; let index = 0;
    while (value >= 1024 && index < units.length - 1) { value /= 1024; index++; }
    return `${value >= 100 ? value.toFixed(0) : value.toFixed(1)} ${units[index]}`;
  }
  function formatTime(seconds) { if (!Number.isFinite(seconds) || seconds < 1) return 'a moment'; const m = Math.floor(seconds / 60); const s = Math.round(seconds % 60); return m ? `${m}m ${s}s` : `${s}s`; }

  document.addEventListener('click', (event) => {
    const candidate = isEligibleClick(event);
    if (!candidate) return;
    if (activeSession) return;
    event.preventDefault();
    event.stopImmediatePropagation();
    showChoice(snapshotLink(candidate.anchor, candidate.url, candidate.control || null));
  }, true);

  document.addEventListener('keydown', (event) => {
    if (event.key === 'Escape' && ui?.backdrop.classList.contains('visible')) {
      if (activeSession) activeSession.cancel(); else closeUi();
    }
  }, true);
})();