Greasy Fork is available in English.

YouTube Cobalt Tools Download Button

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

< Feedback on YouTube Cobalt Tools Download Button

Review: OK - script works, but has bugs

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

})();

Post reply

Đăng nhập để bình luận