YTdl

download YouTube video

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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