Parallel Download Manager

IDM-inspired fast, verified parallel downloads for recognized direct file links.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Parallel Download Manager
// @namespace    https://github.com/paurakh/userscripts
// @version      0.4.4
// @description  IDM-inspired fast, verified parallel downloads for recognized direct file links.
// @author       Px
// @homepageURL  https://github.com/paurakh/userscripts
// @supportURL   https://github.com/paurakh/userscripts/issues
// @license      MIT
// @match        http://*/*
// @match        https://*/*
// @noframes
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @connect      github.com
// @connect      objects.githubusercontent.com
// @connect      release-assets.githubusercontent.com
// @connect      *
// @run-at       document-start
// ==/UserScript==
// Copyright (c) 2026 Px. Licensed under MIT.

(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;
  // Page-like extensions that are almost certainly HTML navigation, not file downloads.
  const PAGE_EXTENSIONS = /\.(?:html?|php|asp|aspx|jsp|cfm|cgi|pl|py|rb)(?:$|[?#])/i;
  const DOWNLOAD_ACTION_TEXT = /\b(?:direct\s+download|free\s+download|download(?:\s+(?:file|now|here|[\d.]+\s*(?:kb|mb|gb|tb)))?|export\s+(?:file|data)|get\s+file|save\s+file)\b/i;
  const DOWNLOAD_URL_HINT = /(?:^|[/?#&=_-])(?:download|downloads|dl|file|files|attachment|export)(?:$|[/?#&=_-])/i;
  const NON_DOWNLOAD_TEXT = /\b(?:download\s+(?:manager|page|app|extension|software|instructions?|guide)|downloads?\s+(?:page|section|category)|how\s+to\s+download)\b/i;

  const bypassedAnchors = new WeakSet();
  const scriptAnchors = new WeakSet();
  let activeSession = null;
  let ui = null;
  let lastClickDecision = 'No eligible download link has been clicked yet.';

  function isEligibleClick(event) {
    if (!event.isTrusted) return rejectClick('The event was generated by a script, not a user click.');
    if (event.button !== 0) return rejectClick('Only an unmodified primary-button click is intercepted.');
    if (event.defaultPrevented) return rejectClick('The page handled this click before the download manager could inspect it.');
    if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return rejectClick('Modifier-clicks stay with the browser so opening a new tab still works.');

    const anchor = anchorFromEvent(event);
    if (!anchor) {
      const oneDriveCandidate = oneDriveDownloadCandidate(event.target);
      return oneDriveCandidate || rejectClick('This control is not a direct HTTP(S) link. Use “Download URL…” from the userscript menu.');
    }
    if (bypassedAnchors.has(anchor) || scriptAnchors.has(anchor)) return rejectClick('This click was started by the download manager.');

    let url;
    try { url = new URL(anchor.href, document.baseURI); } catch { return rejectClick('The link URL could not be read.'); }
    if (!/^https?:$/.test(url.protocol)) return rejectClick('Only HTTP(S) download URLs are supported.');
    if (url.hash && url.pathname === location.pathname && url.search === location.search) return rejectClick('In-page links are not downloads.');

    const hasDownloadAttribute = anchor.hasAttribute('download');
    const pathLooksDownloadable = DOWNLOAD_EXTENSIONS.test(url.pathname + url.search);
    const pathLooksPage = PAGE_EXTENSIONS.test(url.pathname + url.search);

    // If the URL points to an obvious web page (not a direct file download), let the browser navigate normally.
    // Also treat extensionless paths as likely page navigation unless evidence is very strong.
    if (!hasDownloadAttribute && !pathLooksDownloadable) {
      if (pathLooksPage) return rejectClick('This link points to a web page, not a direct file download.');

      const pathHasExt = /\.[a-z0-9]{1,8}(?:$|[?#])/i.test(url.pathname);
      if (!pathHasExt) {
        // No extension at all — likely a page. Require strong, concise download signal.
        const evidence = downloadEvidence(anchor, url);
        // For extensionless paths, the link text must be a short, button-like label (≤ 25 chars)
        // and must NOT match the non-download pattern.
        const visibleText = String(anchor.textContent || '').replace(/\s+/g, ' ').trim();
        const isButtonLabel = visibleText.length <= 25 && DOWNLOAD_ACTION_TEXT.test(visibleText);
        const isNonDownloadPage = NON_DOWNLOAD_TEXT.test(visibleText);
        if (evidence.score < 2 || isNonDownloadPage || !isButtonLabel) {
          return rejectClick(`This link looks like page navigation, not a direct file download (score ${evidence.score}/2). Use "Download URL…" from the userscript menu.`);
        }
        lastClickDecision = `Recognized a direct file link: ${evidence.reasons.join(', ')}.`;
        return { anchor, url };
      }
    }

    const evidence = downloadEvidence(anchor, url);
    if (!hasDownloadAttribute && !pathLooksDownloadable && evidence.score < 2) return rejectClick(`This link lacks enough direct-download evidence (${evidence.reasons.join(', ') || 'no download signals'}; score ${evidence.score}/2). Use “Download URL…” from the userscript menu.`);

    lastClickDecision = `Recognized a direct file link: ${hasDownloadAttribute ? 'download attribute' : pathLooksDownloadable ? 'file extension' : evidence.reasons.join(', ')}.`;
    return { anchor, url };
  }

  function downloadEvidence(anchor, url) {
    const visibleText = String(anchor.textContent || '').replace(/\s+/g, ' ').trim();
    const accessibleLabel = `${anchor.getAttribute('aria-label') || ''} ${anchor.getAttribute('title') || ''}`.replace(/\s+/g, ' ').trim();
    const metadata = `${anchor.id || ''} ${typeof anchor.className === 'string' ? anchor.className : ''} ${anchor.getAttribute('rel') || ''} ${anchor.getAttribute('role') || ''}`;
    const urlText = `${url.pathname}${url.search}`;
    const combinedLabel = `${accessibleLabel} ${visibleText}`.trim();
    const reasons = [];
    let score = 0;

    if (combinedLabel.length <= 120 && DOWNLOAD_ACTION_TEXT.test(combinedLabel) && !NON_DOWNLOAD_TEXT.test(combinedLabel)) {
      score += 2;
      reasons.push('download action label');
    }
    if (DOWNLOAD_URL_HINT.test(urlText)) {
      score += 1;
      reasons.push('download-like URL');
    }
    if (/\b(?:download|downloads|download-btn|download-button|dl-btn)\b/i.test(metadata)) {
      score += 1;
      reasons.push('download element metadata');
    }
    if (/\b(?:download|arrow-down|file-download|save-alt)\b/i.test(`${anchor.querySelector('svg,use,i')?.getAttribute('aria-label') || ''} ${anchor.querySelector('svg,use,i')?.getAttribute('data-icon') || ''} ${anchor.querySelector('use')?.getAttribute('href') || ''}`)) {
      score += 1;
      reasons.push('download icon');
    }
    if (anchor.target?.toLowerCase() === '_blank' && score < 2) {
      score = Math.max(0, score - 1);
      reasons.push('ambiguous new-tab target');
    }
    return { score, reasons };
  }

  function rejectClick(reason) {
    lastClickDecision = reason;
    return null;
  }

  function anchorFromEvent(event) {
    const path = typeof event.composedPath === 'function' ? event.composedPath() : [];
    for (const node of path) {
      if (node instanceof HTMLAnchorElement && node.href) return node;
      if (node instanceof Element) {
        const anchor = node.closest('a[href]');
        if (anchor) return anchor;
      }
    }
    return event.target instanceof Element ? event.target.closest('a[href]') : null;
  }

  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');
    lastClickDecision = 'Recognized a OneDrive download control.';
    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') || '',
      anchor,
      nativeControl,
      filename: sanitizeFilename(anchor?.getAttribute('download') || filenameFromOneDrivePage() || filenameFromUrl(url) || 'download.bin'),
    };
  }

  function normalDownload(link) {
    closeUi();
    const nativeTarget = link.nativeControl || link.anchor;
    if (nativeTarget?.isConnected) {
      bypassedAnchors.add(nativeTarget);
      nativeTarget.click();
      queueMicrotask(() => bypassedAnchors.delete(nativeTarget));
      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; visibility:hidden; background:rgba(0,0,0,.68); pointer-events:none; opacity:0; transition:opacity 140ms ease,visibility 0s linear 140ms; }
        .backdrop.visible { visibility:visible; pointer-events:auto; opacity:1; transition:opacity 140ms ease; }
        .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; }
        .url-entry { display:none; margin-top:12px; }
        .url-entry.show { display:block; }
        .url-entry label { display:block; margin-bottom:6px; color:#c5ced7; font-size:12px; }
        .url-entry input { width:100%; min-height:38px; padding:8px 10px; border:1px solid #56616c; border-radius:2px; background:#101317; color:#f0f4f7; font:12px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace; }
        .url-entry input:focus { outline:2px solid #75bdf0; outline-offset:1px; }
        .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:40px; 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(.96); }
        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="url-entry" id="pdm-url-entry"><label for="pdm-url">Direct HTTP(S) download URL</label><input id="pdm-url" type="url" inputmode="url" autocomplete="off" spellcheck="false" placeholder="https://example.com/file.zip"></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">Browser 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'), urlEntry: root.querySelector('#pdm-url-entry'), urlInput: root.querySelector('#pdm-url'), 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.urlEntry.classList.remove('show');
    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. Browser download uses Chrome’s normal download UI.';
    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 = 'Browser 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 showManualDownloadDialog() {
    if (activeSession) return;
    const view = ensureUi();
    view.previousFocus = document.activeElement;
    view.title.textContent = 'Download a direct URL';
    view.subtitle.textContent = 'Use this when a page download link does not show the choice dialog.';
    view.filename.textContent = 'Paste a direct file URL';
    view.hostName.textContent = window.isSecureContext && typeof window.showSaveFilePicker === 'function' ? 'Direct-to-disk saving is available in this tab.' : 'Files will use memory assembly unless direct-to-disk saving is available.';
    view.urlEntry.classList.add('show');
    view.urlInput.value = '';
    view.size.textContent = '—';
    view.downloaded.textContent = '—';
    view.rate.textContent = '—';
    view.eta.textContent = '—';
    view.detail.textContent = `Status: active. Last click check: ${lastClickDecision}`;
    view.error.textContent = '';
    view.error.classList.remove('show');
    view.progressWrap.classList.remove('show');
    view.cancel.textContent = 'Cancel';
    view.normal.textContent = 'Browser download';
    view.segmented.textContent = 'Continue';
    view.normal.onclick = null;
    view.segmented.onclick = () => {
      const link = linkFromManualUrl(view.urlInput.value);
      if (!link) {
        view.error.textContent = 'Enter a valid HTTP(S) URL. Signed download links are supported if they work directly in this browser.';
        view.error.classList.add('show');
        view.urlInput.focus();
        return;
      }
      showChoice(link);
    };
    view.urlInput.onkeydown = (event) => { if (event.key === 'Enter') view.segmented.click(); };
    setButtons({ cancel: true, normal: false, segmented: true });
    view.backdrop.classList.add('visible');
    requestAnimationFrame(() => view.urlInput.focus());
  }

  function linkFromManualUrl(value) {
    let url;
    try { url = new URL(String(value || '').trim()); } catch { return null; }
    if (!/^https?:$/.test(url.protocol)) return null;
    return snapshotLink(null, url);
  }

  function closeUi() {
    if (!ui) return;
    ui.backdrop.classList.remove('visible');
    ui.urlEntry.classList.remove('show');
    ui.urlInput.onkeydown = null;
    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 downloaded in parallel.';
    view.detail.textContent = allowNormal ? 'Use "Browser download" below to download the traditional way.' : 'This download cannot proceed.';
    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 = 'Browser download';
    view.normal.onclick = allowNormal ? () => normalDownload(link) : null;
    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.failed = false;
      this.sinkAborted = 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() {
      let metadata;
      try {
        metadata = await requestRange(this.link.url, 0, 0, this);
        if (metadata.status !== 206) throw new Error(metadata.status === 200 ? 'The server does not support byte-range (partial) requests, so this file cannot be downloaded in parallel segments. Use "Browser download" instead.' : `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.etag = strongEtag(metadata.headers.etag);
        this.lastModified = metadata.headers['last-modified'] || '';
        this.validator = this.etag || this.lastModified;
        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 Browser 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;
      } catch (error) {
        await this.stop(error);
        throw error;
      }
    }

    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.etag, this.lastModified);
          this.throwIfCancelled();
          if (this.sinkAborted) throw new Error('Download storage is no longer available.');
          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 || this.failed) throw new Error('Download cancelled.');
    }

    async stop(error = null) {
      if (this.cancelled || this.failed) return;
      this.cancelled = !error;
      this.failed = Boolean(error);
      for (const request of this.requests) request.abort();
      this.requests.clear();
      for (const [timer, resolve] of this.retryTimers) {
        clearTimeout(timer);
        resolve();
      }
      this.retryTimers.clear();
      this.sinkAborted = true;
      try { await this.sink?.abort(); } catch {}
    }

    async cancel() {
      if (this.cancelled || this.failed) return;
      await this.stop();
      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;
      let loadedBytes = 0;
      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)) {
            loadedBytes = event.loaded;
            onProgress(event.loaded);
          }
        },
        onload: (response) => {
          let bytes = null;
          // Some userscript managers put the arraybuffer in response.response;
          // others may provide it via response.responseText (binary string).
          if (response.response instanceof ArrayBuffer && response.response.byteLength > 0) {
            bytes = response.response;
          } else if (typeof response.responseText === 'string' && response.responseText.length > 0) {
            // Fallback: convert binary string to ArrayBuffer.
            const len = response.responseText.length;
            bytes = new ArrayBuffer(len);
            const view = new Uint8Array(bytes);
            for (let i = 0; i < len; i++) view[i] = response.responseText.charCodeAt(i) & 0xff;
          } else {
            bytes = new ArrayBuffer(0);
          }
          finish(resolve, { status: response.status, headers: parseHeaders(response.responseHeaders), bytes, 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, etag = '', lastModified = '') {
    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 the server returned more bytes than requested (e.g. the full file despite a Range),
    // extract only the portion we need from the oversized response.
    if (response.bytes.byteLength !== expectedLength) {
      if (response.bytes.byteLength > expectedLength) {
        // Server sent too much data. Extract the correct slice.
        response.bytes = response.bytes.slice(0, expectedLength);
      } else {
        // Truncated — retry.
        const msg = `Segment truncated: received ${formatBytes(response.bytes.byteLength)} of ${formatBytes(expectedLength)}. Retrying…`;
        throw Object.assign(new Error(msg), { retryable: true });
      }
    }

    if (etag && strongEtag(response.headers.etag) && strongEtag(response.headers.etag) !== etag) throw new Error('The file changed while downloading.');
    if (lastModified && response.headers['last-modified'] && response.headers['last-modified'] !== lastModified) 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 truncated/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`; }

  GM_registerMenuCommand('Parallel Download Manager: Download URL…', showManualDownloadDialog, 'd');
  GM_registerMenuCommand('Parallel Download Manager: Status', () => {
    const saveMode = window.isSecureContext && typeof window.showSaveFilePicker === 'function' ? 'direct-to-disk saving available' : 'memory assembly fallback';
    alert(`Parallel Download Manager is active.\n\nSave mode: ${saveMode}\nLast click check: ${lastClickDecision}\n\nUse “Download URL…” for direct links the page does not expose as a standard download anchor.`);
  });

  document.addEventListener('click', (event) => {
    const candidate = isEligibleClick(event);
    if (!candidate) return;
    if (activeSession) return;
    event.preventDefault();
    event.stopImmediatePropagation();
    showChoice(snapshotLink(candidate.anchor || null, 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);
})();