mirror of
https://github.com/anidl/multi-downloader-nx.git
synced 2026-03-11 17:45:30 +00:00
added crunchyroll cbr video + 192 kbps audio download
This commit is contained in:
parent
71b7657208
commit
a297fd0309
10 changed files with 656 additions and 588 deletions
6
@types/crunchyTypes.d.ts
vendored
6
@types/crunchyTypes.d.ts
vendored
|
|
@ -6,8 +6,10 @@ import { CrunchyPlayStreams } from './enums';
|
|||
|
||||
export type CrunchyDownloadOptions = {
|
||||
hslang: string,
|
||||
kstream: number,
|
||||
cstream: keyof typeof CrunchyPlayStreams | 'none',
|
||||
// kstream: number,
|
||||
cstream: keyof typeof CrunchyPlayStreams,
|
||||
vstream: keyof typeof CrunchyPlayStreams,
|
||||
astream: keyof typeof CrunchyPlayStreams,
|
||||
novids?: boolean,
|
||||
noaudio?: boolean,
|
||||
x: number,
|
||||
|
|
|
|||
3
@types/playbackData.d.ts
vendored
3
@types/playbackData.d.ts
vendored
|
|
@ -1,7 +1,8 @@
|
|||
// Generated by https://quicktype.io
|
||||
export interface PlaybackData {
|
||||
total: number;
|
||||
data: { [key: string]: { [key: string]: StreamDetails } }[];
|
||||
vpb: { [key: string]: { [key: string]: StreamDetails } };
|
||||
apb: { [key: string]: { [key: string]: StreamDetails } };
|
||||
meta: Meta;
|
||||
}
|
||||
|
||||
|
|
|
|||
527
crunchy.ts
527
crunchy.ts
|
|
@ -56,7 +56,6 @@ export type sxItem = {
|
|||
|
||||
export default class Crunchy implements ServiceClass {
|
||||
public cfg: yamlCfg.ConfigObject;
|
||||
public api: 'android' | 'web';
|
||||
public locale: string;
|
||||
private token: Record<string, any>;
|
||||
private req: reqModule.Req;
|
||||
|
|
@ -70,7 +69,6 @@ export default class Crunchy implements ServiceClass {
|
|||
this.cfg = yamlCfg.loadCfg();
|
||||
this.token = yamlCfg.loadCRToken();
|
||||
this.req = new reqModule.Req(domain, debug, false, 'cr');
|
||||
this.api = 'android';
|
||||
this.locale = 'en-US';
|
||||
}
|
||||
|
||||
|
|
@ -81,7 +79,6 @@ export default class Crunchy implements ServiceClass {
|
|||
public async cli() {
|
||||
console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`);
|
||||
const argv = yargs.appArgv(this.cfg.cli);
|
||||
this.api = argv.crapi;
|
||||
this.locale = argv.locale;
|
||||
if (argv.debug)
|
||||
this.debug = true;
|
||||
|
|
@ -249,44 +246,32 @@ export default class Crunchy implements ServiceClass {
|
|||
// seasons list
|
||||
let episodeList = { total: 0, data: [], meta: {} } as CrunchyEpisodeList;
|
||||
//get episode info
|
||||
if (this.api == 'android') {
|
||||
const reqEpsListOpts = [
|
||||
api.beta_cms,
|
||||
this.cmsToken.cms.bucket,
|
||||
'/episodes?',
|
||||
new URLSearchParams({
|
||||
'force_locale': '',
|
||||
'preferred_audio_language': 'ja-JP',
|
||||
'locale': this.locale,
|
||||
'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 = await reqEpsList.res.json() as CrunchyAndroidEpisodes;
|
||||
episodeList = {
|
||||
total: episodeListAndroid.total,
|
||||
data: episodeListAndroid.items,
|
||||
meta: {}
|
||||
};
|
||||
} else {
|
||||
const reqEpsList = await this.req.getData(`${api.cms}/seasons/${id}/episodes?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, 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 = await reqEpsList.res.json() as CrunchyEpisodeList;
|
||||
|
||||
|
||||
const reqEpsListOpts = [
|
||||
api.cms_bucket,
|
||||
this.cmsToken.cms.bucket,
|
||||
'/episodes?',
|
||||
new URLSearchParams({
|
||||
'force_locale': '',
|
||||
'preferred_audio_language': 'ja-JP',
|
||||
'locale': this.locale,
|
||||
'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 = await reqEpsList.res.json() as CrunchyAndroidEpisodes;
|
||||
episodeList = {
|
||||
total: episodeListAndroid.total,
|
||||
data: episodeListAndroid.items,
|
||||
meta: {}
|
||||
};
|
||||
for (const item of episodeList.data) {
|
||||
// stringify each object, then a newline
|
||||
console.log(JSON.stringify(item));
|
||||
|
|
@ -514,7 +499,7 @@ export default class Crunchy implements ServiceClass {
|
|||
},
|
||||
useProxy: true
|
||||
};
|
||||
const profileReq = await this.req.getData(api.beta_profile, profileReqOptions);
|
||||
const profileReq = await this.req.getData(api.profile, profileReqOptions);
|
||||
if(!profileReq.ok || !profileReq.res){
|
||||
console.error('Get profile failed!');
|
||||
return false;
|
||||
|
|
@ -652,7 +637,7 @@ export default class Crunchy implements ServiceClass {
|
|||
},
|
||||
useProxy: true
|
||||
};
|
||||
const cmsTokenReq = await this.req.getData(api.beta_cmsToken, cmsTokenReqOpts);
|
||||
const cmsTokenReq = await this.req.getData(api.cmsToken, cmsTokenReqOpts);
|
||||
if(!cmsTokenReq.ok || !cmsTokenReq.res){
|
||||
console.error('Authentication CMS token failed!');
|
||||
return;
|
||||
|
|
@ -669,7 +654,7 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
// opts
|
||||
const indexReqOpts = [
|
||||
api.beta_cms,
|
||||
api.cms_bucket,
|
||||
this.cmsToken.cms.bucket,
|
||||
'/index?',
|
||||
new URLSearchParams({
|
||||
|
|
@ -1060,7 +1045,7 @@ export default class Crunchy implements ServiceClass {
|
|||
n: '25',
|
||||
start: (page ? (page-1)*25 : 0).toString(),
|
||||
}).toString();
|
||||
const newlyAddedReq = await this.req.getData(`${api.beta_browse}?${newlyAddedParams}`, newlyAddedReqOpts);
|
||||
const newlyAddedReq = await this.req.getData(`${api.browse}?${newlyAddedParams}`, newlyAddedReqOpts);
|
||||
if(!newlyAddedReq.ok || !newlyAddedReq.res){
|
||||
console.error('Get newly added FAILED!');
|
||||
return;
|
||||
|
|
@ -1103,42 +1088,32 @@ export default class Crunchy implements ServiceClass {
|
|||
|
||||
let episodeList = { total: 0, data: [], meta: {} } as CrunchyEpisodeList;
|
||||
//get episode info
|
||||
if (this.api == 'android') {
|
||||
const reqEpsListOpts = [
|
||||
api.beta_cms,
|
||||
this.cmsToken.cms.bucket,
|
||||
'/episodes?',
|
||||
new URLSearchParams({
|
||||
'force_locale': '',
|
||||
'preferred_audio_language': 'ja-JP',
|
||||
'locale': this.locale,
|
||||
'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 = await reqEpsList.res.json() as CrunchyAndroidEpisodes;
|
||||
episodeList = {
|
||||
total: episodeListAndroid.total,
|
||||
data: episodeListAndroid.items,
|
||||
meta: {}
|
||||
};
|
||||
} else {
|
||||
const reqEpsList = await this.req.getData(`${api.cms}/seasons/${id}/episodes?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, 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 = await reqEpsList.res.json() as CrunchyEpisodeList;
|
||||
const reqEpsListOpts = [
|
||||
api.cms_bucket,
|
||||
this.cmsToken.cms.bucket,
|
||||
'/episodes?',
|
||||
new URLSearchParams({
|
||||
'force_locale': '',
|
||||
'preferred_audio_language': 'ja-JP',
|
||||
'locale': this.locale,
|
||||
'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 = await reqEpsList.res.json() as CrunchyAndroidEpisodes;
|
||||
episodeList = {
|
||||
total: episodeListAndroid.total,
|
||||
data: episodeListAndroid.items,
|
||||
meta: {}
|
||||
};
|
||||
|
||||
const epNumList: {
|
||||
ep: number[],
|
||||
|
|
@ -1274,7 +1249,7 @@ export default class Crunchy implements ServiceClass {
|
|||
const objectIds = [];
|
||||
for (const ob of epFilter.values) {
|
||||
const extIdReqOpts = [
|
||||
api.beta_cms,
|
||||
api.cms_bucket,
|
||||
this.cmsToken.cms.bucket,
|
||||
'/channels/crunchyroll/objects',
|
||||
'?',
|
||||
|
|
@ -1330,53 +1305,38 @@ export default class Crunchy implements ServiceClass {
|
|||
|
||||
// reqs
|
||||
let objectInfo: ObjectInfo = { total: 0, data: [], meta: {} };
|
||||
if (this.api == 'android') {
|
||||
const objectReqOpts = [
|
||||
api.beta_cms,
|
||||
this.cmsToken.cms.bucket,
|
||||
'/objects/',
|
||||
doEpsFilter.values.join(','),
|
||||
'?',
|
||||
new URLSearchParams({
|
||||
'force_locale': '',
|
||||
'preferred_audio_language': 'ja-JP',
|
||||
'locale': this.locale,
|
||||
'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 = await objectReq.error.res.json();
|
||||
console.info('Body:', JSON.stringify(objectInfo, null, '\t'));
|
||||
objectInfo.error = true;
|
||||
return objectInfo;
|
||||
}
|
||||
return [];
|
||||
const objectReqOpts = [
|
||||
api.cms_bucket,
|
||||
this.cmsToken.cms.bucket,
|
||||
'/objects/',
|
||||
doEpsFilter.values.join(','),
|
||||
'?',
|
||||
new URLSearchParams({
|
||||
'force_locale': '',
|
||||
'preferred_audio_language': 'ja-JP',
|
||||
'locale': this.locale,
|
||||
'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 = await objectReq.error.res.json();
|
||||
console.info('Body:', JSON.stringify(objectInfo, null, '\t'));
|
||||
objectInfo.error = true;
|
||||
return objectInfo;
|
||||
}
|
||||
const objectInfoAndroid = await objectReq.res.json() as CrunchyAndroidObject;
|
||||
objectInfo = {
|
||||
total: objectInfoAndroid.total,
|
||||
data: objectInfoAndroid.items,
|
||||
meta: {}
|
||||
};
|
||||
} else {
|
||||
const objectReq = await this.req.getData(`${api.cms}/objects/${doEpsFilter.values.join(',')}?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders);
|
||||
if(!objectReq.ok || !objectReq.res){
|
||||
console.error('Objects Request FAILED!');
|
||||
if(objectReq.error && objectReq.error.res && objectReq.error.res.body){
|
||||
const objectInfo = await objectReq.error.res.json();
|
||||
console.info('Body:', JSON.stringify(objectInfo, null, '\t'));
|
||||
objectInfo.error = true;
|
||||
return objectInfo;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
objectInfo = await objectReq.res.json() as ObjectInfo;
|
||||
return [];
|
||||
}
|
||||
const objectInfoAndroid = await objectReq.res.json() as CrunchyAndroidObject;
|
||||
objectInfo = {
|
||||
total: objectInfoAndroid.total,
|
||||
data: objectInfoAndroid.items,
|
||||
meta: {}
|
||||
};
|
||||
|
||||
if(earlyReturn){
|
||||
return objectInfo;
|
||||
|
|
@ -1655,40 +1615,75 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
}
|
||||
|
||||
const pbData = { total: 0, data: [{}], meta: {} } as PlaybackData;
|
||||
const pbData = { total: 0, vpb: {}, apb: {}, meta: {} } as PlaybackData;
|
||||
|
||||
let playStream: CrunchyPlayStream | null = null;
|
||||
if (options.cstream !== 'none') {
|
||||
const playbackReq = await this.req.getData(`https://www.crunchyroll.com/playback/v2/${currentVersion ? currentVersion.guid : currentMediaId}/${CrunchyPlayStreams[options.cstream]}/play`, AuthHeaders);
|
||||
if (!playbackReq.ok || !playbackReq.res) {
|
||||
console.warn('Request Stream URLs FAILED!');
|
||||
let videoStream: CrunchyPlayStream | null = null;
|
||||
let audioStream: CrunchyPlayStream | null = null;
|
||||
|
||||
const videoPlaybackReq = await this.req.getData(`https://www.crunchyroll.com/playback/v3/${currentVersion ? currentVersion.guid : currentMediaId}/${CrunchyPlayStreams[options.vstream]}/play`, AuthHeaders);
|
||||
if (!videoPlaybackReq.ok || !videoPlaybackReq.res) {
|
||||
console.warn('Request Video Stream URLs FAILED!');
|
||||
} else {
|
||||
videoStream = await videoPlaybackReq.res.json() as CrunchyPlayStream;
|
||||
const derivedPlaystreams = {} as CrunchyStreams;
|
||||
for (const hardsub in videoStream.hardSubs) {
|
||||
const stream = videoStream.hardSubs[hardsub];
|
||||
derivedPlaystreams[hardsub] = {
|
||||
url: stream.url,
|
||||
'hardsub_locale': stream.hlang
|
||||
};
|
||||
}
|
||||
derivedPlaystreams[''] = {
|
||||
url: videoStream.url,
|
||||
hardsub_locale: ''
|
||||
};
|
||||
pbData.meta = {
|
||||
audio_locale: videoStream.audioLocale,
|
||||
bifs: [videoStream.bifs],
|
||||
captions: videoStream.captions,
|
||||
closed_captions: videoStream.captions,
|
||||
media_id: videoStream.assetId,
|
||||
subtitles: videoStream.subtitles,
|
||||
versions: videoStream.versions
|
||||
};
|
||||
pbData.vpb[`adaptive_${options.vstream}_${videoStream.url.includes('m3u8') ? 'hls' : 'dash'}_drm`] = {
|
||||
...derivedPlaystreams
|
||||
};
|
||||
}
|
||||
|
||||
if (!options.cstream && (options.vstream !== options.astream)) {
|
||||
const audioPlaybackReq = await this.req.getData(`https://www.crunchyroll.com/playback/v3/${currentVersion ? currentVersion.guid : currentMediaId}/${CrunchyPlayStreams[options.astream]}/play`, AuthHeaders);
|
||||
if (!audioPlaybackReq.ok || !audioPlaybackReq.res) {
|
||||
console.warn('Request Audio Stream URLs FAILED!');
|
||||
} else {
|
||||
playStream = await playbackReq.res.json() as CrunchyPlayStream;
|
||||
audioStream = await audioPlaybackReq.res.json() as CrunchyPlayStream;
|
||||
const derivedPlaystreams = {} as CrunchyStreams;
|
||||
for (const hardsub in playStream.hardSubs) {
|
||||
const stream = playStream.hardSubs[hardsub];
|
||||
for (const hardsub in audioStream.hardSubs) {
|
||||
const stream = audioStream.hardSubs[hardsub];
|
||||
derivedPlaystreams[hardsub] = {
|
||||
url: stream.url,
|
||||
'hardsub_locale': stream.hlang
|
||||
};
|
||||
}
|
||||
derivedPlaystreams[''] = {
|
||||
url: playStream.url,
|
||||
url: audioStream.url,
|
||||
hardsub_locale: ''
|
||||
};
|
||||
pbData.meta = {
|
||||
audio_locale: playStream.audioLocale,
|
||||
bifs: [playStream.bifs],
|
||||
captions: playStream.captions,
|
||||
closed_captions: playStream.captions,
|
||||
media_id: playStream.assetId,
|
||||
subtitles: playStream.subtitles,
|
||||
versions: playStream.versions
|
||||
audio_locale: audioStream.audioLocale,
|
||||
bifs: [audioStream.bifs],
|
||||
captions: audioStream.captions,
|
||||
closed_captions: audioStream.captions,
|
||||
media_id: audioStream.assetId,
|
||||
subtitles: audioStream.subtitles,
|
||||
versions: audioStream.versions
|
||||
};
|
||||
pbData.data[0][`adaptive_${options.cstream}_${playStream.url.includes('m3u8') ? 'hls' : 'dash'}_drm`] = {
|
||||
pbData.apb[`adaptive_${options.astream}_${audioStream.url.includes('m3u8') ? 'hls' : 'dash'}_drm`] = {
|
||||
...derivedPlaystreams
|
||||
};
|
||||
}
|
||||
} else {
|
||||
pbData.apb = pbData.vpb;
|
||||
}
|
||||
|
||||
variables.push(...([
|
||||
|
|
@ -1707,9 +1702,11 @@ export default class Crunchy implements ServiceClass {
|
|||
} as Variable;
|
||||
}));
|
||||
|
||||
let streams: any[] = [];
|
||||
let vstreams: any[] = [];
|
||||
let astreams: any[] = [];
|
||||
let hsLangs: string[] = [];
|
||||
const pbStreams = pbData.data[0];
|
||||
const vpbStreams = pbData.vpb;
|
||||
const apbStreams = pbData.apb;
|
||||
|
||||
if (!canDecrypt) {
|
||||
console.error('No valid Widevine or PlayReady CDM detected. Please ensure a supported and functional CDM is installed.');
|
||||
|
|
@ -1721,13 +1718,13 @@ export default class Crunchy implements ServiceClass {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
for (const s of Object.keys(pbStreams)) {
|
||||
for (const s of Object.keys(pbData.vpb)) {
|
||||
if (
|
||||
(s.match(/hls/) || s.match(/dash/))
|
||||
&& !(s.match(/hls/) && s.match(/drm/))
|
||||
&& !s.match(/trailer/)
|
||||
) {
|
||||
const pb = Object.values(pbStreams[s]).map(v => {
|
||||
const pb = Object.values(vpbStreams[s]).map(v => {
|
||||
v.hardsub_lang = v.hardsub_locale
|
||||
? langsData.fixAndFindCrLC(v.hardsub_locale).locale
|
||||
: v.hardsub_locale;
|
||||
|
|
@ -1739,26 +1736,67 @@ export default class Crunchy implements ServiceClass {
|
|||
...{ format: s }
|
||||
};
|
||||
});
|
||||
streams.push(...pb);
|
||||
vstreams.push(...pb);
|
||||
}
|
||||
}
|
||||
|
||||
if (streams.length < 1) {
|
||||
console.warn('No full streams found!');
|
||||
for (const s of Object.keys(pbData.apb)) {
|
||||
if (
|
||||
(s.match(/hls/) || s.match(/dash/))
|
||||
&& !(s.match(/hls/) && s.match(/drm/))
|
||||
&& !s.match(/trailer/)
|
||||
) {
|
||||
const pb = Object.values(apbStreams[s]).map(v => {
|
||||
v.hardsub_lang = v.hardsub_locale
|
||||
? langsData.fixAndFindCrLC(v.hardsub_locale).locale
|
||||
: v.hardsub_locale;
|
||||
if(v.hardsub_lang && hsLangs.indexOf(v.hardsub_lang) < 0){
|
||||
hsLangs.push(v.hardsub_lang);
|
||||
}
|
||||
return {
|
||||
...v,
|
||||
...{ format: s }
|
||||
};
|
||||
});
|
||||
astreams.push(...pb);
|
||||
}
|
||||
}
|
||||
|
||||
if (vstreams.length < 1) {
|
||||
console.warn('No full video streams found!');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (astreams.length < 1) {
|
||||
console.warn('No full audio streams found!');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const audDub = langsData.findLang(langsData.fixLanguageTag(pbData.meta.audio_locale as string) || '').code;
|
||||
hsLangs = langsData.sortTags(hsLangs);
|
||||
|
||||
streams = streams.map((s) => {
|
||||
vstreams = vstreams.map((s) => {
|
||||
s.audio_lang = audDub;
|
||||
s.hardsub_lang = s.hardsub_lang ? s.hardsub_lang : '-';
|
||||
s.type = `${s.format}/${s.audio_lang}/${s.hardsub_lang}`;
|
||||
return s;
|
||||
});
|
||||
|
||||
streams = streams.sort((a, b) => {
|
||||
vstreams = vstreams.sort((a, b) => {
|
||||
if (a.type < b.type) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
astreams = astreams.map((s) => {
|
||||
s.audio_lang = audDub;
|
||||
s.hardsub_lang = s.hardsub_lang ? s.hardsub_lang : '-';
|
||||
s.type = `${s.format}/${s.audio_lang}/${s.hardsub_lang}`;
|
||||
return s;
|
||||
});
|
||||
|
||||
astreams = astreams.sort((a, b) => {
|
||||
if (a.type < b.type) {
|
||||
return -1;
|
||||
}
|
||||
|
|
@ -1768,7 +1806,13 @@ export default class Crunchy implements ServiceClass {
|
|||
if(options.hslang != 'none'){
|
||||
if(hsLangs.indexOf(options.hslang) > -1){
|
||||
console.info('Selecting stream with %s hardsubs', langsData.locale2language(options.hslang).language);
|
||||
streams = streams.filter((s) => {
|
||||
vstreams = vstreams.filter((s) => {
|
||||
if(s.hardsub_lang == '-'){
|
||||
return false;
|
||||
}
|
||||
return s.hardsub_lang == options.hslang;
|
||||
});
|
||||
astreams = astreams.filter((s) => {
|
||||
if(s.hardsub_lang == '-'){
|
||||
return false;
|
||||
}
|
||||
|
|
@ -1783,11 +1827,21 @@ export default class Crunchy implements ServiceClass {
|
|||
dlFailed = true;
|
||||
}
|
||||
} else {
|
||||
streams = streams.filter((s) => {
|
||||
vstreams = vstreams.filter((s) => {
|
||||
return s.hardsub_lang == '-';
|
||||
});
|
||||
if(streams.length < 1){
|
||||
console.warn('Raw streams not available!');
|
||||
astreams = astreams.filter((s) => {
|
||||
return s.hardsub_lang == '-';
|
||||
});
|
||||
if(vstreams.length < 1){
|
||||
console.warn('Raw video streams not available!');
|
||||
if(hsLangs.length > 0){
|
||||
console.warn('Try hardsubs stream:', hsLangs.join(', '));
|
||||
}
|
||||
dlFailed = true;
|
||||
}
|
||||
if(astreams.length < 1){
|
||||
console.warn('Raw audio streams not available!');
|
||||
if(hsLangs.length > 0){
|
||||
console.warn('Try hardsubs stream:', hsLangs.join(', '));
|
||||
}
|
||||
|
|
@ -1796,62 +1850,70 @@ export default class Crunchy implements ServiceClass {
|
|||
console.info('Selecting raw stream');
|
||||
}
|
||||
|
||||
let curStream:
|
||||
undefined|typeof streams[0]
|
||||
let vcurStream:
|
||||
undefined|typeof vstreams[0]
|
||||
= undefined;
|
||||
let acurStream:
|
||||
undefined|typeof astreams[0]
|
||||
= undefined;
|
||||
|
||||
if (!dlFailed) {
|
||||
options.kstream = typeof options.kstream == 'number' ? options.kstream : 1;
|
||||
options.kstream = options.kstream > streams.length ? 1 : options.kstream;
|
||||
console.info('Downloading...');
|
||||
vcurStream = vstreams[0];
|
||||
acurStream = astreams[0];
|
||||
|
||||
streams.forEach((s, i) => {
|
||||
const isSelected = options.kstream == i + 1 ? '✓' : ' ';
|
||||
console.info('Full stream found! (%s%s: %s )', isSelected, i + 1, s.type);
|
||||
});
|
||||
|
||||
console.info('Downloading video...');
|
||||
curStream = streams[options.kstream-1];
|
||||
|
||||
console.info('Playlists URL: %s (%s)', curStream.url, curStream.type);
|
||||
console.info('Video Playlists URL: %s (%s)', vcurStream.url, vcurStream.type);
|
||||
console.info('Audio Playlists URL: %s (%s)', acurStream.url, acurStream.type);
|
||||
}
|
||||
|
||||
let tsFile = undefined;
|
||||
|
||||
// Delete the stream if it's not needed
|
||||
if (options.novids && options.noaudio) {
|
||||
if (playStream) {
|
||||
if (videoStream) {
|
||||
await this.refreshToken(true, true);
|
||||
await this.req.getData(`https://cr-play-service.prd.crunchyrollsvc.com/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${playStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders});
|
||||
await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${videoStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders});
|
||||
}
|
||||
if (audioStream && (videoStream?.token !== audioStream.token)) {
|
||||
await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${audioStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders});
|
||||
}
|
||||
}
|
||||
|
||||
if(!dlFailed && curStream !== undefined && !(options.novids && options.noaudio)){
|
||||
const streamPlaylistsReq = await this.req.getData(curStream.url, AuthHeaders);
|
||||
if(!streamPlaylistsReq.ok || !streamPlaylistsReq.res){
|
||||
if(!dlFailed && vcurStream && acurStream && vcurStream !== undefined && acurStream !== undefined && !(options.novids && options.noaudio)){
|
||||
const vstreamPlaylistsReq = await this.req.getData(vcurStream.url, AuthHeaders);
|
||||
const astreamPlaylistsReq = vcurStream.url !== acurStream.url ? await this.req.getData(acurStream.url, AuthHeaders) : vstreamPlaylistsReq;
|
||||
if(!vstreamPlaylistsReq.ok || !vstreamPlaylistsReq.res || !astreamPlaylistsReq.ok || !astreamPlaylistsReq.res){
|
||||
console.error('CAN\'T FETCH VIDEO PLAYLISTS!');
|
||||
dlFailed = true;
|
||||
} else {
|
||||
const streamPlaylistBody = await streamPlaylistsReq.res.text();
|
||||
if (streamPlaylistBody.match('MPD')) {
|
||||
const vstreamPlaylistBody = await vstreamPlaylistsReq.res.text();
|
||||
const astreamPlaylistBody = vcurStream.url !== acurStream.url ? await astreamPlaylistsReq.res.text() : vstreamPlaylistBody;
|
||||
if (vstreamPlaylistBody.match('MPD') && astreamPlaylistBody.match('MPD')) {
|
||||
//Parse MPD Playlists
|
||||
const streamPlaylists = await parse(streamPlaylistBody, langsData.findLang(langsData.fixLanguageTag(pbData.meta.audio_locale as string) || ''), curStream.url.match(/.*\.urlset\//)[0]);
|
||||
const vstreamPlaylists = await parse(vstreamPlaylistBody, langsData.findLang(langsData.fixLanguageTag(pbData.meta.audio_locale as string) || ''), vcurStream.url.match(/.*\.urlset\//)[0]);
|
||||
const astreamPlaylists = vcurStream.url !== acurStream.url ? await parse(astreamPlaylistBody, langsData.findLang(langsData.fixLanguageTag(pbData.meta.audio_locale as string) || ''), acurStream.url.match(/.*\.urlset\//)[0]) : vstreamPlaylists;
|
||||
|
||||
//Get name of CDNs/Servers
|
||||
const streamServers = Object.keys(streamPlaylists);
|
||||
const vstreamServers = Object.keys(vstreamPlaylists);
|
||||
const astreamServers = Object.keys(astreamPlaylists);
|
||||
|
||||
options.x = options.x > streamServers.length ? 1 : options.x;
|
||||
options.x = options.x > vstreamServers.length ? 1 : options.x;
|
||||
|
||||
const selectedServer = streamServers[options.x - 1];
|
||||
const selectedList = streamPlaylists[selectedServer];
|
||||
const vselectedServer = vstreamServers[options.x - 1];
|
||||
const vselectedList = vstreamPlaylists[vselectedServer];
|
||||
|
||||
const aselectedServer = astreamServers[options.x - 1];
|
||||
const aselectedList = astreamPlaylists[aselectedServer];
|
||||
|
||||
//set Video Qualities
|
||||
const videos = selectedList.video.map(item => {
|
||||
const videos = vselectedList.video.map(item => {
|
||||
return {
|
||||
...item,
|
||||
resolutionText: `${item.quality.width}x${item.quality.height} (${Math.round(item.bandwidth/1024)}KiB/s)`
|
||||
};
|
||||
});
|
||||
|
||||
const audios = selectedList.audio.map(item => {
|
||||
const audios = aselectedList.audio.map(item => {
|
||||
return {
|
||||
...item,
|
||||
resolutionText: `${Math.round(item.bandwidth/1000)}kB/s`
|
||||
|
|
@ -1884,7 +1946,6 @@ export default class Crunchy implements ServiceClass {
|
|||
const chosenVideoSegments = videos[chosenVideoQuality];
|
||||
const chosenAudioSegments = audios[chosenAudioQuality];
|
||||
|
||||
console.info(`Servers available:\n\t${streamServers.join('\n\t')}`);
|
||||
console.info(`Available Video Qualities:\n\t${videos.map((a, ind) => `[${ind+1}] ${a.resolutionText}`).join('\n\t')}`);
|
||||
console.info(`Available Audio Qualities:\n\t${audios.map((a, ind) => `[${ind+1}] ${a.resolutionText}`).join('\n\t')}`);
|
||||
|
||||
|
|
@ -1898,12 +1959,12 @@ export default class Crunchy implements ServiceClass {
|
|||
replaceWith: chosenVideoSegments.quality.width
|
||||
});
|
||||
|
||||
const lang = langsData.languages.find(a => a.code === curStream?.audio_lang);
|
||||
const lang = langsData.languages.find(a => a.code === acurStream?.audio_lang);
|
||||
if (!lang) {
|
||||
console.error(`Unable to find language for code ${curStream.audio_lang}`);
|
||||
console.error(`Unable to find language for code ${acurStream.audio_lang}`);
|
||||
return;
|
||||
}
|
||||
console.info(`Selected quality: \n\tVideo: ${chosenVideoSegments.resolutionText}\n\tAudio: ${chosenAudioSegments.resolutionText}\n\tServer: ${selectedServer}`);
|
||||
console.info(`Selected quality: \n\tVideo: ${chosenVideoSegments.resolutionText}\n\tAudio: ${chosenAudioSegments.resolutionText}\n\tVideo Server: ${vselectedServer}\n\tAudio Server: ${aselectedServer}`);
|
||||
console.info('Stream URL:', chosenVideoSegments.segments[0].uri.split(',.urlset')[0]);
|
||||
// TODO check filename
|
||||
fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep);
|
||||
|
|
@ -1950,7 +2011,7 @@ export default class Crunchy implements ServiceClass {
|
|||
'Cache-Control': 'no-cache',
|
||||
'content-type': 'application/octet-stream',
|
||||
'x-cr-content-id': currentVersion ? currentVersion.guid : currentMediaId,
|
||||
'x-cr-video-token': playStream!.token
|
||||
'x-cr-video-token': videoStream!.token
|
||||
});
|
||||
|
||||
// Check if the audio pssh is different since Crunchyroll started to have different dec keys for audio tracks
|
||||
|
|
@ -1963,16 +2024,19 @@ export default class Crunchy implements ServiceClass {
|
|||
'Cache-Control': 'no-cache',
|
||||
'content-type': 'application/octet-stream',
|
||||
'x-cr-content-id': currentVersion ? currentVersion.guid : currentMediaId,
|
||||
'x-cr-video-token': playStream!.token
|
||||
'x-cr-video-token': audioStream!.token
|
||||
});
|
||||
} else {
|
||||
encryptionKeysAudio = encryptionKeysVideo;
|
||||
}
|
||||
}
|
||||
|
||||
if (playStream) {
|
||||
if (videoStream) {
|
||||
await this.refreshToken(true, true);
|
||||
await this.req.getData(`https://cr-play-service.prd.crunchyrollsvc.com/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${playStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders});
|
||||
await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${videoStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders});
|
||||
}
|
||||
if (audioStream && (videoStream?.token !== audioStream.token)) {
|
||||
await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${audioStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders});
|
||||
}
|
||||
|
||||
// New Crunchyroll DRM endpoint for Playready (currently broken on Crunchyrolls part and therefore disabled)
|
||||
|
|
@ -2199,7 +2263,7 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
}
|
||||
} else if (!options.novids) {
|
||||
const streamPlaylists = m3u8(streamPlaylistBody);
|
||||
const streamPlaylists = m3u8(vstreamPlaylistBody);
|
||||
const plServerList: string[] = [],
|
||||
plStreams: Record<string, Record<string, string>> = {},
|
||||
plQuality: {
|
||||
|
|
@ -2255,9 +2319,7 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
}
|
||||
|
||||
options.x = options.x > plServerList.length ? 1 : options.x;
|
||||
|
||||
const plSelectedServer = plServerList[options.x - 1];
|
||||
const plSelectedServer = plServerList[0];
|
||||
const plSelectedList = plStreams[plSelectedServer];
|
||||
plQuality.sort((a, b) => {
|
||||
const aMatch: RegExpMatchArray | never[] = a.dim.match(/[0-9]+/) || [];
|
||||
|
|
@ -2290,9 +2352,9 @@ export default class Crunchy implements ServiceClass {
|
|||
type: 'number',
|
||||
replaceWith: quality === 0 ? plQuality[plQuality.length - 1].RESOLUTION.width as number : plQuality[quality - 1].RESOLUTION.width
|
||||
});
|
||||
const lang = langsData.languages.find(a => a.code === curStream?.audio_lang);
|
||||
const lang = langsData.languages.find(a => a.code === vcurStream?.audio_lang);
|
||||
if (!lang) {
|
||||
console.error(`Unable to find language for code ${curStream.audio_lang}`);
|
||||
console.error(`Unable to find language for code ${vcurStream.audio_lang}`);
|
||||
return;
|
||||
}
|
||||
console.info(`Selected quality: ${Object.keys(plSelectedList).find(a => plSelectedList[a] === selPlUrl)} @ ${plSelectedServer}`);
|
||||
|
|
@ -2309,9 +2371,12 @@ export default class Crunchy implements ServiceClass {
|
|||
dlFailed = true;
|
||||
} else {
|
||||
// We have the stream, so go ahead and delete the active stream
|
||||
if (playStream) {
|
||||
if (videoStream) {
|
||||
await this.refreshToken(true, true);
|
||||
await this.req.getData(`https://cr-play-service.prd.crunchyrollsvc.com/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${playStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders});
|
||||
await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${videoStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders});
|
||||
}
|
||||
if (audioStream && (videoStream?.token !== audioStream.token)) {
|
||||
await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${audioStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders});
|
||||
}
|
||||
|
||||
const chunkPageBody = await chunkPage.res.text();
|
||||
|
|
@ -2377,9 +2442,9 @@ export default class Crunchy implements ServiceClass {
|
|||
if (!fs.existsSync(dirName)) {
|
||||
fs.mkdirSync(dirName, { recursive: true });
|
||||
}
|
||||
const lang = langsData.languages.find(a => a.code === curStream?.audio_lang);
|
||||
const lang = langsData.languages.find(a => a.code === vcurStream?.audio_lang);
|
||||
if (!lang) {
|
||||
console.error(`Unable to find language for code ${curStream.audio_lang}`);
|
||||
console.error(`Unable to find language for code ${vcurStream.audio_lang}`);
|
||||
return;
|
||||
}
|
||||
fs.writeFileSync(`${tsFile}.txt`, compiledChapters.join('\r\n'));
|
||||
|
|
@ -2884,42 +2949,32 @@ export default class Crunchy implements ServiceClass {
|
|||
|
||||
let episodeList = { total: 0, data: [], meta: {} } as CrunchyEpisodeList;
|
||||
//get episode info
|
||||
if (this.api == 'android') {
|
||||
const reqEpsListOpts = [
|
||||
api.beta_cms,
|
||||
this.cmsToken.cms.bucket,
|
||||
'/episodes?',
|
||||
new URLSearchParams({
|
||||
'force_locale': '',
|
||||
'preferred_audio_language': 'ja-JP',
|
||||
'locale': this.locale,
|
||||
'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 = await reqEpsList.res.json() as CrunchyAndroidEpisodes;
|
||||
episodeList = {
|
||||
total: episodeListAndroid.total,
|
||||
data: episodeListAndroid.items,
|
||||
meta: {}
|
||||
};
|
||||
} else {
|
||||
const reqEpsList = await this.req.getData(`${api.cms}/seasons/${item.id}/episodes?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders);
|
||||
if(!reqEpsList.ok || !reqEpsList.res){
|
||||
console.error('Episode List Request FAILED!');
|
||||
return;
|
||||
}
|
||||
//CrunchyEpisodeList
|
||||
episodeList = await reqEpsList.res.json() as CrunchyEpisodeList;
|
||||
const reqEpsListOpts = [
|
||||
api.cms_bucket,
|
||||
this.cmsToken.cms.bucket,
|
||||
'/episodes?',
|
||||
new URLSearchParams({
|
||||
'force_locale': '',
|
||||
'preferred_audio_language': 'ja-JP',
|
||||
'locale': this.locale,
|
||||
'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 = await reqEpsList.res.json() as CrunchyAndroidEpisodes;
|
||||
episodeList = {
|
||||
total: episodeListAndroid.total,
|
||||
data: episodeListAndroid.items,
|
||||
meta: {}
|
||||
};
|
||||
|
||||
if(episodeList.total < 1){
|
||||
console.info(' Season is empty!');
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ class CrunchyHandler extends Base implements MessageHandler {
|
|||
|
||||
public getDefaults() {
|
||||
const _default = yargs.appArgv(this.crunchy.cfg.cli, true);
|
||||
this.crunchy.api = _default.crapi;
|
||||
this.crunchy.locale = _default.locale;
|
||||
}
|
||||
|
||||
|
|
@ -103,7 +102,6 @@ class CrunchyHandler extends Base implements MessageHandler {
|
|||
console.debug(`Got download options: ${JSON.stringify(data)}`);
|
||||
this.setDownloading(true);
|
||||
const _default = yargs.appArgv(this.crunchy.cfg.cli, true);
|
||||
this.crunchy.api = _default.crapi;
|
||||
const res = await this.crunchy.downloadFromSeriesID(data.id, {
|
||||
dubLang: data.dubLang,
|
||||
e: data.e
|
||||
|
|
|
|||
|
|
@ -24,13 +24,13 @@ export type APIType = {
|
|||
collections: string
|
||||
// beta api
|
||||
defaultUserAgent: string,
|
||||
beta_profile: string
|
||||
beta_cmsToken: string
|
||||
profile: string
|
||||
cmsToken: string
|
||||
browse_all_series: string,
|
||||
search: string
|
||||
cms: string
|
||||
beta_browse: string
|
||||
beta_cms: string,
|
||||
cms_bucket: string
|
||||
browse: string
|
||||
drm: string;
|
||||
drm_widevine: string;
|
||||
drm_playready: string;
|
||||
|
|
@ -66,15 +66,14 @@ const api: APIType = {
|
|||
search3: `${domain.api}/autocomplete.0.json`,
|
||||
session: `${domain.api}/start_session.0.json`,
|
||||
collections: `${domain.api}/list_collections.0.json`,
|
||||
// This User-Agent bypasses Cloudflare security of the newer Endpoint
|
||||
defaultUserAgent: 'Crunchyroll/4.77.3 (bundle_identifier:com.crunchyroll.iphone; build_number:4148147.285670380) iOS/18.3.2 Gravity/4.77.3',
|
||||
beta_profile: `${domain.api_beta}/accounts/v1/me/profile`,
|
||||
beta_cmsToken: `${domain.api_beta}/index/v2`,
|
||||
search: `${domain.api_beta}/content/v2/discover/search`,
|
||||
cms: `${domain.api_beta}/content/v2/cms`,
|
||||
beta_browse: `${domain.api_beta}/content/v1/browse`,
|
||||
beta_cms: `${domain.api_beta}/cms/v2`,
|
||||
browse_all_series: `${domain.api_beta}/content/v2/discover/browse`,
|
||||
defaultUserAgent: 'Crunchyroll/4.83.0 (bundle_identifier:com.crunchyroll.iphone; build_number:4254815.324030705) iOS/19.0.0 Gravity/4.83.0',
|
||||
profile: `${domain.www}/accounts/v1/me/profile`,
|
||||
cmsToken: `${domain.www}/index/v2`,
|
||||
search: `${domain.www}/content/v2/discover/search`,
|
||||
cms: `${domain.www}/content/v2/cms`,
|
||||
cms_bucket: `${domain.api_beta}/cms/v2`,
|
||||
browse: `${domain.www}/content/v1/browse`,
|
||||
browse_all_series: `${domain.www}/content/v2/discover/browse`,
|
||||
// beta api
|
||||
// broken - deprecated since 06.05.2025
|
||||
drm: `${domain.api_beta}/drm/v1/auth`,
|
||||
|
|
|
|||
|
|
@ -45,8 +45,10 @@ let argvC: {
|
|||
extid: string | undefined;
|
||||
q: number;
|
||||
x: number;
|
||||
kstream: number;
|
||||
cstream: keyof typeof CrunchyPlayStreams | 'none';
|
||||
// kstream: number;
|
||||
cstream: keyof typeof CrunchyPlayStreams;
|
||||
vstream: keyof typeof CrunchyPlayStreams;
|
||||
astream: keyof typeof CrunchyPlayStreams;
|
||||
partsize: number;
|
||||
hslang: string;
|
||||
dlsubs: string[];
|
||||
|
|
@ -76,7 +78,7 @@ let argvC: {
|
|||
$0: string;
|
||||
dlVideoOnce: boolean;
|
||||
chapters: boolean;
|
||||
crapi: 'android' | 'web';
|
||||
// crapi: 'android' | 'web';
|
||||
removeBumpers: boolean;
|
||||
originalFontSize: boolean;
|
||||
keepAllVideos: boolean;
|
||||
|
|
|
|||
|
|
@ -255,20 +255,21 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
|||
default: true
|
||||
}
|
||||
},
|
||||
{
|
||||
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: 'web'
|
||||
}
|
||||
},
|
||||
// Deprecated
|
||||
// {
|
||||
// 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: 'web'
|
||||
// }
|
||||
// },
|
||||
{
|
||||
name: 'removeBumpers',
|
||||
describe: 'Remove bumpers from final video',
|
||||
|
|
@ -305,30 +306,43 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
|||
type: 'number',
|
||||
alias: 'server',
|
||||
docDescribe: true,
|
||||
service: ['crunchy'],
|
||||
service: ['all'],
|
||||
usage: '${server}'
|
||||
},
|
||||
{
|
||||
name: 'kstream',
|
||||
group: 'dl',
|
||||
alias: 'k',
|
||||
describe: 'Select specific stream',
|
||||
choices: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
default: {
|
||||
default: 1
|
||||
},
|
||||
docDescribe: true,
|
||||
service: ['crunchy'],
|
||||
type: 'number',
|
||||
usage: '${stream}'
|
||||
},
|
||||
// Deprecated
|
||||
// {
|
||||
// name: 'kstream',
|
||||
// group: 'dl',
|
||||
// alias: 'k',
|
||||
// describe: 'Select specific stream',
|
||||
// choices: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
// default: {
|
||||
// default: 1
|
||||
// },
|
||||
// docDescribe: true,
|
||||
// service: ['crunchy'],
|
||||
// type: 'number',
|
||||
// usage: '${stream}'
|
||||
// },
|
||||
// About to Deprecate
|
||||
{
|
||||
name: 'cstream',
|
||||
group: 'dl',
|
||||
alias: 'cs',
|
||||
service: ['crunchy'],
|
||||
type: 'string',
|
||||
describe: 'Select a specific Crunchyroll playback endpoint by device, or disable the stream using "none". Since Crunchyroll has started rolling out their new VBR encodes, we highly recommend using a TV endpoint (e.g. vidaa, samsungtv, lgtv, rokutv, chromecast, firetv, androidtv) to access the old CBR encodes. Please note: The older encodes do not include the new 192 kbps audio, the new audio is only available with the new VBR encodes.',
|
||||
describe: '(Please use --vstream and --astream instead, this will deprecate soon) Select a specific Crunchyroll playback endpoint by device. Since Crunchyroll has started rolling out their new VBR encodes, we highly recommend using a TV endpoint (e.g. vidaa, samsungtv, lgtv, rokutv, chromecast, firetv, androidtv) to access the old CBR encodes. Please note: The older encodes do not include the new 192 kbps audio, the new audio is only available with the new VBR encodes.',
|
||||
choices: [...Object.keys(CrunchyPlayStreams), 'none'],
|
||||
docDescribe: true,
|
||||
usage: '${device}'
|
||||
},
|
||||
{
|
||||
name: 'vstream',
|
||||
group: 'dl',
|
||||
alias: 'vs',
|
||||
service: ['crunchy'],
|
||||
type: 'string',
|
||||
describe: 'Select a specific Crunchyroll video playback endpoint by device.',
|
||||
choices: [...Object.keys(CrunchyPlayStreams), 'none'],
|
||||
default: {
|
||||
default: 'lgtv'
|
||||
|
|
@ -336,6 +350,20 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
|||
docDescribe: true,
|
||||
usage: '${device}'
|
||||
},
|
||||
{
|
||||
name: 'astream',
|
||||
group: 'dl',
|
||||
alias: 'as',
|
||||
service: ['crunchy'],
|
||||
type: 'string',
|
||||
describe: 'Select a specific Crunchyroll audio playback endpoint by device.',
|
||||
choices: [...Object.keys(CrunchyPlayStreams), 'none'],
|
||||
default: {
|
||||
default: 'firefox'
|
||||
},
|
||||
docDescribe: true,
|
||||
usage: '${device}'
|
||||
},
|
||||
{
|
||||
name: 'hslang',
|
||||
group: 'dl',
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { connect } from 'puppeteer-real-browser';
|
|||
export type Params = {
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
||||
headers?: Record<string, string>;
|
||||
body?: string | Buffer;
|
||||
body?: BodyInit | undefined;
|
||||
binary?: boolean;
|
||||
followRedirect?: 'follow' | 'error' | 'manual';
|
||||
};
|
||||
|
|
|
|||
22
package.json
22
package.json
|
|
@ -41,15 +41,15 @@
|
|||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@bufbuild/buf": "^1.55.1",
|
||||
"@bufbuild/protobuf": "^2.6.1",
|
||||
"@bufbuild/protoc-gen-es": "^2.6.1",
|
||||
"@yao-pkg/pkg": "^6.5.1",
|
||||
"@bufbuild/protobuf": "^2.6.2",
|
||||
"@bufbuild/protoc-gen-es": "^2.6.2",
|
||||
"@yao-pkg/pkg": "^6.6.0",
|
||||
"binary-parser": "^2.2.1",
|
||||
"binary-parser-encoder": "^1.5.3",
|
||||
"bn.js": "^5.2.2",
|
||||
"cors": "^2.8.5",
|
||||
"elliptic": "^6.6.1",
|
||||
"esbuild": "^0.25.6",
|
||||
"esbuild": "^0.25.8",
|
||||
"express": "^5.1.0",
|
||||
"fast-xml-parser": "^5.2.5",
|
||||
"ffprobe": "^1.1.2",
|
||||
|
|
@ -65,31 +65,31 @@
|
|||
"ofetch": "^1.4.1",
|
||||
"open": "^8.4.2",
|
||||
"protobufjs": "^7.5.3",
|
||||
"puppeteer-real-browser": "^1.4.2",
|
||||
"puppeteer-real-browser": "^1.4.3",
|
||||
"ws": "^8.18.3",
|
||||
"yaml": "^2.8.0",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.31.0",
|
||||
"@eslint/js": "^9.32.0",
|
||||
"@types/bn.js": "^5.2.0",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/elliptic": "^6.4.18",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/ffprobe": "^1.1.8",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/node": "^24.0.14",
|
||||
"@types/node": "^24.1.0",
|
||||
"@types/node-forge": "^1.3.13",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@types/yargs": "^17.0.33",
|
||||
"@typescript-eslint/eslint-plugin": "^8.37.0",
|
||||
"@typescript-eslint/parser": "^8.37.0",
|
||||
"eslint": "^9.31.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
||||
"@typescript-eslint/parser": "^8.38.0",
|
||||
"eslint": "^9.32.0",
|
||||
"protoc": "^1.1.3",
|
||||
"removeNPMAbsolutePaths": "^3.0.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.37.0"
|
||||
"typescript-eslint": "^8.38.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prestart": "pnpm run tsc test",
|
||||
|
|
|
|||
561
pnpm-lock.yaml
561
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue