API switching

This commit is contained in:
AnimeDL 2023-12-29 21:14:12 -08:00
parent 942f673934
commit ca01c04961
10 changed files with 314 additions and 160 deletions

View file

@ -1,3 +1,5 @@
import { Images } from './crunchyEpisodeList';
export interface CrunchyAndroidEpisodes {
__class__: string;
__href__: string;
@ -5,13 +7,13 @@ export interface CrunchyAndroidEpisodes {
__links__: Actions;
__actions__: Actions;
total: number;
items: CrunchyEpisode[];
items: CrunchyAndroidEpisode[];
}
export interface Actions {
}
export interface CrunchyEpisode {
export interface CrunchyAndroidEpisode {
__class__: string;
__href__: string;
__resource_key__: string;
@ -19,7 +21,7 @@ export interface CrunchyEpisode {
__actions__: Actions;
playback: string;
id: string;
channel_id: string;
channel_id: ChannelID;
series_id: string;
series_title: string;
series_slug_title: string;
@ -37,19 +39,19 @@ export interface CrunchyEpisode {
next_episode_id: string;
next_episode_title: string;
hd_flag: boolean;
maturity_ratings: string[];
maturity_ratings: MaturityRating[];
extended_maturity_rating: Actions;
is_mature: boolean;
mature_blocked: boolean;
episode_air_date: string;
upload_date: string;
availability_starts: string;
availability_ends: string;
episode_air_date: Date;
upload_date: Date;
availability_starts: Date;
availability_ends: Date;
eligible_region: string;
available_date: Date;
free_available_date: string;
free_available_date: Date;
premium_date: Date;
premium_available_date: string;
premium_available_date: Date;
is_subbed: boolean;
is_dubbed: boolean;
is_clip: boolean;
@ -57,13 +59,13 @@ export interface CrunchyEpisode {
seo_description: string;
season_tags: string[];
available_offline: boolean;
subtitle_locales: string[];
subtitle_locales: Locale[];
availability_notes: string;
audio_locale: string;
audio_locale: Locale;
versions: Version[];
closed_captions_available: boolean;
identifier: string;
media_type: string;
media_type: MediaType;
slug: string;
images: Images;
duration_ms: number;
@ -76,21 +78,17 @@ export interface CrunchyEpisode {
}
export interface Links {
'episode/channel': EpisodeChannel;
'episode/next_episode': EpisodeChannel;
'episode/season': EpisodeChannel;
'episode/series': EpisodeChannel;
streams: EpisodeChannel;
'episode/channel': Link;
'episode/next_episode': Link;
'episode/season': Link;
'episode/series': Link;
streams: Link;
}
export interface EpisodeChannel {
export interface Link {
href: string;
}
export interface Images {
thumbnail: Array<Thumbnail[]>;
}
export interface Thumbnail {
width: number;
height: number;
@ -117,6 +115,18 @@ export enum Locale {
jaJP = 'ja-JP',
}
export enum MediaType {
Episode = 'episode',
}
export enum ChannelID {
Crunchyroll = 'crunchyroll',
}
export enum MaturityRating {
Tv14 = 'TV-14',
}
export interface Version {
audio_locale: Locale;
guid: string;

View file

@ -5,8 +5,8 @@ export interface CrunchyAndroidStreams {
__links__: Links;
__actions__: Actions;
media_id: string;
audio_locale: string;
subtitles: { [key: string]: Subtitle };
audio_locale: Locale;
subtitles: Subtitles;
closed_captions: Actions;
streams: Streams;
bifs: string[];
@ -14,6 +14,26 @@ export interface CrunchyAndroidStreams {
captions: Actions;
}
export interface Subtitles {
'': Subtitle;
'en-US'?: Subtitle;
'es-LA'?: Subtitle;
'es-419'?: Subtitle;
'es-ES'?: Subtitle;
'pt-BR'?: Subtitle;
'fr-FR'?: Subtitle;
'de-DE'?: Subtitle;
'ar-ME'?: Subtitle;
'ar-SA'?: Subtitle;
'it-IT'?: Subtitle;
'ru-RU'?: Subtitle;
'tr-TR'?: Subtitle;
'hi-IN'?: Subtitle;
'zh-CN'?: Subtitle;
'ko-KR'?: Subtitle;
'ja-JP'?: Subtitle;
}
export interface Actions {
}
@ -30,7 +50,7 @@ export interface Streams {
}
export interface Download {
hardsub_locale: string;
hardsub_locale: Locale;
hardsub_lang?: string;
url: string;
}
@ -40,13 +60,13 @@ export interface Urls {
}
export interface Subtitle {
locale: string;
locale: Locale;
url: string;
format: string;
}
export interface Version {
audio_locale: string;
audio_locale: Locale;
guid: string;
original: boolean;
variant: string;
@ -54,3 +74,23 @@ export interface Version {
media_guid: string;
is_premium_only: boolean;
}
export enum Locale {
default = '',
enUS = 'en-US',
esLA = 'es-LA',
es419 = 'es-419',
esES = 'es-ES',
ptBR = 'pt-BR',
frFR = 'fr-FR',
deDE = 'de-DE',
arME = 'ar-ME',
arSA = 'ar-SA',
itIT = 'it-IT',
ruRU = 'ru-RU',
trTR = 'tr-TR',
hiIN = 'hi-IN',
zhCN = 'zh-CN',
koKR = 'ko-KR',
jaJP = 'ja-JP',
}

View file

@ -1,3 +1,5 @@
import { Links } from './crunchyAndroidEpisodes';
export interface CrunchyEpisodeList {
total: number;
data: CrunchyEpisode[];
@ -41,27 +43,28 @@ export interface CrunchyEpisode {
listing_id: string;
episode_air_date: Date;
slug: string;
available_date: null;
available_date: Date;
subtitle_locales: Locale[];
slug_title: string;
available_offline: boolean;
description: string;
is_subbed: boolean;
premium_date: null;
premium_date: Date;
upload_date: Date;
season_slug_title: string;
closed_captions_available: boolean;
episode_number: number;
season_tags: any[];
maturity_ratings: MaturityRating[];
streams_link: string;
streams_link?: string;
mature_blocked: boolean;
is_clip: boolean;
hd_flag: boolean;
hide_season_title?: boolean;
hide_season_number?: boolean;
isSelected?: boolean;
seq_id: string;
hide_season_title?: boolean;
hide_season_number?: boolean;
isSelected?: boolean;
seq_id: string;
__links__?: Links;
}
export enum Locale {
@ -127,5 +130,5 @@ export interface Version {
}
export interface Meta {
versions_considered: boolean;
versions_considered?: boolean;
}

View file

@ -31,13 +31,15 @@ export type CrunchyDownloadOptions = {
dlVideoOnce: boolean,
skipmux?: boolean,
syncTiming: boolean,
apiType: 'web' | 'android'
}
export type CurnchyMultiDownload = {
export type CrunchyMultiDownload = {
dubLang: string[],
all?: boolean,
but?: boolean,
e?: string
e?: string,
crapi: 'web' | 'android'
}
export type CrunchyMuxOptions = {

View file

@ -1,7 +1,7 @@
// Generated by https://quicktype.io
export interface PlaybackData {
total: number;
data: { [key: string]: { [key: string]: StreamDetails } };
data: [{ [key: string]: { [key: string]: StreamDetails } }];
meta: Meta;
}

View file

@ -26,21 +26,23 @@ import getKeys from './modules/cr_widevine';
import { domain, api } from './modules/module.api-urls';
import * as reqModule from './modules/module.req';
import { CrunchySearch } from './@types/crunchySearch';
//import { CrunchyEpisodeList, CrunchyEpisode } from './@types/crunchyEpisodeList';
import { CrunchyDownloadOptions, CrunchyEpMeta, CrunchyMuxOptions, CurnchyMultiDownload, DownloadedMedia, ParseItem, SeriesSearch, SeriesSearchItem } from './@types/crunchyTypes';
import { CrunchyEpisodeList, CrunchyEpisode } from './@types/crunchyEpisodeList';
import { CrunchyDownloadOptions, CrunchyEpMeta, CrunchyMuxOptions, CrunchyMultiDownload, DownloadedMedia, ParseItem, SeriesSearch, SeriesSearchItem } from './@types/crunchyTypes';
import { ObjectInfo } from './@types/objectInfo';
import parseFileName, { Variable } from './modules/module.filename';
//import { PlaybackData } from './@types/playbackData';
import { PlaybackData } from './@types/playbackData';
import { downloaded } from './modules/module.downloadArchive';
import parseSelect from './modules/module.parseSelect';
import { AvailableFilenameVars, getDefault } from './modules/module.args';
import { AuthData, AuthResponse, Episode, ResponseBase, SearchData, SearchResponse, SearchResponseItem } from './@types/messageHandler';
import { ServiceClass } from './@types/serviceClassInterface';
import { CrunchyAndroidStreams } from './@types/crunchyAndroidStreams';
import { CrunchyAndroidEpisodes, CrunchyEpisode } from './@types/crunchyAndroidEpisodes';
import { CrunchyAndroidEpisodes } from './@types/crunchyAndroidEpisodes';
import { parse } from './modules/module.transform-mpd';
import { CrunchyAndroidObject } from './@types/crunchyAndroidObject';
import util from 'util';
export type sxItem = {
language: langsData.LanguageItem,
path: string,
@ -112,7 +114,7 @@ export default class Crunchy implements ServiceClass {
const selected = await this.downloadFromSeriesID(argv.series, { ...argv });
if (selected.isOk) {
for (const select of selected.value) {
if (!(await this.downloadEpisode(select, {...argv, skipsubs: false }, true))) {
if (!(await this.downloadEpisode(select, {...argv, skipsubs: false, apiType: argv.crapi }, true))) {
console.error(`Unable to download selected episode ${select.episodeNumber}`);
return false;
}
@ -130,10 +132,10 @@ export default class Crunchy implements ServiceClass {
console.info('One show can only be downloaded with one dub. Use --srz instead.');
}
argv.dubLang = [argv.dubLang[0]];
const selected = await this.getSeasonById(argv.s, argv.numbers, argv.e, argv.but, argv.all);
const selected = await this.getSeasonById(argv.s, argv.numbers, argv.e, argv.but, argv.all, argv.crapi);
if (selected.isOk) {
for (const select of selected.value) {
if (!(await this.downloadEpisode(select, {...argv, skipsubs: false }))) {
if (!(await this.downloadEpisode(select, {...argv, skipsubs: false, apiType: argv.crapi }))) {
console.error(`Unable to download selected episode ${select.episodeNumber}`);
return false;
}
@ -143,9 +145,9 @@ export default class Crunchy implements ServiceClass {
}
else if(argv.e){
await this.refreshToken();
const selected = await this.getObjectById(argv.e, false);
const selected = await this.getObjectById(argv.crapi, argv.e, false);
for (const select of selected as Partial<CrunchyEpMeta>[]) {
if (!(await this.downloadEpisode(select as CrunchyEpMeta, {...argv, skipsubs: false }))) {
if (!(await this.downloadEpisode(select as CrunchyEpMeta, {...argv, skipsubs: false, apiType: argv.crapi }))) {
console.error(`Unable to download selected episode ${select.episodeNumber}`);
return false;
}
@ -153,9 +155,9 @@ export default class Crunchy implements ServiceClass {
return true;
} else if (argv.extid) {
await this.refreshToken();
const selected = await this.getObjectById(argv.extid, false, true);
const selected = await this.getObjectById(argv.crapi, argv.extid, false, true);
for (const select of selected as Partial<CrunchyEpMeta>[]) {
if (!(await this.downloadEpisode(select as CrunchyEpMeta, {...argv, skipsubs: false }))) {
if (!(await this.downloadEpisode(select as CrunchyEpMeta, {...argv, skipsubs: false, apiType: argv.crapi }))) {
console.error(`Unable to download selected episode ${select.episodeNumber}`);
return false;
}
@ -352,6 +354,7 @@ export default class Crunchy implements ServiceClass {
this.cmsToken.cms.bucket,
'/index?',
new URLSearchParams({
'preferred_audio_language': 'ja-JP',
'Policy': this.cmsToken.cms.policy,
'Signature': this.cmsToken.cms.signature,
'Key-Pair-Id': this.cmsToken.cms.key_pair_id,
@ -745,7 +748,7 @@ export default class Crunchy implements ServiceClass {
console.info(` Total results: ${newlyAddedResults.total} (Page: ${pageCur}/${pageMax})`);
}
public async getSeasonById(id: string, numbers: number, e: string|undefined, but: boolean, all: boolean) : Promise<ResponseBase<CrunchyEpMeta[]>> {
public async getSeasonById(id: string, numbers: number, e: string|undefined, but: boolean, all: boolean, apiType: 'web' | 'android') : Promise<ResponseBase<CrunchyEpMeta[]>> {
if(!this.cmsToken.cms){
console.error('Authentication required!');
return { isOk: false, reason: new Error('Authentication required') };
@ -768,27 +771,42 @@ export default class Crunchy implements ServiceClass {
const showInfo = JSON.parse(showInfoReq.res.body);
this.logObject(showInfo.data[0], 0);
let episodeList = { total: 0, data: [], meta: {} } as CrunchyEpisodeList;
//get episode info
const reqEpsListOpts = [
api.beta_cms,
this.cmsToken.cms.bucket,
'/episodes?',
new URLSearchParams({
'season_id': id,
'Policy': this.cmsToken.cms.policy,
'Signature': this.cmsToken.cms.signature,
'Key-Pair-Id': this.cmsToken.cms.key_pair_id,
}),
].join('');
const reqEpsList = await this.req.getData(reqEpsListOpts, AuthHeaders);
//const reqEpsList = await this.req.getData(`${api.cms}/seasons/${id}/episodes?preferred_audio_language=ja-JP`, AuthHeaders);
if(!reqEpsList.ok || !reqEpsList.res){
console.error('Episode List Request FAILED!');
return { isOk: false, reason: new Error('Episode List request failed. No more information provided.') };
if (apiType == 'android') {
const reqEpsListOpts = [
api.beta_cms,
this.cmsToken.cms.bucket,
'/episodes?',
new URLSearchParams({
'preferred_audio_language': 'ja-JP',
'season_id': id,
'Policy': this.cmsToken.cms.policy,
'Signature': this.cmsToken.cms.signature,
'Key-Pair-Id': this.cmsToken.cms.key_pair_id,
}),
].join('');
const reqEpsList = await this.req.getData(reqEpsListOpts, AuthHeaders);
if(!reqEpsList.ok || !reqEpsList.res){
console.error('Episode List Request FAILED!');
return { isOk: false, reason: new Error('Episode List request failed. No more information provided.') };
}
//CrunchyEpisodeList
const episodeListAndroid = JSON.parse(reqEpsList.res.body) as CrunchyAndroidEpisodes;
episodeList = {
total: episodeListAndroid.total,
data: episodeListAndroid.items,
meta: {}
};
} else {
const reqEpsList = await this.req.getData(`${api.cms}/seasons/${id}/episodes?preferred_audio_language=ja-JP`, AuthHeaders);
if(!reqEpsList.ok || !reqEpsList.res){
console.error('Episode List Request FAILED!');
return { isOk: false, reason: new Error('Episode List request failed. No more information provided.') };
}
//CrunchyEpisodeList
episodeList = JSON.parse(reqEpsList.res.body) as CrunchyEpisodeList;
}
//CrunchyEpisodeList
const episodeList = JSON.parse(reqEpsList.res.body) as CrunchyAndroidEpisodes;
const epNumList: {
ep: number[],
@ -804,7 +822,7 @@ export default class Crunchy implements ServiceClass {
const doEpsFilter = parseSelect(e as string);
const selectedMedia: CrunchyEpMeta[] = [];
episodeList.items.forEach((item) => {
episodeList.data.forEach((item) => {
item.hide_season_title = true;
if(item.season_title == '' && item.series_title != ''){
item.season_title = item.series_title;
@ -853,12 +871,18 @@ export default class Crunchy implements ServiceClass {
image: images[Math.floor(images.length / 2)].source
};
// Check for streams_link and update playback var if needed
if (item.__links__.streams.href) {
if (item.__links__?.streams.href) {
epMeta.data[0].playback = item.__links__.streams.href;
if(!item.playback) {
item.playback = item.__links__.streams.href;
}
}
if (item.streams_link) {
epMeta.data[0].playback = item.streams_link;
if(!item.playback) {
item.playback = item.streams_link;
}
}
if (item.versions) {
epMeta.data[0].versions = item.versions;
}
@ -906,7 +930,7 @@ export default class Crunchy implements ServiceClass {
return true;
}
public async getObjectById(e?: string, earlyReturn?: boolean, external_id?: boolean): Promise<ObjectInfo|Partial<CrunchyEpMeta>[]|undefined> {
public async getObjectById(apiType: 'web' | 'android', e?: string, earlyReturn?: boolean, external_id?: boolean): Promise<ObjectInfo|Partial<CrunchyEpMeta>[]|undefined> {
if(!this.cmsToken.cms){
console.error('Authentication required!');
return [];
@ -923,6 +947,7 @@ export default class Crunchy implements ServiceClass {
'/channels/crunchyroll/objects',
'?',
new URLSearchParams({
'preferred_audio_language': 'ja-JP',
'external_id': ob,
'Policy': this.cmsToken.cms.policy,
'Signature': this.cmsToken.cms.signature,
@ -965,38 +990,53 @@ export default class Crunchy implements ServiceClass {
};
// reqs
const objectReqOpts = [
api.beta_cms,
this.cmsToken.cms.bucket,
'/objects/',
doEpsFilter.values.join(','),
'?',
new URLSearchParams({
'Policy': this.cmsToken.cms.policy,
'Signature': this.cmsToken.cms.signature,
'Key-Pair-Id': this.cmsToken.cms.key_pair_id,
}),
].join('');
const objectReq = await this.req.getData(objectReqOpts, AuthHeaders);
//const objectReq = await this.req.getData(`${api.cms}/objects/${doEpsFilter.values.join(',')}?preferred_audio_language=ja-JP`, AuthHeaders);
if(!objectReq.ok || !objectReq.res){
console.error('Objects Request FAILED!');
if(objectReq.error && objectReq.error.res && objectReq.error.res.body){
const objectInfo = JSON.parse(objectReq.error.res.body as string);
console.info('Body:', JSON.stringify(objectInfo, null, '\t'));
objectInfo.error = true;
return objectInfo;
let objectInfo: ObjectInfo = { total: 0, data: [], meta: {} };
if (apiType == 'android') {
const objectReqOpts = [
api.beta_cms,
this.cmsToken.cms.bucket,
'/objects/',
doEpsFilter.values.join(','),
'?',
new URLSearchParams({
'preferred_audio_language': 'ja-JP',
'Policy': this.cmsToken.cms.policy,
'Signature': this.cmsToken.cms.signature,
'Key-Pair-Id': this.cmsToken.cms.key_pair_id,
}),
].join('');
const objectReq = await this.req.getData(objectReqOpts, AuthHeaders);
if(!objectReq.ok || !objectReq.res){
console.error('Objects Request FAILED!');
if(objectReq.error && objectReq.error.res && objectReq.error.res.body){
const objectInfo = JSON.parse(objectReq.error.res.body as string);
console.info('Body:', JSON.stringify(objectInfo, null, '\t'));
objectInfo.error = true;
return objectInfo;
}
return [];
}
return [];
const objectInfoAndroid = JSON.parse(objectReq.res.body) as CrunchyAndroidObject;
objectInfo = {
total: objectInfoAndroid.total,
data: objectInfoAndroid.items,
meta: {}
};
} else {
const objectReq = await this.req.getData(`${api.cms}/objects/${doEpsFilter.values.join(',')}?preferred_audio_language=ja-JP`, AuthHeaders);
if(!objectReq.ok || !objectReq.res){
console.error('Objects Request FAILED!');
if(objectReq.error && objectReq.error.res && objectReq.error.res.body){
const objectInfo = JSON.parse(objectReq.error.res.body as string);
console.info('Body:', JSON.stringify(objectInfo, null, '\t'));
objectInfo.error = true;
return objectInfo;
}
return [];
}
objectInfo = JSON.parse(objectReq.res.body) as ObjectInfo;
}
//const objectInfo = JSON.parse(objectReq.res.body) as ObjectInfo;
const objectInfoAndroid = JSON.parse(objectReq.res.body) as CrunchyAndroidObject;
const objectInfo: ObjectInfo = {
total: objectInfoAndroid.total,
data: objectInfoAndroid.items,
meta: {}
};
if(earlyReturn){
return objectInfo;
}
@ -1134,29 +1174,15 @@ export default class Crunchy implements ServiceClass {
mediaId = mediaId.split(':')[1];
// /cms/v2/BUCKET/crunchyroll/videos/MEDIAID/streams
const videoStreamsReq = [
api.beta_cms,
`${this.cmsToken.cms.bucket}/videos/${mediaId}/streams`,
'?',
new URLSearchParams({
streams: 'all',
textType: 'all',
'Policy': this.cmsToken.cms.policy,
'Signature': this.cmsToken.cms.signature,
'Key-Pair-Id': this.cmsToken.cms.key_pair_id,
}),
].join('');
let playbackReq = await this.req.getData(videoStreamsReq as string, AuthHeaders);
//console.info(playbackReq.res.body);
//let playbackReq = await this.req.getData(`${api.cms}/videos/${mediaId}/streams`, AuthHeaders);
if(!playbackReq.ok || !playbackReq.res){
console.error('Request Stream URLs FAILED! Attempting fallback');
let pbData = { total: 0, data: {}, meta: {} } as PlaybackData;
if (options.apiType == 'android') {
const videoStreamsReq = [
domain.api_beta,
mMeta.playback,
api.beta_cms,
`${this.cmsToken.cms.bucket}/videos/${mediaId}/streams`,
'?',
new URLSearchParams({
'preferred_audio_language': 'ja-JP',
streams: 'all',
textType: 'all',
'Policy': this.cmsToken.cms.policy,
@ -1164,16 +1190,42 @@ export default class Crunchy implements ServiceClass {
'Key-Pair-Id': this.cmsToken.cms.key_pair_id,
}),
].join('');
playbackReq = await this.req.getData(videoStreamsReq as string, AuthHeaders);
//playbackReq = await this.req.getData(`${domain.api_beta}${mMeta.playback}`, AuthHeaders);
if(!playbackReq.ok || !playbackReq.res){
console.error('Fallback Request Stream URLs FAILED!');
return undefined;
}
}
//const pbData = JSON.parse(playbackReq.res.body) as PlaybackData;
const pbData = JSON.parse(playbackReq.res.body) as CrunchyAndroidStreams;
let playbackReq = await this.req.getData(videoStreamsReq as string, AuthHeaders);
if(!playbackReq.ok || !playbackReq.res){
console.error('Request Stream URLs FAILED! Attempting fallback');
playbackReq = await this.req.getData(videoStreamsReq as string, AuthHeaders);
if(!playbackReq.ok || !playbackReq.res){
console.error('Fallback Request Stream URLs FAILED!');
return undefined;
}
}
const pbDataAndroid = JSON.parse(playbackReq.res.body) as CrunchyAndroidStreams;
pbData = {
total: 0,
data: [pbDataAndroid.streams],
meta: {
audio_locale: pbDataAndroid.audio_locale,
bifs: pbDataAndroid.bifs,
captions: pbDataAndroid.captions,
closed_captions: pbDataAndroid.closed_captions,
media_id: pbDataAndroid.media_id,
subtitles: pbDataAndroid.subtitles,
versions: pbDataAndroid.versions
}
};
} else {
let playbackReq = await this.req.getData(`${api.cms}/videos/${mediaId}/streams`, AuthHeaders);
if(!playbackReq.ok || !playbackReq.res){
console.error('Request Stream URLs FAILED! Attempting fallback');
playbackReq = await this.req.getData(`${domain.api_beta}${mMeta.playback}`, AuthHeaders);
if(!playbackReq.ok || !playbackReq.res){
console.error('Fallback Request Stream URLs FAILED!');
return undefined;
}
}
pbData = JSON.parse(playbackReq.res.body) as PlaybackData;
}
variables.push(...([
['title', medias.episodeTitle, true],
@ -1193,11 +1245,11 @@ export default class Crunchy implements ServiceClass {
let streams: any[] = [];
let hsLangs: string[] = [];
const pbStreams = pbData.streams;
const pbStreams = pbData.data[0];
for(const s of Object.keys(pbStreams)){
//if(s.match(/hls/) && !s.match(/drm/) && !s.match(/trailer/)) {
if((s.match(/hls/) || s.match(/dash/)) && !s.match(/trailer/)) {
if((s.match(/hls/) || s.match(/dash/)) && !(s.match(/hls/) && s.match(/drm/)) && !s.match(/trailer/)) {
const pb = Object.values(pbStreams[s]).map(v => {
v.hardsub_lang = v.hardsub_locale
? langsData.fixAndFindCrLC(v.hardsub_locale).locale
@ -1219,7 +1271,7 @@ export default class Crunchy implements ServiceClass {
return undefined;
}
const audDub = langsData.findLang(langsData.fixLanguageTag(pbData.audio_locale as string) || '').code;
const audDub = langsData.findLang(langsData.fixLanguageTag(pbData.meta.audio_locale as string) || '').code;
hsLangs = langsData.sortTags(hsLangs);
streams = streams.map((s) => {
@ -1738,8 +1790,8 @@ export default class Crunchy implements ServiceClass {
}
if(!options.skipsubs && options.dlsubs.indexOf('none') == -1){
if(pbData.subtitles && Object.values(pbData.subtitles).length > 0){
const subsData = Object.values(pbData.subtitles);
if(pbData.meta.subtitles && Object.values(pbData.meta.subtitles).length > 0){
const subsData = Object.values(pbData.meta.subtitles);
const subsDataMapped = subsData.map((s) => {
const subLang = langsData.fixAndFindCrLC(s.locale);
return {
@ -1880,7 +1932,7 @@ export default class Crunchy implements ServiceClass {
merger.cleanUp();
}
public async listSeriesID(id: string): Promise<{ list: Episode[], data: Record<string, {
public async listSeriesID(id: string, apiType: 'android' | 'web'): Promise<{ list: Episode[], data: Record<string, {
items: CrunchyEpisode[];
langs: langsData.LanguageItem[];
}>}> {
@ -1897,7 +1949,7 @@ export default class Crunchy implements ServiceClass {
for(const season of Object.keys(result) as unknown as number[]) {
for (const key of Object.keys(result[season])) {
const s = result[season][key];
(await this.getSeasonDataById(s))?.items?.forEach(episode => {
(await this.getSeasonDataById(s, apiType))?.data?.forEach(episode => {
//TODO: Make sure the below code is ok
//Prepare the episode array
let item;
@ -1987,8 +2039,8 @@ export default class Crunchy implements ServiceClass {
})};
}
public async downloadFromSeriesID(id: string, data: CurnchyMultiDownload) : Promise<ResponseBase<CrunchyEpMeta[]>> {
const { data: episodes } = await this.listSeriesID(id);
public async downloadFromSeriesID(id: string, data: CrunchyMultiDownload) : Promise<ResponseBase<CrunchyEpMeta[]>> {
const { data: episodes } = await this.listSeriesID(id, data.crapi);
console.info('');
console.info('-'.repeat(30));
console.info('');
@ -2133,7 +2185,7 @@ export default class Crunchy implements ServiceClass {
return seasonsList;
}
public async getSeasonDataById(item: SeriesSearchItem, log = false){
public async getSeasonDataById(item: SeriesSearchItem, apiType: 'android' | 'web', log = false){
if(!this.cmsToken.cms){
console.error('Authentication required!');
return;
@ -2155,28 +2207,43 @@ export default class Crunchy implements ServiceClass {
const showInfo = JSON.parse(showInfoReq.res.body);
if (log)
this.logObject(showInfo, 0);
let episodeList = { total: 0, data: [], meta: {} } as CrunchyEpisodeList;
//get episode info
const reqEpsListOpts = [
api.beta_cms,
this.cmsToken.cms.bucket,
'/episodes?',
new URLSearchParams({
'season_id': item.id,
'Policy': this.cmsToken.cms.policy,
'Signature': this.cmsToken.cms.signature,
'Key-Pair-Id': this.cmsToken.cms.key_pair_id,
}),
].join('');
const reqEpsList = await this.req.getData(reqEpsListOpts, AuthHeaders);
//const reqEpsList = await this.req.getData(`${api.cms}/seasons/${item.id}/episodes?preferred_audio_language=ja-JP`, AuthHeaders);
if(!reqEpsList.ok || !reqEpsList.res){
console.error('Episode List Request FAILED!');
return;
if (apiType == 'android') {
const reqEpsListOpts = [
api.beta_cms,
this.cmsToken.cms.bucket,
'/episodes?',
new URLSearchParams({
'preferred_audio_language': 'ja-JP',
'season_id': item.id,
'Policy': this.cmsToken.cms.policy,
'Signature': this.cmsToken.cms.signature,
'Key-Pair-Id': this.cmsToken.cms.key_pair_id,
}),
].join('');
const reqEpsList = await this.req.getData(reqEpsListOpts, AuthHeaders);
if(!reqEpsList.ok || !reqEpsList.res){
console.error('Episode List Request FAILED!');
return;
}
//CrunchyEpisodeList
const episodeListAndroid = JSON.parse(reqEpsList.res.body) as CrunchyAndroidEpisodes;
episodeList = {
total: episodeListAndroid.total,
data: episodeListAndroid.items,
meta: {}
};
} else {
const reqEpsList = await this.req.getData(`${api.cms}/seasons/${item.id}/episodes?preferred_audio_language=ja-JP`, AuthHeaders);
if(!reqEpsList.ok || !reqEpsList.res){
console.error('Episode List Request FAILED!');
return;
}
//CrunchyEpisodeList
episodeList = JSON.parse(reqEpsList.res.body) as CrunchyEpisodeList;
}
//CrunchyEpisodeList
const episodeList = JSON.parse(reqEpsList.res.body) as CrunchyAndroidEpisodes;
if(episodeList.total < 1){
console.info(' Season is empty!');

View file

@ -64,6 +64,7 @@ let argvC: {
_: (string | number)[];
$0: string;
dlVideoOnce: boolean;
crapi: 'android' | 'web';
removeBumpers: boolean;
originalFontSize: boolean;
keepAllVideos: boolean;

View file

@ -203,6 +203,20 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
default: false
}
},
{
name: 'crapi',
describe: 'Selects the API type for Crunchyroll',
type: 'string',
group: 'dl',
service: ['crunchy'],
docDescribe: 'If set to Android, it has lower quality, but Non-DRM streams,'
+ '\nIf set to Web, it has a higher quality adaptive stream, but everything is DRM.',
usage: '',
choices: ['android', 'web'],
default: {
default: 'android'
}
},
{
name: 'removeBumpers',
describe: 'Remove bumpers from final video',

View file

@ -45,6 +45,7 @@
"@babel/core": "^7.22.9",
"@babel/plugin-syntax-flow": "^7.22.5",
"@babel/plugin-transform-react-jsx": "^7.22.5",
"@types/xmldom": "^0.1.34",
"cheerio": "1.0.0-rc.12",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
@ -65,6 +66,7 @@
"sei-helper": "^3.3.0",
"typescript-eslint": "0.0.1-alpha.0",
"ws": "^8.13.0",
"xmldom": "^0.6.0",
"yaml": "^2.3.1",
"yargs": "^17.7.2"
},

View file

@ -14,6 +14,9 @@ dependencies:
'@babel/plugin-transform-react-jsx':
specifier: ^7.22.5
version: 7.22.5(@babel/core@7.22.9)
'@types/xmldom':
specifier: ^0.1.34
version: 0.1.34
cheerio:
specifier: 1.0.0-rc.12
version: 1.0.0-rc.12
@ -74,6 +77,9 @@ dependencies:
ws:
specifier: ^8.13.0
version: 8.13.0
xmldom:
specifier: ^0.6.0
version: 0.6.0
yaml:
specifier: ^2.3.1
version: 2.3.1
@ -1982,6 +1988,10 @@ packages:
'@types/node': 18.15.11
dev: true
/@types/xmldom@0.1.34:
resolution: {integrity: sha512-7eZFfxI9XHYjJJuugddV6N5YNeXgQE1lArWOcd1eCOKWb/FGs5SIjacSYuEJuwhsGS3gy4RuZ5EUIcqYscuPDA==}
dev: false
/@types/yargs-parser@21.0.0:
resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
dev: true
@ -5681,6 +5691,11 @@ packages:
optional: true
dev: false
/xmldom@0.6.0:
resolution: {integrity: sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg==}
engines: {node: '>=10.0.0'}
dev: false
/y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}