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