// ==UserScript==
// @name Full Date format for Youtube (Enhanced)
// @version 2.0.9
// @description Show full upload dates in DD/MM/YYYY HH:MMam/pm format with smart queuing, retry, dynamic updates, and UI toggle
// @author Ignacio Albiol
// @namespace https://greasyforks.org/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 uploadDateCache = new Map();
const processedVideos = new Map();
const videoQueue = [];
const apiRequestCache = new Map();
let isEnabled = true;
const RETRY_LIMIT = 3;
const RETRY_DELAY = 1000;
const MAX_CONCURRENT = 3;
let currentRequests = 0;
const DEBUG = false;
const EXPIRY_MS = 10 * 60 * 1000;
function debug(...args) {
if (DEBUG) console.log('[YT Date Format]', ...args);
}
function formatDate(iso) {
const date = new Date(iso);
if (isNaN(date)) return '';
const d = String(date.getDate()).padStart(2, '0');
const m = String(date.getMonth() + 1).padStart(2, '0');
const y = date.getFullYear();
let h = date.getHours();
const mm = String(date.getMinutes()).padStart(2, '0');
const ampm = h >= 12 ? 'pm' : 'am';
h = h % 12 || 12;
return `${d}/${m}/${y} ${h}:${mm}${ampm}`;
}
function extractVideoId(url) {
try {
const u = new URL(url, 'https://www.youtube.com');
if (u.pathname.includes('/shorts/')) return u.pathname.split('/')[2];
const id = u.searchParams.get('v');
if (id) return id;
const match = u.pathname.match(/\/([a-zA-Z0-9_-]{11})(?:[/?#]|$)/);
return match?.[1] || '';
} catch {
return '';
}
}
async function fetchUploadDate(videoId, attempt = 0) {
if (uploadDateCache.has(videoId)) return uploadDateCache.get(videoId);
if (apiRequestCache.has(videoId)) return apiRequestCache.get(videoId);
const fetchPromise = (async () => {
try {
const res = 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
})
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
const d = data?.microformat?.playerMicroformatRenderer;
const raw = d?.publishDate || d?.uploadDate || d?.liveBroadcastDetails?.startTimestamp;
if (raw) {
uploadDateCache.set(videoId, raw);
return raw;
} else {
throw new Error('No date found');
}
} catch (err) {
if (attempt < RETRY_LIMIT) {
debug(`Retry ${attempt + 1} for ${videoId}`);
await new Promise(r => setTimeout(r, RETRY_DELAY * (attempt + 1)));
return fetchUploadDate(videoId, attempt + 1);
} else {
console.error('[YT Date Format] Fetch failed:', err);
return null;
}
} finally {
apiRequestCache.delete(videoId);
}
})();
apiRequestCache.set(videoId, fetchPromise);
return fetchPromise;
}
async function processQueue() {
if (currentRequests >= MAX_CONCURRENT || !isEnabled) return;
const task = videoQueue.shift();
if (!task) return;
currentRequests++;
const { el, linkSelector, metaSelector } = task;
try {
const meta = el.querySelector(metaSelector);
if (!meta) return;
let holder = [...meta.querySelectorAll('span')].find(s =>
/ views?|^\d[\d.,]*\s/.test(s.textContent)
)?.nextElementSibling;
if (!holder) {
holder = document.createElement('span');
meta.appendChild(holder);
}
const link = el.querySelector(linkSelector);
if (!link) return;
const videoId = extractVideoId(link.href);
if (!videoId || processedVideos.has(videoId)) return;
processedVideos.set(videoId, Date.now());
const uploadDate = await fetchUploadDate(videoId);
if (uploadDate) {
holder.textContent = formatDate(uploadDate);
holder.style.marginLeft = '4px';
}
} catch (err) {
console.error('[YT Date Format] Error in queue item:', err);
} finally {
currentRequests--;
processQueue();
}
}
function enqueue(el, linkSelector, metaSelector) {
if (!isEnabled) return;
videoQueue.push({ el, linkSelector, metaSelector });
processQueue();
}
function cleanOld() {
const now = Date.now();
for (const [id, ts] of processedVideos.entries()) {
if (now - ts > EXPIRY_MS) processedVideos.delete(id);
}
}
function observeVideos() {
const selectors = [
{
container: 'ytd-video-renderer, ytd-rich-grid-media, ytd-grid-video-renderer, ytd-compact-video-renderer',
link: 'a#thumbnail, h3 a',
meta: '#metadata-line'
},
{
container: 'ytd-channel-video-player-renderer',
link: 'a, yt-formatted-string > a',
meta: '#metadata-line'
}
];
const observer = new MutationObserver(() => {
for (const { container, link, meta } of selectors) {
document.querySelectorAll(container).forEach(el => enqueue(el, link, meta));
}
cleanOld();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// Initial trigger
setTimeout(() => {
selectors.forEach(({ container, link, meta }) => {
document.querySelectorAll(container).forEach(el => enqueue(el, link, meta));
});
}, 500);
}
function injectToggleUI() {
const btn = document.createElement('button');
btn.id = 'yt-date-toggle-btn';
btn.textContent = isEnabled ? '📅 Date ON' : '📅 Date OFF';
btn.style.cssText = `
min-width: 120px;
padding: 6px 12px;
margin-left: 10px;
border-radius: 16px;
background-color: #ffcc00;
color: #000;
font-weight: 600;
border: 2px solid #000;
cursor: pointer;
font-size: 13px;
display: inline-flex;
align-items: center;
justify-content: center;
text-align: center;
transition: background-color 0.3s ease;
z-index: 9999;
`;
btn.onmouseenter = () => btn.style.backgroundColor = '#ffe066';
btn.onmouseleave = () => btn.style.backgroundColor = '#ffcc00';
btn.onclick = () => {
isEnabled = !isEnabled;
btn.textContent = isEnabled ? '📅 Date ON' : '📅 Date OFF';
};
const checkInterval = setInterval(() => {
const startBar = document.querySelector('#start');
if (startBar && !document.querySelector('#yt-date-toggle-btn')) {
startBar.appendChild(btn);
clearInterval(checkInterval);
}
}, 500);
}
function hideDefaultDateCSS() {
const style = document.createElement('style');
style.textContent = `
#info > span:nth-child(3),
#info > span:nth-child(4) {
display: none !important;
}
`;
document.head.appendChild(style);
}
function init() {
hideDefaultDateCSS();
observeVideos();
injectToggleUI();
}
document.readyState === 'loading'
? document.addEventListener('DOMContentLoaded', init)
: init();
})();