Youtube subtitles

show the subtitels like lyrics plane

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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