YouTube Enhancer (Reveal Country Flag)

Reveal Country Flag.

// ==UserScript==
// @name         YouTube Enhancer (Reveal Country Flag)
// @description  Reveal Country Flag.
// @icon         https://raw.githubusercontent.com/exyezed/youtube-enhancer/refs/heads/main/extras/youtube-enhancer.png
// @version      1.8
// @author       exyezed
// @namespace    https://github.com/exyezed/youtube-enhancer/
// @supportURL   https://github.com/exyezed/youtube-enhancer/issues
// @license      MIT
// @match        https://www.youtube.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @require      https://cdn.jsdelivr.net/npm/[email protected]/cdn.min.js
// ==/UserScript==

(function () {
    'use strict';

    const FLAG_CONFIG = {
        BASE_URL: 'https://cdn.jsdelivr.net/gh/lipis/[email protected]/flags/4x3/',
        SIZES: {
            channel: '28px',
            video: '22px',
            shorts: '20px'
        },
        MARGINS: {
            channel: '12px',
            video: '10px',
            shorts: '8px'
        }
    };

    const CACHE_CONFIG = {
        PREFIX: 'yt_enhancer_',
        EXPIRATION: 7 * 24 * 60 * 60 * 1000
    };

    const processedElements = new Set();
    const processedChannelAge = new Set();

    function getCacheKey(type, id) {
        return `${CACHE_CONFIG.PREFIX}${type}_${id}`;
    }

    function getFromCache(type, id) {
        const cacheKey = getCacheKey(type, id);
        const cachedData = GM_getValue(cacheKey);
        if (!cachedData) return null;
        const { value, timestamp } = JSON.parse(cachedData);
        if (Date.now() - timestamp > CACHE_CONFIG.EXPIRATION) {
            GM_setValue(cacheKey, null);
            return null;
        }
        return value;
    }

    function setToCache(type, id, value) {
        const cacheKey = getCacheKey(type, id);
        GM_setValue(cacheKey, JSON.stringify({ value, timestamp: Date.now() }));
    }

    async function getCountryData(type, id) {
        const cachedValue = getFromCache(type, id);
        if (cachedValue) {
            if (cachedValue.creationDate) {
                cachedValue.channelAge = calculateChannelAge(cachedValue.creationDate);
            }
            return cachedValue;
        }

        const url = `https://flagscountry.vercel.app/api/${type}/${id}`;
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                onload: function (response) {
                    if (response.status >= 200 && response.status < 300) {
                        try {
                            const data = JSON.parse(response.responseText);
                            const countryData = {
                                code: data.country.toLowerCase(),
                                name: data.countryName,
                                creationDate: data.creationDate,
                                channelAge: data.creationDate ? calculateChannelAge(data.creationDate) : data.channelAge
                            };
                            setToCache(type, id, countryData);
                            resolve(countryData);
                        } catch (error) {
                            console.error('Error parsing JSON:', error);
                            resolve(null);
                        }
                    } else {
                        console.error('Request failed:', response.status);
                        resolve(null);
                    }
                },
                onerror: function (error) {
                    console.error('Request error:', error);
                    resolve(null);
                },
                ontimeout: function () {
                    console.error('Request timed out');
                    resolve(null);
                }
            });
        });
    }

    function calculateChannelAge(creationDateStr) {
        try {
            const creationDate = new Date(creationDateStr);
            const now = new Date();

            if (!creationDate || isNaN(creationDate.getTime())) {
                return "";
            }

            const years = dateFns.differenceInYears(now, creationDate);
            let tempDate = dateFns.addYears(creationDate, years);

            const months = dateFns.differenceInMonths(now, tempDate);
            tempDate = dateFns.addMonths(tempDate, months);

            const days = dateFns.differenceInDays(now, tempDate);
            tempDate = dateFns.addDays(tempDate, days);

            const hours = dateFns.differenceInHours(now, tempDate);
            tempDate = dateFns.addHours(tempDate, hours);

            const minutes = dateFns.differenceInMinutes(now, tempDate);

            let ageString = "";

            if (years > 0) {
                ageString += `${years}y`;

                if (months > 0) {
                    ageString += ` ${months}m`;
                }
            } else if (months > 0) {
                ageString += `${months}m`;

                if (days > 0) {
                    ageString += ` ${days}d`;
                }
            } else if (days > 0) {
                ageString += `${days}d`;

                if (hours > 0) {
                    ageString += ` ${hours}h`;
                }
            } else if (hours > 0) {
                ageString += `${hours}h`;

                if (minutes > 0) {
                    ageString += ` ${minutes}m`;
                }
            } else if (minutes > 0) {
                ageString += `${minutes}m`;
            } else {
                ageString += "<1m";
            }

            ageString += " ago";
            return ageString;
        } catch (error) {
            console.error('Error calculating channel age:', error);
            return "";
        }
    }

    function createFlag(size, margin, className, countryData) {
        const flag = document.createElement('img');
        flag.src = `${FLAG_CONFIG.BASE_URL}${countryData.code}.svg`;
        flag.className = `country-flag ${className}`;
        flag.style.width = size;
        flag.style.height = 'auto';
        flag.style.marginLeft = margin;
        flag.style.verticalAlign = 'middle';
        flag.style.cursor = 'pointer';
        flag.title = countryData.name;
        return flag;
    }

    function removeExistingFlags(element) {
        const existingFlags = element.querySelectorAll('.country-flag');
        existingFlags.forEach(flag => flag.remove());
    }

    function removeExistingChannelAge() {
        const ageElements = document.querySelectorAll('.channel-age-element');
        ageElements.forEach(el => el.remove());

        const delimiterElements = document.querySelectorAll('.channel-age-delimiter');
        delimiterElements.forEach(el => el.remove());
    }

    function addChannelAge(countryData) {
        if (!countryData || !countryData.channelAge) return;

        removeExistingChannelAge();

        const metadataRows = document.querySelectorAll('.yt-content-metadata-view-model-wiz__metadata-row');
        if (!metadataRows.length) return;

        for (const row of metadataRows) {
            if (row.textContent.includes('video') && !processedChannelAge.has(row)) {
                processedChannelAge.add(row);

                const delimiter = document.createElement('span');
                delimiter.className = 'yt-content-metadata-view-model-wiz__delimiter channel-age-delimiter';
                delimiter.setAttribute('aria-hidden', 'true');
                delimiter.textContent = '•';

                const ageSpan = document.createElement('span');
                ageSpan.className = 'yt-core-attributed-string yt-content-metadata-view-model-wiz__metadata-text yt-core-attributed-string--white-space-pre-wrap yt-core-attributed-string--link-inherit-color channel-age-element';
                ageSpan.setAttribute('dir', 'auto');
                ageSpan.setAttribute('role', 'text');

                const innerSpan = document.createElement('span');
                innerSpan.setAttribute('dir', 'auto');
                innerSpan.textContent = ` ${countryData.channelAge}`;

                ageSpan.appendChild(innerSpan);

                row.appendChild(delimiter);
                row.appendChild(ageSpan);

                break;
            }
        }
    }

    async function addFlag() {
        const channelElement = document.querySelector('.dynamic-text-view-model-wiz__h1 .yt-core-attributed-string');
        if (channelElement && !processedElements.has(channelElement)) {
            removeExistingFlags(channelElement.parentElement);
            processedElements.add(channelElement);
            const channelUrl = window.location.pathname;
            const channelId = channelUrl.includes('@')
                ? channelUrl.split('@')[1].split('/')[0]
                : channelUrl.split('/')[2];
            const countryData = await getCountryData('channel', channelId);
            if (countryData) {
                channelElement.appendChild(
                    createFlag(FLAG_CONFIG.SIZES.channel, FLAG_CONFIG.MARGINS.channel, 'channel-flag', countryData)
                );

                addChannelAge(countryData);
            }
        }

        const videoElement = document.querySelector('#title yt-formatted-string');
        if (videoElement && !processedElements.has(videoElement)) {
            const videoParent = videoElement.closest('#title h1');
            if (videoParent) {
                removeExistingFlags(videoParent);
                processedElements.add(videoElement);
                const videoId = new URLSearchParams(window.location.search).get('v');
                if (videoId) {
                    const countryData = await getCountryData('video', videoId);
                    if (countryData) {
                        videoParent.style.display = 'flex';
                        videoParent.style.alignItems = 'center';
                        videoParent.appendChild(
                            createFlag(FLAG_CONFIG.SIZES.video, FLAG_CONFIG.MARGINS.video, 'video-flag', countryData)
                        );
                    }
                }
            }
        }

        const shortsChannelElements = document.querySelectorAll('.ytReelChannelBarViewModelChannelName');
        for (const element of shortsChannelElements) {
            if (!processedElements.has(element)) {
                removeExistingFlags(element);
                processedElements.add(element);
                const shortsId = window.location.pathname.split('/').pop();
                const countryData = await getCountryData('video', shortsId);
                if (countryData) {
                    element.appendChild(
                        createFlag(FLAG_CONFIG.SIZES.shorts, FLAG_CONFIG.MARGINS.shorts, 'shorts-flag', countryData)
                    );
                }
            }
        }
    }

    function debounce(func, wait) {
        let timeout;
        return function (...args) {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), wait);
        };
    }

    const debouncedAddFlag = debounce(addFlag, 500);

    const observer = new MutationObserver((mutations) => {
        for (const mutation of mutations) {
            if (mutation.addedNodes.length || mutation.type === 'childList') {
                debouncedAddFlag();
                break;
            }
        }
    });

    function startObserver() {
        const watchPage = document.querySelector('ytd-watch-flexy');
        const browsePage = document.querySelector('ytd-browse');
        const content = document.querySelector('#content');

        const targetNode = watchPage || browsePage || content || document.body;

        observer.observe(targetNode, {
            childList: true,
            subtree: true
        });
    }

    async function init() {
        await new Promise(resolve => setTimeout(resolve, 100));

        processedElements.clear();
        processedChannelAge.clear();
        removeExistingChannelAge();
        startObserver();
        addFlag();

        window.addEventListener('yt-navigate-finish', () => {
            observer.disconnect();
            processedElements.clear();
            processedChannelAge.clear();
            removeExistingChannelAge();
            startObserver();
            addFlag();
        });
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();