YouTube Advanced Downloader

Advanced YouTube video downloader with quality selection and progress tracking

// ==UserScript==
// @name         YouTube Advanced Downloader
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Advanced YouTube video downloader with quality selection and progress tracking
// @author       Anassk
// @match        https://www.youtube.com/*
// @match        https://www.youtube.com/watch?v=*
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_notification
// @connect      *
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Configuration with storage
    let API_KEY = GM_getValue('API_KEY', 'Your API Key');
    let API_BASE = GM_getValue('API_BASE', 'Your API Base URL');

    // Define qualities
    const QUALITIES = [
        { label: 'Audio Only (M4A)', value: 'audio' },
        { label: '144p', value: '144p' },
        { label: '240p', value: '240p' },
        { label: '360p', value: '360p' },
        { label: '480p', value: '480p' },
        { label: '720p', value: '720p' },
        { label: '1080p', value: '1080p' },
        { label: 'Highest Quality', value: 'highest' }
    ];

    // Configuration UI
    function showConfig() {
        // Create dialog styles if not exists
        let style = document.getElementById('yt-dl-config-style');
        if (!style) {
            style = document.createElement('style');
            style.id = 'yt-dl-config-style';
            style.textContent = `
                .yt-dl-config-dialog {
                    position: fixed;
                    top: 50%;
                    left: 50%;
                    transform: translate(-50%, -50%);
                    background: #1a1a1a;
                    color: white;
                    padding: 20px;
                    border-radius: 8px;
                    z-index: 10000;
                    width: 400px;
                    box-shadow: 0 4px 12px rgba(0,0,0,0.15);
                    border: 1px solid #333;
                }
                .yt-dl-config-dialog h2 {
                    margin: 0 0 15px 0;
                    font-size: 18px;
                    color: #fff;
                }
                .yt-dl-config-dialog .input-group {
                    margin-bottom: 15px;
                }
                .yt-dl-config-dialog label {
                    display: block;
                    margin-bottom: 5px;
                    color: #aaa;
                }
                .yt-dl-config-dialog input {
                    width: 100%;
                    padding: 8px;
                    background: #333;
                    color: white;
                    border: 1px solid #444;
                    border-radius: 4px;
                    margin-bottom: 10px;
                }
                .yt-dl-config-dialog .buttons {
                    display: flex;
                    justify-content: flex-end;
                    gap: 10px;
                }
                .yt-dl-config-dialog button {
                    padding: 8px 16px;
                    border: none;
                    border-radius: 4px;
                    cursor: pointer;
                }
                .yt-dl-config-dialog .save-btn {
                    background: #2196F3;
                    color: white;
                }
                .yt-dl-config-dialog .cancel-btn {
                    background: #666;
                    color: white;
                }
                .yt-dl-config-overlay {
                    position: fixed;
                    top: 0;
                    left: 0;
                    right: 0;
                    bottom: 0;
                    background: rgba(0,0,0,0.7);
                    z-index: 9999;
                }
            `;
            document.head.appendChild(style);
        }

        // Create dialog
        const overlay = document.createElement('div');
        overlay.className = 'yt-dl-config-overlay';
        
        const dialog = document.createElement('div');
        dialog.className = 'yt-dl-config-dialog';
        dialog.innerHTML = `
            <h2>⚙️ Configure YouTube Downloader</h2>
            <div class="input-group">
                <label for="api-key">API Key:</label>
                <input type="password" id="api-key" value="${API_KEY}" placeholder="Enter your API key">
                <label for="api-base">API Base URL:</label>
                <input type="text" id="api-base" value="${API_BASE}" placeholder="Enter your API base URL">
            </div>
            <div class="buttons">
                <button class="cancel-btn">Cancel</button>
                <button class="save-btn">Save</button>
            </div>
        `;

        overlay.appendChild(dialog);
        document.body.appendChild(overlay);

        // Handle buttons
        dialog.querySelector('.save-btn').addEventListener('click', () => {
            const newApiKey = dialog.querySelector('#api-key').value.trim();
            const newApiBase = dialog.querySelector('#api-base').value.trim();

            if (newApiKey && newApiBase) {
                GM_setValue('API_KEY', newApiKey);
                GM_setValue('API_BASE', newApiBase);
                API_KEY = newApiKey;
                API_BASE = newApiBase;
                showNotification('Configuration saved successfully! ✅');
                document.body.removeChild(overlay);
            } else {
                showNotification('Please fill in all fields! ⚠️');
            }
        });

        dialog.querySelector('.cancel-btn').addEventListener('click', () => {
            document.body.removeChild(overlay);
        });
    }

    // Notification helper
    function showNotification(text, timeout = 3000) {
        GM_notification({
            text: text,
            title: 'YouTube Downloader',
            timeout: timeout
        });
    }

    // Configuration check
    function checkConfig() {
        if (API_KEY === 'Your API Key' || API_BASE === 'Your API Base URL') {
            showNotification('Please configure the downloader first! Click the Tampermonkey icon and select "⚙️ Configure"');
            setTimeout(showConfig, 1000);
            return false;
        }
        return true;
    }

    // Create and show download dialog
    function showDialog() {
        if (!checkConfig()) return;

        // Create dialog styles
        const style = document.createElement('style');
        style.textContent = `
            .yt-dl-dialog {
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                background: #1a1a1a;
                color: white;
                padding: 20px;
                border-radius: 8px;
                z-index: 10000;
                min-width: 300px;
                box-shadow: 0 4px 12px rgba(0,0,0,0.15);
                border: 1px solid #333;
            }
            .yt-dl-dialog h2 {
                margin: 0 0 15px 0;
                font-size: 18px;
                display: flex;
                align-items: center;
                gap: 8px;
            }
            .yt-dl-dialog select {
                width: 100%;
                padding: 8px;
                margin-bottom: 15px;
                background: #333;
                color: white;
                border: 1px solid #444;
                border-radius: 4px;
            }
            .yt-dl-dialog .buttons {
                display: flex;
                justify-content: space-between;
                gap: 10px;
            }
            .yt-dl-dialog button {
                padding: 8px 16px;
                border: none;
                border-radius: 4px;
                cursor: pointer;
                flex: 1;
                display: flex;
                align-items: center;
                justify-content: center;
                gap: 5px;
            }
            .yt-dl-dialog .download-btn {
                background: #2196F3;
                color: white;
            }
            .yt-dl-dialog .link-btn {
                background: #4CAF50;
                color: white;
            }
            .yt-dl-dialog .cancel-btn {
                background: #666;
                color: white;
            }
            .yt-dl-overlay {
                position: fixed;
                top: 0;
                left: 0;
                right: 0;
                bottom: 0;
                background: rgba(0,0,0,0.7);
                z-index: 9999;
            }
            #download-progress {
                position: fixed;
                bottom: 20px;
                right: 20px;
                background: #1a1a1a;
                color: white;
                padding: 15px;
                border-radius: 8px;
                z-index: 10001;
                min-width: 300px;
                box-shadow: 0 4px 12px rgba(0,0,0,0.15);
                display: none;
            }
            #download-progress .progress-bar {
                height: 5px;
                background: #333;
                border-radius: 3px;
                margin: 10px 0;
                overflow: hidden;
            }
            #download-progress .progress-bar-fill {
                height: 100%;
                background: #2196F3;
                width: 0%;
                transition: width 0.3s ease;
            }
        `;
        document.head.appendChild(style);

        // Create dialog
        const overlay = document.createElement('div');
        overlay.className = 'yt-dl-overlay';

        const dialog = document.createElement('div');
        dialog.className = 'yt-dl-dialog';
        dialog.innerHTML = `
            <h2>📥 Download Video</h2>
            <select id="quality-select">
                ${QUALITIES.map(q => `<option value="${q.value}">${q.label}</option>`).join('')}
            </select>
            <div class="buttons">
                <button class="download-btn">💾 Download</button>
                <button class="link-btn">🔗 Get Link</button>
                <button class="cancel-btn">❌ Cancel</button>
            </div>
        `;

        overlay.appendChild(dialog);
        document.body.appendChild(overlay);

        // Handle buttons
        dialog.querySelector('.download-btn').addEventListener('click', () => {
            const quality = dialog.querySelector('#quality-select').value;
            document.body.removeChild(overlay);
            streamDownload(quality);
        });

        dialog.querySelector('.link-btn').addEventListener('click', () => {
            const quality = dialog.querySelector('#quality-select').value;
            document.body.removeChild(overlay);
            quickLink(quality);
        });

        dialog.querySelector('.cancel-btn').addEventListener('click', () => {
            document.body.removeChild(overlay);
        });
    }

    // Progress UI functions
    function showProgress(text, progress = null) {
        let container = document.getElementById('download-progress');
        if (!container) {
            container = document.createElement('div');
            container.id = 'download-progress';
            container.innerHTML = `
                <div class="status"></div>
                <div class="progress-bar">
                    <div class="progress-bar-fill"></div>
                </div>
            `;
            document.body.appendChild(container);
        }
        container.style.display = 'block';
        container.querySelector('.status').textContent = text;
        if (progress !== null) {
            container.querySelector('.progress-bar-fill').style.width = `${progress}%`;
        }
    }

    function hideProgress() {
        const container = document.getElementById('download-progress');
        if (container) {
            container.style.display = 'none';
        }
    }

    // Download functions
    async function streamDownload(quality) {
        if (!checkConfig()) return;

        const videoId = new URLSearchParams(window.location.search).get('v');
        if (!videoId) {
            showNotification('No video found! ❌');
            return;
        }

        try {
            showProgress('Starting download...', 0);
            const url = `${API_BASE}/api/stream/${videoId}?quality=${quality}`;
            const title = document.title.replace(' - YouTube', '').trim();
            const ext = quality === 'audio' ? 'm4a' : 'mp4';

            const response = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url,
                    responseType: 'blob',
                    headers: { 'X-API-Key': API_KEY },
                    onprogress: (progress) => {
                        if (progress.lengthComputable) {
                            const percent = (progress.loaded / progress.total * 100).toFixed(1);
                            showProgress(`Downloading: ${percent}%`, percent);
                        }
                    },
                    onload: resolve,
                    onerror: reject
                });
            });

            const blob = new Blob([response.response]);
            const downloadUrl = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = downloadUrl;
            a.download = `${title}.${ext}`;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(downloadUrl);

            showProgress('Download complete! ✅', 100);
            setTimeout(hideProgress, 3000);
        } catch (error) {
            console.error('Download error:', error);
            showProgress('Download failed! ❌');
            showNotification('Download failed! Check console for details.');
            setTimeout(hideProgress, 3000);
        }
    }

    async function quickLink(quality) {
        if (!checkConfig()) return;

        const videoId = new URLSearchParams(window.location.search).get('v');
        if (!videoId) {
            showNotification('No video found! ❌');
            return;
        }

        try {
            showProgress('Getting download link...');
            const response = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: `${API_BASE}/api/download/${videoId}?quality=${quality}`,
                    headers: { 'X-API-Key': API_KEY },
                    onload: (response) => {
                        if (response.status === 200) {
                            resolve(JSON.parse(response.responseText));
                        } else {
                            reject(new Error(response.statusText));
                        }
                    },
                    onerror: reject
                });
            });

            window.open(response.download_url, '_blank');
            showProgress('Link opened in new tab! ✅');
            setTimeout(hideProgress, 2000);
        } catch (error) {
            console.error('Error:', error);
            showProgress('Failed to get link! ❌');
            showNotification('Failed to get download link! Check console for details.');
            setTimeout(hideProgress, 3000);
        }
    }

    // Add context menu button to video thumbnails
    function addContextMenuToThumbnails() {
        const thumbnails = document.querySelectorAll('a#thumbnail');
        thumbnails.forEach(thumbnail => {
            if (!thumbnail.dataset.dlEnabled) {
                thumbnail.addEventListener('contextmenu', (e) => {
                    const videoId = thumbnail.href?.match(/[?&]v=([^&]+)/)?.[1];
                    if (videoId) {
                        e.preventDefault();
                        const rect = thumbnail.getBoundingClientRect();
                        showContextMenu(videoId, rect.left, rect.top);
                    }
                });
                thumbnail.dataset.dlEnabled = 'true';
            }
        });
    }

    // Context menu for thumbnails
    function showContextMenu(videoId, x, y) {
        const menu = document.createElement('div');
        menu.className = 'yt-dl-context-menu';
        menu.style.cssText = `
            position: fixed;
            left: ${x}px;
            top: ${y}px;
            background: #1a1a1a;
            border: 1px solid #333;
            border-radius: 4px;
            padding: 5px 0;
            z-index: 10000;
            box-shadow: 0 2px 10px rgba(0,0,0,0.2);
        `;

        menu.innerHTML = `
            <div style="padding: 8px 12px; color: #fff; font-size: 14px; cursor: pointer; hover: background-color: #333;">
                📥 Download Video
            </div>
        `;

        document.body.appendChild(menu);

        // Handle click
        menu.addEventListener('click', () => {
            window.location.href = `https://www.youtube.com/watch?v=${videoId}`;
            setTimeout(showDialog, 1000);
        });

        // Remove menu on click outside
        function removeMenu(e) {
            if (!menu.contains(e.target)) {
                document.body.removeChild(menu);
                document.removeEventListener('click', removeMenu);
            }
        }
        setTimeout(() => document.addEventListener('click', removeMenu), 0);
    }

    // YouTube spa navigation handler
    function handleSpaNavigation() {
        const observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                if (mutation.type === 'childList') {
                    addContextMenuToThumbnails();
                }
            });
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    // Initialize
    function init() {
        // Add initial context menus
        addContextMenuToThumbnails();
        
        // Handle SPA navigation
        handleSpaNavigation();
        
        // Add configuration command
        GM_registerMenuCommand('⚙️ Configure', showConfig);
        
        // Add download command
        GM_registerMenuCommand('📥 Download Video', showDialog);
    }

    // Start the script
    init();
})();