// ==UserScript==
// @name 1337x - Steam Hover Preview
// @namespace https://greasyfork.org/users/DeonHolo
// @version 1.8
// @description On-hover Steam thumbnail, description, and community tags 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 https://www.1337x.to/*
// @match http://l337xdarkkaqfwzntnfk5bmoaroivtl6xsbatabvlb52umg6v3ch44yd.onion/*
// @grant GM_xmlhttpRequest
// @connect store.steampowered.com
// @run-at document-idle
// ==/UserScript==
;(function(){
'use strict';
// create tooltip element
const tip = document.createElement('div');
Object.assign(tip.style, {
position : 'fixed',
padding : '8px',
background : 'rgba(255,255,255,0.95)',
border : '1px solid #444',
borderRadius : '4px',
boxShadow : '0 2px 6px rgba(0,0,0,0.2)',
zIndex : 2147483647,
maxWidth : '300px',
fontSize : '12px',
lineHeight : '1.3',
display : 'none',
pointerEvents : 'none'
});
document.body.appendChild(tip);
let hoverCounter = 0;
const cache = new Map();
function gmFetch(url, responseType = 'json') {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url,
responseType,
onload: res => resolve(res.response),
onerror: err => reject(err)
});
});
}
// fetch search + details from Steam API
async function fetchSteam(name) {
if (cache.has(name) && cache.get(name).apiData) {
return cache.get(name).apiData;
}
let search;
try {
search = await gmFetch(
`https://store.steampowered.com/api/storesearch/?cc=us&l=en&term=${encodeURIComponent(name)}`
);
} catch {
return null;
}
const id = search.items?.[0]?.id;
if (!id) return null;
let details;
try {
const resp = await gmFetch(
`https://store.steampowered.com/api/appdetails?appids=${id}&cc=us&l=en`
);
details = resp[id]?.data;
} catch {
return null;
}
cache.set(name, { appid: id, apiData: details });
return details;
}
// scrape community tags from store page HTML
async function fetchTags(appid) {
const key = `tags:${appid}`;
if (cache.has(key)) return cache.get(key);
let html;
try {
html = await gmFetch(`https://store.steampowered.com/app/${appid}/?l=en`, 'text');
} catch {
return [];
}
const doc = new DOMParser().parseFromString(html, 'text/html');
const els = doc.querySelectorAll('.glance_tags.popular_tags a.app_tag');
const tags = Array.from(els).slice(0,5).map(a => a.textContent.trim());
cache.set(key, tags);
return tags;
}
// clean up torrent title
function cleanName(raw) {
let name = raw.trim();
name = name.split(/(?:[-\/\(\[]|Update|Edition|Deluxe)/i)[0].trim();
name = name.replace(/ v[\d.].*$/i, '').trim();
return name;
}
// position tooltip, keep in viewport
function positionTip(e) {
let x = e.clientX + 12;
let y = e.clientY + 12;
const w = tip.offsetWidth, h = tip.offsetHeight;
if (x + w > window.innerWidth) x = window.innerWidth - w - 8;
if (y + h > window.innerHeight) y = window.innerHeight - h - 8;
tip.style.left = x + 'px';
tip.style.top = y + 'px';
}
// show tooltip with image, description, tags
async function showTip(e) {
const thisHover = ++hoverCounter;
const raw = e.target.textContent;
const name = cleanName(raw);
tip.innerHTML = `<p>Loading <strong>${name}</strong>…</p>`;
tip.style.display = 'block';
positionTip(e);
// short debounce
await new Promise(r => setTimeout(r, 100));
if (thisHover !== hoverCounter) return;
const data = await fetchSteam(name);
if (!data || thisHover !== hoverCounter) {
tip.innerHTML = `<p>No Steam info for<br><strong>${name}</strong>.</p>`;
return;
}
// once we have API data, load tags
const tags = await fetchTags(data.steam_appid || data.appid);
if (thisHover !== hoverCounter) return;
// build tag HTML
const tagHtml = tags.length
? `<p style="margin-top:6px"><strong>Tags:</strong> ${tags.join(', ')}</p>`
: '';
tip.innerHTML = `
<img src="${data.header_image}" style="width:100%;margin-bottom:6px">
<p>${data.short_description}</p>
${tagHtml}
`;
}
function hideTip() {
hoverCounter++;
tip.style.display = 'none';
}
// delegate event listeners
const SEL = 'td.coll-1 a[href^="/torrent/"]';
document.addEventListener('mouseover', e => {
if (e.target.matches(SEL)) showTip(e);
});
document.addEventListener('mousemove', e => {
if (e.target.matches(SEL) && tip.style.display === 'block') {
positionTip(e);
}
});
document.addEventListener('mouseout', e => {
if (e.target.matches(SEL)) hideTip();
});
})();