YouTube - Add Download Button with yt-dlp Command

Injects a "Download" button that opens a modal to generate yt-dlp commands on YouTube video pages.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         YouTube - Add Download Button with yt-dlp Command 
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Injects a "Download" button that opens a modal to generate yt-dlp commands on YouTube video pages.
// @author       You
// @match        https://www.youtube.com/watch*
// @grant        GM_addStyle
// @run-at       document-idle
// @license      Unlicense     
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration ---
    const NEW_BUTTON_ID = 'my-custom-yt-dlp-button';
    const NEW_BUTTON_TEXT = 'Download';

    // --- Modal and Styling ---
    GM_addStyle(`
        .yt-dlp-modal-backdrop {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.7);
            z-index: 9999;
            display: flex;
            align-items: center;
            justify-content: center;
            opacity: 0;
            transition: opacity 0.2s ease-in-out;
        }
        .yt-dlp-modal-backdrop.yt-dlp-visible {
            opacity: 1;
        }
        .yt-dlp-modal-content {
            background-color: #282828;
            color: #fff;
            padding: 24px;
            border-radius: 12px;
            width: 90%;
            max-width: 500px;
            font-family: "Roboto", "Arial", sans-serif;
            border: 1px solid #3f3f3f;
            box-shadow: 0 10px 30px rgba(0,0,0,0.2);
            transform: scale(0.95);
            transition: transform 0.2s ease-in-out;
        }
        .yt-dlp-modal-backdrop.yt-dlp-visible .yt-dlp-modal-content {
            transform: scale(1);
        }
        .yt-dlp-modal-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .yt-dlp-modal-header h2 {
            margin: 0;
            font-size: 20px;
        }
        .yt-dlp-modal-close-btn {
            background: none;
            border: none;
            color: #aaa;
            font-size: 28px;
            cursor: pointer;
            line-height: 1;
        }
        p.video-title {
            font-size: 14px;
            color: #aaa;
            margin: 4px 0 20px 0;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }
        .yt-dlp-section {
            margin-bottom: 20px;
        }
        .yt-dlp-section h3 {
            font-size: 16px;
            margin: 0 0 12px 0;
            color: #eee;
            border-bottom: 1px solid #3f3f3f;
            padding-bottom: 8px;
        }
        .yt-dlp-options-grid {
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
        }
        .option-btn {
            background-color: #3f3f3f;
            color: white;
            border: 1px solid transparent;
            border-radius: 18px;
            padding: 8px 16px;
            cursor: pointer;
            font-size: 14px;
            transition: background-color 0.2s, border-color 0.2s;
        }
        .option-btn:hover {
            background-color: #535353;
        }
        .option-btn.selected {
            background-color: #3ea6ff;
            color: #0d0d0d;
            border-color: #3ea6ff;
            font-weight: 500;
        }
        .yt-dlp-command-area input {
            width: 100%;
            box-sizing: border-box;
            background-color: #121212;
            border: 1px solid #3f3f3f;
            color: #fff;
            padding: 10px;
            border-radius: 8px;
            font-family: "Courier New", Courier, monospace;
            margin-bottom: 12px;
        }
        .yt-dlp-copy-btn {
            width: 100%;
            background-color: #3ea6ff;
            color: #0d0d0d;
            border: none;
            border-radius: 8px;
            padding: 10px 16px;
            cursor: pointer;
            font-weight: 500;
            font-size: 15px;
            transition: background-color 0.2s;
        }
        .yt-dlp-copy-btn:hover {
            background-color: #6fc1ff;
        }
    `);

    /**
     * Creates and displays the modal for quality selection.
     */
    function showDownloadModal() {
        const existingModal = document.getElementById('yt-dlp-modal');
        if (existingModal) existingModal.remove();

        const videoUrl = window.location.href;
        const videoTitle = document.querySelector('h1.ytd-watch-metadata')?.textContent.trim() || 'Current Video';

        const backdrop = document.createElement('div');
        backdrop.id = 'yt-dlp-modal';
        backdrop.className = 'yt-dlp-modal-backdrop';
        backdrop.innerHTML = `
            <div class="yt-dlp-modal-content">
                <div class="yt-dlp-modal-header">
                    <h2>Download Command</h2>
                    <button class="yt-dlp-modal-close-btn">&times;</button>
                </div>
                <p class="video-title">${videoTitle}</p>

                <div class="yt-dlp-section">
                    <h3>Video (with Audio)</h3>
                    <div class="yt-dlp-options-grid">
                        <button class="option-btn" data-type="video" data-quality="1080">1080p</button>
                        <button class="option-btn" data-type="video" data-quality="720">720p</button>
                        <button class="option-btn" data-type="video" data-quality="480">480p</button>
                        <button class="option-btn" data-type="video" data-quality="best">Best</button>
                    </div>
                </div>

                <div class="yt-dlp-section">
                    <h3>Audio Only</h3>
                     <div class="yt-dlp-options-grid">
                        <button class="option-btn" data-type="audio" data-format="mp3">MP3</button>
                        <button class="option-btn" data-type="audio" data-format="m4a">M4A</button>
                        <button class="option-btn" data-type="audio" data-format="wav">WAV</button>
                    </div>
                </div>

                <div class="yt-dlp-command-area">
                    <input type="text" readonly placeholder="Select an option...">
                    <button class="yt-dlp-copy-btn">Copy Command</button>
                </div>
            </div>
        `;
        document.body.appendChild(backdrop);

        // Animate modal in
        requestAnimationFrame(() => backdrop.classList.add('yt-dlp-visible'));

        const closeButton = backdrop.querySelector('.yt-dlp-modal-close-btn');
        const optionButtons = backdrop.querySelectorAll('.option-btn');
        const commandInput = backdrop.querySelector('.yt-dlp-command-area input');
        const copyButton = backdrop.querySelector('.yt-dlp-copy-btn');

        const closeModal = () => {
            backdrop.classList.remove('yt-dlp-visible');
            backdrop.addEventListener('transitionend', () => backdrop.remove(), { once: true });
        };

        closeButton.onclick = closeModal;
        backdrop.onclick = (e) => {
            if (e.target === backdrop) closeModal();
        };

        optionButtons.forEach(button => {
            button.onclick = () => {
                optionButtons.forEach(btn => btn.classList.remove('selected'));
                button.classList.add('selected');

                const type = button.dataset.type;
                let command = '';

                if (type === 'video') {
                    const quality = button.dataset.quality;
                    // The format selector for yt-dlp to get video + audio
                    const formatSelector = quality === 'best'
                        ? '-f "bv*+ba/b"'
                        : `-f "bv*[height<=${quality}]+ba/b[height<=${quality}]"`;
                    command = `yt-dlp ${formatSelector} "${videoUrl}"`.trim();
                } else if (type === 'audio') {
                    const format = button.dataset.format;
                    command = `yt-dlp -x --audio-format ${format} "${videoUrl}"`;
                }
                commandInput.value = command;
            };
        });

        copyButton.onclick = () => {
            if (!commandInput.value) return;
            commandInput.select();
            document.execCommand('copy');
            copyButton.textContent = 'Copied!';
            setTimeout(() => { copyButton.textContent = 'Copy Command'; }, 2000);
        };
    }

    /**
     * Injects the custom button into the YouTube UI.
     */
    function injectCustomButton() {
        const actionsMenu = document.querySelector('ytd-watch-metadata #actions-inner #menu');
        if (!actionsMenu || document.getElementById(NEW_BUTTON_ID)) return;
        const shareButtonViewModel = actionsMenu.querySelector('button[aria-label="Share"]');
        if (!shareButtonViewModel) return;
        const shareButtonContainer = shareButtonViewModel.closest('yt-button-view-model');
        if (!shareButtonContainer) return;
        console.log('Download Button: Injecting button...');

        // Clone the share button to inherit its structure and styles
        const newButtonContainer = shareButtonContainer.cloneNode(true);
        const newButton = newButtonContainer.querySelector('button');
        newButton.id = NEW_BUTTON_ID;
        newButton.setAttribute('aria-label', NEW_BUTTON_TEXT);
        newButton.querySelector('.yt-spec-button-shape-next__button-text-content').textContent = NEW_BUTTON_TEXT;

        // --- MODIFICATION START ---
        // Find the icon container element within the new button
        const iconContainer = newButton.querySelector('.yt-spec-button-shape-next__icon');
        if (iconContainer) {
            // Remove the icon container entirely so no space is left for it
            iconContainer.remove();
        }

        // Remove the class that adds padding/styles for a leading icon
        newButton.classList.remove('yt-spec-button-shape-next--icon-leading');
        // --- MODIFICATION END ---

        newButton.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            showDownloadModal();
        });

        shareButtonContainer.parentNode.insertBefore(newButtonContainer, shareButtonContainer.nextSibling);
    }

    /**
     * Use a MutationObserver to wait for the YouTube UI to be ready.
     */
    const observer = new MutationObserver(() => {
        if (document.querySelector('ytd-watch-metadata #actions-inner #menu') && !document.getElementById(NEW_BUTTON_ID)) {
            injectCustomButton();
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });

})();