Compare commits

...

68 commits

Author SHA1 Message Date
AnimeDL
64c927c761 Fix Docker Release + Documentation 2024-08-15 19:28:29 +00:00
AnimeDL
16e7fcb71e Fix Docker Release 2024-08-15 12:27:56 -07:00
AnimeDL
d61d7e471f
Update docker.yml
Change docker account
2024-08-15 11:40:39 -07:00
AnimeDL
64272b7689 [ADN] Fix DE only streams 2024-08-15 10:12:21 -07:00
AnimeDL
35777ecb58 [ADN] Forgot to fix login 2024-07-28 09:15:29 -07:00
AnimeDL
423ad7b2c9 [ADN] Fix refresh token request 2024-07-28 00:03:52 -07:00
AnimeDL
5cbead06bc Minor grammar fix 2024-07-18 09:44:12 -07:00
AnimeDL
1704bfb4ec [CR} Hotfix downloading
Pretty hacked together, but should work just fine. I'll work on a proper removal of the old API here soon + Documentation
2024-07-08 23:56:45 +00:00
AnimeDL
b488834c0f [CR} Hotfix downloading
Pretty hacked together, but should work just fine. I'll work on a proper removal of the old API here soon
2024-07-08 16:56:13 -07:00
AnimeDL
8d59666a6c [CR] Fix issue with too many streams when novids and noaudio
Fixes issue where you would have too many active streams if using no videos and no audio downloading
2024-06-29 07:49:13 -07:00
AnimeDL
c14a963024 [CR] Add device_id caching
Fixes issue with creating a lot of random device IDs by saving the created device id in the config + Documentation
2024-06-28 23:52:33 +00:00
AnimeDL
0026de73bf [CR] Add device_id caching
Fixes issue with creating a lot of random device IDs by saving the created device id in the config
2024-06-28 16:52:05 -07:00
AnimeDL
d3238d22ba [CR] Whoops
Forgot the other 3 login places + Documentation
2024-06-28 16:32:14 +00:00
AnimeDL
ab090a6858 [CR] Whoops
Forgot the other 3 login places
2024-06-28 09:31:42 -07:00
AnimeDL
64783a0529 [CR] Hotfix login
Also increment version + Documentation
2024-06-28 16:21:11 +00:00
AnimeDL
05d679e6ca [CR] Hotfix login
Also increment version
2024-06-28 09:20:35 -07:00
AnimeDL
cd9586ab13 [CR] Migrate to android token
Will require a fresh login.
Web wasn't working for password auth, for some reason.

Increment version + Documentation
2024-06-24 17:33:48 +00:00
AnimeDL
0a7cfcd917 [CR] Migrate to android token
Will require a fresh login.
Web wasn't working for password auth, for some reason.

Increment version
2024-06-24 10:33:08 -07:00
AnimeDL
ff978e2c88 Increment version + Documentation 2024-06-23 15:28:00 -07:00
AnimeDL
d81ca76594 Code cleanup
Thanks to TypeScript 5.5, this workaround is no longer needed

Increment version
2024-06-23 15:28:00 -07:00
AnimeDL
9457ee6d26 Minor types change 2024-06-22 16:00:15 -07:00
AnimeDL
9a94c33c8b [CR] Disable Non-Play streams
Disables the non-play streams since they don't work ATM. If the situation changes, I'll either re-enable them, or remove them in a future update
2024-06-22 15:58:44 -07:00
AnimeDL
e88352af3f [CR] Header changes 2024-06-22 15:57:39 -07:00
AnimeDL
870b775175 [CR] Clear stream ASAP
Clears the stream as soon as it's done being requested, rather than when the download is finished
2024-06-22 15:57:27 -07:00
AnimeDL
78f5016dd3 [CR] Migrate to web token
This will require a fresh login
2024-06-21 10:06:34 -07:00
AnimeDL
eaec9e62a7 [CR] Whoops 2024-06-21 10:03:18 -07:00
AnimeDL
f92e8dfacb spelling fix 2024-06-21 10:03:03 -07:00
AnimeDL
a6d740e9e9 Some code cleanup 2024-06-20 13:32:27 -07:00
AnimeDL
436a4ca4d1 Improve private key detection 2024-06-20 13:20:45 -07:00
AnimeDL
ea7df30aa7 Bump version + Documentation 2024-06-20 19:58:17 +00:00
AnimeDL
240ff3870b Bump version 2024-06-20 12:57:47 -07:00
AnimeDL
1340624742 Add zh-HK language + Documentation 2024-06-20 19:56:17 +00:00
AnimeDL
b0b7cffddf Add zh-HK language 2024-06-20 12:55:43 -07:00
AnimeDL
fe3f978082 Bump GUI dependencies 2024-06-20 12:53:55 -07:00
AnimeDL
0045abe08c update typescript 2024-06-20 10:57:03 -07:00
AnimeDL
ef975868a3 [CR] Rename crunchy play stream flag + Documentation 2024-06-20 17:39:15 +00:00
AnimeDL
345aa0f267 [CR] Rename crunchy play stream flag 2024-06-20 10:38:42 -07:00
AnimeDL
1f8ddb27a1 Delete weirdly unused code
inb4 somehow this breaks the whole project
2024-06-20 10:34:22 -07:00
AnimeDL
575ea260b6 [CR] Update mobile basic auth header 2024-06-20 10:01:03 -07:00
AnimeDL
9fdc1ac4db Bump node ver to 20 + Documentation 2024-06-20 04:58:14 +00:00
AnimeDL
cf921295f8 Bump node ver to 20 2024-06-19 21:54:47 -07:00
AnimeDL
ed22970346 Dependency bump 2024-06-19 21:53:12 -07:00
AnimeDL
5f034dc348 Add esbuild step to build process
Seems to run a bit faster, also allows for newer node versions with pkg, and seems to also decrease false-positives. This likely can be used to further improve our build process
2024-06-19 21:52:02 -07:00
AnimeDL
c294cdc280 [CR] Add crunchy play streams selector 2024-06-19 20:28:43 -07:00
AnimeDL
16dbc4f1eb Increment version + Documentation 2024-06-18 22:18:53 +00:00
AnimeDL
fba1b1cf22 Increment version 2024-06-18 15:18:17 -07:00
AnimeDL
141fdcf552 [HD] Added fallback to seriesTitle
Added fallback to seriesTitle when seasonTitle cannot be parsed (usually due to the seasonNumber being wrong)
2024-06-18 15:12:22 -07:00
AnimeDL
3aa844f90b Add .idea to gitignore 2024-06-17 08:58:17 -07:00
AnimeDL
be95c1f3bc [HD] Fix crashing issue in certain circumstances
Fixes a crashing issue when an API request fails with specific types of errors (such as ECONNRESET)
2024-05-30 09:53:19 -07:00
AnimeDL
6275d5abe3 Increment version + Documentation 2024-05-29 02:35:29 +00:00
AnimeDL
85c5d45829 Increment version 2024-05-28 19:34:58 -07:00
AnimeDL
9feb3d2f13 [CR] Hotfix DRM request
Addresses #701
2024-05-28 19:34:21 -07:00
AnimeDL
8b5cafff3d [CR] Hotfix stream deletion if slow download
Fixes issue where if someone's internet, or the server is so slow that the session expires, it would fail to delete the watch session
2024-05-23 17:54:54 -07:00
AnimeDL
9ea6258fec [CR] Implement deleting streams
Allows endless non-drm downloads + Documentation
2024-05-23 16:46:30 +00:00
AnimeDL
fc0736c686 [CR] Implement deleting streams
Allows endless non-drm downloads
2024-05-23 09:45:52 -07:00
AnimeDL
dbc2c7d52b [CR] Hotfix non-DRM 2024-05-22 06:49:21 -07:00
AnimeDL
38f849f1a8
Merge pull request #685 from Denoder/patch-1
[HiDive] Remove unreleased episodes from list
2024-05-21 07:28:06 -07:00
Tera
33afc263e7
[HiDive] Remove unreleased episodes from list 2024-05-21 10:56:07 +03:00
Tera
ab73931fb9
[HiDive] Remove unreleased episodes from list
Allow listing to be visible but now allow it in the episodes list if it's not available
2024-05-19 00:11:51 +03:00
Tera
87c7de7417
Update hidive.ts
Account for when the episode title is added, but still not released, when it's not available there's a 10 second clip.
2024-05-19 00:06:09 +03:00
AnimeDL
f1042ded9f [ADN] Fix some output problems
Fix season detection, and fix null instead of show title in some circumstances + Documentation
2024-05-17 23:53:56 +00:00
AnimeDL
8dd0725f9a [ADN] Fix some output problems
Fix season detection, and fix null instead of show title in some circumstances
2024-05-17 16:53:10 -07:00
AnimeDL
5730450e11
Merge pull request #692 from anidl/dependabot/npm_and_yarn/ts-proto-1.176.0
Bump ts-proto from 1.171.0 to 1.176.0
2024-05-16 21:44:06 -07:00
dependabot[bot]
8da4074b1b
Bump ts-proto from 1.171.0 to 1.176.0
Bumps [ts-proto](https://github.com/stephenh/ts-proto) from 1.171.0 to 1.176.0.
- [Release notes](https://github.com/stephenh/ts-proto/releases)
- [Changelog](https://github.com/stephenh/ts-proto/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stephenh/ts-proto/compare/v1.171.0...v1.176.0)

---
updated-dependencies:
- dependency-name: ts-proto
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-16 07:01:09 +00:00
Tera
130fa5ee11
[HiDive] Remove unreleased episodes from list
When doing a range download/single selection of an anime that's currently airing episodes that haven't released will still get downloaded at 0 bytes. This will skip them if they have a date range in them, rather than an appropriate title.
2024-05-10 00:27:35 +03:00
AnimeDL
b2488edc02 [HD] Fix movie downloading
Fixes #672
2024-04-23 14:35:56 -07:00
AnimeDL
773bbf034c Add SSL support to websocket 2024-04-23 11:10:27 -07:00
AnimeDL
d507135eaa [ADN] Add episode ID to listing 2024-04-21 08:52:12 -07:00
28 changed files with 1753 additions and 1537 deletions

View file

@ -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

View file

@ -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 }}

View file

@ -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 }}

View file

@ -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
View file

@ -34,6 +34,7 @@ gui/react/build/
docker-compose.yml
crunchyendpoints
.vscode
.idea
/logs
/tmp/*/
!videos/.gitkeep

View file

@ -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 };

View file

@ -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
View 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',
}

View file

@ -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;

25
adn.ts
View file

@ -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 };
}
@ -273,6 +268,7 @@ export default class AnimationDigitalNetwork implements ServiceClass {
} else {
episode.season = '1';
}
show.value.videos[episodeIndex].season = episode.season;
if (!episodeNumber) {
specialIndex++;
const special = show.value.videos.splice(episodeIndex, 1);
@ -280,12 +276,12 @@ export default class AnimationDigitalNetwork implements ServiceClass {
specials.push(...special);
episodeIndex--;
} else {
console.info(` [E${episode.shortNumber}] ${episode.number} - ${episode.name}`);
console.info(` (${episode.id}) [E${episode.shortNumber}] ${episode.number} - ${episode.name}`);
}
episodeIndex++;
}
for (const special of specials) {
console.info(` [${special.shortNumber}] ${special.number} - ${special.name}`);
console.info(` (${special.id}) [${special.shortNumber}] ${special.number} - ${special.name}`);
}
show.value.videos.push(...specials);
return { isOk: true, value: show.value };
@ -446,7 +442,7 @@ export default class AnimationDigitalNetwork implements ServiceClass {
let fileName;
const variables: Variable[] = [];
if(data.show.title && data.shortNumber && data.title){
mediaName = `${data.show.shortTitle} - ${data.shortNumber} - ${data.title}`;
mediaName = `${data.show.shortTitle ?? data.show.title} - ${data.shortNumber} - ${data.title}`;
}
const files: DownloadedMedia[] = [];
@ -462,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){
@ -541,7 +538,7 @@ export default class AnimationDigitalNetwork implements ServiceClass {
['title', data.title, true],
['episode', isNaN(parseFloat(data.shortNumber)) ? data.shortNumber : parseFloat(data.shortNumber), false],
['service', 'ADN', false],
['seriesTitle', data.show.shortTitle, true],
['seriesTitle', data.show.shortTitle ?? data.show.title, true],
['showTitle', data.show.title, true],
['season', isNaN(parseFloat(data.season)) ? data.season : parseFloat(data.season), false]
] as [AvailableFilenameVars, string|number, boolean][]).map((a): Variable => {
@ -614,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
View file

@ -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';

View file

@ -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,16 +1230,19 @@ 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;
const AuthHeaders = {
const AuthHeaders: RequestInit = {
headers: {
Authorization: `Bearer ${this.token.access_token}`,
'X-Cr-Disable-Drm': 'true'
},
useProxy: true
'X-Cr-Disable-Drm': 'true',
'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'
}
};
//Get Media GUID
@ -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

View file

@ -1,4 +1,4 @@
# multi-downloader-nx (5.0.0v)
# 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**

View file

@ -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

View file

@ -70,7 +70,7 @@ const MessageChannelProvider: FCWithChildren = ({ children }) => {
const { enqueueSnackbar } = useSnackbar();
React.useEffect(() => {
const wss = new WebSocket(`ws://${process.env.NODE_ENV === 'development' ? 'localhost:3000' : window.location.host}/public`);
const wss = new WebSocket(`${location.protocol == 'https:' ? 'wss' : 'ws'}://${process.env.NODE_ENV === 'development' ? 'localhost:3000' : window.location.host}/public`);
wss.addEventListener('open', () => {
setPublicWS(wss);
});
@ -103,7 +103,7 @@ const MessageChannelProvider: FCWithChildren = ({ children }) => {
});
}
const wws = new WebSocket(`ws://${process.env.NODE_ENV === 'development' ? 'localhost:3000' : window.location.host}/private?${search}`, );
const wws = new WebSocket(`${location.protocol == 'https:' ? 'wss' : 'ws'}://${process.env.NODE_ENV === 'development' ? 'localhost:3000' : window.location.host}/private?${search}`, );
wws.addEventListener('open', () => {
console.log('[INFO] [WS] Connected');
setSocket(wws);

View file

@ -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,

View file

@ -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) {
@ -369,12 +365,15 @@ export default class Hidive implements ServiceClass {
season.value.paging.moreDataAvailable = seasonPage.value.paging.moreDataAvailable;
}
for (const episode of season.value.episodes) {
const datePattern = /\d{1,2}\/\d{1,2}\/\d{2,4} \d{1,2}:\d{2} UTC/;
if (episode.title.includes(' - ')) {
episode.episodeInformation.episodeNumber = parseFloat(episode.title.split(' - ')[0].replace('E', ''));
episode.title = episode.title.split(' - ')[1];
}
//S${episode.episodeInformation.seasonNumber}E${episode.episodeInformation.episodeNumber} -
episodes.push(episode);
if (!datePattern.test(episode.title) && episode.duration !== 10) {
episodes.push(episode);
}
console.info(` [E.${episode.id}] ${episode.title}`);
}
}
@ -397,12 +396,15 @@ export default class Hidive implements ServiceClass {
}
const episodes: Episode[] = [];
for (const episode of season.value.episodes) {
const datePattern = /\d{1,2}\/\d{1,2}\/\d{2,4} \d{1,2}:\d{2} UTC/;
if (episode.title.includes(' - ')) {
episode.episodeInformation.episodeNumber = parseFloat(episode.title.split(' - ')[0].replace('E', ''));
episode.title = episode.title.split(' - ')[1];
}
//S${episode.episodeInformation.seasonNumber}E${episode.episodeInformation.episodeNumber} -
episodes.push(episode);
if (!datePattern.test(episode.title) && episode.duration !== 10) {
episodes.push(episode);
}
console.info(` [E.${episode.id}] ${episode.title}`);
}
const series: NewHidiveSeriesExtra = {...season.value.series, season: season.value};
@ -429,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++;
@ -566,7 +568,7 @@ export default class Hidive implements ServiceClass {
}
const episodeData = JSON.parse(episodeDataReq.res.body) as NewHidiveEpisode;
if (episodeData.title.includes(' - ')) {
if (episodeData.title.includes(' - ') && episodeData.episodeInformation) {
episodeData.episodeInformation.episodeNumber = parseFloat(episodeData.title.split(' - ')[0].replace('E', ''));
episodeData.title = episodeData.title.split(' - ')[1];
}
@ -1077,4 +1079,4 @@ export default class Hidive implements ServiceClass {
setTimeout(resolve, ms);
});
}
}
}

View file

@ -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'

View file

@ -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,
};

View file

@ -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[];

View file

@ -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',

View file

@ -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

View file

@ -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' },

View file

@ -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')) {

View file

@ -1,7 +1,7 @@
{
"name": "multi-downloader-nx",
"short_name": "aniDL",
"version": "5.0.0",
"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.171.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",

File diff suppressed because it is too large Load diff

View file

@ -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;