YouTube Enhancer (Reveal Views & Upload Time)

Reveal Views & Upload Time.

当前为 2025-03-11 提交的版本,查看 最新版本

  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.2
  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. .revealViewsAndUploadTime-shorts {
  63. height: 48px;
  64. font-size: 14px;
  65. font-weight: 500;
  66. border-radius: 36px;
  67. padding: 0 16px;
  68. font-family: inherit;
  69. position: absolute;
  70. top: 16px;
  71. right: 16px;
  72. z-index: 1000;
  73. background-color: rgba(0, 0, 0, 0.3);
  74. color: #fff;
  75. display: flex;
  76. align-items: center;
  77. gap: 8px;
  78. }
  79. .revealViewsAndUploadTime-shorts .separator {
  80. margin: 0 2px;
  81. width: 1px;
  82. height: 36px;
  83. opacity: 0.3;
  84. background-color: #fff;
  85. }
  86.  
  87. .material-symbols-outlined {
  88. font-size: 24px;
  89. line-height: 1;
  90. font-variation-settings:
  91. 'FILL' 0,
  92. 'wght' 150,
  93. 'GRAD' 0,
  94. 'opsz' 24;
  95. }
  96. `;
  97.  
  98. function createBadge(viewCount, uploadTime, uploadDate, isShorts = false) {
  99. if (isShorts) {
  100. const badge = document.createElement('div');
  101. badge.className = 'revealViewsAndUploadTime-shorts';
  102. const viewIcon = document.createElement('span');
  103. viewIcon.className = 'material-symbols-outlined';
  104. viewIcon.textContent = 'visibility';
  105. const viewSpan = document.createElement('span');
  106. viewSpan.textContent = viewCount;
  107. const separator1 = document.createElement('div');
  108. separator1.className = 'separator';
  109. const dateIcon = document.createElement('span');
  110. dateIcon.className = 'material-symbols-outlined';
  111. dateIcon.textContent = 'calendar_month';
  112. const dateSpan = document.createElement('span');
  113. dateSpan.textContent = uploadDate;
  114. const separator2 = document.createElement('div');
  115. separator2.className = 'separator';
  116. const timeIcon = document.createElement('span');
  117. timeIcon.className = 'material-symbols-outlined';
  118. timeIcon.textContent = 'schedule';
  119. const timeSpan = document.createElement('span');
  120. timeSpan.textContent = uploadTime;
  121. badge.appendChild(viewIcon);
  122. badge.appendChild(viewSpan);
  123. badge.appendChild(separator1);
  124. badge.appendChild(dateIcon);
  125. badge.appendChild(dateSpan);
  126. badge.appendChild(separator2);
  127. badge.appendChild(timeIcon);
  128. badge.appendChild(timeSpan);
  129. return badge;
  130. } else {
  131. const badge = document.createElement('div');
  132. badge.className = 'revealViewsAndUploadTime';
  133. const mainIcon = document.createElement('span');
  134. mainIcon.className = 'material-symbols-outlined';
  135. mainIcon.textContent = 'visibility';
  136. const dataSpan = document.createElement('span');
  137. dataSpan.textContent = viewCount;
  138. const separator = document.createElement('div');
  139. separator.className = 'separator';
  140. const timeIcon = document.createElement('span');
  141. timeIcon.className = 'material-symbols-outlined';
  142. timeIcon.textContent = 'schedule';
  143. const timeSpan = document.createElement('span');
  144. timeSpan.textContent = uploadTime;
  145. badge.appendChild(mainIcon);
  146. badge.appendChild(dataSpan);
  147. badge.appendChild(separator);
  148. badge.appendChild(timeIcon);
  149. badge.appendChild(timeSpan);
  150. let isShowingViews = true;
  151. badge.addEventListener('click', () => {
  152. if (isShowingViews) {
  153. mainIcon.textContent = 'calendar_month';
  154. dataSpan.textContent = uploadDate;
  155. timeIcon.textContent = 'schedule';
  156. timeIcon.style.display = '';
  157. } else {
  158. mainIcon.textContent = 'visibility';
  159. dataSpan.textContent = viewCount;
  160. timeIcon.textContent = 'schedule';
  161. timeIcon.style.display = '';
  162. }
  163. isShowingViews = !isShowingViews;
  164. });
  165. return badge;
  166. }
  167. }
  168.  
  169. function getVideoId() {
  170. const urlObj = new URL(window.location.href);
  171. if (urlObj.pathname.includes('/watch')) {
  172. return urlObj.searchParams.get('v');
  173. } else if (urlObj.pathname.includes('/video/')) {
  174. return urlObj.pathname.split('/video/')[1];
  175. } else if (urlObj.pathname.includes('/shorts/')) {
  176. return urlObj.pathname.split('/shorts/')[1];
  177. }
  178. return null;
  179. }
  180.  
  181. function formatNumber(number) {
  182. return new Intl.NumberFormat('en-US').format(number);
  183. }
  184.  
  185. function formatDate(dateString) {
  186. const date = new Date(dateString);
  187. const today = new Date();
  188. const yesterday = new Date(today);
  189. yesterday.setDate(yesterday.getDate() - 1);
  190.  
  191. if (date.toDateString() === today.toDateString()) {
  192. return 'Today';
  193. } else if (date.toDateString() === yesterday.toDateString()) {
  194. return 'Yesterday';
  195. } else {
  196. const options = {
  197. weekday: 'long',
  198. day: '2-digit',
  199. month: '2-digit',
  200. year: 'numeric',
  201. };
  202. const formattedDate = new Intl.DateTimeFormat('en-GB', options).format(date);
  203. const [dayName, datePart] = formattedDate.split(', ');
  204. return `${dayName}, ${datePart.replace(/\//g, '/')}`;
  205. }
  206. }
  207.  
  208. function formatTime(dateString) {
  209. const date = new Date(dateString);
  210. const options = {
  211. hour: '2-digit',
  212. minute: '2-digit',
  213. second: '2-digit',
  214. hour12: false
  215. };
  216. return new Intl.DateTimeFormat('en-GB', options).format(date);
  217. }
  218.  
  219. function getApiKey() {
  220. const scripts = document.getElementsByTagName('script');
  221. for (const script of scripts) {
  222. const match = script.textContent.match(/"INNERTUBE_API_KEY":\s*"([^"]+)"/);
  223. if (match && match[1]) return match[1];
  224. }
  225. return null;
  226. }
  227.  
  228. function getClientInfo() {
  229. const scripts = document.getElementsByTagName('script');
  230. let clientName = null;
  231. let clientVersion = null;
  232. for (const script of scripts) {
  233. const nameMatch = script.textContent.match(/"INNERTUBE_CLIENT_NAME":\s*"([^"]+)"/);
  234. const versionMatch = script.textContent.match(/"INNERTUBE_CLIENT_VERSION":\s*"([^"]+)"/);
  235. if (nameMatch && nameMatch[1]) clientName = nameMatch[1];
  236. if (versionMatch && versionMatch[1]) clientVersion = versionMatch[1];
  237. }
  238. return { clientName, clientVersion };
  239. }
  240.  
  241. async function fetchVideoInfo(videoId) {
  242. try {
  243. const apiKey = getApiKey();
  244. if (!apiKey) return null;
  245. const { clientName, clientVersion } = getClientInfo();
  246. if (!clientName || !clientVersion) return null;
  247. const response = await fetch(`https://www.youtube.com/youtubei/v1/player?key=${apiKey}`, {
  248. method: 'POST',
  249. headers: {
  250. 'Content-Type': 'application/json',
  251. },
  252. body: JSON.stringify({
  253. videoId: videoId,
  254. context: {
  255. client: {
  256. clientName: clientName,
  257. clientVersion: clientVersion,
  258. }
  259. }
  260. })
  261. });
  262. if (!response.ok) return null;
  263. const data = await response.json();
  264. let viewCount = "Unknown";
  265. if (data.videoDetails?.viewCount) {
  266. viewCount = formatNumber(data.videoDetails.viewCount);
  267. }
  268. let publishDate = "Unknown";
  269. if (data.microformat?.playerMicroformatRenderer?.publishDate) {
  270. publishDate = data.microformat.playerMicroformatRenderer.publishDate;
  271. }
  272. return {
  273. viewCount,
  274. uploadDate: publishDate
  275. };
  276. } catch (error) {
  277. return null;
  278. }
  279. }
  280.  
  281. function updateBadge(viewCount, uploadTime, uploadDate, isShorts = false) {
  282. let badge = document.querySelector(isShorts ? '.revealViewsAndUploadTime-shorts' : '.revealViewsAndUploadTime');
  283. if (badge) {
  284. badge.remove();
  285. }
  286. insertBadge(viewCount, uploadTime, uploadDate, isShorts);
  287. }
  288.  
  289. function insertBadge(viewCount, uploadTime, uploadDate, isShorts = false) {
  290. if (isShorts) {
  291. const target = document.querySelector('ytd-reel-video-renderer[is-active]');
  292. if (target && !document.querySelector('.revealViewsAndUploadTime-shorts')) {
  293. const badge = createBadge(viewCount, uploadTime, uploadDate, isShorts);
  294. target.appendChild(badge);
  295. }
  296. } else {
  297. const targetElement = document.querySelector('#secondary-inner #panels');
  298. if (targetElement && !document.querySelector('.revealViewsAndUploadTime')) {
  299. const badge = createBadge(viewCount, uploadTime, uploadDate, isShorts);
  300. targetElement.parentNode.insertBefore(badge, targetElement);
  301. }
  302. }
  303. }
  304.  
  305. function addStyles() {
  306. if (!document.querySelector('#revealViewsAndUploadTime-styles')) {
  307. const styleElement = document.createElement('style');
  308. styleElement.id = 'revealViewsAndUploadTime-styles';
  309. styleElement.textContent = badgeStyles;
  310. document.head.appendChild(styleElement);
  311. }
  312. }
  313.  
  314. async function updateBadgeWithInfo(videoId, isShorts = false) {
  315. updateBadge('Loading...', 'Loading...', 'Loading...', isShorts);
  316. try {
  317. const videoInfo = await fetchVideoInfo(videoId);
  318. if (videoInfo) {
  319. const uploadTime = formatTime(videoInfo.uploadDate);
  320. const formattedUploadDate = formatDate(videoInfo.uploadDate);
  321. updateBadge(videoInfo.viewCount, uploadTime, formattedUploadDate, isShorts);
  322. } else {
  323. updateBadge('Error', 'Error', 'Error', isShorts);
  324. }
  325. } catch (error) {
  326. updateBadge('Error', 'Error', 'Error', isShorts);
  327. }
  328. }
  329.  
  330. function init() {
  331. addStyles();
  332. const videoId = getVideoId();
  333. const isShorts = window.location.pathname.startsWith('/shorts/');
  334. if (videoId) {
  335. updateBadgeWithInfo(videoId, isShorts);
  336. } else {
  337. updateBadge('N/A', 'N/A', 'N/A', isShorts);
  338. }
  339. }
  340.  
  341. function observePageChanges() {
  342. let lastVideoId = getVideoId();
  343. let lastUrl = location.href;
  344.  
  345. const observer = new MutationObserver(() => {
  346. if (location.href !== lastUrl) {
  347. lastUrl = location.href;
  348. const isShorts = window.location.pathname.startsWith('/shorts/');
  349. const currentVideoId = getVideoId();
  350. if (currentVideoId && currentVideoId !== lastVideoId) {
  351. lastVideoId = currentVideoId;
  352. updateBadgeWithInfo(currentVideoId, isShorts);
  353. } else if (!currentVideoId) {
  354. updateBadge('Not a video', 'Not a video', 'Not a video', isShorts);
  355. }
  356. }
  357. });
  358.  
  359. observer.observe(document.body, { childList: true, subtree: true });
  360. }
  361.  
  362. if (document.readyState === 'loading') {
  363. document.addEventListener('DOMContentLoaded', () => {
  364. init();
  365. observePageChanges();
  366. });
  367. } else {
  368. init();
  369. observePageChanges();
  370. }
  371.  
  372. window.addEventListener('yt-navigate-start', function() {
  373. const isShorts = window.location.pathname.startsWith('/shorts/');
  374. updateBadge('Loading...', 'Loading...', 'Loading...', isShorts);
  375. });
  376.  
  377. window.addEventListener('yt-navigate-finish', function() {
  378. const isShorts = window.location.pathname.startsWith('/shorts/');
  379. const videoId = getVideoId();
  380. if (videoId) {
  381. updateBadgeWithInfo(videoId, isShorts);
  382. } else {
  383. updateBadge('Not a video', 'Not a video', 'Not a video', isShorts);
  384. }
  385. });
  386. })();