YouTube Original Audio Selector

Automatically sets YouTube videos to their original audio language

// ==UserScript==
// @name         YouTube Original Audio Selector
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Automatically sets YouTube videos to their original audio language
// @author       You
// @match        https://www.youtube.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Debug logging function
    function debugLog(message, element = null) {
        console.log(`[YT Audio Debug] ${message}`, element || '');
    }

    // Keep track of our observers and timeouts to clean them up
    let urlObserver = null;
    let activeTimeouts = [];
    let checkInterval = null;

    // Function to clear all pending timeouts
    function clearAllTimeouts() {
        activeTimeouts.forEach(timeout => clearTimeout(timeout));
        activeTimeouts = [];
        if (checkInterval) {
            clearInterval(checkInterval);
            checkInterval = null;
        }
    }

    // Function to safely set a timeout that we can clean up later
    function safeSetTimeout(callback, delay) {
        const timeoutId = setTimeout(() => {
            const index = activeTimeouts.indexOf(timeoutId);
            if (index > -1) {
                activeTimeouts.splice(index, 1);
            }
            callback();
        }, delay);
        activeTimeouts.push(timeoutId);
        return timeoutId;
    }

    // Function to check if video has multiple audio tracks
    function hasMultipleAudioTracks() {
        const autoGeneratedText = document.evaluate(
            "//*[contains(text(),'Audio tracks for some languages were automatically generated')]",
            document,
            null,
            XPathResult.FIRST_ORDERED_NODE_TYPE,
            null
        ).singleNodeValue;

        return !!autoGeneratedText;
    }

    // Function to identify the original audio track from menu items
    function findOriginalAudioTrack(menuItems) {
        debugLog('Analyzing available audio tracks...');

        // Convert NodeList to Array and map to objects with text and analysis
        const tracks = Array.from(menuItems).map(item => {
            const text = item.textContent.toLowerCase();
            return {
                element: item,
                text: text,
                isAutoGenerated: text.includes('auto-generated'),
                hasOriginalMarker: text.includes('original'),
                // Some videos mark the original with a star or different formatting
                hasSpecialMarker: /[★✓]/.test(item.textContent)
            };
        });

        debugLog('Found tracks:', tracks.map(t => t.text));

        // First priority: Track explicitly marked as "original"
        const originalTrack = tracks.find(t => t.hasOriginalMarker);
        if (originalTrack) {
            debugLog('Found track marked as original');
            return originalTrack.element;
        }

        // Second priority: Track with special marking and not auto-generated
        const specialTrack = tracks.find(t => t.hasSpecialMarker && !t.isAutoGenerated);
        if (specialTrack) {
            debugLog('Found track with special marking');
            return specialTrack.element;
        }

        // Third priority: First track that isn't marked as auto-generated
        const nonAutoTrack = tracks.find(t => !t.isAutoGenerated);
        if (nonAutoTrack) {
            debugLog('Found non-auto-generated track');
            return nonAutoTrack.element;
        }

        debugLog('No suitable original track found');
        return null;
    }

    // Function to remove lingering tooltips
    function removeTooltips() {
        document.dispatchEvent(new KeyboardEvent('keydown', {
            key: 'Escape',
            code: 'Escape',
            keyCode: 27,
            which: 27,
            bubbles: true,
            cancelable: true
        }));
    }

    // Function to clean up all resources
    function cleanup() {
        if (urlObserver) {
            urlObserver.disconnect();
            urlObserver = null;
        }
        clearAllTimeouts();
        debugLog('Cleaned up all resources');
    }

    // Function to find and click the original audio option
    function setOriginalAudio() {
        debugLog('Starting audio selection process...');

        // Check if we need to do anything
        if (!hasMultipleAudioTracks()) {
            debugLog('No multiple audio tracks detected, skipping');
            cleanup();
            return;
        }

        // First, find the settings button (gear icon)
        const settingsButton = document.querySelector('.ytp-settings-button');
        if (!settingsButton) {
            debugLog('Settings button not found');
            cleanup();
            return;
        }

        debugLog('Found settings button, clicking...');
        settingsButton.click();

        // After clicking settings, wait for menu and look for Audio track option
        safeSetTimeout(() => {
            const menuItems = document.querySelectorAll('.ytp-menuitem');
            debugLog('Looking for audio track option...');

            // Look specifically for the Audio track option
            const audioTrackItem = Array.from(menuItems).find(item => {
                const label = item.querySelector('.ytp-menuitem-label');
                return label && label.textContent.includes('Audio track');
            });

            if (!audioTrackItem) {
                debugLog('No audio track menu item found, closing menu');
                settingsButton.click();
                removeTooltips();
                cleanup();
                return;
            }

            debugLog('Found audio track item, clicking...');
            audioTrackItem.click();

            // Wait for submenu and select original language
            safeSetTimeout(() => {
                const submenuItems = document.querySelectorAll('.ytp-menuitem');
                const originalAudio = findOriginalAudioTrack(submenuItems);

                if (originalAudio) {
                    debugLog('Found original audio option, selecting...');
                    originalAudio.click();

                    // Wait a bit and close the settings menu
                    safeSetTimeout(() => {
                        settingsButton.click();
                        removeTooltips();
                        cleanup();
                    }, 100);
                } else {
                    debugLog('Original audio option not found');
                    settingsButton.click();
                    removeTooltips();
                    cleanup();
                }
            }, 300);
        }, 300);
    }

    // Function to handle navigation changes
    function handleNavigation() {
        debugLog('Navigation detected, waiting for video player...');
        clearAllTimeouts();
        safeSetTimeout(() => {
            setOriginalAudio();
        }, 1500);
    }

    // Start observing URL changes
    function startObserving() {
        if (urlObserver) {
            debugLog('Cleaning up previous observer before starting new one');
            cleanup();
        }

        let lastUrl = location.href;
        urlObserver = new MutationObserver(() => {
            const url = location.href;
            if (url !== lastUrl) {
                lastUrl = url;
                debugLog('URL changed to:', url);
                handleNavigation();
            }
        });

        urlObserver.observe(document, {subtree: true, childList: true});
    }

    // Add a manual trigger for testing
    window.debugYTAudio = () => {
        debugLog('Manual trigger activated');
        setOriginalAudio();
    };

    // Initial setup
    startObserving();
    handleNavigation();
})();