NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name 国家中小学智慧教育平台 查看电子课本
// @namespace https://gist.github.com/htfc786/3b8a7b79c927ceefc6ebc1c56eb839ce
// @version 0.2
// @description 国家中小学智慧教育平台查看电子课本 by htfc786
// @author htfc786
// @license MIT
// @match https://basic.smartedu.cn/tchMaterial/*
// @icon https://basic.smartedu.cn/favicon.ico
// @grant none
// @require https://cdn.bootcdn.net/ajax/libs/crypto-js/4.2.0/crypto-js.min.js
// ==/UserScript==
// URL解析工具 (原f.parse)
function parseUrl(url) {
const options = {
strictMode: false,
key: ["source", "protocol", "authority", "userInfo", "user", "password", "host", "port", "relative", "path", "directory", "file", "query", "anchor"],
q: {
name: "queryKey",
parser: /(?:^|&)([^&=]*)=?([^&]*)/g
},
parser: {
loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
}
};
const match = options.parser.loose.exec(url) || [];
const result = {};
options.key.forEach((key, index) => {
result[key] = match[index] || "";
});
// 解析查询参数
result.queryKey = {};
result.query.replace(options.q.parser, (_, key, value) => {
if (key) {
if (!result.queryKey[key]) {
result.queryKey[key] = value;
} else if (Array.isArray(result.queryKey[key])) {
result.queryKey[key].push(value);
} else {
result.queryKey[key] = [result.queryKey[key], value];
}
}
});
return result;
}
// 获取地址栏参数
function getQueryVariable(variable) {
var urlInfo = parseUrl(window.location.href)
return urlInfo.queryKey[variable] || null
}
// 生成随机字符串 (原Ze)
function generateRandomString(length = 8) {
const charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
let result = "";
for (let i = 0; i < length; i++) {
result += charset.charAt(Math.floor(Math.random() * charset.length));
}
return result;
}
// 读取Token信息
function getToken() {
try {
const storedToken = localStorage.getItem("ND_UC_AUTH-e5649925-441d-4a53-b525-51a2f1c4e0a8&ncet-xedu&token");
const value = JSON.parse(storedToken)?.value
return storedToken ? JSON.parse(value) : null;
} catch (e) {
console.error("Token解析失败:", e);
return null;
}
}
// getAuthHeader
function getAuthHeader({url, method="GET", useToken} = {}) {
if (!url) {
console.error("UC-SDK: 缺少参数url");
return "";
}
// 获取Token(需要实现存储逻辑)
const token = useToken || getToken();
if (!token) {
console.warn("UC-SDK: 当前SDK实例未登录");
return "";
}
// 生成认证头 (原He)
function generateAuthHeader(url, method = "GET", { accessToken, macKey, diff }) {
// 生成nonce的值
const nonce = `${new Date().getTime() + parseInt(diff, 10)}:${generateRandomString(8)}`;
// 生成MAC签名 (原ze)
const parsedUrl = parseUrl(url);
const normalizedMethod = method.toUpperCase();
const signingData = `${nonce}\n${normalizedMethod}\n${parsedUrl.relative}\n${parsedUrl.authority}\n`;
const macSignature = CryptoJS.HmacSHA256(signingData, macKey).toString(CryptoJS.enc.Base64);
// 拼装
return `MAC id="${accessToken}",nonce="${nonce}",mac="${macSignature}"`;
}
return generateAuthHeader(url, method, {
accessToken: token.access_token,
macKey: token.mac_key,
diff: token.diff
});
}
function getBookInfo(contentId) {
return fetch(`https://s-file-2.ykt.cbern.com.cn/zxx/ndrv2/resources/tch_material/details/${contentId}.json`)
.then(response => response.json())
.then(data => {
let pdfUrl = null;
const title = data.title;
const ti_items = data.ti_items;
// 寻找Array中lc_ti_format为pdf的那一项
const pdfItem = ti_items.find(item => item.lc_ti_format === "pdf");
if (pdfItem) {
pdfUrl = pdfItem.ti_storages[0]; // 返回pdfItem.ti_storages[0]的值
}
// 返回对象
return { pdfUrl, title };
})
}
// 加载样式文本
const buildLoadingCssText = `
.extensionLoading {
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-family: Arial, sans-serif;
}
.loading-progress-container {
width: 80%;
max-width: 600px;
height: 20px;
background-color: #e0e0e0;
border-radius: 10px;
overflow: hidden;
margin: 20px 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.loading-progress-bar {
height: 100%;
background: linear-gradient(90deg, #4285f4, #0ea5e9);
border-radius: 10px;
width: 0%;
}
.loading-text {
font-size: 18px;
color: #333;
margin-bottom: 10px;
}
.loading-percentage {
font-size: 16px;
color: #666;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #4285f4;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
// 构建加载DOM并处理进度
function buildLoadingDom(injectDom, downloadFetch) {
injectDom.innerHTML = "";
// 添加样式
const style = document.createElement("style");
style.innerHTML = buildLoadingCssText;
document.head.appendChild(style);
// 创建加载容器
const extensionLoading = document.createElement("div");
extensionLoading.className = "extensionLoading";
// 加载动画
const spinner = document.createElement("div");
spinner.className = "loading-spinner";
extensionLoading.appendChild(spinner);
// 加载文本
const loadingText = document.createElement("div");
loadingText.className = "loading-text";
loadingText.textContent = "正在加载电子课本...";
extensionLoading.appendChild(loadingText);
// 进度条容器
const progressContainer = document.createElement("div");
progressContainer.className = "loading-progress-container";
extensionLoading.appendChild(progressContainer);
// 进度条
const progressBar = document.createElement("div");
progressBar.className = "loading-progress-bar";
progressContainer.appendChild(progressBar);
// 百分比显示
const percentageText = document.createElement("div");
percentageText.className = "loading-percentage";
percentageText.textContent = "0.00%";
extensionLoading.appendChild(percentageText);
injectDom.append(extensionLoading);
// 处理下载进度
return downloadFetch.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 获取文件总大小
const contentLength = response.headers.get('Content-Length');
const totalSize = contentLength ? parseInt(contentLength) : 0;
let loadedSize = 0;
// 创建读取器
const reader = response.body.getReader();
const chunks = [];
// 读取数据并更新进度
function read() {
return reader.read().then(({ done, value }) => {
if (done) {
// 下载完成,进度设为100%
progressBar.style.width = "100%";
percentageText.textContent = "100.00%";
return chunks;
}
chunks.push(value);
loadedSize += value.length;
// 计算进度百分比(保留两位小数)
const progress = totalSize ? (loadedSize / totalSize) * 100 : 0;
const progressFixed = progress.toFixed(2);
// 更新进度条和文本
progressBar.style.width = `${progressFixed}%`;
percentageText.textContent = `${progressFixed}%`;
return read();
});
}
// 开始读取并拼接Blob
return read().then(chunks => {
const blob = new Blob(chunks, { type: 'application/pdf' });
return blob;
});
});
}
const buildPdfDomCssText = `
.extensionPdfContainer {
padding: 0;
position: relative;
z-index: 1200;
height: 100%;
}
.extensionPdfContainer > embed {
height: 100%;
width: 100%;
}
.extensionPdfContainer.big > embed {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 100000;
}
.extensionPdfContainer > a {
width: 60px;
height: 20px;
font-size: 12px;
background-color: rgba(102,204,255,0.5);
position: absolute;
text-align: center;
line-height: 20px;
color: #fff;
left: 0;
top: 0;
z-index: 100000;
}
.extensionPdfContainer.big > a {
position: fixed;
z-index: 1000000;
}
.extensionNewPageOpen {
width: 70px;
height: 60px;
background-color: red;
position: fixed;
text-align: center;
line-height: 30px;
color: #fff;
font-size: 20px;
left: 0;
bottom: 0;
z-index: 1100;
}
`;
function buildPdfDom(injectDom, pdfUrl) {
injectDom.innerHTML = "";
// style
const style = document.createElement("style");
style.innerHTML = buildPdfDomCssText;
document.head.appendChild(style);
// injectDom
// .extensionPdfContainer
const extensionPdfContainer = document.createElement("div");
extensionPdfContainer.className = "extensionPdfContainer";
// .extensionPdfContainer > embed
const embed = document.createElement("embed");
embed.id = "pdfWatch";
embed.src = pdfUrl;
extensionPdfContainer.appendChild(embed);
// .extensionPdfContainer > a
const pdfBig = document.createElement("a");
pdfBig.id = "pdfBig";
pdfBig.innerText = "网页全屏";
pdfBig.addEventListener("click", ()=>{
if(!extensionPdfContainer.classList.contains("big")){
extensionPdfContainer.classList.add("big")
pdfBig.innerText = "退出全屏"
}else{
extensionPdfContainer.classList.remove("big")
pdfBig.innerText = "网页全屏"
}
});
extensionPdfContainer.appendChild(pdfBig);
injectDom.appendChild(extensionPdfContainer);
// 新页面打开
const extensionNewPageOpen = document.createElement("a");
extensionNewPageOpen.className = "extensionNewPageOpen";
extensionNewPageOpen.innerText = "新页面打开";
extensionNewPageOpen.target = "_blank";
extensionNewPageOpen.href = pdfUrl;
document.body.appendChild(extensionNewPageOpen);
}
(async function() {
'use strict';
// Your code here...
if (!getToken()) { // 没有登陆就不运行
return;
}
var contentId = getQueryVariable("contentId");
const { pdfUrl, title } = await getBookInfo(contentId)
const authHeader = getAuthHeader({ url:pdfUrl }) // key: x-nd-auth
console.log("pdfUrl: ", pdfUrl)
console.log("authHeader: ", authHeader)
// 生成curl和wget的下载链接
const curl = `curl -H 'x-nd-auth: ${authHeader}' '${pdfUrl}' -o '${title}.pdf'`;
const wget = `wget -O '${title}.pdf' --header='x-nd-auth: ${authHeader}' '${pdfUrl}'`;
console.log("curl: ", curl)
console.log("wget: ", wget)
// 下载pdf
const downloadFetch = fetch(pdfUrl, {
headers: {
'x-nd-auth': authHeader,
}
})
function start(injectDom) {
injectDom.innerHTML = ""
buildLoadingDom(injectDom, downloadFetch)
.then(blob => {
console.log("Download Success! blob url:", blob)
const url = URL.createObjectURL(blob, { type: 'application/pdf' });
buildPdfDom(injectDom, url);
})
}
let waitTime = 0;
let wait = () => {
if (waitTime>500){
console.log("wait too many times!!!")
console.log("exit")
return;
}
const injectDom = document.querySelector(".document-context");
if (injectDom) {
start(injectDom)
return;
}
waitTime++;
setTimeout(wait, 100);
}
wait()
})();