Adds a download button to YouTube videos using Cobalt API for downloading videos or audio.
< YouTube Cobalt Tools Download Button 피드백
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, }); })();
댓글을 남기려면 로그인하세요.
Here I have fixed the download button not showing