Full Date format for Youtube

Show full upload dates in DD/MM/YYYY HH:MMam/pm format with improved performance

  1. // ==UserScript==
  2. // @name Full Date format for Youtube
  3. // @version 1.1.2
  4. // @description Show full upload dates in DD/MM/YYYY HH:MMam/pm format with improved performance
  5. // @author Ignacio Albiol
  6. // @namespace https://greatest.deepsurf.us/en/users/1304094
  7. // @match https://www.youtube.com/*
  8. // @iconURL https://seekvectors.com/files/download/youtube-icon-yellow-01.jpg
  9. // @grant none
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. const processedVideos = new Map();
  17. const uploadDateCache = new Map();
  18. const apiRequestCache = new Map();
  19. const PROCESS_INTERVAL = 1500;
  20. const DEBUG = false; // Set to true to enable debug logging
  21.  
  22. function debugLog(...args) {
  23. if (DEBUG) console.log('[YT Date Format]', ...args);
  24. }
  25.  
  26. async function getRemoteUploadDate(videoId) {
  27. if (!videoId) {
  28. debugLog('No video ID provided');
  29. return null;
  30. }
  31. if (uploadDateCache.has(videoId)) {
  32. debugLog('Cache hit for', videoId);
  33. return uploadDateCache.get(videoId);
  34. }
  35. if (apiRequestCache.has(videoId)) {
  36. debugLog('Request already in progress for', videoId);
  37. return apiRequestCache.get(videoId);
  38. }
  39.  
  40. debugLog('Fetching data for', videoId);
  41. const requestPromise = (async () => {
  42. try {
  43. const response = await fetch('https://www.youtube.com/youtubei/v1/player?prettyPrint=false', {
  44. method: 'POST',
  45. headers: {'Content-Type': 'application/json'},
  46. body: JSON.stringify({
  47. "context": { "client": { "clientName": "WEB", "clientVersion": "2.20240416.01.00" } },
  48. "videoId": videoId
  49. })
  50. });
  51. if (!response.ok) {
  52. throw new Error(`Network error: ${response.status}`);
  53. }
  54. const data = await response.json();
  55. const object = data?.microformat?.playerMicroformatRenderer;
  56. const uploadDate = object?.publishDate || object?.uploadDate || object?.liveBroadcastDetails?.startTimestamp || null;
  57. if (uploadDate) {
  58. debugLog('Found date for', videoId, ':', uploadDate);
  59. uploadDateCache.set(videoId, uploadDate);
  60. } else {
  61. debugLog('No date found for', videoId);
  62. }
  63. return uploadDate;
  64. } catch (error) {
  65. console.error('[YT Date Format] Error fetching video data:', error, videoId);
  66. return null;
  67. } finally {
  68. apiRequestCache.delete(videoId);
  69. }
  70. })();
  71.  
  72. apiRequestCache.set(videoId, requestPromise);
  73. return requestPromise;
  74. }
  75.  
  76. function isoToDate(iso) {
  77. if (!iso) return '';
  78. try {
  79. const date = new Date(iso);
  80. if (isNaN(date.getTime())) {
  81. debugLog('Invalid date:', iso);
  82. return '';
  83. }
  84. const day = String(date.getDate()).padStart(2, '0');
  85. const month = String(date.getMonth() + 1).padStart(2, '0');
  86. const year = date.getFullYear();
  87. let hours = date.getHours();
  88. const minutes = String(date.getMinutes()).padStart(2, '0');
  89. const ampm = hours >= 12 ? 'pm' : 'am';
  90. hours = hours % 12 || 12;
  91. return `${day}/${month}/${year} ${hours}:${minutes}${ampm}`;
  92. } catch (error) {
  93. console.error('[YT Date Format] Error formatting date:', error, iso);
  94. return '';
  95. }
  96. }
  97.  
  98. function urlToVideoId(url) {
  99. if (!url) return '';
  100. try {
  101. // Handle various YouTube URL formats
  102. if (url.includes('/shorts/')) {
  103. return url.split('/shorts/')[1].split(/[?#]/)[0];
  104. }
  105. if (url.includes('v=')) {
  106. return url.split('v=')[1].split(/[?&#]/)[0];
  107. }
  108. // Handle direct video IDs (e.g., /watch/VIDEO_ID)
  109. if (url.includes('/watch/')) {
  110. return url.split('/watch/')[1].split(/[?#]/)[0];
  111. }
  112. // Handle URLs with video ID directly in the path
  113. const match = url.match(/\/([a-zA-Z0-9_-]{11})(?:[?#]|$)/);
  114. if (match) return match[1];
  115. return '';
  116. } catch (error) {
  117. console.error('[YT Date Format] Error extracting video ID:', error, url);
  118. return '';
  119. }
  120. }
  121.  
  122. async function processVideoElement(el, linkSelector, metadataSelector) {
  123. try {
  124. const metadataLine = el.querySelector(metadataSelector);
  125. if (!metadataLine) {
  126. debugLog('No metadata line found for selector', metadataSelector);
  127. return;
  128. }
  129. // Find or create the span for holding our date
  130. const spanElements = metadataLine.querySelectorAll('span');
  131. let holder;
  132. // First, try to find an existing text span (likely the view count span)
  133. for (const span of spanElements) {
  134. if (span.textContent.includes(' views') ||
  135. span.textContent.includes(' view') ||
  136. span.textContent.match(/^\d[\d.,]*\s/)) {
  137. holder = span.nextElementSibling;
  138. if (!holder) {
  139. holder = document.createElement('span');
  140. metadataLine.appendChild(holder);
  141. }
  142. break;
  143. }
  144. }
  145. // If we couldn't find a suitable span, create one
  146. if (!holder) {
  147. holder = metadataLine.querySelector('span:nth-child(2)');
  148. if (!holder) {
  149. holder = document.createElement('span');
  150. metadataLine.appendChild(holder);
  151. }
  152. }
  153. const linkElement = el.querySelector(linkSelector);
  154. if (!linkElement) {
  155. debugLog('No link element found for selector', linkSelector);
  156. return;
  157. }
  158. const videoUrl = linkElement.getAttribute('href');
  159. const videoId = urlToVideoId(videoUrl);
  160. if (!videoId) {
  161. debugLog('Failed to extract video ID from', videoUrl);
  162. return;
  163. }
  164. if (processedVideos.has(videoId)) {
  165. debugLog('Video already processed', videoId);
  166. return;
  167. }
  168. debugLog('Processing video', videoId);
  169. processedVideos.set(videoId, Date.now());
  170. const uploadDate = await getRemoteUploadDate(videoId);
  171. if (uploadDate) {
  172. const formattedDate = isoToDate(uploadDate);
  173. debugLog('Setting date for', videoId, ':', formattedDate);
  174. holder.textContent = formattedDate;
  175. holder.style.marginLeft = '4px';
  176. }
  177. } catch (error) {
  178. console.error('[YT Date Format] Error processing video element:', error);
  179. }
  180. }
  181.  
  182. function processAllElements() {
  183. // Updated selectors for different parts of YouTube
  184. const selectors = [
  185. // Related videos in watch page
  186. {
  187. container: '#items.ytd-watch-next-secondary-results-renderer ytd-compact-video-renderer, #related #items ytd-compact-video-renderer',
  188. link: 'a#thumbnail',
  189. metadata: '#metadata-line'
  190. },
  191. // Videos in home page and channel pages
  192. {
  193. container: 'ytd-rich-grid-media, ytd-rich-item-renderer, ytd-grid-video-renderer',
  194. link: 'a#thumbnail, h3 > a#video-title-link',
  195. metadata: '#metadata-line, ytd-video-meta-block #metadata #metadata-line'
  196. },
  197. // Search results
  198. {
  199. container: 'ytd-video-renderer',
  200. link: 'a#thumbnail, h3 a#video-title',
  201. metadata: '#metadata-line, ytd-video-meta-block #metadata #metadata-line'
  202. },
  203. // Channel featured video
  204. {
  205. container: 'ytd-channel-video-player-renderer',
  206. link: 'a, yt-formatted-string > a',
  207. metadata: '#metadata-line'
  208. }
  209. ];
  210.  
  211. selectors.forEach(({ container, link, metadata }) => {
  212. document.querySelectorAll(container).forEach(el => {
  213. processVideoElement(el, link, metadata);
  214. });
  215. });
  216.  
  217. // Clean up old processed videos to prevent memory leaks
  218. const now = Date.now();
  219. for (const [videoId, timestamp] of processedVideos.entries()) {
  220. if (now - timestamp > 10 * 60 * 1000) {
  221. processedVideos.delete(videoId);
  222. }
  223. }
  224. }
  225.  
  226. function handleURLChange() {
  227. debugLog('URL changed, clearing processed videos cache');
  228. processedVideos.clear();
  229. setTimeout(processAllElements, 1000);
  230. }
  231.  
  232. function init() {
  233. debugLog('Initializing YouTube Date Format script');
  234. // Add CSS to hide YouTube's default timestamp in some cases
  235. const styleTag = document.createElement('style');
  236. styleTag.textContent = `
  237. #info > span:nth-child(3),
  238. #info > span:nth-child(4) {
  239. display: none !important;
  240. }
  241. `;
  242. document.head.appendChild(styleTag);
  243.  
  244. // Watch for page navigation
  245. let lastUrl = location.href;
  246. new MutationObserver(() => {
  247. if (location.href !== lastUrl) {
  248. lastUrl = location.href;
  249. handleURLChange();
  250. }
  251. }).observe(document, { subtree: true, childList: true });
  252.  
  253. // Also watch title changes (which often indicate page changes in SPAs)
  254. const titleEl = document.querySelector('head > title');
  255. if (titleEl) {
  256. new MutationObserver(handleURLChange)
  257. .observe(titleEl, { childList: true });
  258. }
  259.  
  260. // Process videos periodically
  261. setInterval(processAllElements, PROCESS_INTERVAL);
  262. // Initial processing
  263. setTimeout(processAllElements, 500);
  264. setTimeout(processAllElements, 2000);
  265. setTimeout(processAllElements, 5000);
  266. }
  267.  
  268. // Initialize the script when the page is ready
  269. if (document.readyState === 'loading') {
  270. document.addEventListener('DOMContentLoaded', init);
  271. } else {
  272. init();
  273. }
  274. })();