IDM-inspired fast, verified parallel downloads for recognized direct file links.
// ==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);
})();