ANIME Pro Matcher Client

ANIME Pro Matcher 客户端 - 强力模式 + TMDB直达

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         ANIME Pro Matcher Client
// @namespace    http://tampermonkey.net/
// @version      3.2.0
// @description  ANIME Pro Matcher 客户端 - 强力模式 + TMDB直达
// @author       User & Refactored
// @match        https://*/detail/*
// @match        https://*/details.php?id=*
// @match        https://*/details_movie.php?id=*
// @match        https://*/details_tv.php?id=*
// @match        https://*/details_animate.php?id=*
// @match        https://bangumi.moe/*
// @match        https://*.acgnx.se/*
// @match        https://*.dmhy.org/*
// @match        https://nyaa.si/*
// @match        https://mikanani.me/*
// @match        https://*.skyey2.com/*
// @match        http://localhost*/*
// @match        http://127.0.0.1*/*
// @match        <all_urls>
// @grant        GM_log
// @grant        GM_xmlhttpRequest
// @grant        GM_setClipboard
// @connect      *
// @license MIT
// ==/UserScript==

// --- 配置区域 ---
const windowPopup = true; // 是否开启划词弹窗
const serverUrl = 'http://192.168.50.202:6868'; // 你的 ANIME Pro Matcher 服务器地址
// ----------------

let ptype = '';
let btype = '';
let site_domain = window.location.hostname;

// 1. 请求函数:保留了 force_filename 和 anime_priority
function recognize(text) {
    return new Promise(function (resolve, reject) {
        const payload = JSON.stringify({
            filename: text,
            anime_priority: true,
            force_filename: true
        });

        GM_xmlhttpRequest({
            url: serverUrl + `/api/recognize`,
            method: "POST",
            headers: {
                "user-agent": navigator.userAgent,
                "content-type": "application/json"
            },
            data: payload,
            responseType: "json",
            onload: (res) => {
                if (res.status === 200) {
                    resolve(res.response);
                } else {
                    GM_log("API Error: " + res.responseText);
                    reject(new Error('识别请求失败: ' + res.status));
                }
            },
            onerror: (err) => {
                GM_log(err)
                reject(new Error('识别网络错误 (请检查 192.168.50.202 是否开启)'));
            }
        });
    });
}

// 辅助工具:等待元素加载
function waitForElements(selectors, timeout = 30000) {
    return new Promise((resolve, reject) => {
        const interval = 500;
        const maxTries = timeout / interval;
        let tries = 0;

        const checkExist = setInterval(() => {
            let allFound = true;
            const elements = selectors.map(selector => {
                const foundElements = document.getElementsByClassName(selector);
                if (foundElements.length === 0) allFound = false;
                return foundElements;
            });

            if (allFound) {
                clearInterval(checkExist);
                resolve(elements);
            } else if (tries >= maxTries) {
                clearInterval(checkExist);
                reject(new Error(`未找到目标元素,脚本停止在该页面运行`));
            }
            tries++;
        }, interval);
    });
}

// 渲染标签样式
function renderTag(ptype, string, background_color) {
    if (!string && string !== 0) return '';
    if (ptype == 'hhanclub') {
        return `<span class="flex justify-center items-center rounded-md text-[12px] h-[18px] mr-2 px-[5px] font-bold" style="background-color:${background_color};color:#ffffff;">${string}</span>`;
    } else {
        return `<span style=\"background-color:${background_color};color:#ffffff;border-radius:2px;font-size:12px;margin:0 4px 0 0;padding:2px 4px\">${string}</span>`;
    }
}

// 渲染项目名称行头
function renderProjectHeader(ptype, content) {
    const projectName = `<span style="font-weight:bold; color:#3e84f4;">ANIME Pro Matcher</span>`;
    
    if (ptype == "common") {
        return `<td class="rowhead nowrap" valign="top" align="right">${projectName}</td><td class="rowfollow" valign="top" align="left">${content}</td>`;
    } else if (ptype == 'm-team') {
        return `<th class="ant-descriptions-item-label" style="width: 135px; text-align: right;" colspan="1"><span>${projectName}</span></th><td class="ant-descriptions-item-content" colspan="1"><span>${content}</span></td>`;
    } else {
        return content;
    }
}

// 2. 核心修改:智能生成 TMDB 直达链接
function getTmdbLink(id, category) {
    if (!id) return '';
    let type = 'tv'; // 默认为剧集 (动漫通常是剧集)
    
    if (category) {
        const cat = String(category).toLowerCase();
        if (cat.includes('电影') || cat.includes('movie')) {
            type = 'movie';
        } else if (cat.includes('剧集') || cat.includes('tv')) {
            type = 'tv';
        }
    }
    return `https://www.themoviedb.org/${type}/${id}`;
}

// 构建识别结果的 HTML 标签
function buildTagsHtml(ptype, final) {
    let html = '';
    html += final.category ? renderTag(ptype, final.category, '#2775b6') : '';
    html += final.title ? renderTag(ptype, final.title, '#c54640') : '';
    
    let se = '';
    if (final.season != null) se += `S${final.season}`;
    if (final.episode != null) se += `E${final.episode}`;
    html += se ? renderTag(ptype, se, '#e6702e') : '';
    
    html += final.year ? renderTag(ptype, final.year, '#e6702e') : '';
    
    // 修改处:使用直达链接
    if (final.tmdb_id) {
        let detail_link = getTmdbLink(final.tmdb_id, final.category);
        html += `<a href="${detail_link}" target="_blank" title="点击跳转 TMDB 详情页">${renderTag(ptype, "TMDB: " + final.tmdb_id, '#5bb053')}</a>`;
    }
    
    html += final.team ? renderTag(ptype, final.team, '#701eeb') : '';
    html += final.resolution ? renderTag(ptype, final.resolution, '#677489') : '';
    html += final.source ? renderTag(ptype, final.source, '#95a5a6') : '';

    return html;
}

// 创建识别行
function creatRecognizeRow(row, ptype, torrent_name) {
    row.innerHTML = renderProjectHeader(ptype, "正在分析...");
    
    recognize(torrent_name).then(data => {
        const final = data.final_result;
        if (final) {
            let html = buildTagsHtml(ptype, final);
            row.innerHTML = renderProjectHeader(ptype, html);
        } else {
            row.innerHTML = renderProjectHeader(ptype, `<span style="color:gray;">未识别到有效信息</span>`);
        }
    }).catch(error => {
        console.error(error);
        row.innerHTML = renderProjectHeader(ptype, `<span style="color:red; cursor:help;" title="${error.message}">连接失败 (悬停查看)</span>`);
    });
}

// 划词弹窗的显示逻辑
function creatRecognizeTip(tip, text) {
    tip.showText(`<b>APM 分析中...</b>`);
    recognize(text).then(data => {
        const final = data.final_result;
        if (final) {
            let html = '<div style="margin-bottom:5px; border-bottom:1px solid #eee; padding-bottom:5px;"><b>✅ 识别成功</b></div>';
            html += final.category ? `📂 分类:${final.category}<br>` : '';
            html += final.title ? `🎬 标题:<b>${final.title}</b><br>` : '';
            
            let se = '';
            if(final.season != null) se += `S${final.season} `;
            if(final.episode != null) se += `E${final.episode}`;
            html += se ? `📺 季集:${se}<br>` : '';
            
            html += final.year ? `📅 年份:${final.year}<br>` : '';
            html += final.team ? `🛠️ 制作:${final.team}<br>` : '';
            html += final.resolution ? `🖥️ 画质:${final.resolution}<br>` : '';
            
            // 修改处:使用直达链接
            if (final.tmdb_id) {
                 let detail_link = getTmdbLink(final.tmdb_id, final.category);
                 html += `🆔 TMDB:<a href="${detail_link}" target="_blank" style="color:#3e84f4;">${final.tmdb_id}</a>`;
            }
            tip.showText(html);
        } else {
            tip.showText(`⚠️ 未能识别出有效元数据`);
        }
    }).catch(error => {
        tip.showText(`❌ <b>错误:</b><br>${error.message}`);
    });
}

// --- 主执行逻辑 ---
(function () {
    'use strict';
    
    // UI 初始化
    class RecognizeTip {
        constructor() {
            const div = document.createElement('div');
            div.hidden = true;
            div.setAttribute('style', `
                position:absolute!important; font-size:13px!important; overflow:auto!important;
                background:#fff!important; font-family:sans-serif,Arial!important;
                text-align:left!important; color:#333!important; padding:10px!important;
                line-height:1.6em!important; border-radius:8px!important;
                border:1px solid #ddd!important; box-shadow:0 4px 12px rgba(0,0,0,0.15)!important;
                max-width:300px!important; z-index:999999!important;
            `);
            document.documentElement.appendChild(div);
            div.addEventListener('mouseup', e => e.stopPropagation());
            this._tip = div;
        }
        showText(text) { this._tip.innerHTML = text; this._tip.hidden = !1; }
        hide() { this._tip.hidden = true; }
        pop(ev) {
            this._tip.style.top = ev.pageY + 15 + 'px';
            this._tip.style.left = (ev.pageX + 320 <= document.body.clientWidth ? ev.pageX : ev.pageX - 320) + 'px';
        }
    }
    const tip = new RecognizeTip();

    class Icon {
        constructor() {
            const icon = document.createElement('span');
            icon.hidden = true;
            icon.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#3e84f4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>`;
            icon.setAttribute('style', `
                width:28px!important; height:28px!important; background:#fff!important;
                border-radius:50%!important; box-shadow:0 2px 8px rgba(0,0,0,0.2)!important;
                position:absolute!important; z-index:999999!important; display:flex;
                align-items:center; justify-content:center; cursor:pointer;
            `);
            document.documentElement.appendChild(icon);
            icon.addEventListener('mousedown', e => e.preventDefault(), true);
            icon.addEventListener('click', ev => {
                const text = window.getSelection().toString().trim();
                if (text) {
                    this._icon.hidden = true;
                    tip.pop(ev);
                    creatRecognizeTip(tip, text);
                }
            });
            this._icon = icon;
        }
        pop(ev) {
            this._icon.style.top = ev.pageY + 10 + 'px';
            this._icon.style.left = ev.pageX + 10 + 'px';
            this._icon.hidden = !1;
        }
        hide() { this._icon.hidden = true; }
    }
    const icon = new Icon();

    document.addEventListener('mouseup', function (e) {
        var text = window.getSelection().toString().trim();
        if (!text) {
            icon.hide();
            tip.hide();
        } else if (windowPopup) {
            icon.pop(e);
        }
    });

    // 站点适配逻辑
    if (site_domain.includes('m-team')) {
        waitForElements(['ant-descriptions-row']).then((elementsArray) => {
            ptype = 'm-team';
            let rows = elementsArray[0];
            let torrent_name = "";
            try {
                 torrent_name = rows[0].innerText.split('\n')[1] || rows[0].textContent; 
            } catch(e) {}
            
            let table = rows[0].parentNode;
            let row = table.insertRow(2);
            row.className = 'ant-descriptions-row';
            if (torrent_name) creatRecognizeRow(row, ptype, torrent_name);
        }).catch(() => {});
    } 
    else if (site_domain.includes('hhanclub')) {
        waitForElements(['font-bold leading-6']).then((elementsArray) => {
            ptype = 'hhanclub';
            let divs = elementsArray[0];
            let torrent_name = divs[3].innerText; 
            if (torrent_name) {
                divs[3].insertAdjacentHTML('afterend', '<div class="font-bold leading-6">ANIME Pro Matcher</div><div class="font-light leading-6 flex flex-wrap"><div id="apm_result" class="font-light leading-6 flex"></div></div>');
                let row = document.getElementById("apm_result");
                creatRecognizeRow(row, ptype, torrent_name);
            }
        }).catch(() => {});
    } 
    else {
        waitForElements(['rowhead']).then((elementsArray) => {
            ptype = 'common';
            let rows = elementsArray[0];
            let torrent_name = "";
            try {
                 let link = rows[0].nextElementSibling.querySelector('a');
                 if(link) torrent_name = link.innerText || link.title;
                 if(!torrent_name) torrent_name = rows[0].nextElementSibling.innerText;
                 torrent_name = torrent_name.replace(/^\[.*?\]\s*/, '');
            } catch (e) {}

            let table = rows[1].parentNode.parentNode.parentNode;
            if (table.tagName !== 'TABLE') table = table.closest('table');
            
            if (torrent_name) {
                let row = table.insertRow(2);
                creatRecognizeRow(row, ptype, torrent_name);
            }
        }).catch(() => {});
    }
})();