Sleazy Fork is available in English.

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

})();