1337x - Steam Hover Preview

On-hover Steam thumbnail, description, and community tags for 1337x torrent titles

2025/04/24のページです。最新版はこちら。

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==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();
  });
})();