// ==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.7
// @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/139.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-shorts-download-btn {
display: flex;
align-items: center;
justify-content: center;
margin-top: 16px;
margin-bottom: 16px;
width: 48px;
height: 48px;
border-radius: 50%;
cursor: pointer;
transition: background-color 0.3s;
}
html[dark] .ytddl-shorts-download-btn {
background-color: rgba(255, 255, 255, 0.1);
}
html:not([dark]) .ytddl-shorts-download-btn {
background-color: rgba(0, 0, 0, 0.05);
}
html[dark] .ytddl-shorts-download-btn:hover {
background-color: rgba(255, 255, 255, 0.2);
}
html:not([dark]) .ytddl-shorts-download-btn:hover {
background-color: rgba(0, 0, 0, 0.1);
}
.ytddl-shorts-download-btn svg {
width: 24px;
height: 24px;
}
html[dark] .ytddl-shorts-download-btn svg {
fill: white;
}
html:not([dark]) .ytddl-shorts-download-btn svg {
fill: black;
}
.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;
}
.quality-separator {
grid-column: 1 / -1;
height: 1px;
background: #333;
margin: 8px 0;
position: relative;
}
.quality-separator::after {
content: 'VP9 (Higher Quality)';
position: absolute;
top: -10px;
left: 50%;
transform: translateX(-50%);
background: #000;
padding: 0 8px;
font-size: 11px;
color: #888;
}
.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-download-manager {
position: fixed;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.95);
color: #e1e1e1;
border-radius: 12px;
padding: 0;
width: 380px;
max-width: 380px;
max-height: 80vh;
z-index: 10000;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
box-shadow: 0 0 0 1px rgba(225,225,225,.1), 0 2px 4px 1px rgba(225,225,225,.18), 0 8px 32px rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(20px);
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease;
overflow: hidden;
}
.ytddl-download-manager.show {
opacity: 1;
transform: translateX(0);
}
.ytddl-manager-header {
padding: 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(255, 255, 255, 0.02);
}
.ytddl-manager-title-section {
display: flex;
align-items: center;
gap: 8px;
}
.ytddl-manager-title {
font-weight: 600;
font-size: 16px;
color: #fff;
margin: 0;
}
.ytddl-manager-counter {
background: #1ed760;
color: #000;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
min-width: 20px;
text-align: center;
}
.ytddl-manager-close {
background: none;
border: none;
color: #ccc;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: all 0.2s;
font-size: 18px;
}
.ytddl-manager-close:hover {
color: #f3727f;
}
.ytddl-downloads-container {
max-height: calc(80vh - 70px);
overflow-y: auto;
padding: 8px 0;
}
.ytddl-download-item {
padding: 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
transition: background-color 0.2s;
}
.ytddl-download-item:hover {
background: rgba(255, 255, 255, 0.02);
}
.ytddl-download-item:last-child {
border-bottom: none;
}
.ytddl-download-filename {
font-weight: 500;
margin-bottom: 8px;
color: #fff;
font-size: 13px;
line-height: 1.3;
word-break: break-word;
}
.ytddl-download-info {
font-size: 11px;
color: #ccc;
font-family: 'Consolas', 'Monaco', 'Lucida Console', monospace;
margin-top: 4px;
}
.ytddl-download-info .download-size {
color: #1ed760;
font-weight: 500;
}
.ytddl-download-info .download-speed {
color: #ccc;
font-weight: 500;
}
`;
document.head.appendChild(style);
let downloadManager = null;
let activeDownloads = new Map();
let downloadCounter = 0;
function safeSetTextContent(element, text) {
try {
element.textContent = text;
} catch (error) {
console.warn("Failed to set textContent, trying alternative:", error);
try {
element.innerText = text;
} catch (altError) {
console.error("Failed to set text content:", altError);
try {
while (element.firstChild) {
element.removeChild(element.firstChild);
}
element.appendChild(document.createTextNode(text));
} catch (finalError) {
console.error("All text setting methods failed:", finalError);
}
}
}
}
function createDownloadManager() {
if (downloadManager) return downloadManager;
const manager = document.createElement("div");
manager.className = "ytddl-download-manager";
const header = document.createElement("div");
header.className = "ytddl-manager-header";
const titleSection = document.createElement("div");
titleSection.className = "ytddl-manager-title-section";
const title = document.createElement("h3");
title.className = "ytddl-manager-title";
safeSetTextContent(title, "Downloads");
const counter = document.createElement("div");
counter.className = "ytddl-manager-counter";
safeSetTextContent(counter, "0");
titleSection.appendChild(title);
titleSection.appendChild(counter);
const closeBtn = document.createElement("button");
closeBtn.className = "ytddl-manager-close";
safeSetTextContent(closeBtn, "×");
closeBtn.addEventListener("click", hideDownloadManager);
header.appendChild(titleSection);
header.appendChild(closeBtn);
const container = document.createElement("div");
container.className = "ytddl-downloads-container";
manager.appendChild(header);
manager.appendChild(container);
try {
document.body.appendChild(manager);
} catch (appendError) {
console.error("Failed to append manager to body:", appendError);
setTimeout(() => {
try {
document.body.appendChild(manager);
} catch (retryError) {
console.error("Retry failed:", retryError);
}
}, 100);
}
downloadManager = manager;
return manager;
}
function showDownloadManager() {
try {
if (!downloadManager) {
createDownloadManager();
}
if (downloadManager) {
downloadManager.classList.add("show");
updateDownloadCounter();
}
} catch (error) {
console.error("Error showing download manager:", error);
}
}
function hideDownloadManager() {
if (downloadManager) {
downloadManager.classList.remove("show");
}
}
function updateDownloadCounter() {
if (!downloadManager) return;
const counter = downloadManager.querySelector(".ytddl-manager-counter");
const activeCount = Array.from(activeDownloads.values()).filter(
download => !["completed", "error"].includes(download.status)
).length;
if (counter) {
safeSetTextContent(counter, activeCount.toString());
}
if (activeCount === 0 && activeDownloads.size > 0) {
setTimeout(() => {
const stillNoActive = Array.from(activeDownloads.values()).filter(
download => !["completed", "error"].includes(download.status)
).length === 0;
if (stillNoActive) {
setTimeout(hideDownloadManager, 3000);
}
}, 2000);
}
}
function createDownloadItem(downloadId, filename, format) {
try {
const item = document.createElement("div");
item.className = "ytddl-download-item";
item.id = `download-${downloadId}`;
const filenameDiv = document.createElement("div");
filenameDiv.className = "ytddl-download-filename";
safeSetTextContent(filenameDiv, truncateTitle(filename || `${format}.${format === "video" ? "mp4" : "mp3"}`, 45));
const infoDiv = document.createElement("div");
infoDiv.className = "ytddl-download-info";
safeSetTextContent(infoDiv, "⏳ ... | ⬇️ ... | ⚡ ...");
item.appendChild(filenameDiv);
item.appendChild(infoDiv);
return item;
} catch (error) {
console.error("Error creating download item:", error);
return null;
}
}
function addDownloadToManager(downloadId, filename, format) {
try {
if (!downloadManager) {
createDownloadManager();
}
if (!downloadManager) {
console.error("Failed to create download manager");
return null;
}
const container = downloadManager.querySelector(".ytddl-downloads-container");
if (!container) {
console.error("Download container not found");
return null;
}
const downloadItem = createDownloadItem(downloadId, filename, format);
container.insertBefore(downloadItem, container.firstChild);
showDownloadManager();
updateDownloadCounter();
return downloadItem;
} catch (error) {
console.error("Error adding download to manager:", error);
return null;
}
}
function updateDownloadItem(downloadId, status, details, fileSize = null, speed = null) {
const item = document.getElementById(`download-${downloadId}`);
if (!item) return;
const infoEl = item.querySelector(".ytddl-download-info");
if (infoEl) {
const download = activeDownloads.get(downloadId);
let elapsed = null;
if (status.toLowerCase() === "downloading" && download && download.downloadStartTime) {
elapsed = (Date.now() - download.downloadStartTime) / 1000;
}
const compactInfo = createCompactInfo(fileSize, elapsed, speed);
safeSetTextContent(infoEl, compactInfo);
}
if (activeDownloads.has(downloadId)) {
activeDownloads.get(downloadId).status = status.toLowerCase().replace(/\s+/g, '-');
}
if (status.toLowerCase() === "completed") {
setTimeout(() => {
removeDownloadItem(downloadId);
}, 3000);
}
if (status.toLowerCase() === "error") {
setTimeout(() => {
removeDownloadItem(downloadId);
}, 3000);
}
updateDownloadCounter();
}
function removeDownloadItem(downloadId) {
try {
const item = document.getElementById(`download-${downloadId}`);
if (item && item.parentNode) {
item.parentNode.removeChild(item);
}
activeDownloads.delete(downloadId);
updateDownloadCounter();
} catch (error) {
console.error("Error removing download item:", error);
}
}
function formatDuration(seconds) {
if (seconds < 60) return `${Math.floor(seconds)}s`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ${Math.floor(seconds % 60)}s`;
const hours = Math.floor(minutes / 60);
return `${hours}h ${Math.floor(minutes % 60)}m`;
}
function createCompactInfo(size, elapsed, speed) {
const timeText = elapsed !== null ? formatDuration(elapsed) : "...";
const sizeText = size || "...";
const speedText = speed || "...";
return `⏳ ${timeText} | ⬇️ ${sizeText} | ⚡ ${speedText}`;
}
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 cleanFilename(filename) {
if (!filename) return "YouTube_Video";
return filename
.replace(/[<>:"/\\|?*]/g, "")
.replace(/[\u0000-\u001f\u007f-\u009f]/g, "")
.replace(/^\.+/, "")
.replace(/\.+$/, "")
.replace(/\s+/g, " ")
.trim()
|| "YouTube_Video";
}
function triggerDirectDownload(url, filename, downloadId) {
const download = activeDownloads.get(downloadId);
if (download) {
download.downloadStartTime = Date.now();
}
updateDownloadItem(downloadId, "downloading", "Connecting to server...", "0 B", "0 B/s");
fetchAndDownload(url, filename, downloadId);
}
function fetchAndDownload(url, filename, downloadId) {
console.log("URL:", url);
console.log("Filename:", filename);
console.log("Download ID:", downloadId);
const download = activeDownloads.get(downloadId);
const downloadStartTime = download ? download.downloadStartTime : Date.now();
console.log("Start time:", new Date(downloadStartTime).toISOString());
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/139.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}%`;
updateDownloadItem(
downloadId,
"downloading",
`Downloading ${percentText}`,
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`;
updateDownloadItem(
downloadId,
"downloading",
`Downloading... (${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("Download 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) {
updateDownloadItem(
downloadId,
"processing",
"Creating download 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);
updateDownloadItem(
downloadId,
"completed",
"Download completed successfully!",
formatBytes(blob.size),
"Complete"
);
console.log(
"✅ Download successful"
);
} catch (blobError) {
console.error("Blob download failed:", blobError);
updateDownloadItem(
downloadId,
"error",
`Blob conversion error: ${blobError.message}`,
null,
null
);
}
} else {
console.error("Download failed with status:", response.status);
updateDownloadItem(
downloadId,
"error",
`Server returned status ${response.status}`,
null,
null
);
}
},
onerror: function (error) {
console.error("GM.xmlHttpRequest download failed:", error);
updateDownloadItem(
downloadId,
"error",
"Network error or invalid URL",
null,
null
);
},
ontimeout: function () {
console.error("GM.xmlHttpRequest download timeout");
updateDownloadItem(
downloadId,
"error",
"Request took too long to complete",
null,
null
);
},
});
}
function createDownloadDialog() {
const dialog = document.createElement("div");
dialog.className = "ytddl-dialog";
const title = document.createElement("h3");
safeSetTextContent(title, "");
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");
safeSetTextContent(videoBtn, "VIDEO (.mp4/.webm)");
const audioBtn = document.createElement("button");
audioBtn.className = `format-button ${
lastSelectedFormat === "audio" ? "selected" : ""
}`;
audioBtn.setAttribute("data-format", "audio");
safeSetTextContent(audioBtn, "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";
const qualityOptions = [
{ quality: "144p", codec: "h264", ext: ".mp4" },
{ quality: "240p", codec: "h264", ext: ".mp4" },
{ quality: "360p", codec: "h264", ext: ".mp4" },
{ quality: "480p", codec: "h264", ext: ".mp4" },
{ quality: "720p", codec: "h264", ext: ".mp4" },
{ quality: "1080p", codec: "h264", ext: ".mp4" },
{ quality: "1440p", codec: "vp9", ext: ".webm" },
{ quality: "2160p", codec: "vp9", ext: ".webm" },
];
qualityOptions.forEach((item, index) => {
if (index === 6) {
const separator = document.createElement("div");
separator.className = "quality-separator";
videoQualities.appendChild(separator);
}
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 = item.quality.replace("p", "");
input.setAttribute("data-codec", item.codec);
input.setAttribute("data-ext", item.ext);
const label = document.createElement("label");
label.setAttribute("for", `quality-${index}`);
safeSetTextContent(label, `${item.quality} ${item.ext}`);
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}`);
safeSetTextContent(label, `${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";
safeSetTextContent(cancelButton, "Cancel");
const downloadButton = document.createElement("button");
downloadButton.className = "ytddl-button";
safeSetTextContent(downloadButton, "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);
const videoId = searchParams.get("v");
if (videoId) {
return videoId;
}
const shortsMatch = url.match(/\/shorts\/([^?]+)/);
if (shortsMatch) {
return shortsMatch[1];
}
return null;
}
async function downloadWithMP3YouTube(
videoUrl,
format,
quality,
codec = "h264"
) {
const downloadId = `download_${++downloadCounter}_${Date.now()}`;
let videoTitle = document.title;
videoTitle = videoTitle.replace(/^\(\d+\)\s*/, "");
videoTitle = videoTitle.replace(" - YouTube", "");
if (!videoTitle || videoTitle.trim() === "") {
const titleElement = document.querySelector("h1.ytd-watch-metadata #title, h1 yt-formatted-string, #title h1");
if (titleElement) {
videoTitle = titleElement.textContent.trim();
}
}
if (!videoTitle || videoTitle.trim() === "") {
videoTitle = "YouTube_Video";
}
const cleanedTitle = cleanFilename(videoTitle);
const downloadInfo = {
id: downloadId,
filename: `${cleanedTitle}.${format === "video" ? "mp4" : "mp3"}`,
format: format,
status: "initializing",
url: videoUrl,
startTime: Date.now()
};
activeDownloads.set(downloadId, downloadInfo);
addDownloadToManager(downloadId, downloadInfo.filename, format);
updateDownloadItem(downloadId, "initializing", "Getting API key...", null, null);
try {
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;
updateDownloadItem(
downloadId,
"processing",
`Processing ${format} (${format === "video" ? quality + "p" : quality + " kbps"})`,
null,
null
);
let payload;
if (format === "video") {
payload = {
link: videoUrl,
format: "mp4",
audioBitrate: "128",
videoQuality: quality,
filenameStyle: "pretty",
vCodec: codec,
};
} else {
payload = {
link: videoUrl,
format: "mp3",
audioBitrate: quality,
filenameStyle: "pretty",
};
}
const customHeaders = {
...REQUEST_HEADERS,
key: key,
};
updateDownloadItem(downloadId, "processing", "Converting media...", null, null);
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 apiDownloadInfo = JSON.parse(downloadResponse.responseText);
if (apiDownloadInfo.url) {
if (apiDownloadInfo.filename) {
activeDownloads.get(downloadId).filename = cleanFilename(apiDownloadInfo.filename);
const item = document.getElementById(`download-${downloadId}`);
if (item) {
const filenameEl = item.querySelector(".ytddl-download-filename");
if (filenameEl) {
safeSetTextContent(filenameEl, truncateTitle(apiDownloadInfo.filename, 45));
}
}
}
updateDownloadItem(
downloadId,
"downloading",
"Starting download...",
null,
null
);
triggerDirectDownload(apiDownloadInfo.url, apiDownloadInfo.filename, downloadId);
return apiDownloadInfo;
} else {
throw new Error("No download URL received from API");
}
} catch (error) {
updateDownloadItem(
downloadId,
"error",
`Error: ${error.message}`,
null,
null
);
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 createShortsDownloadButton() {
const downloadButton = document.createElement("div");
downloadButton.className = "ytddl-shorts-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, codec;
if (selectedFormat === "video") {
const selectedQuality = dialog.querySelector(
'input[name="quality"]:checked'
);
if (!selectedQuality) {
alert("Please select a video quality");
return;
}
quality = selectedQuality.value;
codec = selectedQuality.getAttribute("data-codec");
} 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, codec);
} catch (error) {
console.error("Download error:", error);
}
});
}
function insertDownloadButton() {
const targetSelector = "#owner";
const target = document.querySelector(targetSelector);
if (target && !document.querySelector(".ytddl-download-btn")) {
const downloadButton = createDownloadButton();
target.appendChild(downloadButton);
}
}
function insertShortsDownloadButton() {
const selectors = [
"ytd-reel-video-renderer[is-active] #like-button",
"ytd-shorts #like-button",
"#shorts-player #like-button",
"ytd-reel-video-renderer #like-button",
];
for (const selector of selectors) {
const likeButtonContainer = document.querySelector(selector);
if (
likeButtonContainer &&
!document.querySelector(".ytddl-shorts-download-btn")
) {
const downloadButton = createShortsDownloadButton();
likeButtonContainer.parentNode.insertBefore(
downloadButton,
likeButtonContainer
);
return true;
}
}
return false;
}
function checkAndInsertButton() {
const isShorts = window.location.pathname.includes("/shorts/");
if (isShorts) {
if (!insertShortsDownloadButton()) {
let retryCount = 0;
const maxRetries = 10;
const shortsObserver = new MutationObserver((_mutations, observer) => {
if (insertShortsDownloadButton()) {
observer.disconnect();
} else {
retryCount++;
if (retryCount >= maxRetries) {
observer.disconnect();
}
}
});
const shortsContainer =
document.querySelector("ytd-shorts") || document.body;
shortsObserver.observe(shortsContainer, {
childList: true,
subtree: true,
});
setTimeout(() => {
insertShortsDownloadButton();
}, 1000);
}
} else if (window.location.pathname.includes("/watch")) {
insertDownloadButton();
}
}
const observer = new MutationObserver(() => {
checkAndInsertButton();
});
observer.observe(document.body, { childList: true, subtree: true });
checkAndInsertButton();
let previousUrl = location.href;
function checkUrlChange() {
const currentUrl = location.href;
if (currentUrl !== previousUrl) {
previousUrl = currentUrl;
setTimeout(() => {
checkAndInsertButton();
}, 500);
}
}
history.pushState = (function (f) {
return function () {
const result = f.apply(this, arguments);
checkUrlChange();
return result;
};
})(history.pushState);
history.replaceState = (function (f) {
return function () {
const result = f.apply(this, arguments);
checkUrlChange();
return result;
};
})(history.replaceState);
window.addEventListener("popstate", checkUrlChange);
window.addEventListener("yt-navigate-finish", () => {
checkAndInsertButton();
});
document.addEventListener("yt-action", function (event) {
if (
event.detail &&
event.detail.actionName === "yt-reload-continuation-items-command"
) {
checkAndInsertButton();
}
});
window.addEventListener("yt-navigate-finish", () => {
insertDownloadButton();
});
})();