Greasy Fork

BT4G/BTDig/SOBT/BTMulu 一键复制磁力链和推送到115离线

BT4G/BTDig/SOBT/BTMulu(磁力链搜索引擎)添加一键复制磁力链和推送到115网盘进行离线(推送离线任务需当前浏览器已登录115会员账号)

目前为 2025-04-03 提交的版本。查看 最新版本

// ==UserScript==
// @name         BT4G/BTDig/SOBT/BTMulu 一键复制磁力链和推送到115离线
// @author       [email protected]
// @description  BT4G/BTDig/SOBT/BTMulu(磁力链搜索引擎)添加一键复制磁力链和推送到115网盘进行离线(推送离线任务需当前浏览器已登录115会员账号)
// @version      1.0.0
// @icon         https://115.com/favicon.ico
// @match        https://bt4gprx.com/*
// @match        https://btdig.com/*
// @match        https://sobt18.cfd/*
// @match        https://*.btmulu.work/*
// @grant        GM_setClipboard
// @grant        GM_notification
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @connect      115.com
// @connect      *
// @run-at       document-end
// @namespace    https://greasyfork.org/users/1453515
// @license      MIT

// ==/UserScript==

(function() {
    'use strict';

    // 配置参数
    const CONFIG = {
        notificationTimeout: 3000,
        retryCount: 3,
        retryDelay: 1000
    };

    // 错误码映射
    const ERROR_CODES = {
        10008: '任务已存在,无需重复添加',
        911: '需要账号验证,请确保已登录115会员账号',
        990: '任务包含违规内容,无法添加',
        991: '服务器繁忙,请稍后再试',
        992: '离线下载配额已用完',
        993: '当前账号无权使用离线下载功能',
        994: '文件大小超过限制',
        995: '不支持的链接类型',
        996: '网络错误,请检查连接',
        997: '服务器内部错误',
        998: '请求超时',
        999: '未知错误'
    };

    // 初始化脚本
    initScript();

    function initScript() {
        // 添加Tampermonkey菜单命令
        GM_registerMenuCommand("检查115登录状态", checkAndUpdateLoginStatus);
        GM_registerMenuCommand("打开115网盘", () => window.open("https://115.com", "_blank"));

        // 监听页面变化
        const observer = new MutationObserver(handleDomChanges);
        observer.observe(document, {
            childList: true,
            subtree: true
        });

        // 初始添加按钮
        addActionButtons();
    }

    // DOM变化处理
    function handleDomChanges(mutations) {
        for (const mutation of mutations) {
            if (mutation.addedNodes.length) {
                addActionButtons();
            }
        }
    }

    // 添加操作按钮
    function addActionButtons() {
        // BT4G网站处理
        if (window.location.host.includes('bt4gprx.com')) {
            handleBT4GSite();
        }
        // SOBT网站处理
        else if (window.location.host.includes('sobt18.cfd')) {
            handleSOBTSite();
        }
        // BTDig网站处理
        else if (window.location.host.includes('btdig.com')) {
            handleBTDigSite();
        }
        // BTMulu网站处理
        else if (window.location.host.includes('btmulu.work')) {
            handleBTMuluSite();
        }
    }

    // 处理BTMulu网站
    function handleBTMuluSite() {
        // 处理搜索结果项
        document.querySelectorAll('article.item a[href^="/hash/"]').forEach(titleLink => {
            if (titleLink.dataset.buttonsAdded) return;

            // 标记已处理
            titleLink.dataset.buttonsAdded = true;

            // 创建按钮容器
            const btnContainer = document.createElement('span');
            btnContainer.className = 'magnet-action-buttons';
            btnContainer.style.display = 'inline-block';
            btnContainer.style.marginLeft = '10px';

            // 提取hash
            const hashMatch = titleLink.href.match(/\/hash\/([a-f0-9]+)\.html$/i);
            if (!hashMatch || !hashMatch[1]) return;

            const magnetLink = `magnet:?xt=urn:btih:${hashMatch[1]}`;

            // 添加按钮
            btnContainer.appendChild(createBTMuluCopyButton(magnetLink));
            btnContainer.appendChild(createBTMuluOfflineButton(magnetLink));

            // 插入到DOM - 放在标题后面
            titleLink.parentNode.insertBefore(btnContainer, titleLink.nextSibling);
        });
    }

    // 创建BTMulu风格的复制按钮
    function createBTMuluCopyButton(magnetLink) {
        const btn = document.createElement('button');
        btn.className = 'btn btn-sm copy-magnet-btn';
        btn.innerHTML = '🔗 复制磁力链';
        Object.assign(btn.style, {
            cursor: 'pointer',
            backgroundColor: '#28a745',
            color: '#fff',
            border: '1px solid #28a745',
            borderRadius: '4px',
            padding: '2px 8px',
            fontSize: '12px',
            marginRight: '8px',
            transition: 'all 0.15s ease-in-out',
            fontWeight: '400',
            lineHeight: '1.5',
            verticalAlign: 'middle'
        });

        btn.addEventListener('mouseenter', () => {
            btn.style.backgroundColor = '#218838';
            btn.style.borderColor = '#1e7e34';
        });
        btn.addEventListener('mouseleave', () => {
            btn.style.backgroundColor = '#28a745';
            btn.style.borderColor = '#28a745';
        });

        btn.addEventListener('click', async (e) => {
            e.preventDefault();
            e.stopPropagation();

            GM_setClipboard(magnetLink, 'text');
            showNotification('磁力链接已复制', magnetLink);

            // 按钮反馈效果
            const originalHTML = btn.innerHTML;
            btn.innerHTML = '🔗 复制成功!';
            btn.disabled = true;
            setTimeout(() => {
                btn.innerHTML = originalHTML;
                btn.disabled = false;
            }, 2000);
        });

        return btn;
    }

    // 创建BTMulu风格的115离线按钮
    function createBTMuluOfflineButton(magnetLink) {
        const btn = document.createElement('button');
        btn.className = 'btn btn-sm offline-115-btn';
        btn.innerHTML = '<img src="https://115.com/favicon.ico" style="width:14px;height:14px;vertical-align:text-bottom;margin-right:2px;"> 推送到115离线';
        Object.assign(btn.style, {
            cursor: 'pointer',
            backgroundColor: '#1E50A2',
            color: '#fff',
            border: '1px solid #1a4580',
            borderRadius: '4px',
            padding: '2px 8px',
            fontSize: '12px',
            transition: 'all 0.15s ease-in-out',
            fontWeight: '400',
            lineHeight: '1.5',
            verticalAlign: 'middle'
        });

        btn.addEventListener('mouseenter', () => {
            btn.style.backgroundColor = '#1a4580';
            btn.style.borderColor = '#163c70';
        });
        btn.addEventListener('mouseleave', () => {
            btn.style.backgroundColor = '#1E50A2';
            btn.style.borderColor = '#1a4580';
        });

        btn.addEventListener('click', async (e) => {
            e.preventDefault();
            e.stopPropagation();
            await process115Offline(magnetLink);
        });

        return btn;
    }

    // 处理BT4G网站
    function handleBT4GSite() {
        document.querySelectorAll('.card-body').forEach(cardBody => {
            if (cardBody.dataset.buttonsAdded) return;

            const magnetBtn = cardBody.querySelector('a[href*="downloadtorrentfile.com/hash/"]');
            if (!magnetBtn) return;

            // 标记已处理
            cardBody.dataset.buttonsAdded = true;

            // 创建按钮容器
            const btnContainer = document.createElement('div');
            btnContainer.className = 'magnet-action-buttons';
            btnContainer.style.display = 'inline-block';
            btnContainer.style.marginRight = '10px';

            // 添加按钮
            btnContainer.appendChild(createBT4GCopyButton(magnetBtn));
            btnContainer.appendChild(createBT4GOfflineButton(magnetBtn));

            // 插入到DOM
            magnetBtn.parentNode.insertBefore(btnContainer, magnetBtn);
        });
    }

    // 创建BT4G风格的复制按钮
    function createBT4GCopyButton(element) {
        const btn = document.createElement('button');
        btn.className = 'btn btn-sm copy-magnet-btn';
        btn.innerHTML = '🔗 复制磁力链';
        Object.assign(btn.style, {
            cursor: 'pointer',
            backgroundColor: '#000000',
            color: '#ffffff',
            border: '1px solid #000000',
            borderRadius: '6px',
            padding: '0.25rem 0.5rem',
            fontSize: '0.875rem',
            marginRight: '10px',
            transition: 'all 0.15s ease-in-out',
            fontWeight: '400',
            lineHeight: '2',
            textAlign: 'center',
            verticalAlign: 'middle',
            userSelect: 'none'
        });

        btn.addEventListener('mouseenter', () => {
            btn.style.backgroundColor = '#333333';
            btn.style.borderColor = '#333333';
        });
        btn.addEventListener('mouseleave', () => {
            btn.style.backgroundColor = '#000000';
            btn.style.borderColor = '#000000';
        });

        btn.addEventListener('click', async (e) => {
            e.preventDefault();
            e.stopPropagation();

            const magnetLink = await extractMagnetLink(element);
            if (!magnetLink) return;

            GM_setClipboard(magnetLink, 'text');
            showNotification('磁力链接已复制', magnetLink);

            // 按钮反馈效果
            const originalHTML = btn.innerHTML;
            btn.innerHTML = '🔗 复制成功!';
            btn.disabled = true;
            setTimeout(() => {
                btn.innerHTML = originalHTML;
                btn.disabled = false;
            }, 2000);
        });

        return btn;
    }

    // 创建BT4G风格的115离线按钮
    function createBT4GOfflineButton(element) {
        const btn = document.createElement('button');
        btn.className = 'btn btn-sm offline-115-btn';
        btn.innerHTML = '<img src="https://115.com/favicon.ico" style="width:14px;height:14px;vertical-align:text-bottom;margin-right:2px;"> 推送到115离线';
        Object.assign(btn.style, {
            cursor: 'pointer',
            backgroundColor: '#1E50A2',
            color: '#fff',
            border: '1px solid #1a4580',
            borderRadius: '6px',
            padding: '0.25rem 0.5rem',
            fontSize: '0.875rem',
            transition: 'all 0.15s ease-in-out',
            fontWeight: '400',
            lineHeight: '2',
            textAlign: 'center',
            verticalAlign: 'middle',
            userSelect: 'none'
        });

        btn.addEventListener('mouseenter', () => {
            btn.style.backgroundColor = '#1a4580';
            btn.style.borderColor = '#163c70';
        });
        btn.addEventListener('mouseleave', () => {
            btn.style.backgroundColor = '#1E50A2';
            btn.style.borderColor = '#1a4580';
        });

        btn.addEventListener('click', async (e) => {
            e.preventDefault();
            e.stopPropagation();

            const magnetLink = await extractMagnetLink(element);
            if (!magnetLink) return;

            await process115Offline(magnetLink);
        });

        return btn;
    }

    // 处理SOBT网站
    function handleSOBTSite() {
        // 处理搜索结果页
        document.querySelectorAll('h3 > a[href^="/torrent/"]').forEach(titleLink => {
            if (titleLink.dataset.buttonsAdded) return;

            // 标记已处理
            titleLink.dataset.buttonsAdded = true;

            // 创建按钮容器
            const btnContainer = document.createElement('span');
            btnContainer.className = 'magnet-action-buttons';
            btnContainer.style.display = 'inline-block';
            btnContainer.style.marginLeft = '10px';

            // 添加按钮
            btnContainer.appendChild(createCopyButton(titleLink));
            btnContainer.appendChild(createOfflineButton(titleLink));

            // 插入到DOM
            titleLink.parentNode.insertBefore(btnContainer, titleLink.nextSibling);
        });

        // 处理详情页
        document.querySelectorAll('.panel-body a[href^="magnet:"]').forEach(magnetLink => {
            if (magnetLink.dataset.buttonsAdded) return;

            // 标记已处理
            magnetLink.dataset.buttonsAdded = true;

            // 创建按钮容器
            const btnContainer = document.createElement('div');
            btnContainer.className = 'magnet-action-buttons';
            btnContainer.style.margin = '10px 0';

            // 添加按钮
            btnContainer.appendChild(createCopyButton(magnetLink));
            btnContainer.appendChild(createOfflineButton(magnetLink));

            // 插入到DOM
            magnetLink.parentNode.insertBefore(btnContainer, magnetLink.nextSibling);
        });
    }

    // 处理BTDig网站
    function handleBTDigSite() {
        // 处理搜索结果项
        document.querySelectorAll('.torrent_name > a').forEach(titleLink => {
            if (titleLink.dataset.buttonsAdded) return;

            // 标记已处理
            titleLink.dataset.buttonsAdded = true;

            // 创建按钮容器
            const btnContainer = document.createElement('span');
            btnContainer.className = 'magnet-action-buttons';
            btnContainer.style.display = 'inline-block';
            btnContainer.style.marginRight = '10px';

            // 获取磁力链接
            const magnetLink = document.querySelector('.torrent_magnet a[href^="magnet:"]');
            if (!magnetLink) return;

            // 添加按钮
            btnContainer.appendChild(createCopyButton(magnetLink));
            btnContainer.appendChild(createOfflineButton(magnetLink));

            // 插入到DOM - 放在标题前面
            titleLink.parentNode.insertBefore(btnContainer, titleLink);
        });
    }

    // 创建复制按钮(通用样式)
    function createCopyButton(element) {
        const btn = document.createElement('button');
        btn.className = 'btn btn-info me-2 copy-magnet-btn';
        btn.innerHTML = '🔗 复制磁力链';
        Object.assign(btn.style, {
            cursor: 'pointer',
            backgroundColor: '#000',
            color: '#fff',
            border: 'none',
            borderRadius: '4px',
            padding: '4px 8px',
            fontSize: '12px',
            marginRight: '5px',
            transition: 'opacity 0.3s'
        });

        btn.addEventListener('mouseenter', () => btn.style.opacity = '0.8');
        btn.addEventListener('mouseleave', () => btn.style.opacity = '1');

        btn.addEventListener('click', async (e) => {
            e.preventDefault();
            e.stopPropagation();

            const magnetLink = await extractMagnetLink(element);
            if (!magnetLink) return;

            GM_setClipboard(magnetLink, 'text');
            showNotification('磁力链接已复制', magnetLink);

            // 按钮反馈效果
            const originalHTML = btn.innerHTML;
            btn.innerHTML = '🔗 复制成功!';
            btn.disabled = true;
            setTimeout(() => {
                btn.innerHTML = originalHTML;
                btn.disabled = false;
            }, 2000);
        });

        return btn;
    }

    // 创建115离线按钮(通用样式)
    function createOfflineButton(element) {
        const btn = document.createElement('button');
        btn.className = 'btn btn-danger me-2 offline-115-btn';
        btn.innerHTML = '<img src="https://115.com/favicon.ico" style="width:14px;height:14px;vertical-align:middle;"> 推送到115离线';
        Object.assign(btn.style, {
            cursor: 'pointer',
            backgroundColor: '#1E50A2',
            color: '#fff',
            border: 'none',
            borderRadius: '4px',
            padding: '4px 8px',
            fontSize: '12px',
            transition: 'opacity 0.3s'
        });

        btn.addEventListener('mouseenter', () => btn.style.opacity = '0.8');
        btn.addEventListener('mouseleave', () => btn.style.opacity = '1');

        btn.addEventListener('click', async (e) => {
            e.preventDefault();
            e.stopPropagation();

            const magnetLink = await extractMagnetLink(element);
            if (!magnetLink) return;

            await process115Offline(magnetLink);
        });

        return btn;
    }

    // 从元素提取磁力链接
    async function extractMagnetLink(element) {
        try {
            // 如果是SOBT网站的标题链接
            if (element.href && element.href.includes('/torrent/')) {
                const hashMatch = element.href.match(/\/torrent\/([a-f0-9]+)\.html$/i);
                if (hashMatch && hashMatch[1]) {
                    return `magnet:?xt=urn:btih:${hashMatch[1]}`;
                }
            }
            // 如果是BTMulu网站的标题链接
            else if (element.href && element.href.includes('/hash/')) {
                const hashMatch = element.href.match(/\/hash\/([a-f0-9]+)\.html$/i);
                if (hashMatch && hashMatch[1]) {
                    return `magnet:?xt=urn:btih:${hashMatch[1]}`;
                }
            }
            // 如果是直接磁力链接
            else if (element.href && element.href.startsWith('magnet:')) {
                return element.href;
            }
            // BT4G网站的磁力链接
            else if (element.href && element.href.includes('downloadtorrentfile.com/hash/')) {
                const hashMatch = element.href.match(/hash\/([a-f0-9]+)/i);
                if (hashMatch && hashMatch[1]) {
                    return `magnet:?xt=urn:btih:${hashMatch[1]}`;
                }
            }

            throw new Error('无法提取磁力链接Hash');
        } catch (error) {
            showNotification('错误', error.message);
            return null;
        }
    }

    // 115离线下载处理流程
    async function process115Offline(magnetLink) {
        const notificationId = Date.now();

        try {
            // 步骤1:检查登录状态
            showNotification('115离线', '正在检查登录状态...', notificationId);
            const isLoggedIn = await check115Login();
            if (!isLoggedIn) {
                throw new Error('请先登录115网盘');
            }

            // 步骤2:提交离线任务
            showNotification('115离线', '正在提交离线任务...', notificationId);
            const result = await submit115OfflineTask(magnetLink);

            // 处理结果
            handleOfflineResult(result);

        } catch (error) {
            showNotification('115离线失败', error.message);

            // 如果是未登录错误,显示登录提示
            if (error.message.includes('登录')) {
                setTimeout(() => {
                    if (confirm('需要登录115网盘,是否进入115网盘登录页面?')) {
                        window.open('https://115.com/?mode=login', '_blank');
                    }
                }, 500);
            }

        } finally {
            // 关闭进度通知
            GM_notification({ id: notificationId, done: true });
        }
    }

    // 检查115登录状态
    async function check115Login() {
        try {
            const response = await fetch115Api('https://115.com/');
            const match = response.match(/USER_ID\s*=\s*'(\d+)'/);

            if (match && match[1]) {
                GM_setValue('115ID', match[1]);
                return true;
            }

            GM_setValue('115ID', '');
            return false;

        } catch (error) {
            console.error('检查登录状态失败:', error);
            return false;
        }
    }

    // 提交115离线任务
    async function submit115OfflineTask(magnetLink) {
        const response = await fetch115Api(
            `https://115.com/web/lixian/?ct=lixian&ac=add_task_url&url=${encodeURIComponent(magnetLink)}`
        );

        return tryParseJson(response);
    }

    // 处理离线结果
    function handleOfflineResult(result) {
        if (!result) {
            throw new Error('无效的响应');
        }

        if (result.state) {
            showNotification('115离线成功', '任务已成功添加到离线下载列表');
            return;
        }

        const errorMsg = ERROR_CODES[result.errcode] || result.error_msg || '未知错误';
        throw new Error(errorMsg);
    }

    // 检查并更新登录状态
    async function checkAndUpdateLoginStatus() {
        try {
            const isLoggedIn = await check115Login();
            showNotification('115状态', isLoggedIn ? '已检测到登录状态' : '未检测到登录状态');
            return isLoggedIn;
        } catch (error) {
            console.error('检查登录状态失败:', error);
            showNotification('115状态', '检查登录状态失败');
            return false;
        }
    }

    // 通用请求函数
    function fetch115Api(url, options = {}) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                url: url,
                method: options.method || 'GET',
                headers: {
                    'User-Agent': navigator.userAgent,
                    'Origin': 'https://115.com',
                    ...(options.headers || {})
                },
                data: options.body,
                onload: function(response) {
                    if (response.status >= 200 && response.status < 300) {
                        resolve(response.responseText);
                    } else {
                        reject(new Error(`请求失败: ${response.status}`));
                    }
                },
                onerror: reject
            });
        });
    }

    // 尝试解析JSON
    function tryParseJson(text) {
        try {
            return JSON.parse(text);
        } catch (e) {
            return null;
        }
    }

    // 显示通知
    function showNotification(title, text, id = null) {
        GM_notification({
            title: title,
            text: text,
            timeout: CONFIG.notificationTimeout,
            ...(id ? { id } : {})
        });
    }
})();