diff --git a/package.json b/package.json index c19818eb..7083652c 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "fuse.js": "^6.4.6", "hls.js": "^1.0.7", "json5": "^2.2.0", + "nanoid": "^4.0.0", "react": "^17.0.2", "react-dom": "^17.0.2", "react-router-dom": "^5.2.0", @@ -38,8 +39,8 @@ "@types/node": "^17.0.15", "@types/react": "^17.0.39", "@types/react-dom": "^17.0.11", - "@types/react-router-dom": "^5.3.3", "@types/react-router": "^5.1.18", + "@types/react-router-dom": "^5.3.3", "@typescript-eslint/eslint-plugin": "^5.13.0", "@typescript-eslint/parser": "^5.13.0", "autoprefixer": "^10.4.2", diff --git a/src/providers/list/superstream/index.ts b/src/providers/list/superstream/index.ts new file mode 100644 index 00000000..47ddf9c4 --- /dev/null +++ b/src/providers/list/superstream/index.ts @@ -0,0 +1,236 @@ +// this scraper was taken from recloudstream/cloudstream-extensions. much love +import { + MWMediaProvider, + MWMediaType, + MWPortableMedia, + MWMediaStream, + MWQuery, + MWMediaSeasons, + MWProviderMediaResult, +} from "providers/types"; +import { CORS_PROXY_URL } from "mw_constants"; +import { customAlphabet } from "nanoid"; +import CryptoJS from "crypto-js"; + +const nanoid = customAlphabet("0123456789abcdef", 32); +// 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 fetch(`${CORS_PROXY_URL}${requestUrl}`, { + method: "POST", + headers: { + Platform: "android", + "Content-Type": "application/x-www-form-urlencoded", + }, + body: `${formatted.toString()}&token${nanoid()}`, + }); +}; + +export const superStreamScraper: MWMediaProvider = { + id: "superstream", + enabled: true, + // type: [MWMediaType.MOVIE, MWMediaType.SERIES], + type: [MWMediaType.MOVIE], + displayName: "superstream", + + async getMediaFromPortable( + media: MWPortableMedia, + ): Promise { + let apiQuery: any; + if (media.episodeId) { + apiQuery = { + module: "TV_detail_1", + display_all: "1", + tid: media.mediaId, + }; + } else { + apiQuery = { + module: "Movie_detail", + mid: media.mediaId, + }; + } + const detailRes = (await get(apiQuery, true).then((r) => r.json())).data; + + return { + ...media, + title: detailRes.title, + year: detailRes.year, + seasonCount: detailRes?.season?.length, + } as MWProviderMediaResult; + }, + + async searchForMedia(query: MWQuery): Promise { + const apiQuery = { + module: "Search3", + page: "1", + type: "all", + keyword: query.searchQuery, + pagelimit: "20", + }; + const searchRes = (await get(apiQuery, true).then((r) => r.json())).data; + + const movieResults: MWProviderMediaResult[] = (searchRes || []) + .filter((item: any) => item.box_type === 1) + .map((item: any) => ({ + title: item.title, + year: item.year, + mediaId: item.id, + })); + const seriesResults: MWProviderMediaResult[] = (searchRes || []) + .filter((item: any) => item.box_type === 2) + .map((item: any) => ({ + title: item.title, + year: item.year, + mediaId: item.id, + seasonId: 1, + episodeId: 1, + })); + + if (query.type === "movie") { + return movieResults; + } + return seriesResults; + }, + + async getStream(media: MWPortableMedia): Promise { + if (media.mediaType === MWMediaType.MOVIE) { + const apiQuery = { + uid: "", + module: "Movie_downloadurl_v3", + mid: media.mediaId, + oss: "1", + group: "", + }; + const mediaRes = (await get(apiQuery).then((r) => r.json())).data; + const hdQuality = + mediaRes.list.find((quality: any) => quality.quality === "1080p") ?? + mediaRes.list.find((quality: any) => quality.quality === "720p"); + + return { url: hdQuality.path, type: "mp4", captions: [] }; + } + + const apiQuery = { + uid: "", + module: "TV_downloadurl_v3", + episode: media.episodeId, + tid: media.mediaId, + season: media.seasonId, + oss: "1", + group: "", + }; + const mediaRes = (await get(apiQuery).then((r) => r.json())).data; + const hdQuality = + mediaRes.list.find((quality: any) => quality.quality === "1080p") ?? + mediaRes.list.find((quality: any) => quality.quality === "720p"); + + return { url: hdQuality.path, type: "mp4", captions: [] }; + }, + // async getSeasonDataFromMedia( + // media: MWPortableMedia, + // ): Promise { + // const allSeasonEpisodes = []; + // const apiQuery = { + // module: "TV_detail_1", + // display_all: "1", + // tid: media.mediaId, + // }; + // const detailRes = (await get(apiQuery, true).then((r) => r.json())).data; + // allSeasonEpisodes.push(...detailRes.episode); + + // if (detailRes.seasons.length > 1) { + // for (const season of detailRes.seasons.slice(1)) { + // const seasonApiQuery = { + // module: "TV_detail_1", + // season: season.toString(), + // display_all: "1", + // tid: media.mediaId, + // }; + // const seasonRes = ( + // await get(seasonApiQuery, true).then((r) => r.json()) + // ).data; + // allSeasonEpisodes.push(...seasonRes.episode); + // } + // } + + // return { + // seasons: detailRes.season.map((season: number) => ({ + // sort: season, + // id: season.toString(), + // type: season === 0 ? "special" : "season", + // episodes: detailRes.episode + // .filter((episode: any) => episode.season === season) + // .map((episode: any) => ({ + // title: episode.title, + // sort: episode.episode, + // id: episode.episode.toString(), + // episodeNumber: episode.episode, + // })), + // })), + // }; + // }, +}; diff --git a/src/providers/methods/providers.ts b/src/providers/methods/providers.ts index 1cabfbcc..13c5350a 100644 --- a/src/providers/methods/providers.ts +++ b/src/providers/methods/providers.ts @@ -4,14 +4,16 @@ import { MWWrappedMediaProvider, WrapProvider } from "providers/wrapper"; import { gomostreamScraper } from "providers/list/gomostream"; import { xemovieScraper } from "providers/list/xemovie"; import { flixhqProvider } from "providers/list/flixhq"; +import { superStreamScraper } from "providers/list/superstream"; export const mediaProvidersUnchecked: MWWrappedMediaProvider[] = [ - WrapProvider(theFlixScraper), - WrapProvider(gDrivePlayerScraper), - WrapProvider(gomostreamScraper), - WrapProvider(xemovieScraper), - WrapProvider(flixhqProvider), + WrapProvider(superStreamScraper), + WrapProvider(theFlixScraper), + WrapProvider(gDrivePlayerScraper), + WrapProvider(gomostreamScraper), + WrapProvider(xemovieScraper), + WrapProvider(flixhqProvider), ]; export const mediaProviders: MWWrappedMediaProvider[] = - mediaProvidersUnchecked.filter((v) => v.enabled); + mediaProvidersUnchecked.filter((v) => v.enabled); diff --git a/yarn.lock b/yarn.lock index 32911f41..a1460c6b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5979,6 +5979,11 @@ nanoid@^3.3.3: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== +nanoid@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-4.0.0.tgz#6e144dee117609232c3f415c34b0e550e64999a5" + integrity sha512-IgBP8piMxe/gf73RTQx7hmnhwz0aaEXYakvqZyE302IXW3HyVNhdNGC+O2MwMAVhLEnvXlvKtGbtJf6wvHihCg== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"