Aria2 推送助手

把网站上的磁力或.torrent文件或其他文件(有待验证)推送到 Aria2 下载

当前为 2025-07-28 提交的版本,查看 最新版本

// ==UserScript==
// @name         Aria2 推送助手
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  把网站上的磁力或.torrent文件或其他文件(有待验证)推送到 Aria2 下载
// @author       deepseek & 通义
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_notification
// @license      MIT
// @icon         https://p.sda1.dev/26/796aa0358b33cbd01742d27d7a0a5125/Ariang_A.png
// ==/UserScript==

(function () {
    'use strict';

    // ================== 配置区域 ==================
    const ARIA2_URL = 'http://192.168.123.233:6800/jsonrpc';
    const ARIA2_SECRET = '123456';
    const SHOW_DELAY = 300; // 显示延迟(毫秒)
    const HIDE_DELAY = 275; // 隐藏延迟(毫秒)
    
    // 配置支持推送的资源类型(通过注释禁用不需要的类型)
    const SUPPORTED_TYPES = [
        'magnet',   // 磁力链接
        'torrent',  // BT种子文件
        'http',     // HTTP下载
        'https',    // HTTPS下载
        //'ftp',     // FTP下载
        'ed2k',     // eDonkey链接
        //'metalink'  // Metalink文件
    ].filter(Boolean); // 过滤掉被注释的行
    
    // ================== 配置结束 ==================

    let currentButton = null;
    let showTimeout = null;
    let hideTimeout = null;

    // 资源类型图标映射
    const resourceIcons = {
        'magnet': '🧲',
        'torrent': '📁',
        'http': '🌐',
        'https': '🔒',
        'ftp': '📡',
        'ed2k': '🔗',
        'metalink': '⚙️',
        'default': '⬇️'
    };

    // 获取背景亮度(感知亮度公式)
    function getBackgroundBrightness(element) {
        let el = element;
        while (el && el !== document.body) {
            const style = window.getComputedStyle(el);
            const bg = style.backgroundColor;

            if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') {
                const rgb = bg.match(/(\d+),\s*(\d+),\s*(\d+)/);
                if (rgb) {
                    const r = parseInt(rgb[1]);
                    const g = parseInt(rgb[2]);
                    const b = parseInt(rgb[3]);
                    return (r * 299 + g * 587 + b * 114) / 1000;
                }
            }
            el = el.parentElement;
        }
        return 255; // 默认亮色背景
    }

    // 获取链接类型
    function getLinkType(url) {
        if (url.startsWith('magnet:')) return 'magnet';
        if (url.endsWith('.torrent')) return 'torrent';
        if (url.startsWith('ed2k:')) return 'ed2k';
        if (url.startsWith('metalink:')) return 'metalink';
        if (url.startsWith('http:')) return 'http';
        if (url.startsWith('https:')) return 'https';
        if (url.startsWith('ftp:')) return 'ftp';
        return 'default';
    }

    // 创建按钮
    function createButton(url, text, link) {
        const button = document.createElement('button');
        const linkType = getLinkType(url);
        const icon = resourceIcons[linkType] || resourceIcons.default;
        button.innerHTML = `${icon} 推送到Aria2`;
        button.setAttribute('aria-label', '推送到Aria2下载器');
        button.dataset.linkType = linkType;

        // 判断背景明暗
        const brightness = getBackgroundBrightness(link);
        const isDarkBg = brightness < 150;

        // 根据背景设置按钮样式
        const bgColor = isDarkBg ? '#00bfff' : '#007bff'; // 亮蓝 or 深蓝
        const color = isDarkBg ? '#fff' : '#fff';
        const boxShadow = isDarkBg
            ? '0 2px 5px rgba(0, 0, 0, 0.4)'
            : '0 2px 5px rgba(0, 0, 0, 0.2)';

        button.style.cssText = `
            position: absolute;
            z-index: 99999;
            padding: 4px 8px;
            font-size: 11px;
            font-weight: normal;
            color: ${color};
            background-color: ${bgColor};
            border: none;
            border-radius: 3px;
            cursor: pointer;
            box-shadow: ${boxShadow};
            transition: all 0.2s ease;
            white-space: nowrap;
            opacity: 0.95;
            display: flex;
            align-items: center;
            gap: 4px;
        `;

        // 点击反馈
        button.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            
            // 保存原始状态以便恢复
            const originalText = button.innerHTML;
            const originalBg = button.style.backgroundColor;
            const originalCursor = button.style.cursor;
            
            // 更新按钮状态为"正在推送"
            button.innerHTML = '⏳ 推送中...';
            button.style.backgroundColor = '#ff9800'; // 橙色表示进行中
            button.style.cursor = 'wait';
            
            // 推送到Aria2
            pushToAria2(url, (success, message) => {
                if (success) {
                    // 推送成功
                    button.innerHTML = '✅ 已推送';
                    button.style.backgroundColor = '#4CAF50'; // 绿色表示成功
                    button.style.cursor = 'default';
                    
                    // 2秒后隐藏按钮
                    setTimeout(() => {
                        if (currentButton === button) {
                            currentButton.remove();
                            currentButton = null;
                        }
                    }, 2000);
                    
                    // 显示成功通知(如果不想要油猴脚本在浏览器弹出成功通知,请自行注释掉GM_notification();)
                    GM_notification({
                        title: '推送成功',
                        text: `资源已添加到Aria2下载队列`,
                        timeout: 3000
                    });
                } else {
                    // 推送失败 - 恢复按钮状态并显示错误
                    button.innerHTML = originalText;
                    button.style.backgroundColor = originalBg;
                    button.style.cursor = originalCursor;
                    
                    // 显示错误弹窗
                    alert(`推送失败: ${message}`);
                }
            });
        });

        // 鼠标进入按钮 → 清除隐藏定时器
        button.addEventListener('mouseenter', () => {
            clearTimeout(hideTimeout);
        });

        // 鼠标离开按钮 → 延迟隐藏按钮
        button.addEventListener('mouseleave', () => {
            hideTimeout = setTimeout(() => {
                // 只有非"已推送"状态才隐藏
                if (currentButton && currentButton.innerHTML !== '✅ 已推送') {
                    currentButton.remove();
                    currentButton = null;
                }
            }, HIDE_DELAY);
        });

        return button;
    }

    // 显示按钮在链接上方或下方(智能判断位置)
    function showButtonAboveLink(link) {
        if (currentButton) return;

        clearTimeout(showTimeout);
        showTimeout = setTimeout(() => {
            const rect = link.getBoundingClientRect();
            const scrollTop = window.scrollY || window.pageYOffset;
            const scrollLeft = window.scrollX || window.pageXOffset;

            const url = link.href;
            const text = link.textContent || '下载资源';

            // 获取链接类型
            const linkType = getLinkType(url);
            
            // 检查是否支持该类型
            if (!SUPPORTED_TYPES.includes(linkType)) return;
            
            // 如果是默认类型且不是可下载资源,跳过
            if (linkType === 'default' && !isDownloadableResource(url)) return;

            currentButton = createButton(url, text, link);
            document.body.appendChild(currentButton);

            // 获取按钮尺寸
            const buttonRect = currentButton.getBoundingClientRect();

            // 初始按钮位置:链接上方 20px,水平居中
            let top = rect.top + scrollTop - buttonRect.height - 10;
            let left = rect.left + scrollLeft + (rect.width - buttonRect.width) / 2;

            // 如果上方空间不足 → 放到下方
            if (top < 0) {
                top = rect.top + scrollTop + 20;
            }

            // 如果右侧超出 → 右对齐
            if (left + buttonRect.width > window.innerWidth + scrollLeft) {
                left = rect.right + scrollLeft - buttonRect.width;
            }

            // 如果左侧超出 → 左对齐
            if (left < scrollLeft) {
                left = rect.left + scrollLeft;
            }

            currentButton.style.top = `${top}px`;
            currentButton.style.left = `${left}px`;
        }, SHOW_DELAY);
    }

    // 判断是否为可下载资源
    function isDownloadableResource(url) {
        // 排除常见网页扩展名
        const nonDownloadable = ['.html', '.htm', '.php', '.asp', '.aspx', '.jsp', '.cgi', '.js', '.css'];
        for (const ext of nonDownloadable) {
            if (url.includes(ext)) return false;
        }
        
        // 检查URL是否包含常见下载关键词
        const downloadKeywords = ['/download/', '/dl/', '/file/', '/getfile', '?download=true'];
        for (const keyword of downloadKeywords) {
            if (url.includes(keyword)) return true;
        }
        
        // 检查URL参数中是否有下载指示
        const params = new URL(url).searchParams;
        if (params.get('download') || params.get('dl')) return true;
        
        return false;
    }

    // 设置链接监听
    function setupLinkListeners() {
        // 资源类型到选择器的映射
        const typeToSelector = {
            'magnet': 'a[href^="magnet:"]',
            'torrent': 'a[href*=".torrent"]',
            'ed2k': 'a[href^="ed2k:"]',
            'metalink': 'a[href^="metalink:"]',
            'http': 'a[href^="http://"]',
            'https': 'a[href^="https://"]',
            'ftp': 'a[href^="ftp://"]'
        };
        
        // 生成选择器数组(仅包含支持的类型)
        const selectors = SUPPORTED_TYPES.map(type => typeToSelector[type]).filter(Boolean);
        
        // 如果没有选择器则跳过
        if (selectors.length === 0) return;
        
        const links = document.querySelectorAll(selectors.join(','));
        links.forEach(link => {
            if (link.dataset.aria2Added) return;
            link.dataset.aria2Added = 'true';

            // 鼠标进入链接 → 显示按钮
            link.addEventListener('mouseenter', () => {
                showButtonAboveLink(link);
            });

            // 鼠标离开链接 → 延迟隐藏按钮
            link.addEventListener('mouseleave', () => {
                if (currentButton) {
                    hideTimeout = setTimeout(() => {
                        // 只有非"已推送"状态才隐藏
                        if (currentButton && currentButton.innerHTML !== '✅ 已推送') {
                            currentButton.remove();
                            currentButton = null;
                        }
                    }, HIDE_DELAY);
                }
            });
        });
    }

    // 主函数
    function main() {
        setupLinkListeners();

        // 动态监听 DOM 变化
        const observer = new MutationObserver(setupLinkListeners);
        observer.observe(document.body, { childList: true, subtree: true });
    }

    // 推送到 Aria2
    function pushToAria2(url, callback) {
        console.log('🔗 正在推送链接到 Aria2:', url);
        
        // 特殊处理:.torrent文件需要下载后推送
        if (url.endsWith('.torrent')) {
            return handleTorrentFile(url, callback);
        }

        const payload = {
            jsonrpc: "2.0",
            id: "aria2_push_" + Date.now(),
            method: "aria2.addUri",
            params: [
                `token:${ARIA2_SECRET}`,
                [url],
                {}
            ]
        };

        sendAria2Request(payload, callback);
    }

    // 处理.torrent文件
    function handleTorrentFile(url, callback) {
        GM_xmlhttpRequest({
            method: 'GET',
            url: url,
            responseType: 'arraybuffer',
            onload: function(response) {
                if (response.status === 200) {
                    const base64Torrent = arrayBufferToBase64(response.response);
                    const payload = {
                        jsonrpc: "2.0",
                        id: "aria2_push_" + Date.now(),
                        method: "aria2.addTorrent",
                        params: [
                            `token:${ARIA2_SECRET}`,
                            base64Torrent,
                            []
                        ]
                    };
                    sendAria2Request(payload, callback);
                } else {
                    callback(false, `无法获取.torrent文件: ${response.status} ${response.statusText}`);
                }
            },
            onerror: function(err) {
                callback(false, `下载.torrent文件失败: ${err.status} ${err.statusText}`);
            }
        });
    }

    // 发送Aria2请求
    function sendAria2Request(payload, callback) {
        GM_xmlhttpRequest({
            method: 'POST',
            url: ARIA2_URL,
            headers: {
                'Content-Type': 'application/json',
            },
            data: JSON.stringify(payload),
            onload: function (response) {
                try {
                    const res = JSON.parse(response.responseText);
                    if (res.error) {
                        console.error('❌ Aria2 返回错误:', res.error);
                        callback(false, `Aria2 错误: ${res.error.message || res.error.code}`);
                    } else {
                        console.log('✅ 成功添加到 Aria2');
                        callback(true, '添加成功');
                    }
                } catch (e) {
                    console.error('❌ 解析响应失败:', e);
                    callback(false, '解析响应失败');
                }
            },
            onerror: function (err) {
                console.error('❌ 请求失败:', err);
                callback(false, `网络错误: ${err.status} ${err.statusText}`);
            },
            ontimeout: function () {
                console.error('❌ 请求超时');
                callback(false, '请求超时,请检查Aria2服务状态');
            }
        });
    }

    // ArrayBuffer 转 Base64
    function arrayBufferToBase64(buffer) {
        let binary = '';
        const bytes = new Uint8Array(buffer);
        const len = bytes.byteLength;
        for (let i = 0; i < len; i++) {
            binary += String.fromCharCode(bytes[i]);
        }
        return btoa(binary);
    }

    // 页面加载后执行
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', main);
    } else {
        main();
    }
})();