Adds a download button
// ==UserScript==
// @name Youtube download button
// @namespace Violentmonkey Scripts
// @match https://www.youtube.com/watch
// @include https://*.youtube.com/*
// @grant GM_addStyle
// @run-at document-start
// @version 3.0
// @author Nojyto
// @license MIT
// @description Adds a download button
// ==/UserScript==
(function() {
const TEXT_BUTTON = "Download";
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
const ICON_SVG_PATH_DATA = "M17 18v1H6v-1h11zm-.5-6.6-.7-.7-3.8 3.7V4h-1v10.4l-3.8-3.8-.7.7 5 5 5-4.9z";
const DOWNLOAD_API = "https://www.y2mate.com/download-youtube/";
const BUTTON_ID = "ytDownloadButton";
const TARGET_CONTAINER_SELECTOR = "div#owner";
let lastUrl = null;
let currentVideoId = null;
let buttonObserver = null;
GM_addStyle(`
#${BUTTON_ID} {
background-color: var(--yt-spec-additive-background);
color: var(--yt-spec-text-primary);
margin: 0px 4px;
border-radius: 18px;
width: 120px;
height: 36px;
line-height: 37px;
text-align: center;
font-style: normal;
font-size: 14px;
font-family: Roboto, Noto, sans-serif;
font-weight: 500;
text-decoration: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background-color 0.2s;
white-space: nowrap;
min-width: fit-content;
padding: 0 10px;
}
#${BUTTON_ID}:hover {
background-color: var(--yt-spec-mono-tonal-hover);
color: var(--yt-spec-text-primary);
}
#${BUTTON_ID} .yt-download-icon-container {
height: 84%;
margin-left: -8px;
fill: currentcolor;
display: flex;
align-items: center;
justify-content: center;
}
#${BUTTON_ID} > span {
margin-left: 4px;
}
#owner {
display: flex;
align-items: center;
flex-wrap: wrap;
}
`);
function waitForElement(selector) {
// console.log(`[Youtube Download] Waiting for element: ${selector}`);
return new Promise((resolve) => {
const element = document.querySelector(selector);
if (element) {
// console.log(`[Youtube Download] Element found immediately:`, element);
return resolve(element);
}
const observer = new MutationObserver((mutationsList, obs) => {
const foundElement = document.querySelector(selector);
if (foundElement) {
obs.disconnect();
// console.log(`[Youtube Download] Element found via MutationObserver:`, foundElement);
resolve(foundElement);
}
});
observer.observe(document.documentElement, { childList: true, subtree: true });
});
}
function getVideoId(url) {
try {
const urlObj = new URL(url);
const urlParams = new URLSearchParams(urlObj.search);
return urlParams.get('v');
} catch (e) {
console.error("[Youtube Download] Error parsing URL to get video ID:", e);
return null;
}
}
async function addOrUpdateDownloadButton() {
// console.log("[Youtube Download] addOrUpdateDownloadButton called.");
const currentUrl = window.location.href;
if (!currentUrl.includes("watch?v=")) {
// console.log("[Youtube Download] Not a YouTube watch page. Skipping.");
return;
}
const targetContainer = await waitForElement(TARGET_CONTAINER_SELECTOR);
if (!targetContainer) {
console.warn("[Youtube Download] Target container element not found after waiting. Cannot add download button.");
return;
}
// console.log(`[Youtube Download] Found target container:`, targetContainer);
let downloadLink = document.getElementById(BUTTON_ID);
let videoIdForCurrentUrl = getVideoId(currentUrl);
if (!videoIdForCurrentUrl) {
console.warn("[Youtube Download] Could not get video ID for current URL:", currentUrl);
return;
}
if (!downloadLink || downloadLink.parentNode !== targetContainer) {
// console.log("[Youtube Download] Button missing or detached. Creating/re-creating download button.");
if (downloadLink && downloadLink.parentNode) {
downloadLink.parentNode.removeChild(downloadLink);
// console.log("[Youtube Download] Removed detached old button instance.");
}
downloadLink = document.createElement('a');
downloadLink.id = BUTTON_ID;
downloadLink.target = "_blank";
downloadLink.rel = "noopener noreferrer";
const iconWrapper = document.createElement('div');
iconWrapper.className = 'yt-download-icon-container';
const svgElement = document.createElementNS(SVG_NAMESPACE, "svg");
svgElement.setAttribute("height", "24");
svgElement.setAttribute("viewBox", "0 0 24 24");
svgElement.setAttribute("width", "24");
svgElement.setAttribute("focusable", "false");
svgElement.style.pointerEvents = "none";
svgElement.style.display = "inherit";
svgElement.style.width = "100%";
svgElement.style.height = "100%";
const pathElement = document.createElementNS(SVG_NAMESPACE, "path");
pathElement.setAttribute("d", ICON_SVG_PATH_DATA);
svgElement.appendChild(pathElement);
iconWrapper.appendChild(svgElement);
downloadLink.appendChild(iconWrapper);
const textSpan = document.createElement('span');
textSpan.textContent = TEXT_BUTTON;
downloadLink.appendChild(textSpan);
targetContainer.appendChild(downloadLink);
// console.log("[Youtube Download] Download button created and inserted into container.");
setupButtonObserver(targetContainer);
} else {
// console.log("[Youtube Download] Button exists and is in place.");
if (!buttonObserver || buttonObserver.observedElement !== targetContainer) {
setupButtonObserver(targetContainer);
}
}
if (currentUrl !== lastUrl || !lastUrl || !downloadLink.href.includes(videoIdForCurrentUrl)) {
lastUrl = currentUrl;
currentVideoId = videoIdForCurrentUrl;
downloadLink.href = DOWNLOAD_API + currentVideoId;
// console.log(`[Youtube Download] Button href updated to: ${downloadLink.href}`);
} else {
// console.log("[Youtube Download] URL unchanged, button already updated. No action needed for href.");
}
}
function setupButtonObserver(parentEl) {
if (buttonObserver) {
buttonObserver.disconnect();
// console.log("[Youtube Download] Existing button observer disconnected.");
}
buttonObserver = new MutationObserver((mutationsList) => {
let downloadLink = document.getElementById(BUTTON_ID);
if (!downloadLink) {
// console.log("[Youtube Download] Download button detected as removed. Attempting to re-add.");
if (buttonObserver) {
buttonObserver.disconnect();
buttonObserver = null;
}
setTimeout(() => addOrUpdateDownloadButton(), 500);
}
});
buttonObserver.observedElement = parentEl;
buttonObserver.observe(parentEl, { childList: true, subtree: false });
// console.log("[Youtube Download] Button observer set up on:", parentEl);
}
if (document.readyState === 'loading') {
// console.log("[Youtube Download] DOM still loading, adding DOMContentLoaded listener.");
document.addEventListener('DOMContentLoaded', addOrUpdateDownloadButton);
} else {
// console.log("[Youtube Download] DOM already loaded, running addOrUpdateDownloadButton immediately.");
addOrUpdateDownloadButton();
}
window.addEventListener("yt-navigate-finish", addOrUpdateDownloadButton, true);
// console.log("[Youtube Download] Script initialized. Listening for navigation events.");
})();