wujinjun / Magnet链接一键复制

// ==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();

})();