Greasy Fork

Greasy Fork is available in English.

一键复制当前页磁力链

在网页右上角添加一键复制当前页磁力链的功能,支持Base32和十六进制编码的智能去重

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         一键复制当前页磁力链
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  在网页右上角添加一键复制当前页磁力链的功能,支持Base32和十六进制编码的智能去重
// @author       deepseek
// @match        https://*.nyaa.si/*
// @match        https://share.dmhy.org/*
// @match        https://www.hacg.me/*
// @match        https://www.hacg.icu/wp/*
// @icon         https://img.icons8.com/color/48/000000/magnet.png
// @grant        GM_setClipboard
// @grant        GM_notification
// ==/UserScript==

(function() {
    'use strict';

    // 只在顶级body中插入按钮
    if (window.self !== window.top) {
        return;
    }

    // 创建固定在右上角的复制磁力链按钮
    const copyButton = document.createElement('button');
    copyButton.innerHTML = '复制';
    copyButton.className = 'fixed-copy-button';
    document.body.appendChild(copyButton);

    // 按钮样式
    const style = document.createElement('style');
    style.textContent = `
        .fixed-copy-button {
            position: fixed;
            top: 40px;
            right: 20px;
            padding: 10px 20px;
            background: #ff6b6b;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 14px;
            font-weight: bold;
            z-index: 10000;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
            transition: all 0.3s ease;
        }
        .fixed-copy-button:hover {
            background: #ff5252;
            transform: translateY(-2px);
            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
        }
        .fixed-copy-button:active {
            transform: translateY(0);
        }
        .fixed-copy-button.copied {
            background: #4caf50;
        }
        .message {
            position: fixed;
            top: 90px;
            right: 20px;
            padding: 12px 20px;
            background: rgba(76, 175, 80, 0.9);
            color: white;
            border-radius: 5px;
            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
            transform: translateX(150%);
            transition: transform 0.4s ease-out;
            z-index: 10000;
            font-size: 14px;
            backdrop-filter: blur(8px);
            -webkit-backdrop-filter: blur(8px);
            border: 1px solid rgba(255, 255, 255, 0.2);
        }
        .message.show {
            transform: translateX(0);
        }
        .message.error {
            background: rgba(244, 67, 54, 0.9);
        }
    `;

    if (!document.querySelector('style[data-magnet-copy-style]')) {
        style.setAttribute('data-magnet-copy-style', 'true');
        document.head.appendChild(style);
    }

    // 显示消息通知
    function showMessage(text, isError = false) {
        let message = document.querySelector('.magnet-copy-message');
        if (!message) {
            message = document.createElement('div');
            message.className = 'magnet-copy-message message';
            document.body.appendChild(message);
        }

        message.textContent = text;
        message.classList.remove('error');
        if (isError) {
            message.classList.add('error');
        }
        message.classList.add('show');

        setTimeout(() => {
            message.classList.remove('show');
        }, 3000);
    }

    // Base32 字母表 (RFC 4648)
    const base32Alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
    const base32AlphabetLower = base32Alphabet.toLowerCase();

    // 检测字符串是否为有效的Base32编码
    function isBase32(str) {
        const cleanStr = str.replace(/=/g, '').toUpperCase(); // 移除填充字符并转为大写
        if (cleanStr.length !== 32) return false;

        for (let i = 0; i < cleanStr.length; i++) {
            const char = cleanStr[i];
            if (!base32Alphabet.includes(char)) {
                return false;
            }
        }
        return true;
    }

    // 检测字符串是否为有效的十六进制编码(40字符的BTIH哈希)
    function isHex(str) {
        return /^[0-9a-fA-F]{40}$/.test(str);
    }

    // 十六进制转换为Base32
    function hexToBase32(hex) {
        hex = hex.toLowerCase().replace(/^0x/, '');

        if (!isHex(hex)) {
            throw new Error('无效的十六进制字符串');
        }

        // Base32编码实现
        const chars = base32Alphabet;
        let bits = 0;
        let value = 0;
        let output = '';

        for (let i = 0; i < hex.length; i += 2) {
            value = (value << 8) | parseInt(hex.substr(i, 2), 16);
            bits += 8;

            while (bits >= 5) {
                output += chars[(value >>> (bits - 5)) & 0x1F];
                bits -= 5;
            }
        }

        if (bits > 0) {
            output += chars[(value << (5 - bits)) & 0x1F];
        }

        // 确保输出为32字符(BTIH标准)
        return output.substring(0, 32);
    }

    // Base32转换为十六进制(备用功能)
    function base32ToHex(base32) {
        base32 = base32.toUpperCase().replace(/=/g, '');
        if (!isBase32(base32)) {
            throw new Error('无效的Base32字符串');
        }

        let bits = 0;
        let value = 0;
        let output = '';

        for (let i = 0; i < base32.length; i++) {
            value = (value << 5) | base32Alphabet.indexOf(base32[i]);
            bits += 5;

            if (bits >= 8) {
                output += ((value >>> (bits - 8)) & 0xFF).toString(16).padStart(2, '0');
                bits -= 8;
            }
        }

        return output;
    }

    // 从磁力链接中提取信息哈希
    function extractInfoHash(magnetLink) {
        // 处理完整磁力链接
        const magnetMatch = magnetLink.match(/magnet:\?.*xt=urn:btih:([^&]+)/i);
        if (magnetMatch && magnetMatch[1]) {
            return magnetMatch[1].toUpperCase();
        }

        // 处理纯哈希值(可能是base32或hex)
        if (isBase32(magnetLink) || isHex(magnetLink)) {
            return magnetLink.toUpperCase();
        }

        return null;
    }

    // 检测文本是否为纯哈希值并转换为完整磁力链接
    function normalizeToMagnetLink(text) {
        text = text.trim();

        // 如果已经是完整磁力链接,直接返回
        if (text.startsWith('magnet:')) {
            return text;
        }

        // 检测是否为有效的哈希值
        if (isBase32(text)) {
            return `magnet:?xt=urn:btih:${text.toUpperCase()}`;
        }

        if (isHex(text)) {
            return `magnet:?xt=urn:btih:${text.toLowerCase()}`;
        }

        return null;
    }

    // 在文本内容中查找可能的哈希值
    function findHashInText(text) {
        const hashes = [];

        // 匹配40字符的十六进制(可能被空格或其他字符分隔)
        const hexMatches = text.match(/[0-9a-fA-F]{40}/g);
        if (hexMatches) {
            hexMatches.forEach(match => {
                if (isHex(match)) {
                    hashes.push(match);
                }
            });
        }

        // 匹配32字符的Base32(可能被空格或其他字符分隔)
        const base32Matches = text.match(/[A-Z2-7]{32}/g);
        if (base32Matches) {
            base32Matches.forEach(match => {
                if (isBase32(match)) {
                    hashes.push(match);
                }
            });
        }

        return hashes;
    }

    // 智能去重处理
    function deduplicateMagnetLinks(magnetLinks) {
        const hashMap = new Map(); // 存储Base32哈希 -> 磁力链接
        const result = [];

        for (const link of magnetLinks) {
            const infoHash = extractInfoHash(link);
            if (!infoHash) {
                // 无法提取哈希的链接直接保留
                result.push(link);
                continue;
            }

            let base32Hash;
            if (isHex(infoHash)) {
                try {
                    base32Hash = hexToBase32(infoHash);
                } catch (e) {
                    // 转换失败,保留原链接
                    result.push(link);
                    continue;
                }
            } else if (isBase32(infoHash)) {
                base32Hash = infoHash.toUpperCase();
            } else {
                // 无效哈希格式,保留原链接
                result.push(link);
                continue;
            }

            // 检查是否已存在相同哈希的链接
            if (hashMap.has(base32Hash)) {
                const existingLink = hashMap.get(base32Hash);
                const existingHash = extractInfoHash(existingLink);

                // 优先保留Base32编码的链接
                if (isHex(infoHash) && isBase32(existingHash)) {
                    // 当前是hex,已存在的是base32,跳过当前
                    continue;
                } else if (isBase32(infoHash) && isHex(existingHash)) {
                    // 当前是base32,已存在的是hex,替换为base32
                    hashMap.set(base32Hash, link);
                    // 从结果中移除旧的hex链接,添加新的base32链接
                    const index = result.indexOf(existingLink);
                    if (index > -1) {
                        result.splice(index, 1);
                    }
                    result.push(link);
                } else {
                    // 相同编码格式,去重
                    continue;
                }
            } else {
                // 新哈希,添加到映射和结果中
                hashMap.set(base32Hash, link);
                result.push(link);
            }
        }

        return result;
    }

    // 复制磁力链函数
    function copyMagnetLinks() {
        const magnetLinks = [];

        // 改进的选择器,兼容更多磁力链接格式
        const magnetSelectors = [
            'a[href^="magnet:?"]',
            'a[href^="magnet:?xt="]',
            'a[href*="magnet:"]',
            '.magnet-link',
            '[href*="magnet:?xt="]',
            'a[href*="btih:"]'
        ];

        // 1. 首先收集所有完整的磁力链接
        magnetSelectors.forEach(selector => {
            const foundLinks = document.querySelectorAll(selector);
            foundLinks.forEach(link => {
                if (link.href && link.href.startsWith('magnet:')) {
                    magnetLinks.push(link.href);
                }
            });
        });

        // 2. 查找页面中的纯文本哈希值
        const textElements = document.querySelectorAll('code, pre, .hash, .magnet, .torrent-hash, [class*="hash"], [class*="magnet"]');
        const textHashes = new Set();

        textElements.forEach(element => {
            const text = element.textContent.trim();

            // 尝试直接转换文本为磁力链接
            const magnetLink = normalizeToMagnetLink(text);
            if (magnetLink) {
                textHashes.add(magnetLink);
            } else {
                // 在文本内容中查找可能的哈希值
                const foundHashes = findHashInText(text);
                foundHashes.forEach(hash => {
                    const magnetLink = normalizeToMagnetLink(hash);
                    if (magnetLink) {
                        textHashes.add(magnetLink);
                    }
                });
            }
        });

        // 3. 如果没有找到任何链接,尝试在整个页面正文中搜索
        if (magnetLinks.length === 0 && textHashes.size === 0) {
            const bodyText = document.body.textContent;
            const foundHashes = findHashInText(bodyText);
            foundHashes.forEach(hash => {
                const magnetLink = normalizeToMagnetLink(hash);
                if (magnetLink) {
                    textHashes.add(magnetLink);
                }
            });
        }

        // 合并所有链接
        const allLinks = [...magnetLinks, ...textHashes];

        if (allLinks.length === 0) {
            showMessage('未找到磁力链接!', true);
            return;
        }

        // 基础去重(完全相同的链接)
        const uniqueLinks = [...new Set(allLinks)];

        // 智能去重处理
        const deduplicatedLinks = deduplicateMagnetLinks(uniqueLinks);

        // 统计信息
        const hexCount = uniqueLinks.filter(link => {
            const hash = extractInfoHash(link);
            return hash && isHex(hash);
        }).length;

        const base32Count = uniqueLinks.filter(link => {
            const hash = extractInfoHash(link);
            return hash && isBase32(hash);
        }).length;

        const textHashCount = textHashes.size;

        // 复制到剪贴板
        const textToCopy = deduplicatedLinks.join('\n');
        GM_setClipboard(textToCopy);

        // 更新按钮状态
        copyButton.classList.add('copied');
        const originalText = copyButton.textContent;

        let message = `已复制 ${deduplicatedLinks.length} 个磁力链接`;

        showMessage(message);

        // 恢复按钮状态
        setTimeout(() => {
            copyButton.classList.remove('copied');
            copyButton.textContent = originalText;
        }, 3000);

        // 在控制台输出详细信息(用于调试)
        console.log('原始链接数量:', uniqueLinks.length);
        console.log('去重后链接数量:', deduplicatedLinks.length);
        console.log('十六进制编码链接:', hexCount);
        console.log('Base32编码链接:', base32Count);
        console.log('从文本中提取的哈希:', textHashCount);
    }

    // 绑定点击事件(带防抖)
    let isProcessing = false;
    copyButton.addEventListener('click', function() {
        if (isProcessing) return;
        isProcessing = true;

        copyMagnetLinks();

        setTimeout(() => {
            isProcessing = false;
        }, 1000);
    });
})();