Youtube subtitles

show the subtitels like lyrics plane

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();
})();