YTdl

download YouTube video

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

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

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==UserScript==
// @name         YTdl
// @namespace    https://tampermonkey.net/
// @version      0.2.1
// @description  download YouTube video
// @author       Shiroikoi
// @run-at       document-idle
// @match        https://www.youtube.com/watch*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.min.js
// @require      https://unpkg.com/@ffmpeg/[email protected]/dist/ffmpeg.min.js
// @grant        none
// @compatible   firefox >=79
// @compatible   chrome >=68
// @license      MIT
// ==/UserScript==
(function () {
  const ffmpeg = FFmpeg.createFFmpeg({
    corePath: "https://unpkg.com/@ffmpeg/[email protected]/dist/ffmpeg-core.js",
    log: true,
  });
  let vidseleVm,
    audseleVm,
    infoVm,
    buttVm,
    dataArray,
    controller,
    fileName,
    decryptFun,
    progressText = { totalLength: 0, receivedLength: 0, text: "" },
    videoList = {
      options: [],
    },
    audioList = {
      options: [],
    },
    style = document.createElement("style"),
    optionVideo = document.createElement("option"),
    optionAudio = document.createElement("option"),
    row1 = document.createElement("div"),
    row2 = document.createElement("div"),
    selectVideo = document.createElement("select"),
    selectAudio = document.createElement("select"),
    button = document.createElement("button"),
    spanInfo = document.createElement("span");

  style.innerText =
    "@font-face {\
      font-family: 'Quicksand';\
      font-style: normal;\
      font-weight: 400;\
      font-display: swap;\
      src: url(https://fonts.gstatic.com/s/quicksand/v21/6xK-dSZaM9iE8KbpRA_LJ3z8mH9BOJvgkP8o58a-wg.woff2) format('woff2');\
      unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\
    }";
  document.head.append(style);
  button.setAttribute("v-on:click", "click");
  button.textContent = "{{text}}";
  button.style.width = "110px";
  button.style.height = "35px";
  button.style.fontSize = "20px";
  button.style.fontFamily = "Quicksand";
  button.style.marginLeft = "20px";
  button.style.position = "absolute";
  selectVideo.style.fontSize = "20px";
  selectVideo.style.fontFamily = "Quicksand";
  selectVideo.style.height = "35px";
  selectVideo.style.width = "280px";
  selectAudio.style.fontSize = "20px";
  selectAudio.style.fontFamily = "Quicksand";
  selectAudio.style.height = "35px";
  selectAudio.style.width = "280px";
  optionVideo.setAttribute("v-for", "i in options");
  optionVideo.textContent = "{{ i }}";
  optionAudio.setAttribute("v-for", "i in options");
  optionAudio.textContent = "{{ i }}";
  spanInfo.style.fontSize = "20px";
  spanInfo.style.fontFamily = "Quicksand";
  spanInfo.textContent = "{{text}}";

  infoVm = new Vue({
    data: progressText,
  });
  vidseleVm = new Vue({
    data: videoList,
  });
  audseleVm = new Vue({
    data: audioList,
  });
  buttVm = new Vue({
    data: {
      text: "download",
      signal: false,
    },
    methods: {
      click: function () {
        if (this.signal == false) {
          infoVm.text = " parsing...";
          controller = new AbortController();
          this.stat2();
          vidseleVm.$el.selectedIndex == videoList.options.length - 1 ? this.taskAud() : this.taskVidnAud();
        } else {
          controller.abort();
          this.stat1();
          infoVm.text = " canceled!";
        }
      },
      taskVidnAud: async function () {
        let indexA = videoList.options.length - 1 + audseleVm.$el.selectedIndex,
          indexV = vidseleVm.$el.selectedIndex,
          videoUrl,
          audioUrl;
        if (dataArray[indexV].url == undefined) {
          if (decryptFun == undefined) decryptFun = await fetchCode().then(getDecryptFun);
          videoUrl = decryptUrls(decryptFun, indexV);
          audioUrl = decryptUrls(decryptFun, indexA);
        } else {
          videoUrl = dataArray[indexV].url;
          audioUrl = dataArray[indexA].url;
        }
        console.log(videoUrl) || console.log(audioUrl);
        let mediaArray = await Promise.all([
          fetchMedia(videoUrl, controller, progressText, this.errcb),
          fetchMedia(audioUrl, controller, progressText, this.errcb),
        ]);
        if (mediaArray[0] == null) {
          return;
        } else if (mediaArray[0] == null) {
          return;
        }
        ffmpeg.isLoaded() ? null : await ffmpeg.load();
        await blobToUint8Array(mediaArray[0])
          .then((result) => {
            ffmpeg.FS("writeFile", "video", result);
          })
          .catch((error) => {
            this.errcb(error);
          });
        await blobToUint8Array(mediaArray[1])
          .then((result) => {
            ffmpeg.FS("writeFile", "audio", result);
          })
          .catch((error) => {
            this.errcb(error);
          });
        await ffmpeg.run("-i", "video", "-i", "audio", "-c", "copy", "output.mp4");
        let outPut = ffmpeg.FS("readFile", "output.mp4");
        ffmpeg.FS("unlink", "output.mp4");
        blobLink(new Blob([outPut]), fileName + ".mp4");
        infoVm.text = " merged! total:" + progressText.totalLength + "MiB";
        this.stat1();
      },
      taskAud: async function () {
        let indexA = videoList.options.length - 1 + audseleVm.$el.selectedIndex,
          audioUrl,
          suff;
        if (dataArray[indexA].url == undefined) {
          if (decryptFun == undefined) decryptFun = await fetchCode().then(getDecryptFun);
          audioUrl = decryptUrls(decryptFun, indexA);
        } else {
          audioUrl = dataArray[indexA].url;
        }
        console.log(audioUrl);
        let audio = await fetchMedia(audioUrl, controller, progressText, this.errcb);
        if (audio == null) return;
        if (dataArray[indexA].mimeType.match("mp4")) {
          suff = ".m4a";
        } else if (dataArray[indexA].mimeType.match("webm")) {
          suff = ".weba";
        }
        blobLink(audio, fileName + suff);
        infoVm.text = " merged! total:" + progressText.totalLength + "MiB";
        this.stat1();
      },
      stat1: function () {
        this.signal = false;
        this.text = "download";
        this.$el.style.backgroundColor = "";
      },
      stat2: function () {
        this.signal = true;
        this.text = "cancel";
        this.$el.style.backgroundColor = "#99ccff";
      },
      errcb: function (error) {
        console.log(error.name);
        switch (error.name) {
          case "AbortError":
            this.stat1();
            break;
          default:
            infoVm.text = " error! try refresh the page";
            this.stat1();
            break;
        }
      },
    },
  });
  mountFun();

  function mountFun() {
    if (document.querySelector("#meta") == null) {
      setTimeout(mountFun, 500);
    } else {
      document.querySelector("#meta").append(row1);
      document.querySelector("#meta").append(row2);
      row1.append(selectVideo);
      row1.append(button);
      row2.append(selectAudio);
      row2.append(spanInfo);
      selectVideo.prepend(optionVideo);
      selectAudio.prepend(optionAudio);
      buttVm.$mount(button);
      audseleVm.$mount(selectAudio);
      vidseleVm.$mount(selectVideo);
      infoVm.$mount(spanInfo);
      try {
        dataArray = ytInitialPlayerResponse.streamingData.adaptiveFormats;
        fileName = ytInitialPlayerResponse.videoDetails.title;
        dataArray.forEach((item) => {
          if (item.mimeType.match(/video/)) {
            videoList.options.push(item.qualityLabel + "-" + item.mimeType);
          } else if (item.mimeType.match(/audio/)) {
            audioList.options.push(item.audioQuality + "-" + item.mimeType);
          }
        });
        videoList.options.push("none");
      } catch (error) {
        console.log(error.name);
        infoVm.text = "script currently not available";
        buttVm.$el.disabled = true;
      }
    }
  }
  async function fetchCode(errcb) {
    let code,
      script = document.querySelectorAll("script");
    try {
      for (let i = 0; i < script.length; i++) {
        if (script[i].src.match(/base\.js/)) {
          const res = await fetch(script[i].src);
          code = await res.text();
          break;
        }
      }
      return code;
    } catch (error) {
      errcb(error);
      console.log("fetchCode failed" + error.name);
      return null;
    }
  }
  function getDecryptFun(code) {
    let funName = code.match(/(?<==)[\w]+(?=\(decodeURIC)/)[0],
      funString = code
        .match(new RegExp(`(?<=${funName}=)function\\([\\s\\S]+?}`))[0]
        .replace(/^/, "(")
        .replace(/$/, ")"),
      fun = eval(funString),
      objName = funString.match(/(?<=;).+?./)[0],
      objSting = `var ${objName} =` + code.match(new RegExp(`(?<=var ${objName}=)[\\s\\S]+?}}`))[0];
    eval(objSting);
    console.log(fun) || console.log(objSting);
    return fun;
  }
  function decryptUrls(fun, index) {
    let url = dataArray[index].signatureCipher,
      sig = fun(decodeURIComponent(url.match(/(?<=s=).+?(?=&sp)/)));
    url = decodeURIComponent(url.match(/https.+/)[0].replace(/\%25/g, "%"));
    return url + "&sig=" + encodeURIComponent(sig);
  }
  async function fetchMedia(url, controller, progressText, errcb) {
    try {
      let res = await fetch(url, {
        signal: controller.signal,
      });
      const reader = res.body.getReader();
      const contentLength = res.headers.get("Content-Length");
      progressText.totalLength += parseFloat((parseInt(contentLength) / 1024 / 1024).toFixed(2));
      let chunks = [];
      while (true) {
        const { done, value } = await reader.read();
        if (done) {
          break;
        }
        chunks.push(value);
        progressText.receivedLength += value.length;
        progressText.text =
          "downloaded:" + (parseFloat(progressText.receivedLength) / 1024 / 1024).toFixed(2) + "MiB total:" + progressText.totalLength + "MiB";
      }
      return new Blob(chunks);
    } catch (error) {
      errcb(error);
      return null;
    }
  }
  function blobToUint8Array(blob) {
    return new Promise((rs, rj) => {
      let fileReader = new FileReader();
      fileReader.onload = () => {
        rs(new Uint8Array(fileReader.result));
      };
      fileReader.onerror = () => {
        rj({ name: "b28 failed" });
      };
      fileReader.readAsArrayBuffer(blob);
    });
  }
  function blobLink(blob, fileName) {
    let dl = document.createElement("a");
    dl.download = fileName;
    dl.href = URL.createObjectURL(blob);
    dl.click();
    URL.revokeObjectURL(dl.href);
    dl.remove();
  }
})();