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.

От 07.07.2025. Виж последната версия.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

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

})();