YouTube Direct Downloader

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

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址