Greasy Fork

国家中小学智慧教育平台教材PDF电子课本链接与下载工具

教材列表页与预览页添加了PDF按钮,免登录查看或下载电子课本与课外书籍,可批量下载

当前为 2023-10-10 提交的版本,查看 最新版本

// ==UserScript==
// @name         国家中小学智慧教育平台教材PDF电子课本链接与下载工具
// @namespace    https://greasyfork.org/zh-CN/scripts/466598
// @version      1.2.9
// @description  教材列表页与预览页添加了PDF按钮,免登录查看或下载电子课本与课外书籍,可批量下载
// @match        *://basic.smartedu.cn/*
// @match        *://www.zxx.edu.cn/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    let link;
    const pattern = /contentId=([a-zA-Z0-9_-]+)/; // 匹配 contentId 的正则表达式
    const contentId = window.location.href.match(pattern)?.[1]; // 获取 contentId 的数值
    link = `https://r3-ndr.ykt.cbern.com.cn/edu_product/esp/assets_document/${contentId}.pkg/pdf.pdf`;
    // window.location.href = link; // 直接重定向到该网址,经测试可用

    const maxTimeToCheck = 15000; // 最多检查 15000 毫秒,即 15 秒
    let elapsedTime = 0;

    function checkLoginModal() {
        const loginModal = document.querySelector(".fish-modal-root");
        if (loginModal) {
            clearInterval(intervalId1); // 清除定时器
            // 去除阻碍
            loginModal.remove(); // 去除遮罩
            const body = document.querySelector("html > body"); // 去除 body 属性
            body.removeAttribute("class");
            body.removeAttribute("style");
            // 添加阅读器
            const name = document.querySelectorAll(".fish-breadcrumb-link")[2]?.textContent;
            const indexModuleWrapperECeCo = document.querySelector(".index-module_wrapper_ECeCo");
            if (indexModuleWrapperECeCo) {
                indexModuleWrapperECeCo.innerHTML = `
<div class="index-module_wrapper_ECeCo">
  <div class="index-module_header_tG-zz">
    <h3 class="index-module_title_bnE9V">${name}</h3>
    <div class="index-module_info_evO1d">
      <span class="index-module_origin_nuihE">
        <svg class="index-module_icon_dwVZ4">
          <use xlink:href="#icon_hotel_fill">
          </use>
        </svg>
        <span class="index-module_department_ewVZW">智慧中小学</span>
      </span>
      <div class="index-module_assesment-detail_jhGLz">
        <div class="index-module_assessment-content_06v6Z">
          <div class="fish-dropdown-trigger index-module_assessment-rate_euPTQ">
            <ul class="fish-rate fish-rate-disabled" tabindex="-1" role="radiogroup">
              <li class="fish-rate-star fish-rate-star-full">
                <div role="radio" aria-checked="true" aria-posinset="1" aria-setsize="5" tabindex="-1">
                  <div class="fish-rate-star-first">
                    <div class="custom-star-wrap" style="width: 100%;">
                      <div class="custom-star">
                      </div>
                    </div>
                  </div>
                  <div class="fish-rate-star-second">
                    <div class="custom-star-wrap" style="width: 100%;">
                      <div class="custom-star">
                      </div>
                    </div>
                  </div>
                </div>
              </li>
              <li class="fish-rate-star fish-rate-star-full">
                <div role="radio" aria-checked="true" aria-posinset="2" aria-setsize="5" tabindex="-1">
                  <div class="fish-rate-star-first">
                    <div class="custom-star-wrap" style="width: 100%;">
                      <div class="custom-star">
                      </div>
                    </div>
                  </div>
                  <div class="fish-rate-star-second">
                    <div class="custom-star-wrap" style="width: 100%;">
                      <div class="custom-star">
                      </div>
                    </div>
                  </div>
                </div>
              </li>
              <li class="fish-rate-star fish-rate-star-full">
                <div role="radio" aria-checked="true" aria-posinset="3" aria-setsize="5" tabindex="-1">
                  <div class="fish-rate-star-first">
                    <div class="custom-star-wrap" style="width: 100%;">
                      <div class="custom-star">
                      </div>
                    </div>
                  </div>
                  <div class="fish-rate-star-second">
                    <div class="custom-star-wrap" style="width: 100%;">
                      <div class="custom-star">
                      </div>
                    </div>
                  </div>
                </div>
              </li>
              <li class="fish-rate-star fish-rate-star-full">
                <div role="radio" aria-checked="true" aria-posinset="4" aria-setsize="5" tabindex="-1">
                  <div class="fish-rate-star-first">
                    <div class="custom-star-wrap" style="width: 100%;">
                      <div class="custom-star">
                      </div>
                    </div>
                  </div>
                  <div class="fish-rate-star-second">
                    <div class="custom-star-wrap" style="width: 100%;">
                      <div class="custom-star">
                      </div>
                    </div>
                  </div>
                </div>
              </li>
              <li class="fish-rate-star fish-rate-star-zero">
                <div role="radio" aria-checked="true" aria-posinset="5" aria-setsize="5" tabindex="-1">
                  <div class="fish-rate-star-first">
                    <div class="custom-star-wrap" style="width: 100%;">
                      <div class="custom-star">
                      </div>
                    </div>
                  </div>
                  <div class="fish-rate-star-second">
                    <div class="custom-star-wrap" style="width: 100%;">
                      <div class="custom-star">
                      </div>
                    </div>
                  </div>
                </div>
              </li>
            </ul>
            <span class="index-module_text_rukZW">5.0分</span>
          </div>
        </div>
        <span class="fish-dropdown-trigger index-module_assessment-btn_6imdF ">
          <button type="button" class="fish-btn fish-btn-round">
            <svg class="index-module_rate-icon_YM1Lc">
              <use xlink:href="#icon_evaluate_fill">
              </use>
            </svg>
            <span class="index-module_assessment-btn-text_fw-+Z">评价</span>
          </button>
        </span>
      </div>
      <div class="index-module_extra_tUQog">
        <div class="index-module_like-wrap_NbyLe  ">
          <svg class="index-module_like_qOb9K">
            <use xlink:href="#icon_thumb_fill">
            </use>
          </svg>
          <div class="index-module_like-count_GXOGd">100w+点赞</div>
        </div>
        <div class="index-module_suggestion-wrap_s+Ii+ ">
          <svg class="index-module_suggestion-icon_IrRxU">
            <use xlink:href="#icon_feedback_fill">
            </use>
          </svg>
          <div>建议</div>
        </div>
      </div>
    </div>
  </div>
  <div class="index-module_divider_rI-lg">
  </div>
  <div class="imageList-module_special-edu-image-list-wrapper_18zfs">
    <div class="imageList-module_special-edu-image-list_+ywag" style="max-width: unset;">
      <div class="course-document">
        <div class="document-context" style="overflow: hidden; height: 95%;">
          <embed src="${link}" width="100%" height="100%">
        </div>
        <div class="doc-toolbar">
          <div class="left">
          </div>
          <div class="center">
          </div>
          <div class="right">
            <div class="split-line">
            </div>
            <div class="tool-btn fullscreen" title="全屏">
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
                `;
            }
            // 全屏切换
            const fullscreenBtn = document.querySelector(".tool-btn.fullscreen");
            const courseDocument = document.querySelector(".course-document");
            const htmlElement = document.querySelector("html");
            fullscreenBtn.addEventListener("click", () => {
                if (courseDocument.classList.contains("full-screen")) { // 当前处于"全屏状态",切换为"非全屏状态"
                    courseDocument.classList.remove("full-screen");
                    htmlElement.style.overflow = "";
                } else { // 当前处于"非全屏状态",切换为"全屏状态"
                    courseDocument.classList.add("full-screen");
                    htmlElement.style.overflow = "hidden";
                }
            });
            // 重定向至登录页
            const toLogin = [
                document.querySelector(".fish-dropdown-trigger.index-module_assessment-btn_6imdF"),
                document.querySelector(".index-module_like-wrap_NbyLe"),
                document.querySelector(".index-module_suggestion-wrap_s\\+Ii\\+")
            ];
            toLogin.forEach(element => {
                element.addEventListener("click", () => {
                    window.open("https://auth.smartedu.cn/uias/login/");
                });
            });
        } else {
            elapsedTime += 100;
            if (elapsedTime >= maxTimeToCheck) {
                clearInterval(intervalId1); // 清除定时器,停止检查
            }
        }
    }
    const intervalId1 = setInterval(checkLoginModal, 100); // 每隔 100 毫秒检查一次 .fish-modal-root 元素是否存在

    function checkIndexModule() {
        console.log("生成链接: " + link);
        fetch(link)
            .then(response => {
            if (response.ok) {
                console.log("生成链接有效"); // 有效链接
            } else {
                console.log("生成链接无效"); // 无效链接
                link = `https://r3-ndr.ykt.cbern.com.cn/edu_product/esp/assets/${contentId}.pkg/pdf.pdf`;
            }
        })
            .catch(() => {
            console.log("检测生成链接发生错误"); // 网络错误或请求被拒绝等
        });
        const container = document.querySelector(".index-module_extra_tUQog"); // 找到要添加按钮的容器元素
        if (container && !document.querySelector(".Btns")) {
            clearInterval(intervalId2); // 清除定时器
            const div = document.createElement("div");
            div.className = "Btns";
            div.innerHTML = `<a class="link" href="${link}" style="margin-left: 24px; color: rgb(136, 136, 136);">📓 查看PDF</a><a class="download" style="margin-left: 24px; color: rgb(136, 136, 136);">📓 下载PDF</a>`;
            container.appendChild(div); // 将按钮添加到网页中
            const element1 = document.querySelector("div.Btns > a.link");
            if (element1) {
                element1.addEventListener("mouseover", function() {
                    this.innerHTML = "📘 查看PDF";
                    this.style.color = "#226dec"; // 鼠标移入时修改元素的样式
                });
                element1.addEventListener("mouseout", function() {
                    this.innerHTML = "📓 查看PDF";
                    this.style.color = "#888"; // 鼠标移出时恢复原来的样式
                });
                element1.addEventListener("mousedown", function() {
                    this.innerHTML = "📘 查看PDF";
                    this.style.color = "#226dec"; // 鼠标按下时修改元素的样式
                });
            }
            const element2 = document.querySelector("div.Btns > a.download");
            if (element2) {
                element2.addEventListener("click", function(event) { // 点击下载
                    var fileUrl = link;
                    var xhr = new XMLHttpRequest();
                    xhr.open("GET", fileUrl);
                    xhr.responseType = "blob";
                    xhr.onloadstart = function() { // 初始化进度
                        element2.innerText = "0%";
                    };
                    xhr.onprogress = function(event) {
                        if (event.lengthComputable) {
                            var progress = Math.round((event.loaded / event.total) * 100);
                            element2.innerText = "📓 下载中 (" + progress + "%)";
                        }
                    };
                    xhr.onload = function() {
                        if (xhr.status === 200) {
                            var blob = xhr.response;
                            var downloadLink = document.createElement("a");
                            downloadLink.href = URL.createObjectURL(blob);
                            downloadLink.download = document.querySelector(".index-module_title_bnE9V").innerText; // 文件名
                            downloadLink.dispatchEvent(new MouseEvent("click"));
                            element2.innerText = "📓 下载PDF"; // 下载完成后恢复按钮文字
                        }
                    };
                    xhr.send(); // 发送请求
                });
            }
        } else {
            elapsedTime += 100;
            if (elapsedTime >= maxTimeToCheck) {
                clearInterval(intervalId2); // 清除定时器,停止检查
            }
        }
    }
    const intervalId2 = setInterval(checkIndexModule, 100); // 每隔 100 毫秒检查一次 index-module_extra_tUQog 元素是否存在

    const originalFetch = window.fetch; // 纠正课后阅读、备课和自学页面链接
    window.fetch = function (url, options) {
        return originalFetch(url, options).then(function(response) {
            if (response.ok && url.endsWith(".pdf")) {
                let modifiedURL = url.replace("-private", "");
                console.log("Fetched PDF (modified valid URL): " + modifiedURL);
                const element1 = document.querySelector("a.link"); // 按钮 element1
                if (element1) {
                    element1.href = modifiedURL; // 修改 element1 的链接
                }
                link = modifiedURL; // 修改 element2 的链接
                const prepContainer = document.querySelector(".index-module_resource-info__fav_3xNjm"); // 备课页面添加按钮
                if (prepContainer && !document.querySelector(".index-module_resource-info__fav_3xNjm > div > .link")) {
                    const div = document.createElement("div");
                    div.setAttribute("style", "display: flex; align-items: center;");
                    div.innerHTML = `<a class="link" href="${modifiedURL}" target="_blank" style="font-size: 14px; color: rgb(136, 136, 136);">📓 查看PDF</a>`; // 待纠正时更新链接
                    prepContainer.appendChild(div);
                }
                const syncClassSuggestion = document.getElementsByClassName("index-module_suggestion-wrap_s+Ii+ suggestion")[0]; // 自学页面添加按钮
                if (syncClassSuggestion && !document.querySelector("div > .link")) {
                    const div = document.createElement("div");
                    div.setAttribute("style", "display: flex; align-items: center; margin-left: 30px");
                    div.innerHTML = `<a class="link" href="${modifiedURL}" target="_blank" style="font-size: 14px; color: rgb(136, 136, 136);">📓 查看PDF</a>`; // 待纠正时更新链接
                    syncClassSuggestion.insertAdjacentElement("afterend", div);
                }
            }
            return response;
        });
    };

    function checkIndexModules() {
        const old_added_PDF_btns = document.querySelectorAll(".PDF-btns");
        if (old_added_PDF_btns) {
            old_added_PDF_btns.forEach(element => {
                element.remove();
            });
        }
        const item = document.querySelectorAll("li.index-module_item_GfOnF");
        if (item) {
            const img = document.querySelectorAll("div.index-module_cover_DGT6P > img");
            let pdfArray = []; // 声明PDF空数组
            for (let i = 0; i < img.length; i++) {
                if (img[i]) {
                    clearInterval(intervalId3); // 查找到 img,清除定时器停止检查
                    const src = img[i].getAttribute("src");
                    let pdf;
                    if (src.includes("esp/assets/")) {
                        let extractedText = src.match(/assets\/(.*?)\./)[1];
                        pdf = `https://r3-ndr.ykt.cbern.com.cn/edu_product/esp/assets/${extractedText}.pkg/pdf.pdf`;
                    } else if (src.includes("esp/assets_document/")) {
                        let extractedText = src.match(/assets_document\/(.*?)\./)[1];
                        pdf = `https://r3-ndr.ykt.cbern.com.cn/edu_product/esp/assets_document/${extractedText}.pkg/pdf.pdf`;
                    } else if (src.includes("65/document/")) {
                        let extractedText = src.match(/document\/(.*?)\/image/)[1];
                        pdf = `https://v3.ykt.cbern.com.cn/65/document/${extractedText}/pdf.pdf`;
                    } else {
                        console.log("未识别链接");
                        pdf = `undefined`;
                    }
                    if (pdf) {
                        pdfArray.push(pdf); // 将 pdf 添加到数组中
                        console.log(`pdf${i + 1}: ${pdf}`);
                    }
                }
            }
            const content = document.querySelectorAll("div.index-module_content_KmLzG");
            for (let i = 0; i < content.length; i++) {
                if (content[i]) {
                    content[i].setAttribute("style", "width: 500px;"); // 统一 content 宽度
                }
            }
            const contentName = document.querySelectorAll("div.index-module_line_LgJAC");
            const added_PDF_btns = document.querySelectorAll("li.index-module_item_GfOnF > div.PDF-btns");
            for (let i = 0; i < item.length; i++) {
                if (item[i] && !added_PDF_btns[i] && pdfArray[i]) {
                    const PDF_btns = document.createElement("div"); // 创建 PDF_btns
                    PDF_btns.setAttribute("class", "PDF-btns");
                    PDF_btns.innerHTML = `
                        <a type="button" class="fish-btn" style="margin-left: 24px; margin-bottom: 5px;" href="${pdfArray[i]}" target="_blank">查看PDF</a>
                        <a type="button" class="fish-btn fish-btn-primary" style="margin-left: 24px; margin-bottom: 5px;">下载PDF</a>
                    `;
                    item[i].appendChild(PDF_btns); // 将 PDF_btns 添加到父元素中
                    if (pdfArray[i] !== "undefined") {
                        PDF_btns.addEventListener("click", function(event) { // 停止 PDF_btns 的点击事件向更上一层元素传播
                            event.stopPropagation();
                        });
                    } else {
                        PDF_btns.addEventListener('click', function(event) { // 链接 undefined 则阻止默认点击行为
                            event.preventDefault();
                        });
                    }
                    const element2 = document.querySelectorAll("div.PDF-btns > a.fish-btn.fish-btn-primary")[i];
                    if (element2) {
                        element2.addEventListener("click", function(event) { // 点击下载
                            var fileUrl = pdfArray[i];
                            var xhr = new XMLHttpRequest();
                            xhr.open("GET", fileUrl);
                            xhr.responseType = "blob";
                            xhr.onloadstart = function() { // 初始化进度
                                element2.innerText = "下载中0%";
                            };
                            xhr.onprogress = function(event) {
                                if (event.lengthComputable) {
                                    var progress = Math.round((event.loaded / event.total) * 100);
                                    element2.innerText = "下载中 (" + progress + "%)";
                                }
                            };
                            xhr.onload = function() {
                                if (xhr.status === 200) {
                                    var blob = xhr.response;
                                    var downloadLink = document.createElement("a");
                                    downloadLink.href = URL.createObjectURL(blob);
                                    downloadLink.download = contentName[i].querySelector("span").getAttribute("title"); // 文件名
                                    downloadLink.dispatchEvent(new MouseEvent("click"));
                                    element2.innerText = "下载PDF"; // 下载完成后恢复按钮文字
                                }
                            };
                            xhr.send(); // 发送请求
                        });
                    }
                }
            }
        } else {
            elapsedTime += 100;
            if (elapsedTime >= maxTimeToCheck) {
                clearInterval(intervalId3); // 清除定时器,停止检查
            }
        }
    }
    let intervalId3 = setInterval(checkIndexModules, 100); // 每隔 100 毫秒检查一次

    function doItAgain() {
        clearInterval(intervalId3); // 防止多次启动定时器
        elapsedTime = 0; // 逝去时间归零
        intervalId3 = setInterval(checkIndexModules, 100); // 创建新的定时器以待调用
    }

    const observer = new MutationObserver(function(mutations) { // 创建一个MutationObserver
        mutations.forEach(function(mutation) {
            if (mutation.addedNodes && mutation.addedNodes.length > 0) { // 检查是否有新增节点
                for (let i = 0; i < mutation.addedNodes.length; i++) {
                    const addedNode = mutation.addedNodes[i];
                    if (addedNode.classList && addedNode.classList.contains("PDF-btns")) { // 判断节点的 classList 中是否存在 PDF-btns 类名
                        console.log("PDF-btns已添加");
                    } else {
                        console.log("DOM树已变化");
                        doItAgain(); // 执行新的定时器
                    }
                }
            }
        });
    });
    const config = { // 配置需要观察的内容
        childList: true, // 监听子节点的变化
        subtree: true // 监听所有后代节点的变化
    };
    observer.observe(document.body, config);

})();