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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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