您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add a custom download button and provide options to download the video or audio directly from the YouTube page.
// ==UserScript== // @name YouTube Direct Downloader // @description Add a custom download button and provide options to download the video or audio directly from the YouTube page. // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com // @version 1.5 // @author afkarxyz // @namespace https://github.com/afkarxyz/userscripts/ // @supportURL https://github.com/afkarxyz/userscripts/issues // @license MIT // @match https://www.youtube.com/* // @match https://youtube.com/* // @grant GM.xmlHttpRequest // @grant GM_download // @grant GM.download // @grant GM_setValue // @grant GM_getValue // @connect api.mp3youtube.cc // @connect iframe.y2meta-uk.com // @connect * // @run-at document-end // ==/UserScript== (function() { 'use strict'; let lastSelectedFormat = GM_getValue('lastSelectedFormat', 'video'); let lastSelectedVideoQuality = GM_getValue('lastSelectedVideoQuality', '1080'); let lastSelectedAudioBitrate = GM_getValue('lastSelectedAudioBitrate', '320'); const API_KEY_URL = 'https://api.mp3youtube.cc/v2/sanity/key'; const API_CONVERT_URL = 'https://api.mp3youtube.cc/v2/converter'; const REQUEST_HEADERS = { "Content-Type": "application/json", "Origin": "https://iframe.y2meta-uk.com", "Accept": "*/*", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" }; const style = document.createElement('style'); style.textContent = ` .ytddl-download-btn { width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; margin-left: 8px; transition: background-color 0.2s; } html[dark] .ytddl-download-btn { background-color: #ffffff1a; } html:not([dark]) .ytddl-download-btn { background-color: #0000000d; } html[dark] .ytddl-download-btn:hover { background-color: #ffffff33; } html:not([dark]) .ytddl-download-btn:hover { background-color: #00000014; } .ytddl-download-btn svg { width: 18px; height: 18px; } html[dark] .ytddl-download-btn svg { fill: var(--yt-spec-text-primary, #fff); } html:not([dark]) .ytddl-download-btn svg { fill: var(--yt-spec-text-primary, #030303); } .ytddl-dialog { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #000000; color: #e1e1e1; border-radius: 12px; box-shadow: 0 0 0 1px rgba(225,225,225,.1), 0 2px 4px 1px rgba(225,225,225,.18); font-family: 'IBM Plex Mono', 'Noto Sans Mono Variable', 'Noto Sans Mono', monospace; width: 400px; z-index: 9999; padding: 16px; } .ytddl-backdrop { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 9998; } .ytddl-dialog h3 { margin: 0 0 16px 0; font-size: 18px; font-weight: 700; } .quality-options { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 16px; } .quality-option { display: flex; align-items: center; padding: 8px; cursor: pointer; border-radius: 6px; } .quality-option:hover { background: #191919; } .quality-option input[type="radio"] { margin-right: 8px; } .download-status { text-align: center; margin: 16px 0; font-size: 12px; display: none; color: #1ed760; } .button-container { display: flex; justify-content: center; gap: 8px; margin-top: 16px; } .ytddl-button { background: transparent; border: 1px solid #e1e1e1; color: #e1e1e1; font-size: 14px; font-weight: 500; padding: 8px 16px; border-radius: 18px; cursor: pointer; font-family: inherit; transition: all 0.2s; } .ytddl-button:hover { background: #1ed760; border-color: #1ed760; color: #000000; } .ytddl-button.cancel:hover { background: #f3727f; border-color: #f3727f; color: #000000; } .format-selector { margin-bottom: 16px; display: flex; gap: 8px; justify-content: center; } .format-button { background: transparent; border: 1px solid #e1e1e1; color: #e1e1e1; padding: 6px 12px; border-radius: 14px; cursor: pointer; font-family: inherit; font-size: 12px; transition: all 0.2s ease; } .format-button:hover { background: #808080; color: #000000; } .format-button.selected { background: #1ed760; border-color: #1ed760; color: #000000; } .ytddl-overlay { position: fixed; top: 20px; right: 20px; background: rgba(0, 0, 0, 0.9); color: #e1e1e1; border-radius: 8px; padding: 16px; width: 350px; max-width: 350px; z-index: 10000; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); border: 1px solid rgba(255, 255, 255, 0.1); backdrop-filter: blur(10px); opacity: 0; transform: translateX(100%); transition: all 0.3s ease; } .ytddl-overlay.show { opacity: 1; transform: translateX(0); } .ytddl-overlay-content { line-height: 1.5; } .ytddl-overlay-status { margin-bottom: 8px; color: #1ed760; font-weight: 500; } .ytddl-overlay-details { color: #ccc; font-size: 13px; margin-bottom: 12px; } .ytddl-overlay-file-info { display: flex; justify-content: space-between; margin-bottom: 8px; font-size: 12px; } .ytddl-overlay-size { color: #1ed760; font-weight: 500; } .ytddl-overlay-speed { color: #ffa500; font-weight: 500; } .ytddl-overlay-error { color: #ff6b6b; } .ytddl-overlay-success { color: #1ed760; } `; document.head.appendChild(style); let currentOverlay = null; function createOverlay() { if (currentOverlay) { removeOverlay(); } const overlay = document.createElement('div'); overlay.className = 'ytddl-overlay'; const content = document.createElement('div'); content.className = 'ytddl-overlay-content'; const status = document.createElement('div'); status.className = 'ytddl-overlay-status'; status.textContent = 'Initializing...'; const details = document.createElement('div'); details.className = 'ytddl-overlay-details'; details.textContent = 'Preparing download request'; const fileInfoContainer = document.createElement('div'); fileInfoContainer.className = 'ytddl-overlay-file-info'; const sizeElement = document.createElement('div'); sizeElement.className = 'ytddl-overlay-size'; sizeElement.textContent = 'Size: Calculating...'; const speedElement = document.createElement('div'); speedElement.className = 'ytddl-overlay-speed'; speedElement.textContent = 'Speed: -'; fileInfoContainer.appendChild(sizeElement); fileInfoContainer.appendChild(speedElement); content.appendChild(status); content.appendChild(details); content.appendChild(fileInfoContainer); overlay.appendChild(content); overlay.addEventListener('click', function(e) { if (e.target === overlay) { removeOverlay(); } }); document.body.appendChild(overlay); setTimeout(() => { overlay.classList.add('show'); }, 100); currentOverlay = overlay; return overlay; } function updateOverlay(status, details, fileSize = null, downloadSpeed = null, isError = false, isSuccess = false) { if (!currentOverlay) return; const statusEl = currentOverlay.querySelector('.ytddl-overlay-status'); const detailsEl = currentOverlay.querySelector('.ytddl-overlay-details'); const sizeEl = currentOverlay.querySelector('.ytddl-overlay-size'); const speedEl = currentOverlay.querySelector('.ytddl-overlay-speed'); if (statusEl) { statusEl.textContent = status; statusEl.className = 'ytddl-overlay-status'; if (isError) statusEl.classList.add('ytddl-overlay-error'); if (isSuccess) statusEl.classList.add('ytddl-overlay-success'); } if (detailsEl) { detailsEl.textContent = details; } if (sizeEl) { if (fileSize !== null) { sizeEl.textContent = `Size: ${fileSize}`; sizeEl.style.display = 'block'; } else { sizeEl.style.display = 'none'; } } if (speedEl) { if (downloadSpeed !== null) { speedEl.textContent = `Speed: ${downloadSpeed}`; speedEl.style.display = 'block'; } else { speedEl.style.display = 'none'; } } currentOverlay.offsetHeight; } function removeOverlay() { if (currentOverlay) { currentOverlay.classList.remove('show'); setTimeout(() => { if (currentOverlay && currentOverlay.parentNode) { currentOverlay.parentNode.removeChild(currentOverlay); } currentOverlay = null; }, 300); } } function formatBytes(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } function truncateTitle(title, maxLength = 50) { if (!title || title.length <= maxLength) return title; return title.substring(0, maxLength - 3) + '...'; } function triggerDirectDownload(url, filename) { let downloadStartTime = Date.now(); updateOverlay('Validating download URL', 'Testing download link...', null, null); GM.xmlHttpRequest({ method: 'HEAD', url: url, headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" }, onload: function(response) { console.log('URL Test Response:', response.status, response.statusText); console.log('Content-Length:', response.responseHeaders.match(/content-length:\s*(\d+)/i)); if (response.status === 200 || response.status === 206) { fetchAndDownload(url, filename, downloadStartTime); } else { updateOverlay( 'Download failed', `Invalid download URL (Status: ${response.status})`, null, null, true ); setTimeout(removeOverlay, 2500); } }, onerror: function(error) { console.error('URL validation failed:', error); updateOverlay( 'Download failed', 'Cannot access download URL - may be expired or invalid', null, null, true ); setTimeout(removeOverlay, 2500); } }); } function fetchAndDownload(url, filename, downloadStartTime) { updateOverlay('Starting download', 'Connecting to server...', '0 B', '0 B/s'); console.log('=== FETCH AND DOWNLOAD ==='); console.log('URL:', url); console.log('Filename:', filename); console.log('Method: GM.xmlHttpRequest with responseType blob'); console.log('Start time:', new Date(downloadStartTime).toISOString()); console.log('=========================='); let totalSize = 0; let downloadedSize = 0; let lastUpdateTime = 0; const UPDATE_INTERVAL = 250; GM.xmlHttpRequest({ method: 'GET', url: url, responseType: 'blob', headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Referer": "https://iframe.y2meta-uk.com/", "Accept": "*/*" }, onprogress: function(progressEvent) { const currentTime = Date.now(); const elapsed = (currentTime - downloadStartTime) / 1000; const shouldUpdate = (currentTime - lastUpdateTime) >= UPDATE_INTERVAL || (progressEvent.lengthComputable && progressEvent.loaded === progressEvent.total); if (progressEvent.lengthComputable) { totalSize = progressEvent.total; downloadedSize = progressEvent.loaded; const percentage = Math.round((downloadedSize / totalSize) * 100); const speed = elapsed > 0 ? downloadedSize / elapsed : 0; if (shouldUpdate) { const sizeText = `${formatBytes(downloadedSize)} / ${formatBytes(totalSize)}`; const speedText = `${formatBytes(speed)}/s`; const percentText = `${percentage}%`; updateOverlay( `Downloading ${percentText}`, `${filename || 'video.mp4'}`, sizeText, speedText ); lastUpdateTime = currentTime; } if ((currentTime - lastUpdateTime) >= 1000 || percentage === 100) { console.log(`[${elapsed.toFixed(1)}s] Progress: ${percentage}% | Downloaded: ${formatBytes(downloadedSize)}/${formatBytes(totalSize)} | Speed: ${formatBytes(speed)}/s`); } } else { downloadedSize = progressEvent.loaded || 0; const speed = elapsed > 0 ? downloadedSize / elapsed : 0; if (shouldUpdate) { const sizeText = `${formatBytes(downloadedSize)}`; const speedText = `${formatBytes(speed)}/s`; const timeText = `${elapsed.toFixed(1)}s`; updateOverlay( `Downloading...`, `${filename || 'video.mp4'} - ${timeText}`, sizeText, speedText ); lastUpdateTime = currentTime; } if ((currentTime - lastUpdateTime) >= 1000) { console.log(`[${elapsed.toFixed(1)}s] Downloaded: ${formatBytes(downloadedSize)} | Speed: ${formatBytes(speed)}/s`); } } }, onload: function(response) { console.log('Fetch completed. Response status:', response.status); console.log('Response type:', typeof response.response); console.log('Response size:', response.response?.size || 'unknown'); if (response.status === 200 && response.response) { updateOverlay('Creating download file', 'Converting to downloadable file...', formatBytes(response.response.size || 0), 'Processing'); try { const blob = response.response; const blobUrl = URL.createObjectURL(blob); console.log('Blob created:', blob.size, 'bytes'); console.log('Blob URL:', blobUrl); const a = document.createElement('a'); a.style.display = 'none'; a.href = blobUrl; a.download = filename || 'video.mp4'; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(blobUrl); }, 1000); updateOverlay( 'Download completed successfully!', `${filename || 'video.mp4'}`, formatBytes(blob.size), 'Complete', false, true ); console.log('✅ Download successful via blob method'); setTimeout(() => { removeOverlay(); }, 2500); } catch (blobError) { console.error('Blob download failed:', blobError); updateOverlay( 'Blob conversion failed', 'Trying alternative download methods...', null, null, true ); setTimeout(() => { proceedWithDownload(url, filename, downloadStartTime); }, 2000); } } else { console.error('Fetch failed with status:', response.status); updateOverlay( 'Data fetch failed', `Server returned status ${response.status}`, null, null, true ); setTimeout(() => { proceedWithDownload(url, filename, downloadStartTime); }, 2000); } }, onerror: function(error) { console.error('GM.xmlHttpRequest fetch failed:', error); updateOverlay( 'Data fetch failed', 'Trying native fetch method...', null, null, true ); setTimeout(() => { nativeFetchDownload(url, filename, downloadStartTime); }, 2000); }, ontimeout: function() { console.error('GM.xmlHttpRequest fetch timeout'); updateOverlay( 'Download timeout', 'Trying native fetch method...', null, null, true ); setTimeout(() => { nativeFetchDownload(url, filename, downloadStartTime); }, 2000); } }); } async function nativeFetchDownload(url, filename, downloadStartTime) { updateOverlay('Trying native fetch', 'Using browser fetch API...', 'Starting...', 'Native method'); console.log('=== NATIVE FETCH DOWNLOAD ==='); console.log('URL:', url); console.log('Filename:', filename); console.log('Method: Native fetch API with ReadableStream'); console.log('============================='); try { const response = await fetch(url, { method: 'GET', headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Referer": "https://iframe.y2meta-uk.com/", "Accept": "*/*" } }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const contentLength = response.headers.get('content-length'); const totalSize = contentLength ? parseInt(contentLength, 10) : 0; console.log('Native fetch response OK. Content-Length:', totalSize); if (response.body && totalSize > 0) { const reader = response.body.getReader(); const chunks = []; let downloadedSize = 0; updateOverlay( 'Downloading with native fetch', `Total size: ${formatBytes(totalSize)}`, '0%', 'Starting...' ); while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); downloadedSize += value.length; const percentage = (downloadedSize / totalSize) * 100; const elapsed = (Date.now() - downloadStartTime) / 1000; const speed = elapsed > 0 ? downloadedSize / elapsed : 0; const sizeText = `${formatBytes(downloadedSize)} / ${formatBytes(totalSize)}`; const speedText = `${formatBytes(speed)}/s`; updateOverlay( 'Downloading with native fetch', `${Math.round(percentage)}% - ${filename || 'video.mp4'}`, sizeText, speedText ); console.log(`Native fetch progress: ${Math.round(percentage)}% | ${sizeText} | ${speedText}`); } const blob = new Blob(chunks); console.log('Native fetch blob created from chunks:', blob.size, 'bytes'); const blobUrl = URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = blobUrl; a.download = filename || 'video.mp4'; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(blobUrl); }, 1000); updateOverlay( 'Native fetch download completed!', `${filename || 'video.mp4'}`, formatBytes(blob.size), 'Complete', false, true ); } else { updateOverlay('Downloading with native fetch', 'Size unknown, downloading...', 'Unknown size', 'Downloading...'); const blob = await response.blob(); console.log('Native fetch blob created (no progress):', blob.size, 'bytes'); const blobUrl = URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = blobUrl; a.download = filename || 'video.mp4'; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(blobUrl); }, 1000); updateOverlay( 'Native fetch download completed!', `${filename || 'video.mp4'}`, formatBytes(blob.size), 'Complete', false, true ); } console.log('✅ Download successful via native fetch'); setTimeout(() => { removeOverlay(); }, 2500); } catch (fetchError) { console.error('Native fetch failed:', fetchError); updateOverlay( 'Native fetch failed', `Error: ${fetchError.message}`, null, null, true ); setTimeout(() => { proceedWithDownload(url, filename, downloadStartTime); }, 2000); } }function proceedWithDownload(url, filename, downloadStartTime) { console.log('=== FALLBACK DOWNLOAD METHODS ==='); console.log('GM_download (legacy):', typeof GM_download, GM_download); console.log('GM.download (new):', typeof GM?.download, GM?.download); console.log('Download URL:', url); console.log('Filename:', filename); console.log('=================================='); updateOverlay('Opening download in new tab', 'Most reliable method for video downloads', null, null); try { const downloadWindow = window.open(url, '_blank', 'noopener,noreferrer'); if (downloadWindow) { console.log('New tab opened successfully'); updateOverlay( 'Download opened in new tab', `${truncateTitle(filename || 'video.mp4')} - Check Downloads folder`, 'Via new tab', 'Browser handling', false, true ); setTimeout(() => { removeOverlay(); }, 10000); return; } else { console.log('New tab blocked, trying GM_download'); attemptGMDownload(url, filename, downloadStartTime); } } catch (error) { console.error('New tab method failed:', error); attemptGMDownload(url, filename, downloadStartTime); } } function attemptGMDownload(url, filename, downloadStartTime) { const gmDownloadAvailable = typeof GM_download !== 'undefined' && GM_download; const gmDownloadNewSyntax = typeof GM !== 'undefined' && GM.download; if (gmDownloadAvailable) { try { updateOverlay('Trying GM_download method', `File: ${truncateTitle(filename || 'video.mp4')}`, 'Initializing...', '-'); console.log('Using GM_download (legacy API)'); const downloadId = GM_download(url, filename || 'video.mp4', { headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Referer": "https://iframe.y2meta-uk.com/", "Accept": "*/*" }, onprogress: function(progressEvent) { console.log('Download progress (legacy):', progressEvent); if (progressEvent.lengthComputable) { const totalSize = progressEvent.total; const downloadedSize = progressEvent.loaded; const percentage = (downloadedSize / totalSize) * 100; const elapsed = (Date.now() - downloadStartTime) / 1000; const speed = downloadedSize / elapsed; const sizeText = `${formatBytes(downloadedSize)} / ${formatBytes(totalSize)}`; const speedText = `${formatBytes(speed)}/s`; updateOverlay( 'Downloading via GM_download', `${Math.round(percentage)}% - ${truncateTitle(filename || 'video.mp4')}`, sizeText, speedText ); } else { const elapsed = (Date.now() - downloadStartTime) / 1000; updateOverlay( 'Downloading via GM_download', `${truncateTitle(filename || 'video.mp4')} - ${elapsed.toFixed(1)}s elapsed`, 'Size unknown', 'Progress...' ); } }, onload: function() { console.log('Download completed successfully (legacy)'); updateOverlay( 'Download completed successfully', `${truncateTitle(filename || 'video.mp4')}`, 'Complete', 'Done', false, true ); setTimeout(() => { removeOverlay(); }, 2500); }, onerror: function(error) { console.error('GM_download error (legacy):', error); updateOverlay( 'GM_download failed', 'Trying alternative download methods...', null, null, true ); setTimeout(() => { fallbackDownload(url, filename); }, 2000); } }); console.log('Download ID (legacy):', downloadId); setTimeout(() => { console.log('Checking if GM_download callbacks fired...'); updateOverlay( 'GM_download may have CORS issues', 'Switching to fallback methods...', null, null, true ); setTimeout(() => { fallbackDownload(url, filename); }, 2000); }, 2500); } catch (downloadError) { console.error('GM_download exception:', downloadError); tryNewGMDownload(url, filename, downloadStartTime); } } else if (gmDownloadNewSyntax) { tryNewGMDownload(url, filename, downloadStartTime); } else { console.log('No GM download APIs available, using fallback'); fallbackDownload(url, filename); } } function tryNewGMDownload(url, filename, downloadStartTime) { try { updateOverlay('Trying GM.download (new API)', `File: ${filename || 'video.mp4'}`, 'Initializing...', '-'); console.log('Using GM.download (new API)'); GM.download(url, filename || 'video.mp4', { headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Referer": "https://iframe.y2meta-uk.com/", "Accept": "*/*" }, onprogress: function(progressEvent) { console.log('Download progress (new):', progressEvent); if (progressEvent.lengthComputable) { const totalSize = progressEvent.total; const downloadedSize = progressEvent.loaded; const percentage = (downloadedSize / totalSize) * 100; const elapsed = (Date.now() - downloadStartTime) / 1000; const speed = downloadedSize / elapsed; const sizeText = `${formatBytes(downloadedSize)} / ${formatBytes(totalSize)}`; const speedText = `${formatBytes(speed)}/s`; updateOverlay( 'Downloading via GM.download', `${Math.round(percentage)}% - ${filename || 'video.mp4'}`, sizeText, speedText ); } else { const elapsed = (Date.now() - downloadStartTime) / 1000; updateOverlay( 'Downloading via GM.download', `${filename || 'video.mp4'} - ${elapsed.toFixed(1)}s elapsed`, 'Size unknown', 'Progress...' ); } }, onload: function() { console.log('Download completed successfully (new)'); updateOverlay( 'Download completed successfully', `${filename || 'video.mp4'}`, 'Complete', 'Done', false, true ); setTimeout(() => { removeOverlay(); }, 2500); }, onerror: function(error) { console.error('GM.download error (new):', error); updateOverlay( 'GM.download failed', 'Trying alternative download methods...', null, null, true ); setTimeout(() => { fallbackDownload(url, filename); }, 2000); } }).then(downloadId => { console.log('Download ID (new):', downloadId); setTimeout(() => { console.log('Checking if GM.download callbacks fired...'); updateOverlay( 'GM.download may have CORS issues', 'Switching to fallback methods...', null, null, true ); setTimeout(() => { fallbackDownload(url, filename); }, 2000); }, 2500); }).catch(error => { console.error('GM.download promise error:', error); fallbackDownload(url, filename); }); } catch (downloadError) { console.error('GM.download exception:', downloadError); fallbackDownload(url, filename); } }function fallbackDownload(url, filename) { updateOverlay('Using direct download methods', 'Testing browser download capabilities...', null, null); console.log('=== FALLBACK DOWNLOAD ==='); console.log('URL:', url); console.log('Filename:', filename); console.log('========================='); try { const a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = filename || 'video.mp4'; a.target = '_blank'; a.rel = 'noopener noreferrer'; document.body.appendChild(a); updateOverlay('Method 1: Force download link', 'Creating download trigger...', null, null); const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true, view: window }); a.dispatchEvent(clickEvent); a.click(); setTimeout(() => { document.body.removeChild(a); updateOverlay( 'Download link triggered', `${filename || 'video.mp4'} - Check Downloads folder`, 'Via download link', 'Browser handling', false, true ); setTimeout(() => { trySecondaryMethod(url, filename); }, 3000); }, 1000); } catch (error) { console.error('Direct link method failed:', error); trySecondaryMethod(url, filename); } } function trySecondaryMethod(url, filename) { updateOverlay('Method 2: Location redirect', 'Attempting direct navigation...', null, null); try { const downloadWindow = window.open('', '_blank'); if (downloadWindow) { downloadWindow.location.href = url; updateOverlay( 'Download redirected to new tab', `${filename || 'video.mp4'} - Check new tab`, 'Via location redirect', 'Tab navigation', false, true ); setTimeout(() => { tryFinalMethod(url, filename); }, 2500); } else { tryFinalMethod(url, filename); } } catch (error) { console.error('Secondary method failed:', error); tryFinalMethod(url, filename); } } function tryFinalMethod(url, filename) { updateOverlay('Method 3: Manual URL access', 'Preparing manual download option...', null, null); try { navigator.clipboard.writeText(url).then(() => { updateOverlay( 'URL copied to clipboard!', 'Open new tab and paste (Ctrl+L, Ctrl+V, Enter)', 'Clipboard ready', 'Manual paste', false, true ); console.log('=== MANUAL DOWNLOAD URL ==='); console.log('URL copied to clipboard. Paste in new tab:'); console.log(url); console.log('Filename:', filename); console.log('==========================='); }).catch(() => { showConsoleMethod(url, filename); }); } catch (error) { showConsoleMethod(url, filename); } setTimeout(removeOverlay, 10000); } function showConsoleMethod(url, filename) { console.log('=== MANUAL DOWNLOAD URL ==='); console.log('Copy this URL and paste in new browser tab:'); console.log(url); console.log('Filename:', filename); console.log('==========================='); updateOverlay( 'Check browser console (F12)', 'URL available in console for manual copy', 'Console method', 'Manual copy/paste', false, false ); } function createDownloadDialog() { const dialog = document.createElement('div'); dialog.className = 'ytddl-dialog'; const title = document.createElement('h3'); title.textContent = ''; const formatSelector = document.createElement('div'); formatSelector.className = 'format-selector'; const videoBtn = document.createElement('button'); videoBtn.className = `format-button ${lastSelectedFormat === 'video' ? 'selected' : ''}`; videoBtn.setAttribute('data-format', 'video'); videoBtn.textContent = 'VIDEO (MP4)'; const audioBtn = document.createElement('button'); audioBtn.className = `format-button ${lastSelectedFormat === 'audio' ? 'selected' : ''}`; audioBtn.setAttribute('data-format', 'audio'); audioBtn.textContent = 'AUDIO (MP3)'; formatSelector.appendChild(videoBtn); formatSelector.appendChild(audioBtn); const qualityContainer = document.createElement('div'); qualityContainer.id = 'quality-container'; const videoQualities = document.createElement('div'); videoQualities.className = 'quality-options'; videoQualities.id = 'video-qualities'; videoQualities.style.display = lastSelectedFormat === 'video' ? 'grid' : 'none'; ['144p', '240p', '360p', '480p', '720p', '1080p'].forEach((quality, index) => { const option = document.createElement('div'); option.className = 'quality-option'; const input = document.createElement('input'); input.type = 'radio'; input.id = `quality-${index}`; input.name = 'quality'; input.value = quality.replace('p', ''); const label = document.createElement('label'); label.setAttribute('for', `quality-${index}`); label.textContent = quality; label.style.fontSize = '14px'; label.style.cursor = 'pointer'; option.appendChild(input); option.appendChild(label); videoQualities.appendChild(option); option.addEventListener('click', function() { input.checked = true; GM_setValue('lastSelectedVideoQuality', input.value); lastSelectedVideoQuality = input.value; }); }); const defaultQuality = videoQualities.querySelector(`input[value="${lastSelectedVideoQuality}"]`); if (defaultQuality) { defaultQuality.checked = true; } const audioQualities = document.createElement('div'); audioQualities.className = 'quality-options'; audioQualities.id = 'audio-qualities'; audioQualities.style.display = lastSelectedFormat === 'audio' ? 'grid' : 'none'; ['128', '256', '320'].forEach((bitrate, index) => { const option = document.createElement('div'); option.className = 'quality-option'; const input = document.createElement('input'); input.type = 'radio'; input.id = `bitrate-${index}`; input.name = 'bitrate'; input.value = bitrate; const label = document.createElement('label'); label.setAttribute('for', `bitrate-${index}`); label.textContent = `${bitrate} kbps`; label.style.fontSize = '14px'; label.style.cursor = 'pointer'; option.appendChild(input); option.appendChild(label); audioQualities.appendChild(option); option.addEventListener('click', function() { input.checked = true; GM_setValue('lastSelectedAudioBitrate', input.value); lastSelectedAudioBitrate = input.value; }); }); const defaultBitrate = audioQualities.querySelector(`input[value="${lastSelectedAudioBitrate}"]`); if (defaultBitrate) { defaultBitrate.checked = true; } qualityContainer.appendChild(videoQualities); qualityContainer.appendChild(audioQualities); const downloadStatus = document.createElement('div'); downloadStatus.className = 'download-status'; downloadStatus.id = 'download-status'; const buttonContainer = document.createElement('div'); buttonContainer.className = 'button-container'; const cancelButton = document.createElement('button'); cancelButton.className = 'ytddl-button cancel'; cancelButton.textContent = 'Cancel'; const downloadButton = document.createElement('button'); downloadButton.className = 'ytddl-button'; downloadButton.textContent = 'Download'; buttonContainer.appendChild(cancelButton); buttonContainer.appendChild(downloadButton); dialog.appendChild(title); dialog.appendChild(formatSelector); dialog.appendChild(qualityContainer); dialog.appendChild(downloadStatus); dialog.appendChild(buttonContainer); formatSelector.addEventListener('click', (e) => { if (e.target.classList.contains('format-button')) { formatSelector.querySelectorAll('.format-button').forEach(btn => { btn.classList.remove('selected'); }); e.target.classList.add('selected'); const format = e.target.getAttribute('data-format'); if (format === 'video') { videoQualities.style.display = 'grid'; audioQualities.style.display = 'none'; lastSelectedFormat = 'video'; GM_setValue('lastSelectedFormat', 'video'); } else { videoQualities.style.display = 'none'; audioQualities.style.display = 'grid'; lastSelectedFormat = 'audio'; GM_setValue('lastSelectedFormat', 'audio'); } } }); const backdrop = document.createElement('div'); backdrop.className = 'ytddl-backdrop'; return { dialog, backdrop, cancelButton, downloadButton }; } function closeDialog(dialog, backdrop) { if (dialog && dialog.parentNode) { dialog.parentNode.removeChild(dialog); } if (backdrop && backdrop.parentNode) { backdrop.parentNode.removeChild(backdrop); } } function extractVideoId(url) { const urlObj = new URL(url); const searchParams = new URLSearchParams(urlObj.search); return searchParams.get('v'); } async function downloadWithMP3YouTube(videoUrl, format, quality) { const statusElement = document.getElementById('download-status'); createOverlay(); if (statusElement) { statusElement.style.display = 'block'; statusElement.textContent = 'Getting API key...'; } try { updateOverlay('Getting API key', 'Connecting to MP3YouTube API...'); const keyResponse = await new Promise((resolve, reject) => { GM.xmlHttpRequest({ method: 'GET', url: API_KEY_URL, headers: REQUEST_HEADERS, onload: resolve, onerror: reject, ontimeout: reject }); }); const keyData = JSON.parse(keyResponse.responseText); if (!keyData || !keyData.key) { throw new Error('Failed to get API key'); } const key = keyData.key; updateOverlay('Processing request', `${format} (${format === 'video' ? quality + 'p' : quality + ' kbps'})`); if (statusElement) { statusElement.textContent = 'Processing download...'; } let payload; if (format === 'video') { payload = { "link": videoUrl, "format": "mp4", "audioBitrate": "128", "videoQuality": quality, "filenameStyle": "pretty", "vCodec": "h264" }; } else { payload = { "link": videoUrl, "format": "mp3", "audioBitrate": quality, "filenameStyle": "pretty" }; } const customHeaders = { ...REQUEST_HEADERS, "key": key }; updateOverlay('Converting media', 'Processing video/audio conversion...'); const downloadResponse = await new Promise((resolve, reject) => { GM.xmlHttpRequest({ method: 'POST', url: API_CONVERT_URL, headers: customHeaders, data: JSON.stringify(payload), onload: resolve, onerror: reject, ontimeout: reject }); }); const downloadInfo = JSON.parse(downloadResponse.responseText); if (downloadInfo.url) { updateOverlay('Starting download', `File: ${truncateTitle(downloadInfo.filename || `video.${format === 'video' ? 'mp4' : 'mp3'}`)}`); if (statusElement) { statusElement.textContent = 'Starting download...'; } triggerDirectDownload(downloadInfo.url, downloadInfo.filename); return downloadInfo; } else { throw new Error('No download URL received from API'); }} catch (error) { updateOverlay('Download failed', `Error: ${error.message}`, null, null, true); setTimeout(() => { removeOverlay(); }, 4000); throw error; } } function createDownloadButton() { const downloadButton = document.createElement('div'); downloadButton.className = 'ytddl-download-btn'; const svgNS = "http://www.w3.org/2000/svg"; const svg = document.createElementNS(svgNS, "svg"); svg.setAttribute("viewBox", "0 0 512 512"); const path = document.createElementNS(svgNS, "path"); path.setAttribute("d", "M256 464c114.9 0 208-93.1 208-208c0-13.3 10.7-24 24-24s24 10.7 24 24c0 141.4-114.6 256-256 256S0 397.4 0 256c0-13.3 10.7-24 24-24s24 10.7 24 24c0 114.9 93.1 208 208 208zM377.6 232.3l-104 112c-4.5 4.9-10.9 7.7-17.6 7.7s-13-2.8-17.6-7.7l-104-112c-9-9.7-8.5-24.9 1.3-33.9s24.9-8.5 33.9 1.3L232 266.9 232 24c0-13.3 10.7-24 24-24s24 10.7 24 24l0 242.9 62.4-67.2c9-9.7 24.2-10.3 33.9-1.3s10.3 24.2 1.3 33.9z"); svg.appendChild(path); downloadButton.appendChild(svg); downloadButton.addEventListener('click', function() { showDownloadDialog(); }); return downloadButton; } function showDownloadDialog() { const videoUrl = window.location.href; const videoId = extractVideoId(videoUrl); if (!videoId) { alert('Could not extract video ID from URL'); return; } const { dialog, backdrop, cancelButton, downloadButton } = createDownloadDialog(); document.body.appendChild(backdrop); document.body.appendChild(dialog); backdrop.addEventListener('click', () => { closeDialog(dialog, backdrop); }); cancelButton.addEventListener('click', () => { closeDialog(dialog, backdrop); }); downloadButton.addEventListener('click', async () => { const selectedFormat = dialog.querySelector('.format-button.selected').getAttribute('data-format'); let quality; if (selectedFormat === 'video') { const selectedQuality = dialog.querySelector('input[name="quality"]:checked'); if (!selectedQuality) { alert('Please select a video quality'); return; } quality = selectedQuality.value; } else { const selectedBitrate = dialog.querySelector('input[name="bitrate"]:checked'); if (!selectedBitrate) { alert('Please select an audio bitrate'); return; } quality = selectedBitrate.value; } GM_setValue('lastSelectedFormat', selectedFormat); closeDialog(dialog, backdrop); try { await downloadWithMP3YouTube(videoUrl, selectedFormat, quality); } catch (error) { console.error('Download error:', error); updateOverlay('Download Failed', `Error: ${error.message}`, null, null, true); setTimeout(removeOverlay, 2500); } }); } function insertDownloadButton() { const targetSelector = '#owner'; const target = document.querySelector(targetSelector); if (target && !document.querySelector('.ytddl-download-btn')) { const downloadButton = createDownloadButton(); target.appendChild(downloadButton); } } const observer = new MutationObserver(() => { if (window.location.pathname.includes('/watch')) { insertDownloadButton(); } }); observer.observe(document.body, { childList: true, subtree: true }); insertDownloadButton(); window.addEventListener('yt-navigate-finish', () => { insertDownloadButton(); }); })();