USST Downloader

一键下载一网畅学平台上的课件

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==UserScript==
// @name         USST Downloader
// @namespace    https://www.zhihu.com/people/tekcor_
// @version      1.0
// @description  一键下载一网畅学平台上的课件
// @author       tekcor
// @match        https://1906.usst.edu.cn/course/*
// @grant        GM_download
// @grant        GM_addStyle
// @license      MIT license
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    const TARGET_URL_PART = "sub_course_id";
    const INTERMEDIATE_URL_TEMPLATE ="https://1906.usst.edu.cn//api/uploads/reference/document/{REFERENCE_ID}/url?preview=true";

    let currentFileFinalUrl = null; // 用于存储类型1数据的最终下载链接
    let allActivitiesData = null;   // 用于存储类型2数据的 activities 数组

    console.log("油猴脚本 (v1.0) - USST Downloader - 已启动");

    // --- UI 创建 ---
    function createControlPanel() {
        const panel = document.createElement('div');
        panel.id = 'userscript-download-panel';
        panel.innerHTML = `
            <div style="font-weight: bold; margin-bottom: 5px; padding-bottom: 3px; border-bottom: 1px solid #ccc;">下载助手</div>
            <button id="btnDownloadCurrentUserscript" disabled>下载当前文件</button>
            <button id="btnDownloadAllUserscript" disabled style="margin-top: 5px;">下载全部文件</button>
        `;
        document.body.appendChild(panel);

        GM_addStyle(`
            #userscript-download-panel {
                position: fixed;
                top: 20px;
                right: 20px;
                background-color: #f0f0f0;
                border: 1px solid #ccc;
                padding: 10px;
                z-index: 9999;
                font-family: sans-serif;
                font-size: 12px;
                border-radius: 5px;
                box-shadow: 0 0 10px rgba(0,0,0,0.1);
            }
            #userscript-download-panel button {
                display: block;
                width: 100%;
                padding: 5px;
                font-size: 12px;
                cursor: pointer;
                border: 1px solid #bbb;
                border-radius: 3px;
                background-color: #e7e7e7;
            }
            #userscript-download-panel button:disabled {
                cursor: not-allowed;
                opacity: 0.6;
            }
            #userscript-download-panel button:not(:disabled):hover {
                background-color: #d7d7d7;
            }
        `);

        document.getElementById('btnDownloadCurrentUserscript').addEventListener('click', handleDownloadCurrent);
        document.getElementById('btnDownloadAllUserscript').addEventListener('click', handleDownloadAll);
    }

    function extractFilenameFromUrl(fileUrl, defaultFilenamePrefix, referenceId = '') {
        try {
            const urlObj = new URL(fileUrl);
            const nameParam = urlObj.searchParams.get('name');
            if (nameParam) {
                return decodeURIComponent(nameParam);
            }
        } catch (e) {
            console.error(`[油猴脚本-UI] 解析URL提取文件名时出错: "${fileUrl}"`, e);
        }
        return `${defaultFilenamePrefix}_${referenceId || Date.now()}.pdf`;
    }

    // --- 下载处理函数 ---
    function handleDownloadCurrent() {
        if (!currentFileFinalUrl) {
            alert("没有可供下载的“当前文件”链接。");
            return;
        }
        const btn = document.getElementById('btnDownloadCurrentUserscript');
        btn.disabled = true;
        btn.textContent = "下载中...";

        const filename = extractFilenameFromUrl(currentFileFinalUrl, "current_file");
        console.log(`[油猴脚本-UI] “下载当前文件”点击。文件名: "${filename}", URL: ${currentFileFinalUrl}`);
        GM_download(currentFileFinalUrl, filename);

        // 考虑是否下载后重置状态或让用户可以再次点击
        setTimeout(() => { // 延迟一点恢复按钮状态
            btn.textContent = "下载当前文件";
            // currentFileFinalUrl = null; // 如果只允许一次下载,则取消注释
            // btn.disabled = !currentFileFinalUrl; // 根据currentFileFinalUrl是否重置来决定
             btn.disabled = false; // 允许重复下载同一个“当前文件”
        }, 2000);
    }

    async function handleDownloadAll() {
        if (!allActivitiesData || allActivitiesData.length === 0) {
            alert("没有可供“下载全部”的数据。");
            return;
        }
        const btn = document.getElementById('btnDownloadAllUserscript');
        btn.disabled = true;
        btn.textContent = "下载中...";

        console.log(`[油猴脚本-UI] “下载全部”点击。共 ${allActivitiesData.length} 个 activity。`);
        let downloadedCount = 0;

        for (let index = 0; index < allActivitiesData.length; index++) {
            const activity = allActivitiesData[index];
            const logPrefix = "[油猴脚本-UI DownloadAll]";

            if (!activity) { console.log(`${logPrefix} Activity #${index}: 元素为空,跳过。`); continue; }

            if (activity.uploads && Array.isArray(activity.uploads) && activity.uploads.length > 0) {
                const firstUpload = activity.uploads[0];
                if (firstUpload && typeof firstUpload.reference_id !== 'undefined') {
                    const referenceId = firstUpload.reference_id;
                    console.log(`${logPrefix} Activity #${index}: 找到 reference_id: ${referenceId}`);
                    const intermediateUrl = INTERMEDIATE_URL_TEMPLATE.replace("{REFERENCE_ID}", referenceId);

                    btn.textContent = `下载中 (${index + 1}/${allActivitiesData.length})...`;
                    console.log(`${logPrefix} Activity #${index}: (步骤1) 请求中间URL: ${intermediateUrl}`);
                    try {
                        const res = await originalFetch(intermediateUrl);
                        if (!res.ok) { throw new Error(`网络错误 (状态 ${res.status}) for ${intermediateUrl}`); }
                        const intermediateJsonData = await res.json();
                        console.log(`${logPrefix} Activity #${index}: (步骤2) 收到中间JSON:`, intermediateJsonData);

                        if (intermediateJsonData && typeof intermediateJsonData.url === 'string' && intermediateJsonData.url.startsWith('http') && intermediateJsonData.status === "ready") {
                            const finalDownloadUrl = intermediateJsonData.url;
                            const filename = extractFilenameFromUrl(finalDownloadUrl, `activity_${index}_file`, referenceId);
                            console.log(`${logPrefix} Activity #${index}: (步骤3) 最终URL: ${finalDownloadUrl}, 文件名: "${filename}"`);
                            GM_download(finalDownloadUrl, filename);
                            downloadedCount++;
                            console.log(`${logPrefix} Activity #${index}: (步骤4) GM_download 已调用。`);
                            // 可选:在每个下载间添加短暂延迟
                            // await new Promise(resolve => setTimeout(resolve, 500));
                        } else {
                            console.error(`${logPrefix} Activity #${index}: 中间JSON格式不正确或status不为ready。`, intermediateJsonData);
                        }
                    } catch (err) {
                        console.error(`${logPrefix} Activity #${index}: 处理中间URL (${intermediateUrl}) 时出错:`, err);
                    }
                } else { console.log(`${logPrefix} Activity #${index}: 'uploads[0]' 中未找到 reference_id。`); }
            } else { console.log(`${logPrefix} Activity #${index}: 'uploads' 为空或无效。`); }
        }
        console.log(`[油猴脚本-UI] “下载全部”完成,尝试下载 ${downloadedCount} 个文件。`);
        btn.textContent = "下载全部 (列表)";
        // allActivitiesData = null; // 如果只允许一次下载,则取消注释
        // btn.disabled = !allActivitiesData; // 根据allActivitiesData是否重置来决定
        btn.disabled = false; // 允许重复下载同一个列表
    }

    // --- 网络请求拦截处理 ---
    function processJsonResponse(jsonData, requestTypePrefix) { // requestTypePrefix is "[油猴脚本-Fetch]" or "[油猴脚本-XHR]"
        console.log(`${requestTypePrefix} 正在分析JSON结构...`, jsonData);

        // 类型1: 直接在根级别有 uploads 数组,且没有 activities 数组
        if (jsonData.uploads && Array.isArray(jsonData.uploads) && jsonData.uploads.length > 0 && typeof jsonData.activities === 'undefined') {
            console.log(`${requestTypePrefix} 检测到类型1 JSON (直接uploads)`);
            const firstUpload = jsonData.uploads[0];
            if (firstUpload && typeof firstUpload.reference_id !== 'undefined') {
                const referenceId = firstUpload.reference_id;
                const intermediateUrl = INTERMEDIATE_URL_TEMPLATE.replace("{REFERENCE_ID}", referenceId);

                console.log(`${requestTypePrefix} 类型1: (步骤1) 请求中间URL: ${intermediateUrl}`);
                originalFetch(intermediateUrl)
                    .then(res => {
                        if (!res.ok) { throw new Error(`网络错误 (状态 ${res.status}) for ${intermediateUrl}`); }
                        return res.json();
                    })
                    .then(intermediateJsonData => {
                        console.log(`${requestTypePrefix} 类型1: (步骤2) 收到中间JSON:`, intermediateJsonData);
                        if (intermediateJsonData && typeof intermediateJsonData.url === 'string' && intermediateJsonData.url.startsWith('http') && intermediateJsonData.status === "ready") {
                            currentFileFinalUrl = intermediateJsonData.url; // 存储最终链接
                            console.log(`${requestTypePrefix} 类型1: (步骤3) 已存储“当前文件”下载链接: ${currentFileFinalUrl}`);
                            const btn = document.getElementById('btnDownloadCurrentUserscript');
                            if (btn) { btn.disabled = false; btn.title = `准备下载: ${extractFilenameFromUrl(currentFileFinalUrl, "current_file")}`; }
                        } else {
                            console.error(`${requestTypePrefix} 类型1: 中间JSON格式不正确或status不为ready。`, intermediateJsonData);
                        }
                    })
                    .catch(err => {
                        console.error(`${requestTypePrefix} 类型1: 处理中间URL (${intermediateUrl}) 时出错:`, err);
                    });
            } else { console.log(`${requestTypePrefix} 类型1: 'uploads[0]' 中未找到 reference_id。`); }
        }
        // 类型2: 有 activities 数组 (我们之前的逻辑)
        else if (jsonData.activities && Array.isArray(jsonData.activities) && jsonData.activities.length > 0) {
            console.log(`${requestTypePrefix} 检测到类型2 JSON (activities数组 - ${jsonData.activities.length} 项)`);
            allActivitiesData = jsonData.activities; // 存储整个数组
            console.log(`${requestTypePrefix} 类型2: 已存储 "activities" 数据供“下载全部”使用。`);
            const btn = document.getElementById('btnDownloadAllUserscript');
            if (btn) { btn.disabled = false; btn.title = `准备下载列表中的 ${allActivitiesData.length} 个项目`; }
        }
        // 未知结构
        else {
            console.log(`${requestTypePrefix} 未识别的JSON结构,不包含直接 'uploads' 或 'activities' 数组。`);
        }
    }

    // --- Fetch Interception ---
    const originalFetch = window.fetch;
    window.fetch = async function(...args) {
        const [resource, config] = args;
        let requestUrl = '';
        if (typeof resource === 'string') { requestUrl = resource; }
        else if (resource instanceof Request) { requestUrl = resource.url; }
        else { try { requestUrl = resource.href; } catch (e) { return originalFetch(...args); } }
aswwsse
        const responsePromise = originalFetch(...args);
        if (requestUrl && requestUrl.includes(TARGET_URL_PART)) {
            try {
                const response = await responsePromise;
                const clonedResponse = response.clone();
                const jsonData = await clonedResponse.json();
                processJsonResponse(jsonData, "[油猴脚本-Fetch]");
            } catch (error) {
                console.error("[油猴脚本-Fetch] 拦截和处理主数据时出错:", error);
            }
        }
        return responsePromise;
    };

    // --- XMLHttpRequest Interception ---
    const originalXhrOpen = XMLHttpRequest.prototype.open;
    const originalXhrSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.open = function(method, url, ...rest) {
        this._requestUrl = url;
        return originalXhrOpen.apply(this, [method, url, ...rest]);
    };
    XMLHttpRequest.prototype.send = function(...sendArgs) {
        this.addEventListener('load', function() {
            if (this._requestUrl && typeof this._requestUrl === 'string' && this._requestUrl.includes(TARGET_URL_PART)) {
                if (this.readyState === 4 && (this.status >= 200 && this.status < 300)) {
                    try {
                        const jsonData = JSON.parse(this.responseText);
                        processJsonResponse(jsonData, "[油猴脚本-XHR]");
                    } catch (e) {
                        console.error('[油猴脚本-XHR] 拦截和处理主数据时出错:', e);
                    }
                } else if (this.readyState === 4) { /* ... */ }
            }
        });
        return originalXhrSend.apply(this, sendArgs);
    };

    // --- 脚本启动 ---
    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        createControlPanel();
    } else {
        document.addEventListener('DOMContentLoaded', createControlPanel);
    }
    console.log("[油猴脚本] Fetch 和 XMLHttpRequest 拦截器已设置。UI面板将很快创建。");

})();