├── .gitignore ├── README.md ├── background.js ├── content.js └── manifest.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows 2 | [Dd]esktop.ini 3 | Thumbs.db 4 | $RECYCLE.BIN/ 5 | 6 | # macOS 7 | .DS_Store 8 | .fseventsd 9 | .Spotlight-V100 10 | .TemporaryItems 11 | .Trashes -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 文泉学堂下载器 2 | 3 | > **2022-10-1 更新** 4 | > 5 | > 文泉学堂更新了反爬虫措施,目前在线浏览的图片为几张图片拼接而成,本插件无法正常使用,且将**停止更新**,但**仍将接受 PR**。 6 | 7 | 用于下载文泉学堂教材图片的 Chrome 扩展程序。支持自动下载全部页面,从特定页面开始下载,或手动点击下载特定页面的 jpeg 文件。 8 | 9 | ## 使用 10 | 11 | ### 安装 12 | 13 | 1. 前往 [Releases](https://github.com/liang2kl/wqxuetang-downloader/releases) 下载源代码(`Source Code (zip)`) 14 | 2. 在 Chrome 地址栏输入 `chrome://extensions` 并前往 15 | 3. 打开右上角 `开发者模式` 16 | 4. 点击 `加载已解压的扩展程序` 17 | 5. 选择含源代码的目录 18 | 19 | ### 使用 20 | 21 | 1. 确保扩展程序拥有运行权限 22 | 2. 下载 23 | - 下载特定页面:在页面点击鼠标右键,选择 `文泉学堂下载器-下载当前页面` 24 | - 下载全部页面:点击鼠标右键,选择 `文泉学堂下载器-下载所有` 25 | - 从特定页面开始下载:点击鼠标右键,选择 `文泉学堂下载器-从特定页面开始下载`,并输入合法的页码值。 26 | - 从当前页面开始下载:在页面点击鼠标右键,选择 `文泉学堂下载器-从当前页面开始下载` 27 | 2. 停止与尝试继续 28 | - 停止下载:进行全部页面的下载过程中,点击鼠标右键,选择 `文泉学堂下载器-停止下载` 29 | - 尝试继续下载:在某页卡住时,点击鼠标右键,选择 `文泉学堂下载器-继续下载` 30 | 31 | 同时下载的多个文件将储存在以图书编号为名的文件夹内;使用 `下载当前页面` 下载的文件将直接下载到设定的下载文件夹中。 32 | 33 | ## 问题与解决办法 34 | 35 | ### 使用 `下载所有` 时在某一页面中卡住 36 | 37 | 这是一个已知问题。解决办法: 38 | 39 | 尝试菜单中的 `继续下载`,或者尝试刷新页面,或者: 40 | 41 | 1. 首先打开开发者工具(`F12` 或 `ctrl+shift+I` 或 `option+command+I`) 42 | 2. 然后关闭开发者工具 43 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | const DOWNLOAD_CURRENT_MENU = "DOWNLOAD_CURRENT_MENU" 2 | const DOWNLOAD_ALL_MENU = "DOWNLOAD_ALL_MENU" 3 | const DOWNLOAD_FROM_MENU = "DOWNLOAD_FROM_MENU" 4 | const DOWNLOAD_FROM_CURRENT_MENU = "DOWNLOAD_FROM_CURRENT_MENU" 5 | const TRY_FIX_MENU = "TRY_FIX_MENU" 6 | 7 | const patterns = [ 8 | "https://lib-tsinghua.wqxuetang.com/*", 9 | "https://wqxuetang.com/*" 10 | ] 11 | 12 | var stop = false 13 | 14 | chrome.contextMenus.create({ 15 | id: DOWNLOAD_CURRENT_MENU, 16 | title: "下载当前页面", 17 | documentUrlPatterns: patterns 18 | }) 19 | 20 | 21 | chrome.contextMenus.create({ 22 | id: DOWNLOAD_ALL_MENU, 23 | title: "下载所有", 24 | documentUrlPatterns: patterns, 25 | contexts: ["all"] 26 | }) 27 | 28 | chrome.contextMenus.create({ 29 | id: DOWNLOAD_FROM_MENU, 30 | title: "从特定页面开始下载", 31 | documentUrlPatterns: patterns, 32 | contexts: ["all"], 33 | }) 34 | 35 | chrome.contextMenus.create({ 36 | id: DOWNLOAD_FROM_CURRENT_MENU, 37 | title: "从当前页面开始下载", 38 | documentUrlPatterns: patterns, 39 | contexts: ["all"], 40 | }) 41 | 42 | chrome.contextMenus.create({ 43 | id: TRY_FIX_MENU, 44 | title: "继续下载(若遇到停止下载的情况)", 45 | documentUrlPatterns: patterns, 46 | contexts: ["all"], 47 | enabled: false 48 | }) 49 | 50 | chrome.contextMenus.onClicked.addListener((info, tab) => { 51 | const bookId = getBookId(tab.url) 52 | 53 | if (info.menuItemId == DOWNLOAD_CURRENT_MENU) { 54 | chrome.tabs.sendMessage(tab.id, { name: "getClickedElement" }, null, (data) => { 55 | if (data.url) { 56 | 57 | chrome.downloads.download({ 58 | url: data.url, 59 | filename: "文泉学堂-" + bookId + "-" + data.page + "页.jpeg" 60 | }); 61 | } 62 | }); 63 | } else { 64 | var port = chrome.tabs.connect(tab.id, { name: "downloadAll" }) 65 | 66 | port.onMessage.addListener((message, _) => { 67 | 68 | if (message.finished) { 69 | reverseDownloadState(true) 70 | } 71 | 72 | if (message.url) { 73 | chrome.downloads.download({ 74 | url: message.url, 75 | filename: "文泉学堂-" + bookId + "/" + message.index + ".jpeg" 76 | }); 77 | } 78 | }) 79 | 80 | if (info.menuItemId == DOWNLOAD_ALL_MENU) { 81 | if (stop) { 82 | reverseDownloadState(true) 83 | port.postMessage({ 84 | stop: true 85 | }) 86 | return 87 | } 88 | 89 | port.postMessage({ stop: false }) 90 | reverseDownloadState(false) 91 | 92 | } else if (info.menuItemId == TRY_FIX_MENU) { 93 | port.postMessage({ 94 | forceResume: true 95 | }) 96 | } else if (info.menuItemId == DOWNLOAD_FROM_MENU) { 97 | port.postMessage({ stop: false, from: true }) 98 | reverseDownloadState(false) 99 | } else if (info.menuItemId == DOWNLOAD_FROM_CURRENT_MENU) { 100 | port.postMessage({ stop: false, fromCurrent: true }) 101 | reverseDownloadState(false) 102 | } 103 | } 104 | }) 105 | 106 | getBookId = (url) => { 107 | var numIndex = 1 108 | while (!isNaN(parseInt(url.substring(url.length - numIndex, url.length)))) { 109 | numIndex++ 110 | } 111 | const bookId = url.substring(url.length - numIndex + 1, url.length) 112 | return bookId 113 | } 114 | 115 | reverseDownloadState = (_stop) => { 116 | if (_stop) { 117 | chrome.contextMenus.update(DOWNLOAD_ALL_MENU, { 118 | title: "下载所有", 119 | documentUrlPatterns: patterns, 120 | contexts: ["all"] 121 | }, () => { stop = false }) 122 | chrome.contextMenus.update(TRY_FIX_MENU, { 123 | enabled: false 124 | }) 125 | chrome.contextMenus.update(DOWNLOAD_FROM_MENU, { 126 | enabled: true 127 | }) 128 | chrome.contextMenus.update(DOWNLOAD_FROM_CURRENT_MENU, { 129 | enabled: true 130 | }) 131 | 132 | } else { 133 | chrome.contextMenus.update(DOWNLOAD_ALL_MENU, { 134 | title: "停止下载", 135 | }, () => { stop = true }) 136 | chrome.contextMenus.update(TRY_FIX_MENU, { 137 | enabled: true 138 | }) 139 | chrome.contextMenus.update(DOWNLOAD_FROM_MENU, { 140 | enabled: false 141 | }) 142 | chrome.contextMenus.update(DOWNLOAD_FROM_CURRENT_MENU, { 143 | enabled: false 144 | }) 145 | } 146 | } 147 | 148 | chrome.tabs.onUpdated.addListener(() => { 149 | reverseDownloadState(true) 150 | }) -------------------------------------------------------------------------------- /content.js: -------------------------------------------------------------------------------- 1 | var data = { 2 | url: null, 3 | page: null 4 | }; 5 | 6 | var stop = false 7 | 8 | var loadingIndex = -1 9 | 10 | document.addEventListener("contextmenu", (event) => { data = dataOfChildNode(event.target) }, true); 11 | 12 | chrome.runtime.onMessage.addListener((request, _, sendResponse) => { 13 | if (request.name == "getClickedElement") { 14 | if (data.url) { 15 | sendResponse(data) 16 | data.url = null 17 | data.page = null 18 | } else { 19 | alert("文泉学堂下载器:解析图片数据失败。") 20 | } 21 | } else if (request.name == "resumeDownload") { 22 | if (loadingIndex !== -1) { 23 | forceResume = true 24 | } 25 | sendResponse(null) 26 | } 27 | }); 28 | 29 | chrome.runtime.onConnect.addListener((port) => { 30 | console.assert(port.name == "downloadAll") 31 | 32 | port.onMessage.addListener((message, _) => { 33 | if (message.forceResume) { 34 | const currentIndex = loadingIndex 35 | loadingIndex = -1 36 | downloadImg(currentIndex, port) 37 | return 38 | } 39 | 40 | if (message.stop) { 41 | stop = true 42 | } else { 43 | if (message.from) { 44 | const input = window.prompt("输入页码") 45 | const length = getChildren().length 46 | 47 | if (!isNaN(parseInt(input))) { 48 | if (input >= length - 2) { 49 | alert("页码不在范围内。请输入1~" + (length - 3) + "内的整数。") 50 | port.postMessage({ finished: true }) 51 | return 52 | } else { 53 | stop = false 54 | downloadImg(parseInt(input), port) 55 | } 56 | } else { 57 | alert("输入无效。请输入1~" + (length - 3) + "内的整数。") 58 | port.postMessage({ finished: true }) 59 | return 60 | } 61 | 62 | } else if (message.fromCurrent) { 63 | if (data.page) { 64 | stop = false 65 | const page = data.page 66 | data.url = null 67 | data.page = null 68 | downloadImg(parseInt(page), port) 69 | } else { 70 | alert("文泉学堂下载器:解析图片数据失败。") 71 | } 72 | } else { 73 | stop = false 74 | downloadImg(1, port) 75 | } 76 | } 77 | }) 78 | }) 79 | 80 | function downloadImg(index, port) { 81 | if (stop) { 82 | return 83 | } 84 | const children = getChildren() 85 | if (index === children.length - 2) { 86 | port.postMessage({ finished: true }) 87 | return 88 | } 89 | var child = children[index] 90 | child.scrollIntoView() 91 | 92 | const data = dataOfChildNode(child) 93 | 94 | if (isValidImgSrc(data.url)) { 95 | 96 | download(data.url, index, port) 97 | downloadImg(index + 1, port) 98 | } else { 99 | loadingIndex = index; 100 | var observer = new MutationObserver((mutations) => { 101 | mutations.forEach((mutation) => { 102 | if (mutation.type == "attributes") { 103 | const newData = dataOfChildNode(child) 104 | 105 | if (isValidImgSrc(newData.url)) { 106 | observer.disconnect() 107 | 108 | if (loadingIndex === index) { 109 | loadingIndex = -1 110 | download(newData.url, index, port) 111 | downloadImg(index + 1, port) 112 | } 113 | } 114 | } 115 | }) 116 | }) 117 | 118 | observer.observe(child, { 119 | attributes: true 120 | }) 121 | 122 | } 123 | } 124 | 125 | function getChildren() { 126 | var pageBox = document.getElementById("pagebox") 127 | return pageBox.childNodes 128 | } 129 | 130 | function download(url, index, port) { 131 | port.postMessage({ 132 | url: url, 133 | index: index 134 | }) 135 | } 136 | 137 | function dataOfChildNode(node) { 138 | return { 139 | url: node.childNodes[0].getAttribute("src"), 140 | page: node.getAttribute("index") 141 | } 142 | } 143 | 144 | function isValidImgSrc(src) { 145 | if (!src) { return false } 146 | const image = new Image() 147 | image.src = src 148 | if (image.width > 400) { return true } 149 | else { return false } 150 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "文泉学堂下载器", 3 | "version": "0.2", 4 | "manifest_version": 3, 5 | "permissions": [ 6 | "contextMenus", 7 | "downloads", 8 | "scripting" 9 | ], 10 | "background": { 11 | "service_worker": "background.js" 12 | }, 13 | "content_scripts": [ 14 | { 15 | "matches": ["https://lib-tsinghua.wqxuetang.com/*", "https://wqxuetang.com/*"], 16 | "js": ["content.js"], 17 | "all_frames": true 18 | } 19 | ], 20 | "action": {} 21 | } --------------------------------------------------------------------------------