Simple YouTube Downloader

Download YouTube videos easily

// ==UserScript==
// @name         Simple YouTube Downloader
// @namespace    http://tampermonkey.net/
// @version      2023-12-28
// @description  Download YouTube videos easily
// @author       ethry
// @match        https://www.youtube.com/watch?v=*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant        none
// @license      Apache-2.0
// ==/UserScript==

(function() {
    function waitForElm(selector) {
        return new Promise(resolve => {
            if (document.querySelector(selector)) {
                return resolve(document.querySelector(selector));
            }

            const observer = new MutationObserver(mutations => {
                if (document.querySelector(selector)) {
                    observer.disconnect();
                    resolve(document.querySelector(selector));
                }
            });

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

    waitForElm('ytd-offline-promo-renderer').then((elm) => {
        elm.innerHTML = `
  <style>#noytpremiumupgradedllink{text-decoration:none}.loading-spinner{border:4px solid rgba(255,255,255,1);border-radius:50%;border-top:4px solid rgba(255,255,255,0);width:24px;height:24px;animation:1s linear infinite spin}@keyframes spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}</style>

  <div class="style-scope ytd-offline-promo-renderer">
    <h1 style="filter: invert(); margin-left: 20px;">YouTube Downloader</h1>
  </div>

  <div class="style-scope ytd-offline-promo-renderer">
    <p style="filter: invert(); margin-left: 20px;">This video will be downloaded as an MP4 file:</p>
    <p id="dlVideoTitle" style="background: rgba(0,0,0,0.1); padding: 3px; max-width: calc(100% - 40px); max-height: 19px; overflow: hidden; filter: invert(); margin-left: 20px;">0</p>
    <p style="filter: invert(); margin-left: 20px; opacity: 0.65;">If you don't want this video to be downloaded, wait about 1 minute and try again.</p>
  </div>

  <div class="style-scope ytd-offline-promo-renderer">
    <p style="background-color: #19d1d1; filter: invert(); display: none; margin-left: 20px; margin-right: 20px; padding: 5px; border-radius: 5px;" id="downloaderr">Error while downloading: <span id="downloaderrreason"></span></p>
  </div>

  <div class="buttons style-scope ytd-offline-promo-renderer">
    <a href="#" id="noytpremiumupgradedllink" target="_blank">
      <button class="yt-spec-button-shape-next yt-spec-button-shape-next--filled yt-spec-button-shape-next--call-to-action yt-spec-button-shape-next--size-m" aria-label="Log Out" title="" style="">
        <div class="yt-spec-button-shape-next__button-text-content">
          <span class="yt-core-attributed-string yt-core-attributed-string--white-space-no-wrap" role="text" id="ytpremiumdlbtn"><div class="loading-spinner"></div></span>
        </div>
      </button>
    </a>
  </div>`;

        sendHttpReq((document.getElementById("above-the-fold").querySelector("#title").innerText == null ? window.location.toString() : document.getElementById("above-the-fold").querySelector("#title").innerText));

    });

    function sendHttpReq(videoTitle, oldVideoTitle = "") {
        if (videoTitle == oldVideoTitle) {
            setTimeout(function() {
                sendHttpReq(document.getElementById("above-the-fold").querySelector("#title").innerText, videoTitle)
            }, 10000);
            console.log("request cancelled due to same video");

            return;
        }

        document.getElementById("dlVideoTitle").innerText = (videoTitle == null) ? window.location.toString() : videoTitle;

        const apiUrl = 'https://co.wuk.sh/api/json';
        const requestData = {
            url: window.location.href
        };

        const xhr = new XMLHttpRequest();
        xhr.open('POST', apiUrl, true);
        xhr.setRequestHeader('Content-Type', 'application/json');
        xhr.setRequestHeader('Accept', 'application/json');

        console.log("sending request");

        xhr.onreadystatechange = function() {
            if (xhr.readyState === 4) {
                if (xhr.status >= 200 && xhr.status < 300) {
                    const responseData = JSON.parse(xhr.responseText);
                    // debug: alert('Response data: ' + JSON.stringify(responseData.url));

                    document.getElementById("ytpremiumdlbtn").innerText = "Download";
                    document.getElementById("noytpremiumupgradedllink").href = responseData.url;

                    document.getElementById("downloaderr").style.display = "none";

                    console.log("request success");

                } else {
                    console.error('HTTP error! Status: ' + xhr.status);

                    document.getElementById("downloaderr").style.display = "block";
                    document.getElementById("downloaderrreason").innerText = xhr.status;
                }
            }
        };

        xhr.onerror = function() {
            console.error('Network error');

            document.getElementById("downloaderrreason").innerText = xhr.status + " | " + "Network error";
        };

        xhr.send(JSON.stringify(requestData));


        setTimeout(function() {
            sendHttpReq(document.getElementById("above-the-fold").querySelector("#title").innerText, videoTitle)
        }, 5000);
    }
})();