您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Download PW course content
// ==UserScript== // @name PW Course Downloader // @namespace KorigamiK // @version 1.0.0 // @description Download PW course content // @author KorigamiK // @match https://www.pw.live/* // @grant GM_addStyle // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.9.1/jszip.min.js // @license MIT // ==/UserScript== // deno-lint-ignore-file no-window const API = { BASE_URL: "https://api.penpencil.co", BATCH_ID: "671f543f10df7bb168d0296a", SESSION_TOKEN: /* Paste your session token here */ "Bearer ...", async fetch(endpoint) { const res = await fetch(`${this.BASE_URL}${endpoint}`, { headers: { Authorization: this.SESSION_TOKEN }, }); return res.json(); }, getBatch: (slug) => API.fetch(`/v3/batches/${slug}/details`), getContent: (batchSlug, subjectSlug, type = "videos") => API.fetch( `/v2/batches/${batchSlug}/subject/${subjectSlug}/contents?contentType=${type}`, ), getTests: (subjectId) => API.fetch( `/v3/test-service/tests/check-tests?testSource=BATCH_QUIZ&batchId=${API.BATCH_ID}&batchSubjectId=${subjectId}`, ), getDPPs: (subjectId) => API.fetch( `/v3/test-service/tests/dpp?batchId=${API.BATCH_ID}&batchSubjectId=${subjectId}&isSubjective=false`, ), }; const UI = { styles: ` .pw-dl { position: fixed; bottom: 20px; right: 20px; background: #fff; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.15); padding: 15px; z-index: 9999; width: 300px; font-family: system-ui; } .pw-dl select { width: 100%; margin: 8px 0; padding: 5px; } .pw-dl button { background: #4CAF50; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; width: 100%; } .pw-dl .status { font-size: 14px; color: #666; margin-top: 8px; } `, create() { // Check if UI already exists if (document.querySelector(".pw-dl")) { return document.querySelector(".pw-dl"); } GM_addStyle(this.styles); const container = document.createElement("div"); container.className = "pw-dl"; container.innerHTML = ` <select multiple size="5"></select> <button class="download-selected">Download Selected</button> <div class="status"></div> `; document.body.appendChild(container); return container; }, updateStatus(msg) { document.querySelector(".pw-dl .status").textContent = msg; }, }; class ContentDownloader { constructor(batchSlug) { if (window.pwDownloaderInstance) { return window.pwDownloaderInstance; } this.batchSlug = batchSlug; this.subjects = []; this.zip = new JSZip(); this.init(); window.pwDownloaderInstance = this; } async init() { const ui = UI.create(); const select = ui.querySelector("select"); // Add download all buttons ui.querySelector("button").insertAdjacentHTML( "beforebegin", ` <div style="display: flex; gap: 8px; margin-bottom: 8px;"> <button class="download-all">Download All Content</button> <button class="download-pdfs">Download All PDFs</button> </div> `, ); try { const { data } = await API.getBatch(this.batchSlug); this.subjects = data.subjects; select.innerHTML = this.subjects .map((s) => `<option value="${s._id}">${s.subject}</option>`) .join(""); ui.querySelector(".download-selected").addEventListener( "click", () => this.downloadSelected(select), ); ui.querySelector(".download-all").addEventListener( "click", () => this.downloadAll(), ); ui.querySelector(".download-pdfs").addEventListener( "click", () => this.downloadAllPDFs(), ); } catch (err) { UI.updateStatus("Failed to load subjects"); console.error(err); } } async downloadSelected(select) { const selectedOptions = [...select.selectedOptions]; if (selectedOptions.length === 0) { UI.updateStatus("Please select subjects"); return; } const selectedSubjects = selectedOptions .map((option) => this.subjects.find((s) => s._id === option.value)) .filter(Boolean); UI.updateStatus(`Processing ${selectedSubjects.length} subjects...`); try { this.zip = new JSZip(); const rootFolder = this.zip.folder("PW Selected Subjects"); let totalPdfs = 0; let processedPdfs = 0; let totalSize = 0; for (const [index, subject] of selectedSubjects.entries()) { try { UI.updateStatus( `Processing ${subject.subject} (${ index + 1 }/${selectedSubjects.length})...`, ); const subjectFolder = rootFolder.folder( this.sanitizeFileName(subject.subject), ); const content = await this.getSubjectContent(subject); // Add content JSON const contentJson = JSON.stringify(content, null, 2); subjectFolder.file("content.json", contentJson); totalSize += contentJson.length; // Process PDFs if (content.notes?.length) { const pdfFolder = subjectFolder.folder("PDFs"); const pdfs = this.extractPdfLinks(content.notes); totalPdfs += pdfs.length; if (pdfs.length) { const pdfSizes = await this.processPdfs( pdfs, pdfFolder, (processed) => { processedPdfs += processed; UI.updateStatus( `Downloaded ${processedPdfs}/${totalPdfs} PDFs...`, ); }, ); totalSize += pdfSizes.reduce((a, b) => a + b, 0); } } // Check total size if (totalSize > 1.5 * 1024 * 1024 * 1024) { // 1.5GB limit throw new Error( "Content size too large. Try selecting fewer subjects.", ); } } catch (error) { console.error(`Error processing subject ${subject.subject}:`, error); UI.updateStatus( `Warning: Some content for ${subject.subject} might be missing`, ); await new Promise((resolve) => setTimeout(resolve, 1000)); } } UI.updateStatus("Creating ZIP file... This might take a moment."); try { const zipBlob = await this.generateZipBlob(); if (!this.isValidBlob(zipBlob)) { throw new Error("Invalid ZIP file generated"); } await this.downloadZip(zipBlob); UI.updateStatus("Download complete!"); } catch (error) { throw new Error(`ZIP creation failed: ${error.message}`); } } catch (error) { console.error("Download error:", error); UI.updateStatus( `Error: ${error.message}. Try downloading fewer subjects.`, ); } finally { this.zip = new JSZip(); } } async generateZipBlob() { return await this.zip.generateAsync({ type: "blob", compression: "DEFLATE", compressionOptions: { level: 6 }, }) } isValidBlob(blob) { return blob && blob instanceof Blob && blob.size > 0; } async processPdfs(pdfs, folder, progressCallback) { const chunks = this.chunkArray(pdfs, 3); let processedCount = 0; const sizes = []; for (const chunk of chunks) { const chunkPromises = chunk.map(async (pdf) => { try { const response = await fetch(pdf.url); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const blob = await response.blob(); folder.file(this.sanitizeFileName(pdf.filename), blob); sizes.push(blob.size); return true; } catch (error) { console.error(`Failed to download PDF: ${pdf.filename}`, error); return false; } }); const results = await Promise.allSettled(chunkPromises); const successCount = results.filter((r) => r.status === "fulfilled" && r.value ).length; processedCount += successCount; progressCallback(successCount); await new Promise((resolve) => setTimeout(resolve, 500)); } return sizes; } downloadZip(blob) { return new Promise((resolve, reject) => { try { if (!this.isValidBlob(blob)) { reject(new Error("Invalid ZIP blob")); return; } const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; const timestamp = new Date().toISOString().split("T")[0]; a.download = `pw_selected_subjects_${timestamp}.zip`; a.onclick = () => { setTimeout(() => { URL.revokeObjectURL(url); resolve(); }, 1000); }; a.click(); } catch (error) { reject(error); } }); } extractPdfLinks(notes) { const pdfs = []; notes.forEach((note) => { note.homeworkIds?.forEach((homework) => { homework.attachmentIds?.forEach((attachment) => { pdfs.push({ topic: homework.topic, url: `${attachment.baseUrl}${attachment.key}`, filename: attachment.name, }); }); }); }); return pdfs; } async downloadAll() { UI.updateStatus("Downloading all content..."); const content = {}; for (const subject of this.subjects) { UI.updateStatus(`Downloading ${subject.subject}...`); content[subject.subject] = await this.getSubjectContent(subject); } this.saveToFile(content, "all_content"); UI.updateStatus("All content downloaded!"); } async downloadAllPDFs() { UI.updateStatus("Gathering PDF links..."); const pdfs = []; // Create main folder in zip const rootFolder = this.zip.folder("PW Course PDFs"); for (const subject of this.subjects) { UI.updateStatus(`Processing ${subject.subject}...`); const { notes } = await this.getSubjectContent(subject); // Create subject folder const subjectFolder = rootFolder.folder( this.sanitizeFileName(subject.subject), ); notes.forEach((note) => { note.homeworkIds?.forEach((homework) => { homework.attachmentIds?.forEach((attachment) => { pdfs.push({ subject: subject.subject, topic: homework.topic, url: `${attachment.baseUrl}${attachment.key}`, filename: attachment.name, folder: subjectFolder, }); }); }); }); } if (pdfs.length === 0) { UI.updateStatus("No PDFs found!"); return; } UI.updateStatus(`Found ${pdfs.length} PDFs. Starting download...`); try { await this.downloadAndZipPDFs(pdfs); } catch (error) { console.error("Error creating ZIP:", error); UI.updateStatus("Error creating ZIP file!"); } } async downloadAndZipPDFs(pdfs) { let completed = 0; // Download all PDFs concurrently but with rate limiting const chunks = this.chunkArray(pdfs, 5); // Process 5 PDFs at a time for (const chunk of chunks) { await Promise.all(chunk.map(async (pdf) => { try { const response = await fetch(pdf.url); const blob = await response.blob(); // Add to appropriate folder in zip const safeName = this.sanitizeFileName(pdf.filename); pdf.folder.file(safeName, blob); completed++; UI.updateStatus(`Processing PDFs: ${completed}/${pdfs.length}`); } catch (error) { console.error(`Failed to process ${pdf.filename}:`, error); } })); } UI.updateStatus("Creating ZIP file..."); // Generate and download ZIP const zipBlob = await this.zip.generateAsync({ type: "blob", compression: "DEFLATE", compressionOptions: { level: 6, }, }); const url = URL.createObjectURL(zipBlob); const a = document.createElement("a"); a.href = url; a.download = `pw_course_pdfs_${new Date().toISOString().split("T")[0]}.zip`; a.click(); URL.revokeObjectURL(url); // Reset zip for next use this.zip = new JSZip(); UI.updateStatus("All PDFs downloaded in ZIP!"); } // Utility methods sanitizeFileName(filename) { return filename.replace(/[/\\?%*:|"<>]/g, "-"); } chunkArray(array, size) { const chunks = []; for (let i = 0; i < array.length; i += size) { chunks.push(array.slice(i, i + size)); } return chunks; } async getSubjectContent(subject) { const [videos, notes, tests, dpps] = await Promise.all([ API.getContent(this.batchSlug, subject.slug, "videos"), API.getContent(this.batchSlug, subject.slug, "notes"), API.getTests(subject._id), API.getDPPs(subject._id), ]); return { videos: videos.data.map((v) => ({ title: v.topic, url: v.url, date: v.date, duration: v.videoDetails?.duration, teacher: v.teachers?.[0] ? `${v.teachers[0].firstName} ${v.teachers[0].lastName}`.trim() : "Unknown Teacher", })), notes: notes.data, tests: tests.data, dpps: dpps.data, }; } saveToFile(data, prefix = "content") { const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json", }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `pw_${prefix}_${new Date().toISOString().split("T")[0]}.json`; a.click(); URL.revokeObjectURL(url); } } // Initialize only if not already initialized if (!window.pwDownloaderInstance) { new ContentDownloader( "crash-course-gate-2025-computer-science-and-it-200846", ); }
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址