From 4bd00eb47a23464611820c6a2cdb87e5bf06ce75 Mon Sep 17 00:00:00 2001 From: Jordaar <69628820+Jordaar@users.noreply.github.com> Date: Fri, 16 Jun 2023 14:37:07 +0530 Subject: [PATCH 01/12] feat(embed): add upcloud and streamsb embed scrapers --- src/backend/embeds/streamsb.ts | 212 +++++++++++++++++++++++++++++++++ src/backend/embeds/upcloud.ts | 93 +++++++++++++++ 2 files changed, 305 insertions(+) create mode 100644 src/backend/embeds/streamsb.ts create mode 100644 src/backend/embeds/upcloud.ts diff --git a/src/backend/embeds/streamsb.ts b/src/backend/embeds/streamsb.ts new file mode 100644 index 00000000..c755a0b0 --- /dev/null +++ b/src/backend/embeds/streamsb.ts @@ -0,0 +1,212 @@ +import Base64 from "crypto-js/enc-base64"; +import Utf8 from "crypto-js/enc-utf8"; + +import { MWEmbedType } from "@/backend/helpers/embed"; +import { proxiedFetch } from "@/backend/helpers/fetch"; +import { registerEmbedScraper } from "@/backend/helpers/register"; +import { + MWCaptionType, + MWStreamQuality, + MWStreamType, +} from "@/backend/helpers/streams"; + +const qualityOrder = [ + MWStreamQuality.Q1080P, + MWStreamQuality.Q720P, + MWStreamQuality.Q480P, + MWStreamQuality.Q360P, +]; + +async function fetchCaptchaToken(domain: string, recaptchaKey: string) { + const domainHash = Base64.stringify(Utf8.parse(domain)).replace(/=/g, "."); + + const recaptchaRender = await proxiedFetch( + `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]" + ); + + const dlDetails = []; + for (const func of downloadVideoFunctions) { + const funcContents = func.getAttribute("onclick"); + const regExpFunc = /download_video\('(.+?)','(.+?)','(.+?)'\)/; + const matchesFunc = regExpFunc.exec(funcContents ?? ""); + if (matchesFunc !== null) { + const quality = func.querySelector("span")?.textContent; + const regExpQuality = /(.+?) \((.+?)\)/; + const matchesQuality = regExpQuality.exec(quality ?? ""); + if (matchesQuality !== null) { + dlDetails.push({ + parameters: [matchesFunc[1], matchesFunc[2], matchesFunc[3]], + quality: { + label: matchesQuality[1].trim(), + size: matchesQuality[2], + }, + }); + } + } + } + + progress(40); + + let dls = await Promise.all( + dlDetails.map(async (dl) => { + const getDownload = await proxiedFetch( + `/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"); + + console.log(dlLink); + + return { + quality: dl.quality.label as MWStreamQuality, + url: dlLink, + size: dl.quality.size, + captions: + captionUrl && captionLang + ? [ + { + url: captionUrl, + langIso: captionLang, + type: MWCaptionType.VTT, + }, + ] + : [], + }; + }) + ); + dls = dls.filter((d) => !!d.url); + dls = dls.sort((a, b) => { + const aQuality = qualityOrder.indexOf(a.quality); + const bQuality = qualityOrder.indexOf(b.quality); + return aQuality - bQuality; + }); + + progress(60); + + // TODO: Quality selection for embed scrapers + const dl = dls[0]; + if (!dl.url) throw new Error("No stream url found"); + + return { + embedId: MWEmbedType.STREAMSB, + streamUrl: dl.url, + quality: dl.quality, + captions: dl.captions, + type: MWStreamType.MP4, + }; + }, +}); diff --git a/src/backend/embeds/upcloud.ts b/src/backend/embeds/upcloud.ts new file mode 100644 index 00000000..b2877bb3 --- /dev/null +++ b/src/backend/embeds/upcloud.ts @@ -0,0 +1,93 @@ +import { AES, enc } from "crypto-js"; + +import { MWEmbedType } from "@/backend/helpers/embed"; +import { registerEmbedScraper } from "@/backend/helpers/register"; +import { + MWCaptionType, + MWStreamQuality, + MWStreamType, +} from "@/backend/helpers/streams"; + +import { proxiedFetch } from "../helpers/fetch"; + +interface StreamRes { + server: number; + sources: string; + tracks: { + file: string; + kind: "captions" | "thumbnails"; + label: string; + }[]; +} + +function isJSON(json: string) { + try { + JSON.parse(json); + return true; + } catch { + return false; + } +} + +registerEmbedScraper({ + id: "upcloud", + displayName: "UpCloud", + for: MWEmbedType.UPCLOUD, + rank: 200, + async getStream({ url }) { + // Example url: https://dokicloud.one/embed-4/{id}?z= + const parsedUrl = new URL(url.replace("embed-5", "embed-4")); + + const dataPath = parsedUrl.pathname.split("/"); + const dataId = dataPath[dataPath.length - 1]; + + const streamRes = await proxiedFetch( + `${parsedUrl.origin}/ajax/embed-4/getSources?id=${dataId}`, + { + headers: { + Referer: parsedUrl.origin, + "X-Requested-With": "XMLHttpRequest", + }, + } + ); + + let sources: + | { + file: string; + type: string; + } + | string = streamRes.sources; + + if (!isJSON(sources) || typeof sources === "string") { + const decryptionKey = await proxiedFetch( + `https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt` + ); + + const decryptedStream = AES.decrypt(sources, decryptionKey).toString( + enc.Utf8 + ); + + const parsedStream = JSON.parse(decryptedStream)[0]; + if (!parsedStream) throw new Error("No stream found"); + sources = parsedStream as { file: string; type: string }; + } + + return { + embedId: MWEmbedType.UPCLOUD, + streamUrl: sources.file, + quality: MWStreamQuality.Q1080P, + type: MWStreamType.HLS, + captions: streamRes.tracks + .filter((sub) => sub.kind === "captions") + .map((sub) => { + return { + langIso: sub.label, + url: sub.file, + type: sub.file.endsWith("vtt") + ? MWCaptionType.VTT + : MWCaptionType.UNKNOWN, + }; + }), + }; + }, +}); From 7e696d5c2cad0e8c4f236cad4d7e1ca47f0f59ea Mon Sep 17 00:00:00 2001 From: Jordaar <69628820+Jordaar@users.noreply.github.com> Date: Fri, 16 Jun 2023 14:37:41 +0530 Subject: [PATCH 02/12] feat(provider): add gomovies provider --- src/backend/providers/gomovies.ts | 162 ++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 src/backend/providers/gomovies.ts diff --git a/src/backend/providers/gomovies.ts b/src/backend/providers/gomovies.ts new file mode 100644 index 00000000..9e22d095 --- /dev/null +++ b/src/backend/providers/gomovies.ts @@ -0,0 +1,162 @@ +import { MWEmbedType } from "../helpers/embed"; +import { proxiedFetch } from "../helpers/fetch"; +import { registerProvider } from "../helpers/register"; +import { MWMediaType } from "../metadata/types"; + +const gomoviesBase = "https://gomovies.sx"; + +registerProvider({ + id: "gomovies", + displayName: "GOmovies", + rank: 300, + type: [MWMediaType.MOVIE, MWMediaType.SERIES], + + async scrape({ media, episode }) { + const search = await proxiedFetch("/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 : ep.number === episodeNumber + ); + + 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, + }, + ], + }; + }, +}); From d198760f9c0051c4cf91cab4ad2b84762e09c6ae Mon Sep 17 00:00:00 2001 From: Jordaar <69628820+Jordaar@users.noreply.github.com> Date: Fri, 16 Jun 2023 14:37:57 +0530 Subject: [PATCH 03/12] feat(provider): add kissasian provider --- src/backend/providers/kissasian.ts | 103 +++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 src/backend/providers/kissasian.ts diff --git a/src/backend/providers/kissasian.ts b/src/backend/providers/kissasian.ts new file mode 100644 index 00000000..01fa0a2e --- /dev/null +++ b/src/backend/providers/kissasian.ts @@ -0,0 +1,103 @@ +import { MWEmbedType } from "../helpers/embed"; +import { proxiedFetch } from "../helpers/fetch"; +import { registerProvider } from "../helpers/register"; +import { MWMediaType } from "../metadata/types"; + +const kissasianBase = "https://kissasian.li"; + +registerProvider({ + id: "kissasian", + displayName: "KissAsian", + rank: 10000, + type: [MWMediaType.MOVIE, MWMediaType.SERIES], + + async scrape({ media, episode, progress }) { + let seasonNumber = ""; + let episodeNumber = ""; + + if (media.meta.type === MWMediaType.SERIES) { + seasonNumber = + media.meta.seasonData.number === 1 + ? "" + : `${media.meta.seasonData.number}`; + episodeNumber = `${ + media.meta.seasonData.episodes.find((e) => e.id === episode)?.number ?? + "" + }`; + } + + const searchForm = new FormData(); + searchForm.append("keyword", `${media.meta.title} ${seasonNumber}`.trim()); + searchForm.append("type", "Drama"); + + const search = await proxiedFetch("/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 href = ep?.querySelector("td.episodeSub a")?.getAttribute("href"); + return { number, href }; + }) + .filter((e) => !!e.href); + + const targetEpisode = + media.meta.type === MWMediaType.MOVIE + ? episodes[0] + : episodes.find((e) => e.number === `${episodeNumber}`); + if (!targetEpisode?.href) throw new Error("Episode not found"); + + progress(70); + + const watch = await proxiedFetch(`${targetEpisode.href}&s=sb`, { + baseURL: kissasianBase, + }); + + const watchPage = new DOMParser().parseFromString(watch, "text/html"); + + const streamsbUrl = watchPage + .querySelector("iframe[id=my_video_1]") + ?.getAttribute("src"); + if (!streamsbUrl) throw new Error("Streamsb embed not found"); + + console.log(streamsbUrl); + + return { + embeds: [ + { + type: MWEmbedType.STREAMSB, + url: streamsbUrl, + }, + ], + }; + }, +}); From 2db7e0bef8ffbdf41f74481a21c2c11cc6d6dd2f Mon Sep 17 00:00:00 2001 From: Jordaar <69628820+Jordaar@users.noreply.github.com> Date: Fri, 16 Jun 2023 14:41:30 +0530 Subject: [PATCH 04/12] feat(enum): add upcloud and streamsb enum --- src/backend/helpers/embed.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/backend/helpers/embed.ts b/src/backend/helpers/embed.ts index 0dec6422..a584a0c7 100644 --- a/src/backend/helpers/embed.ts +++ b/src/backend/helpers/embed.ts @@ -4,6 +4,8 @@ export enum MWEmbedType { M4UFREE = "m4ufree", STREAMM4U = "streamm4u", PLAYM4U = "playm4u", + UPCLOUD = "upcloud", + STREAMSB = "streamsb", } export type MWEmbed = { From d4c6dac9f2edecf9c186d8fed18160945e4e7122 Mon Sep 17 00:00:00 2001 From: Jordaar <69628820+Jordaar@users.noreply.github.com> Date: Fri, 16 Jun 2023 14:43:36 +0530 Subject: [PATCH 05/12] disable 2embed --- src/backend/providers/2embed.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/backend/providers/2embed.ts b/src/backend/providers/2embed.ts index 48056020..7cc8938e 100644 --- a/src/backend/providers/2embed.ts +++ b/src/backend/providers/2embed.ts @@ -191,6 +191,7 @@ registerProvider({ displayName: "2Embed", rank: 125, type: [MWMediaType.MOVIE, MWMediaType.SERIES], + disabled: true, // Disabled, not working async scrape({ media, episode, progress }) { let embedUrl = `${twoEmbedBase}/embed/tmdb/movie?id=${media.tmdbId}`; From f6b830d06df314b442ccafdfd6cc9cc303db54ad Mon Sep 17 00:00:00 2001 From: Jordaar <69628820+Jordaar@users.noreply.github.com> Date: Fri, 16 Jun 2023 14:44:54 +0530 Subject: [PATCH 06/12] feat(register): new providers and embed scrapers --- src/backend/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/backend/index.ts b/src/backend/index.ts index eb0ad897..065f1c62 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -9,9 +9,13 @@ import "./providers/m4ufree"; import "./providers/hdwatched"; import "./providers/2embed"; import "./providers/sflix"; +import "./providers/gomovies"; +import "./providers/kissasian"; // embeds import "./embeds/streamm4u"; import "./embeds/playm4u"; +import "./embeds/upcloud"; +import "./embeds/streamsb"; initializeScraperStore(); From 58ca372a49ce1ae8009b1e5f33a4809ef9b4c6a4 Mon Sep 17 00:00:00 2001 From: Jordaar <69628820+Jordaar@users.noreply.github.com> Date: Fri, 16 Jun 2023 14:52:42 +0530 Subject: [PATCH 07/12] refactor(kissasian): change rank --- src/backend/providers/kissasian.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/backend/providers/kissasian.ts b/src/backend/providers/kissasian.ts index 01fa0a2e..8d4e50c4 100644 --- a/src/backend/providers/kissasian.ts +++ b/src/backend/providers/kissasian.ts @@ -8,7 +8,7 @@ const kissasianBase = "https://kissasian.li"; registerProvider({ id: "kissasian", displayName: "KissAsian", - rank: 10000, + rank: 130, type: [MWMediaType.MOVIE, MWMediaType.SERIES], async scrape({ media, episode, progress }) { @@ -89,8 +89,6 @@ registerProvider({ ?.getAttribute("src"); if (!streamsbUrl) throw new Error("Streamsb embed not found"); - console.log(streamsbUrl); - return { embeds: [ { From e912ea4715035b9305a22cbabad3db0f5a4795e3 Mon Sep 17 00:00:00 2001 From: Jordaar <69628820+Jordaar@users.noreply.github.com> Date: Fri, 16 Jun 2023 15:05:42 +0530 Subject: [PATCH 08/12] cleanup --- src/backend/embeds/streamsb.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/backend/embeds/streamsb.ts b/src/backend/embeds/streamsb.ts index c755a0b0..99ed563e 100644 --- a/src/backend/embeds/streamsb.ts +++ b/src/backend/embeds/streamsb.ts @@ -169,8 +169,6 @@ registerEmbedScraper({ .querySelector(".btn.btn-light.btn-lg") ?.getAttribute("href"); - console.log(dlLink); - return { quality: dl.quality.label as MWStreamQuality, url: dlLink, From 9003bf67887e4750e47a093db1a2b7a3ca8e4703 Mon Sep 17 00:00:00 2001 From: Jordaar <69628820+Jordaar@users.noreply.github.com> Date: Fri, 16 Jun 2023 16:12:07 +0530 Subject: [PATCH 09/12] feat(embed): add mp4upload embed scraper --- src/backend/embeds/mp4upload.ts | 32 ++++++++++++++++++++++++++++++++ src/backend/index.ts | 1 + 2 files changed, 33 insertions(+) create mode 100644 src/backend/embeds/mp4upload.ts diff --git a/src/backend/embeds/mp4upload.ts b/src/backend/embeds/mp4upload.ts new file mode 100644 index 00000000..3902e20b --- /dev/null +++ b/src/backend/embeds/mp4upload.ts @@ -0,0 +1,32 @@ +import { MWEmbedType } from "@/backend/helpers/embed"; +import { registerEmbedScraper } from "@/backend/helpers/register"; +import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams"; + +import { proxiedFetch } from "../helpers/fetch"; + +registerEmbedScraper({ + id: "mp4upload", + displayName: "mp4upload", + for: MWEmbedType.MP4UPLOAD, + rank: 170, + async getStream({ url }) { + const embed = await proxiedFetch(url); + + const playerSrcRegex = + /(?<=player\.src\()\s*{\s*type:\s*"[^"]+",\s*src:\s*"([^"]+)"\s*}\s*(?=\);)/s; + + const playerSrc = embed.match(playerSrcRegex); + + const streamUrl = playerSrc[1]; + + if (!streamUrl) throw new Error("Stream url not found"); + + return { + embedId: MWEmbedType.MP4UPLOAD, + streamUrl, + quality: MWStreamQuality.Q1080P, + captions: [], + type: MWStreamType.MP4, + }; + }, +}); diff --git a/src/backend/index.ts b/src/backend/index.ts index 065f1c62..a2beaa2a 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -17,5 +17,6 @@ import "./embeds/streamm4u"; import "./embeds/playm4u"; import "./embeds/upcloud"; import "./embeds/streamsb"; +import "./embeds/mp4upload"; initializeScraperStore(); From 7e948c60c1473dd4bda958add89c62dacdef74a3 Mon Sep 17 00:00:00 2001 From: Jordaar <69628820+Jordaar@users.noreply.github.com> Date: Fri, 16 Jun 2023 16:12:53 +0530 Subject: [PATCH 10/12] feat(enum): add mp4upload enum --- src/backend/helpers/embed.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/backend/helpers/embed.ts b/src/backend/helpers/embed.ts index a584a0c7..1ec3362c 100644 --- a/src/backend/helpers/embed.ts +++ b/src/backend/helpers/embed.ts @@ -6,6 +6,7 @@ export enum MWEmbedType { PLAYM4U = "playm4u", UPCLOUD = "upcloud", STREAMSB = "streamsb", + MP4UPLOAD = "mp4upload", } export type MWEmbed = { From a0bb03790a37fad2eed1ab15f4ada9814a84dc45 Mon Sep 17 00:00:00 2001 From: Jordaar <69628820+Jordaar@users.noreply.github.com> Date: Fri, 16 Jun 2023 16:14:05 +0530 Subject: [PATCH 11/12] refactor(streamsb): improve quality sorting --- src/backend/embeds/streamsb.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/backend/embeds/streamsb.ts b/src/backend/embeds/streamsb.ts index 99ed563e..e91b43c7 100644 --- a/src/backend/embeds/streamsb.ts +++ b/src/backend/embeds/streamsb.ts @@ -100,7 +100,7 @@ registerEmbedScraper({ "[onclick^=download_video]" ); - const dlDetails = []; + let dlDetails = []; for (const func of downloadVideoFunctions) { const funcContents = func.getAttribute("onclick"); const regExpFunc = /download_video\('(.+?)','(.+?)','(.+?)'\)/; @@ -121,6 +121,12 @@ registerEmbedScraper({ } } + dlDetails = dlDetails.sort((a, b) => { + const aQuality = qualityOrder.indexOf(a.quality.label as MWStreamQuality); + const bQuality = qualityOrder.indexOf(b.quality.label as MWStreamQuality); + return aQuality - bQuality; + }); + progress(40); let dls = await Promise.all( @@ -187,11 +193,6 @@ registerEmbedScraper({ }) ); dls = dls.filter((d) => !!d.url); - dls = dls.sort((a, b) => { - const aQuality = qualityOrder.indexOf(a.quality); - const bQuality = qualityOrder.indexOf(b.quality); - return aQuality - bQuality; - }); progress(60); From bc0f9a6abff9ab90b7106456f852de9fffbf8276 Mon Sep 17 00:00:00 2001 From: Jordaar <69628820+Jordaar@users.noreply.github.com> Date: Fri, 16 Jun 2023 16:15:41 +0530 Subject: [PATCH 12/12] feat(kissasian): additional mp4upload embed scraper --- src/backend/providers/kissasian.ts | 54 ++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/src/backend/providers/kissasian.ts b/src/backend/providers/kissasian.ts index 8d4e50c4..90708970 100644 --- a/src/backend/providers/kissasian.ts +++ b/src/backend/providers/kissasian.ts @@ -5,6 +5,17 @@ import { MWMediaType } from "../metadata/types"; const kissasianBase = "https://kissasian.li"; +const embedProviders = [ + { + type: MWEmbedType.MP4UPLOAD, + id: "mp", + }, + { + type: MWEmbedType.STREAMSB, + id: "sb", + }, +]; + registerProvider({ id: "kissasian", displayName: "KissAsian", @@ -65,37 +76,44 @@ registerProvider({ ?.querySelector("td.episodeSub a") ?.textContent?.split("Episode")[1] ?.trim(); - const href = ep?.querySelector("td.episodeSub a")?.getAttribute("href"); - return { number, href }; + const url = ep?.querySelector("td.episodeSub a")?.getAttribute("href"); + return { number, url }; }) - .filter((e) => !!e.href); + .filter((e) => !!e.url); const targetEpisode = media.meta.type === MWMediaType.MOVIE ? episodes[0] : episodes.find((e) => e.number === `${episodeNumber}`); - if (!targetEpisode?.href) throw new Error("Episode not found"); + if (!targetEpisode?.url) throw new Error("Episode not found"); progress(70); - const watch = await proxiedFetch(`${targetEpisode.href}&s=sb`, { - baseURL: kissasianBase, - }); + 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 watchPage = new DOMParser().parseFromString(watch, "text/html"); - const streamsbUrl = watchPage - .querySelector("iframe[id=my_video_1]") - ?.getAttribute("src"); - if (!streamsbUrl) throw new Error("Streamsb embed not found"); + const embedUrl = watchPage + .querySelector("iframe[id=my_video_1]") + ?.getAttribute("src"); + + return { + type: provider.type, + url: embedUrl ?? "", + }; + }) + ); + embeds = embeds.filter((e) => e.url !== ""); return { - embeds: [ - { - type: MWEmbedType.STREAMSB, - url: streamsbUrl, - }, - ], + embeds, }; }, });