Compare commits

..

2 commits

Author SHA1 Message Date
AnimeDL
6d1926cca3 Maybe fix line overlap for progress bars? 2024-04-22 07:41:38 -07:00
AnidlSupport
501928b92d Initial support for multi-download 2024-04-22 07:40:51 -07:00
34 changed files with 1754 additions and 1872 deletions

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

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: Subtitles;
captions: Record<unknown>;
}
export interface Subtitles {
''?: SubtitleInfo;
'': SubtitleInfo;
'en-US'?: SubtitleInfo;
'es-LA'?: SubtitleInfo;
'es-419'?: SubtitleInfo;

21
adn.ts
View file

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

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

View file

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

View file

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

View file

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

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 ?? request.series.title,
seasonTitle: request.series.seasons[item.episodeInformation.seasonNumber-1].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';
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);
});
}
}
}

View file

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

View file

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

View 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);
}

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

File diff suppressed because it is too large Load diff

12
src/hooks/useStore.tsx Normal file
View 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;