YouTube Full Dates (v3)

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==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 (&lt;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!');
})();