diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..d0f0ca6f --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +* @movie-web/core + +.github @binaryoverload diff --git a/package.json b/package.json index 3b508b2c..05a36350 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "movie-web", - "version": "3.1.4", + "version": "3.2.0", "private": true, "homepage": "https://movie-web.app", "dependencies": { diff --git a/src/backend/embeds/upcloud.ts b/src/backend/embeds/upcloud.ts index b2877bb3..4bac2b94 100644 --- a/src/backend/embeds/upcloud.ts +++ b/src/backend/embeds/upcloud.ts @@ -51,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..a30e6772 --- /dev/null +++ b/src/backend/providers/flixhq/index.ts @@ -0,0 +1,36 @@ +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 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/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} `; diff --git a/src/backend/providers/superstream/index.ts b/src/backend/providers/superstream/index.ts index 5af85cb9..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, @@ -248,13 +254,14 @@ registerProvider({ const mappedCaptions = subtitleRes.list .map(convertSubtitles) .filter(Boolean); + return { embeds: [], stream: { quality: qualityMap[ hdQuality.quality as QualityInMap ] as MWStreamQuality, - streamUrl: hdQuality.path, + streamUrl: makeFasterUrl(hdQuality.path), type: MWStreamType.MP4, captions: mappedCaptions, }, 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}

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")] };