// ==UserScript== // @name 一键复制磁力链和推送到115离线 // @author wangzijian0@vip.qq.com // @description 目前支持BT4G/BTDig/SOBT/BTMulu(磁力链搜索引擎)添加一键复制磁力链和推送到115网盘进行离线(推送离线任务需当前浏览器已登录115会员账号) // @version 1.0.2.20250403 // @icon https://115.com/favicon.ico // @include https://bt4gprx.com/* // @include https://btdig.com/* // @include https://*.btmulu.work/* // @include https://sobt*.*/* // @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 // @downloadURL none // ==/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网站处理 - 匹配 sobt*.* else if (/sobt[^.]+\..+/.test(window.location.host)) { handleSOBTSite(); } // BTDig网站处理 - 匹配 *.btdig.com else if (window.location.host.endsWith('.btdig.com')) { handleBTDigSite(); } // BTMulu网站处理 - 匹配 *.btmulu.work else if (window.location.host.endsWith('.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 = ' 推送到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 = ' 推送到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 = ' 推送到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]}`; } } // 如果是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]}`; } } // 如果是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; } 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 } : {}) }); } })();