您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
On-hover Steam thumbnail, description, Steam‐provided tags, and Steam Ratings for 1337x torrent titles
当前为
// ==UserScript== // @name 1337x - Steam Hover Preview // @namespace https://gf.qytechs.cn/en/users/1340389-deonholo // @version 2.5 // @description On-hover Steam thumbnail, description, Steam‐provided tags, and Steam Ratings for 1337x torrent titles // @icon https://greasyfork.s3.us-east-2.amazonaws.com/x432yc9hx5t6o2gbe9ccr7k5l6u8 // @author DeonHolo // @license MIT // @match *://*.1337x.to/* // @match *://*.1337x.ws/* // @match *://*.1337x.is/* // @match *://*.1337x.gd/* // @match *://*.x1337x.cc/* // @match *://*.1337x.st/* // @match *://*.x1337x.ws/* // @match *://*.1337x.eu/* // @match *://*.1337x.se/* // @match *://*.x1337x.eu/* // @match *://*.x1337x.se/* // @match http://l337xdarkkaqfwzntnfk5bmoaroivtl6xsbatabvlb52umg6v3ch44yd.onion/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @connect store.steampowered.com // @connect steamcdn-a.akamaihd.net // @run-at document-idle // ==/UserScript== (() => { 'use strict'; GM_addStyle(` .steamHoverTip { position: fixed; padding: 8px; background: rgba(240, 240, 240, 0.97); border: 1px solid #555; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.25); z-index: 2147483647; max-width: 310px; font-size: 12px; line-height: 1.45; display: none; pointer-events: none; white-space: normal !important; overflow-wrap: break-word; color: #111; } .steamHoverTip p { margin: 0 0 5px 0; padding: 0; white-space: normal; } .steamHoverTip p:last-child { margin-bottom: 0; } .steamHoverTip img { display: block; width: 100%; margin-bottom: 8px; border-radius: 2px; } .steamHoverTip strong { color: #000; } .steamHoverTip .steamRating, .steamHoverTip .steamTags { margin-top: 8px; font-size: 12px; color: #333; } .steamHoverTip .steamTags strong, .steamHoverTip .steamRating strong { color: #111; margin-right: 4px; } .steamHoverTip .ratingStars { color: #f5c518; margin-right: 6px; letter-spacing: 1px; font-size: 14px; display: inline-block; vertical-align: middle; } .steamHoverTip .ratingText { vertical-align: middle; } `); const tip = document.createElement('div'); tip.className = 'steamHoverTip'; document.body.appendChild(tip); const MIN_INTERVAL = 50; let lastRequest = 0; const apiCache = new Map(); const MAX_CACHE = 100; const SCRAPE_TTL = 1000 * 60 * 5; function pruneCache(map) { if (map.size <= MAX_CACHE) return; map.delete(map.keys().next().value); } function gmFetch(url, responseType = 'json', timeout = 10000) { const wait = Math.max(0, MIN_INTERVAL - (Date.now() - lastRequest)); return new Promise(r => setTimeout(r, wait)) .then(() => new Promise((resolve, reject) => { lastRequest = Date.now(); GM_xmlhttpRequest({ method: 'GET', url, responseType, timeout, onload: res => { if (responseType === 'json') { if (typeof res.response === 'object' && res.response !== null) { resolve(res.response); } else { console.warn(`Invalid JSON for ${url}`); try { resolve(JSON.parse(res.responseText)); } catch (e) { reject(new Error('JSON parse error')); } } } else { resolve(res.response); } }, onerror: err => reject(new Error(`Network error: ${err.statusText || err.error || 'Unknown'}`)), ontimeout: () => reject(new Error(`Timeout (${timeout}ms)`)), onabort: () => reject(new Error('Aborted')) }); })); } function parseReviewFromTooltip(tooltipText) { if (!tooltipText) return { percent: null, total: null }; const percentMatch = tooltipText.match(/(\d+)%/); const totalMatch = tooltipText.match(/of the ([\d,]+) user reviews/); return { percent: percentMatch ? percentMatch[1] : null, total: totalMatch ? totalMatch[1].replace(/,/g, '') : null }; } function getRatingStars(percent, desc) { const filledStar = '★'; const emptyStar = '☆'; let stars = ''; let usedPercent = false; const numPercent = parseInt(percent, 10); if (!isNaN(numPercent)) { usedPercent = true; if (numPercent >= 95) stars = filledStar.repeat(5); else if (numPercent >= 80) stars = filledStar.repeat(4) + emptyStar; else if (numPercent >= 70) stars = filledStar.repeat(3) + emptyStar.repeat(2); else if (numPercent >= 40) stars = filledStar.repeat(2) + emptyStar.repeat(3); else if (numPercent >= 20) stars = filledStar.repeat(1) + emptyStar.repeat(4); else stars = emptyStar.repeat(5); } if (!usedPercent && desc) { const lowerDesc = desc.toLowerCase(); if (lowerDesc.includes('overwhelmingly positive')) stars = filledStar.repeat(5); else if (lowerDesc.includes('very positive')) stars = filledStar.repeat(4) + emptyStar; else if (lowerDesc.includes('mostly positive')) stars = filledStar.repeat(4) + emptyStar; else if (lowerDesc.includes('positive')) stars = filledStar.repeat(4) + emptyStar; else if (lowerDesc.includes('mixed')) stars = filledStar.repeat(3) + emptyStar.repeat(2); else if (lowerDesc.includes('mostly negative')) stars = filledStar.repeat(2) + emptyStar.repeat(3); else if (lowerDesc.includes('negative')) stars = filledStar.repeat(1) + emptyStar.repeat(4); else if (lowerDesc.includes('very negative')) stars = filledStar.repeat(1) + emptyStar.repeat(4); else if (lowerDesc.includes('overwhelmingly negative')) stars = filledStar.repeat(1) + emptyStar.repeat(4); } return stars ? `<span class="ratingStars">${stars}</span>` : ''; } async function fetchSteam(name) { const now = Date.now(); let cachedData = apiCache.get(name); if (cachedData && (now - cachedData.ts < SCRAPE_TTL)) return cachedData.data; let appData, reviewInfoFromSearch, appId; try { const search = await gmFetch(`https://store.steampowered.com/api/storesearch/?cc=us&l=en&term=${encodeURIComponent(name)}`); let sr = search?.items?.[0]; if (search?.items?.length > 1) { const lc = name.toLowerCase(); const em = search.items.find(i => i.name.toLowerCase() === lc); if (em) sr = em; } appId = sr?.id; if (!appId) throw new Error('No Store ID'); if (sr?.review_desc) reviewInfoFromSearch = { desc: sr.review_desc, percent: sr.reviews_percent, total: sr.reviews_total?.replace(/,/g,''), source: 'api_search' }; const detailsResp = await gmFetch(`https://store.steampowered.com/api/appdetails?appids=${appId}&cc=us&l=en`); appData = detailsResp?.[appId]?.success ? detailsResp[appId].data : null; if (!appData) throw new Error('No appdetails'); } catch (err) { console.error(`API fetch err for "${name}": ${err.message}`); apiCache.set(name, { data: null, ts: now }); pruneCache(apiCache); return null; } let scrapedTags = [], scrapedReviewInfo = null, finalReviewInfo = reviewInfoFromSearch; try { const html = await gmFetch(`https://store.steampowered.com/app/${appId}/?cc=us&l=en`, 'text', 15000); const doc = new DOMParser().parseFromString(html, 'text/html'); scrapedTags = Array.from(doc.querySelectorAll('.glance_tags.popular_tags a.app_tag')).map(el => el.textContent.trim()).slice(0, 5); if (!finalReviewInfo) { const row = Array.from(doc.querySelectorAll('.user_reviews_summary_row')).find(r => r.querySelector('.subtitle')?.textContent.trim().startsWith('All Review')); if (row) { const span = row.querySelector('.summary .game_review_summary'); if (span) { const desc = span.textContent.trim(); const tooltipData = parseReviewFromTooltip(span.dataset.tooltipText); if (!tooltipData.total) { const cs = row.querySelector('.summary .responsive_hidden'); if(cs){ const cm=cs.textContent.match(/\(([\d,]+)\)/); if(cm) tooltipData.total=cm[1].replace(/,/g,'');}} if (desc) { scrapedReviewInfo = { desc, percent: tooltipData.percent, total: tooltipData.total, source: 'scrape' }; finalReviewInfo = scrapedReviewInfo; } } } } } catch (err) { console.warn(`HTML scrape err for "${name}":`, err); } const finalTags = scrapedTags.length ? scrapedTags : [...new Set([...(appData.genres || []).map(g => g.description), ...(appData.categories || []).map(c => c.description)])].slice(0, 5); if (finalReviewInfo && !finalReviewInfo.desc) finalReviewInfo = null; const combinedData = { ...appData, tags: finalTags, reviewInfo: finalReviewInfo }; apiCache.set(name, { data: combinedData, ts: now }); pruneCache(apiCache); return combinedData; } function cleanName(raw) { if (/soundtrack|ost|demo|dlc pack|artbook|season pass|multiplayer crack/i.test(raw)) return null; let name = raw.trim().replace(/\(\d{4}\)/, '').replace(/S\d{1,2}(E\d{1,2})?/, '').trim(); const delimiters = /(?:[.\-_/(\[]|\bUpdate\b|\bBuild\b|v[\d.]+|\bEdition\b|\bDeluxe\b|\bDirectors? Cut\b|\bComplete\b|\bGold\b|\bGOTY\b|\bRemastered\b|\bAnniversary\b|\bEnhanced\b|\bVR\b)/i; name = name.split(delimiters)[0].trim(); name = name.replace(/[-. ](CODEX|CPY|SKIDROW|PLAZA|HOODLUM|FLT|DOGE|DARKSiDERS|EMPRESS|RUNE|TENOKE|TiNYiSO|ElAmigos|FitGirl|DODI)$/i, '').trim(); name = name.replace(/^(The|Sid Meier'?s|Tom Clancy'?s)\s+/i, '').trim(); return name || null; } function positionTip(e) { let x = e.clientX + 15; let y = e.clientY + 15; const w = tip.offsetWidth; const h = tip.offsetHeight; const margin = 10; if (x + w + margin > window.innerWidth) { x = e.clientX - w - 15; if (x < margin) x = margin; } if (y + h + margin > window.innerHeight) { y = window.innerHeight - h - margin; if (y < margin) y = margin; } tip.style.left = x + 'px'; tip.style.top = y + 'px'; } let hoverId = 0, pointerOn = false, lastEvent = null, showTimeout = null; async function showTip(e) { clearTimeout(showTimeout); pointerOn = true; lastEvent = e; const raw = e.target.textContent; const name = cleanName(raw); if (!name) return; const thisId = ++hoverId; tip.innerHTML = `<p>Loading <strong>${name}</strong>…</p>`; tip.style.display = 'block'; positionTip(e); showTimeout = setTimeout(async () => { if (hoverId !== thisId || !pointerOn || !document.querySelector(`${SEL}:hover`)) { if (!document.querySelector(`${SEL}:hover`)) hideTip(); return; } const data = await fetchSteam(name); if (hoverId !== thisId || !pointerOn || !document.querySelector(`${SEL}:hover`)) { if (!document.querySelector(`${SEL}:hover`)) hideTip(); return; } if (!data) { tip.innerHTML = `<p>No Steam info found for<br><strong>${name}</strong>.</p>`; positionTip(e); return; } let reviewHtml = ''; if (data.reviewInfo && data.reviewInfo.desc) { const { desc, percent, total } = data.reviewInfo; const starsHtml = getRatingStars(percent, desc); const formattedTotal = total ? parseInt(String(total).replace(/,/g, '')).toLocaleString('en-US') : ''; reviewHtml = ` <p class="steamRating"> <strong>Rating:</strong> ${starsHtml} <span class="ratingText"> ${desc} ${formattedTotal ? ` | ${formattedTotal} reviews` : ''} </span> </p>`; } const tags = data.tags || []; const tagHtml = tags.length ? `<p class="steamTags"><strong>Tags:</strong> ${tags.join(' • ')}</p>` : ''; tip.innerHTML = ` <img src="${data.header_image}" alt="${data.name || name} header image"> <p>${data.short_description || 'No description available.'}</p> ${reviewHtml} ${tagHtml}`; positionTip(e); }, 10); } function hideTip() { pointerOn = false; clearTimeout(showTimeout); tip.style.display = 'none'; } function onPointerMove(e) { if (!pointerOn) return; lastEvent = e; } function rafLoop() { if (pointerOn && lastEvent && tip.style.display === 'block') { positionTip(lastEvent); } requestAnimationFrame(rafLoop); } const SEL = 'td.coll-1 a[href^="/torrent/"]'; document.addEventListener('mouseenter', e => { if (e.target.matches(SEL)) { showTip(e); } }, true); document.addEventListener('mouseleave', e => { if (e.target.matches(SEL)) { hideTip(); } }, true); document.addEventListener('pointermove', onPointerMove, true); document.addEventListener('mouseleave', (e) => { if (e.target === document.documentElement) hideTip(); }); console.log("1337x Steam Hover Preview script-timingmod loaded."); rafLoop(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址