Adds preview thumbnails to torrent listings from detail pages
Size
8.6 KB
Version
1.2.5
Created
Jan 17, 2026
Updated
18 days ago
1// ==UserScript==
2// @name Torrenting.com Thumbnail Viewer
3// @description Adds preview thumbnails to torrent listings from detail pages
4// @version 1.2.5
5// @match https://*.torrenting.com/*
6// @icon https://www.torrenting.com/T-favicon.ico
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 console.log('Torrenting.com Thumbnail Viewer loaded');
12
13 // Cache for storing fetched thumbnails
14 const thumbnailCache = {};
15
16 // Add styles for thumbnails
17 TM_addStyle(`
18 .thumbnail-container {
19 display: inline-block;
20 margin-right: 10px;
21 vertical-align: middle;
22 position: relative;
23 }
24
25 .thumbnail-img {
26 width: 100%;
27 height: auto;
28 max-width: 350px;
29 object-fit: contain;
30 border-radius: 4px;
31 box-shadow: 0 2px 4px rgba(0,0,0,0.3);
32 cursor: pointer;
33 transition: transform 0.2s;
34 display: block;
35 }
36
37 .thumbnail-img:hover {
38 transform: scale(1.05);
39 z-index: 1000;
40 box-shadow: 0 4px 12px rgba(0,0,0,0.5);
41 }
42
43 .thumbnail-loading {
44 width: 120px;
45 height: 180px;
46 background: linear-gradient(90deg, #333 25%, #444 50%, #333 75%);
47 background-size: 200% 100%;
48 animation: loading 1.5s infinite;
49 border-radius: 4px;
50 }
51
52 @keyframes loading {
53 0% { background-position: 200% 0; }
54 100% { background-position: -200% 0; }
55 }
56
57 .thumbnail-error {
58 width: 120px;
59 height: 180px;
60 background: #2a2a2a;
61 border: 1px solid #444;
62 border-radius: 4px;
63 display: flex;
64 align-items: center;
65 justify-content: center;
66 font-size: 10px;
67 color: #666;
68 }
69 `);
70
71 // Function to fetch thumbnail from details page
72 async function fetchThumbnail(detailsUrl, torrentId) {
73 // Check cache first
74 if (thumbnailCache[torrentId]) {
75 return thumbnailCache[torrentId];
76 }
77
78 try {
79 console.log('Fetching thumbnail for:', detailsUrl);
80 const response = await fetch(detailsUrl);
81 const html = await response.text();
82
83 const parser = new DOMParser();
84 const doc = parser.parseFromString(html, 'text/html');
85
86 // First, try to find screenshots in the release-content area
87 const releaseContent = doc.querySelector('.release-content');
88 if (releaseContent) {
89 const screenshots = Array.from(releaseContent.querySelectorAll('img')).filter(img => {
90 const src = img.src || '';
91 // Filter for actual screenshot images (not smilies or UI elements)
92 return !src.includes('smilies') &&
93 !src.includes('icon') &&
94 !src.includes('avatar') &&
95 !src.includes('button') &&
96 src.length > 20;
97 });
98
99 if (screenshots.length > 0) {
100 const thumbnailUrl = screenshots[0].src;
101 thumbnailCache[torrentId] = thumbnailUrl;
102 console.log('Found screenshot:', thumbnailUrl);
103 return thumbnailUrl;
104 }
105 }
106
107 // Fallback: Look for images in main content, excluding sidebar
108 const mainContent = doc.querySelector('#mainContent');
109 const latestMov = doc.querySelector('#latestMov');
110
111 if (mainContent) {
112 const allImages = Array.from(mainContent.querySelectorAll('img'));
113 const sidebarImages = latestMov ? Array.from(latestMov.querySelectorAll('img')) : [];
114
115 const contentImages = allImages.filter(img => {
116 const src = img.src || '';
117 return !sidebarImages.includes(img) &&
118 !src.includes('smilies') &&
119 !src.includes('icon') &&
120 !src.includes('avatar') &&
121 !src.includes('logo') &&
122 !src.includes('button') &&
123 !src.includes('TT-Avatar') &&
124 src.length > 20;
125 });
126
127 if (contentImages.length > 0) {
128 const thumbnailUrl = contentImages[0].src;
129 thumbnailCache[torrentId] = thumbnailUrl;
130 console.log('Found content image:', thumbnailUrl);
131 return thumbnailUrl;
132 }
133 }
134
135 console.log('No thumbnail found for:', torrentId);
136 return null;
137 } catch (error) {
138 console.error('Error fetching thumbnail:', error);
139 return null;
140 }
141 }
142
143 // Function to add thumbnail to a torrent row
144 async function addThumbnailToRow(row) {
145 const nameCell = row.querySelector('td.browse a.nameLink');
146 if (!nameCell) return;
147
148 const detailsUrl = nameCell.href;
149 const torrentId = detailsUrl.match(/id=(\d+)/)?.[1];
150 if (!torrentId) return;
151
152 // Check if thumbnail already added
153 if (nameCell.querySelector('.thumbnail-container')) return;
154
155 // Create thumbnail container with loading state
156 const thumbnailContainer = document.createElement('span');
157 thumbnailContainer.className = 'thumbnail-container';
158
159 const loadingDiv = document.createElement('div');
160 loadingDiv.className = 'thumbnail-loading';
161 thumbnailContainer.appendChild(loadingDiv);
162
163 // Insert thumbnail before the link text
164 nameCell.insertBefore(thumbnailContainer, nameCell.firstChild);
165
166 // Fetch and display thumbnail
167 const thumbnailUrl = await fetchThumbnail(detailsUrl, torrentId);
168
169 if (thumbnailUrl) {
170 const img = document.createElement('img');
171 img.className = 'thumbnail-img';
172 img.src = thumbnailUrl;
173 img.alt = 'Thumbnail';
174 img.title = 'Click to view full size';
175
176 // Replace loading with image
177 thumbnailContainer.innerHTML = '';
178 thumbnailContainer.appendChild(img);
179
180 // Add click handler to open details page
181 img.addEventListener('click', (e) => {
182 e.preventDefault();
183 e.stopPropagation();
184 window.open(detailsUrl, '_blank');
185 });
186 } else {
187 // Show error state
188 const errorDiv = document.createElement('div');
189 errorDiv.className = 'thumbnail-error';
190 errorDiv.textContent = 'N/A';
191 thumbnailContainer.innerHTML = '';
192 thumbnailContainer.appendChild(errorDiv);
193 }
194 }
195
196 // Function to process all torrent rows
197 async function processTorrentRows() {
198 const rows = document.querySelectorAll('tr.torrentsTableTR');
199 console.log('Found', rows.length, 'torrent rows');
200
201 // Process rows in batches to avoid overwhelming the server
202 const batchSize = 5;
203 for (let i = 0; i < rows.length; i += batchSize) {
204 const batch = Array.from(rows).slice(i, i + batchSize);
205 await Promise.all(batch.map(row => addThumbnailToRow(row)));
206
207 // Small delay between batches
208 if (i + batchSize < rows.length) {
209 await new Promise(resolve => setTimeout(resolve, 500));
210 }
211 }
212 }
213
214 // Initialize when page is ready
215 function init() {
216 // Check if we're on a browse/search page
217 if (window.location.href.includes('browse.php') ||
218 window.location.href.includes('torrents.php')) {
219
220 console.log('On browse page, adding thumbnails...');
221
222 // Wait for table to be ready
223 const checkTable = setInterval(() => {
224 const table = document.querySelector('#torrentsTable');
225 if (table) {
226 clearInterval(checkTable);
227 processTorrentRows();
228 }
229 }, 500);
230
231 // Clear interval after 10 seconds if table not found
232 setTimeout(() => clearInterval(checkTable), 10000);
233 }
234 }
235
236 // Run when DOM is ready
237 if (document.readyState === 'loading') {
238 document.addEventListener('DOMContentLoaded', init);
239 } else {
240 init();
241 }
242})();