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.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

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

})();