// ==UserScript== // @name 智云课堂在线播放 // @namespace http://tampermonkey.net/ // @version 5.0 // @description 支持在线流式预览、双模式批量下载 & Motrix一键直连 // @author LocalHostCompany // @match https://classroom.zju.edu.cn/coursedetail* // @require https://cdn.jsdelivr.net/npm/hls.js@1.5.0/dist/hls.min.js // @require https://cdn.jsdelivr.net/npm/dplayer@1.27.1/dist/DPlayer.min.js // @grant GM_xmlhttpRequest // @connect 127.0.0.1 // @connect localhost // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/567930/%E6%99%BA%E4%BA%91%E8%AF%BE%E5%A0%82%E5%9C%A8%E7%BA%BF%E6%92%AD%E6%94%BE.user.js // @updateURL https://update.greasyfork.icu/scripts/567930/%E6%99%BA%E4%BA%91%E8%AF%BE%E5%A0%82%E5%9C%A8%E7%BA%BF%E6%92%AD%E6%94%BE.meta.js // ==/UserScript== (function () { 'use strict'; console.log("智云课堂在线播放脚本已启动(全能终极版 v5.0)"); // 动态注入 DPlayer CSS (function injectDPlayerCSS() { if (document.querySelector('link[data-dplayer-css]')) return; const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = 'https://cdn.jsdelivr.net/npm/dplayer@1.27.1/dist/DPlayer.min.css'; link.setAttribute('data-dplayer-css', '1'); document.head.appendChild(link); })(); function getQueryVariable(variable) { const query = window.location.search.substring(1); const vars = query.split("&"); for (let i = 0; i < vars.length; i++) { const pair = vars[i].split("="); if (pair[0] === variable) return decodeURIComponent(pair[1]); if (decodeURIComponent(pair[0]) === variable) return decodeURIComponent(pair[1]); } return (false); } const course_id = getQueryVariable("course_id"); if (!course_id) return; const corsProxy = ''; const VIDEO_TYPES = ['course_live', 'video', 'ilive', 'olive', 'large_class']; function extractVideoUrl(content) { try { const c = typeof content === 'string' ? JSON.parse(content) : content; if (c && c.playback && c.playback.url) return c.playback.url; if (c && c.url) return c.url; } catch (e) { } return null; } function getCookie(name) { const m = document.cookie.match(new RegExp('(?:^|;\\s*)' + name + '=([^;]*)')); return m ? decodeURIComponent(m[1]) : ''; } function getAuthToken() { try { for (const key of ['token', 'access_token', 'Token', 'Authorization', 'auth_token', 'user_token']) { const val = localStorage.getItem(key); if (val) return val.replace(/^Bearer\s+/i, ''); } const ct = getCookie('token') || getCookie('access_token'); if (ct) return ct; } catch (e) { } return ''; } function getStudentAccount() { try { const app = document.querySelector('#app'); if (app && app.__vue__ && app.__vue__.$store) { const userinfo = app.__vue__.$store.state.userinfo; if (userinfo && userinfo.account) return userinfo.account; } for (const key of ['userinfo', 'user_info', 'user']) { const raw = localStorage.getItem(key); if (raw) { try { const u = JSON.parse(raw); if (u.account) return u.account; if (u.username) return u.username; } catch (e) { } } } const ca = getCookie('account') || getCookie('username'); if (ca) return ca; } catch (e) { } return ''; } function authFetchOpts(extraHeaders = {}) { const headers = { ...extraHeaders }; const token = getAuthToken(); if (token) headers['Authorization'] = `Bearer ${token}`; return { credentials: 'include', headers }; } function flattenSubList(list) { if (!Array.isArray(list)) return []; const result = []; for (const item of list) { if (item.type === 'chapter' && Array.isArray(item.child)) { result.push(...flattenSubList(item.child)); } else { result.push(item); } } return result; } function flattenCourseLiveObj(obj) { const result = []; if (!obj || typeof obj !== 'object') return result; for (const year of Object.values(obj)) { if (typeof year !== 'object') continue; for (const month of Object.values(year)) { if (typeof month !== 'object') continue; for (const week of Object.values(month)) { if (Array.isArray(week)) result.push(...week); } } } return result; } async function fetchViaV3() { const student = getStudentAccount(); let apiUrl = `https://classroom.zju.edu.cn/courseapi/v3/multi-search/get-course-detail?course_id=${course_id}`; if (student) apiUrl += `&student=${encodeURIComponent(student)}`; const resp = await fetch(apiUrl, authFetchOpts()); const data = await resp.json(); if (data.code !== 0 || !data.data) throw new Error(`v3 API 返回异常: code=${data.code}`); const courseData = data.data; let subList; if (courseData.auto_chapter) { if (Array.isArray(courseData.origin_sub_list)) subList = flattenSubList(courseData.origin_sub_list); else if (courseData.sub_list && typeof courseData.sub_list === 'object' && !Array.isArray(courseData.sub_list)) subList = flattenCourseLiveObj(courseData.sub_list); else subList = []; } else if (Array.isArray(courseData.sub_list)) { subList = flattenSubList(courseData.sub_list); } else { throw new Error('v3 API 无 sub_list'); } const videoSubs = subList.filter(s => VIDEO_TYPES.includes(s.type)); const videos = []; for (let i = 0; i < videoSubs.length; i++) { const sub = videoSubs[i]; let title = sub.sub_title || sub.title || `第${i + 1}节`; let videoUrl = null; let available = true; const st = Number(sub.sub_status); if (st === 5 || st === 3) { available = false; } else { if (sub.content) videoUrl = extractVideoUrl(sub.content); if (!videoUrl && sub.id) { try { const detailUrl = `https://classroom.zju.edu.cn/courseapi/v3/multi-search/get-sub-detail?sub_id=${sub.id}`; const detailResp = await fetch(detailUrl, authFetchOpts()); const detailData = await detailResp.json(); if (detailData.code === 0 && detailData.data) videoUrl = extractVideoUrl(detailData.data.content); } catch (e) { } } if (!videoUrl) available = false; } if (!available || !videoUrl) title += "(暂无回放)"; videos.push({ title, videoUrl, available, originalIndex: i }); } return videos; } async function fetchViaV2() { const apiUrl = `https://classroom.zju.edu.cn/courseapi/v2/course/catalogue?course_id=${course_id}`; const resp = await fetch(apiUrl, authFetchOpts({ "Content-Type": "application/json" })); const data = await resp.json(); if (!(data.success && data.result && data.result.data)) throw new Error('v2 API 返回数据异常'); const items = data.result.data; const videos = []; for (let i = 0; i < items.length; i++) { const item = items[i]; let title = item.title; let videoUrl = null; let available = true; try { videoUrl = extractVideoUrl(item.content); if (!videoUrl) available = false; } catch (e) { available = false; } if (!available || !videoUrl) title += "(暂无回放)"; videos.push({ title, videoUrl, available, originalIndex: i }); } return videos; } (async function loadVideoList() { await new Promise(r => { if (document.readyState === 'complete') return r(); window.addEventListener('load', r, { once: true }); }); await new Promise(r => setTimeout(r, 2000)); let videos = null; try { videos = await fetchViaV3(); } catch (e) { } if (!videos || videos.length === 0) { try { videos = await fetchViaV2(); } catch (e) { } } if (!videos || videos.length === 0) { console.error("无法获取视频列表,请检查是否已登录。"); return; } addDownloadUI(videos); })(); function getCourseInfo() { let courseName = '未知课程'; let teacherName = '未知教师'; try { const parts = (document.title || '').split('_'); if (parts.length >= 2) { if (parts[0].trim()) courseName = parts[0].trim(); if (parts[1].trim()) teacherName = parts[1].trim(); } } catch (e) { } return { courseName, teacherName }; } /** 强制下划线命名法,同时将原有的横线也替换掉 */ function sanitizeFilename(name) { return name.replace(/[\/\\:*?"<>|\-]/g, '_'); } function addDownloadUI(videos) { const container = document.createElement("div"); container.style.cssText = "position:fixed;bottom:10px;right:10px;background:rgba(255,255,255,0.95);padding:15px;border:1px solid #ccc;z-index:9999;max-height:80%;overflow-y:auto;font-size:14px;box-shadow:0 0 10px rgba(0,0,0,0.1);border-radius:5px;width:320px;display:flex;flex-direction:column;transition:all 0.3s ease;"; const header = document.createElement("div"); header.style.cssText = "display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;"; const title = document.createElement("div"); title.style.fontWeight = "bold"; title.innerText = "批量下载视频"; header.appendChild(title); const minimizeButton = document.createElement("button"); minimizeButton.innerText = "—"; minimizeButton.style.cssText = "border:none;background:none;cursor:pointer;font-size:16px;padding:0;margin-left:10px;"; minimizeButton.addEventListener("click", () => { const isMin = container.classList.toggle("minimized"); minimizeButton.innerText = isMin ? "+" : "—"; const displayStyle = isMin ? "none" : "block"; selectAllContainer.style.display = isMin ? "none" : "flex"; downloadButton.style.display = displayStyle; exportButton.style.display = displayStyle; motrixRpcButton.style.display = displayStyle; softwareHint.style.display = displayStyle; overallProgressContainer.style.display = window._isDownloading && !isMin ? "block" : "none"; status.style.display = displayStyle; list.style.display = displayStyle; stopButton.style.display = window._isDownloading && !isMin ? "block" : "none"; }); header.appendChild(minimizeButton); container.appendChild(header); const selectAllContainer = document.createElement("div"); selectAllContainer.style.cssText = "display:flex;align-items:center;margin-bottom:10px;"; const selectAllCheckbox = document.createElement("input"); selectAllCheckbox.type = "checkbox"; selectAllCheckbox.id = "selectAllCheckbox"; const selectAllLabel = document.createElement("label"); selectAllLabel.htmlFor = "selectAllCheckbox"; selectAllLabel.innerText = " 全选"; selectAllContainer.appendChild(selectAllCheckbox); selectAllContainer.appendChild(selectAllLabel); container.appendChild(selectAllContainer); const downloadButton = document.createElement("button"); downloadButton.innerText = "下载选中视频"; downloadButton.style.cssText = "display:block;margin-top:10px;width:100%;padding:8px;background-color:#4CAF50;color:white;border:none;border-radius:3px;cursor:pointer;font-size:14px;"; container.appendChild(downloadButton); const exportButton = document.createElement("button"); exportButton.innerText = "导出批量链接 (推荐)"; exportButton.style.cssText = "display:block;margin-top:6px;width:100%;padding:8px;background-color:#1976D2;color:white;border:none;border-radius:3px;cursor:pointer;font-size:14px;"; container.appendChild(exportButton); const motrixRpcButton = document.createElement("button"); motrixRpcButton.innerText = "🚀 一键发送至 Motrix (全自动)"; motrixRpcButton.style.cssText = "display:block;margin-top:6px;width:100%;padding:8px;background-color:#9C27B0;color:white;border:none;border-radius:3px;cursor:pointer;font-size:14px;"; container.appendChild(motrixRpcButton); const softwareHint = document.createElement("div"); softwareHint.innerText = "💡 批量全自动重命名请使用 IDM 或 Motrix 导入"; softwareHint.style.cssText = "font-size:11px;color:#666;text-align:center;margin-top:4px;margin-bottom:6px;"; container.appendChild(softwareHint); const stopButton = document.createElement("button"); stopButton.innerText = "停止下载"; stopButton.style.cssText = "display:none;margin-top:6px;width:100%;padding:8px;background-color:#e53935;color:white;border:none;border-radius:3px;cursor:pointer;font-size:14px;"; container.appendChild(stopButton); const status = document.createElement("div"); status.style.cssText = "margin-top:10px;font-size:12px;color:#555;"; container.appendChild(status); const overallProgressContainer = document.createElement("div"); overallProgressContainer.style.cssText = "width:100%;background-color:#f3f3f3;border-radius:5px;margin-top:10px;display:none;"; const overallProgressLabel = document.createElement("div"); overallProgressLabel.style.cssText = "text-align:center;font-size:11px;color:#555;padding:2px 0;"; const overallProgressBar = document.createElement("div"); overallProgressBar.style.cssText = "width:0%;height:20px;background-color:#4CAF50;border-radius:5px;"; overallProgressContainer.appendChild(overallProgressLabel); overallProgressContainer.appendChild(overallProgressBar); container.appendChild(overallProgressContainer); const list = document.createElement("ul"); list.style.cssText = "list-style:none;padding:0;margin-top:10px;"; container.appendChild(list); videos.forEach((video, index) => { const listItem = document.createElement("li"); listItem.style.cssText = "margin-top:10px;display:block;border-bottom:1px solid #ddd;padding-bottom:10px;"; const headerDiv = document.createElement("div"); headerDiv.style.cssText = "display:flex;align-items:center;"; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.value = video.originalIndex; checkbox.className = "videoCheckbox"; checkbox.style.marginRight = "10px"; if (!video.available) checkbox.disabled = true; const label = document.createElement("label"); label.style.flex = "1"; label.style.cursor = "pointer"; label.innerText = video.title; headerDiv.appendChild(checkbox); headerDiv.appendChild(label); if (video.available && video.videoUrl) { const previewBtn = document.createElement("button"); previewBtn.innerText = "▶️ 预览"; previewBtn.style.cssText = "margin-left:8px;padding:2px 7px;font-size:12px;border:1px solid #aaa;border-radius:3px;background:#f5f5f5;cursor:pointer;white-space:nowrap;"; previewBtn.addEventListener("click", () => openPreview(video.videoUrl, video.title)); headerDiv.appendChild(previewBtn); } listItem.appendChild(headerDiv); const progressContainer = document.createElement("div"); progressContainer.style.cssText = "width:100%;background-color:#f3f3f3;border-radius:5px;margin-top:5px;display:none;"; const progressBar = document.createElement("div"); progressBar.style.cssText = "width:0%;height:10px;background-color:#4CAF50;border-radius:5px;"; progressContainer.appendChild(progressBar); const infoDiv = document.createElement("div"); infoDiv.style.cssText = "margin-top:5px;font-size:12px;color:#555;display:none;"; infoDiv.innerText = "速度: 0 KB/s | 预计剩余时间: 0 s"; listItem.appendChild(progressContainer); listItem.appendChild(infoDiv); list.appendChild(listItem); }); document.body.appendChild(container); function openPreview(url, title) { const overlay = document.createElement('div'); overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.82);display:flex;align-items:center;justify-content:center;z-index:999999;'; const modal = document.createElement('div'); modal.style.cssText = 'position:relative;width:800px;max-width:95vw;height:450px;background:#000;border-radius:8px;overflow:hidden;'; const closeBtn = document.createElement('button'); closeBtn.innerText = '❌ 关闭'; closeBtn.style.cssText = 'position:absolute;top:8px;right:8px;z-index:10;background:rgba(0,0,0,0.65);color:#fff;border:none;border-radius:4px;padding:5px 12px;font-size:13px;cursor:pointer;'; const playerContainer = document.createElement('div'); playerContainer.style.width = '100%'; playerContainer.style.height = '100%'; modal.appendChild(closeBtn); modal.appendChild(playerContainer); overlay.appendChild(modal); document.body.appendChild(overlay); const dp = new DPlayer({ container: playerContainer, autoplay: true, video: { url, type: 'auto' }}); function closeModal() { dp.destroy(); overlay.remove(); } closeBtn.addEventListener('click', closeModal); overlay.addEventListener('click', (e) => { if (e.target === overlay) closeModal(); }); } selectAllCheckbox.addEventListener("change", function () { container.querySelectorAll(".videoCheckbox").forEach(cb => { if (!cb.disabled) cb.checked = this.checked; }); }); function restoreButtons() { window._isDownloading = false; downloadButton.disabled = false; downloadButton.innerText = "下载选中视频"; downloadButton.style.backgroundColor = "#4CAF50"; exportButton.disabled = false; exportButton.innerText = "导出批量链接 (推荐)"; exportButton.style.backgroundColor = "#1976D2"; motrixRpcButton.disabled = false; motrixRpcButton.innerText = "🚀 一键发送至 Motrix (全自动)"; motrixRpcButton.style.backgroundColor = "#9C27B0"; stopButton.style.display = "none"; } function updateOverallProgress(done, total, prefix="总进度") { overallProgressBar.style.width = ((done / total) * 100).toFixed(2) + '%'; overallProgressLabel.innerText = `${prefix}:${done} / ${total}`; } // ======================= 模式一:浏览器原生下载 ======================= downloadButton.addEventListener("click", function () { status.innerText = "开始下载..."; status.style.color = "#555"; const checkboxes = container.querySelectorAll(".videoCheckbox"); const selectedVideos = []; checkboxes.forEach(cb => { if (cb.checked) selectedVideos.push({ video: videos[parseInt(cb.value)], index: parseInt(cb.value) }); }); if (selectedVideos.length === 0) { alert("请选择要下载的视频"); return; } window._isDownloading = true; downloadButton.disabled = true; downloadButton.innerText = "下载中..."; downloadButton.style.backgroundColor = "#888"; exportButton.disabled = true; exportButton.style.backgroundColor = "#888"; motrixRpcButton.disabled = true; motrixRpcButton.style.backgroundColor = "#888"; stopButton.style.display = "block"; overallProgressContainer.style.display = "block"; overallProgressBar.style.width = "0%"; let currentDownload = 0; let completed = 0; const controller = new AbortController(); stopButton.onclick = () => controller.abort(); async function downloadNext() { if (controller.signal.aborted) return; if (currentDownload < selectedVideos.length) { const { video, index } = selectedVideos[currentDownload]; const listItem = list.children[index]; const progressContainer = listItem.querySelector("div:nth-child(2)"); const progressBar = progressContainer.querySelector("div"); const infoDiv = listItem.querySelector("div:nth-child(3)"); status.innerText = `正在下载 (${currentDownload + 1}/${selectedVideos.length}): ${video.title}`; progressContainer.style.display = "block"; infoDiv.style.display = "block"; try { const { courseName, teacherName } = getCourseInfo(); const suggestedName = sanitizeFilename(`${courseName}_${teacherName}_${video.title}`) + '.mp4'; const response = await fetch(corsProxy + video.videoUrl, { signal: controller.signal }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const contentLength = response.headers.get('Content-Length'); const totalSize = contentLength ? parseInt(contentLength, 10) : 0; let loadedBytes = 0; const reader = response.body.getReader(); const chunks = []; while(true) { const {done, value} = await reader.read(); if (done) break; chunks.push(value); loadedBytes += value.byteLength; if (totalSize) progressBar.style.width = ((loadedBytes / totalSize) * 100) + '%'; } progressBar.style.width = '100%'; const blob = new Blob(chunks); const objectUrl = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = objectUrl; a.download = suggestedName; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(objectUrl); infoDiv.innerText = '✅ 下载完成'; infoDiv.style.color = '#4CAF50'; } catch (err) { if (err.name === 'AbortError') { status.innerText = '已手动取消'; restoreButtons(); return; } else { infoDiv.innerText = `❌ 下载出错: ${err.message}`; infoDiv.style.color = '#d32f2f'; } } completed++; updateOverallProgress(completed, selectedVideos.length, "下载进度"); currentDownload++; setTimeout(downloadNext, 1000); } else { status.innerText = '所有下载任务已处理!'; setTimeout(() => { overallProgressContainer.style.display = 'none'; }, 5000); restoreButtons(); } } downloadNext(); }); // ======================= 模式二:导出双份配置链接 ======================= exportButton.addEventListener("click", function () { status.innerText = "正在收集链接,请稍候..."; status.style.color = "#555"; const checkboxes = container.querySelectorAll(".videoCheckbox"); const selectedVideos = []; checkboxes.forEach(cb => { if (cb.checked) selectedVideos.push({ video: videos[parseInt(cb.value)], index: parseInt(cb.value) }); }); if (selectedVideos.length === 0) { alert("请选择视频"); return; } exportButton.disabled = true; exportButton.innerText = "导出中..."; exportButton.style.backgroundColor = "#888"; downloadButton.disabled = true; downloadButton.style.backgroundColor = "#888"; motrixRpcButton.disabled = true; motrixRpcButton.style.backgroundColor = "#888"; overallProgressContainer.style.display = "block"; overallProgressBar.style.width = "0%"; let currentIndex = 0, completed = 0; let idmContent = '', motrixContent = ''; const { courseName, teacherName } = getCourseInfo(); function processNext() { if (currentIndex < selectedVideos.length) { const { video, index } = selectedVideos[currentIndex]; const infoDiv = list.children[index].querySelector("div:nth-child(3)"); status.innerText = `处理 (${currentIndex + 1}/${selectedVideos.length}): ${video.title}`; infoDiv.style.display = "block"; if (video.videoUrl) { const filename = sanitizeFilename(`${courseName}_${teacherName}_${video.title}`) + '.mp4'; idmContent += `<\n${video.videoUrl}\nfile>${filename}\n>\n`; motrixContent += `${video.videoUrl}\n out=${filename}\n`; infoDiv.innerText = '✅ 链接已收集'; infoDiv.style.color = '#4CAF50'; } else { infoDiv.innerText = '⚠️ 暂无视频链接,已跳过'; infoDiv.style.color = '#f0a500'; } completed++; updateOverallProgress(completed, selectedVideos.length, "导出进度"); currentIndex++; processNext(); } else { if (idmContent.trim() !== '') { const idmA = document.createElement('a'); idmA.href = window.URL.createObjectURL(new Blob([idmContent], { type: 'text/plain' })); idmA.download = sanitizeFilename(`${courseName}_IDM专用`) + '.ef2'; idmA.click(); setTimeout(() => { const motA = document.createElement('a'); motA.href = window.URL.createObjectURL(new Blob([motrixContent], { type: 'text/plain' })); motA.download = sanitizeFilename(`${courseName}_Motrix专用`) + '.txt'; motA.click(); }, 500); status.innerText = '✅ 导出成功!请根据软件选择导入文件。'; status.style.color = '#1976D2'; } else { status.innerText = '⚠️ 没有可导出的内容。'; } setTimeout(() => { overallProgressContainer.style.display = 'none'; }, 5000); restoreButtons(); } } processNext(); }); // ======================= 模式三:特权级 API 发送至 Motrix ======================= motrixRpcButton.addEventListener("click", async function () { const tokenInput = prompt("【Motrix 连接设置】\n如果你的 Motrix 进阶设置中填写了「RPC 授权密钥」,请输入:\n(如果没有设置,请直接留空并点击确定)", localStorage.getItem("zhiyun_motrix_token") || ""); if (tokenInput === null) return; localStorage.setItem("zhiyun_motrix_token", tokenInput.trim()); status.innerText = "正在发送至 Motrix..."; status.style.color = "#555"; const checkboxes = container.querySelectorAll(".videoCheckbox"); const selectedVideos = []; checkboxes.forEach(cb => { if (cb.checked) selectedVideos.push({ video: videos[parseInt(cb.value)], index: parseInt(cb.value) }); }); if (selectedVideos.length === 0) { alert("请选择要发送的视频"); return; } motrixRpcButton.disabled = true; motrixRpcButton.innerText = "发送中..."; motrixRpcButton.style.backgroundColor = "#888"; downloadButton.disabled = true; downloadButton.style.backgroundColor = "#888"; exportButton.disabled = true; exportButton.style.backgroundColor = "#888"; overallProgressContainer.style.display = "block"; overallProgressBar.style.width = "0%"; const { courseName, teacherName } = getCourseInfo(); let successCount = 0, failCount = 0; for (let i = 0; i < selectedVideos.length; i++) { const { video, index } = selectedVideos[i]; const infoDiv = list.children[index].querySelector("div:nth-child(3)"); infoDiv.style.display = "block"; status.innerText = `正在发送 (${i + 1}/${selectedVideos.length}): ${video.title}`; if (!video.videoUrl) { infoDiv.innerText = '⚠️ 无链接,已跳过'; infoDiv.style.color = '#f0a500'; failCount++; updateOverallProgress(i + 1, selectedVideos.length, "发送进度"); continue; } // 🌟 核心修复点:强制将相对链接转换成完整的绝对链接 (带 http/https) const absoluteUrl = new URL(video.videoUrl, window.location.origin).href; const filename = sanitizeFilename(`${courseName}_${teacherName}_${video.title}`) + '.mp4'; const params = []; if (tokenInput.trim() !== "") { params.push(`token:${tokenInput.trim()}`); } // 🌟 这里发送的是补全后的 absoluteUrl params.push([absoluteUrl]); params.push({ "out": filename, "header": ["User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/114.0.0.0 Safari/537.36"] }); const payload = { jsonrpc: "2.0", id: "zhiyun", method: "aria2.addUri", params: params }; try { await new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest === "undefined") { reject(new Error("浏览器不支持特权API")); return; } GM_xmlhttpRequest({ method: "POST", url: "http://127.0.0.1:16800/jsonrpc", data: JSON.stringify(payload), headers: { "Content-Type": "application/json", "Accept": "application/json" }, onload: function(res) { if (res.status === 200) { try { const resData = JSON.parse(res.responseText); if (resData.result) resolve(resData); else reject(new Error(resData.error?.message || "未知错误")); } catch(e) { reject(new Error("无法解析返回数据")); } } else { let errorMsg = `HTTP ${res.status}`; try { const errObj = JSON.parse(res.responseText); if (errObj.error && errObj.error.message) errorMsg += ` - ${errObj.error.message}`; } catch(e) {} reject(new Error(errorMsg)); } }, onerror: function(err) { reject(new Error("请确保 Motrix 已打开且端口为 16800")); } }); }); infoDiv.innerText = '✅ 成功推送到 Motrix'; infoDiv.style.color = '#4CAF50'; successCount++; } catch (error) { console.error(`发送到 Motrix 失败:`, error); infoDiv.innerText = `❌ 失败: ${error.message}`; infoDiv.style.color = '#d32f2f'; failCount++; } updateOverallProgress(i + 1, selectedVideos.length, "发送进度"); await new Promise(r => setTimeout(r, 100)); } if (successCount > 0) { status.innerText = `✅ 完成!成功发送 ${successCount} 个任务到 Motrix,失败 ${failCount} 个。`; status.style.color = '#9C27B0'; } else { status.innerText = '❌ 全部发送失败。请查看各个视频下方的红色报错信息。'; status.style.color = '#d32f2f'; } setTimeout(() => { overallProgressContainer.style.display = 'none'; }, 6000); restoreButtons(); })(); } })()