// ==UserScript== // @name 网盘直链下载助手 // @name:zh-CN 网盘直链下载助手 // @name:en Cloud Drive Direct Link Assistant // @namespace https://github.com/pan-direct-link/assistant // @version 1.0.0 // @description 支持批量获取 ✅百度网盘 ✅阿里云盘 ✅天翼云盘 ✅迅雷云盘 ✅夸克网盘 ✅移动云盘 ✅123云盘 ✅蓝奏云盘 ✅腾讯微云 ✅Google Drive ✅Dropbox ✅OneDrive ✅pCloud ✅TeraBox 十四大网盘的直链下载地址,配合 IDM,Xdown,Aria2,Curl,比特彗星等工具高效下载 // @description:zh-CN 支持批量获取百度网盘、阿里云盘、天翼云盘、迅雷云盘、夸克网盘、移动云盘、123云盘、蓝奏云盘、腾讯微云、Google Drive、Dropbox、OneDrive、pCloud、TeraBox 十四大网盘的直链下载地址 // @description:en Batch get direct download links from 14 cloud drives: Baidu, Aliyun, Tianyi, Xunlei, Quark, Mobile, 123, Lanzou, Weiyun, Google Drive, Dropbox, OneDrive, pCloud, TeraBox // @author PanDirectLink // @match *://pan.baidu.com/* // @match *://yun.baidu.com/* // @match *://www.alipan.com/* // @match *://www.aliyundrive.com/* // @match *://www.aliyundrive.net/* // @match *://cloud.189.cn/* // @match *://pan.xunlei.com/* // @match *://pan.quark.cn/* // @match *://yun.139.com/* // @match *://yun.139.com.cn/* // @match *://www.123pan.com/* // @match *://www.123912.com/* // @match *://*.lanzou*.com/* // @match *://*.lanzouv*.com/* // @match *://*.lanzouj*.com/* // @match *://*.lanzoux*.com/* // @match *://*.lanzoub*.com/* // @match *://*.lanzoue*.com/* // @match *://*.lanzoup*.com/* // @match *://*.lanzouw*.com/* // @match *://share.weiyun.com/* // @match *://www.weiyun.com/* // @match *://drive.google.com/* // @match *://*.dropbox.com/* // @match *://*.dropboxusercontent.com/* // @match *://*.onedrive.live.com/* // @match *://*.1drv.ms/* // @match *://my.pcloud.com/* // @match *://*.pcloud.com/* // @match *://*.terabox.com/* // @match *://*.teraboxapp.com/* // @match *://*.4funbox.com/* // @match *://*.mirrobox.com/* // @match *://*.momerybox.com/* // @match *://*.tibibox.com/* // @icon data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Cpath fill='%234CAF50' d='M24 4L6 14v20l18 10 18-10V14z'/%3E%3Cpath fill='%23FFC107' d='M24 4l18 10-18 10L6 14z'/%3E%3Cpath fill='%232196F3' d='M6 14l18 10v20L6 34z'/%3E%3Ctext x='24' y='30' text-anchor='middle' fill='white' font-size='14' font-weight='bold'%3EDL%3C/text%3E%3C/svg%3E // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_addStyle // @grant GM_setClipboard // @grant GM_download // @grant GM_registerMenuCommand // @grant GM_notification // @grant unsafeWindow // @connect pan.baidu.com // @connect d.pcs.baidu.com // @connect api.aliyundrive.com // @connect api.aliyundrive.net // @connect alipan.com // @connect cloud.189.cn // @connect api.cloud.189.cn // @connect pan.xunlei.com // @connect api-pan.xunlei.com // @connect drive-pc.quark.cn // @connect pan.quark.cn // @connect yun.139.com // @connect cis-njs-telese.139.com // @connect www.123912.com // @connect www.123pan.com // @connect * // @require https://cdn.jsdelivr.net/npm/js-cookie@3.0.5/dist/js.cookie.min.js // @run-at document-idle // @license MIT // @compatible chrome // @compatible edge // @compatible firefox // @compatible opera // @compatible safari // ==/UserScript== (function () { 'use strict'; /* ========================================================================= * Section 1: Configuration & Constants * ========================================================================= */ const SCRIPT_NAME = '网盘直链下载助手'; const SCRIPT_VERSION = '1.0.0'; const CONFIG_KEYS = { ARIA2_URL: 'aria2_url', ARIA2_SECRET: 'aria2_secret', DOWNLOAD_TOOL: 'download_tool', PANEL_POSITION: 'panel_position', AUTO_FETCH: 'auto_fetch', }; const DEFAULT_CONFIG = { aria2_url: 'http://localhost:6800/jsonrpc', aria2_secret: '', download_tool: 'copy', auto_fetch: false, }; // Supported cloud drive definitions const CLOUD_DRIVES = [ { id: 'baidu', name: '百度网盘', hostPatterns: ['pan.baidu.com', 'yun.baidu.com'], color: '#06A7FF', icon: '📁' }, { id: 'aliyun', name: '阿里云盘', hostPatterns: ['alipan.com', 'aliyundrive.com', 'aliyundrive.net'], color: '#01B2A5', icon: '📂' }, { id: 'tianyi', name: '天翼云盘', hostPatterns: ['cloud.189.cn'], color: '#E60012', icon: '🗂️' }, { id: 'xunlei', name: '迅雷云盘', hostPatterns: ['pan.xunlei.com'], color: '#1D6FE0', icon: '🌩️' }, { id: 'quark', name: '夸克网盘', hostPatterns: ['pan.quark.cn'], color: '#7B5FFF', icon: '⚡' }, { id: 'mobile', name: '移动云盘', hostPatterns: ['yun.139.com', 'yun.139.com.cn'], color: '#FF6B00', icon: '📱' }, { id: 'pan123', name: '123云盘', hostPatterns: ['123pan.com', '123912.com'], color: '#4364FF', icon: '🔢' }, { id: 'lanzou', name: '蓝奏云盘', hostPatterns: ['lanzou', 'lanzouv', 'lanzouj', 'lanzoux', 'lanzoub', 'lanzoue', 'lanzoup', 'lanzouw'], color: '#3B82F6', icon: '☁️' }, { id: 'weiyun', name: '腾讯微云', hostPatterns: ['weiyun.com'], color: '#12B7F5', icon: '🐧' }, { id: 'gdrive', name: 'Google Drive', hostPatterns: ['drive.google.com'], color: '#4285F4', icon: '🎯' }, { id: 'dropbox', name: 'Dropbox', hostPatterns: ['dropbox.com', 'dropboxusercontent.com'], color: '#0061FF', icon: '📦' }, { id: 'onedrive', name: 'OneDrive', hostPatterns: ['onedrive.live.com', '1drv.ms'], color: '#0078D4', icon: '💠' }, { id: 'pcloud', name: 'pCloud', hostPatterns: ['pcloud.com'], color: '#17BEE5', icon: '🅿️' }, { id: 'terabox', name: 'TeraBox', hostPatterns: ['terabox.com', 'teraboxapp.com', '4funbox.com', 'mirrobox.com', 'momerybox.com', 'tibibox.com'], color: '#FF6B35', icon: '🚀' }, ]; const DOWNLOAD_TOOLS = [ { id: 'copy', name: '复制链接', icon: '📋' }, { id: 'curl', name: '生成Curl命令', icon: '💻' }, { id: 'aria2', name: '发送到Aria2', icon: '🌾' }, { id: 'idm', name: '发送到IDM', icon: '⚡' }, { id: 'xdown', name: '发送到Xdown', icon: '✖️' }, { id: 'export', name: '导出链接文件', icon: '📄' }, { id: 'browser', name: '浏览器下载', icon: '🌐' }, ]; /* ========================================================================= * Section 2: Utility Functions * ========================================================================= */ /** * Get config value with default fallback */ function getConfig(key) { return GM_getValue(key, DEFAULT_CONFIG[key]); } /** * Set config value */ function setConfig(key, value) { GM_setValue(key, value); } /** * Detect which cloud drive we're currently on */ function detectCurrentDrive() { const hostname = window.location.hostname; for (const drive of CLOUD_DRIVES) { for (const pattern of drive.hostPatterns) { if (hostname.includes(pattern)) { return drive; } } } return null; } /** * GM_xmlhttpRequest Promise wrapper */ function gmRequest(options) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ timeout: 30000, ...options, onload: (response) => { if (response.status >= 200 && response.status < 400) { resolve(response); } else { reject(new Error(`HTTP ${response.status}: ${response.statusText}`)); } }, onerror: (error) => reject(new Error('Network error: ' + (error.error || 'request failed'))), ontimeout: () => reject(new Error('Request timeout (30s)')), }); }); } /** * GM_xmlhttpRequest with JSON response */ async function gmRequestJSON(options) { const response = await gmRequest(options); try { return JSON.parse(response.responseText); } catch (e) { throw new Error('Failed to parse JSON response'); } } /** * Sleep helper */ function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Format file size */ function formatSize(bytes) { if (!bytes || bytes === 0) return '0 B'; const units = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + units[i]; } /** * Generate a unique ID */ function genId() { return 'dl_' + Math.random().toString(36).substring(2, 11); } /** * Escape HTML */ function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Get cookie value by name */ function getCookie(name) { try { if (typeof Cookies !== 'undefined') { return Cookies.get(name); } } catch (e) {} const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); return match ? match[2] : null; } /** * URL decode helper */ function decodeUrl(str) { try { return decodeURIComponent(str); } catch (e) { return str; } } /* ========================================================================= * Section 3: UI Module — Styles, Panel, Toast, Settings * ========================================================================= */ const CSS = ` /* ===== Main Panel ===== */ #pdl-panel { position: fixed; top: 80px; right: 24px; width: 420px; max-height: 600px; background: #ffffff; border-radius: 14px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18), 0 2px 8px rgba(0, 0, 0, 0.1); z-index: 2147483646; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif; font-size: 14px; color: #1a1a1a; overflow: hidden; transition: opacity 0.2s ease, transform 0.2s ease; user-select: none; } #pdl-panel.pdl-minimized .pdl-body { display: none; } #pdl-panel.pdl-minimized { max-height: 52px; } #pdl-panel.pdl-hidden { display: none; } #pdl-panel.pdl-dragging { opacity: 0.9; } /* ===== Header ===== */ .pdl-header { display: flex; align-items: center; padding: 12px 16px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; cursor: move; font-weight: 600; font-size: 14px; } .pdl-header-icon { font-size: 18px; margin-right: 8px; } .pdl-header-title { flex: 1; } .pdl-header-version { font-size: 11px; opacity: 0.7; margin-left: 6px; font-weight: 400; } .pdl-header-actions { display: flex; gap: 4px; } .pdl-header-btn { background: rgba(255,255,255,0.15); border: none; color: #fff; width: 26px; height: 26px; border-radius: 6px; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; transition: background 0.15s; } .pdl-header-btn:hover { background: rgba(255,255,255,0.3); } /* ===== Drive Badge ===== */ .pdl-drive-badge { display: flex; align-items: center; gap: 8px; padding: 8px 16px; background: #f0f4ff; border-bottom: 1px solid #e8ecf4; font-size: 13px; } .pdl-drive-badge .pdl-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; } .pdl-drive-badge .pdl-status { margin-left: auto; font-size: 11px; color: #888; } /* ===== Body ===== */ .pdl-body { max-height: 520px; overflow-y: auto; } .pdl-body::-webkit-scrollbar { width: 6px; } .pdl-body::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; } .pdl-body::-webkit-scrollbar-track { background: transparent; } /* ===== Toolbar ===== */ .pdl-toolbar { display: flex; gap: 6px; padding: 10px 16px; border-bottom: 1px solid #f0f0f0; flex-wrap: wrap; } .pdl-btn { display: inline-flex; align-items: center; gap: 4px; padding: 7px 12px; border: 1px solid #e0e0e0; border-radius: 8px; background: #fff; color: #333; font-size: 12px; cursor: pointer; transition: all 0.15s; white-space: nowrap; } .pdl-btn:hover { background: #f5f5f5; border-color: #ccc; } .pdl-btn:active { transform: scale(0.97); } .pdl-btn-primary { background: linear-gradient(135deg, #667eea, #764ba2); color: #fff; border-color: transparent; } .pdl-btn-primary:hover { opacity: 0.9; } .pdl-btn-success { background: #28a745; color: #fff; border-color: transparent; } .pdl-btn-danger { background: #dc3545; color: #fff; border-color: transparent; } .pdl-btn:disabled { opacity: 0.5; cursor: not-allowed; } /* ===== Select ===== */ .pdl-select { padding: 7px 10px; border: 1px solid #e0e0e0; border-radius: 8px; font-size: 12px; background: #fff; cursor: pointer; outline: none; } .pdl-select:focus { border-color: #667eea; } /* ===== File List Table ===== */ .pdl-file-list { width: 100%; border-collapse: collapse; font-size: 12px; } .pdl-file-list th { text-align: left; padding: 8px 12px; background: #fafafa; border-bottom: 1px solid #eee; font-weight: 600; color: #666; position: sticky; top: 0; z-index: 1; } .pdl-file-list td { padding: 8px 12px; border-bottom: 1px solid #f5f5f5; vertical-align: middle; } .pdl-file-list tr:hover { background: #f9f9ff; } .pdl-file-checkbox { width: 16px; height: 16px; cursor: pointer; } .pdl-file-name { max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .pdl-file-size { color: #888; white-space: nowrap; } .pdl-file-status { font-size: 11px; } .pdl-status-pending { color: #999; } .pdl-status-loading { color: #f59e0b; } .pdl-status-success { color: #28a745; } .pdl-status-error { color: #dc3545; } .pdl-file-link { max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .pdl-file-link a { color: #667eea; text-decoration: none; } .pdl-file-link a:hover { text-decoration: underline; } .pdl-copy-btn { background: none; border: 1px solid #ddd; border-radius: 4px; padding: 2px 6px; font-size: 11px; cursor: pointer; color: #666; } .pdl-copy-btn:hover { background: #f0f0f0; } /* ===== Empty State ===== */ .pdl-empty { padding: 40px 20px; text-align: center; color: #aaa; font-size: 13px; } .pdl-empty-icon { font-size: 32px; margin-bottom: 8px; opacity: 0.5; } /* ===== Progress Bar ===== */ .pdl-progress { height: 3px; background: #f0f0f0; border-radius: 2px; overflow: hidden; margin: 0 16px; } .pdl-progress-bar { height: 100%; background: linear-gradient(90deg, #667eea, #764ba2); width: 0%; transition: width 0.3s ease; border-radius: 2px; } /* ===== Settings Modal ===== */ #pdl-settings-overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.4); z-index: 2147483647; display: none; align-items: center; justify-content: center; } #pdl-settings-overlay.pdl-show { display: flex; } .pdl-settings-modal { background: #fff; border-radius: 14px; padding: 24px; width: 440px; max-width: 90vw; max-height: 80vh; overflow-y: auto; } .pdl-settings-title { font-size: 18px; font-weight: 700; margin-bottom: 16px; display: flex; align-items: center; justify-content: space-between; } .pdl-settings-close { background: none; border: none; font-size: 22px; cursor: pointer; color: #999; } .pdl-settings-group { margin-bottom: 16px; } .pdl-settings-label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 6px; color: #555; } .pdl-settings-input { width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 13px; outline: none; box-sizing: border-box; } .pdl-settings-input:focus { border-color: #667eea; } .pdl-settings-hint { font-size: 11px; color: #999; margin-top: 4px; } /* ===== Toast ===== */ .pdl-toast-container { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: 2147483647; display: flex; flex-direction: column; gap: 8px; align-items: center; pointer-events: none; } .pdl-toast { padding: 10px 20px; border-radius: 10px; color: #fff; font-size: 13px; font-weight: 500; box-shadow: 0 4px 16px rgba(0,0,0,0.2); opacity: 0; transform: translateY(-20px); transition: all 0.3s ease; pointer-events: auto; max-width: 400px; } .pdl-toast-show { opacity: 1; transform: translateY(0); } .pdl-toast-success { background: #28a745; } .pdl-toast-error { background: #dc3545; } .pdl-toast-info { background: #667eea; } .pdl-toast-warning { background: #f59e0b; } /* ===== Floating Button (minimized state) ===== */ #pdl-fab { position: fixed; bottom: 30px; right: 30px; width: 56px; height: 56px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; font-size: 24px; display: none; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4); z-index: 2147483646; transition: transform 0.2s ease; } #pdl-fab:hover { transform: scale(1.1); } #pdl-fab.pdl-show { display: flex; } /* ===== Link Viewer ===== */ .pdl-link-viewer { margin: 0 16px 12px; padding: 10px; background: #f8f9fa; border-radius: 8px; font-size: 11px; max-height: 150px; overflow-y: auto; word-break: break-all; font-family: 'Consolas', 'Monaco', monospace; display: none; } .pdl-link-viewer.pdl-show { display: block; } `; /** * Toast notification system */ const Toast = { container: null, init() { if (this.container) return; this.container = document.createElement('div'); this.container.className = 'pdl-toast-container'; document.body.appendChild(this.container); }, show(message, type = 'info', duration = 3000) { this.init(); const toast = document.createElement('div'); toast.className = `pdl-toast pdl-toast-${type}`; toast.textContent = message; this.container.appendChild(toast); requestAnimationFrame(() => toast.classList.add('pdl-toast-show')); setTimeout(() => { toast.classList.remove('pdl-toast-show'); setTimeout(() => toast.remove(), 300); }, duration); }, success(msg, dur) { this.show(msg, 'success', dur); }, error(msg, dur) { this.show(msg, 'error', dur || 5000); }, info(msg, dur) { this.show(msg, 'info', dur); }, warning(msg, dur) { this.show(msg, 'warning', dur); }, }; /** * UI Panel controller */ const Panel = { el: null, fileList: [], currentDrive: null, isMinimized: false, init(drive) { this.currentDrive = drive; this.injectStyles(); this.createPanel(); this.createFab(); this.createSettingsModal(); this.restorePosition(); this.setupDrag(); Toast.init(); }, injectStyles() { GM_addStyle(CSS); }, createPanel() { this.el = document.createElement('div'); this.el.id = 'pdl-panel'; this.el.innerHTML = `
⬇️ ${SCRIPT_NAME} v${SCRIPT_VERSION}
${this.currentDrive ? this.currentDrive.icon + ' ' + this.currentDrive.name : '未识别'} 就绪
📂
点击「获取直链」按钮开始提取文件直链
`; document.body.appendChild(this.el); // Restore tool selection const savedTool = getConfig(CONFIG_KEYS.DOWNLOAD_TOOL); if (savedTool) { const select = this.el.querySelector('#pdl-tool-select'); select.value = savedTool; select.addEventListener('change', () => { setConfig(CONFIG_KEYS.DOWNLOAD_TOOL, select.value); }); } this.bindEvents(); }, createFab() { this.fab = document.createElement('div'); this.fab.id = 'pdl-fab'; this.fab.innerHTML = '⬇️'; this.fab.title = SCRIPT_NAME; this.fab.addEventListener('click', () => { this.el.classList.remove('pdl-hidden'); this.fab.classList.remove('pdl-show'); }); document.body.appendChild(this.fab); }, createSettingsModal() { const overlay = document.createElement('div'); overlay.id = 'pdl-settings-overlay'; overlay.innerHTML = `
⚙️ 设置
Aria2 的 JSON-RPC 接口地址
📌 支持的网盘:百度/阿里/天翼/迅雷/夸克/移动/123/蓝奏/微云/GDrive/Dropbox/OneDrive/pCloud/TeraBox
📌 适配工具:IDM / Xdown / Aria2 / Curl / 比特彗星
📌 适配浏览器:Chrome / Edge / Firefox / 360 / QQ / 搜狗 / 百分 / 遨游 / 星愿 / Opera / 猎豹 / Vivaldi / Yandex / Kiwi 等
`; document.body.appendChild(overlay); overlay.querySelector('#pdl-setting-tool').value = getConfig(CONFIG_KEYS.DOWNLOAD_TOOL); overlay.querySelector('#pdl-settings-close').addEventListener('click', () => { overlay.classList.remove('pdl-show'); }); overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.classList.remove('pdl-show'); }); overlay.querySelector('#pdl-settings-save').addEventListener('click', () => { setConfig(CONFIG_KEYS.ARIA2_URL, overlay.querySelector('#pdl-setting-aria2-url').value); setConfig(CONFIG_KEYS.ARIA2_SECRET, overlay.querySelector('#pdl-setting-aria2-secret').value); setConfig(CONFIG_KEYS.DOWNLOAD_TOOL, overlay.querySelector('#pdl-setting-tool').value); this.el.querySelector('#pdl-tool-select').value = overlay.querySelector('#pdl-setting-tool').value; Toast.success('设置已保存'); overlay.classList.remove('pdl-show'); }); overlay.querySelector('#pdl-settings-test-aria2').addEventListener('click', async () => { const url = overlay.querySelector('#pdl-setting-aria2-url').value; const secret = overlay.querySelector('#pdl-setting-aria2-secret').value; try { Toast.info('正在测试 Aria2 连接...'); const result = await Aria2Client.testConnection(url, secret); if (result) { Toast.success('Aria2 连接成功!版本: ' + result); } else { Toast.error('Aria2 连接失败,请检查地址和密钥'); } } catch (e) { Toast.error('Aria2 连接错误: ' + e.message); } }); }, bindEvents() { this.el.querySelector('#pdl-btn-settings').addEventListener('click', () => { document.getElementById('pdl-settings-overlay').classList.add('pdl-show'); }); this.el.querySelector('#pdl-btn-minimize').addEventListener('click', () => this.minimize()); this.el.querySelector('#pdl-btn-close').addEventListener('click', () => this.hide()); this.el.querySelector('#pdl-btn-fetch').addEventListener('click', () => this.handleFetch()); this.el.querySelector('#pdl-btn-batch').addEventListener('click', () => this.handleBatch()); this.el.querySelector('#pdl-btn-selectall').addEventListener('click', () => this.toggleSelectAll()); this.el.querySelector('#pdl-btn-download').addEventListener('click', () => this.handleDownload()); this.el.querySelector('#pdl-btn-clear').addEventListener('click', () => this.clearList()); }, minimize() { this.el.classList.add('pdl-hidden'); this.fab.classList.add('pdl-show'); }, hide() { this.el.classList.add('pdl-hidden'); this.fab.classList.add('pdl-show'); }, setStatus(text) { const el = this.el.querySelector('#pdl-status'); if (el) el.textContent = text; }, showProgress(percent) { const bar = this.el.querySelector('#pdl-progress'); const fill = this.el.querySelector('#pdl-progress-bar'); if (percent >= 0) { bar.style.display = 'block'; fill.style.width = percent + '%'; } else { bar.style.display = 'none'; fill.style.width = '0%'; } }, renderFileList() { const container = this.el.querySelector('#pdl-file-list-container'); if (this.fileList.length === 0) { container.innerHTML = `
📂
点击「获取直链」按钮开始提取文件直链
`; return; } let html = ``; this.fileList.forEach((file, index) => { const statusClass = file.status === 'success' ? 'pdl-status-success' : file.status === 'error' ? 'pdl-status-error' : file.status === 'loading' ? 'pdl-status-loading' : 'pdl-status-pending'; const statusText = file.status === 'success' ? '✅ 成功' : file.status === 'error' ? '❌ 失败' : file.status === 'loading' ? '⏳ 获取中' : '⏸️ 待获取'; html += ``; }); html += '
文件名 大小 状态 操作
${escapeHtml(file.name)} ${file.size ? formatSize(file.size) : '-'} ${statusText} ${file.directLink ? `` : ''}
'; container.innerHTML = html; // Bind checkbox events container.querySelectorAll('.pdl-file-check').forEach(cb => { cb.addEventListener('change', (e) => { const idx = parseInt(e.target.dataset.index); this.fileList[idx].selected = e.target.checked; }); }); const checkAll = container.querySelector('#pdl-check-all'); if (checkAll) { checkAll.addEventListener('change', (e) => { this.fileList.forEach(f => f.selected = e.target.checked); container.querySelectorAll('.pdl-file-check').forEach(cb => { cb.checked = e.target.checked; }); }); } // Bind copy button events container.querySelectorAll('[data-action="copy-link"]').forEach(btn => { btn.addEventListener('click', (e) => { const idx = parseInt(e.target.dataset.index); const link = this.fileList[idx].directLink; if (link) { GM_setClipboard(link); Toast.success('链接已复制到剪贴板'); } }); }); }, async handleFetch() { if (!this.currentDrive) { Toast.error('未识别到支持的网盘'); return; } this.setStatus('正在获取文件列表...'); this.el.querySelector('#pdl-btn-fetch').disabled = true; try { const extractor = LinkExtractors[this.currentDrive.id]; if (!extractor) { Toast.error(`「${this.currentDrive.name}」直链提取功能开发中`); return; } const files = await extractor.getFileList(); if (!files || files.length === 0) { Toast.warning('未找到可下载的文件,请先在网盘页面选择文件'); return; } this.fileList = files.map(f => ({ name: f.name, size: f.size || 0, id: f.id || '', parentId: f.parentId || '', directLink: '', headers: f.headers || {}, status: 'pending', selected: true, raw: f, })); this.renderFileList(); this.setStatus(`找到 ${files.length} 个文件`); Toast.success(`成功获取 ${files.length} 个文件`); } catch (e) { Toast.error('获取文件列表失败: ' + e.message); this.setStatus('获取失败'); } finally { this.el.querySelector('#pdl-btn-fetch').disabled = false; } }, async handleBatch() { const selected = this.fileList.filter(f => f.selected && f.status !== 'success'); if (selected.length === 0) { Toast.warning('请先选择需要获取直链的文件'); return; } if (!this.currentDrive) return; const extractor = LinkExtractors[this.currentDrive.id]; if (!extractor) { Toast.error('该网盘暂不支持直链提取'); return; } this.setStatus('正在批量获取直链...'); this.showProgress(0); this.el.querySelector('#pdl-btn-batch').disabled = true; let successCount = 0; let failCount = 0; const total = selected.length; for (let i = 0; i < total; i++) { const file = selected[i]; file.status = 'loading'; this.renderFileList(); this.showProgress(Math.round((i / total) * 100)); this.setStatus(`正在获取 ${i + 1}/${total}...`); try { const result = await extractor.getDirectLink(file.raw, file); if (result && result.url) { file.directLink = result.url; file.headers = result.headers || file.headers || {}; file.status = 'success'; successCount++; } else { file.status = 'error'; file.error = result && result.error ? result.error : '未知错误'; failCount++; } } catch (e) { file.status = 'error'; file.error = e.message; failCount++; } this.renderFileList(); // Small delay to avoid rate limiting if (i < total - 1) await sleep(300); } this.showProgress(-1); this.el.querySelector('#pdl-btn-batch').disabled = false; this.setStatus(`完成: ${successCount}成功, ${failCount}失败`); if (successCount > 0) { Toast.success(`批量获取完成: ${successCount}个成功${failCount > 0 ? ', ' + failCount + '个失败' : ''}`); } else { Toast.error('全部获取失败'); } // Show links in viewer this.updateLinkViewer(); }, updateLinkViewer() { const viewer = this.el.querySelector('#pdl-link-viewer'); const successFiles = this.fileList.filter(f => f.status === 'success' && f.directLink); if (successFiles.length === 0) { viewer.classList.remove('pdl-show'); return; } const links = successFiles.map(f => f.directLink).join('\n'); viewer.textContent = links; viewer.classList.add('pdl-show'); }, toggleSelectAll() { const allSelected = this.fileList.every(f => f.selected); this.fileList.forEach(f => f.selected = !allSelected); this.renderFileList(); }, async handleDownload() { const tool = this.el.querySelector('#pdl-tool-select').value; const selected = this.fileList.filter(f => f.selected && f.status === 'success' && f.directLink); if (selected.length === 0) { Toast.warning('请先获取直链并选择文件'); return; } switch (tool) { case 'copy': const links = selected.map(f => f.directLink).join('\n'); GM_setClipboard(links); Toast.success(`已复制 ${selected.length} 个链接到剪贴板`); break; case 'curl': this.generateCurlCommands(selected); break; case 'aria2': await DownloadTools.sendToAria2(selected); break; case 'idm': DownloadTools.sendToIDM(selected); break; case 'xdown': DownloadTools.sendToXdown(selected); break; case 'export': DownloadTools.exportLinks(selected); break; case 'browser': DownloadTools.browserDownload(selected); break; } }, generateCurlCommands(files) { const commands = files.map(f => { let cmd = `curl -L -o "${f.name}"`; if (f.headers) { for (const [key, value] of Object.entries(f.headers)) { cmd += ` -H "${key}: ${value}"`; } } cmd += ` "${f.directLink}"`; return cmd; }).join('\n\n'); GM_setClipboard(commands); Toast.success(`已生成 ${files.length} 条 Curl 命令并复制到剪贴板`); }, clearList() { this.fileList = []; this.renderFileList(); this.updateLinkViewer(); this.setStatus('已清空'); Toast.info('列表已清空'); }, // === Drag functionality === setupDrag() { const handle = this.el.querySelector('#pdl-drag-handle'); let isDragging = false; let startX, startY, startLeft, startTop; handle.addEventListener('mousedown', (e) => { if (e.target.tagName === 'BUTTON') return; isDragging = true; startX = e.clientX; startY = e.clientY; const rect = this.el.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; this.el.classList.add('pdl-dragging'); e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; let newLeft = startLeft + (e.clientX - startX); let newTop = startTop + (e.clientY - startY); newLeft = Math.max(0, Math.min(window.innerWidth - this.el.offsetWidth, newLeft)); newTop = Math.max(0, Math.min(window.innerHeight - 50, newTop)); this.el.style.left = newLeft + 'px'; this.el.style.top = newTop + 'px'; this.el.style.right = 'auto'; }); document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; this.el.classList.remove('pdl-dragging'); this.savePosition(); } }); }, savePosition() { const rect = this.el.getBoundingClientRect(); setConfig(CONFIG_KEYS.PANEL_POSITION, { left: rect.left, top: rect.top }); }, restorePosition() { const pos = getConfig(CONFIG_KEYS.PANEL_POSITION); if (pos && pos.left !== undefined) { this.el.style.left = pos.left + 'px'; this.el.style.top = pos.top + 'px'; this.el.style.right = 'auto'; } }, }; /* ========================================================================= * Section 4 & 5: Link Extractors — Per Cloud Drive * ========================================================================= */ /** * Base structure for all extractors. * Each extractor must implement: * - getFileList(): Promise> * - getDirectLink(fileInfo, panelFile): Promise<{url, headers, error}> */ const LinkExtractors = { /* ---------- 百度网盘 ---------- */ baidu: { async getFileList() { // Try to read selected files from Baidu Pan page DOM const items = document.querySelectorAll('div.file-name-wrap, .wpz-file-list .file-name, [data-file-id]'); if (items.length === 0) { // Try list view const rows = document.querySelectorAll('.bd-main .file-list .file-name, .KPDwMS .file-name'); if (rows.length === 0) { throw new Error('未检测到选中的文件,请在百度网盘页面中先选中文件后再点击获取直链'); } } // Attempt to read from page context — Baidu stores file data in various ways // Method 1: Try to get selected file info from the page's Vue/store const fileList = []; try { // Try reading from the grid/list items const fileElements = document.querySelectorAll('[data-file-id], .file-name-wrap'); fileElements.forEach(el => { const nameEl = el.querySelector('.file-name .text, .filename') || el; const name = nameEl ? nameEl.textContent.trim() : ''; const fid = el.getAttribute('data-file-id') || el.getAttribute('data-fid') || ''; if (name && fid) { fileList.push({ name: name, size: 0, id: fid, isDir: el.classList.contains('dir') || el.querySelector('.icon-dir'), }); } }); } catch (e) {} if (fileList.length === 0) { throw new Error('未能自动获取文件列表。请确保已在百度网盘中选中文件,或手动输入分享链接。'); } return fileList; }, async getDirectLink(fileInfo, panelFile) { const bdstoken = getCookie('BDUSS') ? await this._getBdstoken() : null; if (!bdstoken) { throw new Error('无法获取 bdstoken,请确保已登录百度网盘'); } const url = 'https://pan.baidu.com/api/download'; const params = new URLSearchParams({ app_id: '250528', type: 'dlink', fidlist: '[' + fileInfo.id + ']', bdstoken: bdstoken, }); const response = await gmRequestJSON({ method: 'POST', url: url + '?' + params.toString(), headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': 'https://pan.baidu.com/disk/main', }, }); if (response.errno !== 0) { const errMsg = { '-12': '文件数量超限', '2': '参数错误', '111': '需要验证码', '-130': '文件不可下载', }[response.errno] || `错误码: ${response.errno}`; throw new Error(errMsg); } const dlink = response.dlink && response.dlink[0] && response.dlink[0].dlink; if (!dlink) throw new Error('未获取到下载链接'); return { url: dlink, headers: { 'User-Agent': 'LogStatistic', 'Cookie': 'BDUSS=' + getCookie('BDUSS'), }, }; }, async _getBdstoken() { try { const response = await gmRequestJSON({ method: 'GET', url: 'https://pan.baidu.com/api/gettemplatevariable?clienttype=0&app_id=250528&web=1', headers: { 'Referer': 'https://pan.baidu.com/disk/main' }, }); if (response.errno === 0 && response.result && response.result.bdstoken) { return response.result.bdstoken; } } catch (e) {} return null; }, }, /* ---------- 阿里云盘 ---------- */ aliyun: { token: null, async _getToken() { if (this.token) return this.token; // Aliyun Drive stores token in localStorage or window context try { const tokenData = localStorage.getItem('token'); if (tokenData) { const parsed = JSON.parse(tokenData); this.token = parsed.access_token || parsed.token; } } catch (e) {} if (!this.token) { // Try to extract from page scripts try { const scripts = document.querySelectorAll('script:not([src])'); for (const script of scripts) { const match = script.textContent.match(/"access_token"\s*:\s*"([^"]+)"/); if (match) { this.token = match[1]; break; } } } catch (e) {} } if (!this.token) { throw new Error('无法获取阿里云盘 Token,请确保已登录'); } return this.token; }, async getFileList() { const token = await this._getToken(); // Get drive info const driveInfo = await gmRequestJSON({ method: 'POST', url: 'https://api.aliyundrive.com/v2/user/get', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json', }, data: JSON.stringify({}), }); const driveId = driveInfo.default_drive_id || driveInfo.resource_drive_id; if (!driveId) throw new Error('无法获取 Drive ID'); // List files in current directory const fileList = await gmRequestJSON({ method: 'POST', url: 'https://api.aliyundrive.com/adrive/v1.0/openFile/list', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json', }, data: JSON.stringify({ drive_id: driveId, parent_file_id: 'root', limit: 100, order_by: 'name', order_direction: 'ASC', }), }); if (!fileList.items) throw new Error('获取文件列表失败'); return fileList.items .filter(item => item.type === 'file') .map(item => ({ name: item.name, size: item.size || 0, id: item.file_id, parentId: driveId, driveId: driveId, headers: { 'Authorization': 'Bearer ' + token }, })); }, async getDirectLink(fileInfo, panelFile) { const token = await this._getToken(); const driveId = fileInfo.driveId || fileInfo.parentId; const response = await gmRequestJSON({ method: 'POST', url: 'https://api.aliyundrive.com/v2/file/get_download_url', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json', 'Referer': 'https://www.alipan.com/', }, data: JSON.stringify({ drive_id: driveId, file_id: fileInfo.id, }), }); if (response.url) { return { url: response.url, headers: { 'Authorization': 'Bearer ' + token, 'Referer': 'https://www.alipan.com/', }, }; } throw new Error('未获取到下载链接'); }, }, /* ---------- 天翼云盘 ---------- */ tianyi: { async getFileList() { // Try to get file list from page context const fileList = []; const items = document.querySelectorAll('.file-item, [data-file-id], .file-info'); items.forEach(item => { const nameEl = item.querySelector('.file-name, .filename, .name') || item; const name = nameEl.textContent.trim(); const fid = item.getAttribute('data-file-id') || item.getAttribute('data-id') || ''; if (name && fid) { fileList.push({ name: name, size: 0, id: fid, isDir: item.classList.contains('folder') || item.querySelector('.folder-icon'), }); } }); if (fileList.length === 0) { throw new Error('未检测到文件,请先在天翼云盘页面中选中文件'); } return fileList; }, async getDirectLink(fileInfo, panelFile) { const cookie = getCookie('sessionKey') || ''; if (!cookie) { throw new Error('未检测到天翼云盘登录状态'); } // Get file download info via API const response = await gmRequestJSON({ method: 'GET', url: 'https://cloud.189.cn/api/file/getFileInfo.action?fileId=' + fileInfo.id, headers: { 'Referer': 'https://cloud.189.cn/' }, }); if (response.fileDownloadUrl) { return { url: response.fileDownloadUrl, headers: { 'Referer': 'https://cloud.189.cn/' } }; } // Try alternate API const resp2 = await gmRequestJSON({ method: 'POST', url: 'https://cloud.189.cn/api/file/batchCreateDownloadTask.action', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': 'https://cloud.189.cn/', }, data: 'fileIdList=' + encodeURIComponent(JSON.stringify([fileInfo.id])), }); if (resp2 && resp2.fileTaskList && resp2.fileTaskList[0]) { return { url: resp2.fileTaskList[0].downloadUrl, headers: { 'Referer': 'https://cloud.189.cn/' }, }; } throw new Error('获取天翼云盘直链失败'); }, }, /* ---------- 迅雷云盘 ---------- */ xunlei: { async getFileList() { const fileList = []; const token = this._getToken(); if (!token) throw new Error('未检测到迅雷云盘登录状态'); // Get device id and list files const driveInfo = await gmRequestJSON({ method: 'GET', url: 'https://api-pan.xunlei.com/yunpan/v1/drive', headers: { 'Authorization': 'Bearer ' + token }, }); const items = document.querySelectorAll('.file-item, .file-name, [data-file-id]'); items.forEach(item => { const name = (item.querySelector('.name, .file-name') || item).textContent.trim(); const id = item.getAttribute('data-file-id') || item.getAttribute('data-id') || ''; if (name) fileList.push({ name, size: 0, id, isDir: false }); }); if (fileList.length === 0) { throw new Error('未检测到选中的文件,请先在迅雷云盘页面中选中文件'); } return fileList; }, async getDirectLink(fileInfo, panelFile) { const token = this._getToken(); if (!token) throw new Error('未检测到迅雷云盘登录状态'); const response = await gmRequestJSON({ method: 'POST', url: 'https://api-pan.xunlei.com/yunpan/v1/file/' + fileInfo.id + '/download', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json', }, data: JSON.stringify({}), }); if (response && response.web_content_link) { return { url: response.web_content_link, headers: {} }; } if (response && response.download_url) { return { url: response.download_url, headers: {} }; } throw new Error('获取迅雷云盘直链失败'); }, _getToken() { try { const token = localStorage.getItem('token'); if (token) { const parsed = JSON.parse(token); return parsed.access_token || parsed.token || null; } } catch (e) {} return null; }, }, /* ---------- 夸克网盘 ---------- */ quark: { async getFileList() { const fileList = []; // Try to get from page data try { const items = document.querySelectorAll('.file-item, [data-file-id], .ant-table-row'); items.forEach(item => { const nameEl = item.querySelector('.file-name, .file-name-text, .filename') || item; const name = nameEl.textContent.trim(); const fid = item.getAttribute('data-file-id') || item.getAttribute('data-fid') || ''; if (name && fid) { fileList.push({ name, size: 0, id: fid, isDir: false }); } }); } catch (e) {} if (fileList.length === 0) { // Try to extract from page script data try { const pageData = window.__INITIAL_STATE__ || window.__NEXT_DATA__; if (pageData && pageData.fileList) { pageData.fileList.forEach(f => { if (!f.dir) { fileList.push({ name: f.file_name, size: f.size || 0, id: f.fid, isDir: false }); } }); } } catch (e) {} } if (fileList.length === 0) { throw new Error('未检测到选中的文件,请先在夸克网盘页面中选中文件'); } return fileList; }, async getDirectLink(fileInfo, panelFile) { const response = await gmRequestJSON({ method: 'POST', url: 'https://drive-pc.quark.cn/1/clouddrive/file/download', headers: { 'Content-Type': 'application/json', 'Referer': 'https://pan.quark.cn/', }, data: JSON.stringify({ fid: [fileInfo.id], }), }); if (response.data && response.data.length > 0) { return { url: response.data[0].download_url, headers: { 'Referer': 'https://pan.quark.cn/', 'Cookie': document.cookie, }, }; } throw new Error('获取夸克网盘直链失败'); }, }, /* ---------- 移动云盘 ---------- */ mobile: { async getFileList() { const fileList = []; const items = document.querySelectorAll('.file-item, [data-file-id], .file-info, .catalogDtl'); items.forEach(item => { const nameEl = item.querySelector('.file-name, .name, .filename') || item; const name = nameEl.textContent.trim(); const id = item.getAttribute('data-file-id') || item.getAttribute('data-id') || ''; if (name && id) { fileList.push({ name, size: 0, id, isDir: false }); } }); if (fileList.length === 0) { throw new Error('未检测到选中的文件,请先在移动云盘页面中选中文件'); } return fileList; }, async getDirectLink(fileInfo, panelFile) { const response = await gmRequestJSON({ method: 'POST', url: 'https://yun.139.com/oracle/oracle/batchDownloadUrl', headers: { 'Content-Type': 'application/json', 'Referer': 'https://yun.139.com/', }, data: JSON.stringify({ type: '1', contentList: [{ contentID: fileInfo.id }], }), }); if (response && response.result && response.result.url) { return { url: response.result.url, headers: { 'Referer': 'https://yun.139.com/' } }; } throw new Error('获取移动云盘直链失败'); }, }, /* ---------- 123云盘 ---------- */ pan123: { async getFileList() { const fileList = []; // Try page DOM extraction const items = document.querySelectorAll('.file-item, [data-file-id], .ant-table-row'); items.forEach(item => { const nameEl = item.querySelector('.file-name, .filename, .name') || item; const name = nameEl.textContent.trim(); const id = item.getAttribute('data-file-id') || item.getAttribute('data-id') || ''; if (name && id) { fileList.push({ name, size: 0, id, isDir: false }); } }); if (fileList.length === 0) { // Try reading from page data try { const token = this._getToken(); if (token) { const response = await gmRequestJSON({ method: 'GET', url: 'https://www.123912.com/api/file/list/new', headers: { 'Authorization': 'Bearer ' + token, 'Platform': 'web', }, }); if (response.data && response.data.InfoList) { response.data.InfoList.forEach(f => { if (f.Type !== 1) { // Not a folder fileList.push({ name: f.FileName, size: f.Size || 0, id: f.FileId, isDir: false, }); } }); } } } catch (e) {} } if (fileList.length === 0) { throw new Error('未检测到文件,请先在123云盘页面中选中文件'); } return fileList; }, async getDirectLink(fileInfo, panelFile) { const token = this._getToken(); if (!token) throw new Error('未检测到123云盘登录状态'); const response = await gmRequestJSON({ method: 'POST', url: 'https://www.123912.com/api/file/download_info', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json', 'Platform': 'web', }, data: JSON.stringify({ fileId: fileInfo.id, }), }); if (response.data && response.data.DownloadUrl) { return { url: response.data.DownloadUrl, headers: {} }; } throw new Error('获取123云盘直链失败'); }, _getToken() { try { const token = localStorage.getItem('token'); if (token) { const parsed = JSON.parse(token); return parsed.token || parsed.access_token || null; } } catch (e) {} return null; }, }, /* ---------- 蓝奏云盘 ---------- */ lanzou: { async getFileList() { const fileList = []; // Lanzou file share page — parse file links from DOM const fileLinks = document.querySelectorAll('a.file_a, .fileinfo a, #filelink, a[href*="down"]'); fileLinks.forEach(link => { const name = link.textContent.trim() || link.title || ''; const href = link.href || ''; if (name && href) { fileList.push({ name: name, size: 0, id: href, isDir: false, }); } }); // Also check for direct file download links on the page const downloadBtn = document.querySelector('#down1, .down1, a[id*="down"]'); if (downloadBtn && fileList.length === 0) { // Single file page const fileName = document.querySelector('.fileinfo .filename, .filethum .filename, h2') ? document.querySelector('.fileinfo .filename, .filethum .filename, h2').textContent.trim() : 'download'; fileList.push({ name: fileName, size: 0, id: 'single', isDir: false, }); } if (fileList.length === 0) { throw new Error('未检测到蓝奏云文件,请确保当前页面有文件可下载'); } return fileList; }, async getDirectLink(fileInfo, panelFile) { // If it's a direct link, return it if (fileInfo.id && fileInfo.id.startsWith('http')) { // Follow redirect to get actual download URL try { const response = await gmRequest({ method: 'GET', url: fileInfo.id, headers: { 'Referer': window.location.href }, }); const html = response.responseText; // Parse the actual download URL from the redirected page const urlMatch = html.match(/href\s*=\s*["']([^"']*download[^"']*)["']/i) || html.match(/var\s+com\s*=\s*['"]([^'"]+)['"]/) || html.match(/]*href\s*=\s*["']([^"']+\.(?:zip|rar|7z|exe|apk|ipa|dmg|pdf|doc|docx|xls|xlsx|ppt|pptx|mp3|mp4|wav|flac|apk|xapk|iso|txt|epub|mobi|azw3))["']/i); if (urlMatch && urlMatch[1]) { let url = urlMatch[1]; if (url.startsWith('//')) url = 'https:' + url; else if (url.startsWith('/')) url = window.location.origin + url; return { url: url, headers: { 'Referer': window.location.href } }; } } catch (e) { // Fall through to return the original link } return { url: fileInfo.id, headers: {} }; } // Single file download page — parse download URL const pageHtml = document.documentElement.outerHTML; let downloadUrl = null; // Method 1: Look for the download URL in script tags const patterns = [ /var\s+ajax_url\s*=\s*['"]([^'"]+)['"]/, /href\s*=\s*["']([^"']*download[^"']*)["']/i, /'([^']*\.html[^']*)'/, /window\.location\s*=\s*['"]([^'"]+)['"]/, ]; for (const pattern of patterns) { const match = pageHtml.match(pattern); if (match && match[1]) { downloadUrl = match[1]; break; } } // Method 2: Try to find the download button and simulate click if (!downloadUrl) { const downBtn = document.querySelector('#down1, #down2, .down1, .down2, a[onclick*="down"], a[onclick*="download"]'); if (downBtn) { const onclick = downBtn.getAttribute('onclick') || ''; const idMatch = onclick.match(/['"]?(\d+)['"]?/); if (idMatch) { const fileId = idMatch[1]; try { const formData = new URLSearchParams(); formData.append('action', 'down_process'); formData.append('file_id', fileId); formData.append('sign', ''); formData.append('p', '1'); const response = await gmRequest({ method: 'POST', url: window.location.origin + '/ajaxm.php', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': window.location.href, }, data: formData.toString(), }); const data = JSON.parse(response.responseText); if (data && data.url) { downloadUrl = data.url; } } catch (e) {} } } } if (downloadUrl) { if (downloadUrl.startsWith('//')) downloadUrl = 'https:' + downloadUrl; else if (downloadUrl.startsWith('/')) downloadUrl = window.location.origin + downloadUrl; return { url: downloadUrl, headers: { 'Referer': window.location.href } }; } throw new Error('解析蓝奏云下载链接失败'); }, }, /* ---------- 腾讯微云 ---------- */ weiyun: { async getFileList() { const fileList = []; const items = document.querySelectorAll('.file-item, [data-file-id], .file-name'); items.forEach(item => { const name = (item.querySelector('.file-name, .name, .filename') || item).textContent.trim(); const id = item.getAttribute('data-file-id') || item.getAttribute('data-id') || ''; if (name && id) { fileList.push({ name, size: 0, id, isDir: false }); } }); if (fileList.length === 0) { throw new Error('未检测到选中的文件,请先在腾讯微云页面中选中文件'); } return fileList; }, async getDirectLink(fileInfo, panelFile) { const cookie = getCookie('pskey') || getCookie('skey') || ''; if (!cookie) { throw new Error('未检测到腾讯微云登录状态'); } const response = await gmRequestJSON({ method: 'POST', url: 'https://www.weiyun.com/disk/file-list-download', headers: { 'Content-Type': 'application/json', 'Referer': 'https://www.weiyun.com/', }, data: JSON.stringify({ fileList: [{ fileId: fileInfo.id }], }), }); if (response.data && response.data.downloadUrl) { return { url: response.data.downloadUrl, headers: { 'Referer': 'https://www.weiyun.com/' } }; } throw new Error('获取腾讯微云直链失败'); }, }, /* ---------- Google Drive ---------- */ gdrive: { async getFileList() { const fileList = []; // Parse Google Drive file list from DOM const items = document.querySelectorAll('[data-id], .flip-entry, .kHn3Ed'); items.forEach(item => { const id = item.getAttribute('data-id') || ''; const nameEl = item.querySelector('.flip-entry-title, .KL4NAf, [role="link"]') || item; const name = nameEl.textContent.trim(); if (name && id) { fileList.push({ name, size: 0, id, isDir: false }); } }); if (fileList.length === 0) { // Try to get file ID from URL for single file const urlMatch = window.location.href.match(/file\/d\/([^\/]+)/); if (urlMatch) { const fileId = urlMatch[1]; const name = document.querySelector('[role="heading"]') ? document.querySelector('[role="heading"]').textContent.trim() : 'gdrive_file'; fileList.push({ name, size: 0, id: fileId, isDir: false }); } } if (fileList.length === 0) { throw new Error('未检测到文件,请先在 Google Drive 页面中选中文件'); } return fileList; }, async getDirectLink(fileInfo, panelFile) { const fileId = fileInfo.id; // Google Drive direct download URL const downloadUrl = `https://drive.google.com/uc?export=download&id=${fileId}`; // Try to get confirmation token for large files try { const response = await gmRequest({ method: 'GET', url: downloadUrl, }); const html = response.responseText; // Check for virus scan confirmation const confirmMatch = html.match(/confirm=([^&"']+)/); const uuidMatch = html.match(/uuid=([^&"']+)/); if (confirmMatch) { let url = `https://drive.google.com/uc?export=download&id=${fileId}&confirm=${confirmMatch[1]}`; if (uuidMatch) url += `&uuid=${uuidMatch[1]}`; return { url: url, headers: {} }; } } catch (e) {} return { url: downloadUrl, headers: {} }; }, }, /* ---------- Dropbox ---------- */ dropbox: { async getFileList() { const fileList = []; // Parse Dropbox file list from DOM const items = document.querySelectorAll('[data-testid*="file"], .mc-file-row, .brws-file-name-container'); items.forEach(item => { const nameEl = item.querySelector('.file-name, [data-testid="filename"], .mc-file-name-text') || item; const name = nameEl.textContent.trim(); const href = item.getAttribute('data-href') || item.querySelector('a')?.href || ''; if (name && href) { fileList.push({ name, size: 0, id: href, isDir: false }); } }); // Try to get file path from URL if (fileList.length === 0) { const urlPath = window.location.pathname; if (urlPath.includes('/home/') || urlPath.includes('/sh/')) { const fileName = urlPath.split('/').pop() || 'dropbox_file'; fileList.push({ name: fileName, size: 0, id: urlPath, isDir: false }); } } if (fileList.length === 0) { throw new Error('未检测到文件,请先在 Dropbox 页面中选中文件'); } return fileList; }, async getDirectLink(fileInfo, panelFile) { let path = fileInfo.id; if (path.startsWith('http')) { // Convert Dropbox share URL to direct download URL path = path.replace('dl.dropbox.com', 'www.dropbox.com'); // Replace ?dl=0 with ?dl=1 for direct download const directUrl = path.replace(/[?&]dl=0/, '?dl=1').replace(/[?&]dl=1.*$/, '?dl=1'); if (!directUrl.includes('dl=1')) { return { url: directUrl + (directUrl.includes('?') ? '&' : '?') + 'dl=1', headers: {} }; } return { url: directUrl, headers: {} }; } // If it's a path, construct direct download URL const baseUrl = window.location.origin; const downloadUrl = `${baseUrl}${path}?dl=1`; return { url: downloadUrl, headers: {} }; }, }, /* ---------- OneDrive ---------- */ onedrive: { async getFileList() { const fileList = []; // Parse OneDrive file list from DOM const items = document.querySelectorAll('[data-automationid="FileRow"], [role="row"]'); items.forEach(item => { const nameEl = item.querySelector('[data-automationid="name"], .ms-DetailsRow-cell [title]') || item; const name = nameEl.getAttribute('title') || nameEl.textContent.trim(); if (name && !name.includes('Name') && !name.includes('Modified')) { const href = item.getAttribute('data-href') || ''; fileList.push({ name, size: 0, id: href || name, isDir: false }); } }); if (fileList.length === 0) { throw new Error('未检测到文件,请先在 OneDrive 页面中选中文件'); } return fileList; }, async getDirectLink(fileInfo, panelFile) { // OneDrive direct download — modify share link let url = fileInfo.id; if (url && url.startsWith('http')) { // Try to convert to direct download if (url.includes('1drv.ms')) { // Short URL — need to resolve const response = await gmRequest({ method: 'GET', url: url }); const finalUrl = response.finalUrl || url; // Convert to download URL const downloadUrl = finalUrl.replace('redir', 'download') .replace(/e=.*$/, 'download=1'); return { url: downloadUrl, headers: {} }; } // OneDrive live URL — add download parameter if (url.includes('onedrive.live.com')) { const downloadUrl = url.includes('?') ? url + '&download=1' : url + '?download=1'; return { url: downloadUrl, headers: {} }; } } throw new Error('获取 OneDrive 直链失败,请确保已选中文件'); }, }, /* ---------- pCloud ---------- */ pcloud: { async getFileList() { const fileList = []; // Parse pCloud file list from DOM const items = document.querySelectorAll('[data-fileid], .file-item, .list-item'); items.forEach(item => { const nameEl = item.querySelector('.file-name, .name, .filename') || item; const name = nameEl.textContent.trim(); const id = item.getAttribute('data-fileid') || item.getAttribute('data-id') || ''; if (name && id) { fileList.push({ name, size: 0, id, isDir: false }); } }); if (fileList.length === 0) { throw new Error('未检测到文件,请先在 pCloud 页面中选中文件'); } return fileList; }, async getDirectLink(fileInfo, panelFile) { // Try to get token from page context const token = this._getToken(); if (!token) throw new Error('未检测到 pCloud 登录状态'); const response = await gmRequestJSON({ method: 'GET', url: 'https://api.pcloud.com/getfilelink?fileid=' + fileInfo.id + '&access_token=' + token, headers: {}, }); if (response.hosts && response.path) { const url = 'https://' + response.hosts[0] + response.path; return { url: url, headers: {} }; } throw new Error('获取 pCloud 直链失败'); }, _getToken() { try { const token = localStorage.getItem('access_token'); if (token) return token; // Try to find in page context const scripts = document.querySelectorAll('script:not([src])'); for (const script of scripts) { const match = script.textContent.match(/access_token["\s:]+["']([^"']+)["']/); if (match) return match[1]; } } catch (e) {} return null; }, }, /* ---------- TeraBox ---------- */ terabox: { async getFileList() { const fileList = []; // Parse TeraBox file list from DOM const items = document.querySelectorAll('[data-file-id], .file-item, .file-name'); items.forEach(item => { const name = (item.querySelector('.file-name, .name, .filename') || item).textContent.trim(); const id = item.getAttribute('data-file-id') || item.getAttribute('data-id') || ''; if (name && id) { fileList.push({ name, size: 0, id, isDir: false }); } }); // Try to get from URL (share link) if (fileList.length === 0) { const urlMatch = window.location.href.match(/s\/([^\/]+)/); if (urlMatch) { fileList.push({ name: 'terabox_file', size: 0, id: urlMatch[1], isDir: false, }); } } if (fileList.length === 0) { throw new Error('未检测到文件,请先在 TeraBox 页面中选中文件'); } return fileList; }, async getDirectLink(fileInfo, panelFile) { // Try to get download link from TeraBox API const shortUrl = fileInfo.id; try { // Get file info from short URL const response = await gmRequestJSON({ method: 'GET', url: 'https://www.terabox.com/api/shorturlinfo?shorturl=' + shortUrl, headers: { 'Referer': 'https://www.terabox.com/' }, }); if (response.dlink) { return { url: response.dlink, headers: { 'User-Agent': 'Mozilla/5.0', 'Referer': 'https://www.terabox.com/', }, }; } if (response.list && response.list.length > 0) { const file = response.list[0]; if (file.dlink) { return { url: file.dlink, headers: { 'User-Agent': 'Mozilla/5.0', 'Referer': 'https://www.terabox.com/', }, }; } } } catch (e) { // Try alternate approach — get from page const pageHtml = document.documentElement.outerHTML; const dlinkMatch = pageHtml.match(/"dlink"\s*:\s*"([^"]+)"/); if (dlinkMatch) { return { url: dlinkMatch[1].replace(/\\u002f/g, '/').replace(/\\\//g, '/'), headers: { 'Referer': 'https://www.terabox.com/' }, }; } } throw new Error('获取 TeraBox 直链失败'); }, }, }; /* ========================================================================= * Section 6: Download Tool Integration * ========================================================================= */ const Aria2Client = { async rpc(method, params) { const url = getConfig(CONFIG_KEYS.ARIA2_URL); const secret = getConfig(CONFIG_KEYS.ARIA2_SECRET); const rpcParams = []; if (secret) rpcParams.push('token:' + secret); rpcParams.push(...params); const response = await gmRequestJSON({ method: 'POST', url: url, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ jsonrpc: '2.0', id: genId(), method: method, params: rpcParams, }), }); if (response.error) { throw new Error(response.error.message || 'Aria2 RPC error'); } return response.result; }, async testConnection(url, secret) { const rpcParams = []; if (secret) rpcParams.push('token:' + secret); const response = await gmRequestJSON({ method: 'POST', url: url, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ jsonrpc: '2.0', id: 'test', method: 'aria2.getVersion', params: rpcParams, }), }); if (response.error) throw new Error(response.error.message); return response.result ? response.result.version : null; }, }; const DownloadTools = { /** * Send files to Aria2 via JSON-RPC */ async sendToAria2(files) { try { let successCount = 0; for (const file of files) { const options = { 'out': file.name, 'dir': '', }; // Add headers if present if (file.headers) { const headerList = []; for (const [key, value] of Object.entries(file.headers)) { headerList.push(key + ': ' + value); } if (headerList.length > 0) { options['header'] = headerList; } } await Aria2Client.rpc('aria2.addUri', [[file.directLink], options]); successCount++; } Toast.success(`已发送 ${successCount} 个任务到 Aria2`); } catch (e) { Toast.error('发送到 Aria2 失败: ' + e.message); } }, /** * Send to IDM via special protocol or clipboard */ sendToIDM(files) { // IDM supports multiple methods: // 1. idm:// protocol (if IDM browser extension is installed) // 2. Copy links and let user paste into IDM // 3. Try to use IDM's command line interface via a special URL const links = files.map(f => f.directLink).join('\r\n'); // Try IDM protocol try { const idmUrl = 'idm://start?url=' + encodeURIComponent(files[0].directLink); // Create a temporary link and click it const a = document.createElement('a'); a.href = idmUrl; a.click(); Toast.info('正在尝试通过 IDM 协议下载... 如果未弹出 IDM,请复制链接手动添加'); // Also copy all links as backup setTimeout(() => { GM_setClipboard(links); Toast.success('所有链接已复制到剪贴板(备用)'); }, 1000); } catch (e) { GM_setClipboard(links); Toast.info('IDM 协议未响应,链接已复制到剪贴板,请在 IDM 中手动添加'); } }, /** * Send to Xdown */ sendToXdown(files) { const links = files.map(f => f.directLink).join('\r\n'); try { // Xdown protocol const xdownUrl = 'xdown://download?url=' + encodeURIComponent(files[0].directLink); const a = document.createElement('a'); a.href = xdownUrl; a.click(); Toast.info('正在尝试通过 Xdown 协议下载... 如果未弹出 Xdown,请复制链接手动添加'); setTimeout(() => { GM_setClipboard(links); Toast.success('所有链接已复制到剪贴板(备用)'); }, 1000); } catch (e) { GM_setClipboard(links); Toast.info('Xdown 协议未响应,链接已复制到剪贴板'); } }, /** * Export links as a text file */ exportLinks(files) { const content = files.map(f => { let line = f.directLink; if (f.headers && Object.keys(f.headers).length > 0) { line += ' | Headers: '; line += Object.entries(f.headers).map(([k, v]) => `${k}: ${v}`).join('; '); } return line; }).join('\n'); const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'direct_links_' + Date.now() + '.txt'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); Toast.success(`已导出 ${files.length} 个链接到文件`); }, /** * Open in browser (direct download) */ browserDownload(files) { if (files.length > 5) { Toast.warning('浏览器下载最多同时 5 个,将下载前 5 个文件'); files = files.slice(0, 5); } files.forEach((file, index) => { setTimeout(() => { const a = document.createElement('a'); a.href = file.directLink; a.download = file.name; a.target = '_blank'; document.body.appendChild(a); a.click(); document.body.removeChild(a); }, index * 500); }); Toast.success(`已开始下载 ${files.length} 个文件`); }, }; /* ========================================================================= * Section 7: Main Entry Point * ========================================================================= */ function main() { const currentDrive = detectCurrentDrive(); if (!currentDrive) { console.log(`[${SCRIPT_NAME}] 当前网站不受支持`); return; } console.log(`[${SCRIPT_NAME}] 检测到网盘: ${currentDrive.name} (v${SCRIPT_VERSION})`); // Register menu command to toggle panel GM_registerMenuCommand('🔄 显示/隐藏面板', () => { const panel = document.getElementById('pdl-panel'); if (panel) { if (panel.classList.contains('pdl-hidden')) { panel.classList.remove('pdl-hidden'); document.getElementById('pdl-fab')?.classList.remove('pdl-show'); } else { panel.classList.add('pdl-hidden'); document.getElementById('pdl-fab')?.classList.add('pdl-show'); } } else { Panel.init(currentDrive); } }); GM_registerMenuCommand('⚙️ 打开设置', () => { if (!document.getElementById('pdl-settings-overlay')) { Panel.init(currentDrive); } document.getElementById('pdl-settings-overlay')?.classList.add('pdl-show'); }); // Wait for page to be ready function initPanel() { if (!document.body) { setTimeout(initPanel, 100); return; } Panel.init(currentDrive); Toast.info(`已加载「${currentDrive.name}」直链下载助手`); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initPanel); } else { initPanel(); } // Re-init on SPA navigation let lastUrl = window.location.href; const observer = new MutationObserver(() => { if (window.location.href !== lastUrl) { lastUrl = window.location.href; // Delay to allow page content to load setTimeout(() => { if (Panel.currentDrive && Panel.el) { Panel.setStatus('就绪'); } }, 1000); } }); observer.observe(document.body, { childList: true, subtree: true }); } // Start the script try { main(); } catch (e) { console.error(`[${SCRIPT_NAME}] Initialization error:`, e); } })();