Greasy Fork is available in English.

自动转换 ed2k 和磁力链接并一键复制

自动检测网页中的所有 ed2k 和磁力链接并将其转换为可点击的超链接,同时提供一键复制所有链接的功能,并支持排除指定网址。

// ==UserScript==
// @name         自动转换 ed2k 和磁力链接并一键复制
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  自动检测网页中的所有 ed2k 和磁力链接并将其转换为可点击的超链接,同时提供一键复制所有链接的功能,并支持排除指定网址。
// @author       98-liu**
// @match        *://*/*
// @grant        GM_setClipboard
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

(function() {
    'use strict';

    const EXCLUDE_KEY = 'excludedUrls';
    const COPY_BUTTON_POSITION_KEY = 'copyButtonPosition';
    const CONFIG_BUTTON_POSITION_KEY = 'configButtonPosition';
    const TOGGLE_BUTTONS_POSITION_KEY = 'toggleButtonsPosition';
    const COPY_BUTTON_VISIBLE_KEY = 'copyButtonVisible';
    const CONFIG_BUTTON_VISIBLE_KEY = 'configButtonVisible';

    // 检查当前页面是否在排除列表中
    function isExcluded(url) {
        const excludedUrls = GM_getValue(EXCLUDE_KEY, []);
        for (const pattern of excludedUrls) {
            if (new RegExp(pattern).test(url)) {
                return true;
            }
        }
        return false;
    }

    // 自动检测并转换 ed2k 和磁力链接为超链接
    function convertLinks() {
        if (isExcluded(window.location.href)) return;

        // 正则表达式匹配 ed2k 链接
        const ed2kRegex = /ed2k:\/\/\|file\|[^\|]+\|\d+\|[a-fA-F0-9]+\|\//g;
        // 正则表达式匹配磁力链接
        const magnetRegex = /magnet:\?xt=urn:[a-zA-Z0-9:.&=]+/g;

        // 遍历网页中的所有文本节点
        const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
        let node;

        while (node = walker.nextNode()) {
            if (node.nodeValue.match(ed2kRegex) || node.nodeValue.match(magnetRegex)) {
                const parent = node.parentNode;

                // 避免重复处理(如果已经是超链接则跳过)
                if (parent.tagName !== 'A') {
                    let newHTML = node.nodeValue;
                    newHTML = newHTML.replace(ed2kRegex, '<a href="$&" target="_blank">$&</a>');
                    newHTML = newHTML.replace(magnetRegex, '<a href="$&" target="_blank">$&</a>');

                    const temp = document.createElement('div');
                    temp.innerHTML = newHTML;

                    // 将新内容插入到 DOM 中
                    while (temp.firstChild) {
                        parent.insertBefore(temp.firstChild, node);
                    }

                    // 移除原始文本节点
                    parent.removeChild(node);
                }
            }
        }
    }

    // 获取网页中的所有 ed2k 和磁力链接,包括现有的超链接
    function getAllLinks() {
        const ed2kRegex = /ed2k:\/\/\|file\|[^\|]+\|\d+\|[a-fA-F0-9]+\|\//g;
        const magnetRegex = /magnet:\?xt=urn:[a-zA-Z0-9:.&=]+/g;

        const links = [];

        // 查找所有现有的 ed2k 和磁力超链接
        document.querySelectorAll('a').forEach(anchor => {
            if (anchor.href.match(ed2kRegex) || anchor.href.match(magnetRegex)) {
                links.push(anchor.href);
            }
        });

        // 查找所有文本节点中的 ed2k 和磁力链接
        const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
        let node;

        while (node = walker.nextNode()) {
            const matches = node.nodeValue.match(ed2kRegex) || [];
            links.push(...matches);

            const magnetMatches = node.nodeValue.match(magnetRegex) || [];
            links.push(...magnetMatches);
        }

        return links.join('\n');
    }

    // 创建一键复制按钮
    function createCopyButton() {
        const button = document.createElement('button');
        button.id = 'copyAllLinksButton';
        button.textContent = '复制所有链接';
        button.style.position = 'fixed';
        button.style.zIndex = '10000';
        button.style.padding = '10px';
        button.style.backgroundColor = '#007bff';
        button.style.color = '#fff';
        button.style.border = 'none';
        button.style.borderRadius = '5px';
        button.style.cursor = 'pointer';
        button.style.width = '120px'; // 固定宽度
        button.style.height = '40px'; // 固定高度

        // 设置初始位置或恢复保存的位置
        const position = GM_getValue(COPY_BUTTON_POSITION_KEY, { top: '10px', left: '10px' });
        button.style.top = position.top;
        button.style.left = position.left;

        // 点击按钮时复制所有链接
        button.addEventListener('click', () => {
            const links = getAllLinks();
            if (links) {
                GM_setClipboard(links);
                alert('已复制所有链接到剪贴板!');
            } else {
                alert('未找到链接!');
            }
        });

        makeDraggable(button);

        document.body.appendChild(button);
    }

    // 保存排除网址列表
    function saveExcludedUrls(urls) {
        GM_setValue(EXCLUDE_KEY, urls);
    }

    // 打开配置面板
    function openConfigPanel() {
        const panel = document.createElement('div');
        panel.style.position = 'fixed';
        panel.style.top = '10px';
        panel.style.left = '10px';
        panel.style.width = '300px';
        panel.style.height = '200px';
        panel.style.backgroundColor = '#fff';
        panel.style.border = '1px solid #ccc';
        panel.style.padding = '10px';
        panel.style.zIndex = '1001';
        panel.style.boxShadow = '2px 2px 10px rgba(0, 0, 0, 0.1)';
        document.body.appendChild(panel);

        const title = document.createElement('h3');
        title.textContent = '排除网址配置';
        panel.appendChild(title);

        const list = document.createElement('ul');
        list.style.maxHeight = '100px';
        list.style.overflowY = 'auto';
        panel.appendChild(list);

        const input = document.createElement('input');
        input.type = 'text';
        input.placeholder = '输入网址模式(正则表达式)';
        input.style.width = 'calc(100% - 22px)';
        input.style.marginBottom = '5px';
        panel.appendChild(input);

        const addButton = document.createElement('button');
        addButton.textContent = '添加';
        addButton.addEventListener('click', () => {
            const value = input.value.trim();
            if (value) {
                const excludedUrls = GM_getValue(EXCLUDE_KEY, []);
                excludedUrls.push(value);
                saveExcludedUrls(excludedUrls);
                updateList();
                input.value = '';
            }
        });
        panel.appendChild(addButton);

        const closeButton = document.createElement('button');
        closeButton.textContent = '关闭';
        closeButton.style.float = 'right';
        closeButton.addEventListener('click', () => {
            document.body.removeChild(panel);
        });
        panel.appendChild(closeButton);

        updateList();

        function updateList() {
            list.innerHTML = '';
            const excludedUrls = GM_getValue(EXCLUDE_KEY, []);
            excludedUrls.forEach((pattern, index) => {
                const li = document.createElement('li');
                li.textContent = pattern;
                const removeButton = document.createElement('button');
                removeButton.textContent = '删除';
                removeButton.style.float = 'right';
                removeButton.addEventListener('click', () => {
                    excludedUrls.splice(index, 1);
                    saveExcludedUrls(excludedUrls);
                    updateList();
                });
                li.appendChild(removeButton);
                list.appendChild(li);
            });
        }
    }

    // 创建配置按钮
    function createConfigButton() {
        const button = document.createElement('button');
        button.id = 'configExcludeButton';
        button.textContent = '配置排除网址';
        button.style.position = 'fixed';
        button.style.zIndex = '10000';
        button.style.padding = '10px';
        button.style.backgroundColor = '#ffc107';
        button.style.color = '#fff';
        button.style.border = 'none';
        button.style.borderRadius = '5px';
        button.style.cursor = 'pointer';
        button.style.width = '120px'; // 固定宽度
        button.style.height = '40px'; // 固定高度

        // 设置初始位置或恢复保存的位置
        const position = GM_getValue(CONFIG_BUTTON_POSITION_KEY, { top: '60px', left: '10px' });
        button.style.top = position.top;
        button.style.left = position.left;

        // 点击按钮时打开配置面板
        button.addEventListener('click', openConfigPanel);

        makeDraggable(button);

        document.body.appendChild(button);
    }

    // 创建两个切换按钮
    function createToggleButtons() {
        const container = document.createElement('div');
        container.style.position = 'fixed';
        container.style.zIndex = '10000';
        container.style.display = 'flex';
        container.style.flexDirection = 'row';
        container.style.alignItems = 'center';

        // 设置初始位置或恢复保存的位置
        const position = GM_getValue(TOGGLE_BUTTONS_POSITION_KEY, { top: '110px', left: '10px' });
        container.style.top = position.top;
        container.style.left = position.left;

        // 切换复制所有链接按钮
        const toggleCopyButton = document.createElement('button');
        toggleCopyButton.id = 'toggleCopyButton';
        toggleCopyButton.textContent = '隐藏复制按钮';
        toggleCopyButton.style.padding = '10px';
        toggleCopyButton.style.backgroundColor = '#dc3545';
        toggleCopyButton.style.color = '#fff';
        toggleCopyButton.style.border = 'none';
        toggleCopyButton.style.borderRadius = '5px 0 0 5px';
        toggleCopyButton.style.cursor = 'pointer';
        toggleCopyButton.style.width = '80px'; // 固定宽度
        toggleCopyButton.style.height = '50px'; // 固定高度

        toggleCopyButton.addEventListener('click', () => {
            toggleCopyButtonVisibility();
        });

        // 切换配置排除网址按钮
        const toggleConfigButton = document.createElement('button');
        toggleConfigButton.id = 'toggleConfigButton';
        toggleConfigButton.textContent = '隐藏配置网址按钮';
        toggleConfigButton.style.padding = '10px';
        toggleConfigButton.style.backgroundColor = '#28a745';
        toggleConfigButton.style.color = '#fff';
        toggleConfigButton.style.border = 'none';
        toggleConfigButton.style.borderRadius = '0 5px 5px 0';
        toggleConfigButton.style.cursor = 'pointer';
        toggleConfigButton.style.width = '80px'; // 固定宽度
        toggleConfigButton.style.height = '50px'; // 固定高度

        toggleConfigButton.addEventListener('click', () => {
            toggleConfigButtonVisibility();
        });

        // 使容器可拖动
        makeDraggable(container);

        container.appendChild(toggleCopyButton);
        container.appendChild(toggleConfigButton);
        document.body.appendChild(container);
    }

    // 使元素可拖动
    function makeDraggable(element) {
        let offsetX, offsetY;
        let isDragging = false;

        element.addEventListener('mousedown', (e) => {
            if (e.target === element || e.target.parentElement === element) {
                offsetX = e.clientX - element.offsetLeft;
                offsetY = e.clientY - element.offsetTop;
                isDragging = true;
                document.addEventListener('mousemove', mouseMoveHandler);
                document.addEventListener('mouseup', mouseUpHandler);
            }
        });

        function mouseMoveHandler(e) {
            if (!isDragging) return;

            const maxX = window.innerWidth - element.offsetWidth;
            const maxY = window.innerHeight - element.offsetHeight;
            element.style.left = `${Math.min(maxX, Math.max(0, e.clientX - offsetX))}px`;
            element.style.top = `${Math.min(maxY, Math.max(0, e.clientY - offsetY))}px`;

            // 更新位置信息
            if (element.id === 'toggleButtonsContainer') {
                GM_setValue(TOGGLE_BUTTONS_POSITION_KEY, { top: element.style.top, left: element.style.left });
            }
        }

        function mouseUpHandler() {
            isDragging = false;
            document.removeEventListener('mousemove', mouseMoveHandler);
            document.removeEventListener('mouseup', mouseUpHandler);
        }
    }

    // 切换复制所有链接按钮的显示状态
    function toggleCopyButtonVisibility() {
        const copyButton = document.getElementById('copyAllLinksButton');
        const toggleCopyButton = document.getElementById('toggleCopyButton');

        const isVisible = !GM_getValue(COPY_BUTTON_VISIBLE_KEY, true);

        if (isVisible) {
            copyButton.style.display = 'block';
            toggleCopyButton.textContent = '隐藏复制按钮';
        } else {
            copyButton.style.display = 'none';
            toggleCopyButton.textContent = '显示复制按钮';
        }

        GM_setValue(COPY_BUTTON_VISIBLE_KEY, isVisible);
    }

    // 切换配置排除网址按钮的显示状态
    function toggleConfigButtonVisibility() {
        const configButton = document.getElementById('configExcludeButton');
        const toggleConfigButton = document.getElementById('toggleConfigButton');

        const isVisible = !GM_getValue(CONFIG_BUTTON_VISIBLE_KEY, true);

        if (isVisible) {
            configButton.style.display = 'block';
            toggleConfigButton.textContent = '隐藏配置按钮';
        } else {
            configButton.style.display = 'none';
            toggleConfigButton.textContent = '显示配置按钮';
        }

        GM_setValue(CONFIG_BUTTON_VISIBLE_KEY, isVisible);
    }

    // 在页面加载完成后执行
    window.addEventListener('load', () => {
        if (!isExcluded(window.location.href)) {
            convertLinks();
            createCopyButton();
            createConfigButton();
            createToggleButtons();

            // 初始化按钮显示状态
            const copyButtonVisible = GM_getValue(COPY_BUTTON_VISIBLE_KEY, true);
            const configButtonVisible = GM_getValue(CONFIG_BUTTON_VISIBLE_KEY, true);

            const copyButton = document.getElementById('copyAllLinksButton');
            const configButton = document.getElementById('configExcludeButton');
            const toggleCopyButton = document.getElementById('toggleCopyButton');
            const toggleConfigButton = document.getElementById('toggleConfigButton');

            if (!copyButtonVisible) {
                copyButton.style.display = 'none';
                toggleCopyButton.textContent = '显示复制按钮';
            }

            if (!configButtonVisible) {
                configButton.style.display = 'none';
                toggleConfigButton.textContent = '显示配置按钮';
            }
        } else {
            // 如果页面在排除列表中,移除可能存在的按钮
            const copyButton = document.getElementById('copyAllLinksButton');
            if (copyButton) {
                document.body.removeChild(copyButton);
            }
            const configButton = document.getElementById('configExcludeButton');
            if (configButton) {
                document.body.removeChild(configButton);
            }
            const toggleButtonsContainer = document.getElementById('toggleButtonsContainer');
            if (toggleButtonsContainer) {
                document.body.removeChild(toggleButtonsContainer);
            }
        }
    });

    // 监听动态内容加载(例如 AJAX)
    const observer = new MutationObserver(() => {
        if (!isExcluded(window.location.href)) {
            convertLinks();
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });
})();