YouTube Direct Downloader

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

当前为 2025-08-06 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴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.7
// @author       afkarxyz
// @namespace    https://github.com/afkarxyz/userscripts/
// @supportURL   https://github.com/afkarxyz/userscripts/issues
// @license      MIT
// @match        https://www.youtube.com/*
// @match        https://youtube.com/*
// @grant        GM.xmlHttpRequest
// @grant        GM_download
// @grant        GM.download
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      api.mp3youtube.cc
// @connect      iframe.y2meta-uk.com
// @connect      *
// @run-at       document-end
// ==/UserScript==

(function () {
  "use strict";

  let lastSelectedFormat = GM_getValue("lastSelectedFormat", "video");
  let lastSelectedVideoQuality = GM_getValue(
    "lastSelectedVideoQuality",
    "1080"
  );

  let lastSelectedAudioBitrate = GM_getValue("lastSelectedAudioBitrate", "320");

  const API_KEY_URL = "https://api.mp3youtube.cc/v2/sanity/key";
  const API_CONVERT_URL = "https://api.mp3youtube.cc/v2/converter";

  const REQUEST_HEADERS = {
    "Content-Type": "application/json",
    Origin: "https://iframe.y2meta-uk.com",
    Accept: "*/*",
    "User-Agent":
      "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
  };
  
  const style = document.createElement("style");
  style.textContent = `
          .ytddl-download-btn {
              width: 36px;
              height: 36px;
              border-radius: 50%;
              display: flex;
              align-items: center;
              justify-content: center;
              cursor: pointer;
              margin-left: 8px;
              transition: background-color 0.2s;
          }

          html[dark] .ytddl-download-btn {
              background-color: #ffffff1a;
          }

          html:not([dark]) .ytddl-download-btn {
              background-color: #0000000d;
          }

          html[dark] .ytddl-download-btn:hover {
              background-color: #ffffff33;
          }

          html:not([dark]) .ytddl-download-btn:hover {
              background-color: #00000014;
          }

          .ytddl-download-btn svg {
              width: 18px;
              height: 18px;
          }

          html[dark] .ytddl-download-btn svg {
              fill: var(--yt-spec-text-primary, #fff);
          }

          html:not([dark]) .ytddl-download-btn svg {
              fill: var(--yt-spec-text-primary, #030303);
          }

          .ytddl-shorts-download-btn {
              display: flex;
              align-items: center;
              justify-content: center;
              margin-top: 16px;
              margin-bottom: 16px;
              width: 48px;
              height: 48px;
              border-radius: 50%;
              cursor: pointer;
              transition: background-color 0.3s;
          }

          html[dark] .ytddl-shorts-download-btn {
              background-color: rgba(255, 255, 255, 0.1);
          }

          html:not([dark]) .ytddl-shorts-download-btn {
              background-color: rgba(0, 0, 0, 0.05);
          }

          html[dark] .ytddl-shorts-download-btn:hover {
              background-color: rgba(255, 255, 255, 0.2);
          }

          html:not([dark]) .ytddl-shorts-download-btn:hover {
              background-color: rgba(0, 0, 0, 0.1);
          }

          .ytddl-shorts-download-btn svg {
              width: 24px;
              height: 24px;
          }

          html[dark] .ytddl-shorts-download-btn svg {
              fill: white;
          }

          html:not([dark]) .ytddl-shorts-download-btn svg {
              fill: black;
          }

          .ytddl-dialog {
              position: fixed;
              top: 50%;
              left: 50%;
              transform: translate(-50%, -50%);
              background: #000000;
              color: #e1e1e1;
              border-radius: 12px;
              box-shadow: 0 0 0 1px rgba(225,225,225,.1), 0 2px 4px 1px rgba(225,225,225,.18);
              font-family: 'IBM Plex Mono', 'Noto Sans Mono Variable', 'Noto Sans Mono', monospace;
              width: 400px;
              z-index: 9999;
              padding: 16px;
          }

          .ytddl-backdrop {
              position: fixed;
              top: 0;
              left: 0;
              width: 100%;
              height: 100%;
              background: rgba(0, 0, 0, 0.5);
              z-index: 9998;
          }

          .ytddl-dialog h3 {
              margin: 0 0 16px 0;
              font-size: 18px;
              font-weight: 700;
          }

          .quality-options {
              display: grid;
              grid-template-columns: repeat(3, 1fr);
              gap: 8px;
              margin-bottom: 16px;
          }

          .quality-option {
              display: flex;
              align-items: center;
              padding: 8px;
              cursor: pointer;
              border-radius: 6px;
          }

          .quality-option:hover {
              background: #191919;
          }

          .quality-option input[type="radio"] {
              margin-right: 8px;
          }

          .quality-separator {
              grid-column: 1 / -1;
              height: 1px;
              background: #333;
              margin: 8px 0;
              position: relative;
          }

          .quality-separator::after {
              content: 'VP9 (Higher Quality)';
              position: absolute;
              top: -10px;
              left: 50%;
              transform: translateX(-50%);
              background: #000;
              padding: 0 8px;
              font-size: 11px;
              color: #888;
          }

          .download-status {
              text-align: center;
              margin: 16px 0;
              font-size: 12px;
              display: none;
              color: #1ed760;
          }

          .button-container {
              display: flex;
              justify-content: center;
              gap: 8px;
              margin-top: 16px;
          }

          .ytddl-button {
              background: transparent;
              border: 1px solid #e1e1e1;
              color: #e1e1e1;
              font-size: 14px;
              font-weight: 500;
              padding: 8px 16px;
              border-radius: 18px;
              cursor: pointer;
              font-family: inherit;
              transition: all 0.2s;
          }

          .ytddl-button:hover {
              background: #1ed760;
              border-color: #1ed760;
              color: #000000;
          }

          .ytddl-button.cancel:hover {
              background: #f3727f;
              border-color: #f3727f;
              color: #000000;
          }

          .format-selector {
              margin-bottom: 16px;
              display: flex;
              gap: 8px;
              justify-content: center;
          }

          .format-button {
              background: transparent;
              border: 1px solid #e1e1e1;
              color: #e1e1e1;
              padding: 6px 12px;
              border-radius: 14px;
              cursor: pointer;
              font-family: inherit;
              font-size: 12px;
              transition: all 0.2s ease;
          }

          .format-button:hover {
              background: #808080;
              color: #000000;
          }

          .format-button.selected {
              background: #1ed760;
              border-color: #1ed760;
              color: #000000;
          }

          .ytddl-download-manager {
              position: fixed;
              top: 20px;
              right: 20px;
              background: rgba(0, 0, 0, 0.95);
              color: #e1e1e1;
              border-radius: 12px;
              padding: 0;
              width: 380px;
              max-width: 380px;
              max-height: 80vh;
              z-index: 10000;              
              font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
              font-size: 14px;
              box-shadow: 0 0 0 1px rgba(225,225,225,.1), 0 2px 4px 1px rgba(225,225,225,.18), 0 8px 32px rgba(0, 0, 0, 0.4);
              border: 1px solid rgba(255, 255, 255, 0.1);
              backdrop-filter: blur(20px);
              opacity: 0;
              transform: translateX(100%);
              transition: all 0.3s ease;
              overflow: hidden;
          }

          .ytddl-download-manager.show {
              opacity: 1;
              transform: translateX(0);
          }

          .ytddl-manager-header {
              padding: 16px;
              border-bottom: 1px solid rgba(255, 255, 255, 0.1);
              display: flex;
              justify-content: space-between;
              align-items: center;
              background: rgba(255, 255, 255, 0.02);
          }

          .ytddl-manager-title-section {
              display: flex;
              align-items: center;
              gap: 8px;
          }

          .ytddl-manager-title {
              font-weight: 600;
              font-size: 16px;
              color: #fff;
              margin: 0;
          }

          .ytddl-manager-counter {
              background: #1ed760;
              color: #000;
              padding: 4px 8px;
              border-radius: 12px;
              font-size: 12px;
              font-weight: 600;
              min-width: 20px;
              text-align: center;
          }

          .ytddl-manager-close {
              background: none;
              border: none;
              color: #ccc;
              cursor: pointer;
              padding: 4px;
              border-radius: 4px;
              transition: all 0.2s;
              font-size: 18px;
          }

          .ytddl-manager-close:hover {
              color: #f3727f;
          }

          .ytddl-downloads-container {
              max-height: calc(80vh - 70px);
              overflow-y: auto;
              padding: 8px 0;
          }

          .ytddl-download-item {
              padding: 16px;
              border-bottom: 1px solid rgba(255, 255, 255, 0.05);
              transition: background-color 0.2s;
          }

          .ytddl-download-item:hover {
              background: rgba(255, 255, 255, 0.02);
          }

          .ytddl-download-item:last-child {
              border-bottom: none;
          }

          .ytddl-download-filename {
              font-weight: 500;
              margin-bottom: 8px;
              color: #fff;
              font-size: 13px;
              line-height: 1.3;
              word-break: break-word;
          }

          .ytddl-download-info {
              font-size: 11px;
              color: #ccc;
              font-family: 'Consolas', 'Monaco', 'Lucida Console', monospace;
              margin-top: 4px;
          }

          .ytddl-download-info .download-size {
              color: #1ed760;
              font-weight: 500;
          }

          .ytddl-download-info .download-speed {
              color: #ccc;
              font-weight: 500;
          }
      `;
  document.head.appendChild(style);
  let downloadManager = null;
  let activeDownloads = new Map();
  let downloadCounter = 0;
  function safeSetTextContent(element, text) {
    try {
      element.textContent = text;
    } catch (error) {
      console.warn("Failed to set textContent, trying alternative:", error);
      try {
        element.innerText = text;
      } catch (altError) {
        console.error("Failed to set text content:", altError);
        try {
          while (element.firstChild) {
            element.removeChild(element.firstChild);
          }
          element.appendChild(document.createTextNode(text));
        } catch (finalError) {
          console.error("All text setting methods failed:", finalError);
        }
      }
    }
  }

  function createDownloadManager() {
    if (downloadManager) return downloadManager;

    const manager = document.createElement("div");
    manager.className = "ytddl-download-manager";
    const header = document.createElement("div");
    header.className = "ytddl-manager-header";

    const titleSection = document.createElement("div");
    titleSection.className = "ytddl-manager-title-section";

    const title = document.createElement("h3");
    title.className = "ytddl-manager-title";
    safeSetTextContent(title, "Downloads");

    const counter = document.createElement("div");
    counter.className = "ytddl-manager-counter";
    safeSetTextContent(counter, "0");

    titleSection.appendChild(title);
    titleSection.appendChild(counter);

    const closeBtn = document.createElement("button");
    closeBtn.className = "ytddl-manager-close";
    safeSetTextContent(closeBtn, "×");
    closeBtn.addEventListener("click", hideDownloadManager);

    header.appendChild(titleSection);
    header.appendChild(closeBtn);

    const container = document.createElement("div");
    container.className = "ytddl-downloads-container";
    manager.appendChild(header);
    manager.appendChild(container);

    try {
      document.body.appendChild(manager);
    } catch (appendError) {
      console.error("Failed to append manager to body:", appendError);
      setTimeout(() => {
        try {
          document.body.appendChild(manager);
        } catch (retryError) {
          console.error("Retry failed:", retryError);
        }
      }, 100);
    }

    downloadManager = manager;
    return manager;
  }

  function showDownloadManager() {
    try {
      if (!downloadManager) {
        createDownloadManager();
      }
      if (downloadManager) {
        downloadManager.classList.add("show");
        updateDownloadCounter();
      }
    } catch (error) {
      console.error("Error showing download manager:", error);
    }
  }

  function hideDownloadManager() {
    if (downloadManager) {
      downloadManager.classList.remove("show");
    }
  }

  function updateDownloadCounter() {
    if (!downloadManager) return;
    const counter = downloadManager.querySelector(".ytddl-manager-counter");
    const activeCount = Array.from(activeDownloads.values()).filter(
      download => !["completed", "error"].includes(download.status)
    ).length;
      if (counter) {
      safeSetTextContent(counter, activeCount.toString());
    }

    if (activeCount === 0 && activeDownloads.size > 0) {
      setTimeout(() => {
        const stillNoActive = Array.from(activeDownloads.values()).filter(
          download => !["completed", "error"].includes(download.status)
        ).length === 0;

        if (stillNoActive) {
          setTimeout(hideDownloadManager, 3000);
        }
      }, 2000);
    }
  }

  function createDownloadItem(downloadId, filename, format) {
    try {
      const item = document.createElement("div");
      item.className = "ytddl-download-item";
      item.id = `download-${downloadId}`;
      const filenameDiv = document.createElement("div");
      filenameDiv.className = "ytddl-download-filename";
      safeSetTextContent(filenameDiv, truncateTitle(filename || `${format}.${format === "video" ? "mp4" : "mp3"}`, 45));

      const infoDiv = document.createElement("div");
      infoDiv.className = "ytddl-download-info";
      safeSetTextContent(infoDiv, "⏳ ... | ⬇️ ... | ⚡ ...");

      item.appendChild(filenameDiv);
      item.appendChild(infoDiv);

      return item;
    } catch (error) {
      console.error("Error creating download item:", error);
      return null;
    }
  }

  function addDownloadToManager(downloadId, filename, format) {
    try {
      if (!downloadManager) {
        createDownloadManager();
      }

      if (!downloadManager) {
        console.error("Failed to create download manager");
        return null;
      }

      const container = downloadManager.querySelector(".ytddl-downloads-container");
      if (!container) {
        console.error("Download container not found");
        return null;
      }

      const downloadItem = createDownloadItem(downloadId, filename, format);

      container.insertBefore(downloadItem, container.firstChild);

      showDownloadManager();
      updateDownloadCounter();

      return downloadItem;
    } catch (error) {
      console.error("Error adding download to manager:", error);
      return null;
    }
  }

  function updateDownloadItem(downloadId, status, details, fileSize = null, speed = null) {
    const item = document.getElementById(`download-${downloadId}`);
    if (!item) return;

    const infoEl = item.querySelector(".ytddl-download-info");

    if (infoEl) {
      const download = activeDownloads.get(downloadId);
      let elapsed = null;
      if (status.toLowerCase() === "downloading" && download && download.downloadStartTime) {
        elapsed = (Date.now() - download.downloadStartTime) / 1000;
      }

      const compactInfo = createCompactInfo(fileSize, elapsed, speed);
      safeSetTextContent(infoEl, compactInfo);
    }

    if (activeDownloads.has(downloadId)) {
      activeDownloads.get(downloadId).status = status.toLowerCase().replace(/\s+/g, '-');
    }
    if (status.toLowerCase() === "completed") {
      setTimeout(() => {
        removeDownloadItem(downloadId);
      }, 3000);
    }

    if (status.toLowerCase() === "error") {
      setTimeout(() => {
        removeDownloadItem(downloadId);
      }, 3000);
    }

    updateDownloadCounter();
  }

  function removeDownloadItem(downloadId) {
    try {
      const item = document.getElementById(`download-${downloadId}`);
      if (item && item.parentNode) {
        item.parentNode.removeChild(item);
      }

      activeDownloads.delete(downloadId);
      updateDownloadCounter();
    } catch (error) {
      console.error("Error removing download item:", error);
    }
  }

  function formatDuration(seconds) {
    if (seconds < 60) return `${Math.floor(seconds)}s`;
    const minutes = Math.floor(seconds / 60);
    if (minutes < 60) return `${minutes}m ${Math.floor(seconds % 60)}s`;
    const hours = Math.floor(minutes / 60);
    return `${hours}h ${Math.floor(minutes % 60)}m`;
  }

  function createCompactInfo(size, elapsed, speed) {
    const timeText = elapsed !== null ? formatDuration(elapsed) : "...";
    const sizeText = size || "...";
    const speedText = speed || "...";
    return `⏳ ${timeText} | ⬇️ ${sizeText} | ⚡ ${speedText}`;
  }

  function formatBytes(bytes) {
    if (bytes === 0) return "0 Bytes";
    const k = 1024;
    const sizes = ["Bytes", "KB", "MB", "GB"];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
  }

  function truncateTitle(title, maxLength = 50) {
    if (!title || title.length <= maxLength) return title;
    return title.substring(0, maxLength - 3) + "...";
  }

  function cleanFilename(filename) {
    if (!filename) return "YouTube_Video";

    return filename
      .replace(/[<>:"/\\|?*]/g, "")
      .replace(/[\u0000-\u001f\u007f-\u009f]/g, "")
      .replace(/^\.+/, "")
      .replace(/\.+$/, "")
      .replace(/\s+/g, " ")
      .trim()
      || "YouTube_Video";
  }

  function triggerDirectDownload(url, filename, downloadId) {
    const download = activeDownloads.get(downloadId);
    if (download) {
      download.downloadStartTime = Date.now();
    }

    updateDownloadItem(downloadId, "downloading", "Connecting to server...", "0 B", "0 B/s");

    fetchAndDownload(url, filename, downloadId);
  }

  function fetchAndDownload(url, filename, downloadId) {
    console.log("URL:", url);
    console.log("Filename:", filename);
    console.log("Download ID:", downloadId);

    const download = activeDownloads.get(downloadId);
    const downloadStartTime = download ? download.downloadStartTime : Date.now();
    console.log("Start time:", new Date(downloadStartTime).toISOString());

    let totalSize = 0;
    let downloadedSize = 0;
    let lastUpdateTime = 0;
    const UPDATE_INTERVAL = 250;

    GM.xmlHttpRequest({
      method: "GET",
      url: url,
      responseType: "blob",
      headers: {
        "User-Agent":
          "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
        Referer: "https://iframe.y2meta-uk.com/",
        Accept: "*/*",
      },
      onprogress: function (progressEvent) {
        const currentTime = Date.now();
        const elapsed = (currentTime - downloadStartTime) / 1000;

        const shouldUpdate =
          currentTime - lastUpdateTime >= UPDATE_INTERVAL ||
          (progressEvent.lengthComputable &&
            progressEvent.loaded === progressEvent.total);

        if (progressEvent.lengthComputable) {
          totalSize = progressEvent.total;
          downloadedSize = progressEvent.loaded;

          const percentage = Math.round((downloadedSize / totalSize) * 100);
          const speed = elapsed > 0 ? downloadedSize / elapsed : 0;

          if (shouldUpdate) {
            const sizeText = `${formatBytes(downloadedSize)} / ${formatBytes(
              totalSize
            )}`;
            const speedText = `${formatBytes(speed)}/s`;
            const percentText = `${percentage}%`;
            updateDownloadItem(
              downloadId,
              "downloading",
              `Downloading ${percentText}`,
              sizeText,
              speedText
            );

            lastUpdateTime = currentTime;
          }

          if (currentTime - lastUpdateTime >= 1000 || percentage === 100) {
            console.log(
              `[${elapsed.toFixed(
                1
              )}s] Progress: ${percentage}% | Downloaded: ${formatBytes(
                downloadedSize
              )}/${formatBytes(totalSize)} | Speed: ${formatBytes(speed)}/s`
            );
          }
        } else {
          downloadedSize = progressEvent.loaded || 0;
          const speed = elapsed > 0 ? downloadedSize / elapsed : 0;

          if (shouldUpdate) {
            const sizeText = `${formatBytes(downloadedSize)}`;
            const speedText = `${formatBytes(speed)}/s`;
            const timeText = `${elapsed.toFixed(1)}s`;
            updateDownloadItem(
              downloadId,
              "downloading",
              `Downloading... (${timeText})`,
              sizeText,
              speedText
            );

            lastUpdateTime = currentTime;
          }

          if (currentTime - lastUpdateTime >= 1000) {
            console.log(
              `[${elapsed.toFixed(1)}s] Downloaded: ${formatBytes(
                downloadedSize
              )} | Speed: ${formatBytes(speed)}/s`
            );
          }
        }
      },
      onload: function (response) {
        console.log("Download completed. Response status:", response.status);
        console.log("Response type:", typeof response.response);
        console.log("Response size:", response.response?.size || "unknown");

        if (response.status === 200 && response.response) {
          updateDownloadItem(
            downloadId,
            "processing",
            "Creating download file...",
            formatBytes(response.response.size || 0),
            "Processing"
          );

          try {
            const blob = response.response;
            const blobUrl = URL.createObjectURL(blob);

            console.log("Blob created:", blob.size, "bytes");
            console.log("Blob URL:", blobUrl);

            const a = document.createElement("a");
            a.style.display = "none";
            a.href = blobUrl;
            a.download = filename || "video.mp4";

            document.body.appendChild(a);
            a.click();

            setTimeout(() => {
              document.body.removeChild(a);
              URL.revokeObjectURL(blobUrl);
            }, 1000);
            updateDownloadItem(
              downloadId,
              "completed",
              "Download completed successfully!",
              formatBytes(blob.size),
              "Complete"
            );
            console.log(
              "✅ Download successful"
            );
          } catch (blobError) {
            console.error("Blob download failed:", blobError);
            updateDownloadItem(
              downloadId,
              "error",
              `Blob conversion error: ${blobError.message}`,
              null,
              null
            );
          }
        } else {
          console.error("Download failed with status:", response.status);
          updateDownloadItem(
            downloadId,
            "error",
            `Server returned status ${response.status}`,
            null,
            null
          );
        }
      },
      onerror: function (error) {
        console.error("GM.xmlHttpRequest download failed:", error);
        updateDownloadItem(
          downloadId,
          "error",
          "Network error or invalid URL",
          null,
          null
        );
      },
      ontimeout: function () {
        console.error("GM.xmlHttpRequest download timeout");
        updateDownloadItem(
          downloadId,
          "error",
          "Request took too long to complete",
          null,
          null
        );
      },
    });
  }

  function createDownloadDialog() {
    const dialog = document.createElement("div");
    dialog.className = "ytddl-dialog";
    const title = document.createElement("h3");
    safeSetTextContent(title, "");

    const formatSelector = document.createElement("div");
    formatSelector.className = "format-selector";
    const videoBtn = document.createElement("button");
    videoBtn.className = `format-button ${
      lastSelectedFormat === "video" ? "selected" : ""
    }`;
    videoBtn.setAttribute("data-format", "video");
    safeSetTextContent(videoBtn, "VIDEO (.mp4/.webm)");

    const audioBtn = document.createElement("button");
    audioBtn.className = `format-button ${
      lastSelectedFormat === "audio" ? "selected" : ""
    }`;
    audioBtn.setAttribute("data-format", "audio");
    safeSetTextContent(audioBtn, "AUDIO (.mp3)");

    formatSelector.appendChild(videoBtn);
    formatSelector.appendChild(audioBtn);

    const qualityContainer = document.createElement("div");
    qualityContainer.id = "quality-container";
    const videoQualities = document.createElement("div");
    videoQualities.className = "quality-options";
    videoQualities.id = "video-qualities";
    videoQualities.style.display =
      lastSelectedFormat === "video" ? "grid" : "none";
    const qualityOptions = [
      { quality: "144p", codec: "h264", ext: ".mp4" },
      { quality: "240p", codec: "h264", ext: ".mp4" },
      { quality: "360p", codec: "h264", ext: ".mp4" },
      { quality: "480p", codec: "h264", ext: ".mp4" },
      { quality: "720p", codec: "h264", ext: ".mp4" },
      { quality: "1080p", codec: "h264", ext: ".mp4" },
      { quality: "1440p", codec: "vp9", ext: ".webm" },
      { quality: "2160p", codec: "vp9", ext: ".webm" },
    ];

    qualityOptions.forEach((item, index) => {
      if (index === 6) {
        const separator = document.createElement("div");
        separator.className = "quality-separator";
        videoQualities.appendChild(separator);
      }

      const option = document.createElement("div");
      option.className = "quality-option";

      const input = document.createElement("input");
      input.type = "radio";
      input.id = `quality-${index}`;
      input.name = "quality";
      input.value = item.quality.replace("p", "");
      input.setAttribute("data-codec", item.codec);
      input.setAttribute("data-ext", item.ext);
      const label = document.createElement("label");
      label.setAttribute("for", `quality-${index}`);
      safeSetTextContent(label, `${item.quality} ${item.ext}`);
      label.style.fontSize = "14px";
      label.style.cursor = "pointer";

      option.appendChild(input);
      option.appendChild(label);
      videoQualities.appendChild(option);

      option.addEventListener("click", function () {
        input.checked = true;
        GM_setValue("lastSelectedVideoQuality", input.value);
        lastSelectedVideoQuality = input.value;
      });
    });

    const defaultQuality = videoQualities.querySelector(
      `input[value="${lastSelectedVideoQuality}"]`
    );
    if (defaultQuality) {
      defaultQuality.checked = true;
    }
    const audioQualities = document.createElement("div");
    audioQualities.className = "quality-options";
    audioQualities.id = "audio-qualities";
    audioQualities.style.display =
      lastSelectedFormat === "audio" ? "grid" : "none";
    ["128", "256", "320"].forEach((bitrate, index) => {
      const option = document.createElement("div");
      option.className = "quality-option";

      const input = document.createElement("input");
      input.type = "radio";
      input.id = `bitrate-${index}`;
      input.name = "bitrate";
      input.value = bitrate;
      const label = document.createElement("label");
      label.setAttribute("for", `bitrate-${index}`);
      safeSetTextContent(label, `${bitrate} kbps`);
      label.style.fontSize = "14px";
      label.style.cursor = "pointer";

      option.appendChild(input);
      option.appendChild(label);
      audioQualities.appendChild(option);

      option.addEventListener("click", function () {
        input.checked = true;
        GM_setValue("lastSelectedAudioBitrate", input.value);
        lastSelectedAudioBitrate = input.value;
      });
    });

    const defaultBitrate = audioQualities.querySelector(
      `input[value="${lastSelectedAudioBitrate}"]`
    );
    if (defaultBitrate) {
      defaultBitrate.checked = true;
    }

    qualityContainer.appendChild(videoQualities);
    qualityContainer.appendChild(audioQualities);

    const downloadStatus = document.createElement("div");
    downloadStatus.className = "download-status";
    downloadStatus.id = "download-status";

    const buttonContainer = document.createElement("div");
    buttonContainer.className = "button-container";
    const cancelButton = document.createElement("button");
    cancelButton.className = "ytddl-button cancel";
    safeSetTextContent(cancelButton, "Cancel");

    const downloadButton = document.createElement("button");
    downloadButton.className = "ytddl-button";
    safeSetTextContent(downloadButton, "Download");

    buttonContainer.appendChild(cancelButton);
    buttonContainer.appendChild(downloadButton);

    dialog.appendChild(title);
    dialog.appendChild(formatSelector);
    dialog.appendChild(qualityContainer);
    dialog.appendChild(downloadStatus);
    dialog.appendChild(buttonContainer);

    formatSelector.addEventListener("click", (e) => {
      if (e.target.classList.contains("format-button")) {
        formatSelector.querySelectorAll(".format-button").forEach((btn) => {
          btn.classList.remove("selected");
        });
        e.target.classList.add("selected");
        const format = e.target.getAttribute("data-format");
        if (format === "video") {
          videoQualities.style.display = "grid";
          audioQualities.style.display = "none";
          lastSelectedFormat = "video";
          GM_setValue("lastSelectedFormat", "video");
        } else {
          videoQualities.style.display = "none";
          audioQualities.style.display = "grid";
          lastSelectedFormat = "audio";
          GM_setValue("lastSelectedFormat", "audio");
        }
      }
    });

    const backdrop = document.createElement("div");
    backdrop.className = "ytddl-backdrop";

    return { dialog, backdrop, cancelButton, downloadButton };
  }

  function closeDialog(dialog, backdrop) {
    if (dialog && dialog.parentNode) {
      dialog.parentNode.removeChild(dialog);
    }
    if (backdrop && backdrop.parentNode) {
      backdrop.parentNode.removeChild(backdrop);
    }
  }

  function extractVideoId(url) {
    const urlObj = new URL(url);

    const searchParams = new URLSearchParams(urlObj.search);
    const videoId = searchParams.get("v");
    if (videoId) {
      return videoId;
    }

    const shortsMatch = url.match(/\/shorts\/([^?]+)/);
    if (shortsMatch) {
      return shortsMatch[1];
    }

    return null;
  }

  async function downloadWithMP3YouTube(
    videoUrl,
    format,
    quality,
    codec = "h264"
  ) {
    const downloadId = `download_${++downloadCounter}_${Date.now()}`;

    let videoTitle = document.title;
    videoTitle = videoTitle.replace(/^\(\d+\)\s*/, "");
    videoTitle = videoTitle.replace(" - YouTube", "");
    if (!videoTitle || videoTitle.trim() === "") {
      const titleElement = document.querySelector("h1.ytd-watch-metadata #title, h1 yt-formatted-string, #title h1");
      if (titleElement) {
        videoTitle = titleElement.textContent.trim();
      }
    }
    if (!videoTitle || videoTitle.trim() === "") {
      videoTitle = "YouTube_Video";
    }
    const cleanedTitle = cleanFilename(videoTitle);

    const downloadInfo = {
      id: downloadId,
      filename: `${cleanedTitle}.${format === "video" ? "mp4" : "mp3"}`,
      format: format,
      status: "initializing",
      url: videoUrl,
      startTime: Date.now()
    };

    activeDownloads.set(downloadId, downloadInfo);
    addDownloadToManager(downloadId, downloadInfo.filename, format);

    updateDownloadItem(downloadId, "initializing", "Getting API key...", null, null);

    try {
      const keyResponse = await new Promise((resolve, reject) => {
        GM.xmlHttpRequest({
          method: "GET",
          url: API_KEY_URL,
          headers: REQUEST_HEADERS,
          onload: resolve,
          onerror: reject,
          ontimeout: reject,
        });
      });

      const keyData = JSON.parse(keyResponse.responseText);
      if (!keyData || !keyData.key) {
        throw new Error("Failed to get API key");
      }

      const key = keyData.key;      
      updateDownloadItem(
        downloadId,
        "processing",
        `Processing ${format} (${format === "video" ? quality + "p" : quality + " kbps"})`,
        null,
        null
      );

      let payload;
      if (format === "video") {
        payload = {
          link: videoUrl,
          format: "mp4",
          audioBitrate: "128",
          videoQuality: quality,
          filenameStyle: "pretty",
          vCodec: codec,
        };
      } else {
        payload = {
          link: videoUrl,
          format: "mp3",
          audioBitrate: quality,
          filenameStyle: "pretty",
        };
      }

      const customHeaders = {
        ...REQUEST_HEADERS,
        key: key,
      };

      updateDownloadItem(downloadId, "processing", "Converting media...", null, null);

      const downloadResponse = await new Promise((resolve, reject) => {
        GM.xmlHttpRequest({
          method: "POST",
          url: API_CONVERT_URL,
          headers: customHeaders,
          data: JSON.stringify(payload),
          onload: resolve,
          onerror: reject,
          ontimeout: reject,
        });
      });

      const apiDownloadInfo = JSON.parse(downloadResponse.responseText);
      if (apiDownloadInfo.url) {
        if (apiDownloadInfo.filename) {
          activeDownloads.get(downloadId).filename = cleanFilename(apiDownloadInfo.filename);
          const item = document.getElementById(`download-${downloadId}`);
          if (item) {
            const filenameEl = item.querySelector(".ytddl-download-filename");
            if (filenameEl) {
              safeSetTextContent(filenameEl, truncateTitle(apiDownloadInfo.filename, 45));
            }
          }
        }

        updateDownloadItem(
          downloadId,
          "downloading",
          "Starting download...",
          null,
          null
        );

        triggerDirectDownload(apiDownloadInfo.url, apiDownloadInfo.filename, downloadId);

        return apiDownloadInfo;
      } else {
        throw new Error("No download URL received from API");
      }
    } catch (error) {
      updateDownloadItem(
        downloadId,
        "error",
        `Error: ${error.message}`,
        null,
        null
      );

      throw error;
    }
  }

  function createDownloadButton() {
    const downloadButton = document.createElement("div");
    downloadButton.className = "ytddl-download-btn";

    const svgNS = "http://www.w3.org/2000/svg";
    const svg = document.createElementNS(svgNS, "svg");
    svg.setAttribute("viewBox", "0 0 512 512");

    const path = document.createElementNS(svgNS, "path");
    path.setAttribute(
      "d",
      "M256 464c114.9 0 208-93.1 208-208c0-13.3 10.7-24 24-24s24 10.7 24 24c0 141.4-114.6 256-256 256S0 397.4 0 256c0-13.3 10.7-24 24-24s24 10.7 24 24c0 114.9 93.1 208 208 208zM377.6 232.3l-104 112c-4.5 4.9-10.9 7.7-17.6 7.7s-13-2.8-17.6-7.7l-104-112c-9-9.7-8.5-24.9 1.3-33.9s24.9-8.5 33.9 1.3L232 266.9 232 24c0-13.3 10.7-24 24-24s24 10.7 24 24l0 242.9 62.4-67.2c9-9.7 24.2-10.3 33.9-1.3s10.3 24.2 1.3 33.9z"
    );

    svg.appendChild(path);
    downloadButton.appendChild(svg);

    downloadButton.addEventListener("click", function () {
      showDownloadDialog();
    });

    return downloadButton;
  }

  function createShortsDownloadButton() {
    const downloadButton = document.createElement("div");
    downloadButton.className = "ytddl-shorts-download-btn";

    const svgNS = "http://www.w3.org/2000/svg";
    const svg = document.createElementNS(svgNS, "svg");
    svg.setAttribute("viewBox", "0 0 512 512");

    const path = document.createElementNS(svgNS, "path");
    path.setAttribute(
      "d",
      "M256 464c114.9 0 208-93.1 208-208c0-13.3 10.7-24 24-24s24 10.7 24 24c0 141.4-114.6 256-256 256S0 397.4 0 256c0-13.3 10.7-24 24-24s24 10.7 24 24c0 114.9 93.1 208 208 208zM377.6 232.3l-104 112c-4.5 4.9-10.9 7.7-17.6 7.7s-13-2.8-17.6-7.7l-104-112c-9-9.7-8.5-24.9 1.3-33.9s24.9-8.5 33.9 1.3L232 266.9 232 24c0-13.3 10.7-24 24-24s24 10.7 24 24l0 242.9 62.4-67.2c9-9.7 24.2-10.3 33.9-1.3s10.3 24.2 1.3 33.9z"
    );

    svg.appendChild(path);
    downloadButton.appendChild(svg);

    downloadButton.addEventListener("click", function () {
      showDownloadDialog();
    });

    return downloadButton;
  }

  function showDownloadDialog() {
    const videoUrl = window.location.href;
    const videoId = extractVideoId(videoUrl);

    if (!videoId) {
      alert("Could not extract video ID from URL");
      return;
    }

    const { dialog, backdrop, cancelButton, downloadButton } =
      createDownloadDialog();

    document.body.appendChild(backdrop);
    document.body.appendChild(dialog);

    backdrop.addEventListener("click", () => {
      closeDialog(dialog, backdrop);
    });

    cancelButton.addEventListener("click", () => {
      closeDialog(dialog, backdrop);
    });
    downloadButton.addEventListener("click", async () => {
      const selectedFormat = dialog
        .querySelector(".format-button.selected")
        .getAttribute("data-format");
      let quality, codec;

      if (selectedFormat === "video") {
        const selectedQuality = dialog.querySelector(
          'input[name="quality"]:checked'
        );
        if (!selectedQuality) {
          alert("Please select a video quality");
          return;
        }
        quality = selectedQuality.value;
        codec = selectedQuality.getAttribute("data-codec");
      } else {
        const selectedBitrate = dialog.querySelector(
          'input[name="bitrate"]:checked'
        );
        if (!selectedBitrate) {
          alert("Please select an audio bitrate");
          return;
        }
        quality = selectedBitrate.value;
      }

      GM_setValue("lastSelectedFormat", selectedFormat);

      closeDialog(dialog, backdrop);      
      try {
        await downloadWithMP3YouTube(videoUrl, selectedFormat, quality, codec);
      } catch (error) {
        console.error("Download error:", error);
      }
    });
  }

  function insertDownloadButton() {
    const targetSelector = "#owner";
    const target = document.querySelector(targetSelector);

    if (target && !document.querySelector(".ytddl-download-btn")) {
      const downloadButton = createDownloadButton();
      target.appendChild(downloadButton);
    }
  }

  function insertShortsDownloadButton() {
    const selectors = [
      "ytd-reel-video-renderer[is-active] #like-button",
      "ytd-shorts #like-button",
      "#shorts-player #like-button",
      "ytd-reel-video-renderer #like-button",
    ];

    for (const selector of selectors) {
      const likeButtonContainer = document.querySelector(selector);

      if (
        likeButtonContainer &&
        !document.querySelector(".ytddl-shorts-download-btn")
      ) {
        const downloadButton = createShortsDownloadButton();
        likeButtonContainer.parentNode.insertBefore(
          downloadButton,
          likeButtonContainer
        );
        return true;
      }
    }
    return false;
  }

  function checkAndInsertButton() {
    const isShorts = window.location.pathname.includes("/shorts/");
    if (isShorts) {
      if (!insertShortsDownloadButton()) {
        let retryCount = 0;
        const maxRetries = 10;

        const shortsObserver = new MutationObserver((_mutations, observer) => {
          if (insertShortsDownloadButton()) {
            observer.disconnect();
          } else {
            retryCount++;
            if (retryCount >= maxRetries) {
              observer.disconnect();
            }
          }
        });

        const shortsContainer =
          document.querySelector("ytd-shorts") || document.body;
        shortsObserver.observe(shortsContainer, {
          childList: true,
          subtree: true,
        });

        setTimeout(() => {
          insertShortsDownloadButton();
        }, 1000);
      }
    } else if (window.location.pathname.includes("/watch")) {
      insertDownloadButton();
    }
  }

  const observer = new MutationObserver(() => {
    checkAndInsertButton();
  });

  observer.observe(document.body, { childList: true, subtree: true });
  checkAndInsertButton();

  let previousUrl = location.href;

  function checkUrlChange() {
    const currentUrl = location.href;
    if (currentUrl !== previousUrl) {
      previousUrl = currentUrl;
      setTimeout(() => {
        checkAndInsertButton();
      }, 500);
    }
  }

  history.pushState = (function (f) {
    return function () {
      const result = f.apply(this, arguments);
      checkUrlChange();
      return result;
    };
  })(history.pushState);

  history.replaceState = (function (f) {
    return function () {
      const result = f.apply(this, arguments);
      checkUrlChange();
      return result;
    };
  })(history.replaceState);

  window.addEventListener("popstate", checkUrlChange);

  window.addEventListener("yt-navigate-finish", () => {
    checkAndInsertButton();
  });

  document.addEventListener("yt-action", function (event) {
    if (
      event.detail &&
      event.detail.actionName === "yt-reload-continuation-items-command"
    ) {
      checkAndInsertButton();
    }
  });

  window.addEventListener("yt-navigate-finish", () => {
    insertDownloadButton();
  });
})();