Pter Galgame Helper

Fetches game data from VNDB or Ymgal API and fills the upload form on PterClub. Now supports Steam and DLsite data integration with priority controls.

// ==UserScript==
// @name         Pter Galgame Helper
// @name:zh-CN   Pter Galgame 助手
// @namespace    http://tampermonkey.net/
// @version      5.1 (External Links Integration)
// @description  Fetches game data from VNDB or Ymgal API and fills the upload form on PterClub. Now supports Steam and DLsite data integration with priority controls.
// @description:zh-CN 通过 VNDB 或月幕 API 获取游戏信息,并自动填充 PterClub 的游戏上传页面。支持混合模式。新增Steam和DLsite数据集成,支持优先级控制。
// @author       Luofengyuan (AI Refactored & Enhanced)
// @match        https://pterclub.com/uploadgameinfo.php*
// @connect      api.vndb.org
// @connect      www.ymgal.games
// @connect      store.steampowered.com
// @connect      dlsite.com
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM.setValue
// @grant        GM.getValue
// @require      https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
// ==/UserScript==

(async function() {
    'use strict';

    // --- 全局常量 ---
    const VNDB_API_ENDPOINT = 'https://api.vndb.org/kana';
    const YM_API_ENDPOINT = 'https://www.ymgal.games';
    const YM_CLIENT_ID = 'ymgal';
    const YM_CLIENT_SECRET = 'luna0327';

    // --- 优先级开关配置 ---
    const STEAM_PRIORITY = true;    // 是否优先使用Steam数据覆盖VNDB数据
    const DLSITE_PRIORITY = true;   // 是否优先使用Dlsite数据覆盖VNDB数据

    // --- 全局状态与凭证 ---
    let scriptState = {
        source: 'hybrid', // 数据源模式: 'hybrid', 'vndb', 'ymgal'
        hybridSelection: { vndbGame: null, vndbRelease: null, ymgalGame: null }, // 混合模式下的选择状态
        vndbGameDetails: null, // 存储VNDB游戏详情
        vndbReleases: [],      // 存储VNDB发行版本列表
        ymgalGameDetails: null, // 存储月幕游戏详情
    };
    // 月幕 API 的访问令牌
    let ymgalAccessToken = await GM.getValue("ymgal_access_token", null);
    let ymgalTokenExpiresAt = await GM.getValue("ymgal_token_expires_at", 0);

    // --- 样式注入 ---
    GM_addStyle(`
        .pter-helper-container { display: flex; flex-direction: column; gap: 10px; margin-top: 5px; }
        .pter-source-selector { display: flex; gap: 15px; margin-bottom: 5px; }
        .pter-source-selector label { cursor: pointer; }
        .pter-search-bar { display: flex; width: 650px; }
        .pter-search-bar input[type="text"] { flex-grow: 1; margin-right: 5px; }
        .pter-results-panel { width: 650px; margin-top: 10px; }
        .pter-hybrid-container { display: flex; gap: 10px; }
        .pter-hybrid-panel { flex: 1; }
        .pter-step-title { font-weight: bold; margin-bottom: 5px; padding: 4px; background-color: #f5f5f5; border: 1px solid #ccc; border-bottom: none; }
        .pter-results-list { max-height: 220px; overflow-y: auto; border: 1px solid #ccc; background-color: #fff; }
        .pter-list-item { padding: 6px 10px; cursor: pointer; border-bottom: 1px solid #eee; }
        .pter-list-item:last-child { border-bottom: none; }
        .pter-list-item:hover { background-color: #e0e8f0; }
        .pter-list-item small { color: #555; display: block; margin-top: 2px; font-size: 11px; }
        .pter-loader { border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; width: 24px; height: 24px; animation: pter-spin 1s linear infinite; margin: 20px auto; }
        @keyframes pter-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
        .pter-status-message { padding: 10px; text-align: center; color: #333; }
        .pter-error-message { padding: 10px; text-align: center; color: #c00; }
        .pter-success-message { padding: 10px; text-align: center; color: green; font-weight: bold; }
        #hybrid-status-panel { padding: 8px; border: 1px dashed #3498db; margin-top: 10px; background-color: #f0f8ff; font-size: 12px; }
        #hybrid-status-panel .status-item { margin-bottom: 4px; }
        #hybrid-status-panel .status-ok { color: green; font-weight: bold; }
        #hybrid-status-panel .status-pending { color: #c00; }
    `);

    // --- API 请求层 ---
    function apiRequest(config) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                ...config,
                onload: res => {
                    if (res.status >= 200 && res.status < 400) {
                        try {
                            resolve(JSON.parse(res.responseText));
                        } catch (e) {
                            reject(new Error('JSON解析失败'));
                        }
                    } else {
                        reject(new Error(`API错误: ${res.status} ${res.statusText}`));
                    }
                },
                onerror: err => reject(new Error('网络请求错误')),
            });
        });
    }

    // --- VNDB API 封装 ---
    async function searchVndbByName(name) {
        const payload = { filters: ["search", "=", name], fields: "id, title", sort: "searchrank" };
        const data = await apiRequest({ method: 'POST', url: `${VNDB_API_ENDPOINT}/vn`, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(payload) });
        return data.results || [];
    }
    async function getVndbDetails(vnId) {
        const payload = { filters: ["id", "=", vnId], fields: "title,alttitle,description,image.url,screenshots.url,screenshots.sexual,developers.name,extlinks{url,name,id}" };
        const data = await apiRequest({ method: 'POST', url: `${VNDB_API_ENDPOINT}/vn`, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(payload) });
        return data.results?.[0] || null;
    }
    async function getVndbReleasesForVn(vnId) {
        const payload = { filters: ["vn", "=", ["id", "=", vnId]], fields: "id,title,released,platforms,resolution,voiced,engine,producers.name,producers.publisher,languages.lang,extlinks{url,name,id}", sort: "released", results: 100 };
        const data = await apiRequest({ method: 'POST', url: `${VNDB_API_ENDPOINT}/release`, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(payload) });
        return data.results || [];
    }

    // --- 月幕(Ymgal) API 封装 ---
    async function getYmgalAccessToken() {
        if (ymgalAccessToken && Date.now() < ymgalTokenExpiresAt) {
            return ymgalAccessToken;
        }
        const params = new URLSearchParams({ grant_type: 'client_credentials', client_id: YM_CLIENT_ID, client_secret: YM_CLIENT_SECRET, scope: 'public' });
        const data = await apiRequest({ method: 'GET', url: `${YM_API_ENDPOINT}/oauth/token?${params.toString()}` });
        if (data.access_token) {
            ymgalAccessToken = data.access_token;
            // 提前5分钟刷新
            ymgalTokenExpiresAt = Date.now() + (data.expires_in - 300) * 1000;
            await GM.setValue("ymgal_access_token", ymgalAccessToken);
            await GM.setValue("ymgal_token_expires_at", ymgalTokenExpiresAt);
            return ymgalAccessToken;
        }
        throw new Error('月幕认证失败: ' + (data.error_description || '未知错误'));
    }

    async function ymgalApiRequest(path, paramsObj) {
        const token = await getYmgalAccessToken();
        const url = new URL(`${YM_API_ENDPOINT}${path}`);
        if (paramsObj) {
            url.search = new URLSearchParams(paramsObj).toString();
        }
        const data = await apiRequest({ method: 'GET', url: url.href, headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/json;charset=utf-8', 'version': '1' } });
        if (data.success) {
            return data.data;
        }
        throw new Error(data.msg || `月幕API错误码: ${data.code}`);
    }

    async function searchYmgalByName(name) {
        const data = await ymgalApiRequest('/open/archive/search-game', { mode: 'list', keyword: name, pageNum: 1, pageSize: 20 });
        return data.result || [];
    }
    async function getYmgalGameDetails(gid) {
        return await ymgalApiRequest('/open/archive', { gid });
    }
    async function getYmgalOrgDetails(orgId) {
        const data = await ymgalApiRequest('/open/archive', { orgId });
        return data.org || null;
    }

    // --- 外部链接检测和数据获取 ---
    function findExternalLinks(vnDetails, releases) {
        const links = { steam: [], dlsite: [] };

        // 检查VN级别的外部链接
        if (vnDetails.extlinks) {
            vnDetails.extlinks.forEach(link => {
                if (link.name === 'steam') {
                    links.steam.push({ url: link.url, id: link.id });
                } else if (link.name === 'dlsite' || link.url.includes('dlsite.com')) {
                    links.dlsite.push({ url: link.url, id: link.id });
                }
            });
        }

        // 检查Release级别的外部链接
        releases.forEach(release => {
            if (release.extlinks) {
                release.extlinks.forEach(link => {
                    if (link.name === 'steam') {
                        links.steam.push({ url: link.url, id: link.id, releaseId: release.id });
                    } else if (link.name === 'dlsite' || link.url.includes('dlsite.com')) {
                        links.dlsite.push({ url: link.url, id: link.id, releaseId: release.id });
                    }
                });
            }
        });

        return links;
    }

    async function fetchSteamData(steamId) {
        try {
            const url = `https://store.steampowered.com/api/appdetails?l=schinese&appids=${steamId}`;
            // 直接调用2.js的steam_form逻辑,但返回数据而不是填充表单
            window.steamid = steamId; // 设置全局steamid变量供2.js使用

            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: "GET",
                    url: url,
                    responseType: "json",
                    onload: async function(response) {
                        try {
                            if (response.response[steamId] && response.response[steamId].success) {
                                // 使用2.js的逻辑处理Steam数据,但返回数据对象而不是填充表单
                                resolve({
                                    source: 'steam',
                                    usesSteamForm: true,
                                    steamId: steamId,
                                    steamResponse: response
                                });
                            } else {
                                resolve(null);
                            }
                        } catch (e) {
                            console.error('Steam数据处理失败:', e);
                            resolve(null);
                        }
                    },
                    onerror: function(error) {
                        console.error('Steam数据获取失败:', error);
                        resolve(null);
                    }
                });
            });
        } catch (e) {
            console.error('Steam数据获取失败:', e);
        }
        return null;
    }

    async function fetchDlsiteData(dlsiteUrl) {
        try {
            // 从URL中提取DLsite作品ID
            const workIdMatch = dlsiteUrl.match(/\/work\/=\/product_id\/(\w+)/);
            if (!workIdMatch) return null;

            const workId = workIdMatch[1];

            // 直接请求DLsite页面
            const response = await apiRequest({
                method: 'GET',
                url: dlsiteUrl,
                headers: {
                    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
                }
            });

            // 解析HTML内容(这里需要根据实际的DLsite页面结构调整)
            const html = response.responseText || response;

            // 提取基本信息
            const titleMatch = html.match(/<h1[^>]*id="work_name"[^>]*>([^<]+)</);
            const title = titleMatch ? titleMatch[1].trim() : '';

            // 提取系统要求(基于提供的HTML结构)
            const sysReqMatch = html.match(/<div class="work_article work_spec">([\s\S]*?)<\/div>/);
            let systemRequirements = '暂无详细配置信息';
            if (sysReqMatch) {
                const specContent = sysReqMatch[1];
                // 解析dl/dt结构并格式化
                const dlPattern = /<dl[^>]*>([\s\S]*?)<\/dl>/g;
                const dtPattern = /<dt[^>]*>([^<]+)<\/dt>/g;
                const ddPattern = /<dd[^>]*>([^<]+)<\/dd>/g;

                let formattedSpecs = [];

                // 提取所有dt和dd对
                const dtMatches = Array.from(specContent.matchAll(dtPattern));
                const ddMatches = Array.from(specContent.matchAll(ddPattern));

                for (let i = 0; i < Math.min(dtMatches.length, ddMatches.length); i++) {
                    const label = dtMatches[i][1].trim();
                    const value = ddMatches[i][1].trim();
                    if (label && value) {
                        formattedSpecs.push(`${label}:${value}`);
                    }
                }

                if (formattedSpecs.length > 0) {
                    systemRequirements = formattedSpecs.join('\n');
                } else {
                    // 备用解析方法:简单去除HTML标签并添加换行
                    systemRequirements = specContent
                        .replace(/<dt[^>]*>/g, '\n')
                        .replace(/<\/dt>/g, ':')
                        .replace(/<dd[^>]*>/g, '')
                        .replace(/<\/dd>/g, '')
                        .replace(/<[^>]*>/g, '')
                        .replace(/\s+/g, ' ')
                        .split(/\n+/)
                        .filter(line => line.trim())
                        .join('\n')
                        .trim();
                }
            }

            // 提取封面图片
            const coverMatch = html.match(/<img[^>]*class="[^"]*main_work_img[^"]*"[^>]*src="([^"]+)"/);
            const coverImage = coverMatch ? coverMatch[1] : '';

            return {
                source: 'dlsite',
                japaneseTitle: title,
                systemRequirements: systemRequirements,
                coverImage: coverImage,
                screenshots: [], // DLsite截图需要额外处理
                workId: workId
            };
        } catch (e) {
            console.error('DLsite数据获取失败:', e);
        }
        return null;
    }

    // --- 数据适配层 (将不同来源的数据统一为同一格式) ---
    async function adaptVndbData(vnDetails, releaseDetails, statusCallback) {
        // 检查外部链接
        const externalLinks = findExternalLinks(vnDetails, scriptState.vndbReleases);
        let steamData = null;
        let dlsiteData = null;

        // 根据优先级开关获取外部数据
        if (STEAM_PRIORITY && externalLinks.steam.length > 0) {
            if (statusCallback) statusCallback('正在获取Steam数据...');
            const steamId = externalLinks.steam[0].id;
            steamData = await fetchSteamData(steamId);

            // 如果检测到Steam数据且要使用2.js的方法,直接返回特殊标识
            if (steamData && steamData.usesSteamForm) {
                return {
                    source: 'steam-2js',
                    useSteamForm: true,
                    steamId: steamId,
                    steamResponse: steamData.steamResponse,
                    vndbData: { vnDetails, releaseDetails } // 保留VNDB数据作为备用
                };
            }
        }

        if (DLSITE_PRIORITY && externalLinks.dlsite.length > 0) {
            if (statusCallback) statusCallback('正在获取DLsite数据...');
            dlsiteData = await fetchDlsiteData(externalLinks.dlsite[0].url);
        }

        const coverImage = vnDetails.image?.url || '';

        // 筛选截图:优先选择安全(sexual=0)的图片,最多5张。如果不足3张,用暗示性(sexual=1)的图片补充。
        let screenshots = [];
        if (vnDetails.screenshots) {
            const safe = vnDetails.screenshots.filter(s => s.sexual === 0).map(s => s.url);
            const suggestive = vnDetails.screenshots.filter(s => s.sexual === 1).map(s => s.url);
            screenshots = safe.slice(0, 5);
            if (screenshots.length < 3) {
                screenshots.push(...suggestive.slice(0, 3 - screenshots.length));
            }
        }

        const voicedMap = { 1: '无语音', 2: '仅H场景', 3: '部分语音', 4: '全语音' };
        let requirements = [];
        if (releaseDetails.platforms?.length) requirements.push(`平台:${releaseDetails.platforms.join(', ')}`);
        if (releaseDetails.engine) requirements.push(`引擎:${releaseDetails.engine}`);
        if (Array.isArray(releaseDetails.resolution)) requirements.push(`分辨率:${releaseDetails.resolution.join('x')}`);
        if (releaseDetails.voiced) requirements.push(`语音:${voicedMap[releaseDetails.voiced] || '未知'}`);

        // 查找同一平台的最早发行版本,以确定年份
        const platformReleases = scriptState.vndbReleases
            .filter(r => r.platforms?.includes(releaseDetails.platforms?.[0]) && r.released)
            .sort((a, b) => new Date(a.released) - new Date(b.released));

        // 构建基础数据
        let unifiedData = {
            source: 'vndb',
            englishTitle: vnDetails.title || '',
            japaneseTitle: vnDetails.alttitle || '',
            developer: vnDetails.developers?.map(d => d.name).join(', ') || 'N/A',
            publisher: releaseDetails.producers?.filter(p => p.publisher).map(p => p.name).join(', ') || 'N/A',
            firstReleaseDate: releaseDetails.released || '',
            systemRequirements: requirements.length ? requirements.join('\n') : '暂无详细配置信息',
            introduction: vnDetails.description?.replace(/\[url=.*?\](.*?)\[\/url\]/g, '$1') || '', // 移除BBCode链接
            coverImage: coverImage,
            screenshots: screenshots,
            primaryPlatform: releaseDetails.platforms?.[0] || null,
            platformReleases: platformReleases,
            externalSources: []
        };

        // 根据优先级合并DLsite数据
        if (dlsiteData && DLSITE_PRIORITY) {
            unifiedData.source = 'vndb+dlsite';
            unifiedData.systemRequirements = dlsiteData.systemRequirements || unifiedData.systemRequirements;
            if (dlsiteData.coverImage) unifiedData.coverImage = dlsiteData.coverImage;
            if (dlsiteData.japaneseTitle) unifiedData.japaneseTitle = dlsiteData.japaneseTitle;
            unifiedData.externalSources.push('DLsite');
        }

        return unifiedData;
    }

    async function adaptYmgalData(ymgalDetails, selectedRelease) {
        let developerName = 'N/A';
        if (ymgalDetails.game.developerId) {
            try {
                const org = await getYmgalOrgDetails(ymgalDetails.game.developerId);
                if (org) developerName = org.chineseName || org.name;
            } catch (e) {
                console.error("获取月幕开发者信息失败:", e);
            }
        }

        const platform = selectedRelease.platform || 'N/A';
        const platformReleases = ymgalDetails.game.releases
            .filter(r => r.platform === platform && r.release_date)
            .sort((a, b) => new Date(a.release_date) - new Date(b.release_date));

        return {
            source: 'ymgal',
            englishTitle: ymgalDetails.game.chineseName || ymgalDetails.game.name,
            japaneseTitle: ymgalDetails.game.name || '',
            developer: developerName,
            publisher: 'N/A', // 月幕API不直接提供发行商信息
            firstReleaseDate: selectedRelease.release_date || '',
            systemRequirements: `平台:${platform}`,
            introduction: ymgalDetails.game.introduction || '',
            coverImage: ymgalDetails.game.mainImg || '',
            screenshots: [], // 月幕API不提供截图
            primaryPlatform: platform,
            platformReleases: platformReleases,
        };
    }

    // --- 表单填充辅助函数 ---
    const setFieldValue = (selector, value) => {
        const el = document.querySelector(selector);
        if (el) el.value = value;
        else console.warn('Pter 助手: 表单元素未找到:', selector);
    };
    const setCheckboxState = (selector, checked) => {
        const el = document.querySelector(selector);
        if (el) el.checked = checked;
        else console.warn('Pter 助手: 表单元素未找到:', selector);
    };
    const setSelectOption = (selector, targetText) => {
        const selectEl = document.querySelector(selector);
        if (!selectEl) {
            console.warn('Pter 助手: 表单元素未找到:', selector);
            return;
        }
        const option = Array.from(selectEl.options).find(opt => opt.textContent.trim() === targetText);
        if (option) option.selected = true;
    };

    // 从2.js移植的辅助函数
    function html2bb(str) {
        if (!str) return "";
        str = str.replace(/< *br *\/*>/g, "\n\n");
        str = str.replace(/< *b *>/g, "[b]");
        str = str.replace(/< *\/ *b *>/g, "[/b]");
        str = str.replace(/< *u *>/g, "[u]");
        str = str.replace(/< *\/ *u *>/g, "[/u]");
        str = str.replace(/< *i *>/g, "[i]");
        str = str.replace(/< *\/ *i *>/g, "[/i]");
        str = str.replace(/< *strong *>/g, "[b]");
        str = str.replace(/< *\/ *strong *>/g, "[/b]");
        str = str.replace(/< *em *>/g, "[i]");
        str = str.replace(/< *\/ *em *>/g, "[/i]");
        str = str.replace(/< *li *>/g, "[*]");
        str = str.replace(/< *\/ *li *>/g, "");
        str = str.replace(/< *ul *class=\\*\"bb_ul\\*\" *>/g, "");
        str = str.replace(/< *\/ *ul *>/g, "");
        str = str.replace(/< *h2 *class=\"bb_tag\" *>/g, "\n[center][u][b]");
        str = str.replace(/< *h[1234] *>/g, "\n[center][u][b]");
        str = str.replace(/< *\/ *h[1234] *>/g, "[/b][/u][/center]\n");
        str = str.replace(/\&quot;/g, "\"");
        str = str.replace(/\&amp;/g, "&");
        str = str.replace(/< *img *src="([^"]*)".*>/g, "\n");
        str = str.replace(/< *img.*src="([^"]*)".*>/g, "\n");
        str = str.replace(/< *a [^>]*>/g, "");
        str = str.replace(/< *\/ *a *>/g, "");
        str = str.replace(/< *p *>/g, "\n\n");
        str = str.replace(/< *\/ *p *>/g, "");
        str = str.replace(/  +/g, " ");
        str = str.replace(/\n +/g, "\n");
        str = str.replace(/\n\n\n+/gm, "\n\n");
        str = str.replace(/\[\/b\]\[\/u\]\[\/align\]\n\n/g, "[/b][/u][/align]\n");
        str = str.replace(/\n\n\[\*\]/g, "\n[*]");
        str = str.replace(/< *video.*>\n.*?< *\/ *video *>/g,'');
        str = str.replace(/<hr>/g,'\n\n');
        return str;
    }

    function pretty_sr(str) {
        return str.replace(/\n+/g, "\n").trim();
    }

    // --- 核心填充逻辑 ---
    async function populateForm(data, panel) {
        const statusContainer = panel || document.querySelector('.pter-results-panel');

        // 如果是Steam数据,使用2.js的逻辑
        if (data.useSteamForm) {
            render(statusContainer, `<div class="pter-status-message">使用Steam数据填充表单...</div>`);
            try {
                await fillSteamForm(data.steamResponse, data.steamId);
                render(statusContainer, `<div class="pter-success-message">✅ 填充成功!数据来源: STEAM<br>请仔细检查并手动调整。</div>`);
                return;
            } catch (e) {
                console.error('Steam填充失败,回退到VNDB数据:', e);
                // 回退到VNDB数据
                data = await adaptVndbData(data.vndbData.vnDetails, data.vndbData.releaseDetails);
            }
        }

        render(statusContainer, `<div class="pter-status-message">正在生成介绍内容...</div>`);

        // 直接使用原始链接,不再转存
        const finalCoverUrl = data.coverImage;
        const finalScreenshotUrls = data.screenshots;

        setFieldValue('input[name="small_descr"]', data.japaneseTitle);
        setFieldValue('input[name="name"]', data.englishTitle);

        let bbCode = `[center]${finalCoverUrl ? `[img]${finalCoverUrl}[/img]` : ''}[/center]\n\n`;
        bbCode += `[center][b]基本信息[/b]\n`;
        bbCode += `日文名称:${data.japaneseTitle}\n`;
        bbCode += `中文名称:${data.englishTitle}\n`;
        bbCode += `开发商:${data.developer}\n`;
        bbCode += `发行商:${data.publisher}\n`;
        bbCode += `首发日期:${data.firstReleaseDate}[/center]\n\n`;
        bbCode += `[center][b]配置要求[/b]\n${data.systemRequirements}[/center]\n\n`;
        bbCode += `[center][b]游戏简介[/b][/center]\n${data.introduction}\n\n`;

        if (finalScreenshotUrls.length > 0) {
            bbCode += `[center][b]游戏截图[/b][/center]\n`;
            bbCode += finalScreenshotUrls.map(url => `[center][img]${url}[/img][/center]\n`).join('');
        }
        setFieldValue('textarea[name="descr"]', bbCode);

        // 平台映射
        const platformMap = { 'win': 'Windows', 'lin': 'Linux', 'mac': 'MAC', 'and': 'Android', 'ios': 'iOS', 'ps1': 'PS/PSone', 'ps2': 'PS2', 'ps3': 'PS3', 'ps4': 'PS4', 'ps5': 'PS5', 'psp': 'PSP', 'psv': 'PS Vita', 'sfc': 'SFC/SNES', 'n64': 'N64', 'nds': 'DS', '3ds': '3DS', 'swi': 'Switch', 'wii': 'Wii/WiiU', 'dos': 'DOS', 'xbo': 'Xbox', 'xb3': 'Xbox 360', 'Windows': 'Windows', 'PC': 'Windows', 'Linux': 'Linux', 'macOS': 'MAC' };
        const targetText = platformMap[data.primaryPlatform] || data.primaryPlatform;
        if (targetText) {
            setSelectOption('select[name="console"]', targetText);
        }

        // 填充年份
        if (data.platformReleases && data.platformReleases.length > 0) {
            const releaseDate = data.source === 'vndb' ? data.platformReleases[0].released : data.platformReleases[0].release_date;
            setFieldValue('input[name="year"]', new Date(releaseDate).getFullYear());
        } else if (data.firstReleaseDate) {
            setFieldValue('input[name="year"]', new Date(data.firstReleaseDate).getFullYear());
        } else {
            setFieldValue('input[name="year"]', '');
        }

        setCheckboxState('input[name="uplver"]', true); // 勾选"匿名发布"
        setFieldValue('input[name="releasedate"]', data.firstReleaseDate);

        // 显示数据来源信息
        let sourceInfo = `数据来源: ${data.source.toUpperCase()}`;
        if (data.externalSources && data.externalSources.length > 0) {
            sourceInfo += ` (增强: ${data.externalSources.join(', ')})`;
        }

        render(statusContainer, `<div class="pter-success-message">✅ 填充成功!${sourceInfo}<br>请仔细检查并手动调整。</div>`);
    }

    // 使用2.js的Steam填充逻辑
    async function fillSteamForm(response, steamid) {
        const gameInfo = response.response[steamid].data;
        const about = gameInfo.about_the_game;
        const date = gameInfo.release_date.date.split(", ").pop();
        const year = date.split("年").shift().trim();
        const store = 'https://store.steampowered.com/app/' + steamid;
        let genres = [];
        if (gameInfo.hasOwnProperty('genres')) {
            gameInfo.genres.forEach(function (genre) {
                const tag = genre.description.toLowerCase().replace(/ /g, ".");
                genres.push(tag);
            });
        }
        genres = genres.join(",");

        const aboutContent = about || gameInfo.detailed_description;
        const aboutSection = "[center][b][u]关于游戏[/u][/b][/center]\n" +
                            `[b]发行日期[/b]:${date}\n\n[b]商店链接[/b]:${store}\n\n[b]游戏标签[/b]:${genres}\n\n` +
                            html2bb(aboutContent).trim();

        // 处理系统要求
        let recfield = gameInfo.pc_requirements;
        if (typeof(recfield.recommended) === "undefined") {
            recfield.recommended = '\n无推荐配置要求';
        }
        if (typeof(recfield.minimum) === "undefined") {
            recfield.minimum = '\n无配置要求';
            recfield.recommended = '';
        }

        const sr = "\n\n[center][b][u]配置要求[/u][/b][/center]\n\n" +
                   pretty_sr(html2bb("[quote]\n" + recfield.minimum + "\n" + recfield.recommended + "[/quote]\n"));

        // 预告片
        let tr = '';
        try {
            const trailer = gameInfo.movies[0].webm.max.split("?")[0].replace("http","https");
            tr = "\n\n[center][b][u]预告欣赏[/u][/b][/center]\n" + `[center][video]${trailer}[/video][/center]`;
        } catch (e) {
            tr = '';
        }

        // 填充表单
        setFieldValue('input[name="name"]', gameInfo.name);
        setFieldValue('input[name="year"]', year);

        const coverImage = gameInfo.header_image.split("?")[0];
        const screenshots = gameInfo.screenshots.map(s => s.path_full.split("?")[0]);

        let screensBB = '';
        if (screenshots.length > 0) {
            screensBB = "[center][b][u]游戏截图[/u][/b][/center]\n[center]" +
                       screenshots.map(url => `[img]${url}[/img]`).join('\n') +
                       "[/center]";
        }

        const cover = `[center][img]${coverImage}[/img][/center]`;
        const fullContent = cover + aboutSection + sr + tr + (screensBB ? '\n\n' + screensBB : '');

        setFieldValue('textarea[name="descr"]', fullContent);
        setCheckboxState('input[name="uplver"]', true);
    }

    // --- UI 渲染和事件处理 ---
    function render(container, content) {
        if (typeof content === 'string') {
            container.innerHTML = content;
        } else {
            container.innerHTML = '';
            container.appendChild(content);
        }
    }
    const createLoader = () => '<div class="pter-loader"></div>';

    // 单一数据源模式:选择游戏后的处理
    async function handleSingleSourceGameSelect(gameId, panel, source) {
        render(panel, createLoader());
        try {
            if (source === 'vndb') {
                const [vnDetails, releases] = await Promise.all([getVndbDetails(gameId), getVndbReleasesForVn(gameId)]);
                if (!vnDetails) throw new Error('无法获取VNDB游戏详情');
                scriptState.vndbGameDetails = vnDetails;
                scriptState.vndbReleases = releases;
                displayReleaseSelection(releases, panel, 'vndb');
            } else { // 'ymgal'
                const details = await getYmgalGameDetails(gameId);
                if (!details?.game) throw new Error('无法获取月幕游戏详情');
                scriptState.ymgalGameDetails = details;
                displayReleaseSelection(details.game.releases, panel, 'ymgal');
            }
        } catch (e) {
            render(panel, `<div class="pter-error-message">错误: ${e.message}</div>`);
        }
    }

    // 显示发行版本选择列表
    function displayReleaseSelection(releases, panel, source) {
        if (!releases?.length) {
            render(panel, '<div class="pter-status-message">此游戏没有找到任何发行版本。</div>');
            return;
        }
        const sortedReleases = [...releases].sort((a, b) => new Date(b.released || b.release_date) - new Date(a.released || a.release_date));

        const list = document.createElement('div');
        list.className = 'pter-results-list';
        sortedReleases.forEach(release => {
            const item = document.createElement('div');
            item.className = 'pter-list-item';
            let title, subtitle;
            if (source === 'vndb') {
                title = release.title;
                subtitle = `${release.released || 'N/A'} | ${release.platforms?.join(', ') || 'N/A'} | ${release.languages?.map(l => l.lang).join(', ') || 'N/A'}`;
            } else { // 'ymgal'
                title = release.releaseName;
                subtitle = `${release.release_date || 'N/A'} | ${release.platform || 'N/A'} | ${release.releaseLanguage || 'N/A'}`;
            }
            item.innerHTML = `${title} <small>${subtitle}</small>`;
            item.onclick = () => handleReleaseSelection(release.id, panel, source);
            list.appendChild(item);
        });

        const container = document.createElement('div');
        const titleEl = document.createElement('div');
        titleEl.className = 'pter-step-title';
        titleEl.textContent = '步骤 2: 选择一个发行版本';
        container.append(titleEl, list);
        render(panel, container);
    }

    // 选择发行版本后的最终处理
    async function handleReleaseSelection(releaseId, panel, source) {
        render(panel, createLoader());
        try {
            let unifiedData;
            if (source === 'vndb') {
                const selectedRelease = scriptState.vndbReleases.find(r => r.id === releaseId);
                if (!selectedRelease) throw new Error('未找到所选的版本信息');
                const statusCallback = (msg) => render(panel, `<div class="pter-status-message">${msg}</div>`);
                unifiedData = await adaptVndbData(scriptState.vndbGameDetails, selectedRelease, statusCallback);
            } else { // 'ymgal'
                const selectedRelease = scriptState.ymgalGameDetails.game.releases.find(r => r.id === releaseId);
                if (!selectedRelease) throw new Error('未找到所选的版本信息');
                unifiedData = await adaptYmgalData(scriptState.ymgalGameDetails, selectedRelease);
            }
            await populateForm(unifiedData, panel);
        } catch (e) {
            render(panel, `<div class="pter-error-message">处理失败: ${e.message}</div>`);
        }
    }

    // --- 混合模式专属逻辑 ---
    function renderHybridStatusPanel() {
        const panel = document.getElementById('hybrid-status-panel');
        if (!panel) return;
        const { vndbRelease, ymgalGame } = scriptState.hybridSelection;
        let html = '';
        html += `<div class="status-item">VNDB源: <span class="${vndbRelease ? 'status-ok' : 'status-pending'}">${vndbRelease ? `${vndbRelease.title} (已选)` : '待选择'}</span></div>`;
        html += `<div class="status-item">月幕源: <span class="${ymgalGame ? 'status-ok' : 'status-pending'}">${ymgalGame ? `${ymgalGame.game.chineseName || ymgalGame.game.name} (已选)` : '待选择'}</span></div>`;
        panel.innerHTML = html;

        if (vndbRelease && ymgalGame) {
            const button = document.createElement('input');
            button.type = 'button';
            button.value = '生成混合信息';
            button.onclick = handleHybridGeneration;
            panel.appendChild(button);
        }
    }

    async function handleHybridVndbGameSelect(gameId, panel) {
        render(panel, createLoader());
        try {
            const [vnDetails, releases] = await Promise.all([getVndbDetails(gameId), getVndbReleasesForVn(gameId)]);
            if (!vnDetails || !releases?.length) {
                render(panel, `<div class="pter-error-message">此游戏无详情或发行版本</div>`);
                return;
            }
            scriptState.hybridSelection.vndbGame = vnDetails;
            scriptState.vndbReleases = releases;

            const sortedReleases = [...releases].sort((a, b) => new Date(b.released) - new Date(a.released));
            const list = document.createElement('div');
            list.className = 'pter-results-list';
            sortedReleases.forEach(release => {
                const item = document.createElement('div');
                item.className = 'pter-list-item';
                item.innerHTML = `${release.title} <small>${release.released || 'N/A'} | ${release.platforms?.join(', ')}</small>`;
                item.onclick = () => {
                    scriptState.hybridSelection.vndbRelease = release;
                    renderHybridStatusPanel();
                    render(panel, `<div class="pter-success-message">VNDB源已选定: ${release.title}</div>`);
                };
                list.appendChild(item);
            });
            const container = document.createElement('div');
            const titleEl = document.createElement('div');
            titleEl.className = 'pter-step-title';
            titleEl.textContent = '请选择一个VNDB版本';
            container.append(titleEl, list);
            render(panel, container);
        } catch(e) {
            render(panel, `<div class="pter-error-message">处理失败: ${e.message}</div>`);
        }
    }

    async function handleHybridYmgalGameSelect(gameId, panel) {
        render(panel, createLoader());
        try {
            const details = await getYmgalGameDetails(gameId);
            if (!details?.game) {
                render(panel, `<div class="pter-error-message">无法获取月幕游戏详情</div>`);
                return;
            }
            scriptState.hybridSelection.ymgalGame = details;
            renderHybridStatusPanel();
            render(panel, `<div class="pter-success-message">月幕源已选定: ${details.game.chineseName || details.game.name}</div>`);
        } catch(e) {
            render(panel, `<div class="pter-error-message">处理失败: ${e.message}</div>`);
        }
    }

    async function handleHybridGeneration() {
        const { vndbGame, vndbRelease, ymgalGame } = scriptState.hybridSelection;
        if (!vndbGame || !vndbRelease || !ymgalGame) {
            alert("数据选择不完整!请确保VNDB和月幕源都已选定。");
            return;
        }
        const panel = document.querySelector('.pter-results-panel');
        render(panel, createLoader());
        try {
            // 使用VNDB数据作为基础
            const statusCallback = (msg) => render(panel, `<div class="pter-status-message">${msg}</div>`);
            const unifiedData = await adaptVndbData(vndbGame, vndbRelease, statusCallback);
            // 用月幕的中文简介覆盖VNDB的简介
            unifiedData.introduction = ymgalGame.game.introduction || '(无简介)';
            // 填充表单
            await populateForm(unifiedData, panel);
        } catch (e) {
            render(panel, `<div class="pter-error-message">生成混合信息失败: ${e.message}</div>`);
        }
    }

    // --- 主UI与启动逻辑 ---
    function initUI() {
        const referRow = document.querySelector('input[name="detailsgameinfoid"]')?.closest('tr');
        if (!referRow) {
            console.error("Pter 助手: 未找到用于注入UI的参考位置。");
            return;
        }

        const container = document.createElement('div');
        container.className = 'pter-helper-container';

        const sourceSelector = document.createElement('div');
        sourceSelector.className = 'pter-source-selector';
        sourceSelector.innerHTML = `<label><input type="radio" name="pter_source" value="hybrid" checked> 混合模式</label> <label><input type="radio" name="pter_source" value="vndb"> VNDB</label> <label><input type="radio" name="pter_source" value="ymgal"> 月幕</label>`;

        const searchBar = document.createElement('div');
        searchBar.className = 'pter-search-bar';
        const searchInput = document.createElement('input');
        searchInput.type = 'text';
        searchInput.placeholder = '在此输入游戏名 (日文/英文/中文),然后按回车或点击搜索...';
        const searchButton = document.createElement('input');
        searchButton.type = 'button';
        searchButton.value = '搜索';
        searchBar.append(searchInput, searchButton);

        const resultsPanel = document.createElement('div');
        resultsPanel.className = 'pter-results-panel';
        container.append(sourceSelector, searchBar, resultsPanel);

        const newRow = document.createElement('tr');
        const newRowHead = document.createElement('td');
        newRowHead.className = 'rowhead nowrap';
        newRowHead.vAlign = 'top';
        newRowHead.align = 'right';
        newRowHead.textContent = "Galgame 助手:";
        const newRowFollow = document.createElement('td');
        newRowFollow.className = 'rowfollow';
        newRowFollow.vAlign = 'top';
        newRowFollow.align = 'left';
        newRowFollow.appendChild(container);
        newRow.append(newRowHead, newRowFollow);
        referRow.parentNode.insertBefore(newRow, referRow.nextSibling);

        // 重置状态
        const resetState = () => {
            render(resultsPanel, '');
            scriptState = { ...scriptState, hybridSelection: { vndbGame: null, vndbRelease: null, ymgalGame: null }, vndbGameDetails: null, vndbReleases: [], ymgalGameDetails: null };
        };
        sourceSelector.querySelectorAll('input[name="pter_source"]').forEach(radio => {
            radio.onchange = () => {
                scriptState.source = radio.value;
                resetState();
            };
        });

        // 搜索处理函数
        const handleSearch = async () => {
            const query = searchInput.value.trim();
            if (!query) return;
            searchButton.disabled = true;
            searchButton.value = '搜索中...';
            resetState();
            render(resultsPanel, createLoader());

            try {
                if (scriptState.source === 'hybrid') {
                    // 并发请求VNDB和月幕
                    const [vndbResults, ymgalResults] = await Promise.all([
                        searchVndbByName(query).catch(e => { console.error("VNDB搜索失败:", e); return []; }),
                        searchYmgalByName(query).catch(e => { console.error("月幕搜索失败:", e); return []; })
                    ]);
                    resultsPanel.innerHTML = `<div id="hybrid-status-panel"></div> <div class="pter-hybrid-container"> <div id="vndb-panel" class="pter-hybrid-panel"></div> <div id="ymgal-panel" class="pter-hybrid-panel"></div> </div>`;
                    renderHybridStatusPanel();
                    displaySearchResults(vndbResults.map(vn => ({ id: vn.id, title: vn.title, subtitle: `VNDB ID: ${vn.id}` })), document.getElementById('vndb-panel'), 'vndb', true);
                    displaySearchResults(ymgalResults.map(game => ({ id: game.id, title: game.chineseName || game.name, subtitle: `${game.name} | ${game.releaseDate || 'N/A'}` })), document.getElementById('ymgal-panel'), 'ymgal', true);
                } else {
                    const results = scriptState.source === 'vndb' ? await searchVndbByName(query) : await searchYmgalByName(query);
                    const mapped = scriptState.source === 'vndb'
                        ? results.map(vn => ({ id: vn.id, title: vn.title, subtitle: `VNDB ID: ${vn.id}` }))
                        : results.map(game => ({ id: game.id, title: game.chineseName || game.name, subtitle: `${game.name} | ${game.releaseDate || 'N/A'}` }));
                    displaySearchResults(mapped, resultsPanel, scriptState.source, false);
                }
            } catch (e) {
                render(resultsPanel, `<div class="pter-error-message">搜索失败: ${e.message}</div>`);
            }
            finally {
                searchButton.disabled = false;
                searchButton.value = '搜索';
            }
        };

        function displaySearchResults(results, panel, source, isHybrid) {
            if (!results?.length) {
                render(panel, '<div class="pter-status-message">未找到相关结果</div>');
                return;
            }
            const list = document.createElement('div');
            list.className = 'pter-results-list';
            results.forEach(itemData => {
                const item = document.createElement('div');
                item.className = 'pter-list-item';
                item.innerHTML = `${itemData.title} <small>${itemData.subtitle}</small>`;
                if (isHybrid) {
                    item.onclick = source === 'vndb'
                        ? () => handleHybridVndbGameSelect(itemData.id, panel)
                        : () => handleHybridYmgalGameSelect(itemData.id, panel);
                } else {
                    item.onclick = () => handleSingleSourceGameSelect(itemData.id, panel, source);
                }
                list.appendChild(item);
            });
            const container = document.createElement('div');
            const title = document.createElement('div');
            title.className = 'pter-step-title';
            if (isHybrid) {
                title.textContent = source === 'vndb' ? 'VNDB源 (提供图片/配置等)' : '月幕源 (提供中文简介)';
            } else {
                title.textContent = '步骤 1: 选择游戏';
            }
            container.append(title, list);
            render(panel, container);
        }

        searchButton.onclick = handleSearch;
        searchInput.onkeydown = (e) => {
            if (e.key === 'Enter') handleSearch();
        };
    }

    // --- 脚本入口 ---
    if (window.location.href.includes('uploadgameinfo.php')) {
        initUI();
    }

})();