您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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(); })();