You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
250 lines
7.4 KiB
250 lines
7.4 KiB
import { registerProvider } from "@/backend/helpers/register"; |
|
import { MWMediaType } from "@/backend/metadata/types"; |
|
import { conf } from "@/setup/config"; |
|
|
|
import { customAlphabet } from "nanoid"; |
|
// import toWebVTT from "srt-webvtt"; |
|
import CryptoJS from "crypto-js"; |
|
import { proxiedFetch } from "@/backend/helpers/fetch"; |
|
import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams"; |
|
import { MetadataSchema } from "hls.js"; |
|
|
|
const nanoid = customAlphabet("0123456789abcdef", 32); |
|
|
|
const qualityMap = { |
|
"360p": MWStreamQuality.Q360P, |
|
"480p": MWStreamQuality.Q480P, |
|
"720p": MWStreamQuality.Q720P, |
|
"1080p": MWStreamQuality.Q1080P, |
|
}; |
|
type QualityInMap = keyof typeof qualityMap; |
|
|
|
// 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 proxiedFetch<any>(requestUrl, { |
|
method: "POST", |
|
parseResponse: JSON.parse, |
|
headers: { |
|
Platform: "android", |
|
"Content-Type": "application/x-www-form-urlencoded", |
|
}, |
|
body: `${formatted.toString()}&token${nanoid()}`, |
|
}); |
|
}; |
|
|
|
// Find best resolution |
|
const getBestQuality = (list: any[]) => { |
|
return ( |
|
list.find((quality: any) => quality.quality === "1080p" && quality.path) ?? |
|
list.find((quality: any) => quality.quality === "720p" && quality.path) ?? |
|
list.find((quality: any) => quality.quality === "480p" && quality.path) ?? |
|
list.find((quality: any) => quality.quality === "360p" && quality.path) |
|
); |
|
}; |
|
|
|
registerProvider({ |
|
id: "superstream", |
|
displayName: "Superstream", |
|
rank: 200, |
|
type: [MWMediaType.MOVIE, MWMediaType.SERIES], |
|
|
|
async scrape({ media, episode, progress }) { |
|
// Find Superstream ID for show |
|
const searchQuery = { |
|
module: "Search3", |
|
page: "1", |
|
type: "all", |
|
keyword: media.meta.title, |
|
pagelimit: "20", |
|
}; |
|
const searchRes = (await get(searchQuery, true)).data; |
|
progress(33); |
|
|
|
// TODO: add fuzzy search and normalise strings before matching |
|
const superstreamEntry = searchRes.find( |
|
(res: any) => |
|
res.title === media.meta.title && res.year === Number(media.meta.year) |
|
); |
|
|
|
if (!superstreamEntry) throw new Error("No entry found on SuperStream"); |
|
const superstreamId = superstreamEntry.id; |
|
|
|
// Movie logic |
|
if (media.meta.type === MWMediaType.MOVIE) { |
|
const apiQuery = { |
|
uid: "", |
|
module: "Movie_downloadurl_v3", |
|
mid: superstreamId, |
|
oss: "1", |
|
group: "", |
|
}; |
|
|
|
const mediaRes = (await get(apiQuery)).data; |
|
progress(50); |
|
|
|
const hdQuality = getBestQuality(mediaRes.list); |
|
|
|
if (!hdQuality) throw new Error("No quality could be found."); |
|
|
|
console.log(hdQuality); |
|
|
|
// const subtitleApiQuery = { |
|
// fid: hdQuality.fid, |
|
// uid: "", |
|
// module: "Movie_srt_list_v2", |
|
// mid: tmdbId, |
|
// }; |
|
|
|
// const subtitleRes = (await get(subtitleApiQuery).then((r) => r.json())) |
|
// .data; |
|
// const mappedCaptions = await Promise.all( |
|
// subtitleRes.list.map(async (subtitle: any) => { |
|
// const captionBlob = await fetch( |
|
// `${conf().CORS_PROXY_URL}${subtitle.subtitles[0].file_path}` |
|
// ).then((captionRes) => captionRes.blob()); // cross-origin bypass |
|
// const captionUrl = await toWebVTT(captionBlob); // convert to vtt so it's playable |
|
// return { |
|
// id: subtitle.language, |
|
// url: captionUrl, |
|
// label: subtitle.language, |
|
// }; |
|
// }) |
|
// ); |
|
|
|
return { |
|
embeds: [], |
|
stream: { |
|
streamUrl: hdQuality.path, |
|
quality: qualityMap[hdQuality.quality as QualityInMap], |
|
type: MWStreamType.MP4, |
|
}, |
|
}; |
|
} |
|
|
|
if (media.meta.type !== MWMediaType.SERIES) |
|
throw new Error("Unsupported type"); |
|
|
|
// Fetch requested episode |
|
const apiQuery = { |
|
uid: "", |
|
module: "TV_downloadurl_v3", |
|
tid: superstreamId, |
|
season: media.meta.seasonData.number.toString(), |
|
episode: ( |
|
media.meta.seasonData.episodes.find( |
|
(episodeInfo) => episodeInfo.id === episode |
|
)?.number ?? 1 |
|
).toString(), |
|
oss: "1", |
|
group: "", |
|
}; |
|
|
|
const mediaRes = (await get(apiQuery)).data; |
|
progress(66); |
|
|
|
const hdQuality = getBestQuality(mediaRes.list); |
|
|
|
if (!hdQuality) throw new Error("No quality could be found."); |
|
|
|
// const subtitleApiQuery = { |
|
// fid: hdQuality.fid, |
|
// uid: "", |
|
// module: "TV_srt_list_v2", |
|
// episode: media.episodeId, |
|
// tid: media.mediaId, |
|
// season: media.seasonId, |
|
// }; |
|
// const subtitleRes = (await get(subtitleApiQuery).then((r) => r.json())) |
|
// .data; |
|
// const mappedCaptions = await Promise.all( |
|
// subtitleRes.list.map(async (subtitle: any) => { |
|
// const captionBlob = await fetch( |
|
// `${conf().CORS_PROXY_URL}${subtitle.subtitles[0].file_path}` |
|
// ).then((captionRes) => captionRes.blob()); // cross-origin bypass |
|
// const captionUrl = await toWebVTT(captionBlob); // convert to vtt so it's playable |
|
// return { |
|
// id: subtitle.language, |
|
// url: captionUrl, |
|
// label: subtitle.language, |
|
// }; |
|
// }) |
|
// ); |
|
|
|
return { |
|
embeds: [], |
|
stream: { |
|
quality: qualityMap[ |
|
hdQuality.quality as QualityInMap |
|
] as MWStreamQuality, |
|
streamUrl: hdQuality.path, |
|
type: MWStreamType.MP4, |
|
}, |
|
}; |
|
}, |
|
});
|
|
|