1337x – Steam Hover Preview

On-hover Steam thumbnail & description for 1337x torrent titles (cleans up names for better matches)

当前为 2025-04-24 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name 1337x – Steam Hover Preview
  3. // @namespace https://greasyfork.org/users/youruserid
  4. // @version 1.1
  5. // @description On-hover Steam thumbnail & description for 1337x torrent titles (cleans up names for better matches)
  6. // @author DeonHolo
  7. // @license MIT
  8. // @match *://1337x.to/*
  9. // @match *://1337x.st/*
  10. // @match *://1337x.ws/*
  11. // @match *://1337x.eu/*
  12. // @match *://1337x.se/*
  13. // @match *://1337x.is/*
  14. // @match *://1337x.gd/*
  15. // @grant GM_xmlhttpRequest
  16. // @connect store.steampowered.com
  17. // @run-at document-idle
  18. // ==/UserScript==
  19.  
  20. ;(function(){
  21. 'use strict';
  22.  
  23. // create tooltip
  24. const tip = document.createElement('div');
  25. Object.assign(tip.style, {
  26. position : 'fixed',
  27. padding : '8px',
  28. background : 'rgba(255,255,255,0.95)',
  29. border : '1px solid #444',
  30. borderRadius : '4px',
  31. boxShadow : '0 2px 6px rgba(0,0,0,0.2)',
  32. zIndex : 2147483647,
  33. maxWidth : '300px',
  34. fontSize : '12px',
  35. lineHeight : '1.3',
  36. display : 'none',
  37. pointerEvents : 'none'
  38. });
  39. document.body.appendChild(tip);
  40.  
  41. let hoverCounter = 0;
  42. const cache = new Map();
  43.  
  44. function gmFetch(url){
  45. return new Promise((resolve, reject) => {
  46. GM_xmlhttpRequest({
  47. method : 'GET',
  48. url,
  49. responseType : 'json',
  50. onload : r => resolve(r.response),
  51. onerror : e => reject(e)
  52. });
  53. });
  54. }
  55.  
  56. async function fetchSteam(name){
  57. if (cache.has(name)) return cache.get(name);
  58.  
  59. let search;
  60. try {
  61. search = await gmFetch(
  62. `https://store.steampowered.com/api/storesearch/?cc=us&l=en&term=${encodeURIComponent(name)}`
  63. );
  64. } catch {
  65. return null;
  66. }
  67. const id = search.items?.[0]?.id;
  68. if (!id) return null;
  69.  
  70. let details;
  71. try {
  72. details = await gmFetch(
  73. `https://store.steampowered.com/api/appdetails?appids=${id}&cc=us&l=en`
  74. );
  75. } catch {
  76. return null;
  77. }
  78. const data = details[id]?.data;
  79. cache.set(name, data);
  80. return data;
  81. }
  82.  
  83. function cleanName(raw){
  84. let name = raw.trim();
  85. // split at dash, slash, bracket, 'Update' or 'Edition'
  86. name = name.split(/(?:[-\/\(\[]|Update|Edition)/i)[0].trim();
  87. // strip off version suffix
  88. name = name.replace(/ v[\d.].*$/i, '').trim();
  89. return name;
  90. }
  91.  
  92. function positionTip(e){
  93. let x = e.clientX + 12;
  94. let y = e.clientY + 12;
  95. const w = tip.offsetWidth, h = tip.offsetHeight;
  96. if (x + w > window.innerWidth) x = window.innerWidth - w - 8;
  97. if (y + h > window.innerHeight) y = window.innerHeight - h - 8;
  98. tip.style.left = x + 'px';
  99. tip.style.top = y + 'px';
  100. }
  101.  
  102. async function showTip(e){
  103. const thisHover = ++hoverCounter;
  104. const raw = e.target.textContent;
  105. const name = cleanName(raw);
  106.  
  107. tip.innerHTML = `<p>Loading <strong>${name}</strong>…</p>`;
  108. tip.style.display = 'block';
  109. positionTip(e);
  110.  
  111. // smaller debounce for snappier feel
  112. await new Promise(r => setTimeout(r, 100));
  113. if (thisHover !== hoverCounter) return;
  114.  
  115. const data = await fetchSteam(name);
  116. if (thisHover !== hoverCounter) return;
  117.  
  118. if (!data) {
  119. tip.innerHTML = `<p>No Steam info for<br><strong>${name}</strong>.</p>`;
  120. return;
  121. }
  122. tip.innerHTML = `
  123. <img src="${data.header_image}" style="width:100%;margin-bottom:6px">
  124. <p>${data.short_description}</p>
  125. `;
  126. }
  127.  
  128. function hideTip(){
  129. hoverCounter++;
  130. tip.style.display = 'none';
  131. }
  132.  
  133. const SEL = 'td.coll-1 a[href^="/torrent/"]';
  134. document.addEventListener('mouseover', e => {
  135. if (e.target.matches(SEL)) showTip(e);
  136. });
  137. document.addEventListener('mousemove', e => {
  138. if (e.target.matches(SEL) && tip.style.display === 'block') positionTip(e);
  139. });
  140. document.addEventListener('mouseout', e => {
  141. if (e.target.matches(SEL)) hideTip(e);
  142. });
  143. })();