NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Magnet链接一键复制
// @namespace http://tampermonkey.net/
// @version 1.4
// @description 写了一个油猴脚本,实现 Magnet 链接一键复制...就放这边吧。(支持一键复制和发送到种子下载器)
// @author wujinjun
// @license MIT
// @match *://*/*
// @run-at document-end
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @connect localhost
// ==/UserScript==
(function () {
'use strict';
// ==========================
// 可配置模式
// "copy" → 一键复制
// "bitcomet" → 添加到 BitComet
// "qbittorrent" → 添加到 qBittorrent
// "biglybt" → 添加到 BiglyBT
// ==========================
const mode = "copy"; // ← 你可以改成 "bitcomet" / "qbittorrent" 等
// 保存路径
const save_path = "/home/ubuntu/Downloads";
// BitComet API 配置
const bitcomet_url = "http://localhost:8081/panel/task_add_magnet_result"; // 你的 BC 地址
const bitcomet_username = "admin";
const bitcomet_password = "123456";
// qBittorrent API 配置
const qb_url = "http://localhost:8080"; // 你的 qB 地址
const qb_username = "admin";
const qb_password = "adminadmin";
// BiglyBT API 配置 (默认端口 6906)
const biglybt_url = "http://localhost:6906";
const biglybt_apikey = "YOUR_API_KEY_HERE"; // 替换为你的 API Key
// ==========================
// 调试配置
// ==========================
const debug_request = true; // 开启后在控制台打印请求详情
// ==========================
// 调试辅助
// ==========================
function logDebug(title, res) {
if (!debug_request) return;
console.group(`%c[Magnet Debug] ${title}`, "color: #4f46e5; font-weight: bold;");
console.log("Status:", res.status);
console.log("Status Text:", res.statusText);
console.log("Response Headers:", res.responseHeaders);
console.log("Response:", res.responseText);
console.groupEnd();
}
// ==========================
// 样式
// ==========================
const style = `
.magnet-copy-btn {
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: 8px;
padding: 2px 8px;
font-size: 12px;
line-height: 1;
border-radius: 4px;
border: none;
cursor: pointer;
color: #fff;
background: linear-gradient(135deg, #4f46e5, #06b6d4);
background-size: 200% 200%;
animation: magnet-gradient 3s ease infinite;
transition: background 0.2s ease, transform 0.1s ease, opacity 0.2s ease;
}
.magnet-copy-btn:hover {
background: linear-gradient(135deg, #22c55e, #0ea5e9);
transform: translateY(-1px);
}
.magnet-copy-btn:active {
transform: translateY(0);
opacity: 0.9;
}
.magnet-copy-btn span.magnet-copy-text {
margin-left: 4px;
}
.magnet-copy-btn .magnet-icon {
font-size: 12px;
}
.magnet-copy-btn.magnet-success {
background: #16a34a;
animation: none;
}
.magnet-copy-btn.magnet-fail {
background: #dc2626;
animation: none;
}
@keyframes magnet-gradient {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
`;
if (typeof GM_addStyle === 'function') GM_addStyle(style);
else document.head.appendChild(Object.assign(document.createElement("style"), {textContent: style}));
// ==========================
// 复制逻辑
// ==========================
async function copyText(text) {
// 优先 Clipboard API
if (navigator.clipboard && navigator.clipboard.writeText) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (e) {
// 继续尝试降级方案
}
}
// 降级 execCommand
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.top = '-9999px';
// textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
textarea.setSelectionRange(0, textarea.value.length);
let ok = false;
try {
ok = document.execCommand('copy');
} catch (e) {
ok = false;
}
document.body.removeChild(textarea);
return ok;
}
// ==========================
// BitComet API 调用
// ==========================
function sendToBitComet(magnet) {
return new Promise((resolve) => {
const form = new FormData();
form.append("url", magnet);
if (save_path !== "") form.append("save_path", save_path);
// Basic Auth
let headers = {};
if (bitcomet_username || bitcomet_password) {
const token = btoa(`${bitcomet_username}:${bitcomet_password}`);
headers["Authorization"] = `Basic ${token}`;
}
headers["Content-Type"] = `application/x-www-form-urlencoded`;
// GM_xmlhttpRequest 不受混合内容限制
GM_xmlhttpRequest({
method: "POST",
url: bitcomet_url, // 可以是 http:// 或 https://
headers,
data: "url=" + encodeURIComponent(magnet) + "&save_path=" + encodeURIComponent(save_path),
fetch: true,
onload: function (res) {
logDebug("BitComet Response", res);
console.log("[Magnet链接一键复制] BitComet 请求结果: " + res.responseText);
if (res.status === 200 && res.responseText) {
const ok =
res.responseText.includes("Add task succeed")
resolve(ok);
} else {
resolve(false);
}
},
onerror: function () {
resolve(false);
}
});
});
}
// ==========================
// qBittorrent API 调用
// ==========================
async function sendToQBittorrent(magnet) {
// 1. 尝试登录 (qB 必须先登录获取 SID)
const login = () => new Promise((resolve) => {
GM_xmlhttpRequest({
method: "POST",
url: `${qb_url}/api/v2/auth/login`,
headers: { "Referer": qb_url, "Content-Type": "application/x-www-form-urlencoded", "Origin": qb_url },
data: `username=${encodeURIComponent(qb_username)}&password=${encodeURIComponent(qb_password)}`,
onload: (res) => {
logDebug("qBittorrent Login", res);
resolve(res.status === 200 || res.status === 204)
},
onerror: (res) => {
logDebug("qBittorrent Login", res);
resolve(false)
}
});
});
// 2. 提交磁力链接
const addTorrent = () => new Promise((resolve) => {
// qB 的 add 接口使用 FormData
const formData = `urls=${encodeURIComponent(magnet)}`;
GM_xmlhttpRequest({
method: "POST",
url: `${qb_url}/api/v2/torrents/add`,
headers: { "Referer": qb_url, "Content-Type": "application/x-www-form-urlencoded", "Origin": qb_url },
data: formData,
onload: (res) => {
logDebug("qBittorrent Response", res);
// 200 OK 表示提交成功
resolve(res.status === 200);
},
onerror: (res) => {
logDebug("qBittorrent Response", res);
resolve(false);
}
});
});
// 执行流程
const loggedIn = await login();
if (!loggedIn) return false;
return await addTorrent();
}
// ==========================
// BiglyBT API 调用
// ==========================
function sendToBiglyBT(magnet) {
return new Promise((resolve) => {
// 构建 URL 参数
const params = new URLSearchParams({
apikey: biglybt_apikey,
method: "adddownload",
magnet: magnet
});
GM_xmlhttpRequest({
method: "GET",
url: `${biglybt_url}?${params.toString()}`,
onload: function (res) {
logDebug("BiglyBT Response", res);
// BiglyBT 通常返回 JSON,包含 result: "ok" 或类似信息
// 这里简单通过状态码 200 判断,也可以根据具体返回内容微调
resolve(res.status === 200);
},
onerror: function () {
resolve(false);
}
});
});
}
// ==========================
// 按钮反馈
// ==========================
function setButtonState(btn, success) {
const originalText = btn.dataset.originalText || '复制';
const originalIcon = btn.dataset.originalIcon || '⧉';
if (!btn.dataset.originalText) {
const textSpan = btn.querySelector('.magnet-copy-text');
const iconSpan = btn.querySelector('.magnet-icon');
btn.dataset.originalText = textSpan ? textSpan.textContent : '复制';
btn.dataset.originalIcon = iconSpan ? iconSpan.textContent : '⧉';
}
const textSpan = btn.querySelector('.magnet-copy-text');
const iconSpan = btn.querySelector('.magnet-icon');
if (success) {
btn.classList.remove('magnet-fail');
btn.classList.add('magnet-success');
if (iconSpan) iconSpan.textContent = '✓';
if (textSpan) textSpan.textContent = '成功';
} else {
btn.classList.remove('magnet-success');
btn.classList.add('magnet-fail');
if (iconSpan) iconSpan.textContent = '✕';
if (textSpan) textSpan.textContent = '失败';
}
setTimeout(() => {
btn.classList.remove('magnet-success', 'magnet-fail');
if (iconSpan) iconSpan.textContent = btn.dataset.originalIcon || originalIcon;
if (textSpan) textSpan.textContent = btn.dataset.originalText || originalText;
}, 1500);
}
// ==========================
// 为单个磁力链接添加按钮
// ==========================
function addButtonForLink(a) {
if (!a || a.dataset.magnetCopyAttached === '1') return;
if (!a.href || !a.href.startsWith('magnet:')) return;
a.dataset.magnetCopyAttached = '1';
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'magnet-copy-btn';
const labelMap = {
"copy": "复制",
"bitcomet": "添加到BC",
"qbittorrent": "添加到qB",
"biglybt": "添加到BiglyBT"
};
const label = labelMap[mode] || "复制";
btn.innerHTML = `<span class="magnet-icon">⧉</span><span class="magnet-copy-text">${label}</span>`;
btn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
const magnet = a.href;
let ok = false;
if (mode === "copy") {
ok = await copyText(magnet);
} else if (mode === "bitcomet") {
ok = await sendToBitComet(magnet);
} else if (mode === "qbittorrent") {
ok = await sendToQBittorrent(magnet);
} else if (mode === "biglybt") {
ok = await sendToBiglyBT(magnet);
}
setButtonState(btn, ok);
});
a.insertAdjacentElement("afterend", btn);
}
// ==========================
// 扫描所有磁力链接
// ==========================
function scanMagnetLinks(root = document) {
const links = root.querySelectorAll('a[href^="magnet:"]');
links.forEach(addButtonForLink);
}
// ==========================
// MutationObserver
// ==========================
function setupObserver() {
const observer = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.type === 'childList') {
m.addedNodes.forEach((node) => {
if (node.nodeType !== 1) return; // ELEMENT_NODE
// 如果新增节点本身是链接
if (node.matches && node.matches('a[href^="magnet:"]')) {
addButtonForLink(node);
}
// 或者包含链接
scanMagnetLinks(node);
});
}
else if (m.type === 'attributes') {
if (
m.target &&
m.target.nodeType === 1 &&
m.target.matches &&
m.target.matches('a[href^="magnet:"]')
) {
addButtonForLink(m.target);
}
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['href'],
});
}
// ==========================
// 初始化
// ==========================
function init() {
scanMagnetLinks(document);
setupObserver();
}
if (document.readyState === 'loading')
document.addEventListener('DOMContentLoaded', init);
else
init();
})();