Youtube Video Downloader 2025

Download Youtube videos in various formats. Download multiple videos at once.

当前为 2025-04-22 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Youtube Video Downloader 2025
// @namespace    http://tampermonkey.net/
// @author       fb
// @version      1.3.1
// @description  Download Youtube videos in various formats. Download multiple videos at once.
// @match        https://www.youtube.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      p.oceansaver.in
// @license      GPL-3.0-or-later
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    const STORAGE_FORMAT = 'selectedFormat';
    const STORAGE_DOWNLOADS = 'ytDownloads';
    const UI_WRAPPER_ID = 'yt-downloader-wrapper';
    const FORMAT_BUTTON_ID = 'yt-downloader-format-button';
    const FORMAT_POPUP_ID = 'yt-downloader-format-popup';
    const COMBINED_BUTTON_ID = 'yt-downloader-combined-button';
    const DOWNLOAD_ACTION_ID = 'yt-downloader-action-part';
    const DROPDOWN_ACTION_ID = 'yt-downloader-dropdown-part';
    const DOWNLOAD_POPUP_ID = 'download-dropdown-popup';
    const POLL_INTERVALS = {};

    // Format definitions
    const FORMAT_GROUPS = [
        { label: 'Audio', options: [['mp3','MP3'],['m4a','M4A'],['webm','WEBM'],['aac','AAC'],['flac','FLAC'],['opus','OPUS'],['ogg','OGG'],['wav','WAV']] },
        { label: 'Video', options: [['360','MP4 (360p)'],['480','MP4 (480p)'],['720','MP4 (720p)'],['1080','MP4 (1080p)'],['1440','MP4 (1440p)'],['4k','WEBM (4K)']] }
    ];
    const DEFAULT_FORMAT = '1080';

    function getFormatText(value) {
        for (const group of FORMAT_GROUPS) {
            for (const [val, text] of group.options) {
                if (val === value) return text;
            }
        }
        return 'Select Format';
    }

    function isDarkTheme() {
        return document.documentElement.hasAttribute('dark');
    }

    function isYouTubeLiveStream() {
        const ypr = window.ytInitialPlayerResponse || {};
        return !!(
            ypr.videoDetails?.isLiveContent === true ||
            ypr.microformat?.playerMicroformatRenderer?.liveBroadcastDetails ||
            document.querySelector('meta[itemprop="isLiveBroadcast"][content="True"]') ||
            document.querySelector('.ytp-live')
        );
    }

    function checkPageAndInjectUI() {
        const existing = document.getElementById(UI_WRAPPER_ID);
        const container = document.querySelector('#end');

        if (container && !existing) injectUI(container);
        else if (!container && existing) removeUI();

        updateUIState();
    }

    document.addEventListener('yt-navigate-finish', checkPageAndInjectUI);
    window.addEventListener('load', checkPageAndInjectUI);

    function updateUIState() {
        const wrapper = document.getElementById(UI_WRAPPER_ID);
        if (!wrapper) return;
        const isWatchOrShorts = window.location.pathname === '/watch' || window.location.pathname.startsWith('/shorts/');
        const disabled = !isWatchOrShorts || isYouTubeLiveStream();

        const formatButton = wrapper.querySelector(`#${FORMAT_BUTTON_ID}`);
        const downloadActionPart = wrapper.querySelector(`#${DOWNLOAD_ACTION_ID}`);

        if(formatButton) {
             formatButton.disabled = disabled;
             formatButton.style.opacity = disabled ? 0.6 : 1;
             formatButton.style.cursor = disabled ? 'not-allowed' : 'var(--btn-cursor)';
        }
        if(downloadActionPart) {
            downloadActionPart.classList.toggle('disabled', disabled);
            downloadActionPart.style.pointerEvents = disabled ? 'none' : 'auto';
        }
    }

    function injectUI(container) {
        if (document.getElementById(UI_WRAPPER_ID)) return;
        const wrapper = document.createElement('div');
        wrapper.id = UI_WRAPPER_ID;
        wrapper.style.display = 'flex';
        wrapper.style.alignItems = 'center';
        wrapper.style.marginRight = '10px';
        wrapper.style.position = 'relative';

        // --- Format Button ---
        const formatButton = document.createElement('button');
        formatButton.id = FORMAT_BUTTON_ID;
        formatButton.style.marginRight = '8px';
        const savedFormat = GM_getValue(STORAGE_FORMAT, DEFAULT_FORMAT);
        formatButton.dataset.value = savedFormat;
        formatButton.textContent = getFormatText(savedFormat);
        formatButton.addEventListener('click', toggleFormatPopup);
        wrapper.appendChild(formatButton);

        // --- Combined Download/Dropdown Button ---
        const combinedBtn = document.createElement('div');
        combinedBtn.id = COMBINED_BUTTON_ID;
        combinedBtn.style.display = 'inline-flex';
        combinedBtn.style.alignItems = 'stretch';
        combinedBtn.style.height = '36px';
        combinedBtn.style.borderRadius = 'var(--btn-radius)';
        combinedBtn.style.backgroundColor = 'var(--btn-bg)';
        combinedBtn.style.cursor = 'default';
        combinedBtn.style.position = 'relative';

        
        const downloadPart = document.createElement('div');
        downloadPart.id = DOWNLOAD_ACTION_ID;
        downloadPart.title = 'Download Video/Audio';
        downloadPart.style.display = 'inline-flex';
        downloadPart.style.alignItems = 'center';
        downloadPart.style.padding = '0 12px 0 8px';
        downloadPart.style.cursor = 'var(--btn-cursor)';
        downloadPart.style.transition = 'background-color .2s ease';
        downloadPart.style.borderRadius = 'var(--btn-radius) 0 0 var(--btn-radius)';

        // Create SVG element programmatically
        const downloadSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        downloadSvg.setAttribute('viewBox', '0 0 24 24');
        downloadSvg.setAttribute('width', '24');
        downloadSvg.setAttribute('height', '24');
        downloadSvg.style.marginRight = '6px';
        downloadSvg.style.fill = 'none';
        downloadSvg.style.stroke = 'currentColor';
        downloadSvg.style.strokeWidth = '1.5';
        downloadSvg.style.strokeLinecap = 'round';
        downloadSvg.style.strokeLinejoin = 'round';

        const path1 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
        path1.setAttribute('d', 'M12 4v12');
        const path2 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
        path2.setAttribute('d', 'M8 12l4 4 4-4');
        const path3 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
        path3.setAttribute('d', 'M4 18h16');

        downloadSvg.appendChild(path1);
        downloadSvg.appendChild(path2);
        downloadSvg.appendChild(path3);

        // Create span element programmatically
        const downloadSpan = document.createElement('span');
        downloadSpan.textContent = 'Download';

        // Append elements
        downloadPart.appendChild(downloadSvg);
        downloadPart.appendChild(downloadSpan);

        downloadPart.addEventListener('click', startDownload);
        downloadPart.addEventListener('mouseenter', () => { if (!downloadPart.classList.contains('disabled')) downloadPart.style.backgroundColor = 'var(--btn-hover-bg)'; });
        downloadPart.addEventListener('mouseleave', () => { downloadPart.style.backgroundColor = 'transparent'; });


        const separator = document.createElement('div');
        separator.style.width = '1px';
        separator.style.backgroundColor = 'var(--separator-color)';
        separator.style.height = '20px';
        separator.style.alignSelf = 'center';

        const dropdownPart = document.createElement('div');
        dropdownPart.id = DROPDOWN_ACTION_ID;
        dropdownPart.dataset.count = '0';
        dropdownPart.title = 'Show active downloads';
        dropdownPart.style.display = 'inline-flex';
        dropdownPart.style.alignItems = 'center';
        dropdownPart.style.justifyContent = 'center';
        dropdownPart.style.padding = '0 10px';
        dropdownPart.style.cursor = 'var(--btn-cursor)';
        dropdownPart.style.position = 'relative';
        dropdownPart.style.transition = 'background-color .2s ease';
        dropdownPart.style.borderRadius = '0 var(--btn-radius) var(--btn-radius) 0';
        const darkTheme = isDarkTheme();

        // Create SVG element programmatically
        const dropdownSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        dropdownSvg.setAttribute('width', '10');
        dropdownSvg.setAttribute('height', '7');
        dropdownSvg.setAttribute('viewBox', '0 0 10 7');
        dropdownSvg.style.fill = darkTheme ? '#fff' : '#000';

        const dropdownPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
        dropdownPath.setAttribute('d', 'M0 0l5 7 5-7z');
        dropdownSvg.appendChild(dropdownPath);

        // Append SVG
        dropdownPart.appendChild(dropdownSvg);

        dropdownPart.addEventListener('click', toggleDownloadPopup);
        dropdownPart.addEventListener('mouseenter', () => { dropdownPart.style.backgroundColor = 'var(--btn-hover-bg)'; });
        dropdownPart.addEventListener('mouseleave', () => { dropdownPart.style.backgroundColor = 'transparent'; });

        combinedBtn.appendChild(downloadPart);
        combinedBtn.appendChild(separator);
        combinedBtn.appendChild(dropdownPart);
        wrapper.appendChild(combinedBtn);

        container.insertAdjacentElement('afterbegin', wrapper);

        const style = document.createElement('style');

        style.textContent = `
:root {
    --btn-bg: ${darkTheme ? "#272727" : "#f2f2f2"};
    --btn-hover-bg: ${darkTheme ? "#3f3f3f" : "#e5e5e5"};
    --btn-color: ${darkTheme ? "#fff" : "#000"};
    --btn-radius: 18px;
    --btn-padding: 0 12px;
    --btn-font: 500 14px/36px "Roboto", "Arial", sans-serif;
    --btn-cursor: pointer;
    --progress-bg: ${darkTheme ? "#3f3f3f" : "#e5e5e5"};
    --progress-fill-color: #2196F3;
    --progress-text-color: ${darkTheme ? "#fff" : "#000"};
    --popup-bg: ${darkTheme ? "#212121" : "#fff"};
    --popup-border: ${darkTheme ? "#444" : "#ccc"};
    --popup-text: ${darkTheme ? "#fff" : "#030303"};
    --badge-bg: #cc0000;
    --badge-text: #fff;
    --separator-color: ${darkTheme ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'};
    --popup-radius: 6px;
    --popup-shadow: 0 4px 12px rgba(0,0,0,0.15);
}

/* Format Button Styling */
#${FORMAT_BUTTON_ID} {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: var(--btn-color);
    background-color: var(--btn-bg);
    border: none;
    border-radius: var(--btn-radius);
    padding: 0 12px;
    padding-right: 30px;
    white-space: nowrap;
    text-transform: none;
    font: var(--btn-font);
    cursor: var(--btn-cursor);
    transition: background-color .2s ease;
    height: 36px;
    width: 130px;
    box-sizing: border-box;
    position: relative;
    box-shadow: none;
}
#${FORMAT_BUTTON_ID}:disabled {
    cursor: not-allowed;
    opacity: 0.6;
}
#${FORMAT_BUTTON_ID}:hover:not(:disabled) {
    background-color: var(--btn-hover-bg);
}
/* Dropdown Arrow for Format Button */
#${FORMAT_BUTTON_ID}::after {
    content: '';
    position: absolute;
    right: 12px; /* Position arrow within the padding */
    top: 50%;
    transform: translateY(-50%);
    width: 0;
    height: 0;
    border-left: 5px solid transparent;
    border-right: 5px solid transparent;
    border-top: 7px solid var(--btn-color);
}

/* Combined Button Styling */
#${COMBINED_BUTTON_ID} {
    color: var(--btn-color);
    font: var(--btn-font);
    line-height: 36px;
}

#${DOWNLOAD_ACTION_ID}, #${DROPDOWN_ACTION_ID} {
    background-color: transparent;
}

#${DOWNLOAD_ACTION_ID} {
    color: inherit;
}
#${DOWNLOAD_ACTION_ID} svg {
    stroke: currentColor;
}
#${DOWNLOAD_ACTION_ID}.disabled {
    opacity: 0.6;
    cursor: not-allowed;
}
#${DOWNLOAD_ACTION_ID}.disabled:hover {
    background-color: transparent !important;
}

#${DROPDOWN_ACTION_ID} {
    color: inherit;
}
#${DROPDOWN_ACTION_ID} svg {
    fill: currentColor;
}

#${DROPDOWN_ACTION_ID}::after {
    content: attr(data-count);
    position: absolute;
    top: 2px;
    right: -8px;
    background-color: var(--badge-bg);
    color: var(--badge-text);
    border-radius: 50%;
    min-width: 16px;
    height: 16px;
    padding: 0 3px;
    font-size: 10px;
    line-height: 16px;
    text-align: center;
    font-weight: bold;
    display: none;
    font-family: "Roboto", "Arial", sans-serif;
    box-sizing: border-box;
    z-index: 2;
}
#${DROPDOWN_ACTION_ID}[data-count]:not([data-count="0"])::after {
    display: inline-block;
}

/* General Popup Styling */
#${FORMAT_POPUP_ID}, #${DOWNLOAD_POPUP_ID} {
    position: absolute;
    background: var(--popup-bg);
    color: var(--popup-text);
    border: 1px solid var(--popup-border);
    border-radius: var(--popup-radius);
    box-shadow: var(--popup-shadow);
    padding: 10px;
    z-index: 10000;
    max-height: 350px;
    overflow-y: auto;
}

/* Format Popup Specifics */
#${FORMAT_POPUP_ID} {
    width: 200px;
}
.format-group-label {
    font-weight: bold;
    font-size: 12px;
    color: ${darkTheme ? '#aaa' : '#555'};
    margin-top: 8px;
    margin-bottom: 4px;
    padding-left: 5px;
    text-transform: uppercase;
}
.format-group-label:first-child {
    margin-top: 0;
}
.format-item {
    display: block;
    width: 100%;
    padding: 6px 10px;
    font-size: 14px;
    cursor: pointer;
    border-radius: 4px;
    box-sizing: border-box;
    text-align: left;
    background: none;
    border: none;
    color: inherit;
}
.format-item:hover {
    background-color: var(--btn-hover-bg);
}
.format-item.selected {
    font-weight: bold;
    background-color: rgba(0, 100, 255, 0.1);
}


/* Download Popup Specifics */
#${DOWNLOAD_POPUP_ID} {
    width: 280px;
}
.download-item {
    margin-bottom: 12px;
    padding-bottom: 8px;
    border-bottom: 1px solid var(--popup-border);
}
.download-item:last-child {
    margin-bottom: 0;
    border-bottom: none;
}
.progress-bar {
    width: 100%;
    height: 18px;
    background-color: var(--progress-bg);
    border-radius: 9px;
    overflow: hidden;
    position: relative;
    margin-top: 4px;
}
.progress-fill {
    height: 100%;
    width: 0%;
    background-color: var(--progress-fill-color);
    transition: width 0.3s ease-in-out;
    display: flex;
    align-items: center;
    justify-content: center;
}
.progress-text {
    position: absolute; top: 0; left: 0; right: 0; bottom: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--progress-text-color);
    font: var(--btn-font);
    font-size: 11px;
    line-height: 18px;
    white-space: nowrap;
    z-index: 1;
}
.download-item-title {
    font-size: 13px;
    font-weight: 500;
    margin-bottom: 2px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    display: block;
}
.download-item-format {
    font-size: 11px;
    color: ${darkTheme ? '#aaa' : '#555'};
    display: block;
    margin-bottom: 4px;
}
.no-downloads-message {
    font-size: 15px;
    color: ${darkTheme ? '#aaa' : '#555'};
    text-align: center;
    padding: 10px 0;
}
        `;

        document.head.appendChild(style);

        resumeDownloads();
        updateDownloadCountBadge();
        updateUIState();
    }

    function removeUI() {
        const w = document.getElementById(UI_WRAPPER_ID);
        if (w) w.remove();
        // Remove both popups if they exist
        const formatPopup = document.getElementById(FORMAT_POPUP_ID);
        if (formatPopup) formatPopup.remove();
        const downloadPopup = document.getElementById(DOWNLOAD_POPUP_ID);
        if (downloadPopup) downloadPopup.remove();
    }

    function startDownload() {
        const downloadActionPart = document.getElementById(DOWNLOAD_ACTION_ID);
        if (downloadActionPart && downloadActionPart.classList.contains('disabled')) return;

        const formatButton = document.getElementById(FORMAT_BUTTON_ID);
        const fmt = formatButton.dataset.value;
        const formatText = formatButton.textContent;
        const videoUrl = encodeURIComponent(location.href);
        const initUrl = `https://p.oceansaver.in/ajax/download.php?format=${fmt}&url=${videoUrl}`;
        const id = Date.now().toString();
        const title = document.querySelector('h1.ytd-watch-metadata #video-title, h1.title.ytd-video-primary-info-renderer')?.textContent.trim() || 'YouTube Video';

        GM_xmlhttpRequest({
            method: 'GET', url: initUrl, responseType: 'json',
            onload(res) {
                const data = res.response;
                if (!data?.success) return alert('Failed to initialize');
                const downloads = GM_getValue(STORAGE_DOWNLOADS, []);
                downloads.push({ id, title, format: formatText, progress_url: data.progress_url, progress: 0, status: 'in_progress' });
                GM_setValue(STORAGE_DOWNLOADS, downloads);
                renderDownloadPopup(); // Update download popup if open
                updateDownloadCountBadge();
                pollProgress(id);
            },
            onerror() { alert('Network error'); }
        });
    }

    function pollProgress(id) {
        const downloads = GM_getValue(STORAGE_DOWNLOADS, []);
        const dl = downloads.find(d=>d.id===id);
        if (!dl || dl.status !== 'in_progress') return;

        if (POLL_INTERVALS[id]) clearInterval(POLL_INTERVALS[id]);

        const interval = setInterval(()=>{
            const currentDownloads = GM_getValue(STORAGE_DOWNLOADS, []);
            const currentDl = currentDownloads.find(d=>d.id===id);
            if (!currentDl || currentDl.status !== 'in_progress') {
                 console.log(`Stopping poll for ${id}, status changed.`);
                 clearInterval(interval);
                 delete POLL_INTERVALS[id];
                 renderDownloadPopup();
                 updateDownloadCountBadge();
                 return;
            }

            GM_xmlhttpRequest({ method:'GET', url: dl.progress_url, responseType:'json', onload(res){
                    const p = res.response;
                    const all = GM_getValue(STORAGE_DOWNLOADS, []);
                    const obj = all.find(x=>x.id===id);
                    if (!obj) {
                        clearInterval(interval); delete POLL_INTERVALS[id];
                        updateDownloadCountBadge();
                        return;
                    }
                    if (!p) {
                        console.warn(`Empty poll response for ${id}`);
                        return;
                    }
                    let statusChanged = false;
                    if (p.success) {
                        clearInterval(interval); delete POLL_INTERVALS[id];
                        obj.progress=100; obj.status='completed'; obj.download_url=p.download_url;
                        statusChanged = true;
                        GM_setValue(STORAGE_DOWNLOADS, all);
                        renderDownloadPopup();
                        triggerFileDownload(p.download_url);
                    } else if (p.error) {
                        clearInterval(interval); delete POLL_INTERVALS[id];
                        obj.status = 'error'; obj.errorMsg = p.error;
                        statusChanged = true;
                        GM_setValue(STORAGE_DOWNLOADS, all);
                        renderDownloadPopup();
                        console.error(`Download ${id} failed: ${p.error}`);
                    } else {
                        const percent = p.progress ? Math.min(Math.round(p.progress/10),100) : obj.progress;
                        if (obj.progress !== percent || obj.status !== 'in_progress') {
                            obj.progress = percent;
                            obj.status = 'in_progress';
                            GM_setValue(STORAGE_DOWNLOADS, all);
                            renderDownloadPopup();
                        }
                    }
                    if (statusChanged) {
                        updateDownloadCountBadge();
                    }
                },
                onerror(){
                    clearInterval(interval); delete POLL_INTERVALS[id];
                    const all = GM_getValue(STORAGE_DOWNLOADS, []);
                    const obj = all.find(x=>x.id===id);
                    if(obj) {
                        obj.status = 'error'; obj.errorMsg = 'Network error during polling';
                        GM_setValue(STORAGE_DOWNLOADS, all);
                        renderDownloadPopup();
                        updateDownloadCountBadge();
                    }
                    console.error(`Network error polling ${id}`);
                }
            });
        }, 2000);
        POLL_INTERVALS[id] = interval;
    }

    function triggerFileDownload(url) {
        const a = document.createElement('a'); a.href=url; a.download=''; document.body.appendChild(a);
        a.click(); a.remove();
    }

    // --- Popup Toggle Functions ---

    function toggleFormatPopup() {
        let popup = document.getElementById(FORMAT_POPUP_ID);
        if (popup) { popup.remove(); return; }

        // Close download popup if open
        const downloadPopup = document.getElementById(DOWNLOAD_POPUP_ID);
        if (downloadPopup) downloadPopup.remove();

        const wrapper = document.getElementById(UI_WRAPPER_ID);
        const formatButton = document.getElementById(FORMAT_BUTTON_ID);
        if (!wrapper || !formatButton) return;

        popup = document.createElement('div');
        popup.id = FORMAT_POPUP_ID;
        wrapper.appendChild(popup);
        renderFormatPopup();

        // Position popup below format button
        const buttonRect = formatButton.getBoundingClientRect();
        const wrapperRect = wrapper.getBoundingClientRect();
        popup.style.top = (buttonRect.bottom - wrapperRect.top + 5) + 'px';
        popup.style.left = (buttonRect.left - wrapperRect.left) + 'px';

        setTimeout(() => {
            document.addEventListener('click', handleClickOutsideFormatPopup, { capture: true, once: true });
        }, 0);
    }

    function toggleDownloadPopup() {
        let popup = document.getElementById(DOWNLOAD_POPUP_ID);
        if (popup) { popup.remove(); return; }

        // Close format popup if open
        const formatPopup = document.getElementById(FORMAT_POPUP_ID);
        if (formatPopup) formatPopup.remove();

        const wrapper = document.getElementById(UI_WRAPPER_ID);
        const combinedButton = document.getElementById(COMBINED_BUTTON_ID);
        if (!wrapper || !combinedButton) return;

        popup = document.createElement('div');
        popup.id = DOWNLOAD_POPUP_ID;
        wrapper.appendChild(popup);
        renderDownloadPopup();

        // Position popup below combined button, aligned right
        const buttonRect = combinedButton.getBoundingClientRect();
        const wrapperRect = wrapper.getBoundingClientRect();
        popup.style.top = (buttonRect.bottom - wrapperRect.top + 5) + 'px';
        popup.style.right = (wrapperRect.right - buttonRect.right) + 'px';

        setTimeout(() => {
            document.addEventListener('click', handleClickOutsideDownloadPopup, { capture: true, once: true });
        }, 0);
    }

    // --- Popup Click Outside Handlers ---

    function handleClickOutsideFormatPopup(event) {
        const popup = document.getElementById(FORMAT_POPUP_ID);
        const button = document.getElementById(FORMAT_BUTTON_ID);
        if (popup && !popup.contains(event.target) && !button.contains(event.target)) {
            popup.remove();
        } else if (popup) {
            // Re-attach listener if click was inside popup or on button
            document.addEventListener('click', handleClickOutsideFormatPopup, { capture: true, once: true });
        }
    }

    function handleClickOutsideDownloadPopup(event) {
        const popup = document.getElementById(DOWNLOAD_POPUP_ID);
        const button = document.getElementById(DROPDOWN_ACTION_ID); // Check against the dropdown part
        if (popup && !popup.contains(event.target) && !button.contains(event.target)) {
            popup.remove();
        } else if (popup) {
            document.addEventListener('click', handleClickOutsideDownloadPopup, { capture: true, once: true });
        }
    }

    // --- Popup Render Functions ---

    function renderFormatPopup() {
        const popup = document.getElementById(FORMAT_POPUP_ID);
        if (!popup) return;
        popup.textContent = '';
        const currentFormat = GM_getValue(STORAGE_FORMAT, DEFAULT_FORMAT);

        FORMAT_GROUPS.forEach(group => {
            const groupLabel = document.createElement('div');
            groupLabel.className = 'format-group-label';
            groupLabel.textContent = group.label;
            popup.appendChild(groupLabel);

            group.options.forEach(([value, text]) => {
                const item = document.createElement('button');
                item.className = 'format-item';
                item.textContent = text;
                item.dataset.value = value;
                if (value === currentFormat) {
                    item.classList.add('selected');
                }
                item.onclick = () => {
                    GM_setValue(STORAGE_FORMAT, value);
                    const formatButton = document.getElementById(FORMAT_BUTTON_ID);
                    if (formatButton) {
                        formatButton.textContent = text;
                        formatButton.dataset.value = value;
                    }
                    popup.remove();
                };
                popup.appendChild(item);
            });
        });
    }


    function renderDownloadPopup() {
        const popup = document.getElementById(DOWNLOAD_POPUP_ID);
        if (!popup) return;
        popup.textContent = '';

        const downloads = GM_getValue(STORAGE_DOWNLOADS, [])
                         .filter(d => d.status === 'in_progress' || d.status === 'error')
                         .sort((a, b) => (b.id - a.id));

        if (!downloads.length) {
            const noDownloadsMsg = document.createElement('div');
            noDownloadsMsg.className = 'no-downloads-message';
            noDownloadsMsg.textContent = 'No active downloads.';
            popup.appendChild(noDownloadsMsg);
            return;
        }

        downloads.forEach(d => {
            const item = document.createElement('div');
            item.className = 'download-item';

            const titleDiv = document.createElement('div');
            titleDiv.className = 'download-item-title';
            titleDiv.textContent = d.title || `Download ${d.id}`;
            titleDiv.title = d.title || `Download ${d.id}`;
            item.appendChild(titleDiv);

            const formatDiv = document.createElement('div');
            formatDiv.className = 'download-item-format';
            formatDiv.textContent = d.format || 'Unknown Format';
            item.appendChild(formatDiv);

            if (d.status === 'in_progress') {
                const bar = document.createElement('div'); bar.className = 'progress-bar';
                const fill = document.createElement('div'); fill.className = 'progress-fill';
                fill.style.width = `${d.progress}%`;
                bar.appendChild(fill);
                const txt = document.createElement('div'); txt.className = 'progress-text';
                txt.textContent = `${d.progress}%`;
                bar.appendChild(txt);
                item.appendChild(bar);
            } else if (d.status === 'error') {
                const errorDiv = document.createElement('div');
                errorDiv.style.color = '#f44336'; errorDiv.style.fontSize = '12px';
                errorDiv.textContent = `Error: ${d.errorMsg || 'Unknown error'}`;
                item.appendChild(errorDiv);
            }
            popup.appendChild(item);
        });

         if (downloads.some(d => d.status === 'error')) {
            const clearButton = document.createElement('button');
            clearButton.textContent = 'Clear Errors';
            clearButton.style.marginTop = '10px';
            clearButton.style.fontSize = '12px';
            clearButton.style.padding = '4px 8px';
            clearButton.style.backgroundColor = 'var(--btn-bg)';
            clearButton.style.color = 'var(--btn-color)';
            clearButton.style.border = 'none';
            clearButton.style.borderRadius = '4px';
            clearButton.style.cursor = 'pointer';
            clearButton.onmouseover = () => clearButton.style.backgroundColor = 'var(--btn-hover-bg)';
            clearButton.onmouseout = () => clearButton.style.backgroundColor = 'var(--btn-bg)';

            clearButton.onclick = () => {
                const allDownloads = GM_getValue(STORAGE_DOWNLOADS, []);
                const keptDownloads = allDownloads.filter(dl => dl.status !== 'error');
                GM_setValue(STORAGE_DOWNLOADS, keptDownloads);
                renderDownloadPopup();
                updateDownloadCountBadge(); // Badge only shows 'in_progress', errors don't count
            };
            popup.appendChild(clearButton);
        }
    }

    function updateDownloadCountBadge() {
        const dropdownPart = document.getElementById(DROPDOWN_ACTION_ID);
        if (!dropdownPart) return;

        const downloads = GM_getValue(STORAGE_DOWNLOADS, []);
        const activeCount = downloads.filter(d => d.status === 'in_progress').length;

        dropdownPart.dataset.count = activeCount.toString();
    }

    function resumeDownloads() {
        const downloads = GM_getValue(STORAGE_DOWNLOADS, []).filter(d => d.status === 'in_progress');
        console.log(`Resuming ${downloads.length} downloads.`);
        downloads.forEach(d => {
            if (!POLL_INTERVALS[d.id]) {
                 pollProgress(d.id);
            }
        });
    }
})();