您需要先安装一个扩展,例如 篡改猴、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 1.1 // @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 currentPageUrl = window.location.href; let initialInjectDelay = 2000; // Initial delay in milliseconds let navigationInjectDelay = 1000; // Delay on navigation in milliseconds // Check if currentPageUrl is YouTube video function isYouTubeWatchURL() { return window.location.href.includes("youtube.com/watch?"); } function removeElement(elementToRemove) { var element = document.querySelector(elementToRemove); if (element) { element.remove(); } } async function findInstance() { try { const response = await fetch('https://corsproxy.io/?https%3A%2F%2Finstances.hyper.lol%2Finstances.json'); // Kept getting CORS errors, so using a CORS proxy, the proxied url is "https://instances.hyper.lol/instances.json" const instances = await response.json(); // Loop through the instances to find the required one for (const instance of instances) { // Check if 'youtube' is true and 'version' is less than 8 if (instance.services.youtube && instance.protocol == 'https' && parseFloat(instance.version) < 8) { return (instance.protocol + "://" + instance.api); } } return null; } catch (error) { console.error('Error fetching instances:', error); return null; } } // Function to initiate download using Cobalt API async function Cobalt(videoUrl, audioOnly = false, quality = '1080', format = 'mp4') { let codec = 'h264'; if (format === 'mp4' && parseInt(quality.replace('p', '')) > 1100) { 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}`); const options = { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ url: videoUrl, vCodec: codec, vQuality: quality.replace('p', ''), filenamePattern: "pretty", isAudioOnly: audioOnly }) }; const apiUrl = await findInstance(); if (!apiUrl) { console.log('No matching instance found.'); return null; // Early exit if no instance found } console.log('Found API URL:', apiUrl); try { const response = await fetch(`${apiUrl}/api/json`, options); const data = await response.json(); console.log(data); if (data.text) { console.error('Video Length is ', (unsafeWindow.ytplayer.config.args.raw_player_response.videoDetails.lengthSeconds/60), ' minutes.\nError fetching from Cobalt API:', data.text); GM_notification(data.text.charAt(0).toUpperCase() + data.text.slice(1)); } return data.url; } catch (error) { console.error('Error fetching from Cobalt API:', error); return null; } } // Function to fetch video qualities function fetchVideoQualities() { const videoQualities = extractQualities(); console.log('Video Qualities:', videoQualities); return videoQualities } function extractQualities() { // Check if ytInitialPlayerResponse is available if (unsafeWindow.ytInitialPlayerResponse && unsafeWindow.ytInitialPlayerResponse.streamingData) { var qualityLabels = unsafeWindow.ytInitialPlayerResponse.streamingData.adaptiveFormats .filter(format => format.qualityLabel) .map(format => format.qualityLabel); // Use regex to extract the first number followed by the first non-numeric character var extractedQualities = qualityLabels.map(label => { const match = label.match(/^(\d+)/); return match ? match[0] : null; }).filter(Boolean); // Filter out any null values // Remove duplicates using Set var uniqueQualityLabels = [...new Set(extractedQualities)]; return uniqueQualityLabels; } else { console.error('ytInitialPlayerResponse or streamingData not found.'); return []; } } function extractFormats() { // Check if ytInitialPlayerResponse is available if (unsafeWindow.ytInitialPlayerResponse && unsafeWindow.ytInitialPlayerResponse.streamingData) { var formats = unsafeWindow.ytInitialPlayerResponse.streamingData.adaptiveFormats .filter(format => format.mimeType) .map(format => format.mimeType); // Regex to extract format const formatRegex = /\/([^;]+)/; var extractedFormats = formats .map(mimeType => { const match = mimeType.match(formatRegex); return match ? match[1] : null; }) .filter(format => format); // Filter out null values // Add "mp3" if it's not already included if (!extractedFormats.includes("mp3")) { extractedFormats.push("mp3"); } // Move "mp4" to the top if present if (extractedFormats.includes("mp4")) { extractedFormats = extractedFormats.filter(format => format !== "mp4"); extractedFormats.unshift("mp4"); } // Remove duplicates using Set var uniqueFormats = [...new Set(extractedFormats)]; return uniqueFormats; } else { console.error('ytInitialPlayerResponse or streamingData not found.'); return []; } } // 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 removeElement('#cobalt-download-btn'); 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'; downloadButton.style.marginRight = '0px'; 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 removeElement('ytd-menu-service-item-download-renderer'); // Remove download button next to like/dislike buttons removeElement('ytd-download-button-renderer'); }, initialInjectDelay); } // Function to display quality selection popup function showQualityPopup(videoUrl) { if (!window.location.href.includes(ytplayer.config.args.raw_player_response.videoDetails.videoId)) { GM_notification('Player Variables out of sync, refreshing, please try again.') setTimeout(() => { location.reload(); }, 2000); } else { var qualities = fetchVideoQualities(); var formatOptions = extractFormats(); 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}p</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 cobaltToolsPopupContainer = document.createElement('div'); cobaltToolsPopupContainer.innerHTML = qualityPrompt; document.body.appendChild(cobaltToolsPopupContainer); // if clicked outside of popup then close the popup const clickHandler = (event) => { if (!cobaltToolsPopupContainer.contains(event.target)) { removeElement('#cobalt-quality-picker'); document.removeEventListener('click', clickHandler); } }; setTimeout(() => { document.addEventListener('click', clickHandler); }, 300); 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 () => { // Remove the close popup click event listener document.removeEventListener('click', clickHandler); loadingIndicator.style.display = 'block'; // Disable changes after initiating download startDownloadBtn.disabled = true; formatDropdown.disabled = true; qualityDropdown.disabled = true; const format = formatDropdown.value; const quality = qualityDropdown.value; let videoUrl = await Cobalt(currentPageUrl, format === 'mp3' || format === 'opus' || format === 'wav', quality, format); if (!videoUrl) { console.error('Failed to fetch download URL.'); loadingIndicator.style.display = 'none'; startDownloadBtn.disabled = false; return; } console.log(`Downloading ${format} ${quality}`); // Create and trigger download link let link = document.createElement('a'); link.href = videoUrl; link.setAttribute('download', ''); document.body.appendChild(link); link.click(); // Clean up document.body.removeChild(link); loadingIndicator.style.display = 'none'; startDownloadBtn.disabled = false; removeElement('#cobalt-quality-picker'); }); } } // Function to initialize download button on YouTube video page function initializeDownloadButton() { injectDownloadButton(); removeNativeDownloadButton(); } // Initialize on page load if (isYouTubeWatchURL()) { 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); if (isYouTubeWatchURL()) { initializeDownloadButton(); // Reinitialize download button on URL change } // Close the format/quality picker menu if a new video is clicked removeElement('#cobalt-quality-picker'); } }, 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; if (isYouTubeWatchURL()) { initializeDownloadButton(); // Reinitialize download button if video player changes } }, navigationInjectDelay); // Close the format/quality picker menu if a new video is clicked removeElement('#cobalt-quality-picker'); break; } } }); observer.observe(document.body, { childList: true, subtree: true, }); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址