Greasy Fork is available in English.

YouTube Direct Downloader

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

2025-08-04 기준 버전입니다. 최신 버전을 확인하세요.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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