- // ==UserScript==
- // @name YouTube Smart Subtitle Downloader
- // @namespace http://tampermonkey.net/
- // @version 1.1
- // @description Enhanced YouTube subtitle downloader with smart selection and improved code structure
- // @author anassk
- // @match https://www.youtube.com/*
- // @grant GM_xmlhttpRequest
- // @grant GM_registerMenuCommand
- // ==/UserScript==
-
- (function() {
- 'use strict';
-
- // Core configuration
- const CONFIG = {
- MESSAGES: {
- NO_SUBTITLE: 'No Subtitles Available',
- HAVE_SUBTITLE: 'Available Subtitles',
- LOADING: 'Loading Subtitles...',
- COPY_SUCCESS: '✓ Copied!',
- ERROR: {
- COPY: 'Failed to copy to clipboard',
- FETCH: 'Failed to fetch subtitles',
- NO_VIDEO: 'No video found'
- }
- },
- FORMATS: {
- SRT: 'srt',
- TEXT: 'txt'
- },
- TIMINGS: {
- DOWNLOAD_DELAY: 500,
- },
- SELECTORS: {
- VIDEO_ELEMENTS: [
- 'ytd-playlist-panel-video-renderer',
- 'ytd-playlist-video-renderer',
- 'yt-lockup-view-model',
- 'ytd-rich-item-renderer',
- 'ytd-video-renderer',
- 'ytd-compact-video-renderer',
- 'ytd-grid-video-renderer'
- ].join(','),
- TITLE_SELECTORS: [
- '#video-title',
- 'a#video-title',
- 'span#video-title',
- '[title]'
- ]
- },
- STYLES: {
- CHECKBOX_WRAPPER: `
- position: absolute;
- left: 5px;
- top: 0;
- bottom: 0;
- width: 20px;
- display: flex;
- align-items: center;
- z-index: 1;
- `,
- CHECKBOX: `
- width: 16px;
- height: 16px;
- cursor: pointer;
- margin: 0;
- `,
- DIALOG: `
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.7);
- display: flex;
- justify-content: center;
- align-items: center;
- z-index: 9999;
- `,
- DIALOG_CONTENT: `
- background: white;
- padding: 20px;
- border-radius: 8px;
- min-width: 300px;
- max-width: 600px;
- max-height: 80vh;
- overflow-y: auto;
- color: black;
- `
- }
- };
-
- // Utility functions
- const Utils = {
- createError: (message, code, originalError = null) => {
- const error = new Error(message);
- error.code = code;
- error.originalError = originalError;
- return error;
- },
-
- safeJSONParse: (str, fallback = null) => {
- try {
- return JSON.parse(str);
- } catch (e) {
- console.error('JSON Parse Error:', e);
- return fallback;
- }
- },
-
- sanitizeFileName: (name) => {
- return name.replace(/[<>:"/\\|?*\x00-\x1F]/g, '_').substring(0, 100);
- },
-
- delay: (ms) => new Promise(resolve => setTimeout(resolve, ms))
- };
-
- // Subtitle Service - New centralized service for subtitle operations
- class SubtitleService {
- static async fetchSubtitleTracks(videoId) {
- try {
- const response = await fetch(`https://www.youtube.com/watch?v=${videoId}`);
- const html = await response.text();
-
- const playerDataMatch = html.match(/ytInitialPlayerResponse\s*=\s*({.+?});/);
- if (!playerDataMatch) return null;
-
- const playerData = Utils.safeJSONParse(playerDataMatch[1]);
- const captionTracks = playerData?.captions?.playerCaptionsTracklistRenderer?.captionTracks;
-
- if (!captionTracks?.length) return null;
-
- return captionTracks.map(track => ({
- languageCode: track.languageCode,
- languageName: track.name.simpleText,
- baseUrl: track.baseUrl
- }));
- } catch (error) {
- throw Utils.createError('Failed to fetch subtitles', 'SUBTITLE_FETCH_ERROR', error);
- }
- }
-
- static async getSubtitleContent(track, format = CONFIG.FORMATS.SRT) {
- try {
- const response = await fetch(track.baseUrl);
- const xml = await response.text();
- return format === CONFIG.FORMATS.SRT ?
- this.convertToSRT(xml) :
- this.convertToText(xml);
- } catch (error) {
- throw Utils.createError('Failed to fetch subtitle content', 'CONTENT_FETCH_ERROR', error);
- }
- }
-
- static convertToSRT(xml) {
- try {
- const parser = new DOMParser();
- const doc = parser.parseFromString(xml, "text/xml");
- const textNodes = doc.getElementsByTagName('text');
- let srt = '';
-
- Array.from(textNodes).forEach((node, index) => {
- const start = parseFloat(node.getAttribute('start'));
- const duration = parseFloat(node.getAttribute('dur') || '0');
- const end = start + duration;
-
- srt += `${index + 1}\n`;
- srt += `${this.formatTime(start)} --> ${this.formatTime(end)}\n`;
- srt += `${node.textContent}\n\n`;
- });
-
- return srt;
- } catch (error) {
- throw Utils.createError('Failed to convert to SRT', 'SRT_CONVERSION_ERROR', error);
- }
- }
-
- static convertToText(xml) {
- try {
- const parser = new DOMParser();
- const doc = parser.parseFromString(xml, "text/xml");
- const textNodes = doc.getElementsByTagName('text');
-
- return Array.from(textNodes)
- .map(node => node.textContent.trim())
- .filter(text => text)
- .join('\n');
- } catch (error) {
- throw Utils.createError('Failed to convert to text', 'TEXT_CONVERSION_ERROR', error);
- }
- }
-
- static formatTime(seconds) {
- const pad = num => String(num).padStart(2, '0');
- const ms = String(Math.floor((seconds % 1) * 1000)).padStart(3, '0');
-
- seconds = Math.floor(seconds);
- const hours = Math.floor(seconds / 3600);
- const minutes = Math.floor((seconds % 3600) / 60);
- const secs = seconds % 60;
-
- return `${pad(hours)}:${pad(minutes)}:${pad(secs)},${ms}`;
- }
-
- static async downloadSubtitles(tracks, format) {
- const loading = UIComponents.showLoading('Downloading subtitles...');
-
- try {
- for (const track of tracks) {
- const content = await this.getSubtitleContent(track, format);
- const filename = `${Utils.sanitizeFileName(track.videoTitle)}_${track.languageCode}.${format}`;
-
- const blob = new Blob(['\ufeff' + content], { type: 'text/plain;charset=utf-8' });
- const url = URL.createObjectURL(blob);
- const link = document.createElement('a');
-
- link.href = url;
- link.download = filename;
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- URL.revokeObjectURL(url);
-
- await Utils.delay(CONFIG.TIMINGS.DOWNLOAD_DELAY);
- }
- } catch (error) {
- throw Utils.createError('Failed to download subtitles', 'DOWNLOAD_ERROR', error);
- } finally {
- loading.remove();
- }
- }
-
- static async copySubtitles(tracks, format, videoTitle = '') {
- const loading = UIComponents.showLoading('Copying subtitles...');
-
- try {
- let content = '';
- for (const track of tracks) {
- const subtitleContent = await this.getSubtitleContent(track, format);
- const title = videoTitle ? `${videoTitle} - ` : '';
- content += `=== ${title}${track.languageName} ===\n${subtitleContent}\n\n`;
- }
-
- await navigator.clipboard.writeText(content);
- UIComponents.showToast(CONFIG.MESSAGES.COPY_SUCCESS);
- } catch (error) {
- throw Utils.createError('Failed to copy subtitles', 'COPY_ERROR', error);
- } finally {
- loading.remove();
- }
- }
- }
-
- // UI Components
- class UIComponents {
- static showDialog(title, content, onClose) {
- const dialog = document.createElement('div');
- dialog.style.cssText = CONFIG.STYLES.DIALOG;
-
- const dialogContent = document.createElement('div');
- dialogContent.style.cssText = CONFIG.STYLES.DIALOG_CONTENT;
-
- const header = document.createElement('div');
- header.style.cssText = `
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 15px;
- `;
-
- const titleElem = document.createElement('h2');
- titleElem.style.margin = '0';
- titleElem.textContent = title;
-
- const closeButton = document.createElement('button');
- closeButton.textContent = '×';
- closeButton.style.cssText = `
- background: none;
- border: none;
- font-size: 24px;
- cursor: pointer;
- padding: 0 5px;
- `;
- closeButton.onclick = () => {
- dialog.remove();
- if (onClose) onClose();
- };
-
- header.appendChild(titleElem);
- header.appendChild(closeButton);
- dialogContent.appendChild(header);
- dialogContent.appendChild(content);
- dialog.appendChild(dialogContent);
-
- document.body.appendChild(dialog);
- return dialog;
- }
-
- static showToast(message, duration = 3000) {
- const toast = document.createElement('div');
- toast.style.cssText = `
- position: fixed;
- bottom: 20px;
- left: 50%;
- transform: translateX(-50%);
- background: rgba(0, 0, 0, 0.8);
- color: white;
- padding: 10px 20px;
- border-radius: 4px;
- z-index: 10001;
- `;
- toast.textContent = message;
- document.body.appendChild(toast);
- setTimeout(() => toast.remove(), duration);
- }
-
- static showLoading(message = CONFIG.MESSAGES.LOADING) {
- const overlay = document.createElement('div');
- overlay.style.cssText = CONFIG.STYLES.DIALOG;
-
- const content = document.createElement('div');
- content.style.textAlign = 'center';
- content.style.color = 'white';
-
- const spinner = document.createElement('div');
- spinner.style.cssText = `
- width: 40px;
- height: 40px;
- border: 4px solid #f3f3f3;
- border-top: 4px solid #3498db;
- border-radius: 50%;
- margin: 0 auto 10px;
- animation: spin 1s linear infinite;
- `;
-
- if (!document.getElementById('spinner-style')) {
- const style = document.createElement('style');
- style.id = 'spinner-style';
- style.textContent = '@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }';
- document.head.appendChild(style);
- }
-
- content.appendChild(spinner);
- content.appendChild(document.createTextNode(message));
- overlay.appendChild(content);
- document.body.appendChild(overlay);
-
- return overlay;
- }
-
- static createSubtitleDialog(tracks, format = CONFIG.FORMATS.SRT, onDownload, onCopy) {
- const content = document.createElement('div');
-
- // Format selector
- const formatDiv = document.createElement('div');
- formatDiv.innerHTML = `
- <div style="margin-bottom: 15px;">
- <label style="margin-right: 10px;">
- <input type="radio" name="format" value="srt" ${format === 'srt' ? 'checked' : ''}> SRT
- </label>
- <label>
- <input type="radio" name="format" value="txt" ${format === 'txt' ? 'checked' : ''}> Plain Text
- </label>
- </div>
- `;
- content.appendChild(formatDiv);
- //batch select
- if (tracks.length > 0) {
- const selectAllDiv = document.createElement('div');
- selectAllDiv.style.marginBottom = '15px';
- const selectAllBtn = document.createElement('button');
- selectAllBtn.textContent = 'Select All Subtitles';
- selectAllBtn.style.cssText = 'background: #065fd4; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;';
- selectAllBtn.onclick = () => {
- const checkboxes = content.querySelectorAll('input[type="checkbox"][data-lang]');
- checkboxes.forEach(checkbox => {
- checkbox.checked = true;
- });
- };
- selectAllDiv.appendChild(selectAllBtn);
- content.appendChild(selectAllDiv);
- }
-
- // Tracks list
- if (tracks.length > 0) {
- tracks.forEach(track => {
- const trackDiv = document.createElement('div');
- trackDiv.style.margin = '5px 0';
- trackDiv.innerHTML = `
- <label>
- <input type="checkbox" data-lang="${track.languageCode}">
- ${track.languageName}
- </label>
- `;
- content.appendChild(trackDiv);
- });
- } else {
- const noSubs = document.createElement('p');
- noSubs.style.color = '#c00';
- noSubs.textContent = CONFIG.MESSAGES.NO_SUBTITLE;
- content.appendChild(noSubs);
- }
-
- // Action buttons
- const actions = document.createElement('div');
- actions.style.cssText = 'display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px;';
-
- if (tracks.length > 0) {
- const downloadBtn = document.createElement('button');
- downloadBtn.textContent = 'Download Selected';
- downloadBtn.onclick = onDownload;
- actions.appendChild(downloadBtn);
-
- const copyBtn = document.createElement('button');
- copyBtn.textContent = 'Copy Selected';
- copyBtn.onclick = onCopy;
- actions.appendChild(copyBtn);
- }
-
- content.appendChild(actions);
- return content;
- }
- }
-
- // Video Selector with improved structure
- class VideoSelector {
- constructor() {
- this.selectedVideos = new Map();
- this.selectionActive = false;
- }
-
- toggleVideoSelection() {
- if (!this.selectionActive) {
- this.activateSelection();
- } else {
- this.deactivateSelection();
- }
- }
-
- activateSelection() {
- this.selectionActive = true;
- this.selectedVideos.clear();
- this.addSpacingStyle();
-
- const videos = document.querySelectorAll(CONFIG.SELECTORS.VIDEO_ELEMENTS);
- videos.forEach(video => {
- // Skip shorts
- if (video.closest('ytd-reel-shelf-renderer') ||
- video.closest('ytd-shorts') ||
- video.closest('ytm-shorts-lockup-view-model')) {
- return;
- }
-
- video.classList.add('yt-sub-video-padding');
- this.addCheckbox(video);
- });
-
- this.addSelectionUI();
- }
-
- addSpacingStyle() {
- if (!document.getElementById('yt-sub-styles')) {
- const style = document.createElement('style');
- style.id = 'yt-sub-styles';
- style.textContent = `
- .yt-sub-video-padding {
- padding-left: 30px !important;
- position: relative !important;
- }
- `;
- document.head.appendChild(style);
- }
- }
-
- addCheckbox(videoElement) {
- if (videoElement.querySelector('.yt-sub-checkbox-wrapper')) return;
-
- const wrapper = document.createElement('div');
- wrapper.className = 'yt-sub-checkbox-wrapper';
- wrapper.style.cssText = CONFIG.STYLES.CHECKBOX_WRAPPER;
-
- const checkbox = document.createElement('input');
- checkbox.type = 'checkbox';
- checkbox.className = 'yt-sub-checkbox';
- checkbox.style.cssText = CONFIG.STYLES.CHECKBOX;
-
- wrapper.appendChild(checkbox);
- videoElement.insertBefore(wrapper, videoElement.firstChild);
-
- checkbox.addEventListener('change', (e) => {
- const videoId = this.extractVideoId(videoElement);
- const title = this.extractTitle(videoElement);
-
- if (videoId && title) {
- if (e.target.checked) {
- this.selectedVideos.set(videoId, { title });
- } else {
- this.selectedVideos.delete(videoId);
- }
- this.updateSelectionCount();
- }
- });
- }
-
- extractVideoId(video) {
- const thumbnail = video.querySelector('a#thumbnail[href*="/watch?v="]');
- if (thumbnail?.href) {
- const url = new URL(thumbnail.href);
- return url.searchParams.get('v');
- }
-
- const links = video.querySelectorAll('a[href*="/watch?v="]');
- for (const link of links) {
- try {
- const url = new URL(link.href);
- const videoId = url.searchParams.get('v');
- if (videoId) return videoId;
- } catch {
- continue;
- }
- }
- return null;
- }
-
- extractTitle(video) {
- for (const selector of CONFIG.SELECTORS.TITLE_SELECTORS) {
- const element = video.querySelector(selector);
- if (element) {
- const title = element.textContent?.trim() ||
- element.getAttribute('title')?.trim();
- if (title) return title;
- }
- }
-
- const videoId = this.extractVideoId(video);
- return videoId ? `Video_${videoId}` : 'Untitled Video';
- }
-
- addSelectionUI() {
- const ui = document.createElement('div');
- ui.id = 'yt-sub-selection-ui';
- ui.style.cssText = `
- position: fixed;
- top: 10px;
- right: 10px;
- background: rgba(0, 0, 0, 0.8);
- color: white;
- padding: 10px 20px;
- border-radius: 4px;
- z-index: 9999;
- font-size: 14px;
- `;
-
- ui.innerHTML = `
- <div style="margin-bottom: 10px;">
- Selected: <span id="yt-sub-count">0</span> videos
- </div>
- <div style="display: flex; gap: 10px; margin-bottom: 10px;">
- <button id="yt-sub-select-all" style="background: #065fd4; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;">Select All</button>
- <button id="yt-sub-select-x" style="background: #065fd4; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;">Select First X</button>
- </div>
- <div style="display: flex; gap: 10px;">
- <button id="yt-sub-download" style="background: #065fd4; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;">Download</button>
- <button id="yt-sub-cancel" style="background: #909090; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;">Cancel</button>
- </div>
- `;
-
- document.body.appendChild(ui);
-
- document.getElementById('yt-sub-download').onclick = () => {
- if (this.selectedVideos.size > 0) {
- this.processSelectedVideos();
- } else {
- UIComponents.showToast('Please select at least one video');
- }
- };
-
- document.getElementById('yt-sub-cancel').onclick = () => {
- this.deactivateSelection();
- };
-
-
- document.getElementById('yt-sub-select-all').onclick = () => {
- const checkboxes = document.querySelectorAll('.yt-sub-checkbox');
- checkboxes.forEach(checkbox => {
- if (!checkbox.checked) {
- checkbox.click();
- }
- });
- };
-
- document.getElementById('yt-sub-select-x').onclick = () => {
- const input = prompt('How many videos would you like to select?');
- const number = parseInt(input);
-
- if (!isNaN(number) && number > 0) {
- const checkboxes = document.querySelectorAll('.yt-sub-checkbox');
- checkboxes.forEach((checkbox, index) => {
- if (index < number) {
- if (!checkbox.checked) {
- checkbox.click();
- }
- } else if (checkbox.checked) {
- checkbox.click();
- }
- });
- } else if (input !== null) {
- UIComponents.showToast('Please enter a valid number');
- }
- };
-
-
- }
-
- updateSelectionCount() {
- const countElement = document.getElementById('yt-sub-count');
- if (countElement) {
- countElement.textContent = this.selectedVideos.size.toString();
- }
- }
-
- deactivateSelection() {
- this.selectionActive = false;
- this.selectedVideos.clear();
-
- document.querySelectorAll('.yt-sub-video-padding').forEach(video => {
- video.classList.remove('yt-sub-video-padding');
- });
-
- document.querySelectorAll('.yt-sub-checkbox-wrapper').forEach(cb => cb.remove());
- document.getElementById('yt-sub-selection-ui')?.remove();
- document.getElementById('yt-sub-styles')?.remove();
- }
-
- async processSelectedVideos() {
- if (this.selectedVideos.size === 0) {
- UIComponents.showToast('Please select at least one video');
- return;
- }
-
- const loading = UIComponents.showLoading('Fetching subtitles...');
-
- try {
- const videos = Array.from(this.selectedVideos.entries());
- const results = await Promise.all(
- videos.map(async ([videoId, data]) => {
- try {
- const tracks = await SubtitleService.fetchSubtitleTracks(videoId);
- return { ...data, videoId, subtitles: tracks || [] };
- } catch (error) {
- console.error(`Failed to fetch subtitles for ${videoId}:`, error);
- return { ...data, videoId, subtitles: [] };
- }
- })
- );
-
- this.showSubtitleDialog(results);
- } catch (error) {
- UIComponents.showToast(CONFIG.MESSAGES.ERROR.FETCH);
- console.error('Failed to process videos:', error);
- } finally {
- loading.remove();
- }
- }
-
- showSubtitleDialog(videos) {
- const getSelectedTracks = () => {
- return Array.from(document.querySelectorAll('input[type="checkbox"]:checked'))
- .map(cb => {
- const videoId = cb.dataset.videoId;
- const langCode = cb.dataset.lang;
- const video = videos.find(v => v.videoId === videoId);
- const track = video?.subtitles.find(t => t.languageCode === langCode);
- return track ? { ...track, videoTitle: video.title } : null;
- })
- .filter(Boolean);
- };
-
- const content = document.createElement('div');
-
- // Format selector
- const formatDiv = document.createElement('div');
- formatDiv.innerHTML = `
- <div style="margin-bottom: 15px;">
- <label style="margin-right: 10px;">
- <input type="radio" name="format" value="srt" checked> SRT
- </label>
- <label>
- <input type="radio" name="format" value="txt"> Plain Text
- </label>
- </div>
- `;
- content.appendChild(formatDiv);
- //batch Select
- const selectAllDiv = document.createElement('div');
- selectAllDiv.style.marginBottom = '15px';
- const selectAllBtn = document.createElement('button');
- selectAllBtn.textContent = 'Select All Subtitles';
- selectAllBtn.style.cssText = 'background: #065fd4; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;';
- selectAllBtn.onclick = () => {
- const checkboxes = content.querySelectorAll('input[type="checkbox"][data-video-id]');
- checkboxes.forEach(checkbox => {
- checkbox.checked = true;
- });
- };
- selectAllDiv.appendChild(selectAllBtn);
- content.appendChild(selectAllDiv);
-
-
-
- // Videos and their subtitles
- videos.forEach(video => {
- const videoDiv = document.createElement('div');
- videoDiv.style.cssText = 'margin-bottom: 20px; padding: 10px; border: 1px solid #ddd; border-radius: 4px;';
-
- const title = document.createElement('h3');
- title.style.margin = '0 0 10px 0';
- title.textContent = video.title;
- videoDiv.appendChild(title);
-
- if (video.subtitles.length > 0) {
- video.subtitles.forEach(track => {
- const trackDiv = document.createElement('div');
- trackDiv.style.margin = '5px 0';
- trackDiv.innerHTML = `
- <label>
- <input type="checkbox"
- data-video-id="${video.videoId}"
- data-lang="${track.languageCode}">
- ${track.languageName}
- </label>
- `;
- videoDiv.appendChild(trackDiv);
- });
- } else {
- const noSubs = document.createElement('p');
- noSubs.style.color = '#c00';
- noSubs.textContent = CONFIG.MESSAGES.NO_SUBTITLE;
- videoDiv.appendChild(noSubs);
- }
-
- content.appendChild(videoDiv);
- });
-
- // Action buttons
- const actions = document.createElement('div');
- actions.style.cssText = 'display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px;';
-
- const downloadBtn = document.createElement('button');
- downloadBtn.textContent = 'Download Selected';
- downloadBtn.onclick = async () => {
- const tracks = getSelectedTracks();
- const format = document.querySelector('input[name="format"]:checked').value;
-
- if (tracks.length === 0) {
- UIComponents.showToast('Please select at least one subtitle');
- return;
- }
-
- try {
- const selectedTracks = tracks.map(track => ({
- ...track,
- baseUrl: track.baseUrl
- }));
- await SubtitleService.downloadSubtitles(selectedTracks, format);
- UIComponents.showToast('Download complete!');
- } catch (error) {
- UIComponents.showToast(CONFIG.MESSAGES.ERROR.FETCH);
- console.error('Download error:', error);
- }
- };
- actions.appendChild(downloadBtn);
-
- const copyBtn = document.createElement('button');
- copyBtn.textContent = 'Copy Selected';
- copyBtn.onclick = async () => {
- const tracks = getSelectedTracks();
- const format = document.querySelector('input[name="format"]:checked').value;
-
- if (tracks.length === 0) {
- UIComponents.showToast('Please select at least one subtitle');
- return;
- }
-
- try {
- await SubtitleService.copySubtitles(tracks, format);
- } catch (error) {
- UIComponents.showToast(CONFIG.MESSAGES.ERROR.COPY);
- console.error('Copy error:', error);
- }
- };
- actions.appendChild(copyBtn);
-
- content.appendChild(actions);
-
- UIComponents.showDialog('Select Subtitles to Download', content, () => {
- this.deactivateSelection();
- });
- }
- }
-
- // Single Video Downloader
- class SingleVideoDownloader {
- async downloadCurrentVideo() {
- const videoId = this.getCurrentVideoId();
- if (!videoId) {
- UIComponents.showToast(CONFIG.MESSAGES.ERROR.NO_VIDEO);
- return;
- }
-
- const loading = UIComponents.showLoading();
-
- try {
- const tracks = await SubtitleService.fetchSubtitleTracks(videoId);
- if (!tracks?.length) {
- UIComponents.showToast(CONFIG.MESSAGES.NO_SUBTITLE);
- return;
- }
-
- const videoTitle = document.title.split(' - YouTube')[0] || `Video_${videoId}`;
- this.showSubtitleDialog(tracks, videoTitle);
- } catch (error) {
- UIComponents.showToast(CONFIG.MESSAGES.ERROR.FETCH);
- console.error('Failed to fetch subtitles:', error);
- } finally {
- loading.remove();
- }
- }
-
- getCurrentVideoId() {
- const urlParams = new URLSearchParams(window.location.search);
- return urlParams.get('v');
- }
-
- showSubtitleDialog(tracks, videoTitle) {
- const getSelectedTracks = () => {
- return Array.from(document.querySelectorAll('input[type="checkbox"]:checked'))
- .map(cb => {
- const track = tracks.find(t => t.languageCode === cb.dataset.lang);
- return track ? { ...track, videoTitle } : null;
- })
- .filter(Boolean);
- };
-
- const content = UIComponents.createSubtitleDialog(
- tracks,
- CONFIG.FORMATS.SRT,
- async () => {
- const selectedTracks = getSelectedTracks();
- const format = document.querySelector('input[name="format"]:checked').value;
-
- if (selectedTracks.length === 0) {
- UIComponents.showToast('Please select at least one subtitle');
- return;
- }
-
- try {
- const tracksWithTitle = selectedTracks.map(track => ({
- ...track,
- videoTitle
- }));
- await SubtitleService.downloadSubtitles(tracksWithTitle, format);
- UIComponents.showToast('Download complete!');
- } catch (error) {
- UIComponents.showToast(CONFIG.MESSAGES.ERROR.FETCH);
- console.error('Download error:', error);
- }
- },
- async () => {
- const selectedTracks = getSelectedTracks();
- const format = document.querySelector('input[name="format"]:checked').value;
-
- if (selectedTracks.length === 0) {
- UIComponents.showToast('Please select at least one subtitle');
- return;
- }
-
- try {
- await SubtitleService.copySubtitles(selectedTracks, format, videoTitle);
- } catch (error) {
- UIComponents.showToast(CONFIG.MESSAGES.ERROR.COPY);
- console.error('Copy error:', error);
- }
- }
- );
-
- UIComponents.showDialog('Select Subtitles to Download', content);
- }
- }
-
- // Main manager class
- class YouTubeSubtitleManager {
- constructor() {
- this.singleMode = new SingleVideoDownloader();
- this.bulkMode = new VideoSelector();
- this.registerCommands();
- this.setupNavigationHandler();
- }
-
- registerCommands() {
- GM_registerMenuCommand('Download Current Video Subtitles',
- () => this.singleMode.downloadCurrentVideo());
- GM_registerMenuCommand('Select Videos for Subtitles',
- () => this.bulkMode.toggleVideoSelection());
- }
-
- setupNavigationHandler() {
- document.addEventListener('yt-navigate-finish', () => {
- if (this.bulkMode.selectionActive) {
- this.bulkMode.deactivateSelection();
- this.bulkMode.activateSelection();
- }
- });
- }
- }
-
- // Initialize the manager
- new YouTubeSubtitleManager();
-
- })();