From 65d46190e6ca9f259ed972483fff602308f3a9bc Mon Sep 17 00:00:00 2001 From: mrjvs 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(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(`${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( - `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( - `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( - `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( - `${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( - `/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( - `/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( - `${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( - `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 { - let captionBlob: Blob; - if (caption.url.startsWith("blob:")) { - // custom subtitle - captionBlob = await (await fetch(caption.url)).blob(); - } else if (caption.needsProxy) { - captionBlob = await proxiedFetch(caption.url, { - responseType: "blob" as any, - }); - } else { - captionBlob = await mwFetch(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; -}; 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; -}; 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 { - 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 { - 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 { - 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 { - 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( - `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( - `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( - `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( - `${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( - `${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(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( - `${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(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( - `/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 { - const jsonData = await proxiedFetch>( - `/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 { - const searchResults = await proxiedFetch( - `/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( - "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("/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( - `/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( - `/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(`/ajax/v2/episode/servers/${mediaId}`, { - baseURL: gomoviesBase, - headers: { - "X-Requested-With": "XMLHttpRequest", - }, - }); - } else { - sources = await proxiedFetch(`/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 = { - 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(`/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( - `${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(`/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(`/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("/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(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( - `${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( - `${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( - 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(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(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 = { - 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( - `/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( - `/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) => ({ - 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( - `/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( - `/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) => ({ - 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(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( - `/?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(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(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(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 = { - 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(`/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) => ({ - 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. - 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. - - - Copyright (C) - - 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 . - -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: - - Copyright (C) - 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 -. - - 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 -. 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(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 ( -
- Input embed url -
- setUrl(e.target.value)} - /> - -
-
- ); -} - -function MediaScraper(props: MediaScraperProps) { - const [results, setResults] = useState(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

Scraping... ({percentage}%)

; - if (error) return

Errored, check console

; - - return ( -
- Output data - -
{JSON.stringify(results, null, 2)}
-
-
- ); -} - -function EmbedScraperSelector(props: EmbedScraperSelectorProps) { - const embedScrapers = getEmbeds(); - - return ( -
- Choose embed scraper - {embedScrapers.map((v) => ( - props.onSelect(v.id)} - direction="right" - linkText={v.displayName} - /> - ))} -
- ); -} - -export default function EmbedTesterView() { - const [embed, setEmbed] = useState(null); - const [embedScraperId, setEmbedScraperId] = useState(null); - const embedScraper = useMemo( - () => getEmbeds().find((v) => v.id === embedScraperId), - [embedScraperId] - ); - - let content: ReactNode = null; - if (!embedScraperId || !embedScraper) { - content = setEmbedScraperId(id)} />; - } else if (!embed) { - content = ( - setEmbed(v)} - /> - ); - } else { - content = ; - } - - return ( -
- -
{content}
-
- ); -} 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 ( -
- Choose media - {options.map((v) => ( - props.onSelect(v)} - direction="right" - linkText={`${v.meta.title} (${v.meta.type})`} - /> - ))} -
- ); -} - -function MediaScraper(props: MediaScraperProps) { - const [results, setResults] = useState(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

Scraping... ({percentage}%)

; - if (error) return

Errored, check console

; - - return ( -
- Output data - -
{JSON.stringify(results, null, 2)}
-
-
- ); -} - -function ProviderSelector(props: ProviderSelectorProps) { - const providers = getProviders(); - - return ( -
- Choose provider - {providers.map((v) => ( - props.onSelect(v.id)} - direction="right" - linkText={v.displayName} - /> - ))} -
- ); -} - -export default function ProviderTesterView() { - const [media, setMedia] = useState(null); - const [providerId, setProviderId] = useState(null); - - let content: ReactNode = null; - if (!providerId) { - content = setProviderId(id)} />; - } else if (!media) { - content = setMedia(v)} />; - } else { - content = ; - } - - return ( -
- -
{content}
-
- ); -} 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" ? ( - <> - import("@/pages/developer/TestView") - )} - /> - - import("@/pages/developer/ProviderTesterView") - )} - /> - import("@/pages/developer/EmbedTesterView") - )} - /> - + import("@/pages/developer/TestView"))} + /> ) : null} 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;