Compare commits

..

No commits in common. "master" and "guiv3" have entirely different histories.

10 changed files with 98 additions and 69 deletions

View file

@ -27,6 +27,6 @@ jobs:
github-token: ${{ github.token }} github-token: ${{ github.token }}
push: ${{ github.ref == 'refs/heads/master' }} push: ${{ github.ref == 'refs/heads/master' }}
tags: | tags: |
"multidl/multi-downloader-nx:latest" "izuco/multi-downloader-nx:latest"
- name: Image digest - name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }} run: echo ${{ steps.docker_build.outputs.digest }}

View file

@ -61,6 +61,6 @@ jobs:
github-token: ${{ github.token }} github-token: ${{ github.token }}
push: true push: true
tags: | tags: |
"multidl/multi-downloader-nx:${{ github.event.release.tag_name }}" "izuco/multi-downloader-nx:${{ github.event.release.tag_name }}"
- name: Image digest - name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }} run: echo ${{ steps.docker_build.outputs.digest }}

View file

@ -1,8 +1,6 @@
import { Locale } from './playbackData';
export interface CrunchyPlayStream { export interface CrunchyPlayStream {
assetId: string; assetId: string;
audioLocale: Locale; audioLocale: string;
bifs: string; bifs: string;
burnedInLocale: string; burnedInLocale: string;
captions: { [key: string]: Caption }; captions: { [key: string]: Caption };

View file

@ -59,11 +59,11 @@ export interface Meta {
versions: Version[]; versions: Version[];
audio_locale: Locale; audio_locale: Locale;
closed_captions: Subtitles; closed_captions: Subtitles;
captions: Subtitles; captions: Record<unknown>;
} }
export interface Subtitles { export interface Subtitles {
''?: SubtitleInfo; '': SubtitleInfo;
'en-US'?: SubtitleInfo; 'en-US'?: SubtitleInfo;
'es-LA'?: SubtitleInfo; 'es-LA'?: SubtitleInfo;
'es-419'?: SubtitleInfo; 'es-419'?: SubtitleInfo;

14
adn.ts
View file

@ -204,6 +204,8 @@ export default class AnimationDigitalNetwork implements ServiceClass {
return { isOk: false, reason: new Error('Authentication failed') }; return { isOk: false, reason: new Error('Authentication failed') };
} }
this.token = await authReq.res.json(); 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); yamlCfg.saveADNToken(this.token);
console.info('Authentication Success'); console.info('Authentication Success');
return { isOk: true, value: undefined }; return { isOk: true, value: undefined };
@ -214,16 +216,19 @@ export default class AnimationDigitalNetwork implements ServiceClass {
method: 'POST', method: 'POST',
headers: { headers: {
Authorization: `Bearer ${this.token.accessToken}`, Authorization: `Bearer ${this.token.accessToken}`,
'X-Access-Token': this.token.accessToken, 'Cookie': `adnrt=${this.token.refreshToken}`,
'content-type': 'application/json' 'X-Access-Token': this.token.accessToken
}, },
body: JSON.stringify({refreshToken: this.token.refreshToken}) body: '{}'
}); });
if(!authReq.ok || !authReq.res){ if(!authReq.ok || !authReq.res){
console.error('Token refresh failed!'); console.error('Token refresh failed!');
return { isOk: false, reason: new Error('Token refresh failed') }; return { isOk: false, reason: new Error('Token refresh failed') };
} }
this.token = await authReq.res.json(); 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); yamlCfg.saveADNToken(this.token);
return { isOk: true, value: undefined }; return { isOk: true, value: undefined };
} }
@ -458,8 +463,7 @@ export default class AnimationDigitalNetwork implements ServiceClass {
const configReq = await this.req.getData(`https://gw.api.animationdigitalnetwork.fr/player/video/${data.id}/configuration`, { const configReq = await this.req.getData(`https://gw.api.animationdigitalnetwork.fr/player/video/${data.id}/configuration`, {
headers: { headers: {
Authorization: `Bearer ${this.token.accessToken}`, Authorization: `Bearer ${this.token.accessToken}`
'X-Target-Distribution': this.locale
} }
}); });
if(!configReq.ok || !configReq.res){ if(!configReq.ok || !configReq.res){

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 { CrunchyDownloadOptions, CrunchyEpMeta, CrunchyMuxOptions, CrunchyMultiDownload, DownloadedMedia, ParseItem, SeriesSearch, SeriesSearchItem } from './@types/crunchyTypes';
import { ObjectInfo } from './@types/objectInfo'; import { ObjectInfo } from './@types/objectInfo';
import parseFileName, { Variable } from './modules/module.filename'; 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 { downloaded } from './modules/module.downloadArchive';
import parseSelect from './modules/module.parseSelect'; import parseSelect from './modules/module.parseSelect';
import { AvailableFilenameVars, getDefault } from './modules/module.args'; import { AvailableFilenameVars, getDefault } from './modules/module.args';
@ -45,7 +45,6 @@ import { CrunchyChapters, CrunchyChapter, CrunchyOldChapter } from './@types/cru
import vtt2ass from './modules/module.vtt2ass'; import vtt2ass from './modules/module.vtt2ass';
import { CrunchyPlayStream } from './@types/crunchyPlayStreams'; import { CrunchyPlayStream } from './@types/crunchyPlayStreams';
import { CrunchyPlayStreams } from './@types/enums'; import { CrunchyPlayStreams } from './@types/enums';
import { randomUUID } from 'node:crypto';
export type sxItem = { export type sxItem = {
language: langsData.LanguageItem, language: langsData.LanguageItem,
@ -226,18 +225,15 @@ export default class Crunchy implements ServiceClass {
} }
public async doAuth(data: AuthData): Promise<AuthResponse> { public async doAuth(data: AuthData): Promise<AuthResponse> {
const uuid = randomUUID();
const authData = new URLSearchParams({ const authData = new URLSearchParams({
'username': data.username, 'username': data.username,
'password': data.password, 'password': data.password,
'grant_type': 'password', 'grant_type': 'password',
'scope': 'offline_access', 'scope': 'offline_access'
'device_id': uuid,
'device_type': 'Chrome on Windows'
}).toString(); }).toString();
const authReqOpts: reqModule.Params = { const authReqOpts: reqModule.Params = {
method: 'POST', method: 'POST',
headers: api.crunchyAuthHeaderMob, headers: api.crunchyAuthHeader,
body: authData body: authData
}; };
const authReq = await this.req.getData(api.beta_auth, authReqOpts); 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') }; return { isOk: false, reason: new Error('Authentication failed') };
} }
this.token = await authReq.res.json(); this.token = await authReq.res.json();
this.token.device_id = uuid;
this.token.expires = new Date(Date.now() + this.token.expires_in); this.token.expires = new Date(Date.now() + this.token.expires_in);
yamlCfg.saveCRToken(this.token); yamlCfg.saveCRToken(this.token);
await this.getProfile(); await this.getProfile();
@ -255,16 +250,13 @@ export default class Crunchy implements ServiceClass {
} }
public async doAnonymousAuth(){ public async doAnonymousAuth(){
const uuid = randomUUID();
const authData = new URLSearchParams({ const authData = new URLSearchParams({
'grant_type': 'client_id', 'grant_type': 'client_id',
'scope': 'offline_access', 'scope': 'offline_access',
'device_id': uuid,
'device_type': 'Chrome on Windows'
}).toString(); }).toString();
const authReqOpts: reqModule.Params = { const authReqOpts: reqModule.Params = {
method: 'POST', method: 'POST',
headers: api.crunchyAuthHeaderMob, headers: api.crunchyAuthHeader,
body: authData body: authData
}; };
const authReq = await this.req.getData(api.beta_auth, authReqOpts); const authReq = await this.req.getData(api.beta_auth, authReqOpts);
@ -273,7 +265,6 @@ export default class Crunchy implements ServiceClass {
return; return;
} }
this.token = await authReq.res.json(); this.token = await authReq.res.json();
this.token.device_id = uuid;
this.token.expires = new Date(Date.now() + this.token.expires_in); this.token.expires = new Date(Date.now() + this.token.expires_in);
yamlCfg.saveCRToken(this.token); yamlCfg.saveCRToken(this.token);
} }
@ -308,18 +299,13 @@ export default class Crunchy implements ServiceClass {
} }
public async loginWithToken(refreshToken: string) { public async loginWithToken(refreshToken: string) {
const uuid = randomUUID();
const authData = new URLSearchParams({ const authData = new URLSearchParams({
'refresh_token': this.token.refresh_token, 'grant_type': 'etp_rt_cookie',
'grant_type': 'refresh_token', 'scope': 'offline_access'
//'grant_type': 'etp_rt_cookie',
'scope': 'offline_access',
'device_id': uuid,
'device_type': 'Chrome on Windows'
}).toString(); }).toString();
const authReqOpts: reqModule.Params = { const authReqOpts: reqModule.Params = {
method: 'POST', method: 'POST',
headers: {...api.crunchyAuthHeaderMob, Cookie: `etp_rt=${refreshToken}`}, headers: {...api.crunchyAuthHeader, Cookie: `etp_rt=${refreshToken}`},
body: authData body: authData
}; };
const authReq = await this.req.getData(api.beta_auth, authReqOpts); const authReq = await this.req.getData(api.beta_auth, authReqOpts);
@ -331,7 +317,6 @@ export default class Crunchy implements ServiceClass {
return; return;
} }
this.token = await authReq.res.json(); this.token = await authReq.res.json();
this.token.device_id = uuid;
this.token.expires = new Date(Date.now() + this.token.expires_in); this.token.expires = new Date(Date.now() + this.token.expires_in);
yamlCfg.saveCRToken(this.token); yamlCfg.saveCRToken(this.token);
await this.getProfile(false); await this.getProfile(false);
@ -350,18 +335,13 @@ export default class Crunchy implements ServiceClass {
} else { } else {
//console.info('[WARN] The token has expired compleatly. I will try to refresh the token anyway, but you might have to reauth.'); //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({ const authData = new URLSearchParams({
'refresh_token': this.token.refresh_token, 'grant_type': 'etp_rt_cookie',
'grant_type': 'refresh_token', 'scope': 'offline_access'
//'grant_type': 'etp_rt_cookie',
'scope': 'offline_access',
'device_id': uuid,
'device_type': 'Chrome on Windows'
}).toString(); }).toString();
const authReqOpts: reqModule.Params = { const authReqOpts: reqModule.Params = {
method: 'POST', method: 'POST',
headers: {...api.crunchyAuthHeaderMob, Cookie: `etp_rt=${this.token.refresh_token}`}, headers: {...api.crunchyAuthHeader, Cookie: `etp_rt=${this.token.refresh_token}`},
body: authData body: authData
}; };
const authReq = await this.req.getData(api.beta_auth, authReqOpts); const authReq = await this.req.getData(api.beta_auth, authReqOpts);
@ -373,7 +353,6 @@ export default class Crunchy implements ServiceClass {
return; return;
} }
this.token = await authReq.res.json(); this.token = await authReq.res.json();
this.token.device_id = uuid;
this.token.expires = new Date(Date.now() + this.token.expires_in); this.token.expires = new Date(Date.now() + this.token.expires_in);
yamlCfg.saveCRToken(this.token); yamlCfg.saveCRToken(this.token);
} }
@ -1371,7 +1350,73 @@ 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 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;
}
}
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;
pbData.data = [{}];
}
let playStream: CrunchyPlayStream | null = null; let playStream: CrunchyPlayStream | null = null;
if (options.cstream !== 'none') { if (options.cstream !== 'none') {
@ -1392,15 +1437,6 @@ export default class Crunchy implements ServiceClass {
url: playStream.url, url: playStream.url,
hardsub_locale: '' 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`] = { pbData.data[0][`adaptive_${options.cstream}_${playStream.url.includes('m3u8') ? 'hls' : 'dash'}_drm`] = {
...derivedPlaystreams ...derivedPlaystreams
}; };
@ -1527,14 +1563,6 @@ export default class Crunchy implements ServiceClass {
let tsFile = undefined; let tsFile = undefined;
// Delete the stream if it's not needed
if (options.novids && options.noaudio) {
if (playStream) {
await this.refreshToken(true, true);
await this.req.getData(`https://cr-play-service.prd.crunchyrollsvc.com/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${playStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders});
}
}
if(!dlFailed && curStream !== undefined && !(options.novids && options.noaudio)){ if(!dlFailed && curStream !== undefined && !(options.novids && options.noaudio)){
const streamPlaylistsReq = await this.req.getData(curStream.url, AuthHeaders); const streamPlaylistsReq = await this.req.getData(curStream.url, AuthHeaders);
if(!streamPlaylistsReq.ok || !streamPlaylistsReq.res){ if(!streamPlaylistsReq.ok || !streamPlaylistsReq.res){
@ -2041,7 +2069,7 @@ export default class Crunchy implements ServiceClass {
const subsData = Object.values(pbData.meta.subtitles); const subsData = Object.values(pbData.meta.subtitles);
const capsData = Object.values(pbData.meta.closed_captions); const capsData = Object.values(pbData.meta.closed_captions);
const subsDataMapped = subsData.map((s) => { const subsDataMapped = subsData.map((s) => {
const subLang = langsData.fixAndFindCrLC(s.language); const subLang = langsData.fixAndFindCrLC(s.locale);
return { return {
...s, ...s,
isCC: false, isCC: false,
@ -2050,7 +2078,7 @@ export default class Crunchy implements ServiceClass {
}; };
}).concat( }).concat(
capsData.map((s) => { capsData.map((s) => {
const subLang = langsData.fixAndFindCrLC(s.language); const subLang = langsData.fixAndFindCrLC(s.locale);
return { return {
...s, ...s,
isCC: true, isCC: true,

View file

@ -1,4 +1,4 @@
# multi-downloader-nx (5.1.5v) # multi-downloader-nx (5.1.0b5v)
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). 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).

View file

@ -75,7 +75,7 @@ const api: APIType = {
// beta api // beta api
beta_auth: `${domain.api_beta}/auth/v1/token`, beta_auth: `${domain.api_beta}/auth/v1/token`,
authBasic: 'Basic bm9haWhkZXZtXzZpeWcwYThsMHE6', authBasic: 'Basic bm9haWhkZXZtXzZpeWcwYThsMHE6',
authBasicMob: 'Basic dXU4aG0wb2g4dHFpOWV0eXl2aGo6SDA2VnVjRnZUaDJ1dEYxM0FBS3lLNE85UTRhX3BlX1o=', authBasicMob: 'Basic d2piMV90YThta3Y3X2t4aHF6djc6MnlSWlg0Y0psX28yMzRqa2FNaXRTbXNLUVlGaUpQXzU=',
authBasicSwitch: 'Basic dC1rZGdwMmg4YzNqdWI4Zm4wZnE6eWZMRGZNZnJZdktYaDRKWFMxTEVJMmNDcXUxdjVXYW4=', authBasicSwitch: 'Basic dC1rZGdwMmg4YzNqdWI4Zm4wZnE6eWZMRGZNZnJZdktYaDRKWFMxTEVJMmNDcXUxdjVXYW4=',
beta_profile: `${domain.api_beta}/accounts/v1/me/profile`, beta_profile: `${domain.api_beta}/accounts/v1/me/profile`,
beta_cmsToken: `${domain.api_beta}/index/v2`, beta_cmsToken: `${domain.api_beta}/index/v2`,
@ -107,7 +107,6 @@ api.crunchyAuthHeader = {
api.crunchyAuthHeaderMob = { api.crunchyAuthHeaderMob = {
Authorization: api.authBasicMob, Authorization: api.authBasicMob,
'user-agent': 'Crunchyroll/3.60.0 Android/9 okhttp/4.12.0'
}; };
api.crunchyAuthHeaderSwitch = { api.crunchyAuthHeaderSwitch = {

View file

@ -67,7 +67,7 @@ export class Req {
} }
// debug // debug
if(this.debug){ if(this.debug){
console.debug('[DEBUG] FETCH OPTIONS:'); console.debug('[DEBUG] GOT OPTIONS:');
console.debug(options); console.debug(options);
} }
// try do request // try do request

View file

@ -1,7 +1,7 @@
{ {
"name": "multi-downloader-nx", "name": "multi-downloader-nx",
"short_name": "aniDL", "short_name": "aniDL",
"version": "5.1.5", "version": "5.1.0b5",
"description": "Downloader for Crunchyroll, Hidive, AnimeOnegai, and AnimationDigitalNetwork with CLI and GUI", "description": "Downloader for Crunchyroll, Hidive, AnimeOnegai, and AnimationDigitalNetwork with CLI and GUI",
"keywords": [ "keywords": [
"download", "download",