From 072b2d134b0cbb8f0e6e510c7f84cd68799c9a94 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Tue, 15 Aug 2023 20:10:51 +0200 Subject: [PATCH 1/6] Disabled broken providers --- src/backend/embeds/upcloud.ts | 1 + src/backend/providers/hdwatched.ts | 1 + src/backend/providers/sflix.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/src/backend/embeds/upcloud.ts b/src/backend/embeds/upcloud.ts index b2877bb3..bc435530 100644 --- a/src/backend/embeds/upcloud.ts +++ b/src/backend/embeds/upcloud.ts @@ -34,6 +34,7 @@ registerEmbedScraper({ displayName: "UpCloud", for: MWEmbedType.UPCLOUD, rank: 200, + disabled: true, // encryption broke async getStream({ url }) { // Example url: https://dokicloud.one/embed-4/{id}?z= const parsedUrl = new URL(url.replace("embed-5", "embed-4")); diff --git a/src/backend/providers/hdwatched.ts b/src/backend/providers/hdwatched.ts index 458c3424..533f711d 100644 --- a/src/backend/providers/hdwatched.ts +++ b/src/backend/providers/hdwatched.ts @@ -120,6 +120,7 @@ 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; diff --git a/src/backend/providers/sflix.ts b/src/backend/providers/sflix.ts index 2cb1c598..db331e3c 100644 --- a/src/backend/providers/sflix.ts +++ b/src/backend/providers/sflix.ts @@ -9,6 +9,7 @@ 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} `; From 1524a3af39e741adaa417f1ddc07b2a3b479644e Mon Sep 17 00:00:00 2001 From: mrjvs Date: Tue, 15 Aug 2023 20:13:35 +0200 Subject: [PATCH 2/6] faster superstream url --- src/backend/providers/superstream/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/backend/providers/superstream/index.ts b/src/backend/providers/superstream/index.ts index 5af85cb9..548c1ec1 100644 --- a/src/backend/providers/superstream/index.ts +++ b/src/backend/providers/superstream/index.ts @@ -248,13 +248,17 @@ registerProvider({ const mappedCaptions = subtitleRes.list .map(convertSubtitles) .filter(Boolean); + + const fasterUrl = new URL(hdQuality.path); + fasterUrl.host = "mp4.shegu.net"; // this domain is faster + return { embeds: [], stream: { quality: qualityMap[ hdQuality.quality as QualityInMap ] as MWStreamQuality, - streamUrl: hdQuality.path, + streamUrl: fasterUrl.toString(), type: MWStreamType.MP4, captions: mappedCaptions, }, From 31fcd22822339a9b851757ecf18decaa6d766e38 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Tue, 15 Aug 2023 20:19:25 +0200 Subject: [PATCH 3/6] Make superstream a fast boi --- src/backend/providers/superstream/index.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/backend/providers/superstream/index.ts b/src/backend/providers/superstream/index.ts index 548c1ec1..883d1ad5 100644 --- a/src/backend/providers/superstream/index.ts +++ b/src/backend/providers/superstream/index.ts @@ -18,6 +18,12 @@ 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, @@ -199,7 +205,7 @@ registerProvider({ return { embeds: [], stream: { - streamUrl: hdQuality.path, + streamUrl: makeFasterUrl(hdQuality.path), quality: qualityMap[hdQuality.quality as QualityInMap], type: MWStreamType.MP4, captions: mappedCaptions, @@ -249,16 +255,13 @@ registerProvider({ .map(convertSubtitles) .filter(Boolean); - const fasterUrl = new URL(hdQuality.path); - fasterUrl.host = "mp4.shegu.net"; // this domain is faster - return { embeds: [], stream: { quality: qualityMap[ hdQuality.quality as QualityInMap ] as MWStreamQuality, - streamUrl: fasterUrl.toString(), + streamUrl: makeFasterUrl(hdQuality.path), type: MWStreamType.MP4, captions: mappedCaptions, }, From 2e3684eaad886df6f4a8892f8a8a8dda80c7cf54 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Tue, 15 Aug 2023 22:46:48 +0200 Subject: [PATCH 4/6] rip out consumet and fix upcloud --- src/backend/embeds/upcloud.ts | 39 ++++---- src/backend/providers/flixhq.ts | 128 ------------------------- src/backend/providers/flixhq/common.ts | 1 + src/backend/providers/flixhq/index.ts | 34 +++++++ src/backend/providers/flixhq/scrape.ts | 41 ++++++++ src/backend/providers/flixhq/search.ts | 43 +++++++++ tailwind.config.js | 2 +- 7 files changed, 143 insertions(+), 145 deletions(-) delete mode 100644 src/backend/providers/flixhq.ts create mode 100644 src/backend/providers/flixhq/common.ts create mode 100644 src/backend/providers/flixhq/index.ts create mode 100644 src/backend/providers/flixhq/scrape.ts create mode 100644 src/backend/providers/flixhq/search.ts diff --git a/src/backend/embeds/upcloud.ts b/src/backend/embeds/upcloud.ts index bc435530..4bac2b94 100644 --- a/src/backend/embeds/upcloud.ts +++ b/src/backend/embeds/upcloud.ts @@ -34,7 +34,6 @@ registerEmbedScraper({ displayName: "UpCloud", for: MWEmbedType.UPCLOUD, rank: 200, - disabled: true, // encryption broke async getStream({ url }) { // Example url: https://dokicloud.one/embed-4/{id}?z= const parsedUrl = new URL(url.replace("embed-5", "embed-4")); @@ -52,27 +51,35 @@ registerEmbedScraper({ } ); - let sources: - | { - file: string; - type: string; + 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] = ""; } - | 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 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 as { file: string; type: string }; + sources = parsedStream; } + if (!sources) throw new Error("upcloud source not found"); + return { embedId: MWEmbedType.UPCLOUD, streamUrl: sources.file, diff --git a/src/backend/providers/flixhq.ts b/src/backend/providers/flixhq.ts deleted file mode 100644 index 24d90a87..00000000 --- a/src/backend/providers/flixhq.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { compareTitle } from "@/utils/titleMatch"; - -import { - getMWCaptionTypeFromUrl, - isSupportedSubtitle, -} from "../helpers/captions"; -import { mwFetch } from "../helpers/fetch"; -import { registerProvider } from "../helpers/register"; -import { MWCaption, MWStreamQuality, MWStreamType } from "../helpers/streams"; -import { MWMediaType } from "../metadata/types/mw"; - -const flixHqBase = "https://api.consumet.org/meta/tmdb"; - -type FlixHQMediaType = "Movie" | "TV Series"; -interface FLIXMediaBase { - id: number; - title: string; - url: string; - image: string; - type: FlixHQMediaType; - releaseDate: string; -} -interface FLIXSubType { - url: string; - lang: string; -} -function convertSubtitles({ url, lang }: FLIXSubType): MWCaption | null { - if (lang.includes("(maybe)")) return null; - const supported = isSupportedSubtitle(url); - if (!supported) return null; - const type = getMWCaptionTypeFromUrl(url); - return { - url, - langIso: lang, - type, - }; -} - -const qualityMap: Record = { - "360": MWStreamQuality.Q360P, - "540": MWStreamQuality.Q540P, - "480": MWStreamQuality.Q480P, - "720": MWStreamQuality.Q720P, - "1080": MWStreamQuality.Q1080P, -}; - -function flixTypeToMWType(type: FlixHQMediaType) { - if (type === "Movie") return MWMediaType.MOVIE; - return MWMediaType.SERIES; -} - -registerProvider({ - id: "flixhq", - displayName: "FlixHQ", - rank: 100, - type: [MWMediaType.MOVIE, MWMediaType.SERIES], - async scrape({ media, episode, progress }) { - if (!this.type.includes(media.meta.type)) { - throw new Error("Unsupported type"); - } - // search for relevant item - const searchResults = await mwFetch( - `/${encodeURIComponent(media.meta.title)}`, - { - baseURL: flixHqBase, - } - ); - - const foundItem = searchResults.results.find((v: FLIXMediaBase) => { - if (v.type !== "Movie" && v.type !== "TV Series") return false; - return ( - compareTitle(v.title, media.meta.title) && - flixTypeToMWType(v.type) === media.meta.type && - v.releaseDate === media.meta.year - ); - }); - - if (!foundItem) throw new Error("No watchable item found"); - - // get media info - progress(25); - const mediaInfo = await mwFetch(`/info/${foundItem.id}`, { - baseURL: flixHqBase, - params: { - type: flixTypeToMWType(foundItem.type), - }, - }); - if (!mediaInfo.id) throw new Error("No watchable item found"); - // get stream info from media - progress(50); - - let episodeId: string | undefined; - if (media.meta.type === MWMediaType.MOVIE) { - episodeId = mediaInfo.episodeId; - } else if (media.meta.type === MWMediaType.SERIES) { - const seasonNo = media.meta.seasonData.number; - const episodeNo = media.meta.seasonData.episodes.find( - (e) => e.id === episode - )?.number; - - const season = mediaInfo.seasons.find((o: any) => o.season === seasonNo); - episodeId = season.episodes.find((o: any) => o.episode === episodeNo).id; - } - if (!episodeId) throw new Error("No watchable item found"); - progress(75); - const watchInfo = await mwFetch(`/watch/${episodeId}`, { - baseURL: flixHqBase, - params: { - id: mediaInfo.id, - }, - }); - - if (!watchInfo.sources) throw new Error("No watchable item found"); - - // get best quality source - // comes sorted by quality in descending order - const source = watchInfo.sources[0]; - return { - embeds: [], - stream: { - streamUrl: source.url, - quality: qualityMap[source.quality], - type: source.isM3U8 ? MWStreamType.HLS : MWStreamType.MP4, - captions: watchInfo.subtitles.map(convertSubtitles).filter(Boolean), - }, - }; - }, -}); diff --git a/src/backend/providers/flixhq/common.ts b/src/backend/providers/flixhq/common.ts new file mode 100644 index 00000000..a4e6b639 --- /dev/null +++ b/src/backend/providers/flixhq/common.ts @@ -0,0 +1 @@ +export const flixHqBase = "https://flixhq.to"; diff --git a/src/backend/providers/flixhq/index.ts b/src/backend/providers/flixhq/index.ts new file mode 100644 index 00000000..5f0e9003 --- /dev/null +++ b/src/backend/providers/flixhq/index.ts @@ -0,0 +1,34 @@ +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"); + + 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 new file mode 100644 index 00000000..3ca32732 --- /dev/null +++ b/src/backend/providers/flixhq/scrape.ts @@ -0,0 +1,41 @@ +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 new file mode 100644 index 00000000..64db2407 --- /dev/null +++ b/src/backend/providers/flixhq/search.ts @@ -0,0 +1,43 @@ +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/tailwind.config.js b/tailwind.config.js index e22b3b3a..3a6816a6 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -42,5 +42,5 @@ module.exports = { animation: { "loading-pin": "loading-pin 1.8s ease-in-out infinite" } } }, - plugins: [require("tailwind-scrollbar"), require("@tailwindcss/line-clamp")] + plugins: [require("tailwind-scrollbar")] }; From e2022297669675a1bb2dbafde39cfd73674da36a Mon Sep 17 00:00:00 2001 From: mrjvs Date: Tue, 15 Aug 2023 23:04:01 +0200 Subject: [PATCH 5/6] add todo --- src/backend/providers/flixhq/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/backend/providers/flixhq/index.ts b/src/backend/providers/flixhq/index.ts index 5f0e9003..a30e6772 100644 --- a/src/backend/providers/flixhq/index.ts +++ b/src/backend/providers/flixhq/index.ts @@ -16,6 +16,8 @@ registerProvider({ 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" From c1dceab8eba6dc2795ee9da71b9591cbe36a9877 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Tue, 15 Aug 2023 23:17:57 +0200 Subject: [PATCH 6/6] Fix class sorting --- src/components/media/MediaCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index a153d8b4..707c4fe4 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -117,7 +117,7 @@ function MediaCardContent({ /> -

+

{media.title}