YouTube Enhancer (Reveal Views & Upload Time)

Integrating clickable badges that reveal the total views for all video types and detailed upload times.

当前为 2024-10-19 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Enhancer (Reveal Views & Upload Time)
  3. // @description Integrating clickable badges that reveal the total views for all video types and detailed upload times.
  4. // @icon https://raw.githubusercontent.com/exyezed/youtube-enhancer/refs/heads/main/extras/youtube-enhancer.png
  5. // @version 1.0
  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_xmlhttpRequest
  12. // @connect exyezed.vercel.app
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. // Styles for the badge
  19. const badgeStyles = `
  20. @import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200');
  21. .YouTubeEnhancerRevealViewsUploadTime {
  22. height: 36px;
  23. font-size: 14px;
  24. font-weight: 500;
  25. border-radius: 18px;
  26. padding: 0 16px;
  27. font-family: inherit;
  28. border: 1px solid transparent;
  29. margin: 0 8px;
  30. display: flex;
  31. align-items: center;
  32. gap: 8px;
  33. cursor: pointer;
  34. }
  35. html[dark] .YouTubeEnhancerRevealViewsUploadTime {
  36. background-color: #ffffff1a;
  37. color: var(--yt-spec-text-primary, #fff);
  38. }
  39. html:not([dark]) .YouTubeEnhancerRevealViewsUploadTime {
  40. background-color: #0000000d;
  41. color: var(--yt-spec-text-primary, #030303);
  42. }
  43. html[dark] .YouTubeEnhancerRevealViewsUploadTime:hover {
  44. background-color: #ffffff33;
  45. }
  46. html:not([dark]) .YouTubeEnhancerRevealViewsUploadTime:hover {
  47. background-color: #00000014;
  48. }
  49. .YouTubeEnhancerRevealViewsUploadTime .material-symbols-outlined {
  50. font-size: 24px;
  51. line-height: 1;
  52. font-variation-settings:
  53. 'FILL' 0,
  54. 'wght' 150,
  55. 'GRAD' 0,
  56. 'opsz' 24;
  57. }
  58. .YouTubeEnhancerRevealViewsUploadTime .separator {
  59. margin: 0 2px;
  60. width: 1px;
  61. height: 24px;
  62. opacity: 0.3;
  63. }
  64. html[dark] .YouTubeEnhancerRevealViewsUploadTime .separator {
  65. background-color: var(--yt-spec-text-secondary, #aaa);
  66. }
  67. html:not([dark]) .YouTubeEnhancerRevealViewsUploadTime .separator {
  68. background-color: var(--yt-spec-text-secondary, #606060);
  69. }
  70. .YouTubeEnhancerRevealViewsUploadTime-shorts {
  71. height: 48px;
  72. font-size: 14px;
  73. font-weight: 500;
  74. border-radius: 36px;
  75. padding: 0 16px;
  76. font-family: inherit;
  77. position: absolute;
  78. top: 16px;
  79. right: 16px;
  80. z-index: 1000;
  81. background-color: rgba(0, 0, 0, 0.3);
  82. color: #fff;
  83. display: flex;
  84. align-items: center;
  85. gap: 8px;
  86. }
  87. .YouTubeEnhancerRevealViewsUploadTime-shorts .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. .YouTubeEnhancerRevealViewsUploadTime-shorts .separator {
  97. margin: 0 2px;
  98. width: 1px;
  99. height: 36px;
  100. opacity: 0.3;
  101. background-color: #fff;
  102. }
  103. `;
  104.  
  105. // Function to remove TubeLab element
  106. function removeTubeLabElement() {
  107. const element = document.querySelector('#tubelab-video-root');
  108. if (element) {
  109. element.remove();
  110. }
  111. }
  112.  
  113. // Functions for the badge
  114. function createBadge(viewCount, uploadTime, uploadDate, isShorts = false) {
  115. if (isShorts) {
  116. const badge = document.createElement('div');
  117. badge.className = 'YouTubeEnhancerRevealViewsUploadTime-shorts';
  118.  
  119. const viewIcon = document.createElement('span');
  120. viewIcon.className = 'material-symbols-outlined';
  121. viewIcon.textContent = 'visibility';
  122.  
  123. const viewSpan = document.createElement('span');
  124. viewSpan.textContent = viewCount;
  125.  
  126. const separator1 = document.createElement('div');
  127. separator1.className = 'separator';
  128.  
  129. const dateIcon = document.createElement('span');
  130. dateIcon.className = 'material-symbols-outlined';
  131. dateIcon.textContent = 'calendar_month';
  132.  
  133. const dateSpan = document.createElement('span');
  134. dateSpan.textContent = uploadDate;
  135.  
  136. const separator2 = document.createElement('div');
  137. separator2.className = 'separator';
  138.  
  139. const timeIcon = document.createElement('span');
  140. timeIcon.className = 'material-symbols-outlined';
  141. timeIcon.textContent = 'schedule';
  142.  
  143. const timeSpan = document.createElement('span');
  144. timeSpan.textContent = uploadTime;
  145.  
  146. badge.appendChild(viewIcon);
  147. badge.appendChild(viewSpan);
  148. badge.appendChild(separator1);
  149. badge.appendChild(dateIcon);
  150. badge.appendChild(dateSpan);
  151. badge.appendChild(separator2);
  152. badge.appendChild(timeIcon);
  153. badge.appendChild(timeSpan);
  154.  
  155. return badge;
  156. } else {
  157. const badge = document.createElement('div');
  158. badge.className = 'YouTubeEnhancerRevealViewsUploadTime';
  159.  
  160. const icon = document.createElement('span');
  161. icon.className = 'material-symbols-outlined';
  162. icon.textContent = 'visibility';
  163.  
  164. const dataSpan = document.createElement('span');
  165. dataSpan.textContent = viewCount;
  166.  
  167. const separator = document.createElement('div');
  168. separator.className = 'separator';
  169.  
  170. const timeIcon = document.createElement('span');
  171. timeIcon.className = 'material-symbols-outlined';
  172. timeIcon.textContent = 'schedule';
  173.  
  174. const timeSpan = document.createElement('span');
  175. timeSpan.textContent = uploadTime;
  176.  
  177. badge.appendChild(icon);
  178. badge.appendChild(dataSpan);
  179. badge.appendChild(separator);
  180. badge.appendChild(timeIcon);
  181. badge.appendChild(timeSpan);
  182.  
  183. let isShowingViews = true;
  184. badge.addEventListener('click', () => {
  185. if (isShowingViews) {
  186. icon.textContent = 'calendar_clock';
  187. dataSpan.textContent = uploadDate;
  188. timeIcon.style.display = 'none';
  189. } else {
  190. icon.textContent = 'visibility';
  191. dataSpan.textContent = viewCount;
  192. timeIcon.style.display = '';
  193. }
  194. isShowingViews = !isShowingViews;
  195. });
  196.  
  197. return badge;
  198. }
  199. }
  200.  
  201. function getVideoId() {
  202. const urlParams = new URLSearchParams(window.location.search);
  203. return urlParams.get('v');
  204. }
  205.  
  206. function formatNumber(number) {
  207. return new Intl.NumberFormat('en-US').format(number);
  208. }
  209.  
  210. function formatDate(dateString) {
  211. const date = new Date(dateString);
  212. const today = new Date();
  213. const yesterday = new Date(today);
  214. yesterday.setDate(yesterday.getDate() - 1);
  215.  
  216. if (date.toDateString() === today.toDateString()) {
  217. return 'Today';
  218. } else if (date.toDateString() === yesterday.toDateString()) {
  219. return 'Yesterday';
  220. } else {
  221. const options = {
  222. weekday: 'long',
  223. day: '2-digit',
  224. month: '2-digit',
  225. year: 'numeric',
  226. };
  227. const formattedDate = new Intl.DateTimeFormat('en-GB', options).format(date);
  228. const [dayName, datePart] = formattedDate.split(', ');
  229. return `${dayName}, ${datePart.replace(/\//g, '/')}`;
  230. }
  231. }
  232.  
  233. function formatTime(dateString) {
  234. const date = new Date(dateString);
  235. const options = {
  236. hour: '2-digit',
  237. minute: '2-digit',
  238. second: '2-digit',
  239. hour12: false
  240. };
  241. return new Intl.DateTimeFormat('en-GB', options).format(date);
  242. }
  243.  
  244. function fetchVideoInfo(videoId) {
  245. const apiUrl = `https://exyezed.vercel.app/api/video/${videoId}`;
  246.  
  247. return new Promise((resolve, reject) => {
  248. GM_xmlhttpRequest({
  249. method: 'GET',
  250. url: apiUrl,
  251. onload: function(response) {
  252. if (response.status === 200) {
  253. const data = JSON.parse(response.responseText);
  254. resolve({
  255. viewCount: formatNumber(data.viewCount),
  256. uploadDate: data.uploadDate
  257. });
  258. } else {
  259. reject('API request failed');
  260. }
  261. },
  262. onerror: function() {
  263. reject('Network error');
  264. }
  265. });
  266. });
  267. }
  268.  
  269. function updateBadge(viewCount, uploadTime, uploadDate, isShorts = false) {
  270. let badge = document.querySelector(isShorts ? '.YouTubeEnhancerRevealViewsUploadTime-shorts' : '.YouTubeEnhancerRevealViewsUploadTime');
  271. if (badge) {
  272. badge.remove();
  273. }
  274. insertBadge(viewCount, uploadTime, uploadDate, isShorts);
  275. }
  276.  
  277. function insertBadge(viewCount, uploadTime, uploadDate, isShorts = false) {
  278. const targetSelector = isShorts ? 'ytd-reel-video-renderer[is-active]' : '#owner';
  279. const target = document.querySelector(targetSelector);
  280. if (target && !document.querySelector(isShorts ? '.YouTubeEnhancerRevealViewsUploadTime-shorts' : '.YouTubeEnhancerRevealViewsUploadTime')) {
  281. const badge = createBadge(viewCount, uploadTime, uploadDate, isShorts);
  282. target.appendChild(badge);
  283. }
  284. }
  285.  
  286. function addStyles() {
  287. if (!document.querySelector('#YouTubeEnhancerRevealViewsUploadTime-styles')) {
  288. const styleElement = document.createElement('style');
  289. styleElement.id = 'YouTubeEnhancerRevealViewsUploadTime-styles';
  290. styleElement.textContent = badgeStyles;
  291. document.head.appendChild(styleElement);
  292. }
  293. }
  294.  
  295. async function updateBadgeWithInfo(videoId, isShorts = false) {
  296. updateBadge('Loading...', 'Loading...', 'Loading...', isShorts);
  297. try {
  298. const videoInfo = await fetchVideoInfo(videoId);
  299. const uploadTime = formatTime(videoInfo.uploadDate);
  300. const formattedUploadDate = formatDate(videoInfo.uploadDate);
  301. updateBadge(videoInfo.viewCount, uploadTime, formattedUploadDate, isShorts);
  302. } catch (error) {
  303. updateBadge('Error', 'Error', 'Error', isShorts);
  304. }
  305. }
  306.  
  307. function init() {
  308. addStyles();
  309. removeTubeLabElement();
  310. const videoId = getVideoId();
  311. const isShorts = window.location.pathname.startsWith('/shorts/');
  312. if (videoId) {
  313. updateBadgeWithInfo(videoId, false);
  314. } else if (isShorts) {
  315. const shortsId = window.location.pathname.split('/')[2];
  316. updateBadgeWithInfo(shortsId, true);
  317. } else {
  318. updateBadge('N/A', 'N/A', 'N/A', isShorts);
  319. }
  320. }
  321.  
  322. function observePageChanges() {
  323. let lastVideoId = getVideoId();
  324. let lastUrl = location.href;
  325.  
  326. const observer = new MutationObserver(() => {
  327. removeTubeLabElement();
  328. if (location.href !== lastUrl) {
  329. lastUrl = location.href;
  330. const isShorts = window.location.pathname.startsWith('/shorts/');
  331. const currentVideoId = isShorts ? window.location.pathname.split('/')[2] : getVideoId();
  332. if (currentVideoId && currentVideoId !== lastVideoId) {
  333. lastVideoId = currentVideoId;
  334. updateBadgeWithInfo(currentVideoId, isShorts);
  335. } else if (!currentVideoId) {
  336. updateBadge('Not a video', 'Not a video', 'Not a video', isShorts);
  337. }
  338. }
  339. });
  340.  
  341. observer.observe(document.body, { childList: true, subtree: true });
  342. }
  343.  
  344. // Run init and start observing for changes
  345. if (document.readyState === 'loading') {
  346. document.addEventListener('DOMContentLoaded', () => {
  347. init();
  348. observePageChanges();
  349. });
  350. } else {
  351. init();
  352. observePageChanges();
  353. }
  354.  
  355. // Listen for YouTube's navigation events
  356. window.addEventListener('yt-navigate-start', function() {
  357. const isShorts = window.location.pathname.startsWith('/shorts/');
  358. updateBadge('Loading...', 'Loading...', 'Loading...', isShorts);
  359. });
  360.  
  361. window.addEventListener('yt-navigate-finish', function() {
  362. removeTubeLabElement();
  363. const isShorts = window.location.pathname.startsWith('/shorts/');
  364. const videoId = isShorts ? window.location.pathname.split('/')[2] : getVideoId();
  365. if (videoId) {
  366. updateBadgeWithInfo(videoId, isShorts);
  367. } else {
  368. updateBadge('Not a video', 'Not a video', 'Not a video', isShorts);
  369. }
  370. });
  371. })();