- // ==UserScript==
- // @name Full Date format for Youtube
- // @version 1.1.2
- // @description Show full upload dates in DD/MM/YYYY HH:MMam/pm format with improved performance
- // @author Ignacio Albiol
- // @namespace https://greatest.deepsurf.us/en/users/1304094
- // @match https://www.youtube.com/*
- // @iconURL https://seekvectors.com/files/download/youtube-icon-yellow-01.jpg
- // @grant none
- // @license MIT
- // ==/UserScript==
-
- (function() {
- 'use strict';
-
- const processedVideos = new Map();
- const uploadDateCache = new Map();
- const apiRequestCache = new Map();
- const PROCESS_INTERVAL = 1500;
- const DEBUG = false; // Set to true to enable debug logging
-
- function debugLog(...args) {
- if (DEBUG) console.log('[YT Date Format]', ...args);
- }
-
- async function getRemoteUploadDate(videoId) {
- if (!videoId) {
- debugLog('No video ID provided');
- return null;
- }
-
- if (uploadDateCache.has(videoId)) {
- debugLog('Cache hit for', videoId);
- return uploadDateCache.get(videoId);
- }
-
- if (apiRequestCache.has(videoId)) {
- debugLog('Request already in progress for', videoId);
- return apiRequestCache.get(videoId);
- }
-
- debugLog('Fetching data for', videoId);
- const requestPromise = (async () => {
- try {
- const response = await fetch('https://www.youtube.com/youtubei/v1/player?prettyPrint=false', {
- method: 'POST',
- headers: {'Content-Type': 'application/json'},
- body: JSON.stringify({
- "context": { "client": { "clientName": "WEB", "clientVersion": "2.20240416.01.00" } },
- "videoId": videoId
- })
- });
-
- if (!response.ok) {
- throw new Error(`Network error: ${response.status}`);
- }
-
- const data = await response.json();
- const object = data?.microformat?.playerMicroformatRenderer;
- const uploadDate = object?.publishDate || object?.uploadDate || object?.liveBroadcastDetails?.startTimestamp || null;
-
- if (uploadDate) {
- debugLog('Found date for', videoId, ':', uploadDate);
- uploadDateCache.set(videoId, uploadDate);
- } else {
- debugLog('No date found for', videoId);
- }
-
- return uploadDate;
- } catch (error) {
- console.error('[YT Date Format] Error fetching video data:', error, videoId);
- return null;
- } finally {
- apiRequestCache.delete(videoId);
- }
- })();
-
- apiRequestCache.set(videoId, requestPromise);
- return requestPromise;
- }
-
- function isoToDate(iso) {
- if (!iso) return '';
- try {
- const date = new Date(iso);
- if (isNaN(date.getTime())) {
- debugLog('Invalid date:', iso);
- return '';
- }
-
- const day = String(date.getDate()).padStart(2, '0');
- const month = String(date.getMonth() + 1).padStart(2, '0');
- const year = date.getFullYear();
- let hours = date.getHours();
- const minutes = String(date.getMinutes()).padStart(2, '0');
- const ampm = hours >= 12 ? 'pm' : 'am';
- hours = hours % 12 || 12;
-
- return `${day}/${month}/${year} ${hours}:${minutes}${ampm}`;
- } catch (error) {
- console.error('[YT Date Format] Error formatting date:', error, iso);
- return '';
- }
- }
-
- function urlToVideoId(url) {
- if (!url) return '';
-
- try {
- // Handle various YouTube URL formats
- if (url.includes('/shorts/')) {
- return url.split('/shorts/')[1].split(/[?#]/)[0];
- }
-
- if (url.includes('v=')) {
- return url.split('v=')[1].split(/[?&#]/)[0];
- }
-
- // Handle direct video IDs (e.g., /watch/VIDEO_ID)
- if (url.includes('/watch/')) {
- return url.split('/watch/')[1].split(/[?#]/)[0];
- }
-
- // Handle URLs with video ID directly in the path
- const match = url.match(/\/([a-zA-Z0-9_-]{11})(?:[?#]|$)/);
- if (match) return match[1];
-
- return '';
- } catch (error) {
- console.error('[YT Date Format] Error extracting video ID:', error, url);
- return '';
- }
- }
-
- async function processVideoElement(el, linkSelector, metadataSelector) {
- try {
- const metadataLine = el.querySelector(metadataSelector);
- if (!metadataLine) {
- debugLog('No metadata line found for selector', metadataSelector);
- return;
- }
-
- // Find or create the span for holding our date
- const spanElements = metadataLine.querySelectorAll('span');
- let holder;
-
- // First, try to find an existing text span (likely the view count span)
- for (const span of spanElements) {
- if (span.textContent.includes(' views') ||
- span.textContent.includes(' view') ||
- span.textContent.match(/^\d[\d.,]*\s/)) {
- holder = span.nextElementSibling;
- if (!holder) {
- holder = document.createElement('span');
- metadataLine.appendChild(holder);
- }
- break;
- }
- }
-
- // If we couldn't find a suitable span, create one
- if (!holder) {
- holder = metadataLine.querySelector('span:nth-child(2)');
- if (!holder) {
- holder = document.createElement('span');
- metadataLine.appendChild(holder);
- }
- }
-
- const linkElement = el.querySelector(linkSelector);
- if (!linkElement) {
- debugLog('No link element found for selector', linkSelector);
- return;
- }
-
- const videoUrl = linkElement.getAttribute('href');
- const videoId = urlToVideoId(videoUrl);
-
- if (!videoId) {
- debugLog('Failed to extract video ID from', videoUrl);
- return;
- }
-
- if (processedVideos.has(videoId)) {
- debugLog('Video already processed', videoId);
- return;
- }
-
- debugLog('Processing video', videoId);
- processedVideos.set(videoId, Date.now());
-
- const uploadDate = await getRemoteUploadDate(videoId);
- if (uploadDate) {
- const formattedDate = isoToDate(uploadDate);
- debugLog('Setting date for', videoId, ':', formattedDate);
- holder.textContent = formattedDate;
- holder.style.marginLeft = '4px';
- }
- } catch (error) {
- console.error('[YT Date Format] Error processing video element:', error);
- }
- }
-
- function processAllElements() {
- // Updated selectors for different parts of YouTube
- const selectors = [
- // Related videos in watch page
- {
- container: '#items.ytd-watch-next-secondary-results-renderer ytd-compact-video-renderer, #related #items ytd-compact-video-renderer',
- link: 'a#thumbnail',
- metadata: '#metadata-line'
- },
- // Videos in home page and channel pages
- {
- container: 'ytd-rich-grid-media, ytd-rich-item-renderer, ytd-grid-video-renderer',
- link: 'a#thumbnail, h3 > a#video-title-link',
- metadata: '#metadata-line, ytd-video-meta-block #metadata #metadata-line'
- },
- // Search results
- {
- container: 'ytd-video-renderer',
- link: 'a#thumbnail, h3 a#video-title',
- metadata: '#metadata-line, ytd-video-meta-block #metadata #metadata-line'
- },
- // Channel featured video
- {
- container: 'ytd-channel-video-player-renderer',
- link: 'a, yt-formatted-string > a',
- metadata: '#metadata-line'
- }
- ];
-
- selectors.forEach(({ container, link, metadata }) => {
- document.querySelectorAll(container).forEach(el => {
- processVideoElement(el, link, metadata);
- });
- });
-
- // Clean up old processed videos to prevent memory leaks
- const now = Date.now();
- for (const [videoId, timestamp] of processedVideos.entries()) {
- if (now - timestamp > 10 * 60 * 1000) {
- processedVideos.delete(videoId);
- }
- }
- }
-
- function handleURLChange() {
- debugLog('URL changed, clearing processed videos cache');
- processedVideos.clear();
- setTimeout(processAllElements, 1000);
- }
-
- function init() {
- debugLog('Initializing YouTube Date Format script');
-
- // Add CSS to hide YouTube's default timestamp in some cases
- const styleTag = document.createElement('style');
- styleTag.textContent = `
- #info > span:nth-child(3),
- #info > span:nth-child(4) {
- display: none !important;
- }
- `;
- document.head.appendChild(styleTag);
-
- // Watch for page navigation
- let lastUrl = location.href;
- new MutationObserver(() => {
- if (location.href !== lastUrl) {
- lastUrl = location.href;
- handleURLChange();
- }
- }).observe(document, { subtree: true, childList: true });
-
- // Also watch title changes (which often indicate page changes in SPAs)
- const titleEl = document.querySelector('head > title');
- if (titleEl) {
- new MutationObserver(handleURLChange)
- .observe(titleEl, { childList: true });
- }
-
- // Process videos periodically
- setInterval(processAllElements, PROCESS_INTERVAL);
-
- // Initial processing
- setTimeout(processAllElements, 500);
- setTimeout(processAllElements, 2000);
- setTimeout(processAllElements, 5000);
- }
-
- // Initialize the script when the page is ready
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', init);
- } else {
- init();
- }
- })();