YouTube Direct Downloader

Add a custom download button and provide options to download the video or audio directly from the YouTube page.

当前为 2025-06-09 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         YouTube Direct Downloader
// @description  Add a custom download button and provide options to download the video or audio directly from the YouTube page.
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @version      1.5
// @author       afkarxyz
// @namespace    https://github.com/afkarxyz/userscripts/
// @supportURL   https://github.com/afkarxyz/userscripts/issues
// @license      MIT
// @match        https://www.youtube.com/*
// @match        https://youtube.com/*
// @grant        GM.xmlHttpRequest
// @grant        GM_download
// @grant        GM.download
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      api.mp3youtube.cc
// @connect      iframe.y2meta-uk.com
// @connect      *
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    let lastSelectedFormat = GM_getValue('lastSelectedFormat', 'video');
    let lastSelectedVideoQuality = GM_getValue('lastSelectedVideoQuality', '1080');
    let lastSelectedAudioBitrate = GM_getValue('lastSelectedAudioBitrate', '320');

    const API_KEY_URL = 'https://api.mp3youtube.cc/v2/sanity/key';
    const API_CONVERT_URL = 'https://api.mp3youtube.cc/v2/converter';
    
    const REQUEST_HEADERS = {
        "Content-Type": "application/json",
        "Origin": "https://iframe.y2meta-uk.com",
        "Accept": "*/*",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
    };
    const style = document.createElement('style');
    style.textContent = `
        .ytddl-download-btn {
            width: 36px;
            height: 36px;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            margin-left: 8px;
            transition: background-color 0.2s;
        }
        html[dark] .ytddl-download-btn {
            background-color: #ffffff1a;
        }
        html:not([dark]) .ytddl-download-btn {
            background-color: #0000000d;
        }
        html[dark] .ytddl-download-btn:hover {
            background-color: #ffffff33;
        }
        html:not([dark]) .ytddl-download-btn:hover {
            background-color: #00000014;
        }
        .ytddl-download-btn svg {
            width: 18px;
            height: 18px;
        }
        html[dark] .ytddl-download-btn svg {
            fill: var(--yt-spec-text-primary, #fff);
        }
        html:not([dark]) .ytddl-download-btn svg {
            fill: var(--yt-spec-text-primary, #030303);
        }
        
        .ytddl-dialog {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: #000000;
            color: #e1e1e1;
            border-radius: 12px;
            box-shadow: 0 0 0 1px rgba(225,225,225,.1), 0 2px 4px 1px rgba(225,225,225,.18);
            font-family: 'IBM Plex Mono', 'Noto Sans Mono Variable', 'Noto Sans Mono', monospace;
            width: 400px;
            z-index: 9999;
            padding: 16px;
        }
          .ytddl-backdrop {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.5);
            z-index: 9998;
        }
        
        .ytddl-dialog h3 {
            margin: 0 0 16px 0;
            font-size: 18px;
            font-weight: 700;
        }
        
        .quality-options {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 8px;
            margin-bottom: 16px;
        }
        
        .quality-option {
            display: flex;
            align-items: center;
            padding: 8px;
            cursor: pointer;
            border-radius: 6px;
        }
        
        .quality-option:hover {
            background: #191919;
        }
        
        .quality-option input[type="radio"] {
            margin-right: 8px;
        }
        
        .download-status {
            text-align: center;
            margin: 16px 0;
            font-size: 12px;
            display: none;
            color: #1ed760;
        }
        
        .button-container {
            display: flex;
            justify-content: center;
            gap: 8px;
            margin-top: 16px;
        }
          .ytddl-button {
            background: transparent;
            border: 1px solid #e1e1e1;
            color: #e1e1e1;
            font-size: 14px;
            font-weight: 500;
            padding: 8px 16px;
            border-radius: 18px;
            cursor: pointer;
            font-family: inherit;
            transition: all 0.2s;
        }
        
        .ytddl-button:hover {
            background: #1ed760;
            border-color: #1ed760;
            color: #000000;
        }
        
        .ytddl-button.cancel:hover {
            background: #f3727f;
            border-color: #f3727f;
            color: #000000;
        }
        
        .format-selector {
            margin-bottom: 16px;
            display: flex;
            gap: 8px;
            justify-content: center;
        }
        
        .format-button {
            background: transparent;
            border: 1px solid #e1e1e1;
            color: #e1e1e1;
            padding: 6px 12px;
            border-radius: 14px;
            cursor: pointer;
            font-family: inherit;
            font-size: 12px;
            transition: all 0.2s ease;
        }
        
        .format-button:hover {
            background: #808080;
            color: #000000;
        }
          .format-button.selected {
            background: #1ed760;
            border-color: #1ed760;
            color: #000000;
        }
          .ytddl-overlay {
            position: fixed;
            top: 20px;
            right: 20px;
            background: rgba(0, 0, 0, 0.9);
            color: #e1e1e1;
            border-radius: 8px;
            padding: 16px;
            width: 350px;
            max-width: 350px;
            z-index: 10000;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            font-size: 14px;
            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
            border: 1px solid rgba(255, 255, 255, 0.1);
            backdrop-filter: blur(10px);
            opacity: 0;
            transform: translateX(100%);
            transition: all 0.3s ease;
        }
        
        .ytddl-overlay.show {
            opacity: 1;
            transform: translateX(0);
        }
  
        
        .ytddl-overlay-content {
            line-height: 1.5;
        }
        
        .ytddl-overlay-status {
            margin-bottom: 8px;
            color: #1ed760;
            font-weight: 500;
        }
        
        .ytddl-overlay-details {
            color: #ccc;
            font-size: 13px;
            margin-bottom: 12px;
        }
          .ytddl-overlay-file-info {
            display: flex;
            justify-content: space-between;
            margin-bottom: 8px;
            font-size: 12px;
        }
        
        .ytddl-overlay-size {
            color: #1ed760;
            font-weight: 500;
        }
        
        .ytddl-overlay-speed {
            color: #ffa500;
            font-weight: 500;
        }
        
        .ytddl-overlay-error {
            color: #ff6b6b;
        }
        
        .ytddl-overlay-success {
            color: #1ed760;
        }
    `;
    document.head.appendChild(style);

    let currentOverlay = null;

    function createOverlay() {
        if (currentOverlay) {
            removeOverlay();
        }        const overlay = document.createElement('div');
        overlay.className = 'ytddl-overlay';
        
        const content = document.createElement('div');
        content.className = 'ytddl-overlay-content';
        
        const status = document.createElement('div');
        status.className = 'ytddl-overlay-status';
        status.textContent = 'Initializing...';
        
        const details = document.createElement('div');
        details.className = 'ytddl-overlay-details';
        details.textContent = 'Preparing download request';
          const fileInfoContainer = document.createElement('div');
        fileInfoContainer.className = 'ytddl-overlay-file-info';
        
        const sizeElement = document.createElement('div');
        sizeElement.className = 'ytddl-overlay-size';
        sizeElement.textContent = 'Size: Calculating...';
        
        const speedElement = document.createElement('div');
        speedElement.className = 'ytddl-overlay-speed';
        speedElement.textContent = 'Speed: -';
        
        fileInfoContainer.appendChild(sizeElement);
        fileInfoContainer.appendChild(speedElement);
        
        content.appendChild(status);
        content.appendChild(details);
        content.appendChild(fileInfoContainer);        
        overlay.appendChild(content);
        
        overlay.addEventListener('click', function(e) {
            if (e.target === overlay) {
                removeOverlay();
            }
        });
        
        document.body.appendChild(overlay);
        
        setTimeout(() => {
            overlay.classList.add('show');
        }, 100);
          currentOverlay = overlay;
        return overlay;
    }

    function updateOverlay(status, details, fileSize = null, downloadSpeed = null, isError = false, isSuccess = false) {
        if (!currentOverlay) return;
        const statusEl = currentOverlay.querySelector('.ytddl-overlay-status');
        const detailsEl = currentOverlay.querySelector('.ytddl-overlay-details');
        const sizeEl = currentOverlay.querySelector('.ytddl-overlay-size');        
        const speedEl = currentOverlay.querySelector('.ytddl-overlay-speed');
        
        if (statusEl) {
            statusEl.textContent = status;
            statusEl.className = 'ytddl-overlay-status';
            if (isError) statusEl.classList.add('ytddl-overlay-error');
            if (isSuccess) statusEl.classList.add('ytddl-overlay-success');
        }
        
        if (detailsEl) {
            detailsEl.textContent = details;
        }
        
        if (sizeEl) {
            if (fileSize !== null) {
                sizeEl.textContent = `Size: ${fileSize}`;
                sizeEl.style.display = 'block';
            } else {
                sizeEl.style.display = 'none';
            }
        }
        
        if (speedEl) {
            if (downloadSpeed !== null) {
                speedEl.textContent = `Speed: ${downloadSpeed}`;
                speedEl.style.display = 'block';
            } else {
                speedEl.style.display = 'none';
            }
        }
        currentOverlay.offsetHeight;
    }

    function removeOverlay() {
        if (currentOverlay) {
            currentOverlay.classList.remove('show');
            setTimeout(() => {
                if (currentOverlay && currentOverlay.parentNode) {
                    currentOverlay.parentNode.removeChild(currentOverlay);
                }
                currentOverlay = null;
            }, 300);
        }
    }
    
    function formatBytes(bytes) {
        if (bytes === 0) return '0 Bytes';
        const k = 1024;
        const sizes = ['Bytes', 'KB', 'MB', 'GB'];
        const i = Math.floor(Math.log(bytes) / Math.log(k));
        return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
    }
    function truncateTitle(title, maxLength = 50) {
        if (!title || title.length <= maxLength) return title;
        return title.substring(0, maxLength - 3) + '...';
    }

    function triggerDirectDownload(url, filename) {
        let downloadStartTime = Date.now();
        
        updateOverlay('Validating download URL', 'Testing download link...', null, null);
        
        GM.xmlHttpRequest({
            method: 'HEAD',
            url: url,
            headers: {
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
            },
            onload: function(response) {
                console.log('URL Test Response:', response.status, response.statusText);
                console.log('Content-Length:', response.responseHeaders.match(/content-length:\s*(\d+)/i));
                
                if (response.status === 200 || response.status === 206) {
                    fetchAndDownload(url, filename, downloadStartTime);
                } else {
                    updateOverlay(
                        'Download failed', 
                        `Invalid download URL (Status: ${response.status})`,
                        null,
                        null,                        true
                    );
                    setTimeout(removeOverlay, 2500);
                }
            },
            onerror: function(error) {
                console.error('URL validation failed:', error);
                updateOverlay(
                    'Download failed', 
                    'Cannot access download URL - may be expired or invalid',
                    null,
                    null,                    true
                );
                setTimeout(removeOverlay, 2500);
            }
        });
    }
      function fetchAndDownload(url, filename, downloadStartTime) {
        updateOverlay('Starting download', 'Connecting to server...', '0 B', '0 B/s');
        
        console.log('=== FETCH AND DOWNLOAD ===');
        console.log('URL:', url);
        console.log('Filename:', filename);
        console.log('Method: GM.xmlHttpRequest with responseType blob');
        console.log('Start time:', new Date(downloadStartTime).toISOString());
        console.log('==========================');
          let totalSize = 0;
        let downloadedSize = 0;
        let lastUpdateTime = 0;
        const UPDATE_INTERVAL = 250;
        
        GM.xmlHttpRequest({
            method: 'GET',
            url: url,
            responseType: 'blob',
            headers: {
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
                "Referer": "https://iframe.y2meta-uk.com/",
                "Accept": "*/*"
            },            onprogress: function(progressEvent) {
                const currentTime = Date.now();
                const elapsed = (currentTime - downloadStartTime) / 1000;
                
                const shouldUpdate = (currentTime - lastUpdateTime) >= UPDATE_INTERVAL || 
                                   (progressEvent.lengthComputable && progressEvent.loaded === progressEvent.total);
                
                if (progressEvent.lengthComputable) {
                    totalSize = progressEvent.total;
                    downloadedSize = progressEvent.loaded;
                    
                    const percentage = Math.round((downloadedSize / totalSize) * 100);
                    const speed = elapsed > 0 ? downloadedSize / elapsed : 0;
                    
                    if (shouldUpdate) {
                        const sizeText = `${formatBytes(downloadedSize)} / ${formatBytes(totalSize)}`;
                        const speedText = `${formatBytes(speed)}/s`;
                        const percentText = `${percentage}%`;
                        
                        updateOverlay(
                            `Downloading ${percentText}`, 
                            `${filename || 'video.mp4'}`,
                            sizeText,
                            speedText
                        );
                        
                        lastUpdateTime = currentTime;
                    }
                    
                    if ((currentTime - lastUpdateTime) >= 1000 || percentage === 100) {
                        console.log(`[${elapsed.toFixed(1)}s] Progress: ${percentage}% | Downloaded: ${formatBytes(downloadedSize)}/${formatBytes(totalSize)} | Speed: ${formatBytes(speed)}/s`);
                    }
                } else {
                    downloadedSize = progressEvent.loaded || 0;
                    const speed = elapsed > 0 ? downloadedSize / elapsed : 0;
                    
                    if (shouldUpdate) {
                        const sizeText = `${formatBytes(downloadedSize)}`;
                        const speedText = `${formatBytes(speed)}/s`;
                        const timeText = `${elapsed.toFixed(1)}s`;
                        
                        updateOverlay(
                            `Downloading...`, 
                            `${filename || 'video.mp4'} - ${timeText}`,
                            sizeText,
                            speedText
                        );
                        
                        lastUpdateTime = currentTime;
                    }
                    
                    if ((currentTime - lastUpdateTime) >= 1000) {
                        console.log(`[${elapsed.toFixed(1)}s] Downloaded: ${formatBytes(downloadedSize)} | Speed: ${formatBytes(speed)}/s`);
                    }
                }
            },
            onload: function(response) {
                console.log('Fetch completed. Response status:', response.status);
                console.log('Response type:', typeof response.response);
                console.log('Response size:', response.response?.size || 'unknown');
                
                if (response.status === 200 && response.response) {
                    updateOverlay('Creating download file', 'Converting to downloadable file...', formatBytes(response.response.size || 0), 'Processing');
                    
                    try {
                        const blob = response.response;
                        const blobUrl = URL.createObjectURL(blob);
                        
                        console.log('Blob created:', blob.size, 'bytes');
                        console.log('Blob URL:', blobUrl);
                        
                        const a = document.createElement('a');
                        a.style.display = 'none';
                        a.href = blobUrl;
                        a.download = filename || 'video.mp4';
                        
                        document.body.appendChild(a);
                        
                        a.click();
                        
                        setTimeout(() => {
                            document.body.removeChild(a);
                            URL.revokeObjectURL(blobUrl);
                        }, 1000);
                          updateOverlay(
                            'Download completed successfully!', 
                            `${filename || 'video.mp4'}`,
                            formatBytes(blob.size),
                            'Complete',
                            false,
                            true
                        );
                          console.log('✅ Download successful via blob method');
                        
                        setTimeout(() => {
                            removeOverlay();
                        }, 2500);
                        
                    } catch (blobError) {
                        console.error('Blob download failed:', blobError);
                        updateOverlay(
                            'Blob conversion failed', 
                            'Trying alternative download methods...',
                            null,
                            null,
                            true
                        );
                        
                        setTimeout(() => {
                            proceedWithDownload(url, filename, downloadStartTime);
                        }, 2000);
                    }
                    
                } else {
                    console.error('Fetch failed with status:', response.status);
                    updateOverlay(
                        'Data fetch failed', 
                        `Server returned status ${response.status}`,
                        null,
                        null,
                        true
                    );
                    
                    setTimeout(() => {
                        proceedWithDownload(url, filename, downloadStartTime);
                    }, 2000);
                }
            },            onerror: function(error) {
                console.error('GM.xmlHttpRequest fetch failed:', error);
                updateOverlay(
                    'Data fetch failed', 
                    'Trying native fetch method...',
                    null,
                    null,
                    true
                );
                
                setTimeout(() => {
                    nativeFetchDownload(url, filename, downloadStartTime);
                }, 2000);
            },
            ontimeout: function() {
                console.error('GM.xmlHttpRequest fetch timeout');
                updateOverlay(
                    'Download timeout', 
                    'Trying native fetch method...',
                    null,
                    null,
                    true
                );
                
                setTimeout(() => {
                    nativeFetchDownload(url, filename, downloadStartTime);
                }, 2000);
            }
        });
    }
      async function nativeFetchDownload(url, filename, downloadStartTime) {
        updateOverlay('Trying native fetch', 'Using browser fetch API...', 'Starting...', 'Native method');
        
        console.log('=== NATIVE FETCH DOWNLOAD ===');
        console.log('URL:', url);
        console.log('Filename:', filename);
        console.log('Method: Native fetch API with ReadableStream');
        console.log('=============================');
        
        try {
            const response = await fetch(url, {
                method: 'GET',
                headers: {
                    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
                    "Referer": "https://iframe.y2meta-uk.com/",
                    "Accept": "*/*"
                }
            });
            
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}: ${response.statusText}`);
            }
            
            const contentLength = response.headers.get('content-length');
            const totalSize = contentLength ? parseInt(contentLength, 10) : 0;
            
            console.log('Native fetch response OK. Content-Length:', totalSize);
            
            if (response.body && totalSize > 0) {
                const reader = response.body.getReader();
                const chunks = [];
                let downloadedSize = 0;
                
                updateOverlay(
                    'Downloading with native fetch', 
                    `Total size: ${formatBytes(totalSize)}`,
                    '0%',
                    'Starting...'
                );
                
                while (true) {
                    const { done, value } = await reader.read();
                    
                    if (done) break;
                    
                    chunks.push(value);
                    downloadedSize += value.length;
                    
                    const percentage = (downloadedSize / totalSize) * 100;
                    const elapsed = (Date.now() - downloadStartTime) / 1000;
                    const speed = elapsed > 0 ? downloadedSize / elapsed : 0;
                    
                    const sizeText = `${formatBytes(downloadedSize)} / ${formatBytes(totalSize)}`;
                    const speedText = `${formatBytes(speed)}/s`;
                    
                    updateOverlay(
                        'Downloading with native fetch', 
                        `${Math.round(percentage)}% - ${filename || 'video.mp4'}`,
                        sizeText,
                        speedText
                    );
                    
                    console.log(`Native fetch progress: ${Math.round(percentage)}% | ${sizeText} | ${speedText}`);
                }
                
                const blob = new Blob(chunks);
                
                console.log('Native fetch blob created from chunks:', blob.size, 'bytes');
                
                const blobUrl = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.style.display = 'none';
                a.href = blobUrl;
                a.download = filename || 'video.mp4';
                
                document.body.appendChild(a);
                a.click();
                  setTimeout(() => {
                    document.body.removeChild(a);
                    URL.revokeObjectURL(blobUrl);
                }, 1000);
                
                updateOverlay(
                    'Native fetch download completed!', 
                    `${filename || 'video.mp4'}`,
                    formatBytes(blob.size),
                    'Complete',
                    false,
                    true
                );
                
            } else {
                updateOverlay('Downloading with native fetch', 'Size unknown, downloading...', 'Unknown size', 'Downloading...');
                
                const blob = await response.blob();
                
                console.log('Native fetch blob created (no progress):', blob.size, 'bytes');
                
                const blobUrl = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.style.display = 'none';
                a.href = blobUrl;
                a.download = filename || 'video.mp4';
                
                document.body.appendChild(a);
                a.click();
                  setTimeout(() => {
                    document.body.removeChild(a);
                    URL.revokeObjectURL(blobUrl);
                }, 1000);
                
                updateOverlay(
                    'Native fetch download completed!', 
                    `${filename || 'video.mp4'}`,
                    formatBytes(blob.size),
                    'Complete',
                    false,
                    true
                );
            }
              console.log('✅ Download successful via native fetch');
            
            setTimeout(() => {
                removeOverlay();
            }, 2500);
            
        } catch (fetchError) {
            console.error('Native fetch failed:', fetchError);
            updateOverlay(
                'Native fetch failed', 
                `Error: ${fetchError.message}`,
                null,
                null,
                true
            );
            
            setTimeout(() => {
                proceedWithDownload(url, filename, downloadStartTime);
            }, 2000);
        }
    }function proceedWithDownload(url, filename, downloadStartTime) {
        console.log('=== FALLBACK DOWNLOAD METHODS ===');
        console.log('GM_download (legacy):', typeof GM_download, GM_download);
        console.log('GM.download (new):', typeof GM?.download, GM?.download);
        console.log('Download URL:', url);
        console.log('Filename:', filename);
        console.log('==================================');
        
        updateOverlay('Opening download in new tab', 'Most reliable method for video downloads', null, null);
        
        try {
            const downloadWindow = window.open(url, '_blank', 'noopener,noreferrer');
            
            if (downloadWindow) {
                console.log('New tab opened successfully');                
                updateOverlay(
                    'Download opened in new tab', 
                    `${truncateTitle(filename || 'video.mp4')} - Check Downloads folder`,
                    'Via new tab',
                    'Browser handling',
                    false,
                    true
                );
                
                setTimeout(() => {
                    removeOverlay();
                }, 10000);
                
                return;
            } else {
                console.log('New tab blocked, trying GM_download');
                attemptGMDownload(url, filename, downloadStartTime);
            }
            
        } catch (error) {
            console.error('New tab method failed:', error);
            attemptGMDownload(url, filename, downloadStartTime);
        }
    }
    
    function attemptGMDownload(url, filename, downloadStartTime) {
        const gmDownloadAvailable = typeof GM_download !== 'undefined' && GM_download;
        const gmDownloadNewSyntax = typeof GM !== 'undefined' && GM.download;
        
        if (gmDownloadAvailable) {
            try {
                updateOverlay('Trying GM_download method', `File: ${truncateTitle(filename || 'video.mp4')}`, 'Initializing...', '-');
                
                console.log('Using GM_download (legacy API)');
                
                const downloadId = GM_download(url, filename || 'video.mp4', {
                    headers: {
                        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
                        "Referer": "https://iframe.y2meta-uk.com/",
                        "Accept": "*/*"
                    },
                    onprogress: function(progressEvent) {
                        console.log('Download progress (legacy):', progressEvent);
                        
                        if (progressEvent.lengthComputable) {
                            const totalSize = progressEvent.total;
                            const downloadedSize = progressEvent.loaded;
                            
                            const percentage = (downloadedSize / totalSize) * 100;
                            const elapsed = (Date.now() - downloadStartTime) / 1000;
                            const speed = downloadedSize / elapsed;
                            
                            const sizeText = `${formatBytes(downloadedSize)} / ${formatBytes(totalSize)}`;
                            const speedText = `${formatBytes(speed)}/s`;
                              updateOverlay(
                                'Downloading via GM_download', 
                                `${Math.round(percentage)}% - ${truncateTitle(filename || 'video.mp4')}`,
                                sizeText,
                                speedText
                            );
                        } else {
                            const elapsed = (Date.now() - downloadStartTime) / 1000;                            
                            updateOverlay(
                                'Downloading via GM_download', 
                                `${truncateTitle(filename || 'video.mp4')} - ${elapsed.toFixed(1)}s elapsed`,
                                'Size unknown',
                                'Progress...'
                            );
                        }
                    },                    
                    onload: function() {                        
                        console.log('Download completed successfully (legacy)');
                        updateOverlay(
                            'Download completed successfully', 
                            `${truncateTitle(filename || 'video.mp4')}`,
                            'Complete',
                            'Done',
                            false,
                            true
                        );
                        
                        setTimeout(() => {
                            removeOverlay();
                        }, 2500);
                    },
                    onerror: function(error) {
                        console.error('GM_download error (legacy):', error);
                        updateOverlay(
                            'GM_download failed', 
                            'Trying alternative download methods...',
                            null,
                            null,
                            true
                        );
                        
                        setTimeout(() => {
                            fallbackDownload(url, filename);
                        }, 2000);
                    }
                });
                
                console.log('Download ID (legacy):', downloadId);
                
                setTimeout(() => {
                    console.log('Checking if GM_download callbacks fired...');                    
                    updateOverlay(
                        'GM_download may have CORS issues', 
                        'Switching to fallback methods...',
                        null,
                        null,
                        true
                    );
                    
                    setTimeout(() => {
                        fallbackDownload(url, filename);
                    }, 2000);
                }, 2500);
                
            } catch (downloadError) {
                console.error('GM_download exception:', downloadError);
                tryNewGMDownload(url, filename, downloadStartTime);
            }
        } else if (gmDownloadNewSyntax) {
            tryNewGMDownload(url, filename, downloadStartTime);
        } else {
            console.log('No GM download APIs available, using fallback');
            fallbackDownload(url, filename);
        }
    }
      function tryNewGMDownload(url, filename, downloadStartTime) {
        try {
            updateOverlay('Trying GM.download (new API)', `File: ${filename || 'video.mp4'}`, 'Initializing...', '-');
            
            console.log('Using GM.download (new API)');
            
            GM.download(url, filename || 'video.mp4', {
                headers: {
                    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
                    "Referer": "https://iframe.y2meta-uk.com/",
                    "Accept": "*/*"
                },
                onprogress: function(progressEvent) {
                    console.log('Download progress (new):', progressEvent);
                    
                    if (progressEvent.lengthComputable) {
                        const totalSize = progressEvent.total;
                        const downloadedSize = progressEvent.loaded;
                        
                        const percentage = (downloadedSize / totalSize) * 100;
                        const elapsed = (Date.now() - downloadStartTime) / 1000;
                        const speed = downloadedSize / elapsed;
                        
                        const sizeText = `${formatBytes(downloadedSize)} / ${formatBytes(totalSize)}`;
                        const speedText = `${formatBytes(speed)}/s`;
                        
                        updateOverlay(
                            'Downloading via GM.download', 
                            `${Math.round(percentage)}% - ${filename || 'video.mp4'}`,
                            sizeText,
                            speedText
                        );
                    } else {
                        const elapsed = (Date.now() - downloadStartTime) / 1000;
                        updateOverlay(
                            'Downloading via GM.download', 
                            `${filename || 'video.mp4'} - ${elapsed.toFixed(1)}s elapsed`,
                            'Size unknown',
                            'Progress...'
                        );
                    }
                },                
                onload: function() {                    
                    console.log('Download completed successfully (new)');
                    updateOverlay(
                        'Download completed successfully', 
                        `${filename || 'video.mp4'}`,
                        'Complete',
                        'Done',
                        false,
                        true
                    );
                    
                    setTimeout(() => {
                        removeOverlay();
                    }, 2500);
                },
                onerror: function(error) {
                    console.error('GM.download error (new):', error);
                    updateOverlay(
                        'GM.download failed', 
                        'Trying alternative download methods...',
                        null,
                        null,
                        true
                    );
                    
                    setTimeout(() => {
                        fallbackDownload(url, filename);
                    }, 2000);
                }
            }).then(downloadId => {
                console.log('Download ID (new):', downloadId);
                setTimeout(() => {
                    console.log('Checking if GM.download callbacks fired...');
                    updateOverlay(
                        'GM.download may have CORS issues', 
                        'Switching to fallback methods...',
                        null,
                        null,
                        true
                    );
                    
                    setTimeout(() => {
                        fallbackDownload(url, filename);
                    }, 2000);
                }, 2500);
                
            }).catch(error => {
                console.error('GM.download promise error:', error);
                fallbackDownload(url, filename);
            });
            
        } catch (downloadError) {
            console.error('GM.download exception:', downloadError);
            fallbackDownload(url, filename);
        }
    }function fallbackDownload(url, filename) {
        updateOverlay('Using direct download methods', 'Testing browser download capabilities...', null, null);
        
        console.log('=== FALLBACK DOWNLOAD ===');
        console.log('URL:', url);
        console.log('Filename:', filename);
        console.log('=========================');
        
        try {
            const a = document.createElement('a');
            a.style.display = 'none';
            a.href = url;
            a.download = filename || 'video.mp4';
            a.target = '_blank';
            a.rel = 'noopener noreferrer';
            
            document.body.appendChild(a);
            
            updateOverlay('Method 1: Force download link', 'Creating download trigger...', null, null);
            
            const clickEvent = new MouseEvent('click', {
                bubbles: true,
                cancelable: true,
                view: window
            });
            
            a.dispatchEvent(clickEvent);
            
            a.click();
            
            setTimeout(() => {
                document.body.removeChild(a);
                
                updateOverlay(
                    'Download link triggered', 
                    `${filename || 'video.mp4'} - Check Downloads folder`,
                    'Via download link',
                    'Browser handling',
                    false,
                    true
                );
                
                setTimeout(() => {
                    trySecondaryMethod(url, filename);
                }, 3000);
                
            }, 1000);
            
        } catch (error) {
            console.error('Direct link method failed:', error);
            trySecondaryMethod(url, filename);
        }
    }
    
    function trySecondaryMethod(url, filename) {
        updateOverlay('Method 2: Location redirect', 'Attempting direct navigation...', null, null);
        
        try {
            const downloadWindow = window.open('', '_blank');
            
            if (downloadWindow) {
                downloadWindow.location.href = url;
                  updateOverlay(
                    'Download redirected to new tab', 
                    `${filename || 'video.mp4'} - Check new tab`,
                    'Via location redirect',
                    'Tab navigation',
                    false,
                    true
                );
                
                setTimeout(() => {
                    tryFinalMethod(url, filename);
                }, 2500);
            } else {
                tryFinalMethod(url, filename);
            }
            
        } catch (error) {
            console.error('Secondary method failed:', error);
            tryFinalMethod(url, filename);
        }
    }
    
    function tryFinalMethod(url, filename) {
        updateOverlay('Method 3: Manual URL access', 'Preparing manual download option...', null, null);
        
        try {
            navigator.clipboard.writeText(url).then(() => {
                updateOverlay(
                    'URL copied to clipboard!', 
                    'Open new tab and paste (Ctrl+L, Ctrl+V, Enter)',
                    'Clipboard ready',
                    'Manual paste',
                    false,
                    true
                );
                
                console.log('=== MANUAL DOWNLOAD URL ===');
                console.log('URL copied to clipboard. Paste in new tab:');
                console.log(url);
                console.log('Filename:', filename);
                console.log('===========================');
                
            }).catch(() => {
                showConsoleMethod(url, filename);
            });
        } catch (error) {
            showConsoleMethod(url, filename);
        }
        setTimeout(removeOverlay, 10000);
    }
    
    function showConsoleMethod(url, filename) {
        console.log('=== MANUAL DOWNLOAD URL ===');
        console.log('Copy this URL and paste in new browser tab:');
        console.log(url);
        console.log('Filename:', filename);
        console.log('===========================');
        
        updateOverlay(
            'Check browser console (F12)', 
            'URL available in console for manual copy',
            'Console method',
            'Manual copy/paste',
            false,
            false
        );
    }

    function createDownloadDialog() {
        const dialog = document.createElement('div');        
        dialog.className = 'ytddl-dialog';        
        const title = document.createElement('h3');
        title.textContent = '';

        const formatSelector = document.createElement('div');
        formatSelector.className = 'format-selector';        
        const videoBtn = document.createElement('button');
        videoBtn.className = `format-button ${lastSelectedFormat === 'video' ? 'selected' : ''}`;
        videoBtn.setAttribute('data-format', 'video');
        videoBtn.textContent = 'VIDEO (MP4)';

        const audioBtn = document.createElement('button');
        audioBtn.className = `format-button ${lastSelectedFormat === 'audio' ? 'selected' : ''}`;
        audioBtn.setAttribute('data-format', 'audio');
        audioBtn.textContent = 'AUDIO (MP3)';

        formatSelector.appendChild(videoBtn);
        formatSelector.appendChild(audioBtn);

        const qualityContainer = document.createElement('div');
        qualityContainer.id = 'quality-container';        
        const videoQualities = document.createElement('div');
        videoQualities.className = 'quality-options';
        videoQualities.id = 'video-qualities';
        videoQualities.style.display = lastSelectedFormat === 'video' ? 'grid' : 'none';        
        ['144p', '240p', '360p', '480p', '720p', '1080p'].forEach((quality, index) => {
            const option = document.createElement('div');
            option.className = 'quality-option';

            const input = document.createElement('input');
            input.type = 'radio';
            input.id = `quality-${index}`;
            input.name = 'quality';
            input.value = quality.replace('p', '');

            const label = document.createElement('label');
            label.setAttribute('for', `quality-${index}`);
            label.textContent = quality;
            label.style.fontSize = '14px';
            label.style.cursor = 'pointer';

            option.appendChild(input);
            option.appendChild(label);
            videoQualities.appendChild(option);

            option.addEventListener('click', function() {
                input.checked = true;
                GM_setValue('lastSelectedVideoQuality', input.value);
                lastSelectedVideoQuality = input.value;
            });
        });

        const defaultQuality = videoQualities.querySelector(`input[value="${lastSelectedVideoQuality}"]`);
        if (defaultQuality) {
            defaultQuality.checked = true;
        }        
        const audioQualities = document.createElement('div');
        audioQualities.className = 'quality-options';
        audioQualities.id = 'audio-qualities';
        audioQualities.style.display = lastSelectedFormat === 'audio' ? 'grid' : 'none';        
        ['128', '256', '320'].forEach((bitrate, index) => {
            const option = document.createElement('div');
            option.className = 'quality-option';

            const input = document.createElement('input');
            input.type = 'radio';
            input.id = `bitrate-${index}`;
            input.name = 'bitrate';
            input.value = bitrate;

            const label = document.createElement('label');
            label.setAttribute('for', `bitrate-${index}`);
            label.textContent = `${bitrate} kbps`;
            label.style.fontSize = '14px';
            label.style.cursor = 'pointer';

            option.appendChild(input);
            option.appendChild(label);
            audioQualities.appendChild(option);

            option.addEventListener('click', function() {
                input.checked = true;
                GM_setValue('lastSelectedAudioBitrate', input.value);
                lastSelectedAudioBitrate = input.value;
            });
        });

        const defaultBitrate = audioQualities.querySelector(`input[value="${lastSelectedAudioBitrate}"]`);
        if (defaultBitrate) {
            defaultBitrate.checked = true;
        }

        qualityContainer.appendChild(videoQualities);
        qualityContainer.appendChild(audioQualities);

        const downloadStatus = document.createElement('div');
        downloadStatus.className = 'download-status';
        downloadStatus.id = 'download-status';

        const buttonContainer = document.createElement('div');
        buttonContainer.className = 'button-container';

        const cancelButton = document.createElement('button');
        cancelButton.className = 'ytddl-button cancel';
        cancelButton.textContent = 'Cancel';

        const downloadButton = document.createElement('button');
        downloadButton.className = 'ytddl-button';
        downloadButton.textContent = 'Download';

        buttonContainer.appendChild(cancelButton);
        buttonContainer.appendChild(downloadButton);

        dialog.appendChild(title);
        dialog.appendChild(formatSelector);
        dialog.appendChild(qualityContainer);
        dialog.appendChild(downloadStatus);
        dialog.appendChild(buttonContainer);

        formatSelector.addEventListener('click', (e) => {
            if (e.target.classList.contains('format-button')) {
                formatSelector.querySelectorAll('.format-button').forEach(btn => {
                    btn.classList.remove('selected');
                });
                e.target.classList.add('selected');                
                const format = e.target.getAttribute('data-format');
                if (format === 'video') {
                    videoQualities.style.display = 'grid';
                    audioQualities.style.display = 'none';
                    lastSelectedFormat = 'video';
                    GM_setValue('lastSelectedFormat', 'video');
                } else {
                    videoQualities.style.display = 'none';
                    audioQualities.style.display = 'grid';
                    lastSelectedFormat = 'audio';
                    GM_setValue('lastSelectedFormat', 'audio');
                }
            }
        });

        const backdrop = document.createElement('div');
        backdrop.className = 'ytddl-backdrop';

        return { dialog, backdrop, cancelButton, downloadButton };
    }

    function closeDialog(dialog, backdrop) {
        if (dialog && dialog.parentNode) {
            dialog.parentNode.removeChild(dialog);
        }
        if (backdrop && backdrop.parentNode) {
            backdrop.parentNode.removeChild(backdrop);
        }
    }

    function extractVideoId(url) {
        const urlObj = new URL(url);
        const searchParams = new URLSearchParams(urlObj.search);
        return searchParams.get('v');
    }    async function downloadWithMP3YouTube(videoUrl, format, quality) {
        const statusElement = document.getElementById('download-status');
        
        createOverlay();
        
        if (statusElement) {
            statusElement.style.display = 'block';
            statusElement.textContent = 'Getting API key...';
        }

        try {
            updateOverlay('Getting API key', 'Connecting to MP3YouTube API...');

            const keyResponse = await new Promise((resolve, reject) => {
                GM.xmlHttpRequest({
                    method: 'GET',
                    url: API_KEY_URL,
                    headers: REQUEST_HEADERS,
                    onload: resolve,
                    onerror: reject,
                    ontimeout: reject
                });
            });

            const keyData = JSON.parse(keyResponse.responseText);
            if (!keyData || !keyData.key) {
                throw new Error('Failed to get API key');
            }

            const key = keyData.key;
            
            updateOverlay('Processing request', `${format} (${format === 'video' ? quality + 'p' : quality + ' kbps'})`);
            
            if (statusElement) {
                statusElement.textContent = 'Processing download...';
            }

            let payload;
            if (format === 'video') {
                payload = {
                    "link": videoUrl,
                    "format": "mp4",
                    "audioBitrate": "128",
                    "videoQuality": quality,
                    "filenameStyle": "pretty",
                    "vCodec": "h264"
                };
            } else {
                payload = {
                    "link": videoUrl,
                    "format": "mp3",
                    "audioBitrate": quality,
                    "filenameStyle": "pretty"
                };
            }

            const customHeaders = {
                ...REQUEST_HEADERS,
                "key": key
            };

            updateOverlay('Converting media', 'Processing video/audio conversion...');

            const downloadResponse = await new Promise((resolve, reject) => {
                GM.xmlHttpRequest({
                    method: 'POST',
                    url: API_CONVERT_URL,
                    headers: customHeaders,
                    data: JSON.stringify(payload),
                    onload: resolve,
                    onerror: reject,
                    ontimeout: reject
                });
            });

            const downloadInfo = JSON.parse(downloadResponse.responseText);
              if (downloadInfo.url) {
                updateOverlay('Starting download', `File: ${truncateTitle(downloadInfo.filename || `video.${format === 'video' ? 'mp4' : 'mp3'}`)}`);
                
                if (statusElement) {
                    statusElement.textContent = 'Starting download...';
                }
                
                triggerDirectDownload(downloadInfo.url, downloadInfo.filename);
                
                return downloadInfo;
            } else {
                throw new Error('No download URL received from API');
            }} catch (error) {
            updateOverlay('Download failed', `Error: ${error.message}`, null, null, true);
            setTimeout(() => {
                removeOverlay();
            }, 4000);
            
            throw error;
        }
    }

    function createDownloadButton() {
        const downloadButton = document.createElement('div');
        downloadButton.className = 'ytddl-download-btn';
        
        const svgNS = "http://www.w3.org/2000/svg";
        const svg = document.createElementNS(svgNS, "svg");
        svg.setAttribute("viewBox", "0 0 512 512");
        
        const path = document.createElementNS(svgNS, "path");
        path.setAttribute("d", "M256 464c114.9 0 208-93.1 208-208c0-13.3 10.7-24 24-24s24 10.7 24 24c0 141.4-114.6 256-256 256S0 397.4 0 256c0-13.3 10.7-24 24-24s24 10.7 24 24c0 114.9 93.1 208 208 208zM377.6 232.3l-104 112c-4.5 4.9-10.9 7.7-17.6 7.7s-13-2.8-17.6-7.7l-104-112c-9-9.7-8.5-24.9 1.3-33.9s24.9-8.5 33.9 1.3L232 266.9 232 24c0-13.3 10.7-24 24-24s24 10.7 24 24l0 242.9 62.4-67.2c9-9.7 24.2-10.3 33.9-1.3s10.3 24.2 1.3 33.9z");
        
        svg.appendChild(path);
        downloadButton.appendChild(svg);
        
        downloadButton.addEventListener('click', function() {
            showDownloadDialog();
        });
        
        return downloadButton;
    }

    function showDownloadDialog() {
        const videoUrl = window.location.href;
        const videoId = extractVideoId(videoUrl);
        
        if (!videoId) {
            alert('Could not extract video ID from URL');
            return;
        }

        const { dialog, backdrop, cancelButton, downloadButton } = createDownloadDialog();
        
        document.body.appendChild(backdrop);
        document.body.appendChild(dialog);

        backdrop.addEventListener('click', () => {
            closeDialog(dialog, backdrop);
        });

        cancelButton.addEventListener('click', () => {
            closeDialog(dialog, backdrop);
        });        downloadButton.addEventListener('click', async () => {
            const selectedFormat = dialog.querySelector('.format-button.selected').getAttribute('data-format');
            let quality;
            
            if (selectedFormat === 'video') {
                const selectedQuality = dialog.querySelector('input[name="quality"]:checked');
                if (!selectedQuality) {
                    alert('Please select a video quality');
                    return;
                }
                quality = selectedQuality.value;
            } else {
                const selectedBitrate = dialog.querySelector('input[name="bitrate"]:checked');
                if (!selectedBitrate) {
                    alert('Please select an audio bitrate');
                    return;
                }
                quality = selectedBitrate.value;
            }

            GM_setValue('lastSelectedFormat', selectedFormat);
            
            closeDialog(dialog, backdrop);
              try {
                await downloadWithMP3YouTube(videoUrl, selectedFormat, quality);            
            } catch (error) {                
                console.error('Download error:', error);
                updateOverlay('Download Failed', `Error: ${error.message}`, null, null, true);
                setTimeout(removeOverlay, 2500);
            }
        });
    }
    
    function insertDownloadButton() {
        const targetSelector = '#owner';
        const target = document.querySelector(targetSelector);
        
        if (target && !document.querySelector('.ytddl-download-btn')) {
            const downloadButton = createDownloadButton();
            target.appendChild(downloadButton);
        }
    }
    const observer = new MutationObserver(() => {
        if (window.location.pathname.includes('/watch')) {
            insertDownloadButton();
        }
    });
    
    observer.observe(document.body, { childList: true, subtree: true });
    insertDownloadButton();
    
    window.addEventListener('yt-navigate-finish', () => {
        insertDownloadButton();
    });
})();