YouTube Cobalt Tools Download Button

Adds a download button to YouTube videos using Cobalt API for downloading videos or audio.

目前为 2024-10-14 提交的版本。查看 最新版本

// ==UserScript==
// @name         YouTube Cobalt Tools Download Button
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Adds a download button to YouTube videos using Cobalt API for downloading videos or audio.
// @author       yodaluca23
// @license      GNU GPLv3
// @match        *://*.youtube.com/*
// @grant        GM.xmlHttpRequest
// @grant        GM_notification
// ==/UserScript==

(function() {
    'use strict';

    let currentPageUrl = window.location.href;
    let initialInjectDelay = 2000; // Initial delay in milliseconds
    let navigationInjectDelay = 1000; // Delay on navigation in milliseconds

    // Check if currentPageUrl is YouTube video
    function isYouTubeWatchURL() {
    return window.location.href.includes("youtube.com/watch?");
    }

  function removeElement(elementToRemove) {
    var element = document.querySelector(elementToRemove);
    if (element) {
        element.remove();
    }

  }

   async function findInstance() {
        try {
            const response = await fetch('https://corsproxy.io/?https%3A%2F%2Finstances.hyper.lol%2Finstances.json'); // Kept getting CORS errors, so using a CORS proxy, the proxied url is "https://instances.hyper.lol/instances.json"
            const instances = await response.json();

            // Loop through the instances to find the required one
            for (const instance of instances) {
                // Check if 'youtube' is true and 'version' is less than 8
                if (instance.services.youtube && instance.protocol == 'https' && parseFloat(instance.version) < 8) {
                    return (instance.protocol + "://" + instance.api);
                }
            }
            return null;
        } catch (error) {
            console.error('Error fetching instances:', error);
            return null;
        }
    }

    // Function to initiate download using Cobalt API
    async function Cobalt(videoUrl, audioOnly = false, quality = '1080', format = 'mp4') {
      let codec = 'h264';
      if (format === 'mp4' && parseInt(quality.replace('p', '')) > 1100) {
          codec = 'av1';
      } else if (format === 'webm') {
          codec = 'vp9';
      }

      console.log(`Sending request to Cobalt API: URL=${videoUrl}, AudioOnly=${audioOnly}, Quality=${quality}, Format=${format}, Codec=${codec}`);

      const options = {
          method: 'POST',
          headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
          body: JSON.stringify({
              url: videoUrl,
              vCodec: codec,
              vQuality: quality.replace('p', ''),
              filenamePattern: "pretty",
              isAudioOnly: audioOnly
          })
      };

      const apiUrl = await findInstance();

      if (!apiUrl) {
          console.log('No matching instance found.');
          return null; // Early exit if no instance found
      }

      console.log('Found API URL:', apiUrl);

      try {
          const response = await fetch(`${apiUrl}/api/json`, options);
          const data = await response.json();
          console.log(data);
          if (data.text) {
            console.error('Video Length is ', (unsafeWindow.ytplayer.config.args.raw_player_response.videoDetails.lengthSeconds/60), ' minutes.\nError fetching from Cobalt API:', data.text);
            GM_notification(data.text.charAt(0).toUpperCase() + data.text.slice(1));
          }
          return data.url;
      } catch (error) {
          console.error('Error fetching from Cobalt API:', error);
          return null;

      }

    }


    // Function to fetch video qualities
    function fetchVideoQualities() {
      const videoQualities = extractQualities();
      console.log('Video Qualities:', videoQualities);
      return videoQualities
    }

    function extractQualities() {
      // Check if ytInitialPlayerResponse is available
      if (unsafeWindow.ytInitialPlayerResponse && unsafeWindow.ytInitialPlayerResponse.streamingData) {
          var qualityLabels = unsafeWindow.ytInitialPlayerResponse.streamingData.adaptiveFormats
              .filter(format => format.qualityLabel)
              .map(format => format.qualityLabel);

          // Use regex to extract the first number followed by the first non-numeric character
          var extractedQualities = qualityLabels.map(label => {
              const match = label.match(/^(\d+)/);
              return match ? match[0] : null;
          }).filter(Boolean); // Filter out any null values

          // Remove duplicates using Set
          var uniqueQualityLabels = [...new Set(extractedQualities)];
          return uniqueQualityLabels;
      } else {
          console.error('ytInitialPlayerResponse or streamingData not found.');
          return [];
      }
    }


    function extractFormats() {
      // Check if ytInitialPlayerResponse is available
      if (unsafeWindow.ytInitialPlayerResponse && unsafeWindow.ytInitialPlayerResponse.streamingData) {
          var formats = unsafeWindow.ytInitialPlayerResponse.streamingData.adaptiveFormats
              .filter(format => format.mimeType)
              .map(format => format.mimeType);

          // Regex to extract format
          const formatRegex = /\/([^;]+)/;
          var extractedFormats = formats
              .map(mimeType => {
                  const match = mimeType.match(formatRegex);
                  return match ? match[1] : null;
              })
              .filter(format => format); // Filter out null values

          // Add "mp3" if it's not already included
          if (!extractedFormats.includes("mp3")) {
              extractedFormats.push("mp3");
          }

          // Move "mp4" to the top if present
          if (extractedFormats.includes("mp4")) {
              extractedFormats = extractedFormats.filter(format => format !== "mp4");
              extractedFormats.unshift("mp4");
          }

          // Remove duplicates using Set
          var uniqueFormats = [...new Set(extractedFormats)];
          return uniqueFormats;
      } else {
          console.error('ytInitialPlayerResponse or streamingData not found.');
          return [];
      }
    }


    // Helper function to check if two arrays are equal (for detecting changes)
    function arraysEqual(arr1, arr2) {
        if (arr1.length !== arr2.length) return false;
        for (let i = 0; i < arr1.length; i++) {
            if (arr1[i] !== arr2[i]) return false;
        }
        return true;
    }

    // Function to inject download button on the page
    function injectDownloadButton() {
        setTimeout(() => {
            // Remove existing download button if present
            removeElement('#cobalt-download-btn');

            const downloadButton = document.createElement('button');
            downloadButton.id = 'cobalt-download-btn';
            downloadButton.className = 'yt-spec-button-shape-next yt-spec-button-shape-next--tonal yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-leading';
            downloadButton.setAttribute('aria-label', 'Download');
            downloadButton.setAttribute('title', 'Download');
            downloadButton.innerHTML = `
                <div class="yt-spec-button-shape-next__icon">
                    <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" focusable="false" style="pointer-events: none; display: inline-block; width: 24px; height: 24px; vertical-align: middle;">
                        <path fill="currentColor" d="M17 18v1H6v-1h11zm-.5-6.6-.7-.7-3.8 3.7V4h-1v10.4l-3.8-3.8-.7.7 5 5 5-4.9z"></path>
                    </svg>
                </div>
                <div class="yt-spec-button-shape-next__button-text-content">Download</div>
            `;
            downloadButton.style.backgroundColor = 'rgb(44, 44, 44)';
            downloadButton.style.border = '0px solid rgb(204, 204, 204)';
            downloadButton.style.borderRadius = '30px';
            downloadButton.style.fontSize = '14px';
            downloadButton.style.padding = '8px 16px';
            downloadButton.style.cursor = 'pointer';
            downloadButton.style.marginLeft = '8px';
            downloadButton.style.marginRight = '0px';

            downloadButton.onclick = () => showQualityPopup(currentPageUrl);

            const actionMenu = document.querySelector('.top-level-buttons');
            actionMenu.appendChild(downloadButton);
        }, initialInjectDelay);
    }

    // Function to remove native YouTube download button
    function removeNativeDownloadButton() {
        setTimeout(() => {
            // Remove download button from overflow menu
            removeElement('ytd-menu-service-item-download-renderer');

            // Remove download button next to like/dislike buttons
            removeElement('ytd-download-button-renderer');

        }, initialInjectDelay);
    }

    // Function to display quality selection popup
    function showQualityPopup(videoUrl) {
      if (!window.location.href.includes(ytplayer.config.args.raw_player_response.videoDetails.videoId)) {
        GM_notification('Player Variables out of sync, refreshing, please try again.')
        setTimeout(() => {
          location.reload();

        }, 2000);
      } else {
        var qualities = fetchVideoQualities();
        var formatOptions = extractFormats();

        const qualityPrompt = `
            <div id="cobalt-quality-picker" style="background: #fff; padding: 20px; border: 1px solid #ccc; border-radius: 10px; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 9999; max-width: 75px; width: 100%; max-height: 400px; overflow-y: auto;">
                <label for="cobalt-format" style="display: block; margin-bottom: 10px;">Format:</label>
                <select id="cobalt-format" style="margin-bottom: 10px; width: 100%;">
                    ${formatOptions.map(format => `<option value="${format}">${format}</option>`).join('')}
                </select>
                <label id="quality-label" for="cobalt-quality" style="display: block; margin-bottom: 10px;">Quality:</label>
                <select id="cobalt-quality" style="margin-bottom: 10px; width: 100%;">
                    ${qualities.map(q => `<option value="${q}">${q}p</option>`).join('')}
                </select>
                <div id="cobalt-loading" style="display: none; margin-bottom: 10px; text-align: center;">Loading...</div>
                <button id="cobalt-start-download" style="display: block; margin-top: 10px;">Download</button>
            </div>
        `;

        const cobaltToolsPopupContainer = document.createElement('div');
        cobaltToolsPopupContainer.innerHTML = qualityPrompt;
        document.body.appendChild(cobaltToolsPopupContainer);

        // if clicked outside of popup then close the popup
        const clickHandler = (event) => {
          if (!cobaltToolsPopupContainer.contains(event.target)) {
              removeElement('#cobalt-quality-picker');
              document.removeEventListener('click', clickHandler);
          }
        };
        setTimeout(() => {
            document.addEventListener('click', clickHandler);
        }, 300);



        const qualityDropdown = document.getElementById('cobalt-quality');
        const loadingIndicator = document.getElementById('cobalt-loading');
        const formatDropdown = document.getElementById('cobalt-format');
        const startDownloadBtn = document.getElementById('cobalt-start-download');

        formatDropdown.addEventListener('change', () => {
            const isAudioFormat = formatDropdown.value === 'mp3' || formatDropdown.value === 'opus' || formatDropdown.value === 'wav';
            const qualityLabel = document.getElementById('quality-label');
            if (isAudioFormat) {
                qualityLabel.style.display = 'none';
                qualityDropdown.style.display = 'none';
            } else {
                qualityLabel.style.display = 'block';
                qualityDropdown.style.display = 'block';
            }
        });

        startDownloadBtn.addEventListener('click', async () => {
          // Remove the close popup click event listener
          document.removeEventListener('click', clickHandler);
          loadingIndicator.style.display = 'block';

          // Disable changes after initiating download
          startDownloadBtn.disabled = true;
          formatDropdown.disabled = true;
          qualityDropdown.disabled = true;

          const format = formatDropdown.value;
          const quality = qualityDropdown.value;

          let videoUrl = await Cobalt(currentPageUrl, format === 'mp3' || format === 'opus' || format === 'wav', quality, format);

          if (!videoUrl) {
              console.error('Failed to fetch download URL.');
              loadingIndicator.style.display = 'none';
              startDownloadBtn.disabled = false;
              return;
          }

          console.log(`Downloading ${format} ${quality}`);

          // Create and trigger download link
          let link = document.createElement('a');
          link.href = videoUrl;
          link.setAttribute('download', '');
          document.body.appendChild(link);
          link.click();

          // Clean up
          document.body.removeChild(link);
          loadingIndicator.style.display = 'none';
          startDownloadBtn.disabled = false;
          removeElement('#cobalt-quality-picker');
        });
      }
    }

    // Function to initialize download button on YouTube video page
    function initializeDownloadButton() {
        injectDownloadButton();
        removeNativeDownloadButton();
    }

    // Initialize on page load
    if (isYouTubeWatchURL()) {
      setTimeout(() => {
          initializeDownloadButton();
      }, initialInjectDelay);
    }

    // Monitor URL changes using history API
    window.onpopstate = function(event) {
        setTimeout(() => {
            if (currentPageUrl !== window.location.href) {
                currentPageUrl = window.location.href;
                console.log('URL changed:', currentPageUrl);
                if (isYouTubeWatchURL()) {
                  initializeDownloadButton(); // Reinitialize download button on URL change
                }

                // Close the format/quality picker menu if a new video is clicked
                removeElement('#cobalt-quality-picker');
            }
        }, navigationInjectDelay);
    };

    // Monitor DOM changes using MutationObserver
    const observer = new MutationObserver(mutations => {
        for (let mutation of mutations) {
            if (mutation.type === 'childList' && mutation.target.classList.contains('html5-video-player')) {
                console.log('Video player changed');
                setTimeout(() => {
                    currentPageUrl = window.location.href;
                    if (isYouTubeWatchURL()) {
                      initializeDownloadButton(); // Reinitialize download button if video player changes
                    }
                }, navigationInjectDelay);

                // Close the format/quality picker menu if a new video is clicked
                removeElement('#cobalt-quality-picker');
                break;
            }
        }
    });

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

})();

QingJ © 2025

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