HDBits M-Team Linker

在HDBits种子页面显示M-Team对应种子链接

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         HDBits M-Team Linker
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  在HDBits种子页面显示M-Team对应种子链接
// @author       江畔
// @match        *://hdbits.org/browse.php*
// @grant        GM.xmlHttpRequest
// @grant        GM.setValue
// @grant        GM.getValue
// @grant        GM_registerMenuCommand
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // ==================== 配置 ====================
    const MTEAM_CONFIG = {
        apiBaseUrl: 'https://api.m-team.io',
        origin: 'https://kp.m-team.cc',
        apiKey: GM.getValue('mteam_api_key', ''), // 异步获取,但我们会在需要时等待
        enabled: GM.getValue('mteam_enabled', true), // 异步获取,但我们会在需要时等待
        category: 421, // 默认分类
        nameContains: '' // 名称包含关键词
    };

    // ==================== 工具函数 ====================

    /**
     * 格式化文件大小为字节数
     * @param {string} sizeText - 文件大小文本,如 "88.46 GiB"
     * @returns {number} 字节数
     */
    function parseSizeToBytes(sizeText) {
        if (!sizeText) return 0;

        const match = sizeText.trim().match(/^([\d.]+)\s*(B|KB|MB|GB|TB|GiB|MiB|KiB)$/i);
        if (!match) return 0;

        const value = parseFloat(match[1]);
        const unit = match[2].toUpperCase();

        const multipliers = {
            'B': 1,
            'KB': 1024,
            'MB': 1024 * 1024,
            'GB': 1024 * 1024 * 1024,
            'TB': 1024 * 1024 * 1024 * 1024,
            'KIB': 1024,
            'MIB': 1024 * 1024,
            'GIB': 1024 * 1024 * 1024
        };

        return Math.round(value * (multipliers[unit] || 1));
    }

    /**
     * 从种子行中提取IMDb ID
     * @param {HTMLElement} row - 表格行元素
     * @returns {string|null} IMDb ID
     */
    function extractImdbId(row) {
        // 方法1:从data-imdb-link属性获取
        const imdbLink = row.querySelector('a[data-imdb-link]');
        if (imdbLink) {
            const match = imdbLink.getAttribute('data-imdb-link').match(/tt\d+/);
            if (match) return match[0];
        }

        // 方法2:从poster图片src获取
        const posterImg = row.querySelector('.browse_imdb_poster img');
        if (posterImg) {
            const match = posterImg.src.match(/imdb_poster_cache\/(\d+)_big\.jpg/);
            if (match) return 'tt' + match[1].padStart(7, '0');
        }

        // 方法3:从wishlist链接获取
        const wishlistLink = row.querySelector('a[onclick*="addWishlist"]');
        if (wishlistLink) {
            const match = wishlistLink.getAttribute('onclick').match(/addWishlist\(this,\s*'([^']+)'/);
            if (match) return match[1];
        }

        return null;
    }

    /**
     * 从种子行中提取文件大小
     * @param {HTMLElement} row - 表格行元素
     * @returns {string} 文件大小文本
     */
    function extractSize(row) {
        // 尝试第7列(新版本有折扣列)
        let sizeCell = row.querySelector('td.center:nth-child(7)');
        if (!sizeCell) {
            // 如果第7列不是Size列,尝试第6列(兼容旧版本)
            sizeCell = row.querySelector('td.center:nth-child(6)');
        }
        return sizeCell ? sizeCell.textContent.trim() : '';
    }

    /**
     * 从种子行中提取文件名
     * @param {HTMLElement} row - 表格行元素
     * @returns {string} 文件名
     */
    function extractFilename(row) {
        try {
            const nameCell = row.querySelector('td.browse_td_name_cell');
            if (!nameCell) return '';

            // 找到种子标题链接
            const titleLink = nameCell.querySelector('a[href*="/details.php"]');
            if (!titleLink) return '';

            return titleLink.textContent?.trim() || '';
        } catch (error) {
            console.error('提取文件名时出错:', error);
            return '';
        }
    }

    /**
     * 搜索M-Team种子
     * @param {string} imdbId - IMDb ID
     * @returns {Promise<Array>} 种子列表
     */
    function searchMTorrent(imdbId) {
        return new Promise((resolve, reject) => {
            if (!MTEAM_CONFIG.apiKey) {
                reject(new Error('请先配置M-Team API Key'));
                return;
            }

            const imdbUrl = `https://www.imdb.com/title/${imdbId}`;
            const requestData = {
                keyword: imdbUrl,
                categories: MTEAM_CONFIG.category ? [MTEAM_CONFIG.category.toString()] : [],
                pageNumber: 1,
                pageSize: 50,
                visible: 1
            };

            if (MTEAM_CONFIG.nameContains) {
                requestData.keyword = `${imdbUrl} ${MTEAM_CONFIG.nameContains}`;
            }

            GM.xmlHttpRequest({
                method: 'POST',
                url: `${MTEAM_CONFIG.apiBaseUrl}/api/torrent/search`,
                headers: {
                    'Content-Type': 'application/json',
                    'x-api-key': MTEAM_CONFIG.apiKey,
                    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
                    'Accept': 'application/json, text/plain, */*',
                    'Referer': `${MTEAM_CONFIG.origin}/browse`
                },
                data: JSON.stringify(requestData),
                onload: function(response) {
                    try {
                        const result = JSON.parse(response.responseText);
                        console.log('API响应:', result); // 调试用

                        // 检查响应码 - 兼容数字和字符串
                        const codeOk = result.code === 0 || result.code === "0";
                        const hasData = result.data !== null && result.data !== undefined && result.data !== "";

                        if (codeOk && hasData) {
                            // 处理返回的数据结构 - 兼容不同格式
                            let torrents = [];
                            if (Array.isArray(result.data)) {
                                // 直接返回列表
                                torrents = result.data;
                            } else if (typeof result.data === 'object' && result.data !== null) {
                                // 返回嵌套的 data 字段
                                torrents = result.data.data || [];
                            }
                            resolve(torrents);
                        } else {
                            // 对于成功响应但没有数据的情况,也返回空数组
                            if (codeOk) {
                                console.log('API响应成功但无数据');
                                resolve([]);
                            } else {
                                reject(new Error(result.message || '搜索失败'));
                            }
                        }
                    } catch (e) {
                        reject(new Error('解析响应失败: ' + e.message));
                    }
                },
                onerror: function() {
                    reject(new Error('网络请求失败'));
                }
            });
        });
    }

    /**
     * 提取发布组标识(文件名用"-"分割的最后一个元素)
     * @param {string} filename - 文件名
     * @returns {string} 发布组标识,不区分大小写
     */
    function extractReleaseGroup(filename) {
        if (!filename) return '';
        const parts = filename.split('-');
        const group = parts[parts.length - 1]?.trim() || '';
        return group.toLowerCase();
    }

    /**
     * 根据文件大小和发布组匹配种子
     * @param {Array} torrents - 种子列表
     * @param {number} targetSizeBytes - 目标文件大小(字节)
     * @param {string} targetGroup - 目标发布组标识
     * @param {number} tolerancePercent - 容差百分比,默认5%
     * @returns {Array} 匹配的种子
     */
    function matchTorrentsBySizeAndGroup(torrents, targetSizeBytes, targetGroup, tolerancePercent = 5) {
        if (!torrents || !targetSizeBytes || !targetGroup) return [];

        const tolerance = targetSizeBytes * (tolerancePercent / 100);
        const minSize = targetSizeBytes - tolerance;
        const maxSize = targetSizeBytes + tolerance;
        const normalizedTargetGroup = targetGroup.toLowerCase();

        return torrents.filter(torrent => {
            const torrentSize = torrent.size || 0;
            const sizeMatch = torrentSize >= minSize && torrentSize <= maxSize;

            const torrentGroup = extractReleaseGroup(torrent.name || '');
            const groupMatch = torrentGroup === normalizedTargetGroup;

            return sizeMatch && groupMatch;
        });
    }

    /**
     * 获取种子下载链接
     * @param {string} torrentId - 种子ID
     * @returns {Promise<string>} 下载链接
     */
    function getDownloadUrl(torrentId) {
        return new Promise((resolve, reject) => {
            if (!MTEAM_CONFIG.apiKey) {
                reject(new Error('请先配置M-Team API Key'));
                return;
            }

            GM.xmlHttpRequest({
                method: 'POST',
                url: `${MTEAM_CONFIG.apiBaseUrl}/api/torrent/genDlToken`,
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                    'x-api-key': MTEAM_CONFIG.apiKey,
                    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
                    'Accept': 'application/json, text/plain, */*'
                },
                data: `id=${torrentId}`,
                onload: function(response) {
                    try {
                        const result = JSON.parse(response.responseText);
                        console.log('下载链接API响应:', result); // 调试用

                        const codeOk = result.code === 0 || result.code === "0";
                        if (codeOk && result.data && typeof result.data === 'string' && result.data.startsWith('http')) {
                            resolve(result.data);
                        } else {
                            reject(new Error(result.message || '获取下载链接失败'));
                        }
                    } catch (e) {
                        reject(new Error('解析下载链接响应失败: ' + e.message));
                    }
                },
                onerror: function() {
                    reject(new Error('获取下载链接网络请求失败'));
                }
            });
        });
    }

    /**
     * 创建下载链接点击处理器
     * @param {string} torrentId - 种子ID
     * @param {string} torrentName - 种子名称
     * @param {HTMLElement} linkElement - 链接元素
     */
    function createDownloadLinkHandler(torrentId, torrentName, linkElement) {
        return async function(e) {
            e.preventDefault();

            // 显示加载状态 - 直接变红色
            linkElement.style.color = '#f00';

            try {
                const downloadUrl = await getDownloadUrl(torrentId);
                // 在新标签页打开下载链接
                window.open(downloadUrl, '_blank');
                // 恢复原始状态
                linkElement.style.color = '#007cba';
            } catch (error) {
                console.error('获取下载链接失败:', error);
                linkElement.style.color = '#f00';
                linkElement.title = error.message;
                // 3秒后恢复
                setTimeout(() => {
                    linkElement.style.color = '#007cba';
                    linkElement.title = torrentName;
                }, 3000);
            }
        };
    }

    /**
     * 创建M-Team链接HTML
     * @param {Array} matchedTorrents - 匹配的种子
     * @returns {string} HTML字符串
     */
    function createMTorrentLinks(matchedTorrents) {
        if (!matchedTorrents || matchedTorrents.length === 0) {
            return '<span style="color: #999;">未找到</span>';
        }

        const links = matchedTorrents.map(torrent => {
            const detailUrl = `${MTEAM_CONFIG.origin}/detail/${torrent.id}`;
            const title = torrent.name || `种子 ${torrent.id}`;

            // 创建详情链接
            const detailLink = `<a href="${detailUrl}" target="_blank" title="${title} - 详情" style="color: #007cba; text-decoration: none; margin-right: 8px;">详情</a>`;

            // 创建下载链接 - 点击时获取下载URL
            const downloadLinkId = `mteam-dl-${torrent.id}`;
            const downloadLink = `<a id="${downloadLinkId}" href="#" title="${title} - 下载" style="color: #007cba; text-decoration: none; cursor: pointer;">下载</a>`;

            // 添加点击事件处理器
            setTimeout(() => {
                const downloadElement = document.getElementById(downloadLinkId);
                if (downloadElement) {
                    downloadElement.addEventListener('click', createDownloadLinkHandler(torrent.id, title, downloadElement));
                }
            }, 100);

            return `${detailLink}${downloadLink}`;
        });

        return links.join(' | ');
    }

    /**
     * 处理单个种子行
     * @param {HTMLElement} row - 表格行
     */
    async function processTorrentRow(row) {
        try {
            // 检查开关状态
            if (!MTEAM_CONFIG.enabled) {
                return;
            }

            // 检查API Key
            if (!MTEAM_CONFIG.apiKey) {
                const lastCell = row.querySelector('td:last-child');
                if (lastCell) {
                    const newCell = document.createElement('td');
                    newCell.className = 'center';
                    newCell.innerHTML = '<span style="color: #999;" title="请先配置M-Team API Key">未配置</span>';
                    row.appendChild(newCell);
                }
                return;
            }

            // 提取IMDb ID
            const imdbId = extractImdbId(row);
            if (!imdbId) {
                console.log('无法提取IMDb ID,跳过处理');
                // 仍然添加空列保持表格结构
                const lastCell = row.querySelector('td:last-child');
                if (lastCell) {
                    const newCell = document.createElement('td');
                    newCell.className = 'center';
                    newCell.innerHTML = '<span style="color: #999;">N/A</span>';
                    row.appendChild(newCell);
                }
                return;
            }

            // 提取文件大小
            const sizeText = extractSize(row);
            const sizeBytes = parseSizeToBytes(sizeText);
            if (!sizeBytes) {
                console.log('无法提取文件大小,跳过处理');
                const lastCell = row.querySelector('td:last-child');
                if (lastCell) {
                    const newCell = document.createElement('td');
                    newCell.className = 'center';
                    newCell.innerHTML = '<span style="color: #999;">N/A</span>';
                    row.appendChild(newCell);
                }
                return;
            }

            // 提取文件名和发布组
            const filename = extractFilename(row);
            const releaseGroup = extractReleaseGroup(filename);

            console.log(`处理种子: IMDb=${imdbId}, 大小=${sizeText} (${sizeBytes} bytes), 发布组=${releaseGroup}`);

            // 显示处理中状态
            let processingCell = null;
            const existingLastCell = row.querySelector('td:last-child');
            if (existingLastCell) {
                processingCell = document.createElement('td');
                processingCell.className = 'center';
                processingCell.innerHTML = '<span style="color: #ffa500;">处理中...</span>';
                row.appendChild(processingCell);
            }

            // 搜索M-Team种子
            const torrents = await searchMTorrent(imdbId);
            console.log(`找到 ${torrents.length} 个M-Team种子`);

            // 打印所有种子信息用于调试
            if (torrents.length > 0) {
                console.log('=== API获取的所有种子信息 ===');
                torrents.forEach((torrent, index) => {
                    const torrentGroup = extractReleaseGroup(torrent.name || '');
                    const sizeGB = torrent.size ? (torrent.size / (1024 * 1024 * 1024)).toFixed(2) : '未知';
                    console.log(`${index + 1}. ID: ${torrent.id}`);
                    console.log(`   名称: ${torrent.name}`);
                    console.log(`   大小: ${sizeGB} GB (${torrent.size || 0} bytes)`);
                    console.log(`   发布组: ${torrentGroup}`);
                    console.log(`   状态: ${JSON.stringify(torrent.status || {})}`);
                    console.log('');
                });
                console.log('================================');
            }

            // 根据大小和发布组匹配
            const matchedTorrents = matchTorrentsBySizeAndGroup(torrents, sizeBytes, releaseGroup);
            console.log(`大小+发布组匹配找到 ${matchedTorrents.length} 个种子`);

            // 打印匹配的种子信息
            if (matchedTorrents.length > 0) {
                console.log('=== 匹配的种子信息 ===');
                matchedTorrents.forEach((torrent, index) => {
                    const torrentGroup = extractReleaseGroup(torrent.name || '');
                    const sizeGB = torrent.size ? (torrent.size / (1024 * 1024 * 1024)).toFixed(2) : '未知';
                    console.log(`${index + 1}. ID: ${torrent.id}`);
                    console.log(`   名称: ${torrent.name}`);
                    console.log(`   大小: ${sizeGB} GB (${torrent.size || 0} bytes)`);
                    console.log(`   发布组: ${torrentGroup}`);
                    console.log('');
                });
                console.log('======================');
            }

            // 创建链接
            const linksHtml = createMTorrentLinks(matchedTorrents);

            // 更新显示结果
            if (processingCell) {
                processingCell.innerHTML = linksHtml;
            }

        } catch (error) {
            console.error('处理种子行失败:', error);
            // 显示错误信息
            const lastCell = row.querySelector('td:last-child');
            if (lastCell) {
                const newCell = document.createElement('td');
                newCell.className = 'center';
                newCell.innerHTML = '<span style="color: #f00;" title="' + error.message + '">错误</span>';
                row.appendChild(newCell);
            }
        }
    }

    /**
     * 初始化脚本
     */
    function init() {
        // 检查是否在正确的页面
        const torrentTable = document.getElementById('torrent-list');
        if (!torrentTable) {
            console.log('未找到种子表格,跳过处理');
            return;
        }

        // 添加表头
        const headerRow = torrentTable.querySelector('thead tr');
        if (headerRow) {
            const newHeader = document.createElement('th');
            newHeader.className = 'center';
            newHeader.textContent = 'M-Team';
            newHeader.title = 'M-Team对应种子';
            headerRow.appendChild(newHeader);
        }

        // 处理每一行
        const rows = torrentTable.querySelectorAll('tbody tr');
        rows.forEach((row, index) => {
            // 延迟处理,避免同时请求过多
            setTimeout(() => {
                processTorrentRow(row);
            }, index * 100); // 每行间隔100ms
        });
    }

    /**
     * 设置API Key
     */
    async function setApiKey() {
        const currentKey = await GM.getValue('mteam_api_key', '');
        const newKey = prompt('请输入M-Team API Key:', currentKey);
        if (newKey !== null) {
            await GM.setValue('mteam_api_key', newKey.trim());
            alert('API Key已保存!请刷新页面以生效。');
        }
    }

    /**
     * 切换开关
     */
    async function toggleEnabled() {
        const currentEnabled = await GM.getValue('mteam_enabled', true);
        const newEnabled = !currentEnabled;
        await GM.setValue('mteam_enabled', newEnabled);
        alert(`M-Team链接器已${newEnabled ? '开启' : '关闭'}!请刷新页面以生效。`);
    }

    /**
     * 注册菜单
     */
    function registerMenu() {
        GM_registerMenuCommand('设置 M-Team API Key', setApiKey);
        GM_registerMenuCommand(MTEAM_CONFIG.enabled ? '关闭 M-Team 链接器' : '开启 M-Team 链接器', toggleEnabled);
    }

    // ==================== 主逻辑 ====================

    // 启动脚本
    async function startScript() {
        try {
            // 等待配置加载完成
            const [apiKey, enabled] = await Promise.all([
                MTEAM_CONFIG.apiKey,
                MTEAM_CONFIG.enabled
            ]);

            // 更新配置对象
            MTEAM_CONFIG.apiKey = apiKey;
            MTEAM_CONFIG.enabled = enabled;

            // 注册菜单(这时配置已加载,菜单能显示正确状态)
            registerMenu();

            // 检查开关状态
            if (!enabled) {
                console.log('M-Team链接器已关闭');
                return;
            }

            // 检查API Key配置
            if (!apiKey) {
                console.log('M-Team API Key 未配置,请通过油猴脚本菜单设置');
                return;
            }

            console.log('M-Team链接器已启用,API Key已配置');

            // 等待页面加载完成后初始化
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', init);
            } else {
                init();
            }

        } catch (error) {
            console.error('脚本初始化失败:', error);
        }
    }

    // 立即启动脚本(获取配置后再决定是否运行功能)
    startScript();

})();