YouTube Enhancer (Thumbnail Preview)

View Original Avatar, Banner, Video and Shorts Thumbnails.

  1. // ==UserScript==
  2. // @name YouTube Enhancer (Thumbnail Preview)
  3. // @description View Original Avatar, Banner, Video and Shorts Thumbnails.
  4. // @icon https://raw.githubusercontent.com/exyezed/youtube-enhancer/refs/heads/main/extras/youtube-enhancer.png
  5. // @version 1.5
  6. // @author exyezed
  7. // @namespace https://github.com/exyezed/youtube-enhancer/
  8. // @supportURL https://github.com/exyezed/youtube-enhancer/issues
  9. // @license MIT
  10. // @match https://www.youtube.com/*
  11. // @grant GM_addStyle
  12. // @grant GM_openInTab
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. let GM_addStyle;
  19. if (typeof GM_addStyle === 'undefined') {
  20. GM_addStyle = function(css) {
  21. let style = document.createElement('style');
  22. style.textContent = css;
  23. document.head.appendChild(style);
  24. }
  25. }
  26.  
  27. let GM_openInTab;
  28. if (typeof GM_openInTab === 'undefined') {
  29. GM_openInTab = function(url) {
  30. window.open(url, '_blank');
  31. }
  32. }
  33.  
  34. GM_addStyle(`
  35. .thumbnailPreview-button {
  36. position: absolute;
  37. bottom: 10px;
  38. left: 5px;
  39. background-color: rgba(0, 0, 0, 0.75);
  40. color: white;
  41. border: none;
  42. border-radius: 3px;
  43. padding: 3px;
  44. font-size: 18px;
  45. cursor: pointer;
  46. z-index: 2000;
  47. opacity: 0;
  48. transition: opacity 0.3s;
  49. display: flex;
  50. align-items: center;
  51. justify-content: center;
  52. }
  53. .thumbnailPreview-container {
  54. position: relative;
  55. }
  56. .thumbnailPreview-container:hover .thumbnailPreview-button {
  57. opacity: 1;
  58. }
  59. #thumbnailPreview-custom-image {
  60. width: 100%;
  61. height: auto;
  62. margin-bottom: 10px;
  63. box-sizing: border-box;
  64. border-radius: 10px;
  65. cursor: pointer;
  66. }
  67. .thumbnailShortsPreview {
  68. position: absolute;
  69. top: 5px;
  70. left: 5px;
  71. z-index: 2000;
  72. background: rgba(0, 0, 0, 0.5);
  73. color: white;
  74. border: none;
  75. border-radius: 4px;
  76. cursor: pointer;
  77. display: flex;
  78. align-items: center;
  79. justify-content: center;
  80. width: 24px;
  81. height: 24px;
  82. padding: 0;
  83. transition: background-color 0.3s ease;
  84. }
  85. .thumbnailShortsPreview:hover {
  86. background-color: rgba(0, 0, 0, 0.75) !important;
  87. }
  88. .youtube-enhancer-icon {
  89. display: flex;
  90. align-items: center;
  91. justify-content: center;
  92. width: 20px;
  93. height: 20px;
  94. }
  95. .youtube-enhancer-icon svg {
  96. width: 100%;
  97. height: 100%;
  98. }
  99. `);
  100.  
  101. function createSVGElement(pathD) {
  102. const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  103. const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
  104. svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
  105. svg.setAttribute("width", "1em");
  106. svg.setAttribute("height", "1em");
  107. svg.setAttribute("viewBox", "0 0 24 24");
  108. path.setAttribute("fill", "currentColor");
  109. path.setAttribute("d", pathD);
  110. svg.appendChild(path);
  111. return svg;
  112. }
  113.  
  114. function openImageInNewTab(url) {
  115. if (!url) return;
  116. try {
  117. if (typeof GM_openInTab !== 'undefined') {
  118. GM_openInTab(url, { active: true, insert: true, setParent: true });
  119. } else {
  120. const newWindow = window.open(url, '_blank');
  121. if (newWindow) {
  122. newWindow.focus();
  123. }
  124. }
  125. } catch (e) {
  126. const a = document.createElement('a');
  127. a.href = url;
  128. a.target = '_blank';
  129. a.rel = 'noopener noreferrer';
  130. document.body.appendChild(a);
  131. a.click();
  132. setTimeout(() => {
  133. document.body.removeChild(a);
  134. }, 100);
  135. }
  136. }
  137.  
  138. const defaultIconPath = "M18 20H4V6h9V4H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-9h-2zm-7.79-3.17l-1.96-2.36L5.5 18h11l-3.54-4.71zM20 4V1h-2v3h-3c.01.01 0 2 0 2h3v2.99c.01.01 2 0 2 0V6h3V4";
  139. const hoverIconPath = "M19 7v2.99s-1.99.01-2 0V7h-3s.01-1.99 0-2h3V2h2v3h3v2zm-3 4V8h-3V5H5c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2v-8zM5 19l3-4l2 3l3-4l4 5z";
  140.  
  141. let thumbnailPreviewCurrentVideoId = '';
  142. let thumbnailInsertionAttempts = 0;
  143. const MAX_ATTEMPTS = 10;
  144. const RETRY_DELAY = 500;
  145.  
  146. function isWatchPage() {
  147. const url = new URL(window.location.href);
  148. return url.pathname === '/watch' && (
  149. url.searchParams.has('v') ||
  150. url.searchParams.has('list') ||
  151. url.searchParams.has('start_radio')
  152. );
  153. }
  154.  
  155. function addButtonToElement(element, getFullSizeUrl) {
  156. if (!element.closest('.thumbnailPreview-container')) {
  157. const container = document.createElement('div');
  158. container.className = 'thumbnailPreview-container';
  159. element.parentNode.insertBefore(container, element);
  160. container.appendChild(element);
  161.  
  162. const button = document.createElement('button');
  163. button.className = 'thumbnailPreview-button';
  164. const defaultIcon = createSVGElement(defaultIconPath);
  165. button.appendChild(defaultIcon);
  166. button.addEventListener('mouseenter', () => {
  167. button.textContent = '';
  168. button.appendChild(createSVGElement(hoverIconPath));
  169. });
  170. button.addEventListener('mouseleave', () => {
  171. button.textContent = '';
  172. button.appendChild(createSVGElement(defaultIconPath));
  173. });
  174. button.addEventListener('click', function(e) {
  175. if (e.stopPropagation) e.stopPropagation();
  176. if (e.preventDefault) e.preventDefault();
  177. e.cancelBubble = true;
  178. const url = getFullSizeUrl(element.src);
  179. if (url) {
  180. openImageInNewTab(url);
  181. }
  182. return false;
  183. }, true);
  184. container.appendChild(button);
  185. }
  186. }
  187.  
  188. function processAvatars() {
  189. const avatars = document.querySelectorAll('yt-avatar-shape img, yt-img-shadow#avatar img');
  190. avatars.forEach(img => {
  191. if (!img.closest('.thumbnailPreview-container')) {
  192. addButtonToElement(img, (src) => src.replace(/=s\d+-c-k-c0x00ffffff-no-rj.*/, '=s0'));
  193. if (isWatchPage()) {
  194. const button = img.closest('.thumbnailPreview-container').querySelector('.thumbnailPreview-button');
  195. if (button) {
  196. button.style.display = 'none';
  197. }
  198. }
  199. }
  200. });
  201. }
  202.  
  203. function processChannelBanners() {
  204. const banners = document.querySelectorAll('yt-image-banner-view-model img');
  205. banners.forEach(img => {
  206. if (!img.closest('.thumbnailPreview-container')) {
  207. addButtonToElement(img, (src) => src.replace(/=w\d+-.*/, '=s0'));
  208. }
  209. });
  210. }
  211.  
  212. function processVideoThumbnails() {
  213. const thumbnails = document.querySelectorAll('ytd-thumbnail img, ytd-playlist-thumbnail img');
  214. thumbnails.forEach(img => {
  215. if (!img.closest('.thumbnailPreview-container')) {
  216. addButtonToElement(img, (src) => {
  217. const videoId = src.match(/\/vi\/([^\/]+)/);
  218. if (videoId && videoId[1]) {
  219. return `https://i.ytimg.com/vi/${videoId[1]}/maxresdefault.jpg`;
  220. }
  221. return src;
  222. });
  223. }
  224. });
  225. }
  226.  
  227. function addOrUpdateThumbnailImage() {
  228. const newVideoId = new URLSearchParams(window.location.search).get('v');
  229. if (!newVideoId || newVideoId === thumbnailPreviewCurrentVideoId) {
  230. return;
  231. }
  232.  
  233. thumbnailPreviewCurrentVideoId = newVideoId;
  234.  
  235. function attemptInsertion() {
  236. const targetElement = document.querySelector('#secondary-inner #panels');
  237. const existingImg = document.getElementById('thumbnailPreview-custom-image');
  238. if (existingImg) {
  239. existingImg.src = `https://i.ytimg.com/vi/${thumbnailPreviewCurrentVideoId}/mqdefault.jpg`;
  240. thumbnailInsertionAttempts = 0;
  241. return;
  242. }
  243.  
  244. if (!targetElement) {
  245. thumbnailInsertionAttempts++;
  246. if (thumbnailInsertionAttempts < MAX_ATTEMPTS) {
  247. setTimeout(attemptInsertion, RETRY_DELAY);
  248. } else {
  249. thumbnailInsertionAttempts = 0;
  250. }
  251. return;
  252. }
  253.  
  254. const img = document.createElement('img');
  255. img.id = 'thumbnailPreview-custom-image';
  256. img.src = `https://i.ytimg.com/vi/${thumbnailPreviewCurrentVideoId}/mqdefault.jpg`;
  257. img.addEventListener('click', function(e) {
  258. if (e.stopPropagation) e.stopPropagation();
  259. if (e.preventDefault) e.preventDefault();
  260. e.cancelBubble = true;
  261. const maxResUrl = `https://i.ytimg.com/vi/${thumbnailPreviewCurrentVideoId}/maxresdefault.jpg`;
  262. openImageInNewTab(maxResUrl);
  263. return false;
  264. }, true);
  265.  
  266. targetElement.parentNode.insertBefore(img, targetElement);
  267. thumbnailInsertionAttempts = 0;
  268. }
  269.  
  270. attemptInsertion();
  271. }
  272.  
  273. function getVideoIdFromShorts(href) {
  274. const match = href.match(/\/shorts\/([^/?]+)/);
  275. return match ? match[1] : null;
  276. }
  277.  
  278. function findShortsContainers() {
  279. let containers = [];
  280. const reelItems = document.querySelectorAll('ytd-reel-item-renderer');
  281. if (reelItems.length > 0) {
  282. containers = Array.from(reelItems);
  283. } else {
  284. const shortsLockup = document.querySelectorAll('ytm-shorts-lockup-view-model.ShortsLockupViewModelHost');
  285. if (shortsLockup.length > 0) {
  286. containers = Array.from(shortsLockup);
  287. } else {
  288. const gridVideos = document.querySelectorAll('ytd-grid-video-renderer');
  289. containers = Array.from(gridVideos).filter(container => {
  290. const link = container.querySelector('a[href*="/shorts/"]');
  291. return link !== null;
  292. });
  293. if (containers.length === 0) {
  294. const richItems = document.querySelectorAll('ytd-rich-item-renderer');
  295. containers = Array.from(richItems).filter(container => {
  296. const link = container.querySelector('a[href*="/shorts/"]');
  297. return link !== null;
  298. });
  299. }
  300. }
  301. }
  302. return containers;
  303. }
  304.  
  305. function addShortsThumbnailButton(container) {
  306. if (container.querySelector('.thumbnailShortsPreview') || container.dataset.shortsButtonAdded === 'true') {
  307. return;
  308. }
  309. let linkElement = container.querySelector('a[href*="/shorts/"]');
  310. if (!linkElement && container.tagName === 'A' && container.href && container.href.includes('/shorts/')) {
  311. linkElement = container;
  312. }
  313. if (!linkElement) {
  314. return;
  315. }
  316. const videoId = getVideoIdFromShorts(linkElement.href);
  317. if (!videoId) {
  318. return;
  319. }
  320.  
  321. let thumbnailContainer = container.querySelector('#thumbnail');
  322. if (!thumbnailContainer) {
  323. thumbnailContainer = container.querySelector('.thumbnail');
  324. }
  325. if (!thumbnailContainer) {
  326. thumbnailContainer = container;
  327. }
  328.  
  329. const button = document.createElement('button');
  330. button.className = 'thumbnailShortsPreview';
  331. button.title = 'View original thumbnail';
  332. const iconContainer = document.createElement('span');
  333. iconContainer.className = 'youtube-enhancer-icon';
  334. const defaultIcon = createSVGElement(defaultIconPath);
  335. iconContainer.appendChild(defaultIcon);
  336. button.appendChild(iconContainer);
  337. button.addEventListener('mouseenter', () => {
  338. iconContainer.removeChild(iconContainer.firstChild);
  339. iconContainer.appendChild(createSVGElement(hoverIconPath));
  340. });
  341.  
  342. button.addEventListener('mouseleave', () => {
  343. iconContainer.removeChild(iconContainer.firstChild);
  344. iconContainer.appendChild(createSVGElement(defaultIconPath));
  345. });
  346.  
  347. button.addEventListener('click', function(e) {
  348. if (e.stopPropagation) e.stopPropagation();
  349. if (e.preventDefault) e.preventDefault();
  350. e.cancelBubble = true;
  351. const url = `https://i.ytimg.com/vi/${videoId}/oardefault.jpg`;
  352. openImageInNewTab(url);
  353. return false;
  354. }, true);
  355.  
  356. thumbnailContainer.style.position = 'relative';
  357. thumbnailContainer.appendChild(button);
  358. container.dataset.shortsButtonAdded = 'true';
  359. }
  360.  
  361. function observePageChanges() {
  362. const contentObserver = new MutationObserver((mutations) => {
  363. let shouldProcessRegular = false;
  364. let shouldProcessShorts = false;
  365. mutations.forEach(mutation => {
  366. if (mutation.addedNodes.length > 0) {
  367. shouldProcessRegular = true;
  368. shouldProcessShorts = true;
  369. }
  370. });
  371. if (shouldProcessRegular) {
  372. processAvatars();
  373. processChannelBanners();
  374. processVideoThumbnails();
  375. }
  376. if (shouldProcessShorts) {
  377. const shortsContainers = findShortsContainers();
  378. shortsContainers.forEach(addShortsThumbnailButton);
  379. }
  380. });
  381.  
  382. const panelObserver = new MutationObserver((mutations) => {
  383. for (const mutation of mutations) {
  384. if (mutation.type === 'childList' &&
  385. (mutation.target.id === 'secondary' ||
  386. mutation.target.id === 'secondary-inner')) {
  387. addOrUpdateThumbnailImage();
  388. }
  389. }
  390. });
  391.  
  392. contentObserver.observe(document.body, {
  393. childList: true,
  394. subtree: true
  395. });
  396.  
  397. const observeSecondary = () => {
  398. const secondary = document.getElementById('secondary');
  399. if (secondary) {
  400. panelObserver.observe(secondary, {
  401. childList: true,
  402. subtree: true
  403. });
  404. } else {
  405. setTimeout(observeSecondary, 1000);
  406. }
  407. };
  408. observeSecondary();
  409. }
  410.  
  411. function initialize() {
  412. processAvatars();
  413. processChannelBanners();
  414. processVideoThumbnails();
  415. addOrUpdateThumbnailImage();
  416. const shortsContainers = findShortsContainers();
  417. shortsContainers.forEach(addShortsThumbnailButton);
  418. observePageChanges();
  419. window.addEventListener('yt-navigate-finish', () => {
  420. addOrUpdateThumbnailImage();
  421. setTimeout(() => {
  422. const shortsContainers = findShortsContainers();
  423. shortsContainers.forEach(addShortsThumbnailButton);
  424. }, 1000);
  425. });
  426. setInterval(() => {
  427. const shortsContainers = findShortsContainers();
  428. shortsContainers.forEach(addShortsThumbnailButton);
  429. }, 3000);
  430. }
  431.  
  432. if (document.readyState === 'loading') {
  433. document.addEventListener('DOMContentLoaded', initialize);
  434. } else {
  435. initialize();
  436. }
  437. })();