您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Injects a "Download" button that opens a modal to generate yt-dlp commands on YouTube video pages.
// ==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">×</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 }); })();