From 65d46190e6ca9f259ed972483fff602308f3a9bc Mon Sep 17 00:00:00 2001
From: mrjvs <jellevs@gmail.com>
Date: Tue, 17 Oct 2023 19:11:53 +0200
Subject: [PATCH] remove backend from this repo

---
 .vscode/settings.json                      |   5 +-
 src/__tests__/providers/providers.test.ts  |  52 --
 src/__tests__/providers/testdata.ts        |  45 --
 src/__tests__/subtitles/subtitles.test.ts  | 152 -----
 src/__tests__/subtitles/testdata.ts        |  68 ---
 src/backend/embeds/.gitkeep                |   1 -
 src/backend/embeds/mp4upload.ts            |  32 -
 src/backend/embeds/playm4u.ts              |  20 -
 src/backend/embeds/streamm4u.ts            |  66 --
 src/backend/embeds/streamsb.ts             | 211 -------
 src/backend/embeds/upcloud.ts              | 101 ---
 src/backend/helpers/captions.ts            |  62 --
 src/backend/helpers/embed.ts               |  30 -
 src/backend/helpers/provider.ts            |  36 --
 src/backend/helpers/register.ts            |  72 ---
 src/backend/helpers/run.ts                 |  52 --
 src/backend/helpers/scrape.ts              | 173 ------
 src/backend/helpers/streams.ts             |  46 --
 src/backend/index.ts                       |  24 -
 src/backend/providers/2embed.ts            | 252 --------
 src/backend/providers/flixhq/common.ts     |   1 -
 src/backend/providers/flixhq/index.ts      |  36 --
 src/backend/providers/flixhq/scrape.ts     |  41 --
 src/backend/providers/flixhq/search.ts     |  43 --
 src/backend/providers/gdriveplayer.ts      | 107 ----
 src/backend/providers/gomovies.ts          | 162 -----
 src/backend/providers/hdwatched.ts         | 198 ------
 src/backend/providers/kissasian.ts         | 119 ----
 src/backend/providers/m4ufree.ts           | 236 -------
 src/backend/providers/netfilm.ts           | 154 -----
 src/backend/providers/remotestream.ts      |  49 --
 src/backend/providers/sflix.ts             | 100 ---
 src/backend/providers/streamflix.ts        |  70 ---
 src/backend/providers/superstream/LICENSE  | 680 ---------------------
 src/backend/providers/superstream/index.ts | 270 --------
 src/components/player/hooks/usePlayer.ts   |   3 +-
 src/index.tsx                              |   1 -
 src/pages/developer/EmbedTesterView.tsx    | 137 -----
 src/pages/developer/ProviderTesterView.tsx | 119 ----
 src/setup/App.tsx                          |  29 +-
 src/stores/player/types.ts                 |   2 -
 41 files changed, 7 insertions(+), 4050 deletions(-)
 delete mode 100644 src/__tests__/providers/providers.test.ts
 delete mode 100644 src/__tests__/providers/testdata.ts
 delete mode 100644 src/__tests__/subtitles/subtitles.test.ts
 delete mode 100644 src/__tests__/subtitles/testdata.ts
 delete mode 100644 src/backend/embeds/.gitkeep
 delete mode 100644 src/backend/embeds/mp4upload.ts
 delete mode 100644 src/backend/embeds/playm4u.ts
 delete mode 100644 src/backend/embeds/streamm4u.ts
 delete mode 100644 src/backend/embeds/streamsb.ts
 delete mode 100644 src/backend/embeds/upcloud.ts
 delete mode 100644 src/backend/helpers/captions.ts
 delete mode 100644 src/backend/helpers/embed.ts
 delete mode 100644 src/backend/helpers/provider.ts
 delete mode 100644 src/backend/helpers/register.ts
 delete mode 100644 src/backend/helpers/run.ts
 delete mode 100644 src/backend/helpers/scrape.ts
 delete mode 100644 src/backend/helpers/streams.ts
 delete mode 100644 src/backend/index.ts
 delete mode 100644 src/backend/providers/2embed.ts
 delete mode 100644 src/backend/providers/flixhq/common.ts
 delete mode 100644 src/backend/providers/flixhq/index.ts
 delete mode 100644 src/backend/providers/flixhq/scrape.ts
 delete mode 100644 src/backend/providers/flixhq/search.ts
 delete mode 100644 src/backend/providers/gdriveplayer.ts
 delete mode 100644 src/backend/providers/gomovies.ts
 delete mode 100644 src/backend/providers/hdwatched.ts
 delete mode 100644 src/backend/providers/kissasian.ts
 delete mode 100644 src/backend/providers/m4ufree.ts
 delete mode 100644 src/backend/providers/netfilm.ts
 delete mode 100644 src/backend/providers/remotestream.ts
 delete mode 100644 src/backend/providers/sflix.ts
 delete mode 100644 src/backend/providers/streamflix.ts
 delete mode 100644 src/backend/providers/superstream/LICENSE
 delete mode 100644 src/backend/providers/superstream/index.ts
 delete mode 100644 src/pages/developer/EmbedTesterView.tsx
 delete mode 100644 src/pages/developer/ProviderTesterView.tsx

diff --git a/.vscode/settings.json b/.vscode/settings.json
index 2f5fdb18..279011fe 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -4,8 +4,5 @@
   "eslint.format.enable": true,
   "[json]": {
     "editor.defaultFormatter": "esbenp.prettier-vscode"
-  },
-  "[typescriptreact]": {
-    "editor.defaultFormatter": "ms-vsliveshare.vsliveshare"
   }
-}
\ No newline at end of file
+}
diff --git a/src/__tests__/providers/providers.test.ts b/src/__tests__/providers/providers.test.ts
deleted file mode 100644
index 350d4255..00000000
--- a/src/__tests__/providers/providers.test.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { describe, it } from "vitest";
-
-import "@/backend";
-import { testData } from "@/__tests__/providers/testdata";
-import { getProviders } from "@/backend/helpers/register";
-import { runProvider } from "@/backend/helpers/run";
-import { MWMediaType } from "@/backend/metadata/types/mw";
-
-describe("providers", () => {
-  const providers = getProviders();
-
-  it("have at least one provider", ({ expect }) => {
-    expect(providers.length).toBeGreaterThan(0);
-  });
-
-  for (const provider of providers) {
-    describe(provider.displayName, () => {
-      it("must have at least one type", async ({ expect }) => {
-        expect(provider.type.length).toBeGreaterThan(0);
-      });
-
-      if (provider.type.includes(MWMediaType.MOVIE)) {
-        it("must work with movies", async ({ expect }) => {
-          const movie = testData.find((v) => v.meta.type === MWMediaType.MOVIE);
-          if (!movie) throw new Error("no movie to test with");
-          const results = await runProvider(provider, {
-            media: movie,
-            progress() {},
-            type: movie.meta.type as any,
-          });
-          expect(results).toBeTruthy();
-        });
-      }
-
-      if (provider.type.includes(MWMediaType.SERIES)) {
-        it("must work with series", async ({ expect }) => {
-          const show = testData.find((v) => v.meta.type === MWMediaType.SERIES);
-          if (show?.meta.type !== MWMediaType.SERIES)
-            throw new Error("no show to test with");
-          const results = await runProvider(provider, {
-            media: show,
-            progress() {},
-            type: show.meta.type as MWMediaType.SERIES,
-            episode: show.meta.seasonData.episodes[0].id,
-            season: show.meta.seasons[0].id,
-          });
-          expect(results).toBeTruthy();
-        });
-      }
-    });
-  }
-});
diff --git a/src/__tests__/providers/testdata.ts b/src/__tests__/providers/testdata.ts
deleted file mode 100644
index 6db686e3..00000000
--- a/src/__tests__/providers/testdata.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { DetailedMeta } from "@/backend/metadata/getmeta";
-import { MWMediaType } from "@/backend/metadata/types/mw";
-
-export const testData: DetailedMeta[] = [
-  {
-    imdbId: "tt10954562",
-    tmdbId: "572716",
-    meta: {
-      id: "439596",
-      title: "Hamilton",
-      type: MWMediaType.MOVIE,
-      year: "2020",
-      seasons: undefined,
-    },
-  },
-  {
-    imdbId: "tt11126994",
-    tmdbId: "94605",
-    meta: {
-      id: "222333",
-      title: "Arcane",
-      type: MWMediaType.SERIES,
-      year: "2021",
-      seasons: [
-        {
-          id: "230301",
-          number: 1,
-          title: "Season 1",
-        },
-      ],
-      seasonData: {
-        id: "230301",
-        number: 1,
-        title: "Season 1",
-        episodes: [
-          {
-            id: "4243445",
-            number: 1,
-            title: "Welcome to the Playground",
-          },
-        ],
-      },
-    },
-  },
-];
diff --git a/src/__tests__/subtitles/subtitles.test.ts b/src/__tests__/subtitles/subtitles.test.ts
deleted file mode 100644
index 69934f8f..00000000
--- a/src/__tests__/subtitles/subtitles.test.ts
+++ /dev/null
@@ -1,152 +0,0 @@
-import { describe, it } from "vitest";
-
-import {
-  getMWCaptionTypeFromUrl,
-  isSupportedSubtitle,
-  parseSubtitles,
-} from "@/backend/helpers/captions";
-import { MWCaptionType } from "@/backend/helpers/streams";
-
-import {
-  ass,
-  multilineSubtitlesTestVtt,
-  srt,
-  visibleSubtitlesTestVtt,
-  vtt,
-} from "./testdata";
-
-describe("subtitles", () => {
-  it("should return true if given url ends with a known subtitle type", ({
-    expect,
-  }) => {
-    expect(isSupportedSubtitle("https://example.com/test.srt")).toBe(true);
-    expect(isSupportedSubtitle("https://example.com/test.vtt")).toBe(true);
-    expect(isSupportedSubtitle("https://example.com/test.txt")).toBe(false);
-  });
-
-  it("should return corresponding MWCaptionType", ({ expect }) => {
-    expect(getMWCaptionTypeFromUrl("https://example.com/test.srt")).toBe(
-      MWCaptionType.SRT
-    );
-    expect(getMWCaptionTypeFromUrl("https://example.com/test.vtt")).toBe(
-      MWCaptionType.VTT
-    );
-    expect(getMWCaptionTypeFromUrl("https://example.com/test.txt")).toBe(
-      MWCaptionType.UNKNOWN
-    );
-  });
-
-  it("should throw when empty text is given", ({ expect }) => {
-    expect(() => parseSubtitles("")).toThrow("Given text is empty");
-  });
-
-  it("should parse srt", ({ expect }) => {
-    const parsed = parseSubtitles(srt);
-    const parsedSrt = [
-      {
-        type: "caption",
-        index: 1,
-        start: 0,
-        end: 0,
-        duration: 0,
-        content: "Test",
-        text: "Test",
-      },
-      {
-        type: "caption",
-        index: 2,
-        start: 0,
-        end: 0,
-        duration: 0,
-        content: "Test",
-        text: "Test",
-      },
-    ];
-    expect(parsed).toHaveLength(2);
-    expect(parsed).toEqual(parsedSrt);
-  });
-
-  it("should parse vtt", ({ expect }) => {
-    const parsed = parseSubtitles(vtt);
-    const parsedVtt = [
-      {
-        type: "caption",
-        index: 1,
-        start: 0,
-        end: 4000,
-        duration: 4000,
-        content: "Where did he go?",
-        text: "Where did he go?",
-      },
-      {
-        type: "caption",
-        index: 2,
-        start: 3000,
-        end: 6500,
-        duration: 3500,
-        content: "I think he went down this lane.",
-        text: "I think he went down this lane.",
-      },
-      {
-        type: "caption",
-        index: 3,
-        start: 4000,
-        end: 6500,
-        duration: 2500,
-        content: "What are you waiting for?",
-        text: "What are you waiting for?",
-      },
-    ];
-    expect(parsed).toHaveLength(3);
-    expect(parsed).toEqual(parsedVtt);
-  });
-
-  it("should parse ass", ({ expect }) => {
-    const parsed = parseSubtitles(ass);
-    expect(parsed).toHaveLength(3);
-  });
-
-  it("should delay subtitles when given a delay", ({ expect }) => {
-    const videoTime = 11;
-    let delayedSeconds = 0;
-    const parsed = parseSubtitles(visibleSubtitlesTestVtt);
-    const isVisible = (start: number, end: number, delay: number): boolean => {
-      const delayedStart = start / 1000 + delay;
-      const delayedEnd = end / 1000 + delay;
-      return (
-        Math.max(0, delayedStart) <= videoTime &&
-        Math.max(0, delayedEnd) >= videoTime
-      );
-    };
-    const visibleSubtitles = parsed.filter((c) =>
-      isVisible(c.start, c.end, delayedSeconds)
-    );
-    expect(visibleSubtitles).toHaveLength(1);
-
-    delayedSeconds = 10;
-    const delayedVisibleSubtitles = parsed.filter((c) =>
-      isVisible(c.start, c.end, delayedSeconds)
-    );
-    expect(delayedVisibleSubtitles).toHaveLength(1);
-
-    delayedSeconds = -10;
-    const delayedVisibleSubtitles2 = parsed.filter((c) =>
-      isVisible(c.start, c.end, delayedSeconds)
-    );
-    expect(delayedVisibleSubtitles2).toHaveLength(1);
-
-    delayedSeconds = -20;
-    const delayedVisibleSubtitles3 = parsed.filter((c) =>
-      isVisible(c.start, c.end, delayedSeconds)
-    );
-    expect(delayedVisibleSubtitles3).toHaveLength(1);
-  });
-
-  it("should parse multiline captions", ({ expect }) => {
-    const parsed = parseSubtitles(multilineSubtitlesTestVtt);
-
-    expect(parsed[0].text).toBe(`- Test 1\n- Test 2\n- Test 3`);
-    expect(parsed[1].text).toBe(`- Test 4`);
-    expect(parsed[2].text).toBe(`- Test 6`);
-  });
-});
diff --git a/src/__tests__/subtitles/testdata.ts b/src/__tests__/subtitles/testdata.ts
deleted file mode 100644
index 2cf71004..00000000
--- a/src/__tests__/subtitles/testdata.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-const srt = `
-1
-00:00:00,000 --> 00:00:00,000
-Test
-
-2
-00:00:00,000 --> 00:00:00,000
-Test
-`;
-const vtt = `
-WEBVTT
-
-00:00:00.000 --> 00:00:04.000 position:10%,line-left align:left size:35%
-Where did he go?
-
-00:00:03.000 --> 00:00:06.500 position:90% align:right size:35%
-I think he went down this lane.
-
-00:00:04.000 --> 00:00:06.500 position:45%,line-right align:center size:35%
-What are you waiting for?
-`;
-const ass = `[Script Info]
-; Generated by Ebby.co
-Title: 
-Original Script: 
-ScriptType: v4.00+
-Collisions: Normal
-PlayResX: 384
-PlayResY: 288
-PlayDepth: 0
-Timer: 100.0
-WrapStyle: 0
-
-[v4+ Styles]
-Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
-Style: Default, Arial, 16, &H00FFFFFF, &H00000000, &H00000000, &H00000000, 0, 0, 0, 0, 100, 100, 0, 0, 1, 1, 0, 2, 15, 15, 15, 0
-
-[Events]
-Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
-Dialogue: 0,0:00:10.00,0:00:20.00,Default,,0000,0000,0000,,This is the first subtitle.
-Dialogue: 0,0:00:30.00,0:00:34.00,Default,,0000,0000,0000,,This is the second.
-Dialogue: 0,0:00:34.00,0:00:35.00,Default,,0000,0000,0000,,Third`;
-
-const visibleSubtitlesTestVtt = `WEBVTT
-
-00:00:00.000 --> 00:00:10.000 position:10%,line-left align:left size:35%
-Test 1
-
-00:00:10.000 --> 00:00:20.000 position:90% align:right size:35%
-Test 2
-
-00:00:20.000 --> 00:00:31.000 position:45%,line-right align:center size:35%
-Test 3
-`;
-
-const multilineSubtitlesTestVtt = `WEBVTT
-
-00:00:00.000 --> 00:00:10.000
-- Test 1\n- Test 2\n- Test 3
-
-00:00:10.000 --> 00:00:20.000
-- Test 4
-
-00:00:20.000 --> 00:00:31.000
-- Test 6
-`;
-
-export { vtt, srt, ass, visibleSubtitlesTestVtt, multilineSubtitlesTestVtt };
diff --git a/src/backend/embeds/.gitkeep b/src/backend/embeds/.gitkeep
deleted file mode 100644
index f42d5aa9..00000000
--- a/src/backend/embeds/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-embed scrapers go here
diff --git a/src/backend/embeds/mp4upload.ts b/src/backend/embeds/mp4upload.ts
deleted file mode 100644
index 3902e20b..00000000
--- a/src/backend/embeds/mp4upload.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-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/embeds/playm4u.ts b/src/backend/embeds/playm4u.ts
deleted file mode 100644
index 1e5c3ca4..00000000
--- a/src/backend/embeds/playm4u.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { MWEmbedType } from "@/backend/helpers/embed";
-import { registerEmbedScraper } from "@/backend/helpers/register";
-import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams";
-
-registerEmbedScraper({
-  id: "playm4u",
-  displayName: "playm4u",
-  for: MWEmbedType.PLAYM4U,
-  rank: 0,
-  async getStream() {
-    // throw new Error("Oh well 2")
-    return {
-      embedId: "",
-      streamUrl: "",
-      quality: MWStreamQuality.Q1080P,
-      captions: [],
-      type: MWStreamType.MP4,
-    };
-  },
-});
diff --git a/src/backend/embeds/streamm4u.ts b/src/backend/embeds/streamm4u.ts
deleted file mode 100644
index abdc1486..00000000
--- a/src/backend/embeds/streamm4u.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import { MWEmbedType } from "@/backend/helpers/embed";
-import { proxiedFetch } from "@/backend/helpers/fetch";
-import { registerEmbedScraper } from "@/backend/helpers/register";
-import {
-  MWEmbedStream,
-  MWStreamQuality,
-  MWStreamType,
-} from "@/backend/helpers/streams";
-
-const HOST = "streamm4u.club";
-const URL_BASE = `https://${HOST}`;
-const URL_API = `${URL_BASE}/api`;
-const URL_API_SOURCE = `${URL_API}/source`;
-
-async function scrape(embed: string) {
-  const sources: MWEmbedStream[] = [];
-
-  const embedID = embed.split("/").pop();
-
-  console.log(`${URL_API_SOURCE}/${embedID}`);
-  const json = await proxiedFetch<any>(`${URL_API_SOURCE}/${embedID}`, {
-    method: "POST",
-    body: `r=&d=${HOST}`,
-  });
-
-  if (json.success) {
-    const streams = json.data;
-
-    for (const stream of streams) {
-      sources.push({
-        embedId: "",
-        streamUrl: stream.file as string,
-        quality: stream.label as MWStreamQuality,
-        type: stream.type as MWStreamType,
-        captions: [],
-      });
-    }
-  }
-
-  return sources;
-}
-
-// TODO check out 403 / 404 on successfully returned video stream URLs
-registerEmbedScraper({
-  id: "streamm4u",
-  displayName: "streamm4u",
-  for: MWEmbedType.STREAMM4U,
-  rank: 100,
-  async getStream({ progress, url }) {
-    // const scrapingThreads = [];
-    // const streams = [];
-
-    const sources = (await scrape(url)).sort(
-      (a, b) =>
-        Number(b.quality.replace("p", "")) - Number(a.quality.replace("p", ""))
-    );
-    // const preferredSourceIndex = 0;
-    const preferredSource = sources[0];
-
-    if (!preferredSource) throw new Error("No source found");
-
-    progress(100);
-
-    return preferredSource;
-  },
-});
diff --git a/src/backend/embeds/streamsb.ts b/src/backend/embeds/streamsb.ts
deleted file mode 100644
index e91b43c7..00000000
--- a/src/backend/embeds/streamsb.ts
+++ /dev/null
@@ -1,211 +0,0 @@
-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]"
-    );
-
-    let 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],
-            },
-          });
-        }
-      }
-    }
-
-    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(
-      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");
-
-        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);
-
-    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
deleted file mode 100644
index 4bac2b94..00000000
--- a/src/backend/embeds/upcloud.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-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 } | null = null;
-
-    if (!isJSON(streamRes.sources)) {
-      const decryptionKey = JSON.parse(
-        await proxiedFetch<string>(
-          `https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt`
-        )
-      ) as [number, number][];
-
-      let extractedKey = "";
-      const sourcesArray = streamRes.sources.split("");
-      for (const index of decryptionKey) {
-        for (let i: number = index[0]; i < index[1]; i += 1) {
-          extractedKey += streamRes.sources[i];
-          sourcesArray[i] = "";
-        }
-      }
-
-      const decryptedStream = AES.decrypt(
-        sourcesArray.join(""),
-        extractedKey
-      ).toString(enc.Utf8);
-      const parsedStream = JSON.parse(decryptedStream)[0];
-      if (!parsedStream) throw new Error("No stream found");
-      sources = parsedStream;
-    }
-
-    if (!sources) throw new Error("upcloud source not found");
-
-    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,
-          };
-        }),
-    };
-  },
-});
diff --git a/src/backend/helpers/captions.ts b/src/backend/helpers/captions.ts
deleted file mode 100644
index cafd633a..00000000
--- a/src/backend/helpers/captions.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import DOMPurify from "dompurify";
-import { convert, detect, list, parse } from "subsrt-ts";
-import { ContentCaption } from "subsrt-ts/dist/types/handler";
-
-import { mwFetch, proxiedFetch } from "@/backend/helpers/fetch";
-import { MWCaption, MWCaptionType } from "@/backend/helpers/streams";
-
-export const customCaption = "external-custom";
-export function makeCaptionId(caption: MWCaption, isLinked: boolean): string {
-  return isLinked ? `linked-${caption.langIso}` : `external-${caption.langIso}`;
-}
-export const subtitleTypeList = list().map((type) => `.${type}`);
-export function isSupportedSubtitle(url: string): boolean {
-  return subtitleTypeList.some((type) => url.endsWith(type));
-}
-
-export function getMWCaptionTypeFromUrl(url: string): MWCaptionType {
-  if (!isSupportedSubtitle(url)) return MWCaptionType.UNKNOWN;
-  const type = subtitleTypeList.find((t) => url.endsWith(t));
-  if (!type) return MWCaptionType.UNKNOWN;
-  return type.slice(1) as MWCaptionType;
-}
-
-export const sanitize = DOMPurify.sanitize;
-export async function getCaptionUrl(caption: MWCaption): Promise<string> {
-  let captionBlob: Blob;
-  if (caption.url.startsWith("blob:")) {
-    // custom subtitle
-    captionBlob = await (await fetch(caption.url)).blob();
-  } else if (caption.needsProxy) {
-    captionBlob = await proxiedFetch<Blob>(caption.url, {
-      responseType: "blob" as any,
-    });
-  } else {
-    captionBlob = await mwFetch<Blob>(caption.url, {
-      responseType: "blob" as any,
-    });
-  }
-  // convert to vtt for track element source which will be used in PiP mode
-  const text = await captionBlob.text();
-  const vtt = convert(text, "vtt");
-  return URL.createObjectURL(new Blob([vtt], { type: "text/vtt" }));
-}
-
-export function revokeCaptionBlob(url: string | undefined) {
-  if (url && url.startsWith("blob:")) {
-    URL.revokeObjectURL(url);
-  }
-}
-
-export function parseSubtitles(text: string): ContentCaption[] {
-  const textTrimmed = text.trim();
-  if (textTrimmed === "") {
-    throw new Error("Given text is empty");
-  }
-  if (detect(textTrimmed) === "") {
-    throw new Error("Invalid subtitle format");
-  }
-  return parse(textTrimmed).filter(
-    (cue) => cue.type === "caption"
-  ) as ContentCaption[];
-}
diff --git a/src/backend/helpers/embed.ts b/src/backend/helpers/embed.ts
deleted file mode 100644
index 1ec3362c..00000000
--- a/src/backend/helpers/embed.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { MWEmbedStream } from "./streams";
-
-export enum MWEmbedType {
-  M4UFREE = "m4ufree",
-  STREAMM4U = "streamm4u",
-  PLAYM4U = "playm4u",
-  UPCLOUD = "upcloud",
-  STREAMSB = "streamsb",
-  MP4UPLOAD = "mp4upload",
-}
-
-export type MWEmbed = {
-  type: MWEmbedType;
-  url: string;
-};
-
-export type MWEmbedContext = {
-  progress(percentage: number): void;
-  url: string;
-};
-
-export type MWEmbedScraper = {
-  id: string;
-  displayName: string;
-  for: MWEmbedType;
-  rank: number;
-  disabled?: boolean;
-
-  getStream(ctx: MWEmbedContext): Promise<MWEmbedStream>;
-};
diff --git a/src/backend/helpers/provider.ts b/src/backend/helpers/provider.ts
deleted file mode 100644
index 58dea7d4..00000000
--- a/src/backend/helpers/provider.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { MWEmbed } from "./embed";
-import { MWStream } from "./streams";
-import { DetailedMeta } from "../metadata/getmeta";
-import { MWMediaType } from "../metadata/types/mw";
-
-export type MWProviderScrapeResult = {
-  stream?: MWStream;
-  embeds: MWEmbed[];
-};
-
-type MWProviderBase = {
-  progress(percentage: number): void;
-  media: DetailedMeta;
-};
-type MWProviderTypeSpecific =
-  | {
-      type: MWMediaType.MOVIE | MWMediaType.ANIME;
-      episode?: undefined;
-      season?: undefined;
-    }
-  | {
-      type: MWMediaType.SERIES;
-      episode: string;
-      season: string;
-    };
-export type MWProviderContext = MWProviderTypeSpecific & MWProviderBase;
-
-export type MWProvider = {
-  id: string;
-  displayName: string;
-  rank: number;
-  disabled?: boolean;
-  type: MWMediaType[];
-
-  scrape(ctx: MWProviderContext): Promise<MWProviderScrapeResult>;
-};
diff --git a/src/backend/helpers/register.ts b/src/backend/helpers/register.ts
deleted file mode 100644
index 9d3f76c2..00000000
--- a/src/backend/helpers/register.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import { MWEmbedScraper, MWEmbedType } from "./embed";
-import { MWProvider } from "./provider";
-
-let providers: MWProvider[] = [];
-let embeds: MWEmbedScraper[] = [];
-
-export function registerProvider(provider: MWProvider) {
-  if (provider.disabled) return;
-  providers.push(provider);
-}
-export function registerEmbedScraper(embed: MWEmbedScraper) {
-  if (embed.disabled) return;
-  embeds.push(embed);
-}
-
-export function initializeScraperStore() {
-  // sort by ranking
-  providers = providers.sort((a, b) => b.rank - a.rank);
-  embeds = embeds.sort((a, b) => b.rank - a.rank);
-
-  // check for invalid ranks
-  let lastRank: null | number = null;
-  providers.forEach((v) => {
-    if (lastRank === null) {
-      lastRank = v.rank;
-      return;
-    }
-    if (lastRank === v.rank)
-      throw new Error(`Duplicate rank number for provider ${v.id}`);
-    lastRank = v.rank;
-  });
-  lastRank = null;
-  providers.forEach((v) => {
-    if (lastRank === null) {
-      lastRank = v.rank;
-      return;
-    }
-    if (lastRank === v.rank)
-      throw new Error(`Duplicate rank number for embed scraper ${v.id}`);
-    lastRank = v.rank;
-  });
-
-  // check for duplicate ids
-  const providerIds = providers.map((v) => v.id);
-  if (
-    providerIds.length > 0 &&
-    new Set(providerIds).size !== providerIds.length
-  )
-    throw new Error("Duplicate IDS in providers");
-  const embedIds = embeds.map((v) => v.id);
-  if (embedIds.length > 0 && new Set(embedIds).size !== embedIds.length)
-    throw new Error("Duplicate IDS in embed scrapers");
-
-  // check for duplicate embed types
-  const embedTypes = embeds.map((v) => v.for);
-  if (embedTypes.length > 0 && new Set(embedTypes).size !== embedTypes.length)
-    throw new Error("Duplicate types in embed scrapers");
-}
-
-export function getProviders(): MWProvider[] {
-  return providers;
-}
-
-export function getEmbeds(): MWEmbedScraper[] {
-  return embeds;
-}
-
-export function getEmbedScraperByType(
-  type: MWEmbedType
-): MWEmbedScraper | null {
-  return getEmbeds().find((v) => v.for === type) ?? null;
-}
diff --git a/src/backend/helpers/run.ts b/src/backend/helpers/run.ts
deleted file mode 100644
index f2f9bc9c..00000000
--- a/src/backend/helpers/run.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { MWEmbed, MWEmbedContext, MWEmbedScraper } from "./embed";
-import {
-  MWProvider,
-  MWProviderContext,
-  MWProviderScrapeResult,
-} from "./provider";
-import { getEmbedScraperByType } from "./register";
-import { MWStream } from "./streams";
-
-function sortProviderResult(
-  ctx: MWProviderScrapeResult
-): MWProviderScrapeResult {
-  ctx.embeds = ctx.embeds
-    .map<[MWEmbed, MWEmbedScraper | null]>((v) => [
-      v,
-      v.type ? getEmbedScraperByType(v.type) : null,
-    ])
-    .sort(([, a], [, b]) => (b?.rank ?? 0) - (a?.rank ?? 0))
-    .map((v) => v[0]);
-  return ctx;
-}
-
-export async function runProvider(
-  provider: MWProvider,
-  ctx: MWProviderContext
-): Promise<MWProviderScrapeResult> {
-  try {
-    const data = await provider.scrape(ctx);
-    return sortProviderResult(data);
-  } catch (err) {
-    console.error("Failed to run provider", err, {
-      id: provider.id,
-      ctx: { ...ctx },
-    });
-    throw err;
-  }
-}
-
-export async function runEmbedScraper(
-  scraper: MWEmbedScraper,
-  ctx: MWEmbedContext
-): Promise<MWStream> {
-  try {
-    return await scraper.getStream(ctx);
-  } catch (err) {
-    console.error("Failed to run embed scraper", {
-      id: scraper.id,
-      ctx: { ...ctx },
-    });
-    throw err;
-  }
-}
diff --git a/src/backend/helpers/scrape.ts b/src/backend/helpers/scrape.ts
deleted file mode 100644
index 5f1a100c..00000000
--- a/src/backend/helpers/scrape.ts
+++ /dev/null
@@ -1,173 +0,0 @@
-import { MWProviderContext, MWProviderScrapeResult } from "./provider";
-import { getEmbedScraperByType, getProviders } from "./register";
-import { runEmbedScraper, runProvider } from "./run";
-import { MWStream } from "./streams";
-import { DetailedMeta } from "../metadata/getmeta";
-import { MWMediaType } from "../metadata/types/mw";
-
-interface MWProgressData {
-  type: "embed" | "provider";
-  id: string;
-  eventId: string;
-  percentage: number;
-  errored: boolean;
-}
-interface MWNextData {
-  id: string;
-  eventId: string;
-  type: "embed" | "provider";
-}
-
-type MWProviderRunContextBase = {
-  media: DetailedMeta;
-  onProgress?: (data: MWProgressData) => void;
-  onNext?: (data: MWNextData) => void;
-};
-type MWProviderRunContextTypeSpecific =
-  | {
-      type: MWMediaType.MOVIE | MWMediaType.ANIME;
-      episode: undefined;
-      season: undefined;
-    }
-  | {
-      type: MWMediaType.SERIES;
-      episode: string;
-      season: string;
-    };
-
-export type MWProviderRunContext = MWProviderRunContextBase &
-  MWProviderRunContextTypeSpecific;
-
-async function findBestEmbedStream(
-  result: MWProviderScrapeResult,
-  providerId: string,
-  ctx: MWProviderRunContext
-): Promise<MWStream | null> {
-  if (result.stream) {
-    return {
-      ...result.stream,
-      providerId,
-      embedId: providerId,
-    };
-  }
-
-  let embedNum = 0;
-  for (const embed of result.embeds) {
-    embedNum += 1;
-    if (!embed.type) continue;
-    const scraper = getEmbedScraperByType(embed.type);
-    if (!scraper) throw new Error(`Type for embed not found: ${embed.type}`);
-
-    const eventId = [providerId, scraper.id, embedNum].join("|");
-
-    ctx.onNext?.({ id: scraper.id, type: "embed", eventId });
-
-    let stream: MWStream;
-    try {
-      stream = await runEmbedScraper(scraper, {
-        url: embed.url,
-        progress(num) {
-          ctx.onProgress?.({
-            errored: false,
-            eventId,
-            id: scraper.id,
-            percentage: num,
-            type: "embed",
-          });
-        },
-      });
-    } catch {
-      ctx.onProgress?.({
-        errored: true,
-        eventId,
-        id: scraper.id,
-        percentage: 100,
-        type: "embed",
-      });
-      continue;
-    }
-
-    ctx.onProgress?.({
-      errored: false,
-      eventId,
-      id: scraper.id,
-      percentage: 100,
-      type: "embed",
-    });
-
-    stream.providerId = providerId;
-    return stream;
-  }
-
-  return null;
-}
-
-export async function findBestStream(
-  ctx: MWProviderRunContext
-): Promise<MWStream | null> {
-  const providers = getProviders();
-
-  for (const provider of providers) {
-    const eventId = provider.id;
-    ctx.onNext?.({ id: provider.id, type: "provider", eventId });
-    let result: MWProviderScrapeResult;
-    try {
-      let context: MWProviderContext;
-      if (ctx.type === MWMediaType.SERIES) {
-        context = {
-          media: ctx.media,
-          type: ctx.type,
-          episode: ctx.episode,
-          season: ctx.season,
-          progress(num) {
-            ctx.onProgress?.({
-              percentage: num,
-              eventId,
-              errored: false,
-              id: provider.id,
-              type: "provider",
-            });
-          },
-        };
-      } else {
-        context = {
-          media: ctx.media,
-          type: ctx.type,
-          progress(num) {
-            ctx.onProgress?.({
-              percentage: num,
-              eventId,
-              errored: false,
-              id: provider.id,
-              type: "provider",
-            });
-          },
-        };
-      }
-      result = await runProvider(provider, context);
-    } catch (err) {
-      ctx.onProgress?.({
-        percentage: 100,
-        errored: true,
-        eventId,
-        id: provider.id,
-        type: "provider",
-      });
-      continue;
-    }
-
-    ctx.onProgress?.({
-      errored: false,
-      id: provider.id,
-      eventId,
-      percentage: 100,
-      type: "provider",
-    });
-
-    const stream = await findBestEmbedStream(result, provider.id, ctx);
-    if (!stream) continue;
-    return stream;
-  }
-
-  return null;
-}
diff --git a/src/backend/helpers/streams.ts b/src/backend/helpers/streams.ts
deleted file mode 100644
index 95b40503..00000000
--- a/src/backend/helpers/streams.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-export enum MWStreamType {
-  MP4 = "mp4",
-  HLS = "hls",
-}
-
-// subsrt-ts supported types
-export enum MWCaptionType {
-  VTT = "vtt",
-  SRT = "srt",
-  LRC = "lrc",
-  SBV = "sbv",
-  SUB = "sub",
-  SSA = "ssa",
-  ASS = "ass",
-  JSON = "json",
-  UNKNOWN = "unknown",
-}
-
-export enum MWStreamQuality {
-  Q360P = "360p",
-  Q540P = "540p",
-  Q480P = "480p",
-  Q720P = "720p",
-  Q1080P = "1080p",
-  QUNKNOWN = "unknown",
-}
-
-export type MWCaption = {
-  needsProxy?: boolean;
-  url: string;
-  type: MWCaptionType;
-  langIso: string;
-};
-
-export type MWStream = {
-  streamUrl: string;
-  type: MWStreamType;
-  quality: MWStreamQuality;
-  providerId?: string;
-  embedId?: string;
-  captions: MWCaption[];
-};
-
-export type MWEmbedStream = MWStream & {
-  embedId: string;
-};
diff --git a/src/backend/index.ts b/src/backend/index.ts
deleted file mode 100644
index 5fe33bd4..00000000
--- a/src/backend/index.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { initializeScraperStore } from "./helpers/register";
-
-// providers
-// import "./providers/gdriveplayer";
-import "./providers/flixhq";
-import "./providers/superstream";
-import "./providers/netfilm";
-import "./providers/m4ufree";
-import "./providers/hdwatched";
-import "./providers/2embed";
-import "./providers/sflix";
-import "./providers/gomovies";
-import "./providers/kissasian";
-import "./providers/streamflix";
-import "./providers/remotestream";
-
-// embeds
-import "./embeds/streamm4u";
-import "./embeds/playm4u";
-import "./embeds/upcloud";
-import "./embeds/streamsb";
-import "./embeds/mp4upload";
-
-initializeScraperStore();
diff --git a/src/backend/providers/2embed.ts b/src/backend/providers/2embed.ts
deleted file mode 100644
index 507d5a2d..00000000
--- a/src/backend/providers/2embed.ts
+++ /dev/null
@@ -1,252 +0,0 @@
-import Base64 from "crypto-js/enc-base64";
-import Utf8 from "crypto-js/enc-utf8";
-
-import { proxiedFetch, rawProxiedFetch } from "../helpers/fetch";
-import { registerProvider } from "../helpers/register";
-import {
-  MWCaptionType,
-  MWStreamQuality,
-  MWStreamType,
-} from "../helpers/streams";
-import { MWMediaType } from "../metadata/types/mw";
-
-const twoEmbedBase = "https://www.2embed.to";
-
-async function fetchCaptchaToken(recaptchaKey: string) {
-  const domainHash = Base64.stringify(Utf8.parse(twoEmbedBase)).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: twoEmbedBase,
-  };
-
-  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;
-}
-
-interface IEmbedRes {
-  link: string;
-  sources: [];
-  tracks: [];
-  type: string;
-}
-
-interface IStreamData {
-  status: string;
-  message: string;
-  type: string;
-  token: string;
-  result:
-    | {
-        Original: {
-          label: string;
-          file: string;
-          url: string;
-        };
-      }
-    | {
-        label: string;
-        size: number;
-        url: string;
-      }[];
-}
-
-interface ISubtitles {
-  url: string;
-  lang: string;
-}
-
-async function fetchStream(sourceId: string, captchaToken: string) {
-  const embedRes = await proxiedFetch<IEmbedRes>(
-    `${twoEmbedBase}/ajax/embed/play?id=${sourceId}&_token=${captchaToken}`,
-    {
-      headers: {
-        Referer: twoEmbedBase,
-      },
-    }
-  );
-
-  // Link format: https://rabbitstream.net/embed-4/{data-id}?z=
-  const rabbitStreamUrl = new URL(embedRes.link);
-
-  const dataPath = rabbitStreamUrl.pathname.split("/");
-  const dataId = dataPath[dataPath.length - 1];
-
-  // https://rabbitstream.net/embed/m-download/{data-id}
-  const download = await proxiedFetch<any>(
-    `${rabbitStreamUrl.origin}/embed/m-download/${dataId}`,
-    {
-      headers: {
-        referer: twoEmbedBase,
-      },
-    }
-  );
-
-  const downloadPage = new DOMParser().parseFromString(download, "text/html");
-
-  const streamlareEl = Array.from(
-    downloadPage.querySelectorAll(".dls-brand")
-  ).find((el) => el.textContent?.trim() === "Streamlare");
-  if (!streamlareEl) throw new Error("Unable to find streamlare element");
-
-  const streamlareUrl =
-    streamlareEl.nextElementSibling?.querySelector("a")?.href;
-  if (!streamlareUrl) throw new Error("Unable to parse streamlare url");
-
-  const subtitles: ISubtitles[] = [];
-  const subtitlesDropdown = downloadPage.querySelectorAll(
-    "#user_menu .dropdown-item"
-  );
-  subtitlesDropdown.forEach((item) => {
-    const url = item.getAttribute("href");
-    const lang = item.textContent?.trim().replace("Download", "").trim();
-    if (url && lang) subtitles.push({ url, lang });
-  });
-
-  const streamlare = await proxiedFetch<any>(streamlareUrl);
-
-  const streamlarePage = new DOMParser().parseFromString(
-    streamlare,
-    "text/html"
-  );
-
-  const csrfToken = streamlarePage
-    .querySelector("head > meta:nth-child(3)")
-    ?.getAttribute("content");
-
-  if (!csrfToken) throw new Error("Unable to find CSRF token");
-
-  const videoId = streamlareUrl.match("/[ve]/([^?#&/]+)")?.[1];
-  if (!videoId) throw new Error("Unable to get streamlare video id");
-
-  const streamRes = await proxiedFetch<IStreamData>(
-    `${new URL(streamlareUrl).origin}/api/video/download/get`,
-    {
-      method: "POST",
-      body: JSON.stringify({
-        id: videoId,
-      }),
-      headers: {
-        "X-Requested-With": "XMLHttpRequest",
-        "X-CSRF-Token": csrfToken,
-      },
-    }
-  );
-
-  if (streamRes.message !== "OK") throw new Error("Unable to fetch stream");
-
-  const streamData = Array.isArray(streamRes.result)
-    ? streamRes.result[0]
-    : streamRes.result.Original;
-  if (!streamData) throw new Error("Unable to get stream data");
-
-  const followStream = await rawProxiedFetch(streamData.url, {
-    method: "HEAD",
-    referrer: new URL(streamlareUrl).origin,
-  });
-
-  const finalStreamUrl = followStream.headers.get("X-Final-Destination");
-  return { url: finalStreamUrl, subtitles };
-}
-
-registerProvider({
-  id: "2embed",
-  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}`;
-
-    if (media.meta.type === MWMediaType.SERIES) {
-      const seasonNumber = media.meta.seasonData.number;
-      const episodeNumber = media.meta.seasonData.episodes.find(
-        (e) => e.id === episode
-      )?.number;
-
-      embedUrl = `${twoEmbedBase}/embed/tmdb/tv?id=${media.tmdbId}&s=${seasonNumber}&e=${episodeNumber}`;
-    }
-
-    const embed = await proxiedFetch<any>(embedUrl);
-    progress(20);
-
-    const embedPage = new DOMParser().parseFromString(embed, "text/html");
-
-    const pageServerItems = Array.from(
-      embedPage.querySelectorAll(".item-server")
-    );
-    const pageStreamItem = pageServerItems.find((item) =>
-      item.textContent?.includes("Vidcloud")
-    );
-
-    const sourceId = pageStreamItem
-      ? pageStreamItem.getAttribute("data-id")
-      : null;
-    if (!sourceId) throw new Error("Unable to get source id");
-
-    const siteKey = embedPage
-      .querySelector("body")
-      ?.getAttribute("data-recaptcha-key");
-    if (!siteKey) throw new Error("Unable to get site key");
-
-    const captchaToken = await fetchCaptchaToken(siteKey);
-    if (!captchaToken) throw new Error("Unable to fetch captcha token");
-    progress(35);
-
-    const stream = await fetchStream(sourceId, captchaToken);
-    if (!stream.url) throw new Error("Unable to find stream url");
-
-    return {
-      embeds: [],
-      stream: {
-        streamUrl: stream.url,
-        quality: MWStreamQuality.QUNKNOWN,
-        type: MWStreamType.MP4,
-        captions: stream.subtitles.map((sub) => {
-          return {
-            langIso: sub.lang,
-            url: `https://cc.2cdns.com${new URL(sub.url).pathname}`,
-            type: MWCaptionType.VTT,
-          };
-        }),
-      },
-    };
-  },
-});
diff --git a/src/backend/providers/flixhq/common.ts b/src/backend/providers/flixhq/common.ts
deleted file mode 100644
index a4e6b639..00000000
--- a/src/backend/providers/flixhq/common.ts
+++ /dev/null
@@ -1 +0,0 @@
-export const flixHqBase = "https://flixhq.to";
diff --git a/src/backend/providers/flixhq/index.ts b/src/backend/providers/flixhq/index.ts
deleted file mode 100644
index a30e6772..00000000
--- a/src/backend/providers/flixhq/index.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { MWEmbedType } from "@/backend/helpers/embed";
-import { registerProvider } from "@/backend/helpers/register";
-import { MWMediaType } from "@/backend/metadata/types/mw";
-import {
-  getFlixhqSourceDetails,
-  getFlixhqSources,
-} from "@/backend/providers/flixhq/scrape";
-import { getFlixhqId } from "@/backend/providers/flixhq/search";
-
-registerProvider({
-  id: "flixhq",
-  displayName: "FlixHQ",
-  rank: 100,
-  type: [MWMediaType.MOVIE, MWMediaType.SERIES],
-  async scrape({ media }) {
-    const id = await getFlixhqId(media.meta);
-    if (!id) throw new Error("flixhq no matching item found");
-
-    // TODO tv shows not supported. just need to scrape the specific episode sources
-
-    const sources = await getFlixhqSources(id);
-    const upcloudStream = sources.find(
-      (v) => v.embed.toLowerCase() === "upcloud"
-    );
-    if (!upcloudStream) throw new Error("upcloud stream not found for flixhq");
-
-    return {
-      embeds: [
-        {
-          type: MWEmbedType.UPCLOUD,
-          url: await getFlixhqSourceDetails(upcloudStream.episodeId),
-        },
-      ],
-    };
-  },
-});
diff --git a/src/backend/providers/flixhq/scrape.ts b/src/backend/providers/flixhq/scrape.ts
deleted file mode 100644
index 3ca32732..00000000
--- a/src/backend/providers/flixhq/scrape.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { proxiedFetch } from "@/backend/helpers/fetch";
-import { flixHqBase } from "@/backend/providers/flixhq/common";
-
-export async function getFlixhqSources(id: string) {
-  const type = id.split("/")[0];
-  const episodeParts = id.split("-");
-  const episodeId = episodeParts[episodeParts.length - 1];
-
-  const data = await proxiedFetch<string>(
-    `/ajax/${type}/episodes/${episodeId}`,
-    {
-      baseURL: flixHqBase,
-    }
-  );
-  const doc = new DOMParser().parseFromString(data, "text/html");
-
-  const sourceLinks = [...doc.querySelectorAll(".nav-item > a")].map((el) => {
-    const embedTitle = el.getAttribute("title");
-    const linkId = el.getAttribute("data-linkid");
-    if (!embedTitle || !linkId) throw new Error("invalid sources");
-    return {
-      embed: embedTitle,
-      episodeId: linkId,
-    };
-  });
-
-  return sourceLinks;
-}
-
-export async function getFlixhqSourceDetails(
-  sourceId: string
-): Promise<string> {
-  const jsonData = await proxiedFetch<Record<string, any>>(
-    `/ajax/sources/${sourceId}`,
-    {
-      baseURL: flixHqBase,
-    }
-  );
-
-  return jsonData.link;
-}
diff --git a/src/backend/providers/flixhq/search.ts b/src/backend/providers/flixhq/search.ts
deleted file mode 100644
index 64db2407..00000000
--- a/src/backend/providers/flixhq/search.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { proxiedFetch } from "@/backend/helpers/fetch";
-import { MWMediaMeta } from "@/backend/metadata/types/mw";
-import { flixHqBase } from "@/backend/providers/flixhq/common";
-import { compareTitle } from "@/utils/titleMatch";
-
-export async function getFlixhqId(meta: MWMediaMeta): Promise<string | null> {
-  const searchResults = await proxiedFetch<string>(
-    `/search/${meta.title.replaceAll(/[^a-z0-9A-Z]/g, "-")}`,
-    {
-      baseURL: flixHqBase,
-    }
-  );
-
-  const doc = new DOMParser().parseFromString(searchResults, "text/html");
-  const items = [...doc.querySelectorAll(".film_list-wrap > div.flw-item")].map(
-    (el) => {
-      const id = el
-        .querySelector("div.film-poster > a")
-        ?.getAttribute("href")
-        ?.slice(1);
-      const title = el
-        .querySelector("div.film-detail > h2 > a")
-        ?.getAttribute("title");
-      const year = el.querySelector(
-        "div.film-detail > div.fd-infor > span:nth-child(1)"
-      )?.textContent;
-
-      if (!id || !title || !year) return null;
-      return {
-        id,
-        title,
-        year,
-      };
-    }
-  );
-
-  const matchingItem = items.find(
-    (v) => v && compareTitle(meta.title, v.title) && meta.year === v.year
-  );
-
-  if (!matchingItem) return null;
-  return matchingItem.id;
-}
diff --git a/src/backend/providers/gdriveplayer.ts b/src/backend/providers/gdriveplayer.ts
deleted file mode 100644
index c184fea7..00000000
--- a/src/backend/providers/gdriveplayer.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-import CryptoJS from "crypto-js";
-import { unpack } from "unpacker";
-
-import { registerProvider } from "@/backend/helpers/register";
-import { MWStreamQuality } from "@/backend/helpers/streams";
-import { MWMediaType } from "@/backend/metadata/types/mw";
-
-import { proxiedFetch } from "../helpers/fetch";
-
-const format = {
-  stringify: (cipher: any) => {
-    const ct = cipher.ciphertext.toString(CryptoJS.enc.Base64);
-    const iv = cipher.iv.toString() || "";
-    const salt = cipher.salt.toString() || "";
-    return JSON.stringify({
-      ct,
-      iv,
-      salt,
-    });
-  },
-  parse: (jsonStr: string) => {
-    const json = JSON.parse(jsonStr);
-    const ciphertext = CryptoJS.enc.Base64.parse(json.ct);
-    const iv = CryptoJS.enc.Hex.parse(json.iv) || "";
-    const salt = CryptoJS.enc.Hex.parse(json.s) || "";
-
-    const cipher = CryptoJS.lib.CipherParams.create({
-      ciphertext,
-      iv,
-      salt,
-    });
-    return cipher;
-  },
-};
-
-registerProvider({
-  id: "gdriveplayer",
-  displayName: "gdriveplayer",
-  disabled: true,
-  rank: 69,
-  type: [MWMediaType.MOVIE],
-
-  async scrape({ progress, media: { imdbId } }) {
-    if (!imdbId) throw new Error("not enough info");
-    progress(10);
-    const streamRes = await proxiedFetch<string>(
-      "https://database.gdriveplayer.us/player.php",
-      {
-        params: {
-          imdb: imdbId,
-        },
-      }
-    );
-    progress(90);
-    const page = new DOMParser().parseFromString(streamRes, "text/html");
-
-    const script: HTMLElement | undefined = Array.from(
-      page.querySelectorAll("script")
-    ).find((e) => e.textContent?.includes("eval"));
-
-    if (!script || !script.textContent) {
-      throw new Error("Could not find stream");
-    }
-
-    /// NOTE: this code requires re-write, it's not safe
-    const data = unpack(script.textContent)
-      .split("var data=\\'")[1]
-      .split("\\'")[0]
-      .replace(/\\/g, "");
-    const decryptedData = unpack(
-      CryptoJS.AES.decrypt(
-        data,
-        "alsfheafsjklNIWORNiolNIOWNKLNXakjsfwnBdwjbwfkjbJjkopfjweopjASoiwnrflakefneiofrt",
-        { format }
-      ).toString(CryptoJS.enc.Utf8)
-    );
-
-    // eslint-disable-next-line
-    const sources = JSON.parse(
-      JSON.stringify(
-        eval(
-          decryptedData
-            .split("sources:")[1]
-            .split(",image")[0]
-            .replace(/\\/g, "")
-            .replace(/document\.referrer/g, '""')
-        )
-      )
-    );
-    const source = sources[sources.length - 1];
-    /// END
-
-    let quality;
-    if (source.label === "720p") quality = MWStreamQuality.Q720P;
-    else quality = MWStreamQuality.QUNKNOWN;
-
-    return {
-      stream: {
-        streamUrl: `https:${source.file}`,
-        type: source.type,
-        quality,
-        captions: [],
-      },
-      embeds: [],
-    };
-  },
-});
diff --git a/src/backend/providers/gomovies.ts b/src/backend/providers/gomovies.ts
deleted file mode 100644
index 169fcee7..00000000
--- a/src/backend/providers/gomovies.ts
+++ /dev/null
@@ -1,162 +0,0 @@
-import { MWEmbedType } from "../helpers/embed";
-import { proxiedFetch } from "../helpers/fetch";
-import { registerProvider } from "../helpers/register";
-import { MWMediaType } from "../metadata/types/mw";
-
-const gomoviesBase = "https://gomovies.sx";
-
-registerProvider({
-  id: "gomovies",
-  displayName: "GOmovies",
-  rank: 200,
-  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 === episodeNumber : false
-      );
-
-      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,
-        },
-      ],
-    };
-  },
-});
diff --git a/src/backend/providers/hdwatched.ts b/src/backend/providers/hdwatched.ts
deleted file mode 100644
index 533f711d..00000000
--- a/src/backend/providers/hdwatched.ts
+++ /dev/null
@@ -1,198 +0,0 @@
-import { proxiedFetch } from "../helpers/fetch";
-import { MWProviderContext } from "../helpers/provider";
-import { registerProvider } from "../helpers/register";
-import { MWStreamQuality, MWStreamType } from "../helpers/streams";
-import { MWMediaType } from "../metadata/types/mw";
-
-const hdwatchedBase = "https://www.hdwatched.xyz";
-
-const qualityMap: Record<number, MWStreamQuality> = {
-  360: MWStreamQuality.Q360P,
-  540: MWStreamQuality.Q540P,
-  480: MWStreamQuality.Q480P,
-  720: MWStreamQuality.Q720P,
-  1080: MWStreamQuality.Q1080P,
-};
-
-interface SearchRes {
-  title: string;
-  year?: number;
-  href: string;
-  id: string;
-}
-
-function getStreamFromEmbed(stream: string) {
-  const embedPage = new DOMParser().parseFromString(stream, "text/html");
-  const source = embedPage.querySelector("#vjsplayer > source");
-  if (!source) {
-    throw new Error("Unable to fetch stream");
-  }
-
-  const streamSrc = source.getAttribute("src");
-  const streamRes = source.getAttribute("res");
-
-  if (!streamSrc || !streamRes) throw new Error("Unable to find stream");
-
-  return {
-    streamUrl: streamSrc,
-    quality:
-      streamRes && typeof +streamRes === "number"
-        ? qualityMap[+streamRes]
-        : MWStreamQuality.QUNKNOWN,
-  };
-}
-
-async function fetchMovie(targetSource: SearchRes) {
-  const stream = await proxiedFetch<any>(`/embed/${targetSource.id}`, {
-    baseURL: hdwatchedBase,
-  });
-
-  const embedPage = new DOMParser().parseFromString(stream, "text/html");
-  const source = embedPage.querySelector("#vjsplayer > source");
-  if (!source) {
-    throw new Error("Unable to fetch movie stream");
-  }
-
-  return getStreamFromEmbed(stream);
-}
-
-async function fetchSeries(
-  targetSource: SearchRes,
-  { media, episode, progress }: MWProviderContext
-) {
-  if (media.meta.type !== MWMediaType.SERIES)
-    throw new Error("Media type mismatch");
-
-  const seasonNumber = media.meta.seasonData.number;
-  const episodeNumber = media.meta.seasonData.episodes.find(
-    (e) => e.id === episode
-  )?.number;
-
-  if (!seasonNumber || !episodeNumber)
-    throw new Error("Unable to get season or episode number");
-
-  const seriesPage = await proxiedFetch<any>(
-    `${targetSource.href}?season=${media.meta.seasonData.number}`,
-    {
-      baseURL: hdwatchedBase,
-    }
-  );
-
-  const seasonPage = new DOMParser().parseFromString(seriesPage, "text/html");
-  const pageElements = seasonPage.querySelectorAll("div.i-container");
-
-  const seriesList: SearchRes[] = [];
-  pageElements.forEach((pageElement) => {
-    const href = pageElement.querySelector("a")?.getAttribute("href") || "";
-    const title =
-      pageElement?.querySelector("span.content-title")?.textContent || "";
-
-    seriesList.push({
-      title,
-      href,
-      id: href.split("/")[2], // Format: /free/{id}/{series-slug}-season-{season-number}-episode-{episode-number}
-    });
-  });
-
-  const targetEpisode = seriesList.find(
-    (episodeEl) =>
-      episodeEl.title.trim().toLowerCase() === `episode ${episodeNumber}`
-  );
-
-  if (!targetEpisode) throw new Error("Unable to find episode");
-
-  progress(70);
-
-  const stream = await proxiedFetch<any>(`/embed/${targetEpisode.id}`, {
-    baseURL: hdwatchedBase,
-  });
-
-  const embedPage = new DOMParser().parseFromString(stream, "text/html");
-  const source = embedPage.querySelector("#vjsplayer > source");
-  if (!source) {
-    throw new Error("Unable to fetch movie stream");
-  }
-
-  return getStreamFromEmbed(stream);
-}
-
-registerProvider({
-  id: "hdwatched",
-  displayName: "HDwatched",
-  rank: 150,
-  disabled: true, // very slow, haven't seen it work for a while
-  type: [MWMediaType.MOVIE, MWMediaType.SERIES],
-  async scrape(options) {
-    const { media, progress } = options;
-    if (!media.imdbId) throw new Error("not enough info");
-    if (!this.type.includes(media.meta.type)) {
-      throw new Error("Unsupported type");
-    }
-
-    const search = await proxiedFetch<any>(`/search/${media.imdbId}`, {
-      baseURL: hdwatchedBase,
-    });
-
-    const searchPage = new DOMParser().parseFromString(search, "text/html");
-    const pageElements = searchPage.querySelectorAll("div.i-container");
-
-    const searchList: SearchRes[] = [];
-    pageElements.forEach((pageElement) => {
-      const href = pageElement.querySelector("a")?.getAttribute("href") || "";
-      const title =
-        pageElement?.querySelector("span.content-title")?.textContent || "";
-      const year =
-        parseInt(
-          pageElement
-            ?.querySelector("div.duration")
-            ?.textContent?.trim()
-            ?.split(" ")
-            ?.pop() || "",
-          10
-        ) || 0;
-
-      searchList.push({
-        title,
-        year,
-        href,
-        id: href.split("/")[2], // Format: /free/{id}/{movie-slug} or /series/{id}/{series-slug}
-      });
-    });
-
-    progress(20);
-
-    const targetSource = searchList.find(
-      (source) => source.year === (media.meta.year ? +media.meta.year : 0) // Compare year to make the search more robust
-    );
-
-    if (!targetSource) {
-      throw new Error("Could not find stream");
-    }
-
-    progress(40);
-
-    if (media.meta.type === MWMediaType.SERIES) {
-      const series = await fetchSeries(targetSource, options);
-      return {
-        embeds: [],
-        stream: {
-          streamUrl: series.streamUrl,
-          quality: series.quality,
-          type: MWStreamType.MP4,
-          captions: [],
-        },
-      };
-    }
-
-    const movie = await fetchMovie(targetSource);
-    return {
-      embeds: [],
-      stream: {
-        streamUrl: movie.streamUrl,
-        quality: movie.quality,
-        type: MWStreamType.MP4,
-        captions: [],
-      },
-    };
-  },
-});
diff --git a/src/backend/providers/kissasian.ts b/src/backend/providers/kissasian.ts
deleted file mode 100644
index a95e05ab..00000000
--- a/src/backend/providers/kissasian.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-import { MWEmbedType } from "../helpers/embed";
-import { proxiedFetch } from "../helpers/fetch";
-import { registerProvider } from "../helpers/register";
-import { MWMediaType } from "../metadata/types/mw";
-
-const kissasianBase = "https://kissasian.li";
-
-const embedProviders = [
-  {
-    type: MWEmbedType.MP4UPLOAD,
-    id: "mp",
-  },
-  {
-    type: MWEmbedType.STREAMSB,
-    id: "sb",
-  },
-];
-
-registerProvider({
-  id: "kissasian",
-  displayName: "KissAsian",
-  rank: 130,
-  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 url = ep?.querySelector("td.episodeSub a")?.getAttribute("href");
-        return { number, url };
-      })
-      .filter((e) => !!e.url);
-
-    const targetEpisode =
-      media.meta.type === MWMediaType.MOVIE
-        ? episodes[0]
-        : episodes.find((e) => e.number === `${episodeNumber}`);
-    if (!targetEpisode?.url) throw new Error("Episode not found");
-
-    progress(70);
-
-    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 embedUrl = watchPage
-          .querySelector("iframe[id=my_video_1]")
-          ?.getAttribute("src");
-
-        return {
-          type: provider.type,
-          url: embedUrl ?? "",
-        };
-      })
-    );
-    embeds = embeds.filter((e) => e.url !== "");
-
-    return {
-      embeds,
-    };
-  },
-});
diff --git a/src/backend/providers/m4ufree.ts b/src/backend/providers/m4ufree.ts
deleted file mode 100644
index b9d5aef0..00000000
--- a/src/backend/providers/m4ufree.ts
+++ /dev/null
@@ -1,236 +0,0 @@
-import { MWEmbed, MWEmbedType } from "@/backend/helpers/embed";
-
-import { proxiedFetch } from "../helpers/fetch";
-import { registerProvider } from "../helpers/register";
-import { MWMediaType } from "../metadata/types/mw";
-
-const HOST = "m4ufree.com";
-const URL_BASE = `https://${HOST}`;
-const URL_SEARCH = `${URL_BASE}/search`;
-const URL_AJAX = `${URL_BASE}/ajax`;
-const URL_AJAX_TV = `${URL_BASE}/ajaxtv`;
-
-// * Years can be in one of 4 formats:
-// * - "startyear" (for movies, EX: 2022)
-// * - "startyear-" (for TV series which has not ended, EX: 2022-)
-// * - "startyear-endyear" (for TV series which has ended, EX: 2022-2023)
-// * - "startyearendyear" (for TV series which has ended, EX: 20222023)
-const REGEX_TITLE_AND_YEAR = /(.*) \(?(\d*|\d*-|\d*-\d*)\)?$/;
-const REGEX_TYPE = /.*-(movie|tvshow)-online-free-m4ufree\.html/;
-const REGEX_COOKIES = /XSRF-TOKEN=(.*?);.*laravel_session=(.*?);/;
-const REGEX_SEASON_EPISODE = /S(\d*)-E(\d*)/;
-
-function toDom(html: string) {
-  return new DOMParser().parseFromString(html, "text/html");
-}
-
-registerProvider({
-  id: "m4ufree",
-  displayName: "m4ufree",
-  rank: -1,
-  disabled: true, // Disables because the redirector URLs it returns will throw 404 / 403 depending on if you view it in the browser or fetch it respectively. It just does not work.
-  type: [MWMediaType.MOVIE, MWMediaType.SERIES],
-
-  async scrape({ media, type, episode: episodeId, season: seasonId }) {
-    const season =
-      media.meta.seasons?.find((s) => s.id === seasonId)?.number || 1;
-    const episode =
-      media.meta.type === MWMediaType.SERIES
-        ? media.meta.seasonData.episodes.find((ep) => ep.id === episodeId)
-            ?.number || 1
-        : undefined;
-
-    const embeds: MWEmbed[] = [];
-
-    /*
-, {
-      responseType: "text" as any,
-    }
-    */
-    const responseText = await proxiedFetch<string>(
-      `${URL_SEARCH}/${encodeURIComponent(media.meta.title)}.html`
-    );
-    let dom = toDom(responseText);
-
-    const searchResults = [...dom.querySelectorAll(".item")]
-      .map((element) => {
-        const tooltipText = element.querySelector(".tiptitle p")?.innerHTML;
-        if (!tooltipText) return;
-
-        let regexResult = REGEX_TITLE_AND_YEAR.exec(tooltipText);
-
-        if (!regexResult || !regexResult[1] || !regexResult[2]) {
-          return;
-        }
-
-        const title = regexResult[1];
-        const year = Number(regexResult[2].slice(0, 4)); // * Some media stores the start AND end year. Only need start year
-        const a = element.querySelector("a");
-        if (!a) return;
-        const href = a.href;
-
-        regexResult = REGEX_TYPE.exec(href);
-
-        if (!regexResult || !regexResult[1]) {
-          return;
-        }
-
-        let scraperDeterminedType = regexResult[1];
-
-        scraperDeterminedType =
-          scraperDeterminedType === "tvshow" ? "show" : "movie"; // * Map to Trakt type
-
-        return { type: scraperDeterminedType, title, year, href };
-      })
-      .filter((item) => item);
-
-    const mediaInResults = searchResults.find(
-      (item) =>
-        item &&
-        item.title === media.meta.title &&
-        item.year.toString() === media.meta.year
-    );
-
-    if (!mediaInResults) {
-      // * Nothing found
-      return {
-        embeds,
-      };
-    }
-
-    let cookies: string | null = "";
-    const responseTextFromMedia = await proxiedFetch<string>(
-      mediaInResults.href,
-      {
-        onResponse(context) {
-          cookies = context.response.headers.get("X-Set-Cookie");
-        },
-      }
-    );
-    dom = toDom(responseTextFromMedia);
-
-    let regexResult = REGEX_COOKIES.exec(cookies);
-
-    if (!regexResult || !regexResult[1] || !regexResult[2]) {
-      // * DO SOMETHING?
-      throw new Error("No regexResults, yikesssssss kinda gross idk");
-    }
-
-    const cookieHeader = `XSRF-TOKEN=${regexResult[1]}; laravel_session=${regexResult[2]}`;
-
-    const token = dom
-      .querySelector('meta[name="csrf-token"]')
-      ?.getAttribute("content");
-    if (!token) return { embeds };
-
-    if (type === MWMediaType.SERIES) {
-      // * Get the season/episode data
-      const episodes = [...dom.querySelectorAll(".episode")]
-        .map((element) => {
-          regexResult = REGEX_SEASON_EPISODE.exec(element.innerHTML);
-
-          if (!regexResult || !regexResult[1] || !regexResult[2]) {
-            return;
-          }
-
-          const newEpisode = Number(regexResult[1]);
-          const newSeason = Number(regexResult[2]);
-
-          return {
-            id: element.getAttribute("idepisode"),
-            episode: newEpisode,
-            season: newSeason,
-          };
-        })
-        .filter((item) => item);
-
-      const ep = episodes.find(
-        (newEp) => newEp && newEp.episode === episode && newEp.season === season
-      );
-      if (!ep) return { embeds };
-
-      const form = `idepisode=${ep.id}&_token=${token}`;
-
-      const response = await proxiedFetch<string>(URL_AJAX_TV, {
-        method: "POST",
-        headers: {
-          Accept: "*/*",
-          "Accept-Encoding": "gzip, deflate, br",
-          "Accept-Language": "en-US,en;q=0.9",
-          "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
-          "X-Requested-With": "XMLHttpRequest",
-          "Sec-CH-UA":
-            '"Not?A_Brand";v="8", "Chromium";v="108", "Microsoft Edge";v="108"',
-          "Sec-CH-UA-Mobile": "?0",
-          "Sec-CH-UA-Platform": '"Linux"',
-          "Sec-Fetch-Site": "same-origin",
-          "Sec-Fetch-Mode": "cors",
-          "Sec-Fetch-Dest": "empty",
-          "X-Cookie": cookieHeader,
-          "X-Origin": URL_BASE,
-          "X-Referer": mediaInResults.href,
-        },
-        body: form,
-      });
-
-      dom = toDom(response);
-    }
-
-    const servers = [...dom.querySelectorAll(".singlemv")].map((element) =>
-      element.getAttribute("data")
-    );
-
-    for (const server of servers) {
-      const form = `m4u=${server}&_token=${token}`;
-
-      const response = await proxiedFetch<string>(URL_AJAX, {
-        method: "POST",
-        headers: {
-          Accept: "*/*",
-          "Accept-Encoding": "gzip, deflate, br",
-          "Accept-Language": "en-US,en;q=0.9",
-          "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
-          "X-Requested-With": "XMLHttpRequest",
-          "Sec-CH-UA":
-            '"Not?A_Brand";v="8", "Chromium";v="108", "Microsoft Edge";v="108"',
-          "Sec-CH-UA-Mobile": "?0",
-          "Sec-CH-UA-Platform": '"Linux"',
-          "Sec-Fetch-Site": "same-origin",
-          "Sec-Fetch-Mode": "cors",
-          "Sec-Fetch-Dest": "empty",
-          "X-Cookie": cookieHeader,
-          "X-Origin": URL_BASE,
-          "X-Referer": mediaInResults.href,
-        },
-        body: form,
-      });
-
-      const serverDom = toDom(response);
-
-      const link = serverDom.querySelector("iframe")?.src;
-
-      const getEmbedType = (url: string) => {
-        if (url.startsWith("https://streamm4u.club"))
-          return MWEmbedType.STREAMM4U;
-        if (url.startsWith("https://play.playm4u.xyz"))
-          return MWEmbedType.PLAYM4U;
-        return null;
-      };
-
-      if (!link) continue;
-
-      const embedType = getEmbedType(link);
-      if (embedType) {
-        embeds.push({
-          url: link,
-          type: embedType,
-        });
-      }
-    }
-
-    console.log(embeds);
-    return {
-      embeds,
-    };
-  },
-});
diff --git a/src/backend/providers/netfilm.ts b/src/backend/providers/netfilm.ts
deleted file mode 100644
index 54016733..00000000
--- a/src/backend/providers/netfilm.ts
+++ /dev/null
@@ -1,154 +0,0 @@
-import { proxiedFetch } from "../helpers/fetch";
-import { registerProvider } from "../helpers/register";
-import {
-  MWCaptionType,
-  MWStreamQuality,
-  MWStreamType,
-} from "../helpers/streams";
-import { MWMediaType } from "../metadata/types/mw";
-
-const netfilmBase = "https://net-film.vercel.app";
-
-const qualityMap: Record<number, MWStreamQuality> = {
-  360: MWStreamQuality.Q360P,
-  540: MWStreamQuality.Q540P,
-  480: MWStreamQuality.Q480P,
-  720: MWStreamQuality.Q720P,
-  1080: MWStreamQuality.Q1080P,
-};
-
-registerProvider({
-  id: "netfilm",
-  displayName: "NetFilm",
-  rank: 15,
-  type: [MWMediaType.MOVIE, MWMediaType.SERIES],
-  disabled: true, // The creator has asked us (very nicely) to leave him alone. Until (if) we self-host, netfilm should remain disabled
-
-  async scrape({ media, episode, progress }) {
-    if (!this.type.includes(media.meta.type)) {
-      throw new Error("Unsupported type");
-    }
-    // search for relevant item
-    const searchResponse = await proxiedFetch<any>(
-      `/api/search?keyword=${encodeURIComponent(media.meta.title)}`,
-      {
-        baseURL: netfilmBase,
-      }
-    );
-
-    const searchResults = searchResponse.data.results;
-    progress(25);
-
-    if (media.meta.type === MWMediaType.MOVIE) {
-      const foundItem = searchResults.find((v: any) => {
-        return v.name === media.meta.title && v.releaseTime === media.meta.year;
-      });
-      if (!foundItem) throw new Error("No watchable item found");
-      const netfilmId = foundItem.id;
-
-      // get stream info from media
-      progress(75);
-      const watchInfo = await proxiedFetch<any>(
-        `/api/episode?id=${netfilmId}`,
-        {
-          baseURL: netfilmBase,
-        }
-      );
-
-      const data = watchInfo.data;
-
-      // get best quality source
-      const source: { url: string; quality: number } = data.qualities.reduce(
-        (p: any, c: any) => (c.quality > p.quality ? c : p)
-      );
-
-      const mappedCaptions = data.subtitles.map((sub: Record<string, any>) => ({
-        needsProxy: false,
-        url: sub.url.replace("https://convert-srt-to-vtt.vercel.app/?url=", ""),
-        type: MWCaptionType.SRT,
-        langIso: sub.language,
-      }));
-
-      return {
-        embeds: [],
-        stream: {
-          streamUrl: source.url
-            .replace("akm-cdn", "aws-cdn")
-            .replace("gg-cdn", "aws-cdn"),
-          quality: qualityMap[source.quality],
-          type: MWStreamType.HLS,
-          captions: mappedCaptions,
-        },
-      };
-    }
-
-    if (media.meta.type !== MWMediaType.SERIES)
-      throw new Error("Unsupported type");
-
-    const desiredSeason = media.meta.seasonData.number;
-
-    const searchItems = searchResults
-      .filter((v: any) => {
-        return v.name.includes(media.meta.title);
-      })
-      .map((v: any) => {
-        return {
-          ...v,
-          season: parseInt(v.name.split(" ").at(-1), 10) || 1,
-        };
-      });
-
-    const foundItem = searchItems.find((v: any) => {
-      return v.season === desiredSeason;
-    });
-
-    progress(50);
-    const seasonDetail = await proxiedFetch<any>(
-      `/api/detail?id=${foundItem.id}&category=${foundItem.categoryTag[0].id}`,
-      {
-        baseURL: netfilmBase,
-      }
-    );
-
-    const episodeNo = media.meta.seasonData.episodes.find(
-      (v: any) => v.id === episode
-    )?.number;
-    const episodeData = seasonDetail.data.episodeVo.find(
-      (v: any) => v.seriesNo === episodeNo
-    );
-
-    progress(75);
-    const episodeStream = await proxiedFetch<any>(
-      `/api/episode?id=${foundItem.id}&category=1&episode=${episodeData.id}`,
-      {
-        baseURL: netfilmBase,
-      }
-    );
-
-    const data = episodeStream.data;
-
-    // get best quality source
-    const source: { url: string; quality: number } = data.qualities.reduce(
-      (p: any, c: any) => (c.quality > p.quality ? c : p)
-    );
-
-    const mappedCaptions = data.subtitles.map((sub: Record<string, any>) => ({
-      needsProxy: false,
-      url: sub.url.replace("https://convert-srt-to-vtt.vercel.app/?url=", ""),
-      type: MWCaptionType.SRT,
-      langIso: sub.language,
-    }));
-
-    return {
-      embeds: [],
-      stream: {
-        streamUrl: source.url
-          .replace("akm-cdn", "aws-cdn")
-          .replace("gg-cdn", "aws-cdn"),
-        quality: qualityMap[source.quality],
-        type: MWStreamType.HLS,
-        captions: mappedCaptions,
-      },
-    };
-  },
-});
diff --git a/src/backend/providers/remotestream.ts b/src/backend/providers/remotestream.ts
deleted file mode 100644
index 093069e8..00000000
--- a/src/backend/providers/remotestream.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { mwFetch } from "@/backend/helpers/fetch";
-import { registerProvider } from "@/backend/helpers/register";
-import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams";
-import { MWMediaType } from "@/backend/metadata/types/mw";
-
-const remotestreamBase = `https://fsa.remotestre.am`;
-
-registerProvider({
-  id: "remotestream",
-  displayName: "Remote Stream",
-  disabled: false,
-  rank: 55,
-  type: [MWMediaType.MOVIE, MWMediaType.SERIES],
-
-  async scrape({ media, episode, progress }) {
-    if (!this.type.includes(media.meta.type)) {
-      throw new Error("Unsupported type");
-    }
-
-    progress(30);
-    const type = media.meta.type === MWMediaType.MOVIE ? "Movies" : "Shows";
-    let playlistLink = `${remotestreamBase}/${type}/${media.tmdbId}`;
-
-    if (media.meta.type === MWMediaType.SERIES) {
-      const seasonNumber = media.meta.seasonData.number;
-      const episodeNumber = media.meta.seasonData.episodes.find(
-        (e) => e.id === episode
-      )?.number;
-
-      playlistLink += `/${seasonNumber}/${episodeNumber}/${episodeNumber}.m3u8`;
-    } else {
-      playlistLink += `/${media.tmdbId}.m3u8`;
-    }
-
-    const streamRes = await mwFetch<Blob>(playlistLink);
-    if (streamRes.type !== "application/x-mpegurl")
-      throw new Error("No watchable item found");
-    progress(90);
-    return {
-      embeds: [],
-      stream: {
-        streamUrl: playlistLink,
-        quality: MWStreamQuality.QUNKNOWN,
-        type: MWStreamType.HLS,
-        captions: [],
-      },
-    };
-  },
-});
diff --git a/src/backend/providers/sflix.ts b/src/backend/providers/sflix.ts
deleted file mode 100644
index db331e3c..00000000
--- a/src/backend/providers/sflix.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import { proxiedFetch } from "../helpers/fetch";
-import { registerProvider } from "../helpers/register";
-import { MWStreamQuality, MWStreamType } from "../helpers/streams";
-import { MWMediaType } from "../metadata/types/mw";
-
-const sflixBase = "https://sflix.video";
-
-registerProvider({
-  id: "sflix",
-  displayName: "Sflix",
-  rank: 50,
-  disabled: true, // domain dead
-  type: [MWMediaType.MOVIE, MWMediaType.SERIES],
-  async scrape({ media, episode, progress }) {
-    let searchQuery = `${media.meta.title} `;
-
-    if (media.meta.type === MWMediaType.MOVIE)
-      searchQuery += media.meta.year ?? "";
-
-    if (media.meta.type === MWMediaType.SERIES)
-      searchQuery += `S${String(media.meta.seasonData.number).padStart(
-        2,
-        "0"
-      )}`;
-
-    const search = await proxiedFetch<any>(
-      `/?s=${encodeURIComponent(searchQuery)}`,
-      {
-        baseURL: sflixBase,
-      }
-    );
-    const searchPage = new DOMParser().parseFromString(search, "text/html");
-
-    const moviePageUrl = searchPage
-      .querySelector(".movies-list .ml-item:first-child a")
-      ?.getAttribute("href");
-    if (!moviePageUrl) throw new Error("Movie does not exist");
-
-    progress(25);
-
-    const movie = await proxiedFetch<any>(moviePageUrl);
-    const moviePage = new DOMParser().parseFromString(movie, "text/html");
-
-    progress(45);
-
-    let outerEmbedSrc = null;
-    if (media.meta.type === MWMediaType.MOVIE) {
-      outerEmbedSrc = moviePage
-        .querySelector("iframe")
-        ?.getAttribute("data-lazy-src");
-    } else if (media.meta.type === MWMediaType.SERIES) {
-      const series = Array.from(moviePage.querySelectorAll(".desc p a")).map(
-        (a) => ({
-          title: a.getAttribute("title"),
-          link: a.getAttribute("href"),
-        })
-      );
-
-      const episodeNumber = media.meta.seasonData.episodes.find(
-        (e) => e.id === episode
-      )?.number;
-
-      const targetSeries = series.find((s) =>
-        s.title?.endsWith(String(episodeNumber).padStart(2, "0"))
-      );
-      if (!targetSeries) throw new Error("Episode does not exist");
-
-      outerEmbedSrc = targetSeries.link;
-    }
-    if (!outerEmbedSrc) throw new Error("Outer embed source not found");
-
-    progress(65);
-
-    const outerEmbed = await proxiedFetch<any>(outerEmbedSrc);
-    const outerEmbedPage = new DOMParser().parseFromString(
-      outerEmbed,
-      "text/html"
-    );
-
-    const embedSrc = outerEmbedPage
-      .querySelector("iframe")
-      ?.getAttribute("src");
-    if (!embedSrc) throw new Error("Embed source not found");
-
-    const embed = await proxiedFetch<string>(embedSrc);
-
-    const streamUrl = embed.match(/file\s*:\s*"([^"]+\.mp4)"/)?.[1];
-    if (!streamUrl) throw new Error("Unable to get stream");
-
-    return {
-      embeds: [],
-      stream: {
-        streamUrl,
-        quality: MWStreamQuality.Q1080P,
-        type: MWStreamType.MP4,
-        captions: [],
-      },
-    };
-  },
-});
diff --git a/src/backend/providers/streamflix.ts b/src/backend/providers/streamflix.ts
deleted file mode 100644
index d4488b03..00000000
--- a/src/backend/providers/streamflix.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import { proxiedFetch } from "@/backend/helpers/fetch";
-import { registerProvider } from "@/backend/helpers/register";
-import {
-  MWCaptionType,
-  MWStreamQuality,
-  MWStreamType,
-} from "@/backend/helpers/streams";
-import { MWMediaType } from "@/backend/metadata/types/mw";
-
-const streamflixBase = "https://us-west2-compute-proxied.streamflix.one";
-
-const qualityMap: Record<number, MWStreamQuality> = {
-  360: MWStreamQuality.Q360P,
-  540: MWStreamQuality.Q540P,
-  480: MWStreamQuality.Q480P,
-  720: MWStreamQuality.Q720P,
-  1080: MWStreamQuality.Q1080P,
-};
-
-registerProvider({
-  id: "streamflix",
-  displayName: "StreamFlix",
-  disabled: false,
-  rank: 69,
-  type: [MWMediaType.MOVIE, MWMediaType.SERIES],
-
-  async scrape({ media, episode, progress }) {
-    if (!this.type.includes(media.meta.type)) {
-      throw new Error("Unsupported type");
-    }
-
-    progress(30);
-    const type = media.meta.type === MWMediaType.MOVIE ? "movies" : "tv";
-    let seasonNumber: number | undefined;
-    let episodeNumber: number | undefined;
-
-    if (media.meta.type === MWMediaType.SERIES) {
-      // can't do type === "tv" here :(
-      seasonNumber = media.meta.seasonData.number;
-      episodeNumber = media.meta.seasonData.episodes.find(
-        (e: any) => e.id === episode
-      )?.number;
-    }
-
-    const streamRes = await proxiedFetch<any>(`/api/player/${type}`, {
-      baseURL: streamflixBase,
-      params: {
-        id: media.tmdbId,
-        s: seasonNumber,
-        e: episodeNumber,
-      },
-    });
-    if (!streamRes.headers.Referer) throw new Error("No watchable item found");
-    progress(90);
-    return {
-      embeds: [],
-      stream: {
-        streamUrl: streamRes.sources[0].url,
-        quality: qualityMap[streamRes.sources[0].quality],
-        type: MWStreamType.HLS,
-        captions: streamRes.subtitles.map((s: Record<string, any>) => ({
-          needsProxy: true,
-          url: s.url,
-          type: MWCaptionType.VTT,
-          langIso: s.lang,
-        })),
-      },
-    };
-  },
-});
diff --git a/src/backend/providers/superstream/LICENSE b/src/backend/providers/superstream/LICENSE
deleted file mode 100644
index 3f5347b0..00000000
--- a/src/backend/providers/superstream/LICENSE
+++ /dev/null
@@ -1,680 +0,0 @@
-Credit goes to @ImZaw and @Blatzar from https://github.com/recloudstream/cloudstream
-All files in the current directory (src/providers/list/superstream) are derived from https://github.com/recloudstream/cloudstream-extensions/blob/master/SuperStream/src/main/kotlin/com/lagradost/SuperStream.kt
-Below is the license associated with the source of the derived work.
-
-
-
-                    GNU GENERAL PUBLIC LICENSE
-                       Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
-                            Preamble
-
-  The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
-
-  The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works.  By contrast,
-the GNU General Public License is intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users.  We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors.  You can apply it to
-your programs, too.
-
-  When we speak of free software, we are referring to freedom, not
-price.  Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
-  To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights.  Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
-
-  For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received.  You must make sure that they, too, receive
-or can get the source code.  And you must show them these terms so they
-know their rights.
-
-  Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
-
-  For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software.  For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
-  Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so.  This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software.  The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable.  Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products.  If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
-  Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary.  To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
-
-  The precise terms and conditions for copying, distribution and
-modification follow.
-
-                       TERMS AND CONDITIONS
-
-  0. Definitions.
-
-  "This License" refers to version 3 of the GNU General Public License.
-
-  "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
-  "The Program" refers to any copyrightable work licensed under this
-License.  Each licensee is addressed as "you".  "Licensees" and
-"recipients" may be individuals or organizations.
-
-  To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy.  The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
-  A "covered work" means either the unmodified Program or a work based
-on the Program.
-
-  To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy.  Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
-  To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies.  Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
-  An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License.  If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
-  1. Source Code.
-
-  The "source code" for a work means the preferred form of the work
-for making modifications to it.  "Object code" means any non-source
-form of a work.
-
-  A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
-  The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form.  A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
-  The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities.  However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work.  For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
-  The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
-  The Corresponding Source for a work in source code form is that
-same work.
-
-  2. Basic Permissions.
-
-  All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met.  This License explicitly affirms your unlimited
-permission to run the unmodified Program.  The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work.  This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
-  You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force.  You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright.  Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
-  Conveying under any other circumstances is permitted solely under
-the conditions stated below.  Sublicensing is not allowed; section 10
-makes it unnecessary.
-
-  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
-  No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
-  When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
-  4. Conveying Verbatim Copies.
-
-  You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
-  You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
-  5. Conveying Modified Source Versions.
-
-  You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
-    a) The work must carry prominent notices stating that you modified
-    it, and giving a relevant date.
-
-    b) The work must carry prominent notices stating that it is
-    released under this License and any conditions added under section
-    7.  This requirement modifies the requirement in section 4 to
-    "keep intact all notices".
-
-    c) You must license the entire work, as a whole, under this
-    License to anyone who comes into possession of a copy.  This
-    License will therefore apply, along with any applicable section 7
-    additional terms, to the whole of the work, and all its parts,
-    regardless of how they are packaged.  This License gives no
-    permission to license the work in any other way, but it does not
-    invalidate such permission if you have separately received it.
-
-    d) If the work has interactive user interfaces, each must display
-    Appropriate Legal Notices; however, if the Program has interactive
-    interfaces that do not display Appropriate Legal Notices, your
-    work need not make them do so.
-
-  A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit.  Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
-  6. Conveying Non-Source Forms.
-
-  You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
-    a) Convey the object code in, or embodied in, a physical product
-    (including a physical distribution medium), accompanied by the
-    Corresponding Source fixed on a durable physical medium
-    customarily used for software interchange.
-
-    b) Convey the object code in, or embodied in, a physical product
-    (including a physical distribution medium), accompanied by a
-    written offer, valid for at least three years and valid for as
-    long as you offer spare parts or customer support for that product
-    model, to give anyone who possesses the object code either (1) a
-    copy of the Corresponding Source for all the software in the
-    product that is covered by this License, on a durable physical
-    medium customarily used for software interchange, for a price no
-    more than your reasonable cost of physically performing this
-    conveying of source, or (2) access to copy the
-    Corresponding Source from a network server at no charge.
-
-    c) Convey individual copies of the object code with a copy of the
-    written offer to provide the Corresponding Source.  This
-    alternative is allowed only occasionally and noncommercially, and
-    only if you received the object code with such an offer, in accord
-    with subsection 6b.
-
-    d) Convey the object code by offering access from a designated
-    place (gratis or for a charge), and offer equivalent access to the
-    Corresponding Source in the same way through the same place at no
-    further charge.  You need not require recipients to copy the
-    Corresponding Source along with the object code.  If the place to
-    copy the object code is a network server, the Corresponding Source
-    may be on a different server (operated by you or a third party)
-    that supports equivalent copying facilities, provided you maintain
-    clear directions next to the object code saying where to find the
-    Corresponding Source.  Regardless of what server hosts the
-    Corresponding Source, you remain obligated to ensure that it is
-    available for as long as needed to satisfy these requirements.
-
-    e) Convey the object code using peer-to-peer transmission, provided
-    you inform other peers where the object code and Corresponding
-    Source of the work are being offered to the general public at no
-    charge under subsection 6d.
-
-  A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
-  A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling.  In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage.  For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product.  A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
-  "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source.  The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
-  If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information.  But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
-  The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed.  Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
-  Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
-  7. Additional Terms.
-
-  "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law.  If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
-  When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it.  (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.)  You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
-  Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
-    a) Disclaiming warranty or limiting liability differently from the
-    terms of sections 15 and 16 of this License; or
-
-    b) Requiring preservation of specified reasonable legal notices or
-    author attributions in that material or in the Appropriate Legal
-    Notices displayed by works containing it; or
-
-    c) Prohibiting misrepresentation of the origin of that material, or
-    requiring that modified versions of such material be marked in
-    reasonable ways as different from the original version; or
-
-    d) Limiting the use for publicity purposes of names of licensors or
-    authors of the material; or
-
-    e) Declining to grant rights under trademark law for use of some
-    trade names, trademarks, or service marks; or
-
-    f) Requiring indemnification of licensors and authors of that
-    material by anyone who conveys the material (or modified versions of
-    it) with contractual assumptions of liability to the recipient, for
-    any liability that these contractual assumptions directly impose on
-    those licensors and authors.
-
-  All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10.  If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term.  If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
-  If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
-  Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
-  8. Termination.
-
-  You may not propagate or modify a covered work except as expressly
-provided under this License.  Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
-  However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
-  Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
-  Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License.  If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
-  9. Acceptance Not Required for Having Copies.
-
-  You are not required to accept this License in order to receive or
-run a copy of the Program.  Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance.  However,
-nothing other than this License grants you permission to propagate or
-modify any covered work.  These actions infringe copyright if you do
-not accept this License.  Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
-  10. Automatic Licensing of Downstream Recipients.
-
-  Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License.  You are not responsible
-for enforcing compliance by third parties with this License.
-
-  An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations.  If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
-  You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License.  For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
-  11. Patents.
-
-  A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based.  The
-work thus licensed is called the contributor's "contributor version".
-
-  A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version.  For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
-  Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
-  In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement).  To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
-  If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients.  "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
-  If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
-  A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License.  You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
-  Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
-  12. No Surrender of Others' Freedom.
-
-  If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License.  If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all.  For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
-  13. Use with the GNU Affero General Public License.
-
-  Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
-combined work, and to convey the resulting work.  The terms of this
-License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
-
-  14. Revised Versions of this License.
-
-  The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time.  Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
-  Each version is given a distinguishing version number.  If the
-Program specifies that a certain numbered version of the GNU General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation.  If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
-  If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
-  Later license versions may give you additional or different
-permissions.  However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
-  15. Disclaimer of Warranty.
-
-  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
-  16. Limitation of Liability.
-
-  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
-  17. Interpretation of Sections 15 and 16.
-
-  If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
-                     END OF TERMS AND CONDITIONS
-
-            How to Apply These Terms to Your New Programs
-
-  If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
-  To do so, attach the following notices to the program.  It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-    <one line to give the program's name and a brief idea of what it does.>
-    Copyright (C) <year>  <name of author>
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation, either version 3 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
-Also add information on how to contact you by electronic and paper mail.
-
-  If the program does terminal interaction, make it output a short
-notice like this when it starts in an interactive mode:
-
-    <program>  Copyright (C) <year>  <name of author>
-    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
-    This is free software, and you are welcome to redistribute it
-    under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License.  Of course, your program's commands
-might be different; for a GUI interface, you would use an "about box".
-
-  You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU GPL, see
-<https://www.gnu.org/licenses/>.
-
-  The GNU General Public License does not permit incorporating your program
-into proprietary programs.  If your program is a subroutine library, you
-may consider it more useful to permit linking proprietary applications with
-the library.  If this is what you want to do, use the GNU Lesser General
-Public License instead of this License.  But first, please read
-<https://www.gnu.org/licenses/why-not-lgpl.html>.
diff --git a/src/backend/providers/superstream/index.ts b/src/backend/providers/superstream/index.ts
deleted file mode 100644
index 883d1ad5..00000000
--- a/src/backend/providers/superstream/index.ts
+++ /dev/null
@@ -1,270 +0,0 @@
-import CryptoJS from "crypto-js";
-import { customAlphabet } from "nanoid";
-
-import {
-  getMWCaptionTypeFromUrl,
-  isSupportedSubtitle,
-} from "@/backend/helpers/captions";
-import { proxiedFetch } from "@/backend/helpers/fetch";
-import { registerProvider } from "@/backend/helpers/register";
-import {
-  MWCaption,
-  MWCaptionType,
-  MWStreamQuality,
-  MWStreamType,
-} from "@/backend/helpers/streams";
-import { MWMediaType } from "@/backend/metadata/types/mw";
-import { compareTitle } from "@/utils/titleMatch";
-
-const nanoid = customAlphabet("0123456789abcdef", 32);
-
-function makeFasterUrl(url: string) {
-  const fasterUrl = new URL(url);
-  fasterUrl.host = "mp4.shegu.net"; // this domain is faster
-  return fasterUrl.toString();
-}
-
-const qualityMap = {
-  "360p": MWStreamQuality.Q360P,
-  "480p": MWStreamQuality.Q480P,
-  "720p": MWStreamQuality.Q720P,
-  "1080p": MWStreamQuality.Q1080P,
-};
-type QualityInMap = keyof typeof qualityMap;
-
-// CONSTANTS, read below (taken from og)
-// We do not want content scanners to notice this scraping going on so we've hidden all constants
-// The source has its origins in China so I added some extra security with banned words
-// Mayhaps a tiny bit unethical, but this source is just too good :)
-// If you are copying this code please use precautions so they do not change their api.
-const iv = atob("d0VpcGhUbiE=");
-const key = atob("MTIzZDZjZWRmNjI2ZHk1NDIzM2FhMXc2");
-const apiUrls = [
-  atob("aHR0cHM6Ly9zaG93Ym94LnNoZWd1Lm5ldC9hcGkvYXBpX2NsaWVudC9pbmRleC8="),
-  atob("aHR0cHM6Ly9tYnBhcGkuc2hlZ3UubmV0L2FwaS9hcGlfY2xpZW50L2luZGV4Lw=="),
-];
-const appKey = atob("bW92aWVib3g=");
-const appId = atob("Y29tLnRkby5zaG93Ym94");
-
-// cryptography stuff
-const crypto = {
-  encrypt(str: string) {
-    return CryptoJS.TripleDES.encrypt(str, CryptoJS.enc.Utf8.parse(key), {
-      iv: CryptoJS.enc.Utf8.parse(iv),
-    }).toString();
-  },
-  getVerify(str: string, str2: string, str3: string) {
-    if (str) {
-      return CryptoJS.MD5(
-        CryptoJS.MD5(str2).toString() + str3 + str
-      ).toString();
-    }
-    return null;
-  },
-};
-
-// get expire time
-const expiry = () => Math.floor(Date.now() / 1000 + 60 * 60 * 12);
-
-// sending requests
-const get = (data: object, altApi = false) => {
-  const defaultData = {
-    childmode: "0",
-    app_version: "11.5",
-    appid: appId,
-    lang: "en",
-    expired_date: `${expiry()}`,
-    platform: "android",
-    channel: "Website",
-  };
-  const encryptedData = crypto.encrypt(
-    JSON.stringify({
-      ...defaultData,
-      ...data,
-    })
-  );
-  const appKeyHash = CryptoJS.MD5(appKey).toString();
-  const verify = crypto.getVerify(encryptedData, appKey, key);
-  const body = JSON.stringify({
-    app_key: appKeyHash,
-    verify,
-    encrypt_data: encryptedData,
-  });
-  const b64Body = btoa(body);
-
-  const formatted = new URLSearchParams();
-  formatted.append("data", b64Body);
-  formatted.append("appid", "27");
-  formatted.append("platform", "android");
-  formatted.append("version", "129");
-  formatted.append("medium", "Website");
-
-  const requestUrl = altApi ? apiUrls[1] : apiUrls[0];
-  return proxiedFetch<any>(requestUrl, {
-    method: "POST",
-    parseResponse: JSON.parse,
-    headers: {
-      Platform: "android",
-      "Content-Type": "application/x-www-form-urlencoded",
-    },
-    body: `${formatted.toString()}&token${nanoid()}`,
-  });
-};
-
-// Find best resolution
-const getBestQuality = (list: any[]) => {
-  return (
-    list.find((quality: any) => quality.quality === "1080p" && quality.path) ??
-    list.find((quality: any) => quality.quality === "720p" && quality.path) ??
-    list.find((quality: any) => quality.quality === "480p" && quality.path) ??
-    list.find((quality: any) => quality.quality === "360p" && quality.path)
-  );
-};
-
-const convertSubtitles = (subtitleGroup: any): MWCaption | null => {
-  let subtitles = subtitleGroup.subtitles;
-  subtitles = subtitles
-    .map((subFile: any) => {
-      const supported = isSupportedSubtitle(subFile.file_path);
-      if (!supported) return null;
-      const type = getMWCaptionTypeFromUrl(subFile.file_path);
-      return {
-        ...subFile,
-        type: type as MWCaptionType,
-      };
-    })
-    .filter(Boolean);
-
-  if (subtitles.length === 0) return null;
-  const subFile = subtitles[0];
-  return {
-    needsProxy: true,
-    langIso: subtitleGroup.language,
-    url: subFile.file_path,
-    type: subFile.type,
-  };
-};
-
-registerProvider({
-  id: "superstream",
-  displayName: "Superstream",
-  rank: 300,
-  type: [MWMediaType.MOVIE, MWMediaType.SERIES],
-
-  async scrape({ media, episode, progress }) {
-    // Find Superstream ID for show
-    const searchQuery = {
-      module: "Search3",
-      page: "1",
-      type: "all",
-      keyword: media.meta.title,
-      pagelimit: "20",
-    };
-    const searchRes = (await get(searchQuery, true)).data;
-    progress(33);
-
-    const superstreamEntry = searchRes.find(
-      (res: any) =>
-        compareTitle(res.title, media.meta.title) &&
-        res.year === Number(media.meta.year)
-    );
-
-    if (!superstreamEntry) throw new Error("No entry found on SuperStream");
-    const superstreamId = superstreamEntry.id;
-
-    // Movie logic
-    if (media.meta.type === MWMediaType.MOVIE) {
-      const apiQuery = {
-        uid: "",
-        module: "Movie_downloadurl_v3",
-        mid: superstreamId,
-        oss: "1",
-        group: "",
-      };
-
-      const mediaRes = (await get(apiQuery)).data;
-      progress(50);
-
-      const hdQuality = getBestQuality(mediaRes.list);
-
-      if (!hdQuality) throw new Error("No quality could be found.");
-
-      const subtitleApiQuery = {
-        fid: hdQuality.fid,
-        uid: "",
-        module: "Movie_srt_list_v2",
-        mid: superstreamId,
-      };
-
-      const subtitleRes = (await get(subtitleApiQuery)).data;
-
-      const mappedCaptions = subtitleRes.list
-        .map(convertSubtitles)
-        .filter(Boolean);
-
-      return {
-        embeds: [],
-        stream: {
-          streamUrl: makeFasterUrl(hdQuality.path),
-          quality: qualityMap[hdQuality.quality as QualityInMap],
-          type: MWStreamType.MP4,
-          captions: mappedCaptions,
-        },
-      };
-    }
-
-    if (media.meta.type !== MWMediaType.SERIES)
-      throw new Error("Unsupported type");
-
-    // Fetch requested episode
-    const apiQuery = {
-      uid: "",
-      module: "TV_downloadurl_v3",
-      tid: superstreamId,
-      season: media.meta.seasonData.number.toString(),
-      episode: (
-        media.meta.seasonData.episodes.find(
-          (episodeInfo) => episodeInfo.id === episode
-        )?.number ?? 1
-      ).toString(),
-      oss: "1",
-      group: "",
-    };
-
-    const mediaRes = (await get(apiQuery)).data;
-    progress(66);
-
-    const hdQuality = getBestQuality(mediaRes.list);
-
-    if (!hdQuality) throw new Error("No quality could be found.");
-
-    const subtitleApiQuery = {
-      fid: hdQuality.fid,
-      uid: "",
-      module: "TV_srt_list_v2",
-      episode:
-        media.meta.seasonData.episodes.find(
-          (episodeInfo) => episodeInfo.id === episode
-        )?.number ?? 1,
-      tid: superstreamId,
-      season: media.meta.seasonData.number.toString(),
-    };
-
-    const subtitleRes = (await get(subtitleApiQuery)).data;
-    const mappedCaptions = subtitleRes.list
-      .map(convertSubtitles)
-      .filter(Boolean);
-
-    return {
-      embeds: [],
-      stream: {
-        quality: qualityMap[
-          hdQuality.quality as QualityInMap
-        ] as MWStreamQuality,
-        streamUrl: makeFasterUrl(hdQuality.path),
-        type: MWStreamType.MP4,
-        captions: mappedCaptions,
-      },
-    };
-  },
-});
diff --git a/src/components/player/hooks/usePlayer.ts b/src/components/player/hooks/usePlayer.ts
index 6f7abad9..862bd1fc 100644
--- a/src/components/player/hooks/usePlayer.ts
+++ b/src/components/player/hooks/usePlayer.ts
@@ -1,4 +1,3 @@
-import { MWStreamType } from "@/backend/helpers/streams";
 import { useInitializePlayer } from "@/components/player/hooks/useInitializePlayer";
 import { PlayerMeta, playerStatus } from "@/stores/player/slices/source";
 import { usePlayerStore } from "@/stores/player/store";
@@ -7,7 +6,7 @@ import { ProgressMediaItem, useProgressStore } from "@/stores/progress";
 
 export interface Source {
   url: string;
-  type: MWStreamType;
+  type: "hls" | "mp4";
 }
 
 function getProgress(
diff --git a/src/index.tsx b/src/index.tsx
index 2d77bee5..8e56c24f 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -14,7 +14,6 @@ import i18n from "@/setup/i18n";
 import "@/setup/ga";
 import "@/setup/sentry";
 import "@/setup/index.css";
-import "@/backend";
 import { initializeChromecast } from "./setup/chromecast";
 import { SettingsStore } from "./state/settings/store";
 import { initializeStores } from "./utils/storage";
diff --git a/src/pages/developer/EmbedTesterView.tsx b/src/pages/developer/EmbedTesterView.tsx
deleted file mode 100644
index 15315ea4..00000000
--- a/src/pages/developer/EmbedTesterView.tsx
+++ /dev/null
@@ -1,137 +0,0 @@
-import { ReactNode, useCallback, useEffect, useMemo, useState } from "react";
-
-import { MWEmbed, MWEmbedScraper, MWEmbedType } from "@/backend/helpers/embed";
-import { getEmbeds } from "@/backend/helpers/register";
-import { runEmbedScraper } from "@/backend/helpers/run";
-import { MWStream } from "@/backend/helpers/streams";
-import { Button } from "@/components/Button";
-import { Navigation } from "@/components/layout/Navigation";
-import { ArrowLink } from "@/components/text/ArrowLink";
-import { Title } from "@/components/text/Title";
-import { useLoading } from "@/hooks/useLoading";
-
-interface MediaSelectorProps {
-  embedType: MWEmbedType;
-  onSelect: (meta: MWEmbed) => void;
-}
-
-interface EmbedScraperSelectorProps {
-  onSelect: (embedScraperId: string) => void;
-}
-
-interface MediaScraperProps {
-  embed: MWEmbed;
-  scraper: MWEmbedScraper;
-}
-
-function MediaSelector(props: MediaSelectorProps) {
-  const [url, setUrl] = useState("");
-
-  const select = useCallback(
-    (urlSt: string) => {
-      props.onSelect({
-        type: props.embedType,
-        url: urlSt,
-      });
-    },
-    [props]
-  );
-
-  return (
-    <div className="flex flex-col space-y-4">
-      <Title className="mb-8">Input embed url</Title>
-      <div className="mb-4 flex gap-4">
-        <input
-          type="text"
-          placeholder="embed url here..."
-          value={url}
-          onChange={(e) => setUrl(e.target.value)}
-        />
-        <Button onClick={() => select(url)}>Run scraper</Button>
-      </div>
-    </div>
-  );
-}
-
-function MediaScraper(props: MediaScraperProps) {
-  const [results, setResults] = useState<MWStream | null>(null);
-  const [percentage, setPercentage] = useState(0);
-
-  const [scrape, loading, error] = useLoading(async (url: string) => {
-    const data = await runEmbedScraper(props.scraper, {
-      url,
-      progress(num) {
-        console.log(`SCRAPING AT ${num}%`);
-        setPercentage(num);
-      },
-    });
-    console.log("got data", data);
-    setResults(data);
-  });
-
-  useEffect(() => {
-    if (props.embed) {
-      scrape(props.embed.url);
-    }
-  }, [props.embed, scrape]);
-
-  if (loading) return <p>Scraping... ({percentage}%)</p>;
-  if (error) return <p>Errored, check console</p>;
-
-  return (
-    <div>
-      <Title className="mb-8">Output data</Title>
-      <code>
-        <pre>{JSON.stringify(results, null, 2)}</pre>
-      </code>
-    </div>
-  );
-}
-
-function EmbedScraperSelector(props: EmbedScraperSelectorProps) {
-  const embedScrapers = getEmbeds();
-
-  return (
-    <div className="flex flex-col space-y-4">
-      <Title className="mb-8">Choose embed scraper</Title>
-      {embedScrapers.map((v) => (
-        <ArrowLink
-          key={v.id}
-          onClick={() => props.onSelect(v.id)}
-          direction="right"
-          linkText={v.displayName}
-        />
-      ))}
-    </div>
-  );
-}
-
-export default function EmbedTesterView() {
-  const [embed, setEmbed] = useState<MWEmbed | null>(null);
-  const [embedScraperId, setEmbedScraperId] = useState<string | null>(null);
-  const embedScraper = useMemo(
-    () => getEmbeds().find((v) => v.id === embedScraperId),
-    [embedScraperId]
-  );
-
-  let content: ReactNode = null;
-  if (!embedScraperId || !embedScraper) {
-    content = <EmbedScraperSelector onSelect={(id) => setEmbedScraperId(id)} />;
-  } else if (!embed) {
-    content = (
-      <MediaSelector
-        embedType={embedScraper.for}
-        onSelect={(v) => setEmbed(v)}
-      />
-    );
-  } else {
-    content = <MediaScraper scraper={embedScraper} embed={embed} />;
-  }
-
-  return (
-    <div className="py-48">
-      <Navigation />
-      <div className="mx-8 overflow-x-auto">{content}</div>
-    </div>
-  );
-}
diff --git a/src/pages/developer/ProviderTesterView.tsx b/src/pages/developer/ProviderTesterView.tsx
deleted file mode 100644
index 45f2297b..00000000
--- a/src/pages/developer/ProviderTesterView.tsx
+++ /dev/null
@@ -1,119 +0,0 @@
-import { ReactNode, useEffect, useState } from "react";
-
-import { testData } from "@/__tests__/providers/testdata";
-import { MWProviderScrapeResult } from "@/backend/helpers/provider";
-import { getProviders } from "@/backend/helpers/register";
-import { runProvider } from "@/backend/helpers/run";
-import { DetailedMeta } from "@/backend/metadata/getmeta";
-import { Navigation } from "@/components/layout/Navigation";
-import { ArrowLink } from "@/components/text/ArrowLink";
-import { Title } from "@/components/text/Title";
-import { useLoading } from "@/hooks/useLoading";
-
-interface MediaSelectorProps {
-  onSelect: (meta: DetailedMeta) => void;
-}
-
-interface ProviderSelectorProps {
-  onSelect: (providerId: string) => void;
-}
-
-interface MediaScraperProps {
-  media: DetailedMeta | null;
-  id: string;
-}
-
-function MediaSelector(props: MediaSelectorProps) {
-  const options: DetailedMeta[] = testData;
-
-  return (
-    <div className="flex flex-col space-y-4">
-      <Title className="mb-8">Choose media</Title>
-      {options.map((v) => (
-        <ArrowLink
-          key={v.imdbId}
-          onClick={() => props.onSelect(v)}
-          direction="right"
-          linkText={`${v.meta.title} (${v.meta.type})`}
-        />
-      ))}
-    </div>
-  );
-}
-
-function MediaScraper(props: MediaScraperProps) {
-  const [results, setResults] = useState<MWProviderScrapeResult | null>(null);
-  const [percentage, setPercentage] = useState(0);
-
-  const [scrape, loading, error] = useLoading(async (media: DetailedMeta) => {
-    const provider = getProviders().find((v) => v.id === props.id);
-    if (!provider) throw new Error("provider not found");
-    const data = await runProvider(provider, {
-      progress(num) {
-        console.log(`SCRAPING AT ${num}%`);
-        setPercentage(num);
-      },
-      media,
-      type: media.meta.type as any,
-    });
-    console.log("got data", data);
-    setResults(data);
-  });
-
-  useEffect(() => {
-    if (props.media) {
-      scrape(props.media);
-    }
-  }, [props.media, scrape]);
-
-  if (loading) return <p>Scraping... ({percentage}%)</p>;
-  if (error) return <p>Errored, check console</p>;
-
-  return (
-    <div>
-      <Title className="mb-8">Output data</Title>
-      <code>
-        <pre>{JSON.stringify(results, null, 2)}</pre>
-      </code>
-    </div>
-  );
-}
-
-function ProviderSelector(props: ProviderSelectorProps) {
-  const providers = getProviders();
-
-  return (
-    <div className="flex flex-col space-y-4">
-      <Title className="mb-8">Choose provider</Title>
-      {providers.map((v) => (
-        <ArrowLink
-          key={v.id}
-          onClick={() => props.onSelect(v.id)}
-          direction="right"
-          linkText={v.displayName}
-        />
-      ))}
-    </div>
-  );
-}
-
-export default function ProviderTesterView() {
-  const [media, setMedia] = useState<DetailedMeta | null>(null);
-  const [providerId, setProviderId] = useState<string | null>(null);
-
-  let content: ReactNode = null;
-  if (!providerId) {
-    content = <ProviderSelector onSelect={(id) => setProviderId(id)} />;
-  } else if (!media) {
-    content = <MediaSelector onSelect={(v) => setMedia(v)} />;
-  } else {
-    content = <MediaScraper id={providerId} media={media} />;
-  }
-
-  return (
-    <div className="py-48">
-      <Navigation />
-      <div className="mx-8 overflow-x-auto">{content}</div>
-    </div>
-  );
-}
diff --git a/src/setup/App.tsx b/src/setup/App.tsx
index 4d063fa5..fea66130 100644
--- a/src/setup/App.tsx
+++ b/src/setup/App.tsx
@@ -114,30 +114,11 @@ function App() {
                 />
                 {/* developer routes that can abuse workers are disabled in production */}
                 {process.env.NODE_ENV === "development" ? (
-                  <>
-                    <Route
-                      exact
-                      path="/dev/test"
-                      component={lazy(
-                        () => import("@/pages/developer/TestView")
-                      )}
-                    />
-
-                    <Route
-                      exact
-                      path="/dev/providers"
-                      component={lazy(
-                        () => import("@/pages/developer/ProviderTesterView")
-                      )}
-                    />
-                    <Route
-                      exact
-                      path="/dev/embeds"
-                      component={lazy(
-                        () => import("@/pages/developer/EmbedTesterView")
-                      )}
-                    />
-                  </>
+                  <Route
+                    exact
+                    path="/dev/test"
+                    component={lazy(() => import("@/pages/developer/TestView"))}
+                  />
                 ) : null}
                 <Route path="*" component={NotFoundPage} />
               </Switch>
diff --git a/src/stores/player/types.ts b/src/stores/player/types.ts
index b1a183dc..1555372c 100644
--- a/src/stores/player/types.ts
+++ b/src/stores/player/types.ts
@@ -1,4 +1,3 @@
-import { MWCaption } from "@/backend/helpers/streams";
 import { DetailedMeta } from "@/backend/metadata/getmeta";
 
 export interface Thumbnail {
@@ -8,7 +7,6 @@ export interface Thumbnail {
 }
 export type VideoPlayerMeta = {
   meta: DetailedMeta;
-  captions: MWCaption[];
   episode?: {
     episodeId: string;
     seasonId: string;