YouTube Direct Downloader

Add a custom download button and provide options to download the video or audio directly from the YouTube page.

Ekde 2025/08/04. Vidu La ĝisdata versio.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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