Greasy Fork is available in English.
Desktop-only queue helper for MEGA folder pages. Scans files, captures metadata, supports checkboxes and copy selected/all.
// ==UserScript==
// @name MEGA Desktop - Auto Queue + Metadata
// @namespace http://tampermonkey.net/
// @version 2.5.3
// @description Desktop-only queue helper for MEGA folder pages. Scans files, captures metadata, supports checkboxes and copy selected/all.
// @author adapted
// @license MIT
// @match *://mega.nz/folder/*
// @match *://mega.io/folder/*
// @grant GM_setClipboard
// @grant GM_openInTab
// @run-at document-idle
// ==/UserScript==
(function () {
"use strict";
let panel = null;
let listEl = null;
let queue = [];
let scanning = false;
function getBaseUrl() {
const match = window.location.href.match(/^(https?:\/\/[^/]+\/folder\/[^#]+#[^/]+)/);
return match ? match[1] : null;
}
function normaliseHandle(value) {
if (value === null || value === undefined) return null;
let h = String(value).trim();
if (!h) return null;
if (window.M && window.M.d && window.M.d[h]) return h;
if (window.M && window.M.d) {
const known = Object.keys(window.M.d).find((k) => h.includes(k));
if (known) return known;
}
const tokens = h.match(/[A-Za-z0-9_-]{6,}/g);
if (tokens && tokens.length) h = tokens[tokens.length - 1];
return h.length > 5 && !h.includes(" ") ? h : null;
}
function addHandlesFrom(source, out) {
if (!source) return;
if (typeof source === "string") {
const h = normaliseHandle(source);
if (h) out.push(h);
return;
}
if (Array.isArray(source) || source instanceof Set) {
[...source].forEach((v) => {
const h = normaliseHandle(v && (v.h || v.id || v.handle || v.nodeHandle || v));
if (h) out.push(h);
});
return;
}
if (typeof source === "object") {
["selected", "selected_list", "items"].forEach((k) => source[k] && addHandlesFrom(source[k], out));
Object.keys(source).forEach((k) => {
const v = source[k];
const keyHandle = normaliseHandle(k);
if (keyHandle && (v === true || v === 1 || typeof v === "object")) out.push(keyHandle);
const valueHandle = normaliseHandle(v && (v.h || v.id || v.handle || v.nodeHandle));
if (valueHandle) out.push(valueHandle);
});
}
}
function getSelectedHandles() {
const handles = [];
if (window.$ && window.$.selected) addHandlesFrom(window.$.selected, handles);
if (window.selectionManager) {
if (typeof window.selectionManager.get_selected === "function") {
addHandlesFrom(window.selectionManager.get_selected() || [], handles);
}
addHandlesFrom(window.selectionManager.selected_list, handles);
addHandlesFrom(window.selectionManager.selected, handles);
}
const sels = document.querySelectorAll(
[
".ui-selected",
".data-block-view.selected",
"tr.selected",
".grid-node.selected",
".file.selected",
".folder.selected",
".megaListItem.selected",
'[aria-selected="true"]',
].join(",")
);
sels.forEach((el) => {
["data-id", "data-h", "data-handle", "data-node-handle", "id"].forEach((attr) => {
const h = normaliseHandle(el.getAttribute(attr));
if (h) handles.push(h);
});
});
return [...new Set(handles)];
}
function getNode(handle) {
return (window.M && window.M.d && window.M.d[handle]) ||
(window.M && window.M.v && window.M.v.find((n) => n.h === handle)) ||
null;
}
function getChildrenHandles(parent) {
const children = [];
if (window.M && window.M.c && window.M.c[parent]) {
Object.keys(window.M.c[parent]).forEach((h) => children.push(h));
}
if (window.M && window.M.d) {
Object.keys(window.M.d).forEach((h) => {
const node = window.M.d[h];
if (node && node.p === parent) children.push(node.h || h);
});
}
return [...new Set(children)];
}
function getAllDescendantFileHandles(folderHandle) {
const out = [];
const seen = new Set();
const stack = [folderHandle];
while (stack.length) {
const parent = stack.pop();
if (!parent || seen.has(parent)) continue;
seen.add(parent);
getChildrenHandles(parent).forEach((h) => {
const node = getNode(h);
if (node && node.t === 1) stack.push(node.h || h);
else out.push((node && node.h) || h);
});
}
return [...new Set(out)];
}
function formatBytes(size) {
if (typeof size !== "number" || size < 0) return "Unknown";
const units = ["B", "KB", "MB", "GB", "TB"];
let n = size;
let i = 0;
while (n >= 1024 && i < units.length - 1) {
n /= 1024;
i += 1;
}
return n.toFixed(i === 0 ? 0 : 2) + " " + units[i];
}
function formatDuration(seconds) {
if (!seconds || !Number.isFinite(seconds)) return "Unknown";
const total = Math.round(seconds);
const h = Math.floor(total / 3600);
const m = Math.floor((total % 3600) / 60);
const s = total % 60;
if (h > 0) return String(h).padStart(2, "0") + ":" + String(m).padStart(2, "0") + ":" + String(s).padStart(2, "0");
return String(m).padStart(2, "0") + ":" + String(s).padStart(2, "0");
}
function extractResolution(node) {
if (!node) return "Unknown";
if (node.width && node.height) return node.width + "x" + node.height;
const fa = String(node.fa || "");
const match = fa.match(/(\d{3,5})x(\d{3,5})/);
return match ? match[1] + "x" + match[2] : "Unknown";
}
function getMeta(node) {
return {
size: formatBytes(node && node.s),
length: formatDuration((node && (node.playtime || node.duration || node.dur)) || 0),
resolution: extractResolution(node),
};
}
function escapeHtml(value) {
return String(value).replace(/[&<>'"]/g, (c) => ({
"&": "&",
"<": "<",
">": ">",
"'": "'",
'"': """,
}[c]));
}
function makeItem(handle) {
const baseUrl = getBaseUrl();
if (!baseUrl) return null;
const node = getNode(handle);
const meta = getMeta(node);
return {
handle: handle,
name: (node && (node.name || node.n)) || handle,
url: baseUrl + "/file/" + handle,
size: meta.size,
length: meta.length,
resolution: meta.resolution,
checked: true,
status: "Queued",
};
}
function pushUnique(handles) {
const existing = new Set(queue.map((x) => x.url));
let added = 0;
handles.forEach((h) => {
const item = makeItem(h);
if (item && !existing.has(item.url)) {
queue.push(item);
existing.add(item.url);
added += 1;
}
});
return added;
}
function getCurrentFolderFileHandles() {
if (window.M && window.M.currentdirid && window.M.d && window.M.d[window.M.currentdirid]) {
return getAllDescendantFileHandles(window.M.currentdirid);
}
if (window.M && Array.isArray(window.M.v)) {
return window.M.v.filter((n) => n && n.t !== 1).map((n) => n.h).filter(Boolean);
}
return [];
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function getScrollableContainers() {
const selectors = [
".fm-right-files-block",
".files-grid-view",
".grid-scrolling-table",
".file-block-scrolling",
".megaListContainer",
".megaList",
".ps",
];
const elements = [];
selectors.forEach((s) => {
document.querySelectorAll(s).forEach((el) => elements.push(el));
});
// Fallback: any scrollable element that looks like a list viewport.
document.querySelectorAll("div").forEach((el) => {
const cs = window.getComputedStyle(el);
const canScroll = (cs.overflowY === "auto" || cs.overflowY === "scroll") && el.scrollHeight > el.clientHeight + 80;
if (canScroll) elements.push(el);
});
return [...new Set(elements)].filter((el) => el && el.scrollHeight > el.clientHeight + 20);
}
async function hydrateVisibleListWithoutManualScroll() {
const containers = getScrollableContainers();
if (!containers.length) return;
let lastCount = getCurrentFolderFileHandles().length;
let stableRounds = 0;
for (let round = 0; round < 45 && stableRounds < 4; round += 1) {
containers.forEach((el) => {
// Scroll in chunks so virtualized rows get mounted and added into MEGA's in-memory nodes.
const nextTop = Math.min(el.scrollTop + Math.max(300, el.clientHeight), el.scrollHeight);
el.scrollTop = nextTop;
el.dispatchEvent(new Event("scroll", { bubbles: true }));
});
await sleep(160);
const count = getCurrentFolderFileHandles().length;
if (count > lastCount) {
lastCount = count;
stableRounds = 0;
} else {
stableRounds += 1;
}
updateStatus("Loading hidden rows... found " + count + " file handles");
}
}
async function autoScanAll() {
if (scanning) return;
scanning = true;
updateStatus("Scanning all files in current folder tree...");
// MEGA desktop virtualizes long lists. This forces lazy rows to load without manual scrolling.
await hydrateVisibleListWithoutManualScroll();
const handles = getCurrentFolderFileHandles();
if (!handles.length) {
scanning = false;
updateStatus("No files found in current view/folder.");
return;
}
const existing = new Set(queue.map((x) => x.url));
let added = 0;
for (let i = 0; i < handles.length; i += 1) {
const h = handles[i];
const item = makeItem(h);
if (item && !existing.has(item.url)) {
queue.push(item);
existing.add(item.url);
added += 1;
}
if (i % 20 === 0 || i === handles.length - 1) {
renderQueue();
updateStatus("Scanning " + (i + 1) + "/" + handles.length + " files...");
}
await new Promise((resolve) => setTimeout(resolve, 18));
}
scanning = false;
renderQueue();
updateStatus("Scan complete. Added " + added + " new links. Queue size: " + queue.length + ".");
}
function copyText(text) {
if (typeof GM_setClipboard !== "undefined") {
GM_setClipboard(text);
return Promise.resolve();
}
return navigator.clipboard.writeText(text);
}
function renderQueue() {
if (!listEl) return;
if (!queue.length) {
listEl.innerHTML = '<div style="padding:12px;color:#9a9a9a;">Queue is empty.</div>';
return;
}
const rows = queue.map(function (item, index) {
return '<label style="display:grid;grid-template-columns:30px 1.6fr 1fr 0.8fr 0.9fr;gap:10px;padding:10px;border-bottom:1px solid #2b2b2b;align-items:center;">'
+ '<input class="mq-check" type="checkbox" data-index="' + index + '" ' + (item.checked ? 'checked' : '') + ' style="height:18px;width:18px;" />'
+ '<div title="' + escapeHtml(item.name) + '" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#fff;font-size:12px;">' + escapeHtml(item.name) + '</div>'
+ '<div title="' + escapeHtml(item.url) + '" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#8bf4ff;font-size:11px;">' + escapeHtml(item.url) + '</div>'
+ '<div style="font-size:11px;color:#ccc;">' + escapeHtml(item.size) + '</div>'
+ '<div style="font-size:11px;color:#ccc;">' + escapeHtml(item.length) + ' | ' + escapeHtml(item.resolution) + '</div>'
+ '</label>';
}).join("");
listEl.innerHTML = rows;
listEl.querySelectorAll(".mq-check").forEach(function (el) {
el.onchange = function () {
const i = Number(el.getAttribute("data-index"));
if (queue[i]) queue[i].checked = el.checked;
};
});
}
function updateStatus(message) {
const el = document.getElementById("mq-status");
if (el) el.textContent = message;
}
function initUi() {
if (document.getElementById("mq-launch")) return;
const launch = document.createElement("button");
launch.id = "mq-launch";
launch.textContent = "MEGA Queue";
Object.assign(launch.style, {
position: "fixed",
bottom: "20px",
left: "20px",
zIndex: "2147483647",
padding: "12px 16px",
borderRadius: "10px",
border: "1px solid #fff",
background: "#cf1111",
color: "#fff",
fontWeight: "700",
cursor: "pointer",
});
panel = document.createElement("div");
panel.id = "mq-panel";
Object.assign(panel.style, {
position: "fixed",
left: "20px",
bottom: "72px",
width: "min(920px, calc(100vw - 40px))",
maxHeight: "78vh",
zIndex: "2147483647",
background: "#121212",
color: "#fff",
border: "1px solid #3c3c3c",
borderRadius: "12px",
padding: "12px",
display: "none",
boxShadow: "0 10px 40px rgba(0,0,0,0.45)",
fontFamily: "Arial, sans-serif",
});
panel.innerHTML = ''
+ '<div style="display:flex;justify-content:space-between;gap:12px;align-items:center;">'
+ ' <strong style="font-size:15px;">MEGA Desktop Queue</strong>'
+ ' <button id="mq-close" style="border:0;background:transparent;color:#fff;font-size:20px;cursor:pointer;">x</button>'
+ '</div>'
+ '<div id="mq-status" style="margin-top:8px;color:#d0d0d0;font-size:12px;">Ready.</div>'
+ '<div id="mq-list" style="margin-top:10px;height:340px;overflow:auto;border:1px solid #2f2f2f;border-radius:8px;background:#070707;"></div>'
+ '<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-top:10px;">'
+ ' <button id="mq-add-selected" style="padding:10px;border:0;border-radius:8px;background:#c00000;color:#fff;font-weight:700;cursor:pointer;">Add selected</button>'
+ ' <button id="mq-scan-all" style="padding:10px;border:0;border-radius:8px;background:#0058d9;color:#fff;font-weight:700;cursor:pointer;">Auto scan all files</button>'
+ ' <button id="mq-copy-selected" style="padding:10px;border:0;border-radius:8px;background:#008f47;color:#fff;font-weight:700;cursor:pointer;">Copy selected</button>'
+ ' <button id="mq-copy-all" style="padding:10px;border:0;border-radius:8px;background:#6d38d3;color:#fff;font-weight:700;cursor:pointer;">Copy all</button>'
+ ' <button id="mq-open-selected" style="padding:10px;border:0;border-radius:8px;background:#777;color:#fff;font-weight:700;cursor:pointer;">Open selected</button>'
+ ' <button id="mq-toggle" style="padding:10px;border:0;border-radius:8px;background:#444;color:#fff;font-weight:700;cursor:pointer;">Toggle all checks</button>'
+ ' <button id="mq-clear" style="padding:10px;border:0;border-radius:8px;background:#444;color:#fff;font-weight:700;cursor:pointer;">Clear queue</button>'
+ '</div>';
document.body.appendChild(panel);
document.body.appendChild(launch);
listEl = document.getElementById("mq-list");
launch.onclick = function () {
panel.style.display = panel.style.display === "none" ? "block" : "none";
};
document.getElementById("mq-close").onclick = function () { panel.style.display = "none"; };
document.getElementById("mq-add-selected").onclick = function () {
const selected = getSelectedHandles();
const fileHandles = [];
selected.forEach((h) => {
const node = getNode(h);
if (node && node.t === 1) fileHandles.push(...getAllDescendantFileHandles(h));
else fileHandles.push(h);
});
const added = pushUnique([...new Set(fileHandles)]);
renderQueue();
updateStatus("Added " + added + " links from " + selected.length + " selected items.");
};
document.getElementById("mq-scan-all").onclick = autoScanAll;
document.getElementById("mq-copy-selected").onclick = function () {
const links = queue.filter((x) => x.checked).map((x) => x.url).join("\n");
if (!links) return updateStatus("No checked links.");
copyText(links).then(function () { updateStatus("Copied checked links."); });
};
document.getElementById("mq-copy-all").onclick = function () {
const links = queue.map((x) => x.url).join("\n");
if (!links) return updateStatus("Queue is empty.");
copyText(links).then(function () { updateStatus("Copied all links."); });
};
document.getElementById("mq-open-selected").onclick = function () {
const selected = queue.filter((x) => x.checked);
if (!selected.length) return updateStatus("No checked links.");
selected.forEach((item) => {
try {
if (typeof GM_openInTab !== "undefined") GM_openInTab(item.url, { active: false, insert: true, setParent: true });
else window.open(item.url, "_blank", "noopener,noreferrer");
} catch (e) {}
});
updateStatus("Opened " + selected.length + " links.");
};
document.getElementById("mq-toggle").onclick = function () {
const allChecked = queue.length > 0 && queue.every((x) => x.checked);
queue.forEach((x) => { x.checked = !allChecked; });
renderQueue();
};
document.getElementById("mq-clear").onclick = function () {
queue = [];
renderQueue();
updateStatus("Queue cleared.");
};
renderQueue();
}
setInterval(function () {
if (!document.body) return;
initUi();
}, 700);
})();