Compare commits
55 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64c927c761 | ||
|
|
16e7fcb71e | ||
|
|
d61d7e471f | ||
|
|
64272b7689 | ||
|
|
35777ecb58 | ||
|
|
423ad7b2c9 | ||
|
|
5cbead06bc | ||
|
|
1704bfb4ec | ||
|
|
b488834c0f | ||
|
|
8d59666a6c | ||
|
|
c14a963024 | ||
|
|
0026de73bf | ||
|
|
d3238d22ba | ||
|
|
ab090a6858 | ||
|
|
64783a0529 | ||
|
|
05d679e6ca | ||
|
|
cd9586ab13 | ||
|
|
0a7cfcd917 | ||
|
|
ff978e2c88 | ||
|
|
d81ca76594 | ||
|
|
9457ee6d26 | ||
|
|
9a94c33c8b | ||
|
|
e88352af3f | ||
|
|
870b775175 | ||
|
|
78f5016dd3 | ||
|
|
eaec9e62a7 | ||
|
|
f92e8dfacb | ||
|
|
a6d740e9e9 | ||
|
|
436a4ca4d1 | ||
|
|
ea7df30aa7 | ||
|
|
240ff3870b | ||
|
|
1340624742 | ||
|
|
b0b7cffddf | ||
|
|
fe3f978082 | ||
|
|
0045abe08c | ||
|
|
ef975868a3 | ||
|
|
345aa0f267 | ||
|
|
1f8ddb27a1 | ||
|
|
575ea260b6 | ||
|
|
9fdc1ac4db | ||
|
|
cf921295f8 | ||
|
|
ed22970346 | ||
|
|
5f034dc348 | ||
|
|
c294cdc280 | ||
|
|
16dbc4f1eb | ||
|
|
fba1b1cf22 | ||
|
|
141fdcf552 | ||
|
|
3aa844f90b | ||
|
|
be95c1f3bc | ||
|
|
6275d5abe3 | ||
|
|
85c5d45829 | ||
|
|
9feb3d2f13 | ||
|
|
8b5cafff3d | ||
|
|
9ea6258fec | ||
|
|
fc0736c686 |
27 changed files with 1730 additions and 1521 deletions
2
.github/workflows/auto-documentation.yml
vendored
2
.github/workflows/auto-documentation.yml
vendored
|
|
@ -18,7 +18,7 @@ jobs:
|
|||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
- run: pnpm i
|
||||
- run: pnpm run docs
|
||||
- uses: stefanzweifel/git-auto-commit-action@v4
|
||||
|
|
|
|||
4
.github/workflows/docker.yml
vendored
4
.github/workflows/docker.yml
vendored
|
|
@ -27,6 +27,6 @@ jobs:
|
|||
github-token: ${{ github.token }}
|
||||
push: ${{ github.ref == 'refs/heads/master' }}
|
||||
tags: |
|
||||
"izuco/multi-downloader-nx:latest"
|
||||
"multidl/multi-downloader-nx:latest"
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
|
|
|
|||
4
.github/workflows/release-matrix.yml
vendored
4
.github/workflows/release-matrix.yml
vendored
|
|
@ -21,7 +21,7 @@ jobs:
|
|||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
- name: Install Node modules
|
||||
run: |
|
||||
|
|
@ -61,6 +61,6 @@ jobs:
|
|||
github-token: ${{ github.token }}
|
||||
push: true
|
||||
tags: |
|
||||
"izuco/multi-downloader-nx:${{ github.event.release.tag_name }}"
|
||||
"multidl/multi-downloader-nx:${{ github.event.release.tag_name }}"
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
|
|
|
|||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
- run: pnpm i
|
||||
- run: npx eslint .
|
||||
|
|
@ -32,7 +32,7 @@ jobs:
|
|||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
- run: pnpm i
|
||||
- run: pnpm run test
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -34,6 +34,7 @@ gui/react/build/
|
|||
docker-compose.yml
|
||||
crunchyendpoints
|
||||
.vscode
|
||||
.idea
|
||||
/logs
|
||||
/tmp/*/
|
||||
!videos/.gitkeep
|
||||
|
|
|
|||
4
@types/crunchyPlayStreams.d.ts
vendored
4
@types/crunchyPlayStreams.d.ts
vendored
|
|
@ -1,6 +1,8 @@
|
|||
import { Locale } from './playbackData';
|
||||
|
||||
export interface CrunchyPlayStream {
|
||||
assetId: string;
|
||||
audioLocale: string;
|
||||
audioLocale: Locale;
|
||||
bifs: string;
|
||||
burnedInLocale: string;
|
||||
captions: { [key: string]: Caption };
|
||||
|
|
|
|||
2
@types/crunchyTypes.d.ts
vendored
2
@types/crunchyTypes.d.ts
vendored
|
|
@ -2,10 +2,12 @@ import { HLSCallback } from 'hls-download';
|
|||
import { sxItem } from '../crunchy';
|
||||
import { LanguageItem } from '../modules/module.langsData';
|
||||
import { DownloadInfo } from './messageHandler';
|
||||
import { CrunchyPlayStreams } from './enums';
|
||||
|
||||
export type CrunchyDownloadOptions = {
|
||||
hslang: string,
|
||||
kstream: number,
|
||||
cstream: keyof typeof CrunchyPlayStreams | 'none',
|
||||
novids?: boolean,
|
||||
noaudio?: boolean,
|
||||
x: number,
|
||||
|
|
|
|||
16
@types/enums.ts
Normal file
16
@types/enums.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
export enum CrunchyPlayStreams {
|
||||
'chrome' = 'web/chrome',
|
||||
'firefox' = 'web/firefox',
|
||||
'safari' = 'web/safari',
|
||||
'edge' = 'web/edge',
|
||||
'fallback' = 'web/fallback',
|
||||
'ps4' = 'console/ps4',
|
||||
'ps5' = 'console/ps5',
|
||||
'switch' = 'console/switch',
|
||||
'samsungtv' = 'tv/samsung',
|
||||
'lgtv' = 'tv/lg',
|
||||
'rokutv' = 'tv/roku',
|
||||
'android' = 'android/phone',
|
||||
'iphone' = 'ios/iphone',
|
||||
'ipad' = 'ios/ipad',
|
||||
}
|
||||
8
@types/playbackData.d.ts
vendored
8
@types/playbackData.d.ts
vendored
|
|
@ -1,8 +1,8 @@
|
|||
// Generated by https://quicktype.io
|
||||
export interface PlaybackData {
|
||||
total: number;
|
||||
data: [{ [key: string]: { [key: string]: StreamDetails } }];
|
||||
meta: Meta;
|
||||
data: { [key: string]: { [key: string]: StreamDetails } }[];
|
||||
meta: Meta;
|
||||
}
|
||||
|
||||
export interface StreamList {
|
||||
|
|
@ -59,11 +59,11 @@ export interface Meta {
|
|||
versions: Version[];
|
||||
audio_locale: Locale;
|
||||
closed_captions: Subtitles;
|
||||
captions: Record<unknown>;
|
||||
captions: Subtitles;
|
||||
}
|
||||
|
||||
export interface Subtitles {
|
||||
'': SubtitleInfo;
|
||||
''?: SubtitleInfo;
|
||||
'en-US'?: SubtitleInfo;
|
||||
'es-LA'?: SubtitleInfo;
|
||||
'es-419'?: SubtitleInfo;
|
||||
|
|
|
|||
16
adn.ts
16
adn.ts
|
|
@ -204,8 +204,6 @@ export default class AnimationDigitalNetwork implements ServiceClass {
|
|||
return { isOk: false, reason: new Error('Authentication failed') };
|
||||
}
|
||||
this.token = await authReq.res.json();
|
||||
const cookies = this.parseCookies(authReq.res.headers.get('Set-Cookie'));
|
||||
this.token.refreshToken = cookies.adnrt;
|
||||
yamlCfg.saveADNToken(this.token);
|
||||
console.info('Authentication Success');
|
||||
return { isOk: true, value: undefined };
|
||||
|
|
@ -216,19 +214,16 @@ export default class AnimationDigitalNetwork implements ServiceClass {
|
|||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token.accessToken}`,
|
||||
'Cookie': `adnrt=${this.token.refreshToken}`,
|
||||
'X-Access-Token': this.token.accessToken
|
||||
'X-Access-Token': this.token.accessToken,
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
body: '{}'
|
||||
body: JSON.stringify({refreshToken: this.token.refreshToken})
|
||||
});
|
||||
if(!authReq.ok || !authReq.res){
|
||||
console.error('Token refresh failed!');
|
||||
return { isOk: false, reason: new Error('Token refresh failed') };
|
||||
}
|
||||
this.token = await authReq.res.json();
|
||||
const cookies = this.parseCookies(authReq.res.headers.get('Set-Cookie'));
|
||||
//this.token.refreshtoken = this.token.refreshToken;
|
||||
this.token.refreshToken = cookies.adnrt;
|
||||
yamlCfg.saveADNToken(this.token);
|
||||
return { isOk: true, value: undefined };
|
||||
}
|
||||
|
|
@ -463,7 +458,8 @@ export default class AnimationDigitalNetwork implements ServiceClass {
|
|||
|
||||
const configReq = await this.req.getData(`https://gw.api.animationdigitalnetwork.fr/player/video/${data.id}/configuration`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token.accessToken}`
|
||||
Authorization: `Bearer ${this.token.accessToken}`,
|
||||
'X-Target-Distribution': this.locale
|
||||
}
|
||||
});
|
||||
if(!configReq.ok || !configReq.res){
|
||||
|
|
@ -615,7 +611,7 @@ export default class AnimationDigitalNetwork implements ServiceClass {
|
|||
// set plQualityStr
|
||||
const plBandwidth = Math.round(pl.attributes.BANDWIDTH/1024);
|
||||
const qualityStrAdd = `${plResolutionText} (${plBandwidth}KiB/s)`;
|
||||
const qualityStrRegx = new RegExp(qualityStrAdd.replace(/(:|\(|\)|\/)/g, '\\$1'), 'm');
|
||||
const qualityStrRegx = new RegExp(qualityStrAdd.replace(/([:()/])/g, '\\$1'), 'm');
|
||||
const qualityStrMatch = !plQuality.map(a => a.str).join('\r\n').match(qualityStrRegx);
|
||||
if(qualityStrMatch){
|
||||
plQuality.push({
|
||||
|
|
|
|||
2
ao.ts
2
ao.ts
|
|
@ -213,7 +213,7 @@ export default class AnimeOnegai implements ServiceClass {
|
|||
}
|
||||
//Item is movie, lets define it manually
|
||||
if (series.data.asset_type === 1 && series.seasons.length === 0) {
|
||||
let lang: string | undefined = undefined;
|
||||
let lang: string | undefined;
|
||||
if (this.jpnStrings.some(str => series.data.title.includes(str))) lang = 'ja';
|
||||
else if (this.porStrings.some(str => series.data.title.includes(str))) lang = 'pt';
|
||||
else if (this.spaStrings.some(str => series.data.title.includes(str))) lang = 'es';
|
||||
|
|
|
|||
237
crunchy.ts
237
crunchy.ts
|
|
@ -31,7 +31,7 @@ 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 { CrunchyStreams, PlaybackData } from './@types/playbackData';
|
||||
import { CrunchyStreams, PlaybackData, Subtitles } from './@types/playbackData';
|
||||
import { downloaded } from './modules/module.downloadArchive';
|
||||
import parseSelect from './modules/module.parseSelect';
|
||||
import { AvailableFilenameVars, getDefault } from './modules/module.args';
|
||||
|
|
@ -44,6 +44,8 @@ import { CrunchyAndroidObject } from './@types/crunchyAndroidObject';
|
|||
import { CrunchyChapters, CrunchyChapter, CrunchyOldChapter } from './@types/crunchyChapters';
|
||||
import vtt2ass from './modules/module.vtt2ass';
|
||||
import { CrunchyPlayStream } from './@types/crunchyPlayStreams';
|
||||
import { CrunchyPlayStreams } from './@types/enums';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
export type sxItem = {
|
||||
language: langsData.LanguageItem,
|
||||
|
|
@ -224,15 +226,18 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
|
||||
public async doAuth(data: AuthData): Promise<AuthResponse> {
|
||||
const uuid = randomUUID();
|
||||
const authData = new URLSearchParams({
|
||||
'username': data.username,
|
||||
'password': data.password,
|
||||
'grant_type': 'password',
|
||||
'scope': 'offline_access'
|
||||
'scope': 'offline_access',
|
||||
'device_id': uuid,
|
||||
'device_type': 'Chrome on Windows'
|
||||
}).toString();
|
||||
const authReqOpts: reqModule.Params = {
|
||||
method: 'POST',
|
||||
headers: api.crunchyAuthHeaderSwitch,
|
||||
headers: api.crunchyAuthHeaderMob,
|
||||
body: authData
|
||||
};
|
||||
const authReq = await this.req.getData(api.beta_auth, authReqOpts);
|
||||
|
|
@ -241,6 +246,7 @@ export default class Crunchy implements ServiceClass {
|
|||
return { isOk: false, reason: new Error('Authentication failed') };
|
||||
}
|
||||
this.token = await authReq.res.json();
|
||||
this.token.device_id = uuid;
|
||||
this.token.expires = new Date(Date.now() + this.token.expires_in);
|
||||
yamlCfg.saveCRToken(this.token);
|
||||
await this.getProfile();
|
||||
|
|
@ -249,13 +255,16 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
|
||||
public async doAnonymousAuth(){
|
||||
const uuid = randomUUID();
|
||||
const authData = new URLSearchParams({
|
||||
'grant_type': 'client_id',
|
||||
'scope': 'offline_access',
|
||||
'device_id': uuid,
|
||||
'device_type': 'Chrome on Windows'
|
||||
}).toString();
|
||||
const authReqOpts: reqModule.Params = {
|
||||
method: 'POST',
|
||||
headers: api.crunchyAuthHeaderSwitch,
|
||||
headers: api.crunchyAuthHeaderMob,
|
||||
body: authData
|
||||
};
|
||||
const authReq = await this.req.getData(api.beta_auth, authReqOpts);
|
||||
|
|
@ -264,6 +273,7 @@ export default class Crunchy implements ServiceClass {
|
|||
return;
|
||||
}
|
||||
this.token = await authReq.res.json();
|
||||
this.token.device_id = uuid;
|
||||
this.token.expires = new Date(Date.now() + this.token.expires_in);
|
||||
yamlCfg.saveCRToken(this.token);
|
||||
}
|
||||
|
|
@ -298,14 +308,18 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
|
||||
public async loginWithToken(refreshToken: string) {
|
||||
const uuid = randomUUID();
|
||||
const authData = new URLSearchParams({
|
||||
'refresh_token': refreshToken,
|
||||
'refresh_token': this.token.refresh_token,
|
||||
'grant_type': 'refresh_token',
|
||||
'scope': 'offline_access'
|
||||
//'grant_type': 'etp_rt_cookie',
|
||||
'scope': 'offline_access',
|
||||
'device_id': uuid,
|
||||
'device_type': 'Chrome on Windows'
|
||||
}).toString();
|
||||
const authReqOpts: reqModule.Params = {
|
||||
method: 'POST',
|
||||
headers: api.crunchyAuthHeaderSwitch,
|
||||
headers: {...api.crunchyAuthHeaderMob, Cookie: `etp_rt=${refreshToken}`},
|
||||
body: authData
|
||||
};
|
||||
const authReq = await this.req.getData(api.beta_auth, authReqOpts);
|
||||
|
|
@ -317,6 +331,7 @@ export default class Crunchy implements ServiceClass {
|
|||
return;
|
||||
}
|
||||
this.token = await authReq.res.json();
|
||||
this.token.device_id = uuid;
|
||||
this.token.expires = new Date(Date.now() + this.token.expires_in);
|
||||
yamlCfg.saveCRToken(this.token);
|
||||
await this.getProfile(false);
|
||||
|
|
@ -335,14 +350,18 @@ export default class Crunchy implements ServiceClass {
|
|||
} else {
|
||||
//console.info('[WARN] The token has expired compleatly. I will try to refresh the token anyway, but you might have to reauth.');
|
||||
}
|
||||
const uuid = this.token.device_id || randomUUID();
|
||||
const authData = new URLSearchParams({
|
||||
'refresh_token': this.token.refresh_token,
|
||||
'grant_type': 'refresh_token',
|
||||
'scope': 'offline_access'
|
||||
//'grant_type': 'etp_rt_cookie',
|
||||
'scope': 'offline_access',
|
||||
'device_id': uuid,
|
||||
'device_type': 'Chrome on Windows'
|
||||
}).toString();
|
||||
const authReqOpts: reqModule.Params = {
|
||||
method: 'POST',
|
||||
headers: api.crunchyAuthHeaderSwitch,
|
||||
headers: {...api.crunchyAuthHeaderMob, Cookie: `etp_rt=${this.token.refresh_token}`},
|
||||
body: authData
|
||||
};
|
||||
const authReq = await this.req.getData(api.beta_auth, authReqOpts);
|
||||
|
|
@ -354,6 +373,7 @@ export default class Crunchy implements ServiceClass {
|
|||
return;
|
||||
}
|
||||
this.token = await authReq.res.json();
|
||||
this.token.device_id = uuid;
|
||||
this.token.expires = new Date(Date.now() + this.token.expires_in);
|
||||
yamlCfg.saveCRToken(this.token);
|
||||
}
|
||||
|
|
@ -613,8 +633,8 @@ export default class Crunchy implements ServiceClass {
|
|||
if(item.hide_metadata){
|
||||
iMetadata.hide_metadata = item.hide_metadata;
|
||||
}
|
||||
const showObjectMetadata = oMetadata.length > 0 && !iMetadata.hide_metadata ? true : false;
|
||||
const showObjectBooleans = oBooleans.length > 0 && !iMetadata.hide_metadata ? true : false;
|
||||
const showObjectMetadata = oMetadata.length > 0 && !iMetadata.hide_metadata;
|
||||
const showObjectBooleans = oBooleans.length > 0 && !iMetadata.hide_metadata;
|
||||
// make obj ids
|
||||
const objects_ids: string[] = [];
|
||||
objects_ids.push(oTypes[item.type as keyof typeof oTypes] + ':' + item.id);
|
||||
|
|
@ -669,7 +689,7 @@ export default class Crunchy implements ServiceClass {
|
|||
console.info(
|
||||
'%s- Availability notes: %s',
|
||||
''.padStart(pad + 2, ' '),
|
||||
item.availability_notes.replace(/\[[^\]]*\]?/gm, '')
|
||||
item.availability_notes.replace(/\[[^\]]*]?/gm, '')
|
||||
);
|
||||
}
|
||||
if(item.type == 'series' && getSeries){
|
||||
|
|
@ -752,7 +772,7 @@ export default class Crunchy implements ServiceClass {
|
|||
return;
|
||||
}
|
||||
for(const item of movieListing.data){
|
||||
this.logObject(item, pad, false, false);
|
||||
await this.logObject(item, pad, false, false);
|
||||
}
|
||||
|
||||
//Movies
|
||||
|
|
@ -763,7 +783,7 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
const moviesList = await moviesListReq.res.json();
|
||||
for(const item of moviesList.data){
|
||||
this.logObject(item, pad+2);
|
||||
await this.logObject(item, pad + 2);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -821,7 +841,7 @@ export default class Crunchy implements ServiceClass {
|
|||
return { isOk: false, reason: new Error('Show request failed. No more information provided.') };
|
||||
}
|
||||
const showInfo = await showInfoReq.res.json();
|
||||
this.logObject(showInfo.data[0], 0);
|
||||
await this.logObject(showInfo.data[0], 0);
|
||||
|
||||
let episodeList = { total: 0, data: [], meta: {} } as CrunchyEpisodeList;
|
||||
//get episode info
|
||||
|
|
@ -1210,7 +1230,7 @@ export default class Crunchy implements ServiceClass {
|
|||
// Make sure we have a media id without a : in it
|
||||
const currentMediaId = (mMeta.mediaId.includes(':') ? mMeta.mediaId.split(':')[1] : mMeta.mediaId);
|
||||
|
||||
//Make sure token is up to date
|
||||
//Make sure token is up-to-date
|
||||
await this.refreshToken(true, true);
|
||||
let currentVersion;
|
||||
let isPrimary = mMeta.isSubbed;
|
||||
|
|
@ -1218,7 +1238,10 @@ export default class Crunchy implements ServiceClass {
|
|||
headers: {
|
||||
Authorization: `Bearer ${this.token.access_token}`,
|
||||
'X-Cr-Disable-Drm': 'true',
|
||||
'User-Agent': 'Crunchyroll/1.8.0 Nintendo Switch/12.3.12.0 UE4/4.27'
|
||||
'X-Cr-Enable-Drm': 'false',
|
||||
'X-Cr-Stream-Limits': 'false',
|
||||
//'X-Cr-Segment-CDN': 'all',
|
||||
//'User-Agent': 'Crunchyroll/1.8.0 Nintendo Switch/12.3.12.0 UE4/4.27'
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -1348,92 +1371,40 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
}
|
||||
|
||||
let pbData = { total: 0, data: {}, meta: {} } as PlaybackData;
|
||||
if (this.api == 'android') {
|
||||
const videoStreamsReq = [
|
||||
api.beta_cms,
|
||||
`${this.cmsToken.cms.bucket}/videos/${mediaId}/streams`,
|
||||
'?',
|
||||
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 pbData = { total: 0, data: [{}], meta: {} } as PlaybackData;
|
||||
|
||||
let playbackReq = await this.req.getData(videoStreamsReq as string, AuthHeaders);
|
||||
if(!playbackReq.ok || !playbackReq.res){
|
||||
console.error('Request Stream URLs FAILED! Attempting fallback');
|
||||
|
||||
const videoStreamsReq = [
|
||||
domain.api_beta,
|
||||
mMeta.playback,
|
||||
'?',
|
||||
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('');
|
||||
playbackReq = await this.req.getData(videoStreamsReq as string, AuthHeaders);
|
||||
if(!playbackReq.ok || !playbackReq.res){
|
||||
console.error('Fallback Request Stream URLs FAILED!');
|
||||
return undefined;
|
||||
let playStream: CrunchyPlayStream | null = null;
|
||||
if (options.cstream !== 'none') {
|
||||
const playbackReq = await this.req.getData(`https://cr-play-service.prd.crunchyrollsvc.com/v1/${currentVersion ? currentVersion.guid : currentMediaId}/${CrunchyPlayStreams[options.cstream]}/play`, AuthHeaders);
|
||||
if (!playbackReq.ok || !playbackReq.res) {
|
||||
console.error('Non-DRM Request Stream URLs FAILED!');
|
||||
} else {
|
||||
playStream = await playbackReq.res.json() as CrunchyPlayStream;
|
||||
const derivedPlaystreams = {} as CrunchyStreams;
|
||||
for (const hardsub in playStream.hardSubs) {
|
||||
const stream = playStream.hardSubs[hardsub];
|
||||
derivedPlaystreams[hardsub] = {
|
||||
url: stream.url,
|
||||
'hardsub_locale': stream.hlang
|
||||
};
|
||||
}
|
||||
}
|
||||
const pbDataAndroid = await playbackReq.res.json() 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 = await playbackReq.res.json() as PlaybackData;
|
||||
}
|
||||
|
||||
const playbackReq = await this.req.getData(`https://cr-play-service.prd.crunchyrollsvc.com/v1/${currentVersion ? currentVersion.guid : currentMediaId}/console/switch/play`, AuthHeaders);
|
||||
if(!playbackReq.ok || !playbackReq.res){
|
||||
console.error('Non-DRM Request Stream URLs FAILED!');
|
||||
} else {
|
||||
const playStream = await playbackReq.res.json() as CrunchyPlayStream;
|
||||
const derivedPlaystreams = {} as CrunchyStreams;
|
||||
for (const hardsub in playStream.hardSubs) {
|
||||
const stream = playStream.hardSubs[hardsub];
|
||||
derivedPlaystreams[hardsub] = {
|
||||
url: stream.url,
|
||||
'hardsub_locale': stream.hlang
|
||||
derivedPlaystreams[''] = {
|
||||
url: playStream.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
|
||||
};
|
||||
pbData.data[0][`adaptive_${options.cstream}_${playStream.url.includes('m3u8') ? 'hls' : 'dash'}_drm`] = {
|
||||
...derivedPlaystreams
|
||||
};
|
||||
}
|
||||
derivedPlaystreams[''] = {
|
||||
url: playStream.url,
|
||||
hardsub_locale: ''
|
||||
};
|
||||
pbData.data[0]['adaptive_switch_dash'] = {
|
||||
...derivedPlaystreams
|
||||
};
|
||||
}
|
||||
|
||||
variables.push(...([
|
||||
|
|
@ -1460,7 +1431,7 @@ export default class Crunchy implements ServiceClass {
|
|||
console.warn('Decryption not enabled!');
|
||||
}
|
||||
|
||||
for(const s of Object.keys(pbStreams)){
|
||||
for (const s of Object.keys(pbStreams)) {
|
||||
if (
|
||||
(s.match(/hls/) || s.match(/dash/))
|
||||
&& !(s.match(/hls/) && s.match(/drm/))
|
||||
|
|
@ -1483,7 +1454,7 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
}
|
||||
|
||||
if(streams.length < 1){
|
||||
if (streams.length < 1) {
|
||||
console.warn('No full streams found!');
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -1512,7 +1483,7 @@ export default class Crunchy implements ServiceClass {
|
|||
if(s.hardsub_lang == '-'){
|
||||
return false;
|
||||
}
|
||||
return s.hardsub_lang == options.hslang ? true : false;
|
||||
return s.hardsub_lang == options.hslang;
|
||||
});
|
||||
}
|
||||
else{
|
||||
|
|
@ -1522,13 +1493,9 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
dlFailed = true;
|
||||
}
|
||||
}
|
||||
else{
|
||||
} else {
|
||||
streams = streams.filter((s) => {
|
||||
if(s.hardsub_lang != '-'){
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return s.hardsub_lang == '-';
|
||||
});
|
||||
if(streams.length < 1){
|
||||
console.warn('Raw streams not available!');
|
||||
|
|
@ -1543,7 +1510,7 @@ export default class Crunchy implements ServiceClass {
|
|||
let curStream:
|
||||
undefined|typeof streams[0]
|
||||
= undefined;
|
||||
if(!dlFailed){
|
||||
if (!dlFailed) {
|
||||
options.kstream = typeof options.kstream == 'number' ? options.kstream : 1;
|
||||
options.kstream = options.kstream > streams.length ? 1 : options.kstream;
|
||||
|
||||
|
|
@ -1560,6 +1527,14 @@ export default class Crunchy implements ServiceClass {
|
|||
|
||||
let tsFile = undefined;
|
||||
|
||||
// Delete the stream if it's not needed
|
||||
if (options.novids && options.noaudio) {
|
||||
if (playStream) {
|
||||
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});
|
||||
}
|
||||
}
|
||||
|
||||
if(!dlFailed && curStream !== undefined && !(options.novids && options.noaudio)){
|
||||
const streamPlaylistsReq = await this.req.getData(curStream.url, AuthHeaders);
|
||||
if(!streamPlaylistsReq.ok || !streamPlaylistsReq.res){
|
||||
|
|
@ -1568,6 +1543,12 @@ export default class Crunchy implements ServiceClass {
|
|||
} else {
|
||||
const streamPlaylistBody = await streamPlaylistsReq.res.text();
|
||||
if (streamPlaylistBody.match('MPD')) {
|
||||
//We have the stream, so go ahead and delete the active stream
|
||||
if (playStream) {
|
||||
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});
|
||||
}
|
||||
|
||||
//Parse MPD Playlists
|
||||
const streamPlaylists = await parse(streamPlaylistBody, langsData.findLang(langsData.fixLanguageTag(pbData.meta.audio_locale as string) || ''), curStream.url.match(/.*\.urlset\//)[0]);
|
||||
|
||||
|
|
@ -1746,7 +1727,7 @@ export default class Crunchy implements ServiceClass {
|
|||
const sessionId = new Date().getUTCMilliseconds().toString().padStart(3, '0') + process.hrtime.bigint().toString().slice(0, 13);
|
||||
console.info('Decryption Needed, attempting to decrypt');
|
||||
|
||||
const decReq = await this.req.getData('https://pl.crunchyroll.com/drm/v1/auth', {
|
||||
const decReq = await this.req.getData(`${api.drm}`, {
|
||||
'method': 'POST',
|
||||
'body': JSON.stringify({
|
||||
'accounting_id': 'crunchyroll',
|
||||
|
|
@ -1889,7 +1870,7 @@ export default class Crunchy implements ServiceClass {
|
|||
// set plQualityStr
|
||||
const plBandwidth = Math.round(pl.attributes.BANDWIDTH/1024);
|
||||
const qualityStrAdd = `${plResolutionText} (${plBandwidth}KiB/s)`;
|
||||
const qualityStrRegx = new RegExp(qualityStrAdd.replace(/(:|\(|\)|\/)/g, '\\$1'), 'm');
|
||||
const qualityStrRegx = new RegExp(qualityStrAdd.replace(/([:()/])/g, '\\$1'), 'm');
|
||||
const qualityStrMatch = !plQuality.map(a => a.str).join('\r\n').match(qualityStrRegx);
|
||||
if(qualityStrMatch){
|
||||
plQuality.push({
|
||||
|
|
@ -1952,6 +1933,12 @@ export default class Crunchy implements ServiceClass {
|
|||
console.error('CAN\'T FETCH VIDEO PLAYLIST!');
|
||||
dlFailed = true;
|
||||
} else {
|
||||
// We have the stream, so go ahead and delete the active stream
|
||||
if (playStream) {
|
||||
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});
|
||||
}
|
||||
|
||||
const chunkPageBody = await chunkPage.res.text();
|
||||
const chunkPlaylist = m3u8(chunkPageBody);
|
||||
const totalParts = chunkPlaylist.segments.length;
|
||||
|
|
@ -2054,7 +2041,7 @@ export default class Crunchy implements ServiceClass {
|
|||
const subsData = Object.values(pbData.meta.subtitles);
|
||||
const capsData = Object.values(pbData.meta.closed_captions);
|
||||
const subsDataMapped = subsData.map((s) => {
|
||||
const subLang = langsData.fixAndFindCrLC(s.locale);
|
||||
const subLang = langsData.fixAndFindCrLC(s.language);
|
||||
return {
|
||||
...s,
|
||||
isCC: false,
|
||||
|
|
@ -2063,7 +2050,7 @@ export default class Crunchy implements ServiceClass {
|
|||
};
|
||||
}).concat(
|
||||
capsData.map((s) => {
|
||||
const subLang = langsData.fixAndFindCrLC(s.locale);
|
||||
const subLang = langsData.fixAndFindCrLC(s.language);
|
||||
return {
|
||||
...s,
|
||||
isCC: true,
|
||||
|
|
@ -2127,10 +2114,10 @@ export default class Crunchy implements ServiceClass {
|
|||
else{
|
||||
console.warn('Can\'t find urls for subtitles!');
|
||||
}
|
||||
}
|
||||
else{
|
||||
} else{
|
||||
console.info('Subtitles downloading skipped!');
|
||||
}
|
||||
|
||||
await this.sleep(options.waittime);
|
||||
}
|
||||
return {
|
||||
|
|
@ -2150,8 +2137,6 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
const merger = new Merger({
|
||||
onlyVid: hasAudioStreams ? data.filter(a => a.type === 'Video').map((a) : MergerInput => {
|
||||
if (a.type === 'Subtitle')
|
||||
throw new Error('Never');
|
||||
return {
|
||||
lang: a.lang,
|
||||
path: a.path,
|
||||
|
|
@ -2159,8 +2144,6 @@ export default class Crunchy implements ServiceClass {
|
|||
}) : [],
|
||||
skipSubMux: options.skipSubMux,
|
||||
onlyAudio: hasAudioStreams ? data.filter(a => a.type === 'Audio').map((a) : MergerInput => {
|
||||
if (a.type === 'Subtitle')
|
||||
throw new Error('Never');
|
||||
return {
|
||||
lang: a.lang,
|
||||
path: a.path,
|
||||
|
|
@ -2168,12 +2151,6 @@ export default class Crunchy implements ServiceClass {
|
|||
}) : [],
|
||||
output: `${options.output}.${options.mp4 ? 'mp4' : 'mkv'}`,
|
||||
subtitles: data.filter(a => a.type === 'Subtitle').map((a) : SubtitleInput => {
|
||||
if (a.type === 'Video')
|
||||
throw new Error('Never');
|
||||
if (a.type === 'Audio')
|
||||
throw new Error('Never');
|
||||
if (a.type === 'Chapters')
|
||||
throw new Error('Never');
|
||||
return {
|
||||
file: a.path,
|
||||
language: a.language,
|
||||
|
|
@ -2185,20 +2162,12 @@ export default class Crunchy implements ServiceClass {
|
|||
keepAllVideos: options.keepAllVideos,
|
||||
fonts: Merger.makeFontsList(this.cfg.dir.fonts, data.filter(a => a.type === 'Subtitle') as sxItem[]),
|
||||
videoAndAudio: hasAudioStreams ? [] : data.filter(a => a.type === 'Video').map((a) : MergerInput => {
|
||||
if (a.type === 'Subtitle')
|
||||
throw new Error('Never');
|
||||
return {
|
||||
lang: a.lang,
|
||||
path: a.path,
|
||||
};
|
||||
}),
|
||||
chapters: data.filter(a => a.type === 'Chapters').map((a) : MergerInput => {
|
||||
if (a.type === 'Video')
|
||||
throw new Error('Never');
|
||||
if (a.type === 'Audio')
|
||||
throw new Error('Never');
|
||||
if (a.type === 'Subtitle')
|
||||
throw new Error('Never');
|
||||
return {
|
||||
path: a.path,
|
||||
lang: a.lang
|
||||
|
|
@ -2521,7 +2490,7 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
const showInfo = await showInfoReq.res.json();
|
||||
if (log)
|
||||
this.logObject(showInfo, 0);
|
||||
await this.logObject(showInfo, 0);
|
||||
|
||||
let episodeList = { total: 0, data: [], meta: {} } as CrunchyEpisodeList;
|
||||
//get episode info
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# multi-downloader-nx (5.0.1v)
|
||||
# multi-downloader-nx (5.1.5v)
|
||||
|
||||
If you find any bugs in this documentation or in the program itself please report it [over on GitHub](https://github.com/anidl/multi-downloader-nx/issues).
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ The output is organized in pages. Use this command to output the items for the g
|
|||
#### `--locale`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | --- | --- | ---|
|
||||
| Crunchyroll, AnimeOnegai, AnimationDigitalNetwork | `--locale ${locale}` | `string` | `No`| `NaN` | [`''`, `en-US`, `en-IN`, `es-LA`, `es-419`, `es-ES`, `pt-BR`, `pt-PT`, `fr-FR`, `de-DE`, `ar-ME`, `ar-SA`, `it-IT`, `ru-RU`, `tr-TR`, `hi-IN`, `zh-CN`, `zh-TW`, `ko-KR`, `ca-ES`, `pl-PL`, `th-TH`, `ta-IN`, `ms-MY`, `vi-VN`, `id-ID`, `te-IN`, `fr`, `de`, `''`, `es`, `pt`] | `en-US`| `locale: ` |
|
||||
| Crunchyroll, AnimeOnegai, AnimationDigitalNetwork | `--locale ${locale}` | `string` | `No`| `NaN` | [`''`, `en-US`, `en-IN`, `es-LA`, `es-419`, `es-ES`, `pt-BR`, `pt-PT`, `fr-FR`, `de-DE`, `ar-ME`, `ar-SA`, `it-IT`, `ru-RU`, `tr-TR`, `hi-IN`, `zh-CN`, `zh-TW`, `zh-HK`, `ko-KR`, `ca-ES`, `pl-PL`, `th-TH`, `ta-IN`, `ms-MY`, `vi-VN`, `id-ID`, `te-IN`, `fr`, `de`, `''`, `es`, `pt`] | `en-US`| `locale: ` |
|
||||
|
||||
Set the local that will be used for the API.
|
||||
#### `--new`
|
||||
|
|
@ -180,19 +180,25 @@ Select the server to use
|
|||
| Crunchyroll | `--kstream ${stream}` | `number` | `No`| `-k` | [`1`, `2`, `3`, `4`, `5`, `6`, `7`, `8`, `9`, `10`] | `1`| `kstream: ` |
|
||||
|
||||
Select specific stream
|
||||
#### `--cstream`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | --- | --- | ---|
|
||||
| Crunchyroll | `--cstream ${device}` | `string` | `No`| `--cs` | [`chrome`, `firefox`, `safari`, `edge`, `fallback`, `ps4`, `ps5`, `switch`, `samsungtv`, `lgtv`, `rokutv`, `android`, `iphone`, `ipad`, `none`] | `chrome`| `cstream: ` |
|
||||
|
||||
Select specific crunchy play stream by device, or disable stream with "none"
|
||||
#### `--hslang`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | --- | --- | ---|
|
||||
| Crunchyroll | `--hslang ${hslang}` | `string` | `No`| `NaN` | [`none`, `en`, `en-IN`, `es-419`, `es-ES`, `pt-BR`, `pt-PT`, `fr`, `de`, `ar`, `it`, `ru`, `tr`, `hi`, `zh`, `zh-CN`, `zh-TW`, `ko`, `ca-ES`, `pl-PL`, `th-TH`, `ta-IN`, `ms-MY`, `vi-VN`, `id-ID`, `te-IN`, `ja`] | `none`| `hslang: ` |
|
||||
| Crunchyroll | `--hslang ${hslang}` | `string` | `No`| `NaN` | [`none`, `en`, `en-IN`, `es-419`, `es-ES`, `pt-BR`, `pt-PT`, `fr`, `de`, `ar`, `it`, `ru`, `tr`, `hi`, `zh`, `zh-CN`, `zh-TW`, `zh-HK`, `ko`, `ca-ES`, `pl-PL`, `th-TH`, `ta-IN`, `ms-MY`, `vi-VN`, `id-ID`, `te-IN`, `ja`] | `none`| `hslang: ` |
|
||||
|
||||
Download video with specific hardsubs
|
||||
#### `--dlsubs`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | --- | --- | ---|
|
||||
| All | `--dlsubs ${sub1} ${sub2}` | `array` | `No`| `NaN` | [`all`, `none`, `en`, `en-IN`, `es-419`, `es-ES`, `pt-BR`, `pt-PT`, `fr`, `de`, `ar`, `it`, `ru`, `tr`, `hi`, `zh`, `zh-CN`, `zh-TW`, `ko`, `ca-ES`, `pl-PL`, `th-TH`, `ta-IN`, `ms-MY`, `vi-VN`, `id-ID`, `te-IN`, `ja`] | `all`| `dlsubs: ` |
|
||||
| All | `--dlsubs ${sub1} ${sub2}` | `array` | `No`| `NaN` | [`all`, `none`, `en`, `en-IN`, `es-419`, `es-ES`, `pt-BR`, `pt-PT`, `fr`, `de`, `ar`, `it`, `ru`, `tr`, `hi`, `zh`, `zh-CN`, `zh-TW`, `zh-HK`, `ko`, `ca-ES`, `pl-PL`, `th-TH`, `ta-IN`, `ms-MY`, `vi-VN`, `id-ID`, `te-IN`, `ja`] | `all`| `dlsubs: ` |
|
||||
|
||||
Download subtitles by language tag (space-separated)
|
||||
Crunchy Only: en, en-IN, es-419, es-419, es-ES, pt-BR, pt-PT, fr, de, ar, ar, it, ru, tr, hi, zh-CN, zh-TW, ko, ca-ES, pl-PL, th-TH, ta-IN, ms-MY, vi-VN, id-ID, te-IN, ja
|
||||
Crunchy Only: en, en-IN, es-419, es-419, es-ES, pt-BR, pt-PT, fr, de, ar, ar, it, ru, tr, hi, zh-CN, zh-TW, zh-HK, ko, ca-ES, pl-PL, th-TH, ta-IN, ms-MY, vi-VN, id-ID, te-IN, ja
|
||||
#### `--novids`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | ---|
|
||||
|
|
@ -214,10 +220,10 @@ Skip downloading subtitles
|
|||
#### `--dubLang`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | --- | --- | ---|
|
||||
| All | `--dubLang ${dub1} ${dub2}` | `array` | `No`| `NaN` | [`eng`, `spa`, `spa-419`, `spa-ES`, `por`, `fra`, `deu`, `ara-ME`, `ara`, `ita`, `rus`, `tur`, `hin`, `cmn`, `zho`, `chi`, `kor`, `cat`, `pol`, `tha`, `tam`, `may`, `vie`, `ind`, `tel`, `jpn`] | `jpn`| `dubLang: ` |
|
||||
| All | `--dubLang ${dub1} ${dub2}` | `array` | `No`| `NaN` | [`eng`, `spa`, `spa-419`, `spa-ES`, `por`, `fra`, `deu`, `ara-ME`, `ara`, `ita`, `rus`, `tur`, `hin`, `cmn`, `zho`, `chi`, `zh-HK`, `kor`, `cat`, `pol`, `tha`, `tam`, `may`, `vie`, `ind`, `tel`, `jpn`] | `jpn`| `dubLang: ` |
|
||||
|
||||
Set the language to download:
|
||||
Crunchy Only: eng, eng, spa, spa-419, spa-ES, por, por, fra, deu, ara-ME, ara, ita, rus, tur, hin, zho, chi, kor, cat, pol, tha, tam, may, vie, ind, tel, jpn
|
||||
Crunchy Only: eng, eng, spa, spa-419, spa-ES, por, por, fra, deu, ara-ME, ara, ita, rus, tur, hin, zho, chi, zh-HK, kor, cat, pol, tha, tam, may, vie, ind, tel, jpn
|
||||
#### `--all`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | --- | ---|
|
||||
|
|
@ -367,14 +373,14 @@ Set the options given to ffmpeg
|
|||
| All | `--defaultAudio ${args}` | `string` | `No`| `NaN` | `eng`| `defaultAudio: ` |
|
||||
|
||||
Set the default audio track by language code
|
||||
Possible Values: eng, eng, spa, spa-419, spa-ES, por, por, fra, deu, ara-ME, ara, ita, rus, tur, hin, cmn, zho, chi, kor, cat, pol, tha, tam, may, vie, ind, tel, jpn
|
||||
Possible Values: eng, eng, spa, spa-419, spa-ES, por, por, fra, deu, ara-ME, ara, ita, rus, tur, hin, cmn, zho, chi, zh-HK, kor, cat, pol, tha, tam, may, vie, ind, tel, jpn
|
||||
#### `--defaultSub`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | --- | ---|
|
||||
| All | `--defaultSub ${args}` | `string` | `No`| `NaN` | `eng`| `defaultSub: ` |
|
||||
|
||||
Set the default subtitle track by language code
|
||||
Possible Values: eng, eng, spa, spa-419, spa-ES, por, por, fra, deu, ara-ME, ara, ita, rus, tur, hin, cmn, zho, chi, kor, cat, pol, tha, tam, may, vie, ind, tel, jpn
|
||||
Possible Values: eng, eng, spa, spa-419, spa-ES, por, por, fra, deu, ara-ME, ara, ita, rus, tur, hin, cmn, zho, chi, zh-HK, kor, cat, pol, tha, tam, may, vie, ind, tel, jpn
|
||||
### Filename Template
|
||||
#### `--fileName`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||
|
|
|
|||
|
|
@ -5,33 +5,33 @@
|
|||
"dependencies": {
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/styled": "^11.11.5",
|
||||
"@mui/icons-material": "^5.15.15",
|
||||
"@mui/icons-material": "^5.15.20",
|
||||
"@mui/lab": "^5.0.0-alpha.170",
|
||||
"@mui/material": "^5.15.15",
|
||||
"@mui/material": "^5.15.20",
|
||||
"concurrently": "^8.2.2",
|
||||
"notistack": "^2.0.8",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"typescript": "^5.4.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"typescript": "^5.5.2",
|
||||
"uuid": "^9.0.1",
|
||||
"ws": "^8.16.0"
|
||||
"ws": "^8.17.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.24.1",
|
||||
"@babel/core": "^7.24.4",
|
||||
"@babel/preset-env": "^7.24.4",
|
||||
"@babel/preset-react": "^7.24.1",
|
||||
"@babel/preset-typescript": "^7.24.1",
|
||||
"@types/node": "^18.14.0",
|
||||
"@types/react": "^18.2.74",
|
||||
"@types/react-dom": "^18.2.24",
|
||||
"@babel/cli": "^7.24.7",
|
||||
"@babel/core": "^7.24.7",
|
||||
"@babel/preset-env": "^7.24.7",
|
||||
"@babel/preset-react": "^7.24.7",
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"@types/node": "^20.14.6",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"babel-loader": "^9.1.3",
|
||||
"css-loader": "^7.0.0",
|
||||
"css-loader": "^7.1.2",
|
||||
"html-webpack-plugin": "^5.6.0",
|
||||
"style-loader": "^3.3.4",
|
||||
"ts-node": "^10.9.2",
|
||||
"webpack": "^5.91.0",
|
||||
"webpack": "^5.92.1",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^5.0.4"
|
||||
},
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -97,7 +97,7 @@ class HidiveHandler extends Base implements MessageHandler {
|
|||
lang: [],
|
||||
name: item.title,
|
||||
season: item.episodeInformation.seasonNumber+'',
|
||||
seasonTitle: request.series.seasons[item.episodeInformation.seasonNumber-1].title,
|
||||
seasonTitle: request.series.seasons[item.episodeInformation.seasonNumber-1]?.title ?? request.series.title,
|
||||
episode: item.episodeInformation.episodeNumber+'',
|
||||
id: item.id+'',
|
||||
img: item.thumbnailUrl,
|
||||
|
|
|
|||
12
hidive.ts
12
hidive.ts
|
|
@ -127,7 +127,7 @@ export default class Hidive implements ServiceClass {
|
|||
useProxy: true
|
||||
};
|
||||
// get request type
|
||||
const isGet = method == 'GET' ? true : false;
|
||||
const isGet = method == 'GET';
|
||||
if(!isGet){
|
||||
options.body = body == '' ? body : JSON.stringify(body);
|
||||
options.headers['Content-Type'] = 'application/json';
|
||||
|
|
@ -152,7 +152,7 @@ export default class Hidive implements ServiceClass {
|
|||
};
|
||||
let apiReq = await this.req.getData(options.url, apiReqOpts);
|
||||
if(!apiReq.ok || !apiReq.res){
|
||||
if (apiReq.error && apiReq.error.res.statusCode == 401) {
|
||||
if (apiReq.error && apiReq.error.res?.statusCode == 401) {
|
||||
console.warn('Token expired, refreshing token and retrying.');
|
||||
if (await this.refreshToken()) {
|
||||
if (authType == 'other') {
|
||||
|
|
@ -243,11 +243,7 @@ export default class Hidive implements ServiceClass {
|
|||
}, 'auth');
|
||||
if(!authReq.ok || !authReq.res){
|
||||
console.error('Token refresh failed, reinitializing session...');
|
||||
if (!this.initSession()) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
return this.initSession();
|
||||
}
|
||||
const tokens: Record<string, string> = JSON.parse(authReq.res.body);
|
||||
for (const token in tokens) {
|
||||
|
|
@ -435,7 +431,7 @@ export default class Hidive implements ServiceClass {
|
|||
for (let i = 0; i < showData.length; i++) {
|
||||
const titleId = showData[i].id;
|
||||
const seriesTitle = getShowData.series.title;
|
||||
const seasonTitle = getShowData.series.seasons[showData[i].episodeInformation.seasonNumber-1]?.title;
|
||||
const seasonTitle = getShowData.series.seasons[showData[i].episodeInformation.seasonNumber-1]?.title ?? seriesTitle;
|
||||
let nameLong = showData[i].title;
|
||||
if (nameLong.match(/OVA/i)) {
|
||||
nameLong = 'ova' + (('0' + ovaSeq).slice(-2)); ovaSeq++;
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ import modulesCleanup from 'removeNPMAbsolutePaths';
|
|||
import { exec } from '@yao-pkg/pkg';
|
||||
import { execSync } from 'child_process';
|
||||
import { console } from './log';
|
||||
import esbuild from 'esbuild';
|
||||
import path from 'path';
|
||||
|
||||
const buildsDir = './_builds';
|
||||
const nodeVer = 'node18-';
|
||||
const nodeVer = 'node20-';
|
||||
|
||||
type BuildTypes = `${'windows'|'macos'|'linux'|'linuxstatic'|'alpine'}-${'x64'|'arm64'}`|'linuxstatic-armv7'
|
||||
|
||||
|
|
@ -43,8 +45,32 @@ async function buildBinary(buildType: BuildTypes, gui: boolean) {
|
|||
fs.removeSync(buildDir);
|
||||
}
|
||||
fs.mkdirSync(buildDir);
|
||||
console.info('Running esbuild');
|
||||
|
||||
const build = await esbuild.build({
|
||||
entryPoints: [
|
||||
gui ? 'gui.js' : 'index.js',
|
||||
],
|
||||
sourceRoot: './',
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
treeShaking: true,
|
||||
// External source map for debugging
|
||||
sourcemap: true,
|
||||
// Minify and keep the original names
|
||||
minify: true,
|
||||
keepNames: true,
|
||||
outfile: path.join(buildsDir, 'index.cjs'),
|
||||
metafile: true,
|
||||
external: ['cheerio']
|
||||
});
|
||||
|
||||
if (build.errors?.length > 0) console.error(build.errors);
|
||||
if (build.warnings?.length > 0) console.warn(build.warnings);
|
||||
|
||||
const buildConfig = [
|
||||
gui ? 'gui.js' : 'index.js',
|
||||
`${buildsDir}/index.cjs`,
|
||||
'--target', nodeVer + buildType,
|
||||
'--output', `${buildDir}/${pkg.short_name}`,
|
||||
'--compress', 'GZip'
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export type APIType = {
|
|||
cms: string
|
||||
beta_browse: string
|
||||
beta_cms: string,
|
||||
drm: string;
|
||||
/**
|
||||
* Web Header
|
||||
*/
|
||||
|
|
@ -40,7 +41,7 @@ export type APIType = {
|
|||
/**
|
||||
* Mobile Header
|
||||
*/
|
||||
cruncyhAuthHeaderMob: Record<string, string>,
|
||||
crunchyAuthHeaderMob: Record<string, string>,
|
||||
/**
|
||||
* Switch Header
|
||||
*/
|
||||
|
|
@ -74,7 +75,7 @@ const api: APIType = {
|
|||
// beta api
|
||||
beta_auth: `${domain.api_beta}/auth/v1/token`,
|
||||
authBasic: 'Basic bm9haWhkZXZtXzZpeWcwYThsMHE6',
|
||||
authBasicMob: 'Basic bm12anNoZmtueW14eGtnN2ZiaDk6WllJVnJCV1VQYmNYRHRiRDIyVlNMYTZiNFdRb3Mzelg=',
|
||||
authBasicMob: 'Basic dXU4aG0wb2g4dHFpOWV0eXl2aGo6SDA2VnVjRnZUaDJ1dEYxM0FBS3lLNE85UTRhX3BlX1o=',
|
||||
authBasicSwitch: 'Basic dC1rZGdwMmg4YzNqdWI4Zm4wZnE6eWZMRGZNZnJZdktYaDRKWFMxTEVJMmNDcXUxdjVXYW4=',
|
||||
beta_profile: `${domain.api_beta}/accounts/v1/me/profile`,
|
||||
beta_cmsToken: `${domain.api_beta}/index/v2`,
|
||||
|
|
@ -82,8 +83,9 @@ const api: APIType = {
|
|||
cms: `${domain.api_beta}/content/v2/cms`,
|
||||
beta_browse: `${domain.api_beta}/content/v1/browse`,
|
||||
beta_cms: `${domain.api_beta}/cms/v2`,
|
||||
drm: `${domain.api_beta}/drm/v1/auth`,
|
||||
crunchyAuthHeader: {},
|
||||
cruncyhAuthHeaderMob: {},
|
||||
crunchyAuthHeaderMob: {},
|
||||
crunchyAuthHeaderSwitch: {},
|
||||
//hidive API
|
||||
hd_apikey: '508efd7b42d546e19cc24f4d0b414e57e351ca73',
|
||||
|
|
@ -102,9 +104,12 @@ const api: APIType = {
|
|||
api.crunchyAuthHeader = {
|
||||
Authorization: api.authBasic,
|
||||
};
|
||||
api.cruncyhAuthHeaderMob = {
|
||||
|
||||
api.crunchyAuthHeaderMob = {
|
||||
Authorization: api.authBasicMob,
|
||||
'user-agent': 'Crunchyroll/3.60.0 Android/9 okhttp/4.12.0'
|
||||
};
|
||||
|
||||
api.crunchyAuthHeaderSwitch = {
|
||||
Authorization: api.authBasicSwitch,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { DownloadInfo } from '../@types/messageHandler';
|
|||
import { HLSCallback } from './hls-download';
|
||||
import leven from 'leven';
|
||||
import { console } from './log';
|
||||
import { CrunchyPlayStreams } from '../@types/enums';
|
||||
|
||||
let argvC: {
|
||||
[x: string]: unknown;
|
||||
|
|
@ -42,7 +43,8 @@ let argvC: {
|
|||
extid: string | undefined;
|
||||
q: number;
|
||||
x: number;
|
||||
kstream: number;
|
||||
kstream: number;
|
||||
cstream: keyof typeof CrunchyPlayStreams | 'none';
|
||||
partsize: number;
|
||||
hslang: string;
|
||||
dlsubs: string[];
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { aoSearchLocales, dubLanguageCodes, languages, searchLocales, subtitleLanguagesFilter } from './module.langsData';
|
||||
import { CrunchyPlayStreams } from '../@types/enums';
|
||||
|
||||
const groups = {
|
||||
'auth': 'Authentication:',
|
||||
|
|
@ -283,6 +284,20 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
|||
type: 'number',
|
||||
usage: '${stream}'
|
||||
},
|
||||
{
|
||||
name: 'cstream',
|
||||
group: 'dl',
|
||||
alias: 'cs',
|
||||
service: ['crunchy'],
|
||||
type: 'string',
|
||||
describe: 'Select specific crunchy play stream by device, or disable stream with "none"',
|
||||
choices: [...Object.keys(CrunchyPlayStreams), 'none'],
|
||||
default: {
|
||||
default: 'chrome'
|
||||
},
|
||||
docDescribe: true,
|
||||
usage: '${device}'
|
||||
},
|
||||
{
|
||||
name: 'hslang',
|
||||
group: 'dl',
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export class Req {
|
|||
}
|
||||
// debug
|
||||
if(this.debug){
|
||||
console.debug('[DEBUG] GOT OPTIONS:');
|
||||
console.debug('[DEBUG] FETCH OPTIONS:');
|
||||
console.debug(options);
|
||||
}
|
||||
// try do request
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ const languages: LanguageItem[] = [
|
|||
{ locale: 'zh', code: 'cmn', name: 'Chinese (Mandarin, PRC)' },
|
||||
{ cr_locale: 'zh-CN', locale: 'zh-CN', code: 'zho', name: 'Chinese (Mainland China)' },
|
||||
{ cr_locale: 'zh-TW', locale: 'zh-TW', code: 'chi', name: 'Chinese (Taiwan)' },
|
||||
{ cr_locale: 'zh-HK', locale: 'zh-HK', code: 'zh-HK', name: 'Chinese (Hong-Kong)', language: '中文 (粵語)' },
|
||||
{ cr_locale: 'ko-KR', hd_locale: 'Korean', locale: 'ko', code: 'kor', name: 'Korean' },
|
||||
{ cr_locale: 'ca-ES', locale: 'ca-ES', code: 'cat', name: 'Catalan' },
|
||||
{ cr_locale: 'pl-PL', locale: 'pl-PL', code: 'pol', name: 'Polish' },
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ try {
|
|||
const stats = fs.statSync(file);
|
||||
if (stats.size < 1024*8 && stats.isFile()) {
|
||||
const fileContents = fs.readFileSync(file, {'encoding': 'utf8'});
|
||||
if (fileContents.includes('-BEGIN RSA PRIVATE KEY-')) {
|
||||
if (fileContents.includes('-BEGIN PRIVATE KEY-') || fileContents.includes('-BEGIN RSA PRIVATE KEY-')) {
|
||||
privateKey = fs.readFileSync(file);
|
||||
}
|
||||
if (fileContents.includes('widevine_cdm_version')) {
|
||||
|
|
|
|||
26
package.json
26
package.json
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "multi-downloader-nx",
|
||||
"short_name": "aniDL",
|
||||
"version": "5.0.1",
|
||||
"version": "5.1.5",
|
||||
"description": "Downloader for Crunchyroll, Hidive, AnimeOnegai, and AnimationDigitalNetwork with CLI and GUI",
|
||||
"keywords": [
|
||||
"download",
|
||||
|
|
@ -41,8 +41,9 @@
|
|||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/xmldom": "^0.1.34",
|
||||
"@yao-pkg/pkg": "^5.11.1",
|
||||
"@yao-pkg/pkg": "^5.12.0",
|
||||
"cors": "^2.8.5",
|
||||
"esbuild": "^0.21.5",
|
||||
"express": "^4.19.2",
|
||||
"ffprobe": "^1.1.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
|
|
@ -55,10 +56,10 @@
|
|||
"m3u8-parsed": "^1.3.0",
|
||||
"mpd-parser": "^1.3.0",
|
||||
"open": "^8.4.2",
|
||||
"protobufjs": "^7.2.6",
|
||||
"protobufjs": "^7.3.2",
|
||||
"sei-helper": "^3.3.0",
|
||||
"ws": "^8.16.0",
|
||||
"yaml": "^2.4.1",
|
||||
"ws": "^8.17.1",
|
||||
"yaml": "^2.4.5",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -66,21 +67,20 @@
|
|||
"@types/express": "^4.17.21",
|
||||
"@types/ffprobe": "^1.1.8",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/node": "^20.14.6",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@types/yargs": "^17.0.32",
|
||||
"@typescript-eslint/eslint-plugin": "^7.5.0",
|
||||
"@typescript-eslint/parser": "^7.5.0",
|
||||
"@yao-pkg/pkg": "^5.11.1",
|
||||
"@typescript-eslint/eslint-plugin": "^7.13.1",
|
||||
"@typescript-eslint/parser": "^7.13.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-react": "7.34.1",
|
||||
"eslint-plugin-react": "7.34.3",
|
||||
"protoc": "^1.1.3",
|
||||
"removeNPMAbsolutePaths": "^3.0.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"ts-proto": "^1.176.0",
|
||||
"typescript": "5.4.4",
|
||||
"typescript-eslint": "7.5.0"
|
||||
"ts-proto": "^1.180.0",
|
||||
"typescript": "5.5.2",
|
||||
"typescript-eslint": "7.13.1"
|
||||
},
|
||||
"scripts": {
|
||||
"prestart": "pnpm run tsc test",
|
||||
|
|
|
|||
963
pnpm-lock.yaml
963
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -1,12 +0,0 @@
|
|||
import React from 'react';
|
||||
import { StoreAction, StoreContext, StoreState } from '../provider/Store';
|
||||
|
||||
const useStore = () => {
|
||||
const context = React.useContext(StoreContext as unknown as React.Context<[StoreState, React.Dispatch<StoreAction<keyof StoreState>>]>);
|
||||
if (!context) {
|
||||
throw new Error('useStore must be used under Store');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export default useStore;
|
||||
Loading…
Reference in a new issue