YouTube Advanced Downloader

Advanced YouTube video downloader with quality selection and progress tracking

Ajankohdalta 23.2.2025. Katso uusin versio.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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