diff --git a/@types/crunchyTypes.d.ts b/@types/crunchyTypes.d.ts index 4fcc7e0..5839e37 100644 --- a/@types/crunchyTypes.d.ts +++ b/@types/crunchyTypes.d.ts @@ -14,9 +14,11 @@ export type CrunchyDownloadOptions = { partsize: number, callbackMaker?: (data: DownloadInfo) => HLSCallback, timeout: number, + waittime: number, fsRetryTime: number, dlsubs: string[], skipsubs: boolean, + nosubs?: boolean, mp4: boolean, override: string[], videoTitle: string, @@ -40,6 +42,7 @@ export type CurnchyMultiDownload = { export type CrunchyMuxOptions = { output: string, skipSubMux?: boolean + keepAllVideos?: bolean novids?: boolean, mp4: boolean, forceMuxer?: 'ffmpeg'|'mkvmerge', diff --git a/crunchy.ts b/crunchy.ts index 3e54fc5..6d0ac66 100644 --- a/crunchy.ts +++ b/crunchy.ts @@ -53,7 +53,7 @@ export default class Crunchy implements ServiceClass { constructor(private debug = false) { this.cfg = yamlCfg.loadCfg(); this.token = yamlCfg.loadCRToken(); - this.req = new reqModule.Req(domain, false, false, 'cr'); + this.req = new reqModule.Req(domain, debug, false, 'cr'); } public checkToken(): boolean { @@ -144,6 +144,16 @@ export default class Crunchy implements ServiceClass { } } return true; + } else if (argv.extid) { + await this.refreshToken(); + const selected = await this.getObjectById(argv.extid, false, true); + for (const select of selected as Partial[]) { + if (!(await this.downloadEpisode(select as CrunchyEpMeta, {...argv, skipsubs: false }))) { + console.error(`Unable to download selected episode ${select.episodeNumber}`); + return false; + } + } + return true; } else{ console.info('No option selected or invalid value entered. Try --help.'); @@ -228,7 +238,7 @@ export default class Crunchy implements ServiceClass { yamlCfg.saveCRToken(this.token); } - public async getProfile() : Promise { + public async getProfile(silent = false) : Promise { if(!this.token.access_token){ console.error('No access token!'); return false; @@ -245,10 +255,18 @@ export default class Crunchy implements ServiceClass { return false; } const profile = JSON.parse(profileReq.res.body); - console.info('USER: %s (%s)', profile.username, profile.email); + if (!silent) { + console.info('USER: %s (%s)', profile.username, profile.email); + } return true; } + public sleep(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + } + public async refreshToken(ifNeeded = false, silent = false) { if(!this.token.access_token && !this.token.refresh_token || this.token.access_token && !this.token.refresh_token){ await this.doAnonymousAuth(); @@ -281,19 +299,25 @@ export default class Crunchy implements ServiceClass { yamlCfg.saveCRToken(this.token); } if(this.token.refresh_token) { - if (!silent) - await this.getProfile(); + await this.getProfile(silent); } else { console.info('USER: Anonymous'); } - await this.getCMStoken(); + await this.getCMStoken(ifNeeded); } - public async getCMStoken(){ + public async getCMStoken(ifNeeded = false) { if(!this.token.access_token){ console.error('No access token!'); return; } + + if (ifNeeded && this.cmsToken.cms) { + if (!(Date.now() >= new Date(this.cmsToken.cms.expires).getTime())) { + return; + } + } + const cmsTokenReqOpts = { headers: { Authorization: `Bearer ${this.token.access_token}`, @@ -847,22 +871,57 @@ export default class Crunchy implements ServiceClass { downloaded({ service: 'crunchy', type: 's' - }, data.showID, [data.episodeNumber]); + }, data.showID, [data.episodeNumber || data.e]); } return true; } - public async getObjectById(e?: string, earlyReturn?: boolean): Promise[]|undefined> { + public async getObjectById(e?: string, earlyReturn?: boolean, external_id?: boolean): Promise[]|undefined> { if(!this.cmsToken.cms){ console.error('Authentication required!'); - return; + return []; + } + + let convertedObjects; + if (external_id) { + const epFilter = parseSelect(e as string); + const objectIds = []; + for (const ob of epFilter.values) { + const extIdReqOpts = [ + api.beta_cms, + this.cmsToken.cms.bucket, + '/channels/crunchyroll/objects', + '?', + new URLSearchParams({ + 'external_id': ob, + 'Policy': this.cmsToken.cms.policy, + 'Signature': this.cmsToken.cms.signature, + 'Key-Pair-Id': this.cmsToken.cms.key_pair_id, + }), + ].join(''); + + const extIdReq = await this.req.getData(extIdReqOpts); + if (!extIdReq.ok || !extIdReq.res) { + console.error('Objects Request FAILED!'); + if (extIdReq.error && extIdReq.error.res && extIdReq.error.res.body) { + console.info('[INFO] Body:', extIdReq.error.res.body); + } + continue; + } + + const oldObjectInfo = JSON.parse(extIdReq.res.body) as Record; + for (const object of oldObjectInfo.items) { + objectIds.push(object.id); + } + } + convertedObjects = objectIds.join(','); } - const doEpsFilter = parseSelect(e as string); + const doEpsFilter = parseSelect(convertedObjects ?? e as string); if(doEpsFilter.values.length < 1){ console.info('\nObjects not selected!\n'); - return; + return []; } // node index.js --service crunchy -e G6497Z43Y,GRZXCMN1W,G62PEZ2E6,G25FVGDEK,GZ7UVPVX5 @@ -885,7 +944,7 @@ export default class Crunchy implements ServiceClass { objectInfo.error = true; return objectInfo; } - return; + return []; } const objectInfo = JSON.parse(objectReq.res.body) as ObjectInfo; @@ -951,7 +1010,7 @@ export default class Crunchy implements ServiceClass { await this.logObject(item, 2); } console.info(''); - return selectedMedia; + return selectedMedia; } public async downloadMediaList(medias: CrunchyEpMeta, options: CrunchyDownloadOptions) : Promise<{ @@ -1311,6 +1370,11 @@ export default class Crunchy implements ServiceClass { console.warn('Subtitles downloading disabled for hardsubs streams.'); options.skipsubs = true; } + + if (options.nosubs) { + console.info('Subtitles downloading disabled from nosubs flag.'); + options.skipsubs = true; + } if(!options.skipsubs && options.dlsubs.indexOf('none') == -1){ if(pbData.meta.subtitles && Object.values(pbData.meta.subtitles).length > 0){ @@ -1365,6 +1429,7 @@ export default class Crunchy implements ServiceClass { else{ console.info('Subtitles downloading skipped!'); } + await this.sleep(options.waittime); } return { error: dlFailed, @@ -1392,6 +1457,7 @@ export default class Crunchy implements ServiceClass { }; }), simul: false, + keepAllVideos: options.keepAllVideos, fonts: Merger.makeFontsList(this.cfg.dir.fonts, data.filter(a => a.type === 'Subtitle') as sxItem[]), videoAndAudio: data.filter(a => a.type === 'Video').map((a) : MergerInput => { if (a.type === 'Subtitle') @@ -1465,7 +1531,7 @@ export default class Crunchy implements ServiceClass { //Iterate over episode versions for audio languages for (const version of episode.versions) { //Make sure there is only one of the same language - if (!item.langs.find(a => a.cr_locale == version.audio_locale)) { + if (!item.langs.find(a => a?.cr_locale == version.audio_locale)) { //Push to arrays if there is no duplicates of the same language. item.items.push(episode); item.langs.push(langsData.languages.find(a => a.cr_locale == version.audio_locale) as langsData.LanguageItem); @@ -1475,7 +1541,7 @@ export default class Crunchy implements ServiceClass { //Episode didn't have versions, mark it as such to be logged. serieshasversions = false; //Make sure there is only one of the same language - if (!item.langs.find(a => a.cr_locale == episode.audio_locale)) { + if (!item.langs.find(a => a?.cr_locale == episode.audio_locale)) { //Push to arrays if there is no duplicates of the same language. item.items.push(episode); item.langs.push(langsData.languages.find(a => a.cr_locale == episode.audio_locale) as langsData.LanguageItem); @@ -1507,7 +1573,7 @@ export default class Crunchy implements ServiceClass { item.items.find(a => !a.season_title.match(/\(\w+ Dub\)/))?.season_title ?? item.items[0].season_title.replace(/\(\w+ Dub\)/g, '').trimEnd() } - Season ${item.items[0].season_number} - ${item.items[0].title} [${ item.items.map((a, index) => { - return `${a.is_premium_only ? '☆ ' : ''}${item.langs[index].name}`; + return `${a.is_premium_only ? '☆ ' : ''}${item.langs[index]?.name ?? 'Unknown'}`; }).join(', ') }]`); } @@ -1523,7 +1589,7 @@ export default class Crunchy implements ServiceClass { const seconds = Math.floor(value.items[0].duration_ms / 1000); return { e: key.startsWith('E') ? key.slice(1) : key, - lang: value.langs.map(a => a.code), + lang: value.langs.map(a => a?.code), name: value.items[0].title, season: value.items[0].season_number.toString(), seriesTitle: value.items[0].series_title.replace(/\(\w+ Dub\)/g, '').trimEnd(), @@ -1565,7 +1631,7 @@ export default class Crunchy implements ServiceClass { for (const key of Object.keys(eps)) { const itemE = eps[key]; itemE.items.forEach((item, index) => { - if (!dubLang.includes(itemE.langs[index].code)) + if (!dubLang.includes(itemE.langs[index]?.code)) return; item.hide_season_title = true; if(item.season_title == '' && item.series_title != ''){ diff --git a/docs/DOCUMENTATION.md b/docs/DOCUMENTATION.md index 5d5c8f9..8b920d3 100644 --- a/docs/DOCUMENTATION.md +++ b/docs/DOCUMENTATION.md @@ -1,4 +1,4 @@ -# multi-downloader-nx (4.1.0v) +# multi-downloader-nx (4.2.0v) If you find any bugs in this documentation or in the programm itself please report it [over on GitHub](https://github.com/anidl/multi-downloader-nx/issues). @@ -77,7 +77,7 @@ The output is organized in pages. Use this command to output the items for the g #### `--search-locale` | **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry** | --- | --- | --- | --- | --- | --- | --- | ---| -| Crunchyroll | `--search-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`, `ko-KR`] | ``| `search-locale: ` | +| Crunchyroll | `--search-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`, `ja-JP`, `ca-ES`, `pl-PL`, `th-TH`, `ta-IN`, `ms-MY`] | ``| `search-locale: ` | Set the search local that will be used for searching for items. #### `--new` @@ -114,6 +114,13 @@ Used to set the season ID to download from Set the episode(s) to download from any given show. For multiple selection: 1-4 OR 1,2,3,4 For special episodes: S1-4 OR S1,S2,S3,S4 where S is the special letter +#### `--extid` +| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry** +| --- | --- | --- | --- | --- | ---| +| Crunchyroll | `--extid ${selection}` | `string` | `No`| `--externalid` | `NaN` | + +Set the external id to lookup/download. +Allows you to download or view legacy Crunchyroll Ids #### `-q` | **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry** | --- | --- | --- | --- | --- | --- | ---| @@ -157,17 +164,17 @@ Select specific stream #### `--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`, `ko`, `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`, `ja`, `ca-ES`, `pl-PL`, `th-TH`, `ta-IN`, `ms-MY`, `un`] | `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`, `ko`, `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`, `ja`, `ca-ES`, `pl-PL`, `th-TH`, `ta-IN`, `ms-MY`, `un`] | `all`| `dlsubs: ` | Download subtitles by language tag (space-separated) Funi Only: zh -Crunchy Only: en-IN, es-419, es-ES, pt-PT, fr, de, ar, ar, it, ru, tr, hi, zh-CN, ko +Crunchy Only: en-IN, es-419, es-ES, 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 #### `--novids` | **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry** | --- | --- | --- | --- | --- | ---| @@ -189,11 +196,11 @@ 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`, `kor`, `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`, `jpn`, `cat`, `pol`, `tha`, `tam`, `may`, `und`] | `und`| `dubLang: ` | Set the language to download: Funi Only: cmn -Crunchy Only: eng, spa-419, spa-ES, por, fra, deu, ara-ME, ara, ita, rus, tur, hin, zho, kor +Crunchy Only: eng, spa-419, spa-ES, por, fra, deu, ara-ME, ara, ita, rus, tur, hin, zho, chi, kor, cat, pol, tha, tam, may #### `--all` | **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry** | --- | --- | --- | --- | --- | --- | ---| @@ -218,6 +225,12 @@ If selected, all available dubs will get downloaded | All | `--timeout ${timeout}` | `number` | `No`| `NaN` | `15000`| `timeout: ` | Set the timeout of all download reqests. Set in millisecods +#### `--waittime` +| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry** +| --- | --- | --- | --- | --- | --- | ---| +| Crunchyroll, Hidive | `--waittime ${waittime}` | `number` | `No`| `NaN` | `0`| `waittime: ` | + +Set the time the program waits between downloads. Set in millisecods #### `--simul` | **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry** | --- | --- | --- | --- | --- | --- | ---| @@ -268,6 +281,12 @@ If a file already exists, the tool will ask you how to proceed. With this, you c | All | `--mp4 ` | `boolean` | `No`| `NaN` | `false`| `mp4: ` | If selected, the output file will be an mp4 file (not recommended tho) +#### `--keepAllVideos` +| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry** +| --- | --- | --- | --- | --- | --- | ---| +| Crunchyroll, Hidive | `--keepAllVideos ` | `boolean` | `No`| `NaN` | `false`| `keepAllVideos: ` | + +If set to true, it will keep all videos in the merge process, rather than discarding the extra videos. #### `--skipmux` | **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry** | --- | --- | --- | --- | --- | ---| @@ -316,14 +335,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, kor, 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, jpn, cat, pol, tha, tam, may, und #### `--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, kor, 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, jpn, cat, pol, tha, tam, may, und ### Filename Template #### `--fileName` | **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry** diff --git a/gui/server/index.ts b/gui/server/index.ts index 2f29549..2229d78 100644 --- a/gui/server/index.ts +++ b/gui/server/index.ts @@ -6,6 +6,7 @@ import open from 'open'; import path from 'path'; import { PublicWebSocket } from './websocket'; import { console } from '../../modules/log'; +import packageJson from '../../package.json'; process.title = 'AniDL'; @@ -21,6 +22,8 @@ app.use(express.json()); app.use(cors()); app.use(express.static(path.join(workingDir, 'gui', 'server', 'build'), { maxAge: 1000 * 60 * 20 })); +console.info(`\n=== Multi Downloader NX GUI ${packageJson.version} ===\n`); + const server = app.listen(cfg.gui.port, () => { console.info(`GUI server started on port ${cfg.gui.port}`); }); diff --git a/hidive.ts b/hidive.ts index 1e5f7e3..4b0ab6b 100644 --- a/hidive.ts +++ b/hidive.ts @@ -59,7 +59,7 @@ export default class Hidive implements ServiceClass { this.session = yamlCfg.loadHDSession(); this.token = yamlCfg.loadHDToken(); this.client = yamlCfg.loadHDProfile() as {ipAddress: string, xNonce: string, xSignature: string, visitId: string, profile: {userId: number, profileId: number, deviceId : string}}; - this.req = new reqModule.Req(domain, false, false, 'hd'); + this.req = new reqModule.Req(domain, debug, false, 'hd'); } public async doInit() { @@ -494,6 +494,7 @@ export default class Hidive implements ServiceClass { let dlFailed = false; //let dlVideoOnce = false; // Variable to save if best selected video quality was downloaded let subsMargin = 0; + let videoIndex = 0; const chosenFontSize = options.originalFontSize ? fontSize : options.fontSize; for (const videoData of videoUrls) { if(videoData.seriesTitle && videoData.episodeNumber && videoData.episodeTitle){ @@ -616,7 +617,7 @@ export default class Hidive implements ServiceClass { console.info(`Selected quality: ${Object.keys(plSelectedList).find(a => plSelectedList[a] === selPlUrl)} @ ${plSelectedServer}`); console.info('Stream URL:', selPlUrl); // TODO check filename - const outFile = parseFileName(options.fileName + '.' + lang.name, variables, options.numbers, options.override).join(path.sep); + const outFile = parseFileName(options.fileName + '.' + lang.name + '.' + videoIndex, variables, options.numbers, options.override).join(path.sep); fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); console.info(`Output filename: ${outFile}`); const chunkPage = await this.req.getData(selPlUrl); @@ -675,12 +676,19 @@ export default class Hidive implements ServiceClass { fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); console.info('Downloading skipped!'); } + videoIndex++; + await this.sleep(options.waittime); } if(options.dlsubs.indexOf('all') > -1){ options.dlsubs = ['all']; } + if (options.nosubs) { + console.info('Subtitles downloading disabled from nosubs flag.'); + options.skipsubs = true; + } + if(!options.skipsubs && options.dlsubs.indexOf('none') == -1) { if(subUrls.length > 0) { let subIndex = 0; @@ -739,6 +747,7 @@ export default class Hidive implements ServiceClass { onlyVid: [], skipSubMux: options.skipSubMux, inverseTrackOrder: true, + keepAllVideos: options.keepAllVideos, onlyAudio: [], output: `${options.output}.${options.mp4 ? 'mp4' : 'mkv'}`, subtitles: data.filter(a => a.type === 'Subtitle').map((a) : SubtitleInput => { @@ -792,4 +801,10 @@ export default class Hidive implements ServiceClass { if (isMuxed && !options.nocleanup) merger.cleanUp(); } + + public sleep(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + } } \ No newline at end of file diff --git a/index.ts b/index.ts index 1b952fc..e65830f 100644 --- a/index.ts +++ b/index.ts @@ -55,11 +55,11 @@ import update from './modules/module.updater'; if (key.endsWith('crunchy.js') || key.endsWith('funi.js') || key.endsWith('hidive.js')) delete require.cache[key]; }); - const service = new (argv.service === 'funi' ? (await import('./funi')).default : argv.service === 'hidive' ? (await import('./hidive')).default : (await import('./crunchy')).default)() as ServiceClass; + const service = new (argv.service === 'funi' ? (await import('./funi')).default : argv.service === 'hidive' ? (await import('./hidive')).default : (await import('./crunchy')).default)(argv.debug) as ServiceClass; await service.cli(); } } else { - const service = new (argv.service === 'funi' ? (await import('./funi')).default : argv.service === 'hidive' ? (await import('./hidive')).default : (await import('./crunchy')).default)() as ServiceClass; + const service = new (argv.service === 'funi' ? (await import('./funi')).default : argv.service === 'hidive' ? (await import('./hidive')).default : (await import('./crunchy')).default)(argv.debug) as ServiceClass; await service.cli(); } })(); \ No newline at end of file diff --git a/modules/module.app-args.ts b/modules/module.app-args.ts index 0cf7a3e..476c7a1 100644 --- a/modules/module.app-args.ts +++ b/modules/module.app-args.ts @@ -33,6 +33,7 @@ let argvC: { series: string | undefined; s: string | undefined; e: string | undefined; + extid: string | undefined; q: number; x: number; kstream: number; @@ -47,6 +48,7 @@ let argvC: { fontSize: number; allDubs: boolean; timeout: number; + waittime: number; simul: boolean; mp4: boolean; skipmux: boolean | undefined; @@ -64,6 +66,7 @@ let argvC: { dlVideoOnce: boolean; removeBumpers: boolean; originalFontSize: boolean; + keepAllVideos: boolean; }; export type ArgvType = typeof argvC; diff --git a/modules/module.args.ts b/modules/module.args.ts index f13a308..7b7a1b1 100644 --- a/modules/module.args.ts +++ b/modules/module.args.ts @@ -165,6 +165,17 @@ const args: TAppArg[] = [ usage: '${selection}', alias: 'episode' }, + { + name: 'extid', + group: 'dl', + describe: 'Set the external id to lookup/download', + docDescribe: 'Set the external id to lookup/download.' + + '\nAllows you to download or view legacy Crunchyroll Ids ', + service: ['crunchy'], + type: 'string', + usage: '${selection}', + alias: 'externalid' + }, { name: 'q', group: 'dl', @@ -360,6 +371,18 @@ const args: TAppArg[] = [ default: 15 * 1000 } }, + { + name: 'waittime', + group: 'dl', + type: 'number', + describe: 'Set the time the program waits between downloads. Set in millisecods', + docDescribe: true, + service: ['crunchy','hidive'], + usage: '${waittime}', + default: { + default: 0 * 1000 + } + }, { name: 'simul', group: 'dl', @@ -384,6 +407,18 @@ const args: TAppArg[] = [ default: false } }, + { + name: 'keepAllVideos', + group: 'mux', + describe: 'Keeps all videos when merging instead of discarding extras', + docDescribe: 'If set to true, it will keep all videos in the merge process, rather than discarding the extra videos.', + service: ['crunchy','hidive'], + type: 'boolean', + usage: '', + default: { + default: false + } + }, { name: 'skipmux', describe: 'Skip muxing video, audio and subtitles', diff --git a/modules/module.langsData.ts b/modules/module.langsData.ts index 59ab0e5..e289fbe 100644 --- a/modules/module.langsData.ts +++ b/modules/module.langsData.ts @@ -30,8 +30,15 @@ const languages: LanguageItem[] = [ { cr_locale: 'hi-IN', locale: 'hi', code: 'hin', name: 'Hindi' }, { funi_locale: 'zhMN', 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: 'ko-KR', hd_locale: 'Korean', locale: 'ko', code: 'kor', name: 'Korean' }, { cr_locale: 'ja-JP', hd_locale: 'Japanese', funi_locale: 'jaJP', locale: 'ja', code: 'jpn', name: 'Japanese' }, + { cr_locale: 'ca-ES', locale: 'ca-ES', code: 'cat', name: 'Catalan' }, + { cr_locale: 'pl-PL', locale: 'pl-PL', code: 'pol', name: 'Polish' }, + { cr_locale: 'th-TH', locale: 'th-TH', code: 'tha', name: 'Thai' }, + { cr_locale: 'ta-IN', locale: 'ta-IN', code: 'tam', name: 'Tamil (India)' }, + { cr_locale: 'ms-MY', locale: 'ms-MY', code: 'may', name: 'Malay (Malaysia)' }, + { locale: 'un', code: 'und', name: 'Unknown' }, ]; // add en language names diff --git a/modules/module.merger.ts b/modules/module.merger.ts index 9a2f6c5..bd32952 100644 --- a/modules/module.merger.ts +++ b/modules/module.merger.ts @@ -36,6 +36,7 @@ export type MergerOptions = { videoTitle?: string, simul?: boolean, inverseTrackOrder?: boolean, + keepAllVideos?: boolean, fonts?: ParsedFont[], skipSubMux?: boolean, options: { @@ -163,7 +164,7 @@ class Merger { for (const vid of this.options.videoAndAudio) { const audioTrackNum = this.options.inverseTrackOrder ? '0' : '1'; const videoTrackNum = this.options.inverseTrackOrder ? '1' : '0'; - if (!hasVideo) { + if (!hasVideo || this.options.keepAllVideos) { args.push( `--video-tracks ${videoTrackNum}`, `--audio-tracks ${audioTrackNum}` @@ -196,17 +197,16 @@ class Merger { for (const aud of this.options.onlyAudio) { const trackName = aud.lang.name; - const trackNum = this.options.inverseTrackOrder ? '0' : '1'; - args.push('--track-name', `${trackNum}:"${trackName}"`); - args.push(`--language ${trackNum}:${aud.lang.code}`); + args.push('--track-name', `0:"${trackName}"`); + args.push(`--language 0:${aud.lang.code}`); args.push( '--no-video', - `--audio-tracks ${trackNum}` + '--audio-tracks 0' ); if (this.options.defaults.audio.code === aud.lang.code) { - args.push(`--default-track ${trackNum}`); + args.push('--default-track 0'); } else { - args.push(`--default-track ${trackNum}:0`); + args.push('--default-track 0:0'); } args.push(`"${aud.path}"`); } diff --git a/modules/module.parseSelect.ts b/modules/module.parseSelect.ts index 28bdef4..6376097 100644 --- a/modules/module.parseSelect.ts +++ b/modules/module.parseSelect.ts @@ -54,6 +54,9 @@ const parseSelect = (selectString: string, but = false) : { if (part.match(/[0-9A-Z]{9}/)) { select.push(part); return; + } else if (part.match(/[A-Z]{3}\.[0-9]*/)) { + select.push(part); + return; } const match = part.match(/[A-Za-z]+/); if (match && match.length > 0) { diff --git a/package.json b/package.json index eb46b66..8ef213e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "multi-downloader-nx", "short_name": "aniDL", - "version": "4.1.0", + "version": "4.2.0", "description": "Download videos from Funimation, Crunchyroll, or Hidive via cli", "keywords": [ "download",