YouTube Smart Subtitle Downloader

Enhanced YouTube subtitle downloader with smart selection and improved code structure

// ==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();

})();