CYCU ilearning 2.0 PDF downloader

在中原大學 iLearning 2.0 平台自動新增「⬇️ 下載」與「⬇️ 下載全部」按鈕。

目前為 2025-09-14 提交的版本,檢視 最新版本

// ==UserScript==
// @name         CYCU ilearning 2.0 PDF downloader
// @namespace    https://github.com/Mono0713/CYCU-ilearning-2.0-pdf-downloader
// @version      1.0.3-fixed
// @description  在中原大學 iLearning 2.0 平台自動新增「⬇️ 下載」與「⬇️ 下載全部」按鈕。
// @license      MIT
// @match        https://ilearning.cycu.edu.tw/*
// @run-at       document-start
// @grant        none
// ==/UserScript==
// 功能說明:
// 1. 在 PDF 檢視頁,可直接下載當前 PDF 檔案。
// 2. 在課程頁,一鍵下載所有 PDF / PPT / PPTX 檔案,並自動處理檔名、避免重複。


// @license      MIT
// @match        https://ilearning.cycu.edu.tw/*
// @run-at       document-start
// @grant        none
// ==/UserScript==

(() => {
  'use strict';

  const $  = (sel, root = document) => root.querySelector(sel);
  const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
  const sleep = (ms) => new Promise(r => setTimeout(r, ms));

  const IS_PDF_VIEW   = /\/mod\/pdfannotator\/view\.php/i.test(location.pathname + location.search);
  const IS_COURSE_VIEW= /\/course\/view\.php/i.test(location.pathname + location.search);

  const sanitize = (s = '') =>
    (s || '')
      .toString()
      .replace(/&/g, '&')
      .replace(/[\\/:*?"<>|]+/g, '_')
      .replace(/\s+/g, ' ')
      .trim()
      .slice(0, 120);

  function extFromHeadersOrUrl(cd, url, ct) {
    if (cd) {
      let m = /filename\*\s*=\s*[^']+'[^']*'([^;]+)$/i.exec(cd);
      if (m) {
        const n = decodeURIComponent(m[1]);
        const mm = /\.([A-Za-z0-9]{2,5})$/.exec(n);
        if (mm) return mm[1].toLowerCase();
      }
      m = /filename\s*=\s*"?(.*?)"?\s*(?:;|$)/i.exec(cd);
      if (m) {
        const n = m[1];
        const mm = /\.([A-Za-z0-9]{2,5})$/.exec(n);
        if (mm) return mm[1].toLowerCase();
      }
    }

    if (url) {
      try {
        const u = new URL(url, location.href);
        const lastSeg = decodeURIComponent((u.pathname.split('/').pop() || ''));
        let mm = /\.([A-Za-z0-9]{2,5})(?:$|\?)/.exec(lastSeg);
        if (mm) return mm[1].toLowerCase();

        const qsName = new URLSearchParams(u.search).get('filename');
        if (qsName) {
          mm = /\.([A-Za-z0-9]{2,5})$/.exec(decodeURIComponent(qsName));
          if (mm) return mm[1].toLowerCase();
        }
      } catch {}
    }

    if (ct) {
      const map = {
        'application/pdf': 'pdf',
        'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'pptx',
        'application/vnd.ms-powerpoint': 'ppt',
      };
      const t = ct.split(';')[0].trim().toLowerCase();
      if (map[t]) return map[t];
    }
    return '';
  }

  const withExt = (basename, ext) => {
    if (!ext) return basename;
    let need = `.${ext.toLowerCase()}`;
    // === 修正重點:如果 ext 被判斷成 php,直接換成 pdf ===
    if (ext.toLowerCase() === 'php') {
      need = '.pdf';
    }
    return basename.toLowerCase().endsWith(need) ? basename : (basename + need);
  };

  function nameFromLink(a) {
    const inst = a.querySelector('.instancename');
    if (inst) {
      const txtNodes = Array.from(inst.childNodes).filter(n => n.nodeType === Node.TEXT_NODE);
      const t = txtNodes.map(n => n.textContent || '').join(' ');
      return sanitize(t || inst.textContent || a.textContent);
    }
    return sanitize(a.textContent);
  }

  async function downloadBlob(url, name) {
    const r = await fetch(url, { credentials: 'include', redirect: 'follow' });
    if (!r.ok) throw new Error(`HTTP ${r.status}`);
    const cd = r.headers.get('content-disposition') || '';
    const ct = r.headers.get('content-type') || '';
    const finalUrl = r.url || url;
    const ext = extFromHeadersOrUrl(cd, finalUrl, ct);
    const niceName = withExt(sanitize(name), ext);
    const blob = await r.blob();
    const obj = URL.createObjectURL(blob);
    try {
      const a = document.createElement('a');
      a.href = obj;
      a.download = niceName;
      document.body.appendChild(a);
      a.click();
      a.remove();
    } finally {
      URL.revokeObjectURL(obj);
    }
  }

  const viewerTitle = () => {
    const h = $('#page-header .page-header-headings h1') || $('header h1') || $('h1');
    const t = (h && h.textContent) || document.title.replace(/\s*\|.*$/, '') || 'document';
    return sanitize(t);
  };

  async function handleSingleDownload() {
    const title = viewerTitle();
    try {
      const app = (window.PDFViewerApplication || {});
      if (app && app.pdfDocument && typeof app.pdfDocument.getData === 'function') {
        const u = (app.url || app.appConfig?.defaultUrl || '');
        if (u && !String(u).startsWith('blob:')) {
          await downloadBlob(u, title);
          return;
        } else {
          const u8 = await app.pdfDocument.getData();
          const blob = new Blob([u8], { type: 'application/pdf' });
          const obj = URL.createObjectURL(blob);
          const a = document.createElement('a');
          a.href = obj; a.download = withExt(title, 'pdf');
          document.body.appendChild(a); a.click(); a.remove();
          URL.revokeObjectURL(obj);
          return;
        }
      }
    } catch {}

    let url = '';
    try {
      const hit = performance.getEntriesByType('resource')
        .map(e => e.name).reverse()
        .find(u => /\/pluginfile\.php\/.+\.pdf(?:$|\?)/i.test(u));
      if (hit) url = hit;
    } catch {}
    if (!url) {
      const a = $('a[href*="/pluginfile.php/"][href*=".pdf"]');
      if (a) url = a.href;
    }
    if (!url) {
      alert('找不到本頁 PDF,請先翻頁讓檔案載入後再試一次。');
      return;
    }
    await downloadBlob(url, title);
  }

  function mountSingleButton() {
    const ID = 'ilearn-dl-one';
    if (document.getElementById(ID)) return;
    const btn = document.createElement('button');
    btn.id = ID;
    btn.textContent = '⬇️ 下載';
    Object.assign(btn.style, {
      position:'fixed', right:'14px', bottom:'14px', zIndex:2147483647,
      padding:'10px 14px', background:'#0ea5e9', color:'#fff',
      border:'none', borderRadius:'10px', boxShadow:'0 6px 16px rgba(0,0,0,.2)', cursor:'pointer'
    });
    btn.addEventListener('click', () => { btn.disabled = true; handleSingleDownload().finally(()=>btn.disabled=false); }, {passive:true});
    document.documentElement.appendChild(btn);
  }

  function pickResourceLinks() {
    const res = $$('li.activity.resource.modtype_resource a.aalink[href*="/mod/resource/view.php?id="]');
    const pdf = $$('li.activity.modtype_pdfannotator a.aalink[href*="/mod/pdfannotator/view.php?id="]');
    return [...res, ...pdf];
  }

  async function resolveAnnotatorDirect(href) {
    try {
      const res = await fetch(href, { credentials: 'include' });
      const html = await res.text();
      const div = document.createElement('div'); div.innerHTML = html;
      const a = div.querySelector('a[href*="/pluginfile.php/"][href*=".pdf"]');
      if (a) return new URL(a.getAttribute('href'), location.href).href;
    } catch (e) { console.warn('[iLearn] annotator resolve failed', e); }
    return href;
  }

  async function resolveOne(a) {
    const uiName = nameFromLink(a) || 'file';
    const isAnnotator = /\/mod\/pdfannotator\/view\.php/i.test(a.href);
    const href = isAnnotator ? await resolveAnnotatorDirect(a.href) : a.href;
    const r = await fetch(href, { credentials:'include', redirect:'follow' });
    if (!r.ok) throw new Error(`HTTP ${r.status}`);
    const cd = r.headers.get('content-disposition') || '';
    const ct = r.headers.get('content-type') || '';
    const finalUrl = r.url || href;
    const ext = extFromHeadersOrUrl(cd, finalUrl, ct);
    return { url: finalUrl, name: withExt(uiName, ext) };
  }

  async function handleBulkDownload() {
    const links = pickResourceLinks();
    if (!links.length) { alert('這一頁沒有可下載的檔案型資源'); return; }

    const seen = new Set();
    const jobs = [];
    for (const a of links) {
      const base = (nameFromLink(a) || 'file').toLowerCase();
      if (seen.has(base)) continue;
      seen.add(base);
      jobs.push(a);
    }

    for (const a of jobs) {
      try {
        const { url, name } = await resolveOne(a);
        await downloadBlob(url, name);
      } catch (e) {
        console.warn('下載失敗:', a, e);
      }
      await sleep(300);
    }
  }

  function mountBulkButton() {
    const ID = 'ilearn-dl-all';
    if (document.getElementById(ID)) return;
    const btn = document.createElement('button');
    btn.id = ID;
    btn.textContent = '⬇️ 下載全部';
    Object.assign(btn.style, {
      position:'fixed', right:'14px', bottom:'14px', zIndex:2147483647,
      padding:'10px 14px', background:'#16a34a', color:'#fff',
      border:'none', borderRadius:'10px', boxShadow:'0 6px 16px rgba(0,0,0,.2)', cursor:'pointer'
    });
    btn.addEventListener('click', () => { btn.disabled = true; handleBulkDownload().finally(()=>btn.disabled=false); }, {passive:true});
    document.documentElement.appendChild(btn);
  }

  function start() {
    if (IS_PDF_VIEW) {
      if (document.readyState === 'loading') {
        addEventListener('DOMContentLoaded', mountSingleButton, { once:true });
      } else mountSingleButton();
    } else if (IS_COURSE_VIEW) {
      if (document.readyState === 'loading') {
        addEventListener('DOMContentLoaded', mountBulkButton, { once:true });
      } else mountBulkButton();
    }
  }
  start();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址