4 changed files with 251 additions and 7 deletions
@ -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<MWProviderMediaResult> { |
||||||
|
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<MWProviderMediaResult[]> { |
||||||
|
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<MWMediaStream> { |
||||||
|
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<MWMediaSeasons> {
|
||||||
|
// 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,
|
||||||
|
// })),
|
||||||
|
// })),
|
||||||
|
// };
|
||||||
|
// },
|
||||||
|
}; |
Loading…
Reference in new issue