GGn Steam Language

Get Steam language support info for GazelleGames (based on stable HTML parsing)

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         GGn Steam Language
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  Get Steam language support info for GazelleGames (based on stable HTML parsing)
// @author       WhiteLycoris and Deepseek, thanks lucianjp
// @match        https://gazellegames.net/torrents.php?id=*
// @match        https://gazellegames.net/upload.php?groupid=*
// @grant        GM_xmlhttpRequest
// @grant        GM_setClipboard
// @license      MIT
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // Steam 页面解析器 (基于 lucianjp 的稳定逻辑)
    class SteamPageParser {
        constructor() {}

        async getLanguageData(appId) {
            try {
                const html = await this.fetchSteamPage(appId);
                const languages = this.extractLanguagesFromHTML(html);
                return languages;
            } catch (error) {
                console.error('[GGn Steam Language] Failed to get language data:', error);
                return {
                    interfaceSubs: [],
                    fullAudio: [],
                    error: true
                };
            }
        }

        async fetchSteamPage(appId) {
            return new Promise((resolve, reject) => {
                const timeout = setTimeout(() => {
                    reject(new Error('Page request timeout'));
                }, 10000);

                GM_xmlhttpRequest({
                    method: 'GET',
                    url: `https://store.steampowered.com/app/${appId}/`,
                    headers: {
                        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
                        'Accept-Language': 'en-US,en;q=0.9',
                    },
                    anonymous: true,
                    onload: (response) => {
                        clearTimeout(timeout);
                        if (response.status === 200) {
                            resolve(response.responseText);
                        } else if (response.status === 404) {
                            reject(new Error(`Steam app (ID: ${appId}) not found`));
                        } else {
                            reject(new Error(`Page returned ${response.status}`));
                        }
                    },
                    onerror: (error) => {
                        clearTimeout(timeout);
                        reject(error);
                    },
                    ontimeout: () => {
                        clearTimeout(timeout);
                        reject(new Error('Request timeout'));
                    }
                });
            });
        }

        extractLanguagesFromHTML(html) {
            const result = {
                interfaceSubs: [],
                fullAudio: [],
                error: false
            };

            const parser = new DOMParser();
            const doc = parser.parseFromString(html, 'text/html');
            const languageTable = doc.querySelector('table.game_language_options');

            if (!languageTable) {
                console.warn('[GGn Steam Language] Language table not found on Steam page.');
                result.error = true;
                return result;
            }

            const languages = this.parseLanguageTable(languageTable);

            for (const [category, langList] of Object.entries(languages)) {
                const catLower = category.toLowerCase();

                langList.forEach(lang => {
                    if (!result.interfaceSubs.includes(lang)) {
                        result.interfaceSubs.push(lang);
                    }
                });

                if (catLower.includes('full audio') || catLower.includes('audio')) {
                    langList.forEach(lang => {
                        if (!result.fullAudio.includes(lang)) {
                            result.fullAudio.push(lang);
                        }
                    });
                }
            }

            result.interfaceSubs.sort();
            result.fullAudio.sort();

            return result;
        }

        parseLanguageTable(table) {
            const languages = {};

            for (let r = 0; r < table.rows.length; r++) {
                for (let c = 0; c < table.rows[r].cells.length; c++) {
                    const cell = table.rows[r].cells[c];

                    if (cell.textContent.trim() === '✔' || cell.innerHTML.includes('✓')) {
                        if (r === 0) continue;

                        const header = table.rows[0].cells[c].textContent.trim();
                        const languageName = table.rows[r].cells[0].textContent.trim();

                        if (header && languageName) {
                            if (!languages[header]) {
                                languages[header] = [];
                            }
                            if (!languages[header].includes(languageName)) {
                                languages[header].push(languageName);
                            }
                        }
                    }
                }
            }

            return languages;
        }
    }

    // UI管理器 (保持你原有的便捷交互逻辑)
    class UIManager {
        constructor() {
            this.button = null;
            this.isProcessing = false;
            this.appId = null;
            this.hasInitialized = false;
        }

        async init() {
            if (this.hasInitialized) return;
            this.hasInitialized = true;
            this.monitorPage();
        }

        monitorPage() {
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', () => this.findAndInsertButton());
            } else {
                setTimeout(() => this.findAndInsertButton(), 500);
            }
        }

        async findAndInsertButton() {
            // 防止重复插入
            if (document.querySelector('#ggn-steam-language-btn')) return;

            let steamLink = null;
            let pageType = '';

            // 根据当前页面特征,判断是详情页还是上传页,并获取链接
            if (window.location.pathname.includes('torrents.php')) {
                pageType = 'details';
                steamLink = this.findSteamLinkOnDetailsPage();
            } else if (window.location.pathname.includes('upload.php')) {
                pageType = 'upload';
                steamLink = this.findSteamLinkOnUploadPage();
            }

            if (steamLink) {
                this.appId = this.extractAppId(steamLink);
                if (this.appId) {
                    // 根据页面类型,在不同的位置插入按钮
                    if (pageType === 'details') {
                        this.insertButtonOnDetailsPage();
                    } else if (pageType === 'upload') {
                        this.insertButtonOnUploadPage();
                    }
                }
            }
        }

        // 详情页:从Web Links区域获取链接
        findSteamLinkOnDetailsPage() {
            const weblinksDiv = document.querySelector('#weblinksdiv');
            if (weblinksDiv) {
                const steamLink = weblinksDiv.querySelector('a[href*="store.steampowered.com/app/"]');
                if (steamLink) return steamLink.href;
            }
            return null;
        }

        // 上传页:从ID为steamuri的输入框获取链接
        findSteamLinkOnUploadPage() {
            const steamInput = document.querySelector('#steamuri');
            if (steamInput && steamInput.value) {
                return steamInput.value;
            }
            return null;
        }

        extractAppId(url) {
            const match = url.match(/store\.steampowered\.com\/app\/(\d+)/);
            return match ? match[1] : null;
        }

        // 详情页:在标题后插入按钮
        insertButtonOnDetailsPage() {
            const titleElement = document.querySelector('#display_name');
            if (!titleElement) return;

            this.createButton();

            const separator = document.createTextNode(' | ');
            titleElement.appendChild(separator);
            titleElement.appendChild(this.button);
        }

        // 上传页:在Language选择框所在的<td>内插入按钮
        insertButtonOnUploadPage() {
            // 找到包含Language下拉框的td单元格
            const languageLabelTd = Array.from(document.querySelectorAll('td.label')).find(td => td.textContent.trim() === 'Language');
            if (!languageLabelTd) return;

            const targetTd = languageLabelTd.nextElementSibling; // 相邻的右侧td
            if (!targetTd) return;

            this.createButton();

            // 在td内现有内容后添加一个空格和按钮
            targetTd.appendChild(document.createTextNode(' '));
            targetTd.appendChild(this.button);
        }

        // 创建按钮的通用方法
        createButton() {
            this.button = document.createElement('span');
            this.button.id = 'ggn-steam-language-btn';
            this.button.className = 'ggn-steam-language-btn';
            this.button.textContent = 'Steam Language';
            this.button.title = 'Click to copy Steam language info to clipboard';

            this.addButtonStyles();

            this.button.addEventListener('click', (e) => {
                e.preventDefault();
                this.handleButtonClick();
            });
        }

        // 【已修改】按钮样式:默认颜色改为 #ffffff (白色)
        addButtonStyles() {
            if (document.querySelector('#ggn-steam-language-styles')) return;
            const style = document.createElement('style');
            style.id = 'ggn-steam-language-styles';
            style.textContent = `
                .ggn-steam-language-btn {
                    color: #ffffff; /* 修改为白色 */
                    text-decoration: none;
                    margin-left: 8px;
                    margin-right: 8px;
                    font-weight: normal;
                    cursor: pointer;
                    font-size: 13px;
                    font-family: inherit;
                    background: none;
                    border: none;
                    padding: 0;
                    display: inline;
                }
                .ggn-steam-language-btn:hover {
                    text-decoration: underline;
                    color: #cccccc; /* 悬停时改为浅灰色,确保在深色背景下可见 */
                }
                .ggn-steam-language-btn.loading {
                    color: #999;
                    cursor: wait;
                    text-decoration: none;
                }
                .ggn-steam-language-btn.success {
                    color: #2ecc71;
                    font-weight: bold;
                }
                .ggn-steam-language-btn.error {
                    color: #e74c3c;
                }
            `;
            document.head.appendChild(style);
        }

        async handleButtonClick() {
            if (this.isProcessing || !this.button) return;
            this.isProcessing = true;
            const originalText = this.button.textContent;
            const originalTitle = this.button.title;

            this.button.classList.add('loading');
            this.button.textContent = 'Loading...';
            this.button.title = 'Loading Steam language info (this may take a moment)...';

            try {
                const parser = new SteamPageParser();
                const languageData = await parser.getLanguageData(this.appId);

                const textToCopy = this.formatLanguageText(languageData);
                GM_setClipboard(textToCopy, 'text');

                this.button.classList.remove('loading');
                this.button.classList.add('success');
                this.button.textContent = 'Copied!';
                this.button.title = 'Language info copied to clipboard';

                setTimeout(() => {
                    this.button.classList.remove('success');
                    this.button.textContent = originalText;
                    this.button.title = originalTitle;
                    this.isProcessing = false;
                }, 2000);

            } catch (error) {
                console.error('[GGn Steam Language] Failed to copy language info:', error);
                this.button.classList.remove('loading');
                this.button.classList.add('error');
                this.button.textContent = 'Error';
                this.button.title = 'Failed to get language info. Click to retry';

                setTimeout(() => {
                    this.button.classList.remove('error');
                    this.button.textContent = originalText;
                    this.button.title = originalTitle;
                    this.isProcessing = false;
                }, 2000);
            }
        }

        formatLanguageText(languageData) {
            if (languageData.error || languageData.interfaceSubs.length === 0) {
                return 'Steam language info unavailable';
            }

            const { interfaceSubs, fullAudio } = languageData;
            let text = `Interface and Subtitles: ${interfaceSubs.join(', ')}`;

            if (fullAudio.length > 0) {
                text += `\nFull Audio: ${fullAudio.join(', ')}`;
            }

            return text;
        }
    }

    // 主初始化
    function init() {
        if (window.ggnSteamLanguageInitialized) return;
        window.ggnSteamLanguageInitialized = true;
        new UIManager().init();
    }

    // 启动脚本
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();