您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Allows you to download available subtitles for YouTube videos in various languages directly from the video page.
当前为
// ==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.0 // @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'; // Utility function to convert XML subtitles to SRT format function xmlToSrt(xmlText) { const textElements = xmlText.match(/<text[^>]*>(.*?)<\/text>/g) || []; let srtContent = ''; let counter = 1; textElements.forEach((element) => { try { const startMatch = element.match(/start="([^"]+)"/); const durMatch = element.match(/dur="([^"]+)"/); const textMatch = element.match(/<text[^>]*>(.*?)<\/text>/); if (startMatch && textMatch) { const start = parseFloat(startMatch[1]); const duration = durMatch ? parseFloat(durMatch[1]) : 0; const end = start + duration; const text = textMatch[1] .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, "'") .replace(/\n/g, ' ') .trim(); if (text) { srtContent += `${counter}\n`; srtContent += `${formatTime(start)} --> ${formatTime(end)}\n`; srtContent += `${text}\n\n`; counter++; } } } catch (error) { console.error('Error parsing text element:', error); } }); return srtContent; } // Helper function to format time for SRT function 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')}`; } // Function to sanitize filename function sanitizeFilename(filename) { return filename .replace(/[<>:"/\\|?*\x00-\x1F]/g, '') .replace(/\s+/g, ' ') .trim(); } // Create SVG icon for the button 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; } // Create styles for the UI function createStyles(computedStyle) { 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%); } .custom-subtitle-btn .hover-icon { display: none; } .custom-subtitle-btn:hover .default-icon { display: none; } .custom-subtitle-btn:hover .hover-icon { display: block; } .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; } `; return style; } // Create dropdown menu for subtitle languages function createDropdown(languages) { const dropdown = document.createElement('div'); dropdown.className = 'subtitle-dropdown'; const title = document.createElement('div'); title.className = 'subtitle-dropdown-title'; title.textContent = 'Download Subtitles'; dropdown.appendChild(title); 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; } // Get video title with fallback options function getVideoTitle(videoId) { const titleElement = document.querySelector('yt-formatted-string.style-scope.ytd-watch-metadata'); if (titleElement) return titleElement.textContent.trim(); const fallbackSelectors = [ 'h1.title.style-scope.ytd-video-primary-info-renderer', 'h1.watch-title', '.ytd-watch-metadata #title h1' ]; for (const selector of fallbackSelectors) { const element = document.querySelector(selector); if (element) return element.textContent.trim(); } return videoId ? `video_${videoId}` : 'untitled_video'; } // Get video ID from URL function getVideoId() { const urlParams = new URLSearchParams(window.location.search); return urlParams.get('v'); } // Handle subtitle download process async function handleSubtitleDownload(e) { e.preventDefault(); const videoId = getVideoId(); if (!videoId) { alert('Could not detect video ID. Please try refreshing the page.'); return; } try { const player = document.querySelector('#movie_player'); let playerResponse; try { playerResponse = player.getPlayerResponse(); } catch (error) { playerResponse = window.ytInitialPlayerResponse; } if (!playerResponse) { alert('Could not access video data. Please try refreshing the page.'); return; } const captions = playerResponse?.captions?.playerCaptionsTracklistRenderer?.captionTracks || playerResponse?.captions?.captionTracks; if (!captions || captions.length === 0) { alert('No subtitles available for this video'); return; } const languages = captions.map(caption => ({ label: caption.name?.simpleText || caption.name?.runs?.[0]?.text || caption.languageCode || 'Unknown Language', url: caption.baseUrl || caption.url })); const backdrop = document.createElement('div'); backdrop.className = 'subtitle-backdrop'; document.body.appendChild(backdrop); const videoTitle = getVideoTitle(videoId); const dropdown = createDropdown(languages); 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); } }; let downloadInProgress = false; dropdown.addEventListener('click', async (event) => { const option = event.target.closest('.subtitle-option'); if (!option || downloadInProgress) return; downloadInProgress = true; option.classList.add('loading'); const url = option.dataset.url; const langLabel = option.textContent.trim(); try { const response = await fetch(url); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const xmlContent = await response.text(); const srtContent = xmlToSrt(xmlContent); if (!srtContent) { throw new Error('Failed to convert subtitles'); } const blob = new Blob([srtContent], { type: 'text/plain' }); const downloadUrl = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = downloadUrl; const fileName = sanitizeFilename(`${videoTitle} - ${langLabel}.srt`); link.download = fileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(downloadUrl); dropdown.remove(); backdrop.remove(); } catch (error) { console.error('Download error:', error); alert(`Error downloading subtitles: ${error.message}`); option.classList.remove('loading'); } finally { downloadInProgress = false; } }); setTimeout(() => { document.addEventListener('click', closeDropdown); }, 100); } catch (error) { console.error('Error in handleSubtitleDownload:', error); alert(`Error accessing video subtitles: ${error.message}`); } } // Initialize the download button 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'); if (!document.querySelector('#yt-subtitle-downloader-styles')) { const style = createStyles(computedStyle); document.head.appendChild(style); } newButton.append( createSVGIcon('default-icon', false), createSVGIcon('hover-icon', true) ); newButton.addEventListener('click', (e) => { const existingDropdown = document.querySelector('.subtitle-dropdown'); if (existingDropdown) { existingDropdown.remove(); } else { handleSubtitleDownload(e); } }); originalButton.insertAdjacentElement('afterend', newButton); } // Initialize observer to watch for YouTube navigation 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(); } } }); }); let observerActive = false; let retryInterval = null; function startObserving() { if (observerActive) return; const playerContainer = document.getElementById('player-container'); const contentContainer = document.getElementById('content'); if (playerContainer) { observer.observe(playerContainer, { childList: true, subtree: true }); observerActive = true; } if (contentContainer) { observer.observe(contentContainer, { childList: true, subtree: true }); observerActive = true; } if (window.location.pathname === '/watch') { initializeButton(); } } startObserving(); // Retry mechanism for slow-loading pages if (!document.getElementById('player-container')) { retryInterval = setInterval(() => { if (document.getElementById('player-container')) { startObserving(); clearInterval(retryInterval); } }, 1000); // Cleanup after 10 seconds to prevent infinite retries setTimeout(() => { if (retryInterval) { clearInterval(retryInterval); retryInterval = null; } }, 10000); } // Handle YouTube spa navigation const handleNavigation = debounce(() => { if (window.location.pathname === '/watch') { initializeButton(); } }, 250); window.addEventListener('yt-navigate-finish', handleNavigation); // Cleanup function return () => { observer.disconnect(); window.removeEventListener('yt-navigate-finish', handleNavigation); if (retryInterval) { clearInterval(retryInterval); } observerActive = false; }; } // Debounce utility function function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // Start the script initializeObserver(); console.log('YouTube Enhancer (Subtitle Downloader) is running'); })();