Organize and manage YouTube video thumbnails with sorting and filtering options
Size
14.5 KB
Version
1.0.1
Created
Mar 16, 2026
Updated
about 1 month ago
1// ==UserScript==
2// @name YouTube Thumbnail Organizer
3// @description Organize and manage YouTube video thumbnails with sorting and filtering options
4// @version 1.0.1
5// @match https://*.youtube.com/*
6// @icon https://www.youtube.com/s/desktop/c9b3ffed/img/favicon_32x32.png
7// ==/UserScript==
8(function() {
9 'use strict';
10
11 console.log('YouTube Thumbnail Organizer initialized');
12
13 // Add custom styles
14 TM_addStyle(`
15 #thumbnail-organizer-panel {
16 position: fixed;
17 top: 80px;
18 right: 20px;
19 width: 320px;
20 background: #0f0f0f;
21 border: 1px solid #3f3f3f;
22 border-radius: 12px;
23 padding: 16px;
24 z-index: 9999;
25 box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
26 color: #fff;
27 font-family: "Roboto", "Arial", sans-serif;
28 max-height: 80vh;
29 overflow-y: auto;
30 }
31
32 #thumbnail-organizer-panel.hidden {
33 display: none;
34 }
35
36 .organizer-header {
37 display: flex;
38 justify-content: space-between;
39 align-items: center;
40 margin-bottom: 16px;
41 padding-bottom: 12px;
42 border-bottom: 1px solid #3f3f3f;
43 }
44
45 .organizer-header h3 {
46 margin: 0;
47 font-size: 16px;
48 font-weight: 500;
49 color: #fff;
50 }
51
52 .organizer-close {
53 background: none;
54 border: none;
55 color: #aaa;
56 font-size: 24px;
57 cursor: pointer;
58 padding: 0;
59 width: 32px;
60 height: 32px;
61 display: flex;
62 align-items: center;
63 justify-content: center;
64 border-radius: 50%;
65 transition: background 0.2s;
66 }
67
68 .organizer-close:hover {
69 background: #3f3f3f;
70 color: #fff;
71 }
72
73 .organizer-controls {
74 display: flex;
75 flex-direction: column;
76 gap: 12px;
77 margin-bottom: 16px;
78 }
79
80 .organizer-control-group {
81 display: flex;
82 flex-direction: column;
83 gap: 6px;
84 }
85
86 .organizer-control-group label {
87 font-size: 13px;
88 color: #aaa;
89 font-weight: 500;
90 }
91
92 .organizer-select, .organizer-button {
93 background: #272727;
94 border: 1px solid #3f3f3f;
95 color: #fff;
96 padding: 10px 12px;
97 border-radius: 8px;
98 font-size: 14px;
99 cursor: pointer;
100 transition: all 0.2s;
101 }
102
103 .organizer-select:hover, .organizer-button:hover {
104 background: #3f3f3f;
105 border-color: #5f5f5f;
106 }
107
108 .organizer-button {
109 font-weight: 500;
110 text-align: center;
111 }
112
113 .organizer-button.primary {
114 background: #3ea6ff;
115 border-color: #3ea6ff;
116 }
117
118 .organizer-button.primary:hover {
119 background: #4db3ff;
120 border-color: #4db3ff;
121 }
122
123 .thumbnail-stats {
124 background: #272727;
125 padding: 12px;
126 border-radius: 8px;
127 font-size: 13px;
128 line-height: 1.6;
129 }
130
131 .thumbnail-stats div {
132 display: flex;
133 justify-content: space-between;
134 margin-bottom: 6px;
135 }
136
137 .thumbnail-stats div:last-child {
138 margin-bottom: 0;
139 }
140
141 .thumbnail-stats span:first-child {
142 color: #aaa;
143 }
144
145 .thumbnail-stats span:last-child {
146 color: #fff;
147 font-weight: 500;
148 }
149
150 #thumbnail-organizer-toggle {
151 position: fixed;
152 top: 80px;
153 right: 20px;
154 background: #3ea6ff;
155 border: none;
156 color: #fff;
157 padding: 12px 16px;
158 border-radius: 8px;
159 cursor: pointer;
160 font-size: 14px;
161 font-weight: 500;
162 z-index: 9998;
163 box-shadow: 0 2px 8px rgba(62, 166, 255, 0.3);
164 transition: all 0.2s;
165 font-family: "Roboto", "Arial", sans-serif;
166 }
167
168 #thumbnail-organizer-toggle:hover {
169 background: #4db3ff;
170 box-shadow: 0 4px 12px rgba(62, 166, 255, 0.4);
171 transform: translateY(-1px);
172 }
173
174 .thumbnail-highlight {
175 outline: 3px solid #3ea6ff !important;
176 outline-offset: 2px;
177 border-radius: 8px;
178 }
179
180 .thumbnail-grid-view ytd-compact-video-renderer,
181 .thumbnail-grid-view ytd-video-renderer {
182 display: inline-block !important;
183 width: calc(50% - 8px) !important;
184 margin: 4px !important;
185 vertical-align: top !important;
186 }
187 `);
188
189 // Create toggle button
190 function createToggleButton() {
191 const toggleBtn = document.createElement('button');
192 toggleBtn.id = 'thumbnail-organizer-toggle';
193 toggleBtn.textContent = '🖼️ Thumbnail Organizer';
194 toggleBtn.addEventListener('click', togglePanel);
195 document.body.appendChild(toggleBtn);
196 console.log('Toggle button created');
197 }
198
199 // Create organizer panel
200 function createOrganizerPanel() {
201 const panel = document.createElement('div');
202 panel.id = 'thumbnail-organizer-panel';
203 panel.className = 'hidden';
204
205 panel.innerHTML = `
206 <div class="organizer-header">
207 <h3>🖼️ Thumbnail Organizer</h3>
208 <button class="organizer-close">×</button>
209 </div>
210
211 <div class="organizer-controls">
212 <div class="organizer-control-group">
213 <label>Sort Thumbnails By:</label>
214 <select class="organizer-select" id="sort-option">
215 <option value="default">Default Order</option>
216 <option value="size">Size (Largest First)</option>
217 <option value="position">Position (Top to Bottom)</option>
218 </select>
219 </div>
220
221 <div class="organizer-control-group">
222 <label>View Mode:</label>
223 <select class="organizer-select" id="view-mode">
224 <option value="default">Default View</option>
225 <option value="grid">Grid View</option>
226 <option value="highlight">Highlight All</option>
227 </select>
228 </div>
229
230 <button class="organizer-button primary" id="download-thumbnails">
231 📥 Download All Thumbnails
232 </button>
233
234 <button class="organizer-button" id="refresh-stats">
235 🔄 Refresh Stats
236 </button>
237 </div>
238
239 <div class="thumbnail-stats" id="thumbnail-stats">
240 <div>
241 <span>Total Thumbnails:</span>
242 <span id="total-count">0</span>
243 </div>
244 <div>
245 <span>Visible Thumbnails:</span>
246 <span id="visible-count">0</span>
247 </div>
248 <div>
249 <span>Current Page:</span>
250 <span id="current-page">Watch</span>
251 </div>
252 </div>
253 `;
254
255 document.body.appendChild(panel);
256 console.log('Organizer panel created');
257
258 // Add event listeners
259 panel.querySelector('.organizer-close').addEventListener('click', togglePanel);
260 panel.querySelector('#sort-option').addEventListener('change', handleSortChange);
261 panel.querySelector('#view-mode').addEventListener('change', handleViewModeChange);
262 panel.querySelector('#download-thumbnails').addEventListener('click', downloadAllThumbnails);
263 panel.querySelector('#refresh-stats').addEventListener('click', updateStats);
264
265 // Initial stats update
266 updateStats();
267 }
268
269 // Toggle panel visibility
270 function togglePanel() {
271 const panel = document.getElementById('thumbnail-organizer-panel');
272 const toggleBtn = document.getElementById('thumbnail-organizer-toggle');
273
274 if (panel.classList.contains('hidden')) {
275 panel.classList.remove('hidden');
276 toggleBtn.style.display = 'none';
277 updateStats();
278 } else {
279 panel.classList.add('hidden');
280 toggleBtn.style.display = 'block';
281 }
282 }
283
284 // Get all thumbnails
285 function getAllThumbnails() {
286 const thumbnails = document.querySelectorAll('ytd-thumbnail img, ytd-video-preview img');
287 console.log(`Found ${thumbnails.length} thumbnails`);
288 return Array.from(thumbnails);
289 }
290
291 // Update statistics
292 function updateStats() {
293 const thumbnails = getAllThumbnails();
294 const visibleThumbnails = thumbnails.filter(thumb => {
295 const rect = thumb.getBoundingClientRect();
296 return rect.width > 0 && rect.height > 0;
297 });
298
299 document.getElementById('total-count').textContent = thumbnails.length;
300 document.getElementById('visible-count').textContent = visibleThumbnails.length;
301
302 // Detect current page type
303 const url = window.location.href;
304 let pageType = 'Unknown';
305 if (url.includes('/watch')) pageType = 'Watch';
306 else if (url.includes('/results')) pageType = 'Search';
307 else if (url.includes('/channel') || url.includes('/@')) pageType = 'Channel';
308 else if (url === 'https://www.youtube.com/' || url === 'https://www.youtube.com') pageType = 'Home';
309
310 document.getElementById('current-page').textContent = pageType;
311
312 console.log(`Stats updated: ${thumbnails.length} total, ${visibleThumbnails.length} visible`);
313 }
314
315 // Handle sort change
316 function handleSortChange(event) {
317 const sortOption = event.target.value;
318 console.log(`Sort option changed to: ${sortOption}`);
319
320 const thumbnails = getAllThumbnails();
321 const containers = thumbnails.map(thumb => {
322 let container = thumb.closest('ytd-compact-video-renderer, ytd-video-renderer, ytd-grid-video-renderer, ytd-rich-item-renderer');
323 return container;
324 }).filter(c => c !== null);
325
326 if (sortOption === 'default') {
327 // Reload page to restore default order
328 console.log('Restoring default order');
329 return;
330 }
331
332 let sortedContainers = [...containers];
333
334 if (sortOption === 'size') {
335 sortedContainers.sort((a, b) => {
336 const imgA = a.querySelector('img');
337 const imgB = b.querySelector('img');
338 const sizeA = imgA ? imgA.naturalWidth * imgA.naturalHeight : 0;
339 const sizeB = imgB ? imgB.naturalWidth * imgB.naturalHeight : 0;
340 return sizeB - sizeA;
341 });
342 } else if (sortOption === 'position') {
343 sortedContainers.sort((a, b) => {
344 const rectA = a.getBoundingClientRect();
345 const rectB = b.getBoundingClientRect();
346 return rectA.top - rectB.top;
347 });
348 }
349
350 // Reorder elements
351 const parent = containers[0]?.parentElement;
352 if (parent) {
353 sortedContainers.forEach(container => {
354 parent.appendChild(container);
355 });
356 console.log('Thumbnails reordered');
357 }
358 }
359
360 // Handle view mode change
361 function handleViewModeChange(event) {
362 const viewMode = event.target.value;
363 console.log(`View mode changed to: ${viewMode}`);
364
365 const thumbnails = getAllThumbnails();
366
367 // Remove all previous effects
368 thumbnails.forEach(thumb => thumb.classList.remove('thumbnail-highlight'));
369 document.body.classList.remove('thumbnail-grid-view');
370
371 if (viewMode === 'highlight') {
372 thumbnails.forEach(thumb => thumb.classList.add('thumbnail-highlight'));
373 console.log('Highlighted all thumbnails');
374 } else if (viewMode === 'grid') {
375 document.body.classList.add('thumbnail-grid-view');
376 console.log('Applied grid view');
377 }
378 }
379
380 // Download all thumbnails
381 async function downloadAllThumbnails() {
382 console.log('Starting thumbnail download...');
383 const thumbnails = getAllThumbnails();
384
385 if (thumbnails.length === 0) {
386 alert('No thumbnails found on this page!');
387 return;
388 }
389
390 let downloadCount = 0;
391
392 for (let i = 0; i < thumbnails.length; i++) {
393 const thumb = thumbnails[i];
394 const src = thumb.src || thumb.getAttribute('src');
395
396 if (src && !src.includes('data:image')) {
397 try {
398 // Get high quality version
399 const highQualitySrc = src.replace(/=s\d+-/, '=s1920-').replace(/\/hqdefault\./, '/maxresdefault.');
400
401 await GM.openInTab(highQualitySrc, true);
402 downloadCount++;
403
404 // Small delay to avoid overwhelming the browser
405 await new Promise(resolve => setTimeout(resolve, 500));
406 } catch (error) {
407 console.error('Error downloading thumbnail:', error);
408 }
409 }
410 }
411
412 alert(`Opened ${downloadCount} thumbnails in new tabs. Right-click and save each image.`);
413 console.log(`Opened ${downloadCount} thumbnails`);
414 }
415
416 // Initialize the extension
417 function init() {
418 console.log('Initializing YouTube Thumbnail Organizer...');
419
420 // Wait for page to be ready
421 if (document.body) {
422 createToggleButton();
423 createOrganizerPanel();
424
425 // Update stats when thumbnails load
426 const observer = new MutationObserver(() => {
427 updateStats();
428 });
429
430 observer.observe(document.body, {
431 childList: true,
432 subtree: true
433 });
434
435 console.log('YouTube Thumbnail Organizer ready!');
436 } else {
437 setTimeout(init, 1000);
438 }
439 }
440
441 // Start the extension
442 TM_runBody(init);
443})();