// ==UserScript==
// @name qBittorrent & Aria2 推送助手
// @namespace http://tampermonkey.net/
// @version 1.1
// @description 一键将磁力链接、种子资源推送到远程的qBittorrent或Aria2客户端下载
// @author deepseek & 通义
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_notification
// @grant GM_addStyle
// @license MIT
// @icon https://p.sda1.dev/26/90d92c0ef2f191f7f467677361af144f/push.png
// ==/UserScript==
(function () {
'use strict';
// ================== 配置区域 ==================
// qBittorrent 在此处配置
const QBITTORRENT_URL = 'http://192.168.1.23:8080/';
const QBITTORRENT_USER = 'admin';
const QBITTORRENT_PASS = 'adminadmin';
// Aria2 在此处配置配置
const ARIA2_URL = 'http://192.168.1.23:6800/jsonrpc';
const ARIA2_SECRET = '123456';
const SHOW_DELAY = 300; // 显示延迟(毫秒)
const HIDE_DELAY = 275; // 隐藏延迟(毫秒)
// 配置支持推送的资源类型(通过注释禁用不需要的类型,主要作用于Aria2)
const SUPPORTED_TYPES = [
'magnet', // 磁力链接
'torrent', // BT种子文件
//'http', // HTTP下载
//'https', // HTTPS下载
//'ftp', // FTP下载
'ed2k', // eDonkey链接
//'metalink' // Metalink文件
].filter(Boolean); // 过滤掉被注释的行
// ================== 配置结束 ==================
let currentMainButton = null;
let currentSubButtons = null;
let showTimeout = null;
let hideTimeout = null;
// 添加全局样式
GM_addStyle(`
.qba-push-container {
position: absolute;
z-index: 99999;
display: flex;
flex-direction: column;
gap: 6px;
pointer-events: none;
opacity: 0;
transform: translateY(10px);
transition: all 0.3s ease;
}
.qba-push-container.visible {
opacity: 1;
transform: translateY(0);
pointer-events: all;
}
.qba-main-button {
padding: 6px 12px;
font-size: 12px;
font-weight: 600;
color: white;
background: linear-gradient(135deg, #4facfe, #00f2fe);
border: none;
border-radius: 20px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.15);
transition: all 0.3s ease;
white-space: nowrap;
pointer-events: all;
z-index: 100000;
}
.qba-main-button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.25);
}
.qba-sub-buttons {
display: flex;
gap: 6px;
background: rgba(30, 41, 59, 0.95);
border-radius: 10px;
padding: 8px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
border: 1px solid rgba(255, 255, 255, 0.1);
pointer-events: all;
}
.qba-sub-button {
padding: 6px 10px;
font-size: 11px;
font-weight: 500;
color: #e2e8f0;
background: rgba(15, 23, 42, 0.8);
border: none;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
transition: all 0.3s ease;
white-space: nowrap;
}
.qba-sub-button:hover {
background: rgba(79, 172, 254, 0.3);
transform: translateY(-2px);
}
.qba-qb-button {
border-left: 2px solid #4facfe;
}
.qba-aria2-button {
border-left: 2px solid #00f2fe;
}
.qba-working {
background: #ffc107 !important;
cursor: wait !important;
}
.qba-success {
background: #28a745 !important;
}
.qba-error {
background: #dc3545 !important;
}
`);
// 资源类型图标映射
const resourceIcons = {
'magnet': '🧲',
'torrent': '📁',
'http': '🌐',
'https': '🔒',
'ftp': '📡',
'ed2k': '🔗',
'metalink': '⚙️',
'default': '⬇️'
};
// 下载器图标
const downloaderIcons = {
'qb': '🔵', // qBittorrent图标
'aria2': '🟢' // Aria2图标
};
// 获取背景亮度(感知亮度公式)
function getBackgroundBrightness(element) {
let el = element;
while (el && el !== document.body) {
const style = window.getComputedStyle(el);
const bg = style.backgroundColor;
if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') {
const rgb = bg.match(/(\d+),\s*(\d+),\s*(\d+)/);
if (rgb) {
const r = parseInt(rgb[1]);
const g = parseInt(rgb[2]);
const b = parseInt(rgb[3]);
return (r * 299 + g * 587 + b * 114) / 1000;
}
}
el = el.parentElement;
}
return 255; // 默认亮色背景
}
// 获取链接类型
function getLinkType(url) {
if (url.startsWith('magnet:')) return 'magnet';
if (url.endsWith('.torrent') || url.includes('.torrent?')) return 'torrent';
if (url.startsWith('ed2k:')) return 'ed2k';
if (url.startsWith('metalink:')) return 'metalink';
if (url.startsWith('http:')) return 'http';
if (url.startsWith('https:')) return 'https';
if (url.startsWith('ftp:')) return 'ftp';
return 'default';
}
// 创建主按钮
function createMainButton(url, text, link) {
const linkType = getLinkType(url);
// 检查资源类型是否在支持列表中
if (!SUPPORTED_TYPES.includes(linkType)) return null;
const icon = resourceIcons[linkType] || resourceIcons.default;
const button = document.createElement('button');
button.className = 'qba-main-button';
button.innerHTML = `${icon} ${linkType.toUpperCase()}`;
button.setAttribute('aria-label', '推送下载资源');
button.dataset.linkType = linkType;
// 判断背景明暗
const brightness = getBackgroundBrightness(link);
const isDarkBg = brightness < 150;
// 根据背景调整按钮文字颜色
if (isDarkBg) {
button.style.color = '#fff';
} else {
button.style.color = '#fff';
}
return button;
}
// 创建子按钮
function createSubButtons(url, text, link) {
const container = document.createElement('div');
container.className = 'qba-sub-buttons';
const linkType = getLinkType(url);
// 只对磁力链接显示qBittorrent按钮
if (linkType === 'magnet') {
// 创建推送到qBittorrent的按钮
const qbButton = document.createElement('button');
qbButton.className = 'qba-sub-button qba-qb-button';
qbButton.innerHTML = `<span>${downloaderIcons.qb}</span> <span>推送到 qB</span>`;
qbButton.onclick = (e) => {
e.stopPropagation();
pushToDownloader('qb', url, text, link, qbButton);
};
container.appendChild(qbButton);
}
// 对于所有支持的类型,显示Aria2按钮
// 创建推送到Aria2的按钮
const aria2Button = document.createElement('button');
aria2Button.className = 'qba-sub-button qba-aria2-button';
aria2Button.innerHTML = `<span>${downloaderIcons.aria2}</span> <span>推送到 Aria2</span>`;
aria2Button.onclick = (e) => {
e.stopPropagation();
pushToDownloader('aria2', url, text, link, aria2Button);
};
container.appendChild(aria2Button);
return container;
}
// 创建按钮容器
function createButtonContainer(url, text, link) {
// 移除现有的按钮
if (currentMainButton) {
currentMainButton.remove();
currentMainButton = null;
}
if (currentSubButtons) {
currentSubButtons.remove();
currentSubButtons = null;
}
const mainButton = createMainButton(url, text, link);
if (!mainButton) return null; // 如果资源类型不在支持列表中,不创建按钮
const container = document.createElement('div');
container.className = 'qba-push-container';
const subButtons = createSubButtons(url, text, link);
container.appendChild(mainButton);
container.appendChild(subButtons);
// 主按钮悬停事件
mainButton.addEventListener('mouseenter', () => {
clearTimeout(hideTimeout);
});
// 主按钮离开事件
mainButton.addEventListener('mouseleave', () => {
hideTimeout = setTimeout(() => {
container.classList.remove('visible');
setTimeout(() => {
if (container.parentNode) {
container.remove();
}
}, 300);
}, HIDE_DELAY);
});
// 子按钮区域事件
subButtons.addEventListener('mouseenter', () => {
clearTimeout(hideTimeout);
});
subButtons.addEventListener('mouseleave', () => {
hideTimeout = setTimeout(() => {
container.classList.remove('visible');
setTimeout(() => {
if (container.parentNode) {
container.remove();
}
}, 300);
}, HIDE_DELAY);
});
currentMainButton = mainButton;
currentSubButtons = subButtons;
return container;
}
// 显示按钮容器
function showButtonContainer(link) {
clearTimeout(showTimeout);
clearTimeout(hideTimeout);
showTimeout = setTimeout(() => {
const rect = link.getBoundingClientRect();
const scrollTop = window.scrollY || window.pageYOffset;
const scrollLeft = window.scrollX || window.pageXOffset;
const url = link.href;
const text = link.textContent || '下载资源';
// 创建按钮容器
const container = createButtonContainer(url, text, link);
if (!container) return; // 如果资源类型不在支持列表中,不显示按钮
document.body.appendChild(container);
// 获取容器尺寸
container.style.visibility = 'hidden';
container.style.display = 'block';
const containerRect = container.getBoundingClientRect();
container.style.visibility = 'visible';
container.style.display = '';
// 计算位置 - 在链接上方居中显示
let top = rect.top + scrollTop - containerRect.height - 8;
let left = rect.left + scrollLeft + (rect.width - containerRect.width) / 2;
// 如果上方空间不足,显示在下方
if (top < 0) {
top = rect.bottom + scrollTop + 8;
}
// 调整左右位置防止溢出
if (left < 0) left = 8;
if (left + containerRect.width > window.innerWidth) {
left = window.innerWidth - containerRect.width - 8;
}
container.style.top = `${top}px`;
container.style.left = `${left}px`;
// 显示容器
setTimeout(() => {
container.classList.add('visible');
}, 50);
}, SHOW_DELAY);
}
// 设置链接监听
function setupLinkListeners() {
const selectors = [
'a[href^="magnet:?xt=urn:btih:"]',
'a[href*=".torrent"]',
'a[href^="ed2k:"]',
'a[href^="http://"]',
'a[href^="https://"]',
'a[href^="ftp://"]'
];
const links = document.querySelectorAll(selectors.join(','));
links.forEach(link => {
if (link.dataset.qbaAdded) return;
link.dataset.qbaAdded = 'true';
// 鼠标进入链接
link.addEventListener('mouseenter', () => {
showButtonContainer(link);
});
// 鼠标离开链接
link.addEventListener('mouseleave', () => {
clearTimeout(showTimeout);
hideTimeout = setTimeout(() => {
const container = document.querySelector('.qba-push-container');
if (container) {
container.classList.remove('visible');
setTimeout(() => {
if (container.parentNode) {
container.remove();
}
}, 300);
}
}, HIDE_DELAY);
});
});
}
// 推送到下载器
function pushToDownloader(downloader, url, text, link, clickedButton) {
// 保存原始状态以便恢复
const originalHTML = clickedButton.innerHTML;
const originalClass = clickedButton.className;
// 更新按钮状态为"正在推送"
clickedButton.innerHTML = '⏳ 推送中...';
clickedButton.classList.add('qba-working');
clickedButton.disabled = true;
if (downloader === 'qb') {
pushToQbittorrent(url, text, (success, message) => {
handlePushResult(success, message, 'qBittorrent', clickedButton, originalHTML, originalClass);
});
} else {
pushToAria2(url, (success, message) => {
handlePushResult(success, message, 'Aria2', clickedButton, originalHTML, originalClass);
});
}
}
// 处理推送结果
function handlePushResult(success, message, downloaderName, clickedButton, originalHTML, originalClass) {
// 移除工作状态
clickedButton.classList.remove('qba-working');
clickedButton.disabled = false;
if (success) {
// 推送成功
clickedButton.innerHTML = '✅ 已推送';
clickedButton.classList.add('qba-success');
// 显示成功通知(如果想要油猴脚本在浏览器报告推送通知请去掉注释)
// GM_notification({
// title: '推送成功',
// text: `资源已添加到${downloaderName}下载队列`,
// timeout: 3000
// });
} else {
// 推送失败
clickedButton.innerHTML = '❌ 失败';
clickedButton.classList.add('qba-error');
// 延迟显示错误信息
setTimeout(() => {
alert(`推送到${downloaderName}失败: ${message}`);
// 恢复原始状态(在失败时)
clickedButton.innerHTML = originalHTML;
clickedButton.className = originalClass;
}, 100);
}
// 2秒后移除整个容器
setTimeout(() => {
const container = document.querySelector('.qba-push-container');
if (container) {
container.remove();
}
}, 2000);
}
// 推送到 qBittorrent
function pushToQbittorrent(url, description = '', callback) {
console.log('🔗 正在推送到 qBittorrent:', url);
const loginUrl = QBITTORRENT_URL + 'api/v2/auth/login';
const addUrl = QBITTORRENT_URL + 'api/v2/torrents/add';
// 第一步:登录获取 SID
GM_xmlhttpRequest({
method: 'POST',
url: loginUrl,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Referer': QBITTORRENT_URL
},
data: `username=${encodeURIComponent(QBITTORRENT_USER)}&password=${encodeURIComponent(QBITTORRENT_PASS)}`,
withCredentials: true,
onload: function (response) {
if (response.status === 200 && response.responseText === 'Ok.') {
console.log('✅ qBittorrent 登录成功');
// 第二步:推送任务
GM_xmlhttpRequest({
method: 'POST',
url: addUrl,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Referer': QBITTORRENT_URL
},
data: `urls=${encodeURIComponent(url)}`,
withCredentials: true,
onload: function (res) {
if (res.status === 200) {
console.log('✅ qBittorrent 添加成功');
callback(true, '添加成功');
} else {
console.error('❌ qBittorrent 添加失败:', res.status, res.statusText);
callback(false, `服务器错误: ${res.status} ${res.statusText}`);
}
},
onerror: function (err) {
console.error('❌ qBittorrent 网络错误:', err);
callback(false, `网络错误: ${err.status} ${err.statusText}`);
}
});
} else {
console.error('❌ qBittorrent 登录失败:', response.status, response.responseText);
callback(false, '登录失败,请检查用户名密码或 Web UI 设置');
}
},
onerror: function (err) {
console.error('❌ qBittorrent 登录请求失败:', err);
callback(false, `登录请求失败: ${err.status} ${err.statusText}`);
}
});
}
// 推送到 Aria2
function pushToAria2(url, callback) {
console.log('🔗 正在推送到 Aria2:', url);
// 特殊处理:.torrent文件需要下载后推送
if (url.endsWith('.torrent') || url.includes('.torrent?')) {
console.log('🔗 处理.torrent文件');
return handleTorrentFile(url, callback);
}
const payload = {
jsonrpc: "2.0",
id: "aria2_push_" + Date.now(),
method: "aria2.addUri",
params: [
`token:${ARIA2_SECRET}`,
[url],
{}
]
};
console.log('📦 Aria2 请求负载:', payload);
sendAria2Request(payload, callback);
}
// 处理.torrent文件
function handleTorrentFile(url, callback) {
console.log('⬇️ 下载.torrent文件:', url);
GM_xmlhttpRequest({
method: 'GET',
url: url,
responseType: 'arraybuffer',
onload: function(response) {
if (response.status === 200) {
console.log('✅ .torrent文件下载成功');
const base64Torrent = arrayBufferToBase64(response.response);
const payload = {
jsonrpc: "2.0",
id: "aria2_push_" + Date.now(),
method: "aria2.addTorrent",
params: [
`token:${ARIA2_SECRET}`,
base64Torrent,
[]
]
};
sendAria2Request(payload, callback);
} else {
console.error('❌ 无法获取.torrent文件:', response.status, response.statusText);
callback(false, `无法获取.torrent文件: ${response.status} ${response.statusText}`);
}
},
onerror: function(err) {
console.error('❌ 下载.torrent文件失败:', err);
callback(false, `下载.torrent文件失败: ${err.status} ${err.statusText}`);
}
});
}
// 发送Aria2请求
function sendAria2Request(payload, callback) {
GM_xmlhttpRequest({
method: 'POST',
url: ARIA2_URL,
headers: {
'Content-Type': 'application/json',
},
data: JSON.stringify(payload),
onload: function (response) {
try {
console.log('📨 Aria2 响应:', response.status, response.responseText);
const res = JSON.parse(response.responseText);
if (res.error) {
console.error('❌ Aria2 返回错误:', res.error);
callback(false, `Aria2 错误: ${res.error.message || res.error.code}`);
} else {
console.log('✅ 成功添加到 Aria2');
callback(true, '添加成功');
}
} catch (e) {
console.error('❌ 解析响应失败:', e);
callback(false, '解析响应失败');
}
},
onerror: function (err) {
console.error('❌ 请求失败:', err);
callback(false, `网络错误: ${err.status} ${err.statusText}`);
},
ontimeout: function () {
console.error('❌ 请求超时');
callback(false, '请求超时,请检查Aria2服务状态');
}
});
}
// ArrayBuffer 转 Base64
function arrayBufferToBase64(buffer) {
let binary = '';
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
// 主函数
function main() {
console.log('🚀 qBittorrent & Aria2 推送助手已启动');
setupLinkListeners();
// 动态监听 DOM 变化
const observer = new MutationObserver(mutations => {
mutations.forEach(() => setupLinkListeners());
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// 页面加载后执行
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', main);
} else {
main();
}
})();