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