Greasy Fork is available in English.
Replace "1 year ago" with exact dates everywhere. Badges (Green/Purple/Yellow), full day & time tokens, works on ALL pages: Home, Search, History, Shorts, Posts, Comments, Descriptions, Members Only
// ==UserScript==
// @name YouTube Full Dates (v3)
// @namespace YouTube Full Dates
// @version 3
// @description Replace "1 year ago" with exact dates everywhere. Badges (Green/Purple/Yellow), full day & time tokens, works on ALL pages: Home, Search, History, Shorts, Posts, Comments, Descriptions, Members Only
// @author Solomon (improved from InMirrors)
// @match https://www.youtube.com/*
// @icon https://www.youtube.com/s/desktop/814d40a6/img/favicon_144x144.png
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @license MIT
// ==/UserScript==
// Previous Features (v1-v2):
// - Exact upload dates replacing relative timestamps
// - 11 language support (EN, FR, ES, DE, IT, PT, ZH, JA, RU, KO, AR)
// - Custom date format tokens (yyyy, yy, MMMM, MMM, MM, dd)
// - Three-tier color badges (green/purple/yellow) with independent toggles
// - Smart year hiding for current year
// - Settings panel with toggles
// - Works on homepage, search, channels, playlists, watch page, subscriptions
// New in v3 (User Request by Cool722):
// - NEW TOKENS: wwww (Monday), ww (Mon), HH (24h), hh (12h), mm, ss, ap (AM/PM)
// - NEW PAGE: Watch History (/feed/history)
// - NEW PAGE: Shorts (everywhere on YouTube)
// - NEW PAGE: Posts & Community sections (/@.../posts, /@.../community)
// - NEW PAGE: Comments on Watch Videos, Posts, and Community
// - NEW PAGE: Collapsed Description of any Watch Video
// - NEW PAGE: Members Only videos on YouTube Home and in Playlists
// - IMPROVED: Extended container and date element selectors for universal coverage
(function() {
'use strict';
// ═══════════════════════════════════════════════════════════════
// 🌍 LANGUAGE DEFINITIONS (11 languages)
// ═══════════════════════════════════════════════════════════════
const LANGUAGES = {
en: {
name: 'English',
monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
monthsFull: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
daysShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
daysFull: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
agoKeywords: ['ago', 'Streamed', 'just now', 'yesterday', 'today', 'Premiere', 'Premieres', 'Updated', 'Edited', 'Pinned'],
dateKeywords: ['second', 'minute', 'hour', 'day', 'week', 'month', 'year']
},
es: {
name: 'Español',
monthsShort: ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'],
monthsFull: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'],
daysShort: ['dom', 'lun', 'mar', 'mié', 'jue', 'vie', 'sáb'],
daysFull: ['domingo', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado'],
agoKeywords: ['hace', 'ayer', 'hoy', 'Estreno'],
dateKeywords: ['segundo', 'minuto', 'hora', 'día', 'semana', 'mes', 'año']
},
fr: {
name: 'Français',
monthsShort: ['janv', 'févr', 'mars', 'avr', 'mai', 'juin', 'juil', 'août', 'sept', 'oct', 'nov', 'déc'],
monthsFull: ['janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'],
daysShort: ['dim', 'lun', 'mar', 'mer', 'jeu', 'ven', 'sam'],
daysFull: ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'],
agoKeywords: ['il y a', 'hier', "aujourd'hui", 'Première'],
dateKeywords: ['seconde', 'minute', 'heure', 'jour', 'semaine', 'mois', 'an', 'année']
},
de: {
name: 'Deutsch',
monthsShort: ['Jan', 'Feb', 'März', 'Apr', 'Mai', 'Juni', 'Juli', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'],
monthsFull: ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'],
daysShort: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'],
daysFull: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'],
agoKeywords: ['vor', 'gestern', 'heute', 'Premiere'],
dateKeywords: ['Sekunde', 'Minute', 'Stunde', 'Tag', 'Woche', 'Monat', 'Jahr']
},
pt: {
name: 'Português',
monthsShort: ['jan', 'fev', 'mar', 'abr', 'mai', 'jun', 'jul', 'ago', 'set', 'out', 'nov', 'dez'],
monthsFull: ['janeiro', 'fevereiro', 'março', 'abril', 'maio', 'junho', 'julho', 'agosto', 'setembro', 'outubro', 'novembro', 'dezembro'],
daysShort: ['dom', 'seg', 'ter', 'qua', 'qui', 'sex', 'sáb'],
daysFull: ['domingo', 'segunda-feira', 'terça-feira', 'quarta-feira', 'quinta-feira', 'sexta-feira', 'sábado'],
agoKeywords: ['há', 'ontem', 'hoje', 'Estreia'],
dateKeywords: ['segundo', 'minuto', 'hora', 'dia', 'semana', 'mês', 'ano']
},
it: {
name: 'Italiano',
monthsShort: ['gen', 'feb', 'mar', 'apr', 'mag', 'giu', 'lug', 'ago', 'set', 'ott', 'nov', 'dic'],
monthsFull: ['gennaio', 'febbraio', 'marzo', 'aprile', 'maggio', 'giugno', 'luglio', 'agosto', 'settembre', 'ottobre', 'novembre', 'dicembre'],
daysShort: ['dom', 'lun', 'mar', 'mer', 'gio', 'ven', 'sab'],
daysFull: ['domenica', 'lunedì', 'martedì', 'mercoledì', 'giovedì', 'venerdì', 'sabato'],
agoKeywords: ['fa', 'ieri', 'oggi', 'Prima'],
dateKeywords: ['secondo', 'minuto', 'ora', 'giorno', 'settimana', 'mese', 'anno']
},
ru: {
name: 'Русский',
monthsShort: ['янв', 'февр', 'март', 'апр', 'май', 'июнь', 'июль', 'авг', 'сент', 'окт', 'нояб', 'дек'],
monthsFull: ['январь', 'февраль', 'март', 'апрель', 'май', 'июнь', 'июль', 'август', 'сентябрь', 'октябрь', 'ноябрь', 'декабрь'],
daysShort: ['вс', 'пн', 'вт', 'ср', 'чт', 'пт', 'сб'],
daysFull: ['воскресенье', 'понедельник', 'вторник', 'среда', 'четверг', 'пятница', 'суббота'],
agoKeywords: ['назад', 'вчера', 'сегодня', 'Премьера'],
dateKeywords: ['секунд', 'минут', 'час', 'день', 'дней', 'недел', 'месяц', 'год', 'лет']
},
zh: {
name: '中文',
monthsShort: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
monthsFull: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
daysShort: ['日', '一', '二', '三', '四', '五', '六'],
daysFull: ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'],
agoKeywords: ['前', '昨天', '今天', '首播'],
dateKeywords: ['秒', '分', '时', '時', '天', '日', '周', '週', '月', '年']
},
ja: {
name: '日本語',
monthsShort: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
monthsFull: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
daysShort: ['日', '月', '火', '水', '木', '金', '土'],
daysFull: ['日曜日', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日'],
agoKeywords: ['前', '昨日', '今日', 'プレミア'],
dateKeywords: ['秒', '分', '時間', '日', '週間', 'か月', '年']
},
ko: {
name: '한국어',
monthsShort: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'],
monthsFull: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'],
daysShort: ['일', '월', '화', '수', '목', '금', '토'],
daysFull: ['일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일'],
agoKeywords: ['전', '어제', '오늘', '프리미어'],
dateKeywords: ['초', '분', '시간', '일', '주', '개월', '년']
},
ar: {
name: 'العربية',
monthsShort: ['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'],
monthsFull: ['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'],
daysShort: ['أحد', 'إثنين', 'ثلاثاء', 'أربعاء', 'خميس', 'جمعة', 'سبت'],
daysFull: ['الأحد', 'الإثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت'],
agoKeywords: ['قبل', 'منذ', 'أمس', 'اليوم', 'العرض'],
dateKeywords: ['ثانية', 'دقيقة', 'ساعة', 'يوم', 'أسبوع', 'شهر', 'سنة']
}
};
// ═══════════════════════════════════════════════════════════════
// ⚙️ SETTINGS & CONFIG
// ═══════════════════════════════════════════════════════════════
const DEFAULT_CONFIG = {
dateFormat: 'MMMM dd yy',
language: 'en',
smartYear: true,
highlightOldVideos: true,
thisWeekEmoji: true,
thisWeekBadge: '🆕',
thisYearBadge: true,
// v3: new settings
processComments: true,
processDescription: true,
processPosts: true,
debugMode: false
};
const savedSettings = GM_getValue('settings', {});
const SETTINGS = { ...DEFAULT_CONFIG, ...savedSettings };
const PROCESSED_ATTR = 'data-ytfd-done';
const dateCache = new Map();
let isProcessing = false, pendingRequests = 0, lastUrl = window.location.href;
const MAX_CONCURRENT = 8, requestQueue = [];
const log = (...args) => SETTINGS.debugMode && console.log('📅 [v3]', ...args);
// ═══════════════════════════════════════════════════════════════
// 📅 DATE FORMATTING (v3: added day names + time tokens)
// ═══════════════════════════════════════════════════════════════
function isWithinLastWeek(date) {
const diffDays = (new Date() - new Date(date)) / (1000 * 60 * 60 * 24);
return diffDays >= 0 && diffDays < 7;
}
function formatDate(date) {
const d = new Date(date);
if (isNaN(d.getTime())) return { text: '', tier: 'none' };
const lang = LANGUAGES[SETTINGS.language] || LANGUAGES.en;
const pad = (n) => String(n).padStart(2, '0');
const now = new Date(), currentYear = now.getFullYear(), videoYear = d.getFullYear();
const isThisWeek = isWithinLastWeek(d);
const isThisYear = currentYear === videoYear && !isThisWeek;
const isOldYear = videoYear < currentYear;
// Tier 1: This week - green with emoji
if (SETTINGS.thisWeekEmoji && isThisWeek) {
return {
text: SETTINGS.thisWeekBadge + ' ' + lang.monthsShort[d.getMonth()] + ' ' + d.getDate(),
tier: 'thisWeek'
};
}
// v3: Extended token map with day names and time
const hours24 = d.getHours();
const hours12 = hours24 % 12 || 12;
const ampm = hours24 < 12 ? 'AM' : 'PM';
const tokens = {
yyyy: d.getFullYear(),
yy: String(d.getFullYear()).slice(-2),
MMMM: lang.monthsFull[d.getMonth()],
MMM: lang.monthsShort[d.getMonth()],
MM: pad(d.getMonth() + 1),
dd: pad(d.getDate()),
// v3: Day name tokens
wwww: lang.daysFull[d.getDay()],
ww: lang.daysShort[d.getDay()],
// v3: Time tokens
HH: pad(hours24),
hh: pad(hours12),
mm: pad(d.getMinutes()),
ss: pad(d.getSeconds()),
ap: ampm
};
let result = SETTINGS.dateFormat;
// Tier 2: This year - no year shown (smart year)
if (SETTINGS.smartYear && currentYear === videoYear) {
result = result.replace(/\s*yyyy\s*/g, ' ').replace(/\s*yy\s*/g, ' ');
}
// Replace tokens longest-first to avoid partial matches (wwww before ww, MMMM before MMM before MM)
result = result.replace(/wwww|ww|yyyy|yy|MMMM|MMM|MM|dd|HH|hh|mm|ss|ap/g, m => tokens[m]);
result = result.replace(/,/g, '').replace(/\s+/g, ' ').trim();
if (isThisYear) return { text: result, tier: 'thisYear' };
if (isOldYear) return { text: result, tier: 'oldYear' };
return { text: result, tier: 'none' };
}
// ═══════════════════════════════════════════════════════════════
// 🔍 VIDEO ID EXTRACTION
// ═══════════════════════════════════════════════════════════════
function getVideoId(url) {
if (!url) return null;
let m = url.match(/\/shorts\/([^/?&#]+)/) || url.match(/[?&]v=([^&#]+)/) || url.match(/\/embed\/([^/?&#]+)/) || url.match(/\/live\/([^/?&#]+)/);
return m ? m[1] : null;
}
// ═══════════════════════════════════════════════════════════════
// 🔍 RELATIVE DATE DETECTION (v3: expanded keywords)
// ═══════════════════════════════════════════════════════════════
function hasRelativeDate(text) {
if (!text) return false;
const agoKw = Object.values(LANGUAGES).flatMap(l => l.agoKeywords);
const dateKw = Object.values(LANGUAGES).flatMap(l => l.dateKeywords);
const t = text.toLowerCase();
return agoKw.some(k => t.includes(k.toLowerCase())) && dateKw.some(k => t.includes(k.toLowerCase()));
}
function hasYouTubeFullDate(text) {
if (!text) return false;
return /\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec|January|February|March|April|June|July|August|September|October|November|December)[a-z]*\.?\s+\d{1,2}/i.test(text);
}
function extractDateFromText(text) {
if (!text) return null;
const monthMap = { jan: 0, feb: 1, mar: 2, apr: 3, may: 4, jun: 5, jul: 6, aug: 7, sep: 8, oct: 9, nov: 10, dec: 11, january: 0, february: 1, march: 2, april: 3, june: 5, july: 6, august: 7, september: 8, october: 9, november: 10, december: 11 };
let m = text.match(/\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec|January|February|March|April|May|June|July|August|September|October|November|December)[a-z]*\.?\s+(\d{1,2}),?\s*(\d{4})?\b/i);
if (m) {
const mo = monthMap[m[1].toLowerCase().replace(/[^a-z]/g, '').substring(0, 3)];
if (mo !== undefined) {
const year = m[3] ? parseInt(m[3]) : new Date().getFullYear();
return new Date(year, mo, parseInt(m[2]));
}
}
return null;
}
function needsProcessing(el) {
if (el.hasAttribute(PROCESSED_ATTR)) return false;
const text = el.textContent;
return text && (hasRelativeDate(text) || hasYouTubeFullDate(text));
}
// v3: Check if text looks like a comment/post relative date (simpler patterns)
function isRelativeDateText(text) {
if (!text) return false;
const t = text.trim().toLowerCase();
// Match patterns like "2 hours ago", "1 day ago", "3 weeks ago", "il y a 2 jours", etc.
const agoKw = Object.values(LANGUAGES).flatMap(l => l.agoKeywords);
const dateKw = Object.values(LANGUAGES).flatMap(l => l.dateKeywords);
return agoKw.some(k => t.includes(k.toLowerCase())) || dateKw.some(k => t.includes(k.toLowerCase()));
}
// ═══════════════════════════════════════════════════════════════
// 🌐 API FETCH
// ═══════════════════════════════════════════════════════════════
async function fetchUploadDate(videoId) {
if (dateCache.has(videoId)) return dateCache.get(videoId);
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('Network error');
const data = await res.json(), info = data?.microformat?.playerMicroformatRenderer;
let uploadDate = info?.liveBroadcastDetails?.isLiveNow ? info.liveBroadcastDetails.startTimestamp : (info?.publishDate || info?.uploadDate);
if (uploadDate) dateCache.set(videoId, uploadDate);
return uploadDate;
} catch (e) { log('❌ Fetch error:', e); return null; }
}
// v3: Fetch comment/post date from the browse endpoint (for posts and community)
// Note: Comments and posts don't always have a direct API for exact dates,
// so we calculate from relative text when possible
function calculateDateFromRelative(text) {
if (!text) return null;
const t = text.trim().toLowerCase();
const now = new Date();
// English patterns
let m;
// "X seconds/minutes/hours/days/weeks/months/years ago"
m = t.match(/(\d+)\s*(second|minute|hour|day|week|month|year)s?\s*ago/);
if (m) {
const val = parseInt(m[1]);
const unit = m[2];
const d = new Date(now);
switch (unit) {
case 'second': d.setSeconds(d.getSeconds() - val); break;
case 'minute': d.setMinutes(d.getMinutes() - val); break;
case 'hour': d.setHours(d.getHours() - val); break;
case 'day': d.setDate(d.getDate() - val); break;
case 'week': d.setDate(d.getDate() - (val * 7)); break;
case 'month': d.setMonth(d.getMonth() - val); break;
case 'year': d.setFullYear(d.getFullYear() - val); break;
}
return d.toISOString();
}
// French "il y a X jours/heures/etc."
m = t.match(/il y a\s*(\d+)\s*(seconde|minute|heure|jour|semaine|mois|an|année)s?/);
if (m) {
const val = parseInt(m[1]);
const unit = m[2];
const d = new Date(now);
const map = { seconde: 'second', minute: 'minute', heure: 'hour', jour: 'day', semaine: 'week', mois: 'month', an: 'year', année: 'year' };
switch (map[unit]) {
case 'second': d.setSeconds(d.getSeconds() - val); break;
case 'minute': d.setMinutes(d.getMinutes() - val); break;
case 'hour': d.setHours(d.getHours() - val); break;
case 'day': d.setDate(d.getDate() - val); break;
case 'week': d.setDate(d.getDate() - (val * 7)); break;
case 'month': d.setMonth(d.getMonth() - val); break;
case 'year': d.setFullYear(d.getFullYear() - val); break;
}
return d.toISOString();
}
// Spanish "hace X días/horas/etc."
m = t.match(/hace\s*(\d+)\s*(segundo|minuto|hora|día|dia|semana|mes|meses|año)s?/);
if (m) {
const val = parseInt(m[1]);
const unit = m[2];
const d = new Date(now);
const map = { segundo: 'second', minuto: 'minute', hora: 'hour', día: 'day', dia: 'day', semana: 'week', mes: 'month', meses: 'month', año: 'year' };
switch (map[unit]) {
case 'second': d.setSeconds(d.getSeconds() - val); break;
case 'minute': d.setMinutes(d.getMinutes() - val); break;
case 'hour': d.setHours(d.getHours() - val); break;
case 'day': d.setDate(d.getDate() - val); break;
case 'week': d.setDate(d.getDate() - (val * 7)); break;
case 'month': d.setMonth(d.getMonth() - val); break;
case 'year': d.setFullYear(d.getFullYear() - val); break;
}
return d.toISOString();
}
// German "vor X Tagen/Stunden/etc."
m = t.match(/vor\s*(\d+)\s*(sekunde|minute|stunde|tag|tage|woche|monat|jahr)n?e?n?/i);
if (m) {
const val = parseInt(m[1]);
const unit = m[2].toLowerCase();
const d = new Date(now);
const map = { sekunde: 'second', minute: 'minute', stunde: 'hour', tag: 'day', tage: 'day', woche: 'week', monat: 'month', jahr: 'year' };
switch (map[unit]) {
case 'second': d.setSeconds(d.getSeconds() - val); break;
case 'minute': d.setMinutes(d.getMinutes() - val); break;
case 'hour': d.setHours(d.getHours() - val); break;
case 'day': d.setDate(d.getDate() - val); break;
case 'week': d.setDate(d.getDate() - (val * 7)); break;
case 'month': d.setMonth(d.getMonth() - val); break;
case 'year': d.setFullYear(d.getFullYear() - val); break;
}
return d.toISOString();
}
// Generic fallback: try to find any number + time unit pattern
m = t.match(/(\d+)\s*(sec|min|hr|hour|day|week|month|year|mo|yr|wk)/i);
if (m) {
const val = parseInt(m[1]);
const unit = m[2].toLowerCase();
const d = new Date(now);
if (unit.startsWith('sec')) d.setSeconds(d.getSeconds() - val);
else if (unit.startsWith('min')) d.setMinutes(d.getMinutes() - val);
else if (unit.startsWith('hr') || unit.startsWith('hour')) d.setHours(d.getHours() - val);
else if (unit.startsWith('day')) d.setDate(d.getDate() - val);
else if (unit.startsWith('wk') || unit.startsWith('week')) d.setDate(d.getDate() - (val * 7));
else if (unit.startsWith('mo') || unit.startsWith('month')) d.setMonth(d.getMonth() - val);
else if (unit.startsWith('yr') || unit.startsWith('year')) d.setFullYear(d.getFullYear() - val);
return d.toISOString();
}
return null;
}
// ═══════════════════════════════════════════════════════════════
// ⚡ QUEUE SYSTEM
// ═══════════════════════════════════════════════════════════════
async function processQueue() {
while (requestQueue.length > 0 && pendingRequests < MAX_CONCURRENT) {
const task = requestQueue.shift();
pendingRequests++;
try { await task(); } catch (e) { log('❌ Task error:', e); }
pendingRequests--;
}
}
function applyDateToElement(element, result) {
element.textContent = result.text;
element.setAttribute(PROCESSED_ATTR, 'true');
element.classList.remove('ytfd-old-video', 'ytfd-this-week', 'ytfd-this-year');
if (result.tier === 'thisWeek') {
element.classList.add('ytfd-this-week');
} else if (result.tier === 'oldYear' && SETTINGS.highlightOldVideos) {
element.classList.add('ytfd-old-video');
} else if (result.tier === 'thisYear' && SETTINGS.thisYearBadge) {
element.classList.add('ytfd-this-year');
}
// If thisYearBadge is OFF, "thisYear" dates get no badge class = plain text
log('✅', result.tier, result.text);
}
function queueDateUpdate(videoId, element, originalText) {
element.setAttribute(PROCESSED_ATTR, 'pending');
requestQueue.push(async () => {
let uploadDate = null;
if (hasYouTubeFullDate(originalText)) {
const extracted = extractDateFromText(originalText);
if (extracted) {
uploadDate = extracted.toISOString();
log('📅 Extracted:', originalText, '→', extracted);
}
}
if (!uploadDate) {
uploadDate = await fetchUploadDate(videoId);
}
if (!uploadDate) {
element.removeAttribute(PROCESSED_ATTR);
return;
}
const result = formatDate(uploadDate);
if (!result.text) {
element.removeAttribute(PROCESSED_ATTR);
return;
}
applyDateToElement(element, result);
});
processQueue();
}
// v3: Queue for elements that don't have a video ID (comments, posts)
// Uses relative date calculation instead of API fetch
function queueRelativeDateUpdate(element, originalText) {
element.setAttribute(PROCESSED_ATTR, 'pending');
requestQueue.push(async () => {
const calculatedDate = calculateDateFromRelative(originalText);
if (!calculatedDate) {
element.removeAttribute(PROCESSED_ATTR);
return;
}
const result = formatDate(calculatedDate);
if (!result.text) {
element.removeAttribute(PROCESSED_ATTR);
return;
}
applyDateToElement(element, result);
});
processQueue();
}
// ═══════════════════════════════════════════════════════════════
// 🔍 ELEMENT FINDERS (v3: massively expanded selectors)
// ═══════════════════════════════════════════════════════════════
function findDateElement(container) {
// v3: Extended selectors covering all page types
const selectors = [
'#metadata-line > span:first-child',
'#metadata-line > span',
'.inline-metadata-item',
'.yt-core-attributed-string--link-inherit-color',
'span.yt-formatted-string',
// v3: Watch history
'#video-info > span',
'ytd-video-meta-block #metadata-line span',
// v3: Grid subscriptions
'ytd-two-column-browse-results-renderer #metadata-line span',
// v3: Shorts overlay date
'.metadata-stats span',
'.reel-video-in-sequence-metadata span',
// v3: Members-only badge area
'ytd-badge-supported-renderer + #metadata-line span',
// v3: Playlist items
'#byline-container span'
];
for (const selector of selectors) {
const elements = container.querySelectorAll(selector);
for (const el of elements) {
if (needsProcessing(el)) return el;
}
}
return null;
}
function findVideoLink(container) {
const selectors = [
'a#thumbnail',
'a#video-title-link',
'h3 > a',
'.yt-lockup-view-model__content-image',
'a[href*="watch"]',
'a[href*="shorts"]',
'a[href*="live"]'
];
for (const selector of selectors) {
const el = container.querySelector(selector);
if (el?.href) return el.href;
}
return null;
}
// ═══════════════════════════════════════════════════════════════
// 📺 MAIN PROCESSORS (v3: expanded for all page types)
// ═══════════════════════════════════════════════════════════════
function processAllVideos() {
// v3: Extended container selectors for all YouTube page types
const containerSelectors = [
'ytd-rich-item-renderer', // Homepage, Subscriptions grid
'ytd-video-renderer', // Search, Watch History, Channel Videos
'ytd-compact-video-renderer', // Sidebar / Up Next
'ytd-playlist-video-renderer', // Playlist items
'ytd-grid-video-renderer', // Channel videos grid
'ytd-rich-grid-media', // Rich grid items
'yt-lockup-view-model', // New lockup model
'ytd-reel-item-renderer', // Shorts in feeds
'ytd-playlist-panel-video-renderer', // Playlist panel sidebar
'ytd-structured-description-content-renderer', // v3: For description date extraction
];
containerSelectors.forEach(selector => {
document.querySelectorAll(selector).forEach(container => {
const dateEl = findDateElement(container);
if (!dateEl) return;
const href = findVideoLink(container);
const videoId = getVideoId(href);
if (!videoId) return;
const originalText = dateEl.textContent.trim();
queueDateUpdate(videoId, dateEl, originalText);
});
});
}
// v3: Process the watch page description date
function processDescription() {
if (!SETTINGS.processDescription) return;
if (!window.location.pathname.startsWith('/watch')) return;
// YouTube shows date in the description area / info section
const infoSelectors = [
'#info-strings yt-formatted-string',
'ytd-watch-metadata #info-strings span',
'#description-inner ytd-video-primary-info-renderer #info-strings yt-formatted-string',
'#info span.style-scope.yt-formatted-string',
'ytd-watch-metadata #info yt-formatted-string',
'#upload-info span'
];
for (const selector of infoSelectors) {
const elements = document.querySelectorAll(selector);
elements.forEach(el => {
if (needsProcessing(el)) {
const videoId = getVideoId(window.location.href);
if (videoId) {
queueDateUpdate(videoId, el, el.textContent.trim());
}
}
});
}
}
// v3: Process community posts and backstage posts
function processPosts() {
if (!SETTINGS.processPosts) return;
const postSelectors = [
'ytd-backstage-post-renderer', // Community posts
'ytd-post-renderer', // Posts tab
'ytd-backstage-post-thread-renderer' // Post threads
];
postSelectors.forEach(selector => {
document.querySelectorAll(selector).forEach(post => {
// Find the relative date element inside the post
const dateSelectors = [
'#published-time-text a',
'#published-time-text',
'yt-formatted-string.published-time-text',
'.published-time-text'
];
for (const ds of dateSelectors) {
const dateEl = post.querySelector(ds);
if (dateEl && !dateEl.hasAttribute(PROCESSED_ATTR)) {
const text = dateEl.textContent.trim();
if (isRelativeDateText(text)) {
queueRelativeDateUpdate(dateEl, text);
}
}
}
});
});
}
// v3: Process comments
function processComments() {
if (!SETTINGS.processComments) return;
const commentSelectors = [
'ytd-comment-renderer',
'ytd-comment-view-model'
];
commentSelectors.forEach(selector => {
document.querySelectorAll(selector).forEach(comment => {
const dateSelectors = [
'#published-time-text',
'a.yt-simple-endpoint[href*="lc="]',
'#header-author yt-formatted-string a',
'.published-time-text a',
'span.published-time-text'
];
for (const ds of dateSelectors) {
const dateEl = comment.querySelector(ds);
if (dateEl && !dateEl.hasAttribute(PROCESSED_ATTR)) {
const text = dateEl.textContent.trim();
if (isRelativeDateText(text)) {
queueRelativeDateUpdate(dateEl, text);
}
}
}
});
});
}
// v3: Process Shorts page (overlay metadata)
function processShorts() {
if (!window.location.pathname.startsWith('/shorts')) return;
// On the Shorts watch page, try to get the current video ID from URL
const videoId = getVideoId(window.location.href);
if (!videoId) return;
// Shorts can show dates in various overlay elements
const shortDateSelectors = [
'ytd-reel-video-renderer #channel-info #metadata span',
'.reel-player-overlay-renderer #metadata span',
'yt-formatted-string.ytShortsVideoPlayerMetadataSubhead'
];
shortDateSelectors.forEach(selector => {
document.querySelectorAll(selector).forEach(el => {
if (needsProcessing(el)) {
queueDateUpdate(videoId, el, el.textContent.trim());
}
});
});
}
// ═══════════════════════════════════════════════════════════════
// 🔄 CLEAR & RUN
// ═══════════════════════════════════════════════════════════════
function clearAllMarkers() {
log('🔄 Clearing all markers...');
document.querySelectorAll('[' + PROCESSED_ATTR + ']').forEach(el => {
el.removeAttribute(PROCESSED_ATTR);
el.classList.remove('ytfd-old-video', 'ytfd-this-week', 'ytfd-this-year');
});
}
function runProcessors() {
if (isProcessing) return;
isProcessing = true;
try {
processAllVideos();
processDescription(); // v3
processPosts(); // v3
processComments(); // v3
processShorts(); // v3
} catch (e) { log('❌ Error:', e); }
isProcessing = false;
}
// ═══════════════════════════════════════════════════════════════
// 🎨 CSS STYLES
// ═══════════════════════════════════════════════════════════════
GM_addStyle(`
.ytfd-old-video { background-color: #ffeb3b !important; padding: 2px 6px !important; border-radius: 4px !important; color: #000 !important; font-weight: 600 !important; display: inline !important; line-height: 1.2 !important; }
html[dark] .ytfd-old-video, [dark] .ytfd-old-video { background-color: #ffd600 !important; }
.ytfd-this-week { background-color: #81c784 !important; padding: 2px 6px !important; border-radius: 4px !important; color: #000 !important; font-weight: 600 !important; display: inline !important; line-height: 1.2 !important; }
html[dark] .ytfd-this-week, [dark] .ytfd-this-week { background-color: #66bb6a !important; }
.ytfd-this-year { background-color: #b39ddb !important; padding: 2px 6px !important; border-radius: 4px !important; color: #000 !important; font-weight: 600 !important; display: inline !important; line-height: 1.2 !important; }
html[dark] .ytfd-this-year, [dark] .ytfd-this-year { background-color: #9575cd !important; color: #fff !important; }
.ytfd-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #fff; border-radius: 12px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); z-index: 99999; width: 380px; max-height: 85vh; overflow: hidden; opacity: 0; visibility: hidden; transition: all 0.2s ease; }
html[dark] .ytfd-panel, [dark] .ytfd-panel { background: #212121; color: #fff; }
.ytfd-panel.visible { opacity: 1; visibility: visible; }
.ytfd-header { background: #cc0000; color: #fff; padding: 14px; display: flex; justify-content: space-between; align-items: center; }
.ytfd-header h2 { margin: 0; font-size: 15px; }
.ytfd-close { background: rgba(255,255,255,0.2); border: none; color: #fff; width: 26px; height: 26px; border-radius: 50%; cursor: pointer; font-size: 14px; }
.ytfd-content { padding: 14px; max-height: 60vh; overflow-y: auto; }
.ytfd-section { margin-bottom: 14px; }
.ytfd-section-title { font-size: 11px; font-weight: 600; color: #666; margin-bottom: 6px; text-transform: uppercase; }
html[dark] .ytfd-section-title { color: #aaa; }
.ytfd-input-group { margin-bottom: 8px; }
.ytfd-input-group label { display: block; font-size: 12px; margin-bottom: 3px; color: #333; }
html[dark] .ytfd-input-group label { color: #ddd; }
.ytfd-input-group input, .ytfd-input-group select { width: 100%; padding: 6px 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 12px; box-sizing: border-box; }
html[dark] .ytfd-input-group input, html[dark] .ytfd-input-group select { background: #333; border-color: #444; color: #fff; }
.ytfd-toggle-row { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; }
.ytfd-toggle-label { font-size: 12px; color: #333; }
html[dark] .ytfd-toggle-label { color: #ddd; }
.ytfd-toggle { width: 36px; height: 18px; background: #ccc; border-radius: 9px; position: relative; cursor: pointer; flex-shrink: 0; }
.ytfd-toggle::after { content: ''; position: absolute; width: 14px; height: 14px; background: #fff; border-radius: 50%; top: 2px; left: 2px; transition: transform 0.2s; }
.ytfd-toggle.on { background: #cc0000; }
.ytfd-toggle.on::after { transform: translateX(18px); }
.ytfd-footer { padding: 10px 14px; background: #f5f5f5; display: flex; justify-content: flex-end; gap: 6px; }
html[dark] .ytfd-footer { background: #1a1a1a; }
.ytfd-btn { padding: 6px 12px; border: none; border-radius: 4px; font-size: 12px; font-weight: 600; cursor: pointer; }
.ytfd-btn-primary { background: #cc0000; color: #fff; }
.ytfd-btn-secondary { background: #ddd; color: #333; }
html[dark] .ytfd-btn-secondary { background: #444; color: #fff; }
.ytfd-help { font-size: 10px; color: #888; line-height: 1.4; }
`);
// ═══════════════════════════════════════════════════════════════
// ⚙️ SETTINGS PANEL (v3: added new toggles & expanded token help)
// ═══════════════════════════════════════════════════════════════
function createSettingsPanel() {
const panel = document.createElement('div');
panel.className = 'ytfd-panel';
const langOpts = Object.entries(LANGUAGES).map(([c, l]) => '<option value="' + c + '"' + (c === SETTINGS.language ? ' selected' : '') + '>' + l.name + '</option>').join('');
panel.innerHTML = '<div class="ytfd-header"><h2>📅 YouTube Full Dates v3</h2><button class="ytfd-close">✕</button></div>'
+ '<div class="ytfd-content">'
// Language & Format
+ '<div class="ytfd-section"><div class="ytfd-section-title">Language & Format</div>'
+ '<div class="ytfd-input-group"><label>Language</label><select id="ytfd-language">' + langOpts + '</select></div>'
+ '<div class="ytfd-input-group"><label>Date Format</label><input type="text" id="ytfd-format" value="' + SETTINGS.dateFormat + '"></div>'
+ '<div class="ytfd-help">wwww=Monday, ww=Mon, MMMM=January, MMM=Jan, dd=05, yy=25, yyyy=2025<br>HH=18 (24h), hh=06 (12h), mm=30, ss=45, ap=PM</div></div>'
// Badge Toggles
+ '<div class="ytfd-section"><div class="ytfd-section-title">Badge Options</div>'
+ '<div class="ytfd-toggle-row"><span class="ytfd-toggle-label">Smart Year (hide if current)</span><div class="ytfd-toggle' + (SETTINGS.smartYear ? ' on' : '') + '" data-key="smartYear"></div></div>'
+ '<div class="ytfd-toggle-row"><span class="ytfd-toggle-label">🟡 Yellow for old years</span><div class="ytfd-toggle' + (SETTINGS.highlightOldVideos ? ' on' : '') + '" data-key="highlightOldVideos"></div></div>'
+ '<div class="ytfd-toggle-row"><span class="ytfd-toggle-label">🆕 Green for this week</span><div class="ytfd-toggle' + (SETTINGS.thisWeekEmoji ? ' on' : '') + '" data-key="thisWeekEmoji"></div></div>'
+ '<div class="ytfd-toggle-row"><span class="ytfd-toggle-label">🟣 Purple for this year</span><div class="ytfd-toggle' + (SETTINGS.thisYearBadge ? ' on' : '') + '" data-key="thisYearBadge"></div></div></div>'
// v3: Page Coverage Toggles
+ '<div class="ytfd-section"><div class="ytfd-section-title">Page Coverage (v3)</div>'
+ '<div class="ytfd-toggle-row"><span class="ytfd-toggle-label">💬 Process Comments</span><div class="ytfd-toggle' + (SETTINGS.processComments ? ' on' : '') + '" data-key="processComments"></div></div>'
+ '<div class="ytfd-toggle-row"><span class="ytfd-toggle-label">📝 Process Description Dates</span><div class="ytfd-toggle' + (SETTINGS.processDescription ? ' on' : '') + '" data-key="processDescription"></div></div>'
+ '<div class="ytfd-toggle-row"><span class="ytfd-toggle-label">📣 Process Posts & Community</span><div class="ytfd-toggle' + (SETTINGS.processPosts ? ' on' : '') + '" data-key="processPosts"></div></div></div>'
// Color Guide
+ '<div class="ytfd-section"><div class="ytfd-section-title">Color Guide</div>'
+ '<div class="ytfd-help">🟢 Green = This week (<7 days)<br>🟣 Purple = This year (7+ days) — toggle off for plain text<br>🟡 Yellow = Previous years</div></div>'
+ '</div>'
// Footer
+ '<div class="ytfd-footer"><button class="ytfd-btn ytfd-btn-secondary" id="ytfd-reset">Reset</button><button class="ytfd-btn ytfd-btn-primary" id="ytfd-save">Save</button></div>';
document.body.appendChild(panel);
panel.querySelector('.ytfd-close').addEventListener('click', () => panel.classList.remove('visible'));
panel.querySelectorAll('.ytfd-toggle').forEach(t => t.addEventListener('click', () => t.classList.toggle('on')));
panel.querySelector('#ytfd-save').addEventListener('click', () => {
GM_setValue('settings', {
dateFormat: panel.querySelector('#ytfd-format').value,
language: panel.querySelector('#ytfd-language').value,
smartYear: panel.querySelector('[data-key="smartYear"]').classList.contains('on'),
highlightOldVideos: panel.querySelector('[data-key="highlightOldVideos"]').classList.contains('on'),
thisWeekEmoji: panel.querySelector('[data-key="thisWeekEmoji"]').classList.contains('on'),
thisYearBadge: panel.querySelector('[data-key="thisYearBadge"]').classList.contains('on'),
processComments: panel.querySelector('[data-key="processComments"]').classList.contains('on'),
processDescription: panel.querySelector('[data-key="processDescription"]').classList.contains('on'),
processPosts: panel.querySelector('[data-key="processPosts"]').classList.contains('on'),
thisWeekBadge: SETTINGS.thisWeekBadge,
debugMode: SETTINGS.debugMode
});
alert('Saved! Refresh to apply.');
panel.classList.remove('visible');
});
panel.querySelector('#ytfd-reset').addEventListener('click', () => {
GM_setValue('settings', {});
alert('Reset! Refresh to apply.');
panel.classList.remove('visible');
});
document.addEventListener('keydown', e => { if (e.key === 'Escape') panel.classList.remove('visible'); });
return panel;
}
const settingsPanel = createSettingsPanel();
GM_registerMenuCommand('⚙️ Settings', () => settingsPanel.classList.add('visible'));
// ═══════════════════════════════════════════════════════════════
// 🔄 EVENT LISTENERS & OBSERVERS
// ═══════════════════════════════════════════════════════════════
let debounceTimer = null;
function debouncedRun(delay = 200) { clearTimeout(debounceTimer); debounceTimer = setTimeout(runProcessors, delay); }
// Tab/filter click handler
document.addEventListener('click', e => {
const chip = e.target.closest('yt-chip-cloud-chip-renderer, yt-formatted-string[class*="chip"], [role="tab"], iron-selector yt-formatted-string, tp-yt-paper-item, ytd-feed-filter-chip-bar-renderer yt-chip-cloud-chip-renderer');
if (chip) {
log('🎯 Tab/filter clicked');
setTimeout(() => { clearAllMarkers(); runProcessors(); }, 500);
setTimeout(() => { clearAllMarkers(); runProcessors(); }, 1000);
setTimeout(() => { clearAllMarkers(); runProcessors(); }, 2000);
}
}, true);
// Mutation observer for dynamically loaded content
const observer = new MutationObserver((mutations) => {
let shouldRun = false;
for (const m of mutations) {
if (m.addedNodes.length > 0) {
for (const n of m.addedNodes) {
if (n.nodeType === 1 && (
n.matches?.('ytd-rich-item-renderer, ytd-video-renderer, ytd-compact-video-renderer, ytd-comment-renderer, ytd-comment-view-model, ytd-backstage-post-renderer, ytd-post-renderer, ytd-reel-item-renderer, #contents, #comments, #comment, ytd-section-list-renderer') ||
n.querySelector?.('ytd-rich-item-renderer, ytd-video-renderer, ytd-comment-renderer, ytd-backstage-post-renderer')
)) {
shouldRun = true;
break;
}
}
}
}
if (shouldRun) debouncedRun(100);
});
observer.observe(document.body, { childList: true, subtree: true });
// URL change detection
setInterval(() => {
if (window.location.href !== lastUrl) {
log('🔄 URL changed');
lastUrl = window.location.href;
clearAllMarkers();
debouncedRun(300);
}
}, 500);
// YouTube SPA navigation
window.addEventListener('yt-navigate-finish', () => {
log('🧭 Navigation finished');
dateCache.clear();
lastUrl = window.location.href;
clearAllMarkers();
debouncedRun(300);
});
// Scroll handler for lazy-loaded content
window.addEventListener('scroll', () => debouncedRun(150), { passive: true });
// Periodic re-scan (catches edge cases, dynamically loaded comments, etc.)
setInterval(runProcessors, 3000);
// Initial runs with staggered timing
setTimeout(runProcessors, 300);
setTimeout(runProcessors, 1000);
setTimeout(runProcessors, 2000);
// v3: Extra delayed run for comments/posts which load later
setTimeout(runProcessors, 4000);
setTimeout(runProcessors, 6000);
console.log('📅 YouTube Full Dates v3 loaded!');
})();