ANIME Pro Matcher Client

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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(() => {});
    }
})();