Compare commits
2 commits
master
...
multi-down
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d1926cca3 | ||
|
|
501928b92d |
34 changed files with 1754 additions and 1872 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: 20
|
||||
node-version: 18
|
||||
- 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: |
|
||||
"multidl/multi-downloader-nx:latest"
|
||||
"izuco/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: 20
|
||||
node-version: 18
|
||||
check-latest: true
|
||||
- name: Install Node modules
|
||||
run: |
|
||||
|
|
@ -61,6 +61,6 @@ jobs:
|
|||
github-token: ${{ github.token }}
|
||||
push: true
|
||||
tags: |
|
||||
"multidl/multi-downloader-nx:${{ github.event.release.tag_name }}"
|
||||
"izuco/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: 20
|
||||
node-version: 18
|
||||
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: 20
|
||||
node-version: 18
|
||||
check-latest: true
|
||||
- run: pnpm i
|
||||
- run: pnpm run test
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -34,7 +34,6 @@ 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,8 +1,6 @@
|
|||
import { Locale } from './playbackData';
|
||||
|
||||
export interface CrunchyPlayStream {
|
||||
assetId: string;
|
||||
audioLocale: Locale;
|
||||
audioLocale: string;
|
||||
bifs: string;
|
||||
burnedInLocale: string;
|
||||
captions: { [key: string]: Caption };
|
||||
|
|
|
|||
2
@types/crunchyTypes.d.ts
vendored
2
@types/crunchyTypes.d.ts
vendored
|
|
@ -2,12 +2,10 @@ 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,
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
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',
|
||||
}
|
||||
6
@types/messageHandler.d.ts
vendored
6
@types/messageHandler.d.ts
vendored
|
|
@ -107,7 +107,7 @@ export type FuniStreamData = { force?: 'Y'|'y'|'N'|'n'|'C'|'c', callbackMaker?:
|
|||
ffmpegOptions: string[], mkvmergeOptions: string[], defaultAudio: LanguageItem, defaultSub: LanguageItem, ccTag: string }
|
||||
export type FuniSubsData = { nosubs?: boolean, sub: boolean, dlsubs: string[], ccTag: string }
|
||||
export type DownloadData = {
|
||||
hslang?: string; id: string, e: string, dubLang: string[], dlsubs: string[], fileName: string, q: number, novids: boolean, noaudio: boolean, dlVideoOnce: boolean
|
||||
hslang?: string; id: string, e: string, dubLang: string[], dlsubs: string[], fileName: string, q: number, novids: boolean, noaudio: boolean, dlVideoOnce: boolean
|
||||
}
|
||||
|
||||
export type AuthResponse = ResponseBase<undefined>;
|
||||
|
|
@ -136,7 +136,7 @@ export type ProgressData = {
|
|||
|
||||
export type PossibleMessages = keyof ServiceHandler;
|
||||
|
||||
export type DownloadInfo = {
|
||||
export type DownloadInfo = {
|
||||
image: string,
|
||||
parent: {
|
||||
title: string
|
||||
|
|
@ -158,4 +158,4 @@ export type GuiState = {
|
|||
|
||||
export type GuiStateService = {
|
||||
queue: QueueItem[]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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: Subtitles;
|
||||
captions: Record<unknown>;
|
||||
}
|
||||
|
||||
export interface Subtitles {
|
||||
''?: SubtitleInfo;
|
||||
'': SubtitleInfo;
|
||||
'en-US'?: SubtitleInfo;
|
||||
'es-LA'?: SubtitleInfo;
|
||||
'es-419'?: SubtitleInfo;
|
||||
|
|
|
|||
21
adn.ts
21
adn.ts
|
|
@ -204,6 +204,8 @@ 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 };
|
||||
|
|
@ -214,16 +216,19 @@ export default class AnimationDigitalNetwork implements ServiceClass {
|
|||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token.accessToken}`,
|
||||
'X-Access-Token': this.token.accessToken,
|
||||
'content-type': 'application/json'
|
||||
'Cookie': `adnrt=${this.token.refreshToken}`,
|
||||
'X-Access-Token': this.token.accessToken
|
||||
},
|
||||
body: JSON.stringify({refreshToken: this.token.refreshToken})
|
||||
body: '{}'
|
||||
});
|
||||
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 };
|
||||
}
|
||||
|
|
@ -268,7 +273,6 @@ 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);
|
||||
|
|
@ -442,7 +446,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.show.title} - ${data.shortNumber} - ${data.title}`;
|
||||
mediaName = `${data.show.shortTitle} - ${data.shortNumber} - ${data.title}`;
|
||||
}
|
||||
|
||||
const files: DownloadedMedia[] = [];
|
||||
|
|
@ -458,8 +462,7 @@ 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}`,
|
||||
'X-Target-Distribution': this.locale
|
||||
Authorization: `Bearer ${this.token.accessToken}`
|
||||
}
|
||||
});
|
||||
if(!configReq.ok || !configReq.res){
|
||||
|
|
@ -538,7 +541,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 ?? data.show.title, true],
|
||||
['seriesTitle', data.show.shortTitle, 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 => {
|
||||
|
|
@ -611,7 +614,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;
|
||||
let lang: string | undefined = 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';
|
||||
|
|
|
|||
326
crunchy.ts
326
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, Subtitles } from './@types/playbackData';
|
||||
import { CrunchyStreams, PlaybackData } from './@types/playbackData';
|
||||
import { downloaded } from './modules/module.downloadArchive';
|
||||
import parseSelect from './modules/module.parseSelect';
|
||||
import { AvailableFilenameVars, getDefault } from './modules/module.args';
|
||||
|
|
@ -44,8 +44,7 @@ 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';
|
||||
import buildCLIHandler from './modules/downloadProgress';
|
||||
|
||||
export type sxItem = {
|
||||
language: langsData.LanguageItem,
|
||||
|
|
@ -226,18 +225,15 @@ 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',
|
||||
'device_id': uuid,
|
||||
'device_type': 'Chrome on Windows'
|
||||
'scope': 'offline_access'
|
||||
}).toString();
|
||||
const authReqOpts: reqModule.Params = {
|
||||
method: 'POST',
|
||||
headers: api.crunchyAuthHeaderMob,
|
||||
headers: api.crunchyAuthHeaderSwitch,
|
||||
body: authData
|
||||
};
|
||||
const authReq = await this.req.getData(api.beta_auth, authReqOpts);
|
||||
|
|
@ -246,7 +242,6 @@ 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();
|
||||
|
|
@ -255,16 +250,13 @@ 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.crunchyAuthHeaderMob,
|
||||
headers: api.crunchyAuthHeaderSwitch,
|
||||
body: authData
|
||||
};
|
||||
const authReq = await this.req.getData(api.beta_auth, authReqOpts);
|
||||
|
|
@ -273,7 +265,6 @@ 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);
|
||||
}
|
||||
|
|
@ -308,18 +299,14 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
|
||||
public async loginWithToken(refreshToken: string) {
|
||||
const uuid = randomUUID();
|
||||
const authData = new URLSearchParams({
|
||||
'refresh_token': this.token.refresh_token,
|
||||
'refresh_token': refreshToken,
|
||||
'grant_type': 'refresh_token',
|
||||
//'grant_type': 'etp_rt_cookie',
|
||||
'scope': 'offline_access',
|
||||
'device_id': uuid,
|
||||
'device_type': 'Chrome on Windows'
|
||||
'scope': 'offline_access'
|
||||
}).toString();
|
||||
const authReqOpts: reqModule.Params = {
|
||||
method: 'POST',
|
||||
headers: {...api.crunchyAuthHeaderMob, Cookie: `etp_rt=${refreshToken}`},
|
||||
headers: api.crunchyAuthHeaderSwitch,
|
||||
body: authData
|
||||
};
|
||||
const authReq = await this.req.getData(api.beta_auth, authReqOpts);
|
||||
|
|
@ -331,7 +318,6 @@ 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);
|
||||
|
|
@ -350,18 +336,14 @@ 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',
|
||||
//'grant_type': 'etp_rt_cookie',
|
||||
'scope': 'offline_access',
|
||||
'device_id': uuid,
|
||||
'device_type': 'Chrome on Windows'
|
||||
'scope': 'offline_access'
|
||||
}).toString();
|
||||
const authReqOpts: reqModule.Params = {
|
||||
method: 'POST',
|
||||
headers: {...api.crunchyAuthHeaderMob, Cookie: `etp_rt=${this.token.refresh_token}`},
|
||||
headers: api.crunchyAuthHeaderSwitch,
|
||||
body: authData
|
||||
};
|
||||
const authReq = await this.req.getData(api.beta_auth, authReqOpts);
|
||||
|
|
@ -373,7 +355,6 @@ 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);
|
||||
}
|
||||
|
|
@ -633,8 +614,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;
|
||||
const showObjectBooleans = oBooleans.length > 0 && !iMetadata.hide_metadata;
|
||||
const showObjectMetadata = oMetadata.length > 0 && !iMetadata.hide_metadata ? true : false;
|
||||
const showObjectBooleans = oBooleans.length > 0 && !iMetadata.hide_metadata ? true : false;
|
||||
// make obj ids
|
||||
const objects_ids: string[] = [];
|
||||
objects_ids.push(oTypes[item.type as keyof typeof oTypes] + ':' + item.id);
|
||||
|
|
@ -689,7 +670,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){
|
||||
|
|
@ -772,7 +753,7 @@ export default class Crunchy implements ServiceClass {
|
|||
return;
|
||||
}
|
||||
for(const item of movieListing.data){
|
||||
await this.logObject(item, pad, false, false);
|
||||
this.logObject(item, pad, false, false);
|
||||
}
|
||||
|
||||
//Movies
|
||||
|
|
@ -783,7 +764,7 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
const moviesList = await moviesListReq.res.json();
|
||||
for(const item of moviesList.data){
|
||||
await this.logObject(item, pad + 2);
|
||||
this.logObject(item, pad+2);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -841,7 +822,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();
|
||||
await this.logObject(showInfo.data[0], 0);
|
||||
this.logObject(showInfo.data[0], 0);
|
||||
|
||||
let episodeList = { total: 0, data: [], meta: {} } as CrunchyEpisodeList;
|
||||
//get episode info
|
||||
|
|
@ -1203,7 +1184,7 @@ export default class Crunchy implements ServiceClass {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!this.cfg.bin.ffmpeg)
|
||||
if (!this.cfg.bin.ffmpeg)
|
||||
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||
|
||||
let mediaName = '...';
|
||||
|
|
@ -1230,19 +1211,16 @@ 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: RequestInit = {
|
||||
const AuthHeaders = {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token.access_token}`,
|
||||
'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'
|
||||
}
|
||||
'X-Cr-Disable-Drm': 'true'
|
||||
},
|
||||
useProxy: true
|
||||
};
|
||||
|
||||
//Get Media GUID
|
||||
|
|
@ -1290,7 +1268,7 @@ export default class Crunchy implements ServiceClass {
|
|||
const startMS = startTimeMS ? startTimeMS : '00', endMS = endTimeMS ? endTimeMS : '00';
|
||||
const startFormatted = startTime.toISOString().substring(11, 19)+'.'+startMS;
|
||||
const endFormatted = endTime.toISOString().substring(11, 19)+'.'+endMS;
|
||||
|
||||
|
||||
//Push Generated Chapters
|
||||
if (chapterData.startTime > 1) {
|
||||
compiledChapters.push(
|
||||
|
|
@ -1339,7 +1317,7 @@ export default class Crunchy implements ServiceClass {
|
|||
endTime.setSeconds(chapter.end);
|
||||
const startFormatted = startTime.toISOString().substring(11, 19)+'.00';
|
||||
const endFormatted = endTime.toISOString().substring(11, 19)+'.00';
|
||||
|
||||
|
||||
//Push generated chapters
|
||||
if (chapter.type == 'intro') {
|
||||
if (chapter.start > 0) {
|
||||
|
|
@ -1371,40 +1349,92 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
}
|
||||
|
||||
const pbData = { total: 0, data: [{}], meta: {} } as PlaybackData;
|
||||
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('');
|
||||
|
||||
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
|
||||
};
|
||||
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;
|
||||
}
|
||||
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
|
||||
}
|
||||
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.data[0]['adaptive_switch_dash'] = {
|
||||
...derivedPlaystreams
|
||||
};
|
||||
}
|
||||
|
||||
variables.push(...([
|
||||
|
|
@ -1431,10 +1461,10 @@ 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/))
|
||||
(s.match(/hls/) || s.match(/dash/))
|
||||
&& !(s.match(/hls/) && s.match(/drm/))
|
||||
&& !((!canDecrypt || !this.cfg.bin.mp4decrypt) && s.match(/drm/))
|
||||
&& !s.match(/trailer/)
|
||||
) {
|
||||
|
|
@ -1454,7 +1484,7 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
}
|
||||
|
||||
if (streams.length < 1) {
|
||||
if(streams.length < 1){
|
||||
console.warn('No full streams found!');
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -1483,7 +1513,7 @@ export default class Crunchy implements ServiceClass {
|
|||
if(s.hardsub_lang == '-'){
|
||||
return false;
|
||||
}
|
||||
return s.hardsub_lang == options.hslang;
|
||||
return s.hardsub_lang == options.hslang ? true : false;
|
||||
});
|
||||
}
|
||||
else{
|
||||
|
|
@ -1493,9 +1523,13 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
dlFailed = true;
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else{
|
||||
streams = streams.filter((s) => {
|
||||
return s.hardsub_lang == '-';
|
||||
if(s.hardsub_lang != '-'){
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if(streams.length < 1){
|
||||
console.warn('Raw streams not available!');
|
||||
|
|
@ -1510,7 +1544,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;
|
||||
|
||||
|
|
@ -1526,14 +1560,7 @@ 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});
|
||||
}
|
||||
}
|
||||
const downloadStreams = [];
|
||||
|
||||
if(!dlFailed && curStream !== undefined && !(options.novids && options.noaudio)){
|
||||
const streamPlaylistsReq = await this.req.getData(curStream.url, AuthHeaders);
|
||||
|
|
@ -1543,12 +1570,6 @@ 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]);
|
||||
|
||||
|
|
@ -1651,28 +1672,17 @@ export default class Crunchy implements ServiceClass {
|
|||
const videoJson: M3U8Json = {
|
||||
segments: chosenVideoSegments.segments
|
||||
};
|
||||
const videoDownload = await new streamdl({
|
||||
output: chosenVideoSegments.pssh ? `${tempTsFile}.video.enc.m4s` : `${tsFile}.video.m4s`,
|
||||
const output = chosenVideoSegments.pssh ? `${tempTsFile}.video.enc.m4s` : `${tsFile}.video.m4s`;
|
||||
downloadStreams.push(new streamdl({
|
||||
output,
|
||||
timeout: options.timeout,
|
||||
m3u8json: videoJson,
|
||||
// baseurl: chunkPlaylist.baseUrl,
|
||||
threads: options.partsize,
|
||||
fsRetryTime: options.fsRetryTime * 1000,
|
||||
override: options.force,
|
||||
callback: options.callbackMaker ? options.callbackMaker({
|
||||
fileName: `${path.isAbsolute(outFile) ? outFile.slice(this.cfg.dir.content.length) : outFile}`,
|
||||
image: medias.image,
|
||||
parent: {
|
||||
title: medias.seasonTitle
|
||||
},
|
||||
title: medias.episodeTitle,
|
||||
language: lang
|
||||
}) : undefined
|
||||
}).download();
|
||||
if(!videoDownload.ok){
|
||||
console.error(`DL Stats: ${JSON.stringify(videoDownload.parts)}\n`);
|
||||
dlFailed = true;
|
||||
}
|
||||
identifier: output
|
||||
}).download());
|
||||
dlVideoOnce = true;
|
||||
videoDownloaded = true;
|
||||
}
|
||||
|
|
@ -1693,28 +1703,17 @@ export default class Crunchy implements ServiceClass {
|
|||
const audioJson: M3U8Json = {
|
||||
segments: chosenAudioSegments.segments
|
||||
};
|
||||
const audioDownload = await new streamdl({
|
||||
output: chosenAudioSegments.pssh ? `${tempTsFile}.audio.enc.m4s` : `${tsFile}.audio.m4s`,
|
||||
const output = chosenAudioSegments.pssh ? `${tempTsFile}.audio.enc.m4s` : `${tsFile}.audio.m4s`;
|
||||
downloadStreams.push(new streamdl({
|
||||
output,
|
||||
timeout: options.timeout,
|
||||
m3u8json: audioJson,
|
||||
// baseurl: chunkPlaylist.baseUrl,
|
||||
threads: options.partsize,
|
||||
fsRetryTime: options.fsRetryTime * 1000,
|
||||
override: options.force,
|
||||
callback: options.callbackMaker ? options.callbackMaker({
|
||||
fileName: `${path.isAbsolute(outFile) ? outFile.slice(this.cfg.dir.content.length) : outFile}`,
|
||||
image: medias.image,
|
||||
parent: {
|
||||
title: medias.seasonTitle
|
||||
},
|
||||
title: medias.episodeTitle,
|
||||
language: lang
|
||||
}) : undefined
|
||||
}).download();
|
||||
if(!audioDownload.ok){
|
||||
console.error(`DL Stats: ${JSON.stringify(audioDownload.parts)}\n`);
|
||||
dlFailed = true;
|
||||
}
|
||||
identifier: output
|
||||
}).download());
|
||||
audioDownloaded = true;
|
||||
} else if (options.noaudio) {
|
||||
console.info('Skipping audio download...');
|
||||
|
|
@ -1727,7 +1726,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(`${api.drm}`, {
|
||||
const decReq = await this.req.getData('https://pl.crunchyroll.com/drm/v1/auth', {
|
||||
'method': 'POST',
|
||||
'body': JSON.stringify({
|
||||
'accounting_id': 'crunchyroll',
|
||||
|
|
@ -1870,7 +1869,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({
|
||||
|
|
@ -1933,12 +1932,6 @@ 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;
|
||||
|
|
@ -1952,28 +1945,17 @@ export default class Crunchy implements ServiceClass {
|
|||
if (!fs.existsSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val)))
|
||||
fs.mkdirSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val));
|
||||
});
|
||||
const dlStreamByPl = await new streamdl({
|
||||
output: `${tsFile}.ts`,
|
||||
const output = `${tsFile}.ts`;
|
||||
downloadStreams.push(new streamdl({
|
||||
output,
|
||||
timeout: options.timeout,
|
||||
m3u8json: chunkPlaylist,
|
||||
// baseurl: chunkPlaylist.baseUrl,
|
||||
threads: options.partsize,
|
||||
fsRetryTime: options.fsRetryTime * 1000,
|
||||
override: options.force,
|
||||
callback: options.callbackMaker ? options.callbackMaker({
|
||||
fileName: `${path.isAbsolute(outFile) ? outFile.slice(this.cfg.dir.content.length) : outFile}`,
|
||||
image: medias.image,
|
||||
parent: {
|
||||
title: medias.seasonTitle
|
||||
},
|
||||
title: medias.episodeTitle,
|
||||
language: lang
|
||||
}) : undefined
|
||||
}).download();
|
||||
if (!dlStreamByPl.ok) {
|
||||
console.error(`DL Stats: ${JSON.stringify(dlStreamByPl.parts)}\n`);
|
||||
dlFailed = true;
|
||||
}
|
||||
identifier: output
|
||||
}).download());
|
||||
files.push({
|
||||
type: 'Video',
|
||||
path: `${tsFile}.ts`,
|
||||
|
|
@ -1995,6 +1977,14 @@ export default class Crunchy implements ServiceClass {
|
|||
fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep);
|
||||
}
|
||||
|
||||
const downloads = await Promise.all(downloadStreams);
|
||||
for (const download of downloads) {
|
||||
if (!download.ok) {
|
||||
console.error('Download failed, download stats: ', download.parts);
|
||||
dlFailed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (compiledChapters.length > 0) {
|
||||
try {
|
||||
fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep);
|
||||
|
|
@ -2041,7 +2031,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.language);
|
||||
const subLang = langsData.fixAndFindCrLC(s.locale);
|
||||
return {
|
||||
...s,
|
||||
isCC: false,
|
||||
|
|
@ -2050,7 +2040,7 @@ export default class Crunchy implements ServiceClass {
|
|||
};
|
||||
}).concat(
|
||||
capsData.map((s) => {
|
||||
const subLang = langsData.fixAndFindCrLC(s.language);
|
||||
const subLang = langsData.fixAndFindCrLC(s.locale);
|
||||
return {
|
||||
...s,
|
||||
isCC: true,
|
||||
|
|
@ -2114,10 +2104,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 {
|
||||
|
|
@ -2137,6 +2127,8 @@ 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,
|
||||
|
|
@ -2144,6 +2136,8 @@ 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,
|
||||
|
|
@ -2151,6 +2145,12 @@ 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,
|
||||
|
|
@ -2162,12 +2162,20 @@ 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
|
||||
|
|
@ -2490,7 +2498,7 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
const showInfo = await showInfoReq.res.json();
|
||||
if (log)
|
||||
await this.logObject(showInfo, 0);
|
||||
this.logObject(showInfo, 0);
|
||||
|
||||
let episodeList = { total: 0, data: [], meta: {} } as CrunchyEpisodeList;
|
||||
//get episode info
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# multi-downloader-nx (5.1.5v)
|
||||
# multi-downloader-nx (5.0.0v)
|
||||
|
||||
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`, `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: ` |
|
||||
| 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: ` |
|
||||
|
||||
Set the local that will be used for the API.
|
||||
#### `--new`
|
||||
|
|
@ -180,25 +180,19 @@ 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`, `zh-HK`, `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`, `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`, `zh-HK`, `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`, `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, zh-HK, 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, 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**
|
||||
| --- | --- | --- | --- | --- | ---|
|
||||
|
|
@ -220,10 +214,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`, `zh-HK`, `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`, `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, zh-HK, 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, kor, cat, pol, tha, tam, may, vie, ind, tel, jpn
|
||||
#### `--all`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | --- | ---|
|
||||
|
|
@ -373,14 +367,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, zh-HK, 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, 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, zh-HK, 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, 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.20",
|
||||
"@mui/icons-material": "^5.15.15",
|
||||
"@mui/lab": "^5.0.0-alpha.170",
|
||||
"@mui/material": "^5.15.20",
|
||||
"@mui/material": "^5.15.15",
|
||||
"concurrently": "^8.2.2",
|
||||
"notistack": "^2.0.8",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"typescript": "^5.5.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"typescript": "^5.4.4",
|
||||
"uuid": "^9.0.1",
|
||||
"ws": "^8.17.1"
|
||||
"ws": "^8.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
"@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",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"babel-loader": "^9.1.3",
|
||||
"css-loader": "^7.1.2",
|
||||
"css-loader": "^7.0.0",
|
||||
"html-webpack-plugin": "^5.6.0",
|
||||
"style-loader": "^3.3.4",
|
||||
"ts-node": "^10.9.2",
|
||||
"webpack": "^5.92.1",
|
||||
"webpack": "^5.91.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^5.0.4"
|
||||
},
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -70,7 +70,7 @@ const MessageChannelProvider: FCWithChildren = ({ children }) => {
|
|||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
React.useEffect(() => {
|
||||
const wss = new WebSocket(`${location.protocol == 'https:' ? 'wss' : 'ws'}://${process.env.NODE_ENV === 'development' ? 'localhost:3000' : window.location.host}/public`);
|
||||
const wss = new WebSocket(`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(`${location.protocol == 'https:' ? 'wss' : 'ws'}://${process.env.NODE_ENV === 'development' ? 'localhost:3000' : window.location.host}/private?${search}`, );
|
||||
const wws = new WebSocket(`ws://${process.env.NODE_ENV === 'development' ? 'localhost:3000' : window.location.host}/private?${search}`, );
|
||||
wws.addEventListener('open', () => {
|
||||
console.log('[INFO] [WS] Connected');
|
||||
setSocket(wws);
|
||||
|
|
|
|||
|
|
@ -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 ?? request.series.title,
|
||||
seasonTitle: request.series.seasons[item.episodeInformation.seasonNumber-1].title,
|
||||
episode: item.episodeInformation.episodeNumber+'',
|
||||
id: item.id+'',
|
||||
img: item.thumbnailUrl,
|
||||
|
|
|
|||
26
hidive.ts
26
hidive.ts
|
|
@ -127,7 +127,7 @@ export default class Hidive implements ServiceClass {
|
|||
useProxy: true
|
||||
};
|
||||
// get request type
|
||||
const isGet = method == 'GET';
|
||||
const isGet = method == 'GET' ? true : false;
|
||||
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,7 +243,11 @@ export default class Hidive implements ServiceClass {
|
|||
}, 'auth');
|
||||
if(!authReq.ok || !authReq.res){
|
||||
console.error('Token refresh failed, reinitializing session...');
|
||||
return this.initSession();
|
||||
if (!this.initSession()) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const tokens: Record<string, string> = JSON.parse(authReq.res.body);
|
||||
for (const token in tokens) {
|
||||
|
|
@ -365,15 +369,12 @@ 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} -
|
||||
if (!datePattern.test(episode.title) && episode.duration !== 10) {
|
||||
episodes.push(episode);
|
||||
}
|
||||
episodes.push(episode);
|
||||
console.info(` [E.${episode.id}] ${episode.title}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -396,15 +397,12 @@ 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} -
|
||||
if (!datePattern.test(episode.title) && episode.duration !== 10) {
|
||||
episodes.push(episode);
|
||||
}
|
||||
episodes.push(episode);
|
||||
console.info(` [E.${episode.id}] ${episode.title}`);
|
||||
}
|
||||
const series: NewHidiveSeriesExtra = {...season.value.series, season: season.value};
|
||||
|
|
@ -431,7 +429,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 ?? seriesTitle;
|
||||
const seasonTitle = getShowData.series.seasons[showData[i].episodeInformation.seasonNumber-1]?.title;
|
||||
let nameLong = showData[i].title;
|
||||
if (nameLong.match(/OVA/i)) {
|
||||
nameLong = 'ova' + (('0' + ovaSeq).slice(-2)); ovaSeq++;
|
||||
|
|
@ -568,7 +566,7 @@ export default class Hidive implements ServiceClass {
|
|||
}
|
||||
const episodeData = JSON.parse(episodeDataReq.res.body) as NewHidiveEpisode;
|
||||
|
||||
if (episodeData.title.includes(' - ') && episodeData.episodeInformation) {
|
||||
if (episodeData.title.includes(' - ')) {
|
||||
episodeData.episodeInformation.episodeNumber = parseFloat(episodeData.title.split(' - ')[0].replace('E', ''));
|
||||
episodeData.title = episodeData.title.split(' - ')[1];
|
||||
}
|
||||
|
|
@ -1079,4 +1077,4 @@ export default class Hidive implements ServiceClass {
|
|||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
9
index.ts
9
index.ts
|
|
@ -3,7 +3,7 @@ import { ServiceClass } from './@types/serviceClassInterface';
|
|||
import { appArgv, overrideArguments } from './modules/module.app-args';
|
||||
import * as yamlCfg from './modules/module.cfg-loader';
|
||||
import { makeCommand, addToArchive } from './modules/module.downloadArchive';
|
||||
|
||||
import buildCLIHandler from './modules/downloadProgress';
|
||||
import update from './modules/module.updater';
|
||||
|
||||
(async () => {
|
||||
|
|
@ -70,13 +70,14 @@ import update from './modules/module.updater';
|
|||
case 'adn':
|
||||
service = new (await import('./adn')).default;
|
||||
break;
|
||||
default:
|
||||
default:
|
||||
service = new (await import(`./${argv.service}`)).default;
|
||||
break;
|
||||
}
|
||||
await service.cli();
|
||||
}
|
||||
} else {
|
||||
buildCLIHandler();
|
||||
let service: ServiceClass;
|
||||
switch(argv.service) {
|
||||
case 'crunchy':
|
||||
|
|
@ -91,10 +92,10 @@ import update from './modules/module.updater';
|
|||
case 'adn':
|
||||
service = new (await import('./adn')).default;
|
||||
break;
|
||||
default:
|
||||
default:
|
||||
service = new (await import(`./${argv.service}`)).default;
|
||||
break;
|
||||
}
|
||||
await service.cli();
|
||||
}
|
||||
})();
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -5,11 +5,9 @@ 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 = 'node20-';
|
||||
const nodeVer = 'node18-';
|
||||
|
||||
type BuildTypes = `${'windows'|'macos'|'linux'|'linuxstatic'|'alpine'}-${'x64'|'arm64'}`|'linuxstatic-armv7'
|
||||
|
||||
|
|
@ -45,32 +43,8 @@ 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 = [
|
||||
`${buildsDir}/index.cjs`,
|
||||
gui ? 'gui.js' : 'index.js',
|
||||
'--target', nodeVer + buildType,
|
||||
'--output', `${buildDir}/${pkg.short_name}`,
|
||||
'--compress', 'GZip'
|
||||
|
|
|
|||
63
modules/downloadProgress.ts
Normal file
63
modules/downloadProgress.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import path from 'path';
|
||||
import type { HLSCallback } from './hls-download';
|
||||
import { console } from './log';
|
||||
import cliProgress, { SingleBar } from 'cli-progress';
|
||||
import shlp from 'sei-helper';
|
||||
import HLSEvents from './hlsEventEmitter';
|
||||
import { levels } from 'log4js';
|
||||
|
||||
export default function buildCLIHandler() {
|
||||
const mb = new cliProgress.MultiBar({
|
||||
clearOnComplete: true,
|
||||
stream: process.stdout,
|
||||
format: '{filename} [{bar}] {percentage}% | {speed} | {value}/{total} | {time}',
|
||||
hideCursor: true
|
||||
});
|
||||
const bars: Record<string, {
|
||||
bar: SingleBar,
|
||||
textPos: number,
|
||||
filename: string
|
||||
}> = {};
|
||||
|
||||
HLSEvents.on('end', ({ identifier }) => {
|
||||
bars[identifier]?.bar.stop();
|
||||
delete bars[identifier];
|
||||
});
|
||||
HLSEvents.on('message', ({ identifier, severity, msg }) => {
|
||||
if (severity.isGreaterThanOrEqualTo(levels.WARN))
|
||||
console.log(severity, `${identifier.split(path.sep).pop() || ''}: ${msg}`);
|
||||
mb.remove(bars[identifier]?.bar);
|
||||
});
|
||||
HLSEvents.on('progress', ({ identifier, total, cur, downloadSpeed, time }) => {
|
||||
const filename = identifier.split(path.sep).pop() || '';
|
||||
if (!Object.prototype.hasOwnProperty.call(bars, identifier)) {
|
||||
bars[identifier] = {
|
||||
bar: mb.create(total, cur, {
|
||||
filename: filename.slice(0, 30),
|
||||
speed: `${(downloadSpeed / 1000000).toPrecision(2)}Mb/s`,
|
||||
time: `${shlp.formatTime(parseInt((time / 1000).toFixed(0)))}`
|
||||
}),
|
||||
textPos: 0,
|
||||
filename
|
||||
};
|
||||
}
|
||||
bars[identifier].bar.update(cur, {
|
||||
speed: `${(downloadSpeed / 1000000).toPrecision(2)}Mb/s`,
|
||||
time: `${shlp.formatTime(parseInt((time / 1000).toFixed(0)))}`,
|
||||
});
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
for (const item of Object.values(bars)) {
|
||||
if (item.filename.length < 30)
|
||||
continue;
|
||||
if (item.textPos === item.filename.length)
|
||||
item.textPos = 0;
|
||||
item.bar.update({
|
||||
filename: `${item.filename} ${item.filename}`.slice(item.textPos, item.textPos + 30)
|
||||
});
|
||||
item.textPos += 1;
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
|
|
@ -7,15 +7,17 @@ import url from 'url';
|
|||
import shlp from 'sei-helper';
|
||||
import got, { Response } from 'got';
|
||||
|
||||
import { console } from './log';
|
||||
import { ProgressData } from '../@types/messageHandler';
|
||||
import HLSEvents from './hlsEventEmitter';
|
||||
import { levels } from 'log4js';
|
||||
const console = undefined;
|
||||
|
||||
// The following function should fix an issue with downloading. For more information see https://github.com/sindresorhus/got/issues/1489
|
||||
const fixMiddleWare = (res: Response) => {
|
||||
const isResponseOk = (response: Response) => {
|
||||
const {statusCode} = response;
|
||||
const limitStatusCode = response.request.options.followRedirect ? 299 : 399;
|
||||
|
||||
|
||||
return (statusCode >= 200 && statusCode <= limitStatusCode) || statusCode === 304;
|
||||
};
|
||||
if (isResponseOk(res)) {
|
||||
|
|
@ -47,6 +49,7 @@ type Key = {
|
|||
|
||||
export type HLSOptions = {
|
||||
m3u8json: M3U8Json,
|
||||
identifier: string,
|
||||
output?: string,
|
||||
threads?: number,
|
||||
retries?: number,
|
||||
|
|
@ -56,10 +59,10 @@ export type HLSOptions = {
|
|||
timeout?: number,
|
||||
fsRetryTime?: number,
|
||||
override?: 'Y'|'y'|'N'|'n'|'C'|'c'
|
||||
callback?: HLSCallback
|
||||
}
|
||||
|
||||
type Data = {
|
||||
identifier: string,
|
||||
parts: {
|
||||
first: number,
|
||||
total: number,
|
||||
|
|
@ -80,7 +83,6 @@ type Data = {
|
|||
isResume: boolean,
|
||||
bytesDownloaded: number,
|
||||
waitTime: number,
|
||||
callback?: HLSCallback,
|
||||
override?: string,
|
||||
dateStart: number
|
||||
}
|
||||
|
|
@ -92,8 +94,8 @@ class hlsDownload {
|
|||
// check playlist
|
||||
if(
|
||||
!options
|
||||
|| !options.m3u8json
|
||||
|| !options.m3u8json.segments
|
||||
|| !options.m3u8json
|
||||
|| !options.m3u8json.segments
|
||||
|| options.m3u8json.segments.length === 0
|
||||
){
|
||||
throw new Error('Playlist is empty!');
|
||||
|
|
@ -118,7 +120,7 @@ class hlsDownload {
|
|||
isResume: options.offset ? options.offset > 0 : false,
|
||||
bytesDownloaded: 0,
|
||||
waitTime: options.fsRetryTime ?? 1000 * 5,
|
||||
callback: options.callback,
|
||||
identifier: options.identifier,
|
||||
override: options.override,
|
||||
dateStart: 0
|
||||
};
|
||||
|
|
@ -129,28 +131,23 @@ class hlsDownload {
|
|||
// try load resume file
|
||||
if(fs.existsSync(fn) && fs.existsSync(`${fn}.resume`) && this.data.offset < 1){
|
||||
try{
|
||||
console.info('Resume data found! Trying to resume...');
|
||||
HLSEvents.emit('message', { identifier: this.data.identifier, msg: 'Resume data found! Trying to resume...', severity: levels.INFO });
|
||||
const resumeData = JSON.parse(fs.readFileSync(`${fn}.resume`, 'utf-8'));
|
||||
if(
|
||||
resumeData.total == this.data.m3u8json.segments.length
|
||||
&& resumeData.completed != resumeData.total
|
||||
&& !isNaN(resumeData.completed)
|
||||
){
|
||||
console.info('Resume data is ok!');
|
||||
HLSEvents.emit('message', { identifier: this.data.identifier, msg: 'Resume data is ok!', severity: levels.INFO });
|
||||
this.data.offset = resumeData.completed;
|
||||
this.data.isResume = true;
|
||||
}
|
||||
else{
|
||||
console.warn(' Resume data is wrong!');
|
||||
console.warn({
|
||||
resume: { total: resumeData.total, dled: resumeData.completed },
|
||||
current: { total: this.data.m3u8json.segments.length },
|
||||
});
|
||||
HLSEvents.emit('message', { identifier: this.data.identifier, msg: 'Resume data is wrong!', severity: levels.WARN });
|
||||
}
|
||||
}
|
||||
catch(e){
|
||||
console.error('Resume failed, downloading will be not resumed!');
|
||||
console.error(e);
|
||||
HLSEvents.emit('message', { identifier: this.data.identifier, msg: `Resume failed, downloading will be not resumed!\n${e}`, severity: levels.ERROR });
|
||||
}
|
||||
}
|
||||
// ask before rewrite file
|
||||
|
|
@ -158,54 +155,55 @@ class hlsDownload {
|
|||
let rwts = this.data.override ?? await shlp.question(`[Q] File «${fn}» already exists! Rewrite? ([y]es/[N]o/[c]ontinue)`);
|
||||
rwts = rwts || 'N';
|
||||
if (['Y', 'y'].includes(rwts[0])) {
|
||||
console.info(`Deleting «${fn}»...`);
|
||||
fs.unlinkSync(fn);
|
||||
}
|
||||
else if (['C', 'c'].includes(rwts[0])) {
|
||||
HLSEvents.emit('end', { identifier: this.data.identifier });
|
||||
return { ok: true, parts: this.data.parts };
|
||||
}
|
||||
else {
|
||||
HLSEvents.emit('end', { identifier: this.data.identifier });
|
||||
return { ok: false, parts: this.data.parts };
|
||||
}
|
||||
}
|
||||
// show output filename
|
||||
if (fs.existsSync(fn) && this.data.isResume) {
|
||||
console.info(`Adding content to «${fn}»...`);
|
||||
HLSEvents.emit('message', { identifier: this.data.identifier, msg: `Adding content to «${fn}»...`, severity: levels.INFO });
|
||||
}
|
||||
else{
|
||||
console.info(`Saving stream to «${fn}»...`);
|
||||
HLSEvents.emit('message', { identifier: this.data.identifier, msg: `Saving stream to «${fn}»...`, severity: levels.INFO });
|
||||
}
|
||||
// start time
|
||||
this.data.dateStart = Date.now();
|
||||
let segments = this.data.m3u8json.segments;
|
||||
// download init part
|
||||
if (segments[0].map && this.data.offset === 0 && !this.data.skipInit) {
|
||||
console.info('Download and save init part...');
|
||||
HLSEvents.emit('message', { identifier: this.data.identifier, msg: 'Download and save init part...', severity: levels.INFO });
|
||||
const initSeg = segments[0].map as Segment;
|
||||
if(segments[0].key){
|
||||
initSeg.key = segments[0].key as Key;
|
||||
}
|
||||
try{
|
||||
const initDl = await this.downloadPart(initSeg, 0, 0);
|
||||
const initDl = await this.downloadPart(initSeg, 0, 0, this.data.identifier);
|
||||
fs.writeFileSync(fn, initDl.dec, { flag: 'a' });
|
||||
fs.writeFileSync(`${fn}.resume`, JSON.stringify({
|
||||
completed: 0,
|
||||
total: this.data.m3u8json.segments.length
|
||||
}));
|
||||
console.info('Init part downloaded.');
|
||||
}
|
||||
catch(e: any){
|
||||
console.error(`Part init download error:\n\t${e.message}`);
|
||||
HLSEvents.emit('message', { identifier: this.data.identifier, msg: `Part init download error:\n\t${e.message}`, severity: levels.ERROR });
|
||||
HLSEvents.emit('end', { identifier: this.data.identifier });
|
||||
return { ok: false, parts: this.data.parts };
|
||||
}
|
||||
}
|
||||
else if(segments[0].map && this.data.offset === 0 && this.data.skipInit){
|
||||
console.warn('Skipping init part can lead to broken video!');
|
||||
HLSEvents.emit('message', { identifier: this.data.identifier, msg: 'Skipping init part can lead to broken video!', severity: levels.WARN });
|
||||
}
|
||||
// resuming ...
|
||||
if(this.data.offset > 0){
|
||||
segments = segments.slice(this.data.offset);
|
||||
console.info(`Resuming download from part ${this.data.offset+1}...`);
|
||||
HLSEvents.emit('message', { identifier: this.data.identifier, msg: `Resuming download from part ${this.data.offset+1}...`, severity: levels.INFO });
|
||||
this.data.parts.completed = this.data.offset;
|
||||
}
|
||||
// dl process
|
||||
|
|
@ -221,18 +219,19 @@ class hlsDownload {
|
|||
const curp = segments[px];
|
||||
const key = curp.key as Key;
|
||||
if(key && !krq.has(key.uri) && !this.data.keys[key.uri as string]){
|
||||
krq.set(key.uri, this.downloadKey(key, px, this.data.offset));
|
||||
krq.set(key.uri, this.downloadKey(key, px, this.data.offset, this.data.identifier));
|
||||
}
|
||||
}
|
||||
try {
|
||||
await Promise.all(krq.values());
|
||||
} catch (er: any) {
|
||||
console.error(`Key ${er.p + 1} download error:\n\t${er.message}`);
|
||||
HLSEvents.emit('message', { identifier: this.data.identifier, msg: `Key ${er.p + 1} download error:\n\t${er.message}`, severity: levels.ERROR });
|
||||
HLSEvents.emit('end', { identifier: this.data.identifier });
|
||||
return { ok: false, parts: this.data.parts };
|
||||
}
|
||||
for (let px = offset; px < dlOffset && px < segments.length; px++){
|
||||
const curp = segments[px] as Segment;
|
||||
prq.set(px, this.downloadPart(curp, px, this.data.offset));
|
||||
prq.set(px, this.downloadPart(curp, px, this.data.offset, this.data.identifier));
|
||||
}
|
||||
for (let i = prq.size; i--;) {
|
||||
try {
|
||||
|
|
@ -241,15 +240,15 @@ class hlsDownload {
|
|||
res[r.p - offset] = r.dec;
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error('Part %s download error:\n\t%s',
|
||||
error.p + 1 + this.data.offset, error.message);
|
||||
HLSEvents.emit('message', { identifier: this.data.identifier, msg: `Part ${error.p + 1 + this.data.offset} download error:\n\t${error.message}`, severity: levels.ERROR });
|
||||
prq.delete(error.p);
|
||||
errcnt++;
|
||||
}
|
||||
}
|
||||
// catch error
|
||||
if (errcnt > 0) {
|
||||
console.error(`${errcnt} parts not downloaded`);
|
||||
HLSEvents.emit('message', { identifier: this.data.identifier, msg:`${errcnt} parts not downloaded`, severity: levels.ERROR });
|
||||
HLSEvents.emit('end', { identifier: this.data.identifier });
|
||||
return { ok: false, parts: this.data.parts };
|
||||
}
|
||||
// write downloaded
|
||||
|
|
@ -260,15 +259,15 @@ class hlsDownload {
|
|||
fs.writeFileSync(fn, r, { flag: 'a' });
|
||||
break;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
console.error(`Unable to write to file '${fn}' (Attempt ${error+1}/3)`);
|
||||
console.info(`Waiting ${Math.round(this.data.waitTime / 1000)}s before retrying`);
|
||||
HLSEvents.emit('message', { identifier: this.data.identifier, msg: `Unable to write to file '${fn}' (Attempt ${error+1}/3)\n\t${err}`, severity: levels.ERROR });
|
||||
HLSEvents.emit('message', { identifier: this.data.identifier, msg: `Waiting ${Math.round(this.data.waitTime / 1000)}s before retrying`, severity: levels.INFO });
|
||||
await new Promise<void>((resolve) => setTimeout(() => resolve(), this.data.waitTime));
|
||||
}
|
||||
error++;
|
||||
}
|
||||
if (error === 3) {
|
||||
console.error(`Unable to write content to '${fn}'.`);
|
||||
HLSEvents.emit('message', { identifier: this.data.identifier, msg: `Unable to write content to '${fn}'.`, severity: levels.ERROR });
|
||||
HLSEvents.emit('end', { identifier: this.data.identifier });
|
||||
return { ok: false, parts: this.data.parts };
|
||||
}
|
||||
}
|
||||
|
|
@ -284,21 +283,29 @@ class hlsDownload {
|
|||
completed: this.data.parts.completed,
|
||||
total: totalSeg
|
||||
}));
|
||||
console.info(`${downloadedSeg} of ${totalSeg} parts downloaded [${data.percent}%] (${shlp.formatTime(parseInt((data.time / 1000).toFixed(0)))} | ${(data.downloadSpeed / 1000000).toPrecision(2)}Mb/s)`);
|
||||
if (this.data.callback)
|
||||
this.data.callback({ total: this.data.parts.total, cur: this.data.parts.completed, bytes: this.data.bytesDownloaded, percent: data.percent, time: data.time, downloadSpeed: data.downloadSpeed });
|
||||
//console.info(`${downloadedSeg} of ${totalSeg} parts downloaded [${data.percent}%] (${shlp.formatTime(parseInt((data.time / 1000).toFixed(0)))} | ${(data.downloadSpeed / 1000000).toPrecision(2)}Mb/s)`);
|
||||
HLSEvents.emit('progress', {
|
||||
identifier: this.data.identifier,
|
||||
total: this.data.parts.total,
|
||||
cur: this.data.parts.completed,
|
||||
bytes: this.data.bytesDownloaded,
|
||||
percent: data.percent,
|
||||
time: data.time,
|
||||
downloadSpeed: data.downloadSpeed
|
||||
});
|
||||
}
|
||||
// return result
|
||||
fs.unlinkSync(`${fn}.resume`);
|
||||
HLSEvents.emit('end', { identifier: this.data.identifier });
|
||||
return { ok: true, parts: this.data.parts };
|
||||
}
|
||||
async downloadPart(seg: Segment, segIndex: number, segOffset: number){
|
||||
async downloadPart(seg: Segment, segIndex: number, segOffset: number, identifier: string){
|
||||
const sURI = extFn.getURI(seg.uri, this.data.baseurl);
|
||||
let decipher, part, dec;
|
||||
const p = segIndex;
|
||||
try {
|
||||
if (seg.key != undefined) {
|
||||
decipher = await this.getKey(seg.key, p, segOffset);
|
||||
decipher = await this.getKey(seg.key, p, segOffset, identifier);
|
||||
}
|
||||
part = await extFn.getData(p, sURI, {
|
||||
...(seg.byterange ? {
|
||||
|
|
@ -314,10 +321,10 @@ class hlsDownload {
|
|||
}
|
||||
return res;
|
||||
}
|
||||
]);
|
||||
], identifier);
|
||||
if(this.data.checkPartLength && !(part as any).headers['content-length']){
|
||||
this.data.checkPartLength = false;
|
||||
console.warn(`Part ${segIndex+segOffset+1}: can't check parts size!`);
|
||||
HLSEvents.emit('message', { identifier: this.data.identifier, msg: `Part ${segIndex+segOffset+1}: can't check parts size!`, severity: levels.WARN });
|
||||
}
|
||||
if (decipher == undefined) {
|
||||
this.data.bytesDownloaded += (part.body as Buffer).byteLength;
|
||||
|
|
@ -333,7 +340,7 @@ class hlsDownload {
|
|||
}
|
||||
return { dec, p };
|
||||
}
|
||||
async downloadKey(key: Key, segIndex: number, segOffset: number){
|
||||
async downloadKey(key: Key, segIndex: number, segOffset: number, identifier: string){
|
||||
const kURI = extFn.getURI(key.uri, this.data.baseurl);
|
||||
if (!this.data.keys[kURI]) {
|
||||
try {
|
||||
|
|
@ -349,7 +356,7 @@ class hlsDownload {
|
|||
}
|
||||
return res;
|
||||
}
|
||||
]);
|
||||
], identifier);
|
||||
return rkey;
|
||||
}
|
||||
catch (error: any) {
|
||||
|
|
@ -358,12 +365,12 @@ class hlsDownload {
|
|||
}
|
||||
}
|
||||
}
|
||||
async getKey(key: Key, segIndex: number, segOffset: number){
|
||||
async getKey(key: Key, segIndex: number, segOffset: number, identifier: string){
|
||||
const kURI = extFn.getURI(key.uri, this.data.baseurl);
|
||||
const p = segIndex;
|
||||
if (!this.data.keys[kURI]) {
|
||||
try{
|
||||
const rkey = await this.downloadKey(key, segIndex, segOffset);
|
||||
const rkey = await this.downloadKey(key, segIndex, segOffset, identifier);
|
||||
if (!rkey)
|
||||
throw new Error();
|
||||
this.data.keys[kURI] = rkey.body;
|
||||
|
|
@ -382,7 +389,7 @@ class hlsDownload {
|
|||
return crypto.createDecipheriv('aes-128-cbc', this.data.keys[kURI], iv);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const extFn = {
|
||||
getURI: (uri: string, baseurl?: string) => {
|
||||
const httpURI = /^https{0,1}:/.test(uri);
|
||||
|
|
@ -402,7 +409,7 @@ const extFn = {
|
|||
const downloadSpeed = downloadedBytes / (dateElapsed / 1000); //Bytes per second
|
||||
return { percent, time: revParts, downloadSpeed };
|
||||
},
|
||||
getData: (partIndex: number, uri: string, headers: Record<string, string>, segOffset: number, isKey: boolean, timeout: number, retry: number, afterResponse: ((res: Response, retryWithMergedOptions: () => Response) => Response)[]) => {
|
||||
getData: (partIndex: number, uri: string, headers: Record<string, string>, segOffset: number, isKey: boolean, timeout: number, retry: number, afterResponse: ((res: Response, retryWithMergedOptions: () => Response) => Response)[], identifier: string) => {
|
||||
// get file if uri is local
|
||||
if (uri.startsWith('file://')) {
|
||||
return {
|
||||
|
|
@ -437,8 +444,7 @@ const extFn = {
|
|||
if(error){
|
||||
const partType = isKey ? 'Key': 'Part';
|
||||
const partIndx = partIndex + 1 + segOffset;
|
||||
console.warn('%s %s: %d attempt to retrieve data', partType, partIndx, retryCount + 1);
|
||||
console.error(`\t${error.message}`);
|
||||
HLSEvents.emit('message', { identifier: identifier, msg: `${partType} ${partIndx}: ${retryCount + 1} attempt to retrieve data\n\t${error.message}`, severity: levels.WARN });
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -449,5 +455,5 @@ const extFn = {
|
|||
return got(uri, options);
|
||||
}
|
||||
};
|
||||
|
||||
export default hlsDownload;
|
||||
|
||||
export default hlsDownload;
|
||||
|
|
|
|||
31
modules/hlsEventEmitter.ts
Normal file
31
modules/hlsEventEmitter.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import EventEmitter from "events";
|
||||
import { ProgressData } from "../@types/messageHandler";
|
||||
import { Level } from "log4js";
|
||||
|
||||
type BaseEvent = {
|
||||
identifier: string
|
||||
}
|
||||
|
||||
type ProgressEvent = ProgressData & BaseEvent
|
||||
|
||||
type MessageEvent = {
|
||||
msg: string,
|
||||
severity: Level
|
||||
} & BaseEvent
|
||||
|
||||
type HLSEventTypes = {
|
||||
progress: (data: ProgressEvent) => unknown,
|
||||
message: (data: MessageEvent) => unknown,
|
||||
end: (data: BaseEvent) => unknown
|
||||
}
|
||||
|
||||
declare interface HLSEventEmitter {
|
||||
on<T extends keyof HLSEventTypes>(event: T, listener: HLSEventTypes[T]): this;
|
||||
emit<T extends keyof HLSEventTypes>(event: T, data: Parameters<HLSEventTypes[T]>[0]): boolean;
|
||||
}
|
||||
|
||||
class HLSEventEmitter extends EventEmitter {}
|
||||
|
||||
const eventHandler = new HLSEventEmitter();
|
||||
|
||||
export default eventHandler;
|
||||
|
|
@ -2,7 +2,6 @@ import fs from 'fs';
|
|||
import path from 'path';
|
||||
import { workingDir } from './module.cfg-loader';
|
||||
import log4js from 'log4js';
|
||||
|
||||
const logFolder = path.join(workingDir, 'logs');
|
||||
const latest = path.join(logFolder, 'latest.log');
|
||||
|
||||
|
|
@ -16,26 +15,26 @@ const makeLogFolder = () => {
|
|||
};
|
||||
|
||||
const makeLogger = () => {
|
||||
global.console.log =
|
||||
global.console.log =
|
||||
global.console.info =
|
||||
global.console.warn =
|
||||
global.console.error =
|
||||
global.console.warn =
|
||||
global.console.error =
|
||||
global.console.debug = (...data: any[]) => {
|
||||
console.info((data.length >= 1 ? data.shift() : ''), ...data);
|
||||
};
|
||||
makeLogFolder();
|
||||
log4js.configure({
|
||||
appenders: {
|
||||
console: {
|
||||
console: {
|
||||
type: 'console', layout: {
|
||||
type: 'pattern',
|
||||
pattern: process.env.isGUI === 'true' ? '%[%x{info}%m%]' : '%x{info}%m',
|
||||
pattern: process.env.isGUI === 'true' ? '\r%[%x{info}%m%]' : '\r%x{info}%m',
|
||||
tokens: {
|
||||
info: (ev) => {
|
||||
return ev.level.levelStr === 'INFO' ? '' : `[${ev.level.levelStr}] `;
|
||||
return ev.level.levelStr === 'INFO' ? '\r' : `\r[${ev.level.levelStr}] `;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
file: {
|
||||
type: 'file',
|
||||
|
|
@ -66,4 +65,4 @@ const getLogger = () => {
|
|||
return log4js.getLogger();
|
||||
};
|
||||
|
||||
export const console = getLogger();
|
||||
export const console = getLogger();
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ export type APIType = {
|
|||
cms: string
|
||||
beta_browse: string
|
||||
beta_cms: string,
|
||||
drm: string;
|
||||
/**
|
||||
* Web Header
|
||||
*/
|
||||
|
|
@ -41,7 +40,7 @@ export type APIType = {
|
|||
/**
|
||||
* Mobile Header
|
||||
*/
|
||||
crunchyAuthHeaderMob: Record<string, string>,
|
||||
cruncyhAuthHeaderMob: Record<string, string>,
|
||||
/**
|
||||
* Switch Header
|
||||
*/
|
||||
|
|
@ -75,7 +74,7 @@ const api: APIType = {
|
|||
// beta api
|
||||
beta_auth: `${domain.api_beta}/auth/v1/token`,
|
||||
authBasic: 'Basic bm9haWhkZXZtXzZpeWcwYThsMHE6',
|
||||
authBasicMob: 'Basic dXU4aG0wb2g4dHFpOWV0eXl2aGo6SDA2VnVjRnZUaDJ1dEYxM0FBS3lLNE85UTRhX3BlX1o=',
|
||||
authBasicMob: 'Basic bm12anNoZmtueW14eGtnN2ZiaDk6WllJVnJCV1VQYmNYRHRiRDIyVlNMYTZiNFdRb3Mzelg=',
|
||||
authBasicSwitch: 'Basic dC1rZGdwMmg4YzNqdWI4Zm4wZnE6eWZMRGZNZnJZdktYaDRKWFMxTEVJMmNDcXUxdjVXYW4=',
|
||||
beta_profile: `${domain.api_beta}/accounts/v1/me/profile`,
|
||||
beta_cmsToken: `${domain.api_beta}/index/v2`,
|
||||
|
|
@ -83,9 +82,8 @@ 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: {},
|
||||
crunchyAuthHeaderMob: {},
|
||||
cruncyhAuthHeaderMob: {},
|
||||
crunchyAuthHeaderSwitch: {},
|
||||
//hidive API
|
||||
hd_apikey: '508efd7b42d546e19cc24f4d0b414e57e351ca73',
|
||||
|
|
@ -104,12 +102,9 @@ const api: APIType = {
|
|||
api.crunchyAuthHeader = {
|
||||
Authorization: api.authBasic,
|
||||
};
|
||||
|
||||
api.crunchyAuthHeaderMob = {
|
||||
api.cruncyhAuthHeaderMob = {
|
||||
Authorization: api.authBasicMob,
|
||||
'user-agent': 'Crunchyroll/3.60.0 Android/9 okhttp/4.12.0'
|
||||
};
|
||||
|
||||
api.crunchyAuthHeaderSwitch = {
|
||||
Authorization: api.authBasicSwitch,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ 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;
|
||||
|
|
@ -43,8 +42,7 @@ let argvC: {
|
|||
extid: string | undefined;
|
||||
q: number;
|
||||
x: number;
|
||||
kstream: number;
|
||||
cstream: keyof typeof CrunchyPlayStreams | 'none';
|
||||
kstream: number;
|
||||
partsize: number;
|
||||
hslang: string;
|
||||
dlsubs: string[];
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { aoSearchLocales, dubLanguageCodes, languages, searchLocales, subtitleLanguagesFilter } from './module.langsData';
|
||||
import { CrunchyPlayStreams } from '../@types/enums';
|
||||
|
||||
const groups = {
|
||||
'auth': 'Authentication:',
|
||||
|
|
@ -284,20 +283,6 @@ 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] FETCH OPTIONS:');
|
||||
console.debug('[DEBUG] GOT OPTIONS:');
|
||||
console.debug(options);
|
||||
}
|
||||
// try do request
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ 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 PRIVATE KEY-') || fileContents.includes('-BEGIN RSA PRIVATE KEY-')) {
|
||||
if (fileContents.includes('-BEGIN RSA PRIVATE KEY-')) {
|
||||
privateKey = fs.readFileSync(file);
|
||||
}
|
||||
if (fileContents.includes('widevine_cdm_version')) {
|
||||
|
|
|
|||
28
package.json
28
package.json
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "multi-downloader-nx",
|
||||
"short_name": "aniDL",
|
||||
"version": "5.1.5",
|
||||
"version": "5.0.0",
|
||||
"description": "Downloader for Crunchyroll, Hidive, AnimeOnegai, and AnimationDigitalNetwork with CLI and GUI",
|
||||
"keywords": [
|
||||
"download",
|
||||
|
|
@ -41,9 +41,9 @@
|
|||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/xmldom": "^0.1.34",
|
||||
"@yao-pkg/pkg": "^5.12.0",
|
||||
"@yao-pkg/pkg": "^5.11.1",
|
||||
"cli-progress": "^3.12.0",
|
||||
"cors": "^2.8.5",
|
||||
"esbuild": "^0.21.5",
|
||||
"express": "^4.19.2",
|
||||
"ffprobe": "^1.1.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
|
|
@ -56,31 +56,33 @@
|
|||
"m3u8-parsed": "^1.3.0",
|
||||
"mpd-parser": "^1.3.0",
|
||||
"open": "^8.4.2",
|
||||
"protobufjs": "^7.3.2",
|
||||
"protobufjs": "^7.2.6",
|
||||
"sei-helper": "^3.3.0",
|
||||
"ws": "^8.17.1",
|
||||
"yaml": "^2.4.5",
|
||||
"ws": "^8.16.0",
|
||||
"yaml": "^2.4.1",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cli-progress": "^3.11.5",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/ffprobe": "^1.1.8",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/node": "^20.14.6",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@types/yargs": "^17.0.32",
|
||||
"@typescript-eslint/eslint-plugin": "^7.13.1",
|
||||
"@typescript-eslint/parser": "^7.13.1",
|
||||
"@typescript-eslint/eslint-plugin": "^7.5.0",
|
||||
"@typescript-eslint/parser": "^7.5.0",
|
||||
"@yao-pkg/pkg": "^5.11.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-react": "7.34.3",
|
||||
"eslint-plugin-react": "7.34.1",
|
||||
"protoc": "^1.1.3",
|
||||
"removeNPMAbsolutePaths": "^3.0.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"ts-proto": "^1.180.0",
|
||||
"typescript": "5.5.2",
|
||||
"typescript-eslint": "7.13.1"
|
||||
"ts-proto": "^1.171.0",
|
||||
"typescript": "5.4.4",
|
||||
"typescript-eslint": "7.5.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prestart": "pnpm run tsc test",
|
||||
|
|
|
|||
988
pnpm-lock.yaml
988
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
12
src/hooks/useStore.tsx
Normal file
12
src/hooks/useStore.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
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