Greasy Fork

来自缓存

Greasy Fork is available in English.

Gamer520网站游戏Steam好评率显示器

在 gamer520.com 游戏列表和详情页显示 Steam 好评率,替换"免费"标签,仅处理PC PLAY游戏

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Gamer520网站游戏Steam好评率显示器
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  在 gamer520.com 游戏列表和详情页显示 Steam 好评率,替换"免费"标签,仅处理PC PLAY游戏
// @author       icescat
// @match        https://www.gamer520.com/pcplay*
// @match        https://www.gamer520.com/*.html
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      store.steampowered.com
// @run-at       document-end
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // 调试模式
    const DEBUG = true;
    function log(...args) {
        if (DEBUG) console.log('[SteamRating]', ...args);
    }

    // 配置
    const CONFIG = {
        CACHE_DAYS: 7,
        REQUEST_DELAY: 500,
        MAX_RETRIES: 3,
        MIN_SIMILARITY: 0.3
    };

    // 检查是否为 PC PLAY 游戏(支持列表页和详情页)
    function isPCPlayGame(container) {
        // 尝试多种选择器
        const selectors = [
            '.entry-header .meta-category a',
            '.entry-meta .meta-category a',
            '.meta-category a'
        ];
        
        for (const selector of selectors) {
            const categoryLinks = container.querySelectorAll(selector);
            for (const link of categoryLinks) {
                if (link.textContent.includes('PC PLAY')) {
                    return true;
                }
            }
        }
        return false;
    }
    
    // 检查当前页面是否为详情页
    function isDetailPage() {
        return window.location.pathname.match(/\/\d+\.html$/) !== null;
    }

    // 游戏名称清洗规则
    function cleanGameName(name) {
        if (!name) return '';
        
        let cleaned = name
            .replace(/\|?Build\.\d+[^|]*/gi, '')
            .replace(/\|?V\d+\.\d+[^|]*/gi, '')
            .replace(/\|?Fix-\d+[^|]*/gi, '')
            .replace(/\|?豪华中文\|?/g, '')
            .replace(/\|?官方中文\|?/g, '')
            .replace(/\|?中字-国语\|?/g, '')
            .replace(/\|?全DLC[^|]*/g, '')
            .replace(/\|?修改器\|?/g, '')
            .replace(/\|?解压即撸\|?/g, '')
            .replace(/\|?预购[^|]*/g, '')
            .replace(/\|?数字豪华版\|?/g, '')
            .replace(/\|?典藏版\|?/g, '')
            .replace(/\|?放置版\|?/g, '')
            .replace(/\|?非虚拟机[^|]*/g, '')
            .replace(/\|+/g, ' ')
            .trim();
        
        return cleaned;
    }

    // 提取核心游戏名
    function extractCoreName(name) {
        const cleaned = cleanGameName(name);
        const prefix4 = cleaned.substring(0, 4);
        const prefix3 = cleaned.substring(0, 3);
        const spaceIndex = cleaned.indexOf(' ');
        const firstPart = spaceIndex > 0 ? cleaned.substring(0, spaceIndex) : cleaned;
        const prefix5 = cleaned.substring(0, 5);
        
        return [...new Set([cleaned, prefix4, prefix3, firstPart, prefix5])].filter(n => n.length >= 2);
    }

    // 计算字符串相似度
    function calculateSimilarity(str1, str2) {
        str1 = str1.toLowerCase().replace(/\s+/g, '');
        str2 = str2.toLowerCase().replace(/\s+/g, '');
        
        if (str1.includes(str2) || str2.includes(str1)) {
            return 0.8 + (Math.min(str1.length, str2.length) / Math.max(str1.length, str2.length)) * 0.2;
        }
        
        let commonLength = 0;
        for (let i = 0; i < Math.min(str1.length, str2.length); i++) {
            if (str1[i] === str2[i]) {
                commonLength++;
            } else {
                break;
            }
        }
        
        return commonLength / Math.max(str1.length, str2.length);
    }

    // 获取缓存
    function getCache(key) {
        try {
            const data = GM_getValue(key, null);
            if (!data) return null;
            
            const parsed = JSON.parse(data);
            const now = Date.now();
            const maxAge = CONFIG.CACHE_DAYS * 24 * 60 * 60 * 1000;
            
            if (now - parsed.timestamp > maxAge) {
                return null;
            }
            return parsed.value;
        } catch (e) {
            return null;
        }
    }

    // 设置缓存
    function setCache(key, value) {
        try {
            const data = {
                timestamp: Date.now(),
                value: value
            };
            GM_setValue(key, JSON.stringify(data));
        } catch (e) {
            console.error('设置缓存失败:', e);
        }
    }

    // 延迟函数
    function delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    // 搜索 Steam 游戏
    async function searchSteamGame(searchTerm, retries = 0) {
        const cacheKey = `search_${searchTerm}`;
        const cached = getCache(cacheKey);
        if (cached) {
            log('使用缓存:', searchTerm);
            return cached;
        }

        try {
            log('搜索:', searchTerm);
            
            const response = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: "GET",
                    url: `https://store.steampowered.com/api/storesearch/?term=${encodeURIComponent(searchTerm)}&l=schinese&cc=CN`,
                    headers: {
                        'Accept': 'application/json, text/javascript, */*; q=0.01',
                        'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
                        'Referer': 'https://store.steampowered.com/',
                        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
                    },
                    onload: (res) => {
                        if (res.status === 200) {
                            try {
                                const data = JSON.parse(res.responseText);
                                log(`"${searchTerm}" 返回 ${data.total || 0} 个结果`);
                                resolve(data);
                            } catch (e) {
                                reject(new Error('解析JSON失败'));
                            }
                        } else {
                            reject(new Error(`HTTP ${res.status}`));
                        }
                    },
                    onerror: () => reject(new Error('网络请求失败')),
                    ontimeout: () => reject(new Error('请求超时'))
                });
            });

            setCache(cacheKey, response);
            return response;
        } catch (error) {
            if (retries < CONFIG.MAX_RETRIES) {
                await delay(1000 * (retries + 1));
                return searchSteamGame(searchTerm, retries + 1);
            }
            throw error;
        }
    }

    // 智能搜索游戏
    async function smartSearchGame(originalName) {
        const coreNames = extractCoreName(originalName);
        log('原始名称:', originalName);
        log('尝试搜索:', coreNames);
        
        let allResults = [];
        
        for (const name of coreNames) {
            try {
                const result = await searchSteamGame(name);
                if (result.items && result.items.length > 0) {
                    const scoredItems = result.items.map(item => {
                        const nameSimilarity = calculateSimilarity(originalName, item.name);
                        const isGame = item.type === 'game' ? 0.1 : 0;
                        return {
                            ...item,
                            similarity: nameSimilarity + isGame
                        };
                    });
                    
                    allResults.push(...scoredItems);
                }
            } catch (error) {
                log('搜索失败:', name, error.message);
            }
            
            await delay(200);
        }
        
        if (allResults.length === 0) {
            return { items: [], isUncertain: false };
        }
        
        const uniqueResults = [];
        const seenIds = new Set();
        for (const item of allResults) {
            if (!seenIds.has(item.id)) {
                seenIds.add(item.id);
                uniqueResults.push(item);
            }
        }
        
        uniqueResults.sort((a, b) => b.similarity - a.similarity);
        
        log('最佳匹配:', uniqueResults[0].name, '相似度:', uniqueResults[0].similarity.toFixed(2));
        
        const isUncertain = uniqueResults[0].similarity < CONFIG.MIN_SIMILARITY;
        
        return {
            items: uniqueResults,
            isUncertain: isUncertain,
            bestMatch: uniqueResults[0]
        };
    }

    // 获取游戏评价
    async function getGameReviews(appid, retries = 0) {
        const cacheKey = `reviews_${appid}`;
        const cached = getCache(cacheKey);
        if (cached) return cached;

        try {
            const response = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: "GET",
                    url: `https://store.steampowered.com/appreviews/${appid}?json=1&language=schinese`,
                    headers: {
                        'Accept': 'application/json',
                        'Accept-Language': 'zh-CN,zh;q=0.9'
                    },
                    onload: (res) => {
                        if (res.status === 200) {
                            try {
                                const data = JSON.parse(res.responseText);
                                resolve(data);
                            } catch (e) {
                                reject(new Error('解析JSON失败'));
                            }
                        } else {
                            reject(new Error(`HTTP ${res.status}`));
                        }
                    },
                    onerror: () => reject(new Error('网络请求失败'))
                });
            });

            setCache(cacheKey, response);
            return response;
        } catch (error) {
            if (retries < CONFIG.MAX_RETRIES) {
                await delay(1000 * (retries + 1));
                return getGameReviews(appid, retries + 1);
            }
            throw error;
        }
    }

    // 计算好评率
    function calculateRating(positive, negative) {
        const total = positive + negative;
        if (total === 0) return null;
        return Math.round((positive / total) * 100);
    }

    // 获取颜色样式
    function getRatingStyle(rating) {
        if (rating === null) return { bg: '#888', text: '#fff' };
        if (rating >= 80) return { bg: '#4CAF50', text: '#fff' };
        if (rating >= 50) return { bg: '#FFC107', text: '#000' };
        return { bg: '#F44336', text: '#fff' };
    }

    // 创建好评率标签
    function createRatingBadge(rating, positive, negative, isUncertain) {
        const style = getRatingStyle(rating);
        const badge = document.createElement('span');
        badge.className = 'steam-rating-badge';
        badge.style.cssText = `
            background-color: ${style.bg};
            color: ${style.text};
            padding: 2px 6px;
            border-radius: 3px;
            font-size: 11px;
            font-weight: bold;
            display: inline-flex;
            align-items: center;
            gap: 3px;
            cursor: help;
            white-space: nowrap;
        `;
        
        if (rating === null) {
            badge.textContent = '无评价';
        } else {
            const total = positive + negative;
            const uncertainEmoji = isUncertain ? '❓' : '';
            badge.innerHTML = `
                <span>${uncertainEmoji}${rating}%</span>
                <span style="font-size: 9px; opacity: 0.9;">(${total})</span>
            `;
            badge.title = `好评: ${positive} | 差评: ${negative}${isUncertain ? ' (匹配不确定)' : ''}`;
        }
        
        return badge;
    }

    // 处理单个游戏卡片
    async function processCard(card, index) {
        // 检查是否为 PC PLAY 游戏
        if (!isPCPlayGame(card)) {
            log('跳过非PC PLAY游戏');
            return;
        }

        const titleElement = card.querySelector('.entry-title a');
        if (!titleElement) return;
        
        const fullName = titleElement.textContent || titleElement.getAttribute('title');
        const cleanName = cleanGameName(fullName);
        
        if (!cleanName) return;

        const priceElement = card.querySelector('.meta-price');
        const dateElement = card.querySelector('.meta-date');
        
        if (!priceElement) return;

        // 显示加载状态
        priceElement.innerHTML = '<span style="color: #888; font-size: 11px;">...</span>';

        try {
            await delay(index * CONFIG.REQUEST_DELAY);

            const searchResult = await smartSearchGame(cleanName);
            
            if (!searchResult.items || searchResult.items.length === 0) {
                priceElement.innerHTML = '<span style="color: #666; font-size: 10px;">未找到</span>';
                return;
            }

            const game = searchResult.bestMatch;
            const appid = game.id;
            const isUncertain = searchResult.isUncertain;
            
            log('选中游戏:', game.name, 'AppID:', appid, '不确定:', isUncertain);

            const reviewsResult = await getGameReviews(appid);
            
            if (!reviewsResult.query_summary || reviewsResult.query_summary.total_reviews === 0) {
                priceElement.innerHTML = '<span style="color: #666; font-size: 10px;">无评价</span>';
                return;
            }

            const { total_positive, total_negative } = reviewsResult.query_summary;
            const rating = calculateRating(total_positive, total_negative);

            // 替换价格标签为好评率
            const badge = createRatingBadge(rating, total_positive, total_negative, isUncertain);
            priceElement.innerHTML = '';
            priceElement.appendChild(badge);

            // 将时间标签替换为Steam英文名(可点击跳转)
            if (dateElement) {
                const steamName = document.createElement('li');
                steamName.className = 'meta-steam-name';
                steamName.style.cssText = `
                    flex: 1;
                    text-align: left;
                `;
                
                const link = document.createElement('a');
                link.href = `https://store.steampowered.com/app/${appid}/`;
                link.target = '_blank';
                link.style.cssText = `
                    color: #888;
                    font-size: 10px;
                    overflow: hidden;
                    text-overflow: ellipsis;
                    white-space: nowrap;
                    max-width: 120px;
                    display: block;
                    text-decoration: none;
                    cursor: pointer;
                `;
                link.title = `${game.name} - 点击打开Steam商店页面`;
                link.innerHTML = `<i class="fa fa-steam" style="margin-right: 3px;"></i>${game.name}`;
                
                // 鼠标悬停效果
                link.addEventListener('mouseenter', () => {
                    link.style.color = '#66c0f4';
                    link.style.textDecoration = 'underline';
                });
                link.addEventListener('mouseleave', () => {
                    link.style.color = '#888';
                    link.style.textDecoration = 'none';
                });
                
                steamName.appendChild(link);
                
                // 替换原来的时间元素
                dateElement.parentNode.replaceChild(steamName, dateElement);
            }
            
            // 让好评率标签也可以点击跳转
            if (badge) {
                badge.style.cursor = 'pointer';
                badge.title = `${badge.title} - 点击打开Steam商店页面`;
                badge.addEventListener('click', () => {
                    window.open(`https://store.steampowered.com/app/${appid}/`, '_blank');
                });
            }

        } catch (error) {
            console.error('处理失败:', cleanName, error);
            priceElement.innerHTML = '<span style="color: #F44336; font-size: 10px;">错误</span>';
        }
    }

    // 处理详情页
    async function processDetailPage() {
        log('处理详情页');
        
        // 尝试多种选择器查找文章容器
        let article = document.querySelector('article.post');
        if (!article) {
            article = document.querySelector('article');
        }
        if (!article) {
            article = document.querySelector('.content-area');
        }
        if (!article) {
            article = document.body;
        }
        
        log('使用容器:', article.tagName, article.className);
        
        // 检查是否为 PC PLAY 游戏
        if (!isPCPlayGame(article)) {
            log('详情页不是 PC PLAY 游戏,跳过');
            return;
        }
        
        // 获取游戏名称
        let titleElement = article.querySelector('h1.entry-title');
        if (!titleElement) {
            titleElement = document.querySelector('h1.entry-title');
        }
        if (!titleElement) {
            titleElement = document.querySelector('h1');
        }
        if (!titleElement) {
            log('未找到标题');
            return;
        }
        
        const fullName = titleElement.textContent;
        const cleanName = cleanGameName(fullName);
        
        if (!cleanName) return;
        
        log('详情页游戏:', cleanName);
        
        // 查找时间元素(尝试多种选择器)
        let dateElement = article.querySelector('.entry-meta .meta-date');
        if (!dateElement) {
            dateElement = document.querySelector('.entry-meta .meta-date');
        }
        if (!dateElement) {
            dateElement = document.querySelector('.meta-date');
        }
        if (!dateElement) {
            log('未找到时间元素');
            return;
        }
        
        // 显示加载状态
        dateElement.innerHTML = '<span style="color: #888;">查询Steam数据中...</span>';
        
        try {
            const searchResult = await smartSearchGame(cleanName);
            
            if (!searchResult.items || searchResult.items.length === 0) {
                dateElement.innerHTML = '<span style="color: #666;">未找到Steam数据</span>';
                return;
            }
            
            const game = searchResult.bestMatch;
            const appid = game.id;
            const isUncertain = searchResult.isUncertain;
            
            log('详情页选中:', game.name, 'AppID:', appid);
            
            const reviewsResult = await getGameReviews(appid);
            
            if (!reviewsResult.query_summary || reviewsResult.query_summary.total_reviews === 0) {
                // 只显示游戏名
                dateElement.innerHTML = `
                    <a href="https://store.steampowered.com/app/${appid}/" target="_blank" 
                       style="color: #66c0f4; text-decoration: none;">
                        <i class="fa fa-steam"></i> ${game.name} (无评价)
                    </a>
                `;
                return;
            }
            
            const { total_positive, total_negative } = reviewsResult.query_summary;
            const rating = calculateRating(total_positive, total_negative);
            const style = getRatingStyle(rating);
            const uncertainEmoji = isUncertain ? '❓' : '';
            
            // 替换时间元素为 Steam 信息
            dateElement.innerHTML = `
                <a href="https://store.steampowered.com/app/${appid}/" target="_blank" 
                   style="color: #66c0f4; text-decoration: none; display: inline-flex; align-items: center; gap: 5px;">
                    <i class="fa fa-steam"></i>
                    <span>${game.name}</span>
                    <span style="background-color: ${style.bg}; color: ${style.text}; padding: 2px 6px; border-radius: 3px; font-size: 11px; font-weight: bold;">
                        ${uncertainEmoji}${rating}%
                    </span>
                    <span style="color: #888; font-size: 11px;">(${total_positive + total_negative})</span>
                </a>
            `;
            
            log('详情页处理完成');
            
        } catch (error) {
            console.error('详情页处理失败:', error);
            dateElement.innerHTML = '<span style="color: #F44336;">查询失败</span>';
        }
    }

    // 主函数
    async function main() {
        log('脚本开始运行 v1.5 - PC PLAY专用(支持列表页和详情页)');
        
        // 添加样式
        const style = document.createElement('style');
        style.textContent = `
            .steam-rating-badge {
                transition: opacity 0.3s;
            }
            .steam-rating-badge:hover {
                opacity: 0.8;
            }
            .meta-price {
                min-width: 50px;
                text-align: right;
            }
            .meta-steam-name {
                flex: 1;
                overflow: hidden;
                text-align: left !important;
            }
            .meta-steam-name a {
                text-align: left !important;
                justify-content: flex-start !important;
            }
            .post-meta-box {
                display: flex;
                align-items: center;
                justify-content: space-between;
            }
        `;
        document.head.appendChild(style);
        
        // 判断是详情页还是列表页
        if (isDetailPage()) {
            log('当前为详情页模式');
            await processDetailPage();
        } else {
            log('当前为列表页模式');
            
            const cards = document.querySelectorAll('article.post-grid');
            log('找到游戏卡片:', cards.length);
            
            if (cards.length === 0) return;
            
            let pcPlayCount = 0;
            for (let i = 0; i < cards.length; i++) {
                if (isPCPlayGame(cards[i])) {
                    pcPlayCount++;
                    processCard(cards[i], i);
                }
            }
            
            log('PC PLAY游戏数量:', pcPlayCount);
        }
    }

    // 页面加载完成后执行
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', main);
    } else {
        main();
    }

    // 监听页面变化
    const observer = new MutationObserver((mutations) => {
        let shouldReload = false;
        mutations.forEach((mutation) => {
            if (mutation.type === 'childList') {
                mutation.addedNodes.forEach((node) => {
                    if (node.nodeType === 1 && node.matches && node.matches('article.post-grid')) {
                        shouldReload = true;
                    }
                });
            }
        });
        
        if (shouldReload) {
            setTimeout(main, 500);
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

})();