您需要先安装一个扩展,例如 篡改猴、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://greasyfork.org/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.");
- })();