Add a custom download button and provide options to download the video or audio directly from the YouTube page.
As of
// ==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.6
// @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-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-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(
"Starting download",
"Connecting to server...",
"0 B",
"0 B/s"
);
fetchAndDownload(url, filename, downloadStartTime);
}
function fetchAndDownload(url, filename, downloadStartTime) {
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("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) {
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 GM.xmlHttpRequest blob method"
);
setTimeout(() => {
removeOverlay();
}, 2500);
} catch (blobError) {
console.error("Blob download failed:", blobError);
updateOverlay(
"Download failed",
`Blob conversion error: ${blobError.message}`,
null,
null,
true
);
setTimeout(() => {
removeOverlay();
}, 2500);
}
} else {
console.error("Download failed with status:", response.status);
updateOverlay(
"Download failed",
`Server returned status ${response.status}`,
null,
null,
true
);
setTimeout(() => {
removeOverlay();
}, 2500);
}
},
onerror: function (error) {
console.error("GM.xmlHttpRequest download failed:", error);
updateOverlay(
"Download failed",
"Network error or invalid URL",
null,
null,
true
);
setTimeout(() => {
removeOverlay();
}, 2500);
},
ontimeout: function () {
console.error("GM.xmlHttpRequest download timeout");
updateOverlay(
"Download timeout",
"Request took too long to complete",
null,
null,
true
);
setTimeout(() => {
removeOverlay();
}, 2500);
},
});
}
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/.webm)";
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";
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}`);
label.textContent = `${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}`);
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);
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 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: codec,
};
} 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 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);
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);
}
}
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();
});
})();