// ==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://gf.qytechs.cn/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();
}
})();