Youtube subtitles

show the subtitels like lyrics plane

// ==UserScript==
// @name         Youtube subtitles
// @namespace    http://fqdeng.com
// @version      1.0.1
// @description  show the subtitels like lyrics plane
// @author       fqdeng
// @match        https://www.youtube.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant        none
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// @require      https://code.jquery.com/ui/1.12.1/jquery-ui.min.js
// @license MIT
// ==/UserScript==

(function() {
    'use strict';
    let draggableDiv = null;
    let contentDiv = null;
    let windowDiv = null;
    const textArea = document.createElement('textarea');

    function getVideoIdFromUrl() {
        const urlParams = new URLSearchParams(window.location.search);
        return urlParams.get('v');
    }

    function isYouTubeVideoUrl() {
        var url = window.location.href;
        var pattern = /^https:\/\/www\.youtube\.com\/watch\?v=[\w-]+/;
        return pattern.test(url);
    }

    function onPageChanged() {
        console.log('url change');
        if (!isYouTubeVideoUrl()){
            if (draggableDiv){
                $(draggableDiv).hide()
            }
        }else{
            if (draggableDiv){
                $(draggableDiv).show()
            }
        }
        loadSubtitles();
    }

    function decodeHtmlEntities(text) {
        textArea.innerHTML = text;
        return textArea.value;
    }

    function updateSubtitleScroll(videoTime) {
        const subtitles = contentDiv.getElementsByTagName('div');
        let activeFound = false; // Track if the active subtitle has been found and highlighted

        for (let i = 0; i < subtitles.length; i++) {
            const subtitleData = subtitles[i].dataset;
            const start = parseFloat(subtitleData.start);
            const duration = parseFloat(subtitleData.dur);

            if (start <= videoTime && videoTime <= start + duration) {
                subtitles[i].style.backgroundColor = 'orange';
                subtitles[i].scrollIntoView({ behavior: 'smooth', block: 'center' });
                activeFound = true;
            } else {
                subtitles[i].style.backgroundColor = ''; // Reset background color for non-active subtitles
            }
        }

        // If no active subtitle is found (e.g., between subtitles), ensure all are reset
        if (!activeFound) {
            for (let i = 0; i < subtitles.length; i++) {
                subtitles[i].style.backgroundColor = '';
            }
        }
    }

    function setupVideoPlayerListener() {
        var player = document.getElementsByTagName("video")[0];
        if (player) {
            setInterval(() => {
                const currentTime = player.currentTime;
                updateSubtitleScroll(currentTime);
            }, 1000); // Update every second
        } else {
            setTimeout(setupVideoPlayerListener, 1000); // Retry after 1 second if player not ready
        }
    }


    function loadSubtitles() {
        // Clear existing subtitles
        contentDiv.innerHTML = ''; // Or use the loop method if preferred

        // Load and display subtitles
        renderSubtitles(getVideoIdFromUrl()).then(subtitles => {
            if (subtitles) {
                subtitles.forEach(subtitle => {
                    const subtitleDiv = document.createElement('div');
                    subtitleDiv.style.fontSize = '16px'; // Apply font size to each subtitle
                    subtitleDiv.style.lineHeight = '1.4';
                    subtitleDiv.style.marginBottom = '5px'; // Add space between subtitles
                    const startTime = parseFloat(subtitle.start).toFixed(2);
                    const duration = parseFloat(subtitle.dur).toFixed(2);
                    subtitleDiv.textContent = `${decodeHtmlEntities(subtitle.text)}`;
                    subtitleDiv.dataset.start = subtitle.start;
                    subtitleDiv.dataset.dur = subtitle.dur;
                    contentDiv.appendChild(subtitleDiv);
                });
            }
        });
    }


     function renderDragableDiv(){
        // Load jQuery UI CSS
        const cssLink = document.createElement('link');
        cssLink.rel = 'stylesheet';
        cssLink.href = 'https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css';
        document.head.appendChild(cssLink);

        // Function to save position and size to localStorage
        function savePositionAndSize(left, top, width, height) {
            localStorage.setItem('draggableSubtitlesPosition', JSON.stringify({ left, top, width, height }));
        }

        // Function to get position and size from localStorage
        function getPositionAndSize() {
            const savedPosition = localStorage.getItem('draggableSubtitlesPosition');
            return savedPosition ? JSON.parse(savedPosition) : { left: '859px', top: '83px', width: '384.891px', height: '436px' };
        }

        // Wait for the document to be ready
        $(document).ready(function() {
            // Get initial position and size from localStorage
            const initialPositionAndSize = getPositionAndSize();


            // Create draggable and resizable window elements
            draggableDiv = document.createElement('div');
            draggableDiv.id = 'draggableSubtitles';
            Object.assign(draggableDiv.style, {
                width: initialPositionAndSize.width,
                height: initialPositionAndSize.height,
                zIndex: 1000,
                padding: '0.5em',
                backgroundColor: '#f0f0f0',
                border: '1px solid #ccc',
                position: 'absolute',
                left: initialPositionAndSize.left,
                top: initialPositionAndSize.top,
            });
            //init then hide
            $(draggableDiv).hide();

            const headerDiv = document.createElement('div');
            headerDiv.className = 'header';
            Object.assign(headerDiv.style, {
                cursor: 'move',
                backgroundColor: '#ccc',
                padding: '10px',
                textAlign: 'center',
                fontWeight: 'bold'
            });
            headerDiv.textContent = 'Drag me';

            contentDiv = document.createElement('div');
            contentDiv.className = 'content';
            contentDiv.style.padding = '10px';

            windowDiv = document.createElement('div');
            Object.assign(windowDiv.style, {
                overflow: 'auto',
                position: 'absolute',
                width: '95%',
                height: '90%',
            });
            // Assemble the draggable and resizable window
            windowDiv.appendChild(contentDiv);

            draggableDiv.appendChild(headerDiv);
            draggableDiv.appendChild(windowDiv);

            // Append the draggable and resizable window to the body
            document.body.appendChild(draggableDiv);

            // Make the window draggable
            $('#draggableSubtitles').draggable({
                handle: '.header',
                stop: function(event, ui) {
                    const left = ui.position.left + 'px';
                    const top = ui.position.top + 'px';
                    const width = $('#draggableSubtitles').width() + 'px';
                    const height = $('#draggableSubtitles').height() + 'px';
                    savePositionAndSize(left, top, width, height);
                }
            });

            // Make the window resizable
            $('#draggableSubtitles').resizable({
                stop: function(event, ui) {
                    const left = ui.position.left + 'px';
                    const top = ui.position.top + 'px';
                    const width = ui.size.width + 'px';
                    const height = ui.size.height + 'px';
                    savePositionAndSize(left, top, width, height);
                }
            });


        });
   }

    // Function to load subtitles
    async function renderSubtitles(videoId) {
        try {

            if (!videoId) {
                console.error('Invalid YouTube URL');
                return;
            }

            // Fetch video page HTML
            const response = await fetch(`https://www.youtube.com/watch?v=${videoId}`);
            const pageHtml = await response.text();

            // Extract player response JSON from the HTML
            const playerResponseRegex = /ytInitialPlayerResponse\s*=\s*(\{.*?\});/;
            const playerResponseMatch = pageHtml.match(playerResponseRegex);

            if (!playerResponseMatch) {
                console.error('Could not extract player response');
                return;
            }

            const playerResponse = JSON.parse(playerResponseMatch[1]);

            // Get subtitle tracks
            const captionTracks = playerResponse.captions?.playerCaptionsTracklistRenderer?.captionTracks;

            if (!captionTracks || captionTracks.length === 0) {
                console.error('No subtitles found for this video');
                return;
            }

            // Fetch subtitles for the first track
            const subtitleTrackUrl = captionTracks[0].baseUrl;
            const subtitlesResponse = await fetch(subtitleTrackUrl);
            const subtitlesXml = await subtitlesResponse.text();

            // Parse XML and extract subtitles
            const parser = new DOMParser();
            const subtitlesDoc = parser.parseFromString(subtitlesXml, 'text/xml');
            const texts = subtitlesDoc.getElementsByTagName('text');

            // existing code to fetch and parse subtitles...
            const subtitles = [];
            for (let i = 0; i < texts.length; i++) {
                const start = texts[i].getAttribute('start');
                const dur = texts[i].getAttribute('dur');
                let text = texts[i].textContent;
                text = decodeHtmlEntities(text); // Decode HTML entities here
                subtitles.push({ start, dur, text });
            }

            console.log('Subtitles:', subtitles);
            return subtitles;
        } catch (error) {
            console.error('An error occurred:', error);
        }
    }

    function main(){
        renderDragableDiv();
        //handle youtube page changed event
        window.addEventListener('yt-page-data-updated', onPageChanged);
        setupVideoPlayerListener();
    }

    main();
})();