cronchy
Version:
Crunchyroll scraper for https://crunchyroll.com.
416 lines • 19.5 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
const promise_request_1 = require("./libraries/promise-request");
class Cronchy {
/**
* @param email The email of the account.
* @param password Password of the account.
* @param token Optional token. If it has changed, you can pass it in.
*/
constructor(email, password, token) {
// Constant, might need to be changed.
this.token = "a3ZvcGlzdXZ6Yy0teG96Y21kMXk6R21JSTExenVPVnRnTjdlSWZrSlpibzVuLTRHTlZ0cU8=";
this.email = email;
this.password = password;
this.token = token ? token : "a3ZvcGlzdXZ6Yy0teG96Y21kMXk6R21JSTExenVPVnRnTjdlSWZrSlpibzVuLTRHTlZ0cU8=";
this.main = "https://www.crunchyroll.com";
this.api = "https://beta-api.crunchyroll.com";
}
/**
* @important Must be run before any other function.
*/
async login() {
const params = new URLSearchParams();
params.append("username", this.email);
params.append("password", this.password);
params.append("grant_type", "password");
params.append("scope", "offline_access");
const req = new promise_request_1.default(`${this.api}/auth/v1/token`, {
data: params,
headers: {
Authorization: `Basic ${this.token}`,
"Content-Type": "application/x-www-form-urlencoded"
},
method: "POST"
});
const res = await req.request().catch((err) => {
throw new Error(`Request to ${this.api}/auth/v1/token failed.\nEmail: ${this.email}; Password: ${this.password}; Token: ${this.token}`);
});
if (res.status != 200) {
throw new Error(res.toString());
}
const response = res.json();
const data = {
access_token: response["access_token"],
refresh_token: response["refresh_token"],
expires_in: response["expires_in"],
token_type: response["token_type"],
scope: response["scope"],
country: response["country"],
account_id: response["account_id"]
};
this.accessToken = data.access_token;
this.refreshToken = data.refresh_token;
this.accountId = data.account_id;
const sigReq = new promise_request_1.default(`${this.api}/index/v2`, {
headers: {
Authorization: "Bearer " + data.access_token,
},
});
const sig = await sigReq.request().catch((err) => {
throw new Error(`Request to ${this.api}/index/v2 failed.\nBearer: ${data.access_token}`);
});
const signature = sig.json();
if (!signature || !signature.cms) {
throw new Error(`Request to ${this.api}/index/v2 failed.\nBearer: ${data.access_token}`);
}
const sig_data = {
signature: signature.cms.signature,
key_pair_id: signature.cms.key_pair_id,
bucket: signature.cms.bucket,
policy: signature.cms.policy,
};
this.signature = sig_data.signature;
this.key_pair_id = sig_data.key_pair_id;
this.bucket = sig_data.bucket;
this.policy = sig_data.policy;
return {
access_token: data.access_token,
expires_in: data.expires_in,
token_type: data.token_type,
scope: data.scope,
country: data.country,
account_id: data.account_id,
signature: sig_data.signature,
key_pair_id: sig_data.key_pair_id,
bucket: sig_data.bucket,
policy: sig_data.policy,
};
}
/**
* @param query Search query. Takes a string.
* @param amount Max amount of search results. Takes a number.
*/
async search(query, amount) {
amount = amount ? amount : 8;
const req = new promise_request_1.default(`${this.api}/content/v2/discover/search?q=${encodeURIComponent(query)}&n=${amount}&type=&locale=en-US`, {
headers: {
Authorization: `Bearer ${this.accessToken}`,
Referer: this.main
},
});
const search = await req.request().catch((err) => {
throw new Error(`Request to ${this.api}/content/v2/discover/search?q=${encodeURIComponent(query)}&n=${amount}&type=&locale=en-US failed.\nBearer: ${this.accessToken}; Referer: ${this.main}`);
});
if (search.status != 200) {
throw new Error(search.toString());
}
return search.json();
}
async queryShowData(id, locale, mediaType) {
const req = new promise_request_1.default(`${this.api}/content/v2/cms/${mediaType}/${id}?locale=${locale}`, {
headers: {
Authorization: `Bearer ${this.accessToken}`,
Referer: this.main
},
});
const cr_data = await req.request().catch((err) => {
throw new Error(`Request to ${this.api}/content/v2/cms/${mediaType}/${id}?locale=${locale} failed.\nBearer: ${this.accessToken}; Referer: ${this.main}`);
});
return cr_data.json();
}
async queryGenreData(id, locale) {
const req = new promise_request_1.default(`${this.api}/content/v1/tenant_categories?guid=${id}?locale=${locale}`, {
headers: {
Authorization: `Bearer ${this.accessToken}`,
Referer: this.main
},
});
const cr_genre_response = await req.request().catch((err) => {
throw new Error(`Request to ${this.api}/content/v1/tenant_categories?guid=${id}?locale=${locale} failed.\nBearer: ${this.accessToken}; Referer: ${this.main}`);
});
return cr_genre_response.json();
}
async queryRatings(id) {
const req = new promise_request_1.default(`${this.api}/content-reviews/v2/user/${this.accountId}/rating/series/${id}`, {
headers: {
Authorization: `Bearer ${this.accessToken}`,
Referer: this.main
},
});
const cr_ratings_response = await req.request().catch((err) => {
throw new Error(`Request to ${this.api}/content-reviews/v2/user/${this.accountId}/rating/series/${id} failed.\nBearer: ${this.accessToken}; Referer: ${this.main}`);
});
return cr_ratings_response.json();
}
async queryRecommendations(id, locale) {
const req = new promise_request_1.default(`${this.api}/content/v1/${this.accountId}/similar_to?guid=${id}&locale=${locale}&n=30`, {
headers: {
Authorization: `Bearer ${this.accessToken}`,
Referer: this.main
},
});
const cr_recommendations_response = await req.request().catch((err) => {
throw new Error(`Request to ${this.api}/content/v1/${this.accountId}/similar_to?guid=${id}&locale=${locale}&n=30 failed.\nBearer: ${this.accessToken}; Referer: ${this.main}`);
});
return cr_recommendations_response.json();
}
async querySeason(id, locale) {
const req = new promise_request_1.default(`${this.api}/cms/v2${this.bucket}/seasons?series_id=${id}&locale=${locale}&Signature=${this.signature}&Policy=${this.policy}&Key-Pair-Id=${this.key_pair_id}`, {
headers: {
Authorization: `Bearer ${this.accessToken}`,
Referer: this.main
},
});
const seasons_response = await req.request().catch((err) => {
throw new Error(`Request to ${this.api}/cms/v2${this.bucket}/seasons?series_id=${id}&locale=${locale}&Signature=${this.signature}&Policy=${this.policy}&Key-Pair-Id=${this.key_pair_id} failed.\nBearer: ${this.accessToken}; Referer: ${this.main}`);
});
return seasons_response.json();
}
async queryEpisodes(season, locale) {
const req = new promise_request_1.default(`${this.api}/cms/v2${this.bucket}/episodes?season_id=${season?.id}&locale=${locale}&Signature=${this.signature}&Policy=${this.policy}&Key-Pair-Id=${this.key_pair_id}`, {
headers: {
Authorization: `Bearer ${this.accessToken}`,
Referer: this.main
},
});
const episode_response = await req.request().catch((err) => {
throw new Error(`Request to ${this.api}/cms/v2${this.bucket}/episodes?season_id=${season?.id}&locale=${locale}&Signature=${this.signature}&Policy=${this.policy}&Key-Pair-Id=${this.key_pair_id} failed.\nBearer: ${this.accessToken}; Referer: ${this.main}`);
});
return episode_response.json();
}
/**
* @param seriesQuery SearchQuery object. Can be obtained from the search function.
* @returns
*/
getLocaleFromSearchQuery(seriesQuery) {
const locale = seriesQuery.series_metadata["subtitle_locales"].length > 0 ? seriesQuery.series_metadata["subtitle_locales"][0] : seriesQuery.series_metadata["audio_locales"][0];
return locale;
}
/**
* @param seriesQuery SearchQuery object. Can be obtained from the search function.
* @returns
*/
getMediaTypeFromSearchQuery(seriesQuery) {
const type = seriesQuery.type;
return type;
}
/**
* @param id SearchQuery id. Can be obtained from the search function.
* @param locale The locale of the show. Can be obtained from the search function or via getLocaleFromSearchQuery().
* @param mediaType The type of media. Must be a valid Crunchyroll series string.
* @param fetchAll Whether or not to fetch all "seasons" (whatever Crunchyroll means by that lol). If false, only the the episodes from the "season" will be fetched.
*/
async getEpisodes(id, locale, mediaType, fetchAll) {
const cr_data = await this.queryShowData(id, locale, mediaType);
const cr_genre_response = await this.queryGenreData(id, locale);
const cr_ratings_data = await this.queryRatings(id);
const cr_recommendations_data = await (await this.queryRecommendations(id, locale)).items;
const season_response = await this.querySeason(id, locale);
const cr_genre_data = cr_genre_response.items;
const genres = [];
cr_genre_data?.forEach((genre) => {
genres.push(genre.localization.title);
});
const season_data = season_response.items;
let season_list = [];
season_data?.forEach((season) => {
season_list.push({
id: season.id,
title: season.title,
season_number: season.season_number,
type: season.is_dubbed
? season.title.split("(")[1].replace(")", "")
: "subbed",
isDub: season.is_dubbed,
});
});
const episodes = [];
if (fetchAll) {
const promises = [];
season_list.map(((season) => {
const promise = new Promise(async (resolve, reject) => {
const episode_response = await this.queryEpisodes(season, locale);
const episode_data = await episode_response.items;
const season_episode_list = {
[season.type]: [
episode_data.map((episode) => {
return {
id: episode.id,
season_number: season.season_number,
title: episode.title,
image: episode.images.thumbnail[0][episode.images.thumbnail[0].length - 1].source,
description: episode.description,
releaseDate: episode.episode_air_date,
isHD: episode.hd_flag,
isAdult: episode.is_mature,
isDubbed: episode.is_dubbed,
isSubbed: episode.is_subbed,
duration: episode.duration,
};
}),
],
};
episodes.push(season_episode_list);
resolve(season_episode_list);
});
promises.push(promise);
}));
await Promise.all(promises);
}
else {
const season = season_list[0];
const episode_response = await this.queryEpisodes(season, locale);
const episode_data = await episode_response.items;
episode_data?.map((episode) => {
return {
id: episode.id,
season_number: season.season_number,
title: episode.title,
image: episode.images.thumbnail[0][episode.images.thumbnail[0].length - 1].source,
description: episode.description,
releaseDate: episode.episode_air_date,
isHD: episode.hd_flag,
isAdult: episode.is_mature,
isDubbed: episode.is_dubbed,
isSubbed: episode.is_subbed,
duration: episode.duration,
};
});
if (episode_data != undefined) {
episodes?.push(...episode_data);
}
}
let returnData;
if (cr_data?.data) {
returnData = {
id: id,
title: cr_data?.data[0]?.title,
isAdult: cr_data?.data[0]?.title,
image: cr_data?.data[0]?.images.poster_tall[0][cr_data?.data[0].images.poster_tall[0].length - 1].source,
cover: cr_data?.data[0]?.images.poster_wide[0][cr_data?.data[0].images.poster_wide[0].length - 1].source,
description: cr_data?.data[0]?.description,
releaseDate: cr_data?.data[0]?.series_launch_year,
genres: genres,
season: cr_data?.data[0]?.season_tags[0].split("-")[0],
hasDub: cr_data?.data[0]?.is_dubbed,
hasSub: cr_data?.data[0]?.is_subbed,
rating: cr_ratings_data?.average,
recommendations: cr_recommendations_data?.map((rec) => {
return {
id: id,
title: rec.title,
isAdult: rec.is_mature,
image: rec.images.poster_tall[0][rec.images.poster_tall[0].length - 1].source,
popularity: 0,
cover: rec.images.poster_wide[0][rec.images.poster_wide[0].length - 1].source,
description: rec.description,
releaseDate: rec.series_launch_year,
hasDub: rec.is_dubbed,
hasSub: rec.is_subbed,
};
}),
episodes: episodes,
};
}
else {
returnData = {
id: id,
title: "undefined",
isAdult: "undefined",
image: "undefined",
cover: "undefined",
description: "undefined",
releaseDate: -1,
genres: [],
season: "undefined",
hasDub: false,
hasSub: false,
rating: cr_ratings_data?.average,
recommendations: cr_recommendations_data?.map((rec) => {
return {
id: id,
title: rec.title,
isAdult: rec.is_mature,
image: rec.images.poster_tall[0][rec.images.poster_tall[0].length - 1].source,
popularity: 0,
cover: rec.images.poster_wide[0][rec.images.poster_wide[0].length - 1].source,
description: rec.description,
releaseDate: rec.series_launch_year,
hasDub: rec.is_dubbed,
hasSub: rec.is_subbed,
};
}),
episodes: episodes,
};
}
return returnData;
}
/**
* @param episodeId The episode ID of the show.
* @param locale The locale of the episode. For example, "en-US".
*/
async getSources(episodeId, locale) {
const req = new promise_request_1.default(`${this.api}/content/v2/cms/objects/${episodeId}?locale=${locale}`, {
headers: {
Authorization: `Bearer ${this.accessToken}`,
Referer: this.main
},
});
const temp_response = await req.request().catch((err) => {
throw new Error(`Request to ${this.api}/content/v2/cms/objects/${episodeId}?locale=${locale} failed.\nBearer: ${this.accessToken}; Referer: ${this.main}`);
});
const temp_data = temp_response.json().data[0];
const episode_req = new promise_request_1.default(`${this.api}${temp_data.streams_link}?locale=${locale}`, {
headers: {
Authorization: `Bearer ${this.accessToken}`,
Referer: this.main
},
});
const episode_response = await episode_req.request().catch((err) => {
throw new Error(`Request to ${this.api}${temp_data.streams_link}?locale=${locale} failed.\nBearer: ${this.accessToken}; Referer: ${this.main}`);
});
const episode_data = episode_response.json();
const sources = [];
const m3u8_req = new promise_request_1.default(episode_data.data[0].vo_adaptive_hls[""].url, {
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
Referer: this.main,
},
});
const m3u8Urls = await m3u8_req.request().catch((err) => {
throw new Error(`Request to ${episode_data.data[0].vo_adaptive_hls[""].url}; User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36"; Referer: ${this.main}`);
});
const videoList = m3u8Urls.text().split("#EXT-X-STREAM-INF:");
for (const video of videoList ?? []) {
if (!video.includes("m3u8"))
continue;
const url = video.split("\n")[1];
const quality = video.split("RESOLUTION=")[1].split(",")[0].split("x")[1];
sources.push({
url: url,
quality: `${quality}p`,
isM3U8: true,
});
}
sources.push({
quality: "auto",
url: episode_data.data[0].vo_adaptive_hls[""].url,
isM3U8: true,
});
let subtitles = [];
for (var key of Object.keys(episode_data.meta.subtitles)) {
subtitles.push({
url: episode_data.meta.subtitles[key].url,
lang: key,
format: episode_data.meta.subtitles[key].format,
});
}
return {
sources: sources,
subtitles: subtitles,
};
}
}
exports.default = Cronchy;
//# sourceMappingURL=Cronchy.js.map
;