Copy Magnet Links

Enhanced copy magnet links tool with localStorage persistence, SPA support, and smart duplicate detection

目前為 2025-11-20 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Copy Magnet Links
// @namespace    https://github.com/
// @version      2.0
// @description  Enhanced copy magnet links tool with localStorage persistence, SPA support, and smart duplicate detection
// @author       Enhanced Copy Magnet Links
// @match        *://*/*
// @grant        GM_setClipboard
// @grant        unsafeWindow
// @license      MIT
// ==/UserScript==


(function() {
    'use strict';

    // Global state management
    var observer = null;
    var button = null;
    var STORAGE_KEY = 'copy-magnet-links-state';

    // Initialize data from localStorage
    function initializeFromStorage() {
        try {
            var stored = localStorage.getItem(STORAGE_KEY);
            if (stored) {
                var data = JSON.parse(stored);
                return {
                    discoveredLinks: new Set(data.discoveredLinks || []),
                    copiedLinks: new Set(data.copiedLinks || []),
                    lastUpdated: data.lastUpdated || Date.now()
                };
            }
        } catch (e) {
            console.warn('Failed to load from localStorage:', e);
        }
        return {
            discoveredLinks: new Set(),
            copiedLinks: new Set(),
            lastUpdated: Date.now()
        };
    }

    // Save state to localStorage
    function saveToStorage(discoveredLinks, copiedLinks) {
        try {
            var data = {
                discoveredLinks: Array.from(discoveredLinks),
                copiedLinks: Array.from(copiedLinks),
                lastUpdated: Date.now(),
                url: window.location.href
            };
            localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
        } catch (e) {
            console.warn('Failed to save to localStorage:', e);
            // If storage is full, clear old data and try again
            try {
                localStorage.removeItem(STORAGE_KEY);
                var data = {
                    discoveredLinks: Array.from(discoveredLinks),
                    copiedLinks: Array.from(copiedLinks),
                    lastUpdated: Date.now(),
                    url: window.location.href
                };
                localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
            } catch (e2) {
                console.warn('LocalStorage is not available or full');
            }
        }
    }

    // Clear old storage data (older than 24 hours)
    function clearOldStorageData() {
        try {
            var stored = localStorage.getItem(STORAGE_KEY);
            if (stored) {
                var data = JSON.parse(stored);
                // Use 24 hours for localStorage since it's more persistent
                if (data.lastUpdated && Date.now() - data.lastUpdated > 86400000) { // 24 hours
                    localStorage.removeItem(STORAGE_KEY);
                    return true;
                }
            }
        } catch (e) {
            console.warn('Failed to clear old storage data:', e);
        }
        return false;
    }

    // Get storage size information
    function getStorageInfo() {
        try {
            var stored = localStorage.getItem(STORAGE_KEY);
            var size = stored ? new Blob([stored]).size : 0;
            return {
                size: size,
                sizeText: size > 1024 ? (size / 1024).toFixed(2) + ' KB' : size + ' bytes',
                itemCount: stored ? JSON.parse(stored).discoveredLinks.length : 0
            };
        } catch (e) {
            return { size: 0, sizeText: '0 bytes', itemCount: 0 };
        }
    }

    // Initialize state from storage
    var state = initializeFromStorage();
    var discoveredLinks = state.discoveredLinks;
    var copiedLinks = state.copiedLinks;

    // Wait for DOM to be ready
    function waitForDOM() {
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', initScript);
        } else {
            initScript();
        }
    }

    // Scan page for magnet links and update state
    function scanForMagnetLinks() {
        var links = document.querySelectorAll('a[href^="magnet:"]');
        var newLinksFound = false;

        links.forEach(function(link) {
            if (!discoveredLinks.has(link.href)) {
                discoveredLinks.add(link.href);
                newLinksFound = true;
            }
        });

        // Save to storage whenever new links are found
        if (newLinksFound) {
            saveToStorage(discoveredLinks, copiedLinks);
        }

        return newLinksFound;
    }

    // Check if there are magnet links on the page
    function checkAndCreateButton() {
        scanForMagnetLinks(); // Initial scan

        if (discoveredLinks.size > 0) {
            createButton();
            observePageChanges();
        }
    }

    // Initialize the script
    function initScript() {
        // Check if button already exists
        if (document.getElementById('copy-magnet-links-btn')) {
            return;
        }

        // Clear old storage data if needed
        clearOldStorageData();

        checkAndCreateButton();

        // Save initial state
        saveToStorage(discoveredLinks, copiedLinks);
    }

    // Create the copy button
    function createButton() {
        // Update button display with link count
        function updateButtonDisplay() {
            var newCount = discoveredLinks.size - copiedLinks.size;
            if (newCount > 0) {
                button.innerHTML = `
                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="pointer-events: none;">
                        <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
                        <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
                    </svg>
                    <span style="position: absolute; top: -5px; right: -5px; background: #ff4757; color: white; border-radius: 50%; width: 16px; height: 16px; font-size: 10px; display: flex; align-items: center; justify-content: center; font-weight: bold;">${newCount}</span>
                `;
                var storageInfo = getStorageInfo();
            button.title = `Copy ${newCount} magnet link${newCount > 1 ? 's' : ''} (${discoveredLinks.size} total, ${copiedLinks.size} copied)\nStorage: ${storageInfo.sizeText}\nRight-click to clear`;
            } else {
                button.innerHTML = `
                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="pointer-events: none;">
                        <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
                        <polyline points="22 4 12 14.01 9 11.01"></polyline>
                    </svg>
                `;
                button.title = "All magnet links copied";
            }
        }

        // Function to copy magnet links to clipboard
        function copyMagnetLinks() {
            // Get only links that haven't been copied yet
            var linksToCopy = [];
            discoveredLinks.forEach(function(link) {
                if (!copiedLinks.has(link)) {
                    linksToCopy.push(link);
                }
            });

            if (linksToCopy.length === 0) {
                showNotification("All magnet links have been copied!");
                return;
            }

            // Add loading state
            button.disabled = true;
            button.style.cursor = "wait";
            button.innerHTML = `
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="pointer-events: none; animation: spin 1s linear infinite;">
                    <path d="M21 2v6h-6"></path>
                    <path d="M3 12a9 9 0 0 1 15-6.7L21 8"></path>
                    <path d="M21 12a9 9 0 0 1-15 6.7L3 16"></path>
                    <path d="M3 22v-6h6"></path>
                </svg>
            `;

            var magnetLinks = linksToCopy.join("\n");

            // Simulate async operation for better UX
            setTimeout(function() {
                // Try multiple copy methods for better compatibility
                function tryCopy(text) {
                    // Method 1: Modern Clipboard API (Chrome, Edge, Firefox, Safari 13.1+)
                    if (navigator.clipboard && navigator.clipboard.writeText) {
                        navigator.clipboard.writeText(text).then(function() {
                            handleCopySuccess(linksToCopy);
                        }).catch(function() {
                            fallbackCopy(text);
                        });
                    }
                    // Method 2: Greasemonkey/Tampermonkey API
                    else if (typeof GM_setClipboard !== 'undefined') {
                        GM_setClipboard(magnetLinks);
                        handleCopySuccess(linksToCopy);
                    }
                    // Method 3: Legacy execCommand method
                    else {
                        fallbackCopy(text);
                    }
                }

                function fallbackCopy(text) {
                    try {
                        var textarea = document.createElement("textarea");
                        textarea.value = text;
                        textarea.style.position = "fixed"; // Prevent scrolling to bottom
                        textarea.style.opacity = "0";
                        document.body.appendChild(textarea);
                        textarea.select();
                        textarea.setSelectionRange(0, 99999); // For mobile devices

                        var successful = document.execCommand("copy");
                        document.body.removeChild(textarea);

                        if (successful) {
                            handleCopySuccess(linksToCopy);
                        } else {
                            showNotification("Failed to copy. Please try manually.");
                        }
                        resetButton();
                    } catch (err) {
                        showNotification("Copy failed: " + err.message);
                        resetButton();
                    }
                }

                // Handle successful copy
                function handleCopySuccess(copiedItems) {
                    // Mark links as copied
                    copiedItems.forEach(function(link) {
                        copiedLinks.add(link);
                    });

                    // Save updated state to storage
                    saveToStorage(discoveredLinks, copiedLinks);

                    showNotification(`Copied ${copiedItems.length} magnet link${copiedItems.length > 1 ? 's' : ''}!`);
                    updateButtonDisplay();
                    resetButton();
                }

                tryCopy(magnetLinks);
            }, 300); // Small delay for better UX
        }

        // Reset button to original state
        function resetButton() {
            button.disabled = false;
            button.style.cursor = "pointer";
            updateButtonDisplay();
        }

        // Function to show a notification
        function showNotification(message) {
            var notification = document.createElement("div");
            notification.className = "magnet-notification";
            notification.innerText = message;
            notification.style.backgroundColor = "#333";
            notification.style.color = "#fff";
            notification.style.padding = "10px";
            notification.style.borderRadius = "5px";
            notification.style.zIndex = 1001;
            notification.style.fontSize = "14px";
            notification.style.opacity = "0.9";
            document.body.appendChild(notification);

            // Automatically remove the notification after 2 seconds
            setTimeout(function() {
                if (document.body.contains(notification)) {
                    document.body.removeChild(notification);
                }
            }, 2000);
        }

        // Add CSS animation and styles
        var style = document.createElement('style');
        style.textContent = `
            @keyframes spin {
                from { transform: rotate(0deg); }
                to { transform: rotate(360deg); }
            }

            #copy-magnet-links-btn {
                position: fixed !important;
                bottom: 10px !important;
                right: 10px !important;
                left: auto !important;
                top: auto !important;
                transform: none !important;
            }

            .magnet-notification {
                position: fixed !important;
                bottom: 60px !important;
                right: 10px !important;
                left: auto !important;
                top: auto !important;
                transform: none !important;
            }
        `;
        document.head.appendChild(style);

        // Create a button to trigger the copy action
        button = document.createElement("button");
        button.id = "copy-magnet-links-btn";
        button.style.position = "fixed";
        button.style.bottom = "10px";
        button.style.right = "10px"; // Place the button in the bottom-right corner
        button.style.zIndex = "2147483647"; // Maximum z-index to ensure it's on top
        button.style.width = "32px";
        button.style.height = "32px";
        button.style.padding = "6px";
        button.style.backgroundColor = "rgba(0, 123, 255, 0.9)";
        button.style.color = "white";
        button.style.border = "none";
        button.style.borderRadius = "6px";
        button.style.cursor = "pointer";
        button.style.display = "flex";
        button.style.alignItems = "center";
        button.style.justifyContent = "center";
        button.style.boxShadow = "0 2px 6px rgba(0,0,0,0.3)";
        button.style.transition = "all 0.2s ease";
        button.style.position = "relative"; // For badge positioning

        // Add hover effects
        button.addEventListener("mouseenter", function() {
            if (!button.disabled) {
                button.style.backgroundColor = "rgba(0, 86, 179, 0.95)";
                button.style.transform = "scale(1.1)";
            }
        });

        button.addEventListener("mouseleave", function() {
            if (!button.disabled) {
                button.style.backgroundColor = "rgba(0, 123, 255, 0.9)";
                button.style.transform = "scale(1)";
            }
        });

        document.body.appendChild(button);

        // Bind updateButtonDisplay to button for external access
        button.updateButtonDisplay = updateButtonDisplay;

        // Initialize button display
        updateButtonDisplay();

        // Add event listener for the button
        button.addEventListener("click", function() {
            copyMagnetLinks();
        });

        // Add right-click context menu to clear storage
        button.addEventListener("contextmenu", function(e) {
            e.preventDefault();

            var storageInfo = getStorageInfo();
            var message = 'Clear all stored magnet link data?\n\n' +
                         'Storage info:\n' +
                         '- ' + storageInfo.itemCount + ' links stored\n' +
                         '- Size: ' + storageInfo.sizeText + '\n' +
                         '- This will reset all copied states';

            if (confirm(message)) {
                try {
                    localStorage.removeItem(STORAGE_KEY);
                    discoveredLinks.clear();
                    copiedLinks.clear();

                    // Re-scan current page
                    var newLinksFound = scanForMagnetLinks();
                    updateButtonDisplay();

                    showNotification('Storage cleared! ' + (newLinksFound ? 'Found ' + discoveredLinks.size + ' links on current page.' : 'No links found.'));
                } catch (err) {
                    showNotification('Failed to clear storage: ' + err.message);
                }
            }
        });
    }

    // Observe page changes for dynamic content
    function observePageChanges() {
        // Initial scan will be handled by createButton function

        observer = new MutationObserver(function(mutations) {
            var newLinksFound = false;

            mutations.forEach(function(mutation) {
                if (mutation.type === 'childList') {
                    for (var i = 0; i < mutation.addedNodes.length; i++) {
                        var node = mutation.addedNodes[i];
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            // Check if the node itself is a magnet link
                            if (node.tagName === 'A' && node.href && node.href.startsWith('magnet:')) {
                                if (!discoveredLinks.has(node.href)) {
                                    discoveredLinks.add(node.href);
                                    newLinksFound = true;
                                }
                            }
                            // Check if the node contains magnet links
                            else if (node.querySelectorAll) {
                                var magnetLinks = node.querySelectorAll('a[href^="magnet:"]');
                                magnetLinks.forEach(function(link) {
                                    if (!discoveredLinks.has(link.href)) {
                                        discoveredLinks.add(link.href);
                                        newLinksFound = true;
                                    }
                                });
                            }
                        }
                    }
                }
                // Also handle attribute changes (e.g., href changes)
                else if (mutation.type === 'attributes' && mutation.attributeName === 'href') {
                    var target = mutation.target;
                    if (target.tagName === 'A' && target.href && target.href.startsWith('magnet:')) {
                        if (!discoveredLinks.has(target.href)) {
                            discoveredLinks.add(target.href);
                            newLinksFound = true;
                        }
                    }
                }
            });

            // Update button if new links were found
            if (newLinksFound && button && button.updateButtonDisplay) {
                button.updateButtonDisplay();
            }

            // Create button if it doesn't exist and we have links
            if (discoveredLinks.size > 0 && !document.getElementById('copy-magnet-links-btn')) {
                createButton();
            }
        });

        // Start observing with comprehensive options
        observer.observe(document.body, {
            childList: true,      // Watch for added/removed nodes
            subtree: true,        // Watch all descendants
            attributes: true,     // Watch for attribute changes
            attributeFilter: ['href'] // Specifically watch href changes
        });

        // Also do periodic scans to catch any missed changes
        setInterval(function() {
            var hadNewLinks = scanForMagnetLinks();
            if (hadNewLinks && button && button.updateButtonDisplay) {
                button.updateButtonDisplay();
            }
        }, 2000); // Check every 2 seconds
    }

    // Start the script
    waitForDOM();
})();