您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
On-hover Steam thumbnail, description, Steam Ratings, Steam‐provided tags, and a direct “Open on Steam” link for 1337x torrent titles
当前为
// ==UserScript== // @name 1337x - Steam Hover Preview // @namespace https://gf.qytechs.cn/en/users/1340389-deonholo // @version 3.0 // @description On-hover Steam thumbnail, description, Steam Ratings, Steam‐provided tags, and a direct “Open on Steam” link 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'; const tip = document.createElement('div'); tip.className = 'steamHoverTip'; const SEL = 'table.torrent-list td.name a[href^="/torrent/"], table.torrents td.name a[href^="/torrent/"], table.table-list td.name a[href^="/torrent/"]'; const MIN_INTERVAL = 50; const MAX_CACHE = 100; const CACHE_TTL = 15 * 60 * 1000; const HIDE_DELAY = 100; const FADE_DURATION = 200; const API_TIMEOUT = 10000; const TAG_TIMEOUT = 15000; const SHOW_DELAY = 0; async function preloadAll() { const links = Array.from(document.querySelectorAll(SEL)); const toFetch = new Set(); for (const link of links) { const name = cleanName(link.textContent); if (name && !apiCache.has(name)) { toFetch.add(name); } } for (const name of toFetch) { fetchSteam(name).catch(()=>{}); await new Promise(r => setTimeout(r, MIN_INTERVAL)); } } window.addEventListener('load', () => { setTimeout(preloadAll, 50); }); GM_addStyle(` .steamHoverTip { position: absolute; 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; white-space: normal !important; overflow-wrap: break-word; color: #111; opacity: 0; transition: opacity ${FADE_DURATION}ms ease-in-out; pointer-events: none; } .steamHoverTip p { margin: 0 0 5px 0; padding: 0; } .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; } .steamHoverTip a { color: #0645ad; text-decoration: underline; cursor: pointer; } `); const apiCache = new Map(); let lastRequest = 0; let hoverId = 0; let showTimeout = null; let hideTimeout = null; let displayTimeout = null; let currentFetch = null; let trackingMove = false; let lastMoveEvent = null; let currentHoveredLink = null; document.body.appendChild(tip); function pruneCache(map) { if (map.size > MAX_CACHE) { map.delete(map.keys().next().value); } } function getRatingStars(percent, desc) { const filled = '★'; const empty = '☆'; const p = parseInt(percent, 10); let stars = ''; if (!isNaN(p)) { if (p >= 95) stars = filled.repeat(5); else if (p >= 80) stars = filled.repeat(4) + empty; else if (p >= 70) stars = filled.repeat(3) + empty.repeat(2); else if (p >= 40) stars = filled.repeat(2) + empty.repeat(3); else if (p >= 20) stars = filled + empty.repeat(4); else stars = empty.repeat(5); } else if (desc) { const d = desc.toLowerCase(); if (d.includes('overwhelmingly positive')) stars = filled.repeat(5); else if (d.includes('very positive')) stars = filled.repeat(4) + empty; else if (d.includes('mostly positive')) stars = filled.repeat(4) + empty; else if (d.includes('positive')) stars = filled.repeat(4) + empty; else if (d.includes('mixed')) stars = filled.repeat(3) + empty.repeat(2); else if (d.includes('mostly negative')) stars = filled.repeat(2) + empty.repeat(3); else if (d.includes('negative')) stars = filled + empty.repeat(4); else if (d.includes('very negative')) stars = filled + empty.repeat(4); else if (d.includes('overwhelmingly negative')) stars = filled + empty.repeat(4); } return stars ? `<span class="ratingStars">${stars}</span>` : ''; } function cleanName(raw) { if (/soundtrack|ost|demo|dlc pack|artbook|season pass|multiplayer crack/i.test(raw)) { return null; } let name = raw.trim(); name = name.replace(/\(\d{4}\)/, '').replace(/S\d{1,2}(E\d{1,2})?/, '').trim(); const delim = /(?:[.\-_/(\[]|\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|\bUltimate\b)/i; name = name.split(delim)[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 gmFetch(url, responseType = 'json', timeout = API_TIMEOUT) { const wait = Math.max(0, MIN_INTERVAL - (Date.now() - lastRequest)); return new Promise(resolve => setTimeout(resolve, wait)) .then(() => new Promise((resolve, reject) => { lastRequest = Date.now(); GM_xmlhttpRequest({ method: 'GET', url: url, responseType: responseType, timeout: timeout, headers: { 'Accept-Language': 'en-US,en;q=0.9' }, onload: (res) => { if (res.status >= 200 && res.status < 300) { if (responseType === 'json') { if (typeof res.response === 'object' && res.response !== null) { resolve(res.response); } else { try { resolve(JSON.parse(res.responseText)); } catch (e) { console.error(`JSON parse error for ${url}:`, e, res.responseText); reject(new Error(`JSON parse error for ${url}`)); } } } else { resolve(res.response || res.responseText); } } else { console.warn(`HTTP ${res.status} for ${url}`); reject(new Error(`HTTP ${res.status} for ${url}`)); } }, onerror: (err) => { console.error(`Network error for ${url}:`, err); reject(new Error(`Network error for ${url}: ${err.statusText || err.error || 'Unknown'}`)); }, ontimeout: () => { console.warn(`Timeout ${timeout}ms for ${url}`); reject(new Error(`Timeout ${timeout}ms for ${url}`)); }, onabort: () => { console.warn(`Aborted request for ${url}`); reject(new Error(`Aborted request for ${url}`)); } }); })); } async function fetchSteam(name) { const now = Date.now(); const hit = apiCache.get(name); if (hit && now - hit.ts < CACHE_TTL) { return hit.data; } let appId = null; let appData = null; try { const searchUrl = `https://store.steampowered.com/api/storesearch/?cc=us&l=en&term=${encodeURIComponent(name)}`; const searchRes = await gmFetch(searchUrl, 'json'); let result = searchRes?.items?.[0]; if (searchRes?.items?.length > 1) { const exactMatch = searchRes.items.find(item => item.name.toLowerCase() === name.toLowerCase()); if (exactMatch) { result = exactMatch; } } appId = result?.id; if (!appId) { throw new Error('No suitable AppID found in search results.'); } const detailsUrl = `https://store.steampowered.com/api/appdetails?appids=${appId}&cc=us&l=en`; const detailsRes = await gmFetch(detailsUrl, 'json'); if (detailsRes?.[appId]?.success) { appData = detailsRes[appId].data; } else { throw new Error('Failed to fetch app details or API indicated failure.'); } } catch (err) { console.warn(`Steam search/details fetch failed for "${name}":`, err.message); apiCache.set(name, { data: null, ts: now }); pruneCache(apiCache); return null; } let reviewInfo = null; try { const reviewUrl = `https://store.steampowered.com/appreviews/${appId}?json=1&language=all&purchase_type=all&filter=summary`; const reviewRes = await gmFetch(reviewUrl, 'json'); if (reviewRes?.success && reviewRes.query_summary) { const summary = reviewRes.query_summary; const percent = summary.total_reviews ? Math.round((summary.total_positive / summary.total_reviews) * 100) : null; reviewInfo = { desc: summary.review_score_desc || 'No Reviews', percent: percent, total: summary.total_reviews || 0 }; } } catch (revErr) { console.warn(`Steam reviews fetch failed for AppID ${appId}:`, revErr.message); } let tags = []; try { const appPageUrl = `https://store.steampowered.com/app/${appId}/?cc=us&l=en`; const html = await gmFetch(appPageUrl, 'text', TAG_TIMEOUT); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); tags = Array.from(doc.querySelectorAll('.glance_tags.popular_tags a.app_tag')) .map(el => el.textContent.trim()) .slice(0, 5); } catch (tagErr) { console.warn(`Steam tag scrape failed for AppID ${appId}:`, tagErr.message); } if (tags.length === 0 && appData) { const genreTags = (appData.genres || []).map(g => g.description); const categoryTags = (appData.categories || []).map(c => c.description); tags = [...genreTags, ...categoryTags].filter(Boolean).slice(0, 5); } const data = { ...appData, tags: tags, reviewInfo: reviewInfo, storeUrl: `https://store.steampowered.com/app/${appId}/` }; apiCache.set(name, { data: data, ts: now }); pruneCache(apiCache); return data; } function positionTip(ev) { if (!tip) return; let x = ev.pageX + 15; let y = ev.pageY + 15; const tipWidth = tip.offsetWidth; const tipHeight = tip.offsetHeight; const margin = 10; const scrollX = window.scrollX || window.pageXOffset; const scrollY = window.scrollY || window.pageYOffset; const viewWidth = window.innerWidth; const viewHeight = window.innerHeight; if (x + tipWidth + margin > scrollX + viewWidth) { x = ev.pageX - tipWidth - 15; if (x < scrollX + margin) { x = scrollX + margin; } } if (x < scrollX + margin) { x = scrollX + margin; } if (y + tipHeight + margin > scrollY + viewHeight) { let yAbove = ev.pageY - tipHeight - 15; if (yAbove > scrollY + margin) { y = yAbove; } else { y = scrollY + viewHeight - tipHeight - margin; if (y < scrollY + margin) { y = scrollY + margin; } } } if (y < scrollY + margin) { y = scrollY + margin; } tip.style.left = `${x}px`; tip.style.top = `${y}px`; } function startHideAnimation() { if (tip.style.display !== 'none' && tip.style.opacity !== '0') { tip.style.opacity = '0'; tip.style.pointerEvents = 'none'; trackingMove = false; clearTimeout(displayTimeout); displayTimeout = setTimeout(() => { tip.style.display = 'none'; }, FADE_DURATION); } else if (tip.style.display !== 'none') { clearTimeout(displayTimeout); displayTimeout = setTimeout(() => { tip.style.display = 'none'; }, FADE_DURATION); } } function actuallyHideTip() { hoverId++; currentFetch = null; currentHoveredLink = null; clearTimeout(showTimeout); startHideAnimation(); } function scheduleHideTip() { clearTimeout(hideTimeout); clearTimeout(displayTimeout); hideTimeout = setTimeout(actuallyHideTip, HIDE_DELAY); } function cancelHideTip() { clearTimeout(hideTimeout); clearTimeout(displayTimeout); if (tip.style.display === 'block' && tip.style.opacity === '0') { tip.style.opacity = '1'; tip.style.pointerEvents = 'auto'; } } function triggerShowAndFadeIn(event, gameName) { cancelHideTip(); clearTimeout(displayTimeout); tip.innerHTML = `<p>Loading <strong>${gameName}</strong>…</p>`; positionTip(event); tip.style.display = 'block'; void tip.offsetHeight; tip.style.opacity = '1'; tip.style.pointerEvents = 'auto'; } tip.addEventListener('mouseenter', () => { cancelHideTip(); if (trackingMove) { trackingMove = false; } }); tip.addEventListener('mouseleave', () => { scheduleHideTip(); }); document.addEventListener('mouseover', async (e) => { const targetLink = e.target.closest(SEL); const isOverTip = tip.contains(e.target); if (targetLink || isOverTip) { cancelHideTip(); } if (!targetLink || (targetLink === currentHoveredLink && !trackingMove)) { return; } if (currentHoveredLink && targetLink !== currentHoveredLink && tip.style.display === 'block') { tip.style.opacity = '0'; tip.style.pointerEvents = 'none'; tip.style.display = 'none'; hoverId++; trackingMove = false; currentFetch = null; } currentHoveredLink = targetLink; const rawName = targetLink.textContent; const gameName = cleanName(rawName); if (!gameName) { currentHoveredLink = null; return; } clearTimeout(showTimeout); const thisId = ++hoverId; trackingMove = true; lastMoveEvent = e; triggerShowAndFadeIn(e, gameName); showTimeout = setTimeout(async () => { if (hoverId !== thisId || !currentHoveredLink || currentHoveredLink !== targetLink) { if (!currentHoveredLink || currentHoveredLink !== targetLink) { trackingMove = false; } return; } currentFetch = fetchSteam(gameName); const data = await currentFetch; currentFetch = null; if (hoverId !== thisId || !currentHoveredLink || currentHoveredLink !== targetLink) { if (!currentHoveredLink || currentHoveredLink !== targetLink) { trackingMove = false; } return; } if (!data) { tip.innerHTML = `<p>No Steam info found for<br><strong>${gameName}</strong>.</p>`; } else { const tagsHtml = data.tags?.length ? `<p class="steamTags"><strong>Tags:</strong> ${data.tags.join(' • ')}</p>` : ''; const reviewHtml = (data.reviewInfo && data.reviewInfo.desc !== 'N/A' && data.reviewInfo.desc !== 'No Reviews') ? `<p class="steamRating"><strong>Rating:</strong> ${getRatingStars(data.reviewInfo.percent, data.reviewInfo.desc)}<span class="ratingText">${data.reviewInfo.desc}${data.reviewInfo.total ? ` | ${data.reviewInfo.total.toLocaleString()} reviews` : ''}</span></p>` : ''; tip.innerHTML = ` ${data.header_image ? `<img src="${data.header_image}" alt="${data.name || gameName}" onerror="this.style.display='none'">` : ''} <p><strong>${data.name || gameName}</strong></p> <p>${data.short_description || 'No description available.'}</p> ${reviewHtml} ${tagsHtml} ${data.storeUrl ? `<p><a class="steam-link-in-tip" href="${data.storeUrl}" target="_blank" rel="noopener noreferrer">Open on Steam</a></p>`: ''} `; } if (hoverId === thisId && currentHoveredLink === targetLink) { positionTip(lastMoveEvent); trackingMove = false; tip.style.opacity = '1'; tip.style.pointerEvents = 'auto'; } else { startHideAnimation(); } }, SHOW_DELAY); }, true); document.addEventListener('mouseout', (e) => { const leavingCurrentLink = currentHoveredLink && currentHoveredLink === e.target.closest(SEL); const destinationIsTip = tip.contains(e.relatedTarget); if (leavingCurrentLink && !destinationIsTip) { scheduleHideTip(); currentHoveredLink = null; } }, true); document.addEventListener('pointermove', (e) => { if (trackingMove && tip.style.display === 'block') { lastMoveEvent = e; positionTip(e); } }, { capture: true, passive: true }); console.log("1337x Steam Hover Preview script loaded."); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址