您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a download button to YouTube videos using Cobalt API for downloading videos or audio.
当前为
// ==UserScript== // @name YouTube Cobalt Tools Download Button // @namespace http://tampermonkey.net/ // @version 0.3 // @description Adds a download button to YouTube videos using Cobalt API for downloading videos or audio. // @author yodaluca23 // @license GNU GPLv3 // @match *://*.youtube.com/* // @grant GM.xmlHttpRequest // @grant GM_notification // ==/UserScript== (function() { 'use strict'; let lastFetchedQualities = []; let currentPageUrl = window.location.href; let initialInjectDelay = 2000; // Initial delay in milliseconds let navigationInjectDelay = 1000; // Delay on navigation in milliseconds // Function to initiate download using Cobalt API function Cobalt(videoUrl, audioOnly = false, quality = '1080', format = 'mp4') { let codec = 'avc1'; if (format === 'mp4' && parseInt(quality.replace('p', '')) > 1080) { codec = 'av1'; } else if (format === 'webm') { codec = 'vp9'; } console.log(`Sending request to Cobalt API: URL=${videoUrl}, AudioOnly=${audioOnly}, Quality=${quality}, Format=${format}, Codec=${codec}`); return new Promise((resolve, reject) => { GM.xmlHttpRequest({ method: 'POST', url: 'https://api.cobalt.tools/api/json', headers: { 'Cache-Control': 'no-cache', Accept: 'application/json', 'Content-Type': 'application/json', }, data: JSON.stringify({ url: encodeURI(videoUrl), vQuality: audioOnly ? parseInt(quality.replace(/\D/g, '')) : quality.replace('p', ''), // Strip units for audio formats codec: codec, filenamePattern: 'basic', isAudioOnly: audioOnly, disableMetadata: true, }), onload: (response) => { const data = JSON.parse(response.responseText); if (data?.url) resolve(data.url); else reject(data); }, onerror: (err) => reject(err), }); }); } // Function to fetch video qualities function fetchVideoQualities(callback) { GM.xmlHttpRequest({ method: 'GET', url: window.location.href, headers: { 'User-Agent': navigator.userAgent, // Use the same user agent as the user's browser 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' }, onload: function(response) { if (response.status === 200) { // Extract video qualities using regular expressions const videoQualities = extractQualities(response.responseText); const strippedQualities = stripQualityLabels(videoQualities); const filteredQualities = filterAndRemoveDuplicates(strippedQualities); console.log('Video Qualities:', filteredQualities); // Update last fetched qualities lastFetchedQualities = filteredQualities; // Execute callback with fetched qualities callback(filteredQualities); } else { console.error('Failed to fetch video qualities. Status:', response.status); callback([]); // Empty array on failure } }, onerror: function(err) { console.error('Error fetching YouTube video page:', err); callback([]); // Empty array on error } }); } // Function to extract video qualities from the HTML response function extractQualities(html) { // Example regex to extract video qualities (modify as per actual YouTube DOM structure) const regex = /"(qualityLabel|width)":"([^"]+)"/g; const qualities = []; let match; while ((match = regex.exec(html)) !== null) { if (match[1] === 'qualityLabel') { qualities.push(match[2]); } } return qualities; } // Function to strip everything after the first "p" in each quality label function stripQualityLabels(qualities) { return qualities.map(quality => { const index = quality.indexOf('p'); return index !== -1 ? quality.substring(0, index + 1) : quality; }); } // Function to filter out premium formats, remove duplicates, and order from greatest to least function filterAndRemoveDuplicates(qualities) { const filteredQualities = []; const seenQualities = new Set(); for (let quality of qualities) { if (!quality.includes('Premium') && !seenQualities.has(quality)) { filteredQualities.push(quality); seenQualities.add(quality); } } // Sort filtered qualities from greatest to least filteredQualities.sort((a, b) => compareQuality(a, b)); return filteredQualities; } // Helper function to compare video quality labels (e.g., "1080p" > "720p") function compareQuality(a, b) { // Extract resolution (assuming format like "1080p") const regex = /(\d+)p/; const resA = parseInt(a.match(regex)[1]); const resB = parseInt(b.match(regex)[1]); // Compare resolutions descending return resB - resA; } // Helper function to check if two arrays are equal (for detecting changes) function arraysEqual(arr1, arr2) { if (arr1.length !== arr2.length) return false; for (let i = 0; i < arr1.length; i++) { if (arr1[i] !== arr2[i]) return false; } return true; } // Function to inject download button on the page function injectDownloadButton() { setTimeout(() => { // Remove existing download button if present const existingButton = document.getElementById('cobalt-download-btn'); if (existingButton) { existingButton.remove(); } const downloadButton = document.createElement('button'); downloadButton.id = 'cobalt-download-btn'; downloadButton.className = 'yt-spec-button-shape-next yt-spec-button-shape-next--tonal yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-leading'; downloadButton.setAttribute('aria-label', 'Download'); downloadButton.setAttribute('title', 'Download'); downloadButton.innerHTML = ` <div class="yt-spec-button-shape-next__icon"> <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" focusable="false" style="pointer-events: none; display: inline-block; width: 24px; height: 24px; vertical-align: middle;"> <path fill="currentColor" d="M17 18v1H6v-1h11zm-.5-6.6-.7-.7-3.8 3.7V4h-1v10.4l-3.8-3.8-.7.7 5 5 5-4.9z"></path> </svg> </div> <div class="yt-spec-button-shape-next__button-text-content">Download</div> `; downloadButton.style.backgroundColor = 'rgb(44, 44, 44)'; downloadButton.style.border = '0px solid rgb(204, 204, 204)'; downloadButton.style.borderRadius = '30px'; downloadButton.style.fontSize = '14px'; downloadButton.style.padding = '8px 16px'; downloadButton.style.cursor = 'pointer'; downloadButton.style.marginLeft = '8px'; // Add spacing to the left downloadButton.style.marginRight = '0px'; // No spacing on the right downloadButton.onclick = () => showQualityPopup(currentPageUrl); const actionMenu = document.querySelector('.top-level-buttons'); actionMenu.appendChild(downloadButton); }, initialInjectDelay); } // Function to remove native YouTube download button function removeNativeDownloadButton() { setTimeout(() => { // Remove download button from overflow menu const nativeDownloadButtonInOverflow = document.querySelector('ytd-menu-service-item-download-renderer'); if (nativeDownloadButtonInOverflow) { nativeDownloadButtonInOverflow.remove(); } // Remove download button next to like/dislike buttons const nativeDownloadButton = document.querySelector('ytd-download-button-renderer'); if (nativeDownloadButton) { nativeDownloadButton.remove(); } }, initialInjectDelay); } // Function to display quality selection popup function showQualityPopup(videoUrl) { fetchVideoQualities((qualities) => { const formatOptions = ['mp4', 'webm', 'ogg', 'mp3', 'opus', 'wav']; // Adjust based on Cobalt API support const qualityPrompt = ` <div id="cobalt-quality-picker" style="background: #fff; padding: 20px; border: 1px solid #ccc; border-radius: 10px; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 9999; max-width: 75px; width: 100%; max-height: 400px; overflow-y: auto;"> <label for="cobalt-format" style="display: block; margin-bottom: 10px;">Format:</label> <select id="cobalt-format" style="margin-bottom: 10px; width: 100%;"> ${formatOptions.map(format => `<option value="${format}">${format}</option>`).join('')} </select> <label id="quality-label" for="cobalt-quality" style="display: block; margin-bottom: 10px;">Quality:</label> <select id="cobalt-quality" style="margin-bottom: 10px; width: 100%;"> ${qualities.map(q => `<option value="${q}">${q}</option>`).join('')} </select> <div id="cobalt-loading" style="display: none; margin-bottom: 10px; text-align: center;">Loading...</div> <button id="cobalt-start-download" style="display: block; margin-top: 10px;">Download</button> </div> `; const popupContainer = document.createElement('div'); popupContainer.innerHTML = qualityPrompt; document.body.appendChild(popupContainer); // Add click listener to close the popup when clicking outside of it document.addEventListener('click', (event) => { if (!popupContainer.contains(event.target)) { document.body.removeChild(popupContainer); } }, { once: true }); const qualityDropdown = document.getElementById('cobalt-quality'); const loadingIndicator = document.getElementById('cobalt-loading'); const formatDropdown = document.getElementById('cobalt-format'); const startDownloadBtn = document.getElementById('cobalt-start-download'); formatDropdown.addEventListener('change', () => { const isAudioFormat = formatDropdown.value === 'mp3' || formatDropdown.value === 'opus' || formatDropdown.value === 'wav'; const qualityLabel = document.getElementById('quality-label'); if (isAudioFormat) { qualityLabel.style.display = 'none'; qualityDropdown.style.display = 'none'; } else { qualityLabel.style.display = 'block'; qualityDropdown.style.display = 'block'; } }); startDownloadBtn.addEventListener('click', async () => { try { loadingIndicator.style.display = 'block'; startDownloadBtn.disabled = true; startDownloadBtn.style.cursor = 'not-allowed'; const format = formatDropdown.value; const quality = qualityDropdown.value; let videoUrl = await Cobalt(currentPageUrl, format === 'mp3' || format === 'opus' || format === 'wav', quality, format); console.log(`Downloading ${format} ${quality} with codec ${format === 'mp4' && parseInt(quality.replace('p', '')) > 1080 ? 'av1' : (format === 'webm' ? 'vp9' : 'avc1')}`); // Simulate download link click let link = document.createElement('a'); link.href = videoUrl; link.setAttribute('download', ''); document.body.appendChild(link); link.click(); } catch (err) { console.error('Error fetching download URL:', err); GM_notification('Failed to fetch download link. Please try again.', 'Error'); } finally { // Hide loading indicator and enable button loadingIndicator.style.display = 'none'; startDownloadBtn.disabled = false; startDownloadBtn.style.cursor = 'pointer'; } // Close the popup after initiating download document.body.removeChild(popupContainer); }); }); } // Function to initialize download button on YouTube video page function initializeDownloadButton() { injectDownloadButton(); removeNativeDownloadButton(); } // Initialize on page load setTimeout(() => { initializeDownloadButton(); }, initialInjectDelay); // Monitor URL changes using history API window.onpopstate = function(event) { setTimeout(() => { if (currentPageUrl !== window.location.href) { currentPageUrl = window.location.href; console.log('URL changed:', currentPageUrl); initializeDownloadButton(); // Reinitialize download button on URL change // Close the format/quality picker menu if a new video is clicked const existingPopup = document.querySelector('#cobalt-quality-picker'); if (existingPopup) { existingPopup.remove(); } } }, navigationInjectDelay); }; // Monitor DOM changes using MutationObserver const observer = new MutationObserver(mutations => { for (let mutation of mutations) { if (mutation.type === 'childList' && mutation.target.classList.contains('html5-video-player')) { console.log('Video player changed'); setTimeout(() => { currentPageUrl = window.location.href; initializeDownloadButton(); // Reinitialize download button if video player changes }, navigationInjectDelay); // Close the format/quality picker menu if a new video is clicked const existingPopup = document.querySelector('#cobalt-quality-picker'); if (existingPopup) { existingPopup.remove(); } break; } } }); observer.observe(document.body, { childList: true, subtree: true, }); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址