您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
通过 VNDB 或月幕 API 获取游戏信息,并自动填充 PterClub 的游戏上传页面。支持混合模式。新增Steam和DLsite数据集成,支持优先级控制。
// ==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(/\"/g, "\""); str = str.replace(/\&/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(); } })();