Auto-scan all files in MEGA shared folders. Queue/copy links with file name, size, type, and video duration. Per-link checkboxes for selective copying. Desktop version only.
// ==UserScript==
// @name MEGA - Link Queue with Auto-scan & Metadata
// @namespace http://tampermonkey.net/
// @version 2.2.0
// @description Auto-scan all files in MEGA shared folders. Queue/copy links with file name, size, type, and video duration. Per-link checkboxes for selective copying. Desktop version only.
// @author xPokerr + adapted + enhanced
// @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 btn = null;
let panel = null;
let listEl = null;
let queuedLinks = []; // [{ url, handle, name, status, checked, sizeFormatted, ext, timestamp, duration }]
// ─── URL helpers ──────────────────────────────────────────────────────────
function getBaseUrl() {
const match = window.location.href.match(/^(https?:\/\/[^\/]+\/folder\/[^#]+#[^\/]+)/);
return match ? match[1] : null;
}
// ─── Handle normalisation (unchanged from v1) ─────────────────────────────
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];
if (h.length > 5 && !h.includes(' ')) return h;
return 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 => {
if (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() {
let 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 => {
[el.getAttribute('data-id'), el.getAttribute('data-h'),
el.getAttribute('data-handle'), el.getAttribute('data-node-handle'),
el.getAttribute('id')].forEach(v => {
const h = normaliseHandle(v);
if (h) handles.push(h);
});
});
return [...new Set(handles)];
}
// ─── MEGA internal helpers (unchanged from v1) ────────────────────────────
function isMediaViewerOpen() {
const v = document.querySelector('.media-viewer-container');
return !!(v && !v.classList.contains('hidden'));
}
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 getNodeName(handle) {
const node = getNode(handle);
return (node && (node.name || node.n)) || handle;
}
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 files = [], seen = new Set(), stack = [folderHandle];
while (stack.length > 0) {
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 files.push((node && node.h) || h);
});
}
return [...new Set(files)];
}
// ─── Metadata helpers (new) ────────────────────────────────────────────────
function formatBytes(bytes) {
if (bytes === null || bytes === undefined || isNaN(bytes)) return null;
if (bytes === 0) return '0 B';
if (bytes < 1024) return bytes + ' B';
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
if (bytes < 1073741824) return (bytes / 1048576).toFixed(2) + ' MB';
return (bytes / 1073741824).toFixed(2) + ' GB';
}
function formatDuration(secs) {
if (!secs || isNaN(secs)) return null;
secs = Math.floor(secs);
const h = Math.floor(secs / 3600);
const m = Math.floor((secs % 3600) / 60);
const s = secs % 60;
return h > 0
? `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
: `${m}:${String(s).padStart(2, '0')}`;
}
function getFileExtension(name) {
const m = String(name || '').match(/\.([^.]{1,8})$/);
return m ? m[1].toLowerCase() : '';
}
function tryGetVideoDuration(node) {
if (!node) return null;
// Direct node properties (set by MEGA after media attribute parsing)
if (typeof node.playtime === 'number') return node.playtime;
if (typeof node.duration === 'number') return node.duration;
// Extended attribute object
if (node.u && typeof node.u.playtime === 'number') return node.u.playtime;
// Decoded attribute string (JSON)
if (node.at) {
try {
const at = typeof node.at === 'string' ? JSON.parse(node.at) : node.at;
if (at && at.c && typeof at.c.playtime === 'number') return at.c.playtime;
if (at && typeof at.playtime === 'number') return at.playtime;
} catch (e) {}
}
// M.getMediaAttribute (present in some builds)
try {
if (window.M && window.M.getMediaAttribute) {
const a = window.M.getMediaAttribute(node);
if (a && typeof a.playtime === 'number') return a.playtime;
}
} catch (e) {}
return null;
}
function getNodeMeta(handle) {
const node = getNode(handle);
if (!node) return { sizeFormatted: null, ext: '', timestamp: null, duration: null };
const rawDur = tryGetVideoDuration(node);
return {
sizeFormatted: formatBytes(node.s),
ext: getFileExtension(node.name || node.n),
timestamp: node.ts ? new Date(node.ts * 1000).toLocaleDateString() : null,
duration: rawDur ? formatDuration(rawDur) : null
};
}
// ─── Auto-scan (new) ───────────────────────────────────────────────────────
// Reads window.M.d directly — same internal MEGA data structure the manual
// path uses — and collects every file node (files have a numeric 's'/size
// property; folders do not).
function autoScanAndAdd() {
const baseUrl = getBaseUrl();
if (!baseUrl) {
updateStatus('Could not determine base URL. Is MEGA fully loaded?');
return;
}
if (!window.M || !window.M.d) {
updateStatus('MEGA is still loading — please wait a moment and try again.');
return;
}
// Collect file handles: files have a numeric size and a name; folders don't.
const fileHandles = Object.keys(window.M.d).filter(h => {
const n = window.M.d[h];
return n && typeof n.s === 'number' && (n.name || n.n);
}).map(h => {
const n = window.M.d[h];
return n.h || h; // prefer node's own h field
});
if (fileHandles.length === 0) {
updateStatus('No files found yet — MEGA may still be populating the folder.');
return;
}
const existing = new Set(queuedLinks.map(x => x.url));
let added = 0;
fileHandles.forEach(h => {
const url = `${baseUrl}/file/${h}`;
if (existing.has(url)) return;
const meta = getNodeMeta(h);
queuedLinks.push({
url, handle: h,
name: getNodeName(h),
status: 'Queued',
checked: true,
...meta
});
existing.add(url);
added++;
});
renderQueue();
updateStatus(`Auto-scan complete: added ${added} file${added === 1 ? '' : 's'}. Total in queue: ${queuedLinks.length}.`);
}
// ─── Selection-based add — now includes metadata ───────────────────────────
function buildItemsFromSelection() {
const baseUrl = getBaseUrl();
const handles = getSelectedHandles();
if (!baseUrl || handles.length === 0) return [];
const fileHandles = [];
handles.forEach(h => {
const node = getNode(h);
if (node && node.t === 1) fileHandles.push(...getAllDescendantFileHandles(h));
else fileHandles.push(h);
});
return [...new Set(fileHandles)].map(h => {
const meta = getNodeMeta(h);
return {
url: `${baseUrl}/file/${h}`,
handle: h,
name: getNodeName(h),
status: 'Queued',
checked: true,
...meta
};
});
}
// ─── Clipboard ─────────────────────────────────────────────────────────────
function copyText(text) {
if (typeof GM_setClipboard !== 'undefined') { GM_setClipboard(text); return Promise.resolve(); }
return navigator.clipboard.writeText(text);
}
// ─── Render ────────────────────────────────────────────────────────────────
function escapeHtml(value) {
return String(value).replace(/[&<>'"]/g, c =>
({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[c]));
}
function statusColor(s) {
if (s.includes('Opened')) return '#63b3ff';
if (s.includes('Failed')) return '#ff6b6b';
if (s.includes('Copied')) return '#00cc66';
return '#ffd166';
}
function updateCounter() {
const el = document.getElementById('mega-extract-counter');
if (!el) return;
if (queuedLinks.length === 0) { el.textContent = ''; return; }
const checked = queuedLinks.filter(x => x.checked).length;
el.textContent = `${checked} / ${queuedLinks.length} checked`;
}
function renderQueue() {
if (!listEl) return;
if (queuedLinks.length === 0) {
listEl.innerHTML = '<div style="color:#666;font-size:12px;padding:14px;text-align:center;">No links queued yet.</div>';
updateCounter();
return;
}
listEl.innerHTML = queuedLinks.map((item, i) => {
// Metadata badges
const ext = item.ext
? `<span style="background:#222;color:#999;padding:1px 5px;border-radius:4px;font-size:10px;font-family:monospace;">${escapeHtml(item.ext.toUpperCase())}</span>`
: '';
const size = item.sizeFormatted
? `<span style="color:#888;font-size:10px;">${escapeHtml(item.sizeFormatted)}</span>`
: '';
const dur = item.duration
? `<span style="background:#0d2b15;color:#5cdb8a;padding:1px 6px;border-radius:4px;font-size:10px;">▶ ${escapeHtml(item.duration)}</span>`
: '';
const date = item.timestamp
? `<span style="color:#555;font-size:10px;">${escapeHtml(item.timestamp)}</span>`
: '';
const metaRow = (ext || size || dur || date)
? `<div style="display:flex;align-items:center;flex-wrap:wrap;gap:5px;margin-top:3px;">${ext}${size}${dur}${date}</div>`
: '';
return `
<label style="display:grid;grid-template-columns:18px 1fr;gap:9px;padding:9px 10px;border-bottom:1px solid #1c1c1c;cursor:pointer;align-items:start;">
<input class="mega-extract-check" data-index="${i}" type="checkbox" ${item.checked ? 'checked' : ''}
style="margin-top:3px;width:14px;height:14px;accent-color:#d90000;cursor:pointer;flex-shrink:0;">
<div style="min-width:0;">
<div style="display:flex;justify-content:space-between;align-items:center;gap:6px;">
<span title="${escapeHtml(item.name)}"
style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;font-weight:600;color:#f0f0f0;">
${escapeHtml(item.name)}
</span>
<span style="font-size:10px;color:${statusColor(item.status)};white-space:nowrap;flex-shrink:0;">
${escapeHtml(item.status)}
</span>
</div>
${metaRow}
<div title="${escapeHtml(item.url)}"
style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:10px;color:#4ecb71;margin-top:3px;opacity:0.8;">
${escapeHtml(item.url)}
</div>
</div>
</label>`;
}).join('');
listEl.querySelectorAll('.mega-extract-check').forEach(cb => {
cb.onchange = () => {
const i = Number(cb.getAttribute('data-index'));
if (queuedLinks[i]) { queuedLinks[i].checked = cb.checked; updateCounter(); }
};
});
updateCounter();
}
// ─── Panel ─────────────────────────────────────────────────────────────────
function openPanel() {
if (!panel) initPanel();
panel.style.display = 'block';
void panel.offsetWidth;
panel.style.opacity = '1';
panel.style.transform = 'translateY(0)';
renderQueue();
}
function closePanel() {
if (!panel) return;
panel.style.opacity = '0';
panel.style.transform = 'translateY(10px)';
setTimeout(() => { if (panel) panel.style.display = 'none'; }, 180);
}
function updateStatus(msg) {
const el = document.getElementById('mega-extract-status');
if (el) el.textContent = msg;
}
function initPanel() {
if (document.getElementById('mega-extract-panel')) {
panel = document.getElementById('mega-extract-panel');
listEl = document.getElementById('mega-extract-list');
return;
}
panel = document.createElement('div');
panel.id = 'mega-extract-panel';
Object.assign(panel.style, {
position: 'fixed', bottom: '84px', left: '20px', zIndex: '2147483647',
width: '460px', maxWidth: 'calc(100vw - 40px)',
background: '#111', color: '#fff',
border: '1px solid rgba(255,255,255,0.15)', borderRadius: '16px',
boxShadow: '0 16px 48px rgba(0,0,0,0.75)', fontFamily: 'Arial, sans-serif',
padding: '14px', display: 'none', opacity: '0', transform: 'translateY(10px)',
transition: 'all 0.18s ease'
});
panel.innerHTML = `
<!-- Header -->
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;">
<div style="font-weight:700;font-size:15px;letter-spacing:0.3px;">⬇️ MEGA Link Queue</div>
<button id="mega-extract-close"
style="background:transparent;color:#666;border:0;font-size:22px;cursor:pointer;line-height:1;padding:0 2px;transition:color 0.15s;"
onmouseover="this.style.color='#fff'" onmouseout="this.style.color='#666'">×</button>
</div>
<!-- Status bar -->
<div id="mega-extract-status"
style="font-size:11px;color:#aaa;margin-bottom:8px;line-height:1.45;min-height:30px;
background:#1a1a1a;border-radius:7px;padding:6px 9px;border:1px solid #222;">
Press Auto-scan to queue all files, or manually select files and use "Add selected".
</div>
<!-- Queue list -->
<div id="mega-extract-list"
style="width:100%;height:255px;overflow-y:auto;overflow-x:hidden;
background:#080808;border:1px solid #222;border-radius:10px;box-sizing:border-box;">
</div>
<!-- Counter -->
<div id="mega-extract-counter"
style="text-align:right;font-size:10px;color:#555;margin-top:4px;height:14px;"></div>
<!-- Buttons -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-top:8px;">
<!-- Auto-scan: full-width primary action -->
<button id="mega-extract-autoscan"
style="grid-column:1/-1;padding:11px 8px;border:0;border-radius:10px;
background:linear-gradient(135deg,#cc0000,#8a0000);color:#fff;
font-weight:700;font-size:13px;cursor:pointer;letter-spacing:0.4px;
box-shadow:0 3px 12px rgba(204,0,0,0.4);transition:filter 0.15s;"
onmouseover="this.style.filter='brightness(1.15)'"
onmouseout="this.style.filter='brightness(1)'">
🔍 Auto-scan & Add All Files
</button>
<button id="mega-extract-add"
style="padding:9px 6px;border:1px solid #3a0000;border-radius:9px;
background:#1e0000;color:#ff9999;font-weight:700;font-size:12px;cursor:pointer;
transition:background 0.15s;"
onmouseover="this.style.background='#2e0000'" onmouseout="this.style.background='#1e0000'">
+ Add selected
</button>
<button id="mega-extract-refresh"
style="padding:9px 6px;border:1px solid #2a2a2a;border-radius:9px;
background:#1a1a1a;color:#bbb;font-weight:700;font-size:12px;cursor:pointer;
transition:background 0.15s;"
onmouseover="this.style.background='#242424'" onmouseout="this.style.background='#1a1a1a'">
↻ Refresh sel.
</button>
<button id="mega-extract-copy"
style="padding:9px 6px;border:1px solid #00401e;border-radius:9px;
background:#001e0e;color:#4ecb71;font-weight:700;font-size:12px;cursor:pointer;
transition:background 0.15s;"
onmouseover="this.style.background='#002a14'" onmouseout="this.style.background='#001e0e'">
📋 Copy checked
</button>
<button id="mega-extract-download"
style="padding:9px 6px;border:1px solid #003366;border-radius:9px;
background:#001833;color:#7ab3ff;font-weight:700;font-size:12px;cursor:pointer;
transition:background 0.15s;"
onmouseover="this.style.background='#002244'" onmouseout="this.style.background='#001833'">
⬇ Open checked
</button>
<button id="mega-extract-select-all"
style="padding:9px 6px;border:1px solid #2a2a2a;border-radius:9px;
background:#1a1a1a;color:#bbb;font-weight:700;font-size:12px;cursor:pointer;
transition:background 0.15s;"
onmouseover="this.style.background='#242424'" onmouseout="this.style.background='#1a1a1a'">
☑ Toggle all
</button>
<button id="mega-extract-clear"
style="padding:9px 6px;border:1px solid #2a2a2a;border-radius:9px;
background:#1a1a1a;color:#bbb;font-weight:700;font-size:12px;cursor:pointer;
transition:background 0.15s;"
onmouseover="this.style.background='#242424'" onmouseout="this.style.background='#1a1a1a'">
🗑 Clear queue
</button>
</div>
`;
document.body.appendChild(panel);
listEl = document.getElementById('mega-extract-list');
renderQueue();
// ── Button handlers ──────────────────────────────────────────────────
document.getElementById('mega-extract-close').onclick = closePanel;
// AUTO-SCAN: the main new feature
document.getElementById('mega-extract-autoscan').onclick = autoScanAndAdd;
// ADD SELECTED (original behaviour, now with metadata)
document.getElementById('mega-extract-add').onclick = () => {
const items = buildItemsFromSelection();
const existing = new Set(queuedLinks.map(x => x.url));
let added = 0;
items.forEach(item => {
if (!existing.has(item.url)) { queuedLinks.push(item); existing.add(item.url); added++; }
});
renderQueue();
const sel = getSelectedHandles().length;
updateStatus(`Added ${added} file link${added === 1 ? '' : 's'} from ${sel} selected item${sel === 1 ? '' : 's'}. Queue: ${queuedLinks.length}.`);
};
// OPEN (download) checked
document.getElementById('mega-extract-download').onclick = () => {
const sel = queuedLinks.filter(x => x.checked);
if (!sel.length) { updateStatus('Nothing checked — tick some checkboxes first.'); return; }
sel.forEach(item => {
try {
item.status = 'Opened';
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) { item.status = 'Failed'; }
});
renderQueue();
updateStatus(`Opened ${sel.length} tab${sel.length === 1 ? '' : 's'}. Allow popups in your browser if tabs are blocked.`);
};
// COPY checked links
document.getElementById('mega-extract-copy').onclick = () => {
const sel = queuedLinks.filter(x => x.checked);
const text = sel.map(x => x.url).join('\n');
if (!text) { updateStatus('Nothing checked — tick some checkboxes first.'); return; }
copyText(text).then(() => {
sel.forEach(x => { x.status = 'Copied'; });
renderQueue();
updateStatus(`✅ Copied ${sel.length} link${sel.length === 1 ? '' : 's'} to clipboard!`);
});
};
// REFRESH selection preview
document.getElementById('mega-extract-refresh').onclick = () => {
const n = getSelectedHandles().length;
const p = buildItemsFromSelection();
updateStatus(`${n} selected item${n === 1 ? '' : 's'} → ${p.length} file link${p.length === 1 ? '' : 's'} ready to add.`);
};
// TOGGLE ALL checkboxes
document.getElementById('mega-extract-select-all').onclick = () => {
const allOn = queuedLinks.length > 0 && queuedLinks.every(x => x.checked);
queuedLinks.forEach(x => { x.checked = !allOn; });
renderQueue();
updateStatus(allOn ? 'Unchecked all.' : 'Checked all.');
};
// CLEAR queue
document.getElementById('mega-extract-clear').onclick = () => {
queuedLinks = [];
renderQueue();
updateStatus('Queue cleared.');
};
}
// ─── Floating launcher button ──────────────────────────────────────────────
function initButton() {
if (document.getElementById('mega-extract-btn')) {
btn = document.getElementById('mega-extract-btn');
return;
}
btn = document.createElement('button');
btn.id = 'mega-extract-btn';
Object.assign(btn.style, {
position: 'fixed', bottom: '25px', left: '25px', zIndex: '2147483647',
padding: '12px 18px', background: '#cc0000', color: '#fff',
border: '2px solid rgba(255,255,255,0.3)', borderRadius: '50px', cursor: 'pointer',
fontWeight: '700', fontSize: '14px', letterSpacing: '0.3px',
boxShadow: '0 8px 22px rgba(0,0,0,0.55)', fontFamily: 'Arial, sans-serif',
transition: 'all 0.2s ease'
});
btn.innerHTML = '⬇️ MEGA Queue';
btn.onclick = () => {
initPanel();
if (panel && panel.style.display === 'block') closePanel();
else openPanel();
};
document.body.appendChild(btn);
}
// ─── Main loop ─────────────────────────────────────────────────────────────
function updateLogic() {
if (!document.body) return;
if (!btn) initButton();
if (!panel) initPanel();
if (btn) {
const count = getSelectedHandles().length;
const viewerOpen = isMediaViewerOpen();
btn.style.opacity = viewerOpen ? '0.4' : '1';
btn.innerHTML = (count > 0 && !viewerOpen)
? `⬇️ Queue ${count} selected`
: '⬇️ MEGA Queue';
}
}
setInterval(updateLogic, 500);
})();