From 4bd00eb47a23464611820c6a2cdb87e5bf06ce75 Mon Sep 17 00:00:00 2001
From: Jordaar <69628820+Jordaar@users.noreply.github.com>
Date: Fri, 16 Jun 2023 14:37:07 +0530
Subject: [PATCH 01/12] feat(embed): add upcloud and streamsb embed scrapers

---
 src/backend/embeds/streamsb.ts | 212 +++++++++++++++++++++++++++++++++
 src/backend/embeds/upcloud.ts  |  93 +++++++++++++++
 2 files changed, 305 insertions(+)
 create mode 100644 src/backend/embeds/streamsb.ts
 create mode 100644 src/backend/embeds/upcloud.ts

diff --git a/src/backend/embeds/streamsb.ts b/src/backend/embeds/streamsb.ts
new file mode 100644
index 00000000..c755a0b0
--- /dev/null
+++ b/src/backend/embeds/streamsb.ts
@@ -0,0 +1,212 @@
+import Base64 from "crypto-js/enc-base64";
+import Utf8 from "crypto-js/enc-utf8";
+
+import { MWEmbedType } from "@/backend/helpers/embed";
+import { proxiedFetch } from "@/backend/helpers/fetch";
+import { registerEmbedScraper } from "@/backend/helpers/register";
+import {
+  MWCaptionType,
+  MWStreamQuality,
+  MWStreamType,
+} from "@/backend/helpers/streams";
+
+const qualityOrder = [
+  MWStreamQuality.Q1080P,
+  MWStreamQuality.Q720P,
+  MWStreamQuality.Q480P,
+  MWStreamQuality.Q360P,
+];
+
+async function fetchCaptchaToken(domain: string, recaptchaKey: string) {
+  const domainHash = Base64.stringify(Utf8.parse(domain)).replace(/=/g, ".");
+
+  const recaptchaRender = await proxiedFetch<any>(
+    `https://www.google.com/recaptcha/api.js?render=${recaptchaKey}`
+  );
+
+  const vToken = recaptchaRender.substring(
+    recaptchaRender.indexOf("/releases/") + 10,
+    recaptchaRender.indexOf("/recaptcha__en.js")
+  );
+
+  const recaptchaAnchor = await proxiedFetch<any>(
+    `https://www.google.com/recaptcha/api2/anchor?ar=1&hl=en&size=invisible&cb=flicklax&k=${recaptchaKey}&co=${domainHash}&v=${vToken}`
+  );
+
+  const cToken = new DOMParser()
+    .parseFromString(recaptchaAnchor, "text/html")
+    .getElementById("recaptcha-token")
+    ?.getAttribute("value");
+
+  if (!cToken) throw new Error("Unable to find cToken");
+
+  const payload = {
+    v: vToken,
+    reason: "q",
+    k: recaptchaKey,
+    c: cToken,
+    sa: "",
+    co: domain,
+  };
+
+  const tokenData = await proxiedFetch<string>(
+    `https://www.google.com/recaptcha/api2/reload?${new URLSearchParams(
+      payload
+    ).toString()}`,
+    {
+      headers: { referer: "https://www.google.com/recaptcha/api2/" },
+      method: "POST",
+    }
+  );
+
+  const token = tokenData.match('rresp","(.+?)"');
+  return token ? token[1] : null;
+}
+
+registerEmbedScraper({
+  id: "streamsb",
+  displayName: "StreamSB",
+  for: MWEmbedType.STREAMSB,
+  rank: 150,
+  async getStream({ url, progress }) {
+    /* Url variations
+    - domain.com/{id}?.html
+    - domain.com/{id}
+    - domain.com/embed-{id}
+    - domain.com/d/{id}
+    - domain.com/e/{id}
+    - domain.com/e/{id}-embed
+    */
+    const streamsbUrl = url
+      .replace(".html", "")
+      .replace("embed-", "")
+      .replace("e/", "")
+      .replace("d/", "");
+
+    const parsedUrl = new URL(streamsbUrl);
+    const base = await proxiedFetch<any>(
+      `${parsedUrl.origin}/d${parsedUrl.pathname}`
+    );
+
+    progress(20);
+
+    // Parse captions from url
+    const captionUrl = parsedUrl.searchParams.get("caption_1");
+    const captionLang = parsedUrl.searchParams.get("sub_1");
+
+    const basePage = new DOMParser().parseFromString(base, "text/html");
+
+    const downloadVideoFunctions = basePage.querySelectorAll(
+      "[onclick^=download_video]"
+    );
+
+    const dlDetails = [];
+    for (const func of downloadVideoFunctions) {
+      const funcContents = func.getAttribute("onclick");
+      const regExpFunc = /download_video\('(.+?)','(.+?)','(.+?)'\)/;
+      const matchesFunc = regExpFunc.exec(funcContents ?? "");
+      if (matchesFunc !== null) {
+        const quality = func.querySelector("span")?.textContent;
+        const regExpQuality = /(.+?) \((.+?)\)/;
+        const matchesQuality = regExpQuality.exec(quality ?? "");
+        if (matchesQuality !== null) {
+          dlDetails.push({
+            parameters: [matchesFunc[1], matchesFunc[2], matchesFunc[3]],
+            quality: {
+              label: matchesQuality[1].trim(),
+              size: matchesQuality[2],
+            },
+          });
+        }
+      }
+    }
+
+    progress(40);
+
+    let dls = await Promise.all(
+      dlDetails.map(async (dl) => {
+        const getDownload = await proxiedFetch<any>(
+          `/dl?op=download_orig&id=${dl.parameters[0]}&mode=${dl.parameters[1]}&hash=${dl.parameters[2]}`,
+          {
+            baseURL: parsedUrl.origin,
+          }
+        );
+
+        const downloadPage = new DOMParser().parseFromString(
+          getDownload,
+          "text/html"
+        );
+
+        const recaptchaKey = downloadPage
+          .querySelector(".g-recaptcha")
+          ?.getAttribute("data-sitekey");
+        if (!recaptchaKey) throw new Error("Unable to get captcha key");
+
+        const captchaToken = await fetchCaptchaToken(
+          parsedUrl.origin,
+          recaptchaKey
+        );
+        if (!captchaToken) throw new Error("Unable to get captcha token");
+
+        const dlForm = new FormData();
+        dlForm.append("op", "download_orig");
+        dlForm.append("id", dl.parameters[0]);
+        dlForm.append("mode", dl.parameters[1]);
+        dlForm.append("hash", dl.parameters[2]);
+        dlForm.append("g-recaptcha-response", captchaToken);
+
+        const download = await proxiedFetch<any>(
+          `/dl?op=download_orig&id=${dl.parameters[0]}&mode=${dl.parameters[1]}&hash=${dl.parameters[2]}`,
+          {
+            baseURL: parsedUrl.origin,
+            method: "POST",
+            body: dlForm,
+          }
+        );
+
+        const dlLink = new DOMParser()
+          .parseFromString(download, "text/html")
+          .querySelector(".btn.btn-light.btn-lg")
+          ?.getAttribute("href");
+
+        console.log(dlLink);
+
+        return {
+          quality: dl.quality.label as MWStreamQuality,
+          url: dlLink,
+          size: dl.quality.size,
+          captions:
+            captionUrl && captionLang
+              ? [
+                  {
+                    url: captionUrl,
+                    langIso: captionLang,
+                    type: MWCaptionType.VTT,
+                  },
+                ]
+              : [],
+        };
+      })
+    );
+    dls = dls.filter((d) => !!d.url);
+    dls = dls.sort((a, b) => {
+      const aQuality = qualityOrder.indexOf(a.quality);
+      const bQuality = qualityOrder.indexOf(b.quality);
+      return aQuality - bQuality;
+    });
+
+    progress(60);
+
+    // TODO: Quality selection for embed scrapers
+    const dl = dls[0];
+    if (!dl.url) throw new Error("No stream url found");
+
+    return {
+      embedId: MWEmbedType.STREAMSB,
+      streamUrl: dl.url,
+      quality: dl.quality,
+      captions: dl.captions,
+      type: MWStreamType.MP4,
+    };
+  },
+});
diff --git a/src/backend/embeds/upcloud.ts b/src/backend/embeds/upcloud.ts
new file mode 100644
index 00000000..b2877bb3
--- /dev/null
+++ b/src/backend/embeds/upcloud.ts
@@ -0,0 +1,93 @@
+import { AES, enc } from "crypto-js";
+
+import { MWEmbedType } from "@/backend/helpers/embed";
+import { registerEmbedScraper } from "@/backend/helpers/register";
+import {
+  MWCaptionType,
+  MWStreamQuality,
+  MWStreamType,
+} from "@/backend/helpers/streams";
+
+import { proxiedFetch } from "../helpers/fetch";
+
+interface StreamRes {
+  server: number;
+  sources: string;
+  tracks: {
+    file: string;
+    kind: "captions" | "thumbnails";
+    label: string;
+  }[];
+}
+
+function isJSON(json: string) {
+  try {
+    JSON.parse(json);
+    return true;
+  } catch {
+    return false;
+  }
+}
+
+registerEmbedScraper({
+  id: "upcloud",
+  displayName: "UpCloud",
+  for: MWEmbedType.UPCLOUD,
+  rank: 200,
+  async getStream({ url }) {
+    // Example url: https://dokicloud.one/embed-4/{id}?z=
+    const parsedUrl = new URL(url.replace("embed-5", "embed-4"));
+
+    const dataPath = parsedUrl.pathname.split("/");
+    const dataId = dataPath[dataPath.length - 1];
+
+    const streamRes = await proxiedFetch<StreamRes>(
+      `${parsedUrl.origin}/ajax/embed-4/getSources?id=${dataId}`,
+      {
+        headers: {
+          Referer: parsedUrl.origin,
+          "X-Requested-With": "XMLHttpRequest",
+        },
+      }
+    );
+
+    let sources:
+      | {
+          file: string;
+          type: string;
+        }
+      | string = streamRes.sources;
+
+    if (!isJSON(sources) || typeof sources === "string") {
+      const decryptionKey = await proxiedFetch<string>(
+        `https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt`
+      );
+
+      const decryptedStream = AES.decrypt(sources, decryptionKey).toString(
+        enc.Utf8
+      );
+
+      const parsedStream = JSON.parse(decryptedStream)[0];
+      if (!parsedStream) throw new Error("No stream found");
+      sources = parsedStream as { file: string; type: string };
+    }
+
+    return {
+      embedId: MWEmbedType.UPCLOUD,
+      streamUrl: sources.file,
+      quality: MWStreamQuality.Q1080P,
+      type: MWStreamType.HLS,
+      captions: streamRes.tracks
+        .filter((sub) => sub.kind === "captions")
+        .map((sub) => {
+          return {
+            langIso: sub.label,
+            url: sub.file,
+            type: sub.file.endsWith("vtt")
+              ? MWCaptionType.VTT
+              : MWCaptionType.UNKNOWN,
+          };
+        }),
+    };
+  },
+});

From 7e696d5c2cad0e8c4f236cad4d7e1ca47f0f59ea Mon Sep 17 00:00:00 2001
From: Jordaar <69628820+Jordaar@users.noreply.github.com>
Date: Fri, 16 Jun 2023 14:37:41 +0530
Subject: [PATCH 02/12] feat(provider): add gomovies provider

---
 src/backend/providers/gomovies.ts | 162 ++++++++++++++++++++++++++++++
 1 file changed, 162 insertions(+)
 create mode 100644 src/backend/providers/gomovies.ts

diff --git a/src/backend/providers/gomovies.ts b/src/backend/providers/gomovies.ts
new file mode 100644
index 00000000..9e22d095
--- /dev/null
+++ b/src/backend/providers/gomovies.ts
@@ -0,0 +1,162 @@
+import { MWEmbedType } from "../helpers/embed";
+import { proxiedFetch } from "../helpers/fetch";
+import { registerProvider } from "../helpers/register";
+import { MWMediaType } from "../metadata/types";
+
+const gomoviesBase = "https://gomovies.sx";
+
+registerProvider({
+  id: "gomovies",
+  displayName: "GOmovies",
+  rank: 300,
+  type: [MWMediaType.MOVIE, MWMediaType.SERIES],
+
+  async scrape({ media, episode }) {
+    const search = await proxiedFetch<any>("/ajax/search", {
+      baseURL: gomoviesBase,
+      method: "POST",
+      body: JSON.stringify({
+        keyword: media.meta.title,
+      }),
+      headers: {
+        "X-Requested-With": "XMLHttpRequest",
+      },
+    });
+
+    const searchPage = new DOMParser().parseFromString(search, "text/html");
+    const mediaElements = searchPage.querySelectorAll("a.nav-item");
+
+    const mediaData = Array.from(mediaElements).map((movieEl) => {
+      const name = movieEl?.querySelector("h3.film-name")?.textContent;
+      const year = movieEl?.querySelector(
+        "div.film-infor span:first-of-type"
+      )?.textContent;
+      const path = movieEl.getAttribute("href");
+      return { name, year, path };
+    });
+
+    const targetMedia = mediaData.find(
+      (m) =>
+        m.name === media.meta.title &&
+        (media.meta.type === MWMediaType.MOVIE
+          ? m.year === media.meta.year
+          : true)
+    );
+    if (!targetMedia?.path) throw new Error("Media not found");
+
+    // Example movie path: /movie/watch-{slug}-{id}
+    // Example series path: /tv/watch-{slug}-{id}
+    let mediaId = targetMedia.path.split("-").pop()?.replace("/", "");
+
+    let sources = null;
+    if (media.meta.type === MWMediaType.SERIES) {
+      const seasons = await proxiedFetch<any>(
+        `/ajax/v2/tv/seasons/${mediaId}`,
+        {
+          baseURL: gomoviesBase,
+          headers: {
+            "X-Requested-With": "XMLHttpRequest",
+          },
+        }
+      );
+
+      const seasonsEl = new DOMParser()
+        .parseFromString(seasons, "text/html")
+        .querySelectorAll(".ss-item");
+
+      const seasonsData = [...seasonsEl].map((season) => ({
+        number: season.innerHTML.replace("Season ", ""),
+        dataId: season.getAttribute("data-id"),
+      }));
+
+      const seasonNumber = media.meta.seasonData.number;
+      const targetSeason = seasonsData.find(
+        (season) => +season.number === seasonNumber
+      );
+      if (!targetSeason) throw new Error("Season not found");
+
+      const episodes = await proxiedFetch<any>(
+        `/ajax/v2/season/episodes/${targetSeason.dataId}`,
+        {
+          baseURL: gomoviesBase,
+          headers: {
+            "X-Requested-With": "XMLHttpRequest",
+          },
+        }
+      );
+
+      const episodesEl = new DOMParser()
+        .parseFromString(episodes, "text/html")
+        .querySelectorAll(".eps-item");
+
+      const episodesData = Array.from(episodesEl).map((ep) => ({
+        dataId: ep.getAttribute("data-id"),
+        number: ep
+          .querySelector("strong")
+          ?.textContent?.replace("Eps", "")
+          .replace(":", "")
+          .trim(),
+      }));
+
+      const episodeNumber = media.meta.seasonData.episodes.find(
+        (e) => e.id === episode
+      )?.number;
+
+      const targetEpisode = episodesData.find((ep) =>
+        ep.number ? +ep.number : ep.number === episodeNumber
+      );
+
+      if (!targetEpisode?.dataId) throw new Error("Episode not found");
+
+      mediaId = targetEpisode.dataId;
+
+      sources = await proxiedFetch<any>(`/ajax/v2/episode/servers/${mediaId}`, {
+        baseURL: gomoviesBase,
+        headers: {
+          "X-Requested-With": "XMLHttpRequest",
+        },
+      });
+    } else {
+      sources = await proxiedFetch<any>(`/ajax/movie/episodes/${mediaId}`, {
+        baseURL: gomoviesBase,
+        headers: {
+          "X-Requested-With": "XMLHttpRequest",
+        },
+      });
+    }
+
+    const upcloud = new DOMParser()
+      .parseFromString(sources, "text/html")
+      .querySelector('a[title*="upcloud" i]');
+
+    const upcloudDataId =
+      upcloud?.getAttribute("data-id") ?? upcloud?.getAttribute("data-linkid");
+
+    if (!upcloudDataId) throw new Error("Upcloud source not available");
+
+    const upcloudSource = await proxiedFetch<{
+      type: "iframe" | string;
+      link: string;
+      sources: [];
+      title: string;
+      tracks: [];
+    }>(`/ajax/sources/${upcloudDataId}`, {
+      baseURL: gomoviesBase,
+      headers: {
+        "X-Requested-With": "XMLHttpRequest",
+      },
+    });
+
+    if (!upcloudSource.link || upcloudSource.type !== "iframe")
+      throw new Error("No upcloud stream found");
+
+    return {
+      embeds: [
+        {
+          type: MWEmbedType.UPCLOUD,
+          url: upcloudSource.link,
+        },
+      ],
+    };
+  },
+});

From d198760f9c0051c4cf91cab4ad2b84762e09c6ae Mon Sep 17 00:00:00 2001
From: Jordaar <69628820+Jordaar@users.noreply.github.com>
Date: Fri, 16 Jun 2023 14:37:57 +0530
Subject: [PATCH 03/12] feat(provider): add kissasian provider

---
 src/backend/providers/kissasian.ts | 103 +++++++++++++++++++++++++++++
 1 file changed, 103 insertions(+)
 create mode 100644 src/backend/providers/kissasian.ts

diff --git a/src/backend/providers/kissasian.ts b/src/backend/providers/kissasian.ts
new file mode 100644
index 00000000..01fa0a2e
--- /dev/null
+++ b/src/backend/providers/kissasian.ts
@@ -0,0 +1,103 @@
+import { MWEmbedType } from "../helpers/embed";
+import { proxiedFetch } from "../helpers/fetch";
+import { registerProvider } from "../helpers/register";
+import { MWMediaType } from "../metadata/types";
+
+const kissasianBase = "https://kissasian.li";
+
+registerProvider({
+  id: "kissasian",
+  displayName: "KissAsian",
+  rank: 10000,
+  type: [MWMediaType.MOVIE, MWMediaType.SERIES],
+
+  async scrape({ media, episode, progress }) {
+    let seasonNumber = "";
+    let episodeNumber = "";
+
+    if (media.meta.type === MWMediaType.SERIES) {
+      seasonNumber =
+        media.meta.seasonData.number === 1
+          ? ""
+          : `${media.meta.seasonData.number}`;
+      episodeNumber = `${
+        media.meta.seasonData.episodes.find((e) => e.id === episode)?.number ??
+        ""
+      }`;
+    }
+
+    const searchForm = new FormData();
+    searchForm.append("keyword", `${media.meta.title} ${seasonNumber}`.trim());
+    searchForm.append("type", "Drama");
+
+    const search = await proxiedFetch<any>("/Search/SearchSuggest", {
+      baseURL: kissasianBase,
+      method: "POST",
+      body: searchForm,
+    });
+
+    const searchPage = new DOMParser().parseFromString(search, "text/html");
+
+    const dramas = Array.from(searchPage.querySelectorAll("a")).map((drama) => {
+      return {
+        name: drama.textContent,
+        url: drama.href,
+      };
+    });
+
+    const targetDrama =
+      dramas.find(
+        (d) => d.name?.toLowerCase() === media.meta.title.toLowerCase()
+      ) ?? dramas[0];
+    if (!targetDrama) throw new Error("Drama not found");
+
+    progress(30);
+
+    const drama = await proxiedFetch<any>(targetDrama.url);
+
+    const dramaPage = new DOMParser().parseFromString(drama, "text/html");
+
+    const episodesEl = dramaPage.querySelectorAll("tbody tr:not(:first-child)");
+
+    const episodes = Array.from(episodesEl)
+      .map((ep) => {
+        const number = ep
+          ?.querySelector("td.episodeSub a")
+          ?.textContent?.split("Episode")[1]
+          ?.trim();
+        const href = ep?.querySelector("td.episodeSub a")?.getAttribute("href");
+        return { number, href };
+      })
+      .filter((e) => !!e.href);
+
+    const targetEpisode =
+      media.meta.type === MWMediaType.MOVIE
+        ? episodes[0]
+        : episodes.find((e) => e.number === `${episodeNumber}`);
+    if (!targetEpisode?.href) throw new Error("Episode not found");
+
+    progress(70);
+
+    const watch = await proxiedFetch<any>(`${targetEpisode.href}&s=sb`, {
+      baseURL: kissasianBase,
+    });
+
+    const watchPage = new DOMParser().parseFromString(watch, "text/html");
+
+    const streamsbUrl = watchPage
+      .querySelector("iframe[id=my_video_1]")
+      ?.getAttribute("src");
+    if (!streamsbUrl) throw new Error("Streamsb embed not found");
+
+    console.log(streamsbUrl);
+
+    return {
+      embeds: [
+        {
+          type: MWEmbedType.STREAMSB,
+          url: streamsbUrl,
+        },
+      ],
+    };
+  },
+});

From 2db7e0bef8ffbdf41f74481a21c2c11cc6d6dd2f Mon Sep 17 00:00:00 2001
From: Jordaar <69628820+Jordaar@users.noreply.github.com>
Date: Fri, 16 Jun 2023 14:41:30 +0530
Subject: [PATCH 04/12] feat(enum): add upcloud and streamsb enum

---
 src/backend/helpers/embed.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/backend/helpers/embed.ts b/src/backend/helpers/embed.ts
index 0dec6422..a584a0c7 100644
--- a/src/backend/helpers/embed.ts
+++ b/src/backend/helpers/embed.ts
@@ -4,6 +4,8 @@ export enum MWEmbedType {
   M4UFREE = "m4ufree",
   STREAMM4U = "streamm4u",
   PLAYM4U = "playm4u",
+  UPCLOUD = "upcloud",
+  STREAMSB = "streamsb",
 }
 
 export type MWEmbed = {

From d4c6dac9f2edecf9c186d8fed18160945e4e7122 Mon Sep 17 00:00:00 2001
From: Jordaar <69628820+Jordaar@users.noreply.github.com>
Date: Fri, 16 Jun 2023 14:43:36 +0530
Subject: [PATCH 05/12] disable 2embed

---
 src/backend/providers/2embed.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/backend/providers/2embed.ts b/src/backend/providers/2embed.ts
index 48056020..7cc8938e 100644
--- a/src/backend/providers/2embed.ts
+++ b/src/backend/providers/2embed.ts
@@ -191,6 +191,7 @@ registerProvider({
   displayName: "2Embed",
   rank: 125,
   type: [MWMediaType.MOVIE, MWMediaType.SERIES],
+  disabled: true, // Disabled, not working
   async scrape({ media, episode, progress }) {
     let embedUrl = `${twoEmbedBase}/embed/tmdb/movie?id=${media.tmdbId}`;
 

From f6b830d06df314b442ccafdfd6cc9cc303db54ad Mon Sep 17 00:00:00 2001
From: Jordaar <69628820+Jordaar@users.noreply.github.com>
Date: Fri, 16 Jun 2023 14:44:54 +0530
Subject: [PATCH 06/12] feat(register): new providers and embed scrapers

---
 src/backend/index.ts | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/backend/index.ts b/src/backend/index.ts
index eb0ad897..065f1c62 100644
--- a/src/backend/index.ts
+++ b/src/backend/index.ts
@@ -9,9 +9,13 @@ import "./providers/m4ufree";
 import "./providers/hdwatched";
 import "./providers/2embed";
 import "./providers/sflix";
+import "./providers/gomovies";
+import "./providers/kissasian";
 
 // embeds
 import "./embeds/streamm4u";
 import "./embeds/playm4u";
+import "./embeds/upcloud";
+import "./embeds/streamsb";
 
 initializeScraperStore();

From 58ca372a49ce1ae8009b1e5f33a4809ef9b4c6a4 Mon Sep 17 00:00:00 2001
From: Jordaar <69628820+Jordaar@users.noreply.github.com>
Date: Fri, 16 Jun 2023 14:52:42 +0530
Subject: [PATCH 07/12] refactor(kissasian): change rank

---
 src/backend/providers/kissasian.ts | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/backend/providers/kissasian.ts b/src/backend/providers/kissasian.ts
index 01fa0a2e..8d4e50c4 100644
--- a/src/backend/providers/kissasian.ts
+++ b/src/backend/providers/kissasian.ts
@@ -8,7 +8,7 @@ const kissasianBase = "https://kissasian.li";
 registerProvider({
   id: "kissasian",
   displayName: "KissAsian",
-  rank: 10000,
+  rank: 130,
   type: [MWMediaType.MOVIE, MWMediaType.SERIES],
 
   async scrape({ media, episode, progress }) {
@@ -89,8 +89,6 @@ registerProvider({
       ?.getAttribute("src");
     if (!streamsbUrl) throw new Error("Streamsb embed not found");
 
-    console.log(streamsbUrl);
-
     return {
       embeds: [
         {

From e912ea4715035b9305a22cbabad3db0f5a4795e3 Mon Sep 17 00:00:00 2001
From: Jordaar <69628820+Jordaar@users.noreply.github.com>
Date: Fri, 16 Jun 2023 15:05:42 +0530
Subject: [PATCH 08/12] cleanup

---
 src/backend/embeds/streamsb.ts | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/backend/embeds/streamsb.ts b/src/backend/embeds/streamsb.ts
index c755a0b0..99ed563e 100644
--- a/src/backend/embeds/streamsb.ts
+++ b/src/backend/embeds/streamsb.ts
@@ -169,8 +169,6 @@ registerEmbedScraper({
           .querySelector(".btn.btn-light.btn-lg")
           ?.getAttribute("href");
 
-        console.log(dlLink);
-
         return {
           quality: dl.quality.label as MWStreamQuality,
           url: dlLink,

From 9003bf67887e4750e47a093db1a2b7a3ca8e4703 Mon Sep 17 00:00:00 2001
From: Jordaar <69628820+Jordaar@users.noreply.github.com>
Date: Fri, 16 Jun 2023 16:12:07 +0530
Subject: [PATCH 09/12] feat(embed): add mp4upload embed scraper

---
 src/backend/embeds/mp4upload.ts | 32 ++++++++++++++++++++++++++++++++
 src/backend/index.ts            |  1 +
 2 files changed, 33 insertions(+)
 create mode 100644 src/backend/embeds/mp4upload.ts

diff --git a/src/backend/embeds/mp4upload.ts b/src/backend/embeds/mp4upload.ts
new file mode 100644
index 00000000..3902e20b
--- /dev/null
+++ b/src/backend/embeds/mp4upload.ts
@@ -0,0 +1,32 @@
+import { MWEmbedType } from "@/backend/helpers/embed";
+import { registerEmbedScraper } from "@/backend/helpers/register";
+import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams";
+
+import { proxiedFetch } from "../helpers/fetch";
+
+registerEmbedScraper({
+  id: "mp4upload",
+  displayName: "mp4upload",
+  for: MWEmbedType.MP4UPLOAD,
+  rank: 170,
+  async getStream({ url }) {
+    const embed = await proxiedFetch<any>(url);
+
+    const playerSrcRegex =
+      /(?<=player\.src\()\s*{\s*type:\s*"[^"]+",\s*src:\s*"([^"]+)"\s*}\s*(?=\);)/s;
+
+    const playerSrc = embed.match(playerSrcRegex);
+
+    const streamUrl = playerSrc[1];
+
+    if (!streamUrl) throw new Error("Stream url not found");
+
+    return {
+      embedId: MWEmbedType.MP4UPLOAD,
+      streamUrl,
+      quality: MWStreamQuality.Q1080P,
+      captions: [],
+      type: MWStreamType.MP4,
+    };
+  },
+});
diff --git a/src/backend/index.ts b/src/backend/index.ts
index 065f1c62..a2beaa2a 100644
--- a/src/backend/index.ts
+++ b/src/backend/index.ts
@@ -17,5 +17,6 @@ import "./embeds/streamm4u";
 import "./embeds/playm4u";
 import "./embeds/upcloud";
 import "./embeds/streamsb";
+import "./embeds/mp4upload";
 
 initializeScraperStore();

From 7e948c60c1473dd4bda958add89c62dacdef74a3 Mon Sep 17 00:00:00 2001
From: Jordaar <69628820+Jordaar@users.noreply.github.com>
Date: Fri, 16 Jun 2023 16:12:53 +0530
Subject: [PATCH 10/12] feat(enum): add mp4upload enum

---
 src/backend/helpers/embed.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/backend/helpers/embed.ts b/src/backend/helpers/embed.ts
index a584a0c7..1ec3362c 100644
--- a/src/backend/helpers/embed.ts
+++ b/src/backend/helpers/embed.ts
@@ -6,6 +6,7 @@ export enum MWEmbedType {
   PLAYM4U = "playm4u",
   UPCLOUD = "upcloud",
   STREAMSB = "streamsb",
+  MP4UPLOAD = "mp4upload",
 }
 
 export type MWEmbed = {

From a0bb03790a37fad2eed1ab15f4ada9814a84dc45 Mon Sep 17 00:00:00 2001
From: Jordaar <69628820+Jordaar@users.noreply.github.com>
Date: Fri, 16 Jun 2023 16:14:05 +0530
Subject: [PATCH 11/12] refactor(streamsb): improve quality sorting

---
 src/backend/embeds/streamsb.ts | 13 +++++++------
 1 file changed, 7 insertions(+), 6 deletions(-)

diff --git a/src/backend/embeds/streamsb.ts b/src/backend/embeds/streamsb.ts
index 99ed563e..e91b43c7 100644
--- a/src/backend/embeds/streamsb.ts
+++ b/src/backend/embeds/streamsb.ts
@@ -100,7 +100,7 @@ registerEmbedScraper({
       "[onclick^=download_video]"
     );
 
-    const dlDetails = [];
+    let dlDetails = [];
     for (const func of downloadVideoFunctions) {
       const funcContents = func.getAttribute("onclick");
       const regExpFunc = /download_video\('(.+?)','(.+?)','(.+?)'\)/;
@@ -121,6 +121,12 @@ registerEmbedScraper({
       }
     }
 
+    dlDetails = dlDetails.sort((a, b) => {
+      const aQuality = qualityOrder.indexOf(a.quality.label as MWStreamQuality);
+      const bQuality = qualityOrder.indexOf(b.quality.label as MWStreamQuality);
+      return aQuality - bQuality;
+    });
+
     progress(40);
 
     let dls = await Promise.all(
@@ -187,11 +193,6 @@ registerEmbedScraper({
       })
     );
     dls = dls.filter((d) => !!d.url);
-    dls = dls.sort((a, b) => {
-      const aQuality = qualityOrder.indexOf(a.quality);
-      const bQuality = qualityOrder.indexOf(b.quality);
-      return aQuality - bQuality;
-    });
 
     progress(60);
 

From bc0f9a6abff9ab90b7106456f852de9fffbf8276 Mon Sep 17 00:00:00 2001
From: Jordaar <69628820+Jordaar@users.noreply.github.com>
Date: Fri, 16 Jun 2023 16:15:41 +0530
Subject: [PATCH 12/12] feat(kissasian): additional mp4upload embed scraper

---
 src/backend/providers/kissasian.ts | 54 ++++++++++++++++++++----------
 1 file changed, 36 insertions(+), 18 deletions(-)

diff --git a/src/backend/providers/kissasian.ts b/src/backend/providers/kissasian.ts
index 8d4e50c4..90708970 100644
--- a/src/backend/providers/kissasian.ts
+++ b/src/backend/providers/kissasian.ts
@@ -5,6 +5,17 @@ import { MWMediaType } from "../metadata/types";
 
 const kissasianBase = "https://kissasian.li";
 
+const embedProviders = [
+  {
+    type: MWEmbedType.MP4UPLOAD,
+    id: "mp",
+  },
+  {
+    type: MWEmbedType.STREAMSB,
+    id: "sb",
+  },
+];
+
 registerProvider({
   id: "kissasian",
   displayName: "KissAsian",
@@ -65,37 +76,44 @@ registerProvider({
           ?.querySelector("td.episodeSub a")
           ?.textContent?.split("Episode")[1]
           ?.trim();
-        const href = ep?.querySelector("td.episodeSub a")?.getAttribute("href");
-        return { number, href };
+        const url = ep?.querySelector("td.episodeSub a")?.getAttribute("href");
+        return { number, url };
       })
-      .filter((e) => !!e.href);
+      .filter((e) => !!e.url);
 
     const targetEpisode =
       media.meta.type === MWMediaType.MOVIE
         ? episodes[0]
         : episodes.find((e) => e.number === `${episodeNumber}`);
-    if (!targetEpisode?.href) throw new Error("Episode not found");
+    if (!targetEpisode?.url) throw new Error("Episode not found");
 
     progress(70);
 
-    const watch = await proxiedFetch<any>(`${targetEpisode.href}&s=sb`, {
-      baseURL: kissasianBase,
-    });
+    let embeds = await Promise.all(
+      embedProviders.map(async (provider) => {
+        const watch = await proxiedFetch<any>(
+          `${targetEpisode.url}&s=${provider.id}`,
+          {
+            baseURL: kissasianBase,
+          }
+        );
 
-    const watchPage = new DOMParser().parseFromString(watch, "text/html");
+        const watchPage = new DOMParser().parseFromString(watch, "text/html");
 
-    const streamsbUrl = watchPage
-      .querySelector("iframe[id=my_video_1]")
-      ?.getAttribute("src");
-    if (!streamsbUrl) throw new Error("Streamsb embed not found");
+        const embedUrl = watchPage
+          .querySelector("iframe[id=my_video_1]")
+          ?.getAttribute("src");
+
+        return {
+          type: provider.type,
+          url: embedUrl ?? "",
+        };
+      })
+    );
+    embeds = embeds.filter((e) => e.url !== "");
 
     return {
-      embeds: [
-        {
-          type: MWEmbedType.STREAMSB,
-          url: streamsbUrl,
-        },
-      ],
+      embeds,
     };
   },
 });