yt-dlp Python-Downloader

Unified yt-dlp downloader - generates cross-platform Python scripts for video, audio, subtitles

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         yt-dlp Python-Downloader
// @namespace    https://greasyfork.org/en/users/1462137-piknockyou
// @version      9.2
// @author       Piknockyou (vibe-coded)
// @license      AGPL-3.0
// @description  Unified yt-dlp downloader - generates cross-platform Python scripts for video, audio, subtitles
//
// ═══════════════════════════════════════════════════════════════
// CHANGELOG
// ═══════════════════════════════════════════════════════════════
// v9.2
// - Fixed: Submenu no longer closes when moving mouse from row to submenu (bridged hover gap)
// - Fixed: Selected buttons (Merge/Sep) no longer turn gray on hover
//
// v9.1
// - Fixed: JavaScript booleans (true/false) now correctly converted to Python (True/False)
// - Fixed: Generated scripts now execute properly on all platforms
//
// v9.0
// - Complete rewrite: now generates Python scripts instead of batch files
// - Cross-platform: works on Windows, Mac, and Linux
// - Same functionality as v8.4 batch version
// - Python 3 required (pre-installed on Mac/Linux, install on Windows)
// - Uses only Python standard library (no pip packages needed)
//
// v8.4
// - Refactored: Sites now default to full mkvmerge flow (supports Separate)
// - Added: COMBINED_STREAM_SITES blacklist for sites without separate streams
// - Fixed: Reddit, Twitter, etc. now properly support Merge+Separate
// - Note: Add sites to COMBINED_STREAM_SITES if they fail with "format not available"
//
// v8.3
// - Fixed: Non-YouTube sites now use format with /best fallback
// - Fixed: Livestorm and other HLS sites work again (combined streams)
// - Fixed: Non-YouTube sites skip complex merge flow, use yt-dlp native merge
// - Note: Merge/Separate options still work for YouTube; non-YouTube gets best available
//
// v8.2
// - Changed: Error prompts now require Enter key (not any key) to close
// - Uses "set /p" instead of "pause" for consistent behavior
//
// v8.1
// - Fixed: Removed invalid %(lang)s placeholder from subtitle template
// - Fixed: Subtitle files now correctly named as filename.en.srt (not filename.NA.en.srt)
// - Note: yt-dlp automatically inserts language code before extension
//
// v8.0
// - Complete rewrite of batch generation for all merge/separate combinations
// - Fixed: Subtitle glob patterns now match actual downloaded files
// - Fixed: Separate file naming (no temp prefix in output names)
// - Fixed: Video/audio detection using ffprobe for ambiguous formats
// - Fixed: Selective cleanup - only deletes temp files, preserves separate copies
// - Fixed: All 64 combinations of V/A/S × None/Merge/Sep/Both now work correctly
// - Added: Proper language preservation in subtitle filenames
// - Added: Robust file identification with subroutine-based processing
//
// v7.10
// - Fixed: Merge ON now sets ALL components to Merge (user deselects unwanted)
// - Fixed: Merge OFF only removes from clicked component
// - Fixed: Auto-cleanup when only 1 merge remains
//
// v7.9
// - Fixed: Merge OFF now only removes merge from clicked component
// - Fixed: Other components keep their merge when one is deselected
// - Fixed: Auto-cleanup only when going from 2 merges to 1 (can't merge alone)
// - Fixed: Merge ON adds to clicked + other enabled components if first merge
//
// v7.8
// - Fixed: Merge is now a global toggle - on for all or off for all
// - Fixed: Clicking Merge OFF on any component removes merge from ALL
// - Fixed: Auto-removes merge when only 1 component remains with merge
// - Fixed: Audio was incorrectly included in merge when set to Separate only
//
// v7.7
// - Fixed: Merge button always clickable - clicking sets ALL to merge
// - Fixed: Merge persists on remaining components when one is set to None/Sep
// - Fixed: Merge auto-removes only when single component left with merge
//
// v7.6
// - Fixed: Subtitles no longer downloaded twice in separate mode
// - Fixed: Deactivating Merge on one deactivates Merge on ALL
// - Fixed: Merge button disabled when only one component is enabled
// - Fixed: Submenus now available even when None is selected
// - Fixed: Panel width fits content
//
// v7.5
// - Redesigned output modes: Merge and Separate are now independent toggles
// - Removed "Both" button - now click Merge AND Sep to get both behaviors
// - Clicking Merge auto-activates Merge for all other enabled components
// - Fixed: No longer crashes when only subs want merge but no media to merge into
// - Simplified case logic in batch generation
//
// v7.4
// - Fixed: Crash when subs=Both but no media being merged
// - Fixed: Single yt-dlp call for media+subs (no duplicate metadata fetch)
// - Improved needsMerge logic to require actual media track to merge
//
// v7.3
// - Fixed: Batch window now waits for user confirmation after mkvmerge
// - Fixed: Merge with subtitles no longer crashes
// - Fixed: "Both" mode now correctly creates merged + separate files
// - Restructured batch flow to always reach pause at end
//
// v7.2
// - Fixed: Subtitle-only mode now uses --skip-download correctly
// - Fixed: Merge/Both buttons disabled when only one component selected
// - Reordered output buttons: Merge/Separate/Both/None
// - Improved stability and error handling
//
// v7.1
// - Restructured menu: Video/Audio/Subtitle rows with inline output modes
// - Detail submenus open on hover (disabled if output=None)
// - Menu stays open until clicking outside (even after download)
// - Removed error message - just grays out download button
// - Original subtitle format now default and first in list
//
// v7.0
// - Complete redesign: unified Media Download menu
// - New output modes: None/Merge/Separate/Both for each component
// - Removed interactive console prompts (D/E/ALL/ALLS quick-picks)
// - Removed subtitle source selection (always fetches all available)
// - Subtitle language selection remains in .bat file
// - DRY: shared subtitle format options across all modes
//
// ═══════════════════════════════════════════════════════════════
// SIMPLE SITES - Just add @match lines here, nothing else needed!
// ═══════════════════════════════════════════════════════════════
// @match        *://www.arte.tv/*/videos/*
// @match        *://www.dailymotion.com/*
// @match        *://www.facebook.com/*
// @match        *://www.instagram.com/*
// @match        *://www.reddit.com/*
// @match        *://soundcloud.com/*
// @match        *://www.tagesschau.de/*
// @match        *://www.tiktok.com/*
// @match        *://www.twitch.tv/*
// @match        *://twitter.com/*
// @match        *://vimeo.com/*
// @match        *://www.youtube.com/*
// @match        *://x.com/*
//
// ═══════════════════════════════════════════════════════════════
// COMPLEX SITES - These need special extractors (defined below)
// ═══════════════════════════════════════════════════════════════
// @match        *://app.livestorm.co/*
// @icon         https://raw.githubusercontent.com/yt-dlp/yt-dlp/refs/heads/master/devscripts/logo.ico
// @grant        GM_setClipboard
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    // ═══════════════════════════════════════════════════════════════
    // SITE-SPECIFIC URL CONDITIONS (SPA Support)
    // ═══════════════════════════════════════════════════════════════
    const SITE_CONDITIONS = {
        'www.reddit.com': (url) => /\/r\/[^/]+\/comments\/[^/]+/.test(url),
    };

    // ═══════════════════════════════════════════════════════════════
    // COMPLEX SITES - Only define sites that need special URL extraction
    // ═══════════════════════════════════════════════════════════════
    const COMPLEX_SITES = {
        'app.livestorm.co': {
            cookieFile: 'app.livestorm.co_cookies.txt',
            extractUrl: () => {
                const match = document.documentElement.innerHTML.match(/https:\\?\/\\?\/cdn\.livestorm\.co\\?\/[^"'\s]+\.m3u8[^"'\s]*/);
                return match ? { url: match[0].replace(/\\/g, '') } : { error: 'Video URL not found. Make sure the video is loaded.' };
            }
        }
    };

    // ═══════════════════════════════════════════════════════════════
    // COMBINED STREAM SITES - Sites that only have muxed video+audio
    // These sites don't support Separate mode (no individual streams)
    // Add hostname here if download fails with "format not available"
    // ═══════════════════════════════════════════════════════════════
    const COMBINED_STREAM_SITES = [
        'app.livestorm.co',
        'cdn.livestorm.co',
        // Add more sites here as needed, e.g.:
        // 'some-hls-only-site.com',
    ];

    // Helper to check if current site has combined streams only
    const isCombinedStreamSite = () => COMBINED_STREAM_SITES.some(site =>
        window.location.hostname === site ||
        window.location.hostname.endsWith('.' + site)
    );

    //================================================================================
    // CONFIGURATION
    //================================================================================

    // ═══════════════════════════════════════════════════════════════
    // OUTPUT MODES - Merge and Separate are independent toggles
    // Internal values: 'none', 'merge', 'separate', 'merge-separate'
    // ═══════════════════════════════════════════════════════════════
    const OUTPUT_MODES = [
        { id: 'none', label: 'None', desc: 'Do not download this component' },
        { id: 'merge', label: 'Merge', desc: 'Include in merged file' },
        { id: 'separate', label: 'Separate', desc: 'Keep as standalone file' },
        { id: 'merge-separate', label: 'Merge+Sep', desc: 'Merged AND separate file' },
    ];

    // Helper to check if output includes merge
    const hasMergeFlag = (output) => output === 'merge' || output === 'merge-separate';
    const hasSeparateFlag = (output) => output === 'separate' || output === 'merge-separate';
    const isEnabled = (output) => output !== 'none';

    // ═══════════════════════════════════════════════════════════════
    // VIDEO OPTIONS
    // ═══════════════════════════════════════════════════════════════
    const VIDEO_QUALITIES = [
        { id: 'best', label: 'Best', format: 'bestvideo' },
        { id: '1080p', label: '1080p', format: 'bestvideo[height<=1080]' },
        { id: '720p', label: '720p', format: 'bestvideo[height<=720]' },
        { id: '480p', label: '480p', format: 'bestvideo[height<=480]' },
        { id: '360p', label: '360p', format: 'bestvideo[height<=360]' },
    ];

    const VIDEO_CODECS = [
        { id: 'default', label: 'Auto', sortArg: '', desc: 'Let yt-dlp choose best available' },
        { id: 'av1', label: 'AV1', sortArg: '-S "vcodec:av01"', desc: 'Most efficient, needs modern hardware' },
        { id: 'vp9', label: 'VP9', sortArg: '-S "vcodec:vp9"', desc: 'Good efficiency, wide support' },
        { id: 'h264', label: 'H.264', sortArg: '-S "+vcodec:avc"', desc: 'Maximum compatibility' },
    ];

    // ═══════════════════════════════════════════════════════════════
    // AUDIO OPTIONS
    // ═══════════════════════════════════════════════════════════════
    const AUDIO_QUALITIES = [
        { id: 'best', label: 'Best', format: 'bestaudio' },
        { id: 'worst', label: 'Smallest', format: 'worstaudio' },
    ];

    // ═══════════════════════════════════════════════════════════════
    // SUBTITLE OPTIONS (Original first)
    // ═══════════════════════════════════════════════════════════════
    const SUBTITLE_FORMATS = [
        { id: 'original', label: 'Original', convertArg: '', desc: 'Keep original format' },
        { id: 'srt', label: 'SRT', convertArg: '--convert-subs srt', desc: 'Universal, widely supported' },
        { id: 'vtt', label: 'VTT', convertArg: '--convert-subs vtt', desc: 'Web standard format' },
        { id: 'ass', label: 'ASS', convertArg: '--convert-subs ass', desc: 'Advanced styling support' },
    ];

    // ═══════════════════════════════════════════════════════════════
    // STORAGE KEYS
    // ═══════════════════════════════════════════════════════════════
    const STORAGE_KEYS = {
        VIDEO_QUALITY: 'ytdlp_video_quality',
        VIDEO_CODEC: 'ytdlp_video_codec',
        VIDEO_OUTPUT: 'ytdlp_video_output',
        AUDIO_QUALITY: 'ytdlp_audio_quality',
        AUDIO_OUTPUT: 'ytdlp_audio_output',
        SUBS_FORMAT: 'ytdlp_subs_format',
        SUBS_OUTPUT: 'ytdlp_subs_output',
    };

    // ═══════════════════════════════════════════════════════════════
    // DEFAULTS
    // ═══════════════════════════════════════════════════════════════
    const DEFAULTS = {
        VIDEO_QUALITY: 'best',
        VIDEO_CODEC: 'default',
        VIDEO_OUTPUT: 'merge',
        AUDIO_QUALITY: 'best',
        AUDIO_OUTPUT: 'merge',
        SUBS_FORMAT: 'original',
        SUBS_OUTPUT: 'none',
    };

    // ═══════════════════════════════════════════════════════════════
    // OVERWRITE EXISTING FILES
    // ═══════════════════════════════════════════════════════════════
    const FORCE_OVERWRITE = true;

    //================================================================================
    // STORAGE HELPERS
    //================================================================================

    function getStoredValue(key, defaultValue, validOptions = null) {
        try {
            const stored = GM_getValue(key, defaultValue);
            if (validOptions && !validOptions.some(v => v.id === stored)) {
                return defaultValue;
            }
            return stored;
        } catch (e) {
            return defaultValue;
        }
    }

    function setStoredValue(key, value) {
        try {
            GM_setValue(key, value);
        } catch (e) {
            console.warn('[yt-dlp] Failed to save setting:', e);
        }
    }

    // Getters
    const getVideoQuality = () => getStoredValue(STORAGE_KEYS.VIDEO_QUALITY, DEFAULTS.VIDEO_QUALITY, VIDEO_QUALITIES);
    const getVideoCodec = () => getStoredValue(STORAGE_KEYS.VIDEO_CODEC, DEFAULTS.VIDEO_CODEC, VIDEO_CODECS);
    const getVideoOutput = () => getStoredValue(STORAGE_KEYS.VIDEO_OUTPUT, DEFAULTS.VIDEO_OUTPUT, OUTPUT_MODES);
    const getAudioQuality = () => getStoredValue(STORAGE_KEYS.AUDIO_QUALITY, DEFAULTS.AUDIO_QUALITY, AUDIO_QUALITIES);
    const getAudioOutput = () => getStoredValue(STORAGE_KEYS.AUDIO_OUTPUT, DEFAULTS.AUDIO_OUTPUT, OUTPUT_MODES);
    const getSubsFormat = () => getStoredValue(STORAGE_KEYS.SUBS_FORMAT, DEFAULTS.SUBS_FORMAT, SUBTITLE_FORMATS);
    const getSubsOutput = () => getStoredValue(STORAGE_KEYS.SUBS_OUTPUT, DEFAULTS.SUBS_OUTPUT, OUTPUT_MODES);

    // Setters
    const setVideoQuality = (v) => setStoredValue(STORAGE_KEYS.VIDEO_QUALITY, v);
    const setVideoCodec = (v) => setStoredValue(STORAGE_KEYS.VIDEO_CODEC, v);
    const setVideoOutput = (v) => setStoredValue(STORAGE_KEYS.VIDEO_OUTPUT, v);
    const setAudioQuality = (v) => setStoredValue(STORAGE_KEYS.AUDIO_QUALITY, v);
    const setAudioOutput = (v) => setStoredValue(STORAGE_KEYS.AUDIO_OUTPUT, v);
    const setSubsFormat = (v) => setStoredValue(STORAGE_KEYS.SUBS_FORMAT, v);
    const setSubsOutput = (v) => setStoredValue(STORAGE_KEYS.SUBS_OUTPUT, v);

    // Helpers
    const findOption = (options, id) => options.find(o => o.id === id);
    const getLabel = (options, id) => findOption(options, id)?.label || options[0].label;
    const isYouTubeDomain = () => ['www.youtube.com', 'youtube.com', 'm.youtube.com'].includes(window.location.hostname);

    //================================================================================
    // VALIDATION & STATE HELPERS
    //================================================================================

    function getComponentStates() {
        const videoOut = getVideoOutput();
        const audioOut = getAudioOutput();
        const subsOut = getSubsOutput();

        const hasVideo = isEnabled(videoOut);
        const hasAudio = isEnabled(audioOut);
        const hasSubs = isEnabled(subsOut);

        const videoMerge = hasMergeFlag(videoOut);
        const audioMerge = hasMergeFlag(audioOut);
        const subsMerge = hasMergeFlag(subsOut);

        const videoSeparate = hasSeparateFlag(videoOut);
        const audioSeparate = hasSeparateFlag(audioOut);
        const subsSeparate = hasSeparateFlag(subsOut);

        const activeCount = [hasVideo, hasAudio, hasSubs].filter(Boolean).length;

        // Merge requires at least one MEDIA track (video or audio) to be merged
        // Subs alone cannot be merged - they need something to embed into
        const hasMediaMerge = (hasVideo && videoMerge) || (hasAudio && audioMerge);
        const needsMerge = hasMediaMerge;

        return {
            videoOut, audioOut, subsOut,
            hasVideo, hasAudio, hasSubs,
            videoMerge, audioMerge, subsMerge,
            videoSeparate, audioSeparate, subsSeparate,
            activeCount, needsMerge
        };
    }

    // Count how many components have merge flag
    function getMergeCount() {
        const videoOut = getVideoOutput();
        const audioOut = getAudioOutput();
        const subsOut = getSubsOutput();
        return [hasMergeFlag(videoOut), hasMergeFlag(audioOut), hasMergeFlag(subsOut)].filter(Boolean).length;
    }

    // Set ALL components to merge (preserving separate flags)
    function setAllToMerge() {
        const videoOut = getVideoOutput();
        const audioOut = getAudioOutput();
        const subsOut = getSubsOutput();

        setVideoOutput(hasSeparateFlag(videoOut) ? 'merge-separate' : 'merge');
        setAudioOutput(hasSeparateFlag(audioOut) ? 'merge-separate' : 'merge');
        setSubsOutput(hasSeparateFlag(subsOut) ? 'merge-separate' : 'merge');
    }

    // If only one component has merge, remove it (can't merge alone)
    function cleanupLoneMerge() {
        if (getMergeCount() === 1) {
            const videoOut = getVideoOutput();
            const audioOut = getAudioOutput();
            const subsOut = getSubsOutput();

            if (hasMergeFlag(videoOut)) {
                setVideoOutput(hasSeparateFlag(videoOut) ? 'separate' : 'none');
            }
            if (hasMergeFlag(audioOut)) {
                setAudioOutput(hasSeparateFlag(audioOut) ? 'separate' : 'none');
            }
            if (hasMergeFlag(subsOut)) {
                setSubsOutput(hasSeparateFlag(subsOut) ? 'separate' : 'none');
            }
        }
    }

    // Toggle merge for a component
    function toggleMerge(componentType, currentOutput, setter) {
        const hadMerge = hasMergeFlag(currentOutput);

        if (hadMerge) {
            // Clicking merge OFF: remove merge from THIS component only
            setter(hasSeparateFlag(currentOutput) ? 'separate' : 'none');
            // If only 1 merge remains after this, remove it too (can't merge alone)
            cleanupLoneMerge();
        } else {
            // Clicking merge ON: set ALL components to merge
            // User can deselect the ones they don't want
            setAllToMerge();
        }
    }

    // Toggle separate for a component
    function toggleSeparate(componentType, currentOutput, setter) {
        const hadMerge = hasMergeFlag(currentOutput);
        const hadSeparate = hasSeparateFlag(currentOutput);

        if (hadSeparate) {
            // Remove separate
            setter(hadMerge ? 'merge' : 'none');
        } else {
            // Add separate
            if (currentOutput === 'none') {
                setter('separate');
            } else {
                setter('merge-separate');
            }
        }
        // No need to cleanup here - separate doesn't affect merge count
    }

    // Set component to None
    function setToNone(setter) {
        setter('none');
        // After setting to none, check if only 1 merge left
        cleanupLoneMerge();
    }

    function validateSettings() {
        const state = getComponentStates();

        // Rule 1: At least one component must be non-None
        if (state.activeCount === 0) {
            return { valid: false };
        }

        return { valid: true };
    }

    //================================================================================
    // CONTEXT MENU
    //================================================================================

    const ContextMenu = {
        shadowHost: null,
        shadowRoot: null,
        element: null,
        isOpen: false,
        closeHandler: null,
        updateFunctions: [], // Store update functions for dynamic state

        getStyles() {
            return `
                :host { all: initial; }
                * { box-sizing: border-box; }

                .ytdlp-context-menu {
                    position: fixed;
                    background: #2d2d2d;
                    border: 1px solid #444;
                    border-radius: 8px;
                    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
                    padding: 8px;
                    z-index: 2147483647;
                    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
                    font-size: 13px;
                    color: #e0e0e0;
                    user-select: none;
                    pointer-events: auto;
                    display: none;
                    width: fit-content;
                }

                .ytdlp-context-menu.visible { display: block; }

                /* Component rows (Video, Audio, Subtitle) */
                .ytdlp-component-row {
                    padding: 8px 12px;
                    border-radius: 4px;
                    position: relative;
                    transition: background 0.15s;
                }

                .ytdlp-component-row:not(:last-child) {
                    border-bottom: 1px solid #3a3a3a;
                }

                .ytdlp-component-row:hover {
                    background: #3a3a3a;
                }



                .ytdlp-component-header {
                    display: flex;
                    align-items: center;
                    justify-content: space-between;
                    margin-bottom: 6px;
                }

                .ytdlp-component-title {
                    font-size: 11px;
                    font-weight: 600;
                    text-transform: uppercase;
                    letter-spacing: 0.5px;
                    color: #aaa;
                }

                .ytdlp-component-arrow {
                    font-size: 9px;
                    opacity: 0.5;
                    transition: opacity 0.15s;
                }

                .ytdlp-component-row:hover .ytdlp-component-arrow {
                    opacity: 1;
                }

                /* Output mode buttons */
                .ytdlp-output-modes {
                    display: flex;
                    gap: 4px;
                }

                .ytdlp-output-btn {
                    padding: 4px 8px;
                    font-size: 11px;
                    border-radius: 4px;
                    cursor: pointer;
                    background: #3a3a3a;
                    color: #999;
                    transition: all 0.15s;
                    border: 1px solid transparent;
                }

                .ytdlp-output-btn:hover:not(.disabled):not(.selected) {
                    background: #444;
                    color: #ccc;
                }

                .ytdlp-output-btn.selected {
                    background: #4a6da7;
                    color: #fff;
                    border-color: #5a7db7;
                }

                .ytdlp-output-btn.selected:hover {
                    background: #5a7db7;
                    border-color: #6a8dc7;
                }

                .ytdlp-output-btn.disabled {
                    opacity: 0.35;
                    cursor: not-allowed;
                    pointer-events: none;
                }

                .ytdlp-output-btn[data-tooltip] {
                    position: relative;
                }

                .ytdlp-output-btn[data-tooltip]:hover::before {
                    content: attr(data-tooltip);
                    position: absolute;
                    bottom: calc(100% + 6px);
                    left: 50%;
                    transform: translateX(-50%);
                    background: rgba(0, 0, 0, 0.9);
                    color: #fff;
                    padding: 4px 8px;
                    border-radius: 4px;
                    font-size: 10px;
                    white-space: nowrap;
                    z-index: 100;
                    pointer-events: none;
                }

                /* Detail submenu */
                .ytdlp-detail-submenu {
                    display: none;
                    position: absolute;
                    right: calc(100% + 4px);
                    top: 0;
                    background: #2d2d2d;
                    border: 1px solid #444;
                    border-radius: 8px;
                    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
                    padding: 8px;
                    min-width: 140px;
                }

                /* Invisible bridge to prevent hover loss when moving to submenu */
                .ytdlp-detail-submenu::before {
                    content: '';
                    position: absolute;
                    top: 0;
                    bottom: 0;
                    right: -8px;
                    width: 12px;
                }

                .ytdlp-component-row.submenu-open .ytdlp-detail-submenu {
                    display: block;
                }

                .ytdlp-detail-section {
                    padding: 4px 0;
                }

                .ytdlp-detail-section:not(:last-child) {
                    border-bottom: 1px solid #3a3a3a;
                    margin-bottom: 4px;
                    padding-bottom: 8px;
                }

                .ytdlp-detail-header {
                    padding: 4px 10px;
                    font-size: 10px;
                    text-transform: uppercase;
                    color: #888;
                    letter-spacing: 0.5px;
                }

                .ytdlp-detail-item {
                    padding: 6px 10px;
                    font-size: 12px;
                    cursor: pointer;
                    border-radius: 4px;
                    transition: background 0.15s;
                    display: flex;
                    align-items: center;
                    gap: 8px;
                }

                .ytdlp-detail-item:hover {
                    background: #3a3a3a;
                }

                .ytdlp-detail-item.selected::before {
                    content: '✓';
                    font-size: 10px;
                    color: #4CAF50;
                    width: 12px;
                }

                .ytdlp-detail-item:not(.selected)::before {
                    content: '';
                    width: 12px;
                    display: inline-block;
                }

                /* Simple menu row (Comments) */
                .ytdlp-menu-row {
                    display: flex;
                    align-items: center;
                    padding: 10px 12px;
                    border-radius: 4px;
                    cursor: pointer;
                    transition: background 0.15s;
                }

                .ytdlp-menu-row:hover {
                    background: #3a3a3a;
                }

                .ytdlp-menu-row:not(:last-child) {
                    border-bottom: 1px solid #3a3a3a;
                }

                /* Download button */
                .ytdlp-download-btn {
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    padding: 10px 12px;
                    margin-top: 4px;
                    background: #4CAF50;
                    color: #fff;
                    font-weight: 500;
                    font-size: 13px;
                    border-radius: 4px;
                    cursor: pointer;
                    transition: background 0.15s;
                }

                .ytdlp-download-btn:hover:not(.disabled) {
                    background: #43a047;
                }

                .ytdlp-download-btn.disabled {
                    background: #555;
                    cursor: not-allowed;
                    opacity: 0.6;
                }
            `;
        },

        init() {
            if (this.shadowHost) return;

            try {
                this.shadowHost = document.createElement('div');
                this.shadowHost.id = 'ytdlp-context-menu-host';
                Object.assign(this.shadowHost.style, {
                    position: 'fixed',
                    top: '0',
                    left: '0',
                    width: '0',
                    height: '0',
                    overflow: 'visible',
                    zIndex: '2147483647',
                    pointerEvents: 'none'
                });

                this.shadowRoot = this.shadowHost.attachShadow({ mode: 'open' });

                const style = document.createElement('style');
                style.textContent = this.getStyles();
                this.shadowRoot.appendChild(style);

                this.element = document.createElement('div');
                this.element.className = 'ytdlp-context-menu';
                this.shadowRoot.appendChild(this.element);

                document.body.appendChild(this.shadowHost);

                document.addEventListener('keydown', (e) => {
                    if (e.key === 'Escape' && this.isOpen) this.hide();
                }, true);
            } catch (e) {
                console.error('[yt-dlp] Failed to init context menu:', e);
            }
        },

        show(x, y, onDownload) {
            if (!this.shadowHost) this.init();
            if (!this.element) return;

            this.isOpen = true;
            this.element.textContent = '';
            this.updateFunctions = [];

            const showCodecOption = isYouTubeDomain();
            let downloadBtn = null;

            // Master update function - updates all dynamic states
            const updateAllStates = () => {
                this.updateFunctions.forEach(fn => {
                    try { fn(); } catch (e) { console.warn('[yt-dlp] Update error:', e); }
                });
                updateDownloadState();
            };

            // Helper to update download button state
            const updateDownloadState = () => {
                if (downloadBtn) {
                    const valid = validateSettings().valid;
                    downloadBtn.classList.toggle('disabled', !valid);
                }
            };

            // Helper to build a component row (Video, Audio, Subtitle)
            const buildComponentRow = (title, componentType, outputGetter, outputSetter, detailBuilder) => {
                const row = document.createElement('div');
                row.className = 'ytdlp-component-row';

                // Header with title and arrow
                const header = document.createElement('div');
                header.className = 'ytdlp-component-header';

                const titleEl = document.createElement('span');
                titleEl.className = 'ytdlp-component-title';
                titleEl.textContent = title;
                header.appendChild(titleEl);

                const arrow = document.createElement('span');
                arrow.className = 'ytdlp-component-arrow';
                arrow.textContent = '◀';
                header.appendChild(arrow);

                row.appendChild(header);

                // Output mode buttons: [Merge] [Sep] [None]
                const modesContainer = document.createElement('div');
                modesContainer.className = 'ytdlp-output-modes';

                const mergeBtn = document.createElement('div');
                mergeBtn.className = 'ytdlp-output-btn';
                mergeBtn.textContent = 'Merge';
                mergeBtn.setAttribute('data-tooltip', 'Include in merged file (syncs with other components)');

                const sepBtn = document.createElement('div');
                sepBtn.className = 'ytdlp-output-btn';
                sepBtn.textContent = 'Sep';
                sepBtn.setAttribute('data-tooltip', 'Keep as standalone file');

                const noneBtn = document.createElement('div');
                noneBtn.className = 'ytdlp-output-btn';
                noneBtn.textContent = 'None';
                noneBtn.setAttribute('data-tooltip', 'Do not download this component');

                mergeBtn.onclick = (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    toggleMerge(componentType, outputGetter(), outputSetter);
                    updateAllStates();
                };

                sepBtn.onclick = (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    toggleSeparate(componentType, outputGetter(), outputSetter);
                    updateAllStates();
                };

                noneBtn.onclick = (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    setToNone(outputSetter);
                    updateAllStates();
                };

                modesContainer.appendChild(mergeBtn);
                modesContainer.appendChild(sepBtn);
                modesContainer.appendChild(noneBtn);
                row.appendChild(modesContainer);

                // Detail submenu
                const submenu = document.createElement('div');
                submenu.className = 'ytdlp-detail-submenu';
                detailBuilder(submenu);
                row.appendChild(submenu);

                // Update function for this row
                const updateRowState = () => {
                    const currentOutput = outputGetter();
                    const isMerge = hasMergeFlag(currentOutput);
                    const isSeparate = hasSeparateFlag(currentOutput);
                    const isNone = currentOutput === 'none';

                    // Update button states - Merge and Sep can both be selected
                    // Merge is ALWAYS clickable (clicking sets all to merge)
                    mergeBtn.classList.toggle('selected', isMerge);
                    sepBtn.classList.toggle('selected', isSeparate);
                    noneBtn.classList.toggle('selected', isNone);
                };

                // Register update function
                this.updateFunctions.push(updateRowState);
                updateRowState();

                // Hover to open submenu (always available)
                row.addEventListener('mouseenter', () => {
                    row.classList.add('submenu-open');
                    requestAnimationFrame(() => {
                        try {
                            const rowRect = row.getBoundingClientRect();
                            const submenuRect = submenu.getBoundingClientRect();
                            submenu.style.top = '0';
                            submenu.style.bottom = '';
                            if (rowRect.top + submenuRect.height > window.innerHeight - 10) {
                                submenu.style.top = 'auto';
                                submenu.style.bottom = '0';
                            }
                        } catch (e) { /* ignore */ }
                    });
                });

                row.addEventListener('mouseleave', () => {
                    row.classList.remove('submenu-open');
                });

                return row;
            };

            // Helper to build detail items
            const buildDetailSection = (container, headerText, options, currentValueGetter, onSelect) => {
                const section = document.createElement('div');
                section.className = 'ytdlp-detail-section';

                const header = document.createElement('div');
                header.className = 'ytdlp-detail-header';
                header.textContent = headerText;
                section.appendChild(header);

                const items = [];

                options.forEach(opt => {
                    const item = document.createElement('div');
                    item.className = 'ytdlp-detail-item';
                    item.textContent = opt.label;

                    item.onclick = (e) => {
                        e.preventDefault();
                        e.stopPropagation();
                        items.forEach(el => el.classList.remove('selected'));
                        item.classList.add('selected');
                        onSelect(opt.id);
                    };

                    items.push(item);
                    section.appendChild(item);
                });

                // Update selection state
                const updateSelection = () => {
                    const currentValue = currentValueGetter();
                    items.forEach((item, index) => {
                        item.classList.toggle('selected', options[index].id === currentValue);
                    });
                };

                updateSelection();
                container.appendChild(section);
            };

            // ─────────────────────────────────────────────────────────────
            // VIDEO ROW
            // ─────────────────────────────────────────────────────────────
            const videoRow = buildComponentRow('VIDEO', 'video', getVideoOutput, setVideoOutput, (submenu) => {
                buildDetailSection(submenu, 'Quality', VIDEO_QUALITIES, getVideoQuality, setVideoQuality);
                if (showCodecOption) {
                    buildDetailSection(submenu, 'Codec', VIDEO_CODECS, getVideoCodec, setVideoCodec);
                }
            });
            this.element.appendChild(videoRow);

            // ─────────────────────────────────────────────────────────────
            // AUDIO ROW
            // ─────────────────────────────────────────────────────────────
            const audioRow = buildComponentRow('AUDIO', 'audio', getAudioOutput, setAudioOutput, (submenu) => {
                buildDetailSection(submenu, 'Quality', AUDIO_QUALITIES, getAudioQuality, setAudioQuality);
            });
            this.element.appendChild(audioRow);

            // ─────────────────────────────────────────────────────────────
            // SUBTITLE ROW
            // ─────────────────────────────────────────────────────────────
            const subtitleRow = buildComponentRow('SUBTITLE', 'subs', getSubsOutput, setSubsOutput, (submenu) => {
                buildDetailSection(submenu, 'Format', SUBTITLE_FORMATS, getSubsFormat, setSubsFormat);
            });
            this.element.appendChild(subtitleRow);

            // ─────────────────────────────────────────────────────────────
            // COMMENTS ROW
            // ─────────────────────────────────────────────────────────────
            const commentsRow = document.createElement('div');
            commentsRow.className = 'ytdlp-menu-row';
            commentsRow.textContent = '💬 Comments Only';
            commentsRow.onclick = (e) => {
                e.preventDefault();
                e.stopPropagation();
                onDownload('comments');
            };
            this.element.appendChild(commentsRow);

            // ─────────────────────────────────────────────────────────────
            // DOWNLOAD BUTTON
            // ─────────────────────────────────────────────────────────────
            downloadBtn = document.createElement('div');
            downloadBtn.className = 'ytdlp-download-btn';
            downloadBtn.textContent = '⬇ Download';

            downloadBtn.onclick = (e) => {
                e.preventDefault();
                e.stopPropagation();
                if (downloadBtn.classList.contains('disabled')) return;
                if (!validateSettings().valid) return;
                onDownload('media');
            };

            this.element.appendChild(downloadBtn);
            updateDownloadState();

            // Initial update of all states
            updateAllStates();

            // Position menu
            this.element.classList.add('visible');
            requestAnimationFrame(() => {
                try {
                    const rect = this.element.getBoundingClientRect();
                    let left = x - rect.width - 10;
                    let top = y;
                    if (left < 10) left = 10;
                    if (top + rect.height > window.innerHeight - 10) top = window.innerHeight - rect.height - 10;
                    if (top < 10) top = 10;
                    this.element.style.left = left + 'px';
                    this.element.style.top = top + 'px';
                } catch (e) {
                    this.element.style.left = '10px';
                    this.element.style.top = '10px';
                }
            });

            // Outside click handler (only way to close)
            if (this.closeHandler) {
                document.removeEventListener('mousedown', this.closeHandler, true);
            }
            const self = this;
            this.closeHandler = (e) => {
                if (!self.isOpen) return;
                try {
                    if (e.composedPath().includes(self.shadowHost)) return;
                } catch (err) {
                    // composedPath might fail in some edge cases
                    if (self.shadowHost.contains(e.target)) return;
                }
                self.hide();
            };
            setTimeout(() => document.addEventListener('mousedown', this.closeHandler, true), 100);
        },

        hide() {
            if (this.element) {
                this.element.classList.remove('visible');
            }
            this.isOpen = false;
            this.updateFunctions = [];
            if (this.closeHandler) {
                document.removeEventListener('mousedown', this.closeHandler, true);
                this.closeHandler = null;
            }
        },

        isVisible() { return this.isOpen; }
    };

    //================================================================================
    // PYTHON SCRIPT GENERATION
    //================================================================================

    const CONFIG_UI = {
        button: {
            size: 24,
            iconStyle: {
                shadow: { enabled: true, blur: 2, color: 'rgba(255, 255, 255, 0.8)' },
                background: { enabled: true, color: 'rgba(128, 128, 128, 0.25)', borderRadius: '50%' }
            },
            position: { vertical: 'bottom', horizontal: 'right', offsetX: 1, offsetY: 29 },
            opacity: { default: 0.15, hover: 1, active: 0.7 },
            scale: { default: 1, hover: 1.1, active: 0.95 },
            zIndex: 2147483646
        },
        timing: {
            doubleClickThreshold: 350,
            hideTemporarilyDuration: 5000,
            hoverCheckInterval: 100
        }
    };

    //================================================================================
    // GLOBAL STATE
    //================================================================================

    const hostname = window.location.hostname;
    const complex = COMPLEX_SITES[hostname];
    const siteCondition = SITE_CONDITIONS[hostname];
    const COOKIE_FILE = complex?.cookieFile || `${hostname}_cookies.txt`;

    let shadowRoot = null;
    let notificationElement = null;
    let containerElement = null;

    const STATE = {
        hidden: false,
        lastUrl: window.location.href,
        checkInterval: null
    };

    //================================================================================
    // HELPER FUNCTIONS
    //================================================================================

    const sanitize = (str, max = 80) =>
        (str || 'video').replace(/[^a-zA-Z0-9]/g, '_').replace(/_+/g, '_').substring(0, max);

    function getVideoInfo() {
        if (complex?.extractUrl) {
            const result = complex.extractUrl();
            if (result.error) return result;
            return { url: result.url, filename: sanitize(document.title), cookieFile: COOKIE_FILE };
        }
        return { url: window.location.href, filename: sanitize(document.title), cookieFile: COOKIE_FILE };
    }

    const getOverwriteFlag = () => FORCE_OVERWRITE ? '--force-overwrites' : '';

    // Helper to convert JS boolean to Python boolean string
    const pyBool = (val) => val ? 'True' : 'False';

    // ═══════════════════════════════════════════════════════════════
    // Python script header template
    // ═══════════════════════════════════════════════════════════════
    function getPythonHeader() {
        return `#!/usr/bin/env python3
"""
yt-dlp Media Downloader
Generated by yt-dlp Python-Downloader userscript v9.0
Cross-platform: Works on Windows, Mac, and Linux
Requires: Python 3.6+, yt-dlp
Optional: mkvmerge (for advanced merging), ffmpeg/ffprobe
"""

import subprocess
import sys
import os
import shutil
import random
from pathlib import Path
from glob import glob

# ════════════════════════════════════════════════════════════════════════════════
# HELPER FUNCTIONS
# ════════════════════════════════════════════════════════════════════════════════

def print_header(title):
    """Print a formatted header."""
    print()
    print("=" * 60)
    print(f"  {title}")
    print("=" * 60)
    print()

def print_status(status, message):
    """Print a status message."""
    print(f"[{status}] {message}")

def check_command(cmd):
    """Check if a command is available in PATH."""
    return shutil.which(cmd) is not None

def run_command(args, check=False):
    """Run a command and return the result."""
    try:
        result = subprocess.run(args, capture_output=True, text=True)
        if check and result.returncode != 0:
            return None
        return result
    except Exception as e:
        print_status("ERROR", f"Command failed: {e}")
        return None

def wait_for_exit(error=False):
    """Wait for user to press Enter before exiting."""
    print()
    if error:
        print("Press Enter to exit...")
    else:
        print("Press Enter to exit...")
    try:
        input()
    except:
        pass

def identify_file(filepath, has_ffprobe=False):
    """
    Identify if a file is video, audio, or subtitle based on extension.
    For ambiguous formats like .webm, uses ffprobe if available.
    Returns: 'video', 'audio', 'subtitle', or 'unknown'
    """
    path = Path(filepath)
    name = path.stem.lower()
    ext = path.suffix.lower()

    # Check for subtitle marker in filename
    if "_sub" in name:
        return "subtitle"

    # Audio-only extensions
    audio_exts = {".m4a", ".mp3", ".opus", ".ogg", ".aac", ".flac", ".wav"}
    if ext in audio_exts:
        return "audio"

    # Video extensions
    video_exts = {".mp4", ".mkv", ".avi", ".mov", ".flv", ".ts", ".3gp"}
    if ext in video_exts:
        return "video"

    # Ambiguous extension (.webm can be video or audio)
    if ext == ".webm":
        if has_ffprobe:
            try:
                result = subprocess.run(
                    ["ffprobe", "-v", "error", "-select_streams", "v:0",
                     "-show_entries", "stream=codec_type", "-of", "csv=p=0", filepath],
                    capture_output=True, text=True, timeout=10
                )
                if "video" in result.stdout.lower():
                    return "video"
                else:
                    return "audio"
            except:
                pass
        # Fallback: assume video if we can't determine
        return "video"

    return "unknown"

`;
    }

    function generatePythonScript(url, filename, cookieFile, mode) {
        const overwriteFlag = getOverwriteFlag();

        // ═══════════════════════════════════════════════════════════════
        // COMMENTS MODE
        // ═══════════════════════════════════════════════════════════════
        if (mode === 'comments') {
            return `${getPythonHeader()}
# ════════════════════════════════════════════════════════════════════════════════
# CONFIGURATION
# ════════════════════════════════════════════════════════════════════════════════

URL = "${url}"
FILENAME = "${filename}"
COOKIE_FILE = "${cookieFile}"
OVERWRITE_FLAG = "${overwriteFlag}"

# ════════════════════════════════════════════════════════════════════════════════
# MAIN - Comments Download
# ════════════════════════════════════════════════════════════════════════════════

def main():
    # Change to script directory
    os.chdir(Path(__file__).parent)

    print_header("yt-dlp Comments Downloader")

    # Check for yt-dlp
    if not check_command("yt-dlp"):
        print_status("ERROR", "yt-dlp not found")
        print("Install: pip install yt-dlp")
        print("     or: brew install yt-dlp (Mac)")
        print("     or: winget install yt-dlp (Windows)")
        wait_for_exit(error=True)
        sys.exit(1)

    print_status("OK", "yt-dlp found")

    # Build command
    cmd = ["yt-dlp"]

    if OVERWRITE_FLAG:
        cmd.append(OVERWRITE_FLAG)

    # Add cookies if file exists
    if Path(COOKIE_FILE).exists():
        print_status("OK", "Cookie file found")
        cmd.extend(["--cookies", COOKIE_FILE])
    else:
        print_status("INFO", "No cookie file")

    print()
    print(f"URL: {URL}")
    print()

    # Add comment extraction arguments
    cmd.extend([
        URL,
        "--skip-download",
        "--write-comments",
        "--no-playlist",
        "--extractor-args", "youtube:comment_sort=top;max_comments=all,all,all,all",
        "-o", f"{FILENAME}.%(ext)s"
    ])

    # Run yt-dlp
    result = subprocess.run(cmd)

    print()
    if result.returncode != 0:
        print_status("ERROR", "Download failed")
        wait_for_exit(error=True)
        sys.exit(1)
    else:
        print_status("SUCCESS", "Comments downloaded!")
        wait_for_exit()

if __name__ == "__main__":
    main()
`;
        }

        // ═══════════════════════════════════════════════════════════════
        // MEDIA MODE
        // ═══════════════════════════════════════════════════════════════
        const videoQuality = getVideoQuality();
        const videoCodec = getVideoCodec();
        const videoOutput = getVideoOutput();
        const audioQuality = getAudioQuality();
        const audioOutput = getAudioOutput();
        const subsFormat = getSubsFormat();
        const subsOutput = getSubsOutput();

        const hasVideo = isEnabled(videoOutput);
        const hasAudio = isEnabled(audioOutput);
        const hasSubs = isEnabled(subsOutput);

        const videoMerge = hasMergeFlag(videoOutput);
        const videoSeparate = hasSeparateFlag(videoOutput);
        const audioMerge = hasMergeFlag(audioOutput);
        const audioSeparate = hasSeparateFlag(audioOutput);
        const subsMerge = hasMergeFlag(subsOutput);
        const subsSeparate = hasSeparateFlag(subsOutput);

        // needsMerge: at least 2 components have merge flag
        const mergeCount = [videoMerge, audioMerge, subsMerge].filter(Boolean).length;
        const needsMerge = mergeCount >= 2;

        // Build format string
        const videoFormat = findOption(VIDEO_QUALITIES, videoQuality)?.format || 'bestvideo';
        const audioFormat = findOption(AUDIO_QUALITIES, audioQuality)?.format || 'bestaudio';
        const codecArg = (hasVideo && isYouTubeDomain() && videoCodec !== 'default')
            ? findOption(VIDEO_CODECS, videoCodec)?.sortArg || ''
            : '';
        const subsConvertArg = findOption(SUBTITLE_FORMATS, subsFormat)?.convertArg || '';

        // Generate summary labels
        const videoLabel = hasVideo ? `${getLabel(VIDEO_QUALITIES, videoQuality)}${videoCodec !== 'default' ? ` [${getLabel(VIDEO_CODECS, videoCodec)}]` : ''}` : 'None';
        const audioLabel = hasAudio ? getLabel(AUDIO_QUALITIES, audioQuality) : 'None';
        const subsLabel = hasSubs ? getLabel(SUBTITLE_FORMATS, subsFormat) : 'None';
        const getOutputLabel = (output) => getLabel(OUTPUT_MODES, output);

        // Determine merged output extension
        const mergedExt = (hasVideo && videoMerge) ? 'mkv' : 'mka';

        // Check if combined stream site
        const isCombined = isCombinedStreamSite();

        return `${getPythonHeader()}
# ════════════════════════════════════════════════════════════════════════════════
# CONFIGURATION
# ════════════════════════════════════════════════════════════════════════════════

URL = "${url}"
FILENAME = "${filename}"
COOKIE_FILE = "${cookieFile}"
OVERWRITE_FLAG = "${overwriteFlag}"

# Component settings
HAS_VIDEO = ${pyBool(hasVideo)}
HAS_AUDIO = ${pyBool(hasAudio)}
HAS_SUBS = ${pyBool(hasSubs)}

VIDEO_MERGE = ${pyBool(videoMerge)}
VIDEO_SEPARATE = ${pyBool(videoSeparate)}
AUDIO_MERGE = ${pyBool(audioMerge)}
AUDIO_SEPARATE = ${pyBool(audioSeparate)}
SUBS_MERGE = ${pyBool(subsMerge)}
SUBS_SEPARATE = ${pyBool(subsSeparate)}

NEEDS_MERGE = ${pyBool(needsMerge)}
MERGED_EXT = "${mergedExt}"

VIDEO_FORMAT = "${videoFormat}"
AUDIO_FORMAT = "${audioFormat}"
CODEC_ARG = "${codecArg}"
SUBS_CONVERT_ARG = "${subsConvertArg}"

IS_COMBINED_STREAM_SITE = ${pyBool(isCombined)}

# Display labels
VIDEO_LABEL = "${videoLabel}"
AUDIO_LABEL = "${audioLabel}"
SUBS_LABEL = "${subsLabel}"
VIDEO_OUTPUT_LABEL = "${getOutputLabel(videoOutput)}"
AUDIO_OUTPUT_LABEL = "${getOutputLabel(audioOutput)}"
SUBS_OUTPUT_LABEL = "${getOutputLabel(subsOutput)}"

${generateDownloadFunction(url, filename, {
    hasVideo, hasAudio, hasSubs,
    videoMerge, videoSeparate, audioMerge, audioSeparate, subsMerge, subsSeparate,
    needsMerge,
    videoFormat, audioFormat, codecArg, subsConvertArg,
    mergedExt, isCombined
})}

if __name__ == "__main__":
    main()
`;
    }

    function generateDownloadFunction(url, filename, opts) {
        const {
            hasVideo, hasAudio, hasSubs,
            videoMerge, videoSeparate, audioMerge, audioSeparate, subsMerge, subsSeparate,
            needsMerge,
            videoFormat, audioFormat, codecArg, subsConvertArg,
            mergedExt, isCombined
        } = opts;

        // ═══════════════════════════════════════════════════════════════
        // CASE 1: Subtitles ONLY (no video, no audio)
        // ═══════════════════════════════════════════════════════════════
        if (!hasVideo && !hasAudio && hasSubs) {
            return `
# ════════════════════════════════════════════════════════════════════════════════
# MAIN - Subtitle Only Download
# ════════════════════════════════════════════════════════════════════════════════

def main():
    # Change to script directory
    os.chdir(Path(__file__).parent)

    print_header("yt-dlp Subtitle Downloader")

    # Check for yt-dlp
    if not check_command("yt-dlp"):
        print_status("ERROR", "yt-dlp not found")
        print("Install: pip install yt-dlp")
        wait_for_exit(error=True)
        sys.exit(1)

    print_status("OK", "yt-dlp found")

    # Check for cookies
    cookies_arg = []
    if Path(COOKIE_FILE).exists():
        print_status("OK", "Cookie file found")
        cookies_arg = ["--cookies", COOKIE_FILE]
    else:
        print_status("INFO", "No cookie file")

    print()
    print(f"URL: {URL}")
    print()

    # List available subtitles
    print_header("Available Subtitles")
    subprocess.run(["yt-dlp", "--list-subs"] + cookies_arg + [URL])

    print()
    print("=" * 60)
    print("Enter language code (e.g., en, de, ja)")
    print("Or type 'all' for all languages")
    print("Or press Enter to skip subtitles")
    print("=" * 60)
    print()

    sub_lang = input("Language code: ").strip()

    if not sub_lang:
        print()
        print("No subtitles requested. Nothing to download.")
        wait_for_exit()
        return

    print()
    print_header("Starting Download...")

    # Build command
    cmd = ["yt-dlp"]
    if OVERWRITE_FLAG:
        cmd.append(OVERWRITE_FLAG)
    cmd.extend(cookies_arg)
    cmd.extend([
        URL,
        "--skip-download",
        "--write-subs",
        "--write-auto-subs",
        "--sub-langs", sub_lang
    ])

    # Add subtitle conversion if specified
    if SUBS_CONVERT_ARG:
        cmd.extend(SUBS_CONVERT_ARG.split())

    cmd.extend(["-o", f"{FILENAME}.%(ext)s"])

    # Run download
    result = subprocess.run(cmd)

    print()
    if result.returncode != 0:
        print_status("ERROR", "Subtitle download failed")
        wait_for_exit(error=True)
        sys.exit(1)
    else:
        print_status("OK", "Subtitles downloaded")
        wait_for_exit()
`;
        }

        // ═══════════════════════════════════════════════════════════════
        // CASE 2: Combined stream sites (simpler approach)
        // ═══════════════════════════════════════════════════════════════
        if (isCombined) {
            let formatStr = 'bestvideo+bestaudio/best';
            if (hasVideo && !hasAudio) {
                formatStr = 'bestvideo/best';
            } else if (!hasVideo && hasAudio) {
                formatStr = 'bestaudio/best';
            }

            return `
# ════════════════════════════════════════════════════════════════════════════════
# MAIN - Combined Stream Site Download
# ════════════════════════════════════════════════════════════════════════════════

def main():
    # Change to script directory
    os.chdir(Path(__file__).parent)

    print_header("yt-dlp Media Downloader")

    print(f"URL: {URL}")
    print()
    print("Configuration:")
    print(f"  Video:     {VIDEO_LABEL} [{VIDEO_OUTPUT_LABEL}]")
    print(f"  Audio:     {AUDIO_LABEL} [{AUDIO_OUTPUT_LABEL}]")
    print(f"  Subtitles: {SUBS_LABEL} [{SUBS_OUTPUT_LABEL}]")
    print()
    print("=" * 60)

    # Check for yt-dlp
    if not check_command("yt-dlp"):
        print_status("ERROR", "yt-dlp not found")
        print("Install: pip install yt-dlp")
        wait_for_exit(error=True)
        sys.exit(1)

    print_status("OK", "yt-dlp found")

    # Check for cookies
    cookies_arg = []
    if Path(COOKIE_FILE).exists():
        print_status("OK", "Cookie file found")
        cookies_arg = ["--cookies", COOKIE_FILE]
    else:
        print_status("INFO", "No cookie file")

    # Handle subtitles
    sub_args = []
    do_subs = False

    if HAS_SUBS:
        print()
        print_header("Available Subtitles")
        subprocess.run(["yt-dlp", "--list-subs"] + cookies_arg + [URL])

        print()
        print("=" * 60)
        print("Enter language code (e.g., en, de, ja)")
        print("Or type 'all' for all languages")
        print("Or press Enter to skip subtitles")
        print("=" * 60)
        print()

        sub_lang = input("Language code: ").strip()

        if sub_lang:
            do_subs = True
            sub_args = ["--write-subs", "--write-auto-subs", "--sub-langs", sub_lang]
            if SUBS_CONVERT_ARG:
                sub_args.extend(SUBS_CONVERT_ARG.split())
        else:
            print()
            print("Skipping subtitles...")

    print()
    print_header("Starting Download...")
    print("Downloading from combined-stream site...")

    # Build command
    cmd = ["yt-dlp"]
    if OVERWRITE_FLAG:
        cmd.append(OVERWRITE_FLAG)
    cmd.extend(cookies_arg)
    cmd.extend([URL, "-f", "${formatStr}"])

    ${hasVideo ? 'cmd.extend(["--merge-output-format", "mkv"])' : '# No video, skip merge format'}

    if do_subs:
        cmd.extend(sub_args)
        cmd.append("--embed-subs")

    cmd.extend(["-o", f"{FILENAME}.%(ext)s"])

    # Run download
    result = subprocess.run(cmd)

    print()
    if result.returncode != 0:
        print_status("ERROR", "Download failed")
        wait_for_exit(error=True)
        sys.exit(1)
    else:
        print_status("OK", "Download complete")
        print()
        print(f"Saved to: {Path.cwd()}")
        wait_for_exit()
`;
        }

        // Build format list for non-combined sites
        const formats = [];
        if (hasVideo) formats.push(videoFormat);
        if (hasAudio) formats.push(audioFormat);
        const formatStr = formats.join(',');

        // ═══════════════════════════════════════════════════════════════
        // CASE 3: No merging needed - direct download
        // ═══════════════════════════════════════════════════════════════
        if (!needsMerge) {
            return `
# ════════════════════════════════════════════════════════════════════════════════
# MAIN - Direct Download (No Merge)
# ════════════════════════════════════════════════════════════════════════════════

def main():
    # Change to script directory
    os.chdir(Path(__file__).parent)

    print_header("yt-dlp Media Downloader")

    print(f"URL: {URL}")
    print()
    print("Configuration:")
    print(f"  Video:     {VIDEO_LABEL} [{VIDEO_OUTPUT_LABEL}]")
    print(f"  Audio:     {AUDIO_LABEL} [{AUDIO_OUTPUT_LABEL}]")
    print(f"  Subtitles: {SUBS_LABEL} [{SUBS_OUTPUT_LABEL}]")
    print()
    print("=" * 60)

    # Check for yt-dlp
    if not check_command("yt-dlp"):
        print_status("ERROR", "yt-dlp not found")
        print("Install: pip install yt-dlp")
        wait_for_exit(error=True)
        sys.exit(1)

    print_status("OK", "yt-dlp found")

    # Check for cookies
    cookies_arg = []
    if Path(COOKIE_FILE).exists():
        print_status("OK", "Cookie file found")
        cookies_arg = ["--cookies", COOKIE_FILE]
    else:
        print_status("INFO", "No cookie file")

    # Handle subtitles
    sub_args = []
    do_subs = False

    if HAS_SUBS:
        print()
        print_header("Available Subtitles")
        subprocess.run(["yt-dlp", "--list-subs"] + cookies_arg + [URL])

        print()
        print("=" * 60)
        print("Enter language code (e.g., en, de, ja)")
        print("Or type 'all' for all languages")
        print("Or press Enter to skip subtitles")
        print("=" * 60)
        print()

        sub_lang = input("Language code: ").strip()

        if sub_lang:
            do_subs = True
            sub_args = ["--write-subs", "--write-auto-subs", "--sub-langs", sub_lang]
            if SUBS_CONVERT_ARG:
                sub_args.extend(SUBS_CONVERT_ARG.split())
        else:
            print()
            print("Skipping subtitles...")

    print()
    print_header("Starting Download...")

    components = []
    if HAS_VIDEO:
        components.append("video")
    if HAS_AUDIO:
        components.append("audio")
    if do_subs:
        components.append("subtitles")

    print(f"Downloading {' + '.join(components)}...")

    # Build command
    cmd = ["yt-dlp"]
    if OVERWRITE_FLAG:
        cmd.append(OVERWRITE_FLAG)
    cmd.extend(cookies_arg)
    cmd.extend([URL, "-f", "${formatStr}"])

    # Add codec sorting if specified
    if CODEC_ARG:
        cmd.extend(CODEC_ARG.split())

    # Output templates based on what we're downloading
    ${hasVideo && hasAudio ? `
    # Both video and audio - use descriptive suffixes
    cmd.extend(["-o", f"{FILENAME}.video.%(ext)s", "-o", f"audio:{FILENAME}.audio.%(ext)s"])
    ` : `
    # Single component
    cmd.extend(["-o", f"{FILENAME}.%(ext)s"])
    `}

    # Add subtitle args if enabled
    if do_subs:
        cmd.extend(sub_args)
        cmd.extend(["-o", f"subtitle:{FILENAME}.%(ext)s"])

    # Run download
    result = subprocess.run(cmd)

    print()
    if result.returncode != 0:
        print_status("ERROR", "Download failed")
        wait_for_exit(error=True)
        sys.exit(1)
    else:
        print_status("OK", "Download complete")
        print()
        print(f"Saved to: {Path.cwd()}")
        wait_for_exit()
`;
        }

        // ═══════════════════════════════════════════════════════════════
        // CASE 4: Merging required - complex multi-phase download
        // ═══════════════════════════════════════════════════════════════
        return `
# ════════════════════════════════════════════════════════════════════════════════
# MAIN - Multi-Phase Download with Merge
# ════════════════════════════════════════════════════════════════════════════════

def main():
    # Change to script directory
    os.chdir(Path(__file__).parent)

    print_header("yt-dlp Media Downloader")

    print(f"URL: {URL}")
    print()
    print("Configuration:")
    print(f"  Video:     {VIDEO_LABEL} [{VIDEO_OUTPUT_LABEL}]")
    print(f"  Audio:     {AUDIO_LABEL} [{AUDIO_OUTPUT_LABEL}]")
    print(f"  Subtitles: {SUBS_LABEL} [{SUBS_OUTPUT_LABEL}]")
    print()
    print("=" * 60)

    # === Check tools ===
    if not check_command("yt-dlp"):
        print_status("ERROR", "yt-dlp not found")
        print("Install: pip install yt-dlp")
        wait_for_exit(error=True)
        sys.exit(1)

    print_status("OK", "yt-dlp found")

    # Check for cookies
    cookies_arg = []
    if Path(COOKIE_FILE).exists():
        print_status("OK", "Cookie file found")
        cookies_arg = ["--cookies", COOKIE_FILE]
    else:
        print_status("INFO", "No cookie file")

    # Check for mkvmerge
    has_mkvmerge = check_command("mkvmerge")
    if has_mkvmerge:
        print_status("OK", "mkvmerge found")
    else:
        print_status("INFO", "mkvmerge not found - using FFmpeg")

    # Check for ffprobe (for file identification)
    has_ffprobe = check_command("ffprobe")

    # Handle subtitles
    sub_args = []
    do_subs = False

    if HAS_SUBS:
        print()
        print_header("Available Subtitles")
        subprocess.run(["yt-dlp", "--list-subs"] + cookies_arg + [URL])

        print()
        print("=" * 60)
        print("Enter language code (e.g., en, de, ja)")
        print("Or type 'all' for all languages")
        print("Or press Enter to skip subtitles")
        print("=" * 60)
        print()

        sub_lang = input("Language code: ").strip()

        if sub_lang:
            do_subs = True
            sub_args = ["--write-subs", "--write-auto-subs", "--sub-langs", sub_lang]
            if SUBS_CONVERT_ARG:
                sub_args.extend(SUBS_CONVERT_ARG.split())
        else:
            print()
            print("Skipping subtitles...")

    print()
    print_header("Starting Download...")

    # Generate unique temp prefix
    temp_base = f"_ytdlp_tmp_{random.randint(10000, 99999)}"
    dl_error = False

    # ════════════════════════════════════════════════════════════════════════════
    # PHASE 1: Download all components in ONE call (single metadata fetch)
    # ════════════════════════════════════════════════════════════════════════════
    print()
    print("[PHASE 1] Downloading all components...")

    cmd = ["yt-dlp"]
    if OVERWRITE_FLAG:
        cmd.append(OVERWRITE_FLAG)
    cmd.extend(cookies_arg)
    cmd.extend([URL, "-f", "${formatStr}"])

    # Add codec sorting if specified
    if CODEC_ARG:
        cmd.extend(CODEC_ARG.split())

    # Add subtitle args if enabled
    if do_subs:
        cmd.extend(sub_args)
        cmd.extend(["-o", f"{temp_base}.%(format_id)s.%(ext)s", "-o", f"subtitle:{temp_base}_sub.%(ext)s"])
    else:
        cmd.extend(["-o", f"{temp_base}.%(format_id)s.%(ext)s"])

    result = subprocess.run(cmd)

    if result.returncode != 0:
        dl_error = True
        print()
        print_status("ERROR", "Download failed")
        # Cleanup any temp files
        for f in glob(f"{temp_base}*.*"):
            try:
                os.remove(f)
            except:
                pass
        wait_for_exit(error=True)
        sys.exit(1)

    # ════════════════════════════════════════════════════════════════════════════
    # PHASE 2: Identify downloaded files
    # ════════════════════════════════════════════════════════════════════════════
    print()
    print("[PHASE 2] Identifying downloaded files...")

    video_file = None
    audio_file = None
    subtitle_files = []

    for filepath in glob(f"{temp_base}*.*"):
        file_type = identify_file(filepath, has_ffprobe)
        basename = os.path.basename(filepath)

        if file_type == "subtitle":
            subtitle_files.append(filepath)
            print(f"  [SUB {len(subtitle_files)}] {basename}")
        elif file_type == "video":
            video_file = filepath
            print(f"  [VIDEO] {basename}")
        elif file_type == "audio":
            audio_file = filepath
            print(f"  [AUDIO] {basename}")
        else:
            # Unknown - try to categorize
            if video_file is None:
                video_file = filepath
                print(f"  [VIDEO] {basename} (assumed)")
            else:
                audio_file = filepath
                print(f"  [AUDIO] {basename} (assumed)")

    # Verify we found expected files
    ${hasVideo ? `
    if not video_file:
        print_status("WARNING", "Expected video file not found")
    ` : ''}
    ${hasAudio ? `
    if not audio_file:
        print_status("WARNING", "Expected audio file not found")
    ` : ''}

    # ════════════════════════════════════════════════════════════════════════════
    # PHASE 3: Merge files (only components with Merge flag)
    # ════════════════════════════════════════════════════════════════════════════
    print()
    print("[PHASE 3] Merging selected components...")

    if has_mkvmerge:
        mkv_inputs = []

        ${videoMerge && hasVideo ? `
        if video_file:
            mkv_inputs.append(video_file)
            print("  Adding to merge: VIDEO")
        ` : '# Video not included in merge'}

        ${audioMerge && hasAudio ? `
        if audio_file:
            mkv_inputs.append(audio_file)
            print("  Adding to merge: AUDIO")
        ` : '# Audio not included in merge'}

        ${subsMerge && hasSubs ? `
        if do_subs:
            for sub_file in subtitle_files:
                mkv_inputs.append(sub_file)
                print(f"  Adding to merge: {os.path.basename(sub_file)}")
        ` : '# Subtitles not included in merge'}

        if len(mkv_inputs) >= 2:
            print()
            print("  Running mkvmerge...")

            merge_cmd = ["mkvmerge", "-o", f"{FILENAME}.{MERGED_EXT}"] + mkv_inputs
            merge_result = subprocess.run(merge_cmd)

            if merge_result.returncode != 0:
                print_status("ERROR", "mkvmerge failed")
                dl_error = True
            else:
                print_status("OK", f"Created: {FILENAME}.{MERGED_EXT}")

        elif len(mkv_inputs) == 1:
            print_status("WARNING", "Only 1 component for merge - copying instead")
            shutil.copy(mkv_inputs[0], f"{FILENAME}.{MERGED_EXT}")
            print_status("OK", f"Created: {FILENAME}.{MERGED_EXT}")

        else:
            print_status("WARNING", "No components to merge")
            dl_error = True

    else:
        # FFmpeg fallback
        print("  mkvmerge not found - using FFmpeg fallback...")
        ${(videoMerge && hasVideo && audioMerge && hasAudio) ? `
        # Re-download with FFmpeg merge
        ffmpeg_cmd = ["yt-dlp"]
        if OVERWRITE_FLAG:
            ffmpeg_cmd.append(OVERWRITE_FLAG)
        ffmpeg_cmd.extend(cookies_arg)
        ffmpeg_cmd.extend([
            URL,
            "-f", "${videoFormat}+${audioFormat}",
            "--merge-output-format", "mkv"
        ])

        if CODEC_ARG:
            ffmpeg_cmd.extend(CODEC_ARG.split())

        ${subsMerge && hasSubs ? `
        if do_subs:
            ffmpeg_cmd.extend(sub_args)
            ffmpeg_cmd.append("--embed-subs")
        ` : ''}

        ffmpeg_cmd.extend(["-o", f"{FILENAME}.%(ext)s"])

        ffmpeg_result = subprocess.run(ffmpeg_cmd)

        if ffmpeg_result.returncode != 0:
            print_status("ERROR", "FFmpeg merge failed")
            dl_error = True
        else:
            print_status("OK", f"Created: {FILENAME}.mkv")
        ` : `
        print_status("WARNING", "Complex merge not supported without mkvmerge")
        print("  Please install mkvmerge: https://mkvtoolnix.download/")
        dl_error = True
        `}

    # ════════════════════════════════════════════════════════════════════════════
    # PHASE 4: Copy files that need to be kept separately
    # ════════════════════════════════════════════════════════════════════════════
    print()
    print("[PHASE 4] Creating separate copies...")

    ${videoSeparate && hasVideo ? `
    if video_file:
        ext = Path(video_file).suffix
        dest = f"{FILENAME}.video{ext}"
        try:
            shutil.copy(video_file, dest)
            print_status("OK", f"Created: {dest}")
        except Exception as e:
            print_status("ERROR", f"Failed to copy video: {e}")
    ` : '# Video separate not requested'}

    ${audioSeparate && hasAudio ? `
    if audio_file:
        ext = Path(audio_file).suffix
        dest = f"{FILENAME}.audio{ext}"
        try:
            shutil.copy(audio_file, dest)
            print_status("OK", f"Created: {dest}")
        except Exception as e:
            print_status("ERROR", f"Failed to copy audio: {e}")
    ` : '# Audio separate not requested'}

    ${subsSeparate && hasSubs ? `
    if do_subs:
        for sub_file in subtitle_files:
            # Extract lang.ext from temp_sub.lang.ext
            basename = os.path.basename(sub_file)
            # Remove the temp prefix to get lang.ext
            if "_sub." in basename:
                lang_ext = basename.split("_sub.", 1)[1]
                dest = f"{FILENAME}.{lang_ext}"
                try:
                    shutil.copy(sub_file, dest)
                    print_status("OK", f"Created: {dest}")
                except Exception as e:
                    print_status("ERROR", f"Failed to copy subtitle: {e}")
    ` : '# Subtitle separate not requested'}

    # ════════════════════════════════════════════════════════════════════════════
    # PHASE 5: Cleanup temporary files
    # ════════════════════════════════════════════════════════════════════════════
    print()
    print("[PHASE 5] Cleaning up temporary files...")

    cleaned = 0
    for f in glob(f"{temp_base}*.*"):
        try:
            os.remove(f)
            cleaned += 1
        except:
            pass

    print(f"  Removed {cleaned} temporary file(s)")

    # Final status
    if dl_error:
        print()
        print("=" * 60)
        print("  [WARNING] Some operations may have failed - check messages above")
        print("=" * 60)
        wait_for_exit(error=True)
    else:
        print()
        print_status("SUCCESS", "Download complete!")
        print()
        print(f"Saved to: {Path.cwd()}")
        wait_for_exit()
`;
    }

    //================================================================================
    // NOTIFICATIONS
    //================================================================================

    function showNotification(message, type = 'info') {
        if (!shadowRoot || !notificationElement) return;
        try {
            notificationElement.classList.remove('show');
            notificationElement.textContent = message;
            notificationElement.setAttribute('data-type', type);
            void notificationElement.offsetWidth;
            notificationElement.classList.add('show');
            setTimeout(() => notificationElement.classList.remove('show'), 2500);
        } catch (e) {
            console.warn('[yt-dlp] Notification error:', e);
        }
    }

    //================================================================================
    // MAIN FUNCTIONS
    //================================================================================

    function executeDownload(mode) {
        const info = getVideoInfo();
        if (info.error) {
            showNotification(info.error, 'error');
            return;
        }

        if (mode === 'media') {
            const validation = validateSettings();
            if (!validation.valid) {
                showNotification('Select at least one component', 'error');
                return;
            }
        }

        if (window._ytdlpUpdateTooltip) window._ytdlpUpdateTooltip();

        const content = generatePythonScript(info.url, info.filename, info.cookieFile, mode);
        const scriptFilename = `dl_${info.filename}.py`;

        try {
            const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
            const blobUrl = URL.createObjectURL(blob);
            const a = Object.assign(document.createElement('a'), { href: blobUrl, download: scriptFilename });
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(blobUrl);
            showNotification('⬇ Downloading .py', 'info');
        } catch (e) {
            console.error("[yt-dlp] Download failed:", e);
            showNotification('Download failed', 'error');
        }
    }

    //================================================================================
    // VISIBILITY & SPA LOGIC
    //================================================================================

    function shouldBeVisible() {
        return !STATE.hidden && (!siteCondition || siteCondition(window.location.href));
    }

    function updateVisibility() {
        if (containerElement) {
            containerElement.classList.toggle('hidden', !shouldBeVisible());
        }
    }

    function handleUrlChange() {
        if (window.location.href !== STATE.lastUrl) {
            STATE.hidden = false;
            STATE.lastUrl = window.location.href;
            updateVisibility();
        }
    }

    function startSPAMonitoring() {
        ['pushState', 'replaceState'].forEach(method => {
            const original = history[method];
            history[method] = function(...args) {
                const result = original.apply(this, args);
                setTimeout(handleUrlChange, 0);
                return result;
            };
        });
        ['popstate', 'hashchange'].forEach(event =>
            window.addEventListener(event, () => setTimeout(handleUrlChange, 0))
        );
        STATE.checkInterval = setInterval(handleUrlChange, 500);
    }

    //================================================================================
    // STYLES
    //================================================================================

    function getStyles() {
        const { size, iconStyle, position: pos, opacity, scale, zIndex } = CONFIG_UI.button;
        const iconSize = size - 4;

        return `
            :host { all: initial; }
            * { box-sizing: border-box; }

            .ytdlp-container {
                position: fixed;
                ${pos.vertical}: ${pos.offsetY}px;
                ${pos.horizontal}: ${pos.offsetX}px;
                z-index: ${zIndex};
                pointer-events: auto;
                user-select: none;
            }

            .ytdlp-container.hidden { display: none !important; }

            .ytdlp-btn {
                position: relative;
                width: ${size}px;
                height: ${size}px;
                background: transparent;
                border: none;
                cursor: pointer;
                opacity: ${opacity.default};
                display: flex;
                align-items: center;
                justify-content: center;
                padding: 0;
                margin: 0;
                transform: scale(${scale.default});
                touch-action: manipulation;
                -webkit-tap-highlight-color: transparent;
            }

            .ytdlp-btn[data-hover="true"] { opacity: ${opacity.hover}; transform: scale(${scale.hover}); }

            .ytdlp-icon-container {
                display: flex;
                align-items: center;
                justify-content: center;
                width: ${size}px;
                height: ${size}px;
                border-radius: ${iconStyle.background.enabled ? iconStyle.background.borderRadius : '0'};
                background-color: ${iconStyle.background.enabled ? iconStyle.background.color : 'transparent'};
                pointer-events: none;
            }

            .ytdlp-icon {
                width: ${iconSize}px;
                height: ${iconSize}px;
                display: block;
                pointer-events: none;
                ${iconStyle.shadow.enabled ? `filter: drop-shadow(0 0 ${iconStyle.shadow.blur}px ${iconStyle.shadow.color}) drop-shadow(0 0 ${iconStyle.shadow.blur * 0.5}px ${iconStyle.shadow.color});` : ''}
            }

            .ytdlp-notification {
                position: fixed;
                ${pos.vertical}: ${pos.offsetY + size + 10}px;
                ${pos.horizontal}: ${pos.offsetX}px;
                padding: 8px 16px;
                color: white;
                border-radius: 6px;
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
                font-size: 13px;
                font-weight: 500;
                z-index: ${zIndex - 1};
                box-shadow: 0 3px 12px rgba(0,0,0,0.25);
                opacity: 0;
                transform: translateY(10px);
                transition: opacity 0.3s, transform 0.3s;
                pointer-events: none;
            }

            .ytdlp-notification.show { opacity: 1; transform: translateY(0); }
            .ytdlp-notification[data-type="success"] { background: #4CAF50; border: 1px solid #388E3C; }
            .ytdlp-notification[data-type="error"] { background: #f44336; border: 1px solid #c62828; }
            .ytdlp-notification[data-type="info"] { background: #2196F3; border: 1px solid #1565C0; }
            .ytdlp-notification[data-type="warning"] { background: #FF9800; border: 1px solid #EF6C00; }

            .ytdlp-btn::after {
                content: attr(data-tooltip);
                position: absolute;
                ${pos.horizontal === 'right' ? 'right' : 'left'}: ${size + 8}px;
                ${pos.vertical === 'bottom' ? 'bottom' : 'top'}: 0;
                background: rgba(30, 30, 30, 0.95);
                color: #fff;
                padding: 8px 12px;
                border-radius: 6px;
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
                font-size: 12px;
                line-height: 1.5;
                white-space: pre;
                pointer-events: none;
                opacity: 0;
                visibility: hidden;
                transition: opacity 0.15s ease, visibility 0.15s ease;
                box-shadow: 0 2px 8px rgba(0,0,0,0.3);
                z-index: ${zIndex + 1};
            }

            .ytdlp-btn[data-hover="true"]::after { opacity: 1; visibility: visible; }
        `;
    }

    //================================================================================
    // FLOATING BUTTON
    //================================================================================

    function addFloatingDownloadButton() {
        if (!document.body) return;

        try {
            const shadowHost = document.createElement('div');
            shadowHost.id = 'ytdlp-downloader-host';
            Object.assign(shadowHost.style, {
                position: 'fixed', top: '0', left: '0', width: '0', height: '0',
                overflow: 'visible', zIndex: CONFIG_UI.button.zIndex.toString(),
                pointerEvents: 'none', userSelect: 'none'
            });

            shadowRoot = shadowHost.attachShadow({ mode: 'closed' });
            shadowRoot.appendChild(Object.assign(document.createElement('style'), { textContent: getStyles() }));

            containerElement = Object.assign(document.createElement('div'), { className: 'ytdlp-container' });

            const btn = Object.assign(document.createElement('div'), { className: 'ytdlp-btn' });

            const updateTooltip = () => {
                const videoOut = getVideoOutput();
                const audioOut = getAudioOutput();
                const subsOut = getSubsOutput();

                const parts = [];
                if (videoOut !== 'none') {
                    parts.push(`Video: ${getLabel(VIDEO_QUALITIES, getVideoQuality())} [${getLabel(OUTPUT_MODES, videoOut)}]`);
                }
                if (audioOut !== 'none') {
                    parts.push(`Audio: ${getLabel(AUDIO_QUALITIES, getAudioQuality())} [${getLabel(OUTPUT_MODES, audioOut)}]`);
                }
                if (subsOut !== 'none') {
                    parts.push(`Subs: ${getLabel(SUBTITLE_FORMATS, getSubsFormat())} [${getLabel(OUTPUT_MODES, subsOut)}]`);
                }
                if (parts.length === 0) {
                    parts.push('(Nothing selected)');
                }

                btn.setAttribute('data-tooltip', [
                    '𝘆𝘁-𝗱𝗹𝗽 𝗗𝗼𝘄𝗻𝗹𝗼𝗮𝗱𝗲𝗿',
                    '',
                    ...parts,
                    '',
                    'Click: Download',
                    'Right-click: Options',
                    'Double-click: Hide 5s'
                ].join('\n'));
            };

            updateTooltip();
            window._ytdlpUpdateTooltip = updateTooltip;

            const iconContainer = Object.assign(document.createElement('div'), { className: 'ytdlp-icon-container' });
            const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
            ['class', 'ytdlp-icon', 'viewBox', '0 0 24 24', 'fill', 'none', 'stroke', '#555', 'stroke-width', '2', 'stroke-linecap', 'round', 'stroke-linejoin', 'round']
                .reduce((acc, val, i, arr) => (i % 2 === 0 && iconSvg.setAttribute(val, arr[i + 1]), acc), null);

            const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
            [['x', '2'], ['y', '6'], ['width', '14'], ['height', '12'], ['rx', '2'], ['ry', '2']].forEach(([k, v]) => rect.setAttribute(k, v));

            const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
            polygon.setAttribute('points', '22 8 16 12 22 16 22 8');

            iconSvg.append(rect, polygon);
            iconContainer.appendChild(iconSvg);
            btn.appendChild(iconContainer);
            containerElement.appendChild(btn);

            ContextMenu.init();
            shadowRoot.appendChild(containerElement);

            notificationElement = Object.assign(document.createElement('div'), { className: 'ytdlp-notification' });
            notificationElement.setAttribute('data-type', 'info');
            shadowRoot.appendChild(notificationElement);

            document.body.appendChild(shadowHost);

            //================================================================================
            // INTERACTION HANDLERS
            //================================================================================

            let lastClickTime = 0, hideTimeout = null;
            let hoverCheckInterval = null, lastMouseX = 0, lastMouseY = 0, isHovering = false;

            const setAttr = (attr, val) => btn.setAttribute(`data-${attr}`, val ? 'true' : 'false');
            const setHover = (val) => { if (isHovering !== val) { isHovering = val; setAttr('hover', val); } };

            const isInsideButton = (x, y) => {
                try {
                    const r = btn.getBoundingClientRect();
                    return x >= r.left - 2 && x <= r.right + 2 && y >= r.top - 2 && y <= r.bottom + 2;
                } catch (e) {
                    return false;
                }
            };

            const onGlobalMouseMove = (e) => { lastMouseX = e.clientX; lastMouseY = e.clientY; };

            const startHoverTracking = () => {
                if (hoverCheckInterval) return;
                document.addEventListener('mousemove', onGlobalMouseMove, { passive: true });
                hoverCheckInterval = setInterval(() => {
                    if (!isInsideButton(lastMouseX, lastMouseY)) { setHover(false); stopHoverTracking(); }
                }, CONFIG_UI.timing.hoverCheckInterval);
            };

            const stopHoverTracking = () => {
                if (hoverCheckInterval) { clearInterval(hoverCheckInterval); hoverCheckInterval = null; }
                document.removeEventListener('mousemove', onGlobalMouseMove);
            };

            btn.addEventListener('mouseenter', (e) => { lastMouseX = e.clientX; lastMouseY = e.clientY; setHover(true); startHoverTracking(); });
            btn.addEventListener('mouseleave', () => {});

            btn.addEventListener('click', (e) => {
                e.preventDefault();
                const now = Date.now();
                const timeSinceLastClick = now - lastClickTime;
                lastClickTime = now;

                if (ContextMenu.isVisible()) {
                    ContextMenu.hide();
                    return;
                }

                // Double-click to hide
                if (timeSinceLastClick < CONFIG_UI.timing.doubleClickThreshold) {
                    window.getSelection?.().removeAllRanges();
                    STATE.hidden = true;
                    stopHoverTracking();
                    setHover(false);
                    updateVisibility();
                    clearTimeout(hideTimeout);
                    hideTimeout = setTimeout(() => { STATE.hidden = false; updateVisibility(); }, CONFIG_UI.timing.hideTemporarilyDuration);
                    return;
                }

                // Single click - download with current settings
                executeDownload('media');
            });

            btn.addEventListener('contextmenu', (e) => {
                e.preventDefault();
                e.stopPropagation();
                ContextMenu.show(e.clientX, e.clientY, (mode) => executeDownload(mode));
            });

            window.addEventListener('beforeunload', stopHoverTracking);

            updateVisibility();
            startSPAMonitoring();
            console.log('[yt-dlp Downloader v9.2] Initialized - Python edition');
        } catch (e) {
            console.error('[yt-dlp] Failed to initialize:', e);
        }
    }

    //================================================================================
    // INIT
    //================================================================================

    if (document.body) addFloatingDownloadButton();
    else document.addEventListener('DOMContentLoaded', addFloatingDownloadButton);

})();