MEGA Desktop - Auto Queue + Metadata

Desktop-only queue helper for MEGA folder pages. Scans files, captures metadata, supports checkboxes and copy selected/all.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Advertisement:

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

Advertisement:

// ==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) => ({
      "&": "&amp;",
      "<": "&lt;",
      ">": "&gt;",
      "'": "&#39;",
      '"': "&quot;",
    }[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);
})();