NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name YouTube Enhancer (Subtitle Downloader) // @description Allows you to download available subtitles for YouTube videos in various languages directly from the video page. // @icon https://raw.githubusercontent.com/exyezed/youtube-enhancer/refs/heads/main/extras/youtube-enhancer.png // @version 1.1 // @author exyezed // @namespace https://github.com/exyezed/youtube-enhancer/ // @supportURL https://github.com/exyezed/youtube-enhancer/issues // @license MIT // @match https://www.youtube.com/* // @match https://youtube.com/* // @grant none // @run-at document-idle // ==/UserScript== (function() { 'use strict'; function xmlToSrt(xmlText) { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlText, "text/xml"); const textNodes = xmlDoc.getElementsByTagName("text"); let srtContent = ''; for (let i = 0; i < textNodes.length; i++) { const node = textNodes[i]; const start = parseFloat(node.getAttribute("start")); const duration = parseFloat(node.getAttribute("dur") || "0"); const end = start + duration; const formatTime = (time) => { const hours = Math.floor(time / 3600); const minutes = Math.floor((time % 3600) / 60); const seconds = Math.floor(time % 60); const milliseconds = Math.floor((time % 1) * 1000); return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')},${String(milliseconds).padStart(3, '0')}`; }; srtContent += `${i + 1}\n`; srtContent += `${formatTime(start)} --> ${formatTime(end)}\n`; srtContent += `${node.textContent}\n\n`; } return srtContent; } function createSVGIcon(className, isHover = false) { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); svg.setAttribute("viewBox", "0 0 576 512"); svg.classList.add(className); path.setAttribute("d", isHover ? "M64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l448 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64L64 32zm56 208l176 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-176 0c-13.3 0-24-10.7-24-24s10.7-24 24-24zm256 0l80 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-80 0c-13.3 0-24-10.7-24-24s10.7-24 24-24zM120 336l80 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-80 0c-13.3 0-24-10.7-24-24s10.7-24 24-24zm160 0l176 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-176 0c-13.3 0-24-10.7-24-24s10.7-24 24-24z" : "M64 80c-8.8 0-16 7.2-16 16l0 320c0 8.8 7.2 16 16 16l448 0c8.8 0 16-7.2 16-16l0-320c0-8.8-7.2-16-16-16L64 80zM0 96C0 60.7 28.7 32 64 32l448 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64L0 96zM120 240l176 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-176 0c-13.3 0-24-10.7-24-24s10.7-24 24-24zm256 0l80 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-80 0c-13.3 0-24-10.7-24-24s10.7-24 24-24zM120 336l80 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-80 0c-13.3 0-24-10.7-24-24s10.7-24 24-24zm160 0l176 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-176 0c-13.3 0-24-10.7-24-24s10.7-24 24-24z" ); svg.appendChild(path); return svg; } function getVideoId() { const urlParams = new URLSearchParams(window.location.search); return urlParams.get('v'); } function createDropdown(languages, videoTitle) { const dropdown = document.createElement('div'); dropdown.className = 'subtitle-dropdown'; const titleDiv = document.createElement('div'); titleDiv.className = 'subtitle-dropdown-title'; titleDiv.textContent = `Download Subtitles (${languages.length})`; dropdown.appendChild(titleDiv); languages.forEach((lang) => { const option = document.createElement('div'); option.className = 'subtitle-option'; option.dataset.url = lang.url; option.textContent = lang.label; dropdown.appendChild(option); }); return dropdown; } async function handleSubtitleDownload(e) { e.preventDefault(); const videoId = getVideoId(); if (!videoId) { console.error('Video ID not found'); return; } try { const player = document.querySelector('#movie_player'); if (!player || !player.getPlayerResponse) { console.error('Player not found or API not available'); return; } const playerResponse = player.getPlayerResponse(); const captions = playerResponse?.captions?.playerCaptionsTracklistRenderer?.captionTracks; if (!captions || captions.length === 0) { alert('No subtitles available for this video'); return; } const languages = captions.map(caption => ({ label: caption.name.simpleText, url: caption.baseUrl })); const backdrop = document.createElement('div'); backdrop.className = 'subtitle-backdrop'; document.body.appendChild(backdrop); const videoTitleElement = document.querySelector('yt-formatted-string.style-scope.ytd-watch-metadata'); const videoTitle = videoTitleElement ? videoTitleElement.textContent.trim() : 'video'; const dropdown = createDropdown(languages, videoTitle); document.body.appendChild(dropdown); const closeDropdown = (e) => { if (!dropdown.contains(e.target) && !e.target.closest('.custom-subtitle-btn')) { dropdown.remove(); backdrop.remove(); document.removeEventListener('click', closeDropdown); } }; dropdown.addEventListener('click', async (event) => { const option = event.target.closest('.subtitle-option'); if (!option) return; const url = option.dataset.url; const langLabel = option.textContent.trim(); try { option.classList.add('loading'); const response = await fetch(url); if (!response.ok) throw new Error('Network response was not ok'); const xmlContent = await response.text(); const srtContent = xmlToSrt(xmlContent); const blob = new Blob([srtContent], { type: 'text/plain' }); const downloadUrl = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = downloadUrl; link.download = `${videoTitle} - ${langLabel}.srt`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(downloadUrl); dropdown.remove(); backdrop.remove(); } catch (error) { console.error('Error downloading subtitles:', error); option.classList.remove('loading'); alert('Error downloading subtitles. Please try again.'); } }); setTimeout(() => { document.addEventListener('click', closeDropdown); }, 100); } catch (error) { console.error('Error handling subtitle download:', error); alert('Error accessing video subtitles. Please try again.'); } } function initializeStyles(computedStyle) { if (document.querySelector('#yt-subtitle-downloader-styles')) return; const style = document.createElement('style'); style.id = 'yt-subtitle-downloader-styles'; style.textContent = ` .custom-subtitle-btn { background: none; border: none; cursor: pointer; padding: 0; width: ${computedStyle.width}; height: ${computedStyle.height}; display: flex; align-items: center; justify-content: center; position: relative; } .custom-subtitle-btn svg { width: 24px; height: 24px; fill: #fff; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); opacity: 1; transition: opacity 0.2s ease-in-out; } .custom-subtitle-btn .hover-icon { opacity: 0; } .custom-subtitle-btn:hover .default-icon { opacity: 0; } .custom-subtitle-btn:hover .hover-icon { opacity: 1; } .subtitle-dropdown { position: fixed; background: rgba(28, 28, 28, 0.95); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; padding: 12px; z-index: 9999; top: 50%; left: 50%; transform: translate(-50%, -50%); min-width: 200px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); backdrop-filter: blur(10px); } .subtitle-dropdown-title { color: #fff; font-size: 14px; font-weight: 500; margin-bottom: 8px; padding: 0 8px; text-align: center; } .subtitle-option { color: #fff; padding: 8px 12px; margin: 2px 0; cursor: pointer; border-radius: 4px; transition: all 0.2s; display: flex; align-items: center; font-size: 13px; white-space: nowrap; } .subtitle-option:hover { background-color: rgba(255, 255, 255, 0.1); } .subtitle-option::before { content: "●"; margin-right: 8px; font-size: 8px; color: #aaa; } .subtitle-option.loading { opacity: 0.5; pointer-events: none; } .subtitle-backdrop { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 9998; } `; document.head.appendChild(style); } function initializeButton() { if (document.querySelector('.custom-subtitle-btn')) return; const originalButton = document.querySelector('.ytp-subtitles-button'); if (!originalButton) return; const newButton = document.createElement('button'); const computedStyle = window.getComputedStyle(originalButton); Object.assign(newButton, { className: 'ytp-button custom-subtitle-btn', title: 'Download Subtitles' }); newButton.setAttribute('aria-pressed', 'false'); initializeStyles(computedStyle); newButton.append( createSVGIcon('default-icon', false), createSVGIcon('hover-icon', true) ); newButton.addEventListener('click', (e) => { const existingDropdown = document.querySelector('.subtitle-dropdown'); existingDropdown ? existingDropdown.remove() : handleSubtitleDownload(e); }); originalButton.insertAdjacentElement('afterend', newButton); } function initializeObserver() { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.addedNodes.length) { const isVideoPage = window.location.pathname === '/watch'; if (isVideoPage && !document.querySelector('.custom-subtitle-btn')) { initializeButton(); } } }); }); function startObserving() { const playerContainer = document.getElementById('player-container'); const contentContainer = document.getElementById('content'); if (playerContainer) { observer.observe(playerContainer, { childList: true, subtree: true }); } if (contentContainer) { observer.observe(contentContainer, { childList: true, subtree: true }); } if (window.location.pathname === '/watch') { initializeButton(); } } startObserving(); if (!document.getElementById('player-container')) { const retryInterval = setInterval(() => { if (document.getElementById('player-container')) { startObserving(); clearInterval(retryInterval); } }, 1000); setTimeout(() => clearInterval(retryInterval), 10000); } const handleNavigation = () => { if (window.location.pathname === '/watch') { initializeButton(); } }; window.addEventListener('yt-navigate-finish', handleNavigation); return () => { observer.disconnect(); window.removeEventListener('yt-navigate-finish', handleNavigation); }; } addSubtitleButton(); function addSubtitleButton() { initializeObserver(); } console.log('YouTube Enhancer (Subtitle Downloader) is running'); })();