Sleazy Fork is available in English.

含羞草破解播放 vip、收费免费观看 fi11.tv

含羞草破解播放 vip、收费免费观看 fi11.tv 免费下载视频

// ==UserScript==
// @name         含羞草破解播放 vip、收费免费观看 fi11.tv
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  含羞草破解播放 vip、收费免费观看 fi11.tv 免费下载视频
// @author       zxyty
// @match        *://*/videoContent/*
// @match        *://*/play/video/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=zogps.com
// @require      https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/crypto-js.min.js
// @grant        none
// @license      MIT
// ==/UserScript==

(async function (exports = {}) {
  const { VITE_APP_AES_KEY: fn, VITE_APP_AES_IV: gn } = {
    VITE_NODE_ENV: "production",
    VITE_APP_ROUTER_NAME: "false",
    VITE_APP_API_BASE_URL: "/api",
    VITE_APP_THEME: "dark",
    VITE_APP_LOG: "false",
    VITE_PORT: "8080",
    VITE_BASE_URL: "/api",
    VITE_OUTPUT_DIR: ".dist",
    VITE_APP_NAME: "fission-friends-pc",
    VITE_APP_WEB_SOCKET_URL: "127.0.0.1",
    VITE_APP_IMGKEY: "46cc793c53dc451b",
    VITE_APP_AES_PASSWORD_KEY: "0123456789123456",
    VITE_APP_AES_PASSWORD_IV: "0123456789123456",
    VITE_APP_AES_KEY: "B77A9FF7F323B5404902102257503C2F",
    VITE_APP_AES_IV: "B77A9FF7F323B5404902102257503C2F",
    BASE_URL: "https://js10.pmeaqve.cn/pc/",
    MODE: "production",
    DEV: !1,
    PROD: !0,
  };

  function encodeRequestData(data) {
    const t = new Date();
    return JSON.stringify({
      endata: encode(JSON.stringify(data || {})),
      ents: encode(parseInt(t.getTime() / 1000) + 60 * t.getTimezoneOffset()),
    });
  }

  function encode(e, { key: n, iv: t } = {}) {
    // 使用cryptojs aes加密
    var a = CryptoJS.enc.Utf8.parse(e),
      i = CryptoJS.AES.encrypt(a, CryptoJS.enc.Utf8.parse(n || fn), {
        iv: CryptoJS.enc.Utf8.parse(t || gn),
        mode: CryptoJS.mode.CBC,
        padding: CryptoJS.pad.Pkcs7,
      });
    return CryptoJS.enc.Base64.stringify(i.ciphertext);
  }

  function removePadding(buffer) {
    const outputBytes = buffer.byteLength;
    const paddingBytes =
      outputBytes && new DataView(buffer).getUint8(outputBytes - 1);
    if (paddingBytes) {
      return buffer.slice(0, outputBytes - paddingBytes);
    } else {
      return buffer;
    }
  }

  function AESDecryptor() {
    return {
      constructor() {
        this.rcon = [
          0x0, 0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36,
        ];
        this.subMix = [
          new Uint32Array(256),
          new Uint32Array(256),
          new Uint32Array(256),
          new Uint32Array(256),
        ];
        this.invSubMix = [
          new Uint32Array(256),
          new Uint32Array(256),
          new Uint32Array(256),
          new Uint32Array(256),
        ];
        this.sBox = new Uint32Array(256);
        this.invSBox = new Uint32Array(256);

        // Changes during runtime
        this.key = new Uint32Array(0);

        this.initTable();
      },

      // Using view.getUint32() also swaps the byte order.
      uint8ArrayToUint32Array_(arrayBuffer) {
        let view = new DataView(arrayBuffer);
        let newArray = new Uint32Array(4);
        for (let i = 0; i < 4; i++) {
          newArray[i] = view.getUint32(i * 4);
        }

        return newArray;
      },

      initTable() {
        let sBox = this.sBox;
        let invSBox = this.invSBox;
        let subMix = this.subMix;
        let subMix0 = subMix[0];
        let subMix1 = subMix[1];
        let subMix2 = subMix[2];
        let subMix3 = subMix[3];
        let invSubMix = this.invSubMix;
        let invSubMix0 = invSubMix[0];
        let invSubMix1 = invSubMix[1];
        let invSubMix2 = invSubMix[2];
        let invSubMix3 = invSubMix[3];

        let d = new Uint32Array(256);
        let x = 0;
        let xi = 0;
        let i = 0;
        for (i = 0; i < 256; i++) {
          if (i < 128) {
            d[i] = i << 1;
          } else {
            d[i] = (i << 1) ^ 0x11b;
          }
        }

        for (i = 0; i < 256; i++) {
          let sx = xi ^ (xi << 1) ^ (xi << 2) ^ (xi << 3) ^ (xi << 4);
          sx = (sx >>> 8) ^ (sx & 0xff) ^ 0x63;
          sBox[x] = sx;
          invSBox[sx] = x;

          // Compute multiplication
          let x2 = d[x];
          let x4 = d[x2];
          let x8 = d[x4];

          // Compute sub/invSub bytes, mix columns tables
          let t = (d[sx] * 0x101) ^ (sx * 0x1010100);
          subMix0[x] = (t << 24) | (t >>> 8);
          subMix1[x] = (t << 16) | (t >>> 16);
          subMix2[x] = (t << 8) | (t >>> 24);
          subMix3[x] = t;

          // Compute inv sub bytes, inv mix columns tables
          t =
            (x8 * 0x1010101) ^ (x4 * 0x10001) ^ (x2 * 0x101) ^ (x * 0x1010100);
          invSubMix0[sx] = (t << 24) | (t >>> 8);
          invSubMix1[sx] = (t << 16) | (t >>> 16);
          invSubMix2[sx] = (t << 8) | (t >>> 24);
          invSubMix3[sx] = t;

          // Compute next counter
          if (!x) {
            x = xi = 1;
          } else {
            x = x2 ^ d[d[d[x8 ^ x2]]];
            xi ^= d[d[xi]];
          }
        }
      },

      expandKey(keyBuffer) {
        // convert keyBuffer to Uint32Array
        let key = this.uint8ArrayToUint32Array_(keyBuffer);
        let sameKey = true;
        let offset = 0;

        while (offset < key.length && sameKey) {
          sameKey = key[offset] === this.key[offset];
          offset++;
        }

        if (sameKey) {
          return;
        }

        this.key = key;
        let keySize = (this.keySize = key.length);

        if (keySize !== 4 && keySize !== 6 && keySize !== 8) {
          throw new Error("Invalid aes key size=" + keySize);
        }

        let ksRows = (this.ksRows = (keySize + 6 + 1) * 4);
        let ksRow;
        let invKsRow;

        let keySchedule = (this.keySchedule = new Uint32Array(ksRows));
        let invKeySchedule = (this.invKeySchedule = new Uint32Array(ksRows));
        let sbox = this.sBox;
        let rcon = this.rcon;

        let invSubMix = this.invSubMix;
        let invSubMix0 = invSubMix[0];
        let invSubMix1 = invSubMix[1];
        let invSubMix2 = invSubMix[2];
        let invSubMix3 = invSubMix[3];

        let prev;
        let t;

        for (ksRow = 0; ksRow < ksRows; ksRow++) {
          if (ksRow < keySize) {
            prev = keySchedule[ksRow] = key[ksRow];
            continue;
          }
          t = prev;

          if (ksRow % keySize === 0) {
            // Rot word
            t = (t << 8) | (t >>> 24);

            // Sub word
            t =
              (sbox[t >>> 24] << 24) |
              (sbox[(t >>> 16) & 0xff] << 16) |
              (sbox[(t >>> 8) & 0xff] << 8) |
              sbox[t & 0xff];

            // Mix Rcon
            t ^= rcon[(ksRow / keySize) | 0] << 24;
          } else if (keySize > 6 && ksRow % keySize === 4) {
            // Sub word
            t =
              (sbox[t >>> 24] << 24) |
              (sbox[(t >>> 16) & 0xff] << 16) |
              (sbox[(t >>> 8) & 0xff] << 8) |
              sbox[t & 0xff];
          }

          keySchedule[ksRow] = prev = (keySchedule[ksRow - keySize] ^ t) >>> 0;
        }

        for (invKsRow = 0; invKsRow < ksRows; invKsRow++) {
          ksRow = ksRows - invKsRow;
          if (invKsRow & 3) {
            t = keySchedule[ksRow];
          } else {
            t = keySchedule[ksRow - 4];
          }

          if (invKsRow < 4 || ksRow <= 4) {
            invKeySchedule[invKsRow] = t;
          } else {
            invKeySchedule[invKsRow] =
              invSubMix0[sbox[t >>> 24]] ^
              invSubMix1[sbox[(t >>> 16) & 0xff]] ^
              invSubMix2[sbox[(t >>> 8) & 0xff]] ^
              invSubMix3[sbox[t & 0xff]];
          }

          invKeySchedule[invKsRow] = invKeySchedule[invKsRow] >>> 0;
        }
      },

      // Adding this as a method greatly improves performance.
      networkToHostOrderSwap(word) {
        return (
          (word << 24) |
          ((word & 0xff00) << 8) |
          ((word & 0xff0000) >> 8) |
          (word >>> 24)
        );
      },

      decrypt(inputArrayBuffer, offset, aesIV, removePKCS7Padding) {
        let nRounds = this.keySize + 6;
        let invKeySchedule = this.invKeySchedule;
        let invSBOX = this.invSBox;

        let invSubMix = this.invSubMix;
        let invSubMix0 = invSubMix[0];
        let invSubMix1 = invSubMix[1];
        let invSubMix2 = invSubMix[2];
        let invSubMix3 = invSubMix[3];

        let initVector = this.uint8ArrayToUint32Array_(aesIV);
        let initVector0 = initVector[0];
        let initVector1 = initVector[1];
        let initVector2 = initVector[2];
        let initVector3 = initVector[3];

        let inputInt32 = new Int32Array(inputArrayBuffer);
        let outputInt32 = new Int32Array(inputInt32.length);

        let t0, t1, t2, t3;
        let s0, s1, s2, s3;
        let inputWords0, inputWords1, inputWords2, inputWords3;

        let ksRow, i;
        let swapWord = this.networkToHostOrderSwap;

        while (offset < inputInt32.length) {
          inputWords0 = swapWord(inputInt32[offset]);
          inputWords1 = swapWord(inputInt32[offset + 1]);
          inputWords2 = swapWord(inputInt32[offset + 2]);
          inputWords3 = swapWord(inputInt32[offset + 3]);

          s0 = inputWords0 ^ invKeySchedule[0];
          s1 = inputWords3 ^ invKeySchedule[1];
          s2 = inputWords2 ^ invKeySchedule[2];
          s3 = inputWords1 ^ invKeySchedule[3];

          ksRow = 4;

          // Iterate through the rounds of decryption
          for (i = 1; i < nRounds; i++) {
            t0 =
              invSubMix0[s0 >>> 24] ^
              invSubMix1[(s1 >> 16) & 0xff] ^
              invSubMix2[(s2 >> 8) & 0xff] ^
              invSubMix3[s3 & 0xff] ^
              invKeySchedule[ksRow];
            t1 =
              invSubMix0[s1 >>> 24] ^
              invSubMix1[(s2 >> 16) & 0xff] ^
              invSubMix2[(s3 >> 8) & 0xff] ^
              invSubMix3[s0 & 0xff] ^
              invKeySchedule[ksRow + 1];
            t2 =
              invSubMix0[s2 >>> 24] ^
              invSubMix1[(s3 >> 16) & 0xff] ^
              invSubMix2[(s0 >> 8) & 0xff] ^
              invSubMix3[s1 & 0xff] ^
              invKeySchedule[ksRow + 2];
            t3 =
              invSubMix0[s3 >>> 24] ^
              invSubMix1[(s0 >> 16) & 0xff] ^
              invSubMix2[(s1 >> 8) & 0xff] ^
              invSubMix3[s2 & 0xff] ^
              invKeySchedule[ksRow + 3];
            // Update state
            s0 = t0;
            s1 = t1;
            s2 = t2;
            s3 = t3;

            ksRow = ksRow + 4;
          }

          // Shift rows, sub bytes, add round key
          t0 =
            (invSBOX[s0 >>> 24] << 24) ^
            (invSBOX[(s1 >> 16) & 0xff] << 16) ^
            (invSBOX[(s2 >> 8) & 0xff] << 8) ^
            invSBOX[s3 & 0xff] ^
            invKeySchedule[ksRow];
          t1 =
            (invSBOX[s1 >>> 24] << 24) ^
            (invSBOX[(s2 >> 16) & 0xff] << 16) ^
            (invSBOX[(s3 >> 8) & 0xff] << 8) ^
            invSBOX[s0 & 0xff] ^
            invKeySchedule[ksRow + 1];
          t2 =
            (invSBOX[s2 >>> 24] << 24) ^
            (invSBOX[(s3 >> 16) & 0xff] << 16) ^
            (invSBOX[(s0 >> 8) & 0xff] << 8) ^
            invSBOX[s1 & 0xff] ^
            invKeySchedule[ksRow + 2];
          t3 =
            (invSBOX[s3 >>> 24] << 24) ^
            (invSBOX[(s0 >> 16) & 0xff] << 16) ^
            (invSBOX[(s1 >> 8) & 0xff] << 8) ^
            invSBOX[s2 & 0xff] ^
            invKeySchedule[ksRow + 3];
          ksRow = ksRow + 3;

          // Write
          outputInt32[offset] = swapWord(t0 ^ initVector0);
          outputInt32[offset + 1] = swapWord(t3 ^ initVector1);
          outputInt32[offset + 2] = swapWord(t2 ^ initVector2);
          outputInt32[offset + 3] = swapWord(t1 ^ initVector3);

          // reset initVector to last 4 unsigned int
          initVector0 = inputWords0;
          initVector1 = inputWords1;
          initVector2 = inputWords2;
          initVector3 = inputWords3;

          offset = offset + 4;
        }

        return removePKCS7Padding
          ? removePadding(outputInt32.buffer)
          : outputInt32.buffer;
      },

      destroy() {
        this.key = undefined;
        this.keySize = undefined;
        this.ksRows = undefined;

        this.sBox = undefined;
        this.invSBox = undefined;
        this.subMix = undefined;
        this.invSubMix = undefined;
        this.keySchedule = undefined;
        this.invKeySchedule = undefined;

        this.rcon = undefined;
      },
    };
  }

  function stringToBuffer(str) {
    return new TextEncoder().encode(str);
  }

  const apiHost = window.origin; // `https://www.16855888.top`;

  const APIS = {
    login: apiHost + "/api/login/userLogin",
    videoType: apiHost + "/api/album/getAlbumList",
    videoList: apiHost + "/api/album/getAlbumVideoList",
    actorVedioList: apiHost + "/api/actor/getActorVideoList",
    previewVideo: apiHost + "/api/videos/getPreUrl",
  };

  const fileDownload = async (url) => {
    const fileBlob = await fetch(url).then((res) => res.arrayBuffer());
    return fileBlob;
  };

  // ts 片段的 AES 解码
  const aesDecrypt = (blobData, index, aesConf) => {
    const iv =
      aesConf.iv ||
      new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, index]);
    return aesConf.decryptor.decrypt(blobData, 0, iv.buffer || iv, true);
  };

  async function getM3u8TsUrlArr(m3u8Url, hostName = "") {
    return new Promise((resolve) => {
      fetch(m3u8Url, { method: "GET" })
        .then((res) => res.text())
        .then(async (response) => {
          const urlArrs = response
            .split("\n")
            .filter((d) => d && !d.startsWith("#EXT"));

          if (urlArrs?.length === 1 && urlArrs[0].indexOf(".m3u8") > 0) {
            // 说明是m3u8地址内嵌m3u8地址
            const result = await getM3u8TsUrlArr(
              `${hostName}${urlArrs[0]}`,
              hostName
            );
            resolve(result);
          } else {
            const aseConfig = await getAESDecodeConfig(response, hostName);
            resolve({ tsArr: urlArrs, m3u8Url, aseConfig });
          }
        });
    });
  }

  async function getAESDecodeConfig(m3u8Str, hostName) {
    // 检测视频 AES 加密
    if (m3u8Str.indexOf("#EXT-X-KEY") > -1) {
      const aseConfig = {};
      aseConfig.method = (m3u8Str.match(/(.*METHOD=([^,\s]+))/) || [
        "",
        "",
        "",
      ])[2];
      aseConfig.uri = (m3u8Str.match(/(.*URI="([^"]+))"/) || ["", "", ""])[2];
      aseConfig.iv = (m3u8Str.match(/(.*IV=([^,\s]+))/) || ["", "", ""])[2];
      aseConfig.iv = aseConfig.iv ? stringToBuffer(aseConfig.iv) : "";
      aseConfig.uri = (() => {
        if (aseConfig.uri.startsWith("/")) {
          return `${hostName}${aseConfig.uri}`;
        }
        return aseConfig.uri;
      })();

      const aseKeyResult = await fetch(aseConfig.uri).then((res) =>
        res.arrayBuffer()
      );
      aseConfig.key = aseKeyResult;
      aseConfig.decryptor = new AESDecryptor();
      aseConfig.decryptor.constructor();
      aseConfig.decryptor.expandKey(aseConfig.key);
      return aseConfig;
    }
    return null;
  }

  const STATE = {
    token: "",
    userId: "",
  };

  const SERVICES = {
    user_login: async () => {
      if (STATE.token) {
        return STATE;
      }

      const loginResult = await fetch(APIS.login, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: encodeRequestData({
          user_login: "xxxxx",
          user_pass: "xxxxx",
        }),
      }).then((res) => res.json());

      // save user info
      const { id: userId, token } = loginResult.data;

      STATE.userId = userId;
      STATE.token = token;

      return STATE;
    },
    get_video_type: async () => {
      const videoTypeResult = await fetch(APIS.videoType, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: encodeRequestData({
          length: 999,
          page: 1,
        }),
      }).then((res) => res.json());

      return videoTypeResult.data.list;
    },
    download_m3u8: async (m3u8Src, outfileName) => {
      const { tsArr, aseConfig } = await getM3u8TsUrlArr(m3u8Src);

      // const download_process = tsArr.map((d, i) => {
      //     return fileDownload(d).then(data => {
      //         if (aseConfig) {
      //             return aesDecrypt(data, i, aseConfig);
      //         }
      //         return data;
      //     });
      //     // then(arrayBuffer => {
      //     //     return new Blob([arrayBuffer]);
      //     // });
      // });

      // 下载结果全部存在此内存变量里
      const fileDataList = new Array(tsArr.length).fill(0);

      let downloadProcessDom = document.querySelector("#m3u8_download_process");
      if (!downloadProcessDom) {
        downloadProcessDom = document.createElement("div");
        downloadProcessDom.id = "m3u8_download_process";
        document.body.appendChild(downloadProcessDom);
      } else {
        downloadProcessDom.classList.remove("hide");
      }
      const innerHtml = tsArr
        .map((d, i) => {
          return `<div class="download_item">${i + 1}</div>`;
        })
        .join("");
      downloadProcessDom.innerHTML = innerHtml;

      const downloadItems =
        downloadProcessDom.querySelectorAll(".download_item");

      const downloadIndexTs = (index) => () => {
        return fileDownload(tsArr[index]).then((data) => {
          let validData = data;
          if (aseConfig) {
            validData = aesDecrypt(data, index, aseConfig);
          }
          fileDataList[index] = validData;
          const item =
            downloadProcessDom.querySelectorAll(".download_item")[index];
          item.classList.add("success");
          return validData;
        });
      };

      try {
        const MAX_REQUEST_SIZE = 6;
        let requestQueue = [];
        for (let i = 0; i < downloadItems.length; i++) {
          const item = downloadItems[i];
          item.title = "click to download again.";
          item.onclick = downloadIndexTs(i);
          requestQueue.push(item.onclick);
          if (requestQueue.length >= MAX_REQUEST_SIZE) {
            await Promise.all(requestQueue.map((func) => func()));
            requestQueue = [];
          }
        }
        await Promise.all(requestQueue.map((func) => func()));
      } catch (error) {}

      // if (fileDataList.every(d => !!d)) {
      // 说明所有的数据已经下载完毕
      // 这里不做判断 允许下载拼接所有当前下载完成的buffer
      const downloadAllBtn = document.createElement("div");
      downloadAllBtn.innerHTML = "Download";
      downloadAllBtn.classList.add("download_btn");
      downloadAllBtn.onclick = () => {
        const fileBlob = new Blob(
          fileDataList.filter((d) => !!d),
          { type: "video/MP2T" }
        ); // 创建一个Blob对象,并设置文件的 MIME 类型
        const a = document.createElement("a");
        a.download = outfileName || `download.ts`;
        a.href = URL.createObjectURL(fileBlob);
        a.style.display = "none";
        document.body.appendChild(a);
        a.click();
        a.remove();
        downloadProcessDom.classList.add("hide");
      };
      downloadProcessDom.appendChild(downloadAllBtn);
    },
    fetch_m3u8Url: async (videoId) => {
      const m3u8UrlResult = await fetch(APIS.previewVideo, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Auth: STATE.token,
        },
        body: encodeRequestData({
          videoId: videoId,
        }),
      }).then((res) => res.json());

      const {
        data: { url },
        code,
      } = m3u8UrlResult;

      if (code === 2010) {
        throw new Error("非vip视频");
      }

      // https://ts.pmeaqve.cn/20230217/7kd7zbuF/index.m3u8?start1=1&end=10&sign=204401fc9a2cd377d7019156e8066bb5b1ee3c9073658c383b28a70f95a22f30e68dd77cdf83da9be5290c1ae3e20ce5
      // 返回的query里有start和end参数需要去掉拿到整个片段
      if (!url) {
        // 如果url不存在
        // 有可能是没有登录
        await SERVICES.user_login();
        return await SERVICES.fetch_m3u8Url(videoId);
      }
      const [m3u8Url, query] = url.split("?");
      const fullUrl = `${m3u8Url}?sign=${query.split("&sign=")[1]}`;
      return fullUrl;
    },
    download_video: async (videoId, outfileName) => {
      const fullUrl = await SERVICES.fetch_m3u8Url(videoId);

      if (!fullUrl) {
        // 如果url不存在
        // 有可能是没有登录
        await SERVICES.user_login();
        return await SERVICES.download_video(videoId, outfileName);
      }

      const { origin: hostName } = new URL(fullUrl);
      const { tsArr, aseConfig } = await getM3u8TsUrlArr(fullUrl, hostName);

      // const download_process = tsArr.map((d, i) => {
      //     return fileDownload(d).then(data => {
      //         if (aseConfig) {
      //             return aesDecrypt(data, i, aseConfig);
      //         }
      //         return data;
      //     });
      //     // then(arrayBuffer => {
      //     //     return new Blob([arrayBuffer]);
      //     // });
      // });

      // 下载结果全部存在此内存变量里
      const fileDataList = new Array(tsArr.length).fill(0);

      let downloadProcessDom = document.querySelector("#m3u8_download_process");
      if (!downloadProcessDom) {
        downloadProcessDom = document.createElement("div");
        downloadProcessDom.id = "m3u8_download_process";
        document.body.appendChild(downloadProcessDom);
      } else {
        downloadProcessDom.classList.remove("hide");
      }
      const innerHtml = tsArr
        .map((d, i) => {
          return `<div class="download_item">${i + 1}</div>`;
        })
        .join("");
      downloadProcessDom.innerHTML = innerHtml;

      const downloadItems =
        downloadProcessDom.querySelectorAll(".download_item");

      const downloadIndexTs = (index) => () => {
        return fileDownload(tsArr[index]).then((data) => {
          let validData = data;
          if (aseConfig) {
            validData = aesDecrypt(data, index, aseConfig);
          }
          fileDataList[index] = validData;
          const item =
            downloadProcessDom.querySelectorAll(".download_item")[index];
          item.classList.add("success");
          return validData;
        });
      };

      try {
        const MAX_REQUEST_SIZE = 6;
        let requestQueue = [];
        for (let i = 0; i < downloadItems.length; i++) {
          const item = downloadItems[i];
          item.title = "click to download again.";
          item.onclick = downloadIndexTs(i);
          requestQueue.push(item.onclick);
          if (requestQueue.length >= MAX_REQUEST_SIZE) {
            await Promise.all(requestQueue.map((func) => func()));
            requestQueue = [];
          }
        }
        await Promise.all(requestQueue.map((func) => func()));
      } catch (error) {}

      // if (fileDataList.every(d => !!d)) {
      // 说明所有的数据已经下载完毕
      // 这里不做判断 允许下载拼接所有当前下载完成的buffer
      const downloadAllBtn = document.createElement("div");
      downloadAllBtn.innerHTML = "Download";
      downloadAllBtn.classList.add("download_btn");
      downloadAllBtn.onclick = () => {
        const fileBlob = new Blob(
          fileDataList.filter((d) => !!d),
          { type: "video/MP2T" }
        ); // 创建一个Blob对象,并设置文件的 MIME 类型
        const a = document.createElement("a");
        a.download = outfileName || `download.ts`;
        a.href = URL.createObjectURL(fileBlob);
        a.style.display = "none";
        document.body.appendChild(a);
        a.click();
        a.remove();
        downloadProcessDom.classList.add("hide");
      };
      downloadProcessDom.appendChild(downloadAllBtn);
      // }
    },
  };

  exports.downHelper = SERVICES;

  // auto inject the download btn;
  const cssInject = `
        #m3u8_download_process {
            display: flex;
            flex-wrap: wrap;
            position: fixed;
            right: 0;
            top: 0;
            width: 460px;
            height: 300px;
            overflow: auto;
            padding: 8px;
            background: #fff;
            box-shadow: 0px 0px 4px #164375;
            z-index: 999999;
        }
        #m3u8_download_process.hide {
            display: none;
        }
        #m3u8_download_process .download_item {
            padding: 4px;
            border-radius: 2px;
            width: 38px;
            justify-content: center;
            background-color: #666;
            margin: 8px;
            color: #fff;
            cursor: pointer;
        }
        #m3u8_download_process .download_item.success {
            background-color: green;
            color: #fff;
        }
        #m3u8_download_process .download_btn {
            display: flex;
            width: 100%;
            align-items: center;
            justify-content: center;
            padding: 4px 8px;
            border-radius: 4px;
            background: green;
            color: red;
            cursor: pointer;
        }
        ._relative {
          // position: relative !important;
        }
        ._relative:hover ._down_btn{
            display: block;
        }
        ._relative:hover ._play_btn{
            display: block;
        }
        ._down_btn {
            display: none;
            position: absolute !important;
            right: 4px;
            top: 4px;
            padding: 4px;
            border-radius: 4px;
            background: #ccc;
            color: red;
            z-index: 999;
            cursor: pointer;
        }
        ._play_btn {
            display: none;
            position: absolute !important;
            left: 4px;
            top: 4px;
            padding: 4px;
            border-radius: 4px;
            background: #ccc;
            color: red;
            z-index: 999;
            cursor: pointer;
        }
    `;

  if (!document.querySelector("#styleDom")) {
    const styleDom = document.createElement("style");
    styleDom.id = "inject_css";
    styleDom.innerHTML = cssInject;
    document.head.appendChild(styleDom);
  }

  if (document.domain !== apiHost.split("//")[1]) {
    return;
  }

  const [_, pageName, pageType, videoId] = window.location.pathname.split("/");
  // https://www.fi11sm245.com/play/video/30827
  // ['', 'play', 'video', '30827']
  if (!/^[\d]+?$/i.test(videoId) || pageName !== "play") {
    // 没在播放界面
    return;
  }

  const createDownloadBtn = () => {
    // const vi = document.querySelector("#videoContent");
    const vi = document.querySelector(".vip-mask");
    if (!vi) {
      return;
    }

    if (vi.querySelector("._down_btn")) {
      return;
    }

    let titleDom = document.querySelector(".article-title");
    if (!titleDom || !titleDom.innerText) {
      return;
    }
    const title = titleDom.innerText;

    clearInterval(timer);
    vi.classList.remove("_relative");
    vi.classList.add("_relative");

    // download btn
    const newBtn = document.createElement("div");
    newBtn.onclick = (e) => {
      e.stopPropagation();
      SERVICES.download_video(Number(videoId), title + ".ts");
    };
    newBtn.innerHTML = "Download";
    newBtn.classList.add("_down_btn");
    vi.appendChild(newBtn);

    // play btn
    const playBtn = document.createElement("div");
    playBtn.innerHTML = "Go to play url";
    playBtn.classList.add("_play_btn");
    playBtn.onclick = (e) => {
      e.stopPropagation();
      SERVICES.fetch_m3u8Url(Number(videoId)).then((url) => {
        window.open(`https://www.zxyty.com/m3u8-play?link=${url}`);
      });
    };
    vi.appendChild(playBtn);
  };

  let timer = setInterval(createDownloadBtn, 4000);
})(window);