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(() => {});
    }
})();