🏠 Home 

YouTube Cobalt Tools Download Button

Adds a download button to YouTube videos using Cobalt API for downloading videos or audio.

< 脚本YouTube Cobalt Tools Download Button的反馈

评价:一般 - 脚本能用,但还有一些问题

§
发表于:2024-08-08

Here I have fixed the download button not showing

// ==UserScript==
// @name				 YouTube Cobalt Tools Download Button
// @namespace		http://tampermonkey.net/
// @version			0.4
// @description	Adds a download button to YouTube videos using Cobalt API for downloading videos or audio.
// @author			 yodaluca23
// @license			GNU GPLv3
// @match				https://*.youtube.com/*
// @match				http://*.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
// Check if currentPageUrl is YouTube video
function isYouTubeWatchURL() {
return window.location.href.includes("youtube.com/watch?");
}
// 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');
const buttonContent = document.createElement('div');
buttonContent.className = 'yt-spec-button-shape-next__icon';
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
svg.setAttribute('height', '24');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('width', '24');
svg.setAttribute('focusable', 'false');
svg.setAttribute('style', 'pointer-events: none; display: inline-block; width: 24px; height: 24px; vertical-align: middle;');
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('fill', 'currentColor');
path.setAttribute('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');
svg.appendChild(path);
buttonContent.appendChild(svg);
const buttonTextContent = document.createElement('div');
buttonTextContent.className = 'yt-spec-button-shape-next__button-text-content';
buttonTextContent.textContent = 'Download';
downloadButton.appendChild(buttonContent);
downloadButton.appendChild(buttonTextContent);
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'];
// Create popup container
const popupContainer = document.createElement('div');
popupContainer.id = "cobalt-quality-picker";
popupContainer.style.cssText = `
background: black;
padding: 20px;
border: 1px solid #ccc;
border-radius: 10px;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 9999;
max-width: 300px;
width: 100%;
max-height: 400px;
overflow-y: auto;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
`;
// Header
const header = document.createElement('h2');
header.style.cssText = 'text-align: center; margin-bottom: 20px; font-size: 1.5em; color: #fff;';
header.textContent = 'Select Quality';
popupContainer.appendChild(header);
// Format label and dropdown
const formatLabel = document.createElement('label');
formatLabel.htmlFor = 'cobalt-format';
formatLabel.style.cssText = 'display: block; margin-bottom: 10px; font-weight: bold; color: #fff;';
formatLabel.textContent = 'Format:';
popupContainer.appendChild(formatLabel);
const formatDropdown = document.createElement('select');
formatDropdown.id = 'cobalt-format';
formatDropdown.style.cssText = 'margin-bottom: 10px; width: 100%; padding: 5px; border-radius: 5px; border: 1px solid #ccc; color: #fff; background: black;';
formatOptions.forEach(format => {
const option = document.createElement('option');
option.value = format;
option.textContent = format;
formatDropdown.appendChild(option);
});
popupContainer.appendChild(formatDropdown);
// Quality label and dropdown
const qualityLabel = document.createElement('label');
qualityLabel.id = 'quality-label';
qualityLabel.htmlFor = 'cobalt-quality';
qualityLabel.style.cssText = 'display: block; margin-bottom: 10px; font-weight: bold; color: #fff;';
qualityLabel.textContent = 'Quality:';
popupContainer.appendChild(qualityLabel);
const qualityDropdown = document.createElement('select');
qualityDropdown.id = 'cobalt-quality';
qualityDropdown.style.cssText = 'margin-bottom: 10px; width: 100%; padding: 5px; border-radius: 5px; border: 1px solid #ccc; color: #fff; background: black;';
qualities.forEach(q => {
const option = document.createElement('option');
option.value = q;
option.textContent = q;
qualityDropdown.appendChild(option);
});
popupContainer.appendChild(qualityDropdown);
// Loading indicator
const loadingIndicator = document.createElement('div');
loadingIndicator.id = 'cobalt-loading';
loadingIndicator.style.cssText = 'display: none; margin-bottom: 10px; text-align: center; color: #fff;';
loadingIndicator.textContent = 'Loading...';
popupContainer.appendChild(loadingIndicator);
// Download button
const startDownloadBtn = document.createElement('button');
startDownloadBtn.id = 'cobalt-start-download';
startDownloadBtn.style.cssText = 'display: block; width: 100%; padding: 10px; background-color: black; color: #fff; border: none; border-radius: 5px; cursor: pointer;';
startDownloadBtn.textContent = 'Download';
popupContainer.appendChild(startDownloadBtn);
document.body.appendChild(popupContainer);
// Event listeners for interactions
document.addEventListener('click', (event) => {
if (!popupContainer.contains(event.target)) {
document.body.removeChild(popupContainer);
}
}, { once: true });
formatDropdown.addEventListener('change', () => {
const isAudioFormat = formatDropdown.value === 'mp3' || formatDropdown.value === 'opus' || formatDropdown.value === 'wav';
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
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
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;
if (isYouTubeWatchURL()) {
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,
});
})();

发表回复

登录以发表回复。