YouTube Enhancer (Reveal Views & Upload Time)

Reveal Views & Upload Time.

  1. // ==UserScript==
  2. // @name YouTube Enhancer (Reveal Views & Upload Time)
  3. // @description Reveal Views & Upload Time.
  4. // @icon https://raw.githubusercontent.com/exyezed/youtube-enhancer/refs/heads/main/extras/youtube-enhancer.png
  5. // @version 1.3
  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. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. const badgeStyles = `
  17. @import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200');
  18. #secondary-inner .revealViewsAndUploadTime {
  19. height: 36px;
  20. font-size: 14px;
  21. font-weight: 500;
  22. border-radius: 8px;
  23. padding: 0 16px;
  24. font-family: inherit;
  25. border: 1px solid transparent;
  26. margin-bottom: 10px;
  27. width: 100%;
  28. box-sizing: border-box;
  29. display: flex;
  30. align-items: center;
  31. justify-content: center;
  32. gap: 8px;
  33. cursor: pointer;
  34. }
  35. html[dark] #secondary-inner .revealViewsAndUploadTime {
  36. background-color: #ffffff1a;
  37. color: var(--yt-spec-text-primary, #fff);
  38. }
  39. html:not([dark]) #secondary-inner .revealViewsAndUploadTime {
  40. background-color: #0000000d;
  41. color: var(--yt-spec-text-primary, #030303);
  42. }
  43. html[dark] #secondary-inner .revealViewsAndUploadTime:hover {
  44. background-color: #ffffff33;
  45. }
  46. html:not([dark]) #secondary-inner .revealViewsAndUploadTime:hover {
  47. background-color: #00000014;
  48. }
  49. #secondary-inner .revealViewsAndUploadTime .separator {
  50. margin: 0 2px;
  51. width: 1px;
  52. height: 24px;
  53. opacity: 0.3;
  54. }
  55. html[dark] #secondary-inner .revealViewsAndUploadTime .separator {
  56. background-color: var(--yt-spec-text-secondary, #aaa);
  57. }
  58. html:not([dark]) #secondary-inner .revealViewsAndUploadTime .separator {
  59. background-color: var(--yt-spec-text-secondary, #606060);
  60. }
  61.  
  62. .material-symbols-outlined {
  63. font-size: 24px;
  64. line-height: 1;
  65. font-variation-settings:
  66. 'FILL' 0,
  67. 'wght' 150,
  68. 'GRAD' 0,
  69. 'opsz' 24;
  70. }
  71. `;
  72.  
  73. function createBadge(viewCount, uploadTime, uploadDate) {
  74. const badge = document.createElement('div');
  75. badge.className = 'revealViewsAndUploadTime';
  76.  
  77. const mainIcon = document.createElement('span');
  78. mainIcon.className = 'material-symbols-outlined';
  79. mainIcon.textContent = 'visibility';
  80.  
  81. const dataSpan = document.createElement('span');
  82. dataSpan.textContent = viewCount;
  83.  
  84. const separator = document.createElement('div');
  85. separator.className = 'separator';
  86.  
  87. const timeIcon = document.createElement('span');
  88. timeIcon.className = 'material-symbols-outlined';
  89. timeIcon.textContent = 'schedule';
  90.  
  91. const timeSpan = document.createElement('span');
  92. timeSpan.textContent = uploadTime;
  93.  
  94. badge.appendChild(mainIcon);
  95. badge.appendChild(dataSpan);
  96. badge.appendChild(separator);
  97. badge.appendChild(timeIcon);
  98. badge.appendChild(timeSpan);
  99.  
  100. let isShowingViews = true;
  101. badge.addEventListener('click', () => {
  102. if (isShowingViews) {
  103. mainIcon.textContent = 'calendar_month';
  104. dataSpan.textContent = uploadDate;
  105. timeIcon.textContent = 'schedule';
  106. timeIcon.style.display = '';
  107. } else {
  108. mainIcon.textContent = 'visibility';
  109. dataSpan.textContent = viewCount;
  110. timeIcon.textContent = 'schedule';
  111. timeIcon.style.display = '';
  112. }
  113. isShowingViews = !isShowingViews;
  114. });
  115.  
  116. return badge;
  117. }
  118.  
  119. function getVideoId() {
  120. const urlObj = new URL(window.location.href);
  121. if (urlObj.pathname.includes('/watch')) {
  122. return urlObj.searchParams.get('v');
  123. } else if (urlObj.pathname.includes('/video/')) {
  124. return urlObj.pathname.split('/video/')[1];
  125. }
  126. return null;
  127. }
  128.  
  129. function formatNumber(number) {
  130. return new Intl.NumberFormat('en-US').format(number);
  131. }
  132.  
  133. function formatDate(dateString) {
  134. const date = new Date(dateString);
  135. const today = new Date();
  136. const yesterday = new Date(today);
  137. yesterday.setDate(yesterday.getDate() - 1);
  138.  
  139. if (date.toDateString() === today.toDateString()) {
  140. return 'Today';
  141. } else if (date.toDateString() === yesterday.toDateString()) {
  142. return 'Yesterday';
  143. } else {
  144. const options = {
  145. weekday: 'long',
  146. day: '2-digit',
  147. month: '2-digit',
  148. year: 'numeric',
  149. };
  150. const formattedDate = new Intl.DateTimeFormat('en-GB', options).format(date);
  151. const [dayName, datePart] = formattedDate.split(', ');
  152. return `${dayName}, ${datePart.replace(/\//g, '/')}`;
  153. }
  154. }
  155.  
  156. function formatTime(dateString) {
  157. const date = new Date(dateString);
  158. const options = {
  159. hour: '2-digit',
  160. minute: '2-digit',
  161. second: '2-digit',
  162. hour12: false
  163. };
  164. return new Intl.DateTimeFormat('en-GB', options).format(date);
  165. }
  166.  
  167. function getApiKey() {
  168. const scripts = document.getElementsByTagName('script');
  169. for (const script of scripts) {
  170. const match = script.textContent.match(/"INNERTUBE_API_KEY":\s*"([^"]+)"/);
  171. if (match && match[1]) return match[1];
  172. }
  173. return null;
  174. }
  175.  
  176. function getClientInfo() {
  177. const scripts = document.getElementsByTagName('script');
  178. let clientName = null;
  179. let clientVersion = null;
  180. for (const script of scripts) {
  181. const nameMatch = script.textContent.match(/"INNERTUBE_CLIENT_NAME":\s*"([^"]+)"/);
  182. const versionMatch = script.textContent.match(/"INNERTUBE_CLIENT_VERSION":\s*"([^"]+)"/);
  183. if (nameMatch && nameMatch[1]) clientName = nameMatch[1];
  184. if (versionMatch && versionMatch[1]) clientVersion = versionMatch[1];
  185. }
  186. return { clientName, clientVersion };
  187. }
  188.  
  189. async function fetchVideoInfo(videoId) {
  190. try {
  191. const apiKey = getApiKey();
  192. if (!apiKey) return null;
  193. const { clientName, clientVersion } = getClientInfo();
  194. if (!clientName || !clientVersion) return null;
  195. const response = await fetch(`https://www.youtube.com/youtubei/v1/player?key=${apiKey}`, {
  196. method: 'POST',
  197. headers: {
  198. 'Content-Type': 'application/json',
  199. },
  200. body: JSON.stringify({
  201. videoId: videoId,
  202. context: {
  203. client: {
  204. clientName: clientName,
  205. clientVersion: clientVersion,
  206. }
  207. }
  208. })
  209. });
  210. if (!response.ok) return null;
  211. const data = await response.json();
  212. let viewCount = "Unknown";
  213. if (data.videoDetails?.viewCount) {
  214. viewCount = formatNumber(data.videoDetails.viewCount);
  215. }
  216. let publishDate = "Unknown";
  217. if (data.microformat?.playerMicroformatRenderer?.publishDate) {
  218. publishDate = data.microformat.playerMicroformatRenderer.publishDate;
  219. }
  220. return {
  221. viewCount,
  222. uploadDate: publishDate
  223. };
  224. } catch (error) {
  225. return null;
  226. }
  227. }
  228.  
  229. function updateBadge(viewCount, uploadTime, uploadDate) {
  230. let badge = document.querySelector('.revealViewsAndUploadTime');
  231. if (badge) {
  232. badge.remove();
  233. }
  234. insertBadge(viewCount, uploadTime, uploadDate);
  235. }
  236.  
  237. function insertBadge(viewCount, uploadTime, uploadDate) {
  238. const targetElement = document.querySelector('#secondary-inner #panels');
  239. if (targetElement && !document.querySelector('.revealViewsAndUploadTime')) {
  240. const badge = createBadge(viewCount, uploadTime, uploadDate);
  241. targetElement.parentNode.insertBefore(badge, targetElement);
  242. }
  243. }
  244.  
  245. function addStyles() {
  246. if (!document.querySelector('#revealViewsAndUploadTime-styles')) {
  247. const styleElement = document.createElement('style');
  248. styleElement.id = 'revealViewsAndUploadTime-styles';
  249. styleElement.textContent = badgeStyles;
  250. document.head.appendChild(styleElement);
  251. }
  252. }
  253.  
  254. async function updateBadgeWithInfo(videoId) {
  255. updateBadge('Loading...', 'Loading...', 'Loading...');
  256. try {
  257. const videoInfo = await fetchVideoInfo(videoId);
  258. if (videoInfo) {
  259. const uploadTime = formatTime(videoInfo.uploadDate);
  260. const formattedUploadDate = formatDate(videoInfo.uploadDate);
  261. updateBadge(videoInfo.viewCount, uploadTime, formattedUploadDate);
  262. } else {
  263. updateBadge('Error', 'Error', 'Error');
  264. }
  265. } catch (error) {
  266. updateBadge('Error', 'Error', 'Error');
  267. }
  268. }
  269.  
  270. function init() {
  271. addStyles();
  272. const videoId = getVideoId();
  273. if (videoId) {
  274. updateBadgeWithInfo(videoId);
  275. } else {
  276. updateBadge('N/A', 'N/A', 'N/A');
  277. }
  278. }
  279.  
  280. function observePageChanges() {
  281. let lastVideoId = getVideoId();
  282. let lastUrl = location.href;
  283.  
  284. const observer = new MutationObserver(() => {
  285. if (location.href !== lastUrl) {
  286. lastUrl = location.href;
  287. const currentVideoId = getVideoId();
  288. if (currentVideoId && currentVideoId !== lastVideoId) {
  289. lastVideoId = currentVideoId;
  290. updateBadgeWithInfo(currentVideoId);
  291. } else if (!currentVideoId) {
  292. updateBadge('Not a video', 'Not a video', 'Not a video');
  293. }
  294. }
  295. });
  296.  
  297. observer.observe(document.body, { childList: true, subtree: true });
  298. }
  299.  
  300. if (document.readyState === 'loading') {
  301. document.addEventListener('DOMContentLoaded', () => {
  302. init();
  303. observePageChanges();
  304. });
  305. } else {
  306. init();
  307. observePageChanges();
  308. }
  309.  
  310. window.addEventListener('yt-navigate-start', function() {
  311. updateBadge('Loading...', 'Loading...', 'Loading...');
  312. });
  313.  
  314. window.addEventListener('yt-navigate-finish', function() {
  315. const videoId = getVideoId();
  316. if (videoId) {
  317. updateBadgeWithInfo(videoId);
  318. } else {
  319. updateBadge('Not a video', 'Not a video', 'Not a video');
  320. }
  321. });
  322. })();