YouTube Advanced Downloader

Advanced YouTube video downloader with quality selection and progress tracking

Version au 23/02/2025. Voir la dernière version.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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