Feynman FLP: Show Download Links + Download All (Firefox)

Setzt alle flpPlaylist-Items auf free:true, zeigt Download-Liste an und fügt "Download all" für m4a/ogg hinzu

// ==UserScript==
// @name         Feynman FLP: Show Download Links + Download All (Firefox)
// @namespace    https://example.local/
// @version      1.5
// @description  Setzt alle flpPlaylist-Items auf free:true, zeigt Download-Liste an und fügt "Download all" für m4a/ogg hinzu
// @match        *://feynmanlectures.caltech.edu/flptapes.html
// @match        *://www.feynmanlectures.caltech.edu/flptapes.html
// @run-at       document-start
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
  "use strict";

  function inject(fn) {
    const s = document.createElement('script');
    s.textContent = `(${fn})();`;
    (document.head || document.documentElement).appendChild(s);
    s.remove();
  }

  inject(function pageContext() {
    // ---- Helpers ----
    function sanitizeFilename(name) {
      return name
        .replace(/<[^>]+>/g, "") // HTML-Tags weg
        .replace(/[\\/:*?"<>|]+/g, "_") // verbotene FS-Zeichen
        .replace(/\s+/g, " ")
        .trim()
        .slice(0, 120);
    }

    function basename(path) {
      try { return path.split("?")[0].split("#")[0].split("/").pop(); } catch { return path; }
    }

    function clickDownload(url, fileName) {
      const a = document.createElement("a");
      a.href = url;
      if (fileName) a.download = fileName;
      a.rel = "noopener";
      a.target = "_blank"; // in FF hilft das oft beim Download-Flow
      document.body.appendChild(a);
      a.click();
      a.remove();
    }

    // ---- Patch + UI ----
    function patchPlaylistOnce() {
      if (window.flpPlaylist && Array.isArray(window.flpPlaylist)) {
        try {
          window.flpPlaylist.forEach(it => { it.free = true; });
          console.log("[FLP userscript] flpPlaylist gepatcht (free:true).");
        } catch (e) {
          console.warn("[FLP userscript] Patch-Fehler:", e);
        }
        return true;
      }
      return false;
    }

    (function waitForPlaylist() {
      if (!patchPlaylistOnce()) {
        setTimeout(waitForPlaylist, 150);
      } else {
        setTimeout(buildDownloadPanel, 400);
      }
    })();

    function buildDownloadPanel() {
      if (!Array.isArray(window.flpPlaylist) || window.flpPlaylist.length === 0) return;

      const base = new URL(window.location.href);

      const panel = document.createElement('div');
      panel.id = "flp-download-panel";
      Object.assign(panel.style, {
        position: "fixed",
        bottom: "16px",
        right: "16px",
        maxHeight: "70vh",
        overflow: "auto",
        background: "rgba(0,0,0,0.85)",
        color: "#fff",
        font: "14px/1.4 system-ui, sans-serif",
        padding: "12px 14px 14px",
        borderRadius: "10px",
        boxShadow: "0 6px 18px rgba(0,0,0,0.35)",
        zIndex: "99999",
        width: "420px",
      });

      // Header
      const header = document.createElement('div');
      header.textContent = "FLP Downloads (m4a | ogg)";
      header.style.fontWeight = "600";
      header.style.marginBottom = "6px";
      panel.appendChild(header);

      const info = document.createElement('div');
      info.textContent = "Rechtsklick auf Links → „Ziel speichern unter…“ oder nutze „Download all“.";
      info.style.opacity = "0.85";
      info.style.marginBottom = "8px";
      panel.appendChild(info);

      // Controls
      const controls = document.createElement('div');
      controls.style.display = "flex";
      controls.style.gap = "8px";
      controls.style.flexWrap = "wrap";
      controls.style.marginBottom = "8px";

      function makeBtn(label) {
        const b = document.createElement('button');
        b.textContent = label;
        Object.assign(b.style, {
          cursor: "pointer",
          padding: "6px 10px",
          borderRadius: "8px",
          border: "1px solid rgba(255,255,255,0.25)",
          background: "rgba(255,255,255,0.08)",
          color: "#fff",
        });
        b.onmouseenter = () => b.style.background = "rgba(255,255,255,0.14)";
        b.onmouseleave = () => b.style.background = "rgba(255,255,255,0.08)";
        return b;
      }

      const btnAllM4A = makeBtn("Download all (m4a)");
      const btnAllOGG = makeBtn("Download all (ogg)");
      const btnStop = makeBtn("Stop");
      btnStop.style.display = "none";

      // Delay input
      const delayWrap = document.createElement('div');
      delayWrap.style.display = "flex";
      delayWrap.style.alignItems = "center";
      delayWrap.style.gap = "6px";
      delayWrap.style.marginLeft = "auto";

      const delayLabel = document.createElement('label');
      delayLabel.textContent = "Delay (ms):";
      const delayInput = document.createElement('input');
      delayInput.type = "number";
      delayInput.min = "200";
      delayInput.value = "1200";
      Object.assign(delayInput.style, {
        width: "80px",
        padding: "4px 6px",
        borderRadius: "6px",
        border: "1px solid rgba(255,255,255,0.25)",
        background: "rgba(255,255,255,0.06)",
        color: "#fff",
      });

      delayWrap.appendChild(delayLabel);
      delayWrap.appendChild(delayInput);

      controls.appendChild(btnAllM4A);
      controls.appendChild(btnAllOGG);
      controls.appendChild(btnStop);
      controls.appendChild(delayWrap);
      panel.appendChild(controls);

      // Progress
      const progress = document.createElement('div');
      progress.style.marginBottom = "8px";
      progress.style.opacity = "0.9";
      panel.appendChild(progress);

      // Liste
      const list = document.createElement('ol');
      list.style.margin = "0";
      list.style.paddingLeft = "18px";

      window.flpPlaylist.forEach((item, idx) => {
        const li = document.createElement('li');
        li.style.marginBottom = "6px";

        const title = (item.title || "").replace(/<[^>]+>/g, "").trim() || `Track ${idx+1}`;
        const m4aHref = item.m4a ? new URL(item.m4a, base).href : null;
        const oggHref = item.oga ? new URL(item.oga, base).href : null;

        const titleSpan = document.createElement('span');
        titleSpan.textContent = title + " — ";
        li.appendChild(titleSpan);

        function link(href, label) {
          const a = document.createElement('a');
          a.href = href;
          a.textContent = label;
          a.style.color = "#7cc7ff";
          a.setAttribute("download", "");
          a.rel = "noopener";
          a.target = "_blank";
          return a;
        }

        if (m4aHref) li.appendChild(link(m4aHref, "m4a"));
        if (m4aHref && oggHref) li.appendChild(document.createTextNode(" | "));
        if (oggHref) li.appendChild(link(oggHref, "ogg"));

        list.appendChild(li);
      });

      panel.appendChild(list);

      // Close
      const closeBtn = document.createElement('button');
      closeBtn.textContent = "×";
      closeBtn.title = "Panel schließen";
      Object.assign(closeBtn.style, {
        position: "absolute",
        top: "4px",
        right: "8px",
        border: "none",
        background: "transparent",
        color: "#fff",
        fontSize: "18px",
        cursor: "pointer",
      });
      closeBtn.onclick = () => panel.remove();
      panel.appendChild(closeBtn);

      document.documentElement.appendChild(panel);

      // ---- Download-All Logik ----
      let stopFlag = false;
      let running = false;

      async function downloadAll(kind /* 'm4a' | 'ogg' */) {
        if (running) return;
        running = true; stopFlag = false;
        btnStop.style.display = "inline-block";
        progress.textContent = `Start: Alle ${kind.toUpperCase()}-Dateien werden nacheinander geladen…`;
        const delay = Math.max(200, parseInt(delayInput.value || "1200", 10));

        const items = window.flpPlaylist.map((item, idx) => {
          const title = sanitizeFilename(item.title || `Track ${idx+1}`);
          const href = kind === "m4a"
            ? (item.m4a ? new URL(item.m4a, base).href : null)
            : (item.oga ? new URL(item.oga, base).href : null);
          let ext = kind === "m4a" ? ".m4a" : ".ogg";
          if (href) {
            const baseName = basename(href);
            const detectedExt = (baseName.match(/\.(m4a|mp4|ogg|oga)$/i) || [])[0];
            if (detectedExt) ext = detectedExt.toLowerCase();
          }
          const fileName = `${title}${ext}`;
          return { href, fileName, idx };
        }).filter(x => x.href);

        if (items.length === 0) {
          progress.textContent = `Keine ${kind.toUpperCase()}-Links gefunden.`;
          running = false; btnStop.style.display = "none";
          return;
        }

        // Hinweis an Nutzer (Popup-Blocker)
        alert(`Ich starte ${items.length} Downloads (${kind.toUpperCase()}).\nWenn Firefox fragt, erlaube mehrere Downloads von dieser Seite.`);

        for (let i = 0; i < items.length; i++) {
          if (stopFlag) { progress.textContent = `Abgebrochen bei ${i}/${items.length}.`; break; }
          const { href, fileName } = items[i];
          progress.textContent = `(${i+1}/${items.length}) Lade: ${fileName}`;
          clickDownload(href, fileName);
          // Delay zwischen Downloads
          await new Promise(r => setTimeout(r, delay));
        }

        if (!stopFlag) progress.textContent = `Fertig (${kind.toUpperCase()}).`;
        running = false; stopFlag = false;
        btnStop.style.display = "none";
      }

      btnAllM4A.onclick = () => downloadAll("m4a");
      btnAllOGG.onclick = () => downloadAll("ogg");
      btnStop.onclick = () => { stopFlag = true; };

      console.log("[FLP userscript] Download-Panel eingefügt (mit Download-all).");
    }
  });
})();