wujinjun / 移花宫(磁力搜索器)添加磁力链接按钮和复制按钮

// ==UserScript==
// @name         移花宫(磁力搜索器)添加磁力链接按钮和复制按钮
// @namespace    http://tampermonkey.net/
// @version      1.0.1
// @description  "恢复"旧版网站的"[磁力链接]"按钮。从搜索结果提取 Hash、资源名称和资源大小,自动生成标准磁力链接按钮和复制按钮
// @author       wujinjun
// @license      MIT
// @match        *://yhg007.com/search-*.html
// @match        *://yhg007.com/search-*.html?*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=yhg007.com
// @grant        GM_setClipboard
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function () {
    'use strict';

    // 默认使用的 Tracker 列表(包含 opentracker 等)
    const DEFAULT_TRACKERS = [
        "https://opentracker.i2p.rocks/announce",
        "udp://tracker.opentrackr.org:1337/announce",
        "udp://open.demonii.com:1337/announce",
        "udp://tracker.openbittorrent.com:80/announce",
        "udp://exodus.desync.com:6969/announce"
    ];

    /**
     * 获取用户设置的 Tracker 列表
     */
    function getTrackers() {
        const saved = GM_getValue('custom_trackers', null);
        if (saved) {
            return saved.split('\n').map(t => t.trim()).filter(t => t.length > 0);
        }
        return DEFAULT_TRACKERS;
    }

    /**
     * 注册油猴菜单,方便随时修改 Tracker 列表
     */
    GM_registerMenuCommand("⚙️ 配置 Tracker 列表", () => {
        const currentTrackers = getTrackers().join('\n');
        const input = prompt("请输入 Tracker 列表 (每行一个 tracker 地址):", currentTrackers);
        if (input !== null) {
            GM_setValue('custom_trackers', input.trim());
            alert("Tracker 配置已更新!页面刷新后或加载新内容时生效。");
            // 清除已添加标记,重新刷新当前页面的链接
            document.querySelectorAll('[data-magnet-added]').forEach(el => {
                delete el.dataset.magnetAdded;
                const oldBtn = el.querySelector('.magnet-btn-container');
                if (oldBtn) oldBtn.remove();
            });
            processArticles();
        }
    });

    /**
     * 将带有单位的大小字符串转换为字节数 (Byte)
     * 支持 B, KB, MB, GB, TB
     */
    function parseSizeToBytes(sizeStr) {
        if (!sizeStr) return 0;
        
        // 匹配数字与单位(忽略大小写)
        const match = sizeStr.match(/([\d.]+)\s*([a-zA-Z]+)/);
        if (!match) return 0;

        const val = parseFloat(match[1]);
        const unit = match[2].toUpperCase();

        const units = {
            'B': 1,
            'KB': 1024,
            'MB': 1024 * 1024,
            'GB': 1024 * 1024 * 1024,
            'TB': 1024 * 1024 * 1024 * 1024
        };

        return Math.round(val * (units[unit] || 1));
    }

    /**
     * 解析每一个 article 节点并添加磁力链接
     */
    function processArticles() {
        const articles = document.querySelectorAll('#main-content > div > article');
        const trackers = getTrackers();

        articles.forEach(article => {
            // 防止重复处理
            if (article.dataset.magnetAdded) return;

            // 1. 提取 Hash 与 资源名称
            const nameEl = article.querySelector('.zsky-result-body > a');
            if (!nameEl) return;

            const href = nameEl.getAttribute('href') || '';
            const hashMatch = href.match(/([a-fA-F0-9]{40})/); // 匹配 40 位 hex hash
            if (!hashMatch) return;

            const hash = hashMatch[1];
            const rawName = nameEl.textContent.trim();
            const encodedName = encodeURIComponent(rawName);

            // 2. 提取并转换资源大小
            let bytes = 0;
            const metaSpans = article.querySelectorAll('.zsky-result-meta > span');
            metaSpans.forEach(span => {
                const text = span.textContent.trim();
                if (text.includes('大小:') || text.includes('大小:')) {
                    const sizeStr = text.replace(/大小[::]\s*/, '');
                    bytes = parseSizeToBytes(sizeStr);
                }
            });

            // 3. 构造完整磁力链接
            let magnetUrl = `magnet:?xt=urn:btih:${hash}&dn=${encodedName}`;
            if (bytes > 0) {
                magnetUrl += `&xl=${bytes}`;
            }

            // 拼接 Tracker 节点
            trackers.forEach(tr => {
                magnetUrl += `&tr=${encodeURIComponent(tr)}`;
            });

            // 4. 在元素末尾追加 UI 按钮与链接
            const btnContainer = document.createElement('div');
            btnContainer.className = 'magnet-btn-container';
            btnContainer.style.cssText = 'margin-top: 6px; display: flex; gap: 10px; align-items: center;';

            // 磁力超链接
            const magnetLink = document.createElement('a');
            magnetLink.href = magnetUrl;
            // magnetLink.textContent = '🧲 磁力下载';
            magnetLink.textContent = '[磁力链接]';
            magnetLink.style.cssText = 'color: #1e88e5; text-decoration: none; font-weight: bold; font-size: 13px;';
            
            // 一键复制按钮
            const copyBtn = document.createElement('button');
            copyBtn.textContent = '复制磁力链接';
            copyBtn.style.cssText = 'padding: 2px 8px; font-size: 12px; border: 1px solid #ccc; background: #f5f5f5; border-radius: 4px; cursor: pointer;';
            
            copyBtn.addEventListener('click', (e) => {
                e.preventDefault();
                GM_setClipboard(magnetUrl);
                copyBtn.textContent = '已复制!';
                copyBtn.style.background = '#e8f5e9';
                setTimeout(() => {
                    copyBtn.textContent = '复制磁力链接';
                    copyBtn.style.background = '#f5f5f5';
                }, 1500);
            });

            btnContainer.appendChild(magnetLink);
            btnContainer.appendChild(copyBtn);

            const bodyDiv = article.querySelector('.zsky-result-body') || article;
            bodyDiv.appendChild(btnContainer);

            // 标记已处理
            article.dataset.magnetAdded = 'true';
        });
    }

    // 初始化执行
    processArticles();

    // 使用 MutationObserver 监听动态加载或翻页加载的内容
    const observer = new MutationObserver(() => {
        processArticles();
    });

    observer.observe(document.body, { childList: true, subtree: true });
})();