// ==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 = `
${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 = `
⚙️ 设置
📌 支持的网盘:百度/阿里/天翼/迅雷/夸克/移动/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 = `';
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);
}
})();