From 460b4c1d0e12c88459aaff786fbe348c4c3d517a Mon Sep 17 00:00:00 2001 From: AnimeDL Date: Fri, 26 Sep 2025 10:41:01 -0700 Subject: [PATCH 1/4] Convert main files to tabs Still need to do .tsx files and type declaration files --- adn.ts | 1650 ++++---- ao.ts | 1502 ++++---- crunchy.ts | 5652 ++++++++++++++-------------- eslint.config.mjs | 108 +- gui/server/index.ts | 2 +- gui/server/serviceHandler.ts | 226 +- gui/server/services/adn.ts | 236 +- gui/server/services/animeonegai.ts | 260 +- gui/server/services/base.ts | 244 +- gui/server/services/crunchyroll.ts | 214 +- gui/server/services/hidive.ts | 208 +- gui/server/websocket.ts | 176 +- hidive.ts | 1960 +++++----- index.ts | 174 +- modules/build-docs.ts | 76 +- modules/build.ts | 176 +- modules/cdm.ts | 290 +- modules/hls-download.ts | 662 ++-- modules/log.ts | 86 +- modules/module.api-urls.ts | 110 +- modules/module.app-args.ts | 140 +- modules/module.args.ts | 1832 ++++----- modules/module.cfg-loader.ts | 534 +-- modules/module.cookieFile.ts | 30 +- modules/module.downloadArchive.ts | 166 +- modules/module.fetch.ts | 236 +- modules/module.ffmpegChapter.ts | 68 +- modules/module.filename.ts | 124 +- modules/module.fontsData.ts | 168 +- modules/module.helper.ts | 116 +- modules/module.langsData.ts | 224 +- modules/module.merger.ts | 652 ++-- modules/module.parseSelect.ts | 190 +- modules/module.transform-mpd.ts | 366 +- modules/module.updater.ts | 282 +- modules/module.vtt2ass.ts | 840 ++--- modules/module.vttconvert.ts | 224 +- modules/playready/bcert.ts | 812 ++-- modules/playready/cdm.ts | 314 +- modules/playready/device.ts | 136 +- modules/playready/ecc_key.ts | 144 +- modules/playready/elgamal.ts | 50 +- modules/playready/key.ts | 80 +- modules/playready/pssh.ts | 194 +- modules/playready/wrmheader.ts | 152 +- modules/playready/xml_key.ts | 34 +- modules/playready/xmrlicense.ts | 344 +- modules/widevine/cmac.ts | 180 +- modules/widevine/license.ts | 474 +-- 49 files changed, 11559 insertions(+), 11559 deletions(-) diff --git a/adn.ts b/adn.ts index b8fa210..7e0f40e 100644 --- a/adn.ts +++ b/adn.ts @@ -37,556 +37,556 @@ import { ADNStreams } from './@types/adnStreams'; import { ADNSubtitles } from './@types/adnSubtitles'; export default class AnimationDigitalNetwork implements ServiceClass { - public cfg: yamlCfg.ConfigObject; - public locale: string; - private token: Record; - private req: reqModule.Req; - private posAlignMap: { [key: string]: number } = { - 'start': 1, - 'end': 3 - }; - private lineAlignMap: { [key: string]: number } = { - 'middle': 8, - 'end': 4 - }; - private jpnStrings: string[] = [ - 'vostf', - 'vostde' - ]; - private deuStrings: string[] = [ - 'vde' - ]; - private fraStrings: string[] = [ - 'vf' - ]; - private deuSubStrings: string[] = [ - 'vde', - 'vostde' - ]; - private fraSubStrings: string[] = [ - 'vf', - 'vostf' - ]; - - constructor(private debug = false) { - this.cfg = yamlCfg.loadCfg(); - this.token = yamlCfg.loadADNToken(); - this.req = new reqModule.Req(domain, debug, false, 'adn'); - this.locale = 'fr'; - } - - public async cli() { - console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`); - const argv = yargs.appArgv(this.cfg.cli); - if (['fr', 'de'].includes(argv.locale)) - this.locale = argv.locale; - if (argv.debug) - this.debug = true; - - // load binaries - this.cfg.bin = await yamlCfg.loadBinCfg(); - if (argv.allDubs) { - argv.dubLang = langsData.dubLanguageCodes; - } - if (argv.auth) { - //Authenticate - await this.doAuth({ - username: argv.username ?? await Helper.question('[Q] LOGIN/EMAIL: '), - password: argv.password ?? await Helper.question('[Q] PASSWORD: ') - }); - } else if (argv.search && argv.search.length > 2) { - //Search - await this.doSearch({ ...argv, search: argv.search as string }); - } else if (argv.s && !isNaN(parseInt(argv.s,10)) && parseInt(argv.s,10) > 0) { - const selected = await this.selectShow(parseInt(argv.s), argv.e, argv.but, argv.all); - if (selected.isOk) { - for (const select of selected.value) { - if (!(await this.getEpisode(select, {...argv, skipsubs: false}))) { - console.error(`Unable to download selected episode ${select.shortNumber}`); - return false; - } - } - } - return true; - } else { - console.info('No option selected or invalid value entered. Try --help.'); - } - } - - private generateRandomString(length: number) { - const characters = '0123456789abcdef'; - let result = ''; - for (let i = 0; i < length; i++) { - result += characters.charAt(Math.floor(Math.random() * characters.length)); - } - return result; - } - - private parseCookies(cookiesString: string | null): Record { - const cookies: Record = {}; - if (cookiesString) { - cookiesString.split(';').forEach(cookie => { - const parts = cookie.split('='); - const name = parts.shift()?.trim(); - const value = decodeURIComponent(parts.join('=')); - if (name) { - cookies[name] = value; - } - }); - } - return cookies; - } - - private convertToSSATimestamp(timestamp: number): string { - const seconds = Math.floor(timestamp); - const centiseconds = Math.round((timestamp - seconds) * 100); - - const hours = Math.floor(seconds / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - const remainingSeconds = seconds % 60; - - return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`; - } - - public async doSearch(data: SearchData): Promise { - const limit = 12; - const offset = data.page ? data.page * limit : 0; - const searchReq = await this.req.getData(`https://gw.api.animationdigitalnetwork.com/show/catalog?maxAgeCategory=18&offset=${offset}&limit=${limit}&search=${encodeURIComponent(data.search)}`, { - 'headers': { - 'X-Target-Distribution': this.locale - } - }); - if (!searchReq.ok || !searchReq.res) { - console.error('Search FAILED!'); - return { isOk: false, reason: new Error('Search failed. No more information provided') }; - } - const searchData = await searchReq.res.json() as ADNSearch; - const searchItems: ADNSearchShow[] = []; - console.info('Search Results:'); - for (const show of searchData.shows) { - searchItems.push(show); - let fullType: string; - if (show.type == 'EPS') { - fullType = `S.${show.id}`; - } else if (show.type == 'MOV' || show.type == 'OAV') { - fullType = `E.${show.id}`; - } else { - fullType = 'Unknown'; - console.warn(`Unknown type ${show.type}, please report this.`); - } - console.log(`[${fullType}] ${show.title}`); - } - return { isOk: true, value: searchItems.flatMap((a): SearchResponseItem => { - return { - id: a.id+'', - image: a.image ?? '/notFound.png', - name: a.title, - rating: a.rating, - desc: a.summary - }; - })}; - } - - public async doAuth(data: AuthData): Promise { - const authData = JSON.stringify({ - 'username': data.username, - 'password': data.password, - 'source': 'Web' - }); - const authReqOpts: reqModule.Params = { - method: 'POST', - body: authData, - headers: { - 'content-type': 'application/json', - 'x-target-distribution': this.locale, - } + public cfg: yamlCfg.ConfigObject; + public locale: string; + private token: Record; + private req: reqModule.Req; + private posAlignMap: { [key: string]: number } = { + 'start': 1, + 'end': 3 }; - const authReq = await this.req.getData('https://gw.api.animationdigitalnetwork.com/authentication/login', authReqOpts); - if(!authReq.ok || !authReq.res){ - console.error('Authentication failed!'); - return { isOk: false, reason: new Error('Authentication failed') }; + private lineAlignMap: { [key: string]: number } = { + 'middle': 8, + 'end': 4 + }; + private jpnStrings: string[] = [ + 'vostf', + 'vostde' + ]; + private deuStrings: string[] = [ + 'vde' + ]; + private fraStrings: string[] = [ + 'vf' + ]; + private deuSubStrings: string[] = [ + 'vde', + 'vostde' + ]; + private fraSubStrings: string[] = [ + 'vf', + 'vostf' + ]; + + constructor(private debug = false) { + this.cfg = yamlCfg.loadCfg(); + this.token = yamlCfg.loadADNToken(); + this.req = new reqModule.Req(domain, debug, false, 'adn'); + this.locale = 'fr'; } - this.token = await authReq.res.json(); - yamlCfg.saveADNToken(this.token); - console.info('Authentication Success'); - return { isOk: true, value: undefined }; - } - public async refreshToken() { - const authReq = await this.req.getData('https://gw.api.animationdigitalnetwork.com/authentication/refresh', { - method: 'POST', - headers: { - Authorization: `Bearer ${this.token.accessToken}`, - 'X-Access-Token': this.token.accessToken, - 'content-type': 'application/json', - 'x-target-distribution': this.locale - }, - body: JSON.stringify({refreshToken: this.token.refreshToken}) - }); - if(!authReq.ok || !authReq.res){ - console.error('Token refresh failed!'); - return { isOk: false, reason: new Error('Token refresh failed') }; - } - this.token = await authReq.res.json(); - yamlCfg.saveADNToken(this.token); - return { isOk: true, value: undefined }; - } + public async cli() { + console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`); + const argv = yargs.appArgv(this.cfg.cli); + if (['fr', 'de'].includes(argv.locale)) + this.locale = argv.locale; + if (argv.debug) + this.debug = true; - public async getShow(id: number) { - const getShowData = await this.req.getData(`https://gw.api.animationdigitalnetwork.com/video/show/${id}?maxAgeCategory=18&limit=-1&order=asc`, { - 'headers': { - 'X-Target-Distribution': this.locale - } - }); - if (!getShowData.ok || !getShowData.res) { - console.error('Failed to get Series Data'); - return { isOk: false }; + // load binaries + this.cfg.bin = await yamlCfg.loadBinCfg(); + if (argv.allDubs) { + argv.dubLang = langsData.dubLanguageCodes; + } + if (argv.auth) { + //Authenticate + await this.doAuth({ + username: argv.username ?? await Helper.question('[Q] LOGIN/EMAIL: '), + password: argv.password ?? await Helper.question('[Q] PASSWORD: ') + }); + } else if (argv.search && argv.search.length > 2) { + //Search + await this.doSearch({ ...argv, search: argv.search as string }); + } else if (argv.s && !isNaN(parseInt(argv.s,10)) && parseInt(argv.s,10) > 0) { + const selected = await this.selectShow(parseInt(argv.s), argv.e, argv.but, argv.all); + if (selected.isOk) { + for (const select of selected.value) { + if (!(await this.getEpisode(select, {...argv, skipsubs: false}))) { + console.error(`Unable to download selected episode ${select.shortNumber}`); + return false; + } + } + } + return true; + } else { + console.info('No option selected or invalid value entered. Try --help.'); + } } - const showData = await getShowData.res.json() as ADNVideos; - return { isOk: true, value: showData }; - } - public async listShow(id: number) { - const show = await this.getShow(id); - if (!show.isOk || !show.value) { - console.error('Failed to list show data: Failed to get show'); - return { isOk: false }; + private generateRandomString(length: number) { + const characters = '0123456789abcdef'; + let result = ''; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * characters.length)); + } + return result; } - if (show.value.videos.length == 0) { - console.error('No episodes found!'); - return { isOk: false }; - } - const showData = show.value.videos[0].show; - console.info(`[S.${showData.id}] ${showData.title}`); - const specials: ADNVideo[] = []; - const ncs: ADNVideo[] = []; - let episodeIndex = 0, specialIndex = 0, ncIndex = 0; - for (const episode of show.value.videos) { - episode.season = episode.season+''; - const seasonNumberTitleParse = episode.season.match(/\d+/); - const seriesNumberTitleParse = episode.show.title.match(/\d+/); - const episodeNumber = parseInt(episode.shortNumber); - if (seasonNumberTitleParse && !isNaN(parseInt(seasonNumberTitleParse[0]))) { - episode.season = seasonNumberTitleParse[0]; - } else if (seriesNumberTitleParse && !isNaN(parseInt(seriesNumberTitleParse[0]))) { - episode.season = seriesNumberTitleParse[0]; - } else { - episode.season = '1'; - } - show.value.videos[episodeIndex].season = episode.season; - show.value.videos[episodeIndex].shortNumber = episodeIndex+''; - if (!episodeNumber) { - specialIndex++; - episode.shortNumber = 'S'+specialIndex; - specials.push(episode); - episodeIndex--; - } else if (episode.number.includes('(NC)')) { - ncIndex++; - episode.shortNumber = 'NC'+ncIndex; - ncs.push(episode); - episodeIndex--; - } else { - console.info(` (${episode.id}) [E${episode.shortNumber}] ${episode.number} - ${episode.name}`); - } - episodeIndex++; - } - for (const special of specials) { - console.info(` (Special) (${special.id}) [${special.shortNumber}] ${special.number} - ${special.name}`); - show.value.videos.splice(show.value.videos.findIndex(i => i.id === special.id), 1); - } - for (const nc of ncs) { - console.info(` (NC) (${nc.id}) [${nc.shortNumber}] ${nc.number} - ${nc.name}`); - show.value.videos.splice(show.value.videos.findIndex(i => i.id === nc.id), 1); - } - show.value.videos.push(...specials); - show.value.videos.push(...ncs); - return { isOk: true, value: show.value }; - } - public async selectShow(id: number, e: string | undefined, but: boolean, all: boolean) { - const getShowData = await this.listShow(id); - if (!getShowData.isOk || !getShowData.value) { - return { isOk: false, value: [] }; + private parseCookies(cookiesString: string | null): Record { + const cookies: Record = {}; + if (cookiesString) { + cookiesString.split(';').forEach(cookie => { + const parts = cookie.split('='); + const name = parts.shift()?.trim(); + const value = decodeURIComponent(parts.join('=')); + if (name) { + cookies[name] = value; + } + }); + } + return cookies; } - console.info(''); - console.info('-'.repeat(30)); - console.info(''); - const showData = getShowData.value; - const doEpsFilter = parseSelect(e as string); - const selEpsArr: ADNVideo[] = []; - for (const episode of showData.videos) { - if ( - all || + + private convertToSSATimestamp(timestamp: number): string { + const seconds = Math.floor(timestamp); + const centiseconds = Math.round((timestamp - seconds) * 100); + + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const remainingSeconds = seconds % 60; + + return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`; + } + + public async doSearch(data: SearchData): Promise { + const limit = 12; + const offset = data.page ? data.page * limit : 0; + const searchReq = await this.req.getData(`https://gw.api.animationdigitalnetwork.com/show/catalog?maxAgeCategory=18&offset=${offset}&limit=${limit}&search=${encodeURIComponent(data.search)}`, { + 'headers': { + 'X-Target-Distribution': this.locale + } + }); + if (!searchReq.ok || !searchReq.res) { + console.error('Search FAILED!'); + return { isOk: false, reason: new Error('Search failed. No more information provided') }; + } + const searchData = await searchReq.res.json() as ADNSearch; + const searchItems: ADNSearchShow[] = []; + console.info('Search Results:'); + for (const show of searchData.shows) { + searchItems.push(show); + let fullType: string; + if (show.type == 'EPS') { + fullType = `S.${show.id}`; + } else if (show.type == 'MOV' || show.type == 'OAV') { + fullType = `E.${show.id}`; + } else { + fullType = 'Unknown'; + console.warn(`Unknown type ${show.type}, please report this.`); + } + console.log(`[${fullType}] ${show.title}`); + } + return { isOk: true, value: searchItems.flatMap((a): SearchResponseItem => { + return { + id: a.id+'', + image: a.image ?? '/notFound.png', + name: a.title, + rating: a.rating, + desc: a.summary + }; + })}; + } + + public async doAuth(data: AuthData): Promise { + const authData = JSON.stringify({ + 'username': data.username, + 'password': data.password, + 'source': 'Web' + }); + const authReqOpts: reqModule.Params = { + method: 'POST', + body: authData, + headers: { + 'content-type': 'application/json', + 'x-target-distribution': this.locale, + } + }; + const authReq = await this.req.getData('https://gw.api.animationdigitalnetwork.com/authentication/login', authReqOpts); + if(!authReq.ok || !authReq.res){ + console.error('Authentication failed!'); + return { isOk: false, reason: new Error('Authentication failed') }; + } + this.token = await authReq.res.json(); + yamlCfg.saveADNToken(this.token); + console.info('Authentication Success'); + return { isOk: true, value: undefined }; + } + + public async refreshToken() { + const authReq = await this.req.getData('https://gw.api.animationdigitalnetwork.com/authentication/refresh', { + method: 'POST', + headers: { + Authorization: `Bearer ${this.token.accessToken}`, + 'X-Access-Token': this.token.accessToken, + 'content-type': 'application/json', + 'x-target-distribution': this.locale + }, + body: JSON.stringify({refreshToken: this.token.refreshToken}) + }); + if(!authReq.ok || !authReq.res){ + console.error('Token refresh failed!'); + return { isOk: false, reason: new Error('Token refresh failed') }; + } + this.token = await authReq.res.json(); + yamlCfg.saveADNToken(this.token); + return { isOk: true, value: undefined }; + } + + public async getShow(id: number) { + const getShowData = await this.req.getData(`https://gw.api.animationdigitalnetwork.com/video/show/${id}?maxAgeCategory=18&limit=-1&order=asc`, { + 'headers': { + 'X-Target-Distribution': this.locale + } + }); + if (!getShowData.ok || !getShowData.res) { + console.error('Failed to get Series Data'); + return { isOk: false }; + } + const showData = await getShowData.res.json() as ADNVideos; + return { isOk: true, value: showData }; + } + + public async listShow(id: number) { + const show = await this.getShow(id); + if (!show.isOk || !show.value) { + console.error('Failed to list show data: Failed to get show'); + return { isOk: false }; + } + if (show.value.videos.length == 0) { + console.error('No episodes found!'); + return { isOk: false }; + } + const showData = show.value.videos[0].show; + console.info(`[S.${showData.id}] ${showData.title}`); + const specials: ADNVideo[] = []; + const ncs: ADNVideo[] = []; + let episodeIndex = 0, specialIndex = 0, ncIndex = 0; + for (const episode of show.value.videos) { + episode.season = episode.season+''; + const seasonNumberTitleParse = episode.season.match(/\d+/); + const seriesNumberTitleParse = episode.show.title.match(/\d+/); + const episodeNumber = parseInt(episode.shortNumber); + if (seasonNumberTitleParse && !isNaN(parseInt(seasonNumberTitleParse[0]))) { + episode.season = seasonNumberTitleParse[0]; + } else if (seriesNumberTitleParse && !isNaN(parseInt(seriesNumberTitleParse[0]))) { + episode.season = seriesNumberTitleParse[0]; + } else { + episode.season = '1'; + } + show.value.videos[episodeIndex].season = episode.season; + show.value.videos[episodeIndex].shortNumber = episodeIndex+''; + if (!episodeNumber) { + specialIndex++; + episode.shortNumber = 'S'+specialIndex; + specials.push(episode); + episodeIndex--; + } else if (episode.number.includes('(NC)')) { + ncIndex++; + episode.shortNumber = 'NC'+ncIndex; + ncs.push(episode); + episodeIndex--; + } else { + console.info(` (${episode.id}) [E${episode.shortNumber}] ${episode.number} - ${episode.name}`); + } + episodeIndex++; + } + for (const special of specials) { + console.info(` (Special) (${special.id}) [${special.shortNumber}] ${special.number} - ${special.name}`); + show.value.videos.splice(show.value.videos.findIndex(i => i.id === special.id), 1); + } + for (const nc of ncs) { + console.info(` (NC) (${nc.id}) [${nc.shortNumber}] ${nc.number} - ${nc.name}`); + show.value.videos.splice(show.value.videos.findIndex(i => i.id === nc.id), 1); + } + show.value.videos.push(...specials); + show.value.videos.push(...ncs); + return { isOk: true, value: show.value }; + } + + public async selectShow(id: number, e: string | undefined, but: boolean, all: boolean) { + const getShowData = await this.listShow(id); + if (!getShowData.isOk || !getShowData.value) { + return { isOk: false, value: [] }; + } + console.info(''); + console.info('-'.repeat(30)); + console.info(''); + const showData = getShowData.value; + const doEpsFilter = parseSelect(e as string); + const selEpsArr: ADNVideo[] = []; + for (const episode of showData.videos) { + if ( + all || but && !doEpsFilter.isSelected([episode.shortNumber, episode.id+'']) || !but && doEpsFilter.isSelected([episode.shortNumber, episode.id+'']) - ) { - selEpsArr.push({ isSelected: true, ...episode }); - console.info('%s[S%sE%s] %s', - '✓ ', - episode.season, - episode.shortNumber, - episode.name, - ); - } + ) { + selEpsArr.push({ isSelected: true, ...episode }); + console.info('%s[S%sE%s] %s', + '✓ ', + episode.season, + episode.shortNumber, + episode.name, + ); + } + } + return { isOk: true, value: selEpsArr }; } - return { isOk: true, value: selEpsArr }; - } - public async muxStreams(data: DownloadedMedia[], options: yargs.ArgvType) { - this.cfg.bin = await yamlCfg.loadBinCfg(); - let hasAudioStreams = false; - if (options.novids || data.filter(a => a.type === 'Video').length === 0) - return console.info('Skip muxing since no vids are downloaded'); - if (data.some(a => a.type === 'Audio')) { - hasAudioStreams = true; + public async muxStreams(data: DownloadedMedia[], options: yargs.ArgvType) { + this.cfg.bin = await yamlCfg.loadBinCfg(); + let hasAudioStreams = false; + if (options.novids || data.filter(a => a.type === 'Video').length === 0) + return console.info('Skip muxing since no vids are downloaded'); + if (data.some(a => a.type === 'Audio')) { + hasAudioStreams = true; + } + 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, + }; + }) : [], + skipSubMux: options.skipSubMux, + inverseTrackOrder: false, + keepAllVideos: options.keepAllVideos, + 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, + }; + }) : [], + 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'); + return { + file: a.path, + language: a.language, + closedCaption: a.cc + }; + }), + simul: data.filter(a => a.type === 'Video').map((a) : boolean => { + if (a.type === 'Subtitle') + throw new Error('Never'); + return !a.uncut as boolean; + })[0], + 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 => { + return { + path: a.path, + lang: a.lang + }; + }), + videoTitle: options.videoTitle, + options: { + ffmpeg: options.ffmpegOptions, + mkvmerge: options.mkvmergeOptions + }, + defaults: { + audio: options.defaultAudio, + sub: options.defaultSub + }, + ccTag: options.ccTag + }); + const bin = Merger.checkMerger(this.cfg.bin, options.mp4, options.forceMuxer); + // collect fonts info + // mergers + let isMuxed = false; + if (options.syncTiming) { + await merger.createDelays(); + } + if (bin.MKVmerge) { + await merger.merge('mkvmerge', bin.MKVmerge); + isMuxed = true; + } else if (bin.FFmpeg) { + await merger.merge('ffmpeg', bin.FFmpeg); + isMuxed = true; + } else{ + console.info('\nDone!\n'); + return; + } + if (isMuxed && !options.nocleanup) + merger.cleanUp(); } - 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, - }; - }) : [], - skipSubMux: options.skipSubMux, - inverseTrackOrder: false, - keepAllVideos: options.keepAllVideos, - 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, - }; - }) : [], - 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'); - return { - file: a.path, - language: a.language, - closedCaption: a.cc - }; - }), - simul: data.filter(a => a.type === 'Video').map((a) : boolean => { - if (a.type === 'Subtitle') - throw new Error('Never'); - return !a.uncut as boolean; - })[0], - 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 => { - return { - path: a.path, - lang: a.lang - }; - }), - videoTitle: options.videoTitle, - options: { - ffmpeg: options.ffmpegOptions, - mkvmerge: options.mkvmergeOptions - }, - defaults: { - audio: options.defaultAudio, - sub: options.defaultSub - }, - ccTag: options.ccTag - }); - const bin = Merger.checkMerger(this.cfg.bin, options.mp4, options.forceMuxer); - // collect fonts info - // mergers - let isMuxed = false; - if (options.syncTiming) { - await merger.createDelays(); - } - if (bin.MKVmerge) { - await merger.merge('mkvmerge', bin.MKVmerge); - isMuxed = true; - } else if (bin.FFmpeg) { - await merger.merge('ffmpeg', bin.FFmpeg); - isMuxed = true; - } else{ - console.info('\nDone!\n'); - return; - } - if (isMuxed && !options.nocleanup) - merger.cleanUp(); - } - public async getEpisode(data: ADNVideo, options: yargs.ArgvType) { + public async getEpisode(data: ADNVideo, options: yargs.ArgvType) { //TODO: Move all the requests for getting the m3u8 here - const res = await this.downloadEpisode(data, options); - if (res === undefined || res.error) { - console.error('Failed to download media list'); - return { isOk: false, reason: new Error('Failed to download media list') }; - } else { - if (!options.skipmux) { - await this.muxStreams(res.data, { ...options, output: res.fileName }); - } else { - console.info('Skipping mux'); - } - downloaded({ - service: 'adn', - type: 's' - }, data.id+'', [data.shortNumber]); - return { isOk: res, value: undefined }; - } - } - - public async downloadEpisode(data: ADNVideo, options: yargs.ArgvType) { - if(!this.token.accessToken){ - console.error('Authentication required!'); - return; + const res = await this.downloadEpisode(data, options); + if (res === undefined || res.error) { + console.error('Failed to download media list'); + return { isOk: false, reason: new Error('Failed to download media list') }; + } else { + if (!options.skipmux) { + await this.muxStreams(res.data, { ...options, output: res.fileName }); + } else { + console.info('Skipping mux'); + } + downloaded({ + service: 'adn', + type: 's' + }, data.id+'', [data.shortNumber]); + return { isOk: res, value: undefined }; + } } - if (!this.cfg.bin.ffmpeg) - this.cfg.bin = await yamlCfg.loadBinCfg(); + public async downloadEpisode(data: ADNVideo, options: yargs.ArgvType) { + if(!this.token.accessToken){ + console.error('Authentication required!'); + return; + } - let mediaName = '...'; - let fileName; - const variables: Variable[] = []; - if(data.show.title && data.shortNumber && data.title){ - mediaName = `${data.show.shortTitle ?? data.show.title} - ${data.shortNumber} - ${data.title}`; - } + if (!this.cfg.bin.ffmpeg) + this.cfg.bin = await yamlCfg.loadBinCfg(); - const files: DownloadedMedia[] = []; + let mediaName = '...'; + let fileName; + const variables: Variable[] = []; + if(data.show.title && data.shortNumber && data.title){ + mediaName = `${data.show.shortTitle ?? data.show.title} - ${data.shortNumber} - ${data.title}`; + } - let dlFailed = false; - let dlVideoOnce = false; // Variable to save if best selected video quality was downloaded + const files: DownloadedMedia[] = []; - const refreshToken = await this.refreshToken(); - if (!refreshToken.isOk) { - console.error('Failed to refresh token'); - return undefined; - } + let dlFailed = false; + let dlVideoOnce = false; // Variable to save if best selected video quality was downloaded - const configReq = await this.req.getData(`https://gw.api.animationdigitalnetwork.com/player/video/${data.id}/configuration`, { - headers: { - Authorization: `Bearer ${this.token.accessToken}`, - 'X-Target-Distribution': this.locale - } - }); - if(!configReq.ok || !configReq.res){ - console.error('Player Config Request failed!'); - return undefined; - } - const configuration = await configReq.res.json() as ADNPlayerConfig; - if (!configuration.player.options.user.hasAccess) { - console.error('You don\'t have access to this video!'); - return undefined; - } - const tokenReq = await this.req.getData(configuration.player.options.user.refreshTokenUrl || 'https://gw.api.animationdigitalnetwork.com/player/refresh/token', { - method: 'POST', - headers: { - 'X-Player-Refresh-Token': `${configuration.player.options.user.refreshToken}` - } - }); - if(!tokenReq.ok || !tokenReq.res){ - console.error('Player Token Request failed!'); - return undefined; - } - const token = await tokenReq.res.json() as { + const refreshToken = await this.refreshToken(); + if (!refreshToken.isOk) { + console.error('Failed to refresh token'); + return undefined; + } + + const configReq = await this.req.getData(`https://gw.api.animationdigitalnetwork.com/player/video/${data.id}/configuration`, { + headers: { + Authorization: `Bearer ${this.token.accessToken}`, + 'X-Target-Distribution': this.locale + } + }); + if(!configReq.ok || !configReq.res){ + console.error('Player Config Request failed!'); + return undefined; + } + const configuration = await configReq.res.json() as ADNPlayerConfig; + if (!configuration.player.options.user.hasAccess) { + console.error('You don\'t have access to this video!'); + return undefined; + } + const tokenReq = await this.req.getData(configuration.player.options.user.refreshTokenUrl || 'https://gw.api.animationdigitalnetwork.com/player/refresh/token', { + method: 'POST', + headers: { + 'X-Player-Refresh-Token': `${configuration.player.options.user.refreshToken}` + } + }); + if(!tokenReq.ok || !tokenReq.res){ + console.error('Player Token Request failed!'); + return undefined; + } + const token = await tokenReq.res.json() as { refreshToken: string, accessToken: string, token: string }; - const linksUrl = configuration.player.options.video.url || `https://gw.api.animationdigitalnetwork.com/player/video/${data.id}/link`; - const key = this.generateRandomString(16); - const decryptionKey = key + '7fac1178830cfe0c'; + const linksUrl = configuration.player.options.video.url || `https://gw.api.animationdigitalnetwork.com/player/video/${data.id}/link`; + const key = this.generateRandomString(16); + const decryptionKey = key + '7fac1178830cfe0c'; - const authorization = crypto.publicEncrypt({ - 'key': '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCbQrCJBRmaXM4gJidDmcpWDssg\nnumHinCLHAgS4buMtdH7dEGGEUfBofLzoEdt1jqcrCDT6YNhM0aFCqbLOPFtx9cg\n/X2G/G5bPVu8cuFM0L+ehp8s6izK1kjx3OOPH/kWzvstM5tkqgJkNyNEvHdeJl6\nKhS+IFEqwvZqgbBpKuwIDAQAB\n-----END PUBLIC KEY-----', - padding: crypto.constants.RSA_PKCS1_PADDING - }, Buffer.from(JSON.stringify({ - k: key, - t: token.token - }), 'utf-8')).toString('base64'); + const authorization = crypto.publicEncrypt({ + 'key': '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCbQrCJBRmaXM4gJidDmcpWDssg\nnumHinCLHAgS4buMtdH7dEGGEUfBofLzoEdt1jqcrCDT6YNhM0aFCqbLOPFtx9cg\n/X2G/G5bPVu8cuFM0L+ehp8s6izK1kjx3OOPH/kWzvstM5tkqgJkNyNEvHdeJl6\nKhS+IFEqwvZqgbBpKuwIDAQAB\n-----END PUBLIC KEY-----', + padding: crypto.constants.RSA_PKCS1_PADDING + }, Buffer.from(JSON.stringify({ + k: key, + t: token.token + }), 'utf-8')).toString('base64'); - //TODO: Add chapter support - const streamsRequest = await this.req.getData(linksUrl+'?freeWithAds=true&adaptive=true&withMetadata=true&source=Web', { - 'headers': { - 'X-Player-Token': authorization, - 'X-Target-Distribution': this.locale - } - }); - if(!streamsRequest.ok || !streamsRequest.res){ - if (streamsRequest.error?.res!.status == 403 || streamsRequest.res?.status == 403) { - console.error('Georestricted!'); - } else { - console.error('Streams request failed!'); - } - return undefined; - } - const streams = await streamsRequest.res.json() as ADNStreams; - for (const streamName in streams.links.streaming) { - let audDub: langsData.LanguageItem; - if (this.jpnStrings.includes(streamName)) { - audDub = langsData.languages.find(a=>a.code == 'jpn') as langsData.LanguageItem; - } else if (this.deuStrings.includes(streamName)) { - audDub = langsData.languages.find(a=>a.code == 'deu') as langsData.LanguageItem; - } else if (this.fraStrings.includes(streamName)) { - audDub = langsData.languages.find(a=>a.code == 'fra') as langsData.LanguageItem; - } else { - console.error(`Language ${streamName} not recognized, please report this.`); - continue; - } - - if (!options.dubLang.includes(audDub.code)) { - continue; - } - - console.info(`Requesting: [${data.id}] ${mediaName} (${audDub.name})`); - - variables.push(...([ - ['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], - ['showTitle', data.show.title, true], - ['season', isNaN(parseFloat(data.season)) ? data.season : parseFloat(data.season), false] - ] as [AvailableFilenameVars, string|number, boolean][]).map((a): Variable => { - return { - name: a[0], - replaceWith: a[1], - type: typeof a[1], - sanitize: a[2] - } as Variable; - })); - - console.info('Playlists URL: %s', streams.links.streaming[streamName].auto); - - let tsFile = undefined; - - if (!dlFailed && !options.novids) { - const streamPlaylistsLocationReq = await this.req.getData(streams.links.streaming[streamName].auto); - if (!streamPlaylistsLocationReq.ok || !streamPlaylistsLocationReq.res) { - console.error('CAN\'T FETCH VIDEO PLAYLIST LOCATION!'); - return undefined; + //TODO: Add chapter support + const streamsRequest = await this.req.getData(linksUrl+'?freeWithAds=true&adaptive=true&withMetadata=true&source=Web', { + 'headers': { + 'X-Player-Token': authorization, + 'X-Target-Distribution': this.locale + } + }); + if(!streamsRequest.ok || !streamsRequest.res){ + if (streamsRequest.error?.res!.status == 403 || streamsRequest.res?.status == 403) { + console.error('Georestricted!'); + } else { + console.error('Streams request failed!'); + } + return undefined; } - const streamPlaylistLocation = await streamPlaylistsLocationReq.res.json() as {'location': string}; - const streamPlaylistsReq = await this.req.getData(streamPlaylistLocation.location); - if (!streamPlaylistsReq.ok || !streamPlaylistsReq.res) { - console.error('CAN\'T FETCH VIDEO PLAYLISTS!'); - dlFailed = true; - } else { - const streamPlaylistBody = await streamPlaylistsReq.res.text(); - const streamPlaylists = m3u8(streamPlaylistBody); - const plServerList: string[] = [], - plStreams: Record> = {}, - plQuality: { + const streams = await streamsRequest.res.json() as ADNStreams; + for (const streamName in streams.links.streaming) { + let audDub: langsData.LanguageItem; + if (this.jpnStrings.includes(streamName)) { + audDub = langsData.languages.find(a=>a.code == 'jpn') as langsData.LanguageItem; + } else if (this.deuStrings.includes(streamName)) { + audDub = langsData.languages.find(a=>a.code == 'deu') as langsData.LanguageItem; + } else if (this.fraStrings.includes(streamName)) { + audDub = langsData.languages.find(a=>a.code == 'fra') as langsData.LanguageItem; + } else { + console.error(`Language ${streamName} not recognized, please report this.`); + continue; + } + + if (!options.dubLang.includes(audDub.code)) { + continue; + } + + console.info(`Requesting: [${data.id}] ${mediaName} (${audDub.name})`); + + variables.push(...([ + ['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], + ['showTitle', data.show.title, true], + ['season', isNaN(parseFloat(data.season)) ? data.season : parseFloat(data.season), false] + ] as [AvailableFilenameVars, string|number, boolean][]).map((a): Variable => { + return { + name: a[0], + replaceWith: a[1], + type: typeof a[1], + sanitize: a[2] + } as Variable; + })); + + console.info('Playlists URL: %s', streams.links.streaming[streamName].auto); + + let tsFile = undefined; + + if (!dlFailed && !options.novids) { + const streamPlaylistsLocationReq = await this.req.getData(streams.links.streaming[streamName].auto); + if (!streamPlaylistsLocationReq.ok || !streamPlaylistsLocationReq.res) { + console.error('CAN\'T FETCH VIDEO PLAYLIST LOCATION!'); + return undefined; + } + const streamPlaylistLocation = await streamPlaylistsLocationReq.res.json() as {'location': string}; + const streamPlaylistsReq = await this.req.getData(streamPlaylistLocation.location); + if (!streamPlaylistsReq.ok || !streamPlaylistsReq.res) { + console.error('CAN\'T FETCH VIDEO PLAYLISTS!'); + dlFailed = true; + } else { + const streamPlaylistBody = await streamPlaylistsReq.res.text(); + const streamPlaylists = m3u8(streamPlaylistBody); + const plServerList: string[] = [], + plStreams: Record> = {}, + plQuality: { str: string, dim: string, CODECS: string, @@ -595,268 +595,268 @@ export default class AnimationDigitalNetwork implements ServiceClass { height: number } }[] = []; - for(const pl of streamPlaylists.playlists){ - // set quality - const plResolution = pl.attributes.RESOLUTION; - const plResolutionText = `${plResolution.width}x${plResolution.height}`; - // set codecs - const plCodecs = pl.attributes.CODECS; - // parse uri - const plUri = new URL(pl.uri); - let plServer = plUri.hostname; - // set server list - if (plUri.searchParams.get('cdn')){ - plServer += ` (${plUri.searchParams.get('cdn')})`; - } - if (!plServerList.includes(plServer)){ - plServerList.push(plServer); - } - // add to server - if (!Object.keys(plStreams).includes(plServer)){ - plStreams[plServer] = {}; - } - if( - plStreams[plServer][plResolutionText] + for(const pl of streamPlaylists.playlists){ + // set quality + const plResolution = pl.attributes.RESOLUTION; + const plResolutionText = `${plResolution.width}x${plResolution.height}`; + // set codecs + const plCodecs = pl.attributes.CODECS; + // parse uri + const plUri = new URL(pl.uri); + let plServer = plUri.hostname; + // set server list + if (plUri.searchParams.get('cdn')){ + plServer += ` (${plUri.searchParams.get('cdn')})`; + } + if (!plServerList.includes(plServer)){ + plServerList.push(plServer); + } + // add to server + if (!Object.keys(plStreams).includes(plServer)){ + plStreams[plServer] = {}; + } + if( + plStreams[plServer][plResolutionText] && plStreams[plServer][plResolutionText] != pl.uri && typeof plStreams[plServer][plResolutionText] != 'undefined' - ) { - console.error(`Non duplicate url for ${plServer} detected, please report to developer!`); - } else{ - plStreams[plServer][plResolutionText] = pl.uri; + ) { + console.error(`Non duplicate url for ${plServer} detected, please report to developer!`); + } else{ + plStreams[plServer][plResolutionText] = pl.uri; + } + // 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 qualityStrMatch = !plQuality.map(a => a.str).join('\r\n').match(qualityStrRegx); + if(qualityStrMatch){ + plQuality.push({ + str: qualityStrAdd, + dim: plResolutionText, + CODECS: plCodecs, + RESOLUTION: plResolution + }); + } + } + + options.x = options.x > plServerList.length ? 1 : options.x; + + const plSelectedServer = plServerList[options.x - 1]; + const plSelectedList = plStreams[plSelectedServer]; + plQuality.sort((a, b) => { + const aMatch: RegExpMatchArray | never[] = a.dim.match(/[0-9]+/) || []; + const bMatch: RegExpMatchArray | never[] = b.dim.match(/[0-9]+/) || []; + return parseInt(aMatch[0]) - parseInt(bMatch[0]); + }); + let quality = options.q === 0 ? plQuality.length : options.q; + if(quality > plQuality.length) { + console.warn(`The requested quality of ${options.q} is greater than the maximum ${plQuality.length}.\n[WARN] Therefor the maximum will be capped at ${plQuality.length}.`); + quality = plQuality.length; + } + // When best selected video quality is already downloaded + if(dlVideoOnce && options.dlVideoOnce) { + // Select the lowest resolution with the same codecs + while(quality !=1 && plQuality[quality - 1].CODECS == plQuality[quality - 2].CODECS) { + quality--; + } + } + const selPlUrl = plSelectedList[plQuality.map(a => a.dim)[quality - 1]] ? plSelectedList[plQuality.map(a => a.dim)[quality - 1]] : ''; + console.info(`Servers available:\n\t${plServerList.join('\n\t')}`); + console.info(`Available qualities:\n\t${plQuality.map((a, ind) => `[${ind+1}] ${a.str}`).join('\n\t')}`); + + if(selPlUrl != ''){ + variables.push({ + name: 'height', + type: 'number', + replaceWith: quality === 0 ? plQuality[plQuality.length - 1].RESOLUTION.height as number : plQuality[quality - 1].RESOLUTION.height + }, { + name: 'width', + type: 'number', + replaceWith: quality === 0 ? plQuality[plQuality.length - 1].RESOLUTION.width as number : plQuality[quality - 1].RESOLUTION.width + }); + + console.info(`Selected quality: ${Object.keys(plSelectedList).find(a => plSelectedList[a] === selPlUrl)} @ ${plSelectedServer}`); + console.info('Stream URL:', selPlUrl); + // TODO check filename + fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); + const outFile = parseFileName(options.fileName + '.' + audDub.name, variables, options.numbers, options.override).join(path.sep); + console.info(`Output filename: ${outFile}`); + const chunkPage = await this.req.getData(selPlUrl); + if(!chunkPage.ok || !chunkPage.res){ + console.error('CAN\'T FETCH VIDEO PLAYLIST!'); + dlFailed = true; + } else { + const chunkPageBody = await chunkPage.res.text(); + const chunkPlaylist = m3u8(chunkPageBody); + const totalParts = chunkPlaylist.segments.length; + const mathParts = Math.ceil(totalParts / options.partsize); + const mathMsg = `(${mathParts}*${options.partsize})`; + console.info('Total parts in stream:', totalParts, mathMsg); + tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); + const dirName = path.dirname(tsFile); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + const dlStreamByPl = await new streamdl({ + output: `${tsFile}.ts`, + timeout: options.timeout, + m3u8json: chunkPlaylist, + baseurl: selPlUrl.replace('playlist.m3u8',''), + 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: data.image, + parent: { + title: data.show.title + }, + title: data.title, + language: audDub + }) : undefined + }).download(); + if (!dlStreamByPl.ok) { + console.error(`DL Stats: ${JSON.stringify(dlStreamByPl.parts)}\n`); + dlFailed = true; + } + files.push({ + type: 'Video', + path: `${tsFile}.ts`, + lang: audDub + }); + dlVideoOnce = true; + } + } else{ + console.error('Quality not selected!\n'); + dlFailed = true; + } + } + } else if (options.novids) { + console.info('Downloading skipped!'); + fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); } - // 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 qualityStrMatch = !plQuality.map(a => a.str).join('\r\n').match(qualityStrRegx); - if(qualityStrMatch){ - plQuality.push({ - str: qualityStrAdd, - dim: plResolutionText, - CODECS: plCodecs, - RESOLUTION: plResolution - }); - } - } + await this.sleep(options.waittime); + } - options.x = options.x > plServerList.length ? 1 : options.x; - - const plSelectedServer = plServerList[options.x - 1]; - const plSelectedList = plStreams[plSelectedServer]; - plQuality.sort((a, b) => { - const aMatch: RegExpMatchArray | never[] = a.dim.match(/[0-9]+/) || []; - const bMatch: RegExpMatchArray | never[] = b.dim.match(/[0-9]+/) || []; - return parseInt(aMatch[0]) - parseInt(bMatch[0]); - }); - let quality = options.q === 0 ? plQuality.length : options.q; - if(quality > plQuality.length) { - console.warn(`The requested quality of ${options.q} is greater than the maximum ${plQuality.length}.\n[WARN] Therefor the maximum will be capped at ${plQuality.length}.`); - quality = plQuality.length; - } - // When best selected video quality is already downloaded - if(dlVideoOnce && options.dlVideoOnce) { - // Select the lowest resolution with the same codecs - while(quality !=1 && plQuality[quality - 1].CODECS == plQuality[quality - 2].CODECS) { - quality--; - } - } - const selPlUrl = plSelectedList[plQuality.map(a => a.dim)[quality - 1]] ? plSelectedList[plQuality.map(a => a.dim)[quality - 1]] : ''; - console.info(`Servers available:\n\t${plServerList.join('\n\t')}`); - console.info(`Available qualities:\n\t${plQuality.map((a, ind) => `[${ind+1}] ${a.str}`).join('\n\t')}`); - - if(selPlUrl != ''){ - variables.push({ - name: 'height', - type: 'number', - replaceWith: quality === 0 ? plQuality[plQuality.length - 1].RESOLUTION.height as number : plQuality[quality - 1].RESOLUTION.height - }, { - name: 'width', - type: 'number', - replaceWith: quality === 0 ? plQuality[plQuality.length - 1].RESOLUTION.width as number : plQuality[quality - 1].RESOLUTION.width - }); - - console.info(`Selected quality: ${Object.keys(plSelectedList).find(a => plSelectedList[a] === selPlUrl)} @ ${plSelectedServer}`); - console.info('Stream URL:', selPlUrl); - // TODO check filename - fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); - const outFile = parseFileName(options.fileName + '.' + audDub.name, variables, options.numbers, options.override).join(path.sep); - console.info(`Output filename: ${outFile}`); - const chunkPage = await this.req.getData(selPlUrl); - if(!chunkPage.ok || !chunkPage.res){ - console.error('CAN\'T FETCH VIDEO PLAYLIST!'); - dlFailed = true; + const compiledChapters: string[] = []; + if (options.chapters) { + if (streams.video.tcIntroStart) { + if (streams.video.tcIntroStart != '00:00:00') { + compiledChapters.push( + `CHAPTER${(compiledChapters.length/2)+1}=00:00:00.00`, + `CHAPTER${(compiledChapters.length/2)+1}NAME=Prologue` + ); + } + compiledChapters.push( + `CHAPTER${(compiledChapters.length/2)+1}=${streams.video.tcIntroStart+'.00'}`, + `CHAPTER${(compiledChapters.length/2)+1}NAME=Opening` + ); + compiledChapters.push( + `CHAPTER${(compiledChapters.length/2)+1}=${streams.video.tcIntroEnd+'.00'}`, + `CHAPTER${(compiledChapters.length/2)+1}NAME=Episode` + ); } else { - const chunkPageBody = await chunkPage.res.text(); - const chunkPlaylist = m3u8(chunkPageBody); - const totalParts = chunkPlaylist.segments.length; - const mathParts = Math.ceil(totalParts / options.partsize); - const mathMsg = `(${mathParts}*${options.partsize})`; - console.info('Total parts in stream:', totalParts, mathMsg); - tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); - const dirName = path.dirname(tsFile); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }); - } - const dlStreamByPl = await new streamdl({ - output: `${tsFile}.ts`, - timeout: options.timeout, - m3u8json: chunkPlaylist, - baseurl: selPlUrl.replace('playlist.m3u8',''), - 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: data.image, - parent: { - title: data.show.title - }, - title: data.title, - language: audDub - }) : undefined - }).download(); - if (!dlStreamByPl.ok) { - console.error(`DL Stats: ${JSON.stringify(dlStreamByPl.parts)}\n`); - dlFailed = true; - } - files.push({ - type: 'Video', - path: `${tsFile}.ts`, - lang: audDub - }); - dlVideoOnce = true; + compiledChapters.push( + `CHAPTER${(compiledChapters.length/2)+1}=00:00:00.00`, + `CHAPTER${(compiledChapters.length/2)+1}NAME=Episode` + ); + } + + if (streams.video.tcEndingStart) { + compiledChapters.push( + `CHAPTER${(compiledChapters.length/2)+1}=${streams.video.tcEndingStart+'.00'}`, + `CHAPTER${(compiledChapters.length/2)+1}NAME=Ending Start` + ); + compiledChapters.push( + `CHAPTER${(compiledChapters.length/2)+1}=${streams.video.tcEndingEnd+'.00'}`, + `CHAPTER${(compiledChapters.length/2)+1}NAME=Ending End` + ); + } + + if (compiledChapters.length > 0) { + try { + fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); + const outFile = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); + const tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); + const dirName = path.dirname(tsFile); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + fs.writeFileSync(`${tsFile}.txt`, compiledChapters.join('\r\n')); + files.push({ + path: `${tsFile}.txt`, + lang: langsData.languages.find(a=>a.code=='jpn'), + type: 'Chapters' + }); + } catch { + console.error('Failed to write chapter file'); + } } - } else{ - console.error('Quality not selected!\n'); - dlFailed = true; - } } - } else if (options.novids) { - console.info('Downloading skipped!'); - fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); - } - await this.sleep(options.waittime); - } - const compiledChapters: string[] = []; - if (options.chapters) { - if (streams.video.tcIntroStart) { - if (streams.video.tcIntroStart != '00:00:00') { - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=00:00:00.00`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=Prologue` - ); + if(options.dlsubs.indexOf('all') > -1){ + options.dlsubs = ['all']; } - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=${streams.video.tcIntroStart+'.00'}`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=Opening` - ); - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=${streams.video.tcIntroEnd+'.00'}`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=Episode` - ); - } else { - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=00:00:00.00`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=Episode` - ); - } - if (streams.video.tcEndingStart) { - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=${streams.video.tcEndingStart+'.00'}`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=Ending Start` - ); - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=${streams.video.tcEndingEnd+'.00'}`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=Ending End` - ); - } - - if (compiledChapters.length > 0) { - try { - fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); - const outFile = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); - const tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); - const dirName = path.dirname(tsFile); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }); - } - fs.writeFileSync(`${tsFile}.txt`, compiledChapters.join('\r\n')); - files.push({ - path: `${tsFile}.txt`, - lang: langsData.languages.find(a=>a.code=='jpn'), - type: 'Chapters' - }); - } catch { - console.error('Failed to write chapter file'); + if (options.nosubs) { + console.info('Subtitles downloading disabled from nosubs flag.'); + options.skipsubs = true; } - } - } - 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 (Object.keys(streams.links.subtitles).length !== 0) { - const subtitlesUrlReq = await this.req.getData(streams.links.subtitles.all); - if(!subtitlesUrlReq.ok || !subtitlesUrlReq.res){ - console.error('Subtitle location request failed!'); - return undefined; - } - const subtitleUrl = await subtitlesUrlReq.res.json() as {'location': string}; - const encryptedSubtitlesReq = await this.req.getData(subtitleUrl.location); - if(!encryptedSubtitlesReq.ok || !encryptedSubtitlesReq.res){ - console.error('Subtitle request failed!'); - return undefined; - } - const encryptedSubtitles = await encryptedSubtitlesReq.res.text(); - const iv = Buffer.from(encryptedSubtitles.slice(0, 24), 'base64'); - const derivedKey = Buffer.from(decryptionKey, 'hex'); - const encryptedData = Buffer.from(encryptedSubtitles.slice(24), 'base64'); - const decipher = crypto.createDecipheriv('aes-128-cbc', derivedKey, iv); - const decryptedData = Buffer.concat([decipher.update(encryptedData), decipher.final()]).toString('utf8'); + if(!options.skipsubs && options.dlsubs.indexOf('none') == -1) { + if (Object.keys(streams.links.subtitles).length !== 0) { + const subtitlesUrlReq = await this.req.getData(streams.links.subtitles.all); + if(!subtitlesUrlReq.ok || !subtitlesUrlReq.res){ + console.error('Subtitle location request failed!'); + return undefined; + } + const subtitleUrl = await subtitlesUrlReq.res.json() as {'location': string}; + const encryptedSubtitlesReq = await this.req.getData(subtitleUrl.location); + if(!encryptedSubtitlesReq.ok || !encryptedSubtitlesReq.res){ + console.error('Subtitle request failed!'); + return undefined; + } + const encryptedSubtitles = await encryptedSubtitlesReq.res.text(); + const iv = Buffer.from(encryptedSubtitles.slice(0, 24), 'base64'); + const derivedKey = Buffer.from(decryptionKey, 'hex'); + const encryptedData = Buffer.from(encryptedSubtitles.slice(24), 'base64'); + const decipher = crypto.createDecipheriv('aes-128-cbc', derivedKey, iv); + const decryptedData = Buffer.concat([decipher.update(encryptedData), decipher.final()]).toString('utf8'); - let subIndex = 0; - const subtitles = JSON.parse(decryptedData) as ADNSubtitles; - if (Object.keys(subtitles).length === 0) { - console.warn('No subtitles found.'); - } - for (const subName in subtitles) { - let subLang: langsData.LanguageItem; - if (this.deuSubStrings.includes(subName)) { - subLang = langsData.languages.find(a=>a.code == 'deu') as langsData.LanguageItem; - } else if (this.fraSubStrings.includes(subName)) { - subLang = langsData.languages.find(a=>a.code == 'fra') as langsData.LanguageItem; - } else { - console.error(`Language ${subName} not recognized, please report this.`); - continue; - } + let subIndex = 0; + const subtitles = JSON.parse(decryptedData) as ADNSubtitles; + if (Object.keys(subtitles).length === 0) { + console.warn('No subtitles found.'); + } + for (const subName in subtitles) { + let subLang: langsData.LanguageItem; + if (this.deuSubStrings.includes(subName)) { + subLang = langsData.languages.find(a=>a.code == 'deu') as langsData.LanguageItem; + } else if (this.fraSubStrings.includes(subName)) { + subLang = langsData.languages.find(a=>a.code == 'fra') as langsData.LanguageItem; + } else { + console.error(`Language ${subName} not recognized, please report this.`); + continue; + } - if (!options.dlsubs.includes(subLang.locale) && !options.dlsubs.includes('all')) { - continue; - } + if (!options.dlsubs.includes(subLang.locale) && !options.dlsubs.includes('all')) { + continue; + } - const sxData: Partial = {}; - sxData.file = langsData.subsFile(fileName as string, subIndex+'', subLang, false, options.ccTag); - if (path.isAbsolute(sxData.file)) { - sxData.path = sxData.file; - } else { - sxData.path = path.join(this.cfg.dir.content, sxData.file); - } - const dirName = path.dirname(sxData.path); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }); - } - sxData.language = subLang; - if(options.dlsubs.includes('all') || options.dlsubs.includes(subLang.locale)) { - let subBody = '[Script Info]' + const sxData: Partial = {}; + sxData.file = langsData.subsFile(fileName as string, subIndex+'', subLang, false, options.ccTag); + if (path.isAbsolute(sxData.file)) { + sxData.path = sxData.file; + } else { + sxData.path = path.join(this.cfg.dir.content, sxData.file); + } + const dirName = path.dirname(sxData.path); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + sxData.language = subLang; + if(options.dlsubs.includes('all') || options.dlsubs.includes(subLang.locale)) { + let subBody = '[Script Info]' + '\nScriptType:V4.00+' + '\nWrapStyle: 0' + '\nPlayResX: 1280' @@ -869,70 +869,70 @@ export default class AnimationDigitalNetwork implements ServiceClass { + '\n[Events]' + '\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text'; - for (const sub of subtitles[subName]) { - const [start, end, text, lineAlign, positionAlign] = + for (const sub of subtitles[subName]) { + const [start, end, text, lineAlign, positionAlign] = [sub.startTime, sub.endTime, sub.text, sub.lineAlign, sub.positionAlign]; - for (const subProp in sub) { - switch (subProp) { - case 'startTime': - case 'endTime': - case 'text': - case 'lineAlign': - case 'positionAlign': - break; - default: - console.warn(`json2ass: Unknown style: ${subProp}`); + for (const subProp in sub) { + switch (subProp) { + case 'startTime': + case 'endTime': + case 'text': + case 'lineAlign': + case 'positionAlign': + break; + default: + console.warn(`json2ass: Unknown style: ${subProp}`); + } + } + const alignment = (this.posAlignMap[positionAlign] || 2) + (this.lineAlignMap[lineAlign] || 0); + const xtext = text + .replace(/ \\N$/g, '\\N') + .replace(/\\N$/, '') + .replace(/\r/g, '') + .replace(/\n/g, '\\N') + .replace(/\\N +/g, '\\N') + .replace(/ +\\N/g, '\\N') + .replace(/(\\N)+/g, '\\N') + .replace(/]*>([^<]*)<\/b>/g, '{\\b1}$1{\\b0}') + .replace(/]*>([^<]*)<\/i>/g, '{\\i1}$1{\\i0}') + .replace(/]*>([^<]*)<\/u>/g, '{\\u1}$1{\\u0}') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&') + .replace(/<[^>]>/g, '') + .replace(/\\N$/, '') + .replace(/ +$/, ''); + subBody += `\nDialogue: 0,${this.convertToSSATimestamp(start)},${this.convertToSSATimestamp(end)},Default,,0,0,0,,${(alignment !== 2 ? `{\\a${alignment}}` : '')}${xtext}`; + } + sxData.title = `${subLang.language}`; + sxData.fonts = fontsData.assFonts(subBody) as Font[]; + fs.writeFileSync(sxData.path, subBody); + console.info(`Subtitle converted: ${sxData.file}`); + files.push({ + type: 'Subtitle', + ...sxData as sxItem, + cc: false + }); + } + subIndex++; } - } - const alignment = (this.posAlignMap[positionAlign] || 2) + (this.lineAlignMap[lineAlign] || 0); - const xtext = text - .replace(/ \\N$/g, '\\N') - .replace(/\\N$/, '') - .replace(/\r/g, '') - .replace(/\n/g, '\\N') - .replace(/\\N +/g, '\\N') - .replace(/ +\\N/g, '\\N') - .replace(/(\\N)+/g, '\\N') - .replace(/]*>([^<]*)<\/b>/g, '{\\b1}$1{\\b0}') - .replace(/]*>([^<]*)<\/i>/g, '{\\i1}$1{\\i0}') - .replace(/]*>([^<]*)<\/u>/g, '{\\u1}$1{\\u0}') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/&/g, '&') - .replace(/<[^>]>/g, '') - .replace(/\\N$/, '') - .replace(/ +$/, ''); - subBody += `\nDialogue: 0,${this.convertToSSATimestamp(start)},${this.convertToSSATimestamp(end)},Default,,0,0,0,,${(alignment !== 2 ? `{\\a${alignment}}` : '')}${xtext}`; + } else { + console.warn('Couldn\'t find subtitles.'); } - sxData.title = `${subLang.language}`; - sxData.fonts = fontsData.assFonts(subBody) as Font[]; - fs.writeFileSync(sxData.path, subBody); - console.info(`Subtitle converted: ${sxData.file}`); - files.push({ - type: 'Subtitle', - ...sxData as sxItem, - cc: false - }); - } - subIndex++; + } else{ + console.info('Subtitles downloading skipped!'); } - } else { - console.warn('Couldn\'t find subtitles.'); - } - } else{ - console.info('Subtitles downloading skipped!'); + + return { + error: dlFailed, + data: files, + fileName: fileName ? (path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName)) || './unknown' : './unknown' + }; } - return { - error: dlFailed, - data: files, - fileName: fileName ? (path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName)) || './unknown' : './unknown' - }; - } - - public sleep(ms: number) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); - } + public sleep(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + } } \ No newline at end of file diff --git a/ao.ts b/ao.ts index 4674de5..1095335 100644 --- a/ao.ts +++ b/ao.ts @@ -50,799 +50,799 @@ type parsedMultiDubDownload = { } export default class AnimeOnegai implements ServiceClass { - public cfg: yamlCfg.ConfigObject; - private token: Record; - private req: reqModule.Req; - public locale: string; - public jpnStrings: string[] = [ - 'japonés con subtítulos en español', - 'japonés con subtítulos en portugués', - 'japonês com legendas em espanhol', - 'japonês com legendas em português', - 'japonés' - ]; - public spaStrings: string[] = [ - 'doblaje en español', - 'dublagem em espanhol', - 'español', - ]; - public porStrings: string[] = [ - 'doblaje en portugués', - 'dublagem em português' - ]; - private defaultOptions: RequestInit = { - 'headers': { - 'origin': 'https://www.animeonegai.com', - 'referer': 'https://www.animeonegai.com/', - } - }; - - constructor(private debug = false) { - this.cfg = yamlCfg.loadCfg(); - this.token = yamlCfg.loadAOToken(); - this.req = new reqModule.Req(domain, debug, false, 'ao'); - this.locale = 'es'; - } - - public async cli() { - console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`); - const argv = yargs.appArgv(this.cfg.cli); - if (['pt', 'es'].includes(argv.locale)) - this.locale = argv.locale; - if (argv.debug) - this.debug = true; - - // load binaries - this.cfg.bin = await yamlCfg.loadBinCfg(); - if (argv.allDubs) { - argv.dubLang = langsData.dubLanguageCodes; - } - if (argv.auth) { - //Authenticate - await this.doAuth({ - username: argv.username ?? await Helper.question('[Q] LOGIN/EMAIL: '), - password: argv.password ?? await Helper.question('[Q] PASSWORD: ') - }); - } else if (argv.search && argv.search.length > 2) { - //Search - await this.doSearch({ ...argv, search: argv.search as string }); - } else if (argv.s && !isNaN(parseInt(argv.s,10)) && parseInt(argv.s,10) > 0) { - const selected = await this.selectShow(parseInt(argv.s), argv.e, argv.but, argv.all, argv); - if (selected.isOk) { - for (const select of selected.value) { - if (!(await this.downloadEpisode(select, {...argv, skipsubs: false}))) { - console.error(`Unable to download selected episode ${select.episodeNumber}`); - return false; - } + public cfg: yamlCfg.ConfigObject; + private token: Record; + private req: reqModule.Req; + public locale: string; + public jpnStrings: string[] = [ + 'japonés con subtítulos en español', + 'japonés con subtítulos en portugués', + 'japonês com legendas em espanhol', + 'japonês com legendas em português', + 'japonés' + ]; + public spaStrings: string[] = [ + 'doblaje en español', + 'dublagem em espanhol', + 'español', + ]; + public porStrings: string[] = [ + 'doblaje en portugués', + 'dublagem em português' + ]; + private defaultOptions: RequestInit = { + 'headers': { + 'origin': 'https://www.animeonegai.com', + 'referer': 'https://www.animeonegai.com/', } - } - return true; - } else if (argv.token) { - this.token = {token: argv.token}; - yamlCfg.saveAOToken(this.token); - console.info('Saved token'); - } else { - console.info('No option selected or invalid value entered. Try --help.'); - } - } + }; - public async doSearch(data: SearchData): Promise { - const searchReq = await this.req.getData(`https://api.animeonegai.com/v1/search/algolia/${encodeURIComponent(data.search)}?lang=${this.locale}`, this.defaultOptions); - if (!searchReq.ok || !searchReq.res) { - console.error('Search FAILED!'); - return { isOk: false, reason: new Error('Search failed. No more information provided') }; + constructor(private debug = false) { + this.cfg = yamlCfg.loadCfg(); + this.token = yamlCfg.loadAOToken(); + this.req = new reqModule.Req(domain, debug, false, 'ao'); + this.locale = 'es'; } - const searchData = await searchReq.res.json() as AnimeOnegaiSearch; - const searchItems: AOSearchResult[] = []; - console.info('Search Results:'); - for (const hit of searchData.list) { - searchItems.push(hit); - let fullType: string; - if (hit.asset_type == 2) { - fullType = `S.${hit.ID}`; - } else if (hit.asset_type == 1) { - fullType = `E.${hit.ID}`; - } else { - fullType = 'Unknown'; - console.warn(`Unknown asset type ${hit.asset_type}, please report this.`); - } - console.log(`[${fullType}] ${hit.title}`); - } - return { isOk: true, value: searchItems.filter(a => a.asset_type == 2).flatMap((a): SearchResponseItem => { - return { - id: a.ID+'', - image: a.poster ?? '/notFound.png', - name: a.title, - rating: a.likes, - desc: a.description - }; - })}; - } - public async doAuth(data: AuthData): Promise { - data; - console.error('Authentication not possible, manual authentication required due to recaptcha. In order to login use the --token flag. You can get the token by logging into the website, and opening the dev console and running the command "localStorage.ott_token"'); - return { isOk: false, reason: new Error('Authentication not possible, manual authentication required do to recaptcha.') }; - } + public async cli() { + console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`); + const argv = yargs.appArgv(this.cfg.cli); + if (['pt', 'es'].includes(argv.locale)) + this.locale = argv.locale; + if (argv.debug) + this.debug = true; - public async getShow(id: number) { - const getSeriesData = await this.req.getData(`https://api.animeonegai.com/v1/asset/${id}?lang=${this.locale}`, this.defaultOptions); - if (!getSeriesData.ok || !getSeriesData.res) { - console.error('Failed to get Show Data'); - return { isOk: false }; - } - const seriesData = await getSeriesData.res.json() as AnimeOnegaiSeries; - - const getSeasonData = await this.req.getData(`https://api.animeonegai.com/v1/asset/content/${id}?lang=${this.locale}`, this.defaultOptions); - if (!getSeasonData.ok || !getSeasonData.res) { - console.error('Failed to get Show Data'); - return { isOk: false }; - } - const seasonData = await getSeasonData.res.json() as AnimeOnegaiSeasons[]; - - return { isOk: true, data: seriesData, seasons: seasonData }; - } - - public async listShow(id: number, outputEpisode: boolean = true) { - const series = await this.getShow(id); - if (!series.isOk || !series.data) { - console.error('Failed to list series data: Failed to get series'); - return { isOk: false }; - } - console.info(`[S.${series.data.ID}] ${series.data.title} (${series.seasons.length} Seasons)`); - if (series.seasons.length === 0 && series.data.asset_type !== 1) { - console.info(' No Seasons found!'); - return { isOk: false }; - } - const episodes: { [key: string]: (Episode & { lang?: string })[] } = {}; - for (const season of series.seasons) { - let lang: string | undefined = undefined; - if (this.jpnStrings.includes(season.name.trim().toLowerCase())) lang = 'ja'; - else if (this.porStrings.includes(season.name.trim().toLowerCase())) lang = 'pt'; - else if (this.spaStrings.includes(season.name.trim().toLowerCase())) lang = 'es'; - else {lang = 'unknown';console.error(`Language (${season.name.trim()}) not known, please report this!`);} - for (const episode of season.list) { - if (!episodes[episode.number]) { - episodes[episode.number] = []; + // load binaries + this.cfg.bin = await yamlCfg.loadBinCfg(); + if (argv.allDubs) { + argv.dubLang = langsData.dubLanguageCodes; + } + if (argv.auth) { + //Authenticate + await this.doAuth({ + username: argv.username ?? await Helper.question('[Q] LOGIN/EMAIL: '), + password: argv.password ?? await Helper.question('[Q] PASSWORD: ') + }); + } else if (argv.search && argv.search.length > 2) { + //Search + await this.doSearch({ ...argv, search: argv.search as string }); + } else if (argv.s && !isNaN(parseInt(argv.s,10)) && parseInt(argv.s,10) > 0) { + const selected = await this.selectShow(parseInt(argv.s), argv.e, argv.but, argv.all, argv); + if (selected.isOk) { + for (const select of selected.value) { + if (!(await this.downloadEpisode(select, {...argv, skipsubs: false}))) { + console.error(`Unable to download selected episode ${select.episodeNumber}`); + return false; + } + } + } + return true; + } else if (argv.token) { + this.token = {token: argv.token}; + yamlCfg.saveAOToken(this.token); + console.info('Saved token'); + } else { + console.info('No option selected or invalid value entered. Try --help.'); } - /*if (!episodes[episode.number].find(a=>a.lang == lang))*/ episodes[episode.number].push({...episode, lang}); - } } - //Item is movie, lets define it manually - if (series.data.asset_type === 1 && series.seasons.length === 0) { - let lang: string | undefined; - if (this.jpnStrings.some(str => series.data.title.includes(str.toLowerCase()))) lang = 'ja'; - else if (this.porStrings.some(str => series.data.title.includes(str.toLowerCase()))) lang = 'pt'; - else if (this.spaStrings.some(str => series.data.title.includes(str.toLowerCase()))) lang = 'es'; - else {lang = 'unknown';console.error('Language could not be parsed from movie title, please report this!');} - episodes[1] = [{ - 'video_entry': series.data.video_entry, - 'number': 1, - 'season_id': 1, - 'name': series.data.title, - 'ID': series.data.ID, - 'CreatedAt': series.data.CreatedAt, - 'DeletedAt': series.data.DeletedAt, - 'UpdatedAt': series.data.UpdatedAt, - 'active': series.data.active, - 'description': series.data.description, - 'age_restriction': series.data.age_restriction, - 'asset_id': series.data.ID, - 'ending': null, - 'entry': series.data.entry, - 'stream_url': series.data.stream_url, - 'skip_intro': null, - 'thumbnail': series.data.bg, - 'open_free': false, - lang - }]; // as unknown as (Episode & { lang?: string })[]; - // The above needs to be uncommented if the episode number should be M1 instead of 1 - } - //Enable to output episodes seperate from selection - if (outputEpisode) { - for (const episodeKey in episodes) { - const episode = episodes[episodeKey][0]; - const langs = Array.from(new Set(episodes[episodeKey].map(a=>a.lang))); - console.info(` [E.${episode.ID}] E${episode.number} - ${episode.name} (${langs.map(a=>{ - if (a) return langsData.languages.find(b=>b.ao_locale === a)?.name; - return 'Unknown'; - }).join(', ')})`); - } - } - return { isOk: true, value: episodes, series: series }; - } - public async selectShow(id: number, e: string | undefined, but: boolean, all: boolean, options: yargs.ArgvType) { - const getShowData = await this.listShow(id, false); - if (!getShowData.isOk || !getShowData.value) { - return { isOk: false, value: [] }; + public async doSearch(data: SearchData): Promise { + const searchReq = await this.req.getData(`https://api.animeonegai.com/v1/search/algolia/${encodeURIComponent(data.search)}?lang=${this.locale}`, this.defaultOptions); + if (!searchReq.ok || !searchReq.res) { + console.error('Search FAILED!'); + return { isOk: false, reason: new Error('Search failed. No more information provided') }; + } + const searchData = await searchReq.res.json() as AnimeOnegaiSearch; + const searchItems: AOSearchResult[] = []; + console.info('Search Results:'); + for (const hit of searchData.list) { + searchItems.push(hit); + let fullType: string; + if (hit.asset_type == 2) { + fullType = `S.${hit.ID}`; + } else if (hit.asset_type == 1) { + fullType = `E.${hit.ID}`; + } else { + fullType = 'Unknown'; + console.warn(`Unknown asset type ${hit.asset_type}, please report this.`); + } + console.log(`[${fullType}] ${hit.title}`); + } + return { isOk: true, value: searchItems.filter(a => a.asset_type == 2).flatMap((a): SearchResponseItem => { + return { + id: a.ID+'', + image: a.poster ?? '/notFound.png', + name: a.title, + rating: a.likes, + desc: a.description + }; + })}; } - //const showData = getShowData.value; - const doEpsFilter = parseSelect(e as string); - // build selected episodes - const selEpsArr: parsedMultiDubDownload[] = []; - const episodes = getShowData.value; - const seasonNumberTitleParse = getShowData.series.data.title.match(/\d+/); - const seasonNumber = seasonNumberTitleParse ? parseInt(seasonNumberTitleParse[0]) : 1; - for (const episodeKey in getShowData.value) { - const episode = episodes[episodeKey][0]; - const selectedLangs: string[] = []; - const selected: { + + public async doAuth(data: AuthData): Promise { + data; + console.error('Authentication not possible, manual authentication required due to recaptcha. In order to login use the --token flag. You can get the token by logging into the website, and opening the dev console and running the command "localStorage.ott_token"'); + return { isOk: false, reason: new Error('Authentication not possible, manual authentication required do to recaptcha.') }; + } + + public async getShow(id: number) { + const getSeriesData = await this.req.getData(`https://api.animeonegai.com/v1/asset/${id}?lang=${this.locale}`, this.defaultOptions); + if (!getSeriesData.ok || !getSeriesData.res) { + console.error('Failed to get Show Data'); + return { isOk: false }; + } + const seriesData = await getSeriesData.res.json() as AnimeOnegaiSeries; + + const getSeasonData = await this.req.getData(`https://api.animeonegai.com/v1/asset/content/${id}?lang=${this.locale}`, this.defaultOptions); + if (!getSeasonData.ok || !getSeasonData.res) { + console.error('Failed to get Show Data'); + return { isOk: false }; + } + const seasonData = await getSeasonData.res.json() as AnimeOnegaiSeasons[]; + + return { isOk: true, data: seriesData, seasons: seasonData }; + } + + public async listShow(id: number, outputEpisode: boolean = true) { + const series = await this.getShow(id); + if (!series.isOk || !series.data) { + console.error('Failed to list series data: Failed to get series'); + return { isOk: false }; + } + console.info(`[S.${series.data.ID}] ${series.data.title} (${series.seasons.length} Seasons)`); + if (series.seasons.length === 0 && series.data.asset_type !== 1) { + console.info(' No Seasons found!'); + return { isOk: false }; + } + const episodes: { [key: string]: (Episode & { lang?: string })[] } = {}; + for (const season of series.seasons) { + let lang: string | undefined = undefined; + if (this.jpnStrings.includes(season.name.trim().toLowerCase())) lang = 'ja'; + else if (this.porStrings.includes(season.name.trim().toLowerCase())) lang = 'pt'; + else if (this.spaStrings.includes(season.name.trim().toLowerCase())) lang = 'es'; + else {lang = 'unknown';console.error(`Language (${season.name.trim()}) not known, please report this!`);} + for (const episode of season.list) { + if (!episodes[episode.number]) { + episodes[episode.number] = []; + } + /*if (!episodes[episode.number].find(a=>a.lang == lang))*/ episodes[episode.number].push({...episode, lang}); + } + } + //Item is movie, lets define it manually + if (series.data.asset_type === 1 && series.seasons.length === 0) { + let lang: string | undefined; + if (this.jpnStrings.some(str => series.data.title.includes(str.toLowerCase()))) lang = 'ja'; + else if (this.porStrings.some(str => series.data.title.includes(str.toLowerCase()))) lang = 'pt'; + else if (this.spaStrings.some(str => series.data.title.includes(str.toLowerCase()))) lang = 'es'; + else {lang = 'unknown';console.error('Language could not be parsed from movie title, please report this!');} + episodes[1] = [{ + 'video_entry': series.data.video_entry, + 'number': 1, + 'season_id': 1, + 'name': series.data.title, + 'ID': series.data.ID, + 'CreatedAt': series.data.CreatedAt, + 'DeletedAt': series.data.DeletedAt, + 'UpdatedAt': series.data.UpdatedAt, + 'active': series.data.active, + 'description': series.data.description, + 'age_restriction': series.data.age_restriction, + 'asset_id': series.data.ID, + 'ending': null, + 'entry': series.data.entry, + 'stream_url': series.data.stream_url, + 'skip_intro': null, + 'thumbnail': series.data.bg, + 'open_free': false, + lang + }]; // as unknown as (Episode & { lang?: string })[]; + // The above needs to be uncommented if the episode number should be M1 instead of 1 + } + //Enable to output episodes seperate from selection + if (outputEpisode) { + for (const episodeKey in episodes) { + const episode = episodes[episodeKey][0]; + const langs = Array.from(new Set(episodes[episodeKey].map(a=>a.lang))); + console.info(` [E.${episode.ID}] E${episode.number} - ${episode.name} (${langs.map(a=>{ + if (a) return langsData.languages.find(b=>b.ao_locale === a)?.name; + return 'Unknown'; + }).join(', ')})`); + } + } + return { isOk: true, value: episodes, series: series }; + } + + public async selectShow(id: number, e: string | undefined, but: boolean, all: boolean, options: yargs.ArgvType) { + const getShowData = await this.listShow(id, false); + if (!getShowData.isOk || !getShowData.value) { + return { isOk: false, value: [] }; + } + //const showData = getShowData.value; + const doEpsFilter = parseSelect(e as string); + // build selected episodes + const selEpsArr: parsedMultiDubDownload[] = []; + const episodes = getShowData.value; + const seasonNumberTitleParse = getShowData.series.data.title.match(/\d+/); + const seasonNumber = seasonNumberTitleParse ? parseInt(seasonNumberTitleParse[0]) : 1; + for (const episodeKey in getShowData.value) { + const episode = episodes[episodeKey][0]; + const selectedLangs: string[] = []; + const selected: { lang: string, videoId: string episode: Episode }[] = []; - for (const episode of episodes[episodeKey]) { - const lang = langsData.languages.find(a=>a.ao_locale === episode.lang); - let isSelected = false; - if (typeof selected.find(a=>a.lang == episode.lang) == 'undefined') { - if (options.dubLang.includes(lang?.code ?? 'Unknown')) { - if ((but && !doEpsFilter.isSelected([episode.number+'', episode.ID+''])) || all || (!but && doEpsFilter.isSelected([episode.number+'', episode.ID+'']))) { - isSelected = true; - selected.push({lang: episode.lang as string, videoId: episode.video_entry, episode: episode }); + for (const episode of episodes[episodeKey]) { + const lang = langsData.languages.find(a=>a.ao_locale === episode.lang); + let isSelected = false; + if (typeof selected.find(a=>a.lang == episode.lang) == 'undefined') { + if (options.dubLang.includes(lang?.code ?? 'Unknown')) { + if ((but && !doEpsFilter.isSelected([episode.number+'', episode.ID+''])) || all || (!but && doEpsFilter.isSelected([episode.number+'', episode.ID+'']))) { + isSelected = true; + selected.push({lang: episode.lang as string, videoId: episode.video_entry, episode: episode }); + } + } + const selectedLang = isSelected ? `✓ ${lang?.name ?? 'Unknown'}` : `${lang?.name ?? 'Unknown'}`; + if (!selectedLangs.includes(selectedLang)) { + selectedLangs.push(selectedLang); + } + } } - } - const selectedLang = isSelected ? `✓ ${lang?.name ?? 'Unknown'}` : `${lang?.name ?? 'Unknown'}`; - if (!selectedLangs.includes(selectedLang)) { - selectedLangs.push(selectedLang); - } + if (selected.length > 0) { + selEpsArr.push({ + 'data': selected, + 'seasonNumber': seasonNumber, + 'episodeNumber': episode.number, + 'episodeTitle': episode.name, + 'image': episode.thumbnail, + 'seasonID': episode.season_id, + 'seasonTitle': getShowData.series.data.title, + 'seriesTitle': getShowData.series.data.title, + 'seriesID': getShowData.series.data.ID + }); + } + console.info(` [S${seasonNumber}E${episode.number}] - ${episode.name} (${selectedLangs.join(', ')})`); } - } - if (selected.length > 0) { - selEpsArr.push({ - 'data': selected, - 'seasonNumber': seasonNumber, - 'episodeNumber': episode.number, - 'episodeTitle': episode.name, - 'image': episode.thumbnail, - 'seasonID': episode.season_id, - 'seasonTitle': getShowData.series.data.title, - 'seriesTitle': getShowData.series.data.title, - 'seriesID': getShowData.series.data.ID + return { isOk: true, value: selEpsArr, showData: getShowData.series }; + } + + public async downloadEpisode(data: parsedMultiDubDownload, options: yargs.ArgvType): Promise { + const res = await this.downloadMediaList(data, options); + if (res === undefined || res.error) { + return false; + } else { + if (!options.skipmux) { + await this.muxStreams(res.data, { ...options, output: res.fileName }); + } else { + console.info('Skipping mux'); + } + downloaded({ + service: 'ao', + type: 's' + }, data.seasonID+'', [data.episodeNumber+'']); + } + return true; + } + + public async muxStreams(data: DownloadedMedia[], options: yargs.ArgvType) { + this.cfg.bin = await yamlCfg.loadBinCfg(); + let hasAudioStreams = false; + if (options.novids || data.filter(a => a.type === 'Video').length === 0) + return console.info('Skip muxing since no vids are downloaded'); + if (data.some(a => a.type === 'Audio')) { + hasAudioStreams = true; + } + 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, + }; + }) : [], + skipSubMux: options.skipSubMux, + inverseTrackOrder: false, + keepAllVideos: options.keepAllVideos, + 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, + }; + }) : [], + 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'); + return { + file: a.path, + language: a.language, + closedCaption: a.cc + }; + }), + simul: data.filter(a => a.type === 'Video').map((a) : boolean => { + if (a.type === 'Subtitle') + throw new Error('Never'); + return !a.uncut as boolean; + })[0], + 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, + }; + }), + videoTitle: options.videoTitle, + options: { + ffmpeg: options.ffmpegOptions, + mkvmerge: options.mkvmergeOptions + }, + defaults: { + audio: options.defaultAudio, + sub: options.defaultSub + }, + ccTag: options.ccTag }); - } - console.info(` [S${seasonNumber}E${episode.number}] - ${episode.name} (${selectedLangs.join(', ')})`); + const bin = Merger.checkMerger(this.cfg.bin, options.mp4, options.forceMuxer); + // collect fonts info + // mergers + let isMuxed = false; + if (options.syncTiming) { + await merger.createDelays(); + } + if (bin.MKVmerge) { + await merger.merge('mkvmerge', bin.MKVmerge); + isMuxed = true; + } else if (bin.FFmpeg) { + await merger.merge('ffmpeg', bin.FFmpeg); + isMuxed = true; + } else{ + console.info('\nDone!\n'); + return; + } + if (isMuxed && !options.nocleanup) + merger.cleanUp(); } - return { isOk: true, value: selEpsArr, showData: getShowData.series }; - } - public async downloadEpisode(data: parsedMultiDubDownload, options: yargs.ArgvType): Promise { - const res = await this.downloadMediaList(data, options); - if (res === undefined || res.error) { - return false; - } else { - if (!options.skipmux) { - await this.muxStreams(res.data, { ...options, output: res.fileName }); - } else { - console.info('Skipping mux'); - } - downloaded({ - service: 'ao', - type: 's' - }, data.seasonID+'', [data.episodeNumber+'']); - } - return true; - } - - public async muxStreams(data: DownloadedMedia[], options: yargs.ArgvType) { - this.cfg.bin = await yamlCfg.loadBinCfg(); - let hasAudioStreams = false; - if (options.novids || data.filter(a => a.type === 'Video').length === 0) - return console.info('Skip muxing since no vids are downloaded'); - if (data.some(a => a.type === 'Audio')) { - hasAudioStreams = true; - } - 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, - }; - }) : [], - skipSubMux: options.skipSubMux, - inverseTrackOrder: false, - keepAllVideos: options.keepAllVideos, - 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, - }; - }) : [], - 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'); - return { - file: a.path, - language: a.language, - closedCaption: a.cc - }; - }), - simul: data.filter(a => a.type === 'Video').map((a) : boolean => { - if (a.type === 'Subtitle') - throw new Error('Never'); - return !a.uncut as boolean; - })[0], - 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, - }; - }), - videoTitle: options.videoTitle, - options: { - ffmpeg: options.ffmpegOptions, - mkvmerge: options.mkvmergeOptions - }, - defaults: { - audio: options.defaultAudio, - sub: options.defaultSub - }, - ccTag: options.ccTag - }); - const bin = Merger.checkMerger(this.cfg.bin, options.mp4, options.forceMuxer); - // collect fonts info - // mergers - let isMuxed = false; - if (options.syncTiming) { - await merger.createDelays(); - } - if (bin.MKVmerge) { - await merger.merge('mkvmerge', bin.MKVmerge); - isMuxed = true; - } else if (bin.FFmpeg) { - await merger.merge('ffmpeg', bin.FFmpeg); - isMuxed = true; - } else{ - console.info('\nDone!\n'); - return; - } - if (isMuxed && !options.nocleanup) - merger.cleanUp(); - } - - public async downloadMediaList(medias: parsedMultiDubDownload, options: yargs.ArgvType) : Promise<{ + public async downloadMediaList(medias: parsedMultiDubDownload, options: yargs.ArgvType) : Promise<{ data: DownloadedMedia[], fileName: string, error: boolean } | undefined> { - if(!this.token.token){ - console.error('Authentication required!'); - return; - } - - if (!this.cfg.bin.ffmpeg) - this.cfg.bin = await yamlCfg.loadBinCfg(); - - let mediaName = '...'; - let fileName; - const variables: Variable[] = []; - if(medias.seasonTitle && medias.episodeNumber && medias.episodeTitle){ - mediaName = `${medias.seasonTitle} - ${medias.episodeNumber} - ${medias.episodeTitle}`; - } - - const files: DownloadedMedia[] = []; - - let subIndex = 0; - let dlFailed = false; - let dlVideoOnce = false; // Variable to save if best selected video quality was downloaded - - for (const media of medias.data) { - console.info(`Requesting: [E.${media.episode.ID}] ${mediaName}`); - - const AuthHeaders = { - headers: { - Authorization: `Bearer ${this.token.token}`, - 'Referer': 'https://www.animeonegai.com/', - 'Origin': 'https://www.animeonegai.com', - 'Referrer-Policy': 'strict-origin-when-cross-origin', - 'Content-Type': 'application/json' + if(!this.token.token){ + console.error('Authentication required!'); + return; } - }; - const playbackReq = await this.req.getData(`https://api.animeonegai.com/v1/media/${media.videoId}?lang=${this.locale}`, AuthHeaders); - if(!playbackReq.ok || !playbackReq.res){ - console.error('Request Stream URLs FAILED!'); - return undefined; - } - const streamData = await playbackReq.res.json() as AnimeOnegaiStream; + if (!this.cfg.bin.ffmpeg) + this.cfg.bin = await yamlCfg.loadBinCfg(); - variables.push(...([ - ['title', medias.episodeTitle, true], - ['episode', medias.episodeNumber, false], - ['service', 'AO', false], - ['seriesTitle', medias.seriesTitle, true], - ['showTitle', medias.seasonTitle, true], - ['season', medias.seasonNumber, false] - ] as [AvailableFilenameVars, string|number, boolean][]).map((a): Variable => { - return { - name: a[0], - replaceWith: a[1], - type: typeof a[1], - sanitize: a[2] - } as Variable; - })); + let mediaName = '...'; + let fileName; + const variables: Variable[] = []; + if(medias.seasonTitle && medias.episodeNumber && medias.episodeTitle){ + mediaName = `${medias.seasonTitle} - ${medias.episodeNumber} - ${medias.episodeTitle}`; + } - if (!canDecrypt) { - console.error('No valid Widevine or PlayReady CDM detected. Please ensure a supported and functional CDM is installed.'); - return undefined; - } + const files: DownloadedMedia[] = []; + + let subIndex = 0; + let dlFailed = false; + let dlVideoOnce = false; // Variable to save if best selected video quality was downloaded + + for (const media of medias.data) { + console.info(`Requesting: [E.${media.episode.ID}] ${mediaName}`); + + const AuthHeaders = { + headers: { + Authorization: `Bearer ${this.token.token}`, + 'Referer': 'https://www.animeonegai.com/', + 'Origin': 'https://www.animeonegai.com', + 'Referrer-Policy': 'strict-origin-when-cross-origin', + 'Content-Type': 'application/json' + } + }; + + const playbackReq = await this.req.getData(`https://api.animeonegai.com/v1/media/${media.videoId}?lang=${this.locale}`, AuthHeaders); + if(!playbackReq.ok || !playbackReq.res){ + console.error('Request Stream URLs FAILED!'); + return undefined; + } + const streamData = await playbackReq.res.json() as AnimeOnegaiStream; + + variables.push(...([ + ['title', medias.episodeTitle, true], + ['episode', medias.episodeNumber, false], + ['service', 'AO', false], + ['seriesTitle', medias.seriesTitle, true], + ['showTitle', medias.seasonTitle, true], + ['season', medias.seasonNumber, false] + ] as [AvailableFilenameVars, string|number, boolean][]).map((a): Variable => { + return { + name: a[0], + replaceWith: a[1], + type: typeof a[1], + sanitize: a[2] + } as Variable; + })); + + if (!canDecrypt) { + console.error('No valid Widevine or PlayReady CDM detected. Please ensure a supported and functional CDM is installed.'); + return undefined; + } - if (!this.cfg.bin.mp4decrypt && !this.cfg.bin.shaka) { - console.error('Neither Shaka nor MP4Decrypt found. Please ensure at least one of them is installed.'); - return undefined; - } - - const lang = langsData.languages.find(a=>a.ao_locale == media.lang) as langsData.LanguageItem; - if (!lang) { - console.error(`Unable to find language for code ${media.lang}`); - return; - } - let tsFile = undefined; - - if (!streamData.dash) { - console.error('You don\'t have access to download this content'); - continue; - } - - console.info('Playlists URL: %s', streamData.dash); - - if(!dlFailed && !(options.novids && options.noaudio)){ - const streamPlaylistsReq = await this.req.getData(streamData.dash, AuthHeaders); - if(!streamPlaylistsReq.ok || !streamPlaylistsReq.res){ - console.error('CAN\'T FETCH VIDEO PLAYLISTS!'); - dlFailed = true; - } else { - const streamPlaylistBody = (await streamPlaylistsReq.res.text()).replace(/(.*?)<\/BaseURL>/g, `${streamData.dash.split('/dash/')[0]}/dash/$1`); - //Parse MPD Playlists - const streamPlaylists = await parse(streamPlaylistBody, lang as langsData.LanguageItem, streamData.dash.split('/dash/')[0]+'/dash/'); - - //Get name of CDNs/Servers - const streamServers = Object.keys(streamPlaylists); - - options.x = options.x > streamServers.length ? 1 : options.x; - - const selectedServer = streamServers[options.x - 1]; - const selectedList = streamPlaylists[selectedServer]; - - //set Video Qualities - const videos = selectedList.video.map(item => { - return { - ...item, - resolutionText: `${item.quality.width}x${item.quality.height} (${Math.round(item.bandwidth/1024)}KiB/s)` - }; - }); - - const audios = selectedList.audio.map(item => { - return { - ...item, - resolutionText: `${Math.round(item.bandwidth/1024)}kB/s` - }; - }); - - videos.sort((a, b) => { - return a.quality.width - b.quality.width; - }); - - audios.sort((a, b) => { - return a.bandwidth - b.bandwidth; - }); - - let chosenVideoQuality = options.q === 0 ? videos.length : options.q; - if(chosenVideoQuality > videos.length) { - console.warn(`The requested quality of ${options.q} is greater than the maximum ${videos.length}.\n[WARN] Therefor the maximum will be capped at ${videos.length}.`); - chosenVideoQuality = videos.length; - } - chosenVideoQuality--; - - let chosenAudioQuality = options.q === 0 ? audios.length : options.q; - if(chosenAudioQuality > audios.length) { - chosenAudioQuality = audios.length; - } - chosenAudioQuality--; - - const chosenVideoSegments = videos[chosenVideoQuality]; - const chosenAudioSegments = audios[chosenAudioQuality]; - - console.info(`Servers available:\n\t${streamServers.join('\n\t')}`); - console.info(`Available Video Qualities:\n\t${videos.map((a, ind) => `[${ind+1}] ${a.resolutionText}`).join('\n\t')}`); - console.info(`Available Audio Qualities:\n\t${audios.map((a, ind) => `[${ind+1}] ${a.resolutionText}`).join('\n\t')}`); - - variables.push({ - name: 'height', - type: 'number', - replaceWith: chosenVideoSegments.quality.height - }, { - name: 'width', - type: 'number', - replaceWith: chosenVideoSegments.quality.width - }); - - console.info(`Selected quality: \n\tVideo: ${chosenVideoSegments.resolutionText}\n\tAudio: ${chosenAudioSegments.resolutionText}\n\tServer: ${selectedServer}`); - //console.info('Stream URL:', chosenVideoSegments.segments[0].uri); - // TODO check filename - fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); - const outFile = parseFileName(options.fileName + '.' + lang.name, variables, options.numbers, options.override).join(path.sep); - const tempFile = parseFileName(`temp-${media.videoId}`, variables, options.numbers, options.override).join(path.sep); - const tempTsFile = path.isAbsolute(tempFile as string) ? tempFile : path.join(this.cfg.dir.content, tempFile); - - let [audioDownloaded, videoDownloaded] = [false, false]; - - // When best selected video quality is already downloaded - if(dlVideoOnce && options.dlVideoOnce) { - console.info('Already downloaded video, skipping video download...'); - } else if (options.novids) { - console.info('Skipping video download...'); - } else { - //Download Video - const totalParts = chosenVideoSegments.segments.length; - const mathParts = Math.ceil(totalParts / options.partsize); - const mathMsg = `(${mathParts}*${options.partsize})`; - console.info('Total parts in video stream:', totalParts, mathMsg); - tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); - const dirName = path.dirname(tsFile); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }); - } - const videoJson: M3U8Json = { - segments: chosenVideoSegments.segments - }; - try { - const videoDownload = await new streamdl({ - output: chosenVideoSegments.pssh_wvd ? `${tempTsFile}.video.enc.m4s` : `${tsFile}.video.m4s`, - 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; - } else { - dlVideoOnce = true; - videoDownloaded = true; - } - } catch (e) { - console.error(e); - dlFailed = true; - } - } - - if (chosenAudioSegments && !options.noaudio) { - //Download Audio (if available) - const totalParts = chosenAudioSegments.segments.length; - const mathParts = Math.ceil(totalParts / options.partsize); - const mathMsg = `(${mathParts}*${options.partsize})`; - console.info('Total parts in audio stream:', totalParts, mathMsg); - tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); - const dirName = path.dirname(tsFile); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }); - } - const audioJson: M3U8Json = { - segments: chosenAudioSegments.segments - }; - try { - const audioDownload = await new streamdl({ - output: chosenAudioSegments.pssh_wvd ? `${tempTsFile}.audio.enc.m4s` : `${tsFile}.audio.m4s`, - 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; - } else { - audioDownloaded = true; - } - } catch (e) { - console.error(e); - dlFailed = true; - } - } else if (options.noaudio) { - console.info('Skipping audio download...'); - } - - //Handle Decryption if needed - if ((chosenVideoSegments.pssh_wvd || chosenAudioSegments.pssh_wvd) && (videoDownloaded || audioDownloaded)) { - console.info('Decryption Needed, attempting to decrypt'); - let encryptionKeys; - - if (cdm === 'widevine') { - encryptionKeys = await getKeysWVD(chosenVideoSegments.pssh_wvd, streamData.widevine_proxy, {}); - } - if (cdm === 'playready') { - encryptionKeys = await getKeysPRD(chosenVideoSegments.pssh_prd, streamData.playready_proxy, {}); + if (!this.cfg.bin.mp4decrypt && !this.cfg.bin.shaka) { + console.error('Neither Shaka nor MP4Decrypt found. Please ensure at least one of them is installed.'); + return undefined; } - if (!encryptionKeys || encryptionKeys.length == 0) { - console.error('Failed to get encryption keys'); - return undefined; + const lang = langsData.languages.find(a=>a.ao_locale == media.lang) as langsData.LanguageItem; + if (!lang) { + console.error(`Unable to find language for code ${media.lang}`); + return; } - /*const keys = {} as Record; + let tsFile = undefined; + + if (!streamData.dash) { + console.error('You don\'t have access to download this content'); + continue; + } + + console.info('Playlists URL: %s', streamData.dash); + + if(!dlFailed && !(options.novids && options.noaudio)){ + const streamPlaylistsReq = await this.req.getData(streamData.dash, AuthHeaders); + if(!streamPlaylistsReq.ok || !streamPlaylistsReq.res){ + console.error('CAN\'T FETCH VIDEO PLAYLISTS!'); + dlFailed = true; + } else { + const streamPlaylistBody = (await streamPlaylistsReq.res.text()).replace(/(.*?)<\/BaseURL>/g, `${streamData.dash.split('/dash/')[0]}/dash/$1`); + //Parse MPD Playlists + const streamPlaylists = await parse(streamPlaylistBody, lang as langsData.LanguageItem, streamData.dash.split('/dash/')[0]+'/dash/'); + + //Get name of CDNs/Servers + const streamServers = Object.keys(streamPlaylists); + + options.x = options.x > streamServers.length ? 1 : options.x; + + const selectedServer = streamServers[options.x - 1]; + const selectedList = streamPlaylists[selectedServer]; + + //set Video Qualities + const videos = selectedList.video.map(item => { + return { + ...item, + resolutionText: `${item.quality.width}x${item.quality.height} (${Math.round(item.bandwidth/1024)}KiB/s)` + }; + }); + + const audios = selectedList.audio.map(item => { + return { + ...item, + resolutionText: `${Math.round(item.bandwidth/1024)}kB/s` + }; + }); + + videos.sort((a, b) => { + return a.quality.width - b.quality.width; + }); + + audios.sort((a, b) => { + return a.bandwidth - b.bandwidth; + }); + + let chosenVideoQuality = options.q === 0 ? videos.length : options.q; + if(chosenVideoQuality > videos.length) { + console.warn(`The requested quality of ${options.q} is greater than the maximum ${videos.length}.\n[WARN] Therefor the maximum will be capped at ${videos.length}.`); + chosenVideoQuality = videos.length; + } + chosenVideoQuality--; + + let chosenAudioQuality = options.q === 0 ? audios.length : options.q; + if(chosenAudioQuality > audios.length) { + chosenAudioQuality = audios.length; + } + chosenAudioQuality--; + + const chosenVideoSegments = videos[chosenVideoQuality]; + const chosenAudioSegments = audios[chosenAudioQuality]; + + console.info(`Servers available:\n\t${streamServers.join('\n\t')}`); + console.info(`Available Video Qualities:\n\t${videos.map((a, ind) => `[${ind+1}] ${a.resolutionText}`).join('\n\t')}`); + console.info(`Available Audio Qualities:\n\t${audios.map((a, ind) => `[${ind+1}] ${a.resolutionText}`).join('\n\t')}`); + + variables.push({ + name: 'height', + type: 'number', + replaceWith: chosenVideoSegments.quality.height + }, { + name: 'width', + type: 'number', + replaceWith: chosenVideoSegments.quality.width + }); + + console.info(`Selected quality: \n\tVideo: ${chosenVideoSegments.resolutionText}\n\tAudio: ${chosenAudioSegments.resolutionText}\n\tServer: ${selectedServer}`); + //console.info('Stream URL:', chosenVideoSegments.segments[0].uri); + // TODO check filename + fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); + const outFile = parseFileName(options.fileName + '.' + lang.name, variables, options.numbers, options.override).join(path.sep); + const tempFile = parseFileName(`temp-${media.videoId}`, variables, options.numbers, options.override).join(path.sep); + const tempTsFile = path.isAbsolute(tempFile as string) ? tempFile : path.join(this.cfg.dir.content, tempFile); + + let [audioDownloaded, videoDownloaded] = [false, false]; + + // When best selected video quality is already downloaded + if(dlVideoOnce && options.dlVideoOnce) { + console.info('Already downloaded video, skipping video download...'); + } else if (options.novids) { + console.info('Skipping video download...'); + } else { + //Download Video + const totalParts = chosenVideoSegments.segments.length; + const mathParts = Math.ceil(totalParts / options.partsize); + const mathMsg = `(${mathParts}*${options.partsize})`; + console.info('Total parts in video stream:', totalParts, mathMsg); + tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); + const dirName = path.dirname(tsFile); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + const videoJson: M3U8Json = { + segments: chosenVideoSegments.segments + }; + try { + const videoDownload = await new streamdl({ + output: chosenVideoSegments.pssh_wvd ? `${tempTsFile}.video.enc.m4s` : `${tsFile}.video.m4s`, + 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; + } else { + dlVideoOnce = true; + videoDownloaded = true; + } + } catch (e) { + console.error(e); + dlFailed = true; + } + } + + if (chosenAudioSegments && !options.noaudio) { + //Download Audio (if available) + const totalParts = chosenAudioSegments.segments.length; + const mathParts = Math.ceil(totalParts / options.partsize); + const mathMsg = `(${mathParts}*${options.partsize})`; + console.info('Total parts in audio stream:', totalParts, mathMsg); + tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); + const dirName = path.dirname(tsFile); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + const audioJson: M3U8Json = { + segments: chosenAudioSegments.segments + }; + try { + const audioDownload = await new streamdl({ + output: chosenAudioSegments.pssh_wvd ? `${tempTsFile}.audio.enc.m4s` : `${tsFile}.audio.m4s`, + 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; + } else { + audioDownloaded = true; + } + } catch (e) { + console.error(e); + dlFailed = true; + } + } else if (options.noaudio) { + console.info('Skipping audio download...'); + } + + //Handle Decryption if needed + if ((chosenVideoSegments.pssh_wvd || chosenAudioSegments.pssh_wvd) && (videoDownloaded || audioDownloaded)) { + console.info('Decryption Needed, attempting to decrypt'); + let encryptionKeys; + + if (cdm === 'widevine') { + encryptionKeys = await getKeysWVD(chosenVideoSegments.pssh_wvd, streamData.widevine_proxy, {}); + } + if (cdm === 'playready') { + encryptionKeys = await getKeysPRD(chosenVideoSegments.pssh_prd, streamData.playready_proxy, {}); + } + + if (!encryptionKeys || encryptionKeys.length == 0) { + console.error('Failed to get encryption keys'); + return undefined; + } + /*const keys = {} as Record; encryptionKeys.forEach(function(key) { keys[key.kid] = key.key; });*/ - if (this.cfg.bin.mp4decrypt || this.cfg.bin.shaka) { - let commandBase = `--show-progress --key ${encryptionKeys[cdm === 'playready' ? 0 : 1].kid}:${encryptionKeys[cdm === 'playready' ? 0 : 1].key} `; - let commandVideo = commandBase+`"${tempTsFile}.video.enc.m4s" "${tempTsFile}.video.m4s"`; - let commandAudio = commandBase+`"${tempTsFile}.audio.enc.m4s" "${tempTsFile}.audio.m4s"`; + if (this.cfg.bin.mp4decrypt || this.cfg.bin.shaka) { + let commandBase = `--show-progress --key ${encryptionKeys[cdm === 'playready' ? 0 : 1].kid}:${encryptionKeys[cdm === 'playready' ? 0 : 1].key} `; + let commandVideo = commandBase+`"${tempTsFile}.video.enc.m4s" "${tempTsFile}.video.m4s"`; + let commandAudio = commandBase+`"${tempTsFile}.audio.enc.m4s" "${tempTsFile}.audio.m4s"`; - if (this.cfg.bin.shaka) { - commandBase = ` --enable_raw_key_decryption ${encryptionKeys.map(kb => '--keys key_id='+kb.kid+':key='+kb.key).join(' ')}`; - commandVideo = `input="${tempTsFile}.video.enc.m4s",stream=video,output="${tempTsFile}.video.m4s"`+commandBase; - commandAudio = `input="${tempTsFile}.audio.enc.m4s",stream=audio,output="${tempTsFile}.audio.m4s"`+commandBase; - } + if (this.cfg.bin.shaka) { + commandBase = ` --enable_raw_key_decryption ${encryptionKeys.map(kb => '--keys key_id='+kb.kid+':key='+kb.key).join(' ')}`; + commandVideo = `input="${tempTsFile}.video.enc.m4s",stream=video,output="${tempTsFile}.video.m4s"`+commandBase; + commandAudio = `input="${tempTsFile}.audio.enc.m4s",stream=audio,output="${tempTsFile}.audio.m4s"`+commandBase; + } - if (videoDownloaded) { - console.info('Started decrypting video,', this.cfg.bin.shaka ? 'using shaka' : 'using mp4decrypt'); - const decryptVideo = Helper.exec(this.cfg.bin.shaka ? 'shaka-packager' : 'mp4decrypt', this.cfg.bin.shaka ? `"${this.cfg.bin.shaka}"` : `"${this.cfg.bin.mp4decrypt}"`, commandVideo); - if (!decryptVideo.isOk) { - console.error(decryptVideo.err); - console.error(`Decryption failed with exit code ${decryptVideo.err.code}`); - fs.renameSync(`${tempTsFile}.video.enc.m4s`, `${tsFile}.video.enc.m4s`); - return undefined; - } else { - console.info('Decryption done for video'); - if (!options.nocleanup) { - fs.removeSync(`${tempTsFile}.video.enc.m4s`); - } - fs.copyFileSync(`${tempTsFile}.video.m4s`, `${tsFile}.video.m4s`); - fs.unlinkSync(`${tempTsFile}.video.m4s`); - files.push({ - type: 'Video', - path: `${tsFile}.video.m4s`, - lang: lang - }); + if (videoDownloaded) { + console.info('Started decrypting video,', this.cfg.bin.shaka ? 'using shaka' : 'using mp4decrypt'); + const decryptVideo = Helper.exec(this.cfg.bin.shaka ? 'shaka-packager' : 'mp4decrypt', this.cfg.bin.shaka ? `"${this.cfg.bin.shaka}"` : `"${this.cfg.bin.mp4decrypt}"`, commandVideo); + if (!decryptVideo.isOk) { + console.error(decryptVideo.err); + console.error(`Decryption failed with exit code ${decryptVideo.err.code}`); + fs.renameSync(`${tempTsFile}.video.enc.m4s`, `${tsFile}.video.enc.m4s`); + return undefined; + } else { + console.info('Decryption done for video'); + if (!options.nocleanup) { + fs.removeSync(`${tempTsFile}.video.enc.m4s`); + } + fs.copyFileSync(`${tempTsFile}.video.m4s`, `${tsFile}.video.m4s`); + fs.unlinkSync(`${tempTsFile}.video.m4s`); + files.push({ + type: 'Video', + path: `${tsFile}.video.m4s`, + lang: lang + }); + } + } + + if (audioDownloaded) { + console.info('Started decrypting audio,', this.cfg.bin.shaka ? 'using shaka' : 'using mp4decrypt'); + const decryptAudio = Helper.exec(this.cfg.bin.shaka ? 'shaka-packager' : 'mp4decrypt', this.cfg.bin.shaka ? `"${this.cfg.bin.shaka}"` : `"${this.cfg.bin.mp4decrypt}"`, commandAudio); + if (!decryptAudio.isOk) { + console.error(decryptAudio.err); + console.error(`Decryption failed with exit code ${decryptAudio.err.code}`); + fs.renameSync(`${tempTsFile}.audio.enc.m4s`, `${tsFile}.audio.enc.m4s`); + return undefined; + } else { + if (!options.nocleanup) { + fs.removeSync(`${tempTsFile}.audio.enc.m4s`); + } + fs.copyFileSync(`${tempTsFile}.audio.m4s`, `${tsFile}.audio.m4s`); + fs.unlinkSync(`${tempTsFile}.audio.m4s`); + files.push({ + type: 'Audio', + path: `${tsFile}.audio.m4s`, + lang: lang + }); + console.info('Decryption done for audio'); + } + } + } else { + console.warn('mp4decrypt/shaka not found, files need decryption. Decryption Keys:', encryptionKeys); + } + } else { + if (videoDownloaded) { + files.push({ + type: 'Video', + path: `${tsFile}.video.m4s`, + lang: lang + }); + } + if (audioDownloaded) { + files.push({ + type: 'Audio', + path: `${tsFile}.audio.m4s`, + lang: lang + }); + } + } } - } + } else if (options.novids && options.noaudio) { + fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); + } - if (audioDownloaded) { - console.info('Started decrypting audio,', this.cfg.bin.shaka ? 'using shaka' : 'using mp4decrypt'); - const decryptAudio = Helper.exec(this.cfg.bin.shaka ? 'shaka-packager' : 'mp4decrypt', this.cfg.bin.shaka ? `"${this.cfg.bin.shaka}"` : `"${this.cfg.bin.mp4decrypt}"`, commandAudio); - if (!decryptAudio.isOk) { - console.error(decryptAudio.err); - console.error(`Decryption failed with exit code ${decryptAudio.err.code}`); - fs.renameSync(`${tempTsFile}.audio.enc.m4s`, `${tsFile}.audio.enc.m4s`); - return undefined; - } else { - if (!options.nocleanup) { - fs.removeSync(`${tempTsFile}.audio.enc.m4s`); - } - fs.copyFileSync(`${tempTsFile}.audio.m4s`, `${tsFile}.audio.m4s`); - fs.unlinkSync(`${tempTsFile}.audio.m4s`); - files.push({ - type: 'Audio', - path: `${tsFile}.audio.m4s`, - lang: lang - }); - console.info('Decryption done for audio'); + 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(streamData.subtitles.length > 0) { + for(const sub of streamData.subtitles) { + const subLang = langsData.languages.find(a => a.ao_locale === sub.lang); + if (!subLang) { + console.warn(`Language not found for subtitle language: ${sub.lang}, Skipping`); + continue; + } + const sxData: Partial = {}; + sxData.file = langsData.subsFile(fileName as string, subIndex+'', subLang, false, options.ccTag); + if (path.isAbsolute(sxData.file)) { + sxData.path = sxData.file; + } else { + sxData.path = path.join(this.cfg.dir.content, sxData.file); + } + const dirName = path.dirname(sxData.path); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + sxData.language = subLang; + if((options.dlsubs.includes('all') || options.dlsubs.includes(subLang.locale)) && sub.url.includes('.ass')) { + const getSubtitle = await this.req.getData(sub.url, AuthHeaders); + if (getSubtitle.ok && getSubtitle.res) { + console.info(`Subtitle Downloaded: ${sub.url}`); + const sBody = await getSubtitle.res.text(); + sxData.title = `${subLang.language}`; + sxData.fonts = fontsData.assFonts(sBody) as Font[]; + fs.writeFileSync(sxData.path, sBody); + files.push({ + type: 'Subtitle', + ...sxData as sxItem, + cc: false + }); + } else{ + console.warn(`Failed to download subtitle: ${sxData.file}`); + } + } + subIndex++; + } + } else{ + console.warn('Can\'t find urls for subtitles!'); } - } - } else { - console.warn('mp4decrypt/shaka not found, files need decryption. Decryption Keys:', encryptionKeys); } - } else { - if (videoDownloaded) { - files.push({ - type: 'Video', - path: `${tsFile}.video.m4s`, - lang: lang - }); + else{ + console.info('Subtitles downloading skipped!'); } - if (audioDownloaded) { - files.push({ - type: 'Audio', - path: `${tsFile}.audio.m4s`, - lang: lang - }); - } - } + await this.sleep(options.waittime); } - } else if (options.novids && options.noaudio) { - fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); - } - - 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(streamData.subtitles.length > 0) { - for(const sub of streamData.subtitles) { - const subLang = langsData.languages.find(a => a.ao_locale === sub.lang); - if (!subLang) { - console.warn(`Language not found for subtitle language: ${sub.lang}, Skipping`); - continue; - } - const sxData: Partial = {}; - sxData.file = langsData.subsFile(fileName as string, subIndex+'', subLang, false, options.ccTag); - if (path.isAbsolute(sxData.file)) { - sxData.path = sxData.file; - } else { - sxData.path = path.join(this.cfg.dir.content, sxData.file); - } - const dirName = path.dirname(sxData.path); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }); - } - sxData.language = subLang; - if((options.dlsubs.includes('all') || options.dlsubs.includes(subLang.locale)) && sub.url.includes('.ass')) { - const getSubtitle = await this.req.getData(sub.url, AuthHeaders); - if (getSubtitle.ok && getSubtitle.res) { - console.info(`Subtitle Downloaded: ${sub.url}`); - const sBody = await getSubtitle.res.text(); - sxData.title = `${subLang.language}`; - sxData.fonts = fontsData.assFonts(sBody) as Font[]; - fs.writeFileSync(sxData.path, sBody); - files.push({ - type: 'Subtitle', - ...sxData as sxItem, - cc: false - }); - } else{ - console.warn(`Failed to download subtitle: ${sxData.file}`); - } - } - subIndex++; - } - } else{ - console.warn('Can\'t find urls for subtitles!'); - } - } - else{ - console.info('Subtitles downloading skipped!'); - } - await this.sleep(options.waittime); + return { + error: dlFailed, + data: files, + fileName: fileName ? (path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName)) || './unknown' : './unknown' + }; } - return { - error: dlFailed, - data: files, - fileName: fileName ? (path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName)) || './unknown' : './unknown' - }; - } - public sleep(ms: number) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); - } + public sleep(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + } } diff --git a/crunchy.ts b/crunchy.ts index 5ff4abf..1d66757 100644 --- a/crunchy.ts +++ b/crunchy.ts @@ -55,2288 +55,2288 @@ export type sxItem = { } export default class Crunchy implements ServiceClass { - public cfg: yamlCfg.ConfigObject; - public locale: string; - private token: Record; - private req: reqModule.Req; - private cmsToken: { + public cfg: yamlCfg.ConfigObject; + public locale: string; + private token: Record; + private req: reqModule.Req; + private cmsToken: { cms?: Record, cms_beta?: Record, cms_web?: Record } = {}; - constructor(private debug = false) { - this.cfg = yamlCfg.loadCfg(); - this.token = yamlCfg.loadCRToken(); - this.req = new reqModule.Req(domain, debug, false, 'cr'); - this.locale = 'en-US'; - } + constructor(private debug = false) { + this.cfg = yamlCfg.loadCfg(); + this.token = yamlCfg.loadCRToken(); + this.req = new reqModule.Req(domain, debug, false, 'cr'); + this.locale = 'en-US'; + } - public checkToken(): boolean { - return Object.keys(this.cmsToken.cms_web ?? {}).length > 0; - } + public checkToken(): boolean { + return Object.keys(this.cmsToken.cms_web ?? {}).length > 0; + } - public async cli() { - console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`); - const argv = yargs.appArgv(this.cfg.cli); - this.locale = argv.locale; - if (argv.debug) - this.debug = true; + public async cli() { + console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`); + const argv = yargs.appArgv(this.cfg.cli); + this.locale = argv.locale; + if (argv.debug) + this.debug = true; - // load binaries - this.cfg.bin = await yamlCfg.loadBinCfg(); - if (argv.allDubs) { - argv.dubLang = langsData.dubLanguageCodes; - } - // select mode - if (argv.silentAuth && !argv.auth) { - await this.doAuth({ - username: argv.username ?? await Helper.question('[Q] LOGIN/EMAIL: '), - password: argv.password ?? await Helper.question('[Q] PASSWORD: ') - }); - } - if(argv.dlFonts){ - await this.getFonts(); - } - else if(argv.auth){ - await this.doAuth({ - username: argv.username ?? await Helper.question('[Q] LOGIN/EMAIL: '), - password: argv.password ?? await Helper.question('[Q] PASSWORD: ') - }); - } - else if (argv.token) { - await this.loginWithToken(argv.token); - } - else if(argv.cmsindex){ - await this.refreshToken(); - await this.getCmsData(); - } - else if(argv.new){ - await this.refreshToken(); - await this.getNewlyAdded(argv.page, argv['search-type'], argv.raw, argv.rawoutput); - } - else if(argv.search && argv.search.length > 2){ - await this.refreshToken(); - await this.doSearch({ ...argv, search: argv.search as string }); - } - else if(argv.series && argv.series.match(/^[0-9A-Z]{9}$/)){ - await this.refreshToken(); - await this.logSeriesById(argv.series as string); - const selected = await this.downloadFromSeriesID(argv.series, { ...argv }); - if (selected.isOk) { - for (const select of selected.value) { - if (!(await this.downloadEpisode(select, {...argv, skipsubs: false}, true))) { - console.error(`Unable to download selected episode ${select.episodeNumber}`); - return false; - } + // load binaries + this.cfg.bin = await yamlCfg.loadBinCfg(); + if (argv.allDubs) { + argv.dubLang = langsData.dubLanguageCodes; } - } - return true; - } - else if(argv['movie-listing'] && argv['movie-listing'].match(/^[0-9A-Z]{9}$/)){ - await this.refreshToken(); - await this.logMovieListingById(argv['movie-listing'] as string); - } - else if(argv['show-raw'] && argv['show-raw'].match(/^[0-9A-Z]{9}$/)){ - await this.refreshToken(); - await this.logShowRawById(argv['show-raw'] as string); - } - else if(argv['season-raw'] && argv['season-raw'].match(/^[0-9A-Z]{9}$/)){ - await this.refreshToken(); - await this.logSeasonRawById(argv['season-raw'] as string); - } - else if(argv['show-list-raw']){ - await this.refreshToken(); - await this.logShowListRaw(); - } - else if(argv.s && argv.s.match(/^[0-9A-Z]{9}$/)){ - await this.refreshToken(); - if (argv.dubLang.length > 1) { - console.info('One show can only be downloaded with one dub. Use --srz instead.'); - } - argv.dubLang = [argv.dubLang[0]]; - const selected = await this.getSeasonById(argv.s, argv.numbers, argv.e, argv.but, argv.all); - if (selected.isOk) { - for (const select of selected.value) { - if (!(await this.downloadEpisode(select, {...argv, skipsubs: false }))) { - console.error(`Unable to download selected episode ${select.episodeNumber}`); - return false; - } + // select mode + if (argv.silentAuth && !argv.auth) { + await this.doAuth({ + username: argv.username ?? await Helper.question('[Q] LOGIN/EMAIL: '), + password: argv.password ?? await Helper.question('[Q] PASSWORD: ') + }); } - } - return true; - } - else if(argv.e){ - await this.refreshToken(); - if (argv.dubLang.length > 1) { - console.info('One show can only be downloaded with one dub. Use --srz instead.'); - } - argv.dubLang = [argv.dubLang[0]]; - const selected = await this.getObjectById(argv.e, false); - 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; + if(argv.dlFonts){ + await this.getFonts(); } - } - return true; - } else if (argv.extid) { - await this.refreshToken(); - if (argv.dubLang.length > 1) { - console.info('One show can only be downloaded with one dub. Use --srz instead.'); - } - argv.dubLang = [argv.dubLang[0]]; - 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; + else if(argv.auth){ + await this.doAuth({ + username: argv.username ?? await Helper.question('[Q] LOGIN/EMAIL: '), + password: argv.password ?? await Helper.question('[Q] PASSWORD: ') + }); } - } - return true; - } - else{ - console.info('No option selected or invalid value entered. Try --help.'); - } - } - - public async logShowRawById(id: string){ - // check token - if(!this.cmsToken.cms_web){ - console.error('Authentication required!'); - return; - } - // opts - const AuthHeaders = { - headers: { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader - }, - useProxy: true - }; - // seasons list - const seriesSeasonListReq = await this.req.getData(`${api.content_cms}/series/${id}/seasons?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); - if(!seriesSeasonListReq.ok || !seriesSeasonListReq.res){ - console.error('Series Request FAILED!'); - return; - } - const seriesData = await seriesSeasonListReq.res.json(); - for (const item of seriesData.data) { - // stringify each object, then a newline - console.log(JSON.stringify(item)); - } - return seriesData.data; - } - - - public async logSeasonRawById(id: string){ - // check token - if(!this.cmsToken.cms_web){ - console.error('Authentication required!'); - return; - } - // opts - const AuthHeaders = { - headers: { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader - }, - useProxy: true - }; - // seasons list - let episodeList = { total: 0, data: [], meta: {} } as CrunchyEpisodeList; - //get episode info - const reqEpsListOpts = [ - api.cms_bucket, - this.cmsToken.cms_web.bucket, - '/episodes?', - new URLSearchParams({ - 'force_locale': '', - 'preferred_audio_language': 'ja-JP', - 'locale': this.locale, - 'season_id': id, - 'Policy': this.cmsToken.cms_web.policy, - 'Signature': this.cmsToken.cms_web.signature, - 'Key-Pair-Id': this.cmsToken.cms_web.key_pair_id, - }), - ].join(''); - const reqEpsList = await this.req.getData(reqEpsListOpts, AuthHeaders); - if(!reqEpsList.ok || !reqEpsList.res){ - console.error('Episode List Request FAILED!'); - return { isOk: false, reason: new Error('Episode List request failed. No more information provided.') }; - } - //CrunchyEpisodeList - const episodeListAndroid = await reqEpsList.res.json() as CrunchyAndroidEpisodes; - episodeList = { - total: episodeListAndroid.total, - data: episodeListAndroid.items, - meta: {} - }; - for (const item of episodeList.data) { - // stringify each object, then a newline - console.log(JSON.stringify(item)); - } - - // Return the data directly if this function is called by other code - return episodeList.data; - } - - public async logShowListRaw() { - // check token - if(!this.cmsToken.cms_web){ - console.error('Authentication required!'); - return; - } - - // opts - const AuthHeaders = { - headers: { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader - }, - useProxy: true - }; - - const allShows: any[] = []; - let page = 1; - let hasMorePages = true; - - if(this.debug){ - console.info('Retrieving complete show list...'); - } - - while (hasMorePages) { - const searchStart = (page - 1) * 50; - const params = new URLSearchParams({ - 'preferred_audio_language': 'ja-JP', - 'locale': this.locale, - 'ratings': 'true', - 'sort_by': 'alphabetical', - 'n': '50', - 'start': searchStart.toString() - }).toString(); - - const showListReq = await this.req.getData(`${api.browse_all_series}?${params}`, AuthHeaders); - - if (!showListReq.ok || !showListReq.res) { - console.error(`Show List Request FAILED on page ${page}!`); - return allShows; - } - - const showListData = await showListReq.res.json(); - - // Add current page data - for (const item of showListData.data) { - // stringify each object, then a newline - console.log(JSON.stringify(item)); - allShows.push(item); - } - - // Calculate pagination info - const totalItems = showListData.total; - const totalPages = Math.ceil(totalItems / 50); - if(this.debug){ - console.info(`Retrieved page ${page}/${totalPages} (${allShows.length}/${totalItems} items)`); - } - - // Check if we need to fetch more pages - if (page >= totalPages) { - hasMorePages = false; - } else { - page++; - // Add a small delay to avoid rate limiting - await this.sleep(1000); - } - } - - if(this.debug){ - console.info(`Complete show list retrieved: ${allShows.length} items`); - } - return allShows; - } - - public async getFonts() { - console.info('Downloading fonts...'); - const fonts = Object.values(fontsData.fontFamilies).reduce((pre, curr) => pre.concat(curr)); - for(const f of fonts) { - const fontLoc = path.join(this.cfg.dir.fonts, f); - if(fs.existsSync(fontLoc) && fs.statSync(fontLoc).size != 0){ - console.info(`${f} already downloaded!`); - } - else{ - const fontFolder = path.dirname(fontLoc); - if(fs.existsSync(fontLoc) && fs.statSync(fontLoc).size == 0){ - fs.unlinkSync(fontLoc); + else if (argv.token) { + await this.loginWithToken(argv.token); } - try{ - fs.ensureDirSync(fontFolder); + else if(argv.cmsindex){ + await this.refreshToken(); + await this.getCmsData(); } - catch(e){ - console.info(''); + else if(argv.new){ + await this.refreshToken(); + await this.getNewlyAdded(argv.page, argv['search-type'], argv.raw, argv.rawoutput); } - const fontUrl = fontsData.root + f; - const getFont = await this.req.getData(fontUrl, { - headers: api.crunchyDefHeader - }); - if(getFont.ok && getFont.res){ - fs.writeFileSync(fontLoc, Buffer.from(await getFont.res.arrayBuffer())); - console.info(`Downloaded: ${f}`); + else if(argv.search && argv.search.length > 2){ + await this.refreshToken(); + await this.doSearch({ ...argv, search: argv.search as string }); + } + else if(argv.series && argv.series.match(/^[0-9A-Z]{9}$/)){ + await this.refreshToken(); + await this.logSeriesById(argv.series as string); + const selected = await this.downloadFromSeriesID(argv.series, { ...argv }); + if (selected.isOk) { + for (const select of selected.value) { + if (!(await this.downloadEpisode(select, {...argv, skipsubs: false}, true))) { + console.error(`Unable to download selected episode ${select.episodeNumber}`); + return false; + } + } + } + return true; + } + else if(argv['movie-listing'] && argv['movie-listing'].match(/^[0-9A-Z]{9}$/)){ + await this.refreshToken(); + await this.logMovieListingById(argv['movie-listing'] as string); + } + else if(argv['show-raw'] && argv['show-raw'].match(/^[0-9A-Z]{9}$/)){ + await this.refreshToken(); + await this.logShowRawById(argv['show-raw'] as string); + } + else if(argv['season-raw'] && argv['season-raw'].match(/^[0-9A-Z]{9}$/)){ + await this.refreshToken(); + await this.logSeasonRawById(argv['season-raw'] as string); + } + else if(argv['show-list-raw']){ + await this.refreshToken(); + await this.logShowListRaw(); + } + else if(argv.s && argv.s.match(/^[0-9A-Z]{9}$/)){ + await this.refreshToken(); + if (argv.dubLang.length > 1) { + console.info('One show can only be downloaded with one dub. Use --srz instead.'); + } + argv.dubLang = [argv.dubLang[0]]; + const selected = await this.getSeasonById(argv.s, argv.numbers, argv.e, argv.but, argv.all); + if (selected.isOk) { + for (const select of selected.value) { + if (!(await this.downloadEpisode(select, {...argv, skipsubs: false }))) { + console.error(`Unable to download selected episode ${select.episodeNumber}`); + return false; + } + } + } + return true; + } + else if(argv.e){ + await this.refreshToken(); + if (argv.dubLang.length > 1) { + console.info('One show can only be downloaded with one dub. Use --srz instead.'); + } + argv.dubLang = [argv.dubLang[0]]; + const selected = await this.getObjectById(argv.e, false); + 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 if (argv.extid) { + await this.refreshToken(); + if (argv.dubLang.length > 1) { + console.info('One show can only be downloaded with one dub. Use --srz instead.'); + } + argv.dubLang = [argv.dubLang[0]]; + 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.warn(`Failed to download: ${f}`); + console.info('No option selected or invalid value entered. Try --help.'); } - } - } - console.info('All required fonts downloaded!'); - } - - // private async productionToken() { - // const tokenReq = await this.req.getData(api.bundlejs); - - // if (!tokenReq.ok || !tokenReq.res) { - // console.error('Failed to get Production Token!'); - // return { isOk: false, reason: new Error('Failed to get Production Token') }; - // } - - // const rawjs = await tokenReq.res.text(); - - // const tokens = rawjs.match(/prod="([\w-]+:[\w-]+)"/); - - // if (!tokens) { - // console.error('Failed to find Production Token in js!'); - // return { isOk: false, reason: new Error('Failed to find Production Token in js') }; - // } - - // return Buffer.from(tokens[1], 'latin1').toString('base64'); - // } - - public async doAuth(data: AuthData): Promise { - const uuid = randomUUID(); - const authData = new URLSearchParams({ - 'username': data.username, - 'password': data.password, - 'grant_type': 'password', - 'scope': 'offline_access', - 'device_id': uuid, - 'device_name': 'iPhone', - 'device_type': 'iPhone 13' - }).toString(); - const authReqOpts: reqModule.Params = { - method: 'POST', - headers: { ...api.crunchyAuthHeader, 'ETP-Anonymous-ID': uuid }, - body: authData - }; - const authReq = await this.req.getData(api.auth, authReqOpts); - if(!authReq.ok || !authReq.res){ - console.error('Authentication failed!'); - return { isOk: false, reason: new Error('Authentication failed') }; - } - // To prevent any Cloudflare errors in the future - if (authReq.res.headers.get('Set-Cookie')) { - api.crunchyDefHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; - api.crunchyAuthHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; - } - if (authReq.headers && authReq.headers['Set-Cookie']) { - api.crunchyDefHeader['Cookie'] = authReq.headers['Set-Cookie']; - api.crunchyAuthHeader['Cookie'] = authReq.headers['Set-Cookie']; - api.crunchyDefHeader['User-Agent'] = authReq.headers['User-Agent']; - api.crunchyAuthHeader['User-Agent'] = authReq.headers['User-Agent']; - } - 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(); - console.info('Your Country: %s', this.token.country); - return { isOk: true, value: undefined }; - } - - public async doAnonymousAuth(){ - const uuid = randomUUID(); - const authData = new URLSearchParams({ - 'grant_type': 'client_id', - 'scope': 'offline_access', - 'device_id': uuid, - 'device_name': 'iPhone', - 'device_type': 'iPhone 13' - }).toString(); - const authReqOpts: reqModule.Params = { - method: 'POST', - headers: { ...api.crunchyAuthHeader, 'ETP-Anonymous-ID': uuid }, - body: authData - }; - const authReq = await this.req.getData(api.auth, authReqOpts); - if(!authReq.ok || !authReq.res){ - console.error('Anonymous Authentication failed!'); - return; - } - // To prevent any Cloudflare errors in the future - if (authReq.res.headers.get('Set-Cookie')) { - api.crunchyDefHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; - api.crunchyAuthHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; - } - if (authReq.headers && authReq.headers['Set-Cookie']) { - api.crunchyDefHeader['Cookie'] = authReq.headers['Set-Cookie']; - api.crunchyAuthHeader['Cookie'] = authReq.headers['Set-Cookie']; - api.crunchyDefHeader['User-Agent'] = authReq.headers['User-Agent']; - api.crunchyAuthHeader['User-Agent'] = authReq.headers['User-Agent']; - } - 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); - } - - public async getProfile(silent = false) : Promise { - if(!this.token.access_token){ - console.error('No access token!'); - return false; - } - const profileReqOptions = { - headers: { - ...api.crunchyDefHeader, - Authorization: `Bearer ${this.token.access_token}` - }, - useProxy: true - }; - const profileReq = await this.req.getData(api.profile, profileReqOptions); - if(!profileReq.ok || !profileReq.res){ - console.error('Get profile failed!'); - return false; - } - const profile = await profileReq.res.json(); - 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 loginWithToken(refreshToken: string) { - const uuid = 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_name': 'iPhone', - 'device_type': 'iPhone 13' - }).toString(); - const authReqOpts: reqModule.Params = { - method: 'POST', - headers: {...api.crunchyAuthHeader, 'ETP-Anonymous-ID': uuid, Cookie: `etp_rt=${refreshToken}`}, - body: authData - }; - const authReq = await this.req.getData(api.auth, authReqOpts); - if(!authReq.ok || !authReq.res){ - console.error('Token Authentication failed!'); - if (authReq.res?.status == 400) { - console.warn('Token is likely wrong (Or invalid for given API), please login again!'); - } - return; - } - // To prevent any Cloudflare errors in the future - if (authReq.res.headers.get('Set-Cookie')) { - api.crunchyDefHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; - api.crunchyAuthHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; - } - if (authReq.headers && authReq.headers['Set-Cookie']) { - api.crunchyDefHeader['Cookie'] = authReq.headers['Set-Cookie']; - api.crunchyAuthHeader['Cookie'] = authReq.headers['Set-Cookie']; - api.crunchyDefHeader['User-Agent'] = authReq.headers['User-Agent']; - api.crunchyAuthHeader['User-Agent'] = authReq.headers['User-Agent']; - } - 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); - await this.getCMStoken(true); - } - - 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(); - } - else{ - /*if (ifNeeded) - return;*/ - if (!(Date.now() > new Date(this.token.expires).getTime()) && ifNeeded) { - return; - } 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', - 'device_id': uuid, - 'device_name': 'iPhone', - 'device_type': 'iPhone 13' - }).toString(); - const authReqOpts: reqModule.Params = { - method: 'POST', - headers: { ...api.crunchyAuthHeader, 'ETP-Anonymous-ID': uuid }, - body: authData - }; - const authReq = await this.req.getData(api.auth, authReqOpts); - if(!authReq.ok || !authReq.res){ - console.error('Token Refresh Failed!'); - if (authReq.res?.status == 400) { - console.warn('Token is likely wrong, please login again!'); - } - return; - } - // To prevent any Cloudflare errors in the future - if (authReq.res.headers.get('Set-Cookie')) { - api.crunchyDefHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; - api.crunchyAuthHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; - } - if (authReq.headers && authReq.headers['Set-Cookie']) { - api.crunchyDefHeader['Cookie'] = authReq.headers['Set-Cookie']; - api.crunchyAuthHeader['Cookie'] = authReq.headers['Set-Cookie']; - api.crunchyDefHeader['User-Agent'] = authReq.headers['User-Agent']; - api.crunchyAuthHeader['User-Agent'] = authReq.headers['User-Agent']; - } - 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); - } - if(this.token.refresh_token) { - await this.getProfile(silent); - } else { - console.info('USER: Anonymous'); - } - await this.getCMStoken(ifNeeded); - } - - public async getCMStoken(ifNeeded = false) { - if(!this.token.access_token){ - console.error('No access token!'); - return; } - if (ifNeeded && this.cmsToken.cms_web) { - if (!(Date.now() >= new Date(this.cmsToken.cms_web.expires).getTime())) { - return; - } - } - - const cmsTokenReqOpts = { - headers: { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader - }, - useProxy: true - }; - const cmsTokenReq = await this.req.getData(api.cms_auth, cmsTokenReqOpts); - if(!cmsTokenReq.ok || !cmsTokenReq.res){ - console.error('Authentication CMS token failed!'); - return; - } - this.cmsToken = await cmsTokenReq.res.json(); - console.info('Your Country: %s\n', this.cmsToken.cms_web?.bucket.split('/')[1]); - } - - public async getCmsData(){ + public async logShowRawById(id: string){ // check token - if(!this.cmsToken.cms_web){ - console.error('Authentication required!'); - return; - } - // opts - const indexReqOpts = [ - api.cms_bucket, - this.cmsToken.cms_web.bucket, - '/index?', - new URLSearchParams({ - 'force_locale': '', - 'preferred_audio_language': 'ja-JP', - 'locale': this.locale, - 'Policy': this.cmsToken.cms_web.policy, - 'Signature': this.cmsToken.cms_web.signature, - 'Key-Pair-Id': this.cmsToken.cms_web.key_pair_id, - }), - ].join(''); - const indexReq = await this.req.getData(indexReqOpts, { - headers: { - 'User-Agent': api.crunchyDefUserAgent - } - }); - if(!indexReq.ok || ! indexReq.res){ - console.error('Get CMS index FAILED!'); - return; - } - console.info(await indexReq.res.json()); - } - - public async doSearch(data: SearchData): Promise{ - if(!this.token.access_token){ - console.error('Authentication required!'); - return { isOk: false, reason: new Error('Not authenticated') }; - } - const searchReqOpts = { - headers: { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader - }, - useProxy: true - }; - const searchStart = data.page ? (data.page-1)*5 : 0; - const searchParams = new URLSearchParams({ - q: data.search, - n: '5', - start: data.page ? `${(data.page-1)*5}` : '0', - type: data['search-type'] ?? getDefault('search-type', this.cfg.cli), - locale: this.locale, - }).toString(); - const searchReq = await this.req.getData(`${api.search}?${searchParams}`, searchReqOpts); - if(!searchReq.ok || ! searchReq.res){ - console.error('Search FAILED!'); - return { isOk: false, reason: new Error('Search failed. No more information provided') }; - } - const searchResults = await searchReq.res.json() as CrunchySearch; - if(searchResults.total < 1){ - console.info('Nothing Found!'); - return { isOk: true, value: [] }; - } - - const searchTypesInfo = { - 'top_results': 'Top results', - 'series': 'Found series', - 'movie_listing': 'Found movie lists', - 'episode': 'Found episodes' - }; - for(const search_item of searchResults.data){ - console.info('%s:', searchTypesInfo[search_item.type as keyof typeof searchTypesInfo]); - // calculate pages - const pageCur = searchStart > 0 ? Math.ceil(searchStart/5) + 1 : 1; - const pageMax = Math.ceil(search_item.count/5); - // pages per category - if(search_item.count < 1){ - console.info(' Nothing Found...'); - } - if(search_item.count > 0){ - if(pageCur > pageMax){ - console.info(' Last page is %s...', pageMax); - continue; + if(!this.cmsToken.cms_web){ + console.error('Authentication required!'); + return; } - for(const item of search_item.items){ - await this.logObject(item); - } - console.info(` Total results: ${search_item.count} (Page: ${pageCur}/${pageMax})`); - } - } - const toSend = searchResults.data.filter(a => a.type === 'series' || a.type === 'movie_listing'); - return { isOk: true, value: toSend.map(a => { - return a.items.map((a): SearchResponseItem => { - const images = (a.images.poster_tall ?? [[ { source: '/notFound.png' } ]])[0]; - return { - id: a.id, - image: images[Math.floor(images.length / 2)].source, - name: a.title, - rating: -1, - desc: a.description + // opts + const AuthHeaders = { + headers: { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader + }, + useProxy: true }; - }); - }).reduce((pre, cur) => pre.concat(cur))}; - } - - public async logObject(item: ParseItem, pad?: number, getSeries?: boolean, getMovieListing?: boolean){ - if(this.debug){ - console.info(item); - } - pad = pad ?? 2; - getSeries = getSeries === undefined ? true : getSeries; - getMovieListing = getMovieListing === undefined ? true : getMovieListing; - item.isSelected = item.isSelected === undefined ? false : item.isSelected; - if(!item.type) { - item.type = item.__class__; - } - - //guess item type - //TODO: look into better methods of getting item type - let iType = item.type; - if (!iType) { - if (item.episode_number) { - iType = 'episode'; - } else if (item.season_number) { - iType = 'season'; - } else if (item.season_count) { - iType = 'series'; - } else if (item.media_type == 'movie') { - iType = 'movie'; - } else if (item.movie_release_year) { - iType = 'movie_listing'; - } else { - if (item.identifier !== '') { - const iTypeCheck = item.identifier?.split('|'); - if (iTypeCheck) { - if (iTypeCheck[1] == 'M') { - iType = 'movie'; - } else if (!iTypeCheck[2]) { - iType = 'season'; - } else { - iType = 'episode'; - } - } else { - iType = 'series'; - } - } else { - iType = 'movie_listing'; + // seasons list + const seriesSeasonListReq = await this.req.getData(`${api.content_cms}/series/${id}/seasons?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); + if(!seriesSeasonListReq.ok || !seriesSeasonListReq.res){ + console.error('Series Request FAILED!'); + return; } - } - item.type = iType; - } - - const oTypes = { - 'series': 'Z', // SRZ - 'season': 'S', // VOL - 'episode': 'E', // EPI - 'movie_listing': 'F', // FLM - 'movie': 'M', // MED - }; - // check title - item.title = item.title != '' ? item.title : 'NO_TITLE'; - // static data - const oMetadata: string[] = [], - oBooleans: string[] = [], - tMetadata = item.type + '_metadata', - iMetadata = (Object.prototype.hasOwnProperty.call(item, tMetadata) ? item[tMetadata as keyof ParseItem] : item) as Record, - iTitle = [ item.title ]; - - const audio_languages: string[] = []; - - // set object booleans - if(iMetadata.duration_ms){ - oBooleans.push(Helper.formatTime(iMetadata.duration_ms/1000)); - } - if(iMetadata.is_simulcast) { - oBooleans.push('SIMULCAST'); - } - if(iMetadata.is_mature) { - oBooleans.push('MATURE'); - } - if (item.versions) { - for(const version of item.versions) { - audio_languages.push(version.audio_locale); - if (version.original) { - oBooleans.push('SUB'); - } else { - if (!oBooleans.includes('DUB')) { - oBooleans.push('DUB'); - } + const seriesData = await seriesSeasonListReq.res.json(); + for (const item of seriesData.data) { + // stringify each object, then a newline + console.log(JSON.stringify(item)); } - } - } else { - if(iMetadata.is_subbed){ - oBooleans.push('SUB'); - } - if(iMetadata.is_dubbed){ - oBooleans.push('DUB'); - } - } - if(item.playback && item.type != 'movie_listing') { - oBooleans.push('STREAM'); - } - // set object metadata - if(iMetadata.season_count){ - oMetadata.push(`Seasons: ${iMetadata.season_count}`); - } - if(iMetadata.episode_count){ - oMetadata.push(`EPs: ${iMetadata.episode_count}`); - } - if(item.season_number && !iMetadata.hide_season_title && !iMetadata.hide_season_number){ - oMetadata.push(`Season: ${item.season_number}`); - } - if(item.type == 'episode'){ - if(iMetadata.episode){ - iTitle.unshift(iMetadata.episode); - } - if(!iMetadata.hide_season_title && iMetadata.season_title){ - iTitle.unshift(iMetadata.season_title); - } - } - if(item.is_premium_only){ - iTitle[0] = `☆ ${iTitle[0]}`; - } - // display metadata - 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; - // make obj ids - const objects_ids: string[] = []; - objects_ids.push(oTypes[item.type as keyof typeof oTypes] + ':' + item.id); - if(item.seq_id){ - objects_ids.unshift(item.seq_id); - } - if(item.f_num){ - objects_ids.unshift(item.f_num); - } - if(item.s_num){ - objects_ids.unshift(item.s_num); - } - if(item.external_id){ - objects_ids.push(item.external_id); - } - if(item.ep_num){ - objects_ids.push(item.ep_num); + return seriesData.data; } - // show entry - console.info( - '%s%s[%s] %s%s%s', - ''.padStart(item.isSelected ? pad-1 : pad, ' '), - item.isSelected ? '✓' : '', - objects_ids.join('|'), - iTitle.join(' - '), - showObjectMetadata ? ` (${oMetadata.join(', ')})` : '', - showObjectBooleans ? ` [${oBooleans.join(', ')}]` : '', - ); - if(item.last_public){ - console.info(''.padStart(pad+1, ' '), '- Last updated:', item.last_public); - } - if(item.subtitle_locales){ - iMetadata.subtitle_locales = item.subtitle_locales; - } - if (item.versions && audio_languages.length > 0) { - console.info( - '%s- Versions: %s', - ''.padStart(pad + 2, ' '), - langsData.parseSubtitlesArray(audio_languages) - ); - } - if(iMetadata.subtitle_locales && iMetadata.subtitle_locales.length > 0){ - console.info( - '%s- Subtitles: %s', - ''.padStart(pad + 2, ' '), - langsData.parseSubtitlesArray(iMetadata.subtitle_locales) - ); - } - if(item.availability_notes){ - console.info( - '%s- Availability notes: %s', - ''.padStart(pad + 2, ' '), - item.availability_notes.replace(/\[[^\]]*]?/gm, '') - ); - } - if(item.type == 'series' && getSeries){ - await this.logSeriesById(item.id, pad, true); - console.info(''); - } - if(item.type == 'movie_listing' && getMovieListing){ - await this.logMovieListingById(item.id, pad+2); - console.info(''); - } - } - - public async logSeriesById(id: string, pad?: number, hideSeriesTitle?: boolean){ - // parse - pad = pad || 0; - hideSeriesTitle = hideSeriesTitle !== undefined ? hideSeriesTitle : false; + public async logSeasonRawById(id: string){ // check token - if(!this.cmsToken.cms_web){ - console.error('Authentication required!'); - return; - } - // opts - const AuthHeaders = { - headers: { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader - }, - useProxy: true - }; - // reqs - if(!hideSeriesTitle){ - const seriesReq = await this.req.getData(`${api.content_cms}/series/${id}?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); - if(!seriesReq.ok || !seriesReq.res){ - console.error('Series Request FAILED!'); - return; - } - const seriesData = await seriesReq.res.json(); - await this.logObject(seriesData.data[0], pad, false); - } - // seasons list - const seriesSeasonListReq = await this.req.getData(`${api.content_cms}/series/${id}/seasons?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); - if(!seriesSeasonListReq.ok || !seriesSeasonListReq.res){ - console.error('Series Request FAILED!'); - return; - } - // parse data - const seasonsList = await seriesSeasonListReq.res.json() as SeriesSearch; - if(seasonsList.total < 1){ - console.info('Series is empty!'); - return; - } - for(const item of seasonsList.data){ - await this.logObject(item, pad+2); - } - } - - public async logMovieListingById(id: string, pad?: number){ - pad = pad || 2; - if(!this.cmsToken.cms_web){ - console.error('Authentication required!'); - return; - } - - // opts - const AuthHeaders = { - headers: { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader - }, - useProxy: true - }; - - //Movie Listing - const movieListingReq = await this.req.getData(`${api.content_cms}/movie_listings/${id}?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); - if(!movieListingReq.ok || !movieListingReq.res){ - console.error('Movie Listing Request FAILED!'); - return; - } - const movieListing = await movieListingReq.res.json(); - if(movieListing.total < 1){ - console.info('Movie Listing is empty!'); - return; - } - for(const item of movieListing.data){ - await this.logObject(item, pad, false, false); - } - - //Movies - const moviesListReq = await this.req.getData(`${api.content_cms}/movie_listings/${id}/movies?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); - if(!moviesListReq.ok || !moviesListReq.res){ - console.error('Movies List Request FAILED!'); - return; - } - const moviesList = await moviesListReq.res.json(); - for(const item of moviesList.data){ - await this.logObject(item, pad + 2); - } - } - - public async getNewlyAdded(page: number = 1, type: string, raw: boolean = false, rawoutput?: string) { - if(!this.token.access_token){ - console.error('Authentication required!'); - return; - } - const newlyAddedReqOpts = { - headers: { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader - }, - useProxy: true - }; - const newlyAddedParams = new URLSearchParams({ - sort_by: 'newly_added', - type: type || 'series', - n: '50', - start: (page ? (page-1)*25 : 0).toString(), - preferred_audio_language: 'ja-JP', - force_locale: '', - locale: this.locale - }).toString(); - const newlyAddedReq = await this.req.getData(`${api.browse}?${newlyAddedParams}`, newlyAddedReqOpts); - if(!newlyAddedReq.ok || !newlyAddedReq.res){ - console.error('Get newly added FAILED!'); - return; - } - const newlyAddedResults = await newlyAddedReq.res.json(); - - if (raw) { - console.info(JSON.stringify(newlyAddedResults, null, 2)); - if (rawoutput) { - try { - fs.writeFileSync(rawoutput, JSON.stringify(newlyAddedResults), { encoding: 'utf-8' }); - console.info(`Raw output saved to ${rawoutput}`); - } catch (e) { - console.error(`Failed to save raw output to ${rawoutput}:`, e); + if(!this.cmsToken.cms_web){ + console.error('Authentication required!'); + return; } - } - return; + // opts + const AuthHeaders = { + headers: { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader + }, + useProxy: true + }; + // seasons list + let episodeList = { total: 0, data: [], meta: {} } as CrunchyEpisodeList; + //get episode info + const reqEpsListOpts = [ + api.cms_bucket, + this.cmsToken.cms_web.bucket, + '/episodes?', + new URLSearchParams({ + 'force_locale': '', + 'preferred_audio_language': 'ja-JP', + 'locale': this.locale, + 'season_id': id, + 'Policy': this.cmsToken.cms_web.policy, + 'Signature': this.cmsToken.cms_web.signature, + 'Key-Pair-Id': this.cmsToken.cms_web.key_pair_id, + }), + ].join(''); + const reqEpsList = await this.req.getData(reqEpsListOpts, AuthHeaders); + if(!reqEpsList.ok || !reqEpsList.res){ + console.error('Episode List Request FAILED!'); + return { isOk: false, reason: new Error('Episode List request failed. No more information provided.') }; + } + //CrunchyEpisodeList + const episodeListAndroid = await reqEpsList.res.json() as CrunchyAndroidEpisodes; + episodeList = { + total: episodeListAndroid.total, + data: episodeListAndroid.items, + meta: {} + }; + for (const item of episodeList.data) { + // stringify each object, then a newline + console.log(JSON.stringify(item)); + } + + // Return the data directly if this function is called by other code + return episodeList.data; } - console.info('Newly added:'); - for(const i of newlyAddedResults.items){ - await this.logObject(i, 2); - } - // calculate pages - const itemPad = parseInt(new URL(newlyAddedResults.__href__, domain.cr_www).searchParams.get('start') as string); - const pageCur = itemPad > 0 ? Math.ceil(itemPad/25) + 1 : 1; - const pageMax = Math.ceil(newlyAddedResults.total/25); - console.info(` Total results: ${newlyAddedResults.total} (Page: ${pageCur}/${pageMax})`); - } + public async logShowListRaw() { + // check token + if(!this.cmsToken.cms_web){ + console.error('Authentication required!'); + return; + } - public async getSeasonById(id: string, numbers: number, e: string|undefined, but: boolean, all: boolean) : Promise> { - if(!this.cmsToken.cms_web){ - console.error('Authentication required!'); - return { isOk: false, reason: new Error('Authentication required') }; + // opts + const AuthHeaders = { + headers: { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader + }, + useProxy: true + }; + + const allShows: any[] = []; + let page = 1; + let hasMorePages = true; + + if(this.debug){ + console.info('Retrieving complete show list...'); + } + + while (hasMorePages) { + const searchStart = (page - 1) * 50; + const params = new URLSearchParams({ + 'preferred_audio_language': 'ja-JP', + 'locale': this.locale, + 'ratings': 'true', + 'sort_by': 'alphabetical', + 'n': '50', + 'start': searchStart.toString() + }).toString(); + + const showListReq = await this.req.getData(`${api.browse_all_series}?${params}`, AuthHeaders); + + if (!showListReq.ok || !showListReq.res) { + console.error(`Show List Request FAILED on page ${page}!`); + return allShows; + } + + const showListData = await showListReq.res.json(); + + // Add current page data + for (const item of showListData.data) { + // stringify each object, then a newline + console.log(JSON.stringify(item)); + allShows.push(item); + } + + // Calculate pagination info + const totalItems = showListData.total; + const totalPages = Math.ceil(totalItems / 50); + if(this.debug){ + console.info(`Retrieved page ${page}/${totalPages} (${allShows.length}/${totalItems} items)`); + } + + // Check if we need to fetch more pages + if (page >= totalPages) { + hasMorePages = false; + } else { + page++; + // Add a small delay to avoid rate limiting + await this.sleep(1000); + } + } + + if(this.debug){ + console.info(`Complete show list retrieved: ${allShows.length} items`); + } + return allShows; } - const AuthHeaders = { - headers: { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader - }, - useProxy: true - }; - - - //get show info - const showInfoReq = await this.req.getData(`${api.content_cms}/seasons/${id}?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); - if(!showInfoReq.ok || !showInfoReq.res){ - console.error('Show Request FAILED!'); - return { isOk: false, reason: new Error('Show request failed. No more information provided.') }; + public async getFonts() { + console.info('Downloading fonts...'); + const fonts = Object.values(fontsData.fontFamilies).reduce((pre, curr) => pre.concat(curr)); + for(const f of fonts) { + const fontLoc = path.join(this.cfg.dir.fonts, f); + if(fs.existsSync(fontLoc) && fs.statSync(fontLoc).size != 0){ + console.info(`${f} already downloaded!`); + } + else{ + const fontFolder = path.dirname(fontLoc); + if(fs.existsSync(fontLoc) && fs.statSync(fontLoc).size == 0){ + fs.unlinkSync(fontLoc); + } + try{ + fs.ensureDirSync(fontFolder); + } + catch(e){ + console.info(''); + } + const fontUrl = fontsData.root + f; + const getFont = await this.req.getData(fontUrl, { + headers: api.crunchyDefHeader + }); + if(getFont.ok && getFont.res){ + fs.writeFileSync(fontLoc, Buffer.from(await getFont.res.arrayBuffer())); + console.info(`Downloaded: ${f}`); + } + else{ + console.warn(`Failed to download: ${f}`); + } + } + } + console.info('All required fonts downloaded!'); } - const showInfo = await showInfoReq.res.json(); - await this.logObject(showInfo.data[0], 0); - let episodeList = { total: 0, data: [], meta: {} } as CrunchyEpisodeList; - //get episode info - const reqEpsListOpts = [ - api.cms_bucket, - this.cmsToken.cms_web.bucket, - '/episodes?', - new URLSearchParams({ - 'force_locale': '', - 'preferred_audio_language': 'ja-JP', - 'locale': this.locale, - 'season_id': id, - 'Policy': this.cmsToken.cms_web.policy, - 'Signature': this.cmsToken.cms_web.signature, - 'Key-Pair-Id': this.cmsToken.cms_web.key_pair_id, - }), - ].join(''); - const reqEpsList = await this.req.getData(reqEpsListOpts, AuthHeaders); - if(!reqEpsList.ok || !reqEpsList.res){ - console.error('Episode List Request FAILED!'); - return { isOk: false, reason: new Error('Episode List request failed. No more information provided.') }; + // private async productionToken() { + // const tokenReq = await this.req.getData(api.bundlejs); + + // if (!tokenReq.ok || !tokenReq.res) { + // console.error('Failed to get Production Token!'); + // return { isOk: false, reason: new Error('Failed to get Production Token') }; + // } + + // const rawjs = await tokenReq.res.text(); + + // const tokens = rawjs.match(/prod="([\w-]+:[\w-]+)"/); + + // if (!tokens) { + // console.error('Failed to find Production Token in js!'); + // return { isOk: false, reason: new Error('Failed to find Production Token in js') }; + // } + + // return Buffer.from(tokens[1], 'latin1').toString('base64'); + // } + + public async doAuth(data: AuthData): Promise { + const uuid = randomUUID(); + const authData = new URLSearchParams({ + 'username': data.username, + 'password': data.password, + 'grant_type': 'password', + 'scope': 'offline_access', + 'device_id': uuid, + 'device_name': 'iPhone', + 'device_type': 'iPhone 13' + }).toString(); + const authReqOpts: reqModule.Params = { + method: 'POST', + headers: { ...api.crunchyAuthHeader, 'ETP-Anonymous-ID': uuid }, + body: authData + }; + const authReq = await this.req.getData(api.auth, authReqOpts); + if(!authReq.ok || !authReq.res){ + console.error('Authentication failed!'); + return { isOk: false, reason: new Error('Authentication failed') }; + } + // To prevent any Cloudflare errors in the future + if (authReq.res.headers.get('Set-Cookie')) { + api.crunchyDefHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; + api.crunchyAuthHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; + } + if (authReq.headers && authReq.headers['Set-Cookie']) { + api.crunchyDefHeader['Cookie'] = authReq.headers['Set-Cookie']; + api.crunchyAuthHeader['Cookie'] = authReq.headers['Set-Cookie']; + api.crunchyDefHeader['User-Agent'] = authReq.headers['User-Agent']; + api.crunchyAuthHeader['User-Agent'] = authReq.headers['User-Agent']; + } + 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(); + console.info('Your Country: %s', this.token.country); + return { isOk: true, value: undefined }; } - //CrunchyEpisodeList - const episodeListAndroid = await reqEpsList.res.json() as CrunchyAndroidEpisodes; - episodeList = { - total: episodeListAndroid.total, - data: episodeListAndroid.items, - meta: {} - }; - const epNumList: { + public async doAnonymousAuth(){ + const uuid = randomUUID(); + const authData = new URLSearchParams({ + 'grant_type': 'client_id', + 'scope': 'offline_access', + 'device_id': uuid, + 'device_name': 'iPhone', + 'device_type': 'iPhone 13' + }).toString(); + const authReqOpts: reqModule.Params = { + method: 'POST', + headers: { ...api.crunchyAuthHeader, 'ETP-Anonymous-ID': uuid }, + body: authData + }; + const authReq = await this.req.getData(api.auth, authReqOpts); + if(!authReq.ok || !authReq.res){ + console.error('Anonymous Authentication failed!'); + return; + } + // To prevent any Cloudflare errors in the future + if (authReq.res.headers.get('Set-Cookie')) { + api.crunchyDefHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; + api.crunchyAuthHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; + } + if (authReq.headers && authReq.headers['Set-Cookie']) { + api.crunchyDefHeader['Cookie'] = authReq.headers['Set-Cookie']; + api.crunchyAuthHeader['Cookie'] = authReq.headers['Set-Cookie']; + api.crunchyDefHeader['User-Agent'] = authReq.headers['User-Agent']; + api.crunchyAuthHeader['User-Agent'] = authReq.headers['User-Agent']; + } + 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); + } + + public async getProfile(silent = false) : Promise { + if(!this.token.access_token){ + console.error('No access token!'); + return false; + } + const profileReqOptions = { + headers: { + ...api.crunchyDefHeader, + Authorization: `Bearer ${this.token.access_token}` + }, + useProxy: true + }; + const profileReq = await this.req.getData(api.profile, profileReqOptions); + if(!profileReq.ok || !profileReq.res){ + console.error('Get profile failed!'); + return false; + } + const profile = await profileReq.res.json(); + 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 loginWithToken(refreshToken: string) { + const uuid = 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_name': 'iPhone', + 'device_type': 'iPhone 13' + }).toString(); + const authReqOpts: reqModule.Params = { + method: 'POST', + headers: {...api.crunchyAuthHeader, 'ETP-Anonymous-ID': uuid, Cookie: `etp_rt=${refreshToken}`}, + body: authData + }; + const authReq = await this.req.getData(api.auth, authReqOpts); + if(!authReq.ok || !authReq.res){ + console.error('Token Authentication failed!'); + if (authReq.res?.status == 400) { + console.warn('Token is likely wrong (Or invalid for given API), please login again!'); + } + return; + } + // To prevent any Cloudflare errors in the future + if (authReq.res.headers.get('Set-Cookie')) { + api.crunchyDefHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; + api.crunchyAuthHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; + } + if (authReq.headers && authReq.headers['Set-Cookie']) { + api.crunchyDefHeader['Cookie'] = authReq.headers['Set-Cookie']; + api.crunchyAuthHeader['Cookie'] = authReq.headers['Set-Cookie']; + api.crunchyDefHeader['User-Agent'] = authReq.headers['User-Agent']; + api.crunchyAuthHeader['User-Agent'] = authReq.headers['User-Agent']; + } + 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); + await this.getCMStoken(true); + } + + 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(); + } + else{ + /*if (ifNeeded) + return;*/ + if (!(Date.now() > new Date(this.token.expires).getTime()) && ifNeeded) { + return; + } 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', + 'device_id': uuid, + 'device_name': 'iPhone', + 'device_type': 'iPhone 13' + }).toString(); + const authReqOpts: reqModule.Params = { + method: 'POST', + headers: { ...api.crunchyAuthHeader, 'ETP-Anonymous-ID': uuid }, + body: authData + }; + const authReq = await this.req.getData(api.auth, authReqOpts); + if(!authReq.ok || !authReq.res){ + console.error('Token Refresh Failed!'); + if (authReq.res?.status == 400) { + console.warn('Token is likely wrong, please login again!'); + } + return; + } + // To prevent any Cloudflare errors in the future + if (authReq.res.headers.get('Set-Cookie')) { + api.crunchyDefHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; + api.crunchyAuthHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; + } + if (authReq.headers && authReq.headers['Set-Cookie']) { + api.crunchyDefHeader['Cookie'] = authReq.headers['Set-Cookie']; + api.crunchyAuthHeader['Cookie'] = authReq.headers['Set-Cookie']; + api.crunchyDefHeader['User-Agent'] = authReq.headers['User-Agent']; + api.crunchyAuthHeader['User-Agent'] = authReq.headers['User-Agent']; + } + 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); + } + if(this.token.refresh_token) { + await this.getProfile(silent); + } else { + console.info('USER: Anonymous'); + } + await this.getCMStoken(ifNeeded); + } + + public async getCMStoken(ifNeeded = false) { + if(!this.token.access_token){ + console.error('No access token!'); + return; + } + + if (ifNeeded && this.cmsToken.cms_web) { + if (!(Date.now() >= new Date(this.cmsToken.cms_web.expires).getTime())) { + return; + } + } + + const cmsTokenReqOpts = { + headers: { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader + }, + useProxy: true + }; + const cmsTokenReq = await this.req.getData(api.cms_auth, cmsTokenReqOpts); + if(!cmsTokenReq.ok || !cmsTokenReq.res){ + console.error('Authentication CMS token failed!'); + return; + } + this.cmsToken = await cmsTokenReq.res.json(); + console.info('Your Country: %s\n', this.cmsToken.cms_web?.bucket.split('/')[1]); + } + + public async getCmsData(){ + // check token + if(!this.cmsToken.cms_web){ + console.error('Authentication required!'); + return; + } + // opts + const indexReqOpts = [ + api.cms_bucket, + this.cmsToken.cms_web.bucket, + '/index?', + new URLSearchParams({ + 'force_locale': '', + 'preferred_audio_language': 'ja-JP', + 'locale': this.locale, + 'Policy': this.cmsToken.cms_web.policy, + 'Signature': this.cmsToken.cms_web.signature, + 'Key-Pair-Id': this.cmsToken.cms_web.key_pair_id, + }), + ].join(''); + const indexReq = await this.req.getData(indexReqOpts, { + headers: { + 'User-Agent': api.crunchyDefUserAgent + } + }); + if(!indexReq.ok || ! indexReq.res){ + console.error('Get CMS index FAILED!'); + return; + } + console.info(await indexReq.res.json()); + } + + public async doSearch(data: SearchData): Promise{ + if(!this.token.access_token){ + console.error('Authentication required!'); + return { isOk: false, reason: new Error('Not authenticated') }; + } + const searchReqOpts = { + headers: { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader + }, + useProxy: true + }; + const searchStart = data.page ? (data.page-1)*5 : 0; + const searchParams = new URLSearchParams({ + q: data.search, + n: '5', + start: data.page ? `${(data.page-1)*5}` : '0', + type: data['search-type'] ?? getDefault('search-type', this.cfg.cli), + locale: this.locale, + }).toString(); + const searchReq = await this.req.getData(`${api.search}?${searchParams}`, searchReqOpts); + if(!searchReq.ok || ! searchReq.res){ + console.error('Search FAILED!'); + return { isOk: false, reason: new Error('Search failed. No more information provided') }; + } + const searchResults = await searchReq.res.json() as CrunchySearch; + if(searchResults.total < 1){ + console.info('Nothing Found!'); + return { isOk: true, value: [] }; + } + + const searchTypesInfo = { + 'top_results': 'Top results', + 'series': 'Found series', + 'movie_listing': 'Found movie lists', + 'episode': 'Found episodes' + }; + for(const search_item of searchResults.data){ + console.info('%s:', searchTypesInfo[search_item.type as keyof typeof searchTypesInfo]); + // calculate pages + const pageCur = searchStart > 0 ? Math.ceil(searchStart/5) + 1 : 1; + const pageMax = Math.ceil(search_item.count/5); + // pages per category + if(search_item.count < 1){ + console.info(' Nothing Found...'); + } + if(search_item.count > 0){ + if(pageCur > pageMax){ + console.info(' Last page is %s...', pageMax); + continue; + } + for(const item of search_item.items){ + await this.logObject(item); + } + console.info(` Total results: ${search_item.count} (Page: ${pageCur}/${pageMax})`); + } + } + const toSend = searchResults.data.filter(a => a.type === 'series' || a.type === 'movie_listing'); + return { isOk: true, value: toSend.map(a => { + return a.items.map((a): SearchResponseItem => { + const images = (a.images.poster_tall ?? [[ { source: '/notFound.png' } ]])[0]; + return { + id: a.id, + image: images[Math.floor(images.length / 2)].source, + name: a.title, + rating: -1, + desc: a.description + }; + }); + }).reduce((pre, cur) => pre.concat(cur))}; + } + + public async logObject(item: ParseItem, pad?: number, getSeries?: boolean, getMovieListing?: boolean){ + if(this.debug){ + console.info(item); + } + pad = pad ?? 2; + getSeries = getSeries === undefined ? true : getSeries; + getMovieListing = getMovieListing === undefined ? true : getMovieListing; + item.isSelected = item.isSelected === undefined ? false : item.isSelected; + if(!item.type) { + item.type = item.__class__; + } + + //guess item type + //TODO: look into better methods of getting item type + let iType = item.type; + if (!iType) { + if (item.episode_number) { + iType = 'episode'; + } else if (item.season_number) { + iType = 'season'; + } else if (item.season_count) { + iType = 'series'; + } else if (item.media_type == 'movie') { + iType = 'movie'; + } else if (item.movie_release_year) { + iType = 'movie_listing'; + } else { + if (item.identifier !== '') { + const iTypeCheck = item.identifier?.split('|'); + if (iTypeCheck) { + if (iTypeCheck[1] == 'M') { + iType = 'movie'; + } else if (!iTypeCheck[2]) { + iType = 'season'; + } else { + iType = 'episode'; + } + } else { + iType = 'series'; + } + } else { + iType = 'movie_listing'; + } + } + item.type = iType; + } + + const oTypes = { + 'series': 'Z', // SRZ + 'season': 'S', // VOL + 'episode': 'E', // EPI + 'movie_listing': 'F', // FLM + 'movie': 'M', // MED + }; + // check title + item.title = item.title != '' ? item.title : 'NO_TITLE'; + // static data + const oMetadata: string[] = [], + oBooleans: string[] = [], + tMetadata = item.type + '_metadata', + iMetadata = (Object.prototype.hasOwnProperty.call(item, tMetadata) ? item[tMetadata as keyof ParseItem] : item) as Record, + iTitle = [ item.title ]; + + const audio_languages: string[] = []; + + // set object booleans + if(iMetadata.duration_ms){ + oBooleans.push(Helper.formatTime(iMetadata.duration_ms/1000)); + } + if(iMetadata.is_simulcast) { + oBooleans.push('SIMULCAST'); + } + if(iMetadata.is_mature) { + oBooleans.push('MATURE'); + } + if (item.versions) { + for(const version of item.versions) { + audio_languages.push(version.audio_locale); + if (version.original) { + oBooleans.push('SUB'); + } else { + if (!oBooleans.includes('DUB')) { + oBooleans.push('DUB'); + } + } + } + } else { + if(iMetadata.is_subbed){ + oBooleans.push('SUB'); + } + if(iMetadata.is_dubbed){ + oBooleans.push('DUB'); + } + } + if(item.playback && item.type != 'movie_listing') { + oBooleans.push('STREAM'); + } + // set object metadata + if(iMetadata.season_count){ + oMetadata.push(`Seasons: ${iMetadata.season_count}`); + } + if(iMetadata.episode_count){ + oMetadata.push(`EPs: ${iMetadata.episode_count}`); + } + if(item.season_number && !iMetadata.hide_season_title && !iMetadata.hide_season_number){ + oMetadata.push(`Season: ${item.season_number}`); + } + if(item.type == 'episode'){ + if(iMetadata.episode){ + iTitle.unshift(iMetadata.episode); + } + if(!iMetadata.hide_season_title && iMetadata.season_title){ + iTitle.unshift(iMetadata.season_title); + } + } + if(item.is_premium_only){ + iTitle[0] = `☆ ${iTitle[0]}`; + } + // display metadata + 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; + // make obj ids + const objects_ids: string[] = []; + objects_ids.push(oTypes[item.type as keyof typeof oTypes] + ':' + item.id); + if(item.seq_id){ + objects_ids.unshift(item.seq_id); + } + if(item.f_num){ + objects_ids.unshift(item.f_num); + } + if(item.s_num){ + objects_ids.unshift(item.s_num); + } + if(item.external_id){ + objects_ids.push(item.external_id); + } + if(item.ep_num){ + objects_ids.push(item.ep_num); + } + + // show entry + console.info( + '%s%s[%s] %s%s%s', + ''.padStart(item.isSelected ? pad-1 : pad, ' '), + item.isSelected ? '✓' : '', + objects_ids.join('|'), + iTitle.join(' - '), + showObjectMetadata ? ` (${oMetadata.join(', ')})` : '', + showObjectBooleans ? ` [${oBooleans.join(', ')}]` : '', + + ); + if(item.last_public){ + console.info(''.padStart(pad+1, ' '), '- Last updated:', item.last_public); + } + if(item.subtitle_locales){ + iMetadata.subtitle_locales = item.subtitle_locales; + } + if (item.versions && audio_languages.length > 0) { + console.info( + '%s- Versions: %s', + ''.padStart(pad + 2, ' '), + langsData.parseSubtitlesArray(audio_languages) + ); + } + if(iMetadata.subtitle_locales && iMetadata.subtitle_locales.length > 0){ + console.info( + '%s- Subtitles: %s', + ''.padStart(pad + 2, ' '), + langsData.parseSubtitlesArray(iMetadata.subtitle_locales) + ); + } + if(item.availability_notes){ + console.info( + '%s- Availability notes: %s', + ''.padStart(pad + 2, ' '), + item.availability_notes.replace(/\[[^\]]*]?/gm, '') + ); + } + if(item.type == 'series' && getSeries){ + await this.logSeriesById(item.id, pad, true); + console.info(''); + } + if(item.type == 'movie_listing' && getMovieListing){ + await this.logMovieListingById(item.id, pad+2); + console.info(''); + } + } + + public async logSeriesById(id: string, pad?: number, hideSeriesTitle?: boolean){ + // parse + pad = pad || 0; + hideSeriesTitle = hideSeriesTitle !== undefined ? hideSeriesTitle : false; + // check token + if(!this.cmsToken.cms_web){ + console.error('Authentication required!'); + return; + } + // opts + const AuthHeaders = { + headers: { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader + }, + useProxy: true + }; + // reqs + if(!hideSeriesTitle){ + const seriesReq = await this.req.getData(`${api.content_cms}/series/${id}?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); + if(!seriesReq.ok || !seriesReq.res){ + console.error('Series Request FAILED!'); + return; + } + const seriesData = await seriesReq.res.json(); + await this.logObject(seriesData.data[0], pad, false); + } + // seasons list + const seriesSeasonListReq = await this.req.getData(`${api.content_cms}/series/${id}/seasons?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); + if(!seriesSeasonListReq.ok || !seriesSeasonListReq.res){ + console.error('Series Request FAILED!'); + return; + } + // parse data + const seasonsList = await seriesSeasonListReq.res.json() as SeriesSearch; + if(seasonsList.total < 1){ + console.info('Series is empty!'); + return; + } + for(const item of seasonsList.data){ + await this.logObject(item, pad+2); + } + } + + public async logMovieListingById(id: string, pad?: number){ + pad = pad || 2; + if(!this.cmsToken.cms_web){ + console.error('Authentication required!'); + return; + } + + // opts + const AuthHeaders = { + headers: { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader + }, + useProxy: true + }; + + //Movie Listing + const movieListingReq = await this.req.getData(`${api.content_cms}/movie_listings/${id}?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); + if(!movieListingReq.ok || !movieListingReq.res){ + console.error('Movie Listing Request FAILED!'); + return; + } + const movieListing = await movieListingReq.res.json(); + if(movieListing.total < 1){ + console.info('Movie Listing is empty!'); + return; + } + for(const item of movieListing.data){ + await this.logObject(item, pad, false, false); + } + + //Movies + const moviesListReq = await this.req.getData(`${api.content_cms}/movie_listings/${id}/movies?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); + if(!moviesListReq.ok || !moviesListReq.res){ + console.error('Movies List Request FAILED!'); + return; + } + const moviesList = await moviesListReq.res.json(); + for(const item of moviesList.data){ + await this.logObject(item, pad + 2); + } + } + + public async getNewlyAdded(page: number = 1, type: string, raw: boolean = false, rawoutput?: string) { + if(!this.token.access_token){ + console.error('Authentication required!'); + return; + } + const newlyAddedReqOpts = { + headers: { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader + }, + useProxy: true + }; + const newlyAddedParams = new URLSearchParams({ + sort_by: 'newly_added', + type: type || 'series', + n: '50', + start: (page ? (page-1)*25 : 0).toString(), + preferred_audio_language: 'ja-JP', + force_locale: '', + locale: this.locale + }).toString(); + const newlyAddedReq = await this.req.getData(`${api.browse}?${newlyAddedParams}`, newlyAddedReqOpts); + if(!newlyAddedReq.ok || !newlyAddedReq.res){ + console.error('Get newly added FAILED!'); + return; + } + const newlyAddedResults = await newlyAddedReq.res.json(); + + if (raw) { + console.info(JSON.stringify(newlyAddedResults, null, 2)); + if (rawoutput) { + try { + fs.writeFileSync(rawoutput, JSON.stringify(newlyAddedResults), { encoding: 'utf-8' }); + console.info(`Raw output saved to ${rawoutput}`); + } catch (e) { + console.error(`Failed to save raw output to ${rawoutput}:`, e); + } + } + return; + } + + console.info('Newly added:'); + for(const i of newlyAddedResults.items){ + await this.logObject(i, 2); + } + // calculate pages + const itemPad = parseInt(new URL(newlyAddedResults.__href__, domain.cr_www).searchParams.get('start') as string); + const pageCur = itemPad > 0 ? Math.ceil(itemPad/25) + 1 : 1; + const pageMax = Math.ceil(newlyAddedResults.total/25); + console.info(` Total results: ${newlyAddedResults.total} (Page: ${pageCur}/${pageMax})`); + } + + public async getSeasonById(id: string, numbers: number, e: string|undefined, but: boolean, all: boolean) : Promise> { + if(!this.cmsToken.cms_web){ + console.error('Authentication required!'); + return { isOk: false, reason: new Error('Authentication required') }; + } + + const AuthHeaders = { + headers: { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader + }, + useProxy: true + }; + + + //get show info + const showInfoReq = await this.req.getData(`${api.content_cms}/seasons/${id}?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); + if(!showInfoReq.ok || !showInfoReq.res){ + console.error('Show Request FAILED!'); + 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); + + let episodeList = { total: 0, data: [], meta: {} } as CrunchyEpisodeList; + //get episode info + const reqEpsListOpts = [ + api.cms_bucket, + this.cmsToken.cms_web.bucket, + '/episodes?', + new URLSearchParams({ + 'force_locale': '', + 'preferred_audio_language': 'ja-JP', + 'locale': this.locale, + 'season_id': id, + 'Policy': this.cmsToken.cms_web.policy, + 'Signature': this.cmsToken.cms_web.signature, + 'Key-Pair-Id': this.cmsToken.cms_web.key_pair_id, + }), + ].join(''); + const reqEpsList = await this.req.getData(reqEpsListOpts, AuthHeaders); + if(!reqEpsList.ok || !reqEpsList.res){ + console.error('Episode List Request FAILED!'); + return { isOk: false, reason: new Error('Episode List request failed. No more information provided.') }; + } + //CrunchyEpisodeList + const episodeListAndroid = await reqEpsList.res.json() as CrunchyAndroidEpisodes; + episodeList = { + total: episodeListAndroid.total, + data: episodeListAndroid.items, + meta: {} + }; + + const epNumList: { ep: number[], sp: number } = { ep: [], sp: 0 }; - const epNumLen = numbers; + const epNumLen = numbers; - if(episodeList.total < 1){ - console.info(' Season is empty!'); - return { isOk: true, value: [] }; - } - - const doEpsFilter = parseSelect(e as string); - const selectedMedia: CrunchyEpMeta[] = []; - - episodeList.data.forEach((item) => { - item.hide_season_title = true; - if(item.season_title == '' && item.series_title != ''){ - item.season_title = item.series_title; - item.hide_season_title = false; - item.hide_season_number = true; - } - if(item.season_title == '' && item.series_title == ''){ - item.season_title = 'NO_TITLE'; - item.series_title = 'NO_TITLE'; - } - const epNum = item.episode; - let isSpecial = false; - item.isSelected = false; - if(!epNum.match(/^\d+$/) || epNumList.ep.indexOf(parseInt(epNum, 10)) > -1){ - isSpecial = true; - epNumList.sp++; - } - else{ - epNumList.ep.push(parseInt(epNum, 10)); - } - const selEpId = ( - isSpecial - ? 'S' + epNumList.sp.toString().padStart(epNumLen, '0') - : '' + parseInt(epNum, 10).toString().padStart(epNumLen, '0') - ); - // set data - const images = (item.images.thumbnail ?? [[ { source: '/notFound.png' } ]])[0]; - const epMeta: CrunchyEpMeta = { - data: [ - { - mediaId: item.id, - versions: null, - lang: langsData.languages.find(a => a.code == yargs.appArgv(this.cfg.cli).dubLang[0]), - isSubbed: item.is_subbed, - isDubbed: item.is_dubbed - } - ], - seriesTitle: item.series_title, - seasonTitle: item.season_title, - episodeNumber: item.episode, - episodeTitle: item.title, - seasonID: item.season_id, - season: item.season_number, - showID: id, - e: selEpId, - image: images[Math.floor(images.length / 2)].source - }; - // Check for streams_link and update playback var if needed - if (item.__links__?.streams?.href) { - epMeta.data[0].playback = item.__links__.streams.href; - if(!item.playback) { - item.playback = item.__links__.streams.href; + if(episodeList.total < 1){ + console.info(' Season is empty!'); + return { isOk: true, value: [] }; } - } - if (item.streams_link) { - epMeta.data[0].playback = item.streams_link; - if(!item.playback) { - item.playback = item.streams_link; - } - } - if (item.versions) { - epMeta.data[0].versions = item.versions; - } - // find episode numbers - if((but && item.playback && !doEpsFilter.isSelected([selEpId, item.id])) || (all && item.playback) || (!but && doEpsFilter.isSelected([selEpId, item.id]) && !item.isSelected && item.playback)){ - selectedMedia.push(epMeta); - item.isSelected = true; - } - // show ep - item.seq_id = selEpId; - this.logObject(item); - }); - // display - if(selectedMedia.length < 1){ - console.info('\nEpisodes not selected!\n'); - } + const doEpsFilter = parseSelect(e as string); + const selectedMedia: CrunchyEpMeta[] = []; - console.info(''); - return { isOk: true, value: selectedMedia }; - } - - public async downloadEpisode(data: CrunchyEpMeta, options: CrunchyDownloadOptions, isSeries?: boolean): Promise { - const res = await this.downloadMediaList(data, options); - if (res === undefined || res.error) { - return false; - } else { - if (!options.skipmux) { - await this.muxStreams(res.data, { ...options, output: res.fileName }); - } else { - console.info('Skipping mux'); - } - if (!isSeries) { - downloaded({ - service: 'crunchy', - type: 's' - }, data.seasonID, [data.e]); - } else { - downloaded({ - service: 'crunchy', - type: 'srz' - }, data.showID, [data.e]); - } - } - return true; - } - - public async getObjectById(e?: string, earlyReturn?: boolean, external_id?: boolean): Promise[]|undefined> { - if(!this.cmsToken.cms_web){ - console.error('Authentication required!'); - return []; - } - - let convertedObjects; - if (external_id) { - const epFilter = parseSelect(e as string); - const objectIds = []; - for (const ob of epFilter.values) { - const extIdReqOpts = [ - api.cms_bucket, - this.cmsToken.cms_web.bucket, - '/channels/crunchyroll/objects', - '?', - new URLSearchParams({ - 'force_locale': '', - 'preferred_audio_language': 'ja-JP', - 'locale': this.locale, - 'external_id': ob, - 'Policy': this.cmsToken.cms_web.policy, - 'Signature': this.cmsToken.cms_web.signature, - 'Key-Pair-Id': this.cmsToken.cms_web.key_pair_id, - }), - ].join(''); - - const extIdReq = await this.req.getData(extIdReqOpts, { - headers: { - 'User-Agent': api.crunchyDefUserAgent - } + episodeList.data.forEach((item) => { + item.hide_season_title = true; + if(item.season_title == '' && item.series_title != ''){ + item.season_title = item.series_title; + item.hide_season_title = false; + item.hide_season_number = true; + } + if(item.season_title == '' && item.series_title == ''){ + item.season_title = 'NO_TITLE'; + item.series_title = 'NO_TITLE'; + } + const epNum = item.episode; + let isSpecial = false; + item.isSelected = false; + if(!epNum.match(/^\d+$/) || epNumList.ep.indexOf(parseInt(epNum, 10)) > -1){ + isSpecial = true; + epNumList.sp++; + } + else{ + epNumList.ep.push(parseInt(epNum, 10)); + } + const selEpId = ( + isSpecial + ? 'S' + epNumList.sp.toString().padStart(epNumLen, '0') + : '' + parseInt(epNum, 10).toString().padStart(epNumLen, '0') + ); + // set data + const images = (item.images.thumbnail ?? [[ { source: '/notFound.png' } ]])[0]; + const epMeta: CrunchyEpMeta = { + data: [ + { + mediaId: item.id, + versions: null, + lang: langsData.languages.find(a => a.code == yargs.appArgv(this.cfg.cli).dubLang[0]), + isSubbed: item.is_subbed, + isDubbed: item.is_dubbed + } + ], + seriesTitle: item.series_title, + seasonTitle: item.season_title, + episodeNumber: item.episode, + episodeTitle: item.title, + seasonID: item.season_id, + season: item.season_number, + showID: id, + e: selEpId, + image: images[Math.floor(images.length / 2)].source + }; + // Check for streams_link and update playback var if needed + if (item.__links__?.streams?.href) { + epMeta.data[0].playback = item.__links__.streams.href; + if(!item.playback) { + item.playback = item.__links__.streams.href; + } + } + if (item.streams_link) { + epMeta.data[0].playback = item.streams_link; + if(!item.playback) { + item.playback = item.streams_link; + } + } + if (item.versions) { + epMeta.data[0].versions = item.versions; + } + // find episode numbers + if((but && item.playback && !doEpsFilter.isSelected([selEpId, item.id])) || (all && item.playback) || (!but && doEpsFilter.isSelected([selEpId, item.id]) && !item.isSelected && item.playback)){ + selectedMedia.push(epMeta); + item.isSelected = true; + } + // show ep + item.seq_id = selEpId; + this.logObject(item); }); - 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; + + // display + if(selectedMedia.length < 1){ + console.info('\nEpisodes not selected!\n'); } - const oldObjectInfo = await extIdReq.res.json() as Record; - for (const object of oldObjectInfo.items) { - objectIds.push(object.id); + console.info(''); + return { isOk: true, value: selectedMedia }; + } + + public async downloadEpisode(data: CrunchyEpMeta, options: CrunchyDownloadOptions, isSeries?: boolean): Promise { + const res = await this.downloadMediaList(data, options); + if (res === undefined || res.error) { + return false; + } else { + if (!options.skipmux) { + await this.muxStreams(res.data, { ...options, output: res.fileName }); + } else { + console.info('Skipping mux'); + } + if (!isSeries) { + downloaded({ + service: 'crunchy', + type: 's' + }, data.seasonID, [data.e]); + } else { + downloaded({ + service: 'crunchy', + type: 'srz' + }, data.showID, [data.e]); + } } - } - convertedObjects = objectIds.join(','); + return true; } - const doEpsFilter = parseSelect(convertedObjects ?? e as string); - - if(doEpsFilter.values.length < 1){ - console.info('\nObjects not selected!\n'); - return []; - } - - // node index.js --service crunchy -e G6497Z43Y,GRZXCMN1W,G62PEZ2E6,G25FVGDEK,GZ7UVPVX5 - console.info('Requested object ID: %s', doEpsFilter.values.join(', ')); - - const AuthHeaders = { - headers: { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader - }, - useProxy: true - }; - - // reqs - let objectInfo: ObjectInfo = { total: 0, data: [], meta: {} }; - const objectReqOpts = [ - api.cms_bucket, - this.cmsToken.cms_web.bucket, - '/objects/', - doEpsFilter.values.join(','), - '?', - new URLSearchParams({ - 'force_locale': '', - 'preferred_audio_language': 'ja-JP', - 'locale': this.locale, - 'Policy': this.cmsToken.cms_web.policy, - 'Signature': this.cmsToken.cms_web.signature, - 'Key-Pair-Id': this.cmsToken.cms_web.key_pair_id, - }), - ].join(''); - const objectReq = await this.req.getData(objectReqOpts, AuthHeaders); - if(!objectReq.ok || !objectReq.res){ - console.error('Objects Request FAILED!'); - if(objectReq.error && objectReq.error.res && objectReq.error.res.body){ - const objectInfo = await objectReq.error.res.json(); - console.info('Body:', JSON.stringify(objectInfo, null, '\t')); - objectInfo.error = true; - return objectInfo; - } - return []; - } - const objectInfoAndroid = await objectReq.res.json() as CrunchyAndroidObject; - objectInfo = { - total: objectInfoAndroid.total, - data: objectInfoAndroid.items, - meta: {} - }; - - if(earlyReturn){ - return objectInfo; - } - - const selectedMedia: Partial[] = []; - - for(const item of objectInfo.data){ - if(item.type != 'episode' && item.type != 'movie'){ - await this.logObject(item, 2, true, false); - continue; - } - const epMeta: Partial = {}; - - epMeta.data = []; - if (item.episode_metadata) { - item.s_num = 'S:' + item.episode_metadata.season_id; - epMeta.data = [ - { - mediaId: 'E:'+ item.id, - versions: item.episode_metadata.versions, - isSubbed: item.episode_metadata.is_subbed, - isDubbed: item.episode_metadata.is_dubbed - } - ]; - epMeta.seriesTitle = item.episode_metadata.series_title; - epMeta.seasonTitle = item.episode_metadata.season_title; - epMeta.episodeNumber = item.episode_metadata.episode; - epMeta.episodeTitle = item.title; - epMeta.season = item.episode_metadata.season_number; - } else if (item.movie_listing_metadata) { - item.f_num = 'F:' + item.id; - epMeta.data = [ - { - mediaId: 'M:'+ item.id, - isSubbed: item.movie_listing_metadata.is_subbed, - isDubbed: item.movie_listing_metadata.is_dubbed - } - ]; - epMeta.seriesTitle = item.title; - epMeta.seasonTitle = item.title; - epMeta.episodeNumber = 'Movie'; - epMeta.episodeTitle = item.title; - } else if (item.movie_metadata) { - item.f_num = 'F:' + item.id; - epMeta.data = [ - { - mediaId: 'M:'+ item.id, - isSubbed: item.movie_metadata.is_subbed, - isDubbed: item.movie_metadata.is_dubbed - } - ]; - epMeta.season = 0; - epMeta.seriesTitle = item.title; - epMeta.seasonTitle = item.title; - epMeta.episodeNumber = 'Movie'; - epMeta.episodeTitle = item.title; - } - if (item.streams_link) { - epMeta.data[0].playback = item.streams_link; - if(!item.playback) { - item.playback = item.streams_link; + public async getObjectById(e?: string, earlyReturn?: boolean, external_id?: boolean): Promise[]|undefined> { + if(!this.cmsToken.cms_web){ + console.error('Authentication required!'); + return []; } - selectedMedia.push(epMeta); - item.isSelected = true; - } else if (item.__links__) { - epMeta.data[0].playback = item.__links__.streams.href; - if(!item.playback) { - item.playback = item.__links__.streams.href; + + let convertedObjects; + if (external_id) { + const epFilter = parseSelect(e as string); + const objectIds = []; + for (const ob of epFilter.values) { + const extIdReqOpts = [ + api.cms_bucket, + this.cmsToken.cms_web.bucket, + '/channels/crunchyroll/objects', + '?', + new URLSearchParams({ + 'force_locale': '', + 'preferred_audio_language': 'ja-JP', + 'locale': this.locale, + 'external_id': ob, + 'Policy': this.cmsToken.cms_web.policy, + 'Signature': this.cmsToken.cms_web.signature, + 'Key-Pair-Id': this.cmsToken.cms_web.key_pair_id, + }), + ].join(''); + + const extIdReq = await this.req.getData(extIdReqOpts, { + headers: { + 'User-Agent': api.crunchyDefUserAgent + } + }); + 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 = await extIdReq.res.json() as Record; + for (const object of oldObjectInfo.items) { + objectIds.push(object.id); + } + } + convertedObjects = objectIds.join(','); } - selectedMedia.push(epMeta); - item.isSelected = true; - } - await this.logObject(item, 2); + + const doEpsFilter = parseSelect(convertedObjects ?? e as string); + + if(doEpsFilter.values.length < 1){ + console.info('\nObjects not selected!\n'); + return []; + } + + // node index.js --service crunchy -e G6497Z43Y,GRZXCMN1W,G62PEZ2E6,G25FVGDEK,GZ7UVPVX5 + console.info('Requested object ID: %s', doEpsFilter.values.join(', ')); + + const AuthHeaders = { + headers: { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader + }, + useProxy: true + }; + + // reqs + let objectInfo: ObjectInfo = { total: 0, data: [], meta: {} }; + const objectReqOpts = [ + api.cms_bucket, + this.cmsToken.cms_web.bucket, + '/objects/', + doEpsFilter.values.join(','), + '?', + new URLSearchParams({ + 'force_locale': '', + 'preferred_audio_language': 'ja-JP', + 'locale': this.locale, + 'Policy': this.cmsToken.cms_web.policy, + 'Signature': this.cmsToken.cms_web.signature, + 'Key-Pair-Id': this.cmsToken.cms_web.key_pair_id, + }), + ].join(''); + const objectReq = await this.req.getData(objectReqOpts, AuthHeaders); + if(!objectReq.ok || !objectReq.res){ + console.error('Objects Request FAILED!'); + if(objectReq.error && objectReq.error.res && objectReq.error.res.body){ + const objectInfo = await objectReq.error.res.json(); + console.info('Body:', JSON.stringify(objectInfo, null, '\t')); + objectInfo.error = true; + return objectInfo; + } + return []; + } + const objectInfoAndroid = await objectReq.res.json() as CrunchyAndroidObject; + objectInfo = { + total: objectInfoAndroid.total, + data: objectInfoAndroid.items, + meta: {} + }; + + if(earlyReturn){ + return objectInfo; + } + + const selectedMedia: Partial[] = []; + + for(const item of objectInfo.data){ + if(item.type != 'episode' && item.type != 'movie'){ + await this.logObject(item, 2, true, false); + continue; + } + const epMeta: Partial = {}; + + epMeta.data = []; + if (item.episode_metadata) { + item.s_num = 'S:' + item.episode_metadata.season_id; + epMeta.data = [ + { + mediaId: 'E:'+ item.id, + versions: item.episode_metadata.versions, + isSubbed: item.episode_metadata.is_subbed, + isDubbed: item.episode_metadata.is_dubbed + } + ]; + epMeta.seriesTitle = item.episode_metadata.series_title; + epMeta.seasonTitle = item.episode_metadata.season_title; + epMeta.episodeNumber = item.episode_metadata.episode; + epMeta.episodeTitle = item.title; + epMeta.season = item.episode_metadata.season_number; + } else if (item.movie_listing_metadata) { + item.f_num = 'F:' + item.id; + epMeta.data = [ + { + mediaId: 'M:'+ item.id, + isSubbed: item.movie_listing_metadata.is_subbed, + isDubbed: item.movie_listing_metadata.is_dubbed + } + ]; + epMeta.seriesTitle = item.title; + epMeta.seasonTitle = item.title; + epMeta.episodeNumber = 'Movie'; + epMeta.episodeTitle = item.title; + } else if (item.movie_metadata) { + item.f_num = 'F:' + item.id; + epMeta.data = [ + { + mediaId: 'M:'+ item.id, + isSubbed: item.movie_metadata.is_subbed, + isDubbed: item.movie_metadata.is_dubbed + } + ]; + epMeta.season = 0; + epMeta.seriesTitle = item.title; + epMeta.seasonTitle = item.title; + epMeta.episodeNumber = 'Movie'; + epMeta.episodeTitle = item.title; + } + if (item.streams_link) { + epMeta.data[0].playback = item.streams_link; + if(!item.playback) { + item.playback = item.streams_link; + } + selectedMedia.push(epMeta); + item.isSelected = true; + } else if (item.__links__) { + epMeta.data[0].playback = item.__links__.streams.href; + if(!item.playback) { + item.playback = item.__links__.streams.href; + } + selectedMedia.push(epMeta); + item.isSelected = true; + } + await this.logObject(item, 2); + } + console.info(''); + return selectedMedia; } - console.info(''); - return selectedMedia; - } - private convertDownloadToPlayback(audioUrl: string, videoUrl: string): string { - try { - const url = new URL(audioUrl); - const urla = new URL(videoUrl); - url.pathname = url.pathname.replace('/manifest/download/', '/manifest/'); - url.searchParams.delete('downloadGuid'); - url.searchParams.set('playbackGuid', urla.searchParams.get('playbackGuid') as string); + private convertDownloadToPlayback(audioUrl: string, videoUrl: string): string { + try { + const url = new URL(audioUrl); + const urla = new URL(videoUrl); + url.pathname = url.pathname.replace('/manifest/download/', '/manifest/'); + url.searchParams.delete('downloadGuid'); + url.searchParams.set('playbackGuid', urla.searchParams.get('playbackGuid') as string); - return url.toString(); - } catch (err) { - return audioUrl; + return url.toString(); + } catch (err) { + return audioUrl; + } } - } - public async downloadMediaList(medias: CrunchyEpMeta, options: CrunchyDownloadOptions) : Promise<{ + public async downloadMediaList(medias: CrunchyEpMeta, options: CrunchyDownloadOptions) : Promise<{ data: DownloadedMedia[], fileName: string, error: boolean } | undefined> { - if(!this.cmsToken.cms_web){ - console.error('Authentication required!'); - return; - } - - if (!this.cfg.bin.ffmpeg) - this.cfg.bin = await yamlCfg.loadBinCfg(); - - let mediaName = '...'; - let fileName; - const variables: Variable[] = []; - if(medias.seasonTitle && medias.episodeNumber && medias.episodeTitle){ - mediaName = `${medias.seasonTitle} - ${medias.episodeNumber} - ${medias.episodeTitle}`; - } - - const files: DownloadedMedia[] = []; - - if(medias.data.every(a => !a.playback)){ - console.warn('Video not available!'); - return undefined; - } - - let dlFailed = false; - let dlVideoOnce = false; // Variable to save if best selected video quality was downloaded - - - for (const mMeta of medias.data) { - console.info(`Requesting: [${mMeta.mediaId}] ${mediaName}`); - - // 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 - await this.refreshToken(true, true); - let currentVersion; - let isPrimary = mMeta.isSubbed; - const AuthHeaders: RequestInit = { - headers: { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader + if(!this.cmsToken.cms_web){ + console.error('Authentication required!'); + return; } - }; - //Get Media GUID - let mediaId = mMeta.mediaId; - if (mMeta.versions) { - if (mMeta.lang) { - currentVersion = mMeta.versions.find(a => a.audio_locale == mMeta.lang?.cr_locale); - } else if (options.dubLang.length == 1) { - const currentLang = langsData.languages.find(a => a.code == options.dubLang[0]); - currentVersion = mMeta.versions.find(a => a.audio_locale == currentLang?.cr_locale); - } else if (mMeta.versions.length == 1) { - currentVersion = mMeta.versions[0]; + if (!this.cfg.bin.ffmpeg) + this.cfg.bin = await yamlCfg.loadBinCfg(); + + let mediaName = '...'; + let fileName; + const variables: Variable[] = []; + if(medias.seasonTitle && medias.episodeNumber && medias.episodeTitle){ + mediaName = `${medias.seasonTitle} - ${medias.episodeNumber} - ${medias.episodeTitle}`; } - if (!currentVersion?.media_guid) { - console.error('Selected language not found in versions.'); - continue; + + const files: DownloadedMedia[] = []; + + if(medias.data.every(a => !a.playback)){ + console.warn('Video not available!'); + return undefined; } - isPrimary = currentVersion.original; - mediaId = currentVersion?.media_guid; - } - // If for whatever reason mediaId has a :, return the ID only - if (mediaId.includes(':')) - mediaId = mediaId.split(':')[1]; + let dlFailed = false; + let dlVideoOnce = false; // Variable to save if best selected video quality was downloaded - const compiledChapters: string[] = []; - if (options.chapters) { - //Make Chapter Request - const chapterRequest = await this.req.getData(`https://static.crunchyroll.com/skip-events/production/${currentMediaId}.json`, { - headers: api.crunchyDefHeader - }); - if(!chapterRequest.ok || !chapterRequest.res){ - //Old Chapter Request Fallback - console.warn('Chapter request failed, attempting old API'); - const oldChapterRequest = await this.req.getData(`https://static.crunchyroll.com/datalab-intro-v2/${currentMediaId}.json`, { - headers: api.crunchyDefHeader - }); - if(!oldChapterRequest.ok || !oldChapterRequest.res) { - console.warn('Old Chapter API request failed'); - } else { - console.info('Old Chapter request successful'); - const chapterData = await oldChapterRequest.res.json() as CrunchyOldChapter; - //Generate Timestamps - const startTime = new Date(0), endTime = new Date(0); - startTime.setSeconds(chapterData.startTime); - endTime.setSeconds(chapterData.endTime); - const startTimeMS = String(chapterData.startTime).split('.')[1], endTimeMS = String(chapterData.endTime).split('.')[1]; - 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; + for (const mMeta of medias.data) { + console.info(`Requesting: [${mMeta.mediaId}] ${mediaName}`); + + // 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 + await this.refreshToken(true, true); + let currentVersion; + let isPrimary = mMeta.isSubbed; + const AuthHeaders: RequestInit = { + headers: { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader + } + }; + + //Get Media GUID + let mediaId = mMeta.mediaId; + if (mMeta.versions) { + if (mMeta.lang) { + currentVersion = mMeta.versions.find(a => a.audio_locale == mMeta.lang?.cr_locale); + } else if (options.dubLang.length == 1) { + const currentLang = langsData.languages.find(a => a.code == options.dubLang[0]); + currentVersion = mMeta.versions.find(a => a.audio_locale == currentLang?.cr_locale); + } else if (mMeta.versions.length == 1) { + currentVersion = mMeta.versions[0]; + } + if (!currentVersion?.media_guid) { + console.error('Selected language not found in versions.'); + continue; + } + isPrimary = currentVersion.original; + mediaId = currentVersion?.media_guid; + } + + // If for whatever reason mediaId has a :, return the ID only + if (mediaId.includes(':')) + mediaId = mediaId.split(':')[1]; + + const compiledChapters: string[] = []; + if (options.chapters) { + //Make Chapter Request + const chapterRequest = await this.req.getData(`https://static.crunchyroll.com/skip-events/production/${currentMediaId}.json`, { + headers: api.crunchyDefHeader + }); + if(!chapterRequest.ok || !chapterRequest.res){ + //Old Chapter Request Fallback + console.warn('Chapter request failed, attempting old API'); + const oldChapterRequest = await this.req.getData(`https://static.crunchyroll.com/datalab-intro-v2/${currentMediaId}.json`, { + headers: api.crunchyDefHeader + }); + if(!oldChapterRequest.ok || !oldChapterRequest.res) { + console.warn('Old Chapter API request failed'); + } else { + console.info('Old Chapter request successful'); + const chapterData = await oldChapterRequest.res.json() as CrunchyOldChapter; + + //Generate Timestamps + const startTime = new Date(0), endTime = new Date(0); + startTime.setSeconds(chapterData.startTime); + endTime.setSeconds(chapterData.endTime); + const startTimeMS = String(chapterData.startTime).split('.')[1], endTimeMS = String(chapterData.endTime).split('.')[1]; + 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( - `CHAPTER${(compiledChapters.length/2)+1}=00:00:00.00`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=Prologue` - ); - } - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=${startFormatted}`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=Intro` - ); - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=${endFormatted}`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=Episode` - ); - } - } else { - //Chapter request succeeded, now let's parse them - console.info('Chapter request successful'); - const chapterData = await chapterRequest.res.json() as CrunchyChapters; - const chapters: CrunchyChapter[] = []; + //Push Generated Chapters + if (chapterData.startTime > 1) { + compiledChapters.push( + `CHAPTER${(compiledChapters.length/2)+1}=00:00:00.00`, + `CHAPTER${(compiledChapters.length/2)+1}NAME=Prologue` + ); + } + compiledChapters.push( + `CHAPTER${(compiledChapters.length/2)+1}=${startFormatted}`, + `CHAPTER${(compiledChapters.length/2)+1}NAME=Intro` + ); + compiledChapters.push( + `CHAPTER${(compiledChapters.length/2)+1}=${endFormatted}`, + `CHAPTER${(compiledChapters.length/2)+1}NAME=Episode` + ); + } + } else { + //Chapter request succeeded, now let's parse them + console.info('Chapter request successful'); + const chapterData = await chapterRequest.res.json() as CrunchyChapters; + const chapters: CrunchyChapter[] = []; - //Make a format more usable for the crunchy chapters - for (const chapter in chapterData) { - if (typeof chapterData[chapter] == 'object') { - chapters.push(chapterData[chapter]); - } - } + //Make a format more usable for the crunchy chapters + for (const chapter in chapterData) { + if (typeof chapterData[chapter] == 'object') { + chapters.push(chapterData[chapter]); + } + } - if (chapters.length > 0) { - chapters.sort((a, b) => a.start - b.start); - //Check if chapters has an intro - //if (!(chapters.find(c => c.type === 'intro') || chapters.find(c => c.type === 'recap'))) { - if (!(chapters.find(c => c.type === 'intro'))) { - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=00:00:00.00`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=Episode` - ); - } + if (chapters.length > 0) { + chapters.sort((a, b) => a.start - b.start); + //Check if chapters has an intro + //if (!(chapters.find(c => c.type === 'intro') || chapters.find(c => c.type === 'recap'))) { + if (!(chapters.find(c => c.type === 'intro'))) { + compiledChapters.push( + `CHAPTER${(compiledChapters.length/2)+1}=00:00:00.00`, + `CHAPTER${(compiledChapters.length/2)+1}NAME=Episode` + ); + } - //Loop through all the chapters - for (const chapter of chapters) { - if (typeof chapter.start == 'undefined' || typeof chapter.end == 'undefined') continue; - //Generate timestamps - const startTime = new Date(0), endTime = new Date(0); - startTime.setSeconds(chapter.start); - endTime.setSeconds(chapter.end); - const startFormatted = startTime.toISOString().substring(11, 19)+'.00'; - const endFormatted = endTime.toISOString().substring(11, 19)+'.00'; - //Find the max start time from the chapters - const maxStart = Math.max( - ...chapters - .map(obj => obj.start) - .filter((start): start is number => start !== null && start !== undefined) - ); - //We need the duration of the ep - let epDuration: number | undefined; - const epiMeta = await this.req.getData(`${api.content_cms}/objects/${currentMediaId}?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); - if(!epiMeta.ok || !epiMeta.res){ - epDuration = 7200; - } else { - epDuration = Math.floor((await epiMeta.res.json()).data[0].episode_metadata.duration_ms / 1000 - 3); - } + //Loop through all the chapters + for (const chapter of chapters) { + if (typeof chapter.start == 'undefined' || typeof chapter.end == 'undefined') continue; + //Generate timestamps + const startTime = new Date(0), endTime = new Date(0); + startTime.setSeconds(chapter.start); + endTime.setSeconds(chapter.end); + const startFormatted = startTime.toISOString().substring(11, 19)+'.00'; + const endFormatted = endTime.toISOString().substring(11, 19)+'.00'; + //Find the max start time from the chapters + const maxStart = Math.max( + ...chapters + .map(obj => obj.start) + .filter((start): start is number => start !== null && start !== undefined) + ); + //We need the duration of the ep + let epDuration: number | undefined; + const epiMeta = await this.req.getData(`${api.content_cms}/objects/${currentMediaId}?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); + if(!epiMeta.ok || !epiMeta.res){ + epDuration = 7200; + } else { + epDuration = Math.floor((await epiMeta.res.json()).data[0].episode_metadata.duration_ms / 1000 - 3); + } - //Push generated chapters - if (chapter.type == 'intro') { - if (chapter.start > 0) { - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=00:00:00.00`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=Episode` - ); + //Push generated chapters + if (chapter.type == 'intro') { + if (chapter.start > 0) { + compiledChapters.push( + `CHAPTER${(compiledChapters.length/2)+1}=00:00:00.00`, + `CHAPTER${(compiledChapters.length/2)+1}NAME=Episode` + ); + } + compiledChapters.push( + `CHAPTER${(compiledChapters.length/2)+1}=${startFormatted}`, + `CHAPTER${(compiledChapters.length/2)+1}NAME=${chapter.type.charAt(0).toUpperCase() + chapter.type.slice(1)}` + ); + if (chapter.end < epDuration && chapter.end != maxStart) { + compiledChapters.push( + `CHAPTER${(compiledChapters.length/2)+1}=${endFormatted}`, + `CHAPTER${(compiledChapters.length/2)+1}NAME=Episode` + ); + } + } else { + if (chapter.type !== 'recap') { + compiledChapters.push( + `CHAPTER${(compiledChapters.length/2)+1}=${startFormatted}`, + `CHAPTER${(compiledChapters.length/2)+1}NAME=${chapter.type.charAt(0).toUpperCase() + chapter.type.slice(1)}` + ); + if (chapter.end < epDuration && chapter.end != maxStart) { + compiledChapters.push( + `CHAPTER${(compiledChapters.length/2)+1}=${endFormatted}`, + `CHAPTER${(compiledChapters.length/2)+1}NAME=Episode` + ); + } + } + } + } + } } - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=${startFormatted}`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=${chapter.type.charAt(0).toUpperCase() + chapter.type.slice(1)}` - ); - if (chapter.end < epDuration && chapter.end != maxStart) { - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=${endFormatted}`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=Episode` - ); - } - } else { - if (chapter.type !== 'recap') { - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=${startFormatted}`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=${chapter.type.charAt(0).toUpperCase() + chapter.type.slice(1)}` - ); - if (chapter.end < epDuration && chapter.end != maxStart) { - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=${endFormatted}`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=Episode` - ); - } - } - } } - } - } - } - const pbData = { total: 0, vpb: {}, apb: {}, meta: {} } as PlaybackData; + const pbData = { total: 0, vpb: {}, apb: {}, meta: {} } as PlaybackData; - let videoStream: CrunchyPlayStream | null = null; - let audioStream: CrunchyPlayStream | null = null; - let isDLVideoBypass: boolean = options.vstream === 'android' || options.vstream === 'androidtab' ? true : false; - let isDLAudioBypass: boolean = options.astream === 'android' || options.astream === 'androidtab' ? true : false; - let isDLBypassCapable: boolean = true; + let videoStream: CrunchyPlayStream | null = null; + let audioStream: CrunchyPlayStream | null = null; + let isDLVideoBypass: boolean = options.vstream === 'android' || options.vstream === 'androidtab' ? true : false; + let isDLAudioBypass: boolean = options.astream === 'android' || options.astream === 'androidtab' ? true : false; + let isDLBypassCapable: boolean = true; - if (isDLVideoBypass || isDLAudioBypass) { - const me = await this.req.getData(api.me, AuthHeaders); - if (me.ok && me.res) { - const data_me = await me.res.json(); - const benefits = await this.req.getData(`https://www.crunchyroll.com/subs/v1/subscriptions/${data_me.external_id}/benefits`, AuthHeaders); - if (benefits.ok && benefits.res) { - const data_benefits = await benefits.res.json() as { items: { benefit: string }[] }; - if (data_benefits?.items && !data_benefits.items.find(i => i.benefit === 'offline_viewing')) { - isDLBypassCapable = false; + if (isDLVideoBypass || isDLAudioBypass) { + const me = await this.req.getData(api.me, AuthHeaders); + if (me.ok && me.res) { + const data_me = await me.res.json(); + const benefits = await this.req.getData(`https://www.crunchyroll.com/subs/v1/subscriptions/${data_me.external_id}/benefits`, AuthHeaders); + if (benefits.ok && benefits.res) { + const data_benefits = await benefits.res.json() as { items: { benefit: string }[] }; + if (data_benefits?.items && !data_benefits.items.find(i => i.benefit === 'offline_viewing')) { + isDLBypassCapable = false; + } + } else { + isDLBypassCapable = false; + } + } else { + isDLBypassCapable = false; + } } - } else { - isDLBypassCapable = false; - } - } else { - isDLBypassCapable = false; - } - } - if (isDLVideoBypass && !isDLBypassCapable) { - isDLVideoBypass = false; - options.vstream = 'androidtv'; - console.warn('VBR video downloads are not available on your current Crunchyroll plan. Please upgrade to the "Mega Fan" plan to enable this feature. Falling back to CBR video stream.'); - } + if (isDLVideoBypass && !isDLBypassCapable) { + isDLVideoBypass = false; + options.vstream = 'androidtv'; + console.warn('VBR video downloads are not available on your current Crunchyroll plan. Please upgrade to the "Mega Fan" plan to enable this feature. Falling back to CBR video stream.'); + } - if (isDLAudioBypass && !isDLBypassCapable) { - isDLAudioBypass = false; - options.astream = 'androidtv'; - console.warn('192 kb/s audio downloads are not available on your current Crunchyroll plan. Please upgrade to the "Mega Fan" plan to enable this feature. Falling back to 128 kb/s CBR stream.'); - } + if (isDLAudioBypass && !isDLBypassCapable) { + isDLAudioBypass = false; + options.astream = 'androidtv'; + console.warn('192 kb/s audio downloads are not available on your current Crunchyroll plan. Please upgrade to the "Mega Fan" plan to enable this feature. Falling back to 128 kb/s CBR stream.'); + } - if (options.tsd) { - console.warn('Total Session Death Active'); - const activeStreamsReq = await this.req.getData(api.streaming_sessions, AuthHeaders); - if (activeStreamsReq.ok && activeStreamsReq.res){ - const data = await activeStreamsReq.res.json(); - for (const s of data.items) { - await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${s.contentId}/${s.token}`, {...{method: 'DELETE'}, ...AuthHeaders}); - } - console.warn(`Killed ${data.items?.length ?? 0} Sessions`); - } - } + if (options.tsd) { + console.warn('Total Session Death Active'); + const activeStreamsReq = await this.req.getData(api.streaming_sessions, AuthHeaders); + if (activeStreamsReq.ok && activeStreamsReq.res){ + const data = await activeStreamsReq.res.json(); + for (const s of data.items) { + await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${s.contentId}/${s.token}`, {...{method: 'DELETE'}, ...AuthHeaders}); + } + console.warn(`Killed ${data.items?.length ?? 0} Sessions`); + } + } - const videoPlaybackReq = await this.req.getData(`https://www.crunchyroll.com/playback/v3/${currentVersion ? currentVersion.guid : currentMediaId}/${CrunchyVideoPlayStreams['androidtv']}/play?queue=0`, AuthHeaders); - if (!videoPlaybackReq.ok || !videoPlaybackReq.res) { - console.warn('Request Video Stream URLs FAILED!'); - } else { - videoStream = await videoPlaybackReq.res.json() as CrunchyPlayStream; - const derivedPlaystreams = {} as CrunchyStreams; - for (const hardsub in videoStream.hardSubs) { - const stream = videoStream.hardSubs[hardsub]; - derivedPlaystreams[hardsub] = { - url: stream.url, - 'hardsub_locale': stream.hlang - }; - } - if (isDLVideoBypass) { - const videoDLReq = await this.req.getData(`https://www.crunchyroll.com/playback/v3/${currentVersion ? currentVersion.guid : currentMediaId}/${CrunchyVideoPlayStreams[options.vstream]}/download`, AuthHeaders); - if (videoDLReq.ok && videoDLReq.res) { - const data = await videoDLReq.res.json() as CrunchyPlayStream; - derivedPlaystreams[''] = { - url: this.convertDownloadToPlayback(data.url, videoStream.url), - hardsub_locale: '' - }; - } else { - derivedPlaystreams[''] = { - url: videoStream.url, - hardsub_locale: '' - }; - } - } else { - derivedPlaystreams[''] = { - url: videoStream.url, - hardsub_locale: '' - }; - } - pbData.meta = { - audio_locale: videoStream.audioLocale, - bifs: [videoStream.bifs], - captions: videoStream.captions, - closed_captions: videoStream.captions, - media_id: videoStream.assetId, - subtitles: videoStream.subtitles, - versions: videoStream.versions - }; - pbData.vpb[`adaptive_${options.vstream}_${videoStream.url.includes('m3u8') ? 'hls' : 'dash'}_drm`] = { - ...derivedPlaystreams - }; - } + const videoPlaybackReq = await this.req.getData(`https://www.crunchyroll.com/playback/v3/${currentVersion ? currentVersion.guid : currentMediaId}/${CrunchyVideoPlayStreams['androidtv']}/play?queue=0`, AuthHeaders); + if (!videoPlaybackReq.ok || !videoPlaybackReq.res) { + console.warn('Request Video Stream URLs FAILED!'); + } else { + videoStream = await videoPlaybackReq.res.json() as CrunchyPlayStream; + const derivedPlaystreams = {} as CrunchyStreams; + for (const hardsub in videoStream.hardSubs) { + const stream = videoStream.hardSubs[hardsub]; + derivedPlaystreams[hardsub] = { + url: stream.url, + 'hardsub_locale': stream.hlang + }; + } + if (isDLVideoBypass) { + const videoDLReq = await this.req.getData(`https://www.crunchyroll.com/playback/v3/${currentVersion ? currentVersion.guid : currentMediaId}/${CrunchyVideoPlayStreams[options.vstream]}/download`, AuthHeaders); + if (videoDLReq.ok && videoDLReq.res) { + const data = await videoDLReq.res.json() as CrunchyPlayStream; + derivedPlaystreams[''] = { + url: this.convertDownloadToPlayback(data.url, videoStream.url), + hardsub_locale: '' + }; + } else { + derivedPlaystreams[''] = { + url: videoStream.url, + hardsub_locale: '' + }; + } + } else { + derivedPlaystreams[''] = { + url: videoStream.url, + hardsub_locale: '' + }; + } + pbData.meta = { + audio_locale: videoStream.audioLocale, + bifs: [videoStream.bifs], + captions: videoStream.captions, + closed_captions: videoStream.captions, + media_id: videoStream.assetId, + subtitles: videoStream.subtitles, + versions: videoStream.versions + }; + pbData.vpb[`adaptive_${options.vstream}_${videoStream.url.includes('m3u8') ? 'hls' : 'dash'}_drm`] = { + ...derivedPlaystreams + }; + } - if (!options.cstream && (options.vstream !== options.astream) && videoStream) { - const audioPlaybackReq = await this.req.getData(`https://www.crunchyroll.com/playback/v3/${currentVersion ? currentVersion.guid : currentMediaId}/${CrunchyAudioPlayStreams[options.astream]}/${isDLAudioBypass ? 'download' : 'play?queue=1'}`, AuthHeaders); - if (!audioPlaybackReq.ok || !audioPlaybackReq.res) { - console.warn('Request Audio Stream URLs FAILED!'); - } else { - audioStream = await audioPlaybackReq.res.json() as CrunchyPlayStream; - const derivedPlaystreams = {} as CrunchyStreams; - for (const hardsub in audioStream.hardSubs) { - const stream = audioStream.hardSubs[hardsub]; - derivedPlaystreams[hardsub] = { - url: stream.url, - 'hardsub_locale': stream.hlang - }; - } - if (isDLAudioBypass) { - audioStream.token = videoStream.token; - derivedPlaystreams[''] = { - url: this.convertDownloadToPlayback(audioStream.url, videoStream.url), - hardsub_locale: '' - }; - } else { - derivedPlaystreams[''] = { - url: audioStream.url, - hardsub_locale: '' - }; - }; - pbData.meta = { - audio_locale: audioStream.audioLocale, - bifs: [audioStream.bifs], - captions: audioStream.captions, - closed_captions: audioStream.captions, - media_id: audioStream.assetId, - subtitles: audioStream.subtitles, - versions: audioStream.versions - }; - pbData.apb[`adaptive_${options.astream}_${audioStream.url.includes('m3u8') ? 'hls' : 'dash'}_drm`] = { - ...derivedPlaystreams - }; - } - } else { - pbData.apb = pbData.vpb; - } + if (!options.cstream && (options.vstream !== options.astream) && videoStream) { + const audioPlaybackReq = await this.req.getData(`https://www.crunchyroll.com/playback/v3/${currentVersion ? currentVersion.guid : currentMediaId}/${CrunchyAudioPlayStreams[options.astream]}/${isDLAudioBypass ? 'download' : 'play?queue=1'}`, AuthHeaders); + if (!audioPlaybackReq.ok || !audioPlaybackReq.res) { + console.warn('Request Audio Stream URLs FAILED!'); + } else { + audioStream = await audioPlaybackReq.res.json() as CrunchyPlayStream; + const derivedPlaystreams = {} as CrunchyStreams; + for (const hardsub in audioStream.hardSubs) { + const stream = audioStream.hardSubs[hardsub]; + derivedPlaystreams[hardsub] = { + url: stream.url, + 'hardsub_locale': stream.hlang + }; + } + if (isDLAudioBypass) { + audioStream.token = videoStream.token; + derivedPlaystreams[''] = { + url: this.convertDownloadToPlayback(audioStream.url, videoStream.url), + hardsub_locale: '' + }; + } else { + derivedPlaystreams[''] = { + url: audioStream.url, + hardsub_locale: '' + }; + }; + pbData.meta = { + audio_locale: audioStream.audioLocale, + bifs: [audioStream.bifs], + captions: audioStream.captions, + closed_captions: audioStream.captions, + media_id: audioStream.assetId, + subtitles: audioStream.subtitles, + versions: audioStream.versions + }; + pbData.apb[`adaptive_${options.astream}_${audioStream.url.includes('m3u8') ? 'hls' : 'dash'}_drm`] = { + ...derivedPlaystreams + }; + } + } else { + pbData.apb = pbData.vpb; + } - variables.push(...([ - ['title', medias.episodeTitle, true], - ['episode', isNaN(parseFloat(medias.episodeNumber)) ? medias.episodeNumber : parseFloat(medias.episodeNumber), false], - ['service', 'CR', false], - ['seriesTitle', medias.seriesTitle, true], - ['showTitle', medias.seriesTitle ?? medias.seasonTitle, true], - ['season', medias.season, false] - ] as [AvailableFilenameVars, string|number, boolean][]).map((a): Variable => { - return { - name: a[0], - replaceWith: a[1], - type: typeof a[1], - sanitize: a[2] - } as Variable; - })); + variables.push(...([ + ['title', medias.episodeTitle, true], + ['episode', isNaN(parseFloat(medias.episodeNumber)) ? medias.episodeNumber : parseFloat(medias.episodeNumber), false], + ['service', 'CR', false], + ['seriesTitle', medias.seriesTitle, true], + ['showTitle', medias.seriesTitle ?? medias.seasonTitle, true], + ['season', medias.season, false] + ] as [AvailableFilenameVars, string|number, boolean][]).map((a): Variable => { + return { + name: a[0], + replaceWith: a[1], + type: typeof a[1], + sanitize: a[2] + } as Variable; + })); - let vstreams: any[] = []; - let astreams: any[] = []; - let hsLangs: string[] = []; - const vpbStreams = pbData.vpb; - const apbStreams = pbData.apb; + let vstreams: any[] = []; + let astreams: any[] = []; + let hsLangs: string[] = []; + const vpbStreams = pbData.vpb; + const apbStreams = pbData.apb; - if (!canDecrypt && (!options.novids || !options.noaudio)) { - console.error('No valid Widevine or PlayReady CDM detected. Please ensure a supported and functional CDM is installed.'); - return undefined; - } + if (!canDecrypt && (!options.novids || !options.noaudio)) { + console.error('No valid Widevine or PlayReady CDM detected. Please ensure a supported and functional CDM is installed.'); + return undefined; + } - if (!this.cfg.bin.mp4decrypt && !this.cfg.bin.shaka && (!options.novids || !options.noaudio)) { - console.error('Neither Shaka nor MP4Decrypt found. Please ensure at least one of them is installed.'); - return undefined; - } + if (!this.cfg.bin.mp4decrypt && !this.cfg.bin.shaka && (!options.novids || !options.noaudio)) { + console.error('Neither Shaka nor MP4Decrypt found. Please ensure at least one of them is installed.'); + return undefined; + } - for (const s of Object.keys(pbData.vpb)) { - if ( - (s.match(/hls/) || s.match(/dash/)) + for (const s of Object.keys(pbData.vpb)) { + if ( + (s.match(/hls/) || s.match(/dash/)) && !(s.match(/hls/) && s.match(/drm/)) && !s.match(/trailer/) - ) { - const pb = Object.values(vpbStreams[s]).map(v => { - v.hardsub_lang = v.hardsub_locale - ? langsData.fixAndFindCrLC(v.hardsub_locale).locale - : v.hardsub_locale; - if(v.hardsub_lang && hsLangs.indexOf(v.hardsub_lang) < 0){ - hsLangs.push(v.hardsub_lang); + ) { + const pb = Object.values(vpbStreams[s]).map(v => { + v.hardsub_lang = v.hardsub_locale + ? langsData.fixAndFindCrLC(v.hardsub_locale).locale + : v.hardsub_locale; + if(v.hardsub_lang && hsLangs.indexOf(v.hardsub_lang) < 0){ + hsLangs.push(v.hardsub_lang); + } + return { + ...v, + ...{ format: s } + }; + }); + vstreams.push(...pb); + } } - return { - ...v, - ...{ format: s } - }; - }); - vstreams.push(...pb); - } - } - for (const s of Object.keys(pbData.apb)) { - if ( - (s.match(/hls/) || s.match(/dash/)) + for (const s of Object.keys(pbData.apb)) { + if ( + (s.match(/hls/) || s.match(/dash/)) && !(s.match(/hls/) && s.match(/drm/)) && !s.match(/trailer/) - ) { - const pb = Object.values(apbStreams[s]).map(v => { - v.hardsub_lang = v.hardsub_locale - ? langsData.fixAndFindCrLC(v.hardsub_locale).locale - : v.hardsub_locale; - if(v.hardsub_lang && hsLangs.indexOf(v.hardsub_lang) < 0){ - hsLangs.push(v.hardsub_lang); + ) { + const pb = Object.values(apbStreams[s]).map(v => { + v.hardsub_lang = v.hardsub_locale + ? langsData.fixAndFindCrLC(v.hardsub_locale).locale + : v.hardsub_locale; + if(v.hardsub_lang && hsLangs.indexOf(v.hardsub_lang) < 0){ + hsLangs.push(v.hardsub_lang); + } + return { + ...v, + ...{ format: s } + }; + }); + astreams.push(...pb); + } } - return { - ...v, - ...{ format: s } - }; - }); - astreams.push(...pb); - } - } - if (vstreams.length < 1) { - console.warn('No full video streams found!'); - return undefined; - } - - if (astreams.length < 1) { - console.warn('No full audio streams found!'); - return undefined; - } - - const audDub = langsData.findLang(langsData.fixLanguageTag(pbData.meta.audio_locale as string) || '').code; - hsLangs = langsData.sortTags(hsLangs); - - vstreams = vstreams.map((s) => { - s.audio_lang = audDub; - s.hardsub_lang = s.hardsub_lang ? s.hardsub_lang : '-'; - s.type = `${s.format}/${s.audio_lang}/${s.hardsub_lang}`; - return s; - }); - - vstreams = vstreams.sort((a, b) => { - if (a.type < b.type) { - return -1; - } - return 0; - }); - - astreams = astreams.map((s) => { - s.audio_lang = audDub; - s.hardsub_lang = s.hardsub_lang ? s.hardsub_lang : '-'; - s.type = `${s.format}/${s.audio_lang}/${s.hardsub_lang}`; - return s; - }); - - astreams = astreams.sort((a, b) => { - if (a.type < b.type) { - return -1; - } - return 0; - }); - - if(options.hslang != 'none'){ - if(hsLangs.indexOf(options.hslang) > -1){ - console.info('Selecting stream with %s hardsubs', langsData.locale2language(options.hslang).language); - vstreams = vstreams.filter((s) => { - if(s.hardsub_lang == '-'){ - return false; + if (vstreams.length < 1) { + console.warn('No full video streams found!'); + return undefined; } - return s.hardsub_lang == options.hslang; - }); - astreams = astreams.filter((s) => { - if(s.hardsub_lang == '-'){ - return false; - } - return s.hardsub_lang == options.hslang; - }); - } - else{ - console.warn('Selected stream with %s hardsubs not available', langsData.locale2language(options.hslang).language); - if(hsLangs.length > 0){ - console.warn('Try other hardsubs stream:', hsLangs.join(', ')); - } - dlFailed = true; - } - } else { - vstreams = vstreams.filter((s) => { - return s.hardsub_lang == '-'; - }); - astreams = astreams.filter((s) => { - return s.hardsub_lang == '-'; - }); - if(vstreams.length < 1){ - console.warn('Raw video streams not available!'); - if(hsLangs.length > 0){ - console.warn('Try hardsubs stream:', hsLangs.join(', ')); - } - dlFailed = true; - } - if(astreams.length < 1){ - console.warn('Raw audio streams not available!'); - if(hsLangs.length > 0){ - console.warn('Try hardsubs stream:', hsLangs.join(', ')); - } - dlFailed = true; - } - console.info('Selecting raw stream'); - } - let vcurStream: + if (astreams.length < 1) { + console.warn('No full audio streams found!'); + return undefined; + } + + const audDub = langsData.findLang(langsData.fixLanguageTag(pbData.meta.audio_locale as string) || '').code; + hsLangs = langsData.sortTags(hsLangs); + + vstreams = vstreams.map((s) => { + s.audio_lang = audDub; + s.hardsub_lang = s.hardsub_lang ? s.hardsub_lang : '-'; + s.type = `${s.format}/${s.audio_lang}/${s.hardsub_lang}`; + return s; + }); + + vstreams = vstreams.sort((a, b) => { + if (a.type < b.type) { + return -1; + } + return 0; + }); + + astreams = astreams.map((s) => { + s.audio_lang = audDub; + s.hardsub_lang = s.hardsub_lang ? s.hardsub_lang : '-'; + s.type = `${s.format}/${s.audio_lang}/${s.hardsub_lang}`; + return s; + }); + + astreams = astreams.sort((a, b) => { + if (a.type < b.type) { + return -1; + } + return 0; + }); + + if(options.hslang != 'none'){ + if(hsLangs.indexOf(options.hslang) > -1){ + console.info('Selecting stream with %s hardsubs', langsData.locale2language(options.hslang).language); + vstreams = vstreams.filter((s) => { + if(s.hardsub_lang == '-'){ + return false; + } + return s.hardsub_lang == options.hslang; + }); + astreams = astreams.filter((s) => { + if(s.hardsub_lang == '-'){ + return false; + } + return s.hardsub_lang == options.hslang; + }); + } + else{ + console.warn('Selected stream with %s hardsubs not available', langsData.locale2language(options.hslang).language); + if(hsLangs.length > 0){ + console.warn('Try other hardsubs stream:', hsLangs.join(', ')); + } + dlFailed = true; + } + } else { + vstreams = vstreams.filter((s) => { + return s.hardsub_lang == '-'; + }); + astreams = astreams.filter((s) => { + return s.hardsub_lang == '-'; + }); + if(vstreams.length < 1){ + console.warn('Raw video streams not available!'); + if(hsLangs.length > 0){ + console.warn('Try hardsubs stream:', hsLangs.join(', ')); + } + dlFailed = true; + } + if(astreams.length < 1){ + console.warn('Raw audio streams not available!'); + if(hsLangs.length > 0){ + console.warn('Try hardsubs stream:', hsLangs.join(', ')); + } + dlFailed = true; + } + console.info('Selecting raw stream'); + } + + let vcurStream: undefined|typeof vstreams[0] = undefined; - let acurStream: + let acurStream: undefined|typeof astreams[0] = undefined; - if (!dlFailed) { - console.info('Downloading...'); - vcurStream = vstreams[0]; - acurStream = astreams[0]; + if (!dlFailed) { + console.info('Downloading...'); + vcurStream = vstreams[0]; + acurStream = astreams[0]; - console.info('Video Playlists URL: %s (%s)', vcurStream.url, vcurStream.type); - console.info('Audio Playlists URL: %s (%s)', acurStream.url, acurStream.type); - } - - let tsFile = undefined; - - // Delete the stream if it's not needed - if (options.novids && options.noaudio) { - if (videoStream) { - await this.refreshToken(true, true); - await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${videoStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders}); - } - if (audioStream && (videoStream?.token !== audioStream.token)) { - await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${audioStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders}); - } - } - - if(!dlFailed && vcurStream && acurStream && vcurStream !== undefined && acurStream !== undefined && !(options.novids && options.noaudio)){ - const vstreamPlaylistsReq = await this.req.getData(vcurStream.url, AuthHeaders); - const astreamPlaylistsReq = vcurStream.url !== acurStream.url ? await this.req.getData(acurStream.url, AuthHeaders) : vstreamPlaylistsReq; - if(!vstreamPlaylistsReq.ok || !vstreamPlaylistsReq.res || !astreamPlaylistsReq.ok || !astreamPlaylistsReq.res){ - console.error('CAN\'T FETCH VIDEO PLAYLISTS!'); - dlFailed = true; - } else { - const vstreamPlaylistBody = await vstreamPlaylistsReq.res.text(); - const astreamPlaylistBody = vcurStream.url !== acurStream.url ? await astreamPlaylistsReq.res.text() : vstreamPlaylistBody; - if (vstreamPlaylistBody.match('MPD') && astreamPlaylistBody.match('MPD')) { - //Parse MPD Playlists - const vstreamPlaylists = await parse(vstreamPlaylistBody, langsData.findLang(langsData.fixLanguageTag(pbData.meta.audio_locale as string) || ''), vcurStream.url.match(/.*\.urlset\//)?.[0]); - const astreamPlaylists = vcurStream.url !== acurStream.url ? await parse(astreamPlaylistBody, langsData.findLang(langsData.fixLanguageTag(pbData.meta.audio_locale as string) || ''), acurStream.url.match(/.*\.urlset\//)?.[0]) : vstreamPlaylists; - - //Get name of CDNs/Servers - const vstreamServers = Object.keys(vstreamPlaylists); - const astreamServers = Object.keys(astreamPlaylists); - - options.x = options.x > vstreamServers.length ? 1 : options.x; - - const vselectedServer = vstreamServers[options.x - 1]; - const vselectedList = vstreamPlaylists[vselectedServer]; - - const aselectedServer = astreamServers[options.x - 1]; - const aselectedList = astreamPlaylists[aselectedServer]; - - //set Video Qualities - const videos = vselectedList.video.map(item => { - return { - ...item, - resolutionText: `${item.quality.width}x${item.quality.height} (${Math.round(item.bandwidth/1024)}KiB/s)` - }; - }); - - const audios = aselectedList.audio.map(item => { - return { - ...item, - resolutionText: `${Math.round(item.bandwidth/1000)}kB/s` - }; - }); - - - videos.sort((a, b) => { - return a.quality.width - b.quality.width; - }); - - audios.sort((a, b) => { - return a.bandwidth - b.bandwidth; - }); - - let chosenVideoQuality = options.q === 0 ? videos.length : options.q; - if(chosenVideoQuality > videos.length) { - console.warn(`The requested quality of ${options.q} is greater than the maximum ${videos.length}.\n[WARN] Therefor the maximum will be capped at ${videos.length}.`); - chosenVideoQuality = videos.length; + console.info('Video Playlists URL: %s (%s)', vcurStream.url, vcurStream.type); + console.info('Audio Playlists URL: %s (%s)', acurStream.url, acurStream.type); } - chosenVideoQuality--; - let chosenAudioQuality = options.q === 0 ? audios.length : options.q; - if(chosenAudioQuality > audios.length) { - chosenAudioQuality = audios.length; + let tsFile = undefined; + + // Delete the stream if it's not needed + if (options.novids && options.noaudio) { + if (videoStream) { + await this.refreshToken(true, true); + await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${videoStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders}); + } + if (audioStream && (videoStream?.token !== audioStream.token)) { + await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${audioStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders}); + } } - chosenAudioQuality--; + + if(!dlFailed && vcurStream && acurStream && vcurStream !== undefined && acurStream !== undefined && !(options.novids && options.noaudio)){ + const vstreamPlaylistsReq = await this.req.getData(vcurStream.url, AuthHeaders); + const astreamPlaylistsReq = vcurStream.url !== acurStream.url ? await this.req.getData(acurStream.url, AuthHeaders) : vstreamPlaylistsReq; + if(!vstreamPlaylistsReq.ok || !vstreamPlaylistsReq.res || !astreamPlaylistsReq.ok || !astreamPlaylistsReq.res){ + console.error('CAN\'T FETCH VIDEO PLAYLISTS!'); + dlFailed = true; + } else { + const vstreamPlaylistBody = await vstreamPlaylistsReq.res.text(); + const astreamPlaylistBody = vcurStream.url !== acurStream.url ? await astreamPlaylistsReq.res.text() : vstreamPlaylistBody; + if (vstreamPlaylistBody.match('MPD') && astreamPlaylistBody.match('MPD')) { + //Parse MPD Playlists + const vstreamPlaylists = await parse(vstreamPlaylistBody, langsData.findLang(langsData.fixLanguageTag(pbData.meta.audio_locale as string) || ''), vcurStream.url.match(/.*\.urlset\//)?.[0]); + const astreamPlaylists = vcurStream.url !== acurStream.url ? await parse(astreamPlaylistBody, langsData.findLang(langsData.fixLanguageTag(pbData.meta.audio_locale as string) || ''), acurStream.url.match(/.*\.urlset\//)?.[0]) : vstreamPlaylists; + + //Get name of CDNs/Servers + const vstreamServers = Object.keys(vstreamPlaylists); + const astreamServers = Object.keys(astreamPlaylists); + + options.x = options.x > vstreamServers.length ? 1 : options.x; + + const vselectedServer = vstreamServers[options.x - 1]; + const vselectedList = vstreamPlaylists[vselectedServer]; + + const aselectedServer = astreamServers[options.x - 1]; + const aselectedList = astreamPlaylists[aselectedServer]; + + //set Video Qualities + const videos = vselectedList.video.map(item => { + return { + ...item, + resolutionText: `${item.quality.width}x${item.quality.height} (${Math.round(item.bandwidth/1024)}KiB/s)` + }; + }); + + const audios = aselectedList.audio.map(item => { + return { + ...item, + resolutionText: `${Math.round(item.bandwidth/1000)}kB/s` + }; + }); - const chosenVideoSegments = videos[chosenVideoQuality]; - const chosenAudioSegments = audios[chosenAudioQuality]; + videos.sort((a, b) => { + return a.quality.width - b.quality.width; + }); - console.info(`Available Video Qualities:\n\t${videos.map((a, ind) => `[${ind+1}] ${a.resolutionText}`).join('\n\t')}`); - console.info(`Available Audio Qualities:\n\t${audios.map((a, ind) => `[${ind+1}] ${a.resolutionText}`).join('\n\t')}`); + audios.sort((a, b) => { + return a.bandwidth - b.bandwidth; + }); - variables.push({ - name: 'height', - type: 'number', - replaceWith: chosenVideoSegments.quality.height - }, { - name: 'width', - type: 'number', - replaceWith: chosenVideoSegments.quality.width - }); + let chosenVideoQuality = options.q === 0 ? videos.length : options.q; + if(chosenVideoQuality > videos.length) { + console.warn(`The requested quality of ${options.q} is greater than the maximum ${videos.length}.\n[WARN] Therefor the maximum will be capped at ${videos.length}.`); + chosenVideoQuality = videos.length; + } + chosenVideoQuality--; - const lang = langsData.languages.find(a => a.code === acurStream?.audio_lang); - if (!lang) { - console.error(`Unable to find language for code ${acurStream.audio_lang}`); - return; - } - console.info(`Selected quality: \n\tVideo: ${chosenVideoSegments.resolutionText}\n\tAudio: ${chosenAudioSegments.resolutionText}\n\tVideo Server: ${vselectedServer}\n\tAudio Server: ${aselectedServer}`); - console.info('Stream URL:', chosenVideoSegments.segments[0].uri.split(',.urlset')[0]); - // TODO check filename - fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); - const outFile = parseFileName(options.fileName + '.' + (mMeta.lang?.name || lang.name), variables, options.numbers, options.override).join(path.sep); - const tempFile = parseFileName(`temp-${currentVersion ? currentVersion.guid : currentMediaId}`, variables, options.numbers, options.override).join(path.sep); - const tempTsFile = path.isAbsolute(tempFile as string) ? tempFile : path.join(this.cfg.dir.content, tempFile); + let chosenAudioQuality = options.q === 0 ? audios.length : options.q; + if(chosenAudioQuality > audios.length) { + chosenAudioQuality = audios.length; + } + chosenAudioQuality--; - let encryptionKeysVideo; - let encryptionKeysAudio; + + const chosenVideoSegments = videos[chosenVideoQuality]; + const chosenAudioSegments = audios[chosenAudioQuality]; + + console.info(`Available Video Qualities:\n\t${videos.map((a, ind) => `[${ind+1}] ${a.resolutionText}`).join('\n\t')}`); + console.info(`Available Audio Qualities:\n\t${audios.map((a, ind) => `[${ind+1}] ${a.resolutionText}`).join('\n\t')}`); + + variables.push({ + name: 'height', + type: 'number', + replaceWith: chosenVideoSegments.quality.height + }, { + name: 'width', + type: 'number', + replaceWith: chosenVideoSegments.quality.width + }); + + const lang = langsData.languages.find(a => a.code === acurStream?.audio_lang); + if (!lang) { + console.error(`Unable to find language for code ${acurStream.audio_lang}`); + return; + } + console.info(`Selected quality: \n\tVideo: ${chosenVideoSegments.resolutionText}\n\tAudio: ${chosenAudioSegments.resolutionText}\n\tVideo Server: ${vselectedServer}\n\tAudio Server: ${aselectedServer}`); + console.info('Stream URL:', chosenVideoSegments.segments[0].uri.split(',.urlset')[0]); + // TODO check filename + fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); + const outFile = parseFileName(options.fileName + '.' + (mMeta.lang?.name || lang.name), variables, options.numbers, options.override).join(path.sep); + const tempFile = parseFileName(`temp-${currentVersion ? currentVersion.guid : currentMediaId}`, variables, options.numbers, options.override).join(path.sep); + const tempTsFile = path.isAbsolute(tempFile as string) ? tempFile : path.join(this.cfg.dir.content, tempFile); + + let encryptionKeysVideo; + let encryptionKeysAudio; - //Handle Getting Decryption Keys if needed - if ((chosenVideoSegments.pssh_wvd ||chosenVideoSegments.pssh_prd || chosenAudioSegments.pssh_wvd || chosenAudioSegments.pssh_prd)) { - await this.refreshToken(true, true); - if (videoStream) { - await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${videoStream.token}/keepAlive?playhead=1`, {...{method: 'PATCH'}, ...AuthHeaders}); - } - if (audioStream && (videoStream?.token !== audioStream.token)) { - await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${audioStream.token}/keepAlive?playhead=1`, {...{method: 'PATCH'}, ...AuthHeaders}); - } + //Handle Getting Decryption Keys if needed + if ((chosenVideoSegments.pssh_wvd ||chosenVideoSegments.pssh_prd || chosenAudioSegments.pssh_wvd || chosenAudioSegments.pssh_prd)) { + await this.refreshToken(true, true); + if (videoStream) { + await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${videoStream.token}/keepAlive?playhead=1`, {...{method: 'PATCH'}, ...AuthHeaders}); + } + if (audioStream && (videoStream?.token !== audioStream.token)) { + await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${audioStream.token}/keepAlive?playhead=1`, {...{method: 'PATCH'}, ...AuthHeaders}); + } - console.info(`Getting decryption keys with ${cdm}`); - // New Crunchyroll DRM endpoint for Widevine - if (cdm === 'widevine') { - encryptionKeysVideo = await getKeysWVD(chosenVideoSegments.pssh_wvd, api.drm_widevine, { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader, - Pragma: 'no-cache', - 'Cache-Control': 'no-cache', - 'content-type': 'application/octet-stream', - 'x-cr-content-id': currentVersion ? currentVersion.guid : currentMediaId, - 'x-cr-video-token': videoStream!.token - }); + console.info(`Getting decryption keys with ${cdm}`); + // New Crunchyroll DRM endpoint for Widevine + if (cdm === 'widevine') { + encryptionKeysVideo = await getKeysWVD(chosenVideoSegments.pssh_wvd, api.drm_widevine, { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader, + Pragma: 'no-cache', + 'Cache-Control': 'no-cache', + 'content-type': 'application/octet-stream', + 'x-cr-content-id': currentVersion ? currentVersion.guid : currentMediaId, + 'x-cr-video-token': videoStream!.token + }); - // Check if the audio pssh is different since Crunchyroll started to have different dec keys for audio tracks - if (chosenAudioSegments.pssh_wvd && chosenAudioSegments.pssh_wvd !== chosenVideoSegments.pssh_wvd) { - encryptionKeysAudio = await getKeysWVD(chosenAudioSegments.pssh_wvd, api.drm_widevine, { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader, - Pragma: 'no-cache', - 'Cache-Control': 'no-cache', - 'content-type': 'application/octet-stream', - 'x-cr-content-id': currentVersion ? currentVersion.guid : currentMediaId, - 'x-cr-video-token': audioStream!.token - }); - } else { - encryptionKeysAudio = encryptionKeysVideo; - } - } + // Check if the audio pssh is different since Crunchyroll started to have different dec keys for audio tracks + if (chosenAudioSegments.pssh_wvd && chosenAudioSegments.pssh_wvd !== chosenVideoSegments.pssh_wvd) { + encryptionKeysAudio = await getKeysWVD(chosenAudioSegments.pssh_wvd, api.drm_widevine, { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader, + Pragma: 'no-cache', + 'Cache-Control': 'no-cache', + 'content-type': 'application/octet-stream', + 'x-cr-content-id': currentVersion ? currentVersion.guid : currentMediaId, + 'x-cr-video-token': audioStream!.token + }); + } else { + encryptionKeysAudio = encryptionKeysVideo; + } + } - // New Crunchyroll DRM endpoint for Playready - if (cdm === 'playready') { - encryptionKeysVideo = await getKeysPRD(chosenVideoSegments.pssh_prd, api.drm_playready, { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader, - Pragma: 'no-cache', - 'Cache-Control': 'no-cache', - 'content-type': 'application/octet-stream', - 'x-cr-content-id': currentVersion ? currentVersion.guid : currentMediaId, - 'x-cr-video-token': videoStream!.token - }); + // New Crunchyroll DRM endpoint for Playready + if (cdm === 'playready') { + encryptionKeysVideo = await getKeysPRD(chosenVideoSegments.pssh_prd, api.drm_playready, { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader, + Pragma: 'no-cache', + 'Cache-Control': 'no-cache', + 'content-type': 'application/octet-stream', + 'x-cr-content-id': currentVersion ? currentVersion.guid : currentMediaId, + 'x-cr-video-token': videoStream!.token + }); - // Check if the audio pssh is different since Crunchyroll started to have different dec keys for audio tracks - if (chosenAudioSegments.pssh_prd && chosenAudioSegments.pssh_prd !== chosenVideoSegments.pssh_prd) { - encryptionKeysAudio = await getKeysPRD(chosenAudioSegments.pssh_prd, api.drm_playready, { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader, - Pragma: 'no-cache', - 'Cache-Control': 'no-cache', - 'content-type': 'application/octet-stream', - 'x-cr-content-id': currentVersion ? currentVersion.guid : currentMediaId, - 'x-cr-video-token': audioStream!.token - }); - } else { - encryptionKeysAudio = encryptionKeysVideo; - } - } + // Check if the audio pssh is different since Crunchyroll started to have different dec keys for audio tracks + if (chosenAudioSegments.pssh_prd && chosenAudioSegments.pssh_prd !== chosenVideoSegments.pssh_prd) { + encryptionKeysAudio = await getKeysPRD(chosenAudioSegments.pssh_prd, api.drm_playready, { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader, + Pragma: 'no-cache', + 'Cache-Control': 'no-cache', + 'content-type': 'application/octet-stream', + 'x-cr-content-id': currentVersion ? currentVersion.guid : currentMediaId, + 'x-cr-video-token': audioStream!.token + }); + } else { + encryptionKeysAudio = encryptionKeysVideo; + } + } - if (!encryptionKeysVideo || encryptionKeysVideo.length == 0 || !encryptionKeysAudio || encryptionKeysAudio.length == 0) { - console.error('Failed to get encryption keys'); - return undefined; - } + if (!encryptionKeysVideo || encryptionKeysVideo.length == 0 || !encryptionKeysAudio || encryptionKeysAudio.length == 0) { + console.error('Failed to get encryption keys'); + return undefined; + } - console.info('Got decryption keys'); - } + console.info('Got decryption keys'); + } - if (videoStream) { - await this.refreshToken(true, true); - await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${videoStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders}); - } - if (audioStream && (videoStream?.token !== audioStream.token)) { - await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${audioStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders}); - } + if (videoStream) { + await this.refreshToken(true, true); + await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${videoStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders}); + } + if (audioStream && (videoStream?.token !== audioStream.token)) { + await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${audioStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders}); + } - let [audioDownloaded, videoDownloaded] = [false, false]; + let [audioDownloaded, videoDownloaded] = [false, false]; - // When best selected video quality is already downloaded - if(dlVideoOnce && options.dlVideoOnce) { - console.info('Already downloaded video, skipping video download...'); - } else if (options.novids) { - console.info('Skipping video download...'); - } else { - //Download Video - const totalParts = chosenVideoSegments.segments.length; - const mathParts = Math.ceil(totalParts / options.partsize); - const mathMsg = `(${mathParts}*${options.partsize})`; - console.info('Total parts in video stream:', totalParts, mathMsg); - tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); - const dirName = path.dirname(tsFile); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }); - } - const videoJson: M3U8Json = { - segments: chosenVideoSegments.segments - }; - const videoDownload = await new streamdl({ - output: chosenVideoSegments.pssh_wvd || chosenVideoSegments.pssh_prd ? `${tempTsFile}.video.enc.m4s` : `${tsFile}.video.m4s`, - 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; - } - dlVideoOnce = true; - videoDownloaded = true; - } + // When best selected video quality is already downloaded + if(dlVideoOnce && options.dlVideoOnce) { + console.info('Already downloaded video, skipping video download...'); + } else if (options.novids) { + console.info('Skipping video download...'); + } else { + //Download Video + const totalParts = chosenVideoSegments.segments.length; + const mathParts = Math.ceil(totalParts / options.partsize); + const mathMsg = `(${mathParts}*${options.partsize})`; + console.info('Total parts in video stream:', totalParts, mathMsg); + tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); + const dirName = path.dirname(tsFile); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + const videoJson: M3U8Json = { + segments: chosenVideoSegments.segments + }; + const videoDownload = await new streamdl({ + output: chosenVideoSegments.pssh_wvd || chosenVideoSegments.pssh_prd ? `${tempTsFile}.video.enc.m4s` : `${tsFile}.video.m4s`, + 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; + } + dlVideoOnce = true; + videoDownloaded = true; + } - if (chosenAudioSegments && !options.noaudio) { - //Download Audio (if available) - const totalParts = chosenAudioSegments.segments.length; - const mathParts = Math.ceil(totalParts / options.partsize); - const mathMsg = `(${mathParts}*${options.partsize})`; - console.info('Total parts in audio stream:', totalParts, mathMsg); - tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); - const dirName = path.dirname(tsFile); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }); - } - const audioJson: M3U8Json = { - segments: chosenAudioSegments.segments - }; - const audioDownload = await new streamdl({ - output: chosenVideoSegments.pssh_wvd || chosenVideoSegments.pssh_prd ? `${tempTsFile}.audio.enc.m4s` : `${tsFile}.audio.m4s`, - 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; - } - audioDownloaded = true; - } else if (options.noaudio) { - console.info('Skipping audio download...'); - } + if (chosenAudioSegments && !options.noaudio) { + //Download Audio (if available) + const totalParts = chosenAudioSegments.segments.length; + const mathParts = Math.ceil(totalParts / options.partsize); + const mathMsg = `(${mathParts}*${options.partsize})`; + console.info('Total parts in audio stream:', totalParts, mathMsg); + tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); + const dirName = path.dirname(tsFile); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + const audioJson: M3U8Json = { + segments: chosenAudioSegments.segments + }; + const audioDownload = await new streamdl({ + output: chosenVideoSegments.pssh_wvd || chosenVideoSegments.pssh_prd ? `${tempTsFile}.audio.enc.m4s` : `${tsFile}.audio.m4s`, + 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; + } + audioDownloaded = true; + } else if (options.noaudio) { + console.info('Skipping audio download...'); + } - //Handle Decryption if needed - if ((chosenVideoSegments.pssh_wvd || chosenVideoSegments.pssh_prd || chosenAudioSegments.pssh_wvd || chosenAudioSegments.pssh_prd) && (videoDownloaded || audioDownloaded) && !dlFailed) { - console.info('Decryption Needed, attempting to decrypt'); - if (this.cfg.bin.mp4decrypt || this.cfg.bin.shaka) { - let commandBaseVideo = `--show-progress --key ${encryptionKeysVideo?.[cdm === 'playready' ? 0 : 1].kid}:${encryptionKeysVideo?.[cdm === 'playready' ? 0 : 1].key} `; - let commandBaseAudio = `--show-progress --key ${encryptionKeysAudio?.[cdm === 'playready' ? 0 : 1].kid}:${encryptionKeysAudio?.[cdm === 'playready' ? 0 : 1].key} `; - let commandVideo = commandBaseVideo+`"${tempTsFile}.video.enc.m4s" "${tempTsFile}.video.m4s"`; - let commandAudio = commandBaseAudio+`"${tempTsFile}.audio.enc.m4s" "${tempTsFile}.audio.m4s"`; + //Handle Decryption if needed + if ((chosenVideoSegments.pssh_wvd || chosenVideoSegments.pssh_prd || chosenAudioSegments.pssh_wvd || chosenAudioSegments.pssh_prd) && (videoDownloaded || audioDownloaded) && !dlFailed) { + console.info('Decryption Needed, attempting to decrypt'); + if (this.cfg.bin.mp4decrypt || this.cfg.bin.shaka) { + let commandBaseVideo = `--show-progress --key ${encryptionKeysVideo?.[cdm === 'playready' ? 0 : 1].kid}:${encryptionKeysVideo?.[cdm === 'playready' ? 0 : 1].key} `; + let commandBaseAudio = `--show-progress --key ${encryptionKeysAudio?.[cdm === 'playready' ? 0 : 1].kid}:${encryptionKeysAudio?.[cdm === 'playready' ? 0 : 1].key} `; + let commandVideo = commandBaseVideo+`"${tempTsFile}.video.enc.m4s" "${tempTsFile}.video.m4s"`; + let commandAudio = commandBaseAudio+`"${tempTsFile}.audio.enc.m4s" "${tempTsFile}.audio.m4s"`; - if (this.cfg.bin.shaka) { - commandBaseVideo = ` --enable_raw_key_decryption ${encryptionKeysVideo?.map(kb => '--keys key_id='+kb.kid+':key='+kb.key).join(' ')}`; - commandBaseAudio = ` --enable_raw_key_decryption ${encryptionKeysAudio?.map(kb => '--keys key_id='+kb.kid+':key='+kb.key).join(' ')}`; - commandVideo = `input="${tempTsFile}.video.enc.m4s",stream=video,output="${tempTsFile}.video.m4s"`+commandBaseVideo; - commandAudio = `input="${tempTsFile}.audio.enc.m4s",stream=audio,output="${tempTsFile}.audio.m4s"`+commandBaseAudio; - } + if (this.cfg.bin.shaka) { + commandBaseVideo = ` --enable_raw_key_decryption ${encryptionKeysVideo?.map(kb => '--keys key_id='+kb.kid+':key='+kb.key).join(' ')}`; + commandBaseAudio = ` --enable_raw_key_decryption ${encryptionKeysAudio?.map(kb => '--keys key_id='+kb.kid+':key='+kb.key).join(' ')}`; + commandVideo = `input="${tempTsFile}.video.enc.m4s",stream=video,output="${tempTsFile}.video.m4s"`+commandBaseVideo; + commandAudio = `input="${tempTsFile}.audio.enc.m4s",stream=audio,output="${tempTsFile}.audio.m4s"`+commandBaseAudio; + } - if (videoDownloaded) { - console.info('Started decrypting video,', this.cfg.bin.shaka ? 'using shaka' : 'using mp4decrypt'); - const decryptVideo = Helper.exec(this.cfg.bin.shaka ? 'shaka-packager' : 'mp4decrypt', this.cfg.bin.shaka ? `"${this.cfg.bin.shaka}"` : `"${this.cfg.bin.mp4decrypt}"`, commandVideo); - if (!decryptVideo.isOk) { - console.error(decryptVideo.err); - console.error(`Decryption failed with exit code ${decryptVideo.err.code}`); - fs.renameSync(`${tempTsFile}.video.enc.m4s`, `${tsFile}.video.enc.m4s`); - return undefined; - } else { - console.info('Decryption done for video'); - if (!options.nocleanup) { - fs.removeSync(`${tempTsFile}.video.enc.m4s`); - } - fs.copyFileSync(`${tempTsFile}.video.m4s`, `${tsFile}.video.m4s`); - fs.unlinkSync(`${tempTsFile}.video.m4s`); - files.push({ - type: 'Video', - path: `${tsFile}.video.m4s`, - lang: lang, - isPrimary: isPrimary - }); - } - } + if (videoDownloaded) { + console.info('Started decrypting video,', this.cfg.bin.shaka ? 'using shaka' : 'using mp4decrypt'); + const decryptVideo = Helper.exec(this.cfg.bin.shaka ? 'shaka-packager' : 'mp4decrypt', this.cfg.bin.shaka ? `"${this.cfg.bin.shaka}"` : `"${this.cfg.bin.mp4decrypt}"`, commandVideo); + if (!decryptVideo.isOk) { + console.error(decryptVideo.err); + console.error(`Decryption failed with exit code ${decryptVideo.err.code}`); + fs.renameSync(`${tempTsFile}.video.enc.m4s`, `${tsFile}.video.enc.m4s`); + return undefined; + } else { + console.info('Decryption done for video'); + if (!options.nocleanup) { + fs.removeSync(`${tempTsFile}.video.enc.m4s`); + } + fs.copyFileSync(`${tempTsFile}.video.m4s`, `${tsFile}.video.m4s`); + fs.unlinkSync(`${tempTsFile}.video.m4s`); + files.push({ + type: 'Video', + path: `${tsFile}.video.m4s`, + lang: lang, + isPrimary: isPrimary + }); + } + } - if (audioDownloaded) { - console.info('Started decrypting audio,', this.cfg.bin.shaka ? 'using shaka' : 'using mp4decrypt'); - const decryptAudio = Helper.exec(this.cfg.bin.shaka ? 'shaka-packager' : 'mp4decrypt', this.cfg.bin.shaka ? `"${this.cfg.bin.shaka}"` : `"${this.cfg.bin.mp4decrypt}"`, commandAudio); - if (!decryptAudio.isOk) { - console.error(decryptAudio.err); - console.error(`Decryption failed with exit code ${decryptAudio.err.code}`); - fs.renameSync(`${tempTsFile}.audio.enc.m4s`, `${tsFile}.audio.enc.m4s`); - return undefined; - } else { - if (!options.nocleanup) { - fs.removeSync(`${tempTsFile}.audio.enc.m4s`); - } - fs.copyFileSync(`${tempTsFile}.audio.m4s`, `${tsFile}.audio.m4s`); - fs.unlinkSync(`${tempTsFile}.audio.m4s`); - files.push({ - type: 'Audio', - path: `${tsFile}.audio.m4s`, - lang: lang, - isPrimary: isPrimary - }); - console.info('Decryption done for audio'); - } - } - } else { - console.warn('mp4decrypt/shaka not found, files need decryption. Decryption Keys:', encryptionKeysVideo, encryptionKeysAudio); - } - } else if (dlFailed) { - console.error('Download failed, skipping decryption'); - } else { - if (videoDownloaded) { - files.push({ - type: 'Video', - path: `${tsFile}.video.m4s`, - lang: lang, - isPrimary: isPrimary - }); - } - if (audioDownloaded) { - files.push({ - type: 'Audio', - path: `${tsFile}.audio.m4s`, - lang: lang, - isPrimary: isPrimary - }); - } - } - } else if (!options.novids) { - const streamPlaylists = m3u8(vstreamPlaylistBody); - const plServerList: string[] = [], - plStreams: Record> = {}, - plQuality: { + if (audioDownloaded) { + console.info('Started decrypting audio,', this.cfg.bin.shaka ? 'using shaka' : 'using mp4decrypt'); + const decryptAudio = Helper.exec(this.cfg.bin.shaka ? 'shaka-packager' : 'mp4decrypt', this.cfg.bin.shaka ? `"${this.cfg.bin.shaka}"` : `"${this.cfg.bin.mp4decrypt}"`, commandAudio); + if (!decryptAudio.isOk) { + console.error(decryptAudio.err); + console.error(`Decryption failed with exit code ${decryptAudio.err.code}`); + fs.renameSync(`${tempTsFile}.audio.enc.m4s`, `${tsFile}.audio.enc.m4s`); + return undefined; + } else { + if (!options.nocleanup) { + fs.removeSync(`${tempTsFile}.audio.enc.m4s`); + } + fs.copyFileSync(`${tempTsFile}.audio.m4s`, `${tsFile}.audio.m4s`); + fs.unlinkSync(`${tempTsFile}.audio.m4s`); + files.push({ + type: 'Audio', + path: `${tsFile}.audio.m4s`, + lang: lang, + isPrimary: isPrimary + }); + console.info('Decryption done for audio'); + } + } + } else { + console.warn('mp4decrypt/shaka not found, files need decryption. Decryption Keys:', encryptionKeysVideo, encryptionKeysAudio); + } + } else if (dlFailed) { + console.error('Download failed, skipping decryption'); + } else { + if (videoDownloaded) { + files.push({ + type: 'Video', + path: `${tsFile}.video.m4s`, + lang: lang, + isPrimary: isPrimary + }); + } + if (audioDownloaded) { + files.push({ + type: 'Audio', + path: `${tsFile}.audio.m4s`, + lang: lang, + isPrimary: isPrimary + }); + } + } + } else if (!options.novids) { + const streamPlaylists = m3u8(vstreamPlaylistBody); + const plServerList: string[] = [], + plStreams: Record> = {}, + plQuality: { str: string, dim: string, CODECS: string, @@ -2345,723 +2345,723 @@ export default class Crunchy implements ServiceClass { height: number } }[] = []; - for(const pl of streamPlaylists.playlists){ - // set quality - const plResolution = pl.attributes.RESOLUTION; - const plResolutionText = `${plResolution.width}x${plResolution.height}`; - // set codecs - const plCodecs = pl.attributes.CODECS; - // parse uri - const plUri = new URL(pl.uri); - let plServer = plUri.hostname; - // set server list - if (plUri.searchParams.get('cdn')){ - plServer += ` (${plUri.searchParams.get('cdn')})`; - } - if (!plServerList.includes(plServer)){ - plServerList.push(plServer); - } - // add to server - if (!Object.keys(plStreams).includes(plServer)){ - plStreams[plServer] = {}; - } - if( - plStreams[plServer][plResolutionText] + for(const pl of streamPlaylists.playlists){ + // set quality + const plResolution = pl.attributes.RESOLUTION; + const plResolutionText = `${plResolution.width}x${plResolution.height}`; + // set codecs + const plCodecs = pl.attributes.CODECS; + // parse uri + const plUri = new URL(pl.uri); + let plServer = plUri.hostname; + // set server list + if (plUri.searchParams.get('cdn')){ + plServer += ` (${plUri.searchParams.get('cdn')})`; + } + if (!plServerList.includes(plServer)){ + plServerList.push(plServer); + } + // add to server + if (!Object.keys(plStreams).includes(plServer)){ + plStreams[plServer] = {}; + } + if( + plStreams[plServer][plResolutionText] && plStreams[plServer][plResolutionText] != pl.uri && typeof plStreams[plServer][plResolutionText] != 'undefined' - ) { - console.error(`Non duplicate url for ${plServer} detected, please report to developer!`); - } else{ - plStreams[plServer][plResolutionText] = pl.uri; - } - // 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 qualityStrMatch = !plQuality.map(a => a.str).join('\r\n').match(qualityStrRegx); - if(qualityStrMatch){ - plQuality.push({ - str: qualityStrAdd, - dim: plResolutionText, - CODECS: plCodecs, - RESOLUTION: plResolution - }); - } + ) { + console.error(`Non duplicate url for ${plServer} detected, please report to developer!`); + } else{ + plStreams[plServer][plResolutionText] = pl.uri; + } + // 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 qualityStrMatch = !plQuality.map(a => a.str).join('\r\n').match(qualityStrRegx); + if(qualityStrMatch){ + plQuality.push({ + str: qualityStrAdd, + dim: plResolutionText, + CODECS: plCodecs, + RESOLUTION: plResolution + }); + } + } + + const plSelectedServer = plServerList[0]; + const plSelectedList = plStreams[plSelectedServer]; + plQuality.sort((a, b) => { + const aMatch: RegExpMatchArray | never[] = a.dim.match(/[0-9]+/) || []; + const bMatch: RegExpMatchArray | never[] = b.dim.match(/[0-9]+/) || []; + return parseInt(aMatch[0]) - parseInt(bMatch[0]); + }); + let quality = options.q === 0 ? plQuality.length : options.q; + if(quality > plQuality.length) { + console.warn(`The requested quality of ${options.q} is greater than the maximum ${plQuality.length}.\n[WARN] Therefor the maximum will be capped at ${plQuality.length}.`); + quality = plQuality.length; + } + // When best selected video quality is already downloaded + if(dlVideoOnce && options.dlVideoOnce) { + // Select the lowest resolution with the same codecs + while(quality !=1 && plQuality[quality - 1].CODECS == plQuality[quality - 2].CODECS) { + quality--; + } + } + const selPlUrl = plSelectedList[plQuality.map(a => a.dim)[quality - 1]] ? plSelectedList[plQuality.map(a => a.dim)[quality - 1]] : ''; + console.info(`Servers available:\n\t${plServerList.join('\n\t')}`); + console.info(`Available qualities:\n\t${plQuality.map((a, ind) => `[${ind+1}] ${a.str}`).join('\n\t')}`); + + if(selPlUrl != ''){ + variables.push({ + name: 'height', + type: 'number', + replaceWith: quality === 0 ? plQuality[plQuality.length - 1].RESOLUTION.height as number : plQuality[quality - 1].RESOLUTION.height + }, { + name: 'width', + type: 'number', + replaceWith: quality === 0 ? plQuality[plQuality.length - 1].RESOLUTION.width as number : plQuality[quality - 1].RESOLUTION.width + }); + const lang = langsData.languages.find(a => a.code === vcurStream?.audio_lang); + if (!lang) { + console.error(`Unable to find language for code ${vcurStream.audio_lang}`); + return; + } + console.info(`Selected quality: ${Object.keys(plSelectedList).find(a => plSelectedList[a] === selPlUrl)} @ ${plSelectedServer}`); + console.info('Stream URL:', selPlUrl); + // TODO check filename + fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); + const outFile = parseFileName(options.fileName + '.' + (mMeta.lang?.name || lang.name), variables, options.numbers, options.override).join(path.sep); + console.info(`Output filename: ${outFile}`); + const chunkPage = await this.req.getData(selPlUrl, { + headers: api.crunchyDefHeader, + }); + if(!chunkPage.ok || !chunkPage.res){ + console.error('CAN\'T FETCH VIDEO PLAYLIST!'); + dlFailed = true; + } else { + // We have the stream, so go ahead and delete the active stream + if (videoStream) { + await this.refreshToken(true, true); + await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${videoStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders}); + } + if (audioStream && (videoStream?.token !== audioStream.token)) { + await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${audioStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders}); + } + + const chunkPageBody = await chunkPage.res.text(); + const chunkPlaylist = m3u8(chunkPageBody); + const totalParts = chunkPlaylist.segments.length; + const mathParts = Math.ceil(totalParts / options.partsize); + const mathMsg = `(${mathParts}*${options.partsize})`; + console.info('Total parts in stream:', totalParts, mathMsg); + tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); + const dirName = path.dirname(tsFile); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + const dlStreamByPl = await new streamdl({ + output: `${tsFile}.ts`, + 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; + } + files.push({ + type: 'Video', + path: `${tsFile}.ts`, + lang: lang, + isPrimary: isPrimary + }); + dlVideoOnce = true; + } + } else{ + console.error('Quality not selected!\n'); + dlFailed = true; + } + } else if (options.novids) { + fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); + console.info('Downloading skipped!'); + } + } + } else if (options.novids && options.noaudio) { + fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); } - const plSelectedServer = plServerList[0]; - const plSelectedList = plStreams[plSelectedServer]; - plQuality.sort((a, b) => { - const aMatch: RegExpMatchArray | never[] = a.dim.match(/[0-9]+/) || []; - const bMatch: RegExpMatchArray | never[] = b.dim.match(/[0-9]+/) || []; - return parseInt(aMatch[0]) - parseInt(bMatch[0]); - }); - let quality = options.q === 0 ? plQuality.length : options.q; - if(quality > plQuality.length) { - console.warn(`The requested quality of ${options.q} is greater than the maximum ${plQuality.length}.\n[WARN] Therefor the maximum will be capped at ${plQuality.length}.`); - quality = plQuality.length; + if (compiledChapters.length > 0) { + try { + fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); + const outFile = parseFileName(options.fileName + '.' + mMeta.lang?.name, variables, options.numbers, options.override).join(path.sep); + tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); + const dirName = path.dirname(tsFile); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + const lang = langsData.languages.find(a => a.code === vcurStream?.audio_lang); + if (!lang) { + console.error(`Unable to find language for code ${vcurStream.audio_lang}`); + return; + } + fs.writeFileSync(`${tsFile}.txt`, compiledChapters.join('\r\n')); + files.push({ + path: `${tsFile}.txt`, + lang: lang, + type: 'Chapters' + }); + } catch { + console.error('Failed to write chapter file'); + } } - // When best selected video quality is already downloaded - if(dlVideoOnce && options.dlVideoOnce) { - // Select the lowest resolution with the same codecs - while(quality !=1 && plQuality[quality - 1].CODECS == plQuality[quality - 2].CODECS) { - quality--; - } + + if(options.dlsubs.indexOf('all') > -1){ + options.dlsubs = ['all']; } - const selPlUrl = plSelectedList[plQuality.map(a => a.dim)[quality - 1]] ? plSelectedList[plQuality.map(a => a.dim)[quality - 1]] : ''; - console.info(`Servers available:\n\t${plServerList.join('\n\t')}`); - console.info(`Available qualities:\n\t${plQuality.map((a, ind) => `[${ind+1}] ${a.str}`).join('\n\t')}`); - if(selPlUrl != ''){ - variables.push({ - name: 'height', - type: 'number', - replaceWith: quality === 0 ? plQuality[plQuality.length - 1].RESOLUTION.height as number : plQuality[quality - 1].RESOLUTION.height - }, { - name: 'width', - type: 'number', - replaceWith: quality === 0 ? plQuality[plQuality.length - 1].RESOLUTION.width as number : plQuality[quality - 1].RESOLUTION.width - }); - const lang = langsData.languages.find(a => a.code === vcurStream?.audio_lang); - if (!lang) { - console.error(`Unable to find language for code ${vcurStream.audio_lang}`); - return; - } - console.info(`Selected quality: ${Object.keys(plSelectedList).find(a => plSelectedList[a] === selPlUrl)} @ ${plSelectedServer}`); - console.info('Stream URL:', selPlUrl); - // TODO check filename - fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); - const outFile = parseFileName(options.fileName + '.' + (mMeta.lang?.name || lang.name), variables, options.numbers, options.override).join(path.sep); - console.info(`Output filename: ${outFile}`); - const chunkPage = await this.req.getData(selPlUrl, { - headers: api.crunchyDefHeader, - }); - if(!chunkPage.ok || !chunkPage.res){ - console.error('CAN\'T FETCH VIDEO PLAYLIST!'); - dlFailed = true; - } else { - // We have the stream, so go ahead and delete the active stream - if (videoStream) { - await this.refreshToken(true, true); - await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${videoStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders}); - } - if (audioStream && (videoStream?.token !== audioStream.token)) { - await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${audioStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders}); - } + if(options.hslang != 'none'){ + console.warn('Subtitles downloading disabled for hardsubs streams.'); + options.skipsubs = true; + } - const chunkPageBody = await chunkPage.res.text(); - const chunkPlaylist = m3u8(chunkPageBody); - const totalParts = chunkPlaylist.segments.length; - const mathParts = Math.ceil(totalParts / options.partsize); - const mathMsg = `(${mathParts}*${options.partsize})`; - console.info('Total parts in stream:', totalParts, mathMsg); - tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); - const dirName = path.dirname(tsFile); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: 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) || (pbData.meta.closed_captions && Object.values(pbData.meta.closed_captions).length > 0)) { + 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); + return { + ...s, + isCC: false, + locale: subLang, + language: subLang.locale + }; + }).concat( + capsData.map((s) => { + const subLang = langsData.fixAndFindCrLC(s.language); + return { + ...s, + isCC: true, + locale: subLang, + language: subLang.locale + }; + }) + ); + const subsArr = langsData.sortSubtitles(subsDataMapped, 'language'); + for(const subsIndex in subsArr){ + const subsItem = subsArr[subsIndex]; + const langItem = subsItem.locale; + const sxData: Partial = {}; + sxData.language = langItem; + const isSigns = langItem.code === audDub && !subsItem.isCC; + const isCC = subsItem.isCC; + sxData.file = langsData.subsFile(fileName as string, subsIndex, langItem, isCC, options.ccTag, isSigns, subsItem.format); + if (path.isAbsolute(sxData.file)) { + sxData.path = sxData.file; + } else { + sxData.path = path.join(this.cfg.dir.content, sxData.file); + } + const dirName = path.dirname(sxData.path); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + if (files.some(a => a.type === 'Subtitle' && (a.language.cr_locale == langItem.cr_locale || a.language.locale == langItem.locale) && a.cc === isCC && a.signs === isSigns)) + continue; + if(options.dlsubs.includes('all') || options.dlsubs.includes(langItem.locale)){ + const subsAssReq = await this.req.getData(subsItem.url, { + headers: api.crunchyDefHeader + }); + if(subsAssReq.ok && subsAssReq.res){ + let sBody = await subsAssReq.res.text(); + if (subsItem.format == 'vtt') { + const chosenFontSize = options.originalFontSize ? undefined : options.fontSize; + if (!options.originalFontSize) sBody = sBody.replace(/( font-size:.+?;)/g, '').replace(/(font-size:.+?;)/g, ''); + sBody = vtt2ass(undefined, chosenFontSize, sBody, '', undefined, options.fontName); + sxData.fonts = fontsData.assFonts(sBody) as Font[]; + sxData.file = sxData.file.replace('.vtt','.ass'); + } else { + sBody = '\ufeff' + sBody; + const sBodySplit = sBody.split('\r\n'); + sBodySplit.splice(2, 0, 'ScaledBorderAndShadow: yes'); + sBody = sBodySplit.join('\r\n'); + sxData.title = sBody.split('\r\n')[1].replace(/^Title: /, ''); + sxData.title = `${langItem.language} / ${sxData.title}`; + sxData.fonts = fontsData.assFonts(sBody) as Font[]; + } + fs.writeFileSync(sxData.path, sBody); + console.info(`Subtitle downloaded: ${sxData.file}`); + files.push({ + type: 'Subtitle', + ...sxData as sxItem, + cc: isCC, + signs: isSigns, + }); + } + else{ + console.warn(`Failed to download subtitle: ${sxData.file}`); + } + } + } } - const dlStreamByPl = await new streamdl({ - output: `${tsFile}.ts`, - 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; + else{ + console.warn('Can\'t find urls for subtitles!'); } - files.push({ - type: 'Video', - path: `${tsFile}.ts`, - lang: lang, - isPrimary: isPrimary - }); - dlVideoOnce = true; - } } else{ - console.error('Quality not selected!\n'); - dlFailed = true; + console.info('Subtitles downloading skipped!'); } - } else if (options.novids) { - fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); - console.info('Downloading skipped!'); - } - } - } else if (options.novids && options.noaudio) { - fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); - } - if (compiledChapters.length > 0) { - try { - fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); - const outFile = parseFileName(options.fileName + '.' + mMeta.lang?.name, variables, options.numbers, options.override).join(path.sep); - tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); - const dirName = path.dirname(tsFile); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }); - } - const lang = langsData.languages.find(a => a.code === vcurStream?.audio_lang); - if (!lang) { - console.error(`Unable to find language for code ${vcurStream.audio_lang}`); + await this.sleep(options.waittime); + } + return { + error: dlFailed, + data: files, + fileName: fileName ? (path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName)) || './unknown' : './unknown' + }; + } + + public async muxStreams(data: DownloadedMedia[], options: CrunchyMuxOptions) { + this.cfg.bin = await yamlCfg.loadBinCfg(); + let hasAudioStreams = false; + if (options.novids || data.filter(a => a.type === 'Video').length === 0) + return console.info('Skip muxing since no vids are downloaded'); + if (data.some(a => a.type === 'Audio')) { + hasAudioStreams = true; + } + const merger = new Merger({ + onlyVid: hasAudioStreams ? data.filter(a => a.type === 'Video').map((a) : MergerInput => { + return { + lang: a.lang, + path: a.path, + }; + }) : [], + skipSubMux: options.skipSubMux, + onlyAudio: hasAudioStreams ? data.filter(a => a.type === 'Audio').map((a) : MergerInput => { + return { + lang: a.lang, + path: a.path, + }; + }) : [], + output: `${options.output}.${options.mp4 ? 'mp4' : 'mkv'}`, + subtitles: data.filter(a => a.type === 'Subtitle').map((a) : SubtitleInput => { + return { + file: a.path, + language: a.language, + closedCaption: a.cc, + signs: a.signs, + }; + }), + simul: false, + 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 => { + return { + lang: a.lang, + path: a.path, + }; + }), + chapters: data.filter(a => a.type === 'Chapters').map((a) : MergerInput => { + return { + path: a.path, + lang: a.lang + }; + }), + videoTitle: options.videoTitle, + options: { + ffmpeg: options.ffmpegOptions, + mkvmerge: options.mkvmergeOptions + }, + defaults: { + audio: options.defaultAudio, + sub: options.defaultSub + }, + ccTag: options.ccTag + }); + const bin = Merger.checkMerger(this.cfg.bin, options.mp4, options.forceMuxer); + // collect fonts info + // mergers + let isMuxed = false; + if (options.syncTiming) { + await merger.createDelays(); + } + if (bin.MKVmerge) { + await merger.merge('mkvmerge', bin.MKVmerge); + isMuxed = true; + } else if (bin.FFmpeg) { + await merger.merge('ffmpeg', bin.FFmpeg); + isMuxed = true; + } else{ + console.info('\nDone!\n'); return; - } - fs.writeFileSync(`${tsFile}.txt`, compiledChapters.join('\r\n')); - files.push({ - path: `${tsFile}.txt`, - lang: lang, - type: 'Chapters' - }); - } catch { - console.error('Failed to write chapter file'); } - } - - if(options.dlsubs.indexOf('all') > -1){ - options.dlsubs = ['all']; - } - - if(options.hslang != 'none'){ - 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) || (pbData.meta.closed_captions && Object.values(pbData.meta.closed_captions).length > 0)) { - 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); - return { - ...s, - isCC: false, - locale: subLang, - language: subLang.locale - }; - }).concat( - capsData.map((s) => { - const subLang = langsData.fixAndFindCrLC(s.language); - return { - ...s, - isCC: true, - locale: subLang, - language: subLang.locale - }; - }) - ); - const subsArr = langsData.sortSubtitles(subsDataMapped, 'language'); - for(const subsIndex in subsArr){ - const subsItem = subsArr[subsIndex]; - const langItem = subsItem.locale; - const sxData: Partial = {}; - sxData.language = langItem; - const isSigns = langItem.code === audDub && !subsItem.isCC; - const isCC = subsItem.isCC; - sxData.file = langsData.subsFile(fileName as string, subsIndex, langItem, isCC, options.ccTag, isSigns, subsItem.format); - if (path.isAbsolute(sxData.file)) { - sxData.path = sxData.file; - } else { - sxData.path = path.join(this.cfg.dir.content, sxData.file); - } - const dirName = path.dirname(sxData.path); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }); - } - if (files.some(a => a.type === 'Subtitle' && (a.language.cr_locale == langItem.cr_locale || a.language.locale == langItem.locale) && a.cc === isCC && a.signs === isSigns)) - continue; - if(options.dlsubs.includes('all') || options.dlsubs.includes(langItem.locale)){ - const subsAssReq = await this.req.getData(subsItem.url, { - headers: api.crunchyDefHeader - }); - if(subsAssReq.ok && subsAssReq.res){ - let sBody = await subsAssReq.res.text(); - if (subsItem.format == 'vtt') { - const chosenFontSize = options.originalFontSize ? undefined : options.fontSize; - if (!options.originalFontSize) sBody = sBody.replace(/( font-size:.+?;)/g, '').replace(/(font-size:.+?;)/g, ''); - sBody = vtt2ass(undefined, chosenFontSize, sBody, '', undefined, options.fontName); - sxData.fonts = fontsData.assFonts(sBody) as Font[]; - sxData.file = sxData.file.replace('.vtt','.ass'); - } else { - sBody = '\ufeff' + sBody; - const sBodySplit = sBody.split('\r\n'); - sBodySplit.splice(2, 0, 'ScaledBorderAndShadow: yes'); - sBody = sBodySplit.join('\r\n'); - sxData.title = sBody.split('\r\n')[1].replace(/^Title: /, ''); - sxData.title = `${langItem.language} / ${sxData.title}`; - sxData.fonts = fontsData.assFonts(sBody) as Font[]; - } - fs.writeFileSync(sxData.path, sBody); - console.info(`Subtitle downloaded: ${sxData.file}`); - files.push({ - type: 'Subtitle', - ...sxData as sxItem, - cc: isCC, - signs: isSigns, - }); - } - else{ - console.warn(`Failed to download subtitle: ${sxData.file}`); - } - } - } - } - else{ - console.warn('Can\'t find urls for subtitles!'); - } - } else{ - console.info('Subtitles downloading skipped!'); - } - - await this.sleep(options.waittime); + if (isMuxed && !options.nocleanup) + merger.cleanUp(); } - return { - error: dlFailed, - data: files, - fileName: fileName ? (path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName)) || './unknown' : './unknown' - }; - } - public async muxStreams(data: DownloadedMedia[], options: CrunchyMuxOptions) { - this.cfg.bin = await yamlCfg.loadBinCfg(); - let hasAudioStreams = false; - if (options.novids || data.filter(a => a.type === 'Video').length === 0) - return console.info('Skip muxing since no vids are downloaded'); - if (data.some(a => a.type === 'Audio')) { - hasAudioStreams = true; - } - const merger = new Merger({ - onlyVid: hasAudioStreams ? data.filter(a => a.type === 'Video').map((a) : MergerInput => { - return { - lang: a.lang, - path: a.path, - }; - }) : [], - skipSubMux: options.skipSubMux, - onlyAudio: hasAudioStreams ? data.filter(a => a.type === 'Audio').map((a) : MergerInput => { - return { - lang: a.lang, - path: a.path, - }; - }) : [], - output: `${options.output}.${options.mp4 ? 'mp4' : 'mkv'}`, - subtitles: data.filter(a => a.type === 'Subtitle').map((a) : SubtitleInput => { - return { - file: a.path, - language: a.language, - closedCaption: a.cc, - signs: a.signs, - }; - }), - simul: false, - 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 => { - return { - lang: a.lang, - path: a.path, - }; - }), - chapters: data.filter(a => a.type === 'Chapters').map((a) : MergerInput => { - return { - path: a.path, - lang: a.lang - }; - }), - videoTitle: options.videoTitle, - options: { - ffmpeg: options.ffmpegOptions, - mkvmerge: options.mkvmergeOptions - }, - defaults: { - audio: options.defaultAudio, - sub: options.defaultSub - }, - ccTag: options.ccTag - }); - const bin = Merger.checkMerger(this.cfg.bin, options.mp4, options.forceMuxer); - // collect fonts info - // mergers - let isMuxed = false; - if (options.syncTiming) { - await merger.createDelays(); - } - if (bin.MKVmerge) { - await merger.merge('mkvmerge', bin.MKVmerge); - isMuxed = true; - } else if (bin.FFmpeg) { - await merger.merge('ffmpeg', bin.FFmpeg); - isMuxed = true; - } else{ - console.info('\nDone!\n'); - return; - } - if (isMuxed && !options.nocleanup) - merger.cleanUp(); - } - - public async listSeriesID(id: string, data?: CrunchyMultiDownload): Promise<{ list: Episode[], data: Record}> { - await this.refreshToken(true, true); - let serieshasversions = true; - const parsed = await this.parseSeriesById(id); - if (!parsed) - return { data: {}, list: [] }; - const result = this.parseSeriesResult(parsed); - const episodes : Record = {}; - for(const season of Object.keys(result) as unknown as number[]) { - for (const key of Object.keys(result[season])) { - const s = result[season][key]; - if (data?.s && s.id !== data.s) continue; - (await this.getSeasonDataById(s))?.data?.forEach(episode => { - //TODO: Make sure the below code is ok - //Prepare the episode array - let item; - const seasonIdentifier = s.identifier ? s.identifier.split('|')[1] : `S${episode.season_number}`; - if (!(Object.prototype.hasOwnProperty.call(episodes, `${seasonIdentifier}E${episode.episode || episode.episode_number}`))) { - item = episodes[`${seasonIdentifier}E${episode.episode || episode.episode_number}`] = { - items: [] as CrunchyEpisode[], - langs: [] as langsData.LanguageItem[] + for(const season of Object.keys(result) as unknown as number[]) { + for (const key of Object.keys(result[season])) { + const s = result[season][key]; + if (data?.s && s.id !== data.s) continue; + (await this.getSeasonDataById(s))?.data?.forEach(episode => { + //TODO: Make sure the below code is ok + //Prepare the episode array + let item; + const seasonIdentifier = s.identifier ? s.identifier.split('|')[1] : `S${episode.season_number}`; + if (!(Object.prototype.hasOwnProperty.call(episodes, `${seasonIdentifier}E${episode.episode || episode.episode_number}`))) { + item = episodes[`${seasonIdentifier}E${episode.episode || episode.episode_number}`] = { + items: [] as CrunchyEpisode[], + langs: [] as langsData.LanguageItem[] + }; + } else { + item = episodes[`${seasonIdentifier}E${episode.episode || episode.episode_number}`]; + } + + if (episode.versions) { + //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)) { + //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); + } + } + //Sort audio tracks according to the order of languages passed to the 'dubLang' option + const argv = yargs.appArgv(this.cfg.cli); + if(!argv.allDubs) { + item.langs.sort((a,b) => argv.dubLang.indexOf(a.code) - argv.dubLang.indexOf(b.code)); + } + } else { + //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)) { + //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); + } + } + }); + } + } + + const itemIndexes = { + sp: 1, + no: 1 + }; + + for (const key of Object.keys(episodes)) { + const item = episodes[key]; + const isSpecial = !item.items[0].episode.match(/^\d+$/); + episodes[`${isSpecial ? 'S' : 'E'}${itemIndexes[isSpecial ? 'sp' : 'no']}`] = item; + if (isSpecial) + itemIndexes.sp++; + else + itemIndexes.no++; + delete episodes[key]; + } + + // Sort episodes to have specials at the end + const specials = Object.entries(episodes).filter(a => a[0].startsWith('S')), + normal = Object.entries(episodes).filter(a => a[0].startsWith('E')), + sortedEpisodes = Object.fromEntries([...normal, ...specials]); + + for (const key of Object.keys(sortedEpisodes)) { + const item = sortedEpisodes[key]; + const epNum = key.startsWith('E') ? (`E${data?.absolute ? (item.items[0].episode_number?.toString() || item.items[0].episode) : key.slice(1)}`) : key; + console.info(`[${data?.absolute ? epNum : key}] [${item.items[0].upload_date ? new Date(item.items[0].upload_date).toISOString().slice(0, 10) : '0000-00-00'}] ${ + 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 ?? 'Unknown'}`; + }).join(', ') + }]`); + } + + if (!serieshasversions) { + console.warn('Couldn\'t find versions on some episodes, fell back to old method.'); + } + + return { data: sortedEpisodes, list: Object.entries(sortedEpisodes).map(([key, value]) => { + const images = (value.items[0].images.thumbnail ?? [[ { source: '/notFound.png' } ]])[0]; + const seconds = Math.floor(value.items[0].duration_ms / 1000); + let epNum; + if (data?.absolute) { + epNum = value.items[0].episode_number !== null && value.items[0].episode_number !== undefined ? value.items[0].episode_number.toString() : (value.items[0].episode !== null && value.items[0].episode !== undefined ? value.items[0].episode : (key.startsWith('E') ? key.slice(1) : key)); + } else { + epNum = key.startsWith('E') ? key.slice(1) : key; + } + return { + e: epNum, + 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(), + seasonTitle: value.items[0].season_title.replace(/\(\w+ Dub\)/g, '').trimEnd(), + episode: value.items[0].episode_number?.toString() ?? value.items[0].episode ?? '?', + id: value.items[0].season_id, + img: images[Math.floor(images.length / 2)].source, + description: value.items[0].description, + time: `${Math.floor(seconds / 60)}:${seconds % 60}` }; - } else { - item = episodes[`${seasonIdentifier}E${episode.episode || episode.episode_number}`]; - } - - if (episode.versions) { - //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)) { - //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); - } - } - //Sort audio tracks according to the order of languages passed to the 'dubLang' option - const argv = yargs.appArgv(this.cfg.cli); - if(!argv.allDubs) { - item.langs.sort((a,b) => argv.dubLang.indexOf(a.code) - argv.dubLang.indexOf(b.code)); - } - } else { - //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)) { - //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); - } - } - }); - } + })}; } - const itemIndexes = { - sp: 1, - no: 1 - }; - - for (const key of Object.keys(episodes)) { - const item = episodes[key]; - const isSpecial = !item.items[0].episode.match(/^\d+$/); - episodes[`${isSpecial ? 'S' : 'E'}${itemIndexes[isSpecial ? 'sp' : 'no']}`] = item; - if (isSpecial) - itemIndexes.sp++; - else - itemIndexes.no++; - delete episodes[key]; + public async downloadFromSeriesID(id: string, data: CrunchyMultiDownload) : Promise> { + const { data: episodes } = await this.listSeriesID(id, data); + console.info(''); + console.info('-'.repeat(30)); + console.info(''); + const selected = this.itemSelectMultiDub(episodes, data.dubLang, data.but, data.all, data.e, data.absolute); + for (const key of Object.keys(selected)) { + const item = selected[key]; + console.info(`[S${item.season}E${item.episodeNumber}] - ${item.episodeTitle} [${ + item.data.map(a => { + return `✓ ${a.lang?.name || 'Unknown Language'}`; + }).join(', ') + }]`); + } + return { isOk: true, value: Object.values(selected) }; } - // Sort episodes to have specials at the end - const specials = Object.entries(episodes).filter(a => a[0].startsWith('S')), - normal = Object.entries(episodes).filter(a => a[0].startsWith('E')), - sortedEpisodes = Object.fromEntries([...normal, ...specials]); - - for (const key of Object.keys(sortedEpisodes)) { - const item = sortedEpisodes[key]; - const epNum = key.startsWith('E') ? (`E${data?.absolute ? (item.items[0].episode_number?.toString() || item.items[0].episode) : key.slice(1)}`) : key; - console.info(`[${data?.absolute ? epNum : key}] [${item.items[0].upload_date ? new Date(item.items[0].upload_date).toISOString().slice(0, 10) : '0000-00-00'}] ${ - 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 ?? 'Unknown'}`; - }).join(', ') - }]`); - } - - if (!serieshasversions) { - console.warn('Couldn\'t find versions on some episodes, fell back to old method.'); - } - - return { data: sortedEpisodes, list: Object.entries(sortedEpisodes).map(([key, value]) => { - const images = (value.items[0].images.thumbnail ?? [[ { source: '/notFound.png' } ]])[0]; - const seconds = Math.floor(value.items[0].duration_ms / 1000); - let epNum; - if (data?.absolute) { - epNum = value.items[0].episode_number !== null && value.items[0].episode_number !== undefined ? value.items[0].episode_number.toString() : (value.items[0].episode !== null && value.items[0].episode !== undefined ? value.items[0].episode : (key.startsWith('E') ? key.slice(1) : key)); - } else { - epNum = key.startsWith('E') ? key.slice(1) : key; - } - return { - e: epNum, - 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(), - seasonTitle: value.items[0].season_title.replace(/\(\w+ Dub\)/g, '').trimEnd(), - episode: value.items[0].episode_number?.toString() ?? value.items[0].episode ?? '?', - id: value.items[0].season_id, - img: images[Math.floor(images.length / 2)].source, - description: value.items[0].description, - time: `${Math.floor(seconds / 60)}:${seconds % 60}` - }; - })}; - } - - public async downloadFromSeriesID(id: string, data: CrunchyMultiDownload) : Promise> { - const { data: episodes } = await this.listSeriesID(id, data); - console.info(''); - console.info('-'.repeat(30)); - console.info(''); - const selected = this.itemSelectMultiDub(episodes, data.dubLang, data.but, data.all, data.e, data.absolute); - for (const key of Object.keys(selected)) { - const item = selected[key]; - console.info(`[S${item.season}E${item.episodeNumber}] - ${item.episodeTitle} [${ - item.data.map(a => { - return `✓ ${a.lang?.name || 'Unknown Language'}`; - }).join(', ') - }]`); - } - return { isOk: true, value: Object.values(selected) }; - } - - public itemSelectMultiDub (eps: Record, dubLang: string[], but?: boolean, all?: boolean, e?: string, absolute?: boolean) { - const doEpsFilter = parseSelect(e as string); + const doEpsFilter = parseSelect(e as string); - const ret: Record = {}; + const ret: Record = {}; - for (const key of Object.keys(eps)) { - const itemE = eps[key]; - itemE.items.forEach((item, index) => { - if (!dubLang.includes(itemE.langs[index]?.code)) - return; - item.hide_season_title = true; - if(item.season_title == '' && item.series_title != ''){ - item.season_title = item.series_title; - item.hide_season_title = false; - item.hide_season_number = true; - } - if(item.season_title == '' && item.series_title == ''){ - item.season_title = 'NO_TITLE'; - item.series_title = 'NO_TITLE'; - } + for (const key of Object.keys(eps)) { + const itemE = eps[key]; + itemE.items.forEach((item, index) => { + if (!dubLang.includes(itemE.langs[index]?.code)) + return; + item.hide_season_title = true; + if(item.season_title == '' && item.series_title != ''){ + item.season_title = item.series_title; + item.hide_season_title = false; + item.hide_season_number = true; + } + if(item.season_title == '' && item.series_title == ''){ + item.season_title = 'NO_TITLE'; + item.series_title = 'NO_TITLE'; + } - let epNum; - if (absolute) { - epNum = item.episode_number !== null && item.episode_number !== undefined ? item.episode_number.toString() : (item.episode !== null && item.episode !== undefined ? item.episode : (key.startsWith('E') ? key.slice(1) : key)); - } else { - epNum = key.startsWith('E') ? key.slice(1) : key; - } + let epNum; + if (absolute) { + epNum = item.episode_number !== null && item.episode_number !== undefined ? item.episode_number.toString() : (item.episode !== null && item.episode !== undefined ? item.episode : (key.startsWith('E') ? key.slice(1) : key)); + } else { + epNum = key.startsWith('E') ? key.slice(1) : key; + } - // set data - const images = (item.images.thumbnail ?? [[ { source: '/notFound.png' } ]])[0]; - const epMeta: CrunchyEpMeta = { - data: [ - { - mediaId: item.id, - versions: item.versions, - isSubbed: item.is_subbed, - isDubbed: item.is_dubbed - } - ], - seriesTitle: itemE.items.find(a => !a.series_title.match(/\(\w+ Dub\)/))?.series_title ?? itemE.items[0].series_title.replace(/\(\w+ Dub\)/g, '').trimEnd(), - seasonTitle: itemE.items.find(a => !a.season_title.match(/\(\w+ Dub\)/))?.season_title ?? itemE.items[0].season_title.replace(/\(\w+ Dub\)/g, '').trimEnd(), - episodeNumber: item.episode, - episodeTitle: item.title, - seasonID: item.season_id, - season: item.season_number, - showID: item.series_id, - e: epNum, - image: images[Math.floor(images.length / 2)].source, - }; - if (item.__links__?.streams?.href) { - epMeta.data[0].playback = item.__links__.streams.href; - if(!item.playback) { - item.playback = item.__links__.streams.href; - } - } - if (item.streams_link) { - epMeta.data[0].playback = item.streams_link; - if(!item.playback) { - item.playback = item.streams_link; - } - } + // set data + const images = (item.images.thumbnail ?? [[ { source: '/notFound.png' } ]])[0]; + const epMeta: CrunchyEpMeta = { + data: [ + { + mediaId: item.id, + versions: item.versions, + isSubbed: item.is_subbed, + isDubbed: item.is_dubbed + } + ], + seriesTitle: itemE.items.find(a => !a.series_title.match(/\(\w+ Dub\)/))?.series_title ?? itemE.items[0].series_title.replace(/\(\w+ Dub\)/g, '').trimEnd(), + seasonTitle: itemE.items.find(a => !a.season_title.match(/\(\w+ Dub\)/))?.season_title ?? itemE.items[0].season_title.replace(/\(\w+ Dub\)/g, '').trimEnd(), + episodeNumber: item.episode, + episodeTitle: item.title, + seasonID: item.season_id, + season: item.season_number, + showID: item.series_id, + e: epNum, + image: images[Math.floor(images.length / 2)].source, + }; + if (item.__links__?.streams?.href) { + epMeta.data[0].playback = item.__links__.streams.href; + if(!item.playback) { + item.playback = item.__links__.streams.href; + } + } + if (item.streams_link) { + epMeta.data[0].playback = item.streams_link; + if(!item.playback) { + item.playback = item.streams_link; + } + } - if(item.playback && ((but && !doEpsFilter.isSelected([epNum, item.id])) || (all || (doEpsFilter.isSelected([epNum, item.id])) && !but))) { - if (Object.prototype.hasOwnProperty.call(ret, key)) { - const epMe = ret[key]; - epMe.data.push({ - lang: itemE.langs[index], - ...epMeta.data[0] + if(item.playback && ((but && !doEpsFilter.isSelected([epNum, item.id])) || (all || (doEpsFilter.isSelected([epNum, item.id])) && !but))) { + if (Object.prototype.hasOwnProperty.call(ret, key)) { + const epMe = ret[key]; + epMe.data.push({ + lang: itemE.langs[index], + ...epMeta.data[0] + }); + } else { + epMeta.data[0].lang = itemE.langs[index]; + ret[key] = { + ...epMeta + }; + } + } + // show ep + item.seq_id = epNum; }); - } else { - epMeta.data[0].lang = itemE.langs[index]; - ret[key] = { - ...epMeta + } + return ret; + } + + public parseSeriesResult (seasonsList: SeriesSearch) : Record> { + const ret: Record> = {}; + let i = 0; + for (const item of seasonsList.data) { + i++; + for (const lang of langsData.languages) { + //TODO: Make sure the below code is fine + let season_number = item.season_number; + if (item.versions) { + season_number = i; + } + if (!Object.prototype.hasOwnProperty.call(ret, season_number)) + ret[season_number] = {}; + if (item.title.includes(`(${lang.name} Dub)`) || item.title.includes(`(${lang.name})`)) { + ret[season_number][lang.code] = item; + } else if (item.is_subbed && !item.is_dubbed && lang.code == 'jpn') { + ret[season_number][lang.code] = item; + } else if (item.is_dubbed && lang.code === 'eng' && !langsData.languages.some(a => item.title.includes(`(${a.name})`) || item.title.includes(`(${a.name} Dub)`))) { // Dubbed with no more infos will be treated as eng dubs + ret[season_number][lang.code] = item; + //TODO: look into if below is stable + } else if (item.audio_locale == lang.cr_locale) { + ret[season_number][lang.code] = item; + } + } + } + return ret; + } + + public async parseSeriesById(id: string) { + if(!this.cmsToken.cms_web){ + console.error('Authentication required!'); + return; + } + + const AuthHeaders = { + headers: { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader + }, + useProxy: true + }; + + // seasons list + const seriesSeasonListReq = await this.req.getData(`${api.content_cms}/series/${id}/seasons?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); + if(!seriesSeasonListReq.ok || !seriesSeasonListReq.res){ + console.error('Series Request FAILED!'); + return; + } + // parse data + const seasonsList = await seriesSeasonListReq.res.json() as SeriesSearch; + if(seasonsList.total < 1){ + console.info('Series is empty!'); + return; + } + return seasonsList; + } + + public async getSeasonDataById(item: SeriesSearchItem, log = false){ + if(!this.cmsToken.cms_web){ + console.error('Authentication required!'); + return; + } + + const AuthHeaders = { + headers: { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader + }, + useProxy: true + }; + + //get show info + const showInfoReq = await this.req.getData(`${api.content_cms}/seasons/${item.id}?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); + if(!showInfoReq.ok || !showInfoReq.res){ + console.error('Show Request FAILED!'); + return; + } + const showInfo = await showInfoReq.res.json(); + if (log) + await this.logObject(showInfo, 0); + + let episodeList = { total: 0, data: [], meta: {} } as CrunchyEpisodeList; + //get episode info + for (const s of showInfo.data) { + const original_id = s.versions?.find((v: { original: boolean; }) => v.original)?.guid; + const id = original_id ? original_id : s.id; + + const reqEpsListOpts = [ + api.cms_bucket, + this.cmsToken.cms_web.bucket, + '/episodes?', + new URLSearchParams({ + 'force_locale': '', + 'preferred_audio_language': 'ja-JP', + 'locale': this.locale, + 'season_id': id, + 'Policy': this.cmsToken.cms_web.policy, + 'Signature': this.cmsToken.cms_web.signature, + 'Key-Pair-Id': this.cmsToken.cms_web.key_pair_id, + }), + ].join(''); + const reqEpsList = await this.req.getData(reqEpsListOpts, AuthHeaders); + if(!reqEpsList.ok || !reqEpsList.res){ + console.error('Episode List Request FAILED!'); + return; + } + + const episodeListAndroid = await reqEpsList.res.json() as CrunchyAndroidEpisodes; + episodeList = { + total: episodeList.total + episodeListAndroid.total, + data: [...episodeList.data, ...episodeListAndroid.items], + meta: {} }; - } } - // show ep - item.seq_id = epNum; - }); - } - return ret; - } - public parseSeriesResult (seasonsList: SeriesSearch) : Record> { - const ret: Record> = {}; - let i = 0; - for (const item of seasonsList.data) { - i++; - for (const lang of langsData.languages) { - //TODO: Make sure the below code is fine - let season_number = item.season_number; - if (item.versions) { - season_number = i; + if(episodeList.total < 1){ + console.info(' Season is empty!'); + return; } - if (!Object.prototype.hasOwnProperty.call(ret, season_number)) - ret[season_number] = {}; - if (item.title.includes(`(${lang.name} Dub)`) || item.title.includes(`(${lang.name})`)) { - ret[season_number][lang.code] = item; - } else if (item.is_subbed && !item.is_dubbed && lang.code == 'jpn') { - ret[season_number][lang.code] = item; - } else if (item.is_dubbed && lang.code === 'eng' && !langsData.languages.some(a => item.title.includes(`(${a.name})`) || item.title.includes(`(${a.name} Dub)`))) { // Dubbed with no more infos will be treated as eng dubs - ret[season_number][lang.code] = item; - //TODO: look into if below is stable - } else if (item.audio_locale == lang.cr_locale) { - ret[season_number][lang.code] = item; - } - } + return episodeList; } - return ret; - } - - public async parseSeriesById(id: string) { - if(!this.cmsToken.cms_web){ - console.error('Authentication required!'); - return; - } - - const AuthHeaders = { - headers: { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader - }, - useProxy: true - }; - - // seasons list - const seriesSeasonListReq = await this.req.getData(`${api.content_cms}/series/${id}/seasons?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); - if(!seriesSeasonListReq.ok || !seriesSeasonListReq.res){ - console.error('Series Request FAILED!'); - return; - } - // parse data - const seasonsList = await seriesSeasonListReq.res.json() as SeriesSearch; - if(seasonsList.total < 1){ - console.info('Series is empty!'); - return; - } - return seasonsList; - } - - public async getSeasonDataById(item: SeriesSearchItem, log = false){ - if(!this.cmsToken.cms_web){ - console.error('Authentication required!'); - return; - } - - const AuthHeaders = { - headers: { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader - }, - useProxy: true - }; - - //get show info - const showInfoReq = await this.req.getData(`${api.content_cms}/seasons/${item.id}?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); - if(!showInfoReq.ok || !showInfoReq.res){ - console.error('Show Request FAILED!'); - return; - } - const showInfo = await showInfoReq.res.json(); - if (log) - await this.logObject(showInfo, 0); - - let episodeList = { total: 0, data: [], meta: {} } as CrunchyEpisodeList; - //get episode info - for (const s of showInfo.data) { - const original_id = s.versions?.find((v: { original: boolean; }) => v.original)?.guid; - const id = original_id ? original_id : s.id; - - const reqEpsListOpts = [ - api.cms_bucket, - this.cmsToken.cms_web.bucket, - '/episodes?', - new URLSearchParams({ - 'force_locale': '', - 'preferred_audio_language': 'ja-JP', - 'locale': this.locale, - 'season_id': id, - 'Policy': this.cmsToken.cms_web.policy, - 'Signature': this.cmsToken.cms_web.signature, - 'Key-Pair-Id': this.cmsToken.cms_web.key_pair_id, - }), - ].join(''); - const reqEpsList = await this.req.getData(reqEpsListOpts, AuthHeaders); - if(!reqEpsList.ok || !reqEpsList.res){ - console.error('Episode List Request FAILED!'); - return; - } - - const episodeListAndroid = await reqEpsList.res.json() as CrunchyAndroidEpisodes; - episodeList = { - total: episodeList.total + episodeListAndroid.total, - data: [...episodeList.data, ...episodeListAndroid.items], - meta: {} - }; - } - - if(episodeList.total < 1){ - console.info(' Season is empty!'); - return; - } - return episodeList; - } } diff --git a/eslint.config.mjs b/eslint.config.mjs index 3749d4f..95dc8d6 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -4,60 +4,60 @@ import eslint from '@eslint/js'; import tseslint from 'typescript-eslint'; export default tseslint.config( - eslint.configs.recommended, - ...tseslint.configs.recommended, - { - rules: { - 'no-console': 2, - 'react/prop-types': 0, - 'react-hooks/exhaustive-deps': 0, - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-unsafe-declaration-merging': 'warn', - '@typescript-eslint/no-unused-vars' : 'warn', - '@typescript-eslint/no-unused-expressions': 'warn', - 'indent': [ - 'error', - 2 - ], - 'linebreak-style': [ - 'warn', - 'windows' - ], - 'quotes': [ - 'error', - 'single' - ], - 'semi': [ - 'error', - 'always' - ] - }, - languageOptions: { - parserOptions: { - ecmaFeatures: { - jsx: true, + eslint.configs.recommended, + ...tseslint.configs.recommended, + { + rules: { + 'no-console': 2, + 'react/prop-types': 0, + 'react-hooks/exhaustive-deps': 0, + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unsafe-declaration-merging': 'warn', + '@typescript-eslint/no-unused-vars' : 'warn', + '@typescript-eslint/no-unused-expressions': 'warn', + 'indent': [ + 'error', + 4 + ], + 'linebreak-style': [ + 'warn', + 'windows' + ], + 'quotes': [ + 'error', + 'single' + ], + 'semi': [ + 'error', + 'always' + ] }, - ecmaVersion: 2020, - sourceType: 'module' - }, - parser: tseslint.parser + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: 2020, + sourceType: 'module' + }, + parser: tseslint.parser + } + }, + { + ignores: [ + '**/lib', + '**/videos/*.ts', + '**/build', + 'dev.js', + 'tsc.ts' + ] + }, + { + files: ['gui/react/**/*'], + rules: { + 'no-console': 0, + // Disabled because ESLint bugs around on .tsx files somehow? + indent: 'off' + } } - }, - { - ignores: [ - '**/lib', - '**/videos/*.ts', - '**/build', - 'dev.js', - 'tsc.ts' - ] - }, - { - files: ['gui/react/**/*'], - rules: { - 'no-console': 0, - // Disabled because ESLint bugs around on .tsx files somehow? - indent: 'off' - } - } ); \ No newline at end of file diff --git a/gui/server/index.ts b/gui/server/index.ts index 2229d78..c1bb446 100644 --- a/gui/server/index.ts +++ b/gui/server/index.ts @@ -25,7 +25,7 @@ app.use(express.static(path.join(workingDir, 'gui', 'server', 'build'), { maxAge 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}`); + console.info(`GUI server started on port ${cfg.gui.port}`); }); new PublicWebSocket(server); diff --git a/gui/server/serviceHandler.ts b/gui/server/serviceHandler.ts index db8618e..17e997e 100644 --- a/gui/server/serviceHandler.ts +++ b/gui/server/serviceHandler.ts @@ -12,123 +12,123 @@ import packageJson from '../../package.json'; export default class ServiceHandler { - private service: MessageHandler|undefined = undefined; - private ws: WebSocketHandler; - private state: GuiState; + private service: MessageHandler|undefined = undefined; + private ws: WebSocketHandler; + private state: GuiState; - constructor(server: Server) { - this.ws = new WebSocketHandler(server); - this.handleMessages(); - this.state = getState(); - } + constructor(server: Server) { + this.ws = new WebSocketHandler(server); + this.handleMessages(); + this.state = getState(); + } - private handleMessages() { - this.ws.events.on('setupServer', ({ data }, respond) => { - writeYamlCfgFile('gui', data); - this.state.setup = true; - setState(this.state); - respond(true); - process.exit(0); - }); + private handleMessages() { + this.ws.events.on('setupServer', ({ data }, respond) => { + writeYamlCfgFile('gui', data); + this.state.setup = true; + setState(this.state); + respond(true); + process.exit(0); + }); - this.ws.events.on('setup', ({ data }) => { - if (data === 'crunchy') { - this.service = new CrunchyHandler(this.ws); - } else if (data === 'hidive') { - this.service = new HidiveHandler(this.ws); - } else if (data === 'ao') { - this.service = new AnimeOnegaiHandler(this.ws); - } else if (data === 'adn') { - this.service = new ADNHandler(this.ws); - } - }); + this.ws.events.on('setup', ({ data }) => { + if (data === 'crunchy') { + this.service = new CrunchyHandler(this.ws); + } else if (data === 'hidive') { + this.service = new HidiveHandler(this.ws); + } else if (data === 'ao') { + this.service = new AnimeOnegaiHandler(this.ws); + } else if (data === 'adn') { + this.service = new ADNHandler(this.ws); + } + }); - this.ws.events.on('changeProvider', async (_, respond) => { - if (await this.service?.isDownloading()) - return respond(false); - this.service = undefined; - respond(true); - }); + this.ws.events.on('changeProvider', async (_, respond) => { + if (await this.service?.isDownloading()) + return respond(false); + this.service = undefined; + respond(true); + }); - this.ws.events.on('auth', async ({ data }, respond) => { - if (this.service === undefined) - return respond({ isOk: false, reason: new Error('No service selected') }); - respond(await this.service.auth(data)); - }); - this.ws.events.on('version', async (_, respond) => { - respond(packageJson.version); - }); - this.ws.events.on('type', async (_, respond) => respond(this.service === undefined ? undefined : this.service.name as 'hidive'|'crunchy'|'ao'|'adn')); - this.ws.events.on('checkToken', async (_, respond) => { - if (this.service === undefined) - return respond({ isOk: false, reason: new Error('No service selected') }); - respond(await this.service.checkToken()); - }); - this.ws.events.on('search', async ({ data }, respond) => { - if (this.service === undefined) - return respond({ isOk: false, reason: new Error('No service selected') }); - respond(await this.service.search(data)); - }); - this.ws.events.on('default', async ({ data }, respond) => { - if (this.service === undefined) - return respond({ isOk: false, reason: new Error('No service selected') }); - respond(await this.service.handleDefault(data)); - }); - this.ws.events.on('availableDubCodes', async (_, respond) => { - if (this.service === undefined) - return respond([]); - respond(await this.service.availableDubCodes()); - }); - this.ws.events.on('availableSubCodes', async (_, respond) => { - if (this.service === undefined) - return respond([]); - respond(await this.service.availableSubCodes()); - }); - this.ws.events.on('resolveItems', async ({ data }, respond) => { - if (this.service === undefined) - return respond(false); - respond(await this.service.resolveItems(data)); - }); - this.ws.events.on('listEpisodes', async ({ data }, respond) => { - if (this.service === undefined) - return respond({ isOk: false, reason: new Error('No service selected') }); - respond(await this.service.listEpisodes(data)); - }); - this.ws.events.on('downloadItem', async ({ data }, respond) => { - this.service?.downloadItem(data); - respond(undefined); - }); - this.ws.events.on('openFolder', async ({ data }, respond) => { - this.service?.openFolder(data); - respond(undefined); - }); - this.ws.events.on('openFile', async ({ data }, respond) => { - this.service?.openFile(data); - respond(undefined); - }); - this.ws.events.on('openURL', async ({ data }, respond) => { - this.service?.openURL(data); - respond(undefined); - }); - this.ws.events.on('getQueue', async (_, respond) => { - respond(await this.service?.getQueue() ?? []); - }); - this.ws.events.on('removeFromQueue', async ({ data }, respond) => { - this.service?.removeFromQueue(data); - respond(undefined); - }); - this.ws.events.on('clearQueue', async (_, respond) => { - this.service?.clearQueue(); - respond(undefined); - }); - this.ws.events.on('setDownloadQueue', async ({ data }, respond) => { - this.service?.setDownloadQueue(data); - respond(undefined); - }); - this.ws.events.on('getDownloadQueue', async (_, respond) => { - respond(await this.service?.getDownloadQueue() ?? false); - }); - this.ws.events.on('isDownloading', async (_, respond) => respond(await this.service?.isDownloading() ?? false)); - } + this.ws.events.on('auth', async ({ data }, respond) => { + if (this.service === undefined) + return respond({ isOk: false, reason: new Error('No service selected') }); + respond(await this.service.auth(data)); + }); + this.ws.events.on('version', async (_, respond) => { + respond(packageJson.version); + }); + this.ws.events.on('type', async (_, respond) => respond(this.service === undefined ? undefined : this.service.name as 'hidive'|'crunchy'|'ao'|'adn')); + this.ws.events.on('checkToken', async (_, respond) => { + if (this.service === undefined) + return respond({ isOk: false, reason: new Error('No service selected') }); + respond(await this.service.checkToken()); + }); + this.ws.events.on('search', async ({ data }, respond) => { + if (this.service === undefined) + return respond({ isOk: false, reason: new Error('No service selected') }); + respond(await this.service.search(data)); + }); + this.ws.events.on('default', async ({ data }, respond) => { + if (this.service === undefined) + return respond({ isOk: false, reason: new Error('No service selected') }); + respond(await this.service.handleDefault(data)); + }); + this.ws.events.on('availableDubCodes', async (_, respond) => { + if (this.service === undefined) + return respond([]); + respond(await this.service.availableDubCodes()); + }); + this.ws.events.on('availableSubCodes', async (_, respond) => { + if (this.service === undefined) + return respond([]); + respond(await this.service.availableSubCodes()); + }); + this.ws.events.on('resolveItems', async ({ data }, respond) => { + if (this.service === undefined) + return respond(false); + respond(await this.service.resolveItems(data)); + }); + this.ws.events.on('listEpisodes', async ({ data }, respond) => { + if (this.service === undefined) + return respond({ isOk: false, reason: new Error('No service selected') }); + respond(await this.service.listEpisodes(data)); + }); + this.ws.events.on('downloadItem', async ({ data }, respond) => { + this.service?.downloadItem(data); + respond(undefined); + }); + this.ws.events.on('openFolder', async ({ data }, respond) => { + this.service?.openFolder(data); + respond(undefined); + }); + this.ws.events.on('openFile', async ({ data }, respond) => { + this.service?.openFile(data); + respond(undefined); + }); + this.ws.events.on('openURL', async ({ data }, respond) => { + this.service?.openURL(data); + respond(undefined); + }); + this.ws.events.on('getQueue', async (_, respond) => { + respond(await this.service?.getQueue() ?? []); + }); + this.ws.events.on('removeFromQueue', async ({ data }, respond) => { + this.service?.removeFromQueue(data); + respond(undefined); + }); + this.ws.events.on('clearQueue', async (_, respond) => { + this.service?.clearQueue(); + respond(undefined); + }); + this.ws.events.on('setDownloadQueue', async ({ data }, respond) => { + this.service?.setDownloadQueue(data); + respond(undefined); + }); + this.ws.events.on('getDownloadQueue', async (_, respond) => { + respond(await this.service?.getDownloadQueue() ?? false); + }); + this.ws.events.on('isDownloading', async (_, respond) => respond(await this.service?.isDownloading() ?? false)); + } } \ No newline at end of file diff --git a/gui/server/services/adn.ts b/gui/server/services/adn.ts index 88bbe2d..e9dee9a 100644 --- a/gui/server/services/adn.ts +++ b/gui/server/services/adn.ts @@ -8,132 +8,132 @@ import { console } from '../../../modules/log'; import * as yargs from '../../../modules/module.app-args'; class ADNHandler extends Base implements MessageHandler { - private adn: AnimationDigitalNetwork; - public name = 'adn'; - constructor(ws: WebSocketHandler) { - super(ws); - this.adn = new AnimationDigitalNetwork(); - this.initState(); - this.getDefaults(); - } + private adn: AnimationDigitalNetwork; + public name = 'adn'; + constructor(ws: WebSocketHandler) { + super(ws); + this.adn = new AnimationDigitalNetwork(); + this.initState(); + this.getDefaults(); + } - public getDefaults() { - const _default = yargs.appArgv(this.adn.cfg.cli, true); - if (['fr', 'de'].includes(_default.locale)) - this.adn.locale = _default.locale; - } + public getDefaults() { + const _default = yargs.appArgv(this.adn.cfg.cli, true); + if (['fr', 'de'].includes(_default.locale)) + this.adn.locale = _default.locale; + } - public async auth(data: AuthData) { - return this.adn.doAuth(data); - } + public async auth(data: AuthData) { + return this.adn.doAuth(data); + } - public async checkToken(): Promise { + public async checkToken(): Promise { //TODO: implement proper method to check token - return { isOk: true, value: undefined }; - } - - public async search(data: SearchData): Promise { - console.debug(`Got search options: ${JSON.stringify(data)}`); - const search = await this.adn.doSearch(data); - if (!search.isOk) { - return search; + return { isOk: true, value: undefined }; } - return { isOk: true, value: search.value }; - } - public async handleDefault(name: string) { - return getDefault(name, this.adn.cfg.cli); - } - - public async availableDubCodes(): Promise { - const dubLanguageCodesArray: string[] = []; - for(const language of languages){ - if (language.adn_locale) - dubLanguageCodesArray.push(language.code); - } - return [...new Set(dubLanguageCodesArray)]; - } - - public async availableSubCodes(): Promise { - const subLanguageCodesArray: string[] = []; - for(const language of languages){ - if (language.adn_locale) - subLanguageCodesArray.push(language.locale); - } - return ['all', 'none', ...new Set(subLanguageCodesArray)]; - } - - public async resolveItems(data: ResolveItemsData): Promise { - const parse = parseInt(data.id); - if (isNaN(parse) || parse <= 0) - return false; - console.debug(`Got resolve options: ${JSON.stringify(data)}`); - const res = await this.adn.selectShow(parseInt(data.id), data.e, data.but, data.all); - if (!res.isOk || !res.value) - return res.isOk; - this.addToQueue(res.value.map(a => { - return { - ...data, - ids: [a.id], - title: a.title, - parent: { - title: a.show.shortTitle, - season: a.season - }, - e: a.shortNumber, - image: a.image, - episode: a.shortNumber - }; - })); - return true; - } - - public async listEpisodes(id: string): Promise { - const parse = parseInt(id); - if (isNaN(parse) || parse <= 0) - return { isOk: false, reason: new Error('The ID is invalid') }; - - const request = await this.adn.listShow(parse); - if (!request.isOk || !request.value) - return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')}; - - return { isOk: true, value: request.value.videos.map(function(item) { - return { - e: item.shortNumber, - lang: [], - name: item.title, - season: item.season, - seasonTitle: item.show.title, - episode: item.shortNumber, - id: item.id+'', - img: item.image, - description: item.summary, - time: item.duration+'' - }; - })}; - } - - public async downloadItem(data: DownloadData) { - this.setDownloading(true); - console.debug(`Got download options: ${JSON.stringify(data)}`); - const _default = yargs.appArgv(this.adn.cfg.cli, true); - const res = await this.adn.selectShow(parseInt(data.id), data.e, false, false); - if (res.isOk) { - for (const select of res.value) { - if (!(await this.adn.getEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y', - novids: data.novids, noaudio: data.noaudio, hslang: data.hslang || 'none', dubLang: data.dubLang }))) { - const er = new Error(`Unable to download episode ${data.e} from ${data.id}`); - er.name = 'Download error'; - this.alertError(er); + public async search(data: SearchData): Promise { + console.debug(`Got search options: ${JSON.stringify(data)}`); + const search = await this.adn.doSearch(data); + if (!search.isOk) { + return search; } - } - } else { - this.alertError(new Error('Failed to download episode, check for additional logs.')); + return { isOk: true, value: search.value }; + } + + public async handleDefault(name: string) { + return getDefault(name, this.adn.cfg.cli); + } + + public async availableDubCodes(): Promise { + const dubLanguageCodesArray: string[] = []; + for(const language of languages){ + if (language.adn_locale) + dubLanguageCodesArray.push(language.code); + } + return [...new Set(dubLanguageCodesArray)]; + } + + public async availableSubCodes(): Promise { + const subLanguageCodesArray: string[] = []; + for(const language of languages){ + if (language.adn_locale) + subLanguageCodesArray.push(language.locale); + } + return ['all', 'none', ...new Set(subLanguageCodesArray)]; + } + + public async resolveItems(data: ResolveItemsData): Promise { + const parse = parseInt(data.id); + if (isNaN(parse) || parse <= 0) + return false; + console.debug(`Got resolve options: ${JSON.stringify(data)}`); + const res = await this.adn.selectShow(parseInt(data.id), data.e, data.but, data.all); + if (!res.isOk || !res.value) + return res.isOk; + this.addToQueue(res.value.map(a => { + return { + ...data, + ids: [a.id], + title: a.title, + parent: { + title: a.show.shortTitle, + season: a.season + }, + e: a.shortNumber, + image: a.image, + episode: a.shortNumber + }; + })); + return true; + } + + public async listEpisodes(id: string): Promise { + const parse = parseInt(id); + if (isNaN(parse) || parse <= 0) + return { isOk: false, reason: new Error('The ID is invalid') }; + + const request = await this.adn.listShow(parse); + if (!request.isOk || !request.value) + return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')}; + + return { isOk: true, value: request.value.videos.map(function(item) { + return { + e: item.shortNumber, + lang: [], + name: item.title, + season: item.season, + seasonTitle: item.show.title, + episode: item.shortNumber, + id: item.id+'', + img: item.image, + description: item.summary, + time: item.duration+'' + }; + })}; + } + + public async downloadItem(data: DownloadData) { + this.setDownloading(true); + console.debug(`Got download options: ${JSON.stringify(data)}`); + const _default = yargs.appArgv(this.adn.cfg.cli, true); + const res = await this.adn.selectShow(parseInt(data.id), data.e, false, false); + if (res.isOk) { + for (const select of res.value) { + if (!(await this.adn.getEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y', + novids: data.novids, noaudio: data.noaudio, hslang: data.hslang || 'none', dubLang: data.dubLang }))) { + const er = new Error(`Unable to download episode ${data.e} from ${data.id}`); + er.name = 'Download error'; + this.alertError(er); + } + } + } else { + this.alertError(new Error('Failed to download episode, check for additional logs.')); + } + this.sendMessage({ name: 'finish', data: undefined }); + this.setDownloading(false); + this.onFinish(); } - this.sendMessage({ name: 'finish', data: undefined }); - this.setDownloading(false); - this.onFinish(); - } } export default ADNHandler; \ No newline at end of file diff --git a/gui/server/services/animeonegai.ts b/gui/server/services/animeonegai.ts index ef413bf..cfe7ccb 100644 --- a/gui/server/services/animeonegai.ts +++ b/gui/server/services/animeonegai.ts @@ -8,144 +8,144 @@ import { console } from '../../../modules/log'; import * as yargs from '../../../modules/module.app-args'; class AnimeOnegaiHandler extends Base implements MessageHandler { - private ao: AnimeOnegai; - public name = 'ao'; - constructor(ws: WebSocketHandler) { - super(ws); - this.ao = new AnimeOnegai(); - this.initState(); - this.getDefaults(); - } + private ao: AnimeOnegai; + public name = 'ao'; + constructor(ws: WebSocketHandler) { + super(ws); + this.ao = new AnimeOnegai(); + this.initState(); + this.getDefaults(); + } - public getDefaults() { - const _default = yargs.appArgv(this.ao.cfg.cli, true); - if (['es', 'pt'].includes(_default.locale)) - this.ao.locale = _default.locale; - } + public getDefaults() { + const _default = yargs.appArgv(this.ao.cfg.cli, true); + if (['es', 'pt'].includes(_default.locale)) + this.ao.locale = _default.locale; + } - public async auth(data: AuthData) { - return this.ao.doAuth(data); - } + public async auth(data: AuthData) { + return this.ao.doAuth(data); + } - public async checkToken(): Promise { + public async checkToken(): Promise { //TODO: implement proper method to check token - return { isOk: true, value: undefined }; - } - - public async search(data: SearchData): Promise { - console.debug(`Got search options: ${JSON.stringify(data)}`); - const search = await this.ao.doSearch(data); - if (!search.isOk) { - return search; + return { isOk: true, value: undefined }; } - return { isOk: true, value: search.value }; - } - public async handleDefault(name: string) { - return getDefault(name, this.ao.cfg.cli); - } - - public async availableDubCodes(): Promise { - const dubLanguageCodesArray: string[] = []; - for(const language of languages){ - if (language.ao_locale) - dubLanguageCodesArray.push(language.code); - } - return [...new Set(dubLanguageCodesArray)]; - } - - public async availableSubCodes(): Promise { - const subLanguageCodesArray: string[] = []; - for(const language of languages){ - if (language.ao_locale) - subLanguageCodesArray.push(language.locale); - } - return ['all', 'none', ...new Set(subLanguageCodesArray)]; - } - - public async resolveItems(data: ResolveItemsData): Promise { - const parse = parseInt(data.id); - if (isNaN(parse) || parse <= 0) - return false; - console.debug(`Got resolve options: ${JSON.stringify(data)}`); - const _default = yargs.appArgv(this.ao.cfg.cli, true); - const res = await this.ao.selectShow(parseInt(data.id), data.e, data.but, data.all, _default); - if (!res.isOk || !res.value) - return res.isOk; - this.addToQueue(res.value.map(a => { - return { - ...data, - ids: a.data.map(a => a.videoId), - title: a.episodeTitle, - parent: { - title: a.seasonTitle, - season: a.seasonTitle - }, - e: a.episodeNumber+'', - image: a.image, - episode: a.episodeNumber+'' - }; - })); - return true; - } - - public async listEpisodes(id: string): Promise { - const parse = parseInt(id); - if (isNaN(parse) || parse <= 0) - return { isOk: false, reason: new Error('The ID is invalid') }; - - const request = await this.ao.listShow(parse); - if (!request.isOk || !request.value) - return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')}; - - const episodes: Episode[] = []; - const seasonNumberTitleParse = request.series.data.title.match(/\d+$/); - const seasonNumber = seasonNumberTitleParse ? parseInt(seasonNumberTitleParse[0]) : 1; - //request.value - for (const episodeKey in request.value) { - const episode = request.value[episodeKey][0]; - const langs = Array.from(new Set(request.value[episodeKey].map(a=>a.lang))); - episodes.push({ - e: episode.number+'', - lang: langs as string[], - name: episode.name, - season: seasonNumber+'', - seasonTitle: '', - episode: episode.number+'', - id: episode.video_entry+'', - img: episode.thumbnail, - description: episode.description, - time: '' - }); - } - return { isOk: true, value: episodes }; - } - - public async downloadItem(data: DownloadData) { - this.setDownloading(true); - console.debug(`Got download options: ${JSON.stringify(data)}`); - const _default = yargs.appArgv(this.ao.cfg.cli, true); - const res = await this.ao.selectShow(parseInt(data.id), data.e, false, false, { - ..._default, - dubLang: data.dubLang, - e: data.e - }); - if (res.isOk) { - for (const select of res.value) { - if (!(await this.ao.downloadEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y', - novids: data.novids, noaudio: data.noaudio, hslang: data.hslang || 'none', dubLang: data.dubLang }))) { - const er = new Error(`Unable to download episode ${data.e} from ${data.id}`); - er.name = 'Download error'; - this.alertError(er); + public async search(data: SearchData): Promise { + console.debug(`Got search options: ${JSON.stringify(data)}`); + const search = await this.ao.doSearch(data); + if (!search.isOk) { + return search; } - } - } else { - this.alertError(new Error('Failed to download episode, check for additional logs.')); + return { isOk: true, value: search.value }; + } + + public async handleDefault(name: string) { + return getDefault(name, this.ao.cfg.cli); + } + + public async availableDubCodes(): Promise { + const dubLanguageCodesArray: string[] = []; + for(const language of languages){ + if (language.ao_locale) + dubLanguageCodesArray.push(language.code); + } + return [...new Set(dubLanguageCodesArray)]; + } + + public async availableSubCodes(): Promise { + const subLanguageCodesArray: string[] = []; + for(const language of languages){ + if (language.ao_locale) + subLanguageCodesArray.push(language.locale); + } + return ['all', 'none', ...new Set(subLanguageCodesArray)]; + } + + public async resolveItems(data: ResolveItemsData): Promise { + const parse = parseInt(data.id); + if (isNaN(parse) || parse <= 0) + return false; + console.debug(`Got resolve options: ${JSON.stringify(data)}`); + const _default = yargs.appArgv(this.ao.cfg.cli, true); + const res = await this.ao.selectShow(parseInt(data.id), data.e, data.but, data.all, _default); + if (!res.isOk || !res.value) + return res.isOk; + this.addToQueue(res.value.map(a => { + return { + ...data, + ids: a.data.map(a => a.videoId), + title: a.episodeTitle, + parent: { + title: a.seasonTitle, + season: a.seasonTitle + }, + e: a.episodeNumber+'', + image: a.image, + episode: a.episodeNumber+'' + }; + })); + return true; + } + + public async listEpisodes(id: string): Promise { + const parse = parseInt(id); + if (isNaN(parse) || parse <= 0) + return { isOk: false, reason: new Error('The ID is invalid') }; + + const request = await this.ao.listShow(parse); + if (!request.isOk || !request.value) + return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')}; + + const episodes: Episode[] = []; + const seasonNumberTitleParse = request.series.data.title.match(/\d+$/); + const seasonNumber = seasonNumberTitleParse ? parseInt(seasonNumberTitleParse[0]) : 1; + //request.value + for (const episodeKey in request.value) { + const episode = request.value[episodeKey][0]; + const langs = Array.from(new Set(request.value[episodeKey].map(a=>a.lang))); + episodes.push({ + e: episode.number+'', + lang: langs as string[], + name: episode.name, + season: seasonNumber+'', + seasonTitle: '', + episode: episode.number+'', + id: episode.video_entry+'', + img: episode.thumbnail, + description: episode.description, + time: '' + }); + } + return { isOk: true, value: episodes }; + } + + public async downloadItem(data: DownloadData) { + this.setDownloading(true); + console.debug(`Got download options: ${JSON.stringify(data)}`); + const _default = yargs.appArgv(this.ao.cfg.cli, true); + const res = await this.ao.selectShow(parseInt(data.id), data.e, false, false, { + ..._default, + dubLang: data.dubLang, + e: data.e + }); + if (res.isOk) { + for (const select of res.value) { + if (!(await this.ao.downloadEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y', + novids: data.novids, noaudio: data.noaudio, hslang: data.hslang || 'none', dubLang: data.dubLang }))) { + const er = new Error(`Unable to download episode ${data.e} from ${data.id}`); + er.name = 'Download error'; + this.alertError(er); + } + } + } else { + this.alertError(new Error('Failed to download episode, check for additional logs.')); + } + this.sendMessage({ name: 'finish', data: undefined }); + this.setDownloading(false); + this.onFinish(); } - this.sendMessage({ name: 'finish', data: undefined }); - this.setDownloading(false); - this.onFinish(); - } } export default AnimeOnegaiHandler; \ No newline at end of file diff --git a/gui/server/services/base.ts b/gui/server/services/base.ts index a192d18..93a4bd9 100644 --- a/gui/server/services/base.ts +++ b/gui/server/services/base.ts @@ -9,140 +9,140 @@ import { getState, setState } from '../../../modules/module.cfg-loader'; import packageJson from '../../../package.json'; export default class Base { - private state: GuiState; - public name = 'default'; - constructor(private ws: WebSocketHandler) { - this.state = getState(); - } - - private downloading = false; - - private queue: QueueItem[] = []; - private workOnQueue = false; - - version(): Promise { - return new Promise(() => { - return packageJson.version; - }); - } - - initState() { - if (this.state.services[this.name]) { - this.queue = this.state.services[this.name].queue; - this.queueChange(); - } else { - this.state.services[this.name] = { - 'queue': [] - }; + private state: GuiState; + public name = 'default'; + constructor(private ws: WebSocketHandler) { + this.state = getState(); } - } - setDownloading(downloading: boolean) { - this.downloading = downloading; - } + private downloading = false; - getDownloading() { - return this.downloading; - } + private queue: QueueItem[] = []; + private workOnQueue = false; - alertError(error: Error) { - console.error(`${error}`); - } + version(): Promise { + return new Promise(() => { + return packageJson.version; + }); + } - makeProgressHandler(videoInfo: DownloadInfo) { - return ((data: ProgressData) => { - this.sendMessage({ - name: 'progress', - data: { - downloadInfo: videoInfo, - progress: data + initState() { + if (this.state.services[this.name]) { + this.queue = this.state.services[this.name].queue; + this.queueChange(); + } else { + this.state.services[this.name] = { + 'queue': [] + }; } - }); - }); - } - - sendMessage(data: RandomEvent) { - this.ws.sendMessage(data); - } - - async isDownloading() { - return this.downloading; - } - - async openFolder(folderType: FolderTypes) { - switch (folderType) { - case 'content': - open(cfg.dir.content); - break; - case 'config': - open(cfg.dir.config); - break; } - } - async openFile(data: [FolderTypes, string]) { - switch (data[0]) { - case 'config': - open(path.join(cfg.dir.config, data[1])); - break; - case 'content': - throw new Error('No subfolders'); + setDownloading(downloading: boolean) { + this.downloading = downloading; } - } - async openURL(data: string) { - open(data); - } - - public async getQueue(): Promise { - return this.queue; - } - - public async removeFromQueue(index: number) { - this.queue.splice(index, 1); - this.queueChange(); - } - - public async clearQueue() { - this.queue = []; - this.queueChange(); - } - - public addToQueue(data: QueueItem[]) { - this.queue = this.queue.concat(...data); - this.queueChange(); - } - - public setDownloadQueue(data: boolean) { - this.workOnQueue = data; - this.queueChange(); - } - - public async getDownloadQueue(): Promise { - return this.workOnQueue; - } - - private async queueChange() { - this.sendMessage({ name: 'queueChange', data: this.queue }); - if (this.workOnQueue && this.queue.length > 0 && !await this.isDownloading()) { - this.setDownloading(true); - this.sendMessage({ name: 'current', data: this.queue[0] }); - this.downloadItem(this.queue[0]); - this.queue = this.queue.slice(1); - this.queueChange(); + getDownloading() { + return this.downloading; } - this.state.services[this.name].queue = this.queue; - setState(this.state); - } - public async onFinish() { - this.sendMessage({ name: 'current', data: undefined }); - this.queueChange(); - } + alertError(error: Error) { + console.error(`${error}`); + } - //Overriten - // eslint-disable-next-line + makeProgressHandler(videoInfo: DownloadInfo) { + return ((data: ProgressData) => { + this.sendMessage({ + name: 'progress', + data: { + downloadInfo: videoInfo, + progress: data + } + }); + }); + } + + sendMessage(data: RandomEvent) { + this.ws.sendMessage(data); + } + + async isDownloading() { + return this.downloading; + } + + async openFolder(folderType: FolderTypes) { + switch (folderType) { + case 'content': + open(cfg.dir.content); + break; + case 'config': + open(cfg.dir.config); + break; + } + } + + async openFile(data: [FolderTypes, string]) { + switch (data[0]) { + case 'config': + open(path.join(cfg.dir.config, data[1])); + break; + case 'content': + throw new Error('No subfolders'); + } + } + + async openURL(data: string) { + open(data); + } + + public async getQueue(): Promise { + return this.queue; + } + + public async removeFromQueue(index: number) { + this.queue.splice(index, 1); + this.queueChange(); + } + + public async clearQueue() { + this.queue = []; + this.queueChange(); + } + + public addToQueue(data: QueueItem[]) { + this.queue = this.queue.concat(...data); + this.queueChange(); + } + + public setDownloadQueue(data: boolean) { + this.workOnQueue = data; + this.queueChange(); + } + + public async getDownloadQueue(): Promise { + return this.workOnQueue; + } + + private async queueChange() { + this.sendMessage({ name: 'queueChange', data: this.queue }); + if (this.workOnQueue && this.queue.length > 0 && !await this.isDownloading()) { + this.setDownloading(true); + this.sendMessage({ name: 'current', data: this.queue[0] }); + this.downloadItem(this.queue[0]); + this.queue = this.queue.slice(1); + this.queueChange(); + } + this.state.services[this.name].queue = this.queue; + setState(this.state); + } + + public async onFinish() { + this.sendMessage({ name: 'current', data: undefined }); + this.queueChange(); + } + + //Overriten + // eslint-disable-next-line public async downloadItem(_: QueueItem) { - throw new Error('downloadItem not overriden'); - } + throw new Error('downloadItem not overriden'); + } } \ No newline at end of file diff --git a/gui/server/services/crunchyroll.ts b/gui/server/services/crunchyroll.ts index ab4235f..3535a3c 100644 --- a/gui/server/services/crunchyroll.ts +++ b/gui/server/services/crunchyroll.ts @@ -8,120 +8,120 @@ import { console } from '../../../modules/log'; import * as yargs from '../../../modules/module.app-args'; class CrunchyHandler extends Base implements MessageHandler { - private crunchy: Crunchy; - public name = 'crunchy'; - constructor(ws: WebSocketHandler) { - super(ws); - this.crunchy = new Crunchy(); - this.crunchy.refreshToken(); - this.initState(); - this.getDefaults(); - } + private crunchy: Crunchy; + public name = 'crunchy'; + constructor(ws: WebSocketHandler) { + super(ws); + this.crunchy = new Crunchy(); + this.crunchy.refreshToken(); + this.initState(); + this.getDefaults(); + } - public getDefaults() { - const _default = yargs.appArgv(this.crunchy.cfg.cli, true); - this.crunchy.locale = _default.locale; - } + public getDefaults() { + const _default = yargs.appArgv(this.crunchy.cfg.cli, true); + this.crunchy.locale = _default.locale; + } - public async listEpisodes (id: string): Promise { - this.getDefaults(); - await this.crunchy.refreshToken(true); - return { isOk: true, value: (await this.crunchy.listSeriesID(id)).list }; - } + public async listEpisodes (id: string): Promise { + this.getDefaults(); + await this.crunchy.refreshToken(true); + return { isOk: true, value: (await this.crunchy.listSeriesID(id)).list }; + } - public async handleDefault(name: string) { - return getDefault(name, this.crunchy.cfg.cli); - } - - public async availableDubCodes(): Promise { - const dubLanguageCodesArray: string[] = []; - for(const language of languages){ - if (language.cr_locale) - dubLanguageCodesArray.push(language.code); + public async handleDefault(name: string) { + return getDefault(name, this.crunchy.cfg.cli); } - return [...new Set(dubLanguageCodesArray)]; - } - public async availableSubCodes(): Promise { - return subtitleLanguagesFilter; - } - - public async resolveItems(data: ResolveItemsData): Promise { - this.getDefaults(); - await this.crunchy.refreshToken(true); - console.debug(`Got resolve options: ${JSON.stringify(data)}`); - const res = await this.crunchy.downloadFromSeriesID(data.id, data); - if (!res.isOk) - return res.isOk; - this.addToQueue(res.value.map(a => { - return { - ...data, - - ids: a.data.map(a => a.mediaId), - title: a.episodeTitle, - parent: { - title: a.seasonTitle, - season: a.season.toString() - }, - e: a.e, - image: a.image, - episode: a.episodeNumber - }; - })); - return true; - } - - public async search(data: SearchData): Promise { - this.getDefaults(); - await this.crunchy.refreshToken(true); - if (!data['search-type']) data['search-type'] = 'series'; - console.debug(`Got search options: ${JSON.stringify(data)}`); - const crunchySearch = await this.crunchy.doSearch(data); - if (!crunchySearch.isOk) { - this.crunchy.refreshToken(); - return crunchySearch; - } - return { isOk: true, value: crunchySearch.value }; - } - - public async checkToken(): Promise { - if (await this.crunchy.getProfile()) { - return { isOk: true, value: undefined }; - } else { - return { isOk: false, reason: new Error('') }; - } - } - - public auth(data: AuthData) { - return this.crunchy.doAuth(data); - } - - public async downloadItem(data: DownloadData) { - this.getDefaults(); - await this.crunchy.refreshToken(true); - console.debug(`Got download options: ${JSON.stringify(data)}`); - this.setDownloading(true); - const _default = yargs.appArgv(this.crunchy.cfg.cli, true); - const res = await this.crunchy.downloadFromSeriesID(data.id, { - dubLang: data.dubLang, - e: data.e - }); - if (res.isOk) { - for (const select of res.value) { - if (!(await this.crunchy.downloadEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y', - novids: data.novids, noaudio: data.noaudio, hslang: data.hslang || 'none' }))) { - const er = new Error(`Unable to download episode ${data.e} from ${data.id}`); - er.name = 'Download error'; - this.alertError(er); + public async availableDubCodes(): Promise { + const dubLanguageCodesArray: string[] = []; + for(const language of languages){ + if (language.cr_locale) + dubLanguageCodesArray.push(language.code); } - } - } else { - this.alertError(res.reason); + return [...new Set(dubLanguageCodesArray)]; + } + + public async availableSubCodes(): Promise { + return subtitleLanguagesFilter; + } + + public async resolveItems(data: ResolveItemsData): Promise { + this.getDefaults(); + await this.crunchy.refreshToken(true); + console.debug(`Got resolve options: ${JSON.stringify(data)}`); + const res = await this.crunchy.downloadFromSeriesID(data.id, data); + if (!res.isOk) + return res.isOk; + this.addToQueue(res.value.map(a => { + return { + ...data, + + ids: a.data.map(a => a.mediaId), + title: a.episodeTitle, + parent: { + title: a.seasonTitle, + season: a.season.toString() + }, + e: a.e, + image: a.image, + episode: a.episodeNumber + }; + })); + return true; + } + + public async search(data: SearchData): Promise { + this.getDefaults(); + await this.crunchy.refreshToken(true); + if (!data['search-type']) data['search-type'] = 'series'; + console.debug(`Got search options: ${JSON.stringify(data)}`); + const crunchySearch = await this.crunchy.doSearch(data); + if (!crunchySearch.isOk) { + this.crunchy.refreshToken(); + return crunchySearch; + } + return { isOk: true, value: crunchySearch.value }; + } + + public async checkToken(): Promise { + if (await this.crunchy.getProfile()) { + return { isOk: true, value: undefined }; + } else { + return { isOk: false, reason: new Error('') }; + } + } + + public auth(data: AuthData) { + return this.crunchy.doAuth(data); + } + + public async downloadItem(data: DownloadData) { + this.getDefaults(); + await this.crunchy.refreshToken(true); + console.debug(`Got download options: ${JSON.stringify(data)}`); + this.setDownloading(true); + const _default = yargs.appArgv(this.crunchy.cfg.cli, true); + const res = await this.crunchy.downloadFromSeriesID(data.id, { + dubLang: data.dubLang, + e: data.e + }); + if (res.isOk) { + for (const select of res.value) { + if (!(await this.crunchy.downloadEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y', + novids: data.novids, noaudio: data.noaudio, hslang: data.hslang || 'none' }))) { + const er = new Error(`Unable to download episode ${data.e} from ${data.id}`); + er.name = 'Download error'; + this.alertError(er); + } + } + } else { + this.alertError(res.reason); + } + this.sendMessage({ name: 'finish', data: undefined }); + this.setDownloading(false); + this.onFinish(); } - this.sendMessage({ name: 'finish', data: undefined }); - this.setDownloading(false); - this.onFinish(); - } } export default CrunchyHandler; \ No newline at end of file diff --git a/gui/server/services/hidive.ts b/gui/server/services/hidive.ts index ee98426..5fc9c5f 100644 --- a/gui/server/services/hidive.ts +++ b/gui/server/services/hidive.ts @@ -8,120 +8,120 @@ import { console } from '../../../modules/log'; import * as yargs from '../../../modules/module.app-args'; class HidiveHandler extends Base implements MessageHandler { - private hidive: Hidive; - public name = 'hidive'; - constructor(ws: WebSocketHandler) { - super(ws); - this.hidive = new Hidive(); - this.initState(); - } + private hidive: Hidive; + public name = 'hidive'; + constructor(ws: WebSocketHandler) { + super(ws); + this.hidive = new Hidive(); + this.initState(); + } - public async auth(data: AuthData) { - return this.hidive.doAuth(data); - } + public async auth(data: AuthData) { + return this.hidive.doAuth(data); + } - public async checkToken(): Promise { + public async checkToken(): Promise { //TODO: implement proper method to check token - return { isOk: true, value: undefined }; - } - - public async search(data: SearchData): Promise { - console.debug(`Got search options: ${JSON.stringify(data)}`); - const hidiveSearch = await this.hidive.doSearch(data); - if (!hidiveSearch.isOk) { - return hidiveSearch; + return { isOk: true, value: undefined }; } - return { isOk: true, value: hidiveSearch.value }; - } - public async handleDefault(name: string) { - return getDefault(name, this.hidive.cfg.cli); - } - - public async availableDubCodes(): Promise { - const dubLanguageCodesArray: string[] = []; - for(const language of languages){ - if (language.new_hd_locale) - dubLanguageCodesArray.push(language.code); + public async search(data: SearchData): Promise { + console.debug(`Got search options: ${JSON.stringify(data)}`); + const hidiveSearch = await this.hidive.doSearch(data); + if (!hidiveSearch.isOk) { + return hidiveSearch; + } + return { isOk: true, value: hidiveSearch.value }; } - return [...new Set(dubLanguageCodesArray)]; - } - public async availableSubCodes(): Promise { - const subLanguageCodesArray: string[] = []; - for(const language of languages){ - if (language.new_hd_locale) - subLanguageCodesArray.push(language.locale); + public async handleDefault(name: string) { + return getDefault(name, this.hidive.cfg.cli); } - return ['all', 'none', ...new Set(subLanguageCodesArray)]; - } - public async resolveItems(data: ResolveItemsData): Promise { - const parse = parseInt(data.id); - if (isNaN(parse) || parse <= 0) - return false; - console.debug(`Got resolve options: ${JSON.stringify(data)}`); - const res = await this.hidive.selectSeries(parseInt(data.id), data.e, data.but, data.all); - if (!res.isOk || !res.value) - return res.isOk; - this.addToQueue(res.value.map(item => { - return { - ...data, - ids: [item.id], - title: item.title, - parent: { - title: item.seriesTitle, - season: item.episodeInformation.seasonNumber+'' - }, - image: item.thumbnailUrl, - e: item.episodeInformation.episodeNumber+'', - episode: item.episodeInformation.episodeNumber+'', - }; - })); - return true; - } - - public async listEpisodes(id: string): Promise { - const parse = parseInt(id); - if (isNaN(parse) || parse <= 0) - return { isOk: false, reason: new Error('The ID is invalid') }; - - const request = await this.hidive.listSeries(parse); - if (!request.isOk || !request.value) - return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')}; - - return { isOk: true, value: request.value.map(function(item) { - const description = item.description.split('\r\n'); - return { - e: item.episodeInformation.episodeNumber+'', - lang: [], - name: item.title, - season: item.episodeInformation.seasonNumber+'', - seasonTitle: request.series.seasons[item.episodeInformation.seasonNumber-1]?.title ?? request.series.title, - episode: item.episodeInformation.episodeNumber+'', - id: item.id+'', - img: item.thumbnailUrl, - description: description ? description[0] : '', - time: '' - }; - })}; - } - - public async downloadItem(data: DownloadData) { - this.setDownloading(true); - console.debug(`Got download options: ${JSON.stringify(data)}`); - const _default = yargs.appArgv(this.hidive.cfg.cli, true); - const res = await this.hidive.selectSeries(parseInt(data.id), data.e, false, false); - if (!res.isOk || !res.showData) - return this.alertError(new Error('Download failed upstream, check for additional logs')); - - for (const ep of res.value) { - await this.hidive.downloadEpisode(ep, {..._default, callbackMaker: this.makeProgressHandler.bind(this), dubLang: data.dubLang, dlsubs: data.dlsubs, fileName: data.fileName, q: data.q, force: 'y', noaudio: data.noaudio, novids: data.novids }); + public async availableDubCodes(): Promise { + const dubLanguageCodesArray: string[] = []; + for(const language of languages){ + if (language.new_hd_locale) + dubLanguageCodesArray.push(language.code); + } + return [...new Set(dubLanguageCodesArray)]; + } + + public async availableSubCodes(): Promise { + const subLanguageCodesArray: string[] = []; + for(const language of languages){ + if (language.new_hd_locale) + subLanguageCodesArray.push(language.locale); + } + return ['all', 'none', ...new Set(subLanguageCodesArray)]; + } + + public async resolveItems(data: ResolveItemsData): Promise { + const parse = parseInt(data.id); + if (isNaN(parse) || parse <= 0) + return false; + console.debug(`Got resolve options: ${JSON.stringify(data)}`); + const res = await this.hidive.selectSeries(parseInt(data.id), data.e, data.but, data.all); + if (!res.isOk || !res.value) + return res.isOk; + this.addToQueue(res.value.map(item => { + return { + ...data, + ids: [item.id], + title: item.title, + parent: { + title: item.seriesTitle, + season: item.episodeInformation.seasonNumber+'' + }, + image: item.thumbnailUrl, + e: item.episodeInformation.episodeNumber+'', + episode: item.episodeInformation.episodeNumber+'', + }; + })); + return true; + } + + public async listEpisodes(id: string): Promise { + const parse = parseInt(id); + if (isNaN(parse) || parse <= 0) + return { isOk: false, reason: new Error('The ID is invalid') }; + + const request = await this.hidive.listSeries(parse); + if (!request.isOk || !request.value) + return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')}; + + return { isOk: true, value: request.value.map(function(item) { + const description = item.description.split('\r\n'); + return { + e: item.episodeInformation.episodeNumber+'', + lang: [], + name: item.title, + season: item.episodeInformation.seasonNumber+'', + seasonTitle: request.series.seasons[item.episodeInformation.seasonNumber-1]?.title ?? request.series.title, + episode: item.episodeInformation.episodeNumber+'', + id: item.id+'', + img: item.thumbnailUrl, + description: description ? description[0] : '', + time: '' + }; + })}; + } + + public async downloadItem(data: DownloadData) { + this.setDownloading(true); + console.debug(`Got download options: ${JSON.stringify(data)}`); + const _default = yargs.appArgv(this.hidive.cfg.cli, true); + const res = await this.hidive.selectSeries(parseInt(data.id), data.e, false, false); + if (!res.isOk || !res.showData) + return this.alertError(new Error('Download failed upstream, check for additional logs')); + + for (const ep of res.value) { + await this.hidive.downloadEpisode(ep, {..._default, callbackMaker: this.makeProgressHandler.bind(this), dubLang: data.dubLang, dlsubs: data.dlsubs, fileName: data.fileName, q: data.q, force: 'y', noaudio: data.noaudio, novids: data.novids }); + } + this.sendMessage({ name: 'finish', data: undefined }); + this.setDownloading(false); + this.onFinish(); } - this.sendMessage({ name: 'finish', data: undefined }); - this.setDownloading(false); - this.onFinish(); - } } export default HidiveHandler; \ No newline at end of file diff --git a/gui/server/websocket.ts b/gui/server/websocket.ts index 88044c2..68e5312 100644 --- a/gui/server/websocket.ts +++ b/gui/server/websocket.ts @@ -16,108 +16,108 @@ class ExternalEvent extends EventEmitter {} export default class WebSocketHandler { - private wsServer: ws.Server; + private wsServer: ws.Server; - public events: ExternalEvent = new ExternalEvent(); + public events: ExternalEvent = new ExternalEvent(); - constructor(server: Server) { - this.wsServer = new ws.WebSocketServer({ noServer: true, path: '/private' }); + constructor(server: Server) { + this.wsServer = new ws.WebSocketServer({ noServer: true, path: '/private' }); - this.wsServer.on('connection', (socket, req) => { - console.info(`[WS] Connection from '${req.socket.remoteAddress}'`); - socket.on('error', (er) => console.error(`[WS] ${er}`)); - socket.on('message', (data) => { - const json = JSON.parse(data.toString()) as UnknownWSMessage; - this.events.emit(json.name, json as any, (data) => { - this.wsServer.clients.forEach(client => { - if (client.readyState !== WebSocket.OPEN) - return; - client.send(JSON.stringify({ - data, - id: json.id, - name: json.name - }), (er) => { - if (er) - console.error(`[WS] ${er}`); + this.wsServer.on('connection', (socket, req) => { + console.info(`[WS] Connection from '${req.socket.remoteAddress}'`); + socket.on('error', (er) => console.error(`[WS] ${er}`)); + socket.on('message', (data) => { + const json = JSON.parse(data.toString()) as UnknownWSMessage; + this.events.emit(json.name, json as any, (data) => { + this.wsServer.clients.forEach(client => { + if (client.readyState !== WebSocket.OPEN) + return; + client.send(JSON.stringify({ + data, + id: json.id, + name: json.name + }), (er) => { + if (er) + console.error(`[WS] ${er}`); + }); + }); + }); }); - }); }); - }); - }); - server.on('upgrade', (request, socket, head) => { - if (!this.wsServer.shouldHandle(request)) - return; - if (!this.authenticate(request)) { - socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); - socket.destroy(); - console.info(`[WS] ${request.socket.remoteAddress} tried to connect but used a wrong password.`); - return; - } - this.wsServer.handleUpgrade(request, socket, head, socket => { - this.wsServer.emit('connection', socket, request); - }); - }); - } + server.on('upgrade', (request, socket, head) => { + if (!this.wsServer.shouldHandle(request)) + return; + if (!this.authenticate(request)) { + socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); + socket.destroy(); + console.info(`[WS] ${request.socket.remoteAddress} tried to connect but used a wrong password.`); + return; + } + this.wsServer.handleUpgrade(request, socket, head, socket => { + this.wsServer.emit('connection', socket, request); + }); + }); + } - public sendMessage(data: RandomEvent) { - this.wsServer.clients.forEach(client => { - if (client.readyState !== WebSocket.OPEN) - return; - client.send(JSON.stringify(data), (er) => { - if (er) - console.error(`[WS] ${er}`); - }); - }); - } + public sendMessage(data: RandomEvent) { + this.wsServer.clients.forEach(client => { + if (client.readyState !== WebSocket.OPEN) + return; + client.send(JSON.stringify(data), (er) => { + if (er) + console.error(`[WS] ${er}`); + }); + }); + } - private authenticate(request: IncomingMessage): boolean { - const search = new URL(`http://${request.headers.host}${request.url}`).searchParams; - return cfg.gui.password === (search.get('password') ?? undefined); - } + private authenticate(request: IncomingMessage): boolean { + const search = new URL(`http://${request.headers.host}${request.url}`).searchParams; + return cfg.gui.password === (search.get('password') ?? undefined); + } } export class PublicWebSocket { - private wsServer: ws.Server; + private wsServer: ws.Server; - private state = getState(); - constructor(server: Server) { - this.wsServer = new ws.WebSocketServer({ noServer: true, path: '/public' }); + private state = getState(); + constructor(server: Server) { + this.wsServer = new ws.WebSocketServer({ noServer: true, path: '/public' }); - this.wsServer.on('connection', (socket, req) => { - console.info(`[WS] Connection to public ws from '${req.socket.remoteAddress}'`); - socket.on('error', (er) => console.error(`[WS] ${er}`)); - socket.on('message', (msg) => { - const data = JSON.parse(msg.toString()) as UnknownWSMessage; - switch (data.name) { - case 'isSetup': - this.send(socket, data.id, data.name, this.state.setup); - break; - case 'requirePassword': - this.send(socket, data.id, data.name, cfg.gui.password !== undefined); - break; - } - }); - }); + this.wsServer.on('connection', (socket, req) => { + console.info(`[WS] Connection to public ws from '${req.socket.remoteAddress}'`); + socket.on('error', (er) => console.error(`[WS] ${er}`)); + socket.on('message', (msg) => { + const data = JSON.parse(msg.toString()) as UnknownWSMessage; + switch (data.name) { + case 'isSetup': + this.send(socket, data.id, data.name, this.state.setup); + break; + case 'requirePassword': + this.send(socket, data.id, data.name, cfg.gui.password !== undefined); + break; + } + }); + }); - server.on('upgrade', (request, socket, head) => { - if (!this.wsServer.shouldHandle(request)) - return; - this.wsServer.handleUpgrade(request, socket, head, socket => { - this.wsServer.emit('connection', socket, request); - }); - }); - } + server.on('upgrade', (request, socket, head) => { + if (!this.wsServer.shouldHandle(request)) + return; + this.wsServer.handleUpgrade(request, socket, head, socket => { + this.wsServer.emit('connection', socket, request); + }); + }); + } - private send(client: ws.WebSocket, id: string, name: string, data: any) { - client.send(JSON.stringify({ - data, - id, - name - }), (er) => { - if (er) - console.error(`[WS] ${er}`); - }); - } + private send(client: ws.WebSocket, id: string, name: string, data: any) { + client.send(JSON.stringify({ + data, + id, + name + }), (er) => { + if (er) + console.error(`[WS] ${er}`); + }); + } } diff --git a/hidive.ts b/hidive.ts index ccde87f..7ccc794 100644 --- a/hidive.ts +++ b/hidive.ts @@ -39,378 +39,378 @@ import { canDecrypt, getKeysWVD, cdm, getKeysPRD } from './modules/cdm'; import { KeyContainer } from './modules/widevine/license'; export default class Hidive implements ServiceClass { - public cfg: yamlCfg.ConfigObject; - private token: Record; - private req: reqModule.Req; + public cfg: yamlCfg.ConfigObject; + private token: Record; + private req: reqModule.Req; - constructor(private debug = false) { - this.cfg = yamlCfg.loadCfg(); - this.token = yamlCfg.loadNewHDToken(); - this.req = new reqModule.Req(domain, debug, false, 'hd'); - } + constructor(private debug = false) { + this.cfg = yamlCfg.loadCfg(); + this.token = yamlCfg.loadNewHDToken(); + this.req = new reqModule.Req(domain, debug, false, 'hd'); + } - public async cli() { - console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`); - const argv = yargs.appArgv(this.cfg.cli); - if (argv.debug) - this.debug = true; + public async cli() { + console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`); + const argv = yargs.appArgv(this.cfg.cli); + if (argv.debug) + this.debug = true; - //below is for quickly testing API calls - /*const apiTest = await this.apiReq('/v4/season/18871', '', 'auth', 'GET'); + //below is for quickly testing API calls + /*const apiTest = await this.apiReq('/v4/season/18871', '', 'auth', 'GET'); if(!apiTest.ok || !apiTest.res){return;} console.info(apiTest.res.body); fs.writeFileSync('apitest.json', JSON.stringify(JSON.parse(apiTest.res.body), null, 2)); return console.info('test done');*/ - // load binaries - this.cfg.bin = await yamlCfg.loadBinCfg(); - if (argv.allDubs) { - argv.dubLang = langsData.dubLanguageCodes; - } - if (argv.auth) { - //Authenticate - await this.doAuth({ - username: argv.username ?? await Helper.question('[Q] LOGIN/EMAIL: '), - password: argv.password ?? await Helper.question('[Q] PASSWORD: ') - }); - } else if (argv.search && argv.search.length > 2){ - await this.doSearch({ ...argv, search: argv.search as string }); - } else if (argv.s && !isNaN(parseInt(argv.s,10)) && parseInt(argv.s,10) > 0) { - const selected = await this.selectSeason(parseInt(argv.s), argv.e, argv.but, argv.all); - if (selected.isOk && selected.showData) { - for (const select of selected.value) { - //download episode - if (!(await this.downloadEpisode(select, {...argv}))) { - console.error(`Unable to download selected episode ${select.episodeInformation.episodeNumber}`); - return false; - } + // load binaries + this.cfg.bin = await yamlCfg.loadBinCfg(); + if (argv.allDubs) { + argv.dubLang = langsData.dubLanguageCodes; } - } - return true; - } else if (argv.srz && !isNaN(parseInt(argv.srz,10)) && parseInt(argv.srz,10) > 0) { - const selected = await this.selectSeries(parseInt(argv.srz), argv.e, argv.but, argv.all); - if (selected.isOk && selected.showData) { - for (const select of selected.value) { - //download episode - if (!(await this.downloadEpisode(select, {...argv}))) { - console.error(`Unable to download selected episode ${select.episodeInformation.episodeNumber}`); - return false; - } - } - } - } else if (argv.new) { - console.error('--new is not yet implemented in the new API'); - } else if(argv.e) { - if (!(await this.downloadSingleEpisode(parseInt(argv.e), {...argv}))) { - console.error(`Unable to download selected episode ${argv.e}`); - return false; - } - } else { - console.info('No option selected or invalid value entered. Try --help.'); - } - } - - - public async apiReq(endpoint: string, body: string | object = '', authType: 'refresh' | 'auth' | 'both' | 'other' | 'none' = 'none', method: 'GET' | 'POST' = 'POST', authHeader?: string) { - const options = { - headers: { - 'X-Api-Key': api.hd_new_apiKey, - 'X-App-Var': api.hd_new_version, - 'realm': 'dce.hidive', - 'Referer': 'https://www.hidive.com/', - 'Origin': 'https://www.hidive.com' - } as Record, - method: method as 'GET'|'POST', - url: api.hd_new_api+endpoint as string, - body: body, - useProxy: true - }; - // get request type - const isGet = method == 'GET'; - if(!isGet){ - options.body = body == '' ? body : JSON.stringify(body); - options.headers['Content-Type'] = 'application/json'; - } - if (authType == 'other') { - options.headers['Authorization'] = authHeader; - } else if (authType == 'auth') { - options.headers['Authorization'] = `Bearer ${this.token.authorisationToken}`; - } else if (authType == 'refresh') { - options.headers['Authorization'] = `Bearer ${this.token.refreshToken}`; - } else if (authType == 'both') { - options.headers['Authorization'] = `Mixed ${this.token.authorisationToken} ${this.token.refreshToken}`; - } - if (this.debug) { - console.debug('[DEBUG] Request params:'); - console.debug(options); - } - const apiReqOpts: reqModule.Params = { - method: options.method, - headers: options.headers as Record, - body: options.body as string - }; - let apiReq = await this.req.getData(options.url, apiReqOpts); - if(!apiReq.ok || !apiReq.res){ - if ((apiReq.error && apiReq.error.res?.status == 401) || (apiReq.res && apiReq.res.status == 401)) { - console.warn('Token expired, refreshing token and retrying.'); - if (await this.refreshToken()) { - if (authType == 'other') { - options.headers['Authorization'] = authHeader; - } else if (authType == 'auth') { - options.headers['Authorization'] = `Bearer ${this.token.authorisationToken}`; - } else if (authType == 'refresh') { - options.headers['Authorization'] = `Bearer ${this.token.refreshToken}`; - } else if (authType == 'both') { - options.headers['Authorization'] = `Mixed ${this.token.authorisationToken} ${this.token.refreshToken}`; - } - apiReq = await this.req.getData(options.url, apiReqOpts); - if(!apiReq.ok || !apiReq.res) { - console.error('API Request Failed!'); - return { - ok: false, - res: apiReq.res, - error: apiReq.error - }; - } + if (argv.auth) { + //Authenticate + await this.doAuth({ + username: argv.username ?? await Helper.question('[Q] LOGIN/EMAIL: '), + password: argv.password ?? await Helper.question('[Q] PASSWORD: ') + }); + } else if (argv.search && argv.search.length > 2){ + await this.doSearch({ ...argv, search: argv.search as string }); + } else if (argv.s && !isNaN(parseInt(argv.s,10)) && parseInt(argv.s,10) > 0) { + const selected = await this.selectSeason(parseInt(argv.s), argv.e, argv.but, argv.all); + if (selected.isOk && selected.showData) { + for (const select of selected.value) { + //download episode + if (!(await this.downloadEpisode(select, {...argv}))) { + console.error(`Unable to download selected episode ${select.episodeInformation.episodeNumber}`); + return false; + } + } + } + return true; + } else if (argv.srz && !isNaN(parseInt(argv.srz,10)) && parseInt(argv.srz,10) > 0) { + const selected = await this.selectSeries(parseInt(argv.srz), argv.e, argv.but, argv.all); + if (selected.isOk && selected.showData) { + for (const select of selected.value) { + //download episode + if (!(await this.downloadEpisode(select, {...argv}))) { + console.error(`Unable to download selected episode ${select.episodeInformation.episodeNumber}`); + return false; + } + } + } + } else if (argv.new) { + console.error('--new is not yet implemented in the new API'); + } else if(argv.e) { + if (!(await this.downloadSingleEpisode(parseInt(argv.e), {...argv}))) { + console.error(`Unable to download selected episode ${argv.e}`); + return false; + } } else { - console.error('Failed to refresh token...'); - return { - ok: false, - res: apiReq.res, - error: apiReq.error - }; + console.info('No option selected or invalid value entered. Try --help.'); } - } else { - console.error('API Request Failed!'); - return { - ok: false, - res: apiReq.res, - error: apiReq.error + } + + + public async apiReq(endpoint: string, body: string | object = '', authType: 'refresh' | 'auth' | 'both' | 'other' | 'none' = 'none', method: 'GET' | 'POST' = 'POST', authHeader?: string) { + const options = { + headers: { + 'X-Api-Key': api.hd_new_apiKey, + 'X-App-Var': api.hd_new_version, + 'realm': 'dce.hidive', + 'Referer': 'https://www.hidive.com/', + 'Origin': 'https://www.hidive.com' + } as Record, + method: method as 'GET'|'POST', + url: api.hd_new_api+endpoint as string, + body: body, + useProxy: true }; - } - } - return { - ok: true, - res: apiReq.res, - }; - } - - public async doAuth(data: AuthData): Promise { - if (!this.token.refreshToken || !this.token.authorisationToken) { - await this.doAnonymousAuth(); - } - const authReq = await this.apiReq('/v2/login', { - id: data.username, - secret: data.password - }, 'auth'); - if(!authReq.ok || !authReq.res){ - console.error('Authentication failed!'); - return { isOk: false, reason: new Error('Authentication failed') }; - } - const tokens: Record = JSON.parse(await authReq.res.text()); - for (const token in tokens) { - this.token[token] = tokens[token]; - } - this.token.guest = false; - yamlCfg.saveNewHDToken(this.token); - console.info('Auth complete!'); - return { isOk: true, value: undefined }; - } - - public async doAnonymousAuth() { - const authReq = await this.apiReq('/v2/login/guest/checkin'); - if(!authReq.ok || !authReq.res){ - console.error('Authentication failed!'); - return false; - } - const tokens: Record = JSON.parse(await authReq.res.text()); - for (const token in tokens) { - this.token[token] = tokens[token]; - } - //this.token.expires = new Date(Date.now() + 300); - this.token.guest = true; - yamlCfg.saveNewHDToken(this.token); - return true; - } - - public async refreshToken() { - if (!this.token.refreshToken || !this.token.authorisationToken) { - return await this.doAnonymousAuth(); - } else { - const authReq = await this.apiReq('/v2/token/refresh', { - 'refreshToken': this.token.refreshToken - }, 'auth'); - if(!authReq.ok || !authReq.res){ - console.error('Token refresh failed, reinitializing session...'); - return this.initSession(); - } - const tokens: Record = JSON.parse(await authReq.res.text()); - for (const token in tokens) { - this.token[token] = tokens[token]; - } - yamlCfg.saveNewHDToken(this.token); - return true; - } - } - - public async initSession() { - const authReq = await this.apiReq('/v1/init/', '', 'both', 'GET'); - if(!authReq.ok || !authReq.res){ - console.error('Failed to initialize session.'); - return false; - } - const tokens: Record = JSON.parse(await authReq.res.text()).authentication; - for (const token in tokens) { - this.token[token] = tokens[token]; - } - yamlCfg.saveNewHDToken(this.token); - return true; - } - - public async doSearch(data: SearchData): Promise { - const searchReq = await this.req.getData('https://h99xldr8mj-dsn.algolia.net/1/indexes/*/queries?x-algolia-agent=Algolia%20for%20JavaScript%20(3.35.1)%3B%20Browser&x-algolia-application-id=H99XLDR8MJ&x-algolia-api-key=e55ccb3db0399eabe2bfc37a0314c346', { - method: 'POST', - body: JSON.stringify({'requests': - [ - {'indexName':'prod-dce.hidive-livestreaming-events','params':'query='+encodeURIComponent(data.search)+'&facetFilters=%5B%22type%3ALIVE_EVENT%22%5D&hitsPerPage=25'+(data.page ? '&page='+(data.page-1) : '')}, - {'indexName':'prod-dce.hidive-livestreaming-events','params':'query='+encodeURIComponent(data.search)+'&facetFilters=%5B%22type%3AVOD_VIDEO%22%5D&hitsPerPage=25'+(data.page ? '&page='+(data.page-1) : '')}, - {'indexName':'prod-dce.hidive-livestreaming-events','params':'query='+encodeURIComponent(data.search)+'&facetFilters=%5B%22type%3AVOD_PLAYLIST%22%5D&hitsPerPage=25'+(data.page ? '&page='+(data.page-1) : '')}, - {'indexName':'prod-dce.hidive-livestreaming-events','params':'query='+encodeURIComponent(data.search)+'&facetFilters=%5B%22type%3AVOD_SERIES%22%5D&hitsPerPage=25'+(data.page ? '&page='+(data.page-1) : '')} - ] - }) - }); - if(!searchReq.ok || !searchReq.res){ - console.error('Search FAILED!'); - return { isOk: false, reason: new Error('Search failed. No more information provided') }; - } - const searchData = JSON.parse(await searchReq.res.text()) as NewHidiveSearch; - const searchItems: Hit[] = []; - console.info('Search Results:'); - for (const category of searchData.results) { - for (const hit of category.hits) { - searchItems.push(hit); - let fullType: string; - if (hit.type == 'VOD_SERIES') { - fullType = `Z.${hit.id}`; - } else if (hit.type == 'VOD_VIDEO') { - fullType = `E.${hit.id}`; - } else { - fullType = `${hit.type} #${hit.id}`; + // get request type + const isGet = method == 'GET'; + if(!isGet){ + options.body = body == '' ? body : JSON.stringify(body); + options.headers['Content-Type'] = 'application/json'; } - console.log(`[${fullType}] ${hit.name} ${hit.seasonsCount ? '('+hit.seasonsCount+' Seasons)' : ''}`); - } + if (authType == 'other') { + options.headers['Authorization'] = authHeader; + } else if (authType == 'auth') { + options.headers['Authorization'] = `Bearer ${this.token.authorisationToken}`; + } else if (authType == 'refresh') { + options.headers['Authorization'] = `Bearer ${this.token.refreshToken}`; + } else if (authType == 'both') { + options.headers['Authorization'] = `Mixed ${this.token.authorisationToken} ${this.token.refreshToken}`; + } + if (this.debug) { + console.debug('[DEBUG] Request params:'); + console.debug(options); + } + const apiReqOpts: reqModule.Params = { + method: options.method, + headers: options.headers as Record, + body: options.body as string + }; + let apiReq = await this.req.getData(options.url, apiReqOpts); + if(!apiReq.ok || !apiReq.res){ + if ((apiReq.error && apiReq.error.res?.status == 401) || (apiReq.res && apiReq.res.status == 401)) { + console.warn('Token expired, refreshing token and retrying.'); + if (await this.refreshToken()) { + if (authType == 'other') { + options.headers['Authorization'] = authHeader; + } else if (authType == 'auth') { + options.headers['Authorization'] = `Bearer ${this.token.authorisationToken}`; + } else if (authType == 'refresh') { + options.headers['Authorization'] = `Bearer ${this.token.refreshToken}`; + } else if (authType == 'both') { + options.headers['Authorization'] = `Mixed ${this.token.authorisationToken} ${this.token.refreshToken}`; + } + apiReq = await this.req.getData(options.url, apiReqOpts); + if(!apiReq.ok || !apiReq.res) { + console.error('API Request Failed!'); + return { + ok: false, + res: apiReq.res, + error: apiReq.error + }; + } + } else { + console.error('Failed to refresh token...'); + return { + ok: false, + res: apiReq.res, + error: apiReq.error + }; + } + } else { + console.error('API Request Failed!'); + return { + ok: false, + res: apiReq.res, + error: apiReq.error + }; + } + } + return { + ok: true, + res: apiReq.res, + }; } - return { isOk: true, value: searchItems.filter(a => a.type == 'VOD_SERIES').flatMap((a): SearchResponseItem => { - return { - id: a.id+'', - image: a.coverUrl ?? '/notFound.png', - name: a.name, - rating: -1, - desc: a.description - }; - })}; - } - public async getSeries(id: number) { - const getSeriesData = await this.apiReq(`/v4/series/${id}?rpp=20`, '', 'auth', 'GET'); - if (!getSeriesData.ok || !getSeriesData.res) { - console.error('Failed to get Series Data'); - return { isOk: false }; + public async doAuth(data: AuthData): Promise { + if (!this.token.refreshToken || !this.token.authorisationToken) { + await this.doAnonymousAuth(); + } + const authReq = await this.apiReq('/v2/login', { + id: data.username, + secret: data.password + }, 'auth'); + if(!authReq.ok || !authReq.res){ + console.error('Authentication failed!'); + return { isOk: false, reason: new Error('Authentication failed') }; + } + const tokens: Record = JSON.parse(await authReq.res.text()); + for (const token in tokens) { + this.token[token] = tokens[token]; + } + this.token.guest = false; + yamlCfg.saveNewHDToken(this.token); + console.info('Auth complete!'); + return { isOk: true, value: undefined }; + } + + public async doAnonymousAuth() { + const authReq = await this.apiReq('/v2/login/guest/checkin'); + if(!authReq.ok || !authReq.res){ + console.error('Authentication failed!'); + return false; + } + const tokens: Record = JSON.parse(await authReq.res.text()); + for (const token in tokens) { + this.token[token] = tokens[token]; + } + //this.token.expires = new Date(Date.now() + 300); + this.token.guest = true; + yamlCfg.saveNewHDToken(this.token); + return true; } - const seriesData = JSON.parse(await getSeriesData.res.text()) as NewHidiveSeries; - return { isOk: true, value: seriesData }; - } - /** + public async refreshToken() { + if (!this.token.refreshToken || !this.token.authorisationToken) { + return await this.doAnonymousAuth(); + } else { + const authReq = await this.apiReq('/v2/token/refresh', { + 'refreshToken': this.token.refreshToken + }, 'auth'); + if(!authReq.ok || !authReq.res){ + console.error('Token refresh failed, reinitializing session...'); + return this.initSession(); + } + const tokens: Record = JSON.parse(await authReq.res.text()); + for (const token in tokens) { + this.token[token] = tokens[token]; + } + yamlCfg.saveNewHDToken(this.token); + return true; + } + } + + public async initSession() { + const authReq = await this.apiReq('/v1/init/', '', 'both', 'GET'); + if(!authReq.ok || !authReq.res){ + console.error('Failed to initialize session.'); + return false; + } + const tokens: Record = JSON.parse(await authReq.res.text()).authentication; + for (const token in tokens) { + this.token[token] = tokens[token]; + } + yamlCfg.saveNewHDToken(this.token); + return true; + } + + public async doSearch(data: SearchData): Promise { + const searchReq = await this.req.getData('https://h99xldr8mj-dsn.algolia.net/1/indexes/*/queries?x-algolia-agent=Algolia%20for%20JavaScript%20(3.35.1)%3B%20Browser&x-algolia-application-id=H99XLDR8MJ&x-algolia-api-key=e55ccb3db0399eabe2bfc37a0314c346', { + method: 'POST', + body: JSON.stringify({'requests': + [ + {'indexName':'prod-dce.hidive-livestreaming-events','params':'query='+encodeURIComponent(data.search)+'&facetFilters=%5B%22type%3ALIVE_EVENT%22%5D&hitsPerPage=25'+(data.page ? '&page='+(data.page-1) : '')}, + {'indexName':'prod-dce.hidive-livestreaming-events','params':'query='+encodeURIComponent(data.search)+'&facetFilters=%5B%22type%3AVOD_VIDEO%22%5D&hitsPerPage=25'+(data.page ? '&page='+(data.page-1) : '')}, + {'indexName':'prod-dce.hidive-livestreaming-events','params':'query='+encodeURIComponent(data.search)+'&facetFilters=%5B%22type%3AVOD_PLAYLIST%22%5D&hitsPerPage=25'+(data.page ? '&page='+(data.page-1) : '')}, + {'indexName':'prod-dce.hidive-livestreaming-events','params':'query='+encodeURIComponent(data.search)+'&facetFilters=%5B%22type%3AVOD_SERIES%22%5D&hitsPerPage=25'+(data.page ? '&page='+(data.page-1) : '')} + ] + }) + }); + if(!searchReq.ok || !searchReq.res){ + console.error('Search FAILED!'); + return { isOk: false, reason: new Error('Search failed. No more information provided') }; + } + const searchData = JSON.parse(await searchReq.res.text()) as NewHidiveSearch; + const searchItems: Hit[] = []; + console.info('Search Results:'); + for (const category of searchData.results) { + for (const hit of category.hits) { + searchItems.push(hit); + let fullType: string; + if (hit.type == 'VOD_SERIES') { + fullType = `Z.${hit.id}`; + } else if (hit.type == 'VOD_VIDEO') { + fullType = `E.${hit.id}`; + } else { + fullType = `${hit.type} #${hit.id}`; + } + console.log(`[${fullType}] ${hit.name} ${hit.seasonsCount ? '('+hit.seasonsCount+' Seasons)' : ''}`); + } + } + return { isOk: true, value: searchItems.filter(a => a.type == 'VOD_SERIES').flatMap((a): SearchResponseItem => { + return { + id: a.id+'', + image: a.coverUrl ?? '/notFound.png', + name: a.name, + rating: -1, + desc: a.description + }; + })}; + } + + public async getSeries(id: number) { + const getSeriesData = await this.apiReq(`/v4/series/${id}?rpp=20`, '', 'auth', 'GET'); + if (!getSeriesData.ok || !getSeriesData.res) { + console.error('Failed to get Series Data'); + return { isOk: false }; + } + const seriesData = JSON.parse(await getSeriesData.res.text()) as NewHidiveSeries; + return { isOk: true, value: seriesData }; + } + + /** * Function to get the season data from the API * @param id ID of the season * @param lastSeen Last episode ID seen, used for paging * @returns */ - public async getSeason(id: number, lastSeen?: number) { - const getSeasonData = await this.apiReq(`/v4/season/${id}?rpp=20${lastSeen ? '&lastSeen='+lastSeen : ''}`, '', 'auth', 'GET'); - if (!getSeasonData.ok || !getSeasonData.res) { - console.error('Failed to get Season Data'); - return { isOk: false }; - } - const seasonData = JSON.parse(await getSeasonData.res.text()) as NewHidiveSeason; - return { isOk: true, value: seasonData }; - } - - public async listSeries(id: number) { - const series = await this.getSeries(id); - if (!series.isOk || !series.value) { - console.error('Failed to list series data: Failed to get series'); - return { isOk: false }; - } - console.info(`[Z.${series.value.id}] ${series.value.title} (${series.value.seasons.length} Seasons)`); - if (series.value.seasons.length === 0) { - console.info(' No Seasons found!'); - return { isOk: false }; - } - const episodes: Episode[] = []; - for (const seasonData of series.value.seasons) { - const season = await this.getSeason(seasonData.id); - if (!season.isOk || !season.value) { - console.error('Failed to list series data: Failed to get season '+seasonData.id); - return { isOk: false }; - } - console.info(` [S.${season.value.id}] ${season.value.title} (${season.value.episodeCount} Episodes)`); - while (season.value.paging.moreDataAvailable) { - const seasonPage = await this.getSeason(seasonData.id, season.value.paging.lastSeen); - if (!seasonPage.isOk || !seasonPage.value) break; - season.value.episodes = season.value.episodes.concat(seasonPage.value.episodes); - season.value.paging.lastSeen = seasonPage.value.paging.lastSeen; - 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]; + public async getSeason(id: number, lastSeen?: number) { + const getSeasonData = await this.apiReq(`/v4/season/${id}?rpp=20${lastSeen ? '&lastSeen='+lastSeen : ''}`, '', 'auth', 'GET'); + if (!getSeasonData.ok || !getSeasonData.res) { + console.error('Failed to get Season Data'); + return { isOk: false }; } - //S${episode.episodeInformation.seasonNumber}E${episode.episodeInformation.episodeNumber} - - if (!datePattern.test(episode.title) && episode.duration !== 10) { - episodes.push(episode); + const seasonData = JSON.parse(await getSeasonData.res.text()) as NewHidiveSeason; + return { isOk: true, value: seasonData }; + } + + public async listSeries(id: number) { + const series = await this.getSeries(id); + if (!series.isOk || !series.value) { + console.error('Failed to list series data: Failed to get series'); + return { isOk: false }; } - console.info(` [E.${episode.id}] ${episode.title}`); - } + console.info(`[Z.${series.value.id}] ${series.value.title} (${series.value.seasons.length} Seasons)`); + if (series.value.seasons.length === 0) { + console.info(' No Seasons found!'); + return { isOk: false }; + } + const episodes: Episode[] = []; + for (const seasonData of series.value.seasons) { + const season = await this.getSeason(seasonData.id); + if (!season.isOk || !season.value) { + console.error('Failed to list series data: Failed to get season '+seasonData.id); + return { isOk: false }; + } + console.info(` [S.${season.value.id}] ${season.value.title} (${season.value.episodeCount} Episodes)`); + while (season.value.paging.moreDataAvailable) { + const seasonPage = await this.getSeason(seasonData.id, season.value.paging.lastSeen); + if (!seasonPage.isOk || !seasonPage.value) break; + season.value.episodes = season.value.episodes.concat(seasonPage.value.episodes); + season.value.paging.lastSeen = seasonPage.value.paging.lastSeen; + 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); + } + console.info(` [E.${episode.id}] ${episode.title}`); + } + } + return { isOk: true, value: episodes, series: series.value }; } - return { isOk: true, value: episodes, series: series.value }; - } - public async listSeason(id: number) { - const season = await this.getSeason(id); - if (!season.isOk || !season.value) { - console.error('Failed to list series data: Failed to get season '+id); - return { isOk: false }; + public async listSeason(id: number) { + const season = await this.getSeason(id); + if (!season.isOk || !season.value) { + console.error('Failed to list series data: Failed to get season '+id); + return { isOk: false }; + } + console.info(` [S.${season.value.id}] ${season.value.title} (${season.value.episodeCount} Episodes)`); + while (season.value.paging.moreDataAvailable) { + const seasonPage = await this.getSeason(id, season.value.paging.lastSeen); + if (!seasonPage.isOk || !seasonPage.value) break; + season.value.episodes = season.value.episodes.concat(seasonPage.value.episodes); + season.value.paging.lastSeen = seasonPage.value.paging.lastSeen; + season.value.paging.moreDataAvailable = seasonPage.value.paging.moreDataAvailable; + } + 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); + } + console.info(` [E.${episode.id}] ${episode.title}`); + } + const series: NewHidiveSeriesExtra = {...season.value.series, season: season.value}; + return { isOk: true, value: episodes, series: series }; } - console.info(` [S.${season.value.id}] ${season.value.title} (${season.value.episodeCount} Episodes)`); - while (season.value.paging.moreDataAvailable) { - const seasonPage = await this.getSeason(id, season.value.paging.lastSeen); - if (!seasonPage.isOk || !seasonPage.value) break; - season.value.episodes = season.value.episodes.concat(seasonPage.value.episodes); - season.value.paging.lastSeen = seasonPage.value.paging.lastSeen; - season.value.paging.moreDataAvailable = seasonPage.value.paging.moreDataAvailable; - } - 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); - } - console.info(` [E.${episode.id}] ${episode.title}`); - } - const series: NewHidiveSeriesExtra = {...season.value.series, season: season.value}; - return { isOk: true, value: episodes, series: series }; - } - /** + /** * Lists the requested series, and returns the selected episodes * @param id Series ID * @param e Selector @@ -418,43 +418,43 @@ export default class Hidive implements ServiceClass { * @param all Whether to download all available videos * @returns */ - public async selectSeries(id: number, e: string | undefined, but: boolean, all: boolean) { - const getShowData = await this.listSeries(id); - if (!getShowData.isOk || !getShowData.value) { - return { isOk: false, value: [] }; - } - const showData = getShowData.value; - const doEpsFilter = parseSelect(e as string); - // build selected episodes - const selEpsArr: NewHidiveEpisodeExtra[] = []; let ovaSeq = 1; let movieSeq = 1; - 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; - let nameLong = showData[i].title; - if (nameLong.match(/OVA/i)) { - nameLong = 'ova' + (('0' + ovaSeq).slice(-2)); ovaSeq++; - } else if (nameLong.match(/Theatrical/i)) { - nameLong = 'movie' + (('0' + movieSeq).slice(-2)); movieSeq++; - } - let selMark = ''; - if (all || + public async selectSeries(id: number, e: string | undefined, but: boolean, all: boolean) { + const getShowData = await this.listSeries(id); + if (!getShowData.isOk || !getShowData.value) { + return { isOk: false, value: [] }; + } + const showData = getShowData.value; + const doEpsFilter = parseSelect(e as string); + // build selected episodes + const selEpsArr: NewHidiveEpisodeExtra[] = []; let ovaSeq = 1; let movieSeq = 1; + 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; + let nameLong = showData[i].title; + if (nameLong.match(/OVA/i)) { + nameLong = 'ova' + (('0' + ovaSeq).slice(-2)); ovaSeq++; + } else if (nameLong.match(/Theatrical/i)) { + nameLong = 'movie' + (('0' + movieSeq).slice(-2)); movieSeq++; + } + let selMark = ''; + if (all || but && !doEpsFilter.isSelected([parseFloat(showData[i].episodeInformation.episodeNumber+'')+'', showData[i].id+'']) || !but && doEpsFilter.isSelected([parseFloat(showData[i].episodeInformation.episodeNumber+'')+'', showData[i].id+'']) - ) { - selEpsArr.push({ isSelected: true, titleId, nameLong, seasonTitle, seriesTitle, ...showData[i] }); - selMark = '✓ '; - } - console.info('%s[%s] %s', - selMark, - 'S'+parseFloat(showData[i].episodeInformation.seasonNumber+'')+'E'+parseFloat(showData[i].episodeInformation.episodeNumber+''), - showData[i].title, - ); + ) { + selEpsArr.push({ isSelected: true, titleId, nameLong, seasonTitle, seriesTitle, ...showData[i] }); + selMark = '✓ '; + } + console.info('%s[%s] %s', + selMark, + 'S'+parseFloat(showData[i].episodeInformation.seasonNumber+'')+'E'+parseFloat(showData[i].episodeInformation.episodeNumber+''), + showData[i].title, + ); + } + return { isOk: true, value: selEpsArr, showData: getShowData.series }; } - return { isOk: true, value: selEpsArr, showData: getShowData.series }; - } - /** + /** * Lists the requested season, and returns the selected episodes * @param id Season ID * @param e Selector @@ -462,651 +462,651 @@ export default class Hidive implements ServiceClass { * @param all Whether to download all available videos * @returns */ - public async selectSeason(id: number, e: string | undefined, but: boolean, all: boolean) { - const getShowData = await this.listSeason(id); - if (!getShowData.isOk || !getShowData.value) { - return { isOk: false, value: [] }; - } - const showData = getShowData.value; - const doEpsFilter = parseSelect(e as string); - // build selected episodes - const selEpsArr: NewHidiveEpisodeExtra[] = []; let ovaSeq = 1; let movieSeq = 1; - for (let i = 0; i < showData.length; i++) { - const titleId = showData[i].id; - const seriesTitle = getShowData.series.title; - const seasonTitle = getShowData.series.season.title; - let nameLong = showData[i].title; - if (nameLong.match(/OVA/i)) { - nameLong = 'ova' + (('0' + ovaSeq).slice(-2)); ovaSeq++; - } else if (nameLong.match(/Theatrical/i)) { - nameLong = 'movie' + (('0' + movieSeq).slice(-2)); movieSeq++; - } - let selMark = ''; - if (all || + public async selectSeason(id: number, e: string | undefined, but: boolean, all: boolean) { + const getShowData = await this.listSeason(id); + if (!getShowData.isOk || !getShowData.value) { + return { isOk: false, value: [] }; + } + const showData = getShowData.value; + const doEpsFilter = parseSelect(e as string); + // build selected episodes + const selEpsArr: NewHidiveEpisodeExtra[] = []; let ovaSeq = 1; let movieSeq = 1; + for (let i = 0; i < showData.length; i++) { + const titleId = showData[i].id; + const seriesTitle = getShowData.series.title; + const seasonTitle = getShowData.series.season.title; + let nameLong = showData[i].title; + if (nameLong.match(/OVA/i)) { + nameLong = 'ova' + (('0' + ovaSeq).slice(-2)); ovaSeq++; + } else if (nameLong.match(/Theatrical/i)) { + nameLong = 'movie' + (('0' + movieSeq).slice(-2)); movieSeq++; + } + let selMark = ''; + if (all || but && !doEpsFilter.isSelected([parseFloat(showData[i].episodeInformation.episodeNumber+'')+'', showData[i].id+'']) || !but && doEpsFilter.isSelected([parseFloat(showData[i].episodeInformation.episodeNumber+'')+'', showData[i].id+'']) - ) { - selEpsArr.push({ isSelected: true, titleId, nameLong, seasonTitle, seriesTitle, ...showData[i] }); - selMark = '✓ '; - } - console.info('%s[%s] %s', - selMark, - 'S'+parseFloat(showData[i].episodeInformation.seasonNumber+'')+'E'+parseFloat(showData[i].episodeInformation.episodeNumber+''), - showData[i].title, - ); + ) { + selEpsArr.push({ isSelected: true, titleId, nameLong, seasonTitle, seriesTitle, ...showData[i] }); + selMark = '✓ '; + } + console.info('%s[%s] %s', + selMark, + 'S'+parseFloat(showData[i].episodeInformation.seasonNumber+'')+'E'+parseFloat(showData[i].episodeInformation.episodeNumber+''), + showData[i].title, + ); + } + return { isOk: true, value: selEpsArr, showData: getShowData.series }; } - return { isOk: true, value: selEpsArr, showData: getShowData.series }; - } - public async downloadEpisode(selectedEpisode: NewHidiveEpisodeExtra, options: Record) { + public async downloadEpisode(selectedEpisode: NewHidiveEpisodeExtra, options: Record) { //Get Episode data - const episodeDataReq = await this.apiReq(`/v4/vod/${selectedEpisode.id}?includePlaybackDetails=URL`, '', 'auth', 'GET'); - if (!episodeDataReq.ok || !episodeDataReq.res) { - console.error('Failed to get episode data'); - return { isOk: false, reason: new Error('Failed to get Episode Data') }; - } - const episodeData = JSON.parse(await episodeDataReq.res.text()) as NewHidiveEpisode; + const episodeDataReq = await this.apiReq(`/v4/vod/${selectedEpisode.id}?includePlaybackDetails=URL`, '', 'auth', 'GET'); + if (!episodeDataReq.ok || !episodeDataReq.res) { + console.error('Failed to get episode data'); + return { isOk: false, reason: new Error('Failed to get Episode Data') }; + } + const episodeData = JSON.parse(await episodeDataReq.res.text()) as NewHidiveEpisode; - if (!episodeData.playerUrlCallback) { - console.error('Failed to download episode: You do not have access to this'); - return { isOk: false, reason: new Error('You do not have access to this') }; + if (!episodeData.playerUrlCallback) { + console.error('Failed to download episode: You do not have access to this'); + return { isOk: false, reason: new Error('You do not have access to this') }; + } + + //Get Playback data + const playbackReq = await this.req.getData(episodeData.playerUrlCallback); + if(!playbackReq.ok || !playbackReq.res){ + console.error('Playback Request Failed'); + return { isOk: false, reason: new Error('Playback request failed') }; + } + const playbackData = JSON.parse(await playbackReq.res.text()) as NewHidivePlayback; + + //Get actual MPD + const mpdRequest = await this.req.getData(playbackData.dash[0].url); + if(!mpdRequest.ok || !mpdRequest.res){ + console.error('MPD Request Failed'); + return { isOk: false, reason: new Error('MPD request failed') }; + } + const mpd = await mpdRequest.res.text() as string; + + selectedEpisode.jwtToken = playbackData.dash[0].drm.jwtToken; + + //Output metadata and prepare for download + const availableSubs = playbackData.dash[0].subtitles.filter(a => a.format === 'vtt'); + const showTitle = `${selectedEpisode.seriesTitle} S${selectedEpisode.episodeInformation.seasonNumber}`; + console.info(`[INFO] ${showTitle} - ${selectedEpisode.episodeInformation.episodeNumber}`); + console.info('[INFO] Available dubs and subtitles:'); + console.info('\tAudios: ' + episodeData.offlinePlaybackLanguages.map(a => langsData.languages.find(b => b.code == a)?.name).join('\n\t\t')); + console.info('\tSubs : ' + availableSubs.map(a => langsData.languages.find(b => b.new_hd_locale == a.language)?.name).join('\n\t\t')); + console.info(`[INFO] Selected dub(s): ${options.dubLang.join(', ')}`); + const baseUrl = playbackData.dash[0].url.split('master')[0]; + const parsedmpd = await parse(mpd, undefined, baseUrl); + const res = await this.downloadMPD(parsedmpd, availableSubs, selectedEpisode, options); + if (res === undefined || res.error) { + console.error('Failed to download media list'); + return { isOk: false, reason: new Error('Failed to download media list') }; + } else { + if (!options.skipmux) { + await this.muxStreams(res.data, { ...options, output: res.fileName }, false); + } else { + console.info('Skipping mux'); + } + downloaded({ + service: 'hidive', + type: 's' + }, selectedEpisode.titleId+'', [selectedEpisode.episodeInformation.episodeNumber+'']); + return { isOk: res, value: undefined }; + } } - //Get Playback data - const playbackReq = await this.req.getData(episodeData.playerUrlCallback); - if(!playbackReq.ok || !playbackReq.res){ - console.error('Playback Request Failed'); - return { isOk: false, reason: new Error('Playback request failed') }; - } - const playbackData = JSON.parse(await playbackReq.res.text()) as NewHidivePlayback; - - //Get actual MPD - const mpdRequest = await this.req.getData(playbackData.dash[0].url); - if(!mpdRequest.ok || !mpdRequest.res){ - console.error('MPD Request Failed'); - return { isOk: false, reason: new Error('MPD request failed') }; - } - const mpd = await mpdRequest.res.text() as string; - - selectedEpisode.jwtToken = playbackData.dash[0].drm.jwtToken; - - //Output metadata and prepare for download - const availableSubs = playbackData.dash[0].subtitles.filter(a => a.format === 'vtt'); - const showTitle = `${selectedEpisode.seriesTitle} S${selectedEpisode.episodeInformation.seasonNumber}`; - console.info(`[INFO] ${showTitle} - ${selectedEpisode.episodeInformation.episodeNumber}`); - console.info('[INFO] Available dubs and subtitles:'); - console.info('\tAudios: ' + episodeData.offlinePlaybackLanguages.map(a => langsData.languages.find(b => b.code == a)?.name).join('\n\t\t')); - console.info('\tSubs : ' + availableSubs.map(a => langsData.languages.find(b => b.new_hd_locale == a.language)?.name).join('\n\t\t')); - console.info(`[INFO] Selected dub(s): ${options.dubLang.join(', ')}`); - const baseUrl = playbackData.dash[0].url.split('master')[0]; - const parsedmpd = await parse(mpd, undefined, baseUrl); - const res = await this.downloadMPD(parsedmpd, availableSubs, selectedEpisode, options); - if (res === undefined || res.error) { - console.error('Failed to download media list'); - return { isOk: false, reason: new Error('Failed to download media list') }; - } else { - if (!options.skipmux) { - await this.muxStreams(res.data, { ...options, output: res.fileName }, false); - } else { - console.info('Skipping mux'); - } - downloaded({ - service: 'hidive', - type: 's' - }, selectedEpisode.titleId+'', [selectedEpisode.episodeInformation.episodeNumber+'']); - return { isOk: res, value: undefined }; - } - } - - public async downloadSingleEpisode(id: number, options: Record) { + public async downloadSingleEpisode(id: number, options: Record) { //Get Episode data - const episodeDataReq = await this.apiReq(`/v4/vod/${id}?includePlaybackDetails=URL`, '', 'auth', 'GET'); - if (!episodeDataReq.ok || !episodeDataReq.res) { - console.error('Failed to get episode data'); - return { isOk: false, reason: new Error('Failed to get Episode Data') }; - } - const episodeData = JSON.parse(await episodeDataReq.res.text()) as NewHidiveEpisode; + const episodeDataReq = await this.apiReq(`/v4/vod/${id}?includePlaybackDetails=URL`, '', 'auth', 'GET'); + if (!episodeDataReq.ok || !episodeDataReq.res) { + console.error('Failed to get episode data'); + return { isOk: false, reason: new Error('Failed to get Episode Data') }; + } + const episodeData = JSON.parse(await episodeDataReq.res.text()) as NewHidiveEpisode; - if (episodeData.title.includes(' - ') && episodeData.episodeInformation) { - episodeData.episodeInformation.episodeNumber = parseFloat(episodeData.title.split(' - ')[0].replace('E', '')); - episodeData.title = episodeData.title.split(' - ')[1]; - } + if (episodeData.title.includes(' - ') && episodeData.episodeInformation) { + episodeData.episodeInformation.episodeNumber = parseFloat(episodeData.title.split(' - ')[0].replace('E', '')); + episodeData.title = episodeData.title.split(' - ')[1]; + } - if (!episodeData.playerUrlCallback) { - console.error('Failed to download episode: You do not have access to this'); - return { isOk: false, reason: new Error('You do not have access to this') }; - } + if (!episodeData.playerUrlCallback) { + console.error('Failed to download episode: You do not have access to this'); + return { isOk: false, reason: new Error('You do not have access to this') }; + } - let seasonData: Awaited> | undefined = undefined; - if (episodeData.episodeInformation) { - seasonData = await this.getSeason(episodeData.episodeInformation.season); - if (!seasonData.isOk || !seasonData.value) { - console.error('Failed to get season data'); - return { isOk: false, reason: new Error('Failed to get season data') }; - } - } else { - episodeData.episodeInformation = { - season: 0, - seasonNumber: 0, - episodeNumber: 0, - }; - } + let seasonData: Awaited> | undefined = undefined; + if (episodeData.episodeInformation) { + seasonData = await this.getSeason(episodeData.episodeInformation.season); + if (!seasonData.isOk || !seasonData.value) { + console.error('Failed to get season data'); + return { isOk: false, reason: new Error('Failed to get season data') }; + } + } else { + episodeData.episodeInformation = { + season: 0, + seasonNumber: 0, + episodeNumber: 0, + }; + } - //Get Playback data - const playbackReq = await this.req.getData(episodeData.playerUrlCallback); - if(!playbackReq.ok || !playbackReq.res){ - console.error('Playback Request Failed'); - return { isOk: false, reason: new Error('Playback request failed') }; - } - const playbackData = JSON.parse(await playbackReq.res.text()) as NewHidivePlayback; + //Get Playback data + const playbackReq = await this.req.getData(episodeData.playerUrlCallback); + if(!playbackReq.ok || !playbackReq.res){ + console.error('Playback Request Failed'); + return { isOk: false, reason: new Error('Playback request failed') }; + } + const playbackData = JSON.parse(await playbackReq.res.text()) as NewHidivePlayback; - //Get actual MPD - const mpdRequest = await this.req.getData(playbackData.dash[0].url); - if(!mpdRequest.ok || !mpdRequest.res){ - console.error('MPD Request Failed'); - return { isOk: false, reason: new Error('MPD request failed') }; - } - const mpd = await mpdRequest.res.text() as string; + //Get actual MPD + const mpdRequest = await this.req.getData(playbackData.dash[0].url); + if(!mpdRequest.ok || !mpdRequest.res){ + console.error('MPD Request Failed'); + return { isOk: false, reason: new Error('MPD request failed') }; + } + const mpd = await mpdRequest.res.text() as string; - const selectedEpisode: NewHidiveEpisodeExtra = { - ...episodeData, - nameLong: episodeData.title, - titleId: episodeData.id, - seasonTitle: seasonData?.value.title ?? episodeData.title, - seriesTitle: seasonData?.value.series.title ?? episodeData.title, - isSelected: true - }; + const selectedEpisode: NewHidiveEpisodeExtra = { + ...episodeData, + nameLong: episodeData.title, + titleId: episodeData.id, + seasonTitle: seasonData?.value.title ?? episodeData.title, + seriesTitle: seasonData?.value.series.title ?? episodeData.title, + isSelected: true + }; - selectedEpisode.jwtToken = playbackData.dash[0].drm.jwtToken; + selectedEpisode.jwtToken = playbackData.dash[0].drm.jwtToken; - //Output metadata and prepare for download - const availableSubs = playbackData.dash[0].subtitles.filter(a => a.format === 'vtt'); - const showTitle = `${selectedEpisode.seriesTitle} S${selectedEpisode.episodeInformation.seasonNumber}`; - console.info(`[INFO] ${showTitle} - ${selectedEpisode.episodeInformation.episodeNumber}`); - console.info('[INFO] Available dubs and subtitles:'); - console.info('\tAudios: ' + episodeData.offlinePlaybackLanguages.map(a => langsData.languages.find(b => b.code == a)?.name).join('\n\t\t')); - console.info('\tSubs : ' + availableSubs.map(a => langsData.languages.find(b => b.new_hd_locale == a.language)?.name).join('\n\t\t')); - console.info(`[INFO] Selected dub(s): ${options.dubLang.join(', ')}`); - const baseUrl = playbackData.dash[0].url.split('master')[0]; - const parsedmpd = await parse(mpd, undefined, baseUrl); - const res = await this.downloadMPD(parsedmpd, availableSubs, selectedEpisode, options); - if (res === undefined || res.error) { - console.error('Failed to download media list'); - return { isOk: false, reason: new Error('Failed to download media list') }; - } else { - if (!options.skipmux) { - await this.muxStreams(res.data, { ...options, output: res.fileName }, false); - } else { - console.info('Skipping mux'); - } - downloaded({ - service: 'hidive', - type: 's' - }, selectedEpisode.titleId+'', [selectedEpisode.episodeInformation.episodeNumber+'']); - return { isOk: res, value: undefined }; + //Output metadata and prepare for download + const availableSubs = playbackData.dash[0].subtitles.filter(a => a.format === 'vtt'); + const showTitle = `${selectedEpisode.seriesTitle} S${selectedEpisode.episodeInformation.seasonNumber}`; + console.info(`[INFO] ${showTitle} - ${selectedEpisode.episodeInformation.episodeNumber}`); + console.info('[INFO] Available dubs and subtitles:'); + console.info('\tAudios: ' + episodeData.offlinePlaybackLanguages.map(a => langsData.languages.find(b => b.code == a)?.name).join('\n\t\t')); + console.info('\tSubs : ' + availableSubs.map(a => langsData.languages.find(b => b.new_hd_locale == a.language)?.name).join('\n\t\t')); + console.info(`[INFO] Selected dub(s): ${options.dubLang.join(', ')}`); + const baseUrl = playbackData.dash[0].url.split('master')[0]; + const parsedmpd = await parse(mpd, undefined, baseUrl); + const res = await this.downloadMPD(parsedmpd, availableSubs, selectedEpisode, options); + if (res === undefined || res.error) { + console.error('Failed to download media list'); + return { isOk: false, reason: new Error('Failed to download media list') }; + } else { + if (!options.skipmux) { + await this.muxStreams(res.data, { ...options, output: res.fileName }, false); + } else { + console.info('Skipping mux'); + } + downloaded({ + service: 'hidive', + type: 's' + }, selectedEpisode.titleId+'', [selectedEpisode.episodeInformation.episodeNumber+'']); + return { isOk: res, value: undefined }; + } } - } - public async downloadMPD(streamPlaylists: MPDParsed, subs: Subtitle[], selectedEpisode: NewHidiveEpisodeExtra, options: Record) { + public async downloadMPD(streamPlaylists: MPDParsed, subs: Subtitle[], selectedEpisode: NewHidiveEpisodeExtra, options: Record) { //let fileName: string; - const files: DownloadedMedia[] = []; - const variables: Variable[] = []; - let dlFailed = false; - const subsMargin = 0; - const chosenFontSize = options.originalFontSize ? undefined : options.fontSize; - let encryptionKeys: KeyContainer[] = []; - if (!canDecrypt && (!options.novids || !options.noaudio)) { - console.error('No valid Widevine or PlayReady CDM detected. Please ensure a supported and functional CDM is installed.'); - return undefined; - } - - if (!this.cfg.bin.ffmpeg) - this.cfg.bin = await yamlCfg.loadBinCfg(); - - if (!this.cfg.bin.mp4decrypt && !this.cfg.bin.shaka && (!options.novids || !options.noaudio)) { - console.error('Neither Shaka nor MP4Decrypt found. Please ensure at least one of them is installed.'); - return undefined; - } - - variables.push(...([ - ['title', selectedEpisode.title, true], - ['episode', selectedEpisode.episodeInformation.episodeNumber, false], - ['service', 'HD', false], - ['seriesTitle', selectedEpisode.seasonTitle, true], - ['showTitle', selectedEpisode.seriesTitle, true], - ['season', selectedEpisode.episodeInformation.seasonNumber, false] - ] as [AvailableFilenameVars, string|number, boolean][]).map((a): Variable => { - return { - name: a[0], - replaceWith: a[1], - type: typeof a[1], - sanitize: a[2] - } as Variable; - })); - - //Get name of CDNs/Servers - const streamServers = Object.keys(streamPlaylists); - - options.x = options.x > streamServers.length ? 1 : options.x; - - const selectedServer = streamServers[options.x - 1]; - const selectedList = streamPlaylists[selectedServer]; - - //set Video Qualities - const videos = selectedList.video.map(item => { - return { - ...item, - resolutionText: `${item.quality.width}x${item.quality.height} (${Math.round(item.bandwidth/1024)}KiB/s)` - }; - }); - - const audios = selectedList.audio.map(item => { - return { - ...item, - resolutionText: `${Math.round(item.bandwidth/1000)}kB/s` - }; - }); - - - videos.sort((a, b) => { - return a.bandwidth - b.bandwidth; - }); - - videos.sort((a, b) => { - return a.quality.width - b.quality.width; - }); - - audios.sort((a, b) => { - return a.bandwidth - b.bandwidth; - }); - - let chosenVideoQuality = options.q === 0 ? videos.length : options.q; - if(chosenVideoQuality > videos.length) { - console.warn(`The requested quality of ${options.q} is greater than the maximum ${videos.length}.\n[WARN] Therefor the maximum will be capped at ${videos.length}.`); - chosenVideoQuality = videos.length; - } - chosenVideoQuality--; - - const chosenVideoSegments = videos[chosenVideoQuality]; - - console.info(`Servers available:\n\t${streamServers.join('\n\t')}`); - console.info(`Available Video Qualities:\n\t${videos.map((a, ind) => `[${ind+1}] ${a.resolutionText}`).join('\n\t')}`); - console.info(`Available Audio Qualities:\n\t${audios.map((a, ind) => `[${ind+1}] ${a.resolutionText}`).join('\n\t')}`); - - variables.push({ - name: 'height', - type: 'number', - replaceWith: chosenVideoSegments.quality.height - }, { - name: 'width', - type: 'number', - replaceWith: chosenVideoSegments.quality.width - }); - - const chosenAudios: typeof audios[0][] = []; - const audioByLanguage: Record = {}; - for (const audio of audios) { - if (!audioByLanguage[audio.language.code]) audioByLanguage[audio.language.code] = []; - audioByLanguage[audio.language.code].push(audio); - } - for (const dubLang of options.dubLang as string[]) { - if (audioByLanguage[dubLang]) { - let chosenAudioQuality = options.q === 0 ? audios.length : options.q; - if(chosenAudioQuality > audioByLanguage[dubLang].length) { - chosenAudioQuality = audioByLanguage[dubLang].length; + const files: DownloadedMedia[] = []; + const variables: Variable[] = []; + let dlFailed = false; + const subsMargin = 0; + const chosenFontSize = options.originalFontSize ? undefined : options.fontSize; + let encryptionKeys: KeyContainer[] = []; + if (!canDecrypt && (!options.novids || !options.noaudio)) { + console.error('No valid Widevine or PlayReady CDM detected. Please ensure a supported and functional CDM is installed.'); + return undefined; } - chosenAudioQuality--; - chosenAudios.push(audioByLanguage[dubLang][chosenAudioQuality]); - } - } - if (chosenAudios.length == 0) { - console.error(`Chosen audio language(s) does not exist for episode ${selectedEpisode.episodeInformation.episodeNumber}`); - return undefined; - } - const fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); + if (!this.cfg.bin.ffmpeg) + this.cfg.bin = await yamlCfg.loadBinCfg(); - console.info(`Selected quality: \n\tVideo: ${chosenVideoSegments.resolutionText}\n\tAudio: ${chosenAudios[0].resolutionText}\n\tServer: ${selectedServer}`); - console.info(`Selected (Available) Audio Languages: ${chosenAudios.map(a => a.language.name).join(', ')}`); - console.info('Stream URL:', chosenVideoSegments.segments[0].map.uri.split('/init.mp4')[0]); + if (!this.cfg.bin.mp4decrypt && !this.cfg.bin.shaka && (!options.novids || !options.noaudio)) { + console.error('Neither Shaka nor MP4Decrypt found. Please ensure at least one of them is installed.'); + return undefined; + } - if (chosenAudios[0].pssh_wvd && cdm === 'widevine' || chosenVideoSegments.pssh_wvd && cdm === 'widevine') { - encryptionKeys = await getKeysWVD(chosenVideoSegments.pssh_wvd, 'https://shield-drm.imggaming.com/api/v2/license', { - 'Authorization': `Bearer ${selectedEpisode.jwtToken}`, - 'X-Drm-Info': 'eyJzeXN0ZW0iOiJjb20ud2lkZXZpbmUuYWxwaGEifQ==', - }); - } + variables.push(...([ + ['title', selectedEpisode.title, true], + ['episode', selectedEpisode.episodeInformation.episodeNumber, false], + ['service', 'HD', false], + ['seriesTitle', selectedEpisode.seasonTitle, true], + ['showTitle', selectedEpisode.seriesTitle, true], + ['season', selectedEpisode.episodeInformation.seasonNumber, false] + ] as [AvailableFilenameVars, string|number, boolean][]).map((a): Variable => { + return { + name: a[0], + replaceWith: a[1], + type: typeof a[1], + sanitize: a[2] + } as Variable; + })); - if (chosenAudios[0].pssh_prd && cdm === 'playready' || chosenVideoSegments.pssh_prd && cdm === 'playready') { - encryptionKeys = await getKeysPRD(chosenVideoSegments.pssh_prd, 'https://shield-drm.imggaming.com/api/v2/license', { - 'Authorization': `Bearer ${selectedEpisode.jwtToken}`, - 'X-Drm-Info': 'eyJzeXN0ZW0iOiJjb20ubWljcm9zb2Z0LnBsYXlyZWFkeSJ9', - }); - } + //Get name of CDNs/Servers + const streamServers = Object.keys(streamPlaylists); + + options.x = options.x > streamServers.length ? 1 : options.x; + + const selectedServer = streamServers[options.x - 1]; + const selectedList = streamPlaylists[selectedServer]; + + //set Video Qualities + const videos = selectedList.video.map(item => { + return { + ...item, + resolutionText: `${item.quality.width}x${item.quality.height} (${Math.round(item.bandwidth/1024)}KiB/s)` + }; + }); + + const audios = selectedList.audio.map(item => { + return { + ...item, + resolutionText: `${Math.round(item.bandwidth/1000)}kB/s` + }; + }); + + + videos.sort((a, b) => { + return a.bandwidth - b.bandwidth; + }); + + videos.sort((a, b) => { + return a.quality.width - b.quality.width; + }); + + audios.sort((a, b) => { + return a.bandwidth - b.bandwidth; + }); + + let chosenVideoQuality = options.q === 0 ? videos.length : options.q; + if(chosenVideoQuality > videos.length) { + console.warn(`The requested quality of ${options.q} is greater than the maximum ${videos.length}.\n[WARN] Therefor the maximum will be capped at ${videos.length}.`); + chosenVideoQuality = videos.length; + } + chosenVideoQuality--; + + const chosenVideoSegments = videos[chosenVideoQuality]; + + console.info(`Servers available:\n\t${streamServers.join('\n\t')}`); + console.info(`Available Video Qualities:\n\t${videos.map((a, ind) => `[${ind+1}] ${a.resolutionText}`).join('\n\t')}`); + console.info(`Available Audio Qualities:\n\t${audios.map((a, ind) => `[${ind+1}] ${a.resolutionText}`).join('\n\t')}`); + + variables.push({ + name: 'height', + type: 'number', + replaceWith: chosenVideoSegments.quality.height + }, { + name: 'width', + type: 'number', + replaceWith: chosenVideoSegments.quality.width + }); + + const chosenAudios: typeof audios[0][] = []; + const audioByLanguage: Record = {}; + for (const audio of audios) { + if (!audioByLanguage[audio.language.code]) audioByLanguage[audio.language.code] = []; + audioByLanguage[audio.language.code].push(audio); + } + for (const dubLang of options.dubLang as string[]) { + if (audioByLanguage[dubLang]) { + let chosenAudioQuality = options.q === 0 ? audios.length : options.q; + if(chosenAudioQuality > audioByLanguage[dubLang].length) { + chosenAudioQuality = audioByLanguage[dubLang].length; + } + chosenAudioQuality--; + chosenAudios.push(audioByLanguage[dubLang][chosenAudioQuality]); + } + } + if (chosenAudios.length == 0) { + console.error(`Chosen audio language(s) does not exist for episode ${selectedEpisode.episodeInformation.episodeNumber}`); + return undefined; + } + + const fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); + + console.info(`Selected quality: \n\tVideo: ${chosenVideoSegments.resolutionText}\n\tAudio: ${chosenAudios[0].resolutionText}\n\tServer: ${selectedServer}`); + console.info(`Selected (Available) Audio Languages: ${chosenAudios.map(a => a.language.name).join(', ')}`); + console.info('Stream URL:', chosenVideoSegments.segments[0].map.uri.split('/init.mp4')[0]); + + if (chosenAudios[0].pssh_wvd && cdm === 'widevine' || chosenVideoSegments.pssh_wvd && cdm === 'widevine') { + encryptionKeys = await getKeysWVD(chosenVideoSegments.pssh_wvd, 'https://shield-drm.imggaming.com/api/v2/license', { + 'Authorization': `Bearer ${selectedEpisode.jwtToken}`, + 'X-Drm-Info': 'eyJzeXN0ZW0iOiJjb20ud2lkZXZpbmUuYWxwaGEifQ==', + }); + } + + if (chosenAudios[0].pssh_prd && cdm === 'playready' || chosenVideoSegments.pssh_prd && cdm === 'playready') { + encryptionKeys = await getKeysPRD(chosenVideoSegments.pssh_prd, 'https://shield-drm.imggaming.com/api/v2/license', { + 'Authorization': `Bearer ${selectedEpisode.jwtToken}`, + 'X-Drm-Info': 'eyJzeXN0ZW0iOiJjb20ubWljcm9zb2Z0LnBsYXlyZWFkeSJ9', + }); + } - if (!options.novids) { - //Download Video - const totalParts = chosenVideoSegments.segments.length; - const mathParts = Math.ceil(totalParts / options.partsize); - const mathMsg = `(${mathParts}*${options.partsize})`; - console.info('Total parts in video stream:', totalParts, mathMsg); - const tsFile = path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName); - const tempFile = parseFileName(`temp-${selectedEpisode.id}`, variables, options.numbers, options.override).join(path.sep); - const tempTsFile = path.isAbsolute(tempFile as string) ? tempFile : path.join(this.cfg.dir.content, tempFile); - const dirName = path.dirname(tsFile); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }); - } - const videoJson: M3U8Json = { - segments: chosenVideoSegments.segments - }; - const videoDownload = await new streamdl({ - output: `${tempTsFile}.video.enc.m4s`, - 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(fileName) ? fileName.slice(this.cfg.dir.content.length) : fileName}`, - image: selectedEpisode.thumbnailUrl, - parent: { - title: selectedEpisode.seriesTitle - }, - title: selectedEpisode.title, - language: chosenAudios[0].language - }) : undefined - }).download(); - if(!videoDownload.ok){ - console.error(`DL Stats: ${JSON.stringify(videoDownload.parts)}\n`); - dlFailed = true; - } else { - if (chosenVideoSegments.pssh_wvd || chosenVideoSegments.pssh_prd) { - console.info('Decryption Needed, attempting to decrypt'); - if (encryptionKeys.length == 0) { - console.error('Failed to get encryption keys'); - return undefined; - } - if (this.cfg.bin.mp4decrypt || this.cfg.bin.shaka) { - let commandBase = `--show-progress --key ${encryptionKeys[cdm === 'playready' ? 0 : 1].kid}:${encryptionKeys[cdm === 'playready' ? 0 : 1].key} `; - let commandVideo = commandBase+`"${tempTsFile}.video.enc.m4s" "${tempTsFile}.video.m4s"`; - - if (this.cfg.bin.shaka) { - commandBase = ` --enable_raw_key_decryption ${encryptionKeys.map(kb => '--keys key_id='+kb.kid+':key='+kb.key).join(' ')}`; - commandVideo = `input="${tempTsFile}.video.enc.m4s",stream=video,output="${tempTsFile}.video.m4s"`+commandBase; + if (!options.novids) { + //Download Video + const totalParts = chosenVideoSegments.segments.length; + const mathParts = Math.ceil(totalParts / options.partsize); + const mathMsg = `(${mathParts}*${options.partsize})`; + console.info('Total parts in video stream:', totalParts, mathMsg); + const tsFile = path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName); + const tempFile = parseFileName(`temp-${selectedEpisode.id}`, variables, options.numbers, options.override).join(path.sep); + const tempTsFile = path.isAbsolute(tempFile as string) ? tempFile : path.join(this.cfg.dir.content, tempFile); + const dirName = path.dirname(tsFile); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); } - - console.info('Started decrypting video,', this.cfg.bin.shaka ? 'using shaka' : 'using mp4decrypt'); - const decryptVideo = Helper.exec(this.cfg.bin.shaka ? 'shaka-packager' : 'mp4decrypt', this.cfg.bin.shaka ? `"${this.cfg.bin.shaka}"` : `"${this.cfg.bin.mp4decrypt}"`, commandVideo); - if (!decryptVideo.isOk) { - console.error(decryptVideo.err); - console.error(`Decryption failed with exit code ${decryptVideo.err.code}`); - fs.renameSync(`${tempTsFile}.video.enc.m4s`, `${tsFile}.video.enc.m4s`); - return undefined; + const videoJson: M3U8Json = { + segments: chosenVideoSegments.segments + }; + const videoDownload = await new streamdl({ + output: `${tempTsFile}.video.enc.m4s`, + 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(fileName) ? fileName.slice(this.cfg.dir.content.length) : fileName}`, + image: selectedEpisode.thumbnailUrl, + parent: { + title: selectedEpisode.seriesTitle + }, + title: selectedEpisode.title, + language: chosenAudios[0].language + }) : undefined + }).download(); + if(!videoDownload.ok){ + console.error(`DL Stats: ${JSON.stringify(videoDownload.parts)}\n`); + dlFailed = true; } else { - console.info('Decryption done for video'); - if (!options.nocleanup) { - fs.removeSync(`${tempTsFile}.video.enc.m4s`); - } - fs.copyFileSync(`${tempTsFile}.video.m4s`, `${tsFile}.video.m4s`); - fs.unlinkSync(`${tempTsFile}.video.m4s`); - files.push({ - type: 'Video', - path: `${tsFile}.video.m4s`, - lang: chosenAudios[0].language, - isPrimary: true - }); + if (chosenVideoSegments.pssh_wvd || chosenVideoSegments.pssh_prd) { + console.info('Decryption Needed, attempting to decrypt'); + if (encryptionKeys.length == 0) { + console.error('Failed to get encryption keys'); + return undefined; + } + if (this.cfg.bin.mp4decrypt || this.cfg.bin.shaka) { + let commandBase = `--show-progress --key ${encryptionKeys[cdm === 'playready' ? 0 : 1].kid}:${encryptionKeys[cdm === 'playready' ? 0 : 1].key} `; + let commandVideo = commandBase+`"${tempTsFile}.video.enc.m4s" "${tempTsFile}.video.m4s"`; + + if (this.cfg.bin.shaka) { + commandBase = ` --enable_raw_key_decryption ${encryptionKeys.map(kb => '--keys key_id='+kb.kid+':key='+kb.key).join(' ')}`; + commandVideo = `input="${tempTsFile}.video.enc.m4s",stream=video,output="${tempTsFile}.video.m4s"`+commandBase; + } + + console.info('Started decrypting video,', this.cfg.bin.shaka ? 'using shaka' : 'using mp4decrypt'); + const decryptVideo = Helper.exec(this.cfg.bin.shaka ? 'shaka-packager' : 'mp4decrypt', this.cfg.bin.shaka ? `"${this.cfg.bin.shaka}"` : `"${this.cfg.bin.mp4decrypt}"`, commandVideo); + if (!decryptVideo.isOk) { + console.error(decryptVideo.err); + console.error(`Decryption failed with exit code ${decryptVideo.err.code}`); + fs.renameSync(`${tempTsFile}.video.enc.m4s`, `${tsFile}.video.enc.m4s`); + return undefined; + } else { + console.info('Decryption done for video'); + if (!options.nocleanup) { + fs.removeSync(`${tempTsFile}.video.enc.m4s`); + } + fs.copyFileSync(`${tempTsFile}.video.m4s`, `${tsFile}.video.m4s`); + fs.unlinkSync(`${tempTsFile}.video.m4s`); + files.push({ + type: 'Video', + path: `${tsFile}.video.m4s`, + lang: chosenAudios[0].language, + isPrimary: true + }); + } + } else { + console.warn('mp4decrypt/shaka not found, files need decryption. Decryption Keys:', encryptionKeys); + } + } } - } else { - console.warn('mp4decrypt/shaka not found, files need decryption. Decryption Keys:', encryptionKeys); - } + } else { + console.info('Skipping Video'); } - } - } else { - console.info('Skipping Video'); - } - if (!options.noaudio) { - for (const audio of chosenAudios) { - const chosenAudioSegments = audio; - //Download Audio (if available) - const totalParts = chosenAudioSegments.segments.length; - const mathParts = Math.ceil(totalParts / options.partsize); - const mathMsg = `(${mathParts}*${options.partsize})`; - console.info('Total parts in audio stream:', totalParts, mathMsg); - const tempFile = parseFileName(`temp-${selectedEpisode.id}.${chosenAudioSegments.language.name}`, variables, options.numbers, options.override).join(path.sep); - const tempTsFile = path.isAbsolute(tempFile as string) ? tempFile : path.join(this.cfg.dir.content, tempFile); - const outFile = parseFileName(options.fileName + '.' + (chosenAudioSegments.language.name), variables, options.numbers, options.override).join(path.sep); - const tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); - const dirName = path.dirname(tsFile); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }); - } - const audioJson: M3U8Json = { - segments: chosenAudioSegments.segments - }; - const audioDownload = await new streamdl({ - output: `${tempTsFile}.audio.enc.m4s`, - 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: selectedEpisode.thumbnailUrl, - parent: { - title: selectedEpisode.seriesTitle - }, - title: selectedEpisode.title, - language: chosenAudioSegments.language - }) : undefined - }).download(); - if(!audioDownload.ok){ - console.error(`DL Stats: ${JSON.stringify(audioDownload.parts)}\n`); - dlFailed = true; - } - if (chosenAudioSegments.pssh_wvd || chosenAudioSegments.pssh_prd) { - console.info('Decryption Needed, attempting to decrypt'); - if (encryptionKeys.length == 0) { - console.error('Failed to get encryption keys'); - return undefined; - } - if (this.cfg.bin.mp4decrypt || this.cfg.bin.shaka) { - let commandBase = `--show-progress --key ${encryptionKeys[cdm === 'playready' ? 0 : 1].kid}:${encryptionKeys[cdm === 'playready' ? 0 : 1].key} `; - let commandAudio = commandBase+`"${tempTsFile}.audio.enc.m4s" "${tempTsFile}.audio.m4s"`; + if (!options.noaudio) { + for (const audio of chosenAudios) { + const chosenAudioSegments = audio; + //Download Audio (if available) + const totalParts = chosenAudioSegments.segments.length; + const mathParts = Math.ceil(totalParts / options.partsize); + const mathMsg = `(${mathParts}*${options.partsize})`; + console.info('Total parts in audio stream:', totalParts, mathMsg); + const tempFile = parseFileName(`temp-${selectedEpisode.id}.${chosenAudioSegments.language.name}`, variables, options.numbers, options.override).join(path.sep); + const tempTsFile = path.isAbsolute(tempFile as string) ? tempFile : path.join(this.cfg.dir.content, tempFile); + const outFile = parseFileName(options.fileName + '.' + (chosenAudioSegments.language.name), variables, options.numbers, options.override).join(path.sep); + const tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); + const dirName = path.dirname(tsFile); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + const audioJson: M3U8Json = { + segments: chosenAudioSegments.segments + }; + const audioDownload = await new streamdl({ + output: `${tempTsFile}.audio.enc.m4s`, + 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: selectedEpisode.thumbnailUrl, + parent: { + title: selectedEpisode.seriesTitle + }, + title: selectedEpisode.title, + language: chosenAudioSegments.language + }) : undefined + }).download(); + if(!audioDownload.ok){ + console.error(`DL Stats: ${JSON.stringify(audioDownload.parts)}\n`); + dlFailed = true; + } + if (chosenAudioSegments.pssh_wvd || chosenAudioSegments.pssh_prd) { + console.info('Decryption Needed, attempting to decrypt'); + if (encryptionKeys.length == 0) { + console.error('Failed to get encryption keys'); + return undefined; + } + if (this.cfg.bin.mp4decrypt || this.cfg.bin.shaka) { + let commandBase = `--show-progress --key ${encryptionKeys[cdm === 'playready' ? 0 : 1].kid}:${encryptionKeys[cdm === 'playready' ? 0 : 1].key} `; + let commandAudio = commandBase+`"${tempTsFile}.audio.enc.m4s" "${tempTsFile}.audio.m4s"`; - if (this.cfg.bin.shaka) { - commandBase = ` --enable_raw_key_decryption ${encryptionKeys.map(kb => '--keys key_id='+kb.kid+':key='+kb.key).join(' ')}`; - commandAudio = `input="${tempTsFile}.audio.enc.m4s",stream=audio,output="${tempTsFile}.audio.m4s"`+commandBase; + if (this.cfg.bin.shaka) { + commandBase = ` --enable_raw_key_decryption ${encryptionKeys.map(kb => '--keys key_id='+kb.kid+':key='+kb.key).join(' ')}`; + commandAudio = `input="${tempTsFile}.audio.enc.m4s",stream=audio,output="${tempTsFile}.audio.m4s"`+commandBase; + } + + console.info('Started decrypting audio'); + const decryptAudio = Helper.exec(this.cfg.bin.shaka ? 'shaka-packager' : 'mp4decrypt', this.cfg.bin.shaka ? `"${this.cfg.bin.shaka}"` : `"${this.cfg.bin.mp4decrypt}"`, commandAudio); + if (!decryptAudio.isOk) { + console.error(decryptAudio.err); + console.error(`Decryption failed with exit code ${decryptAudio.err.code}`); + fs.renameSync(`${tempTsFile}.audio.enc.m4s`, `${tsFile}.audio.enc.m4s`); + return undefined; + } else { + if (!options.nocleanup) { + fs.removeSync(`${tempTsFile}.audio.enc.m4s`); + } + fs.copyFileSync(`${tempTsFile}.audio.m4s`, `${tsFile}.audio.m4s`); + fs.unlinkSync(`${tempTsFile}.audio.m4s`); + files.push({ + type: 'Audio', + path: `${tsFile}.audio.m4s`, + lang: chosenAudioSegments.language, + isPrimary: chosenAudioSegments.default + }); + console.info('Decryption done for audio'); + } + } else { + console.warn('mp4decrypt not found, files need decryption. Decryption Keys:', encryptionKeys); + } + } } - - console.info('Started decrypting audio'); - const decryptAudio = Helper.exec(this.cfg.bin.shaka ? 'shaka-packager' : 'mp4decrypt', this.cfg.bin.shaka ? `"${this.cfg.bin.shaka}"` : `"${this.cfg.bin.mp4decrypt}"`, commandAudio); - if (!decryptAudio.isOk) { - console.error(decryptAudio.err); - console.error(`Decryption failed with exit code ${decryptAudio.err.code}`); - fs.renameSync(`${tempTsFile}.audio.enc.m4s`, `${tsFile}.audio.enc.m4s`); - return undefined; - } else { - if (!options.nocleanup) { - fs.removeSync(`${tempTsFile}.audio.enc.m4s`); - } - fs.copyFileSync(`${tempTsFile}.audio.m4s`, `${tsFile}.audio.m4s`); - fs.unlinkSync(`${tempTsFile}.audio.m4s`); - files.push({ - type: 'Audio', - path: `${tsFile}.audio.m4s`, - lang: chosenAudioSegments.language, - isPrimary: chosenAudioSegments.default - }); - console.info('Decryption done for audio'); - } - } else { - console.warn('mp4decrypt not found, files need decryption. Decryption Keys:', encryptionKeys); - } + } else { + console.info('Skipping Audio'); } - } - } else { - console.info('Skipping Audio'); - } - if(options.dlsubs.indexOf('all') > -1){ - options.dlsubs = ['all']; - } + 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.nosubs) { + console.info('Subtitles downloading disabled from nosubs flag.'); + options.skipsubs = true; + } - if(!options.skipsubs && options.dlsubs.indexOf('none') == -1) { - if(subs.length > 0) { - let subIndex = 0; - for(const sub of subs) { - const subLang = langsData.languages.find(a => a.new_hd_locale === sub.language); - if (!subLang) { - console.warn(`Language not found for subtitle language: ${sub.language}, Skipping`); - continue; - } - const sxData: Partial = {}; - sxData.file = langsData.subsFile(fileName as string, subIndex+'', subLang, false, options.ccTag); - if (path.isAbsolute(sxData.file)) { - sxData.path = sxData.file; - } else { - sxData.path = path.join(this.cfg.dir.content, sxData.file); - } - const dirName = path.dirname(sxData.path); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }); - } - sxData.language = subLang; - if(options.dlsubs.includes('all') || options.dlsubs.includes(subLang.locale)) { - const getVttContent = await this.req.getData(sub.url); - if (getVttContent.ok && getVttContent.res) { - console.info(`Subtitle Downloaded: ${sub.url}`); - //vttConvert(getVttContent.res.body, false, subLang.name, fontSize); - const sBody = vtt2ass(undefined, chosenFontSize, await getVttContent.res.text(), '', subsMargin, options.fontName, options.combineLines); - sxData.title = `${subLang.language} / ${sxData.title}`; - sxData.fonts = fontsData.assFonts(sBody) as Font[]; - fs.writeFileSync(sxData.path, sBody); - console.info(`Subtitle converted: ${sxData.file}`); - files.push({ - type: 'Subtitle', - ...sxData as sxItem, - cc: false - }); + if(!options.skipsubs && options.dlsubs.indexOf('none') == -1) { + if(subs.length > 0) { + let subIndex = 0; + for(const sub of subs) { + const subLang = langsData.languages.find(a => a.new_hd_locale === sub.language); + if (!subLang) { + console.warn(`Language not found for subtitle language: ${sub.language}, Skipping`); + continue; + } + const sxData: Partial = {}; + sxData.file = langsData.subsFile(fileName as string, subIndex+'', subLang, false, options.ccTag); + if (path.isAbsolute(sxData.file)) { + sxData.path = sxData.file; + } else { + sxData.path = path.join(this.cfg.dir.content, sxData.file); + } + const dirName = path.dirname(sxData.path); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + sxData.language = subLang; + if(options.dlsubs.includes('all') || options.dlsubs.includes(subLang.locale)) { + const getVttContent = await this.req.getData(sub.url); + if (getVttContent.ok && getVttContent.res) { + console.info(`Subtitle Downloaded: ${sub.url}`); + //vttConvert(getVttContent.res.body, false, subLang.name, fontSize); + const sBody = vtt2ass(undefined, chosenFontSize, await getVttContent.res.text(), '', subsMargin, options.fontName, options.combineLines); + sxData.title = `${subLang.language} / ${sxData.title}`; + sxData.fonts = fontsData.assFonts(sBody) as Font[]; + fs.writeFileSync(sxData.path, sBody); + console.info(`Subtitle converted: ${sxData.file}`); + files.push({ + type: 'Subtitle', + ...sxData as sxItem, + cc: false + }); + } else{ + console.warn(`Failed to download subtitle: ${sxData.file}`); + } + } + subIndex++; + } } else{ - console.warn(`Failed to download subtitle: ${sxData.file}`); + console.warn('Can\'t find urls for subtitles!'); } - } - subIndex++; + } else{ + console.info('Subtitles downloading skipped!'); } - } else{ - console.warn('Can\'t find urls for subtitles!'); - } - } else{ - console.info('Subtitles downloading skipped!'); + + return { + error: dlFailed, + data: files, + fileName: fileName ? (path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName)) || './unknown' : './unknown' + }; } - return { - error: dlFailed, - data: files, - fileName: fileName ? (path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName)) || './unknown' : './unknown' - }; - } - - public async muxStreams(data: DownloadedMedia[], options: Record, inverseTrackOrder: boolean = true) { - this.cfg.bin = await yamlCfg.loadBinCfg(); - let hasAudioStreams = false; - if (options.novids || data.filter(a => a.type === 'Video').length === 0) - return console.info('Skip muxing since no vids are downloaded'); - if (data.some(a => a.type === 'Audio')) { - hasAudioStreams = true; + public async muxStreams(data: DownloadedMedia[], options: Record, inverseTrackOrder: boolean = true) { + this.cfg.bin = await yamlCfg.loadBinCfg(); + let hasAudioStreams = false; + if (options.novids || data.filter(a => a.type === 'Video').length === 0) + return console.info('Skip muxing since no vids are downloaded'); + if (data.some(a => a.type === 'Audio')) { + hasAudioStreams = true; + } + 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, + }; + }) : [], + skipSubMux: options.skipSubMux, + inverseTrackOrder: inverseTrackOrder, + keepAllVideos: options.keepAllVideos, + 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, + }; + }) : [], + 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'); + return { + file: a.path, + language: a.language, + closedCaption: a.cc + }; + }), + simul: data.filter(a => a.type === 'Video').map((a) : boolean => { + if (a.type === 'Subtitle') + throw new Error('Never'); + return !a.uncut as boolean; + })[0], + 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, + }; + }), + videoTitle: options.videoTitle, + options: { + ffmpeg: options.ffmpegOptions, + mkvmerge: options.mkvmergeOptions + }, + defaults: { + audio: options.defaultAudio, + sub: options.defaultSub + }, + ccTag: options.ccTag + }); + const bin = Merger.checkMerger(this.cfg.bin, options.mp4, options.forceMuxer); + // collect fonts info + // mergers + let isMuxed = false; + if (options.syncTiming) { + await merger.createDelays(); + } + if (bin.MKVmerge) { + await merger.merge('mkvmerge', bin.MKVmerge); + isMuxed = true; + } else if (bin.FFmpeg) { + await merger.merge('ffmpeg', bin.FFmpeg); + isMuxed = true; + } else{ + console.info('\nDone!\n'); + return; + } + if (isMuxed && !options.nocleanup) + merger.cleanUp(); } - 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, - }; - }) : [], - skipSubMux: options.skipSubMux, - inverseTrackOrder: inverseTrackOrder, - keepAllVideos: options.keepAllVideos, - 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, - }; - }) : [], - 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'); - return { - file: a.path, - language: a.language, - closedCaption: a.cc - }; - }), - simul: data.filter(a => a.type === 'Video').map((a) : boolean => { - if (a.type === 'Subtitle') - throw new Error('Never'); - return !a.uncut as boolean; - })[0], - 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, - }; - }), - videoTitle: options.videoTitle, - options: { - ffmpeg: options.ffmpegOptions, - mkvmerge: options.mkvmergeOptions - }, - defaults: { - audio: options.defaultAudio, - sub: options.defaultSub - }, - ccTag: options.ccTag - }); - const bin = Merger.checkMerger(this.cfg.bin, options.mp4, options.forceMuxer); - // collect fonts info - // mergers - let isMuxed = false; - if (options.syncTiming) { - await merger.createDelays(); - } - if (bin.MKVmerge) { - await merger.merge('mkvmerge', bin.MKVmerge); - isMuxed = true; - } else if (bin.FFmpeg) { - await merger.merge('ffmpeg', bin.FFmpeg); - isMuxed = true; - } else{ - console.info('\nDone!\n'); - return; - } - if (isMuxed && !options.nocleanup) - merger.cleanUp(); - } - public sleep(ms: number) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); - } + public sleep(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + } } diff --git a/index.ts b/index.ts index 7ef4cf7..6b6e90e 100644 --- a/index.ts +++ b/index.ts @@ -7,94 +7,94 @@ import { makeCommand, addToArchive } from './modules/module.downloadArchive'; import update from './modules/module.updater'; (async () => { - const cfg = yamlCfg.loadCfg(); - const argv = appArgv(cfg.cli); - if (!argv.skipUpdate) - await update(argv.update); + const cfg = yamlCfg.loadCfg(); + const argv = appArgv(cfg.cli); + if (!argv.skipUpdate) + await update(argv.update); - if (argv.all && argv.but) { - console.error('--all and --but exclude each other!'); - return; - } + if (argv.all && argv.but) { + console.error('--all and --but exclude each other!'); + return; + } - if (argv.addArchive) { - if (argv.service === 'crunchy') { - if (argv.s === undefined && argv.series === undefined) - return console.error('`-s` or `--srz` not found'); - if (argv.s && argv.series) - return console.error('Both `-s` and `--srz` found'); - addToArchive({ - service: 'crunchy', - type: argv.s === undefined ? 'srz' : 's' - }, (argv.s === undefined ? argv.series : argv.s) as string); - console.info('Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s)); - } else if (argv.service === 'hidive') { - if (argv.s === undefined) - return console.error('`-s` not found'); - addToArchive({ - service: 'hidive', - //type: argv.s === undefined ? 'srz' : 's' - type: 's' - }, (argv.s === undefined ? argv.series : argv.s) as string); - console.info('Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s)); - } else if (argv.service === 'ao') { - if (argv.s === undefined) - return console.error('`-s` not found'); - addToArchive({ - service: 'hidive', - //type: argv.s === undefined ? 'srz' : 's' - type: 's' - }, (argv.s === undefined ? argv.series : argv.s) as string); - console.info('Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s)); + if (argv.addArchive) { + if (argv.service === 'crunchy') { + if (argv.s === undefined && argv.series === undefined) + return console.error('`-s` or `--srz` not found'); + if (argv.s && argv.series) + return console.error('Both `-s` and `--srz` found'); + addToArchive({ + service: 'crunchy', + type: argv.s === undefined ? 'srz' : 's' + }, (argv.s === undefined ? argv.series : argv.s) as string); + console.info('Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s)); + } else if (argv.service === 'hidive') { + if (argv.s === undefined) + return console.error('`-s` not found'); + addToArchive({ + service: 'hidive', + //type: argv.s === undefined ? 'srz' : 's' + type: 's' + }, (argv.s === undefined ? argv.series : argv.s) as string); + console.info('Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s)); + } else if (argv.service === 'ao') { + if (argv.s === undefined) + return console.error('`-s` not found'); + addToArchive({ + service: 'hidive', + //type: argv.s === undefined ? 'srz' : 's' + type: 's' + }, (argv.s === undefined ? argv.series : argv.s) as string); + console.info('Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s)); + } + } else if (argv.downloadArchive) { + const ids = makeCommand(argv.service); + for (const id of ids) { + overrideArguments(cfg.cli, id); + /* Reimport module to override appArgv */ + Object.keys(require.cache).forEach(key => { + if (key.endsWith('crunchy.js') || key.endsWith('hidive.js') || key.endsWith('ao.js')) + delete require.cache[key]; + }); + let service: ServiceClass; + switch(argv.service) { + case 'crunchy': + service = new (await import('./crunchy')).default; + break; + case 'hidive': + service = new (await import('./hidive')).default; + break; + case 'ao': + service = new (await import('./ao')).default; + break; + case 'adn': + service = new (await import('./adn')).default; + break; + default: + service = new (await import(`./${argv.service}`)).default; + break; + } + await service.cli(); + } + } else { + let service: ServiceClass; + switch(argv.service) { + case 'crunchy': + service = new (await import('./crunchy')).default; + break; + case 'hidive': + service = new (await import('./hidive')).default; + break; + case 'ao': + service = new (await import('./ao')).default; + break; + case 'adn': + service = new (await import('./adn')).default; + break; + default: + service = new (await import(`./${argv.service}`)).default; + break; + } + await service.cli(); } - } else if (argv.downloadArchive) { - const ids = makeCommand(argv.service); - for (const id of ids) { - overrideArguments(cfg.cli, id); - /* Reimport module to override appArgv */ - Object.keys(require.cache).forEach(key => { - if (key.endsWith('crunchy.js') || key.endsWith('hidive.js') || key.endsWith('ao.js')) - delete require.cache[key]; - }); - let service: ServiceClass; - switch(argv.service) { - case 'crunchy': - service = new (await import('./crunchy')).default; - break; - case 'hidive': - service = new (await import('./hidive')).default; - break; - case 'ao': - service = new (await import('./ao')).default; - break; - case 'adn': - service = new (await import('./adn')).default; - break; - default: - service = new (await import(`./${argv.service}`)).default; - break; - } - await service.cli(); - } - } else { - let service: ServiceClass; - switch(argv.service) { - case 'crunchy': - service = new (await import('./crunchy')).default; - break; - case 'hidive': - service = new (await import('./hidive')).default; - break; - case 'ao': - service = new (await import('./ao')).default; - break; - case 'adn': - service = new (await import('./adn')).default; - break; - default: - service = new (await import(`./${argv.service}`)).default; - break; - } - await service.cli(); - } })(); \ No newline at end of file diff --git a/modules/build-docs.ts b/modules/build-docs.ts index b902da8..07efb8d 100644 --- a/modules/build-docs.ts +++ b/modules/build-docs.ts @@ -4,27 +4,27 @@ import path from 'path'; import { args, groups } from './module.args'; const transformService = (str: Array<'crunchy'|'hidive'|'ao'|'adn'|'all'>) => { - const services: string[] = []; - str.forEach(function(part) { - switch(part) { - case 'crunchy': - services.push('Crunchyroll'); - break; - case 'hidive': - services.push('Hidive'); - break; - case 'ao': - services.push('AnimeOnegai'); - break; - case 'adn': - services.push('AnimationDigitalNetwork'); - break; - case 'all': - services.push('All'); - break; - } - }); - return services.join(', '); + const services: string[] = []; + str.forEach(function(part) { + switch(part) { + case 'crunchy': + services.push('Crunchyroll'); + break; + case 'hidive': + services.push('Hidive'); + break; + case 'ao': + services.push('AnimeOnegai'); + break; + case 'adn': + services.push('AnimationDigitalNetwork'); + break; + case 'all': + services.push('All'); + break; + } + }); + return services.join(', '); }; let docs = `# ${packageJSON.name} (v${packageJSON.version}) @@ -45,30 +45,30 @@ This tool is not responsible for your actions; please make an informed decision `; Object.entries(groups).forEach(([key, value]) => { - docs += `\n### ${value.slice(0, -1)}\n`; + docs += `\n### ${value.slice(0, -1)}\n`; - docs += args.filter(a => a.group === key).map(argument => { - return [`#### \`${argument.name.length > 1 ? '--' : '-'}${argument.name}\``, - `| **Service** | **Usage** | **Type** | **Required** | **Alias** | ${argument.choices ? '**Choices** |' : ''} ${argument.default ? '**Default** |' : ''}**cli-default Entry**`, - `| --- | --- | --- | --- | --- | ${argument.choices ? '--- | ' : ''}${argument.default ? '--- | ' : ''}---| `, - `| ${transformService(argument.service)} | \`${argument.name.length > 1 ? '--' : '-'}${argument.name} ${argument.usage}\` | \`${argument.type}\` | \`${argument.demandOption ? 'Yes' : 'No'}\`|` + docs += args.filter(a => a.group === key).map(argument => { + return [`#### \`${argument.name.length > 1 ? '--' : '-'}${argument.name}\``, + `| **Service** | **Usage** | **Type** | **Required** | **Alias** | ${argument.choices ? '**Choices** |' : ''} ${argument.default ? '**Default** |' : ''}**cli-default Entry**`, + `| --- | --- | --- | --- | --- | ${argument.choices ? '--- | ' : ''}${argument.default ? '--- | ' : ''}---| `, + `| ${transformService(argument.service)} | \`${argument.name.length > 1 ? '--' : '-'}${argument.name} ${argument.usage}\` | \`${argument.type}\` | \`${argument.demandOption ? 'Yes' : 'No'}\`|` + ` \`${(argument.alias ? `${argument.alias.length > 1 ? '--' : '-'}${argument.alias}` : undefined) ?? 'NaN'}\` |` + `${argument.choices ? ` [${argument.choices.map(a => `\`${a || '\'\''}\``).join(', ')}] |` : ''}` + `${argument.default ? ` \`${ - typeof argument.default === 'object' - ? Array.isArray(argument.default) - ? JSON.stringify(argument.default) - : (argument.default as any).default - : argument.default + typeof argument.default === 'object' + ? Array.isArray(argument.default) + ? JSON.stringify(argument.default) + : (argument.default as any).default + : argument.default }\`|` : ''}` + ` ${typeof argument.default === 'object' && !Array.isArray(argument.default) - ? `\`${argument.default.name || argument.name}: \`` - : '`NaN`' + ? `\`${argument.default.name || argument.name}: \`` + : '`NaN`' } |`, - '', - argument.docDescribe === true ? argument.describe : argument.docDescribe - ].join('\n'); - }).join('\n'); + '', + argument.docDescribe === true ? argument.describe : argument.docDescribe + ].join('\n'); + }).join('\n'); }); diff --git a/modules/build.ts b/modules/build.ts index 8ce6d32..cb8a925 100644 --- a/modules/build.ts +++ b/modules/build.ts @@ -15,104 +15,104 @@ const nodeVer = 'node20-'; type BuildTypes = `${'windows'|'macos'|'linux'|'linuxstatic'|'alpine'}-${'x64'|'arm64'}`|'linuxstatic-armv7' (async () => { - const buildType = process.argv[2] as BuildTypes; - const isGUI = process.argv[3] === 'true'; + const buildType = process.argv[2] as BuildTypes; + const isGUI = process.argv[3] === 'true'; - buildBinary(buildType, isGUI); + buildBinary(buildType, isGUI); })(); // main async function buildBinary(buildType: BuildTypes, gui: boolean) { - const buildStr = 'multi-downloader-nx'; - const acceptablePlatforms = ['windows','linux','linuxstatic','macos','alpine']; - const acceptableArchs = ['x64','arm64']; - const acceptableBuilds: string[] = ['linuxstatic-armv7']; - for (const platform of acceptablePlatforms) { - for (const arch of acceptableArchs) { - acceptableBuilds.push(platform+'-'+arch); + const buildStr = 'multi-downloader-nx'; + const acceptablePlatforms = ['windows','linux','linuxstatic','macos','alpine']; + const acceptableArchs = ['x64','arm64']; + const acceptableBuilds: string[] = ['linuxstatic-armv7']; + for (const platform of acceptablePlatforms) { + for (const arch of acceptableArchs) { + acceptableBuilds.push(platform+'-'+arch); + } } - } - if(!acceptableBuilds.includes(buildType)){ - console.error('Unknown build type!'); - process.exit(1); - } - await modulesCleanup('.'); - if(!fs.existsSync(buildsDir)){ - fs.mkdirSync(buildsDir); - } - const buildFull = `${buildStr}-${getFriendlyName(buildType)}-${gui ? 'gui' : 'cli'}`; - const buildDir = `${buildsDir}/${buildFull}`; - if(fs.existsSync(buildDir)){ - fs.removeSync(buildDir); - } - fs.mkdirSync(buildDir); - console.info('Running esbuild'); + if(!acceptableBuilds.includes(buildType)){ + console.error('Unknown build type!'); + process.exit(1); + } + await modulesCleanup('.'); + if(!fs.existsSync(buildsDir)){ + fs.mkdirSync(buildsDir); + } + const buildFull = `${buildStr}-${getFriendlyName(buildType)}-${gui ? 'gui' : 'cli'}`; + const buildDir = `${buildsDir}/${buildFull}`; + if(fs.existsSync(buildDir)){ + 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', 'sleep', ...builtinModules] - }); + 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', 'sleep', ...builtinModules] + }); - if (build.errors?.length > 0) console.error(build.errors); - if (build.warnings?.length > 0) console.warn(build.warnings); + if (build.errors?.length > 0) console.error(build.errors); + if (build.warnings?.length > 0) console.warn(build.warnings); - const buildConfig = [ - `${buildsDir}/index.cjs`, - '--target', nodeVer + buildType, - '--output', `${buildDir}/${pkg.short_name}`, - '--compress', 'GZip' - ]; - console.info(`[Build] Build configuration: ${buildFull}`); - try { - await exec(buildConfig); - } - catch(e){ - console.info(e); - process.exit(1); - } - fs.mkdirSync(`${buildDir}/config`); - fs.mkdirSync(`${buildDir}/videos`); - fs.mkdirSync(`${buildDir}/widevine`); - fs.mkdirSync(`${buildDir}/playready`); - fs.copySync('./config/bin-path.yml', `${buildDir}/config/bin-path.yml`); - fs.copySync('./config/cli-defaults.yml', `${buildDir}/config/cli-defaults.yml`); - fs.copySync('./config/dir-path.yml', `${buildDir}/config/dir-path.yml`); - fs.copySync('./config/gui.yml', `${buildDir}/config/gui.yml`); - fs.copySync('./modules/cmd-here.bat', `${buildDir}/cmd-here.bat`); - fs.copySync('./modules/NotoSans-Regular.ttf', `${buildDir}/NotoSans-Regular.ttf`); - fs.copySync('./package.json', `${buildDir}/package.json`); - fs.copySync('./docs/', `${buildDir}/docs/`); - fs.copySync('./LICENSE.md', `${buildDir}/docs/LICENSE.md`); - if (gui) { - fs.copySync('./gui', `${buildDir}/gui`); - fs.copySync('./node_modules/open/xdg-open', `${buildDir}/xdg-open`); - } - if(fs.existsSync(`${buildsDir}/${buildFull}.7z`)){ - fs.removeSync(`${buildsDir}/${buildFull}.7z`); - } - execSync(`7z a -t7z "${buildsDir}/${buildFull}.7z" "${buildDir}"`,{stdio:[0,1,2]}); + const buildConfig = [ + `${buildsDir}/index.cjs`, + '--target', nodeVer + buildType, + '--output', `${buildDir}/${pkg.short_name}`, + '--compress', 'GZip' + ]; + console.info(`[Build] Build configuration: ${buildFull}`); + try { + await exec(buildConfig); + } + catch(e){ + console.info(e); + process.exit(1); + } + fs.mkdirSync(`${buildDir}/config`); + fs.mkdirSync(`${buildDir}/videos`); + fs.mkdirSync(`${buildDir}/widevine`); + fs.mkdirSync(`${buildDir}/playready`); + fs.copySync('./config/bin-path.yml', `${buildDir}/config/bin-path.yml`); + fs.copySync('./config/cli-defaults.yml', `${buildDir}/config/cli-defaults.yml`); + fs.copySync('./config/dir-path.yml', `${buildDir}/config/dir-path.yml`); + fs.copySync('./config/gui.yml', `${buildDir}/config/gui.yml`); + fs.copySync('./modules/cmd-here.bat', `${buildDir}/cmd-here.bat`); + fs.copySync('./modules/NotoSans-Regular.ttf', `${buildDir}/NotoSans-Regular.ttf`); + fs.copySync('./package.json', `${buildDir}/package.json`); + fs.copySync('./docs/', `${buildDir}/docs/`); + fs.copySync('./LICENSE.md', `${buildDir}/docs/LICENSE.md`); + if (gui) { + fs.copySync('./gui', `${buildDir}/gui`); + fs.copySync('./node_modules/open/xdg-open', `${buildDir}/xdg-open`); + } + if(fs.existsSync(`${buildsDir}/${buildFull}.7z`)){ + fs.removeSync(`${buildsDir}/${buildFull}.7z`); + } + execSync(`7z a -t7z "${buildsDir}/${buildFull}.7z" "${buildDir}"`,{stdio:[0,1,2]}); } function getFriendlyName(buildString: string): string { - if (buildString.includes('armv7')) { - return 'android'; - } - if (buildString.includes('linuxstatic')) { - buildString = buildString.replace('linuxstatic', 'linux'); - } - return buildString; + if (buildString.includes('armv7')) { + return 'android'; + } + if (buildString.includes('linuxstatic')) { + buildString = buildString.replace('linuxstatic', 'linux'); + } + return buildString; } \ No newline at end of file diff --git a/modules/cdm.ts b/modules/cdm.ts index f106dce..66b413d 100644 --- a/modules/cdm.ts +++ b/modules/cdm.ts @@ -10,176 +10,176 @@ import { ofetch } from 'ofetch'; //read cdm files located in the same directory let privateKey: Buffer = Buffer.from([]), - identifierBlob: Buffer = Buffer.from([]), - prd: Buffer = Buffer.from([]), - prd_cdm: Cdm | undefined; + identifierBlob: Buffer = Buffer.from([]), + prd: Buffer = Buffer.from([]), + prd_cdm: Cdm | undefined; export let cdm: 'widevine' | 'playready'; export let canDecrypt: boolean; try { - const files_prd = fs.readdirSync(path.join(workingDir, 'playready')); - const prd_file_found = files_prd.find((f) => f.includes('.prd')); - try { - if (prd_file_found) { - const file_prd = path.join(workingDir, 'playready', prd_file_found); - const stats = fs.statSync(file_prd); - if (stats.size < 1024 * 8 && stats.isFile()) { - const fileContents = fs.readFileSync(file_prd, { - encoding: 'utf8' - }); - if (fileContents.includes('CERT')) { - prd = fs.readFileSync(file_prd); - const device = Device.loads(prd); - prd_cdm = Cdm.fromDevice(device); + const files_prd = fs.readdirSync(path.join(workingDir, 'playready')); + const prd_file_found = files_prd.find((f) => f.includes('.prd')); + try { + if (prd_file_found) { + const file_prd = path.join(workingDir, 'playready', prd_file_found); + const stats = fs.statSync(file_prd); + if (stats.size < 1024 * 8 && stats.isFile()) { + const fileContents = fs.readFileSync(file_prd, { + encoding: 'utf8' + }); + if (fileContents.includes('CERT')) { + prd = fs.readFileSync(file_prd); + const device = Device.loads(prd); + prd_cdm = Cdm.fromDevice(device); + } + } } - } + } catch (e) { + console.error('Error loading Playready CDM, ensure the CDM is provisioned as a V3 Device and not malformed. For more informations read the readme.'); + prd = Buffer.from([]); } - } catch (e) { - console.error('Error loading Playready CDM, ensure the CDM is provisioned as a V3 Device and not malformed. For more informations read the readme.'); - prd = Buffer.from([]); - } - const files_wvd = fs.readdirSync(path.join(workingDir, 'widevine')); - try { - files_wvd.forEach(function (file) { - file = path.join(workingDir, 'widevine', file); - const stats = fs.statSync(file); - if (stats.size < 1024 * 8 && stats.isFile()) { - const fileContents = fs.readFileSync(file, { encoding: 'utf8' }); - if ((fileContents.startsWith('-----BEGIN RSA PRIVATE KEY-----') && fileContents.endsWith('-----END RSA PRIVATE KEY-----')) || (fileContents.startsWith('-----BEGIN PRIVATE KEY-----') && fileContents.endsWith('-----END PRIVATE KEY-----'))) { - privateKey = fs.readFileSync(file); - } - if (fileContents.includes('widevine_cdm_version') && fileContents.includes('oem_crypto_security_patch_level') && !fileContents.startsWith('WVD')) { - identifierBlob = fs.readFileSync(file); - } - if (fileContents.startsWith('WVD')) { - console.warn('Found WVD file in folder, AniDL currently only supports device_client_id_blob and device_private_key, make sure to have them in the widevine folder.'); - } - } - }); - } catch (e) { - console.error('Error loading Widevine CDM, malformed client blob or private key.'); - privateKey = Buffer.from([]); - identifierBlob = Buffer.from([]); - } + const files_wvd = fs.readdirSync(path.join(workingDir, 'widevine')); + try { + files_wvd.forEach(function (file) { + file = path.join(workingDir, 'widevine', file); + const stats = fs.statSync(file); + if (stats.size < 1024 * 8 && stats.isFile()) { + const fileContents = fs.readFileSync(file, { encoding: 'utf8' }); + if ((fileContents.startsWith('-----BEGIN RSA PRIVATE KEY-----') && fileContents.endsWith('-----END RSA PRIVATE KEY-----')) || (fileContents.startsWith('-----BEGIN PRIVATE KEY-----') && fileContents.endsWith('-----END PRIVATE KEY-----'))) { + privateKey = fs.readFileSync(file); + } + if (fileContents.includes('widevine_cdm_version') && fileContents.includes('oem_crypto_security_patch_level') && !fileContents.startsWith('WVD')) { + identifierBlob = fs.readFileSync(file); + } + if (fileContents.startsWith('WVD')) { + console.warn('Found WVD file in folder, AniDL currently only supports device_client_id_blob and device_private_key, make sure to have them in the widevine folder.'); + } + } + }); + } catch (e) { + console.error('Error loading Widevine CDM, malformed client blob or private key.'); + privateKey = Buffer.from([]); + identifierBlob = Buffer.from([]); + } - if (privateKey.length !== 0 && identifierBlob.length !== 0) { - cdm = 'widevine'; - canDecrypt = true; - } else if (prd.length !== 0) { - cdm = 'playready'; - canDecrypt = true; - } else if (privateKey.length === 0 && identifierBlob.length !== 0) { - console.warn('Private key missing'); - canDecrypt = false; - } else if (identifierBlob.length === 0 && privateKey.length !== 0) { - console.warn('Identifier blob missing'); - canDecrypt = false; - } else if (prd.length == 0) { - canDecrypt = false; - } else { - canDecrypt = false; - } + if (privateKey.length !== 0 && identifierBlob.length !== 0) { + cdm = 'widevine'; + canDecrypt = true; + } else if (prd.length !== 0) { + cdm = 'playready'; + canDecrypt = true; + } else if (privateKey.length === 0 && identifierBlob.length !== 0) { + console.warn('Private key missing'); + canDecrypt = false; + } else if (identifierBlob.length === 0 && privateKey.length !== 0) { + console.warn('Identifier blob missing'); + canDecrypt = false; + } else if (prd.length == 0) { + canDecrypt = false; + } else { + canDecrypt = false; + } } catch (e) { - console.error(e); - canDecrypt = false; + console.error(e); + canDecrypt = false; } export async function getKeysWVD(pssh: string | undefined, licenseServer: string, authData: Record): Promise { - if (!pssh || !canDecrypt) return []; - //pssh found in the mpd manifest - const psshBuffer = Buffer.from(pssh, 'base64'); + if (!pssh || !canDecrypt) return []; + //pssh found in the mpd manifest + const psshBuffer = Buffer.from(pssh, 'base64'); - //Create a new widevine session - const session = new Session({ privateKey, identifierBlob }, psshBuffer); + //Create a new widevine session + const session = new Session({ privateKey, identifierBlob }, psshBuffer); - //Generate license - const data = await ofetch(licenseServer, { - method: 'POST', - body: session.createLicenseRequest(), - headers: authData, - responseType: 'arrayBuffer' - }).catch((error) => { - if (error.status && error.statusText) { - console.error(`${error.name} ${error.status}: ${error.statusText}`); - } else { - console.error(`${error.name}: ${error.message}`); - } + //Generate license + const data = await ofetch(licenseServer, { + method: 'POST', + body: session.createLicenseRequest(), + headers: authData, + responseType: 'arrayBuffer' + }).catch((error) => { + if (error.status && error.statusText) { + console.error(`${error.name} ${error.status}: ${error.statusText}`); + } else { + console.error(`${error.name}: ${error.message}`); + } + + if (!error.data) return; + const data = error.data instanceof ArrayBuffer ? new TextDecoder().decode(error.data) : error.data; + if (data) { + const docTitle = data.match(/(.*)<\/title>/); + if (docTitle) { + console.error(docTitle[1]); + } + if (error.status && error.status != 404 && error.status != 403) { + console.error('Body:', data); + } + } + }); - if (!error.data) return; - const data = error.data instanceof ArrayBuffer ? new TextDecoder().decode(error.data) : error.data; if (data) { - const docTitle = data.match(/<title>(.*)<\/title>/); - if (docTitle) { - console.error(docTitle[1]); - } - if (error.status && error.status != 404 && error.status != 403) { - console.error('Body:', data); - } - } - }); - - if (data) { //Parse License and return keys - const text = new TextDecoder().decode(data); - try { - const json = JSON.parse(text); - return session.parseLicense(Buffer.from(json['license'], 'base64')) as KeyContainer[]; - } catch { - return session.parseLicense(Buffer.from(new Uint8Array(data))) as KeyContainer[]; + const text = new TextDecoder().decode(data); + try { + const json = JSON.parse(text); + return session.parseLicense(Buffer.from(json['license'], 'base64')) as KeyContainer[]; + } catch { + return session.parseLicense(Buffer.from(new Uint8Array(data))) as KeyContainer[]; + } + } else { + console.error('License request failed'); + return []; } - } else { - console.error('License request failed'); - return []; - } } export async function getKeysPRD(pssh: string | undefined, licenseServer: string, authData: Record<string, string>): Promise<KeyContainer[]> { - if (!pssh || !canDecrypt || !prd_cdm) return []; - const pssh_parsed = new PSSH(pssh); + if (!pssh || !canDecrypt || !prd_cdm) return []; + const pssh_parsed = new PSSH(pssh); - //Create a new playready session - const session = prd_cdm.getLicenseChallenge(pssh_parsed.get_wrm_headers(true)[0]); + //Create a new playready session + const session = prd_cdm.getLicenseChallenge(pssh_parsed.get_wrm_headers(true)[0]); - //Generate license - const data = await ofetch(licenseServer, { - method: 'POST', - body: session, - headers: authData, - responseType: 'text' - }).catch((error) => { - if (error && error.status && error.statusText) { - console.error(`${error.name} ${error.status}: ${error.statusText}`); - } else { - console.error(`${error.name}: ${error.message}`); - } + //Generate license + const data = await ofetch(licenseServer, { + method: 'POST', + body: session, + headers: authData, + responseType: 'text' + }).catch((error) => { + if (error && error.status && error.statusText) { + console.error(`${error.name} ${error.status}: ${error.statusText}`); + } else { + console.error(`${error.name}: ${error.message}`); + } - if (!error.data) return; - const docTitle = error.data.match(/<title>(.*)<\/title>/); - if (docTitle) { - console.error(docTitle[1]); - } - if (error.status && error.status != 404 && error.status != 403) { - console.error('Body:', error.data); - } - }); + if (!error.data) return; + const docTitle = error.data.match(/<title>(.*)<\/title>/); + if (docTitle) { + console.error(docTitle[1]); + } + if (error.status && error.status != 404 && error.status != 403) { + console.error('Body:', error.data); + } + }); - if (data) { + if (data) { //Parse License and return keys - try { - const keys = prd_cdm.parseLicense(data); + try { + const keys = prd_cdm.parseLicense(data); - return keys.map((k) => { - return { - kid: k.key_id, - key: k.key - }; - }); - } catch { - console.error('License parsing failed'); - return []; + return keys.map((k) => { + return { + kid: k.key_id, + key: k.key + }; + }); + } catch { + console.error('License parsing failed'); + return []; + } + } else { + console.error('License request failed'); + return []; } - } else { - console.error('License request failed'); - return []; - } } diff --git a/modules/hls-download.ts b/modules/hls-download.ts index 7bf0f90..82d4551 100644 --- a/modules/hls-download.ts +++ b/modules/hls-download.ts @@ -72,352 +72,352 @@ type Data = { // hls class class hlsDownload { - private data: Data; - constructor(options: HLSOptions) { + private data: Data; + constructor(options: HLSOptions) { // check playlist - if (!options || !options.m3u8json || !options.m3u8json.segments || options.m3u8json.segments.length === 0) { - throw new Error('Playlist is empty!'); + if (!options || !options.m3u8json || !options.m3u8json.segments || options.m3u8json.segments.length === 0) { + throw new Error('Playlist is empty!'); + } + // init options + this.data = { + parts: { + first: options.m3u8json.mediaSequence || 0, + total: options.m3u8json.segments.length, + completed: 0 + }, + m3u8json: options.m3u8json, + outputFile: options.output || 'stream.ts', + threads: options.threads || 5, + retries: options.retries || 4, + offset: options.offset || 0, + baseurl: options.baseurl, + skipInit: options.skipInit, + keys: {}, + timeout: options.timeout ? options.timeout : 60 * 1000, + checkPartLength: false, + isResume: options.offset ? options.offset > 0 : false, + bytesDownloaded: 0, + waitTime: options.fsRetryTime ?? 1000 * 5, + callback: options.callback, + override: options.override, + dateStart: 0 + }; } - // init options - this.data = { - parts: { - first: options.m3u8json.mediaSequence || 0, - total: options.m3u8json.segments.length, - completed: 0 - }, - m3u8json: options.m3u8json, - outputFile: options.output || 'stream.ts', - threads: options.threads || 5, - retries: options.retries || 4, - offset: options.offset || 0, - baseurl: options.baseurl, - skipInit: options.skipInit, - keys: {}, - timeout: options.timeout ? options.timeout : 60 * 1000, - checkPartLength: false, - isResume: options.offset ? options.offset > 0 : false, - bytesDownloaded: 0, - waitTime: options.fsRetryTime ?? 1000 * 5, - callback: options.callback, - override: options.override, - dateStart: 0 - }; - } - async download() { + async download() { // set output - const fn = this.data.outputFile; - // try load resume file - if (fsp.existsSync(fn) && fsp.existsSync(`${fn}.resume`) && this.data.offset < 1) { - try { - console.info('Resume data found! Trying to resume...'); - const resumeData = JSON.parse(await fs.readFile(`${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!'); - 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 } - }); - } - } catch (e) { - console.error('Resume failed, downloading will be not resumed!'); - console.error(e); - } - } - // ask before rewrite file - if (fsp.existsSync(`${fn}`) && !this.data.isResume) { - let rwts = this.data.override ?? (await Helper.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}»...`); - await fs.unlink(fn); - } else if (['C', 'c'].includes(rwts[0])) { - return { ok: true, parts: this.data.parts }; - } else { - return { ok: false, parts: this.data.parts }; - } - } - // show output filename - if (fsp.existsSync(fn) && this.data.isResume) { - console.info(`Adding content to «${fn}»...`); - } else { - console.info(`Saving stream to «${fn}»...`); - } - // 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...'); - 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); - await fs.writeFile(fn, initDl.dec, { flag: 'a' }); - await fs.writeFile( - `${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}`); - 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!'); - } - // resuming ... - if (this.data.offset > 0) { - segments = segments.slice(this.data.offset); - console.info(`Resuming download from part ${this.data.offset + 1}...`); - this.data.parts.completed = this.data.offset; - } - // dl process - for (let p = 0; p < segments.length / this.data.threads; p++) { - // set offsets - const offset = p * this.data.threads; - const dlOffset = offset + this.data.threads; - // map download threads - const krq = new Map(), - prq = new Map(); - const res: any[] = []; - let errcnt = 0; - for (let px = offset; px < dlOffset && px < segments.length; px++) { - 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)); - } - } - try { - await Promise.all(krq.values()); - } catch (er: any) { - console.error(`Key ${er.p + 1} download error:\n\t${er.message}`); - 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)); - } - // Parallelized part download with retry logic and optional concurrency limit - const maxConcurrency = this.data.threads; - const partEntries = [...prq.entries()]; - let index = 0; - - async function worker(this: hlsDownload) { - while (index < partEntries.length) { - const i = index++; - const [px, downloadFn] = partEntries[i]; - - let retriesLeft = this.data.retries; - let success = false; - while (retriesLeft > 0 && !success) { + const fn = this.data.outputFile; + // try load resume file + if (fsp.existsSync(fn) && fsp.existsSync(`${fn}.resume`) && this.data.offset < 1) { try { - const r = await downloadFn(); - res[px - offset] = r.dec; - success = true; + console.info('Resume data found! Trying to resume...'); + const resumeData = JSON.parse(await fs.readFile(`${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!'); + 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 } + }); + } + } catch (e) { + console.error('Resume failed, downloading will be not resumed!'); + console.error(e); + } + } + // ask before rewrite file + if (fsp.existsSync(`${fn}`) && !this.data.isResume) { + let rwts = this.data.override ?? (await Helper.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}»...`); + await fs.unlink(fn); + } else if (['C', 'c'].includes(rwts[0])) { + return { ok: true, parts: this.data.parts }; + } else { + return { ok: false, parts: this.data.parts }; + } + } + // show output filename + if (fsp.existsSync(fn) && this.data.isResume) { + console.info(`Adding content to «${fn}»...`); + } else { + console.info(`Saving stream to «${fn}»...`); + } + // 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...'); + 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); + await fs.writeFile(fn, initDl.dec, { flag: 'a' }); + await fs.writeFile( + `${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}`); + 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!'); + } + // resuming ... + if (this.data.offset > 0) { + segments = segments.slice(this.data.offset); + console.info(`Resuming download from part ${this.data.offset + 1}...`); + this.data.parts.completed = this.data.offset; + } + // dl process + for (let p = 0; p < segments.length / this.data.threads; p++) { + // set offsets + const offset = p * this.data.threads; + const dlOffset = offset + this.data.threads; + // map download threads + const krq = new Map(), + prq = new Map(); + const res: any[] = []; + let errcnt = 0; + for (let px = offset; px < dlOffset && px < segments.length; px++) { + 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)); + } + } + try { + await Promise.all(krq.values()); + } catch (er: any) { + console.error(`Key ${er.p + 1} download error:\n\t${er.message}`); + 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)); + } + // Parallelized part download with retry logic and optional concurrency limit + const maxConcurrency = this.data.threads; + const partEntries = [...prq.entries()]; + let index = 0; + + async function worker(this: hlsDownload) { + while (index < partEntries.length) { + const i = index++; + const [px, downloadFn] = partEntries[i]; + + let retriesLeft = this.data.retries; + let success = false; + while (retriesLeft > 0 && !success) { + try { + const r = await downloadFn(); + res[px - offset] = r.dec; + success = true; + } catch (error: any) { + retriesLeft--; + console.warn(`Retrying part ${error.p + 1 + this.data.offset} (${this.data.retries - retriesLeft}/${this.data.retries})`); + if (retriesLeft > 0) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + } else { + console.error(`Part ${error.p + 1 + this.data.offset} download failed after ${this.data.retries} retries:\n\t${error.message}`); + errcnt++; + } + } + } + } + } + + const workers = []; + for (let i = 0; i < maxConcurrency; i++) { + workers.push(worker.call(this)); + } + await Promise.all(workers); + + // catch error + if (errcnt > 0) { + console.error(`${errcnt} parts not downloaded`); + return { ok: false, parts: this.data.parts }; + } + // write downloaded + for (const r of res) { + let error = 0; + while (error < 3) { + try { + await fs.writeFile(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`); + await new Promise<void>((resolve) => setTimeout(() => resolve(), this.data.waitTime)); + } + error++; + } + if (error === 3) { + console.error(`Unable to write content to '${fn}'.`); + return { ok: false, parts: this.data.parts }; + } + } + // log downloaded + const totalSeg = segments.length + this.data.offset; // Add the sliced lenght back so the resume data will be correct even if an resumed download fails + const downloadedSeg = dlOffset < totalSeg ? dlOffset : totalSeg; + this.data.parts.completed = downloadedSeg + this.data.offset; + const data = extFn.getDownloadInfo(this.data.dateStart, downloadedSeg, totalSeg, this.data.bytesDownloaded); + await fs.writeFile( + `${fn}.resume`, + JSON.stringify({ + completed: this.data.parts.completed, + total: totalSeg + }) + ); + function formatDLSpeedB(s: number) { + if (s < 1000000) return `${(s / 1000).toFixed(2)} KB/s`; + if (s < 1000000000) return `${(s / 1000000).toFixed(2)} MB/s`; + return `${(s / 1000000000).toFixed(2)} GB/s`; + } + function formatDLSpeedBit(s: number) { + if (s * 8 < 1000000) return `${(s * 8 / 1000).toFixed(2)} KBit/s`; + if (s * 8 < 1000000000) return `${(s * 8 / 1000000).toFixed(2)} MBit/s`; + return `${(s * 8 / 1000000000).toFixed(2)} GBit/s`; + } + console.info( + `${downloadedSeg} of ${totalSeg} parts downloaded [${data.percent}%] (${Helper.formatTime(parseInt((data.time / 1000).toFixed(0)))} | ${formatDLSpeedB(data.downloadSpeed)} / ${formatDLSpeedBit(data.downloadSpeed)})` + ); + 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 + }); + } + // return result + await fs.unlink(`${fn}.resume`); + return { ok: true, parts: this.data.parts }; + } + async downloadPart(seg: Segment, segIndex: number, segOffset: number) { + 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); + } + part = await extFn.getData( + p, + sURI, + { + ...(seg.byterange + ? { + Range: `bytes=${seg.byterange.offset}-${seg.byterange.offset + seg.byterange.length - 1}` + } + : {}) + }, + segOffset, + false + ); + // if (this.data.checkPartLength) { + // this.data.checkPartLength = false; + // console.warn(`Part ${segIndex + segOffset + 1}: can't check parts size!`); + // } + if (decipher == undefined) { + this.data.bytesDownloaded += Buffer.from(part).byteLength; + return { dec: Buffer.from(part), p }; + } + dec = decipher.update(Buffer.from(part)); + dec = Buffer.concat([dec, decipher.final()]); + this.data.bytesDownloaded += dec.byteLength; + } catch (error: any) { + error.p = p; + throw error; + } + return { dec, p }; + } + async downloadKey(key: Key, segIndex: number, segOffset: number) { + const kURI = extFn.getURI(key.uri, this.data.baseurl); + if (!this.data.keys[kURI]) { + try { + const rkey = await extFn.getData(segIndex, kURI, {}, segOffset, true); + return rkey; } catch (error: any) { - retriesLeft--; - console.warn(`Retrying part ${error.p + 1 + this.data.offset} (${this.data.retries - retriesLeft}/${this.data.retries})`); - if (retriesLeft > 0) { - await new Promise((resolve) => setTimeout(resolve, 1000)); - } else { - console.error(`Part ${error.p + 1 + this.data.offset} download failed after ${this.data.retries} retries:\n\t${error.message}`); - errcnt++; - } + error.p = segIndex; + throw error; } - } } - } - - const workers = []; - for (let i = 0; i < maxConcurrency; i++) { - workers.push(worker.call(this)); - } - await Promise.all(workers); - - // catch error - if (errcnt > 0) { - console.error(`${errcnt} parts not downloaded`); - return { ok: false, parts: this.data.parts }; - } - // write downloaded - for (const r of res) { - let error = 0; - while (error < 3) { - try { - await fs.writeFile(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`); - await new Promise<void>((resolve) => setTimeout(() => resolve(), this.data.waitTime)); - } - error++; - } - if (error === 3) { - console.error(`Unable to write content to '${fn}'.`); - return { ok: false, parts: this.data.parts }; - } - } - // log downloaded - const totalSeg = segments.length + this.data.offset; // Add the sliced lenght back so the resume data will be correct even if an resumed download fails - const downloadedSeg = dlOffset < totalSeg ? dlOffset : totalSeg; - this.data.parts.completed = downloadedSeg + this.data.offset; - const data = extFn.getDownloadInfo(this.data.dateStart, downloadedSeg, totalSeg, this.data.bytesDownloaded); - await fs.writeFile( - `${fn}.resume`, - JSON.stringify({ - completed: this.data.parts.completed, - total: totalSeg - }) - ); - function formatDLSpeedB(s: number) { - if (s < 1000000) return `${(s / 1000).toFixed(2)} KB/s`; - if (s < 1000000000) return `${(s / 1000000).toFixed(2)} MB/s`; - return `${(s / 1000000000).toFixed(2)} GB/s`; - } - function formatDLSpeedBit(s: number) { - if (s * 8 < 1000000) return `${(s * 8 / 1000).toFixed(2)} KBit/s`; - if (s * 8 < 1000000000) return `${(s * 8 / 1000000).toFixed(2)} MBit/s`; - return `${(s * 8 / 1000000000).toFixed(2)} GBit/s`; - } - console.info( - `${downloadedSeg} of ${totalSeg} parts downloaded [${data.percent}%] (${Helper.formatTime(parseInt((data.time / 1000).toFixed(0)))} | ${formatDLSpeedB(data.downloadSpeed)} / ${formatDLSpeedBit(data.downloadSpeed)})` - ); - 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 - }); } - // return result - await fs.unlink(`${fn}.resume`); - return { ok: true, parts: this.data.parts }; - } - async downloadPart(seg: Segment, segIndex: number, segOffset: number) { - 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); - } - part = await extFn.getData( - p, - sURI, - { - ...(seg.byterange - ? { - Range: `bytes=${seg.byterange.offset}-${seg.byterange.offset + seg.byterange.length - 1}` + async getKey(key: Key, segIndex: number, segOffset: number) { + 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); + if (!rkey) throw new Error(); + this.data.keys[kURI] = Buffer.from(rkey); + } catch (error: any) { + error.p = p; + throw error; } - : {}) - }, - segOffset, - false - ); - // if (this.data.checkPartLength) { - // this.data.checkPartLength = false; - // console.warn(`Part ${segIndex + segOffset + 1}: can't check parts size!`); - // } - if (decipher == undefined) { - this.data.bytesDownloaded += Buffer.from(part).byteLength; - return { dec: Buffer.from(part), p }; - } - dec = decipher.update(Buffer.from(part)); - dec = Buffer.concat([dec, decipher.final()]); - this.data.bytesDownloaded += dec.byteLength; - } catch (error: any) { - error.p = p; - throw error; + } + // get ivs + const iv = Buffer.alloc(16); + const ivs = key.iv ? key.iv : [0, 0, 0, p + 1]; + for (let i = 0; i < ivs.length; i++) { + iv.writeUInt32BE(ivs[i], i * 4); + } + return crypto.createDecipheriv('aes-128-cbc', this.data.keys[kURI], iv); } - return { dec, p }; - } - async downloadKey(key: Key, segIndex: number, segOffset: number) { - const kURI = extFn.getURI(key.uri, this.data.baseurl); - if (!this.data.keys[kURI]) { - try { - const rkey = await extFn.getData(segIndex, kURI, {}, segOffset, true); - return rkey; - } catch (error: any) { - error.p = segIndex; - throw error; - } - } - } - async getKey(key: Key, segIndex: number, segOffset: number) { - 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); - if (!rkey) throw new Error(); - this.data.keys[kURI] = Buffer.from(rkey); - } catch (error: any) { - error.p = p; - throw error; - } - } - // get ivs - const iv = Buffer.alloc(16); - const ivs = key.iv ? key.iv : [0, 0, 0, p + 1]; - for (let i = 0; i < ivs.length; i++) { - iv.writeUInt32BE(ivs[i], i * 4); - } - 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); - if (!baseurl && !httpURI) { - throw new Error('No base and not http(s) uri'); - } else if (httpURI) { - return uri; - } - return baseurl + uri; - }, - getDownloadInfo: (dateStart: number, partsDL: number, partsTotal: number, downloadedBytes: number) => { - const dateElapsed = Date.now() - dateStart; - const percentFxd = parseInt(((partsDL / partsTotal) * 100).toFixed()); - const percent = percentFxd < 100 ? percentFxd : partsTotal == partsDL ? 100 : 99; - const revParts = dateElapsed * (partsTotal / partsDL - 1); - const downloadSpeed = downloadedBytes / (dateElapsed / 1000); //Bytes per second - return { percent, time: revParts, downloadSpeed }; - }, - getData: async (partIndex: number, uri: string, headers: Record<string, string>, segOffset: number, isKey: boolean) => { + getURI: (uri: string, baseurl?: string) => { + const httpURI = /^https{0,1}:/.test(uri); + if (!baseurl && !httpURI) { + throw new Error('No base and not http(s) uri'); + } else if (httpURI) { + return uri; + } + return baseurl + uri; + }, + getDownloadInfo: (dateStart: number, partsDL: number, partsTotal: number, downloadedBytes: number) => { + const dateElapsed = Date.now() - dateStart; + const percentFxd = parseInt(((partsDL / partsTotal) * 100).toFixed()); + const percent = percentFxd < 100 ? percentFxd : partsTotal == partsDL ? 100 : 99; + const revParts = dateElapsed * (partsTotal / partsDL - 1); + const downloadSpeed = downloadedBytes / (dateElapsed / 1000); //Bytes per second + return { percent, time: revParts, downloadSpeed }; + }, + getData: async (partIndex: number, uri: string, headers: Record<string, string>, segOffset: number, isKey: boolean) => { // get file if uri is local - if (uri.startsWith('file://')) { - const buffer = await fs.readFile(url.fileURLToPath(uri)); - return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); + if (uri.startsWith('file://')) { + const buffer = await fs.readFile(url.fileURLToPath(uri)); + return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); + } + // do request + return await ofetch(uri, { + method: 'GET', + headers: headers, + responseType: 'arrayBuffer', + retry: 0, + async onRequestError({ error }) { + const partType = isKey ? 'Key' : 'Part'; + const partIndx = partIndex + 1 + segOffset; + console.warn(`%s %s: ${error.message}`, partType, partIndx); + } + }); } - // do request - return await ofetch(uri, { - method: 'GET', - headers: headers, - responseType: 'arrayBuffer', - retry: 0, - async onRequestError({ error }) { - const partType = isKey ? 'Key' : 'Part'; - const partIndx = partIndex + 1 + segOffset; - console.warn(`%s %s: ${error.message}`, partType, partIndx); - } - }); - } }; export default hlsDownload; diff --git a/modules/log.ts b/modules/log.ts index e77ba43..e747e45 100644 --- a/modules/log.ts +++ b/modules/log.ts @@ -7,63 +7,63 @@ const logFolder = path.join(workingDir, 'logs'); const latest = path.join(logFolder, 'latest.log'); const makeLogFolder = () => { - if (!fs.existsSync(logFolder)) - fs.mkdirSync(logFolder); - if (fs.existsSync(latest)) { - const stats = fs.statSync(latest); - fs.renameSync(latest, path.join(logFolder, `${stats.mtimeMs}.log`)); - } + if (!fs.existsSync(logFolder)) + fs.mkdirSync(logFolder); + if (fs.existsSync(latest)) { + const stats = fs.statSync(latest); + fs.renameSync(latest, path.join(logFolder, `${stats.mtimeMs}.log`)); + } }; const makeLogger = () => { - global.console.log = + global.console.log = global.console.info = global.console.warn = global.console.error = global.console.debug = (...data: any[]) => { - console.info((data.length >= 1 ? data.shift() : ''), ...data); + console.info((data.length >= 1 ? data.shift() : ''), ...data); }; - makeLogFolder(); - log4js.configure({ - appenders: { - console: { - type: 'console', layout: { - type: 'pattern', - pattern: process.env.isGUI === 'true' ? '%[%x{info}%m%]' : '%x{info}%m', - tokens: { - info: (ev) => { - return ev.level.levelStr === 'INFO' ? '' : `[${ev.level.levelStr}] `; + makeLogFolder(); + log4js.configure({ + appenders: { + console: { + type: 'console', layout: { + type: 'pattern', + pattern: process.env.isGUI === 'true' ? '%[%x{info}%m%]' : '%x{info}%m', + tokens: { + info: (ev) => { + return ev.level.levelStr === 'INFO' ? '' : `[${ev.level.levelStr}] `; + } + } + } + }, + file: { + type: 'file', + filename: latest, + layout: { + type: 'pattern', + pattern: '%x{info}%m', + tokens: { + info: (ev) => { + return ev.level.levelStr === 'INFO' ? '' : `[${ev.level.levelStr}] `; + } + } + } } - } - } - }, - file: { - type: 'file', - filename: latest, - layout: { - type: 'pattern', - pattern: '%x{info}%m', - tokens: { - info: (ev) => { - return ev.level.levelStr === 'INFO' ? '' : `[${ev.level.levelStr}] `; + }, + categories: { + default: { + appenders: ['console', 'file'], + level: 'all', } - } } - } - }, - categories: { - default: { - appenders: ['console', 'file'], - level: 'all', - } - } - }); + }); }; const getLogger = () => { - if (!log4js.isConfigured()) - makeLogger(); - return log4js.getLogger(); + if (!log4js.isConfigured()) + makeLogger(); + return log4js.getLogger(); }; export const console = getLogger(); \ No newline at end of file diff --git a/modules/module.api-urls.ts b/modules/module.api-urls.ts index f402c56..214d2c9 100644 --- a/modules/module.api-urls.ts +++ b/modules/module.api-urls.ts @@ -1,10 +1,10 @@ // api domains const domain = { - cr_www: 'https://www.crunchyroll.com', - cr_api: 'https://api.crunchyroll.com', - hd_www: 'https://www.hidive.com', - hd_api: 'https://api.hidive.com', - hd_new: 'https://dce-frontoffice.imggaming.com' + cr_www: 'https://www.crunchyroll.com', + cr_api: 'https://api.crunchyroll.com', + hd_www: 'https://www.hidive.com', + hd_api: 'https://api.hidive.com', + hd_new: 'https://dce-frontoffice.imggaming.com' }; export type APIType = { @@ -42,63 +42,63 @@ export type APIType = { }; const api: APIType = { - // - // - // Crunchyroll - // Vilos bundle.js (where we can extract the basic token thats needed for the initial auth) - bundlejs: 'https://static.crunchyroll.com/vilos-v2/web/vilos/js/bundle.js', - // - // Crunchyroll API - basic_auth_token: 'Y2I5bnpybWh0MzJ2Z3RleHlna286S1V3bU1qSlh4eHVyc0hJVGQxenZsMkMyeVFhUW84TjQ=', - auth: `${domain.cr_www}/auth/v1/token`, - me: `${domain.cr_www}/accounts/v1/me`, - profile: `${domain.cr_www}/accounts/v1/me/profile`, - search: `${domain.cr_www}/content/v2/discover/search`, - content_cms: `${domain.cr_www}/content/v2/cms`, - browse: `${domain.cr_www}/content/v1/browse`, - browse_all_series: `${domain.cr_www}/content/v2/discover/browse`, - streaming_sessions: `${domain.cr_www}/playback/v1/sessions/streaming`, - drm_widevine: `${domain.cr_www}/license/v1/license/widevine`, - drm_playready: `${domain.cr_www}/license/v1/license/playReady`, - // - // Crunchyroll Bucket - cms_bucket: `${domain.cr_www}/cms/v2`, - cms_auth: `${domain.cr_www}/index/v2`, - // - // Crunchyroll Headers - crunchyDefUserAgent: 'Crunchyroll/ANDROIDTV/3.45.2_22274 (Android 12; en-US; SHIELD Android TV Build/SR1A.211012.001)', - crunchyDefHeader: {}, - crunchyAuthHeader: {}, - // - // - // Hidive - // Hidive API - hd_apikey: '508efd7b42d546e19cc24f4d0b414e57e351ca73', - hd_devName: 'Android', - hd_appId: '24i-Android', - hd_clientWeb: 'okhttp/3.4.1', - hd_clientExo: 'smartexoplayer/1.6.0.R (Linux;Android 6.0) ExoPlayerLib/2.6.0', - hd_api: `${domain.hd_api}/api/v1`, - // Hidive New API - hd_new_api: `${domain.hd_new}/api`, - hd_new_apiKey: '857a1e5d-e35e-4fdf-805b-a87b6f8364bf', - hd_new_version: '6.0.1.bbf09a2' + // + // + // Crunchyroll + // Vilos bundle.js (where we can extract the basic token thats needed for the initial auth) + bundlejs: 'https://static.crunchyroll.com/vilos-v2/web/vilos/js/bundle.js', + // + // Crunchyroll API + basic_auth_token: 'Y2I5bnpybWh0MzJ2Z3RleHlna286S1V3bU1qSlh4eHVyc0hJVGQxenZsMkMyeVFhUW84TjQ=', + auth: `${domain.cr_www}/auth/v1/token`, + me: `${domain.cr_www}/accounts/v1/me`, + profile: `${domain.cr_www}/accounts/v1/me/profile`, + search: `${domain.cr_www}/content/v2/discover/search`, + content_cms: `${domain.cr_www}/content/v2/cms`, + browse: `${domain.cr_www}/content/v1/browse`, + browse_all_series: `${domain.cr_www}/content/v2/discover/browse`, + streaming_sessions: `${domain.cr_www}/playback/v1/sessions/streaming`, + drm_widevine: `${domain.cr_www}/license/v1/license/widevine`, + drm_playready: `${domain.cr_www}/license/v1/license/playReady`, + // + // Crunchyroll Bucket + cms_bucket: `${domain.cr_www}/cms/v2`, + cms_auth: `${domain.cr_www}/index/v2`, + // + // Crunchyroll Headers + crunchyDefUserAgent: 'Crunchyroll/ANDROIDTV/3.45.2_22274 (Android 12; en-US; SHIELD Android TV Build/SR1A.211012.001)', + crunchyDefHeader: {}, + crunchyAuthHeader: {}, + // + // + // Hidive + // Hidive API + hd_apikey: '508efd7b42d546e19cc24f4d0b414e57e351ca73', + hd_devName: 'Android', + hd_appId: '24i-Android', + hd_clientWeb: 'okhttp/3.4.1', + hd_clientExo: 'smartexoplayer/1.6.0.R (Linux;Android 6.0) ExoPlayerLib/2.6.0', + hd_api: `${domain.hd_api}/api/v1`, + // Hidive New API + hd_new_api: `${domain.hd_new}/api`, + hd_new_apiKey: '857a1e5d-e35e-4fdf-805b-a87b6f8364bf', + hd_new_version: '6.0.1.bbf09a2' }; api.crunchyDefHeader = { - 'User-Agent': api.crunchyDefUserAgent, - Accept: '*/*', - 'Accept-Encoding': 'gzip', - Connection: 'Keep-Alive', - Host: 'www.crunchyroll.com' + 'User-Agent': api.crunchyDefUserAgent, + Accept: '*/*', + 'Accept-Encoding': 'gzip', + Connection: 'Keep-Alive', + Host: 'www.crunchyroll.com' }; // set header api.crunchyAuthHeader = { - Authorization: `Basic ${api.basic_auth_token}`, - 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', - 'Request-Type': 'SignIn', - ...api.crunchyDefHeader + Authorization: `Basic ${api.basic_auth_token}`, + 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', + 'Request-Type': 'SignIn', + ...api.crunchyDefHeader }; export { domain, api }; diff --git a/modules/module.app-args.ts b/modules/module.app-args.ts index 32e17e6..0a8ff1d 100644 --- a/modules/module.app-args.ts +++ b/modules/module.app-args.ts @@ -94,91 +94,91 @@ export type ArgvType = typeof argvC; const appArgv = (cfg: { [key: string]: unknown }, isGUI = false) => { - if (argvC) - return argvC; - yargs(process.argv.slice(2)); - const argv = getArgv(cfg, isGUI) - .parseSync(); - argvC = argv; - return argv; + if (argvC) + return argvC; + yargs(process.argv.slice(2)); + const argv = getArgv(cfg, isGUI) + .parseSync(); + argvC = argv; + return argv; }; const overrideArguments = (cfg: { [key:string]: unknown }, override: Partial<typeof argvC>, isGUI = false) => { - const argv = getArgv(cfg, isGUI).middleware((ar) => { - for (const key of Object.keys(override)) { - ar[key] = override[key]; - } - }).parseSync(); - argvC = argv; + const argv = getArgv(cfg, isGUI).middleware((ar) => { + for (const key of Object.keys(override)) { + ar[key] = override[key]; + } + }).parseSync(); + argvC = argv; }; export { - appArgv, - overrideArguments + appArgv, + overrideArguments }; const getArgv = (cfg: { [key:string]: unknown }, isGUI: boolean) => { - const parseDefault = <T = unknown>(key: string, _default: T) : T=> { - if (Object.prototype.hasOwnProperty.call(cfg, key)) { - return cfg[key] as T; - } else - return _default; - }; - const argv = yargs.parserConfiguration({ - 'duplicate-arguments-array': false, - 'camel-case-expansion': false, - }) - .wrap(yargs.terminalWidth()) - .usage('Usage: $0 [options]') - .help(true); + const parseDefault = <T = unknown>(key: string, _default: T) : T=> { + if (Object.prototype.hasOwnProperty.call(cfg, key)) { + return cfg[key] as T; + } else + return _default; + }; + const argv = yargs.parserConfiguration({ + 'duplicate-arguments-array': false, + 'camel-case-expansion': false, + }) + .wrap(yargs.terminalWidth()) + .usage('Usage: $0 [options]') + .help(true); //.strictOptions() - const data = args.map(a => { - return { - ...a, - demandOption: !isGUI && a.demandOption, - group: groups[a.group], - default: typeof a.default === 'object' && !Array.isArray(a.default) ? - parseDefault((a.default as any).name || a.name, (a.default as any).default) : a.default - }; - }); - for (const item of data) - argv.option(item.name, { - ...item, - coerce: (value) => { - if (item.transformer) { - return item.transformer(value); - } else { - return value; - } - }, - choices: item.name === 'service' && isGUI ? undefined : item.choices as unknown as Choices + const data = args.map(a => { + return { + ...a, + demandOption: !isGUI && a.demandOption, + group: groups[a.group], + default: typeof a.default === 'object' && !Array.isArray(a.default) ? + parseDefault((a.default as any).name || a.name, (a.default as any).default) : a.default + }; }); + for (const item of data) + argv.option(item.name, { + ...item, + coerce: (value) => { + if (item.transformer) { + return item.transformer(value); + } else { + return value; + } + }, + choices: item.name === 'service' && isGUI ? undefined : item.choices as unknown as Choices + }); - // Custom logic for suggesting corrections for misspelled options - argv.middleware((argv: Record<string, any>) => { + // Custom logic for suggesting corrections for misspelled options + argv.middleware((argv: Record<string, any>) => { // List of valid options - const validOptions = [ - ...args.map(a => a.name), - ...args.map(a => a.alias).filter(alias => alias !== undefined) as string[] - ]; - const unknownOptions = Object.keys(argv).filter(key => !validOptions.includes(key) && key !== '_' && key !== '$0'); // Filter out known options + const validOptions = [ + ...args.map(a => a.name), + ...args.map(a => a.alias).filter(alias => alias !== undefined) as string[] + ]; + const unknownOptions = Object.keys(argv).filter(key => !validOptions.includes(key) && key !== '_' && key !== '$0'); // Filter out known options - const suggestedOptions: Record<string, boolean> = {}; - unknownOptions.forEach(actualOption => { - const closestOption = validOptions.find(option => { - const levenVal = leven(option, actualOption); - return levenVal <= 2 && levenVal > 0; - }); + const suggestedOptions: Record<string, boolean> = {}; + unknownOptions.forEach(actualOption => { + const closestOption = validOptions.find(option => { + const levenVal = leven(option, actualOption); + return levenVal <= 2 && levenVal > 0; + }); - if (closestOption && !suggestedOptions[closestOption]) { - suggestedOptions[closestOption] = true; - console.info(`Unknown option ${actualOption}, did you mean ${closestOption}?`); - } else if (!suggestedOptions[actualOption]) { - suggestedOptions[actualOption] = true; - console.info(`Unknown option ${actualOption}`); - } + if (closestOption && !suggestedOptions[closestOption]) { + suggestedOptions[closestOption] = true; + console.info(`Unknown option ${actualOption}, did you mean ${closestOption}?`); + } else if (!suggestedOptions[actualOption]) { + suggestedOptions[actualOption] = true; + console.info(`Unknown option ${actualOption}`); + } + }); }); - }); - return argv as unknown as yargs.Argv<typeof argvC>; + return argv as unknown as yargs.Argv<typeof argvC>; }; \ No newline at end of file diff --git a/modules/module.args.ts b/modules/module.args.ts index 6ab35e6..33eece1 100644 --- a/modules/module.args.ts +++ b/modules/module.args.ts @@ -2,29 +2,29 @@ import { aoSearchLocales, dubLanguageCodes, languages, searchLocales, subtitleLa import { CrunchyVideoPlayStreams, CrunchyAudioPlayStreams } from '../@types/enums'; const groups = { - 'auth': 'Authentication:', - 'fonts': 'Fonts:', - 'search': 'Search:', - 'dl': 'Downloading:', - 'mux': 'Muxing:', - 'fileName': 'Filename Template:', - 'debug': 'Debug:', - 'util': 'Utilities:', - 'help': 'Help:', - 'gui': 'GUI:' + 'auth': 'Authentication:', + 'fonts': 'Fonts:', + 'search': 'Search:', + 'dl': 'Downloading:', + 'mux': 'Muxing:', + 'fileName': 'Filename Template:', + 'debug': 'Debug:', + 'util': 'Utilities:', + 'help': 'Help:', + 'gui': 'GUI:' }; export type AvailableFilenameVars = 'title' | 'episode' | 'showTitle' | 'seriesTitle' | 'season' | 'width' | 'height' | 'service' const availableFilenameVars: AvailableFilenameVars[] = [ - 'title', - 'episode', - 'showTitle', - 'seriesTitle', - 'season', - 'width', - 'height', - 'service' + 'title', + 'episode', + 'showTitle', + 'seriesTitle', + 'season', + 'width', + 'height', + 'service' ]; export type AvailableMuxer = 'ffmpeg' | 'mkvmerge' @@ -49,942 +49,942 @@ export type TAppArg<T extends boolean|string|number|unknown[], K = any> = { } const args: TAppArg<boolean|number|string|unknown[]>[] = [ - { - name: 'absolute', - describe: 'Use absolute numbers for the episode', - docDescribe: 'Use absolute numbers for the episode. If not set, it will use the default index numbers', - group: 'dl', - service: ['crunchy'], - type: 'boolean', - usage: '', - }, - { - name: 'auth', - describe: 'Enter authentication mode', - type: 'boolean', - group: 'auth', - service: ['all'], - docDescribe: 'Most of the shows on both services are only accessible if you payed for the service.' + { + name: 'absolute', + describe: 'Use absolute numbers for the episode', + docDescribe: 'Use absolute numbers for the episode. If not set, it will use the default index numbers', + group: 'dl', + service: ['crunchy'], + type: 'boolean', + usage: '', + }, + { + name: 'auth', + describe: 'Enter authentication mode', + type: 'boolean', + group: 'auth', + service: ['all'], + docDescribe: 'Most of the shows on both services are only accessible if you payed for the service.' + '\nIn order for them to know who you are you are required to log in.' + '\nIf you trigger this command, you will be prompted for the username and password for the selected service', - usage: '' - }, - { - name: 'dlFonts', - group: 'fonts', - describe: 'Download all required fonts for mkv muxing', - docDescribe: 'Crunchyroll uses a variaty of fonts for the subtitles.' - + '\nUse this command to download all the fonts and add them to the muxed **mkv** file.', - service: ['crunchy'], - type: 'boolean', - usage: '' - }, - { - name: 'search', - group: 'search', - alias: 'f', - describe: 'Search of an anime by the given string', - type: 'string', - docDescribe: true, - service: ['all'], - usage: '${search}' - }, - { - name: 'search-type', - describe: 'Search by type', - docDescribe: 'Search only for type of anime listings (e.g. episodes, series)', - group: 'search', - service: ['crunchy'], - type: 'string', - usage: '${type}', - choices: [ '', 'top_results', 'series', 'movie_listing', 'episode' ], - default: { - default: '' - } - }, - { - name: 'page', - alias: 'p', - describe: 'Set the page number for search results', - docDescribe: 'The output is organized in pages. Use this command to output the items for the given page', - group: 'search', - service: ['crunchy', 'hidive'], - type: 'number', - usage: '${page}' - }, - { - name: 'locale', - describe: 'Set the service locale', - docDescribe: 'Set the local that will be used for the API.', - group: 'search', - choices: ([...searchLocales.filter(a => a !== undefined), ...aoSearchLocales.filter(a => a !== undefined)] as string[]), - default: { - default: 'en-US' + usage: '' }, - type: 'string', - service: ['crunchy', 'ao', 'adn'], - usage: '${locale}' - }, - { - group: 'search', - name: 'new', - describe: 'Get last updated series list', - docDescribe: true, - service: ['crunchy', 'hidive'], - type: 'boolean', - usage: '', - }, - { - group: 'dl', - alias: 'flm', - name: 'movie-listing', - describe: 'Get video list by Movie Listing ID', - docDescribe: true, - service: ['crunchy'], - type: 'string', - usage: '${ID}', - }, - { - group: 'dl', - alias: 'sraw', - name: 'show-raw', - describe: 'Get Raw Show data', - docDescribe: true, - service: ['crunchy'], - type: 'string', - usage: '${ID}', - }, - { - group: 'dl', - alias: 'seraw', - name: 'season-raw', - describe: 'Get Raw Season data', - docDescribe: true, - service: ['crunchy'], - type: 'string', - usage: '${ID}', - }, - { - group: 'dl', - alias: 'slraw', - name: 'show-list-raw', - describe: 'Get Raw Show list data', - docDescribe: true, - service: ['crunchy'], - type: 'boolean', - usage: '', - }, - { - name: 'series', - group: 'dl', - alias: 'srz', - describe: 'Get season list by series ID', - docDescribe: 'Requested is the ID of a show not a season.', - service: ['crunchy'], - type: 'string', - usage: '${ID}' - }, - { - name: 's', - group: 'dl', - type: 'string', - describe: 'Set the season ID', - docDescribe: 'Used to set the season ID to download from', - service: ['all'], - usage: '${ID}' - }, - { - name: 'e', - group: 'dl', - describe: 'Set the episode(s) to download from any given show', - docDescribe: 'Set the episode(s) to download from any given show.' + { + name: 'dlFonts', + group: 'fonts', + describe: 'Download all required fonts for mkv muxing', + docDescribe: 'Crunchyroll uses a variaty of fonts for the subtitles.' + + '\nUse this command to download all the fonts and add them to the muxed **mkv** file.', + service: ['crunchy'], + type: 'boolean', + usage: '' + }, + { + name: 'search', + group: 'search', + alias: 'f', + describe: 'Search of an anime by the given string', + type: 'string', + docDescribe: true, + service: ['all'], + usage: '${search}' + }, + { + name: 'search-type', + describe: 'Search by type', + docDescribe: 'Search only for type of anime listings (e.g. episodes, series)', + group: 'search', + service: ['crunchy'], + type: 'string', + usage: '${type}', + choices: [ '', 'top_results', 'series', 'movie_listing', 'episode' ], + default: { + default: '' + } + }, + { + name: 'page', + alias: 'p', + describe: 'Set the page number for search results', + docDescribe: 'The output is organized in pages. Use this command to output the items for the given page', + group: 'search', + service: ['crunchy', 'hidive'], + type: 'number', + usage: '${page}' + }, + { + name: 'locale', + describe: 'Set the service locale', + docDescribe: 'Set the local that will be used for the API.', + group: 'search', + choices: ([...searchLocales.filter(a => a !== undefined), ...aoSearchLocales.filter(a => a !== undefined)] as string[]), + default: { + default: 'en-US' + }, + type: 'string', + service: ['crunchy', 'ao', 'adn'], + usage: '${locale}' + }, + { + group: 'search', + name: 'new', + describe: 'Get last updated series list', + docDescribe: true, + service: ['crunchy', 'hidive'], + type: 'boolean', + usage: '', + }, + { + group: 'dl', + alias: 'flm', + name: 'movie-listing', + describe: 'Get video list by Movie Listing ID', + docDescribe: true, + service: ['crunchy'], + type: 'string', + usage: '${ID}', + }, + { + group: 'dl', + alias: 'sraw', + name: 'show-raw', + describe: 'Get Raw Show data', + docDescribe: true, + service: ['crunchy'], + type: 'string', + usage: '${ID}', + }, + { + group: 'dl', + alias: 'seraw', + name: 'season-raw', + describe: 'Get Raw Season data', + docDescribe: true, + service: ['crunchy'], + type: 'string', + usage: '${ID}', + }, + { + group: 'dl', + alias: 'slraw', + name: 'show-list-raw', + describe: 'Get Raw Show list data', + docDescribe: true, + service: ['crunchy'], + type: 'boolean', + usage: '', + }, + { + name: 'series', + group: 'dl', + alias: 'srz', + describe: 'Get season list by series ID', + docDescribe: 'Requested is the ID of a show not a season.', + service: ['crunchy'], + type: 'string', + usage: '${ID}' + }, + { + name: 's', + group: 'dl', + type: 'string', + describe: 'Set the season ID', + docDescribe: 'Used to set the season ID to download from', + service: ['all'], + usage: '${ID}' + }, + { + name: 'e', + group: 'dl', + describe: 'Set the episode(s) to download from any given show', + docDescribe: 'Set the episode(s) to download from any given show.' + '\nFor multiple selection: 1-4 OR 1,2,3,4 ' + '\nFor special episodes: S1-4 OR S1,S2,S3,S4 where S is the special letter', - service: ['all'], - type: 'string', - 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', - describe: 'Set the quality level. Use 0 to use the maximum quality.', - default: { - default: 0 + service: ['all'], + type: 'string', + usage: '${selection}', + alias: 'episode' }, - docDescribe: true, - service: ['all'], - type: 'number', - usage: '${qualityLevel}' - }, - { - name: 'dlVideoOnce', - describe: 'Download only once the video with the best selected quality', - type: 'boolean', - group: 'dl', - service: ['crunchy', 'ao'], - docDescribe: 'If selected, the best selected quality will be downloaded only for the first language,' + { + 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', + describe: 'Set the quality level. Use 0 to use the maximum quality.', + default: { + default: 0 + }, + docDescribe: true, + service: ['all'], + type: 'number', + usage: '${qualityLevel}' + }, + { + name: 'dlVideoOnce', + describe: 'Download only once the video with the best selected quality', + type: 'boolean', + group: 'dl', + service: ['crunchy', 'ao'], + docDescribe: 'If selected, the best selected quality will be downloaded only for the first language,' + '\nthen the worst video quality with the same audio quality will be downloaded for every other language.' + '\nBy the later merge of the videos, no quality difference will be present.' + '\nThis will speed up the download speed, if multiple languages are selected.', - usage: '', - default: { - default: false - } - }, - { - name: 'chapters', - describe: 'Will fetch the chapters and add them into the final video', - type: 'boolean', - group: 'dl', - service: ['crunchy', 'adn'], - docDescribe: 'Will fetch the chapters and add them into the final video.', - usage: '', - default: { - default: true - } - }, - // Deprecated - // { - // name: 'crapi', - // describe: 'Selects the API type for Crunchyroll', - // type: 'string', - // group: 'dl', - // service: ['crunchy'], - // docDescribe: 'If set to Android, it has lower quality, but Non-DRM streams,' - // + '\nIf set to Web, it has a higher quality adaptive stream, but everything is DRM.', - // usage: '', - // choices: ['android', 'web'], - // default: { - // default: 'web' - // } - // }, - { - name: 'removeBumpers', - describe: 'Remove bumpers from final video', - type: 'boolean', - group: 'dl', - service: ['hidive'], - docDescribe: 'If selected, it will remove the bumpers such as the hidive intro from the final file.' + usage: '', + default: { + default: false + } + }, + { + name: 'chapters', + describe: 'Will fetch the chapters and add them into the final video', + type: 'boolean', + group: 'dl', + service: ['crunchy', 'adn'], + docDescribe: 'Will fetch the chapters and add them into the final video.', + usage: '', + default: { + default: true + } + }, + // Deprecated + // { + // name: 'crapi', + // describe: 'Selects the API type for Crunchyroll', + // type: 'string', + // group: 'dl', + // service: ['crunchy'], + // docDescribe: 'If set to Android, it has lower quality, but Non-DRM streams,' + // + '\nIf set to Web, it has a higher quality adaptive stream, but everything is DRM.', + // usage: '', + // choices: ['android', 'web'], + // default: { + // default: 'web' + // } + // }, + { + name: 'removeBumpers', + describe: 'Remove bumpers from final video', + type: 'boolean', + group: 'dl', + service: ['hidive'], + docDescribe: 'If selected, it will remove the bumpers such as the hidive intro from the final file.' + '\nCurrently disabling this sometimes results in bugs such as video/audio desync', - usage: '', - default: { - default: true - } - }, - { - name: 'originalFontSize', - describe: 'Keep original font size', - type: 'boolean', - group: 'dl', - service: ['hidive'], - docDescribe: 'If selected, it will prefer to keep the original Font Size defined by the service.', - usage: '', - default: { - default: true - } - }, - { - name: 'x', - group: 'dl', - describe: 'Select the server to use', - choices: [1, 2, 3, 4], - default: { - default: 1 + usage: '', + default: { + default: true + } }, - type: 'number', - alias: 'server', - docDescribe: true, - service: ['all'], - usage: '${server}' - }, - // Deprecated - // { - // name: 'kstream', - // group: 'dl', - // alias: 'k', - // describe: 'Select specific stream', - // choices: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], - // default: { - // default: 1 - // }, - // docDescribe: true, - // service: ['crunchy'], - // type: 'number', - // usage: '${stream}' - // }, - // About to Deprecate - { - name: 'cstream', - group: 'dl', - alias: 'cs', - service: ['crunchy'], - type: 'string', - describe: '(Please use --vstream and --astream instead, this will deprecate soon) Select a specific Crunchyroll playback endpoint by device. Since Crunchyroll has started rolling out their new VBR encodes, we highly recommend using a TV endpoint (e.g. vidaa, samsungtv, lgtv, rokutv, chromecast, firetv, androidtv) to access the old CBR encodes. Please note: The older encodes do not include the new 192 kbps audio, the new audio is only available with the new VBR encodes.', - choices: [...Object.keys(CrunchyVideoPlayStreams), 'none'], - docDescribe: true, - usage: '${device}' - }, - { - name: 'vstream', - group: 'dl', - alias: 'vs', - service: ['crunchy'], - type: 'string', - describe: 'Select a specific Crunchyroll video playback endpoint by device.', - choices: [...Object.keys(CrunchyVideoPlayStreams), 'none'], - default: { - default: 'androidtv' + { + name: 'originalFontSize', + describe: 'Keep original font size', + type: 'boolean', + group: 'dl', + service: ['hidive'], + docDescribe: 'If selected, it will prefer to keep the original Font Size defined by the service.', + usage: '', + default: { + default: true + } }, - docDescribe: true, - usage: '${device}' - }, - { - name: 'astream', - group: 'dl', - alias: 'as', - service: ['crunchy'], - type: 'string', - describe: 'Select a specific Crunchyroll audio playback endpoint by device.', - choices: [...Object.keys(CrunchyAudioPlayStreams), 'none'], - default: { - default: 'android' + { + name: 'x', + group: 'dl', + describe: 'Select the server to use', + choices: [1, 2, 3, 4], + default: { + default: 1 + }, + type: 'number', + alias: 'server', + docDescribe: true, + service: ['all'], + usage: '${server}' }, - docDescribe: true, - usage: '${device}' - }, - { - name: 'tsd', - group: 'dl', - describe: '(Total Session Death) Kills all active Crunchyroll Streaming Sessions to prevent getting the "TOO_MANY_ACTIVE_STREAMS" error.', - docDescribe: true, - service: ['crunchy'], - type: 'boolean', - usage: '', - default: { - default: false - } - }, - { - name: 'hslang', - group: 'dl', - describe: 'Download video with specific hardsubs', - choices: subtitleLanguagesFilter.slice(1), - default: { - default: 'none' + // Deprecated + // { + // name: 'kstream', + // group: 'dl', + // alias: 'k', + // describe: 'Select specific stream', + // choices: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + // default: { + // default: 1 + // }, + // docDescribe: true, + // service: ['crunchy'], + // type: 'number', + // usage: '${stream}' + // }, + // About to Deprecate + { + name: 'cstream', + group: 'dl', + alias: 'cs', + service: ['crunchy'], + type: 'string', + describe: '(Please use --vstream and --astream instead, this will deprecate soon) Select a specific Crunchyroll playback endpoint by device. Since Crunchyroll has started rolling out their new VBR encodes, we highly recommend using a TV endpoint (e.g. vidaa, samsungtv, lgtv, rokutv, chromecast, firetv, androidtv) to access the old CBR encodes. Please note: The older encodes do not include the new 192 kbps audio, the new audio is only available with the new VBR encodes.', + choices: [...Object.keys(CrunchyVideoPlayStreams), 'none'], + docDescribe: true, + usage: '${device}' }, - type: 'string', - usage: '${hslang}', - docDescribe: true, - service: ['crunchy'] - }, - { - name: 'dlsubs', - group: 'dl', - describe: 'Download subtitles by language tag (space-separated)' + { + name: 'vstream', + group: 'dl', + alias: 'vs', + service: ['crunchy'], + type: 'string', + describe: 'Select a specific Crunchyroll video playback endpoint by device.', + choices: [...Object.keys(CrunchyVideoPlayStreams), 'none'], + default: { + default: 'androidtv' + }, + docDescribe: true, + usage: '${device}' + }, + { + name: 'astream', + group: 'dl', + alias: 'as', + service: ['crunchy'], + type: 'string', + describe: 'Select a specific Crunchyroll audio playback endpoint by device.', + choices: [...Object.keys(CrunchyAudioPlayStreams), 'none'], + default: { + default: 'android' + }, + docDescribe: true, + usage: '${device}' + }, + { + name: 'tsd', + group: 'dl', + describe: '(Total Session Death) Kills all active Crunchyroll Streaming Sessions to prevent getting the "TOO_MANY_ACTIVE_STREAMS" error.', + docDescribe: true, + service: ['crunchy'], + type: 'boolean', + usage: '', + default: { + default: false + } + }, + { + name: 'hslang', + group: 'dl', + describe: 'Download video with specific hardsubs', + choices: subtitleLanguagesFilter.slice(1), + default: { + default: 'none' + }, + type: 'string', + usage: '${hslang}', + docDescribe: true, + service: ['crunchy'] + }, + { + name: 'dlsubs', + group: 'dl', + describe: 'Download subtitles by language tag (space-separated)' + `\nCrunchy Only: ${languages.filter(a => a.cr_locale).map(a => a.locale).join(', ')}`, - docDescribe: true, - service: ['all'], - type: 'array', - choices: subtitleLanguagesFilter, - default: { - default: [ 'all' ] + docDescribe: true, + service: ['all'], + type: 'array', + choices: subtitleLanguagesFilter, + default: { + default: [ 'all' ] + }, + usage: '${sub1} ${sub2}' }, - usage: '${sub1} ${sub2}' - }, - { - name: 'novids', - group: 'dl', - describe: 'Skip downloading videos', - docDescribe: true, - service: ['all'], - type: 'boolean', - usage: '' - }, - { - name: 'noaudio', - group: 'dl', - describe: 'Skip downloading audio', - docDescribe: true, - service: ['crunchy', 'hidive'], - type: 'boolean', - usage: '' - }, - { - name: 'nosubs', - group: 'dl', - describe: 'Skip downloading subtitles', - docDescribe: true, - service: ['all'], - type: 'boolean', - usage: '' - }, - { - name: 'dubLang', - describe: 'Set the language to download: ' + { + name: 'novids', + group: 'dl', + describe: 'Skip downloading videos', + docDescribe: true, + service: ['all'], + type: 'boolean', + usage: '' + }, + { + name: 'noaudio', + group: 'dl', + describe: 'Skip downloading audio', + docDescribe: true, + service: ['crunchy', 'hidive'], + type: 'boolean', + usage: '' + }, + { + name: 'nosubs', + group: 'dl', + describe: 'Skip downloading subtitles', + docDescribe: true, + service: ['all'], + type: 'boolean', + usage: '' + }, + { + name: 'dubLang', + describe: 'Set the language to download: ' + `\nCrunchy Only: ${languages.filter(a => a.cr_locale).map(a => a.code).join(', ')}`, - docDescribe: true, - group: 'dl', - choices: dubLanguageCodes, - default: { - default: [dubLanguageCodes.slice(-1)[0]] + docDescribe: true, + group: 'dl', + choices: dubLanguageCodes, + default: { + default: [dubLanguageCodes.slice(-1)[0]] + }, + service: ['all'], + type: 'array', + usage: '${dub1} ${dub2}', }, - service: ['all'], - type: 'array', - usage: '${dub1} ${dub2}', - }, - { - name: 'all', - describe: 'Used to download all episodes from the show', - docDescribe: true, - group: 'dl', - service: ['all'], - default: { - default: false + { + name: 'all', + describe: 'Used to download all episodes from the show', + docDescribe: true, + group: 'dl', + service: ['all'], + default: { + default: false + }, + type: 'boolean', + usage: '' }, - type: 'boolean', - usage: '' - }, - { - name: 'fontSize', - describe: 'Used to set the fontsize of the subtitles', - default: { - default: 55 - }, - docDescribe: 'When converting the subtitles to ass, this will change the font size' + { + name: 'fontSize', + describe: 'Used to set the fontsize of the subtitles', + default: { + default: 55 + }, + docDescribe: 'When converting the subtitles to ass, this will change the font size' + '\nIn most cases, requires "--originaFontSize false" to take effect', - group: 'dl', - service: ['all'], - type: 'number', - usage: '${fontSize}' - }, - { - name: 'combineLines', - describe: 'Merge adjacent lines with same style and text', - docDescribe: 'If selected, will prevent a line from shifting downwards', - group: 'dl', - service: ['hidive'], - type: 'boolean', - usage: '' - }, - { - name: 'allDubs', - describe: 'If selected, all available dubs will get downloaded', - docDescribe: true, - group: 'dl', - service: ['all'], - type: 'boolean', - usage: '' - }, - { - name: 'timeout', - group: 'dl', - type: 'number', - describe: 'Set the timeout of all download reqests. Set in millisecods', - docDescribe: true, - service: ['all'], - usage: '${timeout}', - default: { - 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', - describe: 'Force downloading simulcast version instead of uncut version (if available).', - docDescribe: true, - service: ['hidive'], - type: 'boolean', - usage: '', - default: { - default: false - } - }, - { - name: 'mp4', - group: 'mux', - describe: 'Mux video into mp4', - docDescribe: 'If selected, the output file will be an mp4 file (not recommended tho)', - service: ['all'], - type: 'boolean', - usage: '', - default: { - 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: 'syncTiming', - group: 'mux', - describe: 'Attempts to sync timing for multi-dub downloads EXPERIMENTAL', - docDescribe: 'If enabled attempts to sync timing for multi-dub downloads.' + group: 'dl', + service: ['all'], + type: 'number', + usage: '${fontSize}' + }, + { + name: 'combineLines', + describe: 'Merge adjacent lines with same style and text', + docDescribe: 'If selected, will prevent a line from shifting downwards', + group: 'dl', + service: ['hidive'], + type: 'boolean', + usage: '' + }, + { + name: 'allDubs', + describe: 'If selected, all available dubs will get downloaded', + docDescribe: true, + group: 'dl', + service: ['all'], + type: 'boolean', + usage: '' + }, + { + name: 'timeout', + group: 'dl', + type: 'number', + describe: 'Set the timeout of all download reqests. Set in millisecods', + docDescribe: true, + service: ['all'], + usage: '${timeout}', + default: { + 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', + describe: 'Force downloading simulcast version instead of uncut version (if available).', + docDescribe: true, + service: ['hidive'], + type: 'boolean', + usage: '', + default: { + default: false + } + }, + { + name: 'mp4', + group: 'mux', + describe: 'Mux video into mp4', + docDescribe: 'If selected, the output file will be an mp4 file (not recommended tho)', + service: ['all'], + type: 'boolean', + usage: '', + default: { + 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: 'syncTiming', + group: 'mux', + describe: 'Attempts to sync timing for multi-dub downloads EXPERIMENTAL', + docDescribe: 'If enabled attempts to sync timing for multi-dub downloads.' + '\nNOTE: This is currently experimental and syncs audio and subtitles, though subtitles has a lot of guesswork' + '\nIf you find bugs with this, please report it in the discord or github', - service: ['crunchy','hidive'], - type: 'boolean', - usage: '', - default: { - default: false - } - }, - { - name: 'skipmux', - describe: 'Skip muxing video, audio and subtitles', - docDescribe: true, - group: 'mux', - service: ['all'], - type: 'boolean', - usage: '' - }, - { - name: 'fileName', - group: 'fileName', - describe: `Set the filename template. Use \${variable_name} to insert variables.\nYou can also create folders by inserting a path seperator in the filename\nYou may use ${availableFilenameVars - .map(a => `'${a}'`).join(', ')} as variables.`, - docDescribe: true, - service: ['all'], - type: 'string', - usage: '${fileName}', - default: { - default: '[${service}] ${showTitle} - S${season}E${episode} [${height}p]' - } - }, - { - name: 'numbers', - group: 'fileName', - describe: `Set how long a number in the title should be at least.\n${[[3, 5, '005'], [2, 1, '01'], [1, 20, '20']] - .map(val => `Set in config: ${val[0]}; Episode number: ${val[1]}; Output: ${val[2]}`).join('\n')}`, - type: 'number', - default: { - default: 2 + service: ['crunchy','hidive'], + type: 'boolean', + usage: '', + default: { + default: false + } }, - docDescribe: true, - service: ['all'], - usage: '${number}' - }, - { - name: 'nosess', - group: 'debug', - describe: 'Reset session cookie for testing purposes', - docDescribe: true, - service: ['all'], - type: 'boolean', - usage: '', - default: { - default: false - } - }, - { - name: 'debug', - group: 'debug', - describe: 'Debug mode (tokens may be revealed in the console output)', - docDescribe: true, - service: ['all'], - type: 'boolean', - usage: '', - default: { - default: false - } - }, - { - name: 'nocleanup', - describe: 'Don\'t delete subtitle, audio and video files after muxing', - docDescribe: true, - group: 'mux', - service: ['all'], - type: 'boolean', - default: { - default: false + { + name: 'skipmux', + describe: 'Skip muxing video, audio and subtitles', + docDescribe: true, + group: 'mux', + service: ['all'], + type: 'boolean', + usage: '' }, - usage: '' - }, - { - name: 'help', - alias: 'h', - describe: 'Show the help output', - docDescribe: true, - group: 'help', - service: ['all'], - type: 'boolean', - usage: '' - }, - { - name: 'service', - describe: 'Set the service you want to use', - docDescribe: true, - group: 'util', - service: ['all'], - type: 'string', - choices: ['crunchy', 'hidive', 'ao', 'adn'], - usage: '${service}', - default: { - default: '' + { + name: 'fileName', + group: 'fileName', + describe: `Set the filename template. Use \${variable_name} to insert variables.\nYou can also create folders by inserting a path seperator in the filename\nYou may use ${availableFilenameVars + .map(a => `'${a}'`).join(', ')} as variables.`, + docDescribe: true, + service: ['all'], + type: 'string', + usage: '${fileName}', + default: { + default: '[${service}] ${showTitle} - S${season}E${episode} [${height}p]' + } }, - demandOption: true - }, - { - name: 'update', - group: 'util', - describe: 'Force the tool to check for updates (code version only)', - docDescribe: true, - service: ['all'], - type: 'boolean', - usage: '' - }, - { - name: 'fontName', - group: 'fonts', - describe: 'Set the font to use in subtiles', - docDescribe: true, - service: ['hidive', 'adn'], - type: 'string', - usage: '${fontName}', - }, - { - name: 'but', - describe: 'Download everything but the -e selection', - docDescribe: true, - group: 'dl', - service: ['all'], - type: 'boolean', - usage: '' - }, - { - name: 'downloadArchive', - describe: 'Used to download all archived shows', - group: 'dl', - docDescribe: true, - service: ['all'], - type: 'boolean', - usage: '' - }, - { - name: 'addArchive', - describe: 'Used to add the `-s` and `--srz` to downloadArchive', - group: 'dl', - docDescribe: true, - service: ['all'], - type: 'boolean', - usage: '' - }, - { - name: 'skipSubMux', - describe: 'Skip muxing the subtitles', - docDescribe: true, - group: 'mux', - service: ['all'], - type: 'boolean', - usage: '', - default: { - default: false - } - }, - { - name: 'partsize', - describe: 'Set the amount of parts to download at once', - docDescribe: 'Set the amount of parts to download at once\nIf you have a good connection try incresing this number to get a higher overall speed', - group: 'dl', - service: ['all'], - type: 'number', - usage: '${amount}', - default: { - default: 10 - } - }, - { - name: 'username', - describe: 'Set the username to use for the authentication. If not provided, you will be prompted for the input', - docDescribe: true, - group: 'auth', - service: ['all'], - type: 'string', - usage: '${username}', - default: { - default: undefined - } - }, - { - name: 'password', - describe: 'Set the password to use for the authentication. If not provided, you will be prompted for the input', - docDescribe: true, - group: 'auth', - service: ['all'], - type: 'string', - usage: '${password}', - default: { - default: undefined - } - }, - { - name: 'silentAuth', - describe: 'Authenticate every time the script runs. Use at your own risk.', - docDescribe: true, - group: 'auth', - service: ['crunchy'], - type: 'boolean', - usage: '', - default: { - default: false - } - }, - { - name: 'token', - describe: 'Allows you to login with your token (Example on crunchy is Refresh Token/etp-rt cookie)', - docDescribe: true, - group: 'auth', - service: ['crunchy', 'ao'], - type: 'string', - usage: '${token}', - default: { - default: undefined - } - }, - { - name: 'forceMuxer', - describe: 'Force the program to use said muxer or don\'t mux if the given muxer is not present', - docDescribe: true, - group: 'mux', - service: ['all'], - type: 'string', - usage: '${muxer}', - choices: muxer, - default: { - default: undefined - } - }, - { - name: 'fsRetryTime', - describe: 'Set the time the downloader waits before retrying if an error while writing the file occurs', - docDescribe: true, - group: 'dl', - service: ['all'], - type: 'number', - usage: '${time in seconds}', - default: { - default: 5 + { + name: 'numbers', + group: 'fileName', + describe: `Set how long a number in the title should be at least.\n${[[3, 5, '005'], [2, 1, '01'], [1, 20, '20']] + .map(val => `Set in config: ${val[0]}; Episode number: ${val[1]}; Output: ${val[2]}`).join('\n')}`, + type: 'number', + default: { + default: 2 + }, + docDescribe: true, + service: ['all'], + usage: '${number}' }, - }, - { - name: 'override', - describe: 'Override a template variable', - docDescribe: true, - group: 'fileName', - service: ['all'], - type: 'array', - usage: '"${toOverride}=\'${value}\'"', - default: { - default: [ ] - } - }, - { - name: 'videoTitle', - describe: 'Set the video track name of the merged file', - docDescribe: true, - group: 'mux', - service: ['all'], - type: 'string', - usage: '${title}' - }, - { - name: 'skipUpdate', - describe: 'If true, the tool won\'t check for updates', - docDescribe: true, - group: 'util', - service: ['all'], - type: 'boolean', - usage: '', - default: { - default: false - } - }, - { - name: 'raw', - describe: 'If true, the tool will output the raw data from the API (Where applicable, the feature is a WIP)', - docDescribe: true, - group: 'util', - service: ['all'], - type: 'boolean', - usage: '', - default: { - default: false - } - }, - { - name: 'rawoutput', - describe: 'Provide a path to output the raw data from the API into a file (Where applicable, the feature is a WIP)', - docDescribe: true, - group: 'util', - service: ['all'], - type: 'string', - usage: '', - default: { - default: '' - } - }, - { - name: 'force', - describe: 'Set the default option for the \'alredy exists\' prompt', - docDescribe: 'If a file already exists, the tool will ask you how to proceed. With this, you can answer in advance.', - group: 'dl', - service: ['all'], - type: 'string', - usage: '${option}', - choices: [ 'y', 'Y', 'n', 'N', 'c', 'C' ] - }, - { - name: 'mkvmergeOptions', - describe: 'Set the options given to mkvmerge', - docDescribe: true, - group: 'mux', - service: ['all'], - type: 'array', - usage: '${args}', - default: { - default: [ - '--no-date', - '--disable-track-statistics-tags', - '--engage no_variable_data' - ] - } - }, - { - name: 'ffmpegOptions', - describe: 'Set the options given to ffmpeg', - docDescribe: true, - group: 'mux', - service: ['all'], - type: 'array', - usage: '${args}', - default: { - default: [] - } - }, - { - name: 'defaultAudio', - describe: `Set the default audio track by language code\nPossible Values: ${languages.map(a => a.code).join(', ')}`, - docDescribe: true, - group: 'mux', - service: ['all'], - type: 'string', - usage: '${args}', - default: { - default: 'eng' + { + name: 'nosess', + group: 'debug', + describe: 'Reset session cookie for testing purposes', + docDescribe: true, + service: ['all'], + type: 'boolean', + usage: '', + default: { + default: false + } }, - transformer: (val) => { - const item = languages.find(a => a.code === val); - if (!item) { - throw new Error(`Unable to find language code ${val}!`); - } - return item; - } - }, - { - name: 'defaultSub', - describe: `Set the default subtitle track by language code\nPossible Values: ${languages.map(a => a.code).join(', ')}`, - docDescribe: true, - group: 'mux', - service: ['all'], - type: 'string', - usage: '${args}', - default: { - default: 'eng' + { + name: 'debug', + group: 'debug', + describe: 'Debug mode (tokens may be revealed in the console output)', + docDescribe: true, + service: ['all'], + type: 'boolean', + usage: '', + default: { + default: false + } }, - transformer: (val) => { - const item = languages.find(a => a.code === val); - if (!item) { - throw new Error(`Unable to find language code ${val}!`); - } - return item; + { + name: 'nocleanup', + describe: 'Don\'t delete subtitle, audio and video files after muxing', + docDescribe: true, + group: 'mux', + service: ['all'], + type: 'boolean', + default: { + default: false + }, + usage: '' + }, + { + name: 'help', + alias: 'h', + describe: 'Show the help output', + docDescribe: true, + group: 'help', + service: ['all'], + type: 'boolean', + usage: '' + }, + { + name: 'service', + describe: 'Set the service you want to use', + docDescribe: true, + group: 'util', + service: ['all'], + type: 'string', + choices: ['crunchy', 'hidive', 'ao', 'adn'], + usage: '${service}', + default: { + default: '' + }, + demandOption: true + }, + { + name: 'update', + group: 'util', + describe: 'Force the tool to check for updates (code version only)', + docDescribe: true, + service: ['all'], + type: 'boolean', + usage: '' + }, + { + name: 'fontName', + group: 'fonts', + describe: 'Set the font to use in subtiles', + docDescribe: true, + service: ['hidive', 'adn'], + type: 'string', + usage: '${fontName}', + }, + { + name: 'but', + describe: 'Download everything but the -e selection', + docDescribe: true, + group: 'dl', + service: ['all'], + type: 'boolean', + usage: '' + }, + { + name: 'downloadArchive', + describe: 'Used to download all archived shows', + group: 'dl', + docDescribe: true, + service: ['all'], + type: 'boolean', + usage: '' + }, + { + name: 'addArchive', + describe: 'Used to add the `-s` and `--srz` to downloadArchive', + group: 'dl', + docDescribe: true, + service: ['all'], + type: 'boolean', + usage: '' + }, + { + name: 'skipSubMux', + describe: 'Skip muxing the subtitles', + docDescribe: true, + group: 'mux', + service: ['all'], + type: 'boolean', + usage: '', + default: { + default: false + } + }, + { + name: 'partsize', + describe: 'Set the amount of parts to download at once', + docDescribe: 'Set the amount of parts to download at once\nIf you have a good connection try incresing this number to get a higher overall speed', + group: 'dl', + service: ['all'], + type: 'number', + usage: '${amount}', + default: { + default: 10 + } + }, + { + name: 'username', + describe: 'Set the username to use for the authentication. If not provided, you will be prompted for the input', + docDescribe: true, + group: 'auth', + service: ['all'], + type: 'string', + usage: '${username}', + default: { + default: undefined + } + }, + { + name: 'password', + describe: 'Set the password to use for the authentication. If not provided, you will be prompted for the input', + docDescribe: true, + group: 'auth', + service: ['all'], + type: 'string', + usage: '${password}', + default: { + default: undefined + } + }, + { + name: 'silentAuth', + describe: 'Authenticate every time the script runs. Use at your own risk.', + docDescribe: true, + group: 'auth', + service: ['crunchy'], + type: 'boolean', + usage: '', + default: { + default: false + } + }, + { + name: 'token', + describe: 'Allows you to login with your token (Example on crunchy is Refresh Token/etp-rt cookie)', + docDescribe: true, + group: 'auth', + service: ['crunchy', 'ao'], + type: 'string', + usage: '${token}', + default: { + default: undefined + } + }, + { + name: 'forceMuxer', + describe: 'Force the program to use said muxer or don\'t mux if the given muxer is not present', + docDescribe: true, + group: 'mux', + service: ['all'], + type: 'string', + usage: '${muxer}', + choices: muxer, + default: { + default: undefined + } + }, + { + name: 'fsRetryTime', + describe: 'Set the time the downloader waits before retrying if an error while writing the file occurs', + docDescribe: true, + group: 'dl', + service: ['all'], + type: 'number', + usage: '${time in seconds}', + default: { + default: 5 + }, + }, + { + name: 'override', + describe: 'Override a template variable', + docDescribe: true, + group: 'fileName', + service: ['all'], + type: 'array', + usage: '"${toOverride}=\'${value}\'"', + default: { + default: [ ] + } + }, + { + name: 'videoTitle', + describe: 'Set the video track name of the merged file', + docDescribe: true, + group: 'mux', + service: ['all'], + type: 'string', + usage: '${title}' + }, + { + name: 'skipUpdate', + describe: 'If true, the tool won\'t check for updates', + docDescribe: true, + group: 'util', + service: ['all'], + type: 'boolean', + usage: '', + default: { + default: false + } + }, + { + name: 'raw', + describe: 'If true, the tool will output the raw data from the API (Where applicable, the feature is a WIP)', + docDescribe: true, + group: 'util', + service: ['all'], + type: 'boolean', + usage: '', + default: { + default: false + } + }, + { + name: 'rawoutput', + describe: 'Provide a path to output the raw data from the API into a file (Where applicable, the feature is a WIP)', + docDescribe: true, + group: 'util', + service: ['all'], + type: 'string', + usage: '', + default: { + default: '' + } + }, + { + name: 'force', + describe: 'Set the default option for the \'alredy exists\' prompt', + docDescribe: 'If a file already exists, the tool will ask you how to proceed. With this, you can answer in advance.', + group: 'dl', + service: ['all'], + type: 'string', + usage: '${option}', + choices: [ 'y', 'Y', 'n', 'N', 'c', 'C' ] + }, + { + name: 'mkvmergeOptions', + describe: 'Set the options given to mkvmerge', + docDescribe: true, + group: 'mux', + service: ['all'], + type: 'array', + usage: '${args}', + default: { + default: [ + '--no-date', + '--disable-track-statistics-tags', + '--engage no_variable_data' + ] + } + }, + { + name: 'ffmpegOptions', + describe: 'Set the options given to ffmpeg', + docDescribe: true, + group: 'mux', + service: ['all'], + type: 'array', + usage: '${args}', + default: { + default: [] + } + }, + { + name: 'defaultAudio', + describe: `Set the default audio track by language code\nPossible Values: ${languages.map(a => a.code).join(', ')}`, + docDescribe: true, + group: 'mux', + service: ['all'], + type: 'string', + usage: '${args}', + default: { + default: 'eng' + }, + transformer: (val) => { + const item = languages.find(a => a.code === val); + if (!item) { + throw new Error(`Unable to find language code ${val}!`); + } + return item; + } + }, + { + name: 'defaultSub', + describe: `Set the default subtitle track by language code\nPossible Values: ${languages.map(a => a.code).join(', ')}`, + docDescribe: true, + group: 'mux', + service: ['all'], + type: 'string', + usage: '${args}', + default: { + default: 'eng' + }, + transformer: (val) => { + const item = languages.find(a => a.code === val); + if (!item) { + throw new Error(`Unable to find language code ${val}!`); + } + return item; + } + }, + { + name: 'ccTag', + describe: 'Used to set the name for subtitles that contain tranlations for none verbal communication (e.g. signs)', + docDescribe: true, + group: 'fileName', + service: ['all'], + type: 'string', + usage: '${tag}', + default: { + default: 'cc' + } } - }, - { - name: 'ccTag', - describe: 'Used to set the name for subtitles that contain tranlations for none verbal communication (e.g. signs)', - docDescribe: true, - group: 'fileName', - service: ['all'], - type: 'string', - usage: '${tag}', - default: { - default: 'cc' - } - } ]; const getDefault = <T extends boolean|string|number|unknown[]>(name: string, cfg: Record<string, T>): T => { - const option = args.find(item => item.name === name); - if (!option) - throw new Error(`Unable to find option ${name}`); - if (option.default === undefined) - throw new Error(`Option ${name} has no default`); - if (typeof option.default === 'object') { - if (Array.isArray(option.default)) - return option.default as T; - if (Object.prototype.hasOwnProperty.call(cfg, (option.default as any).name ?? option.name)) { - return cfg[(option.default as any).name ?? option.name]; + const option = args.find(item => item.name === name); + if (!option) + throw new Error(`Unable to find option ${name}`); + if (option.default === undefined) + throw new Error(`Option ${name} has no default`); + if (typeof option.default === 'object') { + if (Array.isArray(option.default)) + return option.default as T; + if (Object.prototype.hasOwnProperty.call(cfg, (option.default as any).name ?? option.name)) { + return cfg[(option.default as any).name ?? option.name]; + } else { + return (option.default as any).default as T; + } } else { - return (option.default as any).default as T; + return option.default as T; } - } else { - return option.default as T; - } }; const buildDefault = () => { - const data: Record<string, unknown> = {}; - const defaultArgs = args.filter(a => a.default); - defaultArgs.forEach(item => { - if (typeof item.default === 'object') { - if (Array.isArray(item.default)) { - data[item.name] = item.default; - } else { - data[(item.default as any).name ?? item.name] = (item.default as any).default; - } - } else { - data[item.name] = item.default; - } - }); - return data; + const data: Record<string, unknown> = {}; + const defaultArgs = args.filter(a => a.default); + defaultArgs.forEach(item => { + if (typeof item.default === 'object') { + if (Array.isArray(item.default)) { + data[item.name] = item.default; + } else { + data[(item.default as any).name ?? item.name] = (item.default as any).default; + } + } else { + data[item.name] = item.default; + } + }); + return data; }; export { - getDefault, - buildDefault, - args, - groups, - availableFilenameVars + getDefault, + buildDefault, + args, + groups, + availableFilenameVars }; diff --git a/modules/module.cfg-loader.ts b/modules/module.cfg-loader.ts index 3839f6c..3e61964 100644 --- a/modules/module.cfg-loader.ts +++ b/modules/module.cfg-loader.ts @@ -18,45 +18,45 @@ const guiCfgFile = path.join(workingDir, 'config', 'gui'); const cliCfgFile = path.join(workingDir, 'config', 'cli-defaults'); const hdPflCfgFile = path.join(workingDir, 'config', 'hd_profile'); const sessCfgFile = { - cr: path.join(workingDir, 'config', 'cr_sess'), - hd: path.join(workingDir, 'config', 'hd_sess'), - ao: path.join(workingDir, 'config', 'ao_sess'), - adn: path.join(workingDir, 'config', 'adn_sess') + cr: path.join(workingDir, 'config', 'cr_sess'), + hd: path.join(workingDir, 'config', 'hd_sess'), + ao: path.join(workingDir, 'config', 'ao_sess'), + adn: path.join(workingDir, 'config', 'adn_sess') }; const stateFile = path.join(workingDir, 'config', 'guistate'); const tokenFile = { - cr: path.join(workingDir, 'config', 'cr_token'), - hd: path.join(workingDir, 'config', 'hd_token'), - hdNew:path.join(workingDir, 'config', 'hd_new_token'), - ao: path.join(workingDir, 'config', 'ao_token'), - adn: path.join(workingDir, 'config', 'adn_token') + cr: path.join(workingDir, 'config', 'cr_token'), + hd: path.join(workingDir, 'config', 'hd_token'), + hdNew:path.join(workingDir, 'config', 'hd_new_token'), + ao: path.join(workingDir, 'config', 'ao_token'), + adn: path.join(workingDir, 'config', 'adn_token') }; export const ensureConfig = () => { - if (!fs.existsSync(path.join(workingDir, 'config'))) - fs.mkdirSync(path.join(workingDir, 'config')); - if (process.env.contentDirectory) - [binCfgFile, dirCfgFile, cliCfgFile, guiCfgFile].forEach(a => { - if (!fs.existsSync(`${a}.yml`)) - fs.copyFileSync(path.join(__dirname, '..', 'config', `${path.basename(a)}.yml`), `${a}.yml`); - }); + if (!fs.existsSync(path.join(workingDir, 'config'))) + fs.mkdirSync(path.join(workingDir, 'config')); + if (process.env.contentDirectory) + [binCfgFile, dirCfgFile, cliCfgFile, guiCfgFile].forEach(a => { + if (!fs.existsSync(`${a}.yml`)) + fs.copyFileSync(path.join(__dirname, '..', 'config', `${path.basename(a)}.yml`), `${a}.yml`); + }); }; const loadYamlCfgFile = <T extends Record<string, any>>(file: string, isSess?: boolean): T => { - if(fs.existsSync(`${file}.user.yml`) && !isSess){ - file += '.user'; - } - file += '.yml'; - if(fs.existsSync(file)){ - try{ - return yaml.parse(fs.readFileSync(file, 'utf8')); + if(fs.existsSync(`${file}.user.yml`) && !isSess){ + file += '.user'; } - catch(e){ - console.error('[ERROR]', e); - return {} as T; + file += '.yml'; + if(fs.existsSync(file)){ + try{ + return yaml.parse(fs.readFileSync(file, 'utf8')); + } + catch(e){ + console.error('[ERROR]', e); + return {} as T; + } } - } - return {} as T; + return {} as T; }; export type WriteObjects = { @@ -64,10 +64,10 @@ export type WriteObjects = { } const writeYamlCfgFile = <T extends keyof WriteObjects>(file: T, data: WriteObjects[T]) => { - const fn = path.join(workingDir, 'config', `${file}.yml`); - if (fs.existsSync(fn)) - fs.removeSync(fn); - fs.writeFileSync(fn, yaml.stringify(data)); + const fn = path.join(workingDir, 'config', `${file}.yml`); + if (fs.existsSync(fn)) + fs.removeSync(fn); + fs.writeFileSync(fn, yaml.stringify(data)); }; export type GUIConfig = { @@ -96,317 +96,317 @@ export type ConfigObject = { } const loadCfg = () : ConfigObject => { - // load cfgs - const defaultCfg: ConfigObject = { - bin: {}, - dir: loadYamlCfgFile<{ + // load cfgs + const defaultCfg: ConfigObject = { + bin: {}, + dir: loadYamlCfgFile<{ content: string, trash: string, fonts: string config: string }>(dirCfgFile), - cli: loadYamlCfgFile<{ + cli: loadYamlCfgFile<{ [key: string]: any }>(cliCfgFile), - gui: loadYamlCfgFile<GUIConfig>(guiCfgFile) - }; - const defaultDirs = { - fonts: '${wdir}/fonts/', - content: '${wdir}/videos/', - trash: '${wdir}/videos/_trash/', - config: '${wdir}/config' - }; - if (typeof defaultCfg.dir !== 'object' || defaultCfg.dir === null || Array.isArray(defaultCfg.dir)) { - defaultCfg.dir = defaultDirs; - } + gui: loadYamlCfgFile<GUIConfig>(guiCfgFile) + }; + const defaultDirs = { + fonts: '${wdir}/fonts/', + content: '${wdir}/videos/', + trash: '${wdir}/videos/_trash/', + config: '${wdir}/config' + }; + if (typeof defaultCfg.dir !== 'object' || defaultCfg.dir === null || Array.isArray(defaultCfg.dir)) { + defaultCfg.dir = defaultDirs; + } - const keys = Object.keys(defaultDirs) as (keyof typeof defaultDirs)[]; - for (const key of keys) { - if (!Object.prototype.hasOwnProperty.call(defaultCfg.dir, key) || typeof defaultCfg.dir[key] !== 'string') { - defaultCfg.dir[key] = defaultDirs[key]; + const keys = Object.keys(defaultDirs) as (keyof typeof defaultDirs)[]; + for (const key of keys) { + if (!Object.prototype.hasOwnProperty.call(defaultCfg.dir, key) || typeof defaultCfg.dir[key] !== 'string') { + defaultCfg.dir[key] = defaultDirs[key]; + } + if (!path.isAbsolute(defaultCfg.dir[key])) { + defaultCfg.dir[key] = path.join(workingDir, defaultCfg.dir[key].replace(/^\${wdir}/, '')); + } } - if (!path.isAbsolute(defaultCfg.dir[key])) { - defaultCfg.dir[key] = path.join(workingDir, defaultCfg.dir[key].replace(/^\${wdir}/, '')); + if(!fs.existsSync(defaultCfg.dir.content)){ + try{ + fs.ensureDirSync(defaultCfg.dir.content); + } + catch(e){ + console.error('Content directory not accessible!'); + return defaultCfg; + } } - } - if(!fs.existsSync(defaultCfg.dir.content)){ - try{ - fs.ensureDirSync(defaultCfg.dir.content); + if(!fs.existsSync(defaultCfg.dir.trash)){ + defaultCfg.dir.trash = defaultCfg.dir.content; } - catch(e){ - console.error('Content directory not accessible!'); - return defaultCfg; - } - } - if(!fs.existsSync(defaultCfg.dir.trash)){ - defaultCfg.dir.trash = defaultCfg.dir.content; - } - // output - return defaultCfg; + // output + return defaultCfg; }; const loadBinCfg = async () => { - const binCfg = loadYamlCfgFile<ConfigObject['bin']>(binCfgFile); - // binaries - const defaultBin = { - ffmpeg: 'ffmpeg', - mkvmerge: 'mkvmerge', - ffprobe: 'ffprobe', - mp4decrypt: 'mp4decrypt', - shaka: 'shaka-packager' - }; - const keys = Object.keys(defaultBin) as (keyof typeof defaultBin)[]; - for(const dir of keys){ - if(!Object.prototype.hasOwnProperty.call(binCfg, dir) || typeof binCfg[dir] != 'string'){ - binCfg[dir] = defaultBin[dir]; + const binCfg = loadYamlCfgFile<ConfigObject['bin']>(binCfgFile); + // binaries + const defaultBin = { + ffmpeg: 'ffmpeg', + mkvmerge: 'mkvmerge', + ffprobe: 'ffprobe', + mp4decrypt: 'mp4decrypt', + shaka: 'shaka-packager' + }; + const keys = Object.keys(defaultBin) as (keyof typeof defaultBin)[]; + for(const dir of keys){ + if(!Object.prototype.hasOwnProperty.call(binCfg, dir) || typeof binCfg[dir] != 'string'){ + binCfg[dir] = defaultBin[dir]; + } + if ((binCfg[dir] as string).match(/^\${wdir}/)) { + binCfg[dir] = (binCfg[dir] as string).replace(/^\${wdir}/, ''); + binCfg[dir] = path.join(workingDir, binCfg[dir] as string); + } + if (!path.isAbsolute(binCfg[dir] as string)){ + binCfg[dir] = path.join(workingDir, binCfg[dir] as string); + } + binCfg[dir] = await lookpath(binCfg[dir] as string); + binCfg[dir] = binCfg[dir] ? binCfg[dir] : undefined; + if(!binCfg[dir]){ + const binFile = await lookpath(path.basename(defaultBin[dir])); + binCfg[dir] = binFile ? binFile : binCfg[dir]; + } } - if ((binCfg[dir] as string).match(/^\${wdir}/)) { - binCfg[dir] = (binCfg[dir] as string).replace(/^\${wdir}/, ''); - binCfg[dir] = path.join(workingDir, binCfg[dir] as string); - } - if (!path.isAbsolute(binCfg[dir] as string)){ - binCfg[dir] = path.join(workingDir, binCfg[dir] as string); - } - binCfg[dir] = await lookpath(binCfg[dir] as string); - binCfg[dir] = binCfg[dir] ? binCfg[dir] : undefined; - if(!binCfg[dir]){ - const binFile = await lookpath(path.basename(defaultBin[dir])); - binCfg[dir] = binFile ? binFile : binCfg[dir]; - } - } - return binCfg; + return binCfg; }; const loadCRSession = () => { - let session = loadYamlCfgFile(sessCfgFile.cr, true); - if(typeof session !== 'object' || session === null || Array.isArray(session)){ - session = {}; - } - for(const cv of Object.keys(session)){ - if(typeof session[cv] !== 'object' || session[cv] === null || Array.isArray(session[cv])){ - session[cv] = {}; + let session = loadYamlCfgFile(sessCfgFile.cr, true); + if(typeof session !== 'object' || session === null || Array.isArray(session)){ + session = {}; } - } - return session; + for(const cv of Object.keys(session)){ + if(typeof session[cv] !== 'object' || session[cv] === null || Array.isArray(session[cv])){ + session[cv] = {}; + } + } + return session; }; const saveCRSession = (data: Record<string, unknown>) => { - const cfgFolder = path.dirname(sessCfgFile.cr); - try{ - fs.ensureDirSync(cfgFolder); - fs.writeFileSync(`${sessCfgFile.cr}.yml`, yaml.stringify(data)); - } - catch(e){ - console.error('Can\'t save session file to disk!'); - } + const cfgFolder = path.dirname(sessCfgFile.cr); + try{ + fs.ensureDirSync(cfgFolder); + fs.writeFileSync(`${sessCfgFile.cr}.yml`, yaml.stringify(data)); + } + catch(e){ + console.error('Can\'t save session file to disk!'); + } }; const loadCRToken = () => { - let token = loadYamlCfgFile(tokenFile.cr, true); - if(typeof token !== 'object' || token === null || Array.isArray(token)){ - token = {}; - } - return token; + let token = loadYamlCfgFile(tokenFile.cr, true); + if(typeof token !== 'object' || token === null || Array.isArray(token)){ + token = {}; + } + return token; }; const saveCRToken = (data: Record<string, unknown>) => { - const cfgFolder = path.dirname(tokenFile.cr); - try{ - fs.ensureDirSync(cfgFolder); - fs.writeFileSync(`${tokenFile.cr}.yml`, yaml.stringify(data)); - } - catch(e){ - console.error('Can\'t save token file to disk!'); - } + const cfgFolder = path.dirname(tokenFile.cr); + try{ + fs.ensureDirSync(cfgFolder); + fs.writeFileSync(`${tokenFile.cr}.yml`, yaml.stringify(data)); + } + catch(e){ + console.error('Can\'t save token file to disk!'); + } }; const loadADNToken = () => { - let token = loadYamlCfgFile(tokenFile.adn, true); - if(typeof token !== 'object' || token === null || Array.isArray(token)){ - token = {}; - } - return token; + let token = loadYamlCfgFile(tokenFile.adn, true); + if(typeof token !== 'object' || token === null || Array.isArray(token)){ + token = {}; + } + return token; }; const saveADNToken = (data: Record<string, unknown>) => { - const cfgFolder = path.dirname(tokenFile.adn); - try{ - fs.ensureDirSync(cfgFolder); - fs.writeFileSync(`${tokenFile.adn}.yml`, yaml.stringify(data)); - } - catch(e){ - console.error('Can\'t save token file to disk!'); - } + const cfgFolder = path.dirname(tokenFile.adn); + try{ + fs.ensureDirSync(cfgFolder); + fs.writeFileSync(`${tokenFile.adn}.yml`, yaml.stringify(data)); + } + catch(e){ + console.error('Can\'t save token file to disk!'); + } }; const loadAOToken = () => { - let token = loadYamlCfgFile(tokenFile.ao, true); - if(typeof token !== 'object' || token === null || Array.isArray(token)){ - token = {}; - } - return token; + let token = loadYamlCfgFile(tokenFile.ao, true); + if(typeof token !== 'object' || token === null || Array.isArray(token)){ + token = {}; + } + return token; }; const saveAOToken = (data: Record<string, unknown>) => { - const cfgFolder = path.dirname(tokenFile.ao); - try{ - fs.ensureDirSync(cfgFolder); - fs.writeFileSync(`${tokenFile.ao}.yml`, yaml.stringify(data)); - } - catch(e){ - console.error('Can\'t save token file to disk!'); - } + const cfgFolder = path.dirname(tokenFile.ao); + try{ + fs.ensureDirSync(cfgFolder); + fs.writeFileSync(`${tokenFile.ao}.yml`, yaml.stringify(data)); + } + catch(e){ + console.error('Can\'t save token file to disk!'); + } }; const loadHDSession = () => { - let session = loadYamlCfgFile(sessCfgFile.hd, true); - if(typeof session !== 'object' || session === null || Array.isArray(session)){ - session = {}; - } - for(const cv of Object.keys(session)){ - if(typeof session[cv] !== 'object' || session[cv] === null || Array.isArray(session[cv])){ - session[cv] = {}; + let session = loadYamlCfgFile(sessCfgFile.hd, true); + if(typeof session !== 'object' || session === null || Array.isArray(session)){ + session = {}; } - } - return session; + for(const cv of Object.keys(session)){ + if(typeof session[cv] !== 'object' || session[cv] === null || Array.isArray(session[cv])){ + session[cv] = {}; + } + } + return session; }; const saveHDSession = (data: Record<string, unknown>) => { - const cfgFolder = path.dirname(sessCfgFile.hd); - try{ - fs.ensureDirSync(cfgFolder); - fs.writeFileSync(`${sessCfgFile.hd}.yml`, yaml.stringify(data)); - } - catch(e){ - console.error('Can\'t save session file to disk!'); - } + const cfgFolder = path.dirname(sessCfgFile.hd); + try{ + fs.ensureDirSync(cfgFolder); + fs.writeFileSync(`${sessCfgFile.hd}.yml`, yaml.stringify(data)); + } + catch(e){ + console.error('Can\'t save session file to disk!'); + } }; const loadHDToken = () => { - let token = loadYamlCfgFile(tokenFile.hd, true); - if(typeof token !== 'object' || token === null || Array.isArray(token)){ - token = {}; - } - return token; + let token = loadYamlCfgFile(tokenFile.hd, true); + if(typeof token !== 'object' || token === null || Array.isArray(token)){ + token = {}; + } + return token; }; const saveHDToken = (data: Record<string, unknown>) => { - const cfgFolder = path.dirname(tokenFile.hd); - try{ - fs.ensureDirSync(cfgFolder); - fs.writeFileSync(`${tokenFile.hd}.yml`, yaml.stringify(data)); - } - catch(e){ - console.error('Can\'t save token file to disk!'); - } + const cfgFolder = path.dirname(tokenFile.hd); + try{ + fs.ensureDirSync(cfgFolder); + fs.writeFileSync(`${tokenFile.hd}.yml`, yaml.stringify(data)); + } + catch(e){ + console.error('Can\'t save token file to disk!'); + } }; const saveHDProfile = (data: Record<string, unknown>) => { - const cfgFolder = path.dirname(hdPflCfgFile); - try{ - fs.ensureDirSync(cfgFolder); - fs.writeFileSync(`${hdPflCfgFile}.yml`, yaml.stringify(data)); - } - catch(e){ - console.error('Can\'t save profile file to disk!'); - } + const cfgFolder = path.dirname(hdPflCfgFile); + try{ + fs.ensureDirSync(cfgFolder); + fs.writeFileSync(`${hdPflCfgFile}.yml`, yaml.stringify(data)); + } + catch(e){ + console.error('Can\'t save profile file to disk!'); + } }; const loadHDProfile = () => { - let profile = loadYamlCfgFile(hdPflCfgFile, true); - if(typeof profile !== 'object' || profile === null || Array.isArray(profile) || Object.keys(profile).length === 0){ - profile = { - // base - ipAddress : '', - xNonce : '', - xSignature: '', - // personal - visitId : '', - // profile data - profile: { - userId : 0, - profileId: 0, - deviceId : '', - }, - }; - } - return profile; + let profile = loadYamlCfgFile(hdPflCfgFile, true); + if(typeof profile !== 'object' || profile === null || Array.isArray(profile) || Object.keys(profile).length === 0){ + profile = { + // base + ipAddress : '', + xNonce : '', + xSignature: '', + // personal + visitId : '', + // profile data + profile: { + userId : 0, + profileId: 0, + deviceId : '', + }, + }; + } + return profile; }; const loadNewHDToken = () => { - let token = loadYamlCfgFile(tokenFile.hdNew, true); - if(typeof token !== 'object' || token === null || Array.isArray(token)){ - token = {}; - } - return token; + let token = loadYamlCfgFile(tokenFile.hdNew, true); + if(typeof token !== 'object' || token === null || Array.isArray(token)){ + token = {}; + } + return token; }; const saveNewHDToken = (data: Record<string, unknown>) => { - const cfgFolder = path.dirname(tokenFile.hdNew); - try{ - fs.ensureDirSync(cfgFolder); - fs.writeFileSync(`${tokenFile.hdNew}.yml`, yaml.stringify(data)); - } - catch(e){ - console.error('Can\'t save token file to disk!'); - } + const cfgFolder = path.dirname(tokenFile.hdNew); + try{ + fs.ensureDirSync(cfgFolder); + fs.writeFileSync(`${tokenFile.hdNew}.yml`, yaml.stringify(data)); + } + catch(e){ + console.error('Can\'t save token file to disk!'); + } }; const cfgDir = path.join(workingDir, 'config'); const getState = (): GuiState => { - const fn = `${stateFile}.json`; - if (!fs.existsSync(fn)) { - return { - 'setup': false, - 'services': {} - }; - } - try { - return JSON.parse(fs.readFileSync(fn).toString()); - } catch(e) { - console.error('Invalid state file, regenerating'); - return { - 'setup': false, - 'services': {} - }; - } + const fn = `${stateFile}.json`; + if (!fs.existsSync(fn)) { + return { + 'setup': false, + 'services': {} + }; + } + try { + return JSON.parse(fs.readFileSync(fn).toString()); + } catch(e) { + console.error('Invalid state file, regenerating'); + return { + 'setup': false, + 'services': {} + }; + } }; const setState = (state: GuiState) => { - const fn = `${stateFile}.json`; - try { - fs.writeFileSync(fn, JSON.stringify(state, null, 2)); - } catch(e) { - console.error('Failed to write state file.'); - } + const fn = `${stateFile}.json`; + try { + fs.writeFileSync(fn, JSON.stringify(state, null, 2)); + } catch(e) { + console.error('Failed to write state file.'); + } }; export { - loadBinCfg, - loadCfg, - saveCRSession, - loadCRSession, - saveCRToken, - loadCRToken, - saveADNToken, - loadADNToken, - saveHDSession, - loadHDSession, - saveHDToken, - loadHDToken, - saveNewHDToken, - loadNewHDToken, - saveHDProfile, - loadHDProfile, - saveAOToken, - loadAOToken, - getState, - setState, - writeYamlCfgFile, - sessCfgFile, - hdPflCfgFile, - cfgDir + loadBinCfg, + loadCfg, + saveCRSession, + loadCRSession, + saveCRToken, + loadCRToken, + saveADNToken, + loadADNToken, + saveHDSession, + loadHDSession, + saveHDToken, + loadHDToken, + saveNewHDToken, + loadNewHDToken, + saveHDProfile, + loadHDProfile, + saveAOToken, + loadAOToken, + getState, + setState, + writeYamlCfgFile, + sessCfgFile, + hdPflCfgFile, + cfgDir }; \ No newline at end of file diff --git a/modules/module.cookieFile.ts b/modules/module.cookieFile.ts index b9bd2c7..f355084 100644 --- a/modules/module.cookieFile.ts +++ b/modules/module.cookieFile.ts @@ -1,26 +1,26 @@ const parse = (data: string) => { - const res: Record<string, { + const res: Record<string, { value: string, expires: Date, path: string, domain: string, secure: boolean }> = {}; - const split = data.replace(/\r/g,'').split('\n'); - for (const line of split) { - const c = line.split('\t'); - if(c.length < 7){ - continue; + const split = data.replace(/\r/g,'').split('\n'); + for (const line of split) { + const c = line.split('\t'); + if(c.length < 7){ + continue; + } + res[c[5]] = { + value: c[6], + expires: new Date(parseInt(c[4])*1000), + path: c[2], + domain: c[0].replace(/^\./,''), + secure: c[3] == 'TRUE' ? true : false + }; } - res[c[5]] = { - value: c[6], - expires: new Date(parseInt(c[4])*1000), - path: c[2], - domain: c[0].replace(/^\./,''), - secure: c[3] == 'TRUE' ? true : false - }; - } - return res; + return res; }; export default parse; diff --git a/modules/module.downloadArchive.ts b/modules/module.downloadArchive.ts index 46833ac..76f999a 100644 --- a/modules/module.downloadArchive.ts +++ b/modules/module.downloadArchive.ts @@ -39,59 +39,59 @@ const addToArchive = (kind: { service: 'adn', type: 's' }, ID: string) => { - const data = loadData(); + const data = loadData(); - if (Object.prototype.hasOwnProperty.call(data, kind.service)) { - const items = kind.service === 'crunchy' ? data[kind.service][kind.type] : data[kind.service][kind.type]; - if (items.findIndex(a => a.id === ID) >= 0) // Prevent duplicate - return; - items.push({ - id: ID, - already: [] - }); - (data as any)[kind.service][kind.type] = items; - } else { - if (kind.service === 'ao') { - data['ao'] = { - s: [ - { + if (Object.prototype.hasOwnProperty.call(data, kind.service)) { + const items = kind.service === 'crunchy' ? data[kind.service][kind.type] : data[kind.service][kind.type]; + if (items.findIndex(a => a.id === ID) >= 0) // Prevent duplicate + return; + items.push({ id: ID, already: [] - } - ] - }; - } else if (kind.service === 'crunchy') { - data['crunchy'] = { - s: ([] as ItemType).concat(kind.type === 's' ? { - id: ID, - already: [] as string[] - } : []), - srz: ([] as ItemType).concat(kind.type === 'srz' ? { - id: ID, - already: [] as string[] - } : []), - }; - } else if (kind.service === 'adn') { - data['adn'] = { - s: [ - { - id: ID, - already: [] - } - ] - }; + }); + (data as any)[kind.service][kind.type] = items; } else { - data['hidive'] = { - s: [ - { - id: ID, - already: [] - } - ] - }; + if (kind.service === 'ao') { + data['ao'] = { + s: [ + { + id: ID, + already: [] + } + ] + }; + } else if (kind.service === 'crunchy') { + data['crunchy'] = { + s: ([] as ItemType).concat(kind.type === 's' ? { + id: ID, + already: [] as string[] + } : []), + srz: ([] as ItemType).concat(kind.type === 'srz' ? { + id: ID, + already: [] as string[] + } : []), + }; + } else if (kind.service === 'adn') { + data['adn'] = { + s: [ + { + id: ID, + already: [] + } + ] + }; + } else { + data['hidive'] = { + s: [ + { + id: ID, + already: [] + } + ] + }; + } } - } - fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4)); + fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4)); }; const downloaded = (kind: { @@ -107,49 +107,49 @@ const downloaded = (kind: { service: 'adn', type: 's' }, ID: string, episode: string[]) => { - let data = loadData(); - if (!Object.prototype.hasOwnProperty.call(data, kind.service) || !Object.prototype.hasOwnProperty.call(data[kind.service], kind.type) + let data = loadData(); + if (!Object.prototype.hasOwnProperty.call(data, kind.service) || !Object.prototype.hasOwnProperty.call(data[kind.service], kind.type) || !Object.prototype.hasOwnProperty.call((data as any)[kind.service][kind.type], ID)) { - addToArchive(kind, ID); - data = loadData(); // Load updated version - } + addToArchive(kind, ID); + data = loadData(); // Load updated version + } - const archivedata = (kind.service == 'crunchy' ? data[kind.service][kind.type] : data[kind.service][kind.type]); - const alreadyData = archivedata.find(a => a.id === ID)?.already; - for (const ep of episode) { - if (alreadyData?.includes(ep)) continue; - alreadyData?.push(ep); - } - fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4)); + const archivedata = (kind.service == 'crunchy' ? data[kind.service][kind.type] : data[kind.service][kind.type]); + const alreadyData = archivedata.find(a => a.id === ID)?.already; + for (const ep of episode) { + if (alreadyData?.includes(ep)) continue; + alreadyData?.push(ep); + } + fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4)); }; const makeCommand = (service: 'crunchy'|'hidive'|'ao'|'adn') : Partial<ArgvType>[] => { - const data = loadData(); - const ret: Partial<ArgvType>[] = []; - const kind = data[service]; - for (const type of Object.keys(kind)) { - const item = kind[type as 's']; // 'srz' is also possible but will be ignored for the compiler - item.forEach(i => ret.push({ - but: true, - all: false, - service, - e: i.already.join(','), - ...(type === 's' ? { - s: i.id, - series: undefined - } : { - series: i.id, - s: undefined - }) - })); - } - return ret; + const data = loadData(); + const ret: Partial<ArgvType>[] = []; + const kind = data[service]; + for (const type of Object.keys(kind)) { + const item = kind[type as 's']; // 'srz' is also possible but will be ignored for the compiler + item.forEach(i => ret.push({ + but: true, + all: false, + service, + e: i.already.join(','), + ...(type === 's' ? { + s: i.id, + series: undefined + } : { + series: i.id, + s: undefined + }) + })); + } + return ret; }; const loadData = () : DataType => { - if (fs.existsSync(archiveFile)) - return JSON.parse(fs.readFileSync(archiveFile).toString()) as DataType; - return {} as DataType; + if (fs.existsSync(archiveFile)) + return JSON.parse(fs.readFileSync(archiveFile).toString()) as DataType; + return {} as DataType; }; export { addToArchive, downloaded, makeCommand }; \ No newline at end of file diff --git a/modules/module.fetch.ts b/modules/module.fetch.ts index 616bfcb..e716981 100644 --- a/modules/module.fetch.ts +++ b/modules/module.fetch.ts @@ -22,18 +22,18 @@ type GetDataResponse = { }; function hasDisplay(): boolean { - if (process.platform === 'linux') { - return !!process.env.DISPLAY || !!process.env.WAYLAND_DISPLAY; - } - // Win and Mac true by default - return true; + if (process.platform === 'linux') { + return !!process.env.DISPLAY || !!process.env.WAYLAND_DISPLAY; + } + // Win and Mac true by default + return true; } // req export class Req { - private sessCfg: string; - private service: 'cr' | 'hd' | 'ao' | 'adn'; - private session: Record< + private sessCfg: string; + private service: 'cr' | 'hd' | 'ao' | 'adn'; + private session: Record< string, { value: string; @@ -44,133 +44,133 @@ export class Req { 'Max-Age'?: string; } > = {}; - private cfgDir = yamlCfg.cfgDir; - private curl: boolean | string = false; + private cfgDir = yamlCfg.cfgDir; + private curl: boolean | string = false; - constructor(private domain: Record<string, unknown>, private debug: boolean, private nosess = false, private type: 'cr' | 'hd' | 'ao' | 'adn') { - this.sessCfg = yamlCfg.sessCfgFile[type]; - this.service = type; - } - - async getData(durl: string, params?: RequestInit): Promise<GetDataResponse> { - params = params || {}; - // options - const options: RequestInit = { - method: params.method ? params.method : 'GET' - }; - // additional params - if (params.headers) { - options.headers = params.headers; + constructor(private domain: Record<string, unknown>, private debug: boolean, private nosess = false, private type: 'cr' | 'hd' | 'ao' | 'adn') { + this.sessCfg = yamlCfg.sessCfgFile[type]; + this.service = type; } - if (params.body) { - options.body = params.body; - } - if (typeof params.redirect == 'string') { - options.redirect = params.redirect; - } - // debug - if (this.debug) { - console.debug('[DEBUG] FETCH OPTIONS:'); - console.debug(options); - } - // try do request - try { - const res = await fetch(durl, options); - if (!res.ok) { - console.error(`${res.status}: ${res.statusText}`); - const body = await res.text(); - const docTitle = body.match(/<title>(.*)<\/title>/); - if (body && docTitle) { - if (docTitle[1] === 'Just a moment...' && durl.includes('crunchyroll') && hasDisplay()) { - console.warn('Cloudflare triggered, trying to get cookies...'); - const { page } = await connect({ - headless: false, - turnstile: true - }); - - await page.goto('https://www.crunchyroll.com/', { - waitUntil: 'networkidle2' - }); - - await page.waitForRequest('https://www.crunchyroll.com/auth/v1/token'); - - const cookies = await page.cookies(); - - await page.close(); - - params.headers = { - ...params.headers, - Cookie: cookies.map((c) => `${c.name}=${c.value}`).join('; '), - 'Set-Cookie': cookies.map((c) => `${c.name}=${c.value}`).join('; ') - }; - - (params as any).headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36'; - - return await this.getData(durl, params); - } else { - console.error(docTitle[1]); - } - } else { - console.error(body); + async getData(durl: string, params?: RequestInit): Promise<GetDataResponse> { + params = params || {}; + // options + const options: RequestInit = { + method: params.method ? params.method : 'GET' + }; + // additional params + if (params.headers) { + options.headers = params.headers; } - } - return { - ok: res.ok, - res, - headers: params.headers as Record<string, string> - }; - } catch (_error) { - const error = _error as { + if (params.body) { + options.body = params.body; + } + if (typeof params.redirect == 'string') { + options.redirect = params.redirect; + } + // debug + if (this.debug) { + console.debug('[DEBUG] FETCH OPTIONS:'); + console.debug(options); + } + // try do request + try { + const res = await fetch(durl, options); + if (!res.ok) { + console.error(`${res.status}: ${res.statusText}`); + const body = await res.text(); + const docTitle = body.match(/<title>(.*)<\/title>/); + if (body && docTitle) { + if (docTitle[1] === 'Just a moment...' && durl.includes('crunchyroll') && hasDisplay()) { + console.warn('Cloudflare triggered, trying to get cookies...'); + + const { page } = await connect({ + headless: false, + turnstile: true + }); + + await page.goto('https://www.crunchyroll.com/', { + waitUntil: 'networkidle2' + }); + + await page.waitForRequest('https://www.crunchyroll.com/auth/v1/token'); + + const cookies = await page.cookies(); + + await page.close(); + + params.headers = { + ...params.headers, + Cookie: cookies.map((c) => `${c.name}=${c.value}`).join('; '), + 'Set-Cookie': cookies.map((c) => `${c.name}=${c.value}`).join('; ') + }; + + (params as any).headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36'; + + return await this.getData(durl, params); + } else { + console.error(docTitle[1]); + } + } else { + console.error(body); + } + } + return { + ok: res.ok, + res, + headers: params.headers as Record<string, string> + }; + } catch (_error) { + const error = _error as { name: string; } & TypeError & { res: Response; }; - if (error.res && error.res.status && error.res.statusText) { - console.error(`${error.name} ${error.res.status}: ${error.res.statusText}`); - } else { - console.error(`${error.name}: ${error.res?.statusText || error.message}`); - } - if (error.res) { - const body = await error.res.text(); - const docTitle = body.match(/<title>(.*)<\/title>/); - if (body && docTitle) { - console.error(docTitle[1]); + if (error.res && error.res.status && error.res.statusText) { + console.error(`${error.name} ${error.res.status}: ${error.res.statusText}`); + } else { + console.error(`${error.name}: ${error.res?.statusText || error.message}`); + } + if (error.res) { + const body = await error.res.text(); + const docTitle = body.match(/<title>(.*)<\/title>/); + if (body && docTitle) { + console.error(docTitle[1]); + } + } + return { + ok: false, + error + }; } - } - return { - ok: false, - error - }; } - } } export function buildProxy(proxyBaseUrl: string, proxyAuth: string) { - if (!proxyBaseUrl.match(/^(https?|socks4|socks5):/)) { - proxyBaseUrl = 'http://' + proxyBaseUrl; - } + if (!proxyBaseUrl.match(/^(https?|socks4|socks5):/)) { + proxyBaseUrl = 'http://' + proxyBaseUrl; + } - const proxyCfg = new URL(proxyBaseUrl); - let proxyStr = `${proxyCfg.protocol}//`; + const proxyCfg = new URL(proxyBaseUrl); + let proxyStr = `${proxyCfg.protocol}//`; - if (typeof proxyCfg.hostname != 'string' || proxyCfg.hostname == '') { - throw new Error('[ERROR] Hostname and port required for proxy!'); - } + if (typeof proxyCfg.hostname != 'string' || proxyCfg.hostname == '') { + throw new Error('[ERROR] Hostname and port required for proxy!'); + } - if (proxyAuth && typeof proxyAuth == 'string' && proxyAuth.match(':')) { - proxyCfg.username = proxyAuth.split(':')[0]; - proxyCfg.password = proxyAuth.split(':')[1]; - proxyStr += `${proxyCfg.username}:${proxyCfg.password}@`; - } + if (proxyAuth && typeof proxyAuth == 'string' && proxyAuth.match(':')) { + proxyCfg.username = proxyAuth.split(':')[0]; + proxyCfg.password = proxyAuth.split(':')[1]; + proxyStr += `${proxyCfg.username}:${proxyCfg.password}@`; + } - proxyStr += proxyCfg.hostname; + proxyStr += proxyCfg.hostname; - if (!proxyCfg.port && proxyCfg.protocol == 'http:') { - proxyStr += ':80'; - } else if (!proxyCfg.port && proxyCfg.protocol == 'https:') { - proxyStr += ':443'; - } + if (!proxyCfg.port && proxyCfg.protocol == 'http:') { + proxyStr += ':80'; + } else if (!proxyCfg.port && proxyCfg.protocol == 'https:') { + proxyStr += ':443'; + } - return proxyStr; + return proxyStr; } diff --git a/modules/module.ffmpegChapter.ts b/modules/module.ffmpegChapter.ts index 4b86dc2..c701a78 100644 --- a/modules/module.ffmpegChapter.ts +++ b/modules/module.ffmpegChapter.ts @@ -1,51 +1,51 @@ import fs from 'fs'; export function convertChaptersToFFmpegFormat(inputFilePath: string): string { - const content = fs.readFileSync(inputFilePath, 'utf-8'); + const content = fs.readFileSync(inputFilePath, 'utf-8'); - const chapterMatches = Array.from(content.matchAll(/CHAPTER(\d+)=([\d:.]+)/g)); - const nameMatches = Array.from(content.matchAll(/CHAPTER(\d+)NAME=([^\n]+)/g)); + const chapterMatches = Array.from(content.matchAll(/CHAPTER(\d+)=([\d:.]+)/g)); + const nameMatches = Array.from(content.matchAll(/CHAPTER(\d+)NAME=([^\n]+)/g)); - const chapters = chapterMatches.map((m) => ({ - index: parseInt(m[1], 10), - time: m[2], - })).sort((a, b) => a.index - b.index); + const chapters = chapterMatches.map((m) => ({ + index: parseInt(m[1], 10), + time: m[2], + })).sort((a, b) => a.index - b.index); - const nameDict: Record<number, string> = {}; - nameMatches.forEach((m) => { - nameDict[parseInt(m[1], 10)] = m[2]; - }); + const nameDict: Record<number, string> = {}; + nameMatches.forEach((m) => { + nameDict[parseInt(m[1], 10)] = m[2]; + }); - let ffmpegContent = ';FFMETADATA1\n'; - let startTimeInNs = 0; + let ffmpegContent = ';FFMETADATA1\n'; + let startTimeInNs = 0; - for (let i = 0; i < chapters.length; i++) { - const chapterStartTime = timeToNanoSeconds(chapters[i].time); - const chapterEndTime = (i + 1 < chapters.length) - ? timeToNanoSeconds(chapters[i + 1].time) - : chapterStartTime + 1000000000; + for (let i = 0; i < chapters.length; i++) { + const chapterStartTime = timeToNanoSeconds(chapters[i].time); + const chapterEndTime = (i + 1 < chapters.length) + ? timeToNanoSeconds(chapters[i + 1].time) + : chapterStartTime + 1000000000; - const chapterName = nameDict[chapters[i].index] || `Chapter ${chapters[i].index}`; + const chapterName = nameDict[chapters[i].index] || `Chapter ${chapters[i].index}`; - ffmpegContent += '[CHAPTER]\n'; - ffmpegContent += 'TIMEBASE=1/1000000000\n'; - ffmpegContent += `START=${startTimeInNs}\n`; - ffmpegContent += `END=${chapterEndTime}\n`; - ffmpegContent += `title=${chapterName}\n`; + ffmpegContent += '[CHAPTER]\n'; + ffmpegContent += 'TIMEBASE=1/1000000000\n'; + ffmpegContent += `START=${startTimeInNs}\n`; + ffmpegContent += `END=${chapterEndTime}\n`; + ffmpegContent += `title=${chapterName}\n`; - startTimeInNs = chapterEndTime; - } + startTimeInNs = chapterEndTime; + } - return ffmpegContent; + return ffmpegContent; } export function timeToNanoSeconds(time: string): number { - const parts = time.split(':'); - const hours = parseInt(parts[0], 10); - const minutes = parseInt(parts[1], 10); - const secondsAndMs = parts[2].split('.'); - const seconds = parseInt(secondsAndMs[0], 10); - const milliseconds = parseInt(secondsAndMs[1], 10); + const parts = time.split(':'); + const hours = parseInt(parts[0], 10); + const minutes = parseInt(parts[1], 10); + const secondsAndMs = parts[2].split('.'); + const seconds = parseInt(secondsAndMs[0], 10); + const milliseconds = parseInt(secondsAndMs[1], 10); - return (hours * 3600 + minutes * 60 + seconds) * 1000000000 + milliseconds * 1000000; + return (hours * 3600 + minutes * 60 + seconds) * 1000000000 + milliseconds * 1000000; } \ No newline at end of file diff --git a/modules/module.filename.ts b/modules/module.filename.ts index 6957e68..ec7346b 100644 --- a/modules/module.filename.ts +++ b/modules/module.filename.ts @@ -15,77 +15,77 @@ export type Variable<T extends string = AvailableFilenameVars> = ({ } const parseFileName = (input: string, variables: Variable[], numbers: number, override: string[]): string[] => { - const varRegex = /\${[A-Za-z1-9]+}/g; - const vars = input.match(varRegex); - const overridenVars = parseOverride(variables, override); - if (!vars) - return [input]; - for (let i = 0; i < vars.length; i++) { - const type = vars[i]; - const varName = type.slice(2, -1); - let use = overridenVars.find(a => a.name === varName); - if (use === undefined && type === '${height}') { - use = { type: 'number', replaceWith: 0 } as Variable<string>; - } - if (use === undefined) { - console.info(`[ERROR] Found variable '${type}' in fileName but no values was internally found!`); - continue; - } + const varRegex = /\${[A-Za-z1-9]+}/g; + const vars = input.match(varRegex); + const overridenVars = parseOverride(variables, override); + if (!vars) + return [input]; + for (let i = 0; i < vars.length; i++) { + const type = vars[i]; + const varName = type.slice(2, -1); + let use = overridenVars.find(a => a.name === varName); + if (use === undefined && type === '${height}') { + use = { type: 'number', replaceWith: 0 } as Variable<string>; + } + if (use === undefined) { + console.info(`[ERROR] Found variable '${type}' in fileName but no values was internally found!`); + continue; + } - if (use.type === 'number') { - const len = use.replaceWith.toFixed(0).length; - const replaceStr = len < numbers ? '0'.repeat(numbers - len) + use.replaceWith : use.replaceWith+''; - input = input.replace(type, replaceStr); - } else { - if (use.sanitize) - use.replaceWith = Helper.cleanupFilename(use.replaceWith); - input = input.replace(type, use.replaceWith); + if (use.type === 'number') { + const len = use.replaceWith.toFixed(0).length; + const replaceStr = len < numbers ? '0'.repeat(numbers - len) + use.replaceWith : use.replaceWith+''; + input = input.replace(type, replaceStr); + } else { + if (use.sanitize) + use.replaceWith = Helper.cleanupFilename(use.replaceWith); + input = input.replace(type, use.replaceWith); + } } - } - return input.split(path.sep).map(a => Helper.cleanupFilename(a)); + return input.split(path.sep).map(a => Helper.cleanupFilename(a)); }; const parseOverride = (variables: Variable[], override: string[]): Variable<string>[] => { - const vars: Variable<string>[] = variables; - override.forEach(item => { - const index = item.indexOf('='); - if (index === -1) - return logError(item, 'invalid'); - const parts = [ item.slice(0, index), item.slice(index + 1) ]; - if (!(parts[1].startsWith('\'') && parts[1].endsWith('\'') && parts[1].length >= 2)) - return logError(item, 'invalid'); - parts[1] = parts[1].slice(1, -1); - const already = vars.findIndex(a => a.name === parts[0]); - if (already > -1) { - if (vars[already].type === 'number') { - if (isNaN(parseFloat(parts[1]))) - return logError(item, 'wrongType'); - vars[already].replaceWith = parseFloat(parts[1]); - } else { - vars[already].replaceWith = parts[1]; - } - } else { - const isNumber = !isNaN(parseFloat(parts[1])); - vars.push({ - name: parts[0], - replaceWith: isNumber ? parseFloat(parts[1]) : parts[1], - type: isNumber ? 'number' : 'string' - } as Variable<string>); - } - }); + const vars: Variable<string>[] = variables; + override.forEach(item => { + const index = item.indexOf('='); + if (index === -1) + return logError(item, 'invalid'); + const parts = [ item.slice(0, index), item.slice(index + 1) ]; + if (!(parts[1].startsWith('\'') && parts[1].endsWith('\'') && parts[1].length >= 2)) + return logError(item, 'invalid'); + parts[1] = parts[1].slice(1, -1); + const already = vars.findIndex(a => a.name === parts[0]); + if (already > -1) { + if (vars[already].type === 'number') { + if (isNaN(parseFloat(parts[1]))) + return logError(item, 'wrongType'); + vars[already].replaceWith = parseFloat(parts[1]); + } else { + vars[already].replaceWith = parts[1]; + } + } else { + const isNumber = !isNaN(parseFloat(parts[1])); + vars.push({ + name: parts[0], + replaceWith: isNumber ? parseFloat(parts[1]) : parts[1], + type: isNumber ? 'number' : 'string' + } as Variable<string>); + } + }); - return variables; + return variables; }; const logError = (override: string, reason: 'invalid'|'wrongType') => { - switch (reason) { - case 'wrongType': - console.error(`[ERROR] Invalid type on \`${override}\`. Expected number but found string. It has been ignored`); - break; - case 'invalid': - default: - console.error(`[ERROR] Invalid override \`${override}\`. It has been ignored`); - } + switch (reason) { + case 'wrongType': + console.error(`[ERROR] Invalid type on \`${override}\`. Expected number but found string. It has been ignored`); + break; + case 'invalid': + default: + console.error(`[ERROR] Invalid override \`${override}\`. It has been ignored`); + } }; export default parseFileName; \ No newline at end of file diff --git a/modules/module.fontsData.ts b/modules/module.fontsData.ts index 4aa5acf..7fc69d9 100644 --- a/modules/module.fontsData.ts +++ b/modules/module.fontsData.ts @@ -3,99 +3,99 @@ const root = 'https://static.crunchyroll.com/vilos-v2/web/vilos/assets/libass-fo // file list const fontFamilies = { - 'Adobe Arabic': ['AdobeArabic-Bold.otf'], - 'Andale Mono': ['andalemo.ttf'], - 'Arial': ['arial.ttf'], - 'Arial Black': ['ariblk.ttf'], - 'Arial Bold': ['arialbd.ttf'], - 'Arial Bold Italic': ['arialbi.ttf'], - 'Arial Italic': ['ariali.ttf'], - 'Arial Unicode MS': ['arialuni.ttf'], - 'Comic Sans MS': ['comic.ttf'], - 'Comic Sans MS Bold': ['comicbd.ttf'], - 'Courier New': ['cour.ttf'], - 'Courier New Bold': ['courbd.ttf'], - 'Courier New Bold Italic': ['courbi.ttf'], - 'Courier New Italic': ['couri.ttf'], - 'DejaVu LGC Sans Mono': ['DejaVuLGCSansMono.ttf'], - 'DejaVu LGC Sans Mono Bold': ['DejaVuLGCSansMono-Bold.ttf'], - 'DejaVu LGC Sans Mono Bold Oblique': ['DejaVuLGCSansMono-BoldOblique.ttf'], - 'DejaVu LGC Sans Mono Oblique': ['DejaVuLGCSansMono-Oblique.ttf'], - 'DejaVu Sans': ['DejaVuSans.ttf'], - 'DejaVu Sans Bold': ['DejaVuSans-Bold.ttf'], - 'DejaVu Sans Bold Oblique': ['DejaVuSans-BoldOblique.ttf'], - 'DejaVu Sans Condensed': ['DejaVuSansCondensed.ttf'], - 'DejaVu Sans Condensed Bold': ['DejaVuSansCondensed-Bold.ttf'], - 'DejaVu Sans Condensed Bold Oblique': ['DejaVuSansCondensed-BoldOblique.ttf'], - 'DejaVu Sans Condensed Oblique': ['DejaVuSansCondensed-Oblique.ttf'], - 'DejaVu Sans ExtraLight': ['DejaVuSans-ExtraLight.ttf'], - 'DejaVu Sans Mono': ['DejaVuSansMono.ttf'], - 'DejaVu Sans Mono Bold': ['DejaVuSansMono-Bold.ttf'], - 'DejaVu Sans Mono Bold Oblique': ['DejaVuSansMono-BoldOblique.ttf'], - 'DejaVu Sans Mono Oblique': ['DejaVuSansMono-Oblique.ttf'], - 'DejaVu Sans Oblique': ['DejaVuSans-Oblique.ttf'], - 'Gautami': ['gautami.ttf'], - 'Georgia': ['georgia.ttf'], - 'Georgia Bold': ['georgiab.ttf'], - 'Georgia Bold Italic': ['georgiaz.ttf'], - 'Georgia Italic': ['georgiai.ttf'], - 'Impact': ['impact.ttf'], - 'Meera Inimai': ['MeeraInimai-Regular.ttf'], - 'Noto Sans Thai': ['NotoSansThai.ttf'], - 'Rubik': ['Rubik-Regular.ttf'], - 'Rubik Black': ['Rubik-Black.ttf'], - 'Rubik Black Italic': ['Rubik-BlackItalic.ttf'], - 'Rubik Bold': ['Rubik-Bold.ttf'], - 'Rubik Bold Italic': ['Rubik-BoldItalic.ttf'], - 'Rubik Italic': ['Rubik-Italic.ttf'], - 'Rubik Light': ['Rubik-Light.ttf'], - 'Rubik Light Italic': ['Rubik-LightItalic.ttf'], - 'Rubik Medium': ['Rubik-Medium.ttf'], - 'Rubik Medium Italic': ['Rubik-MediumItalic.ttf'], - 'Tahoma': ['tahoma.ttf'], - 'Times New Roman': ['times.ttf'], - 'Times New Roman Bold': ['timesbd.ttf'], - 'Times New Roman Bold Italic': ['timesbi.ttf'], - 'Times New Roman Italic': ['timesi.ttf'], - 'Trebuchet MS': ['trebuc.ttf'], - 'Trebuchet MS Bold': ['trebucbd.ttf'], - 'Trebuchet MS Bold Italic': ['trebucbi.ttf'], - 'Trebuchet MS Italic': ['trebucit.ttf'], - 'Verdana': ['verdana.ttf'], - 'Verdana Bold': ['verdanab.ttf'], - 'Verdana Bold Italic': ['verdanaz.ttf'], - 'Verdana Italic': ['verdanai.ttf'], - 'Vrinda': ['vrinda.ttf'], - 'Vrinda Bold': ['vrindab.ttf'], - 'Webdings': ['webdings.ttf'], + 'Adobe Arabic': ['AdobeArabic-Bold.otf'], + 'Andale Mono': ['andalemo.ttf'], + 'Arial': ['arial.ttf'], + 'Arial Black': ['ariblk.ttf'], + 'Arial Bold': ['arialbd.ttf'], + 'Arial Bold Italic': ['arialbi.ttf'], + 'Arial Italic': ['ariali.ttf'], + 'Arial Unicode MS': ['arialuni.ttf'], + 'Comic Sans MS': ['comic.ttf'], + 'Comic Sans MS Bold': ['comicbd.ttf'], + 'Courier New': ['cour.ttf'], + 'Courier New Bold': ['courbd.ttf'], + 'Courier New Bold Italic': ['courbi.ttf'], + 'Courier New Italic': ['couri.ttf'], + 'DejaVu LGC Sans Mono': ['DejaVuLGCSansMono.ttf'], + 'DejaVu LGC Sans Mono Bold': ['DejaVuLGCSansMono-Bold.ttf'], + 'DejaVu LGC Sans Mono Bold Oblique': ['DejaVuLGCSansMono-BoldOblique.ttf'], + 'DejaVu LGC Sans Mono Oblique': ['DejaVuLGCSansMono-Oblique.ttf'], + 'DejaVu Sans': ['DejaVuSans.ttf'], + 'DejaVu Sans Bold': ['DejaVuSans-Bold.ttf'], + 'DejaVu Sans Bold Oblique': ['DejaVuSans-BoldOblique.ttf'], + 'DejaVu Sans Condensed': ['DejaVuSansCondensed.ttf'], + 'DejaVu Sans Condensed Bold': ['DejaVuSansCondensed-Bold.ttf'], + 'DejaVu Sans Condensed Bold Oblique': ['DejaVuSansCondensed-BoldOblique.ttf'], + 'DejaVu Sans Condensed Oblique': ['DejaVuSansCondensed-Oblique.ttf'], + 'DejaVu Sans ExtraLight': ['DejaVuSans-ExtraLight.ttf'], + 'DejaVu Sans Mono': ['DejaVuSansMono.ttf'], + 'DejaVu Sans Mono Bold': ['DejaVuSansMono-Bold.ttf'], + 'DejaVu Sans Mono Bold Oblique': ['DejaVuSansMono-BoldOblique.ttf'], + 'DejaVu Sans Mono Oblique': ['DejaVuSansMono-Oblique.ttf'], + 'DejaVu Sans Oblique': ['DejaVuSans-Oblique.ttf'], + 'Gautami': ['gautami.ttf'], + 'Georgia': ['georgia.ttf'], + 'Georgia Bold': ['georgiab.ttf'], + 'Georgia Bold Italic': ['georgiaz.ttf'], + 'Georgia Italic': ['georgiai.ttf'], + 'Impact': ['impact.ttf'], + 'Meera Inimai': ['MeeraInimai-Regular.ttf'], + 'Noto Sans Thai': ['NotoSansThai.ttf'], + 'Rubik': ['Rubik-Regular.ttf'], + 'Rubik Black': ['Rubik-Black.ttf'], + 'Rubik Black Italic': ['Rubik-BlackItalic.ttf'], + 'Rubik Bold': ['Rubik-Bold.ttf'], + 'Rubik Bold Italic': ['Rubik-BoldItalic.ttf'], + 'Rubik Italic': ['Rubik-Italic.ttf'], + 'Rubik Light': ['Rubik-Light.ttf'], + 'Rubik Light Italic': ['Rubik-LightItalic.ttf'], + 'Rubik Medium': ['Rubik-Medium.ttf'], + 'Rubik Medium Italic': ['Rubik-MediumItalic.ttf'], + 'Tahoma': ['tahoma.ttf'], + 'Times New Roman': ['times.ttf'], + 'Times New Roman Bold': ['timesbd.ttf'], + 'Times New Roman Bold Italic': ['timesbi.ttf'], + 'Times New Roman Italic': ['timesi.ttf'], + 'Trebuchet MS': ['trebuc.ttf'], + 'Trebuchet MS Bold': ['trebucbd.ttf'], + 'Trebuchet MS Bold Italic': ['trebucbi.ttf'], + 'Trebuchet MS Italic': ['trebucit.ttf'], + 'Verdana': ['verdana.ttf'], + 'Verdana Bold': ['verdanab.ttf'], + 'Verdana Bold Italic': ['verdanaz.ttf'], + 'Verdana Italic': ['verdanai.ttf'], + 'Vrinda': ['vrinda.ttf'], + 'Vrinda Bold': ['vrindab.ttf'], + 'Webdings': ['webdings.ttf'], }; // collect styles from ass string function assFonts(ass: string){ - const strings = ass.replace(/\r/g,'').split('\n'); - const styles: string[] = []; - for(const s of strings){ - if(s.match(/^Style: /)){ - const addStyle = s.split(','); - styles.push(addStyle[1]); + const strings = ass.replace(/\r/g,'').split('\n'); + const styles: string[] = []; + for(const s of strings){ + if(s.match(/^Style: /)){ + const addStyle = s.split(','); + styles.push(addStyle[1]); + } } - } - const fontMatches = ass.matchAll(/\\fn([^\\}]+)/g); - for (const match of fontMatches) { - styles.push(match[1]); - } - return [...new Set(styles)]; + const fontMatches = ass.matchAll(/\\fn([^\\}]+)/g); + for (const match of fontMatches) { + styles.push(match[1]); + } + return [...new Set(styles)]; } // font mime type function fontMime(fontFile: string){ - if(fontFile.match(/\.otf$/)){ - return 'application/vnd.ms-opentype'; - } - if(fontFile.match(/\.ttf$/)){ - return 'application/x-truetype-font'; - } - return 'application/octet-stream'; + if(fontFile.match(/\.otf$/)){ + return 'application/vnd.ms-opentype'; + } + if(fontFile.match(/\.ttf$/)){ + return 'application/x-truetype-font'; + } + return 'application/octet-stream'; } export type AvailableFonts = keyof typeof fontFamilies; diff --git a/modules/module.helper.ts b/modules/module.helper.ts index 3f74d50..da60eab 100644 --- a/modules/module.helper.ts +++ b/modules/module.helper.ts @@ -5,46 +5,46 @@ import childProcess from 'child_process'; import { console } from './log'; export default class Helper { - static async question(q: string) { - const rl = readline.createInterface({ input, output }); - const a = await rl.question(q); - rl.close(); - return a; - } - static formatTime(t: number) { - const days = Math.floor(t / 86400); - const hours = Math.floor((t % 86400) / 3600); - const minutes = Math.floor(((t % 86400) % 3600) / 60); - const seconds = t % 60; - const daysS = days > 0 ? `${days}d` : ''; - const hoursS = daysS || hours ? `${daysS}${daysS && hours < 10 ? '0' : ''}${hours}h` : ''; - const minutesS = minutes || hoursS ? `${hoursS}${hoursS && minutes < 10 ? '0' : ''}${minutes}m` : ''; - const secondsS = `${minutesS}${minutesS && seconds < 10 ? '0' : ''}${seconds}s`; - return secondsS; - } + static async question(q: string) { + const rl = readline.createInterface({ input, output }); + const a = await rl.question(q); + rl.close(); + return a; + } + static formatTime(t: number) { + const days = Math.floor(t / 86400); + const hours = Math.floor((t % 86400) / 3600); + const minutes = Math.floor(((t % 86400) % 3600) / 60); + const seconds = t % 60; + const daysS = days > 0 ? `${days}d` : ''; + const hoursS = daysS || hours ? `${daysS}${daysS && hours < 10 ? '0' : ''}${hours}h` : ''; + const minutesS = minutes || hoursS ? `${hoursS}${hoursS && minutes < 10 ? '0' : ''}${minutes}m` : ''; + const secondsS = `${minutesS}${minutesS && seconds < 10 ? '0' : ''}${seconds}s`; + return secondsS; + } - static cleanupFilename(n: string) { - /* eslint-disable no-extra-boolean-cast, no-useless-escape, no-control-regex */ - const fixingChar = '_'; - const illegalRe = /[\/\?<>\\:\*\|":]/g; - const controlRe = /[\x00-\x1f\x80-\x9f]/g; - const reservedRe = /^\.+$/; - const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i; - const windowsTrailingRe = /[\. ]+$/; - return n - .replace(illegalRe, fixingChar) - .replace(controlRe, fixingChar) - .replace(reservedRe, fixingChar) - .replace(windowsReservedRe, fixingChar) - .replace(windowsTrailingRe, fixingChar); - } + static cleanupFilename(n: string) { + /* eslint-disable no-useless-escape, no-control-regex */ + const fixingChar = '_'; + const illegalRe = /[\/\?<>\\:\*\|":]/g; + const controlRe = /[\x00-\x1f\x80-\x9f]/g; + const reservedRe = /^\.+$/; + const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i; + const windowsTrailingRe = /[\. ]+$/; + return n + .replace(illegalRe, fixingChar) + .replace(controlRe, fixingChar) + .replace(reservedRe, fixingChar) + .replace(windowsReservedRe, fixingChar) + .replace(windowsTrailingRe, fixingChar); + } - static exec( - pname: string, - fpath: string, - pargs: string, - spc = false - ): + static exec( + pname: string, + fpath: string, + pargs: string, + spc = false + ): | { isOk: true; } @@ -52,26 +52,26 @@ export default class Helper { isOk: false; err: Error & { code: number }; } { - pargs = pargs ? ' ' + pargs : ''; - console.info(`\n> "${pname}"${pargs}${spc ? '\n' : ''}`); - try { - if (process.platform === 'win32') { - childProcess.execSync('& ' + fpath + pargs, { stdio: 'inherit', shell: 'powershell.exe', windowsHide: true }); - } else { - childProcess.execSync(fpath + pargs, { stdio: 'inherit' }); - } - return { - isOk: true - }; - } catch (er) { - const err = er as Error & { status: number }; - return { - isOk: false, - err: { - ...err, - code: err.status + pargs = pargs ? ' ' + pargs : ''; + console.info(`\n> "${pname}"${pargs}${spc ? '\n' : ''}`); + try { + if (process.platform === 'win32') { + childProcess.execSync('& ' + fpath + pargs, { stdio: 'inherit', shell: 'powershell.exe', windowsHide: true }); + } else { + childProcess.execSync(fpath + pargs, { stdio: 'inherit' }); + } + return { + isOk: true + }; + } catch (er) { + const err = er as Error & { status: number }; + return { + isOk: false, + err: { + ...err, + code: err.status + } + }; } - }; } - } } diff --git a/modules/module.langsData.ts b/modules/module.langsData.ts index 1d9efa2..043b32e 100644 --- a/modules/module.langsData.ts +++ b/modules/module.langsData.ts @@ -13,155 +13,155 @@ export type LanguageItem = { } const languages: LanguageItem[] = [ - { locale: 'un', code: 'und', name: 'Undetermined', language: 'Undetermined', new_hd_locale: 'und', cr_locale: 'und', adn_locale: 'und', ao_locale: 'und' }, - { cr_locale: 'en-US', new_hd_locale: 'en-US', hd_locale: 'English', locale: 'en', code: 'eng', name: 'English' }, - { cr_locale: 'en-IN', locale: 'en-IN', code: 'eng', name: 'English (India)', }, - { cr_locale: 'es-LA', new_hd_locale: 'es-MX', hd_locale: 'Spanish LatAm', locale: 'es-419', code: 'spa', name: 'Spanish', language: 'Latin American Spanish' }, - { cr_locale: 'es-419',ao_locale: 'es',hd_locale: 'Spanish', locale: 'es-419', code: 'spa-419', name: 'Spanish', language: 'Latin American Spanish' }, - { cr_locale: 'es-ES', new_hd_locale: 'es-ES', hd_locale: 'Spanish Europe', locale: 'es-ES', code: 'spa-ES', name: 'Castilian', language: 'European Spanish' }, - { cr_locale: 'pt-BR', ao_locale: 'pt',new_hd_locale: 'pt-BR', hd_locale: 'Portuguese', locale: 'pt-BR', code: 'por', name: 'Portuguese', language: 'Brazilian Portuguese' }, - { cr_locale: 'pt-PT', locale: 'pt-PT', code: 'por', name: 'Portuguese (Portugal)', language: 'Portugues (Portugal)' }, - { cr_locale: 'fr-FR', adn_locale: 'fr', hd_locale: 'French', locale: 'fr', code: 'fra', name: 'French' }, - { cr_locale: 'de-DE', adn_locale: 'de', hd_locale: 'German', locale: 'de', code: 'deu', name: 'German' }, - { cr_locale: 'ar-ME', locale: 'ar', code: 'ara-ME', name: 'Arabic' }, - { cr_locale: 'ar-SA', hd_locale: 'Arabic', locale: 'ar', code: 'ara', name: 'Arabic (Saudi Arabia)' }, - { cr_locale: 'it-IT', hd_locale: 'Italian', locale: 'it', code: 'ita', name: 'Italian' }, - { cr_locale: 'ru-RU', hd_locale: 'Russian', locale: 'ru', code: 'rus', name: 'Russian' }, - { cr_locale: 'tr-TR', hd_locale: 'Turkish', locale: 'tr', code: 'tur', name: 'Turkish' }, - { cr_locale: 'hi-IN', locale: 'hi', code: 'hin', name: 'Hindi' }, - { 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' }, - { cr_locale: 'th-TH', locale: 'th-TH', code: 'tha', name: 'Thai', language: 'ไทย' }, - { cr_locale: 'ta-IN', locale: 'ta-IN', code: 'tam', name: 'Tamil (India)', language: 'தமிழ்' }, - { cr_locale: 'ms-MY', locale: 'ms-MY', code: 'may', name: 'Malay (Malaysia)', language: 'Bahasa Melayu' }, - { cr_locale: 'vi-VN', locale: 'vi-VN', code: 'vie', name: 'Vietnamese', language: 'Tiếng Việt' }, - { cr_locale: 'id-ID', locale: 'id-ID', code: 'ind', name: 'Indonesian', language: 'Bahasa Indonesia' }, - { cr_locale: 'te-IN', locale: 'te-IN', code: 'tel', name: 'Telugu (India)', language: 'తెలుగు' }, - { cr_locale: 'ja-JP', adn_locale: 'ja', ao_locale: 'ja', hd_locale: 'Japanese', locale: 'ja', code: 'jpn', name: 'Japanese' }, + { locale: 'un', code: 'und', name: 'Undetermined', language: 'Undetermined', new_hd_locale: 'und', cr_locale: 'und', adn_locale: 'und', ao_locale: 'und' }, + { cr_locale: 'en-US', new_hd_locale: 'en-US', hd_locale: 'English', locale: 'en', code: 'eng', name: 'English' }, + { cr_locale: 'en-IN', locale: 'en-IN', code: 'eng', name: 'English (India)', }, + { cr_locale: 'es-LA', new_hd_locale: 'es-MX', hd_locale: 'Spanish LatAm', locale: 'es-419', code: 'spa', name: 'Spanish', language: 'Latin American Spanish' }, + { cr_locale: 'es-419',ao_locale: 'es',hd_locale: 'Spanish', locale: 'es-419', code: 'spa-419', name: 'Spanish', language: 'Latin American Spanish' }, + { cr_locale: 'es-ES', new_hd_locale: 'es-ES', hd_locale: 'Spanish Europe', locale: 'es-ES', code: 'spa-ES', name: 'Castilian', language: 'European Spanish' }, + { cr_locale: 'pt-BR', ao_locale: 'pt',new_hd_locale: 'pt-BR', hd_locale: 'Portuguese', locale: 'pt-BR', code: 'por', name: 'Portuguese', language: 'Brazilian Portuguese' }, + { cr_locale: 'pt-PT', locale: 'pt-PT', code: 'por', name: 'Portuguese (Portugal)', language: 'Portugues (Portugal)' }, + { cr_locale: 'fr-FR', adn_locale: 'fr', hd_locale: 'French', locale: 'fr', code: 'fra', name: 'French' }, + { cr_locale: 'de-DE', adn_locale: 'de', hd_locale: 'German', locale: 'de', code: 'deu', name: 'German' }, + { cr_locale: 'ar-ME', locale: 'ar', code: 'ara-ME', name: 'Arabic' }, + { cr_locale: 'ar-SA', hd_locale: 'Arabic', locale: 'ar', code: 'ara', name: 'Arabic (Saudi Arabia)' }, + { cr_locale: 'it-IT', hd_locale: 'Italian', locale: 'it', code: 'ita', name: 'Italian' }, + { cr_locale: 'ru-RU', hd_locale: 'Russian', locale: 'ru', code: 'rus', name: 'Russian' }, + { cr_locale: 'tr-TR', hd_locale: 'Turkish', locale: 'tr', code: 'tur', name: 'Turkish' }, + { cr_locale: 'hi-IN', locale: 'hi', code: 'hin', name: 'Hindi' }, + { 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' }, + { cr_locale: 'th-TH', locale: 'th-TH', code: 'tha', name: 'Thai', language: 'ไทย' }, + { cr_locale: 'ta-IN', locale: 'ta-IN', code: 'tam', name: 'Tamil (India)', language: 'தமிழ்' }, + { cr_locale: 'ms-MY', locale: 'ms-MY', code: 'may', name: 'Malay (Malaysia)', language: 'Bahasa Melayu' }, + { cr_locale: 'vi-VN', locale: 'vi-VN', code: 'vie', name: 'Vietnamese', language: 'Tiếng Việt' }, + { cr_locale: 'id-ID', locale: 'id-ID', code: 'ind', name: 'Indonesian', language: 'Bahasa Indonesia' }, + { cr_locale: 'te-IN', locale: 'te-IN', code: 'tel', name: 'Telugu (India)', language: 'తెలుగు' }, + { cr_locale: 'ja-JP', adn_locale: 'ja', ao_locale: 'ja', hd_locale: 'Japanese', locale: 'ja', code: 'jpn', name: 'Japanese' }, ]; // add en language names (() =>{ - for(const languageIndex in languages){ - if(!languages[languageIndex].language){ - languages[languageIndex].language = languages[languageIndex].name; + for(const languageIndex in languages){ + if(!languages[languageIndex].language){ + languages[languageIndex].language = languages[languageIndex].name; + } } - } })(); // construct dub language codes const dubLanguageCodes = (() => { - const dubLanguageCodesArray: string[] = []; - for(const language of languages){ - dubLanguageCodesArray.push(language.code); - } - return [...new Set(dubLanguageCodesArray)]; + const dubLanguageCodesArray: string[] = []; + for(const language of languages){ + dubLanguageCodesArray.push(language.code); + } + return [...new Set(dubLanguageCodesArray)]; })(); // construct subtitle languages filter const subtitleLanguagesFilter = (() => { - const subtitleLanguagesExtraParameters = ['all', 'none']; - return [...subtitleLanguagesExtraParameters, ...new Set(languages.map(l => { return l.locale; }))]; + const subtitleLanguagesExtraParameters = ['all', 'none']; + return [...subtitleLanguagesExtraParameters, ...new Set(languages.map(l => { return l.locale; }))]; })(); const searchLocales = (() => { - return ['', ...new Set(languages.map(l => { return l.cr_locale; }).slice(0, -1)), ...new Set(languages.map(l => { return l.adn_locale; }).slice(0, -1))]; + return ['', ...new Set(languages.map(l => { return l.cr_locale; }).slice(0, -1)), ...new Set(languages.map(l => { return l.adn_locale; }).slice(0, -1))]; })(); export const aoSearchLocales = (() => { - return ['', ...new Set(languages.map(l => { return l.ao_locale; }).slice(0, -1))]; + return ['', ...new Set(languages.map(l => { return l.ao_locale; }).slice(0, -1))]; })(); // convert const fixLanguageTag = (tag: string) => { - tag = typeof tag == 'string' ? tag : 'und'; - const tagLangLC = tag.match(/^(\w{2})-?(\w{2})$/); - if(tagLangLC){ - const tagLang = `${tagLangLC[1]}-${tagLangLC[2].toUpperCase()}`; - if(findLang(tagLang).cr_locale != 'und'){ - return findLang(tagLang).cr_locale; + tag = typeof tag == 'string' ? tag : 'und'; + const tagLangLC = tag.match(/^(\w{2})-?(\w{2})$/); + if(tagLangLC){ + const tagLang = `${tagLangLC[1]}-${tagLangLC[2].toUpperCase()}`; + if(findLang(tagLang).cr_locale != 'und'){ + return findLang(tagLang).cr_locale; + } + else{ + return tagLang; + } } else{ - return tagLang; + return tag; } - } - else{ - return tag; - } }; // find lang by cr_locale const findLang = (cr_locale: string) => { - const lang = languages.find(l => { return l.cr_locale == cr_locale; }); - return lang ? lang : languages.find(l => l.code === 'und') || { cr_locale: 'und', locale: 'un', code: 'und', name: 'Undetermined', language: 'Undetermined' }; + const lang = languages.find(l => { return l.cr_locale == cr_locale; }); + return lang ? lang : languages.find(l => l.code === 'und') || { cr_locale: 'und', locale: 'un', code: 'und', name: 'Undetermined', language: 'Undetermined' }; }; const fixAndFindCrLC = (cr_locale: string) => { - const str = fixLanguageTag(cr_locale); - return findLang(str || ''); + const str = fixLanguageTag(cr_locale); + return findLang(str || ''); }; // rss subs lang parser const parseRssSubtitlesString = (subs: string) => { - const splitMap = subs.replace(/\s/g, '').split(',').map((s) => { - return fixAndFindCrLC(s).locale; - }); - const sort = sortTags(splitMap); - return sort.join(', '); + const splitMap = subs.replace(/\s/g, '').split(',').map((s) => { + return fixAndFindCrLC(s).locale; + }); + const sort = sortTags(splitMap); + return sort.join(', '); }; // parse subtitles Array const parseSubtitlesArray = (tags: string[]) => { - const sort = sortSubtitles(tags.map((t) => { - return { locale: fixAndFindCrLC(t).locale }; - })); - return sort.map((t) => { return t.locale; }).join(', '); + const sort = sortSubtitles(tags.map((t) => { + return { locale: fixAndFindCrLC(t).locale }; + })); + return sort.map((t) => { return t.locale; }).join(', '); }; // sort subtitles const sortSubtitles = <T extends { [key: string]: unknown } = Record<string, string>> (data: T[], sortkey?: keyof T) : T[] => { - const idx: Record<string, number> = {}; - const key = sortkey || 'locale' as keyof T; - const tags = [...new Set(Object.values(languages).map(e => e.locale))]; - for(const l of tags){ - idx[l] = Object.keys(idx).length + 1; - } - data.sort((a, b) => { - const ia = idx[a[key] as string] ? idx[a[key] as string] : 50; - const ib = idx[b[key] as string] ? idx[b[key] as string] : 50; - return ia - ib; - }); - return data; + const idx: Record<string, number> = {}; + const key = sortkey || 'locale' as keyof T; + const tags = [...new Set(Object.values(languages).map(e => e.locale))]; + for(const l of tags){ + idx[l] = Object.keys(idx).length + 1; + } + data.sort((a, b) => { + const ia = idx[a[key] as string] ? idx[a[key] as string] : 50; + const ib = idx[b[key] as string] ? idx[b[key] as string] : 50; + return ia - ib; + }); + return data; }; const sortTags = (data: string[]) => { - const retData = data.map(e => { return { locale: e }; }); - const sort = sortSubtitles(retData); - return sort.map(e => e.locale as string); + const retData = data.map(e => { return { locale: e }; }); + const sort = sortSubtitles(retData); + return sort.map(e => e.locale as string); }; const subsFile = (fnOutput:string, subsIndex: string, langItem: LanguageItem, isCC: boolean, ccTag: string, isSigns?: boolean, format?: string) => { - subsIndex = (parseInt(subsIndex) + 1).toString().padStart(2, '0'); - return `${fnOutput}.${subsIndex}.${langItem.code}.${langItem.language}${isCC ? `.${ccTag}` : ''}${isSigns ? '.signs' : ''}.${format ? format : 'ass'}`; + subsIndex = (parseInt(subsIndex) + 1).toString().padStart(2, '0'); + return `${fnOutput}.${subsIndex}.${langItem.code}.${langItem.language}${isCC ? `.${ccTag}` : ''}${isSigns ? '.signs' : ''}.${format ? format : 'ass'}`; }; // construct dub langs const const dubLanguages = (() => { - const dubDb: Record<string, string> = {}; - for(const lang of languages){ - if(!Object.keys(dubDb).includes(lang.name)){ - dubDb[lang.name] = lang.code; + const dubDb: Record<string, string> = {}; + for(const lang of languages){ + if(!Object.keys(dubDb).includes(lang.name)){ + dubDb[lang.name] = lang.code; + } } - } - return dubDb; + return dubDb; })(); // dub regex @@ -171,34 +171,34 @@ const dubRegExp = new RegExp(dubRegExpStr); // code to lang name const langCode2name = (code: string) => { - const codeIdx = dubLanguageCodes.indexOf(code); - return Object.keys(dubLanguages)[codeIdx]; + const codeIdx = dubLanguageCodes.indexOf(code); + return Object.keys(dubLanguages)[codeIdx]; }; // locale to lang name const locale2language = (locale: string) => { - const filteredLocale = languages.filter(l => { - return l.locale == locale; - }); - return filteredLocale[0]; + const filteredLocale = languages.filter(l => { + return l.locale == locale; + }); + return filteredLocale[0]; }; // output export { - languages, - dubLanguageCodes, - dubLanguages, - langCode2name, - locale2language, - dubRegExp, - subtitleLanguagesFilter, - searchLocales, - fixLanguageTag, - findLang, - fixAndFindCrLC, - parseRssSubtitlesString, - parseSubtitlesArray, - sortSubtitles, - sortTags, - subsFile, + languages, + dubLanguageCodes, + dubLanguages, + langCode2name, + locale2language, + dubRegExp, + subtitleLanguagesFilter, + searchLocales, + fixLanguageTag, + findLang, + fixAndFindCrLC, + parseRssSubtitlesString, + parseSubtitlesArray, + sortSubtitles, + sortTags, + subsFile, }; diff --git a/modules/module.merger.ts b/modules/module.merger.ts index 8ed6c28..b8f585b 100644 --- a/modules/module.merger.ts +++ b/modules/module.merger.ts @@ -60,370 +60,370 @@ export type MergerOptions = { class Merger { - constructor(private options: MergerOptions) { - if (this.options.skipSubMux) - this.options.subtitles = []; - if (this.options.videoTitle) - this.options.videoTitle = this.options.videoTitle.replace(/"/g, '\''); - } + constructor(private options: MergerOptions) { + if (this.options.skipSubMux) + this.options.subtitles = []; + if (this.options.videoTitle) + this.options.videoTitle = this.options.videoTitle.replace(/"/g, '\''); + } - public async createDelays() { + public async createDelays() { //Don't bother scanning it if there is only 1 vna stream - if (this.options.videoAndAudio.length > 1) { - const bin = await yamlCfg.loadBinCfg(); - const vnas = this.options.videoAndAudio; - //get and set durations on each videoAndAudio Stream - for (const [vnaIndex, vna] of vnas.entries()) { - const streamInfo = await ffprobe(vna.path, { path: bin.ffprobe as string }); - const videoInfo = streamInfo.streams.filter(stream => stream.codec_type == 'video'); - vnas[vnaIndex].duration = parseInt(videoInfo[0].duration as string); - } - //Sort videoAndAudio streams by duration (shortest first) - vnas.sort((a,b) => { - if (!a.duration || !b.duration) return -1; - return a.duration - b.duration; - }); - //Set Delays - const shortestDuration = vnas[0].duration; - for (const [vnaIndex, vna] of vnas.entries()) { - //Don't calculate the shortestDuration track - if (vnaIndex == 0) { - if (!vna.isPrimary && vna.isPrimary !== undefined) - console.warn('Shortest video isn\'t primary, this might lead to problems with subtitles. Please report on github or discord if you experience issues.'); - continue; + if (this.options.videoAndAudio.length > 1) { + const bin = await yamlCfg.loadBinCfg(); + const vnas = this.options.videoAndAudio; + //get and set durations on each videoAndAudio Stream + for (const [vnaIndex, vna] of vnas.entries()) { + const streamInfo = await ffprobe(vna.path, { path: bin.ffprobe as string }); + const videoInfo = streamInfo.streams.filter(stream => stream.codec_type == 'video'); + vnas[vnaIndex].duration = parseInt(videoInfo[0].duration as string); + } + //Sort videoAndAudio streams by duration (shortest first) + vnas.sort((a,b) => { + if (!a.duration || !b.duration) return -1; + return a.duration - b.duration; + }); + //Set Delays + const shortestDuration = vnas[0].duration; + for (const [vnaIndex, vna] of vnas.entries()) { + //Don't calculate the shortestDuration track + if (vnaIndex == 0) { + if (!vna.isPrimary && vna.isPrimary !== undefined) + console.warn('Shortest video isn\'t primary, this might lead to problems with subtitles. Please report on github or discord if you experience issues.'); + continue; + } + if (vna.duration && shortestDuration) { + //Calculate the tracks delay + vna.delay = Math.ceil((vna.duration-shortestDuration) * 1000) / 1000; + //TODO: set primary language for audio so it can be used to determine which track needs the delay + //The above is a problem in the event that it isn't the dub that needs the delay, but rather the sub. + //Alternatively: Might not work: it could be checked if there are multiple of the same video language, and if there is + //more than 1 of the same video language, then do the subtitle delay on CC, else normal language. + const subtitles = this.options.subtitles.filter(sub => sub.language.code == vna.lang.code); + for (const [subIndex, sub] of subtitles.entries()) { + if (vna.isPrimary) subtitles[subIndex].delay = vna.delay; + else if (sub.closedCaption) subtitles[subIndex].delay = vna.delay; + } + } + } } - if (vna.duration && shortestDuration) { - //Calculate the tracks delay - vna.delay = Math.ceil((vna.duration-shortestDuration) * 1000) / 1000; - //TODO: set primary language for audio so it can be used to determine which track needs the delay - //The above is a problem in the event that it isn't the dub that needs the delay, but rather the sub. - //Alternatively: Might not work: it could be checked if there are multiple of the same video language, and if there is - //more than 1 of the same video language, then do the subtitle delay on CC, else normal language. - const subtitles = this.options.subtitles.filter(sub => sub.language.code == vna.lang.code); - for (const [subIndex, sub] of subtitles.entries()) { - if (vna.isPrimary) subtitles[subIndex].delay = vna.delay; - else if (sub.closedCaption) subtitles[subIndex].delay = vna.delay; - } + } + + public FFmpeg() : string { + const args: string[] = []; + const metaData: string[] = []; + + let index = 0; + let audioIndex = 0; + let hasVideo = false; + + for (const vid of this.options.videoAndAudio) { + if (vid.delay && hasVideo) { + args.push( + `-itsoffset -${Math.ceil(vid.delay*1000)}ms` + ); + } + args.push(`-i "${vid.path}"`); + if (!hasVideo || this.options.keepAllVideos) { + metaData.push(`-map ${index}:a -map ${index}:v`); + metaData.push(`-metadata:s:a:${audioIndex} language=${vid.lang.code}`); + metaData.push(`-metadata:s:v:${index} title="${this.options.videoTitle}"`); + hasVideo = true; + } else { + metaData.push(`-map ${index}:a`); + metaData.push(`-metadata:s:a:${audioIndex} language=${vid.lang.code}`); + } + audioIndex++; + index++; } - } - } - } - public FFmpeg() : string { - const args: string[] = []; - const metaData: string[] = []; - - let index = 0; - let audioIndex = 0; - let hasVideo = false; - - for (const vid of this.options.videoAndAudio) { - if (vid.delay && hasVideo) { - args.push( - `-itsoffset -${Math.ceil(vid.delay*1000)}ms` - ); - } - args.push(`-i "${vid.path}"`); - if (!hasVideo || this.options.keepAllVideos) { - metaData.push(`-map ${index}:a -map ${index}:v`); - metaData.push(`-metadata:s:a:${audioIndex} language=${vid.lang.code}`); - metaData.push(`-metadata:s:v:${index} title="${this.options.videoTitle}"`); - hasVideo = true; - } else { - metaData.push(`-map ${index}:a`); - metaData.push(`-metadata:s:a:${audioIndex} language=${vid.lang.code}`); - } - audioIndex++; - index++; - } - - for (const vid of this.options.onlyVid) { - if (!hasVideo || this.options.keepAllVideos) { - args.push(`-i "${vid.path}"`); - metaData.push(`-map ${index} -map -${index}:a`); - metaData.push(`-metadata:s:v:${index} title="${this.options.videoTitle}"`); - hasVideo = true; - index++; - } - } - - for (const aud of this.options.onlyAudio) { - args.push(`-i "${aud.path}"`); - metaData.push(`-map ${index}`); - metaData.push(`-metadata:s:a:${audioIndex} language=${aud.lang.code}`); - index++; - audioIndex++; - } - - for (const index in this.options.subtitles) { - const sub = this.options.subtitles[index]; - if (sub.delay) { - args.push( - `-itsoffset -${Math.ceil(sub.delay*1000)}ms` - ); - } - args.push(`-i "${sub.file}"`); - } - - if (this.options.chapters && this.options.chapters.length > 0) { - const chapterFilePath = this.options.chapters[0].path; - const chapterData = convertChaptersToFFmpegFormat(this.options.chapters[0].path); - fs.writeFileSync(chapterFilePath, chapterData, 'utf-8'); - args.push(`-i "${chapterFilePath}" -map_metadata 1`); - } - - if (this.options.output.split('.').pop() === 'mkv') { - if (this.options.fonts) { - let fontIndex = 0; - for (const font of this.options.fonts) { - args.push(`-attach ${font.path} -metadata:s:t:${fontIndex} mimetype=${font.mime} -metadata:s:t:${fontIndex} filename=${font.name}`); - fontIndex++; + for (const vid of this.options.onlyVid) { + if (!hasVideo || this.options.keepAllVideos) { + args.push(`-i "${vid.path}"`); + metaData.push(`-map ${index} -map -${index}:a`); + metaData.push(`-metadata:s:v:${index} title="${this.options.videoTitle}"`); + hasVideo = true; + index++; + } } - } + + for (const aud of this.options.onlyAudio) { + args.push(`-i "${aud.path}"`); + metaData.push(`-map ${index}`); + metaData.push(`-metadata:s:a:${audioIndex} language=${aud.lang.code}`); + index++; + audioIndex++; + } + + for (const index in this.options.subtitles) { + const sub = this.options.subtitles[index]; + if (sub.delay) { + args.push( + `-itsoffset -${Math.ceil(sub.delay*1000)}ms` + ); + } + args.push(`-i "${sub.file}"`); + } + + if (this.options.chapters && this.options.chapters.length > 0) { + const chapterFilePath = this.options.chapters[0].path; + const chapterData = convertChaptersToFFmpegFormat(this.options.chapters[0].path); + fs.writeFileSync(chapterFilePath, chapterData, 'utf-8'); + args.push(`-i "${chapterFilePath}" -map_metadata 1`); + } + + if (this.options.output.split('.').pop() === 'mkv') { + if (this.options.fonts) { + let fontIndex = 0; + for (const font of this.options.fonts) { + args.push(`-attach ${font.path} -metadata:s:t:${fontIndex} mimetype=${font.mime} -metadata:s:t:${fontIndex} filename=${font.name}`); + fontIndex++; + } + } + } + + args.push(...metaData); + args.push(...this.options.subtitles.map((_, subIndex) => `-map ${subIndex + index}`)); + args.push( + '-c:v copy', + '-c:a copy', + this.options.output.split('.').pop()?.toLowerCase() === 'mp4' ? '-c:s mov_text' : '-c:s ass', + ...this.options.subtitles.map((sub, subindex) => `-metadata:s:s:${subindex} title="${ + (sub.language.language || sub.language.name) + `${sub.closedCaption === true ? ` ${this.options.ccTag}` : ''}` + `${sub.signs === true ? ' Signs' : ''}` + }" -metadata:s:s:${subindex} language=${sub.language.code}`) + ); + args.push(...this.options.options.ffmpeg); + args.push(`"${this.options.output}"`); + return args.join(' '); } - args.push(...metaData); - args.push(...this.options.subtitles.map((_, subIndex) => `-map ${subIndex + index}`)); - args.push( - '-c:v copy', - '-c:a copy', - this.options.output.split('.').pop()?.toLowerCase() === 'mp4' ? '-c:s mov_text' : '-c:s ass', - ...this.options.subtitles.map((sub, subindex) => `-metadata:s:s:${subindex} title="${ - (sub.language.language || sub.language.name) + `${sub.closedCaption === true ? ` ${this.options.ccTag}` : ''}` + `${sub.signs === true ? ' Signs' : ''}` - }" -metadata:s:s:${subindex} language=${sub.language.code}`) - ); - args.push(...this.options.options.ffmpeg); - args.push(`"${this.options.output}"`); - return args.join(' '); - } + public static getLanguageCode = (from: string, _default = 'eng'): string => { + if (from === 'cmn') return 'chi'; + for (const lang in iso639.iso_639_2) { + const langObj = iso639.iso_639_2[lang]; + if (Object.prototype.hasOwnProperty.call(langObj, '639-1') && langObj['639-1'] === from) { + return langObj['639-2'] as string; + } + } + return _default; + }; - public static getLanguageCode = (from: string, _default = 'eng'): string => { - if (from === 'cmn') return 'chi'; - for (const lang in iso639.iso_639_2) { - const langObj = iso639.iso_639_2[lang]; - if (Object.prototype.hasOwnProperty.call(langObj, '639-1') && langObj['639-1'] === from) { - return langObj['639-2'] as string; - } - } - return _default; - }; + public MkvMerge = () => { + const args: string[] = []; - public MkvMerge = () => { - const args: string[] = []; + let hasVideo = false; - let hasVideo = false; + args.push(`-o "${this.options.output}"`); + args.push(...this.options.options.mkvmerge); - args.push(`-o "${this.options.output}"`); - args.push(...this.options.options.mkvmerge); + for (const vid of this.options.onlyVid) { + if (!hasVideo || this.options.keepAllVideos) { + args.push( + '--video-tracks 0', + '--no-audio' + ); + const trackName = ((this.options.videoTitle ?? vid.lang.name) + (this.options.simul ? ' [Simulcast]' : ' [Uncut]')); + args.push('--track-name', `0:"${trackName}"`); + args.push(`--language 0:${vid.lang.code}`); + hasVideo = true; + args.push(`"${vid.path}"`); + } + } - for (const vid of this.options.onlyVid) { - if (!hasVideo || this.options.keepAllVideos) { - args.push( - '--video-tracks 0', - '--no-audio' - ); - const trackName = ((this.options.videoTitle ?? vid.lang.name) + (this.options.simul ? ' [Simulcast]' : ' [Uncut]')); - args.push('--track-name', `0:"${trackName}"`); - args.push(`--language 0:${vid.lang.code}`); - hasVideo = true; - args.push(`"${vid.path}"`); - } - } + for (const vid of this.options.videoAndAudio) { + const audioTrackNum = this.options.inverseTrackOrder ? '0' : '1'; + const videoTrackNum = this.options.inverseTrackOrder ? '1' : '0'; + if (vid.delay) { + args.push( + `--sync ${audioTrackNum}:-${Math.ceil(vid.delay*1000)}` + ); + } + if (!hasVideo || this.options.keepAllVideos) { + args.push( + `--video-tracks ${videoTrackNum}`, + `--audio-tracks ${audioTrackNum}` + ); + const trackName = ((this.options.videoTitle ?? vid.lang.name) + (this.options.simul ? ' [Simulcast]' : ' [Uncut]')); + args.push('--track-name', `0:"${trackName}"`); + //args.push('--track-name', `1:"${trackName}"`); + args.push(`--language ${audioTrackNum}:${vid.lang.code}`); + if (this.options.defaults.audio.code === vid.lang.code) { + args.push(`--default-track ${audioTrackNum}`); + } else { + args.push(`--default-track ${audioTrackNum}:0`); + } + hasVideo = true; + } else { + args.push( + '--no-video', + `--audio-tracks ${audioTrackNum}` + ); + if (this.options.defaults.audio.code === vid.lang.code) { + args.push(`--default-track ${audioTrackNum}`); + } else { + args.push(`--default-track ${audioTrackNum}:0`); + } + args.push('--track-name', `${audioTrackNum}:"${vid.lang.name}"`); + args.push(`--language ${audioTrackNum}:${vid.lang.code}`); + } + args.push(`"${vid.path}"`); + } - for (const vid of this.options.videoAndAudio) { - const audioTrackNum = this.options.inverseTrackOrder ? '0' : '1'; - const videoTrackNum = this.options.inverseTrackOrder ? '1' : '0'; - if (vid.delay) { - args.push( - `--sync ${audioTrackNum}:-${Math.ceil(vid.delay*1000)}` - ); - } - if (!hasVideo || this.options.keepAllVideos) { - args.push( - `--video-tracks ${videoTrackNum}`, - `--audio-tracks ${audioTrackNum}` - ); - const trackName = ((this.options.videoTitle ?? vid.lang.name) + (this.options.simul ? ' [Simulcast]' : ' [Uncut]')); - args.push('--track-name', `0:"${trackName}"`); - //args.push('--track-name', `1:"${trackName}"`); - args.push(`--language ${audioTrackNum}:${vid.lang.code}`); - if (this.options.defaults.audio.code === vid.lang.code) { - args.push(`--default-track ${audioTrackNum}`); + for (const aud of this.options.onlyAudio) { + const trackName = aud.lang.name; + args.push('--track-name', `0:"${trackName}"`); + args.push(`--language 0:${aud.lang.code}`); + args.push( + '--no-video', + '--audio-tracks 0' + ); + if (this.options.defaults.audio.code === aud.lang.code) { + args.push('--default-track 0'); + } else { + args.push('--default-track 0:0'); + } + args.push(`"${aud.path}"`); + } + + if (this.options.subtitles.length > 0) { + for (const subObj of this.options.subtitles) { + if (subObj.delay) { + args.push( + `--sync 0:-${Math.ceil(subObj.delay*1000)}` + ); + } + args.push('--track-name', `0:"${(subObj.language.language || subObj.language.name) + `${subObj.closedCaption === true ? ` ${this.options.ccTag}` : ''}` + `${subObj.signs === true ? ' Signs' : ''}`}"`); + args.push('--language', `0:"${subObj.language.code}"`); + //TODO: look into making Closed Caption default if it's the only sub of the default language downloaded + if (this.options.defaults.sub.code === subObj.language.code && !subObj.closedCaption) { + args.push('--default-track 0'); + } else { + args.push('--default-track 0:0'); + } + args.push(`"${subObj.file}"`); + } } else { - args.push(`--default-track ${audioTrackNum}:0`); + args.push( + '--no-subtitles', + ); } - hasVideo = true; - } else { - args.push( - '--no-video', - `--audio-tracks ${audioTrackNum}` - ); - if (this.options.defaults.audio.code === vid.lang.code) { - args.push(`--default-track ${audioTrackNum}`); + + if (this.options.fonts && this.options.fonts.length > 0) { + for (const f of this.options.fonts) { + args.push('--attachment-name', f.name); + args.push('--attachment-mime-type', f.mime); + args.push('--attach-file', `"${f.path}"`); + } } else { - args.push(`--default-track ${audioTrackNum}:0`); + args.push( + '--no-attachments' + ); } - args.push('--track-name', `${audioTrackNum}:"${vid.lang.name}"`); - args.push(`--language ${audioTrackNum}:${vid.lang.code}`); - } - args.push(`"${vid.path}"`); - } - for (const aud of this.options.onlyAudio) { - const trackName = aud.lang.name; - args.push('--track-name', `0:"${trackName}"`); - args.push(`--language 0:${aud.lang.code}`); - args.push( - '--no-video', - '--audio-tracks 0' - ); - if (this.options.defaults.audio.code === aud.lang.code) { - args.push('--default-track 0'); - } else { - args.push('--default-track 0:0'); - } - args.push(`"${aud.path}"`); - } - - if (this.options.subtitles.length > 0) { - for (const subObj of this.options.subtitles) { - if (subObj.delay) { - args.push( - `--sync 0:-${Math.ceil(subObj.delay*1000)}` - ); + if (this.options.chapters && this.options.chapters.length > 0) { + args.push(`--chapters "${this.options.chapters[0].path}"`); } - args.push('--track-name', `0:"${(subObj.language.language || subObj.language.name) + `${subObj.closedCaption === true ? ` ${this.options.ccTag}` : ''}` + `${subObj.signs === true ? ' Signs' : ''}`}"`); - args.push('--language', `0:"${subObj.language.code}"`); - //TODO: look into making Closed Caption default if it's the only sub of the default language downloaded - if (this.options.defaults.sub.code === subObj.language.code && !subObj.closedCaption) { - args.push('--default-track 0'); - } else { - args.push('--default-track 0:0'); - } - args.push(`"${subObj.file}"`); - } - } else { - args.push( - '--no-subtitles', - ); - } - if (this.options.fonts && this.options.fonts.length > 0) { - for (const f of this.options.fonts) { - args.push('--attachment-name', f.name); - args.push('--attachment-mime-type', f.mime); - args.push('--attach-file', `"${f.path}"`); - } - } else { - args.push( - '--no-attachments' - ); - } + return args.join(' '); + }; - if (this.options.chapters && this.options.chapters.length > 0) { - args.push(`--chapters "${this.options.chapters[0].path}"`); - } - - return args.join(' '); - }; - - public static checkMerger(bin: { + public static checkMerger(bin: { mkvmerge?: string, ffmpeg?: string, }, useMP4format: boolean, force: AvailableMuxer|undefined) : { MKVmerge?: string, FFmpeg?: string } { - if (force && bin[force]) { - return { - FFmpeg: force === 'ffmpeg' ? bin.ffmpeg : undefined, - MKVmerge: force === 'mkvmerge' ? bin.mkvmerge : undefined - }; + if (force && bin[force]) { + return { + FFmpeg: force === 'ffmpeg' ? bin.ffmpeg : undefined, + MKVmerge: force === 'mkvmerge' ? bin.mkvmerge : undefined + }; + } + if (useMP4format && bin.ffmpeg) { + return { + FFmpeg: bin.ffmpeg + }; + } else if (!useMP4format && (bin.mkvmerge || bin.ffmpeg)) { + return { + MKVmerge: bin.mkvmerge, + FFmpeg: bin.ffmpeg + }; + } else if (useMP4format) { + console.warn('FFmpeg not found, skip muxing...'); + } else if (!bin.mkvmerge) { + console.warn('MKVMerge not found, skip muxing...'); + } + return {}; } - if (useMP4format && bin.ffmpeg) { - return { - FFmpeg: bin.ffmpeg - }; - } else if (!useMP4format && (bin.mkvmerge || bin.ffmpeg)) { - return { - MKVmerge: bin.mkvmerge, - FFmpeg: bin.ffmpeg - }; - } else if (useMP4format) { - console.warn('FFmpeg not found, skip muxing...'); - } else if (!bin.mkvmerge) { - console.warn('MKVMerge not found, skip muxing...'); - } - return {}; - } - public static makeFontsList (fontsDir: string, subs: { + public static makeFontsList (fontsDir: string, subs: { language: LanguageItem, fonts: Font[] }[]) : ParsedFont[] { - let fontsNameList: Font[] = []; const fontsList: { name: string, path: string, mime: string }[] = [], subsList: string[] = []; let isNstr = true; - for(const s of subs){ - fontsNameList.push(...s.fonts); - subsList.push(s.language.locale); - } - fontsNameList = [...new Set(fontsNameList)]; - if(subsList.length > 0){ - console.info('\nSubtitles: %s (Total: %s)', subsList.join(', '), subsList.length); - isNstr = false; - } - if(fontsNameList.length > 0){ - console.info((isNstr ? '\n' : '') + 'Required fonts: %s (Total: %s)', fontsNameList.join(', '), fontsNameList.length); - } - for(const f of fontsNameList){ - const fontFiles = fontFamilies[f]; - if(fontFiles){ - for (const fontFile of fontFiles) { - const fontPath = path.join(fontsDir, fontFile); - const mime = fontMime(fontFile); - if(fs.existsSync(fontPath) && fs.statSync(fontPath).size != 0){ - fontsList.push({ - name: fontFile, - path: fontPath, - mime: mime, - }); - } + let fontsNameList: Font[] = []; const fontsList: { name: string, path: string, mime: string }[] = [], subsList: string[] = []; let isNstr = true; + for(const s of subs){ + fontsNameList.push(...s.fonts); + subsList.push(s.language.locale); } - } + fontsNameList = [...new Set(fontsNameList)]; + if(subsList.length > 0){ + console.info('\nSubtitles: %s (Total: %s)', subsList.join(', '), subsList.length); + isNstr = false; + } + if(fontsNameList.length > 0){ + console.info((isNstr ? '\n' : '') + 'Required fonts: %s (Total: %s)', fontsNameList.join(', '), fontsNameList.length); + } + for(const f of fontsNameList){ + const fontFiles = fontFamilies[f]; + if(fontFiles){ + for (const fontFile of fontFiles) { + const fontPath = path.join(fontsDir, fontFile); + const mime = fontMime(fontFile); + if(fs.existsSync(fontPath) && fs.statSync(fontPath).size != 0){ + fontsList.push({ + name: fontFile, + path: fontPath, + mime: mime, + }); + } + } + } + } + return fontsList; } - return fontsList; - } - public async merge(type: 'ffmpeg'|'mkvmerge', bin: string) { - let command: string|undefined = undefined; - switch (type) { - case 'ffmpeg': - command = this.FFmpeg(); - break; - case 'mkvmerge': - command = this.MkvMerge(); - break; + public async merge(type: 'ffmpeg'|'mkvmerge', bin: string) { + let command: string|undefined = undefined; + switch (type) { + case 'ffmpeg': + command = this.FFmpeg(); + break; + case 'mkvmerge': + command = this.MkvMerge(); + break; + } + if (command === undefined) { + console.warn('Unable to merge files.'); + return; + } + console.info(`[${type}] Started merging`); + const res = Helper.exec(type, `"${bin}"`, command); + if (!res.isOk && type === 'mkvmerge' && res.err.code === 1) { + console.info(`[${type}] Mkvmerge finished with at least one warning`); + } else if (!res.isOk) { + console.error(res.err); + console.error(`[${type}] Merging failed with exit code ${res.err.code}`); + } else { + console.info(`[${type} Done]`); + } } - if (command === undefined) { - console.warn('Unable to merge files.'); - return; - } - console.info(`[${type}] Started merging`); - const res = Helper.exec(type, `"${bin}"`, command); - if (!res.isOk && type === 'mkvmerge' && res.err.code === 1) { - console.info(`[${type}] Mkvmerge finished with at least one warning`); - } else if (!res.isOk) { - console.error(res.err); - console.error(`[${type}] Merging failed with exit code ${res.err.code}`); - } else { - console.info(`[${type} Done]`); - } - } - public cleanUp() { - this.options.onlyAudio.concat(this.options.onlyVid).concat(this.options.videoAndAudio).forEach(a => fs.unlinkSync(a.path)); - this.options.chapters?.forEach(a => fs.unlinkSync(a.path)); - this.options.subtitles.forEach(a => fs.unlinkSync(a.file)); - } + public cleanUp() { + this.options.onlyAudio.concat(this.options.onlyVid).concat(this.options.videoAndAudio).forEach(a => fs.unlinkSync(a.path)); + this.options.chapters?.forEach(a => fs.unlinkSync(a.path)); + this.options.subtitles.forEach(a => fs.unlinkSync(a.file)); + } } diff --git a/modules/module.parseSelect.ts b/modules/module.parseSelect.ts index 6376097..d438294 100644 --- a/modules/module.parseSelect.ts +++ b/modules/module.parseSelect.ts @@ -4,107 +4,107 @@ const parseSelect = (selectString: string, but = false) : { isSelected: (val: string|string[]) => boolean, values: string[] } => { - if (!selectString) - return { - values: [], - isSelected: () => but - }; - const parts = selectString.split(','); - const select: string[] = []; + if (!selectString) + return { + values: [], + isSelected: () => but + }; + const parts = selectString.split(','); + const select: string[] = []; - parts.forEach(part => { - if (part.includes('-')) { - const splits = part.split('-'); - if (splits.length !== 2) { - console.warn(`[WARN] Unable to parse input "${part}"`); - return; - } + parts.forEach(part => { + if (part.includes('-')) { + const splits = part.split('-'); + if (splits.length !== 2) { + console.warn(`[WARN] Unable to parse input "${part}"`); + return; + } - const firstPart = splits[0]; - const match = firstPart.match(/[A-Za-z]+/); - if (match && match.length > 0) { - if (match.index && match.index !== 0) { - console.warn(`[WARN] Unable to parse input "${part}"`); - return; - } - const letters = firstPart.substring(0, match[0].length); - const number = parseFloat(firstPart.substring(match[0].length)); - const b = parseFloat(splits[1]); - if (isNaN(number) || isNaN(b)) { - console.warn(`[WARN] Unable to parse input "${part}"`); - return; - } - for (let i = number; i <= b; i++) { - select.push(`${letters}${i}`); - } + const firstPart = splits[0]; + const match = firstPart.match(/[A-Za-z]+/); + if (match && match.length > 0) { + if (match.index && match.index !== 0) { + console.warn(`[WARN] Unable to parse input "${part}"`); + return; + } + const letters = firstPart.substring(0, match[0].length); + const number = parseFloat(firstPart.substring(match[0].length)); + const b = parseFloat(splits[1]); + if (isNaN(number) || isNaN(b)) { + console.warn(`[WARN] Unable to parse input "${part}"`); + return; + } + for (let i = number; i <= b; i++) { + select.push(`${letters}${i}`); + } - } else { - const a = parseFloat(firstPart); - const b = parseFloat(splits[1]); - if (isNaN(a) || isNaN(b)) { - console.warn(`[WARN] Unable to parse input "${part}"`); - return; - } - for (let i = a; i <= b; i++) { - select.push(`${i}`); - } - } + } else { + const a = parseFloat(firstPart); + const b = parseFloat(splits[1]); + if (isNaN(a) || isNaN(b)) { + console.warn(`[WARN] Unable to parse input "${part}"`); + return; + } + for (let i = a; i <= b; i++) { + select.push(`${i}`); + } + } - } else { - 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) { - if (match.index && match.index !== 0) { - console.warn(`[WARN] Unable to parse input "${part}"`); - return; - } - const letters = part.substring(0, match[0].length); - const number = parseFloat(part.substring(match[0].length)); - if (isNaN(number)) { - console.warn(`[WARN] Unable to parse input "${part}"`); - return; - } - select.push(`${letters}${number}`); - } else { - select.push(`${parseFloat(part)}`); - } - } - }); - - return { - values: select, - isSelected: (st) => { - if (typeof st === 'string') - st = [st]; - return st.some(st => { - const match = st.match(/[A-Za-z]+/); - if (st.match(/[0-9A-Z]{9}/)) { - const included = select.includes(st); - return but ? !included : included; - } else if (match && match.length > 0) { - if (match.index && match.index !== 0) { - return false; - } - const letter = st.substring(0, match[0].length); - const number = parseFloat(st.substring(match[0].length)); - if (isNaN(number)) { - return false; - } - const included = select.includes(`${letter}${number}`); - return but ? !included : included; } else { - const included = select.includes(`${parseFloat(st)}`); - return but ? !included : included; + 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) { + if (match.index && match.index !== 0) { + console.warn(`[WARN] Unable to parse input "${part}"`); + return; + } + const letters = part.substring(0, match[0].length); + const number = parseFloat(part.substring(match[0].length)); + if (isNaN(number)) { + console.warn(`[WARN] Unable to parse input "${part}"`); + return; + } + select.push(`${letters}${number}`); + } else { + select.push(`${parseFloat(part)}`); + } } - }); - } - }; + }); + + return { + values: select, + isSelected: (st) => { + if (typeof st === 'string') + st = [st]; + return st.some(st => { + const match = st.match(/[A-Za-z]+/); + if (st.match(/[0-9A-Z]{9}/)) { + const included = select.includes(st); + return but ? !included : included; + } else if (match && match.length > 0) { + if (match.index && match.index !== 0) { + return false; + } + const letter = st.substring(0, match[0].length); + const number = parseFloat(st.substring(match[0].length)); + if (isNaN(number)) { + return false; + } + const included = select.includes(`${letter}${number}`); + return but ? !included : included; + } else { + const included = select.includes(`${parseFloat(st)}`); + return but ? !included : included; + } + }); + } + }; }; export default parseSelect; \ No newline at end of file diff --git a/modules/module.transform-mpd.ts b/modules/module.transform-mpd.ts index 4796af5..c15fe5d 100644 --- a/modules/module.transform-mpd.ts +++ b/modules/module.transform-mpd.ts @@ -49,212 +49,212 @@ export type MPDParsed = { } function extractPSSH( - manifest: string, - schemeIdUri: string, - psshTagNames: string[] + manifest: string, + schemeIdUri: string, + psshTagNames: string[] ): string | null { - const regex = new RegExp( - `<ContentProtection[^>]*schemeIdUri=["']${schemeIdUri}["'][^>]*>([\\s\\S]*?)</ContentProtection>`, - 'i' - ); - const match = regex.exec(manifest); - if (match && match[1]) { - const innerContent = match[1]; - for (const tagName of psshTagNames) { - const psshRegex = new RegExp(`<${tagName}[^>]*>([\\s\\S]*?)</${tagName}>`, 'i'); - const psshMatch = psshRegex.exec(innerContent); - if (psshMatch && psshMatch[1]) { - return psshMatch[1].trim(); - } + const regex = new RegExp( + `<ContentProtection[^>]*schemeIdUri=["']${schemeIdUri}["'][^>]*>([\\s\\S]*?)</ContentProtection>`, + 'i' + ); + const match = regex.exec(manifest); + if (match && match[1]) { + const innerContent = match[1]; + for (const tagName of psshTagNames) { + const psshRegex = new RegExp(`<${tagName}[^>]*>([\\s\\S]*?)</${tagName}>`, 'i'); + const psshMatch = psshRegex.exec(innerContent); + if (psshMatch && psshMatch[1]) { + return psshMatch[1].trim(); + } + } } - } - return null; + return null; } export async function parse(manifest: string, language?: LanguageItem, url?: string) { - if (!manifest.includes('BaseURL') && url) { - manifest = manifest.replace(/(<MPD*\b[^>]*>)/gm, `$1<BaseURL>${url}</BaseURL>`); - } - const parsed = mpdParse(manifest); - const ret: MPDParsed = {}; + if (!manifest.includes('BaseURL') && url) { + manifest = manifest.replace(/(<MPD*\b[^>]*>)/gm, `$1<BaseURL>${url}</BaseURL>`); + } + const parsed = mpdParse(manifest); + const ret: MPDParsed = {}; - // Audio Loop - for (const item of Object.values(parsed.mediaGroups.AUDIO.audio)){ - for (const playlist of item.playlists) { - const host = new URL(playlist.resolvedUri).hostname; - if (!Object.prototype.hasOwnProperty.call(ret, host)) - ret[host] = { audio: [], video: [] }; + // Audio Loop + for (const item of Object.values(parsed.mediaGroups.AUDIO.audio)){ + for (const playlist of item.playlists) { + const host = new URL(playlist.resolvedUri).hostname; + if (!Object.prototype.hasOwnProperty.call(ret, host)) + ret[host] = { audio: [], video: [] }; - if (playlist.sidx && playlist.segments.length == 0) { - const options: RequestInit = { - method: 'head' - }; - if (playlist.sidx.uri.includes('animecdn')) options.headers = { - 'origin': 'https://www.animeonegai.com', - 'referer': 'https://www.animeonegai.com/', - }; - const item = await fetch(playlist.sidx.uri, options); - if (!item.ok) console.warn(`${item.status}: ${item.statusText}, Unable to fetch byteLength for audio stream ${Math.round(playlist.attributes.BANDWIDTH/1024)}KiB/s`); - const byteLength = parseInt(item.headers.get('content-length') as string); - let currentByte = playlist.sidx.map.byterange.length; - while (currentByte <= byteLength) { - playlist.segments.push({ - 'duration': 0, - 'map': { - 'uri': playlist.resolvedUri, - 'resolvedUri': playlist.resolvedUri, - 'byterange': playlist.sidx.map.byterange - }, - 'uri': playlist.resolvedUri, - 'resolvedUri': playlist.resolvedUri, - 'byterange': { - 'length': 500000, - 'offset': currentByte - }, - timeline: 0, - number: 0, - presentationTime: 0 - }); - currentByte = currentByte + 500000; - } - } + if (playlist.sidx && playlist.segments.length == 0) { + const options: RequestInit = { + method: 'head' + }; + if (playlist.sidx.uri.includes('animecdn')) options.headers = { + 'origin': 'https://www.animeonegai.com', + 'referer': 'https://www.animeonegai.com/', + }; + const item = await fetch(playlist.sidx.uri, options); + if (!item.ok) console.warn(`${item.status}: ${item.statusText}, Unable to fetch byteLength for audio stream ${Math.round(playlist.attributes.BANDWIDTH/1024)}KiB/s`); + const byteLength = parseInt(item.headers.get('content-length') as string); + let currentByte = playlist.sidx.map.byterange.length; + while (currentByte <= byteLength) { + playlist.segments.push({ + 'duration': 0, + 'map': { + 'uri': playlist.resolvedUri, + 'resolvedUri': playlist.resolvedUri, + 'byterange': playlist.sidx.map.byterange + }, + 'uri': playlist.resolvedUri, + 'resolvedUri': playlist.resolvedUri, + 'byterange': { + 'length': 500000, + 'offset': currentByte + }, + timeline: 0, + number: 0, + presentationTime: 0 + }); + currentByte = currentByte + 500000; + } + } - //Find and add audio language if it is found in the MPD - let audiolang: LanguageItem; - const foundlanguage = findLang(languages.find(a => a.code === item.language)?.cr_locale ?? 'unknown'); - if (item.language) { - audiolang = foundlanguage; - } else { - audiolang = language ? language : foundlanguage; - } - const pItem: AudioPlayList = { - bandwidth: playlist.attributes.BANDWIDTH, - language: audiolang, - default: item.default, - segments: playlist.segments.map((segment): Segment => { - const uri = segment.resolvedUri; - const map_uri = segment.map.resolvedUri; - return { - duration: segment.duration, - map: { uri: map_uri, byterange: segment.map.byterange }, - number: segment.number, - presentationTime: segment.presentationTime, - timeline: segment.timeline, - byterange: segment.byterange, - uri - }; - }) - }; + //Find and add audio language if it is found in the MPD + let audiolang: LanguageItem; + const foundlanguage = findLang(languages.find(a => a.code === item.language)?.cr_locale ?? 'unknown'); + if (item.language) { + audiolang = foundlanguage; + } else { + audiolang = language ? language : foundlanguage; + } + const pItem: AudioPlayList = { + bandwidth: playlist.attributes.BANDWIDTH, + language: audiolang, + default: item.default, + segments: playlist.segments.map((segment): Segment => { + const uri = segment.resolvedUri; + const map_uri = segment.map.resolvedUri; + return { + duration: segment.duration, + map: { uri: map_uri, byterange: segment.map.byterange }, + number: segment.number, + presentationTime: segment.presentationTime, + timeline: segment.timeline, + byterange: segment.byterange, + uri + }; + }) + }; - const playreadyPssh = extractPSSH( - manifest, - 'urn:uuid:9A04F079-9840-4286-AB92-E65BE0885F95', - ['cenc:pssh', 'mspr:pro'] - ); + const playreadyPssh = extractPSSH( + manifest, + 'urn:uuid:9A04F079-9840-4286-AB92-E65BE0885F95', + ['cenc:pssh', 'mspr:pro'] + ); - const widevinePssh = extractPSSH( - manifest, - 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', - ['cenc:pssh'] - ); + const widevinePssh = extractPSSH( + manifest, + 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', + ['cenc:pssh'] + ); - if (widevinePssh) { - pItem.pssh_wvd = widevinePssh; - } + if (widevinePssh) { + pItem.pssh_wvd = widevinePssh; + } - if (playreadyPssh) { - pItem.pssh_prd = playreadyPssh; - } + if (playreadyPssh) { + pItem.pssh_prd = playreadyPssh; + } - ret[host].audio.push(pItem); - } - } - - // Video Loop - for (const playlist of parsed.playlists) { - const host = new URL(playlist.resolvedUri).hostname; - if (!Object.prototype.hasOwnProperty.call(ret, host)) - ret[host] = { audio: [], video: [] }; - - if (playlist.sidx && playlist.segments.length == 0) { - const options: RequestInit = { - method: 'head' - }; - if (playlist.sidx.uri.includes('animecdn')) options.headers = { - 'origin': 'https://www.animeonegai.com', - 'referer': 'https://www.animeonegai.com/', - }; - const item = await fetch(playlist.sidx.uri, options); - if (!item.ok) console.warn(`${item.status}: ${item.statusText}, Unable to fetch byteLength for video stream ${playlist.attributes.RESOLUTION?.height}x${playlist.attributes.RESOLUTION?.width}@${Math.round(playlist.attributes.BANDWIDTH/1024)}KiB/s`); - const byteLength = parseInt(item.headers.get('content-length') as string); - let currentByte = playlist.sidx.map.byterange.length; - while (currentByte <= byteLength) { - playlist.segments.push({ - 'duration': 0, - 'map': { - 'uri': playlist.resolvedUri, - 'resolvedUri': playlist.resolvedUri, - 'byterange': playlist.sidx.map.byterange - }, - 'uri': playlist.resolvedUri, - 'resolvedUri': playlist.resolvedUri, - 'byterange': { - 'length': 2000000, - 'offset': currentByte - }, - timeline: 0, - number: 0, - presentationTime: 0 - }); - currentByte = currentByte + 2000000; - } + ret[host].audio.push(pItem); + } } - const pItem: VideoPlayList = { - bandwidth: playlist.attributes.BANDWIDTH, - quality: playlist.attributes.RESOLUTION!, - segments: playlist.segments.map((segment): Segment => { - const uri = segment.resolvedUri; - const map_uri = segment.map.resolvedUri; - return { - duration: segment.duration, - map: { uri: map_uri, byterange: segment.map.byterange }, - number: segment.number, - presentationTime: segment.presentationTime, - timeline: segment.timeline, - byterange: segment.byterange, - uri + // Video Loop + for (const playlist of parsed.playlists) { + const host = new URL(playlist.resolvedUri).hostname; + if (!Object.prototype.hasOwnProperty.call(ret, host)) + ret[host] = { audio: [], video: [] }; + + if (playlist.sidx && playlist.segments.length == 0) { + const options: RequestInit = { + method: 'head' + }; + if (playlist.sidx.uri.includes('animecdn')) options.headers = { + 'origin': 'https://www.animeonegai.com', + 'referer': 'https://www.animeonegai.com/', + }; + const item = await fetch(playlist.sidx.uri, options); + if (!item.ok) console.warn(`${item.status}: ${item.statusText}, Unable to fetch byteLength for video stream ${playlist.attributes.RESOLUTION?.height}x${playlist.attributes.RESOLUTION?.width}@${Math.round(playlist.attributes.BANDWIDTH/1024)}KiB/s`); + const byteLength = parseInt(item.headers.get('content-length') as string); + let currentByte = playlist.sidx.map.byterange.length; + while (currentByte <= byteLength) { + playlist.segments.push({ + 'duration': 0, + 'map': { + 'uri': playlist.resolvedUri, + 'resolvedUri': playlist.resolvedUri, + 'byterange': playlist.sidx.map.byterange + }, + 'uri': playlist.resolvedUri, + 'resolvedUri': playlist.resolvedUri, + 'byterange': { + 'length': 2000000, + 'offset': currentByte + }, + timeline: 0, + number: 0, + presentationTime: 0 + }); + currentByte = currentByte + 2000000; + } + } + + const pItem: VideoPlayList = { + bandwidth: playlist.attributes.BANDWIDTH, + quality: playlist.attributes.RESOLUTION!, + segments: playlist.segments.map((segment): Segment => { + const uri = segment.resolvedUri; + const map_uri = segment.map.resolvedUri; + return { + duration: segment.duration, + map: { uri: map_uri, byterange: segment.map.byterange }, + number: segment.number, + presentationTime: segment.presentationTime, + timeline: segment.timeline, + byterange: segment.byterange, + uri + }; + }) }; - }) - }; - const playreadyPssh = extractPSSH( - manifest, - 'urn:uuid:9A04F079-9840-4286-AB92-E65BE0885F95', - ['cenc:pssh', 'mspr:pro'] - ); + const playreadyPssh = extractPSSH( + manifest, + 'urn:uuid:9A04F079-9840-4286-AB92-E65BE0885F95', + ['cenc:pssh', 'mspr:pro'] + ); - const widevinePssh = extractPSSH( - manifest, - 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', - ['cenc:pssh'] - ); + const widevinePssh = extractPSSH( + manifest, + 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', + ['cenc:pssh'] + ); - if (widevinePssh) { - pItem.pssh_wvd = widevinePssh; + if (widevinePssh) { + pItem.pssh_wvd = widevinePssh; + } + + if (playreadyPssh) { + pItem.pssh_prd = playreadyPssh; + } + + ret[host].video.push(pItem); } - if (playreadyPssh) { - pItem.pssh_prd = playreadyPssh; - } - - ret[host].video.push(pItem); - } - - return ret; + return ret; } function arrayBufferToBase64(buffer: Uint8Array): string { - return Buffer.from(buffer).toString('base64'); + return Buffer.from(buffer).toString('base64'); } diff --git a/modules/module.updater.ts b/modules/module.updater.ts index a7e7d07..b6add24 100644 --- a/modules/module.updater.ts +++ b/modules/module.updater.ts @@ -12,17 +12,17 @@ import Helper from './module.helper'; const updateFilePlace = path.join(workingDir, 'config', 'updates.json'); const updateIgnore = [ - '*.d.ts', - '.git', - 'lib', - 'node_modules', - '@types', - path.join('bin', 'mkvtoolnix'), - path.join('config', 'token.yml'), - '.eslint', - 'tsconfig.json', - 'updates.json', - 'tsc.ts' + '*.d.ts', + '.git', + 'lib', + 'node_modules', + '@types', + path.join('bin', 'mkvtoolnix'), + path.join('config', 'token.yml'), + '.eslint', + 'tsconfig.json', + 'updates.json', + 'tsc.ts' ]; const askBeforeUpdate = ['*.yml']; @@ -40,158 +40,158 @@ export type ApplyItem = { }; export default async (force = false) => { - const isPackaged = ( + const isPackaged = ( process as NodeJS.Process & { pkg?: unknown; } - ).pkg - ? true - : !!process.env.contentDirectory; - if (isPackaged) { - return; - } - let updateFile: UpdateFile | undefined; - if (fs.existsSync(updateFilePlace)) { - updateFile = JSON.parse(fs.readFileSync(updateFilePlace).toString()) as UpdateFile; - if (new Date() < new Date(updateFile.nextCheck) && !force) { - return; + ).pkg + ? true + : !!process.env.contentDirectory; + if (isPackaged) { + return; } - } - console.info('Checking for updates...'); - const tagRequest = await fetch('https://api.github.com/repos/anidl/multi-downloader-nx/tags'); - const tags = JSON.parse(await tagRequest.text()) as GithubTag[]; - - if (tags.length > 0) { - const newer = tags.filter((a) => { - return isNewer(packageJson.version, a.name); - }); - console.info(`Found ${tags.length} release tags and ${newer.length} that are new.`); - - if (newer.length < 1) { - console.info('No new tags found'); - return done(); + let updateFile: UpdateFile | undefined; + if (fs.existsSync(updateFilePlace)) { + updateFile = JSON.parse(fs.readFileSync(updateFilePlace).toString()) as UpdateFile; + if (new Date() < new Date(updateFile.nextCheck) && !force) { + return; + } } - const newest = newer.sort((a, b) => (a.name < b.name ? 1 : a.name > b.name ? -1 : 0))[0]; - const compareRequest = await fetch(`https://api.github.com/repos/anidl/multi-downloader-nx/compare/${packageJson.version}...${newest.name}`); + console.info('Checking for updates...'); + const tagRequest = await fetch('https://api.github.com/repos/anidl/multi-downloader-nx/tags'); + const tags = JSON.parse(await tagRequest.text()) as GithubTag[]; - const compareJSON = JSON.parse(await compareRequest.text()) as TagCompare; + if (tags.length > 0) { + const newer = tags.filter((a) => { + return isNewer(packageJson.version, a.name); + }); + console.info(`Found ${tags.length} release tags and ${newer.length} that are new.`); - console.info(`You are behind by ${compareJSON.ahead_by} releases!`); - const changedFiles = compareJSON.files - .map((a) => ({ - ...a, - filename: path.join(...a.filename.split('/')) - })) - .filter((a) => { - return !updateIgnore.some((_filter) => matchString(_filter, a.filename)); - }); - if (changedFiles.length < 1) { - console.info('No file changes found... updating package.json. If you think this is an error please get the newst version yourself.'); - return done(newest.name); + if (newer.length < 1) { + console.info('No new tags found'); + return done(); + } + const newest = newer.sort((a, b) => (a.name < b.name ? 1 : a.name > b.name ? -1 : 0))[0]; + const compareRequest = await fetch(`https://api.github.com/repos/anidl/multi-downloader-nx/compare/${packageJson.version}...${newest.name}`); + + const compareJSON = JSON.parse(await compareRequest.text()) as TagCompare; + + console.info(`You are behind by ${compareJSON.ahead_by} releases!`); + const changedFiles = compareJSON.files + .map((a) => ({ + ...a, + filename: path.join(...a.filename.split('/')) + })) + .filter((a) => { + return !updateIgnore.some((_filter) => matchString(_filter, a.filename)); + }); + if (changedFiles.length < 1) { + console.info('No file changes found... updating package.json. If you think this is an error please get the newst version yourself.'); + return done(newest.name); + } + console.info(`Found file changes: \n${changedFiles.map((a) => ` [${a.status === 'modified' ? '*' : a.status === 'added' ? '+' : '-'}] ${a.filename}`).join('\n')}`); + + const remove: string[] = []; + + for (const a of changedFiles.filter((a) => a.status !== 'added')) { + if (!askBeforeUpdate.some((pattern) => matchString(pattern, a.filename))) continue; + const answer = await Helper.question( + `The developer decided that the file '${a.filename}' may contain information you changed yourself. Should they be overriden to be updated? [y/N]` + ); + if (answer.toLowerCase() === 'y') remove.push(a.sha); + } + + const changesToApply = await Promise.all( + changedFiles + .filter((a) => !remove.includes(a.sha)) + .map(async (a): Promise<ApplyItem> => { + if (a.filename.endsWith('.ts') || a.filename.endsWith('tsx')) { + const isTSX = a.filename.endsWith('tsx'); + const ret = { + path: a.filename.slice(0, isTSX ? -3 : -2) + `js${isTSX ? 'x' : ''}`, + content: transpileModule(await (await fetch(a.raw_url)).text(), { + compilerOptions: tsConfig.compilerOptions as unknown as CompilerOptions + }).outputText, + type: a.status === 'modified' ? ApplyType.UPDATE : a.status === 'added' ? ApplyType.ADD : ApplyType.DELETE + }; + console.info('✓ Transpiled %s', ret.path); + return ret; + } else { + const ret = { + path: a.filename, + content: await (await fetch(a.raw_url)).text(), + type: a.status === 'modified' ? ApplyType.UPDATE : a.status === 'added' ? ApplyType.ADD : ApplyType.DELETE + }; + console.info('✓ Got %s', ret.path); + return ret; + } + }) + ); + + changesToApply.forEach((a) => { + try { + fsextra.ensureDirSync(path.dirname(a.path)); + fs.writeFileSync(path.join(__dirname, '..', a.path), a.content); + console.info('✓ Written %s', a.path); + } catch (er) { + console.info('✗ Error while writing %s', a.path); + } + }); + + console.info('Done'); + return done(); } - console.info(`Found file changes: \n${changedFiles.map((a) => ` [${a.status === 'modified' ? '*' : a.status === 'added' ? '+' : '-'}] ${a.filename}`).join('\n')}`); - - const remove: string[] = []; - - for (const a of changedFiles.filter((a) => a.status !== 'added')) { - if (!askBeforeUpdate.some((pattern) => matchString(pattern, a.filename))) continue; - const answer = await Helper.question( - `The developer decided that the file '${a.filename}' may contain information you changed yourself. Should they be overriden to be updated? [y/N]` - ); - if (answer.toLowerCase() === 'y') remove.push(a.sha); - } - - const changesToApply = await Promise.all( - changedFiles - .filter((a) => !remove.includes(a.sha)) - .map(async (a): Promise<ApplyItem> => { - if (a.filename.endsWith('.ts') || a.filename.endsWith('tsx')) { - const isTSX = a.filename.endsWith('tsx'); - const ret = { - path: a.filename.slice(0, isTSX ? -3 : -2) + `js${isTSX ? 'x' : ''}`, - content: transpileModule(await (await fetch(a.raw_url)).text(), { - compilerOptions: tsConfig.compilerOptions as unknown as CompilerOptions - }).outputText, - type: a.status === 'modified' ? ApplyType.UPDATE : a.status === 'added' ? ApplyType.ADD : ApplyType.DELETE - }; - console.info('✓ Transpiled %s', ret.path); - return ret; - } else { - const ret = { - path: a.filename, - content: await (await fetch(a.raw_url)).text(), - type: a.status === 'modified' ? ApplyType.UPDATE : a.status === 'added' ? ApplyType.ADD : ApplyType.DELETE - }; - console.info('✓ Got %s', ret.path); - return ret; - } - }) - ); - - changesToApply.forEach((a) => { - try { - fsextra.ensureDirSync(path.dirname(a.path)); - fs.writeFileSync(path.join(__dirname, '..', a.path), a.content); - console.info('✓ Written %s', a.path); - } catch (er) { - console.info('✗ Error while writing %s', a.path); - } - }); - - console.info('Done'); - return done(); - } }; function done(newVersion?: string) { - const next = new Date(Date.now() + 1000 * 60 * 60 * 24); - fs.writeFileSync( - updateFilePlace, - JSON.stringify( + const next = new Date(Date.now() + 1000 * 60 * 60 * 24); + fs.writeFileSync( + updateFilePlace, + JSON.stringify( { - lastCheck: Date.now(), - nextCheck: next.getTime() + lastCheck: Date.now(), + nextCheck: next.getTime() } as UpdateFile, null, 2 - ) - ); - if (newVersion) { - fs.writeFileSync( - '../package.json', - JSON.stringify( - { - ...packageJson, - version: newVersion - }, - null, - 4 - ) + ) ); - } - console.info('[INFO] Searching for update finished. Next time running on the ' + next.toLocaleDateString() + ' at ' + next.toLocaleTimeString() + '.'); + if (newVersion) { + fs.writeFileSync( + '../package.json', + JSON.stringify( + { + ...packageJson, + version: newVersion + }, + null, + 4 + ) + ); + } + console.info('[INFO] Searching for update finished. Next time running on the ' + next.toLocaleDateString() + ' at ' + next.toLocaleTimeString() + '.'); } function isNewer(curr: string, compare: string): boolean { - const currParts = curr.split('.').map((a) => parseInt(a)); - const compareParts = compare.split('.').map((a) => parseInt(a)); + const currParts = curr.split('.').map((a) => parseInt(a)); + const compareParts = compare.split('.').map((a) => parseInt(a)); - for (let i = 0; i < Math.max(currParts.length, compareParts.length); i++) { - if (currParts.length <= i) return true; - if (compareParts.length <= i) return false; - if (currParts[i] !== compareParts[i]) return compareParts[i] > currParts[i]; - } + for (let i = 0; i < Math.max(currParts.length, compareParts.length); i++) { + if (currParts.length <= i) return true; + if (compareParts.length <= i) return false; + if (currParts[i] !== compareParts[i]) return compareParts[i] > currParts[i]; + } - return false; + return false; } function matchString(pattern: string, toMatch: string): boolean { - const filter = path.join('..', pattern); - if (pattern.startsWith('*')) { - return toMatch.endsWith(pattern.slice(1)); - } else if (filter.split(path.sep).pop()?.indexOf('.') === -1) { - return toMatch.startsWith(filter); - } else { - return toMatch.split(path.sep).pop() === pattern; - } + const filter = path.join('..', pattern); + if (pattern.startsWith('*')) { + return toMatch.endsWith(pattern.slice(1)); + } else if (filter.split(path.sep).pop()?.indexOf('.') === -1) { + return toMatch.startsWith(filter); + } else { + return toMatch.split(path.sep).pop() === pattern; + } } diff --git a/modules/module.vtt2ass.ts b/modules/module.vtt2ass.ts index e716826..5846b73 100644 --- a/modules/module.vtt2ass.ts +++ b/modules/module.vtt2ass.ts @@ -31,498 +31,498 @@ type Vtt = { }; function loadCSS(cssStr: string): Css { - const css = cssStr.replace(cssPrefixRx, '').replace(/[\r\n]+/g, '\n').split('\n'); - const defaultSFont = rFont == '' ? defaultStyleFont : rFont; - let defaultStyle = `${defaultSFont},40,&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,1,0,2,20,20,20,1`; //base for nonDialog - const styles: Record<string, { + const css = cssStr.replace(cssPrefixRx, '').replace(/[\r\n]+/g, '\n').split('\n'); + const defaultSFont = rFont == '' ? defaultStyleFont : rFont; + let defaultStyle = `${defaultSFont},40,&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,1,0,2,20,20,20,1`; //base for nonDialog + const styles: Record<string, { params: string, list: string[] }> = { [defaultStyleName]: { params: defaultStyle, list: [] } }; - const classList: Record<string, number> = { [defaultStyleName]: 1 }; - for (const i in css) { - let clx, clz, clzx, rgx; - const l = css[i]; - if (l === '') continue; - const m = l.match(/^(.*)\{(.*)\}$/); - if (!m) { - console.error(`VTT2ASS: Invalid css in line ${i}: ${l}`); - continue; - } + const classList: Record<string, number> = { [defaultStyleName]: 1 }; + for (const i in css) { + let clx, clz, clzx, rgx; + const l = css[i]; + if (l === '') continue; + const m = l.match(/^(.*)\{(.*)\}$/); + if (!m) { + console.error(`VTT2ASS: Invalid css in line ${i}: ${l}`); + continue; + } - if (m[1] === '') { - const style = parseStyle(defaultStyleName, m[2], defaultStyle); - styles[defaultStyleName].params = style; - defaultStyle = style; - } else { - clx = m[1].replace(/\./g, '').split(','); - clz = clx[0].replace(/-C(\d+)_(\d+)$/i, '').replace(/-(\d+)$/i, ''); - classList[clz] = (classList[clz] || 0) + 1; - rgx = classList[clz]; - const classSubNum = rgx > 1 ? `-${rgx}` : ''; - clzx = clz + classSubNum; - const style = parseStyle(clzx, m[2], defaultStyle); - styles[clzx] = { params: style, list: clx }; + if (m[1] === '') { + const style = parseStyle(defaultStyleName, m[2], defaultStyle); + styles[defaultStyleName].params = style; + defaultStyle = style; + } else { + clx = m[1].replace(/\./g, '').split(','); + clz = clx[0].replace(/-C(\d+)_(\d+)$/i, '').replace(/-(\d+)$/i, ''); + classList[clz] = (classList[clz] || 0) + 1; + rgx = classList[clz]; + const classSubNum = rgx > 1 ? `-${rgx}` : ''; + clzx = clz + classSubNum; + const style = parseStyle(clzx, m[2], defaultStyle); + styles[clzx] = { params: style, list: clx }; + } } - } - return styles; + return styles; } function parseStyle(stylegroup: string, line: string, style: any) { - const defaultSFont = rFont == '' ? defaultStyleFont : rFont; //redeclare cause of let + const defaultSFont = rFont == '' ? defaultStyleFont : rFont; //redeclare cause of let - if (stylegroup.startsWith('Subtitle') || stylegroup.startsWith('Song') || stylegroup.startsWith('Q') || stylegroup.startsWith('Default')) { //base for dialog, everything else use defaultStyle - style = `${defaultSFont},${fontSize},&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2.6,0,2,20,20,46,1`; - } - - // Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, - // BackColour, Bold, Italic, Underline, StrikeOut, - // ScaleX, ScaleY, Spacing, Angle, BorderStyle, - // Outline, Shadow, Alignment, MarginL, MarginR, - // MarginV, Encoding - style = style.split(','); - for (const s of line.split(';')) { - if (s == '') continue; - const st = s.trim().split(':'); - if (st[0]) st[0] = st[0].trim(); - if (st[1]) st[1] = st[1].trim(); - let cl, arr, transformed_str; - switch (st[0]) { - case 'font-family': - if (rFont != '') { //do rewrite if rFont is specified - if (stylegroup.startsWith('Subtitle') || stylegroup.startsWith('Song')) { - style[0] = rFont; //dialog to rFont - } else { - style[0] = defaultStyleFont; //non-dialog to Arial - } - } else { //otherwise keep default style - style[0] = st[1].match(/[\s"]*([^",]*)/)![1]; - } - break; - case 'font-size': - style[1] = getPxSize(st[1], style[1]); //scale it based on input style size... so for dialog, this is the dialog font size set in config, for non dialog, it's 40 from default font size - break; - case 'color': - cl = getColor(st[1]); - if (cl !== null) { - if (cl == '&H0000FFFF') { - style[2] = style[3] = '&H00FFFFFF'; - } - else { - style[2] = style[3] = cl; - } - } - break; - case 'font-weight': - if (stylegroup.startsWith('Subtitle') || stylegroup.startsWith('Song')) { //don't touch font-weight if dialog - break; - } - // console.info("Changing bold weight"); - // console.info(stylegroup); - if (st[1] === 'bold') { - style[6] = -1; - break; - } - if (st[1] === 'normal') { - break; - } - break; - case 'text-decoration': - if (st[1] === 'underline') { - style[8] = -1; - } else { - console.warn(`vtt2ass: Unknown text-decoration value: ${st[1]}`); - } - break; - case 'right': - style[17] = 3; - break; - case 'left': - style[17] = 1; - break; - case 'font-style': - if (st[1] === 'italic') { - style[7] = -1; - break; - } - break; - case 'background-color': - case 'background': - if (st[1] === 'none') { - break; - } - break; - case 'text-shadow': - if (stylegroup.startsWith('Subtitle') || stylegroup.startsWith('Song')) { //don't touch shadow if dialog - break; - } - arr = transformed_str = st[1].split(',').map(r => r.trim()); - arr = arr.map(r => { return (r.split(' ').length > 3 ? r.replace(/(\d+)px black$/, '') : r.replace(/black$/, '')).trim(); }); - transformed_str[1] = arr.map(r => r.replace(/-/g, '').replace(/px/g, '').replace(/(^| )0( |$)/g, ' ').trim()).join(' '); - arr = transformed_str[1].split(' '); - if (arr.length != 10) { - console.warn(`VTT2ASS: Can't properly parse text-shadow: ${s.trim()}`); - break; - } - arr = [...new Set(arr)]; - if (arr.length > 1) { - console.warn(`VTT2ASS: Can't properly parse text-shadow: ${s.trim()}`); - break; - } - style[16] = arr[0]; - break; - default: - console.error(`VTT2ASS: Unknown style: ${s.trim()}`); + if (stylegroup.startsWith('Subtitle') || stylegroup.startsWith('Song') || stylegroup.startsWith('Q') || stylegroup.startsWith('Default')) { //base for dialog, everything else use defaultStyle + style = `${defaultSFont},${fontSize},&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2.6,0,2,20,20,46,1`; } - } - return style.join(','); + + // Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, + // BackColour, Bold, Italic, Underline, StrikeOut, + // ScaleX, ScaleY, Spacing, Angle, BorderStyle, + // Outline, Shadow, Alignment, MarginL, MarginR, + // MarginV, Encoding + style = style.split(','); + for (const s of line.split(';')) { + if (s == '') continue; + const st = s.trim().split(':'); + if (st[0]) st[0] = st[0].trim(); + if (st[1]) st[1] = st[1].trim(); + let cl, arr, transformed_str; + switch (st[0]) { + case 'font-family': + if (rFont != '') { //do rewrite if rFont is specified + if (stylegroup.startsWith('Subtitle') || stylegroup.startsWith('Song')) { + style[0] = rFont; //dialog to rFont + } else { + style[0] = defaultStyleFont; //non-dialog to Arial + } + } else { //otherwise keep default style + style[0] = st[1].match(/[\s"]*([^",]*)/)![1]; + } + break; + case 'font-size': + style[1] = getPxSize(st[1], style[1]); //scale it based on input style size... so for dialog, this is the dialog font size set in config, for non dialog, it's 40 from default font size + break; + case 'color': + cl = getColor(st[1]); + if (cl !== null) { + if (cl == '&H0000FFFF') { + style[2] = style[3] = '&H00FFFFFF'; + } + else { + style[2] = style[3] = cl; + } + } + break; + case 'font-weight': + if (stylegroup.startsWith('Subtitle') || stylegroup.startsWith('Song')) { //don't touch font-weight if dialog + break; + } + // console.info("Changing bold weight"); + // console.info(stylegroup); + if (st[1] === 'bold') { + style[6] = -1; + break; + } + if (st[1] === 'normal') { + break; + } + break; + case 'text-decoration': + if (st[1] === 'underline') { + style[8] = -1; + } else { + console.warn(`vtt2ass: Unknown text-decoration value: ${st[1]}`); + } + break; + case 'right': + style[17] = 3; + break; + case 'left': + style[17] = 1; + break; + case 'font-style': + if (st[1] === 'italic') { + style[7] = -1; + break; + } + break; + case 'background-color': + case 'background': + if (st[1] === 'none') { + break; + } + break; + case 'text-shadow': + if (stylegroup.startsWith('Subtitle') || stylegroup.startsWith('Song')) { //don't touch shadow if dialog + break; + } + arr = transformed_str = st[1].split(',').map(r => r.trim()); + arr = arr.map(r => { return (r.split(' ').length > 3 ? r.replace(/(\d+)px black$/, '') : r.replace(/black$/, '')).trim(); }); + transformed_str[1] = arr.map(r => r.replace(/-/g, '').replace(/px/g, '').replace(/(^| )0( |$)/g, ' ').trim()).join(' '); + arr = transformed_str[1].split(' '); + if (arr.length != 10) { + console.warn(`VTT2ASS: Can't properly parse text-shadow: ${s.trim()}`); + break; + } + arr = [...new Set(arr)]; + if (arr.length > 1) { + console.warn(`VTT2ASS: Can't properly parse text-shadow: ${s.trim()}`); + break; + } + style[16] = arr[0]; + break; + default: + console.error(`VTT2ASS: Unknown style: ${s.trim()}`); + } + } + return style.join(','); } function getPxSize(size_line: string, font_size: number) { - const m = size_line.trim().match(/([\d.]+)(.*)/); - if (!m) { - console.error(`VTT2ASS: Unknown size: ${size_line}`); - return; - } - let size = parseFloat(m[1]); - if (m[2] === 'em') size *= font_size; - return Math.round(size); + const m = size_line.trim().match(/([\d.]+)(.*)/); + if (!m) { + console.error(`VTT2ASS: Unknown size: ${size_line}`); + return; + } + let size = parseFloat(m[1]); + if (m[2] === 'em') size *= font_size; + return Math.round(size); } function getColor(c: string) { - if (c[0] !== '#') { - c = colors[c as keyof typeof colors]; - } else if (c.length < 7 || c.length > 7) { - c = `#${c[1]}${c[1]}${c[2]}${c[2]}${c[3]}${c[3]}`; - } - const m = c.match(/#(..)(..)(..)/); - if (!m) return null; - return `&H00${m[3]}${m[2]}${m[1]}`.toUpperCase(); + if (c[0] !== '#') { + c = colors[c as keyof typeof colors]; + } else if (c.length < 7 || c.length > 7) { + c = `#${c[1]}${c[1]}${c[2]}${c[2]}${c[3]}${c[3]}`; + } + const m = c.match(/#(..)(..)(..)/); + if (!m) return null; + return `&H00${m[3]}${m[2]}${m[1]}`.toUpperCase(); } function loadVTT(vttStr: string): Vtt[] { - const rx = /^([\d:.]*) --> ([\d:.]*)\s?(.*?)\s*$/; - const lines = vttStr.replace(/\r?\n/g, '\n').split('\n'); - const data = []; - let record: null|Vtt = null; - let lineBuf = []; - for (const l of lines) { - const m = l.match(rx); - if (m) { - let caption = ''; - if (lineBuf.length > 0) { - caption = lineBuf.pop()!; - } - if (caption !== '' && lineBuf.length > 0) { - lineBuf.pop(); - } - if (record !== null) { + const rx = /^([\d:.]*) --> ([\d:.]*)\s?(.*?)\s*$/; + const lines = vttStr.replace(/\r?\n/g, '\n').split('\n'); + const data = []; + let record: null|Vtt = null; + let lineBuf = []; + for (const l of lines) { + const m = l.match(rx); + if (m) { + let caption = ''; + if (lineBuf.length > 0) { + caption = lineBuf.pop()!; + } + if (caption !== '' && lineBuf.length > 0) { + lineBuf.pop(); + } + if (record !== null) { + record.text = lineBuf.join('\n'); + data.push(record); + } + record = { + caption, + time: { + start: m[1], + end: m[2], + ext: m[3].split(' ').map(x => x.split(':')).reduce((p, c) => ((p as any)[c[0]] = c[1] ?? 'invalid-input') && p, {}), + } + }; + lineBuf = []; + continue; + } + lineBuf.push(l); + } + if (record !== null) { + if (lineBuf[lineBuf.length - 1] === '') { + lineBuf.pop(); + } record.text = lineBuf.join('\n'); data.push(record); - } - record = { - caption, - time: { - start: m[1], - end: m[2], - ext: m[3].split(' ').map(x => x.split(':')).reduce((p, c) => ((p as any)[c[0]] = c[1] ?? 'invalid-input') && p, {}), - } - }; - lineBuf = []; - continue; } - lineBuf.push(l); - } - if (record !== null) { - if (lineBuf[lineBuf.length - 1] === '') { - lineBuf.pop(); - } - record.text = lineBuf.join('\n'); - data.push(record); - } - return data; + return data; } function timestampToCentiseconds(timestamp: string) { - const timestamp_split = timestamp.split(':'); - const timestamp_sec_split = timestamp_split[2].split('.'); - const hour = parseInt(timestamp_split[0]); - const minute = parseInt(timestamp_split[1]); - const second = parseInt(timestamp_sec_split[0]); - const centisecond = parseInt(timestamp_sec_split[1]); + const timestamp_split = timestamp.split(':'); + const timestamp_sec_split = timestamp_split[2].split('.'); + const hour = parseInt(timestamp_split[0]); + const minute = parseInt(timestamp_split[1]); + const second = parseInt(timestamp_sec_split[0]); + const centisecond = parseInt(timestamp_sec_split[1]); - return 360000 * hour + 6000 * minute + 100 * second + centisecond; + return 360000 * hour + 6000 * minute + 100 * second + centisecond; } function combineLines(events: string[]): string[] { - if (!doCombineLines) { - return events; - } - // This function is for combining adjacent lines with same information - const newLines: string[] = []; - for (const currentLine of events) { - let hasCombined: boolean = false; - // Check previous 7 elements, arbritary lookback amount - for (let j = 1; j < 8 && j < newLines.length; j++) { - const checkLine = newLines[newLines.length - j]; - const checkLineSplit = checkLine.split(','); - const currentLineSplit = currentLine.split(','); - // 1 = start, 2 = end, 3 = style, 9+ = text - if (checkLineSplit.slice(9).join(',') == currentLineSplit.slice(9).join(',') && + if (!doCombineLines) { + return events; + } + // This function is for combining adjacent lines with same information + const newLines: string[] = []; + for (const currentLine of events) { + let hasCombined: boolean = false; + // Check previous 7 elements, arbritary lookback amount + for (let j = 1; j < 8 && j < newLines.length; j++) { + const checkLine = newLines[newLines.length - j]; + const checkLineSplit = checkLine.split(','); + const currentLineSplit = currentLine.split(','); + // 1 = start, 2 = end, 3 = style, 9+ = text + if (checkLineSplit.slice(9).join(',') == currentLineSplit.slice(9).join(',') && checkLineSplit[3] == currentLineSplit[3] && checkLineSplit[2] == currentLineSplit[1] - ) { - checkLineSplit[2] = currentLineSplit[2]; - newLines[newLines.length - j] = checkLineSplit.join(','); - hasCombined = true; - break; - } + ) { + checkLineSplit[2] = currentLineSplit[2]; + newLines[newLines.length - j] = checkLineSplit.join(','); + hasCombined = true; + break; + } + } + if (!hasCombined) { + newLines.push(currentLine); + } } - if (!hasCombined) { - newLines.push(currentLine); - } - } - return newLines; + return newLines; } function pushBuffer(buffer: ReturnType<typeof convertLine>[], events: string[]) { - buffer.reverse(); - const bufferStrings: string[] = buffer.map(line => - `Dialogue: 1,${line.start},${line.end},${line.style},,0,0,0,,${line.text}`); - events.push(...bufferStrings); - buffer.splice(0,buffer.length); + buffer.reverse(); + const bufferStrings: string[] = buffer.map(line => + `Dialogue: 1,${line.start},${line.end},${line.style},,0,0,0,,${line.text}`); + events.push(...bufferStrings); + buffer.splice(0,buffer.length); } function convert(css: Css, vtt: Vtt[]) { - const stylesMap: Record<string, string> = {}; - let ass = [ - '\ufeff[Script Info]', - 'Title: ' + relGroup, - 'ScriptType: v4.00+', - 'WrapStyle: 0', - 'PlayResX: 1280', - 'PlayResY: 720', - 'ScaledBorderAndShadow: yes', - '', - '[V4+ Styles]', - 'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding', - ]; - for (const s in css) { - ass.push(`Style: ${s},${css[s].params}`); - css[s].list.forEach(x => stylesMap[x] = s); - } - ass = ass.concat([ - '', - '[Events]', - 'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text' - ]); - const events: { + const stylesMap: Record<string, string> = {}; + let ass = [ + '\ufeff[Script Info]', + 'Title: ' + relGroup, + 'ScriptType: v4.00+', + 'WrapStyle: 0', + 'PlayResX: 1280', + 'PlayResY: 720', + 'ScaledBorderAndShadow: yes', + '', + '[V4+ Styles]', + 'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding', + ]; + for (const s in css) { + ass.push(`Style: ${s},${css[s].params}`); + css[s].list.forEach(x => stylesMap[x] = s); + } + ass = ass.concat([ + '', + '[Events]', + 'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text' + ]); + const events: { subtitle: string[], caption: string[], capt_pos: string[], song_cap: string[], } = { - subtitle: [], - caption: [], - capt_pos: [], - song_cap: [], + subtitle: [], + caption: [], + capt_pos: [], + song_cap: [], }; - const linesMap: Record<string, number> = {}; - const buffer: ReturnType<typeof convertLine>[] = []; - const captionsBuffer: string[] = []; - for (const l in vtt) { - const x = convertLine(stylesMap, vtt[l]); - if (x.ind !== '' && linesMap[x.ind] !== undefined) { - if (x.subInd > 1) { - const fx = convertLine(stylesMap, vtt[parseInt(l) - x.subInd + 1]); - if (x.style != fx.style) { - x.text = `{\\r${x.style}}${x.text}{\\r}`; + const linesMap: Record<string, number> = {}; + const buffer: ReturnType<typeof convertLine>[] = []; + const captionsBuffer: string[] = []; + for (const l in vtt) { + const x = convertLine(stylesMap, vtt[l]); + if (x.ind !== '' && linesMap[x.ind] !== undefined) { + if (x.subInd > 1) { + const fx = convertLine(stylesMap, vtt[parseInt(l) - x.subInd + 1]); + if (x.style != fx.style) { + x.text = `{\\r${x.style}}${x.text}{\\r}`; + } + } + events[x.type as keyof typeof events][linesMap[x.ind]] += '\\N' + x.text; } - } - events[x.type as keyof typeof events][linesMap[x.ind]] += '\\N' + x.text; - } - else { - events[x.type as keyof typeof events].push(x.res); - if (x.ind !== '') { - linesMap[x.ind] = events[x.type as keyof typeof events].length - 1; - } - } - /** + else { + events[x.type as keyof typeof events].push(x.res); + if (x.ind !== '') { + linesMap[x.ind] = events[x.type as keyof typeof events].length - 1; + } + } + /** * What cursed code have I brought upon this land? * This handles making lines multi-line when neccesary and reverses * order of subtitles so that they display correctly */ - if (x.type != 'subtitle') { - // Do nothing - } else if (x.text.includes('\\pos')) { - events['subtitle'].pop(); - captionsBuffer.push(x.res); - } else if (buffer.length > 0) { - const previousBufferLine = buffer[buffer.length - 1]; - const previousStart = timestampToCentiseconds(previousBufferLine.start); - const currentStart = timestampToCentiseconds(x.start); - events['subtitle'].pop(); - if ((currentStart - previousStart) <= 2) { - x.start = previousBufferLine.start; - if (previousBufferLine.style == x.style) { - buffer.pop(); - x.text = previousBufferLine.text + '\\N' + x.text; + if (x.type != 'subtitle') { + // Do nothing + } else if (x.text.includes('\\pos')) { + events['subtitle'].pop(); + captionsBuffer.push(x.res); + } else if (buffer.length > 0) { + const previousBufferLine = buffer[buffer.length - 1]; + const previousStart = timestampToCentiseconds(previousBufferLine.start); + const currentStart = timestampToCentiseconds(x.start); + events['subtitle'].pop(); + if ((currentStart - previousStart) <= 2) { + x.start = previousBufferLine.start; + if (previousBufferLine.style == x.style) { + buffer.pop(); + x.text = previousBufferLine.text + '\\N' + x.text; + } + } else { + pushBuffer(buffer, events['subtitle']); + } + buffer.push(x); + } + else { + events['subtitle'].pop(); + buffer.push(x); } - } else { - pushBuffer(buffer, events['subtitle']); - } - buffer.push(x); } - else { - events['subtitle'].pop(); - buffer.push(x); + + pushBuffer(buffer, events['subtitle']); + events['subtitle'].push(...captionsBuffer); + events['subtitle'] = combineLines(events['subtitle']); + + if (events.subtitle.length > 0) { + ass = ass.concat( + //`Comment: 0,0:00:00.00,0:00:00.00,${defaultStyleName},,0,0,0,,** Subtitles **`, + events.subtitle + ); } - } - - pushBuffer(buffer, events['subtitle']); - events['subtitle'].push(...captionsBuffer); - events['subtitle'] = combineLines(events['subtitle']); - - if (events.subtitle.length > 0) { - ass = ass.concat( - //`Comment: 0,0:00:00.00,0:00:00.00,${defaultStyleName},,0,0,0,,** Subtitles **`, - events.subtitle - ); - } - if (events.caption.length > 0) { - ass = ass.concat( - //`Comment: 0,0:00:00.00,0:00:00.00,${defaultStyleName},,0,0,0,,** Captions **`, - events.caption - ); - } - if (events.capt_pos.length > 0) { - ass = ass.concat( - //`Comment: 0,0:00:00.00,0:00:00.00,${defaultStyleName},,0,0,0,,** Captions with position **`, - events.capt_pos - ); - } - if (events.song_cap.length > 0) { - ass = ass.concat( - //`Comment: 0,0:00:00.00,0:00:00.00,${defaultStyleName},,0,0,0,,** Song captions **`, - events.song_cap - ); - } - return ass.join('\r\n') + '\r\n'; + if (events.caption.length > 0) { + ass = ass.concat( + //`Comment: 0,0:00:00.00,0:00:00.00,${defaultStyleName},,0,0,0,,** Captions **`, + events.caption + ); + } + if (events.capt_pos.length > 0) { + ass = ass.concat( + //`Comment: 0,0:00:00.00,0:00:00.00,${defaultStyleName},,0,0,0,,** Captions with position **`, + events.capt_pos + ); + } + if (events.song_cap.length > 0) { + ass = ass.concat( + //`Comment: 0,0:00:00.00,0:00:00.00,${defaultStyleName},,0,0,0,,** Song captions **`, + events.song_cap + ); + } + return ass.join('\r\n') + '\r\n'; } function convertLine(css: Record<string, string>, l: Record<any, any>) { - const start = convertTime(l.time.start); - const end = convertTime(l.time.end); - const txt = convertText(l.text); - let type = txt.style.match(/Caption/i) ? 'caption' : (txt.style.match(/SongCap/i) ? 'song_cap' : 'subtitle'); - type = type == 'caption' && l.time.ext?.position !== undefined ? 'capt_pos' : type; - if (l.time.ext?.align === 'left') { - txt.text = `{\\an7}${txt.text}`; - } - let ind = '', subInd = 1; - const sMinus = 0; // (19.2 * 2); - if (l.time.ext?.position !== undefined) { - const pos = parseInt(l.time.ext.position); - const PosX = pos < 0 ? (1280 / 100 * (100 - pos)) : ((1280 - sMinus) / 100 * pos); - const line = parseInt(l.time.ext.line) || 0; - const PosY = line < 0 ? (720 / 100 * (100 - line)) : ((720 - sMinus) / 100 * line); - txt.text = `{\\pos(${parseFloat(PosX.toFixed(3))},${parseFloat(PosY.toFixed(3))})}${txt.text}`; - } - else if (l.time.ext?.line !== undefined && type == 'caption') { - const line = parseInt(l.time.ext.line); - const PosY = line < 0 ? (720 / 100 * (100 - line)) : ((720 - sMinus) / 100 * line); - txt.text = `{\\pos(640,${parseFloat(PosY.toFixed(3))})}${txt.text}`; - } - else { - const indregx = txt.style.match(/(.*)_(\d+)$/); - if (indregx !== null) { - ind = indregx[1]; - subInd = parseInt(indregx[2]); + const start = convertTime(l.time.start); + const end = convertTime(l.time.end); + const txt = convertText(l.text); + let type = txt.style.match(/Caption/i) ? 'caption' : (txt.style.match(/SongCap/i) ? 'song_cap' : 'subtitle'); + type = type == 'caption' && l.time.ext?.position !== undefined ? 'capt_pos' : type; + if (l.time.ext?.align === 'left') { + txt.text = `{\\an7}${txt.text}`; } - } - const style = css[txt.style as any] || defaultStyleName; - const res = `Dialogue: 0,${start},${end},${style},,0,0,0,,${txt.text}`; - return { type, ind, subInd, start, end, style, text: txt.text, res }; + let ind = '', subInd = 1; + const sMinus = 0; // (19.2 * 2); + if (l.time.ext?.position !== undefined) { + const pos = parseInt(l.time.ext.position); + const PosX = pos < 0 ? (1280 / 100 * (100 - pos)) : ((1280 - sMinus) / 100 * pos); + const line = parseInt(l.time.ext.line) || 0; + const PosY = line < 0 ? (720 / 100 * (100 - line)) : ((720 - sMinus) / 100 * line); + txt.text = `{\\pos(${parseFloat(PosX.toFixed(3))},${parseFloat(PosY.toFixed(3))})}${txt.text}`; + } + else if (l.time.ext?.line !== undefined && type == 'caption') { + const line = parseInt(l.time.ext.line); + const PosY = line < 0 ? (720 / 100 * (100 - line)) : ((720 - sMinus) / 100 * line); + txt.text = `{\\pos(640,${parseFloat(PosY.toFixed(3))})}${txt.text}`; + } + else { + const indregx = txt.style.match(/(.*)_(\d+)$/); + if (indregx !== null) { + ind = indregx[1]; + subInd = parseInt(indregx[2]); + } + } + const style = css[txt.style as any] || defaultStyleName; + const res = `Dialogue: 0,${start},${end},${style},,0,0,0,,${txt.text}`; + return { type, ind, subInd, start, end, style, text: txt.text, res }; } function convertText(text: string) { - //const m = text.match(/<c\.([^>]*)>([\S\s]*)<\/c>/); - const m = text.match(/<(?:c\.|)([^>]*)>([\S\s]*)<\/(?:c|Default)>/); - let style = ''; - if (m) { - style = m[1]; - text = m[2]; - } - const xtext = text + //const m = text.match(/<c\.([^>]*)>([\S\s]*)<\/c>/); + const m = text.match(/<(?:c\.|)([^>]*)>([\S\s]*)<\/(?:c|Default)>/); + let style = ''; + if (m) { + style = m[1]; + text = m[2]; + } + const xtext = text // .replace(/<c[^>]*>[^<]*<\/c>/g, '') // .replace(/<ruby[^>]*>[^<]*<\/ruby>/g, '') - .replace(/ \\N$/g, '\\N') + .replace(/ \\N$/g, '\\N') //.replace(/<[^>]>/g, '') - .replace(/\\N$/, '') - .replace(/\r/g, '') - .replace(/\n/g, '\\N') - .replace(/\\N +/g, '\\N') - .replace(/ +\\N/g, '\\N') - .replace(/(\\N)+/g, '\\N') - .replace(/<b[^>]*>([^<]*)<\/b>/g, '{\\b1}$1{\\b0}') - .replace(/<i[^>]*>([^<]*)<\/i>/g, '{\\i1}$1{\\i0}') - .replace(/<u[^>]*>([^<]*)<\/u>/g, '{\\u1}$1{\\u0}') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/&/g, '&') - .replace(/<[^>]>/g, '') - .replace(/\\N$/, '') - .replace(/ +$/, ''); - text = xtext; - return { style, text }; + .replace(/\\N$/, '') + .replace(/\r/g, '') + .replace(/\n/g, '\\N') + .replace(/\\N +/g, '\\N') + .replace(/ +\\N/g, '\\N') + .replace(/(\\N)+/g, '\\N') + .replace(/<b[^>]*>([^<]*)<\/b>/g, '{\\b1}$1{\\b0}') + .replace(/<i[^>]*>([^<]*)<\/i>/g, '{\\i1}$1{\\i0}') + .replace(/<u[^>]*>([^<]*)<\/u>/g, '{\\u1}$1{\\u0}') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&') + .replace(/<[^>]>/g, '') + .replace(/\\N$/, '') + .replace(/ +$/, ''); + text = xtext; + return { style, text }; } function convertTime(tm: string) { - const m = tm.match(/([\d:]*)\.?(\d*)/); - if (!m) return '0:00:00.00'; - return toSubTime(m[0]); + const m = tm.match(/([\d:]*)\.?(\d*)/); + if (!m) return '0:00:00.00'; + return toSubTime(m[0]); } function toSubTime(str: string) { - const n = []; - let sx; - const x: any[] = str.split(/[:.]/).map(x => Number(x)); - x[3] = '0.' + ('00' + x[3]).slice(-3); - sx = (x[0] * 60 * 60 + x[1] * 60 + x[2] + Number(x[3]) - tmMrg).toFixed(2); - sx = sx.toString().split('.'); - n.unshift(sx[1]); - sx = Number(sx[0]); - n.unshift(('0' + ((sx % 60).toString())).slice(-2)); - n.unshift(('0' + ((Math.floor(sx / 60) % 60).toString())).slice(-2)); - n.unshift((Math.floor(sx / 3600) % 60).toString()); - return n.slice(0, 3).join(':') + '.' + n[3]; + const n = []; + let sx; + const x: any[] = str.split(/[:.]/).map(x => Number(x)); + x[3] = '0.' + ('00' + x[3]).slice(-3); + sx = (x[0] * 60 * 60 + x[1] * 60 + x[2] + Number(x[3]) - tmMrg).toFixed(2); + sx = sx.toString().split('.'); + n.unshift(sx[1]); + sx = Number(sx[0]); + n.unshift(('0' + ((sx % 60).toString())).slice(-2)); + n.unshift(('0' + ((Math.floor(sx / 60) % 60).toString())).slice(-2)); + n.unshift((Math.floor(sx / 3600) % 60).toString()); + return n.slice(0, 3).join(':') + '.' + n[3]; } export default function vtt2ass(group: string | undefined, xFontSize: number | undefined, vttStr: string, cssStr: string, timeMargin?: number, replaceFont?: string, combineLines?: boolean) { - relGroup = group ?? ''; - fontSize = xFontSize && xFontSize > 0 ? xFontSize : 34; // 1em to pix - tmMrg = timeMargin ? timeMargin : 0; // - rFont = replaceFont ? replaceFont : rFont; - doCombineLines = combineLines ? combineLines : doCombineLines; - if (vttStr.match(/::cue(?:.(.+)\) *)?{([^}]+)}/g)) { - const cssLines = []; - let defaultCss = ''; - const cssGroups = vttStr.matchAll(/::cue(?:.(.+)\) *)?{([^}]+)}/g); - for (const cssGroup of cssGroups) { - //Below code will bulldoze defined sizes for custom ones - /*if (!options.originalFontSize) { + relGroup = group ?? ''; + fontSize = xFontSize && xFontSize > 0 ? xFontSize : 34; // 1em to pix + tmMrg = timeMargin ? timeMargin : 0; // + rFont = replaceFont ? replaceFont : rFont; + doCombineLines = combineLines ? combineLines : doCombineLines; + if (vttStr.match(/::cue(?:.(.+)\) *)?{([^}]+)}/g)) { + const cssLines = []; + let defaultCss = ''; + const cssGroups = vttStr.matchAll(/::cue(?:.(.+)\) *)?{([^}]+)}/g); + for (const cssGroup of cssGroups) { + //Below code will bulldoze defined sizes for custom ones + /*if (!options.originalFontSize) { cssGroup[2] = cssGroup[2].replace(/( font-size:.+?;)/g, '').replace(/(font-size:.+?;)/g, ''); }*/ - if (cssGroup[1]) { - cssLines.push(`${cssGroup[1]}{${defaultCss}${cssGroup[2].replace(/(\r\n|\n|\r)/gm, '')}}`); - } else { - defaultCss = cssGroup[2].replace(/(\r\n|\n|\r)/gm, ''); - //cssLines.push(`{${defaultCss}}`); - } + if (cssGroup[1]) { + cssLines.push(`${cssGroup[1]}{${defaultCss}${cssGroup[2].replace(/(\r\n|\n|\r)/gm, '')}}`); + } else { + defaultCss = cssGroup[2].replace(/(\r\n|\n|\r)/gm, ''); + //cssLines.push(`{${defaultCss}}`); + } + } + cssStr += cssLines.join('\r\n'); } - cssStr += cssLines.join('\r\n'); - } - return convert( - loadCSS(cssStr), - loadVTT(vttStr) - ); + return convert( + loadCSS(cssStr), + loadVTT(vttStr) + ); } diff --git a/modules/module.vttconvert.ts b/modules/module.vttconvert.ts index 4226049..9d53b97 100644 --- a/modules/module.vttconvert.ts +++ b/modules/module.vttconvert.ts @@ -8,167 +8,167 @@ export type Record = { export type NullRecord = Record | null; function loadVtt(vttStr: string) { - const rx = /^([\d:.]*) --> ([\d:.]*)\s?(.*?)\s*$/; - const lines = vttStr.replace(/\r?\n/g, '\n').split('\n'); - const data: Record[] = []; let lineBuf: string[] = [], record: NullRecord = null; - // check lines - for (const l of lines) { - const m = l.match(rx); - if (m) { - if (lineBuf.length > 0) { - lineBuf.pop(); - } - if (record !== null) { + const rx = /^([\d:.]*) --> ([\d:.]*)\s?(.*?)\s*$/; + const lines = vttStr.replace(/\r?\n/g, '\n').split('\n'); + const data: Record[] = []; let lineBuf: string[] = [], record: NullRecord = null; + // check lines + for (const l of lines) { + const m = l.match(rx); + if (m) { + if (lineBuf.length > 0) { + lineBuf.pop(); + } + if (record !== null) { + record.text = lineBuf.join('\n'); + data.push(record); + } + record = { + time_start: m[1], + time_end: m[2], + ext_param: m[3].split(' ').map(x => x.split(':')).reduce((p: any, c: any) => (p[c[0]] = c[1]) && p, {}), + }; + lineBuf = []; + continue; + } + lineBuf.push(l); + } + if (record !== null) { + if (lineBuf[lineBuf.length - 1] === '') { + lineBuf.pop(); + } record.text = lineBuf.join('\n'); data.push(record); - } - record = { - time_start: m[1], - time_end: m[2], - ext_param: m[3].split(' ').map(x => x.split(':')).reduce((p: any, c: any) => (p[c[0]] = c[1]) && p, {}), - }; - lineBuf = []; - continue; } - lineBuf.push(l); - } - if (record !== null) { - if (lineBuf[lineBuf.length - 1] === '') { - lineBuf.pop(); - } - record.text = lineBuf.join('\n'); - data.push(record); - } - return data; + return data; } // ass specific function convertToAss(vttStr: string, lang: string, fontSize: number, fontName?: string){ - let ass = [ - '\ufeff[Script Info]', - `Title: ${lang}`, - 'ScriptType: v4.00+', - 'PlayResX: 1280', - 'PlayResY: 720', - 'WrapStyle: 0', - 'ScaledBorderAndShadow: yes', - '', - '[V4+ Styles]', - 'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, ' + let ass = [ + '\ufeff[Script Info]', + `Title: ${lang}`, + 'ScriptType: v4.00+', + 'PlayResX: 1280', + 'PlayResY: 720', + 'WrapStyle: 0', + 'ScaledBorderAndShadow: yes', + '', + '[V4+ Styles]', + 'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, ' + 'Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, ' + 'BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding', - `Style: Main,${fontName || 'Noto Sans'},${fontSize},&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,10,10,10,1`, - `Style: MainTop,${fontName || 'Noto Sans'},${fontSize},&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,3,0,8,10,10,10,1`, - '', - '[Events]', - 'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text', - ]; + `Style: Main,${fontName || 'Noto Sans'},${fontSize},&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,10,10,10,1`, + `Style: MainTop,${fontName || 'Noto Sans'},${fontSize},&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,3,0,8,10,10,10,1`, + '', + '[Events]', + 'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text', + ]; - const vttData = loadVtt(vttStr); - for (const l of vttData) { - const line = convertToAssLine(l, 'Main'); - ass = ass.concat(line); - } + const vttData = loadVtt(vttStr); + for (const l of vttData) { + const line = convertToAssLine(l, 'Main'); + ass = ass.concat(line); + } - return ass.join('\r\n') + '\r\n'; + return ass.join('\r\n') + '\r\n'; } function convertToAssLine(l: Record, style: string) { - const start = convertTime(l.time_start as string); - const end = convertTime(l.time_end as string); - const text = convertToAssText(l.text as string); + const start = convertTime(l.time_start as string); + const end = convertTime(l.time_end as string); + const text = convertToAssText(l.text as string); - // debugger - if ((l.ext_param as any).line === '7%') { - style = 'MainTop'; - } + // debugger + if ((l.ext_param as any).line === '7%') { + style = 'MainTop'; + } - if ((l.ext_param as any).line === '10%') { - style = 'MainTop'; - } + if ((l.ext_param as any).line === '10%') { + style = 'MainTop'; + } - return `Dialogue: 0,${start},${end},${style},,0,0,0,,${text}`; + return `Dialogue: 0,${start},${end},${style},,0,0,0,,${text}`; } function convertToAssText(text: string) { - text = text - .replace(/\r/g, '') - .replace(/\n/g, '\\N') - .replace(/\\N +/g, '\\N') - .replace(/ +\\N/g, '\\N') - .replace(/(\\N)+/g, '\\N') - .replace(/<b[^>]*>([^<]*)<\/b>/g, '{\\b1}$1{\\b0}') - .replace(/<i[^>]*>([^<]*)<\/i>/g, '{\\i1}$1{\\i0}') - .replace(/<u[^>]*>([^<]*)<\/u>/g, '{\\u1}$1{\\u0}') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/&/g, '&') - .replace(/<[^>]>/g, '') - .replace(/\\N$/, '') - .replace(/ +$/, ''); - return text; + text = text + .replace(/\r/g, '') + .replace(/\n/g, '\\N') + .replace(/\\N +/g, '\\N') + .replace(/ +\\N/g, '\\N') + .replace(/(\\N)+/g, '\\N') + .replace(/<b[^>]*>([^<]*)<\/b>/g, '{\\b1}$1{\\b0}') + .replace(/<i[^>]*>([^<]*)<\/i>/g, '{\\i1}$1{\\i0}') + .replace(/<u[^>]*>([^<]*)<\/u>/g, '{\\u1}$1{\\u0}') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&') + .replace(/<[^>]>/g, '') + .replace(/\\N$/, '') + .replace(/ +$/, ''); + return text; } // srt specific function convertToSrt(vttStr: string){ - let srt: string[] = [], srtLineIdx = 0; + let srt: string[] = [], srtLineIdx = 0; - const vttData = loadVtt(vttStr); - for (const l of vttData) { - srtLineIdx++; - const line = convertToSrtLine(l, srtLineIdx); - srt = srt.concat(line); - } + const vttData = loadVtt(vttStr); + for (const l of vttData) { + srtLineIdx++; + const line = convertToSrtLine(l, srtLineIdx); + srt = srt.concat(line); + } - return srt.join('\r\n') + '\r\n'; + return srt.join('\r\n') + '\r\n'; } function convertToSrtLine(l: Record, idx: number) : string { - const bom = idx == 1 ? '\ufeff' : ''; - const start = convertTime(l.time_start as string, true); - const end = convertTime(l.time_end as string, true); - const text = l.text; - return `${bom}${idx}\r\n${start} --> ${end}\r\n${text}\r\n`; + const bom = idx == 1 ? '\ufeff' : ''; + const start = convertTime(l.time_start as string, true); + const end = convertTime(l.time_end as string, true); + const text = l.text; + return `${bom}${idx}\r\n${start} --> ${end}\r\n${text}\r\n`; } // time parser function convertTime(time: string, srtFormat = false) { - const mTime = time.match(/([\d:]*)\.?(\d*)/); - if (!mTime){ - return srtFormat ? '00:00:00,000' : '0:00:00.00'; - } - return toSubsTime(mTime[0], srtFormat); + const mTime = time.match(/([\d:]*)\.?(\d*)/); + if (!mTime){ + return srtFormat ? '00:00:00,000' : '0:00:00.00'; + } + return toSubsTime(mTime[0], srtFormat); } function toSubsTime(str: string, srtFormat: boolean) : string { - const n: string[] = [], x: (string|number)[] = str.split(/[:.]/).map(x => Number(x)); let sx; + const n: string[] = [], x: (string|number)[] = str.split(/[:.]/).map(x => Number(x)); let sx; - const msLen = srtFormat ? 3 : 2; - const hLen = srtFormat ? 2 : 1; + const msLen = srtFormat ? 3 : 2; + const hLen = srtFormat ? 2 : 1; - x[3] = '0.' + ('' + x[3]).padStart(3, '0'); - sx = (x[0] as number)*60*60 + (x[1] as number)*60 + (x[2] as number) + Number(x[3]); - sx = sx.toFixed(msLen).split('.'); + x[3] = '0.' + ('' + x[3]).padStart(3, '0'); + sx = (x[0] as number)*60*60 + (x[1] as number)*60 + (x[2] as number) + Number(x[3]); + sx = sx.toFixed(msLen).split('.'); - n.unshift(padTimeNum((srtFormat ? ',' : '.'), sx[1], msLen)); - sx = Number(sx[0]); + n.unshift(padTimeNum((srtFormat ? ',' : '.'), sx[1], msLen)); + sx = Number(sx[0]); - n.unshift(padTimeNum(':', sx%60, 2)); - n.unshift(padTimeNum(':', Math.floor(sx/60)%60, 2)); - n.unshift(padTimeNum('', Math.floor(sx/3600)%60, hLen)); + n.unshift(padTimeNum(':', sx%60, 2)); + n.unshift(padTimeNum(':', Math.floor(sx/60)%60, 2)); + n.unshift(padTimeNum('', Math.floor(sx/3600)%60, hLen)); - return n.join(''); + return n.join(''); } function padTimeNum(sep: string, input: string|number , pad:number){ - return sep + ('' + input).padStart(pad, '0'); + return sep + ('' + input).padStart(pad, '0'); } // export module const _default = (vttStr: string, toSrt: boolean, lang = 'English', fontSize: number, fontName?: string) => { - const convert = toSrt ? convertToSrt : convertToAss; - return convert(vttStr, lang, fontSize, fontName); + const convert = toSrt ? convertToSrt : convertToAss; + return convert(vttStr, lang, fontSize, fontName); }; export default _default; diff --git a/modules/playready/bcert.ts b/modules/playready/bcert.ts index 886e2a0..7f85ab4 100644 --- a/modules/playready/bcert.ts +++ b/modules/playready/bcert.ts @@ -5,474 +5,474 @@ import ECCKey from './ecc_key'; import { console } from '../log'; function alignUp(length: number, alignment: number): number { - return Math.ceil(length / alignment) * alignment; + return Math.ceil(length / alignment) * alignment; } export class BCertStructs { - static DrmBCertBasicInfo = new Parser() - .buffer('cert_id', { length: 16 }) - .uint32be('security_level') - .uint32be('flags') - .uint32be('cert_type') - .buffer('public_key_digest', { length: 32 }) - .uint32be('expiration_date') - .buffer('client_id', { length: 16 }); + static DrmBCertBasicInfo = new Parser() + .buffer('cert_id', { length: 16 }) + .uint32be('security_level') + .uint32be('flags') + .uint32be('cert_type') + .buffer('public_key_digest', { length: 32 }) + .uint32be('expiration_date') + .buffer('client_id', { length: 16 }); - static DrmBCertDomainInfo = new Parser() - .buffer('service_id', { length: 16 }) - .buffer('account_id', { length: 16 }) - .uint32be('revision_timestamp') - .uint32be('domain_url_length') - .buffer('domain_url', { - length: function () { - return alignUp((this as any).domain_url_length, 4); - }, - }); + static DrmBCertDomainInfo = new Parser() + .buffer('service_id', { length: 16 }) + .buffer('account_id', { length: 16 }) + .uint32be('revision_timestamp') + .uint32be('domain_url_length') + .buffer('domain_url', { + length: function () { + return alignUp((this as any).domain_url_length, 4); + }, + }); - static DrmBCertPCInfo = new Parser().uint32be('security_version'); + static DrmBCertPCInfo = new Parser().uint32be('security_version'); - static DrmBCertDeviceInfo = new Parser() - .uint32be('max_license') - .uint32be('max_header') - .uint32be('max_chain_depth'); + static DrmBCertDeviceInfo = new Parser() + .uint32be('max_license') + .uint32be('max_header') + .uint32be('max_chain_depth'); - static DrmBCertFeatureInfo = new Parser() - .uint32be('feature_count') - .array('features', { - type: 'uint32be', - length: 'feature_count', - }); + static DrmBCertFeatureInfo = new Parser() + .uint32be('feature_count') + .array('features', { + type: 'uint32be', + length: 'feature_count', + }); - static CertKey = new Parser() - .uint16be('type') - .uint16be('length') - .uint32be('flags') - .buffer('key', { - length: function () { - return (this as any).length / 8; - }, - }) - .uint32be('usages_count') - .array('usages', { - type: 'uint32be', - length: 'usages_count', - }); + static CertKey = new Parser() + .uint16be('type') + .uint16be('length') + .uint32be('flags') + .buffer('key', { + length: function () { + return (this as any).length / 8; + }, + }) + .uint32be('usages_count') + .array('usages', { + type: 'uint32be', + length: 'usages_count', + }); - static DrmBCertKeyInfo = new Parser() - .uint32be('key_count') - .array('cert_keys', { - type: BCertStructs.CertKey, - length: 'key_count', - }); + static DrmBCertKeyInfo = new Parser() + .uint32be('key_count') + .array('cert_keys', { + type: BCertStructs.CertKey, + length: 'key_count', + }); - static DrmBCertManufacturerInfo = new Parser() - .uint32be('flags') - .uint32be('manufacturer_name_length') - .buffer('manufacturer_name', { - length: function () { - return alignUp((this as any).manufacturer_name_length, 4); - }, - }) - .uint32be('model_name_length') - .buffer('model_name', { - length: function () { - return alignUp((this as any).model_name_length, 4); - }, - }) - .uint32be('model_number_length') - .buffer('model_number', { - length: function () { - return alignUp((this as any).model_number_length, 4); - }, - }); + static DrmBCertManufacturerInfo = new Parser() + .uint32be('flags') + .uint32be('manufacturer_name_length') + .buffer('manufacturer_name', { + length: function () { + return alignUp((this as any).manufacturer_name_length, 4); + }, + }) + .uint32be('model_name_length') + .buffer('model_name', { + length: function () { + return alignUp((this as any).model_name_length, 4); + }, + }) + .uint32be('model_number_length') + .buffer('model_number', { + length: function () { + return alignUp((this as any).model_number_length, 4); + }, + }); - static DrmBCertSignatureInfo = new Parser() - .uint16be('signature_type') - .uint16be('signature_size') - .buffer('signature', { length: 'signature_size' }) - .uint32be('signature_key_size') - .buffer('signature_key', { - length: function () { - return (this as any).signature_key_size / 8; - }, - }); + static DrmBCertSignatureInfo = new Parser() + .uint16be('signature_type') + .uint16be('signature_size') + .buffer('signature', { length: 'signature_size' }) + .uint32be('signature_key_size') + .buffer('signature_key', { + length: function () { + return (this as any).signature_key_size / 8; + }, + }); - static DrmBCertSilverlightInfo = new Parser() - .uint32be('security_version') - .uint32be('platform_identifier'); + static DrmBCertSilverlightInfo = new Parser() + .uint32be('security_version') + .uint32be('platform_identifier'); - static DrmBCertMeteringInfo = new Parser() - .buffer('metering_id', { length: 16 }) - .uint32be('metering_url_length') - .buffer('metering_url', { - length: function () { - return alignUp((this as any).metering_url_length, 4); - }, - }); + static DrmBCertMeteringInfo = new Parser() + .buffer('metering_id', { length: 16 }) + .uint32be('metering_url_length') + .buffer('metering_url', { + length: function () { + return alignUp((this as any).metering_url_length, 4); + }, + }); - static DrmBCertExtDataSignKeyInfo = new Parser() - .uint16be('key_type') - .uint16be('key_length') - .uint32be('flags') - .buffer('key', { - length: function () { - return (this as any).length / 8; - }, - }); + static DrmBCertExtDataSignKeyInfo = new Parser() + .uint16be('key_type') + .uint16be('key_length') + .uint32be('flags') + .buffer('key', { + length: function () { + return (this as any).length / 8; + }, + }); - static BCertExtDataRecord = new Parser() - .uint32be('data_size') - .buffer('data', { - length: 'data_size', - }); + static BCertExtDataRecord = new Parser() + .uint32be('data_size') + .buffer('data', { + length: 'data_size', + }); - static DrmBCertExtDataSignature = new Parser() - .uint16be('signature_type') - .uint16be('signature_size') - .buffer('signature', { - length: 'signature_size', - }); + static DrmBCertExtDataSignature = new Parser() + .uint16be('signature_type') + .uint16be('signature_size') + .buffer('signature', { + length: 'signature_size', + }); - static BCertExtDataContainer = new Parser() - .uint32be('record_count') - .array('records', { - length: 'record_count', - type: BCertStructs.BCertExtDataRecord, - }) - .nest('signature', { - type: BCertStructs.DrmBCertExtDataSignature, - }); + static BCertExtDataContainer = new Parser() + .uint32be('record_count') + .array('records', { + length: 'record_count', + type: BCertStructs.BCertExtDataRecord, + }) + .nest('signature', { + type: BCertStructs.DrmBCertExtDataSignature, + }); - static DrmBCertServerInfo = new Parser().uint32be('warning_days'); + static DrmBCertServerInfo = new Parser().uint32be('warning_days'); - static DrmBcertSecurityVersion = new Parser() - .uint32be('security_version') - .uint32be('platform_identifier'); + static DrmBcertSecurityVersion = new Parser() + .uint32be('security_version') + .uint32be('platform_identifier'); - static Attribute = new Parser() - .uint16be('flags') - .uint16be('tag') - .uint32be('length') - .choice('attribute', { - tag: 'tag', - choices: { - 1: BCertStructs.DrmBCertBasicInfo, - 2: BCertStructs.DrmBCertDomainInfo, - 3: BCertStructs.DrmBCertPCInfo, - 4: BCertStructs.DrmBCertDeviceInfo, - 5: BCertStructs.DrmBCertFeatureInfo, - 6: BCertStructs.DrmBCertKeyInfo, - 7: BCertStructs.DrmBCertManufacturerInfo, - 8: BCertStructs.DrmBCertSignatureInfo, - 9: BCertStructs.DrmBCertSilverlightInfo, - 10: BCertStructs.DrmBCertMeteringInfo, - 11: BCertStructs.DrmBCertExtDataSignKeyInfo, - 12: BCertStructs.BCertExtDataContainer, - 13: BCertStructs.DrmBCertExtDataSignature, - 14: new Parser().buffer('data', { - length: function () { - return (this as any).length - 8; - }, - }), - 15: BCertStructs.DrmBCertServerInfo, - 16: BCertStructs.DrmBcertSecurityVersion, - 17: BCertStructs.DrmBcertSecurityVersion, - }, - defaultChoice: new Parser().buffer('data', { - length: function () { - return (this as any).length - 8; - }, - }), - }); + static Attribute = new Parser() + .uint16be('flags') + .uint16be('tag') + .uint32be('length') + .choice('attribute', { + tag: 'tag', + choices: { + 1: BCertStructs.DrmBCertBasicInfo, + 2: BCertStructs.DrmBCertDomainInfo, + 3: BCertStructs.DrmBCertPCInfo, + 4: BCertStructs.DrmBCertDeviceInfo, + 5: BCertStructs.DrmBCertFeatureInfo, + 6: BCertStructs.DrmBCertKeyInfo, + 7: BCertStructs.DrmBCertManufacturerInfo, + 8: BCertStructs.DrmBCertSignatureInfo, + 9: BCertStructs.DrmBCertSilverlightInfo, + 10: BCertStructs.DrmBCertMeteringInfo, + 11: BCertStructs.DrmBCertExtDataSignKeyInfo, + 12: BCertStructs.BCertExtDataContainer, + 13: BCertStructs.DrmBCertExtDataSignature, + 14: new Parser().buffer('data', { + length: function () { + return (this as any).length - 8; + }, + }), + 15: BCertStructs.DrmBCertServerInfo, + 16: BCertStructs.DrmBcertSecurityVersion, + 17: BCertStructs.DrmBcertSecurityVersion, + }, + defaultChoice: new Parser().buffer('data', { + length: function () { + return (this as any).length - 8; + }, + }), + }); - static BCert = new Parser() - .string('signature', { length: 4, assert: 'CERT' }) - .int32be('version') - .int32be('total_length') - .int32be('certificate_length') - .array('attributes', { - type: BCertStructs.Attribute, - lengthInBytes: function () { - return (this as any).total_length - 16; - }, - }); + static BCert = new Parser() + .string('signature', { length: 4, assert: 'CERT' }) + .int32be('version') + .int32be('total_length') + .int32be('certificate_length') + .array('attributes', { + type: BCertStructs.Attribute, + lengthInBytes: function () { + return (this as any).total_length - 16; + }, + }); - static BCertChain = new Parser() - .string('signature', { length: 4, assert: 'CHAI' }) - .int32be('version') - .int32be('total_length') - .int32be('flags') - .int32be('certificate_count') - .array('certificates', { - type: BCertStructs.BCert, - length: 'certificate_count', - }); + static BCertChain = new Parser() + .string('signature', { length: 4, assert: 'CHAI' }) + .int32be('version') + .int32be('total_length') + .int32be('flags') + .int32be('certificate_count') + .array('certificates', { + type: BCertStructs.BCert, + length: 'certificate_count', + }); } export class Certificate { - parsed: any; - _BCERT: Parser; + parsed: any; + _BCERT: Parser; - constructor(parsed_bcert: any, bcert_obj: Parser = BCertStructs.BCert) { - this.parsed = parsed_bcert; - this._BCERT = bcert_obj; - } + constructor(parsed_bcert: any, bcert_obj: Parser = BCertStructs.BCert) { + this.parsed = parsed_bcert; + this._BCERT = bcert_obj; + } - // UNSTABLE - static new_leaf_cert( - cert_id: Buffer, - security_level: number, - client_id: Buffer, - signing_key: ECCKey, - encryption_key: ECCKey, - group_key: ECCKey, - parent: CertificateChain, - expiry: number = 0xffffffff, - max_license: number = 10240, - max_header: number = 15360, - max_chain_depth: number = 2 - ): Certificate { - const basic_info = { - cert_id: cert_id, - security_level: security_level, - flags: 0, - cert_type: 2, - public_key_digest: signing_key.publicSha256Digest(), - expiration_date: expiry, - client_id: client_id, - }; - const basic_info_attribute = { - flags: 1, - tag: 1, - length: BCertStructs.DrmBCertBasicInfo.encode(basic_info).length + 8, - attribute: basic_info, - }; + // UNSTABLE + static new_leaf_cert( + cert_id: Buffer, + security_level: number, + client_id: Buffer, + signing_key: ECCKey, + encryption_key: ECCKey, + group_key: ECCKey, + parent: CertificateChain, + expiry: number = 0xffffffff, + max_license: number = 10240, + max_header: number = 15360, + max_chain_depth: number = 2 + ): Certificate { + const basic_info = { + cert_id: cert_id, + security_level: security_level, + flags: 0, + cert_type: 2, + public_key_digest: signing_key.publicSha256Digest(), + expiration_date: expiry, + client_id: client_id, + }; + const basic_info_attribute = { + flags: 1, + tag: 1, + length: BCertStructs.DrmBCertBasicInfo.encode(basic_info).length + 8, + attribute: basic_info, + }; - const device_info = { - max_license: max_license, - max_header: max_header, - max_chain_depth: max_chain_depth, - }; + const device_info = { + max_license: max_license, + max_header: max_header, + max_chain_depth: max_chain_depth, + }; - const device_info_attribute = { - flags: 1, - tag: 4, - length: BCertStructs.DrmBCertDeviceInfo.encode(device_info).length + 8, - attribute: device_info, - }; + const device_info_attribute = { + flags: 1, + tag: 4, + length: BCertStructs.DrmBCertDeviceInfo.encode(device_info).length + 8, + attribute: device_info, + }; - const feature = { - feature_count: 3, - features: [4, 9, 13], - }; - const feature_attribute = { - flags: 1, - tag: 5, - length: BCertStructs.DrmBCertFeatureInfo.encode(feature).length + 8, - attribute: feature, - }; + const feature = { + feature_count: 3, + features: [4, 9, 13], + }; + const feature_attribute = { + flags: 1, + tag: 5, + length: BCertStructs.DrmBCertFeatureInfo.encode(feature).length + 8, + attribute: feature, + }; - const cert_key_sign = { - type: 1, - length: 512, // bits - flags: 0, - key: signing_key.privateBytes(), - usages_count: 1, - usages: [1], - }; - const cert_key_encrypt = { - type: 1, - length: 512, // bits - flags: 0, - key: encryption_key.privateBytes(), - usages_count: 1, - usages: [2], - }; - const key_info = { - key_count: 2, - cert_keys: [cert_key_sign, cert_key_encrypt], - }; - const key_info_attribute = { - flags: 1, - tag: 6, - length: BCertStructs.DrmBCertKeyInfo.encode(key_info).length + 8, - attribute: key_info, - }; + const cert_key_sign = { + type: 1, + length: 512, // bits + flags: 0, + key: signing_key.privateBytes(), + usages_count: 1, + usages: [1], + }; + const cert_key_encrypt = { + type: 1, + length: 512, // bits + flags: 0, + key: encryption_key.privateBytes(), + usages_count: 1, + usages: [2], + }; + const key_info = { + key_count: 2, + cert_keys: [cert_key_sign, cert_key_encrypt], + }; + const key_info_attribute = { + flags: 1, + tag: 6, + length: BCertStructs.DrmBCertKeyInfo.encode(key_info).length + 8, + attribute: key_info, + }; - const manufacturer_info = parent.get_certificate(0).get_attribute(7); + const manufacturer_info = parent.get_certificate(0).get_attribute(7); - const new_bcert_container = { - signature: 'CERT', - version: 1, - total_length: 0, - certificate_length: 0, - attributes: [ - basic_info_attribute, - device_info_attribute, - feature_attribute, - key_info_attribute, - manufacturer_info, - ], - }; + const new_bcert_container = { + signature: 'CERT', + version: 1, + total_length: 0, + certificate_length: 0, + attributes: [ + basic_info_attribute, + device_info_attribute, + feature_attribute, + key_info_attribute, + manufacturer_info, + ], + }; - let payload = BCertStructs.BCert.encode(new_bcert_container); - new_bcert_container.certificate_length = payload.length; - new_bcert_container.total_length = payload.length + 144; - payload = BCertStructs.BCert.encode(new_bcert_container); + let payload = BCertStructs.BCert.encode(new_bcert_container); + new_bcert_container.certificate_length = payload.length; + new_bcert_container.total_length = payload.length + 144; + payload = BCertStructs.BCert.encode(new_bcert_container); - const hash = createHash('sha256'); - hash.update(payload); - const digest = hash.digest(); + const hash = createHash('sha256'); + hash.update(payload); + const digest = hash.digest(); - const signatureObj = group_key.keyPair.sign(digest); - const r = Buffer.from(signatureObj.r.toArray('be', 32)); - const s = Buffer.from(signatureObj.s.toArray('be', 32)); - const signature = Buffer.concat([r, s]); + const signatureObj = group_key.keyPair.sign(digest); + const r = Buffer.from(signatureObj.r.toArray('be', 32)); + const s = Buffer.from(signatureObj.s.toArray('be', 32)); + const signature = Buffer.concat([r, s]); - const signature_info = { - signature_type: 1, - signature_size: 64, - signature: signature, - signature_key_size: 512, // bits - signature_key: group_key.publicBytes(), - }; - const signature_info_attribute = { - flags: 1, - tag: 8, - length: + const signature_info = { + signature_type: 1, + signature_size: 64, + signature: signature, + signature_key_size: 512, // bits + signature_key: group_key.publicBytes(), + }; + const signature_info_attribute = { + flags: 1, + tag: 8, + length: BCertStructs.DrmBCertSignatureInfo.encode(signature_info).length + 8, - attribute: signature_info, - }; - new_bcert_container.attributes.push(signature_info_attribute); + attribute: signature_info, + }; + new_bcert_container.attributes.push(signature_info_attribute); - return new Certificate(new_bcert_container); - } - - static loads(data: string | Buffer): Certificate { - if (typeof data === 'string') { - data = Buffer.from(data, 'base64'); - } - if (!Buffer.isBuffer(data)) { - throw new Error(`Expecting Bytes or Base64 input, got ${data}`); + return new Certificate(new_bcert_container); } - const cert = BCertStructs.BCert; - const parsed_bcert = cert.parse(data); - return new Certificate(parsed_bcert, cert); - } + static loads(data: string | Buffer): Certificate { + if (typeof data === 'string') { + data = Buffer.from(data, 'base64'); + } + if (!Buffer.isBuffer(data)) { + throw new Error(`Expecting Bytes or Base64 input, got ${data}`); + } - static load(filePath: string): Certificate { - const data = fs.readFileSync(filePath); - return Certificate.loads(data); - } - - get_attribute(type_: number) { - for (const attribute of this.parsed.attributes) { - if (attribute.tag === type_) { - return attribute; - } + const cert = BCertStructs.BCert; + const parsed_bcert = cert.parse(data); + return new Certificate(parsed_bcert, cert); } - } - get_security_level(): number { - const basic_info_attribute = this.get_attribute(1); - if (basic_info_attribute) { - return basic_info_attribute.attribute.security_level; + static load(filePath: string): Certificate { + const data = fs.readFileSync(filePath); + return Certificate.loads(data); } - return 0; - } - private static _unpad(name: Buffer): string { - return name.toString('utf8').replace(/\0+$/, ''); - } - - get_name(): string { - const manufacturer_info_attribute = this.get_attribute(7); - if (manufacturer_info_attribute) { - const manufacturer_info = manufacturer_info_attribute.attribute; - const manufacturer_name = Certificate._unpad( - manufacturer_info.manufacturer_name - ); - const model_name = Certificate._unpad(manufacturer_info.model_name); - const model_number = Certificate._unpad(manufacturer_info.model_number); - return `${manufacturer_name} ${model_name} ${model_number}`; + get_attribute(type_: number) { + for (const attribute of this.parsed.attributes) { + if (attribute.tag === type_) { + return attribute; + } + } } - return ''; - } - dumps(): Buffer { - return this._BCERT.encode(this.parsed); - } + get_security_level(): number { + const basic_info_attribute = this.get_attribute(1); + if (basic_info_attribute) { + return basic_info_attribute.attribute.security_level; + } + return 0; + } - struct(): Parser { - return this._BCERT; - } + private static _unpad(name: Buffer): string { + return name.toString('utf8').replace(/\0+$/, ''); + } + + get_name(): string { + const manufacturer_info_attribute = this.get_attribute(7); + if (manufacturer_info_attribute) { + const manufacturer_info = manufacturer_info_attribute.attribute; + const manufacturer_name = Certificate._unpad( + manufacturer_info.manufacturer_name + ); + const model_name = Certificate._unpad(manufacturer_info.model_name); + const model_number = Certificate._unpad(manufacturer_info.model_number); + return `${manufacturer_name} ${model_name} ${model_number}`; + } + return ''; + } + + dumps(): Buffer { + return this._BCERT.encode(this.parsed); + } + + struct(): Parser { + return this._BCERT; + } } export class CertificateChain { - parsed: any; - _BCERT_CHAIN: Parser; + parsed: any; + _BCERT_CHAIN: Parser; - constructor( - parsed_bcert_chain: any, - bcert_chain_obj: Parser = BCertStructs.BCertChain - ) { - this.parsed = parsed_bcert_chain; - this._BCERT_CHAIN = bcert_chain_obj; - } - - static loads(data: string | Buffer): CertificateChain { - if (typeof data === 'string') { - data = Buffer.from(data, 'base64'); - } - if (!Buffer.isBuffer(data)) { - throw new Error(`Expecting Bytes or Base64 input, got ${data}`); + constructor( + parsed_bcert_chain: any, + bcert_chain_obj: Parser = BCertStructs.BCertChain + ) { + this.parsed = parsed_bcert_chain; + this._BCERT_CHAIN = bcert_chain_obj; } - const cert_chain = BCertStructs.BCertChain; - try { - const parsed_bcert_chain = cert_chain.parse(data); - return new CertificateChain(parsed_bcert_chain, cert_chain); - } catch (error) { - console.error('Error during parsing:', error); - throw error; + static loads(data: string | Buffer): CertificateChain { + if (typeof data === 'string') { + data = Buffer.from(data, 'base64'); + } + if (!Buffer.isBuffer(data)) { + throw new Error(`Expecting Bytes or Base64 input, got ${data}`); + } + + const cert_chain = BCertStructs.BCertChain; + try { + const parsed_bcert_chain = cert_chain.parse(data); + return new CertificateChain(parsed_bcert_chain, cert_chain); + } catch (error) { + console.error('Error during parsing:', error); + throw error; + } } - } - static load(filePath: string): CertificateChain { - const data = fs.readFileSync(filePath); - return CertificateChain.loads(data); - } + static load(filePath: string): CertificateChain { + const data = fs.readFileSync(filePath); + return CertificateChain.loads(data); + } - dumps(): Buffer { - return this._BCERT_CHAIN.encode(this.parsed); - } + dumps(): Buffer { + return this._BCERT_CHAIN.encode(this.parsed); + } - struct(): Parser { - return this._BCERT_CHAIN; - } + struct(): Parser { + return this._BCERT_CHAIN; + } - get_certificate(index: number): Certificate { - return new Certificate(this.parsed.certificates[index]); - } + get_certificate(index: number): Certificate { + return new Certificate(this.parsed.certificates[index]); + } - get_security_level(): number { - return this.get_certificate(0).get_security_level(); - } + get_security_level(): number { + return this.get_certificate(0).get_security_level(); + } - get_name(): string { - return this.get_certificate(0).get_name(); - } + get_name(): string { + return this.get_certificate(0).get_name(); + } - append(bcert: Certificate): void { - this.parsed.certificate_count += 1; - this.parsed.certificates.push(bcert.parsed); - this.parsed.total_length += bcert.dumps().length; - } + append(bcert: Certificate): void { + this.parsed.certificate_count += 1; + this.parsed.certificates.push(bcert.parsed); + this.parsed.total_length += bcert.dumps().length; + } - prepend(bcert: Certificate): void { - this.parsed.certificate_count += 1; - this.parsed.certificates.unshift(bcert.parsed); - this.parsed.total_length += bcert.dumps().length; - } + prepend(bcert: Certificate): void { + this.parsed.certificate_count += 1; + this.parsed.certificates.unshift(bcert.parsed); + this.parsed.total_length += bcert.dumps().length; + } } diff --git a/modules/playready/cdm.ts b/modules/playready/cdm.ts index 79e46df..a819d35 100644 --- a/modules/playready/cdm.ts +++ b/modules/playready/cdm.ts @@ -12,99 +12,99 @@ import { Device } from './device'; import { XMLParser } from 'fast-xml-parser'; export default class Cdm { - security_level: number; - certificate_chain: CertificateChain; - encryption_key: ECCKey; - signing_key: ECCKey; - client_version: string; - la_version: number; + security_level: number; + certificate_chain: CertificateChain; + encryption_key: ECCKey; + signing_key: ECCKey; + client_version: string; + la_version: number; - curve: elliptic.ec; - elgamal: ElGamal; + curve: elliptic.ec; + elgamal: ElGamal; - private wmrm_key: elliptic.ec.KeyPair; - private xml_key: XmlKey; + private wmrm_key: elliptic.ec.KeyPair; + private xml_key: XmlKey; - constructor( - security_level: number, - certificate_chain: CertificateChain, - encryption_key: ECCKey, - signing_key: ECCKey, - client_version: string = '2.4.117.27', - la_version: number = 1 - ) { - this.security_level = security_level; - this.certificate_chain = certificate_chain; - this.encryption_key = encryption_key; - this.signing_key = signing_key; - this.client_version = client_version; - this.la_version = la_version; + constructor( + security_level: number, + certificate_chain: CertificateChain, + encryption_key: ECCKey, + signing_key: ECCKey, + client_version: string = '2.4.117.27', + la_version: number = 1 + ) { + this.security_level = security_level; + this.certificate_chain = certificate_chain; + this.encryption_key = encryption_key; + this.signing_key = signing_key; + this.client_version = client_version; + this.la_version = la_version; - this.curve = new elliptic.ec('p256'); - this.elgamal = new ElGamal(this.curve); + this.curve = new elliptic.ec('p256'); + this.elgamal = new ElGamal(this.curve); - const x = + const x = 'c8b6af16ee941aadaa5389b4af2c10e356be42af175ef3face93254e7b0b3d9b'; - const y = + const y = '982b27b5cb2341326e56aa857dbfd5c634ce2cf9ea74fca8f2af5957efeea562'; - this.wmrm_key = this.curve.keyFromPublic({ x, y }, 'hex'); - this.xml_key = new XmlKey(); - } + this.wmrm_key = this.curve.keyFromPublic({ x, y }, 'hex'); + this.xml_key = new XmlKey(); + } - static fromDevice(device: Device): Cdm { - return new Cdm( - device.security_level, - device.group_certificate, - device.encryption_key, - device.signing_key - ); - } + static fromDevice(device: Device): Cdm { + return new Cdm( + device.security_level, + device.group_certificate, + device.encryption_key, + device.signing_key + ); + } - private getKeyData(): Buffer { - const messagePoint = this.xml_key.getPoint(this.elgamal.curve); - const [point1, point2] = this.elgamal.encrypt( - messagePoint, + private getKeyData(): Buffer { + const messagePoint = this.xml_key.getPoint(this.elgamal.curve); + const [point1, point2] = this.elgamal.encrypt( + messagePoint, this.wmrm_key.getPublic() as Point - ); + ); - const bufferArray = Buffer.concat([ - ElGamal.toBytes(point1.getX()), - ElGamal.toBytes(point1.getY()), - ElGamal.toBytes(point2.getX()), - ElGamal.toBytes(point2.getY()), - ]); + const bufferArray = Buffer.concat([ + ElGamal.toBytes(point1.getX()), + ElGamal.toBytes(point1.getY()), + ElGamal.toBytes(point2.getX()), + ElGamal.toBytes(point2.getY()), + ]); - return bufferArray; - } + return bufferArray; + } - private getCipherData(): Buffer { - const b64_chain = this.certificate_chain.dumps().toString('base64'); - const body = `<Data><CertificateChains><CertificateChain>${b64_chain}</CertificateChain></CertificateChains><Features><Feature Name="AESCBC"></Feature></Features></Data>`; + private getCipherData(): Buffer { + const b64_chain = this.certificate_chain.dumps().toString('base64'); + const body = `<Data><CertificateChains><CertificateChain>${b64_chain}</CertificateChain></CertificateChains><Features><Feature Name="AESCBC"></Feature></Features></Data>`; - const cipher = crypto.createCipheriv( - 'aes-128-cbc', - this.xml_key.aesKey, - this.xml_key.aesIv - ); + const cipher = crypto.createCipheriv( + 'aes-128-cbc', + this.xml_key.aesKey, + this.xml_key.aesIv + ); - const ciphertext = Buffer.concat([ - cipher.update(Buffer.from(body, 'utf-8')), - cipher.final(), - ]); + const ciphertext = Buffer.concat([ + cipher.update(Buffer.from(body, 'utf-8')), + cipher.final(), + ]); - return Buffer.concat([this.xml_key.aesIv, ciphertext]); - } + return Buffer.concat([this.xml_key.aesIv, ciphertext]); + } - private buildDigestContent( - content_header: string, - nonce: string, - wmrm_cipher: string, - cert_cipher: string - ): string { - const clientTime = Math.floor(Date.now() / 1000); + private buildDigestContent( + content_header: string, + nonce: string, + wmrm_cipher: string, + cert_cipher: string + ): string { + const clientTime = Math.floor(Date.now() / 1000); - return ( - '<LA xmlns="http://schemas.microsoft.com/DRM/2007/03/protocols" Id="SignedData" xml:space="preserve">' + + return ( + '<LA xmlns="http://schemas.microsoft.com/DRM/2007/03/protocols" Id="SignedData" xml:space="preserve">' + '<Version>4</Version>' + `<ContentHeader>${content_header}</ContentHeader>` + '<CLIENTINFO>' + @@ -130,12 +130,12 @@ export default class Cdm { '</CipherData>' + '</EncryptedData>' + '</LA>' - ); - } + ); + } - private static buildSignedInfo(digest_value: string): string { - return ( - '<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">' + + private static buildSignedInfo(digest_value: string): string { + return ( + '<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">' + '<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></CanonicalizationMethod>' + '<SignatureMethod Algorithm="http://schemas.microsoft.com/DRM/2007/03/protocols#ecdsa-sha256"></SignatureMethod>' + '<Reference URI="#SignedData">' + @@ -143,43 +143,43 @@ export default class Cdm { `<DigestValue>${digest_value}</DigestValue>` + '</Reference>' + '</SignedInfo>' - ); - } + ); + } - getLicenseChallenge(content_header: string): string { - const nonce = randomBytes(16).toString('base64'); - const wmrm_cipher = this.getKeyData().toString('base64'); - const cert_cipher = this.getCipherData().toString('base64'); + getLicenseChallenge(content_header: string): string { + const nonce = randomBytes(16).toString('base64'); + const wmrm_cipher = this.getKeyData().toString('base64'); + const cert_cipher = this.getCipherData().toString('base64'); - const la_content = this.buildDigestContent( - content_header, - nonce, - wmrm_cipher, - cert_cipher - ); + const la_content = this.buildDigestContent( + content_header, + nonce, + wmrm_cipher, + cert_cipher + ); - const la_hash = createHash('sha256').update(la_content, 'utf-8').digest(); + const la_hash = createHash('sha256').update(la_content, 'utf-8').digest(); - const signed_info = Cdm.buildSignedInfo(la_hash.toString('base64')); - const signed_info_digest = createHash('sha256') - .update(signed_info, 'utf-8') - .digest(); + const signed_info = Cdm.buildSignedInfo(la_hash.toString('base64')); + const signed_info_digest = createHash('sha256') + .update(signed_info, 'utf-8') + .digest(); - const signatureObj = this.signing_key.keyPair.sign(signed_info_digest); + const signatureObj = this.signing_key.keyPair.sign(signed_info_digest); - const r = signatureObj.r.toArrayLike(Buffer, 'be', 32); - const s = signatureObj.s.toArrayLike(Buffer, 'be', 32); + const r = signatureObj.r.toArrayLike(Buffer, 'be', 32); + const s = signatureObj.s.toArrayLike(Buffer, 'be', 32); - const rawSignature = Buffer.concat([r, s]); - const signatureValue = rawSignature.toString('base64'); + const rawSignature = Buffer.concat([r, s]); + const signatureValue = rawSignature.toString('base64'); - const publicKeyBytes = this.signing_key.keyPair - .getPublic() - .encode('array', false); - const publicKeyBuffer = Buffer.from(publicKeyBytes); - const publicKeyBase64 = publicKeyBuffer.toString('base64'); + const publicKeyBytes = this.signing_key.keyPair + .getPublic() + .encode('array', false); + const publicKeyBuffer = Buffer.from(publicKeyBytes); + const publicKeyBase64 = publicKeyBuffer.toString('base64'); - const main_body = + const main_body = '<?xml version="1.0" encoding="utf-8"?>' + '<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' + 'xmlns:xsd="http://www.w3.org/2001/XMLSchema" ' + @@ -206,73 +206,73 @@ export default class Cdm { '</soap:Body>' + '</soap:Envelope>'; - return main_body; - } + return main_body; + } - private decryptEcc256Key(encrypted_key: Buffer): Buffer { - const point1 = this.curve.curve.point( - encrypted_key.subarray(0, 32).toString('hex'), - encrypted_key.subarray(32, 64).toString('hex') - ); - const point2 = this.curve.curve.point( - encrypted_key.subarray(64, 96).toString('hex'), - encrypted_key.subarray(96, 128).toString('hex') - ); + private decryptEcc256Key(encrypted_key: Buffer): Buffer { + const point1 = this.curve.curve.point( + encrypted_key.subarray(0, 32).toString('hex'), + encrypted_key.subarray(32, 64).toString('hex') + ); + const point2 = this.curve.curve.point( + encrypted_key.subarray(64, 96).toString('hex'), + encrypted_key.subarray(96, 128).toString('hex') + ); - const decrypted = ElGamal.decrypt( - [point1, point2], - this.encryption_key.keyPair.getPrivate() - ); - const decryptedBytes = decrypted.getX().toArray('be', 32).slice(16, 32); + const decrypted = ElGamal.decrypt( + [point1, point2], + this.encryption_key.keyPair.getPrivate() + ); + const decryptedBytes = decrypted.getX().toArray('be', 32).slice(16, 32); - return Buffer.from(decryptedBytes); - } + return Buffer.from(decryptedBytes); + } - parseLicense(license: string | Buffer): { + parseLicense(license: string | Buffer): { key_id: string; key_type: number; cipher_type: number; key_length: number; key: string; }[] { - try { - const parser = new XMLParser({ - removeNSPrefix: true, - }); - const result = parser.parse(license); + try { + const parser = new XMLParser({ + removeNSPrefix: true, + }); + const result = parser.parse(license); - let licenses = + let licenses = result['Envelope']['Body']['AcquireLicenseResponse'][ - 'AcquireLicenseResult' + 'AcquireLicenseResult' ]['Response']['LicenseResponse']['Licenses']['License']; - if (!Array.isArray(licenses)) { - licenses = [licenses]; - } + if (!Array.isArray(licenses)) { + licenses = [licenses]; + } - const keys = []; + const keys = []; - for (const licenseElement of licenses) { - const keyMaterial = XmrUtil.parse(Buffer.from(licenseElement, 'base64')) - .license.license.keyMaterial; + for (const licenseElement of licenses) { + const keyMaterial = XmrUtil.parse(Buffer.from(licenseElement, 'base64')) + .license.license.keyMaterial; - if (!keyMaterial || !keyMaterial.contentKey) - throw new Error('No Content Keys retrieved'); + if (!keyMaterial || !keyMaterial.contentKey) + throw new Error('No Content Keys retrieved'); - keys.push( - new Key( - keyMaterial.contentKey.kid, - keyMaterial.contentKey.keyType, - keyMaterial.contentKey.ciphertype, - keyMaterial.contentKey.length, - this.decryptEcc256Key(keyMaterial.contentKey.value) - ) - ); - } + keys.push( + new Key( + keyMaterial.contentKey.kid, + keyMaterial.contentKey.keyType, + keyMaterial.contentKey.ciphertype, + keyMaterial.contentKey.length, + this.decryptEcc256Key(keyMaterial.contentKey.value) + ) + ); + } - return keys; - } catch (error) { - throw new Error(`Unable to parse license, ${error}`); + return keys; + } catch (error) { + throw new Error(`Unable to parse license, ${error}`); + } } - } } diff --git a/modules/playready/device.ts b/modules/playready/device.ts index 058695d..5baa792 100644 --- a/modules/playready/device.ts +++ b/modules/playready/device.ts @@ -13,89 +13,89 @@ type RawDeviceV2 = { }; class DeviceStructs { - static magic = 'PRD'; + static magic = 'PRD'; - static v1 = new Parser() - .string('signature', { length: 3, assert: DeviceStructs.magic }) - .uint8('version') - .uint32('group_key_length') - .buffer('group_key', { length: 'group_key_length' }) - .uint32('group_certificate_length') - .buffer('group_certificate', { length: 'group_certificate_length' }); + static v1 = new Parser() + .string('signature', { length: 3, assert: DeviceStructs.magic }) + .uint8('version') + .uint32('group_key_length') + .buffer('group_key', { length: 'group_key_length' }) + .uint32('group_certificate_length') + .buffer('group_certificate', { length: 'group_certificate_length' }); - static v2 = new Parser() - .string('signature', { length: 3, assert: DeviceStructs.magic }) - .uint8('version') - .uint32('group_certificate_length') - .buffer('group_certificate', { length: 'group_certificate_length' }) - .buffer('encryption_key', { length: 96 }) - .buffer('signing_key', { length: 96 }); + static v2 = new Parser() + .string('signature', { length: 3, assert: DeviceStructs.magic }) + .uint8('version') + .uint32('group_certificate_length') + .buffer('group_certificate', { length: 'group_certificate_length' }) + .buffer('encryption_key', { length: 96 }) + .buffer('signing_key', { length: 96 }); - static v3 = new Parser() - .string('signature', { length: 3, assert: DeviceStructs.magic }) - .uint8('version') - .buffer('group_key', { length: 96 }) - .buffer('encryption_key', { length: 96 }) - .buffer('signing_key', { length: 96 }) - .uint32('group_certificate_length') - .buffer('group_certificate', { length: 'group_certificate_length' }); + static v3 = new Parser() + .string('signature', { length: 3, assert: DeviceStructs.magic }) + .uint8('version') + .buffer('group_key', { length: 96 }) + .buffer('encryption_key', { length: 96 }) + .buffer('signing_key', { length: 96 }) + .uint32('group_certificate_length') + .buffer('group_certificate', { length: 'group_certificate_length' }); } export class Device { - static CURRENT_STRUCT = DeviceStructs.v3; + static CURRENT_STRUCT = DeviceStructs.v3; - group_certificate: CertificateChain; - encryption_key: ECCKey; - signing_key: ECCKey; - security_level: number; + group_certificate: CertificateChain; + encryption_key: ECCKey; + signing_key: ECCKey; + security_level: number; - constructor(parsedData: RawDeviceV2) { - this.group_certificate = CertificateChain.loads( - parsedData.group_certificate - ); - this.encryption_key = ECCKey.loads(parsedData.encryption_key); - this.signing_key = ECCKey.loads(parsedData.signing_key); - this.security_level = this.group_certificate.get_security_level(); - } + constructor(parsedData: RawDeviceV2) { + this.group_certificate = CertificateChain.loads( + parsedData.group_certificate + ); + this.encryption_key = ECCKey.loads(parsedData.encryption_key); + this.signing_key = ECCKey.loads(parsedData.signing_key); + this.security_level = this.group_certificate.get_security_level(); + } - static loads(data: Buffer): Device { - const parsedData = Device.CURRENT_STRUCT.parse(data); - return new Device(parsedData); - } + static loads(data: Buffer): Device { + const parsedData = Device.CURRENT_STRUCT.parse(data); + return new Device(parsedData); + } - static load(filePath: string): Device { - const data = fs.readFileSync(filePath); - return Device.loads(data); - } + static load(filePath: string): Device { + const data = fs.readFileSync(filePath); + return Device.loads(data); + } - dumps(): Buffer { - const groupCertBytes = this.group_certificate.dumps(); - const encryptionKeyBytes = this.encryption_key.dumps(); - const signingKeyBytes = this.signing_key.dumps(); + dumps(): Buffer { + const groupCertBytes = this.group_certificate.dumps(); + const encryptionKeyBytes = this.encryption_key.dumps(); + const signingKeyBytes = this.signing_key.dumps(); - const buildData = { - signature: DeviceStructs.magic, - version: 2, - group_certificate_length: groupCertBytes.length, - group_certificate: groupCertBytes, - encryption_key: encryptionKeyBytes, - signing_key: signingKeyBytes, - }; + const buildData = { + signature: DeviceStructs.magic, + version: 2, + group_certificate_length: groupCertBytes.length, + group_certificate: groupCertBytes, + encryption_key: encryptionKeyBytes, + signing_key: signingKeyBytes, + }; - return Device.CURRENT_STRUCT.encode(buildData); - } + return Device.CURRENT_STRUCT.encode(buildData); + } - dump(filePath: string): void { - const data = this.dumps(); - fs.writeFileSync(filePath, data); - } + dump(filePath: string): void { + const data = this.dumps(); + fs.writeFileSync(filePath, data); + } - get_name(): string { - const name = `${this.group_certificate.get_name()}_sl${ - this.security_level - }`; - return name.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase(); - } + get_name(): string { + const name = `${this.group_certificate.get_name()}_sl${ + this.security_level + }`; + return name.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase(); + } } // Device V2 disabled because unstable provisioning diff --git a/modules/playready/ecc_key.ts b/modules/playready/ecc_key.ts index 2e7b79d..6ae9a8d 100644 --- a/modules/playready/ecc_key.ts +++ b/modules/playready/ecc_key.ts @@ -3,91 +3,91 @@ import { createHash } from 'crypto'; import * as fs from 'fs'; export default class ECCKey { - keyPair: elliptic.ec.KeyPair; + keyPair: elliptic.ec.KeyPair; - constructor(keyPair: elliptic.ec.KeyPair) { - this.keyPair = keyPair; - } - - static generate(): ECCKey { - const EC = new elliptic.ec('p256'); - const keyPair = EC.genKeyPair(); - return new ECCKey(keyPair); - } - - static construct(privateKey: Buffer | string | number): ECCKey { - if (Buffer.isBuffer(privateKey)) { - privateKey = privateKey.toString('hex'); - } else if (typeof privateKey === 'number') { - privateKey = privateKey.toString(16); + constructor(keyPair: elliptic.ec.KeyPair) { + this.keyPair = keyPair; } - const EC = new elliptic.ec('p256'); - const keyPair = EC.keyFromPrivate(privateKey, 'hex'); - - return new ECCKey(keyPair); - } - - static loads(data: string | Buffer): ECCKey { - if (typeof data === 'string') { - data = Buffer.from(data, 'base64'); - } - if (!Buffer.isBuffer(data)) { - throw new Error(`Expecting Bytes or Base64 input, got ${data}`); + static generate(): ECCKey { + const EC = new elliptic.ec('p256'); + const keyPair = EC.genKeyPair(); + return new ECCKey(keyPair); } - if (data.length !== 96 && data.length !== 32) { - throw new Error( - `Invalid data length. Expecting 96 or 32 bytes, got ${data.length}` - ); + static construct(privateKey: Buffer | string | number): ECCKey { + if (Buffer.isBuffer(privateKey)) { + privateKey = privateKey.toString('hex'); + } else if (typeof privateKey === 'number') { + privateKey = privateKey.toString(16); + } + + const EC = new elliptic.ec('p256'); + const keyPair = EC.keyFromPrivate(privateKey, 'hex'); + + return new ECCKey(keyPair); } - const privateKey = data.subarray(0, 32); - return ECCKey.construct(privateKey); - } + static loads(data: string | Buffer): ECCKey { + if (typeof data === 'string') { + data = Buffer.from(data, 'base64'); + } + if (!Buffer.isBuffer(data)) { + throw new Error(`Expecting Bytes or Base64 input, got ${data}`); + } - static load(filePath: string): ECCKey { - const data = fs.readFileSync(filePath); - return ECCKey.loads(data); - } + if (data.length !== 96 && data.length !== 32) { + throw new Error( + `Invalid data length. Expecting 96 or 32 bytes, got ${data.length}` + ); + } - dumps(): Buffer { - return Buffer.concat([this.privateBytes(), this.publicBytes()]); - } + const privateKey = data.subarray(0, 32); + return ECCKey.construct(privateKey); + } - dump(filePath: string): void { - fs.writeFileSync(filePath, this.dumps()); - } + static load(filePath: string): ECCKey { + const data = fs.readFileSync(filePath); + return ECCKey.loads(data); + } - getPoint(): { x: string; y: string } { - const publicKey = this.keyPair.getPublic(); - return { - x: publicKey.getX().toString('hex'), - y: publicKey.getY().toString('hex'), - }; - } + dumps(): Buffer { + return Buffer.concat([this.privateBytes(), this.publicBytes()]); + } - privateBytes(): Buffer { - const privateKey = this.keyPair.getPrivate(); - return Buffer.from(privateKey.toArray('be', 32)); - } + dump(filePath: string): void { + fs.writeFileSync(filePath, this.dumps()); + } - privateSha256Digest(): Buffer { - const hash = createHash('sha256'); - hash.update(this.privateBytes()); - return hash.digest(); - } + getPoint(): { x: string; y: string } { + const publicKey = this.keyPair.getPublic(); + return { + x: publicKey.getX().toString('hex'), + y: publicKey.getY().toString('hex'), + }; + } - publicBytes(): Buffer { - const publicKey = this.keyPair.getPublic(); - const x = publicKey.getX().toArray('be', 32); - const y = publicKey.getY().toArray('be', 32); - return Buffer.concat([Buffer.from(x), Buffer.from(y)]); - } + privateBytes(): Buffer { + const privateKey = this.keyPair.getPrivate(); + return Buffer.from(privateKey.toArray('be', 32)); + } - publicSha256Digest(): Buffer { - const hash = createHash('sha256'); - hash.update(this.publicBytes()); - return hash.digest(); - } + privateSha256Digest(): Buffer { + const hash = createHash('sha256'); + hash.update(this.privateBytes()); + return hash.digest(); + } + + publicBytes(): Buffer { + const publicKey = this.keyPair.getPublic(); + const x = publicKey.getX().toArray('be', 32); + const y = publicKey.getY().toArray('be', 32); + return Buffer.concat([Buffer.from(x), Buffer.from(y)]); + } + + publicSha256Digest(): Buffer { + const hash = createHash('sha256'); + hash.update(this.publicBytes()); + return hash.digest(); + } } diff --git a/modules/playready/elgamal.ts b/modules/playready/elgamal.ts index 580c6a8..b20c875 100644 --- a/modules/playready/elgamal.ts +++ b/modules/playready/elgamal.ts @@ -11,35 +11,35 @@ export interface Point { } export default class ElGamal { - curve: EC; + curve: EC; - constructor(curve: EC) { - this.curve = curve; - } - - static toBytes(n: BN): Uint8Array { - const byteArray = n.toString(16).padStart(2, '0'); - if (byteArray.length % 2 !== 0) { - return Uint8Array.from(Buffer.from('0' + byteArray, 'hex')); + constructor(curve: EC) { + this.curve = curve; } - return Uint8Array.from(Buffer.from(byteArray, 'hex')); - } - encrypt(messagePoint: Point, publicKey: Point): [Point, Point] { - const ephemeralKey = new BN(randomBytes(32).toString('hex'), 16).mod( + static toBytes(n: BN): Uint8Array { + const byteArray = n.toString(16).padStart(2, '0'); + if (byteArray.length % 2 !== 0) { + return Uint8Array.from(Buffer.from('0' + byteArray, 'hex')); + } + return Uint8Array.from(Buffer.from(byteArray, 'hex')); + } + + encrypt(messagePoint: Point, publicKey: Point): [Point, Point] { + const ephemeralKey = new BN(randomBytes(32).toString('hex'), 16).mod( this.curve.n! - ); - const ephemeralKeyBigInt = BigInt(ephemeralKey.toString(10)); - const point1 = this.curve.g.mul(ephemeralKeyBigInt); - const point2 = messagePoint.add(publicKey.mul(ephemeralKeyBigInt)); + ); + const ephemeralKeyBigInt = BigInt(ephemeralKey.toString(10)); + const point1 = this.curve.g.mul(ephemeralKeyBigInt); + const point2 = messagePoint.add(publicKey.mul(ephemeralKeyBigInt)); - return [point1, point2]; - } + return [point1, point2]; + } - static decrypt(encrypted: [Point, Point], privateKey: BN): Point { - const [point1, point2] = encrypted; - const sharedSecret = point1.mul(privateKey); - const decryptedMessage = point2.add(sharedSecret.neg()); - return decryptedMessage; - } + static decrypt(encrypted: [Point, Point], privateKey: BN): Point { + const [point1, point2] = encrypted; + const sharedSecret = point1.mul(privateKey); + const decryptedMessage = point2.add(sharedSecret.neg()); + return decryptedMessage; + } } diff --git a/modules/playready/key.ts b/modules/playready/key.ts index 24689c9..7e5ae71 100644 --- a/modules/playready/key.ts +++ b/modules/playready/key.ts @@ -9,17 +9,17 @@ enum KeyType { } function getKeyType(value: number): KeyType { - switch (value) { - case KeyType.Invalid: - case KeyType.AES128CTR: - case KeyType.RC4: - case KeyType.AES128ECB: - case KeyType.Cocktail: - case KeyType.AESCBC: - return value; - default: - return KeyType.UNKNOWN; - } + switch (value) { + case KeyType.Invalid: + case KeyType.AES128CTR: + case KeyType.RC4: + case KeyType.AES128ECB: + case KeyType.Cocktail: + case KeyType.AESCBC: + return value; + default: + return KeyType.UNKNOWN; + } } enum CipherType { @@ -33,37 +33,37 @@ enum CipherType { } function getCipherType(value: number): CipherType { - switch (value) { - case CipherType.Invalid: - case CipherType.RSA128: - case CipherType.ChainedLicense: - case CipherType.ECC256: - case CipherType.ECCforScalableLicenses: - case CipherType.Scalable: - return value; - default: - return CipherType.UNKNOWN; - } + switch (value) { + case CipherType.Invalid: + case CipherType.RSA128: + case CipherType.ChainedLicense: + case CipherType.ECC256: + case CipherType.ECCforScalableLicenses: + case CipherType.Scalable: + return value; + default: + return CipherType.UNKNOWN; + } } export class Key { - key_id: string; - key_type: KeyType; - cipher_type: CipherType; - key_length: number; - key: string; + key_id: string; + key_type: KeyType; + cipher_type: CipherType; + key_length: number; + key: string; - constructor( - key_id: string, - key_type: number, - cipher_type: number, - key_length: number, - key: Buffer - ) { - this.key_id = key_id; - this.key_type = getKeyType(key_type); - this.cipher_type = getCipherType(cipher_type); - this.key_length = key_length; - this.key = key.toString('hex'); - } + constructor( + key_id: string, + key_type: number, + cipher_type: number, + key_length: number, + key: Buffer + ) { + this.key_id = key_id; + this.key_type = getKeyType(key_type); + this.cipher_type = getCipherType(cipher_type); + this.key_length = key_length; + this.key = key.toString('hex'); + } } diff --git a/modules/playready/pssh.ts b/modules/playready/pssh.ts index 81942e0..5843046 100644 --- a/modules/playready/pssh.ts +++ b/modules/playready/pssh.ts @@ -5,125 +5,125 @@ import WRMHeader from './wrmheader'; const SYSTEM_ID = Buffer.from('9a04f07998404286ab92e65be0885f95', 'hex'); const PSSHBox = new Parser() - .uint32('length') - .string('pssh', { length: 4, assert: 'pssh' }) - .uint32('fullbox') - .buffer('system_id', { length: 16 }) - .uint32('data_length') - .buffer('data', { - length: 'data_length', - }); + .uint32('length') + .string('pssh', { length: 4, assert: 'pssh' }) + .uint32('fullbox') + .buffer('system_id', { length: 16 }) + .uint32('data_length') + .buffer('data', { + length: 'data_length', + }); const PlayreadyObject = new Parser() - .useContextVars() - .uint16('type') - .uint16('length') - .choice('data', { - tag: 'type', - choices: { - 1: new Parser().string('data', { - length: function () { - return (this as any).$parent.length; + .useContextVars() + .uint16('type') + .uint16('length') + .choice('data', { + tag: 'type', + choices: { + 1: new Parser().string('data', { + length: function () { + return (this as any).$parent.length; + }, + encoding: 'utf16le', + }), }, - encoding: 'utf16le', - }), - }, - defaultChoice: new Parser().buffer('data', { - length: function () { - return (this as any).$parent.length; - }, - }), - }); + defaultChoice: new Parser().buffer('data', { + length: function () { + return (this as any).$parent.length; + }, + }), + }); const PlayreadyHeader = new Parser() - .uint32('length') - .uint16('record_count') - .array('records', { - length: 'record_count', - type: PlayreadyObject, - }); + .uint32('length') + .uint16('record_count') + .array('records', { + length: 'record_count', + type: PlayreadyObject, + }); function isPlayreadyPsshBox(data: Buffer): boolean { - if (data.length < 28) return false; - return data.subarray(12, 28).equals(SYSTEM_ID); + if (data.length < 28) return false; + return data.subarray(12, 28).equals(SYSTEM_ID); } function isUtf16(data: Buffer): boolean { - for (let i = 1; i < data.length; i += 2) { - if (data[i] !== 0) { - return false; + for (let i = 1; i < data.length; i += 2) { + if (data[i] !== 0) { + return false; + } } - } - return true; + return true; } function* getWrmHeaders(wrm_header: any): IterableIterator<string> { - for (const record of wrm_header.records) { - if (record.type === 1 && typeof record.data === 'string') { - yield record.data; + for (const record of wrm_header.records) { + if (record.type === 1 && typeof record.data === 'string') { + yield record.data; + } } - } } export class PSSH { - public wrm_headers: string[]; + public wrm_headers: string[]; - constructor(data: string | Buffer) { - if (!data) { - throw new Error('Data must not be empty'); - } - - if (typeof data === 'string') { - try { - data = Buffer.from(data, 'base64'); - } catch (e) { - throw new Error(`Could not decode data as Base64: ${e}`); - } - } - - try { - if (isPlayreadyPsshBox(data)) { - const pssh_box = PSSHBox.parse(data); - const psshData = pssh_box.data; - - if (isUtf16(psshData)) { - this.wrm_headers = [psshData.toString('utf16le')]; - } else if (isUtf16(psshData.subarray(6))) { - this.wrm_headers = [psshData.subarray(6).toString('utf16le')]; - } else if (isUtf16(psshData.subarray(10))) { - this.wrm_headers = [psshData.subarray(10).toString('utf16le')]; - } else { - const playready_header = PlayreadyHeader.parse(psshData); - this.wrm_headers = Array.from(getWrmHeaders(playready_header)); + constructor(data: string | Buffer) { + if (!data) { + throw new Error('Data must not be empty'); } - } else { - if (isUtf16(data)) { - this.wrm_headers = [data.toString('utf16le')]; - } else if (isUtf16(data.subarray(6))) { - this.wrm_headers = [data.subarray(6).toString('utf16le')]; - } else if (isUtf16(data.subarray(10))) { - this.wrm_headers = [data.subarray(10).toString('utf16le')]; - } else { - const playready_header = PlayreadyHeader.parse(data); - this.wrm_headers = Array.from(getWrmHeaders(playready_header)); + + if (typeof data === 'string') { + try { + data = Buffer.from(data, 'base64'); + } catch (e) { + throw new Error(`Could not decode data as Base64: ${e}`); + } + } + + try { + if (isPlayreadyPsshBox(data)) { + const pssh_box = PSSHBox.parse(data); + const psshData = pssh_box.data; + + if (isUtf16(psshData)) { + this.wrm_headers = [psshData.toString('utf16le')]; + } else if (isUtf16(psshData.subarray(6))) { + this.wrm_headers = [psshData.subarray(6).toString('utf16le')]; + } else if (isUtf16(psshData.subarray(10))) { + this.wrm_headers = [psshData.subarray(10).toString('utf16le')]; + } else { + const playready_header = PlayreadyHeader.parse(psshData); + this.wrm_headers = Array.from(getWrmHeaders(playready_header)); + } + } else { + if (isUtf16(data)) { + this.wrm_headers = [data.toString('utf16le')]; + } else if (isUtf16(data.subarray(6))) { + this.wrm_headers = [data.subarray(6).toString('utf16le')]; + } else if (isUtf16(data.subarray(10))) { + this.wrm_headers = [data.subarray(10).toString('utf16le')]; + } else { + const playready_header = PlayreadyHeader.parse(data); + this.wrm_headers = Array.from(getWrmHeaders(playready_header)); + } + } + } catch (e) { + throw new Error( + 'Could not parse data as a PSSH Box nor a PlayReadyHeader' + ); } - } - } catch (e) { - throw new Error( - 'Could not parse data as a PSSH Box nor a PlayReadyHeader' - ); } - } - // Header downgrade - public get_wrm_headers(downgrade_to_v4: boolean = false): string[] { - return this.wrm_headers.map( - downgrade_to_v4 ? this.downgradePSSH : (_) => _ - ); - } + // Header downgrade + public get_wrm_headers(downgrade_to_v4: boolean = false): string[] { + return this.wrm_headers.map( + downgrade_to_v4 ? this.downgradePSSH : (_) => _ + ); + } - private downgradePSSH(wrm_header: string): string { - const header = new WRMHeader(wrm_header); - return header.to_v4_0_0_0(); - } + private downgradePSSH(wrm_header: string): string { + const header = new WRMHeader(wrm_header); + return header.to_v4_0_0_0(); + } } diff --git a/modules/playready/wrmheader.ts b/modules/playready/wrmheader.ts index 017325c..e8d0102 100644 --- a/modules/playready/wrmheader.ts +++ b/modules/playready/wrmheader.ts @@ -1,11 +1,11 @@ import { XMLParser } from 'fast-xml-parser'; export class SignedKeyID { - constructor( + constructor( public alg_id: string, public value: string, public checksum?: string - ) {} + ) {} } export type Version = '4.0.0.0' | '4.1.0.0' | '4.2.0.0' | '4.3.0.0' | 'UNKNOWN'; @@ -25,88 +25,88 @@ interface ParsedWRMHeader { } export default class WRMHeader { - private header: ParsedWRMHeader['WRMHEADER']; - version: Version; + private header: ParsedWRMHeader['WRMHEADER']; + version: Version; - constructor(data: string) { - if (!data) throw new Error('Data must not be empty'); + constructor(data: string) { + if (!data) throw new Error('Data must not be empty'); - const parser = new XMLParser({ - ignoreAttributes: false, - removeNSPrefix: true, - attributeNamePrefix: '@_', - }); - const parsed = parser.parse(data) as ParsedWRMHeader; + const parser = new XMLParser({ + ignoreAttributes: false, + removeNSPrefix: true, + attributeNamePrefix: '@_', + }); + const parsed = parser.parse(data) as ParsedWRMHeader; - if (!parsed.WRMHEADER) throw new Error('Data is not a valid WRMHEADER'); + if (!parsed.WRMHEADER) throw new Error('Data is not a valid WRMHEADER'); - this.header = parsed.WRMHEADER; - this.version = WRMHeader.fromString(this.header['@_version']); - } - - private static fromString(value: string): Version { - if (['4.0.0.0', '4.1.0.0', '4.2.0.0', '4.3.0.0'].includes(value)) { - return value as Version; + this.header = parsed.WRMHEADER; + this.version = WRMHeader.fromString(this.header['@_version']); } - return 'UNKNOWN'; - } - to_v4_0_0_0(): string { - const [key_ids, la_url, lui_url, ds_id] = this.readAttributes(); - if (key_ids.length === 0) throw new Error('No Key IDs available'); - const key_id = key_ids[0]; - return `<WRMHEADER xmlns="http://schemas.microsoft.com/DRM/2007/03/PlayReadyHeader" version="4.0.0.0"><DATA><PROTECTINFO><KEYLEN>16</KEYLEN><ALGID>AESCTR</ALGID></PROTECTINFO><KID>${ - key_id.value - }</KID>${la_url ? `<LA_URL>${la_url}</LA_URL>` : ''}${ - lui_url ? `<LUI_URL>${lui_url}</LUI_URL>` : '' - }${ds_id ? `<DS_ID>${ds_id}</DS_ID>` : ''}${ - key_id.checksum ? `<CHECKSUM>${key_id.checksum}</CHECKSUM>` : '' - }</DATA></WRMHEADER>`; - } - - readAttributes(): ReturnStructure { - const data = this.header.DATA; - if (!data) - throw new Error( - 'Not a valid PlayReady Header Record, WRMHEADER/DATA required' - ); - switch (this.version) { - case '4.0.0.0': - return WRMHeader.read_v4(data); - case '4.1.0.0': - case '4.2.0.0': - case '4.3.0.0': - return WRMHeader.read_vX(data); - default: - throw new Error(`Unsupported version: ${this.version}`); + private static fromString(value: string): Version { + if (['4.0.0.0', '4.1.0.0', '4.2.0.0', '4.3.0.0'].includes(value)) { + return value as Version; + } + return 'UNKNOWN'; } - } - private static read_v4(data: any): ReturnStructure { - const protectInfo = data.PROTECTINFO; - return [ - [new SignedKeyID(protectInfo.ALGID, data.KID, data.CHECKSUM)], - data.LA_URL || null, - data.LUI_URL || null, - data.DS_ID || null, - ]; - } + to_v4_0_0_0(): string { + const [key_ids, la_url, lui_url, ds_id] = this.readAttributes(); + if (key_ids.length === 0) throw new Error('No Key IDs available'); + const key_id = key_ids[0]; + return `<WRMHEADER xmlns="http://schemas.microsoft.com/DRM/2007/03/PlayReadyHeader" version="4.0.0.0"><DATA><PROTECTINFO><KEYLEN>16</KEYLEN><ALGID>AESCTR</ALGID></PROTECTINFO><KID>${ + key_id.value + }</KID>${la_url ? `<LA_URL>${la_url}</LA_URL>` : ''}${ + lui_url ? `<LUI_URL>${lui_url}</LUI_URL>` : '' + }${ds_id ? `<DS_ID>${ds_id}</DS_ID>` : ''}${ + key_id.checksum ? `<CHECKSUM>${key_id.checksum}</CHECKSUM>` : '' + }</DATA></WRMHEADER>`; + } - private static read_vX(data: any): ReturnStructure { - const protectInfo = data.PROTECTINFO; + readAttributes(): ReturnStructure { + const data = this.header.DATA; + if (!data) + throw new Error( + 'Not a valid PlayReady Header Record, WRMHEADER/DATA required' + ); + switch (this.version) { + case '4.0.0.0': + return WRMHeader.read_v4(data); + case '4.1.0.0': + case '4.2.0.0': + case '4.3.0.0': + return WRMHeader.read_vX(data); + default: + throw new Error(`Unsupported version: ${this.version}`); + } + } - const signedKeyID: SignedKeyID | undefined = protectInfo.KIDS.KID - ? new SignedKeyID( - protectInfo.KIDS.KID['@_ALGID'] || '', - protectInfo.KIDS.KID['@_VALUE'], - protectInfo.KIDS.KID['@_CHECKSUM'] - ) - : undefined; - return [ - signedKeyID ? [signedKeyID] : [], - data.LA_URL || null, - data.LUI_URL || null, - data.DS_ID || null, - ]; - } + private static read_v4(data: any): ReturnStructure { + const protectInfo = data.PROTECTINFO; + return [ + [new SignedKeyID(protectInfo.ALGID, data.KID, data.CHECKSUM)], + data.LA_URL || null, + data.LUI_URL || null, + data.DS_ID || null, + ]; + } + + private static read_vX(data: any): ReturnStructure { + const protectInfo = data.PROTECTINFO; + + const signedKeyID: SignedKeyID | undefined = protectInfo.KIDS.KID + ? new SignedKeyID( + protectInfo.KIDS.KID['@_ALGID'] || '', + protectInfo.KIDS.KID['@_VALUE'], + protectInfo.KIDS.KID['@_CHECKSUM'] + ) + : undefined; + return [ + signedKeyID ? [signedKeyID] : [], + data.LA_URL || null, + data.LUI_URL || null, + data.DS_ID || null, + ]; + } } diff --git a/modules/playready/xml_key.ts b/modules/playready/xml_key.ts index 3ff099f..eacbf6c 100644 --- a/modules/playready/xml_key.ts +++ b/modules/playready/xml_key.ts @@ -4,25 +4,25 @@ import ECCKey from './ecc_key'; import ElGamal, { Point } from './elgamal'; export default class XmlKey { - private _sharedPoint: ECCKey; - public sharedKeyX: BN; - public sharedKeyY: BN; - public _shared_key_x_bytes: Uint8Array; - public aesIv: Uint8Array; - public aesKey: Uint8Array; + private _sharedPoint: ECCKey; + public sharedKeyX: BN; + public sharedKeyY: BN; + public _shared_key_x_bytes: Uint8Array; + public aesIv: Uint8Array; + public aesKey: Uint8Array; - constructor() { - this._sharedPoint = ECCKey.generate(); - this.sharedKeyX = this._sharedPoint.keyPair.getPublic().getX(); - this.sharedKeyY = this._sharedPoint.keyPair.getPublic().getY(); - this._shared_key_x_bytes = ElGamal.toBytes(this.sharedKeyX); - this.aesIv = this._shared_key_x_bytes.subarray(0, 16); - this.aesKey = this._shared_key_x_bytes.subarray(16, 32); - } + constructor() { + this._sharedPoint = ECCKey.generate(); + this.sharedKeyX = this._sharedPoint.keyPair.getPublic().getX(); + this.sharedKeyY = this._sharedPoint.keyPair.getPublic().getY(); + this._shared_key_x_bytes = ElGamal.toBytes(this.sharedKeyX); + this.aesIv = this._shared_key_x_bytes.subarray(0, 16); + this.aesKey = this._shared_key_x_bytes.subarray(16, 32); + } - getPoint(curve: EC): Point { - return curve.curve.point(this.sharedKeyX, this.sharedKeyY); - } + getPoint(curve: EC): Point { + return curve.curve.point(this.sharedKeyX, this.sharedKeyY); + } } // Make it more undetectable (not working right now) diff --git a/modules/playready/xmrlicense.ts b/modules/playready/xmrlicense.ts index 6862b6e..960d154 100644 --- a/modules/playready/xmrlicense.ts +++ b/modules/playready/xmrlicense.ts @@ -44,57 +44,57 @@ type ParsedLicense = { }; export class XMRLicenseStructsV2 { - static CONTENT_KEY = new Parser() - .buffer('kid', { length: 16 }) - .uint16('keytype') - .uint16('ciphertype') - .uint16('length') - .buffer('value', { - length: 'length', - }); + static CONTENT_KEY = new Parser() + .buffer('kid', { length: 16 }) + .uint16('keytype') + .uint16('ciphertype') + .uint16('length') + .buffer('value', { + length: 'length', + }); - static ECC_KEY = new Parser() - .uint16('curve') - .uint16('length') - .buffer('value', { - length: 'length', - }); + static ECC_KEY = new Parser() + .uint16('curve') + .uint16('length') + .buffer('value', { + length: 'length', + }); - static FTLV = new Parser() - .uint16('flags') - .uint16('type') - .uint32('length') - .buffer('value', { - length: function () { - return (this as any).length - 8; - }, - }); + static FTLV = new Parser() + .uint16('flags') + .uint16('type') + .uint32('length') + .buffer('value', { + length: function () { + return (this as any).length - 8; + }, + }); - static AUXILIARY_LOCATIONS = new Parser() - .uint32('location') - .buffer('value', { length: 16 }); + static AUXILIARY_LOCATIONS = new Parser() + .uint32('location') + .buffer('value', { length: 16 }); - static AUXILIARY_KEY_OBJECT = new Parser() - .uint16('count') - .array('locations', { - length: 'count', - type: XMRLicenseStructsV2.AUXILIARY_LOCATIONS, - }); + static AUXILIARY_KEY_OBJECT = new Parser() + .uint16('count') + .array('locations', { + length: 'count', + type: XMRLicenseStructsV2.AUXILIARY_LOCATIONS, + }); - static SIGNATURE = new Parser() - .uint16('type') - .uint16('siglength') - .buffer('signature', { - length: 'siglength', - }); + static SIGNATURE = new Parser() + .uint16('type') + .uint16('siglength') + .buffer('signature', { + length: 'siglength', + }); - static XMR = new Parser() - .string('constant', { length: 4, assert: 'XMR\x00' }) - .int32('version') - .buffer('rightsid', { length: 16 }) - .nest('data', { - type: XMRLicenseStructsV2.FTLV, - }); + static XMR = new Parser() + .string('constant', { length: 4, assert: 'XMR\x00' }) + .int32('version') + .buffer('rightsid', { length: 16 }) + .nest('data', { + type: XMRLicenseStructsV2.FTLV, + }); } enum XMRTYPE { @@ -117,135 +117,135 @@ enum XMRTYPE { } export class XmrUtil { - public data: Buffer; - public license: ParsedLicense; + public data: Buffer; + public license: ParsedLicense; - constructor(data: Buffer, license: ParsedLicense) { - this.data = data; - this.license = license; - } - - static parse(license: Buffer) { - const xmr = XMRLicenseStructsV2.XMR.parse(license); - - const parsed_license: ParsedLicense = { - version: xmr.version, - rights: Buffer.from(xmr.rightsid).toString('hex'), - length: license.length, - license: { - length: xmr.data.length, - }, - }; - const container = parsed_license.license; - const data = xmr.data; - - let pos = 0; - while (pos < data.length - 16) { - const value = XMRLicenseStructsV2.FTLV.parse(data.value.slice(pos)); - - // XMR_SIGNATURE_OBJECT - if (value.type === XMRTYPE.XMR_SIGNATURE_OBJECT) { - const signature = XMRLicenseStructsV2.SIGNATURE.parse(value.value); - - container.signature = { - length: value.length, - type: signature.type, - value: Buffer.from(signature.signature).toString('hex'), - }; - } - - // XMRTYPE.XMR_GLOBAL_POLICY_CONTAINER - if (value.type === XMRTYPE.XMR_GLOBAL_POLICY_CONTAINER) { - container.global_container = {}; - - let index = 0; - while (index < value.length - 16) { - const data = XMRLicenseStructsV2.FTLV.parse(value.value.slice(index)); - - // XMRTYPE.XMR_REVOCATION_INFORMATION_VERSION - if (data.type === XMRTYPE.XMR_REVOCATION_INFORMATION_VERSION) { - container.global_container.revocationInfo = { - version: data.value.readUInt32BE(0), - }; - } - - // XMRTYPE.XMR_SECURITY_LEVEL - if (data.type === XMRTYPE.XMR_SECURITY_LEVEL) { - container.global_container.securityLevel = { - level: data.value.readUInt16BE(0), - }; - } - - index += data.length; - } - } - - // XMRTYPE.XMR_KEY_MATERIAL_CONTAINER - if (value.type === XMRTYPE.XMR_KEY_MATERIAL_CONTAINER) { - container.keyMaterial = {}; - - let index = 0; - while (index < value.length - 16) { - const data = XMRLicenseStructsV2.FTLV.parse(value.value.slice(index)); - - // XMRTYPE.XMR_CONTENT_KEY_OBJECT - if (data.type === XMRTYPE.XMR_CONTENT_KEY_OBJECT) { - const content_key = XMRLicenseStructsV2.CONTENT_KEY.parse( - data.value - ); - - container.keyMaterial.contentKey = { - kid: XmrUtil.fixUUID(content_key.kid).toString('hex'), - keyType: content_key.keytype, - ciphertype: content_key.ciphertype, - length: content_key.length, - value: content_key.value, - }; - } - - // XMRTYPE.XMR_ECC_KEY_OBJECT - if (data.type === XMRTYPE.XMR_ECC_KEY_OBJECT) { - const ecc_key = XMRLicenseStructsV2.ECC_KEY.parse(data.value); - - container.keyMaterial.encryptionKey = { - curve: ecc_key.curve, - length: ecc_key.length, - value: Buffer.from(ecc_key.value).toString('hex'), - }; - } - - // XMRTYPE.XMR_AUXILIARY_KEY_OBJECT - if (data.type === XMRTYPE.XMR_AUXILIARY_KEY_OBJECT) { - const aux_keys = XMRLicenseStructsV2.AUXILIARY_KEY_OBJECT.parse( - data.value - ); - - container.keyMaterial.auxKeys = { - count: aux_keys.count, - value: aux_keys.locations.map((a: any) => { - return { - location: a.location, - value: Buffer.from(a.value).toString('hex'), - }; - }), - }; - } - index += data.length; - } - } - - pos += value.length; + constructor(data: Buffer, license: ParsedLicense) { + this.data = data; + this.license = license; } - return new XmrUtil(license, parsed_license); - } + static parse(license: Buffer) { + const xmr = XMRLicenseStructsV2.XMR.parse(license); - static fixUUID(data: Buffer): Buffer { - return Buffer.concat([ - Buffer.from(data.subarray(0, 4).reverse()), - Buffer.from(data.subarray(4, 6).reverse()), - Buffer.from(data.subarray(6, 8).reverse()), - data.subarray(8, 16), - ]); - } + const parsed_license: ParsedLicense = { + version: xmr.version, + rights: Buffer.from(xmr.rightsid).toString('hex'), + length: license.length, + license: { + length: xmr.data.length, + }, + }; + const container = parsed_license.license; + const data = xmr.data; + + let pos = 0; + while (pos < data.length - 16) { + const value = XMRLicenseStructsV2.FTLV.parse(data.value.slice(pos)); + + // XMR_SIGNATURE_OBJECT + if (value.type === XMRTYPE.XMR_SIGNATURE_OBJECT) { + const signature = XMRLicenseStructsV2.SIGNATURE.parse(value.value); + + container.signature = { + length: value.length, + type: signature.type, + value: Buffer.from(signature.signature).toString('hex'), + }; + } + + // XMRTYPE.XMR_GLOBAL_POLICY_CONTAINER + if (value.type === XMRTYPE.XMR_GLOBAL_POLICY_CONTAINER) { + container.global_container = {}; + + let index = 0; + while (index < value.length - 16) { + const data = XMRLicenseStructsV2.FTLV.parse(value.value.slice(index)); + + // XMRTYPE.XMR_REVOCATION_INFORMATION_VERSION + if (data.type === XMRTYPE.XMR_REVOCATION_INFORMATION_VERSION) { + container.global_container.revocationInfo = { + version: data.value.readUInt32BE(0), + }; + } + + // XMRTYPE.XMR_SECURITY_LEVEL + if (data.type === XMRTYPE.XMR_SECURITY_LEVEL) { + container.global_container.securityLevel = { + level: data.value.readUInt16BE(0), + }; + } + + index += data.length; + } + } + + // XMRTYPE.XMR_KEY_MATERIAL_CONTAINER + if (value.type === XMRTYPE.XMR_KEY_MATERIAL_CONTAINER) { + container.keyMaterial = {}; + + let index = 0; + while (index < value.length - 16) { + const data = XMRLicenseStructsV2.FTLV.parse(value.value.slice(index)); + + // XMRTYPE.XMR_CONTENT_KEY_OBJECT + if (data.type === XMRTYPE.XMR_CONTENT_KEY_OBJECT) { + const content_key = XMRLicenseStructsV2.CONTENT_KEY.parse( + data.value + ); + + container.keyMaterial.contentKey = { + kid: XmrUtil.fixUUID(content_key.kid).toString('hex'), + keyType: content_key.keytype, + ciphertype: content_key.ciphertype, + length: content_key.length, + value: content_key.value, + }; + } + + // XMRTYPE.XMR_ECC_KEY_OBJECT + if (data.type === XMRTYPE.XMR_ECC_KEY_OBJECT) { + const ecc_key = XMRLicenseStructsV2.ECC_KEY.parse(data.value); + + container.keyMaterial.encryptionKey = { + curve: ecc_key.curve, + length: ecc_key.length, + value: Buffer.from(ecc_key.value).toString('hex'), + }; + } + + // XMRTYPE.XMR_AUXILIARY_KEY_OBJECT + if (data.type === XMRTYPE.XMR_AUXILIARY_KEY_OBJECT) { + const aux_keys = XMRLicenseStructsV2.AUXILIARY_KEY_OBJECT.parse( + data.value + ); + + container.keyMaterial.auxKeys = { + count: aux_keys.count, + value: aux_keys.locations.map((a: any) => { + return { + location: a.location, + value: Buffer.from(a.value).toString('hex'), + }; + }), + }; + } + index += data.length; + } + } + + pos += value.length; + } + + return new XmrUtil(license, parsed_license); + } + + static fixUUID(data: Buffer): Buffer { + return Buffer.concat([ + Buffer.from(data.subarray(0, 4).reverse()), + Buffer.from(data.subarray(4, 6).reverse()), + Buffer.from(data.subarray(6, 8).reverse()), + data.subarray(8, 16), + ]); + } } diff --git a/modules/widevine/cmac.ts b/modules/widevine/cmac.ts index c2ea240..95c949d 100644 --- a/modules/widevine/cmac.ts +++ b/modules/widevine/cmac.ts @@ -2,112 +2,112 @@ import crypto from 'crypto'; export class AES_CMAC { - private readonly BLOCK_SIZE = 16; - private readonly XOR_RIGHT = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87]); - private readonly EMPTY_BLOCK_SIZE_BUFFER = Buffer.alloc(this.BLOCK_SIZE) as Buffer; + private readonly BLOCK_SIZE = 16; + private readonly XOR_RIGHT = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87]); + private readonly EMPTY_BLOCK_SIZE_BUFFER = Buffer.alloc(this.BLOCK_SIZE) as Buffer; - private _key: Buffer; - private _subkeys: { first: Buffer; second: Buffer }; + private _key: Buffer; + private _subkeys: { first: Buffer; second: Buffer }; - public constructor(key: Buffer) { - if (![16, 24, 32].includes(key.length)) { - throw new Error('Key size must be 128, 192, or 256 bits.'); - } - this._key = key; - this._subkeys = this._generateSubkeys(); - } - - public calculate(message: Buffer): Buffer { - const blockCount = this._getBlockCount(message); - - let x = this.EMPTY_BLOCK_SIZE_BUFFER; - let y; - - for (let i = 0; i < blockCount - 1; i++) { - const from = i * this.BLOCK_SIZE; - const block = message.subarray(from, from + this.BLOCK_SIZE); - y = this._xor(x, block); - x = this._aes(y); + public constructor(key: Buffer) { + if (![16, 24, 32].includes(key.length)) { + throw new Error('Key size must be 128, 192, or 256 bits.'); + } + this._key = key; + this._subkeys = this._generateSubkeys(); } - y = this._xor(x, this._getLastBlock(message)); - x = this._aes(y); + public calculate(message: Buffer): Buffer { + const blockCount = this._getBlockCount(message); - return x; - } + let x = this.EMPTY_BLOCK_SIZE_BUFFER; + let y; - private _generateSubkeys(): { first: Buffer; second: Buffer } { - const l = this._aes(this.EMPTY_BLOCK_SIZE_BUFFER); + for (let i = 0; i < blockCount - 1; i++) { + const from = i * this.BLOCK_SIZE; + const block = message.subarray(from, from + this.BLOCK_SIZE); + y = this._xor(x, block); + x = this._aes(y); + } - let first = this._bitShiftLeft(l); - if (l[0] & 0x80) { - first = this._xor(first, this.XOR_RIGHT); + y = this._xor(x, this._getLastBlock(message)); + x = this._aes(y); + + return x; } - let second = this._bitShiftLeft(first); - if (first[0] & 0x80) { - second = this._xor(second, this.XOR_RIGHT); + private _generateSubkeys(): { first: Buffer; second: Buffer } { + const l = this._aes(this.EMPTY_BLOCK_SIZE_BUFFER); + + let first = this._bitShiftLeft(l); + if (l[0] & 0x80) { + first = this._xor(first, this.XOR_RIGHT); + } + + let second = this._bitShiftLeft(first); + if (first[0] & 0x80) { + second = this._xor(second, this.XOR_RIGHT); + } + + return { first: first, second: second }; } - return { first: first, second: second }; - } - - private _getBlockCount(message: Buffer): number { - const blockCount = Math.ceil(message.length / this.BLOCK_SIZE); - return blockCount === 0 ? 1 : blockCount; - } - - private _aes(message: Buffer): Buffer { - const cipher = crypto.createCipheriv(`aes-${this._key.length * 8}-cbc`, this._key, Buffer.alloc(this.BLOCK_SIZE)); - const result = cipher.update(message).subarray(0, 16); - cipher.destroy(); - return result; - } - - private _getLastBlock(message: Buffer): Buffer { - const blockCount = this._getBlockCount(message); - const paddedBlock = this._padding(message, blockCount - 1); - - let complete = false; - if (message.length > 0) { - complete = message.length % this.BLOCK_SIZE === 0; + private _getBlockCount(message: Buffer): number { + const blockCount = Math.ceil(message.length / this.BLOCK_SIZE); + return blockCount === 0 ? 1 : blockCount; } - const key = complete ? this._subkeys.first : this._subkeys.second; - return this._xor(paddedBlock, key); - } - - private _padding(message: Buffer, blockIndex: number): Buffer { - const block = Buffer.alloc(this.BLOCK_SIZE); - - const from = blockIndex * this.BLOCK_SIZE; - - const slice = message.subarray(from, from + this.BLOCK_SIZE); - block.set(slice); - - if (slice.length !== this.BLOCK_SIZE) { - block[slice.length] = 0x80; + private _aes(message: Buffer): Buffer { + const cipher = crypto.createCipheriv(`aes-${this._key.length * 8}-cbc`, this._key, Buffer.alloc(this.BLOCK_SIZE)); + const result = cipher.update(message).subarray(0, 16); + cipher.destroy(); + return result; } - return block; - } + private _getLastBlock(message: Buffer): Buffer { + const blockCount = this._getBlockCount(message); + const paddedBlock = this._padding(message, blockCount - 1); - private _bitShiftLeft(input: Buffer): Buffer { - const output = Buffer.alloc(input.length); - let overflow = 0; - for (let i = input.length - 1; i >= 0; i--) { - output[i] = (input[i] << 1) | overflow; - overflow = input[i] & 0x80 ? 1 : 0; - } - return output; - } + let complete = false; + if (message.length > 0) { + complete = message.length % this.BLOCK_SIZE === 0; + } - private _xor(a: Buffer, b: Buffer): Buffer { - const length = Math.min(a.length, b.length); - const output = Buffer.alloc(length); - for (let i = 0; i < length; i++) { - output[i] = a[i] ^ b[i]; + const key = complete ? this._subkeys.first : this._subkeys.second; + return this._xor(paddedBlock, key); + } + + private _padding(message: Buffer, blockIndex: number): Buffer { + const block = Buffer.alloc(this.BLOCK_SIZE); + + const from = blockIndex * this.BLOCK_SIZE; + + const slice = message.subarray(from, from + this.BLOCK_SIZE); + block.set(slice); + + if (slice.length !== this.BLOCK_SIZE) { + block[slice.length] = 0x80; + } + + return block; + } + + private _bitShiftLeft(input: Buffer): Buffer { + const output = Buffer.alloc(input.length); + let overflow = 0; + for (let i = input.length - 1; i >= 0; i--) { + output[i] = (input[i] << 1) | overflow; + overflow = input[i] & 0x80 ? 1 : 0; + } + return output; + } + + private _xor(a: Buffer, b: Buffer): Buffer { + const length = Math.min(a.length, b.length); + const output = Buffer.alloc(length); + for (let i = 0; i < length; i++) { + output[i] = a[i] ^ b[i]; + } + return output; } - return output; - } } diff --git a/modules/widevine/license.ts b/modules/widevine/license.ts index 9a49a49..97b51e5 100644 --- a/modules/widevine/license.ts +++ b/modules/widevine/license.ts @@ -3,76 +3,76 @@ import { AES_CMAC } from './cmac'; import forge from 'node-forge'; import { - ClientIdentification, - ClientIdentificationSchema, - DrmCertificateSchema, - EncryptedClientIdentification, - EncryptedClientIdentificationSchema, - LicenseRequest, - LicenseRequest_ContentIdentification_WidevinePsshDataSchema, - LicenseRequest_ContentIdentificationSchema, - LicenseRequest_RequestType, - LicenseRequestSchema, - LicenseSchema, - LicenseType, - ProtocolVersion, - SignedDrmCertificate, - SignedDrmCertificateSchema, - SignedMessage, - SignedMessage_MessageType, - SignedMessageSchema, - WidevinePsshData, - WidevinePsshDataSchema + ClientIdentification, + ClientIdentificationSchema, + DrmCertificateSchema, + EncryptedClientIdentification, + EncryptedClientIdentificationSchema, + LicenseRequest, + LicenseRequest_ContentIdentification_WidevinePsshDataSchema, + LicenseRequest_ContentIdentificationSchema, + LicenseRequest_RequestType, + LicenseRequestSchema, + LicenseSchema, + LicenseType, + ProtocolVersion, + SignedDrmCertificate, + SignedDrmCertificateSchema, + SignedMessage, + SignedMessage_MessageType, + SignedMessageSchema, + WidevinePsshData, + WidevinePsshDataSchema } from './license_protocol_pb3'; import { create, fromBinary, toBinary } from '@bufbuild/protobuf'; const WIDEVINE_SYSTEM_ID = new Uint8Array([0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, 0xd5, 0x1d, 0x21, 0xed]); const WIDEVINE_ROOT_PUBLIC_KEY = new Uint8Array([ - 0x30, 0x82, 0x01, 0x8a, 0x02, 0x82, 0x01, 0x81, 0x00, 0xb4, 0xfe, 0x39, 0xc3, 0x65, 0x90, 0x03, 0xdb, 0x3c, 0x11, 0x97, 0x09, 0xe8, 0x68, 0xcd, 0xf2, 0xc3, 0x5e, 0x9b, 0xf2, - 0xe7, 0x4d, 0x23, 0xb1, 0x10, 0xdb, 0x87, 0x65, 0xdf, 0xdc, 0xfb, 0x9f, 0x35, 0xa0, 0x57, 0x03, 0x53, 0x4c, 0xf6, 0x6d, 0x35, 0x7d, 0xa6, 0x78, 0xdb, 0xb3, 0x36, 0xd2, 0x3f, - 0x9c, 0x40, 0xa9, 0x95, 0x26, 0x72, 0x7f, 0xb8, 0xbe, 0x66, 0xdf, 0xc5, 0x21, 0x98, 0x78, 0x15, 0x16, 0x68, 0x5d, 0x2f, 0x46, 0x0e, 0x43, 0xcb, 0x8a, 0x84, 0x39, 0xab, 0xfb, - 0xb0, 0x35, 0x80, 0x22, 0xbe, 0x34, 0x23, 0x8b, 0xab, 0x53, 0x5b, 0x72, 0xec, 0x4b, 0xb5, 0x48, 0x69, 0x53, 0x3e, 0x47, 0x5f, 0xfd, 0x09, 0xfd, 0xa7, 0x76, 0x13, 0x8f, 0x0f, - 0x92, 0xd6, 0x4c, 0xdf, 0xae, 0x76, 0xa9, 0xba, 0xd9, 0x22, 0x10, 0xa9, 0x9d, 0x71, 0x45, 0xd6, 0xd7, 0xe1, 0x19, 0x25, 0x85, 0x9c, 0x53, 0x9a, 0x97, 0xeb, 0x84, 0xd7, 0xcc, - 0xa8, 0x88, 0x82, 0x20, 0x70, 0x26, 0x20, 0xfd, 0x7e, 0x40, 0x50, 0x27, 0xe2, 0x25, 0x93, 0x6f, 0xbc, 0x3e, 0x72, 0xa0, 0xfa, 0xc1, 0xbd, 0x29, 0xb4, 0x4d, 0x82, 0x5c, 0xc1, - 0xb4, 0xcb, 0x9c, 0x72, 0x7e, 0xb0, 0xe9, 0x8a, 0x17, 0x3e, 0x19, 0x63, 0xfc, 0xfd, 0x82, 0x48, 0x2b, 0xb7, 0xb2, 0x33, 0xb9, 0x7d, 0xec, 0x4b, 0xba, 0x89, 0x1f, 0x27, 0xb8, - 0x9b, 0x88, 0x48, 0x84, 0xaa, 0x18, 0x92, 0x0e, 0x65, 0xf5, 0xc8, 0x6c, 0x11, 0xff, 0x6b, 0x36, 0xe4, 0x74, 0x34, 0xca, 0x8c, 0x33, 0xb1, 0xf9, 0xb8, 0x8e, 0xb4, 0xe6, 0x12, - 0xe0, 0x02, 0x98, 0x79, 0x52, 0x5e, 0x45, 0x33, 0xff, 0x11, 0xdc, 0xeb, 0xc3, 0x53, 0xba, 0x7c, 0x60, 0x1a, 0x11, 0x3d, 0x00, 0xfb, 0xd2, 0xb7, 0xaa, 0x30, 0xfa, 0x4f, 0x5e, - 0x48, 0x77, 0x5b, 0x17, 0xdc, 0x75, 0xef, 0x6f, 0xd2, 0x19, 0x6d, 0xdc, 0xbe, 0x7f, 0xb0, 0x78, 0x8f, 0xdc, 0x82, 0x60, 0x4c, 0xbf, 0xe4, 0x29, 0x06, 0x5e, 0x69, 0x8c, 0x39, - 0x13, 0xad, 0x14, 0x25, 0xed, 0x19, 0xb2, 0xf2, 0x9f, 0x01, 0x82, 0x0d, 0x56, 0x44, 0x88, 0xc8, 0x35, 0xec, 0x1f, 0x11, 0xb3, 0x24, 0xe0, 0x59, 0x0d, 0x37, 0xe4, 0x47, 0x3c, - 0xea, 0x4b, 0x7f, 0x97, 0x31, 0x1c, 0x81, 0x7c, 0x94, 0x8a, 0x4c, 0x7d, 0x68, 0x15, 0x84, 0xff, 0xa5, 0x08, 0xfd, 0x18, 0xe7, 0xe7, 0x2b, 0xe4, 0x47, 0x27, 0x12, 0x11, 0xb8, - 0x23, 0xec, 0x58, 0x93, 0x3c, 0xac, 0x12, 0xd2, 0x88, 0x6d, 0x41, 0x3d, 0xc5, 0xfe, 0x1c, 0xdc, 0xb9, 0xf8, 0xd4, 0x51, 0x3e, 0x07, 0xe5, 0x03, 0x6f, 0xa7, 0x12, 0xe8, 0x12, - 0xf7, 0xb5, 0xce, 0xa6, 0x96, 0x55, 0x3f, 0x78, 0xb4, 0x64, 0x82, 0x50, 0xd2, 0x33, 0x5f, 0x91, 0x02, 0x03, 0x01, 0x00, 0x01 + 0x30, 0x82, 0x01, 0x8a, 0x02, 0x82, 0x01, 0x81, 0x00, 0xb4, 0xfe, 0x39, 0xc3, 0x65, 0x90, 0x03, 0xdb, 0x3c, 0x11, 0x97, 0x09, 0xe8, 0x68, 0xcd, 0xf2, 0xc3, 0x5e, 0x9b, 0xf2, + 0xe7, 0x4d, 0x23, 0xb1, 0x10, 0xdb, 0x87, 0x65, 0xdf, 0xdc, 0xfb, 0x9f, 0x35, 0xa0, 0x57, 0x03, 0x53, 0x4c, 0xf6, 0x6d, 0x35, 0x7d, 0xa6, 0x78, 0xdb, 0xb3, 0x36, 0xd2, 0x3f, + 0x9c, 0x40, 0xa9, 0x95, 0x26, 0x72, 0x7f, 0xb8, 0xbe, 0x66, 0xdf, 0xc5, 0x21, 0x98, 0x78, 0x15, 0x16, 0x68, 0x5d, 0x2f, 0x46, 0x0e, 0x43, 0xcb, 0x8a, 0x84, 0x39, 0xab, 0xfb, + 0xb0, 0x35, 0x80, 0x22, 0xbe, 0x34, 0x23, 0x8b, 0xab, 0x53, 0x5b, 0x72, 0xec, 0x4b, 0xb5, 0x48, 0x69, 0x53, 0x3e, 0x47, 0x5f, 0xfd, 0x09, 0xfd, 0xa7, 0x76, 0x13, 0x8f, 0x0f, + 0x92, 0xd6, 0x4c, 0xdf, 0xae, 0x76, 0xa9, 0xba, 0xd9, 0x22, 0x10, 0xa9, 0x9d, 0x71, 0x45, 0xd6, 0xd7, 0xe1, 0x19, 0x25, 0x85, 0x9c, 0x53, 0x9a, 0x97, 0xeb, 0x84, 0xd7, 0xcc, + 0xa8, 0x88, 0x82, 0x20, 0x70, 0x26, 0x20, 0xfd, 0x7e, 0x40, 0x50, 0x27, 0xe2, 0x25, 0x93, 0x6f, 0xbc, 0x3e, 0x72, 0xa0, 0xfa, 0xc1, 0xbd, 0x29, 0xb4, 0x4d, 0x82, 0x5c, 0xc1, + 0xb4, 0xcb, 0x9c, 0x72, 0x7e, 0xb0, 0xe9, 0x8a, 0x17, 0x3e, 0x19, 0x63, 0xfc, 0xfd, 0x82, 0x48, 0x2b, 0xb7, 0xb2, 0x33, 0xb9, 0x7d, 0xec, 0x4b, 0xba, 0x89, 0x1f, 0x27, 0xb8, + 0x9b, 0x88, 0x48, 0x84, 0xaa, 0x18, 0x92, 0x0e, 0x65, 0xf5, 0xc8, 0x6c, 0x11, 0xff, 0x6b, 0x36, 0xe4, 0x74, 0x34, 0xca, 0x8c, 0x33, 0xb1, 0xf9, 0xb8, 0x8e, 0xb4, 0xe6, 0x12, + 0xe0, 0x02, 0x98, 0x79, 0x52, 0x5e, 0x45, 0x33, 0xff, 0x11, 0xdc, 0xeb, 0xc3, 0x53, 0xba, 0x7c, 0x60, 0x1a, 0x11, 0x3d, 0x00, 0xfb, 0xd2, 0xb7, 0xaa, 0x30, 0xfa, 0x4f, 0x5e, + 0x48, 0x77, 0x5b, 0x17, 0xdc, 0x75, 0xef, 0x6f, 0xd2, 0x19, 0x6d, 0xdc, 0xbe, 0x7f, 0xb0, 0x78, 0x8f, 0xdc, 0x82, 0x60, 0x4c, 0xbf, 0xe4, 0x29, 0x06, 0x5e, 0x69, 0x8c, 0x39, + 0x13, 0xad, 0x14, 0x25, 0xed, 0x19, 0xb2, 0xf2, 0x9f, 0x01, 0x82, 0x0d, 0x56, 0x44, 0x88, 0xc8, 0x35, 0xec, 0x1f, 0x11, 0xb3, 0x24, 0xe0, 0x59, 0x0d, 0x37, 0xe4, 0x47, 0x3c, + 0xea, 0x4b, 0x7f, 0x97, 0x31, 0x1c, 0x81, 0x7c, 0x94, 0x8a, 0x4c, 0x7d, 0x68, 0x15, 0x84, 0xff, 0xa5, 0x08, 0xfd, 0x18, 0xe7, 0xe7, 0x2b, 0xe4, 0x47, 0x27, 0x12, 0x11, 0xb8, + 0x23, 0xec, 0x58, 0x93, 0x3c, 0xac, 0x12, 0xd2, 0x88, 0x6d, 0x41, 0x3d, 0xc5, 0xfe, 0x1c, 0xdc, 0xb9, 0xf8, 0xd4, 0x51, 0x3e, 0x07, 0xe5, 0x03, 0x6f, 0xa7, 0x12, 0xe8, 0x12, + 0xf7, 0xb5, 0xce, 0xa6, 0x96, 0x55, 0x3f, 0x78, 0xb4, 0x64, 0x82, 0x50, 0xd2, 0x33, 0x5f, 0x91, 0x02, 0x03, 0x01, 0x00, 0x01 ]); export const SERVICE_CERTIFICATE_CHALLENGE = new Uint8Array([0x08, 0x04]); const COMMON_SERVICE_CERTIFICATE = new Uint8Array([ - 0x08, 0x05, 0x12, 0xc7, 0x05, 0x0a, 0xc1, 0x02, 0x08, 0x03, 0x12, 0x10, 0x17, 0x05, 0xb9, 0x17, 0xcc, 0x12, 0x04, 0x86, 0x8b, 0x06, 0x33, 0x3a, 0x2f, 0x77, 0x2a, 0x8c, 0x18, - 0x82, 0xb4, 0x82, 0x92, 0x05, 0x22, 0x8e, 0x02, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0x99, 0xed, 0x5b, 0x3b, 0x32, 0x7d, 0xab, 0x5e, 0x24, 0xef, 0xc3, 0xb6, - 0x2a, 0x95, 0xb5, 0x98, 0x52, 0x0a, 0xd5, 0xbc, 0xcb, 0x37, 0x50, 0x3e, 0x06, 0x45, 0xb8, 0x14, 0xd8, 0x76, 0xb8, 0xdf, 0x40, 0x51, 0x04, 0x41, 0xad, 0x8c, 0xe3, 0xad, 0xb1, - 0x1b, 0xb8, 0x8c, 0x4e, 0x72, 0x5a, 0x5e, 0x4a, 0x9e, 0x07, 0x95, 0x29, 0x1d, 0x58, 0x58, 0x40, 0x23, 0xa7, 0xe1, 0xaf, 0x0e, 0x38, 0xa9, 0x12, 0x79, 0x39, 0x30, 0x08, 0x61, - 0x0b, 0x6f, 0x15, 0x8c, 0x87, 0x8c, 0x7e, 0x21, 0xbf, 0xfb, 0xfe, 0xea, 0x77, 0xe1, 0x01, 0x9e, 0x1e, 0x57, 0x81, 0xe8, 0xa4, 0x5f, 0x46, 0x26, 0x3d, 0x14, 0xe6, 0x0e, 0x80, - 0x58, 0xa8, 0x60, 0x7a, 0xdc, 0xe0, 0x4f, 0xac, 0x84, 0x57, 0xb1, 0x37, 0xa8, 0xd6, 0x7c, 0xcd, 0xeb, 0x33, 0x70, 0x5d, 0x98, 0x3a, 0x21, 0xfb, 0x4e, 0xec, 0xbd, 0x4a, 0x10, - 0xca, 0x47, 0x49, 0x0c, 0xa4, 0x7e, 0xaa, 0x5d, 0x43, 0x82, 0x18, 0xdd, 0xba, 0xf1, 0xca, 0xde, 0x33, 0x92, 0xf1, 0x3d, 0x6f, 0xfb, 0x64, 0x42, 0xfd, 0x31, 0xe1, 0xbf, 0x40, - 0xb0, 0xc6, 0x04, 0xd1, 0xc4, 0xba, 0x4c, 0x95, 0x20, 0xa4, 0xbf, 0x97, 0xee, 0xbd, 0x60, 0x92, 0x9a, 0xfc, 0xee, 0xf5, 0x5b, 0xba, 0xf5, 0x64, 0xe2, 0xd0, 0xe7, 0x6c, 0xd7, - 0xc5, 0x5c, 0x73, 0xa0, 0x82, 0xb9, 0x96, 0x12, 0x0b, 0x83, 0x59, 0xed, 0xce, 0x24, 0x70, 0x70, 0x82, 0x68, 0x0d, 0x6f, 0x67, 0xc6, 0xd8, 0x2c, 0x4a, 0xc5, 0xf3, 0x13, 0x44, - 0x90, 0xa7, 0x4e, 0xec, 0x37, 0xaf, 0x4b, 0x2f, 0x01, 0x0c, 0x59, 0xe8, 0x28, 0x43, 0xe2, 0x58, 0x2f, 0x0b, 0x6b, 0x9f, 0x5d, 0xb0, 0xfc, 0x5e, 0x6e, 0xdf, 0x64, 0xfb, 0xd3, - 0x08, 0xb4, 0x71, 0x1b, 0xcf, 0x12, 0x50, 0x01, 0x9c, 0x9f, 0x5a, 0x09, 0x02, 0x03, 0x01, 0x00, 0x01, 0x3a, 0x14, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x2e, 0x77, 0x69, - 0x64, 0x65, 0x76, 0x69, 0x6e, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x12, 0x80, 0x03, 0xae, 0x34, 0x73, 0x14, 0xb5, 0xa8, 0x35, 0x29, 0x7f, 0x27, 0x13, 0x88, 0xfb, 0x7b, 0xb8, 0xcb, - 0x52, 0x77, 0xd2, 0x49, 0x82, 0x3c, 0xdd, 0xd1, 0xda, 0x30, 0xb9, 0x33, 0x39, 0x51, 0x1e, 0xb3, 0xcc, 0xbd, 0xea, 0x04, 0xb9, 0x44, 0xb9, 0x27, 0xc1, 0x21, 0x34, 0x6e, 0xfd, - 0xbd, 0xea, 0xc9, 0xd4, 0x13, 0x91, 0x7e, 0x6e, 0xc1, 0x76, 0xa1, 0x04, 0x38, 0x46, 0x0a, 0x50, 0x3b, 0xc1, 0x95, 0x2b, 0x9b, 0xa4, 0xe4, 0xce, 0x0f, 0xc4, 0xbf, 0xc2, 0x0a, - 0x98, 0x08, 0xaa, 0xaf, 0x4b, 0xfc, 0xd1, 0x9c, 0x1d, 0xcf, 0xcd, 0xf5, 0x74, 0xcc, 0xac, 0x28, 0xd1, 0xb4, 0x10, 0x41, 0x6c, 0xf9, 0xde, 0x88, 0x04, 0x30, 0x1c, 0xbd, 0xb3, - 0x34, 0xca, 0xfc, 0xd0, 0xd4, 0x09, 0x78, 0x42, 0x3a, 0x64, 0x2e, 0x54, 0x61, 0x3d, 0xf0, 0xaf, 0xcf, 0x96, 0xca, 0x4a, 0x92, 0x49, 0xd8, 0x55, 0xe4, 0x2b, 0x3a, 0x70, 0x3e, - 0xf1, 0x76, 0x7f, 0x6a, 0x9b, 0xd3, 0x6d, 0x6b, 0xf8, 0x2b, 0xe7, 0x6b, 0xbf, 0x0c, 0xba, 0x4f, 0xde, 0x59, 0xd2, 0xab, 0xcc, 0x76, 0xfe, 0xb6, 0x42, 0x47, 0xb8, 0x5c, 0x43, - 0x1f, 0xbc, 0xa5, 0x22, 0x66, 0xb6, 0x19, 0xfc, 0x36, 0x97, 0x95, 0x43, 0xfc, 0xa9, 0xcb, 0xbd, 0xbb, 0xfa, 0xfa, 0x0e, 0x1a, 0x55, 0xe7, 0x55, 0xa3, 0xc7, 0xbc, 0xe6, 0x55, - 0xf9, 0x64, 0x6f, 0x58, 0x2a, 0xb9, 0xcf, 0x70, 0xaa, 0x08, 0xb9, 0x79, 0xf8, 0x67, 0xf6, 0x3a, 0x0b, 0x2b, 0x7f, 0xdb, 0x36, 0x2c, 0x5b, 0xc4, 0xec, 0xd5, 0x55, 0xd8, 0x5b, - 0xca, 0xa9, 0xc5, 0x93, 0xc3, 0x83, 0xc8, 0x57, 0xd4, 0x9d, 0xaa, 0xb7, 0x7e, 0x40, 0xb7, 0x85, 0x1d, 0xdf, 0xd2, 0x49, 0x98, 0x80, 0x8e, 0x35, 0xb2, 0x58, 0xe7, 0x5d, 0x78, - 0xea, 0xc0, 0xca, 0x16, 0xf7, 0x04, 0x73, 0x04, 0xc2, 0x0d, 0x93, 0xed, 0xe4, 0xe8, 0xff, 0x1c, 0x6f, 0x17, 0xe6, 0x24, 0x3e, 0x3f, 0x3d, 0xa8, 0xfc, 0x17, 0x09, 0x87, 0x0e, - 0xc4, 0x5f, 0xba, 0x82, 0x3a, 0x26, 0x3f, 0x0c, 0xef, 0xa1, 0xf7, 0x09, 0x3b, 0x19, 0x09, 0x92, 0x83, 0x26, 0x33, 0x37, 0x05, 0x04, 0x3a, 0x29, 0xbd, 0xa6, 0xf9, 0xb4, 0x34, - 0x2c, 0xc8, 0xdf, 0x54, 0x3c, 0xb1, 0xa1, 0x18, 0x2f, 0x7c, 0x5f, 0xff, 0x33, 0xf1, 0x04, 0x90, 0xfa, 0xca, 0x5b, 0x25, 0x36, 0x0b, 0x76, 0x01, 0x5e, 0x9c, 0x5a, 0x06, 0xab, - 0x8e, 0xe0, 0x2f, 0x00, 0xd2, 0xe8, 0xd5, 0x98, 0x61, 0x04, 0xaa, 0xcc, 0x4d, 0xd4, 0x75, 0xfd, 0x96, 0xee, 0x9c, 0xe4, 0xe3, 0x26, 0xf2, 0x1b, 0x83, 0xc7, 0x05, 0x85, 0x77, - 0xb3, 0x87, 0x32, 0xcd, 0xda, 0xbc, 0x6a, 0x6b, 0xed, 0x13, 0xfb, 0x0d, 0x49, 0xd3, 0x8a, 0x45, 0xeb, 0x87, 0xa5, 0xf4 + 0x08, 0x05, 0x12, 0xc7, 0x05, 0x0a, 0xc1, 0x02, 0x08, 0x03, 0x12, 0x10, 0x17, 0x05, 0xb9, 0x17, 0xcc, 0x12, 0x04, 0x86, 0x8b, 0x06, 0x33, 0x3a, 0x2f, 0x77, 0x2a, 0x8c, 0x18, + 0x82, 0xb4, 0x82, 0x92, 0x05, 0x22, 0x8e, 0x02, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0x99, 0xed, 0x5b, 0x3b, 0x32, 0x7d, 0xab, 0x5e, 0x24, 0xef, 0xc3, 0xb6, + 0x2a, 0x95, 0xb5, 0x98, 0x52, 0x0a, 0xd5, 0xbc, 0xcb, 0x37, 0x50, 0x3e, 0x06, 0x45, 0xb8, 0x14, 0xd8, 0x76, 0xb8, 0xdf, 0x40, 0x51, 0x04, 0x41, 0xad, 0x8c, 0xe3, 0xad, 0xb1, + 0x1b, 0xb8, 0x8c, 0x4e, 0x72, 0x5a, 0x5e, 0x4a, 0x9e, 0x07, 0x95, 0x29, 0x1d, 0x58, 0x58, 0x40, 0x23, 0xa7, 0xe1, 0xaf, 0x0e, 0x38, 0xa9, 0x12, 0x79, 0x39, 0x30, 0x08, 0x61, + 0x0b, 0x6f, 0x15, 0x8c, 0x87, 0x8c, 0x7e, 0x21, 0xbf, 0xfb, 0xfe, 0xea, 0x77, 0xe1, 0x01, 0x9e, 0x1e, 0x57, 0x81, 0xe8, 0xa4, 0x5f, 0x46, 0x26, 0x3d, 0x14, 0xe6, 0x0e, 0x80, + 0x58, 0xa8, 0x60, 0x7a, 0xdc, 0xe0, 0x4f, 0xac, 0x84, 0x57, 0xb1, 0x37, 0xa8, 0xd6, 0x7c, 0xcd, 0xeb, 0x33, 0x70, 0x5d, 0x98, 0x3a, 0x21, 0xfb, 0x4e, 0xec, 0xbd, 0x4a, 0x10, + 0xca, 0x47, 0x49, 0x0c, 0xa4, 0x7e, 0xaa, 0x5d, 0x43, 0x82, 0x18, 0xdd, 0xba, 0xf1, 0xca, 0xde, 0x33, 0x92, 0xf1, 0x3d, 0x6f, 0xfb, 0x64, 0x42, 0xfd, 0x31, 0xe1, 0xbf, 0x40, + 0xb0, 0xc6, 0x04, 0xd1, 0xc4, 0xba, 0x4c, 0x95, 0x20, 0xa4, 0xbf, 0x97, 0xee, 0xbd, 0x60, 0x92, 0x9a, 0xfc, 0xee, 0xf5, 0x5b, 0xba, 0xf5, 0x64, 0xe2, 0xd0, 0xe7, 0x6c, 0xd7, + 0xc5, 0x5c, 0x73, 0xa0, 0x82, 0xb9, 0x96, 0x12, 0x0b, 0x83, 0x59, 0xed, 0xce, 0x24, 0x70, 0x70, 0x82, 0x68, 0x0d, 0x6f, 0x67, 0xc6, 0xd8, 0x2c, 0x4a, 0xc5, 0xf3, 0x13, 0x44, + 0x90, 0xa7, 0x4e, 0xec, 0x37, 0xaf, 0x4b, 0x2f, 0x01, 0x0c, 0x59, 0xe8, 0x28, 0x43, 0xe2, 0x58, 0x2f, 0x0b, 0x6b, 0x9f, 0x5d, 0xb0, 0xfc, 0x5e, 0x6e, 0xdf, 0x64, 0xfb, 0xd3, + 0x08, 0xb4, 0x71, 0x1b, 0xcf, 0x12, 0x50, 0x01, 0x9c, 0x9f, 0x5a, 0x09, 0x02, 0x03, 0x01, 0x00, 0x01, 0x3a, 0x14, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x2e, 0x77, 0x69, + 0x64, 0x65, 0x76, 0x69, 0x6e, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x12, 0x80, 0x03, 0xae, 0x34, 0x73, 0x14, 0xb5, 0xa8, 0x35, 0x29, 0x7f, 0x27, 0x13, 0x88, 0xfb, 0x7b, 0xb8, 0xcb, + 0x52, 0x77, 0xd2, 0x49, 0x82, 0x3c, 0xdd, 0xd1, 0xda, 0x30, 0xb9, 0x33, 0x39, 0x51, 0x1e, 0xb3, 0xcc, 0xbd, 0xea, 0x04, 0xb9, 0x44, 0xb9, 0x27, 0xc1, 0x21, 0x34, 0x6e, 0xfd, + 0xbd, 0xea, 0xc9, 0xd4, 0x13, 0x91, 0x7e, 0x6e, 0xc1, 0x76, 0xa1, 0x04, 0x38, 0x46, 0x0a, 0x50, 0x3b, 0xc1, 0x95, 0x2b, 0x9b, 0xa4, 0xe4, 0xce, 0x0f, 0xc4, 0xbf, 0xc2, 0x0a, + 0x98, 0x08, 0xaa, 0xaf, 0x4b, 0xfc, 0xd1, 0x9c, 0x1d, 0xcf, 0xcd, 0xf5, 0x74, 0xcc, 0xac, 0x28, 0xd1, 0xb4, 0x10, 0x41, 0x6c, 0xf9, 0xde, 0x88, 0x04, 0x30, 0x1c, 0xbd, 0xb3, + 0x34, 0xca, 0xfc, 0xd0, 0xd4, 0x09, 0x78, 0x42, 0x3a, 0x64, 0x2e, 0x54, 0x61, 0x3d, 0xf0, 0xaf, 0xcf, 0x96, 0xca, 0x4a, 0x92, 0x49, 0xd8, 0x55, 0xe4, 0x2b, 0x3a, 0x70, 0x3e, + 0xf1, 0x76, 0x7f, 0x6a, 0x9b, 0xd3, 0x6d, 0x6b, 0xf8, 0x2b, 0xe7, 0x6b, 0xbf, 0x0c, 0xba, 0x4f, 0xde, 0x59, 0xd2, 0xab, 0xcc, 0x76, 0xfe, 0xb6, 0x42, 0x47, 0xb8, 0x5c, 0x43, + 0x1f, 0xbc, 0xa5, 0x22, 0x66, 0xb6, 0x19, 0xfc, 0x36, 0x97, 0x95, 0x43, 0xfc, 0xa9, 0xcb, 0xbd, 0xbb, 0xfa, 0xfa, 0x0e, 0x1a, 0x55, 0xe7, 0x55, 0xa3, 0xc7, 0xbc, 0xe6, 0x55, + 0xf9, 0x64, 0x6f, 0x58, 0x2a, 0xb9, 0xcf, 0x70, 0xaa, 0x08, 0xb9, 0x79, 0xf8, 0x67, 0xf6, 0x3a, 0x0b, 0x2b, 0x7f, 0xdb, 0x36, 0x2c, 0x5b, 0xc4, 0xec, 0xd5, 0x55, 0xd8, 0x5b, + 0xca, 0xa9, 0xc5, 0x93, 0xc3, 0x83, 0xc8, 0x57, 0xd4, 0x9d, 0xaa, 0xb7, 0x7e, 0x40, 0xb7, 0x85, 0x1d, 0xdf, 0xd2, 0x49, 0x98, 0x80, 0x8e, 0x35, 0xb2, 0x58, 0xe7, 0x5d, 0x78, + 0xea, 0xc0, 0xca, 0x16, 0xf7, 0x04, 0x73, 0x04, 0xc2, 0x0d, 0x93, 0xed, 0xe4, 0xe8, 0xff, 0x1c, 0x6f, 0x17, 0xe6, 0x24, 0x3e, 0x3f, 0x3d, 0xa8, 0xfc, 0x17, 0x09, 0x87, 0x0e, + 0xc4, 0x5f, 0xba, 0x82, 0x3a, 0x26, 0x3f, 0x0c, 0xef, 0xa1, 0xf7, 0x09, 0x3b, 0x19, 0x09, 0x92, 0x83, 0x26, 0x33, 0x37, 0x05, 0x04, 0x3a, 0x29, 0xbd, 0xa6, 0xf9, 0xb4, 0x34, + 0x2c, 0xc8, 0xdf, 0x54, 0x3c, 0xb1, 0xa1, 0x18, 0x2f, 0x7c, 0x5f, 0xff, 0x33, 0xf1, 0x04, 0x90, 0xfa, 0xca, 0x5b, 0x25, 0x36, 0x0b, 0x76, 0x01, 0x5e, 0x9c, 0x5a, 0x06, 0xab, + 0x8e, 0xe0, 0x2f, 0x00, 0xd2, 0xe8, 0xd5, 0x98, 0x61, 0x04, 0xaa, 0xcc, 0x4d, 0xd4, 0x75, 0xfd, 0x96, 0xee, 0x9c, 0xe4, 0xe3, 0x26, 0xf2, 0x1b, 0x83, 0xc7, 0x05, 0x85, 0x77, + 0xb3, 0x87, 0x32, 0xcd, 0xda, 0xbc, 0x6a, 0x6b, 0xed, 0x13, 0xfb, 0x0d, 0x49, 0xd3, 0x8a, 0x45, 0xeb, 0x87, 0xa5, 0xf4 ]); export type KeyContainer = { @@ -86,216 +86,216 @@ export type ContentDecryptionModule = { }; export class Session { - private _devicePrivateKey: forge.pki.rsa.PrivateKey; - private _identifierBlob: ClientIdentification; - private _pssh: Buffer; - private _rawLicenseRequest?: Buffer; - private _serviceCertificate?: SignedDrmCertificate; + private _devicePrivateKey: forge.pki.rsa.PrivateKey; + private _identifierBlob: ClientIdentification; + private _pssh: Buffer; + private _rawLicenseRequest?: Buffer; + private _serviceCertificate?: SignedDrmCertificate; - constructor(contentDecryptionModule: ContentDecryptionModule, pssh: Buffer) { - this._devicePrivateKey = forge.pki.privateKeyFromPem(contentDecryptionModule.privateKey.toString('binary')); + constructor(contentDecryptionModule: ContentDecryptionModule, pssh: Buffer) { + this._devicePrivateKey = forge.pki.privateKeyFromPem(contentDecryptionModule.privateKey.toString('binary')); - this._identifierBlob = fromBinary(ClientIdentificationSchema, contentDecryptionModule.identifierBlob); - this._pssh = pssh; - } - - async setDefaultServiceCertificate() { - await this.setServiceCertificate(Buffer.from(COMMON_SERVICE_CERTIFICATE)); - } - - async setServiceCertificateFromMessage(rawSignedMessage: Buffer) { - const signedMessage: SignedMessage = fromBinary(SignedMessageSchema, rawSignedMessage); - if (!signedMessage.msg) { - throw new Error('the service certificate message does not contain a message'); - } - await this.setServiceCertificate(Buffer.from(signedMessage.msg)); - } - - async setServiceCertificate(serviceCertificate: Buffer) { - const signedServiceCertificate: SignedDrmCertificate = fromBinary(SignedDrmCertificateSchema, serviceCertificate); - if (!(await this._verifyServiceCertificate(signedServiceCertificate))) { - throw new Error('Service certificate is not signed by the Widevine root certificate'); - } - this._serviceCertificate = signedServiceCertificate; - } - - createLicenseRequest(licenseType: LicenseType = LicenseType.STREAMING, android: boolean = false): Buffer { - if (!this._pssh.subarray(12, 28).equals(Buffer.from(WIDEVINE_SYSTEM_ID))) { - throw new Error('the pssh is not an actuall pssh'); + this._identifierBlob = fromBinary(ClientIdentificationSchema, contentDecryptionModule.identifierBlob); + this._pssh = pssh; } - const pssh = this._parsePSSH(this._pssh); - if (!pssh) { - throw new Error('pssh is invalid'); + async setDefaultServiceCertificate() { + await this.setServiceCertificate(Buffer.from(COMMON_SERVICE_CERTIFICATE)); } - const licenseRequest: LicenseRequest = create(LicenseRequestSchema, { - type: LicenseRequest_RequestType.NEW, - contentId: create(LicenseRequest_ContentIdentificationSchema, { - contentIdVariant: { - case: 'widevinePsshData', - value: create(LicenseRequest_ContentIdentification_WidevinePsshDataSchema, { - psshData: [this._pssh.subarray(32)], - licenseType: licenseType, - requestId: android ? this._generateAndroidIdentifier() : this._generateGenericIdentifier() - }) + async setServiceCertificateFromMessage(rawSignedMessage: Buffer) { + const signedMessage: SignedMessage = fromBinary(SignedMessageSchema, rawSignedMessage); + if (!signedMessage.msg) { + throw new Error('the service certificate message does not contain a message'); } - }), - requestTime: BigInt(Date.now()) / BigInt(1000), - protocolVersion: ProtocolVersion.VERSION_2_1, - keyControlNonce: Math.floor(Math.random() * 2 ** 31) - }); - - if (this._serviceCertificate) { - const encryptedClientIdentification = this._encryptClientIdentification(this._identifierBlob, this._serviceCertificate); - licenseRequest.encryptedClientId = encryptedClientIdentification; - } else { - licenseRequest.clientId = this._identifierBlob; + await this.setServiceCertificate(Buffer.from(signedMessage.msg)); } - this._rawLicenseRequest = Buffer.from(toBinary(LicenseRequestSchema, licenseRequest)); - - const pss: forge.pss.PSS = forge.pss.create({ md: forge.md.sha1.create(), mgf: forge.mgf.mgf1.create(forge.md.sha1.create()), saltLength: 20 }); - const md = forge.md.sha1.create(); - md.update(this._rawLicenseRequest.toString('binary'), 'raw'); - const signature = Buffer.from(this._devicePrivateKey.sign(md, pss), 'binary'); - - const signedLicenseRequest: SignedMessage = create(SignedMessageSchema, { - type: SignedMessage_MessageType.LICENSE_REQUEST, - msg: this._rawLicenseRequest, - signature: signature - }); - - return Buffer.from(toBinary(SignedMessageSchema, signedLicenseRequest)); - } - - parseLicense(rawLicense: Buffer) { - if (!this._rawLicenseRequest) { - throw new Error('please request a license first'); + async setServiceCertificate(serviceCertificate: Buffer) { + const signedServiceCertificate: SignedDrmCertificate = fromBinary(SignedDrmCertificateSchema, serviceCertificate); + if (!(await this._verifyServiceCertificate(signedServiceCertificate))) { + throw new Error('Service certificate is not signed by the Widevine root certificate'); + } + this._serviceCertificate = signedServiceCertificate; } - const signedLicense = fromBinary(SignedMessageSchema, rawLicense); - if (!signedLicense.sessionKey) { - throw new Error('the license does not contain a session key'); - } - if (!signedLicense.msg) { - throw new Error('the license does not contain a message'); - } - if (!signedLicense.signature) { - throw new Error('the license does not contain a signature'); + createLicenseRequest(licenseType: LicenseType = LicenseType.STREAMING, android: boolean = false): Buffer { + if (!this._pssh.subarray(12, 28).equals(Buffer.from(WIDEVINE_SYSTEM_ID))) { + throw new Error('the pssh is not an actuall pssh'); + } + + const pssh = this._parsePSSH(this._pssh); + if (!pssh) { + throw new Error('pssh is invalid'); + } + + const licenseRequest: LicenseRequest = create(LicenseRequestSchema, { + type: LicenseRequest_RequestType.NEW, + contentId: create(LicenseRequest_ContentIdentificationSchema, { + contentIdVariant: { + case: 'widevinePsshData', + value: create(LicenseRequest_ContentIdentification_WidevinePsshDataSchema, { + psshData: [this._pssh.subarray(32)], + licenseType: licenseType, + requestId: android ? this._generateAndroidIdentifier() : this._generateGenericIdentifier() + }) + } + }), + requestTime: BigInt(Date.now()) / BigInt(1000), + protocolVersion: ProtocolVersion.VERSION_2_1, + keyControlNonce: Math.floor(Math.random() * 2 ** 31) + }); + + if (this._serviceCertificate) { + const encryptedClientIdentification = this._encryptClientIdentification(this._identifierBlob, this._serviceCertificate); + licenseRequest.encryptedClientId = encryptedClientIdentification; + } else { + licenseRequest.clientId = this._identifierBlob; + } + + this._rawLicenseRequest = Buffer.from(toBinary(LicenseRequestSchema, licenseRequest)); + + const pss: forge.pss.PSS = forge.pss.create({ md: forge.md.sha1.create(), mgf: forge.mgf.mgf1.create(forge.md.sha1.create()), saltLength: 20 }); + const md = forge.md.sha1.create(); + md.update(this._rawLicenseRequest.toString('binary'), 'raw'); + const signature = Buffer.from(this._devicePrivateKey.sign(md, pss), 'binary'); + + const signedLicenseRequest: SignedMessage = create(SignedMessageSchema, { + type: SignedMessage_MessageType.LICENSE_REQUEST, + msg: this._rawLicenseRequest, + signature: signature + }); + + return Buffer.from(toBinary(SignedMessageSchema, signedLicenseRequest)); } - const sessionKey = this._devicePrivateKey.decrypt(Buffer.from(signedLicense.sessionKey).toString('binary'), 'RSA-OAEP', { - md: forge.md.sha1.create() - }); + parseLicense(rawLicense: Buffer) { + if (!this._rawLicenseRequest) { + throw new Error('please request a license first'); + } - const cmac = new AES_CMAC(Buffer.from(sessionKey, 'binary')); + const signedLicense = fromBinary(SignedMessageSchema, rawLicense); + if (!signedLicense.sessionKey) { + throw new Error('the license does not contain a session key'); + } + if (!signedLicense.msg) { + throw new Error('the license does not contain a message'); + } + if (!signedLicense.signature) { + throw new Error('the license does not contain a signature'); + } - const encKeyBase = Buffer.concat([Buffer.from('ENCRYPTION'), Buffer.from('\x00', 'ascii'), this._rawLicenseRequest, Buffer.from('\x00\x00\x00\x80', 'ascii')]); - const authKeyBase = Buffer.concat([Buffer.from('AUTHENTICATION'), Buffer.from('\x00', 'ascii'), this._rawLicenseRequest, Buffer.from('\x00\x00\x02\x00', 'ascii')]); + const sessionKey = this._devicePrivateKey.decrypt(Buffer.from(signedLicense.sessionKey).toString('binary'), 'RSA-OAEP', { + md: forge.md.sha1.create() + }); - const encKey = cmac.calculate(Buffer.concat([Buffer.from('\x01'), encKeyBase])); - const serverKey = Buffer.concat([cmac.calculate(Buffer.concat([Buffer.from('\x01'), authKeyBase])), cmac.calculate(Buffer.concat([Buffer.from('\x02'), authKeyBase]))]); - /*const clientKey = Buffer.concat([ + const cmac = new AES_CMAC(Buffer.from(sessionKey, 'binary')); + + const encKeyBase = Buffer.concat([Buffer.from('ENCRYPTION'), Buffer.from('\x00', 'ascii'), this._rawLicenseRequest, Buffer.from('\x00\x00\x00\x80', 'ascii')]); + const authKeyBase = Buffer.concat([Buffer.from('AUTHENTICATION'), Buffer.from('\x00', 'ascii'), this._rawLicenseRequest, Buffer.from('\x00\x00\x02\x00', 'ascii')]); + + const encKey = cmac.calculate(Buffer.concat([Buffer.from('\x01'), encKeyBase])); + const serverKey = Buffer.concat([cmac.calculate(Buffer.concat([Buffer.from('\x01'), authKeyBase])), cmac.calculate(Buffer.concat([Buffer.from('\x02'), authKeyBase]))]); + /*const clientKey = Buffer.concat([ cmac.calculate(Buffer.concat([Buffer.from("\x03"), authKeyBase])), cmac.calculate(Buffer.concat([Buffer.from("\x04"), authKeyBase])) ]);*/ - const hmac = forge.hmac.create(); - hmac.start(forge.md.sha256.create(), serverKey.toString('binary')); - hmac.update(Buffer.from(signedLicense.msg).toString('binary')); - const calculatedSignature = Buffer.from(hmac.digest().data, 'binary'); + const hmac = forge.hmac.create(); + hmac.start(forge.md.sha256.create(), serverKey.toString('binary')); + hmac.update(Buffer.from(signedLicense.msg).toString('binary')); + const calculatedSignature = Buffer.from(hmac.digest().data, 'binary'); - if (!calculatedSignature.equals(signedLicense.signature)) { - throw new Error('signatures do not match'); + if (!calculatedSignature.equals(signedLicense.signature)) { + throw new Error('signatures do not match'); + } + + const license = fromBinary(LicenseSchema, signedLicense.msg); + + const keyContainers = license.key.map((keyContainer) => { + if (keyContainer.type && keyContainer.key && keyContainer.iv) { + const keyId = keyContainer.id ? Buffer.from(keyContainer.id).toString('hex') : '00000000000000000000000000000000'; + const decipher = forge.cipher.createDecipher('AES-CBC', encKey.toString('binary')); + decipher.start({ iv: Buffer.from(keyContainer.iv).toString('binary') }); + decipher.update(forge.util.createBuffer(new Uint8Array(keyContainer.key))); + decipher.finish(); + const decryptedKey = Buffer.from(decipher.output.data, 'binary'); + const key: KeyContainer = { + kid: keyId, + key: decryptedKey.toString('hex') + }; + return key; + } + }); + if (keyContainers.filter((container) => !!container).length < 1) { + throw new Error('there was not a single valid key in the response'); + } + return keyContainers; } - const license = fromBinary(LicenseSchema, signedLicense.msg); + private _encryptClientIdentification(clientIdentification: ClientIdentification, signedServiceCertificate: SignedDrmCertificate): EncryptedClientIdentification { + if (!signedServiceCertificate.drmCertificate) { + throw new Error('the service certificate does not contain an actual certificate'); + } - const keyContainers = license.key.map((keyContainer) => { - if (keyContainer.type && keyContainer.key && keyContainer.iv) { - const keyId = keyContainer.id ? Buffer.from(keyContainer.id).toString('hex') : '00000000000000000000000000000000'; - const decipher = forge.cipher.createDecipher('AES-CBC', encKey.toString('binary')); - decipher.start({ iv: Buffer.from(keyContainer.iv).toString('binary') }); - decipher.update(forge.util.createBuffer(new Uint8Array(keyContainer.key))); - decipher.finish(); - const decryptedKey = Buffer.from(decipher.output.data, 'binary'); - const key: KeyContainer = { - kid: keyId, - key: decryptedKey.toString('hex') - }; - return key; - } - }); - if (keyContainers.filter((container) => !!container).length < 1) { - throw new Error('there was not a single valid key in the response'); - } - return keyContainers; - } + const serviceCertificate = fromBinary(DrmCertificateSchema, signedServiceCertificate.drmCertificate); + if (!serviceCertificate.publicKey) { + throw new Error('the service certificate does not contain a public key'); + } - private _encryptClientIdentification(clientIdentification: ClientIdentification, signedServiceCertificate: SignedDrmCertificate): EncryptedClientIdentification { - if (!signedServiceCertificate.drmCertificate) { - throw new Error('the service certificate does not contain an actual certificate'); + const key = forge.random.getBytesSync(16); + const iv = forge.random.getBytesSync(16); + const cipher = forge.cipher.createCipher('AES-CBC', key); + cipher.start({ iv: iv }); + cipher.update(forge.util.createBuffer(toBinary(ClientIdentificationSchema, clientIdentification))); + cipher.finish(); + const rawEncryptedClientIdentification = Buffer.from(cipher.output.data, 'binary'); + + const publicKey = forge.pki.publicKeyFromAsn1(forge.asn1.fromDer(Buffer.from(serviceCertificate.publicKey).toString('binary'))); + const encryptedKey = publicKey.encrypt(key, 'RSA-OAEP', { md: forge.md.sha1.create() }); + + const encryptedClientIdentification: EncryptedClientIdentification = create(EncryptedClientIdentificationSchema, { + encryptedClientId: rawEncryptedClientIdentification, + encryptedClientIdIv: Buffer.from(iv, 'binary'), + encryptedPrivacyKey: Buffer.from(encryptedKey, 'binary'), + providerId: serviceCertificate.providerId, + serviceCertificateSerialNumber: serviceCertificate.serialNumber + }); + return encryptedClientIdentification; } - const serviceCertificate = fromBinary(DrmCertificateSchema, signedServiceCertificate.drmCertificate); - if (!serviceCertificate.publicKey) { - throw new Error('the service certificate does not contain a public key'); + private async _verifyServiceCertificate(signedServiceCertificate: SignedDrmCertificate): Promise<boolean> { + if (!signedServiceCertificate.drmCertificate) { + throw new Error('the service certificate does not contain an actual certificate'); + } + if (!signedServiceCertificate.signature) { + throw new Error('the service certificate does not contain a signature'); + } + + const publicKey = forge.pki.publicKeyFromAsn1(forge.asn1.fromDer(Buffer.from(WIDEVINE_ROOT_PUBLIC_KEY).toString('binary'))); + const pss: forge.pss.PSS = forge.pss.create({ md: forge.md.sha1.create(), mgf: forge.mgf.mgf1.create(forge.md.sha1.create()), saltLength: 20 }); + const sha1 = forge.md.sha1.create(); + sha1.update(Buffer.from(signedServiceCertificate.drmCertificate).toString('binary'), 'raw'); + return publicKey.verify(sha1.digest().bytes(), Buffer.from(signedServiceCertificate.signature).toString('binary'), pss); } - const key = forge.random.getBytesSync(16); - const iv = forge.random.getBytesSync(16); - const cipher = forge.cipher.createCipher('AES-CBC', key); - cipher.start({ iv: iv }); - cipher.update(forge.util.createBuffer(toBinary(ClientIdentificationSchema, clientIdentification))); - cipher.finish(); - const rawEncryptedClientIdentification = Buffer.from(cipher.output.data, 'binary'); - - const publicKey = forge.pki.publicKeyFromAsn1(forge.asn1.fromDer(Buffer.from(serviceCertificate.publicKey).toString('binary'))); - const encryptedKey = publicKey.encrypt(key, 'RSA-OAEP', { md: forge.md.sha1.create() }); - - const encryptedClientIdentification: EncryptedClientIdentification = create(EncryptedClientIdentificationSchema, { - encryptedClientId: rawEncryptedClientIdentification, - encryptedClientIdIv: Buffer.from(iv, 'binary'), - encryptedPrivacyKey: Buffer.from(encryptedKey, 'binary'), - providerId: serviceCertificate.providerId, - serviceCertificateSerialNumber: serviceCertificate.serialNumber - }); - return encryptedClientIdentification; - } - - private async _verifyServiceCertificate(signedServiceCertificate: SignedDrmCertificate): Promise<boolean> { - if (!signedServiceCertificate.drmCertificate) { - throw new Error('the service certificate does not contain an actual certificate'); - } - if (!signedServiceCertificate.signature) { - throw new Error('the service certificate does not contain a signature'); + private _parsePSSH(pssh: Buffer): WidevinePsshData | null { + try { + return fromBinary(WidevinePsshDataSchema, pssh.subarray(32)); + } catch { + return null; + } } - const publicKey = forge.pki.publicKeyFromAsn1(forge.asn1.fromDer(Buffer.from(WIDEVINE_ROOT_PUBLIC_KEY).toString('binary'))); - const pss: forge.pss.PSS = forge.pss.create({ md: forge.md.sha1.create(), mgf: forge.mgf.mgf1.create(forge.md.sha1.create()), saltLength: 20 }); - const sha1 = forge.md.sha1.create(); - sha1.update(Buffer.from(signedServiceCertificate.drmCertificate).toString('binary'), 'raw'); - return publicKey.verify(sha1.digest().bytes(), Buffer.from(signedServiceCertificate.signature).toString('binary'), pss); - } - - private _parsePSSH(pssh: Buffer): WidevinePsshData | null { - try { - return fromBinary(WidevinePsshDataSchema, pssh.subarray(32)); - } catch { - return null; + private _generateAndroidIdentifier(): Buffer { + return Buffer.from(`${forge.util.bytesToHex(forge.random.getBytesSync(8))}${'01'}${'00000000000000'}`); } - } - private _generateAndroidIdentifier(): Buffer { - return Buffer.from(`${forge.util.bytesToHex(forge.random.getBytesSync(8))}${'01'}${'00000000000000'}`); - } + private _generateGenericIdentifier(): Buffer { + return Buffer.from(forge.random.getBytesSync(16), 'binary'); + } - private _generateGenericIdentifier(): Buffer { - return Buffer.from(forge.random.getBytesSync(16), 'binary'); - } - - get pssh(): Buffer { - return this._pssh; - } + get pssh(): Buffer { + return this._pssh; + } } From a14466ec5d29accbe81b5ffac6e0a1373d04e356 Mon Sep 17 00:00:00 2001 From: AnimeDL <multidownloadernx@gmail.com> Date: Sat, 27 Sep 2025 10:27:32 -0700 Subject: [PATCH 2/4] Rest of the tab conversion --- .prettierrc | 4 +- @types/adnPlayerConfig.d.ts | 54 +- @types/adnSearch.d.ts | 76 +- @types/adnStreams.d.ts | 62 +- @types/adnSubtitles.d.ts | 12 +- @types/adnVideos.d.ts | 132 +-- @types/animeOnegaiSearch.d.ts | 166 ++-- @types/animeOnegaiSeasons.d.ts | 62 +- @types/animeOnegaiSeries.d.ts | 200 ++--- @types/animeOnegaiStream.d.ts | 72 +- @types/crunchyAndroidEpisodes.d.ts | 208 ++--- @types/crunchyAndroidObject.d.ts | 306 +++---- @types/crunchyAndroidStreams.d.ts | 128 +-- @types/crunchyEpisodeList.d.ts | 200 ++--- @types/crunchySearch.d.ts | 274 +++---- @types/crunchyTypes.d.ts | 344 ++++---- @types/downloadedFile.d.ts | 4 +- @types/enums.ts | 12 +- @types/hidiveDashboard.d.ts | 106 +-- @types/hidiveEpisodeList.d.ts | 134 +-- @types/hidiveSearch.d.ts | 78 +- @types/hidiveTypes.d.ts | 86 +- @types/iso639.d.ts | 14 +- @types/items.d.ts | 262 +++--- @types/m3u8-parsed.d.ts | 94 +-- @types/messageHandler.d.ts | 194 ++--- @types/mpd-parser.d.ts | 192 ++--- @types/newHidiveEpisode.d.ts | 64 +- @types/newHidivePlayback.d.ts | 38 +- @types/newHidiveSearch.d.ts | 100 +-- @types/newHidiveSeason.d.ts | 126 +-- @types/newHidiveSeries.d.ts | 48 +- @types/objectInfo.d.ts | 332 ++++---- @types/pkg.d.ts | 2 +- @types/playbackData.d.ts | 188 ++--- @types/randomEvents.d.ts | 12 +- @types/removeNPMAbsolutePaths.d.ts | 2 +- @types/serviceClassInterface.d.ts | 2 +- @types/streamData.d.ts | 30 +- @types/updateFile.d.ts | 4 +- @types/ws.d.ts | 62 +- eslint.config.mjs | 6 +- gui/react/.babelrc | 2 +- gui/react/package.json | 112 +-- gui/react/public/index.html | 24 +- gui/react/src/@types/FC.d.ts | 2 +- gui/react/src/App.tsx | 6 +- gui/react/src/Layout.tsx | 42 +- gui/react/src/Style.tsx | 18 +- .../src/components/AddToQueue/AddToQueue.tsx | 28 +- .../DownloadSelector/DownloadSelector.tsx | 588 ++++++------- .../Listing/EpisodeListing.tsx | 328 ++++---- .../AddToQueue/SearchBox/SearchBox.tsx | 186 ++--- gui/react/src/components/AuthButton.tsx | 178 ++-- gui/react/src/components/LogoutButton.tsx | 44 +- .../DownloadManager/DownloadManager.tsx | 48 +- .../src/components/MainFrame/MainFrame.tsx | 6 +- .../src/components/MainFrame/Queue/Queue.tsx | 772 +++++++++--------- gui/react/src/components/MenuBar/MenuBar.tsx | 192 ++--- gui/react/src/components/Require.tsx | 6 +- gui/react/src/components/StartQueue.tsx | 56 +- .../src/components/reusable/ContextMenu.tsx | 80 +- .../reusable/LinearProgressWithLabel.tsx | 24 +- .../src/components/reusable/MultiSelect.tsx | 96 +-- gui/react/src/hooks/useStore.tsx | 10 +- gui/react/src/index.tsx | 52 +- gui/react/src/provider/ErrorHandler.tsx | 44 +- gui/react/src/provider/MessageChannel.tsx | 386 ++++----- gui/react/src/provider/QueueProvider.tsx | 42 +- gui/react/src/provider/ServiceProvider.tsx | 40 +- gui/react/src/provider/Store.tsx | 56 +- gui/react/tsconfig.json | 54 +- gui/react/webpack.config.ts | 98 +-- package.json | 238 +++--- tsc.ts | 242 +++--- tsconfig.json | 36 +- 76 files changed, 4315 insertions(+), 4313 deletions(-) diff --git a/.prettierrc b/.prettierrc index 64aca77..0fdfbf3 100644 --- a/.prettierrc +++ b/.prettierrc @@ -11,9 +11,9 @@ "requirePragma": false, "semi": true, "singleQuote": true, - "tabWidth": 2, + "tabWidth": 4, "trailingComma": "none", - "useTabs": false, + "useTabs": true, "vueIndentScriptAndStyle": false, "printWidth": 180, "endOfLine": "auto" diff --git a/@types/adnPlayerConfig.d.ts b/@types/adnPlayerConfig.d.ts index 82a81ec..62634f5 100644 --- a/@types/adnPlayerConfig.d.ts +++ b/@types/adnPlayerConfig.d.ts @@ -1,50 +1,50 @@ export interface ADNPlayerConfig { - player: Player; + player: Player; } export interface Player { - image: string; - options: Options; + image: string; + options: Options; } export interface Options { - user: User; - chromecast: Chromecast; - ios: Ios; - video: Video; - dock: any[]; - preference: Preference; + user: User; + chromecast: Chromecast; + ios: Ios; + video: Video; + dock: any[]; + preference: Preference; } export interface Chromecast { - appId: string; - refreshTokenUrl: string; + appId: string; + refreshTokenUrl: string; } export interface Ios { - videoUrl: string; - appUrl: string; - title: string; + videoUrl: string; + appUrl: string; + title: string; } export interface Preference { - quality: string; - autoplay: boolean; - language: string; - green: boolean; + quality: string; + autoplay: boolean; + language: string; + green: boolean; } export interface User { - hasAccess: boolean; - profileId: number; - refreshToken: string; - refreshTokenUrl: string; + hasAccess: boolean; + profileId: number; + refreshToken: string; + refreshTokenUrl: string; } export interface Video { - startDate: null; - currentDate: Date; - available: boolean; - free: boolean; - url: string; + startDate: null; + currentDate: Date; + available: boolean; + free: boolean; + url: string; } diff --git a/@types/adnSearch.d.ts b/@types/adnSearch.d.ts index a8f47b7..c47b33a 100644 --- a/@types/adnSearch.d.ts +++ b/@types/adnSearch.d.ts @@ -1,46 +1,46 @@ export interface ADNSearch { - shows: ADNSearchShow[]; - total: number; + shows: ADNSearchShow[]; + total: number; } export interface ADNSearchShow { - id: number; - title: string; - type: string; - originalTitle: string; - shortTitle: string; - reference: string; - age: string; - languages: string[]; - summary: string; - image: string; - image2x: string; - imageHorizontal: string; - imageHorizontal2x: string; - url: string; - urlPath: string; - episodeCount: number; - genres: string[]; - copyright: string; - rating: number; - ratingsCount: number; - commentsCount: number; - qualities: string[]; - simulcast: boolean; - free: boolean; - available: boolean; - download: boolean; - basedOn: string; - tagline: null; - firstReleaseYear: string; - productionStudio: string; - countryOfOrigin: string; - productionTeam: ProductionTeam[]; - nextVideoReleaseDate: null; - indexable: boolean; + id: number; + title: string; + type: string; + originalTitle: string; + shortTitle: string; + reference: string; + age: string; + languages: string[]; + summary: string; + image: string; + image2x: string; + imageHorizontal: string; + imageHorizontal2x: string; + url: string; + urlPath: string; + episodeCount: number; + genres: string[]; + copyright: string; + rating: number; + ratingsCount: number; + commentsCount: number; + qualities: string[]; + simulcast: boolean; + free: boolean; + available: boolean; + download: boolean; + basedOn: string; + tagline: null; + firstReleaseYear: string; + productionStudio: string; + countryOfOrigin: string; + productionTeam: ProductionTeam[]; + nextVideoReleaseDate: null; + indexable: boolean; } export interface ProductionTeam { - role: string; - name: string; + role: string; + name: string; } diff --git a/@types/adnStreams.d.ts b/@types/adnStreams.d.ts index 8942942..48dd969 100644 --- a/@types/adnStreams.d.ts +++ b/@types/adnStreams.d.ts @@ -1,51 +1,51 @@ export interface ADNStreams { - links: Links; - video: Video; - metadata: Metadata; + links: Links; + video: Video; + metadata: Metadata; } export interface Links { - streaming: Streaming; - subtitles: Subtitles; - history: string; - nextVideoUrl: string; - previousVideoUrl: string; + streaming: Streaming; + subtitles: Subtitles; + history: string; + nextVideoUrl: string; + previousVideoUrl: string; } export interface Streaming { - [streams: string]: Streams; + [streams: string]: Streams; } export interface Streams { - mobile: string; - sd: string; - hd: string; - fhd: string; - auto: string; + mobile: string; + sd: string; + hd: string; + fhd: string; + auto: string; } export interface Subtitles { - all: string; + all: string; } export interface Metadata { - title: string; - subtitle: string; - summary: null; - rating: number; + title: string; + subtitle: string; + summary: null; + rating: number; } export interface Video { - guid: string; - id: number; - currentTime: number; - duration: number; - url: string; - image: string; - tcEpisodeStart?:string; - tcEpisodeEnd?: string; - tcIntroStart?: string; - tcIntroEnd?: string; - tcEndingStart?: string; - tcEndingEnd?: string; + guid: string; + id: number; + currentTime: number; + duration: number; + url: string; + image: string; + tcEpisodeStart?:string; + tcEpisodeEnd?: string; + tcIntroStart?: string; + tcIntroEnd?: string; + tcEndingStart?: string; + tcEndingEnd?: string; } diff --git a/@types/adnSubtitles.d.ts b/@types/adnSubtitles.d.ts index ec45718..dc2cec1 100644 --- a/@types/adnSubtitles.d.ts +++ b/@types/adnSubtitles.d.ts @@ -1,11 +1,11 @@ export interface ADNSubtitles { - [subtitleLang: string]: Subtitle[]; + [subtitleLang: string]: Subtitle[]; } export interface Subtitle { - startTime: number; - endTime: number; - positionAlign: string; - lineAlign: string; - text: string; + startTime: number; + endTime: number; + positionAlign: string; + lineAlign: string; + text: string; } diff --git a/@types/adnVideos.d.ts b/@types/adnVideos.d.ts index ccf3d0b..27a2332 100644 --- a/@types/adnVideos.d.ts +++ b/@types/adnVideos.d.ts @@ -1,77 +1,77 @@ export interface ADNVideos { - videos: ADNVideo[]; + videos: ADNVideo[]; } export interface ADNVideo { - id: number; - title: string; - name: string; - number: string; - shortNumber: string; - season: string; - reference: string; - type: string; - order: number; - image: string; - image2x: string; - summary: string; - releaseDate: Date; - duration: number; - url: string; - urlPath: string; - embeddedUrl: string; - languages: string[]; - qualities: string[]; - rating: number; - ratingsCount: number; - commentsCount: number; - available: boolean; - download: boolean; - free: boolean; - freeWithAds: boolean; - show: Show; - indexable: boolean; - isSelected?: boolean; + id: number; + title: string; + name: string; + number: string; + shortNumber: string; + season: string; + reference: string; + type: string; + order: number; + image: string; + image2x: string; + summary: string; + releaseDate: Date; + duration: number; + url: string; + urlPath: string; + embeddedUrl: string; + languages: string[]; + qualities: string[]; + rating: number; + ratingsCount: number; + commentsCount: number; + available: boolean; + download: boolean; + free: boolean; + freeWithAds: boolean; + show: Show; + indexable: boolean; + isSelected?: boolean; } export interface Show { - id: number; - title: string; - type: string; - originalTitle: string; - shortTitle: string; - reference: string; - age: string; - languages: string[]; - summary: string; - image: string; - image2x: string; - imageHorizontal: string; - imageHorizontal2x: string; - url: string; - urlPath: string; - episodeCount: number; - genres: string[]; - copyright: string; - rating: number; - ratingsCount: number; - commentsCount: number; - qualities: string[]; - simulcast: boolean; - free: boolean; - available: boolean; - download: boolean; - basedOn: string; - tagline: string; - firstReleaseYear: string; - productionStudio: string; - countryOfOrigin: string; - productionTeam: ProductionTeam[]; - nextVideoReleaseDate: Date; - indexable: boolean; + id: number; + title: string; + type: string; + originalTitle: string; + shortTitle: string; + reference: string; + age: string; + languages: string[]; + summary: string; + image: string; + image2x: string; + imageHorizontal: string; + imageHorizontal2x: string; + url: string; + urlPath: string; + episodeCount: number; + genres: string[]; + copyright: string; + rating: number; + ratingsCount: number; + commentsCount: number; + qualities: string[]; + simulcast: boolean; + free: boolean; + available: boolean; + download: boolean; + basedOn: string; + tagline: string; + firstReleaseYear: string; + productionStudio: string; + countryOfOrigin: string; + productionTeam: ProductionTeam[]; + nextVideoReleaseDate: Date; + indexable: boolean; } export interface ProductionTeam { - role: string; - name: string; + role: string; + name: string; } diff --git a/@types/animeOnegaiSearch.d.ts b/@types/animeOnegaiSearch.d.ts index 74bdfc1..e030157 100644 --- a/@types/animeOnegaiSearch.d.ts +++ b/@types/animeOnegaiSearch.d.ts @@ -1,88 +1,88 @@ export interface AnimeOnegaiSearch { - text: string; - list: AOSearchResult[]; + text: string; + list: AOSearchResult[]; } export interface AOSearchResult { - /** - * Asset ID - */ - ID: number; - CreatedAt: Date; - UpdatedAt: Date; - DeletedAt: null; - title: string; - active: boolean; - excerpt: string; - description: string; - bg: string; - poster: string; - entry: string; - code_name: string; - /** - * The Video ID required to get the streams - */ - video_entry: string; - trailer: string; - year: number; - /** - * Asset Type, Known Possibilities - * * 1 - Video - * * 2 - Series - */ - asset_type: 1 | 2; - status: number; - permalink: string; - duration: string; - subtitles: boolean; - price: number; - rent_price: number; - rating: number; - color: number | null; - classification: number; - brazil_classification: null | string; - likes: number; - views: number; - button: string; - stream_url: string; - stream_url_backup: string; - copyright: null | string; - skip_intro: null | string; - ending: null | string; - bumper_intro: string; - ads: string; - age_restriction: boolean | null; - epg: null; - allow_languages: string[] | null; - allow_countries: string[] | null; - classification_text: string; - locked: boolean; - resign: boolean; - favorite: boolean; - actors_list: null; - voiceactors_list: null; - artdirectors_list: null; - audios_list: null; - awards_list: null; - companies_list: null; - countries_list: null; - directors_list: null; - edition_list: null; - genres_list: null; - music_list: null; - photograpy_list: null; - producer_list: null; - screenwriter_list: null; - season_list: null; - tags_list: null; - chapter_id: number; - chapter_entry: string; - chapter_poster: string; - progress_time: number; - progress_percent: number; - included_subscription: number; - paid_content: number; - rent_content: number; - objectID: string; - lang: string; + /** + * Asset ID + */ + ID: number; + CreatedAt: Date; + UpdatedAt: Date; + DeletedAt: null; + title: string; + active: boolean; + excerpt: string; + description: string; + bg: string; + poster: string; + entry: string; + code_name: string; + /** + * The Video ID required to get the streams + */ + video_entry: string; + trailer: string; + year: number; + /** + * Asset Type, Known Possibilities + * * 1 - Video + * * 2 - Series + */ + asset_type: 1 | 2; + status: number; + permalink: string; + duration: string; + subtitles: boolean; + price: number; + rent_price: number; + rating: number; + color: number | null; + classification: number; + brazil_classification: null | string; + likes: number; + views: number; + button: string; + stream_url: string; + stream_url_backup: string; + copyright: null | string; + skip_intro: null | string; + ending: null | string; + bumper_intro: string; + ads: string; + age_restriction: boolean | null; + epg: null; + allow_languages: string[] | null; + allow_countries: string[] | null; + classification_text: string; + locked: boolean; + resign: boolean; + favorite: boolean; + actors_list: null; + voiceactors_list: null; + artdirectors_list: null; + audios_list: null; + awards_list: null; + companies_list: null; + countries_list: null; + directors_list: null; + edition_list: null; + genres_list: null; + music_list: null; + photograpy_list: null; + producer_list: null; + screenwriter_list: null; + season_list: null; + tags_list: null; + chapter_id: number; + chapter_entry: string; + chapter_poster: string; + progress_time: number; + progress_percent: number; + included_subscription: number; + paid_content: number; + rent_content: number; + objectID: string; + lang: string; } \ No newline at end of file diff --git a/@types/animeOnegaiSeasons.d.ts b/@types/animeOnegaiSeasons.d.ts index 3ffe879..90e851d 100644 --- a/@types/animeOnegaiSeasons.d.ts +++ b/@types/animeOnegaiSeasons.d.ts @@ -1,36 +1,36 @@ export interface AnimeOnegaiSeasons { - ID: number; - CreatedAt: Date; - UpdatedAt: Date; - DeletedAt: null; - name: string; - number: number; - asset_id: number; - entry: string; - description: string; - active: boolean; - allow_languages: string[]; - allow_countries: string[]; - list: Episode[]; + ID: number; + CreatedAt: Date; + UpdatedAt: Date; + DeletedAt: null; + name: string; + number: number; + asset_id: number; + entry: string; + description: string; + active: boolean; + allow_languages: string[]; + allow_countries: string[]; + list: Episode[]; } export interface Episode { - ID: number; - CreatedAt: Date; - UpdatedAt: Date; - DeletedAt: null; - name: string; - number: number; - description: string; - thumbnail: string; - entry: string; - video_entry: string; - active: boolean; - season_id: number; - stream_url: string; - skip_intro: null; - ending: null; - open_free: boolean; - asset_id: number; - age_restriction: boolean; + ID: number; + CreatedAt: Date; + UpdatedAt: Date; + DeletedAt: null; + name: string; + number: number; + description: string; + thumbnail: string; + entry: string; + video_entry: string; + active: boolean; + season_id: number; + stream_url: string; + skip_intro: null; + ending: null; + open_free: boolean; + asset_id: number; + age_restriction: boolean; } diff --git a/@types/animeOnegaiSeries.d.ts b/@types/animeOnegaiSeries.d.ts index 52662ff..f8e5b21 100644 --- a/@types/animeOnegaiSeries.d.ts +++ b/@types/animeOnegaiSeries.d.ts @@ -1,111 +1,111 @@ export interface AnimeOnegaiSeries { - ID: number; - CreatedAt: Date; - UpdatedAt: Date; - DeletedAt: null; - title: string; - active: boolean; - excerpt: string; - description: string; - bg: string; - poster: string; - entry: string; - code_name: string; - /** - * The Video ID required to get the streams - */ - video_entry: string; - trailer: string; - year: number; - asset_type: number; - status: number; - permalink: string; - duration: string; - subtitles: boolean; - price: number; - rent_price: number; - rating: number; - color: number; - classification: number; - brazil_classification: string; - likes: number; - views: number; - button: string; - stream_url: string; - stream_url_backup: string; - copyright: string; - skip_intro: null; - ending: null; - bumper_intro: string; - ads: string; - age_restriction: boolean; - epg: null; - allow_languages: string[]; - allow_countries: string[]; - classification_text: string; - locked: boolean; - resign: boolean; - favorite: boolean; - actors_list: CtorsList[]; - voiceactors_list: CtorsList[]; - artdirectors_list: any[]; - audios_list: SList[]; - awards_list: any[]; - companies_list: any[]; - countries_list: any[]; - directors_list: CtorsList[]; - edition_list: any[]; - genres_list: SList[]; - music_list: any[]; - photograpy_list: any[]; - producer_list: any[]; - screenwriter_list: any[]; - season_list: any[]; - tags_list: TagsList[]; - chapter_id: number; - chapter_entry: string; - chapter_poster: string; - progress_time: number; - progress_percent: number; - included_subscription: number; - paid_content: number; - rent_content: number; - objectID: string; - lang: string; + ID: number; + CreatedAt: Date; + UpdatedAt: Date; + DeletedAt: null; + title: string; + active: boolean; + excerpt: string; + description: string; + bg: string; + poster: string; + entry: string; + code_name: string; + /** + * The Video ID required to get the streams + */ + video_entry: string; + trailer: string; + year: number; + asset_type: number; + status: number; + permalink: string; + duration: string; + subtitles: boolean; + price: number; + rent_price: number; + rating: number; + color: number; + classification: number; + brazil_classification: string; + likes: number; + views: number; + button: string; + stream_url: string; + stream_url_backup: string; + copyright: string; + skip_intro: null; + ending: null; + bumper_intro: string; + ads: string; + age_restriction: boolean; + epg: null; + allow_languages: string[]; + allow_countries: string[]; + classification_text: string; + locked: boolean; + resign: boolean; + favorite: boolean; + actors_list: CtorsList[]; + voiceactors_list: CtorsList[]; + artdirectors_list: any[]; + audios_list: SList[]; + awards_list: any[]; + companies_list: any[]; + countries_list: any[]; + directors_list: CtorsList[]; + edition_list: any[]; + genres_list: SList[]; + music_list: any[]; + photograpy_list: any[]; + producer_list: any[]; + screenwriter_list: any[]; + season_list: any[]; + tags_list: TagsList[]; + chapter_id: number; + chapter_entry: string; + chapter_poster: string; + progress_time: number; + progress_percent: number; + included_subscription: number; + paid_content: number; + rent_content: number; + objectID: string; + lang: string; } export interface CtorsList { - ID: number; - CreatedAt: Date; - UpdatedAt: Date; - DeletedAt: null; - name: string; - Permalink?: string; - country: number | null; - year: number | null; - death: number | null; - image: string; - genre: null; - description: string; - permalink?: string; - background?: string; + ID: number; + CreatedAt: Date; + UpdatedAt: Date; + DeletedAt: null; + name: string; + Permalink?: string; + country: number | null; + year: number | null; + death: number | null; + image: string; + genre: null; + description: string; + permalink?: string; + background?: string; } export interface SList { - ID: number; - CreatedAt: Date; - UpdatedAt: Date; - DeletedAt: null; - name: string; - age_restriction?: number; + ID: number; + CreatedAt: Date; + UpdatedAt: Date; + DeletedAt: null; + name: string; + age_restriction?: number; } export interface TagsList { - ID: number; - CreatedAt: Date; - UpdatedAt: Date; - DeletedAt: null; - name: string; - position: number; - status: boolean; + ID: number; + CreatedAt: Date; + UpdatedAt: Date; + DeletedAt: null; + name: string; + position: number; + status: boolean; } diff --git a/@types/animeOnegaiStream.d.ts b/@types/animeOnegaiStream.d.ts index 7199316..9eb3519 100644 --- a/@types/animeOnegaiStream.d.ts +++ b/@types/animeOnegaiStream.d.ts @@ -1,41 +1,41 @@ export interface AnimeOnegaiStream { - ID: number; - CreatedAt: Date; - UpdatedAt: Date; - DeletedAt: null; - name: string; - source_url: string; - backup_url: string; - live: boolean; - token_handler: number; - entry: string; - job: string; - drm: boolean; - transcoding_content_id: string; - transcoding_asset_id: string; - status: number; - thumbnail: string; - hls: string; - dash: string; - widevine_proxy: string; - playready_proxy: string; - apple_licence: string; - apple_certificate: string; - dpath: string; - dbin: string; - subtitles: Subtitle[]; - origin: number; - offline_entry: string; - offline_status: boolean; + ID: number; + CreatedAt: Date; + UpdatedAt: Date; + DeletedAt: null; + name: string; + source_url: string; + backup_url: string; + live: boolean; + token_handler: number; + entry: string; + job: string; + drm: boolean; + transcoding_content_id: string; + transcoding_asset_id: string; + status: number; + thumbnail: string; + hls: string; + dash: string; + widevine_proxy: string; + playready_proxy: string; + apple_licence: string; + apple_certificate: string; + dpath: string; + dbin: string; + subtitles: Subtitle[]; + origin: number; + offline_entry: string; + offline_status: boolean; } export interface Subtitle { - ID: number; - CreatedAt: Date; - UpdatedAt: Date; - DeletedAt: null; - name: string; - lang: string; - entry_id: string; - url: string; + ID: number; + CreatedAt: Date; + UpdatedAt: Date; + DeletedAt: null; + name: string; + lang: string; + entry_id: string; + url: string; } diff --git a/@types/crunchyAndroidEpisodes.d.ts b/@types/crunchyAndroidEpisodes.d.ts index 44a1269..9ee26b0 100644 --- a/@types/crunchyAndroidEpisodes.d.ts +++ b/@types/crunchyAndroidEpisodes.d.ts @@ -1,136 +1,136 @@ import { Images } from './crunchyEpisodeList'; export interface CrunchyAndroidEpisodes { - __class__: string; - __href__: string; - __resource_key__: string; - __links__: object; - __actions__: object; - total: number; - items: CrunchyAndroidEpisode[]; + __class__: string; + __href__: string; + __resource_key__: string; + __links__: object; + __actions__: object; + total: number; + items: CrunchyAndroidEpisode[]; } export interface CrunchyAndroidEpisode { - __class__: string; - __href__: string; - __resource_key__: string; - __links__: Links; - __actions__: Actions; - playback: string; - id: string; - channel_id: ChannelID; - series_id: string; - series_title: string; - series_slug_title: string; - season_id: string; - season_title: string; - season_slug_title: string; - season_number: number; - episode: string; - episode_number: number; - sequence_number: number; - production_episode_id: string; - title: string; - slug_title: string; - description: string; - next_episode_id: string; - next_episode_title: string; - hd_flag: boolean; - maturity_ratings: MaturityRating[]; - extended_maturity_rating: Actions; - is_mature: boolean; - mature_blocked: boolean; - episode_air_date: Date; - upload_date: Date; - availability_starts: Date; - availability_ends: Date; - eligible_region: string; - available_date: Date; - free_available_date: Date; - premium_date: Date; - premium_available_date: Date; - is_subbed: boolean; - is_dubbed: boolean; - is_clip: boolean; - seo_title: string; - seo_description: string; - season_tags: string[]; - available_offline: boolean; - subtitle_locales: Locale[]; - availability_notes: string; - audio_locale: Locale; - versions: Version[]; - closed_captions_available: boolean; - identifier: string; - media_type: MediaType; - slug: string; - images: Images; - duration_ms: number; - is_premium_only: boolean; - listing_id: string; - hide_season_title?: boolean; - hide_season_number?: boolean; - isSelected?: boolean; - seq_id: string; + __class__: string; + __href__: string; + __resource_key__: string; + __links__: Links; + __actions__: Actions; + playback: string; + id: string; + channel_id: ChannelID; + series_id: string; + series_title: string; + series_slug_title: string; + season_id: string; + season_title: string; + season_slug_title: string; + season_number: number; + episode: string; + episode_number: number; + sequence_number: number; + production_episode_id: string; + title: string; + slug_title: string; + description: string; + next_episode_id: string; + next_episode_title: string; + hd_flag: boolean; + maturity_ratings: MaturityRating[]; + extended_maturity_rating: Actions; + is_mature: boolean; + mature_blocked: boolean; + episode_air_date: Date; + upload_date: Date; + availability_starts: Date; + availability_ends: Date; + eligible_region: string; + available_date: Date; + free_available_date: Date; + premium_date: Date; + premium_available_date: Date; + is_subbed: boolean; + is_dubbed: boolean; + is_clip: boolean; + seo_title: string; + seo_description: string; + season_tags: string[]; + available_offline: boolean; + subtitle_locales: Locale[]; + availability_notes: string; + audio_locale: Locale; + versions: Version[]; + closed_captions_available: boolean; + identifier: string; + media_type: MediaType; + slug: string; + images: Images; + duration_ms: number; + is_premium_only: boolean; + listing_id: string; + hide_season_title?: boolean; + hide_season_number?: boolean; + isSelected?: boolean; + seq_id: string; } export interface Links { - 'episode/channel': Link; - 'episode/next_episode': Link; - 'episode/season': Link; - 'episode/series': Link; - streams: Link; + 'episode/channel': Link; + 'episode/next_episode': Link; + 'episode/season': Link; + 'episode/series': Link; + streams: Link; } export interface Link { - href: string; + href: string; } export interface Thumbnail { - width: number; - height: number; - type: string; - source: string; + width: number; + height: number; + type: string; + source: string; } export enum Locale { - enUS = 'en-US', - esLA = 'es-LA', - es419 = 'es-419', - esES = 'es-ES', - ptBR = 'pt-BR', - frFR = 'fr-FR', - deDE = 'de-DE', - arME = 'ar-ME', - arSA = 'ar-SA', - itIT = 'it-IT', - ruRU = 'ru-RU', - trTR = 'tr-TR', - hiIN = 'hi-IN', - zhCN = 'zh-CN', - koKR = 'ko-KR', - jaJP = 'ja-JP', + enUS = 'en-US', + esLA = 'es-LA', + es419 = 'es-419', + esES = 'es-ES', + ptBR = 'pt-BR', + frFR = 'fr-FR', + deDE = 'de-DE', + arME = 'ar-ME', + arSA = 'ar-SA', + itIT = 'it-IT', + ruRU = 'ru-RU', + trTR = 'tr-TR', + hiIN = 'hi-IN', + zhCN = 'zh-CN', + koKR = 'ko-KR', + jaJP = 'ja-JP', } export enum MediaType { - Episode = 'episode', + Episode = 'episode', } export enum ChannelID { - Crunchyroll = 'crunchyroll', + Crunchyroll = 'crunchyroll', } export enum MaturityRating { - Tv14 = 'TV-14', + Tv14 = 'TV-14', } export interface Version { - audio_locale: Locale; - guid: string; - original: boolean; - variant: string; - season_guid: string; - media_guid: string; - is_premium_only: boolean; + audio_locale: Locale; + guid: string; + original: boolean; + variant: string; + season_guid: string; + media_guid: string; + is_premium_only: boolean; } diff --git a/@types/crunchyAndroidObject.d.ts b/@types/crunchyAndroidObject.d.ts index 85835c9..5bccbb2 100644 --- a/@types/crunchyAndroidObject.d.ts +++ b/@types/crunchyAndroidObject.d.ts @@ -1,186 +1,186 @@ import { ImageType, Images, Image } from './objectInfo'; export interface CrunchyAndroidObject { - __class__: string; - __href__: string; - __resource_key__: string; - __links__: object; - __actions__: object; - total: number; - items: AndroidObject[]; + __class__: string; + __href__: string; + __resource_key__: string; + __links__: object; + __actions__: object; + total: number; + items: AndroidObject[]; } export interface AndroidObject { - __class__: string; - __href__: string; - __links__: Links; - __actions__: Actions; - id: string; - external_id: string; - channel_id: string; - title: string; - description: string; - promo_title: string; - promo_description: string; - type: string; - slug: string; - slug_title: string; - images: Images; - movie_listing_metadata?: MovieListingMetadata; - movie_metadata?: MovieMetadata; - playback?: string; - episode_metadata?: EpisodeMetadata; - streams_link?: string; - season_metadata?: SeasonMetadata; - linked_resource_key: string; - isSelected?: boolean; - f_num: string; - s_num: string; + __class__: string; + __href__: string; + __links__: Links; + __actions__: Actions; + id: string; + external_id: string; + channel_id: string; + title: string; + description: string; + promo_title: string; + promo_description: string; + type: string; + slug: string; + slug_title: string; + images: Images; + movie_listing_metadata?: MovieListingMetadata; + movie_metadata?: MovieMetadata; + playback?: string; + episode_metadata?: EpisodeMetadata; + streams_link?: string; + season_metadata?: SeasonMetadata; + linked_resource_key: string; + isSelected?: boolean; + f_num: string; + s_num: string; } export interface Links { - 'episode/season': LinkData; - 'episode/series': LinkData; - resource: LinkData; - 'resource/channel': LinkData; - streams: LinkData; + 'episode/season': LinkData; + 'episode/series': LinkData; + resource: LinkData; + 'resource/channel': LinkData; + streams: LinkData; } export interface LinkData { - href: string; + href: string; } export interface EpisodeMetadata { - audio_locale: Locale; - availability_ends: Date; - availability_notes: string; - availability_starts: Date; - available_date: null; - available_offline: boolean; - closed_captions_available: boolean; - duration_ms: number; - eligible_region: string; - episode: string; - episode_air_date: Date; - episode_number: number; - extended_maturity_rating: Record<unknown>; - free_available_date: Date; - identifier: string; - is_clip: boolean; - is_dubbed: boolean; - is_mature: boolean; - is_premium_only: boolean; - is_subbed: boolean; - mature_blocked: boolean; - maturity_ratings: string[]; - premium_available_date: Date; - premium_date: null; - season_id: string; - season_number: number; - season_slug_title: string; - season_title: string; - sequence_number: number; - series_id: string; - series_slug_title: string; - series_title: string; - subtitle_locales: Locale[]; - tenant_categories?: string[]; - upload_date: Date; - versions: EpisodeMetadataVersion[]; + audio_locale: Locale; + availability_ends: Date; + availability_notes: string; + availability_starts: Date; + available_date: null; + available_offline: boolean; + closed_captions_available: boolean; + duration_ms: number; + eligible_region: string; + episode: string; + episode_air_date: Date; + episode_number: number; + extended_maturity_rating: Record<unknown>; + free_available_date: Date; + identifier: string; + is_clip: boolean; + is_dubbed: boolean; + is_mature: boolean; + is_premium_only: boolean; + is_subbed: boolean; + mature_blocked: boolean; + maturity_ratings: string[]; + premium_available_date: Date; + premium_date: null; + season_id: string; + season_number: number; + season_slug_title: string; + season_title: string; + sequence_number: number; + series_id: string; + series_slug_title: string; + series_title: string; + subtitle_locales: Locale[]; + tenant_categories?: string[]; + upload_date: Date; + versions: EpisodeMetadataVersion[]; } export interface MovieListingMetadata { - availability_notes: string; - available_date: null; - available_offline: boolean; - duration_ms: number; - extended_description: string; - extended_maturity_rating: Record<unknown>; - first_movie_id: string; - free_available_date: Date; - is_dubbed: boolean; - is_mature: boolean; - is_premium_only: boolean; - is_subbed: boolean; - mature_blocked: boolean; - maturity_ratings: string[]; - movie_release_year: number; - premium_available_date: Date; - premium_date: null; - subtitle_locales: Locale[]; - tenant_categories: string[]; + availability_notes: string; + available_date: null; + available_offline: boolean; + duration_ms: number; + extended_description: string; + extended_maturity_rating: Record<unknown>; + first_movie_id: string; + free_available_date: Date; + is_dubbed: boolean; + is_mature: boolean; + is_premium_only: boolean; + is_subbed: boolean; + mature_blocked: boolean; + maturity_ratings: string[]; + movie_release_year: number; + premium_available_date: Date; + premium_date: null; + subtitle_locales: Locale[]; + tenant_categories: string[]; } export interface MovieMetadata { - availability_notes: string; - available_offline: boolean; - closed_captions_available: boolean; - duration_ms: number; - extended_maturity_rating: Record<unknown>; - is_dubbed: boolean; - is_mature: boolean; - is_premium_only: boolean; - is_subbed: boolean; - mature_blocked: boolean; - maturity_ratings: string[]; - movie_listing_id: string; - movie_listing_slug_title: string; - movie_listing_title: string; + availability_notes: string; + available_offline: boolean; + closed_captions_available: boolean; + duration_ms: number; + extended_maturity_rating: Record<unknown>; + is_dubbed: boolean; + is_mature: boolean; + is_premium_only: boolean; + is_subbed: boolean; + mature_blocked: boolean; + maturity_ratings: string[]; + movie_listing_id: string; + movie_listing_slug_title: string; + movie_listing_title: string; } export interface SeasonMetadata { - audio_locale: Locale; - audio_locales: Locale[]; - extended_maturity_rating: Record<unknown>; - identifier: string; - is_mature: boolean; - mature_blocked: boolean; - maturity_ratings: string[]; - season_display_number: string; - season_sequence_number: number; - subtitle_locales: Locale[]; - versions: SeasonMetadataVersion[]; + audio_locale: Locale; + audio_locales: Locale[]; + extended_maturity_rating: Record<unknown>; + identifier: string; + is_mature: boolean; + mature_blocked: boolean; + maturity_ratings: string[]; + season_display_number: string; + season_sequence_number: number; + subtitle_locales: Locale[]; + versions: SeasonMetadataVersion[]; } export interface SeasonMetadataVersion { - audio_locale: Locale; - guid: string; - original: boolean; - variant: string; + audio_locale: Locale; + guid: string; + original: boolean; + variant: string; } export interface SeriesMetadata { - audio_locales: Locale[]; - availability_notes: string; - episode_count: number; - extended_description: string; - extended_maturity_rating: Record<unknown>; - is_dubbed: boolean; - is_mature: boolean; - is_simulcast: boolean; - is_subbed: boolean; - mature_blocked: boolean; - maturity_ratings: string[]; - season_count: number; - series_launch_year: number; - subtitle_locales: Locale[]; - tenant_categories?: string[]; + audio_locales: Locale[]; + availability_notes: string; + episode_count: number; + extended_description: string; + extended_maturity_rating: Record<unknown>; + is_dubbed: boolean; + is_mature: boolean; + is_simulcast: boolean; + is_subbed: boolean; + mature_blocked: boolean; + maturity_ratings: string[]; + season_count: number; + series_launch_year: number; + subtitle_locales: Locale[]; + tenant_categories?: string[]; } export enum Locale { - enUS = 'en-US', - esLA = 'es-LA', - es419 = 'es-419', - esES = 'es-ES', - ptBR = 'pt-BR', - frFR = 'fr-FR', - deDE = 'de-DE', - arME = 'ar-ME', - arSA = 'ar-SA', - itIT = 'it-IT', - ruRU = 'ru-RU', - trTR = 'tr-TR', - hiIN = 'hi-IN', - zhCN = 'zh-CN', - koKR = 'ko-KR', - jaJP = 'ja-JP', + enUS = 'en-US', + esLA = 'es-LA', + es419 = 'es-419', + esES = 'es-ES', + ptBR = 'pt-BR', + frFR = 'fr-FR', + deDE = 'de-DE', + arME = 'ar-ME', + arSA = 'ar-SA', + itIT = 'it-IT', + ruRU = 'ru-RU', + trTR = 'tr-TR', + hiIN = 'hi-IN', + zhCN = 'zh-CN', + koKR = 'ko-KR', + jaJP = 'ja-JP', } \ No newline at end of file diff --git a/@types/crunchyAndroidStreams.d.ts b/@types/crunchyAndroidStreams.d.ts index c5e9889..f3674fd 100644 --- a/@types/crunchyAndroidStreams.d.ts +++ b/@types/crunchyAndroidStreams.d.ts @@ -1,93 +1,93 @@ export interface CrunchyAndroidStreams { - __class__: string; - __href__: string; - __resource_key__: string; - __links__: Links; - __actions__: Record<unknown, unknown>; - media_id: string; - audio_locale: Locale; - subtitles: Subtitles; - closed_captions: Subtitles; - streams: Streams; - bifs: string[]; - versions: Version[]; - captions: Record<unknown, unknown>; + __class__: string; + __href__: string; + __resource_key__: string; + __links__: Links; + __actions__: Record<unknown, unknown>; + media_id: string; + audio_locale: Locale; + subtitles: Subtitles; + closed_captions: Subtitles; + streams: Streams; + bifs: string[]; + versions: Version[]; + captions: Record<unknown, unknown>; } export interface Subtitles { - '': Subtitle; - 'en-US'?: Subtitle; - 'es-LA'?: Subtitle; - 'es-419'?: Subtitle; - 'es-ES'?: Subtitle; - 'pt-BR'?: Subtitle; - 'fr-FR'?: Subtitle; - 'de-DE'?: Subtitle; - 'ar-ME'?: Subtitle; - 'ar-SA'?: Subtitle; - 'it-IT'?: Subtitle; - 'ru-RU'?: Subtitle; - 'tr-TR'?: Subtitle; - 'hi-IN'?: Subtitle; - 'zh-CN'?: Subtitle; - 'ko-KR'?: Subtitle; - 'ja-JP'?: Subtitle; + '': Subtitle; + 'en-US'?: Subtitle; + 'es-LA'?: Subtitle; + 'es-419'?: Subtitle; + 'es-ES'?: Subtitle; + 'pt-BR'?: Subtitle; + 'fr-FR'?: Subtitle; + 'de-DE'?: Subtitle; + 'ar-ME'?: Subtitle; + 'ar-SA'?: Subtitle; + 'it-IT'?: Subtitle; + 'ru-RU'?: Subtitle; + 'tr-TR'?: Subtitle; + 'hi-IN'?: Subtitle; + 'zh-CN'?: Subtitle; + 'ko-KR'?: Subtitle; + 'ja-JP'?: Subtitle; } export interface Links { - resource: Resource; + resource: Resource; } export interface Resource { - href: string; + href: string; } export interface Streams { - [key: string]: { [key: string]: Download }; + [key: string]: { [key: string]: Download }; } export interface Download { - hardsub_locale: Locale; - hardsub_lang?: string; - url: string; + hardsub_locale: Locale; + hardsub_lang?: string; + url: string; } export interface Urls { - '': Download; + '': Download; } export interface Subtitle { - locale: Locale; - url: string; - format: string; + locale: Locale; + url: string; + format: string; } export interface Version { - audio_locale: Locale; - guid: string; - original: boolean; - variant: string; - season_guid: string; - media_guid: string; - is_premium_only: boolean; + audio_locale: Locale; + guid: string; + original: boolean; + variant: string; + season_guid: string; + media_guid: string; + is_premium_only: boolean; } export enum Locale { - default = '', - enUS = 'en-US', - esLA = 'es-LA', - es419 = 'es-419', - esES = 'es-ES', - ptBR = 'pt-BR', - frFR = 'fr-FR', - deDE = 'de-DE', - arME = 'ar-ME', - arSA = 'ar-SA', - itIT = 'it-IT', - ruRU = 'ru-RU', - trTR = 'tr-TR', - hiIN = 'hi-IN', - zhCN = 'zh-CN', - koKR = 'ko-KR', - jaJP = 'ja-JP', + default = '', + enUS = 'en-US', + esLA = 'es-LA', + es419 = 'es-419', + esES = 'es-ES', + ptBR = 'pt-BR', + frFR = 'fr-FR', + deDE = 'de-DE', + arME = 'ar-ME', + arSA = 'ar-SA', + itIT = 'it-IT', + ruRU = 'ru-RU', + trTR = 'tr-TR', + hiIN = 'hi-IN', + zhCN = 'zh-CN', + koKR = 'ko-KR', + jaJP = 'ja-JP', } \ No newline at end of file diff --git a/@types/crunchyEpisodeList.d.ts b/@types/crunchyEpisodeList.d.ts index 2f57ca4..8a573fd 100644 --- a/@types/crunchyEpisodeList.d.ts +++ b/@types/crunchyEpisodeList.d.ts @@ -1,134 +1,134 @@ import { Links } from './crunchyAndroidEpisodes'; export interface CrunchyEpisodeList { - total: number; - data: CrunchyEpisode[]; - meta: Meta; + total: number; + data: CrunchyEpisode[]; + meta: Meta; } export interface CrunchyEpisode { - next_episode_id: string; - series_id: string; - season_number: number; - next_episode_title: string; - availability_notes: string; - duration_ms: number; - series_slug_title: string; - series_title: string; - is_dubbed: boolean; - versions: Version[] | null; - identifier: string; - sequence_number: number; - eligible_region: Record<unknown>; - availability_starts: Date; - images: Images; - season_id: string; - seo_title: string; - is_premium_only: boolean; - extended_maturity_rating: Record<unknown>; - title: string; - production_episode_id: string; - premium_available_date: Date; - season_title: string; - seo_description: string; - audio_locale: Locale; - id: string; - media_type: MediaType; - availability_ends: Date; - free_available_date: Date; - playback: string; - channel_id: ChannelID; - episode: string; - is_mature: boolean; - listing_id: string; - episode_air_date: Date; - slug: string; - available_date: Date; - subtitle_locales: Locale[]; - slug_title: string; - available_offline: boolean; - description: string; - is_subbed: boolean; - premium_date: Date; - upload_date: Date; - season_slug_title: string; - closed_captions_available: boolean; - episode_number: number; - season_tags: any[]; - maturity_ratings: MaturityRating[]; - streams_link?: string; - mature_blocked: boolean; - is_clip: boolean; - hd_flag: boolean; - hide_season_title?: boolean; - hide_season_number?: boolean; - isSelected?: boolean; - seq_id: string; - __links__?: Links; + next_episode_id: string; + series_id: string; + season_number: number; + next_episode_title: string; + availability_notes: string; + duration_ms: number; + series_slug_title: string; + series_title: string; + is_dubbed: boolean; + versions: Version[] | null; + identifier: string; + sequence_number: number; + eligible_region: Record<unknown>; + availability_starts: Date; + images: Images; + season_id: string; + seo_title: string; + is_premium_only: boolean; + extended_maturity_rating: Record<unknown>; + title: string; + production_episode_id: string; + premium_available_date: Date; + season_title: string; + seo_description: string; + audio_locale: Locale; + id: string; + media_type: MediaType; + availability_ends: Date; + free_available_date: Date; + playback: string; + channel_id: ChannelID; + episode: string; + is_mature: boolean; + listing_id: string; + episode_air_date: Date; + slug: string; + available_date: Date; + subtitle_locales: Locale[]; + slug_title: string; + available_offline: boolean; + description: string; + is_subbed: boolean; + premium_date: Date; + upload_date: Date; + season_slug_title: string; + closed_captions_available: boolean; + episode_number: number; + season_tags: any[]; + maturity_ratings: MaturityRating[]; + streams_link?: string; + mature_blocked: boolean; + is_clip: boolean; + hd_flag: boolean; + hide_season_title?: boolean; + hide_season_number?: boolean; + isSelected?: boolean; + seq_id: string; + __links__?: Links; } export enum Locale { - enUS = 'en-US', - esLA = 'es-LA', - es419 = 'es-419', - esES = 'es-ES', - ptBR = 'pt-BR', - frFR = 'fr-FR', - deDE = 'de-DE', - arME = 'ar-ME', - arSA = 'ar-SA', - itIT = 'it-IT', - ruRU = 'ru-RU', - trTR = 'tr-TR', - hiIN = 'hi-IN', - zhCN = 'zh-CN', - koKR = 'ko-KR', - jaJP = 'ja-JP', + enUS = 'en-US', + esLA = 'es-LA', + es419 = 'es-419', + esES = 'es-ES', + ptBR = 'pt-BR', + frFR = 'fr-FR', + deDE = 'de-DE', + arME = 'ar-ME', + arSA = 'ar-SA', + itIT = 'it-IT', + ruRU = 'ru-RU', + trTR = 'tr-TR', + hiIN = 'hi-IN', + zhCN = 'zh-CN', + koKR = 'ko-KR', + jaJP = 'ja-JP', } export enum ChannelID { - Crunchyroll = 'crunchyroll', + Crunchyroll = 'crunchyroll', } export interface Images { - poster_tall?: Array<Image[]>; - poster_wide?: Array<Image[]>; - promo_image?: Array<Image[]>; - thumbnail?: Array<Image[]>; + poster_tall?: Array<Image[]>; + poster_wide?: Array<Image[]>; + promo_image?: Array<Image[]>; + thumbnail?: Array<Image[]>; } export interface Image { - height: number; - source: string; - type: ImageType; - width: number; + height: number; + source: string; + type: ImageType; + width: number; } export enum ImageType { - PosterTall = 'poster_tall', - PosterWide = 'poster_wide', - PromoImage = 'promo_image', - Thumbnail = 'thumbnail', + PosterTall = 'poster_tall', + PosterWide = 'poster_wide', + PromoImage = 'promo_image', + Thumbnail = 'thumbnail', } export enum MaturityRating { - Tv14 = 'TV-14', + Tv14 = 'TV-14', } export enum MediaType { - Episode = 'episode', + Episode = 'episode', } export interface Version { - audio_locale: Locale; - guid: string; - is_premium_only: boolean; - media_guid: string; - original: boolean; - season_guid: string; - variant: string; + audio_locale: Locale; + guid: string; + is_premium_only: boolean; + media_guid: string; + original: boolean; + season_guid: string; + variant: string; } export interface Meta { - versions_considered?: boolean; + versions_considered?: boolean; } \ No newline at end of file diff --git a/@types/crunchySearch.d.ts b/@types/crunchySearch.d.ts index 09ade69..9a61e7d 100644 --- a/@types/crunchySearch.d.ts +++ b/@types/crunchySearch.d.ts @@ -1,183 +1,183 @@ // Generated by https://quicktype.io export interface CrunchySearch { - total: number; - data: CrunchySearchData[]; - meta: Record<string, unknown>; + total: number; + data: CrunchySearchData[]; + meta: Record<string, unknown>; } export interface CrunchySearchData { - type: string; - count: number; - items: CrunchySearchItem[]; + type: string; + count: number; + items: CrunchySearchItem[]; } export interface CrunchySearchItem { - title: string; - images: Images; - series_metadata?: SeriesMetadata; - promo_description: string; - external_id: string; - slug: string; - new: boolean; - slug_title: string; - channel_id: ChannelID; - description: string; - linked_resource_key: string; - type: ItemType; - id: string; - promo_title: string; - search_metadata: SearchMetadata; - movie_listing_metadata?: MovieListingMetadata; - playback?: string; - streams_link?: string; - episode_metadata?: EpisodeMetadata; + title: string; + images: Images; + series_metadata?: SeriesMetadata; + promo_description: string; + external_id: string; + slug: string; + new: boolean; + slug_title: string; + channel_id: ChannelID; + description: string; + linked_resource_key: string; + type: ItemType; + id: string; + promo_title: string; + search_metadata: SearchMetadata; + movie_listing_metadata?: MovieListingMetadata; + playback?: string; + streams_link?: string; + episode_metadata?: EpisodeMetadata; } export enum ChannelID { - Crunchyroll = 'crunchyroll', + Crunchyroll = 'crunchyroll', } export interface EpisodeMetadata { - audio_locale: Locale; - availability_ends: Date; - availability_notes: string; - availability_starts: Date; - available_date: null; - available_offline: boolean; - closed_captions_available: boolean; - duration_ms: number; - eligible_region: string[]; - episode: string; - episode_air_date: Date; - episode_number: number; - extended_maturity_rating: Record<unknown>; - free_available_date: Date; - identifier: string; - is_clip: boolean; - is_dubbed: boolean; - is_mature: boolean; - is_premium_only: boolean; - is_subbed: boolean; - mature_blocked: boolean; - maturity_ratings: MaturityRating[]; - premium_available_date: Date; - premium_date: null; - season_id: string; - season_number: number; - season_slug_title: string; - season_title: string; - sequence_number: number; - series_id: string; - series_slug_title: string; - series_title: string; - subtitle_locales: Locale[]; - upload_date: Date; - versions: Version[] | null; - tenant_categories?: string[]; + audio_locale: Locale; + availability_ends: Date; + availability_notes: string; + availability_starts: Date; + available_date: null; + available_offline: boolean; + closed_captions_available: boolean; + duration_ms: number; + eligible_region: string[]; + episode: string; + episode_air_date: Date; + episode_number: number; + extended_maturity_rating: Record<unknown>; + free_available_date: Date; + identifier: string; + is_clip: boolean; + is_dubbed: boolean; + is_mature: boolean; + is_premium_only: boolean; + is_subbed: boolean; + mature_blocked: boolean; + maturity_ratings: MaturityRating[]; + premium_available_date: Date; + premium_date: null; + season_id: string; + season_number: number; + season_slug_title: string; + season_title: string; + sequence_number: number; + series_id: string; + series_slug_title: string; + series_title: string; + subtitle_locales: Locale[]; + upload_date: Date; + versions: Version[] | null; + tenant_categories?: string[]; } export enum Locale { - enUS = 'en-US', - esLA = 'es-LA', - es419 = 'es-419', - esES = 'es-ES', - ptBR = 'pt-BR', - frFR = 'fr-FR', - deDE = 'de-DE', - arME = 'ar-ME', - arSA = 'ar-SA', - itIT = 'it-IT', - ruRU = 'ru-RU', - trTR = 'tr-TR', - hiIN = 'hi-IN', - zhCN = 'zh-CN', - koKR = 'ko-KR', - jaJP = 'ja-JP', + enUS = 'en-US', + esLA = 'es-LA', + es419 = 'es-419', + esES = 'es-ES', + ptBR = 'pt-BR', + frFR = 'fr-FR', + deDE = 'de-DE', + arME = 'ar-ME', + arSA = 'ar-SA', + itIT = 'it-IT', + ruRU = 'ru-RU', + trTR = 'tr-TR', + hiIN = 'hi-IN', + zhCN = 'zh-CN', + koKR = 'ko-KR', + jaJP = 'ja-JP', } export enum MaturityRating { - Tv14 = 'TV-14', - TvMa = 'TV-MA', + Tv14 = 'TV-14', + TvMa = 'TV-MA', } export interface Version { - audio_locale: Locale; - guid: string; - is_premium_only: boolean; - media_guid: string; - original: boolean; - season_guid: string; - variant: string; + audio_locale: Locale; + guid: string; + is_premium_only: boolean; + media_guid: string; + original: boolean; + season_guid: string; + variant: string; } export interface Images { - poster_tall?: Array<Image[]>; - poster_wide?: Array<Image[]>; - promo_image?: Array<Image[]>; - thumbnail?: Array<Image[]>; + poster_tall?: Array<Image[]>; + poster_wide?: Array<Image[]>; + promo_image?: Array<Image[]>; + thumbnail?: Array<Image[]>; } export interface Image { - height: number; - source: string; - type: ImageType; - width: number; + height: number; + source: string; + type: ImageType; + width: number; } export enum ImageType { - PosterTall = 'poster_tall', - PosterWide = 'poster_wide', - PromoImage = 'promo_image', - Thumbnail = 'thumbnail', + PosterTall = 'poster_tall', + PosterWide = 'poster_wide', + PromoImage = 'promo_image', + Thumbnail = 'thumbnail', } export interface MovieListingMetadata { - availability_notes: string; - available_date: null; - available_offline: boolean; - duration_ms: number; - extended_description: string; - extended_maturity_rating: Record<unknown>; - first_movie_id: string; - free_available_date: Date; - is_dubbed: boolean; - is_mature: boolean; - is_premium_only: boolean; - is_subbed: boolean; - mature_blocked: boolean; - maturity_ratings: string[]; - movie_release_year: number; - premium_available_date: Date; - premium_date: null; - subtitle_locales: any[]; - tenant_categories: string[]; + availability_notes: string; + available_date: null; + available_offline: boolean; + duration_ms: number; + extended_description: string; + extended_maturity_rating: Record<unknown>; + first_movie_id: string; + free_available_date: Date; + is_dubbed: boolean; + is_mature: boolean; + is_premium_only: boolean; + is_subbed: boolean; + mature_blocked: boolean; + maturity_ratings: string[]; + movie_release_year: number; + premium_available_date: Date; + premium_date: null; + subtitle_locales: any[]; + tenant_categories: string[]; } export interface SearchMetadata { - score: number; + score: number; } export interface SeriesMetadata { - audio_locales: Locale[]; - availability_notes: string; - episode_count: number; - extended_description: string; - extended_maturity_rating: Record<unknown>; - is_dubbed: boolean; - is_mature: boolean; - is_simulcast: boolean; - is_subbed: boolean; - mature_blocked: boolean; - maturity_ratings: MaturityRating[]; - season_count: number; - series_launch_year: number; - subtitle_locales: Locale[]; - tenant_categories?: string[]; + audio_locales: Locale[]; + availability_notes: string; + episode_count: number; + extended_description: string; + extended_maturity_rating: Record<unknown>; + is_dubbed: boolean; + is_mature: boolean; + is_simulcast: boolean; + is_subbed: boolean; + mature_blocked: boolean; + maturity_ratings: MaturityRating[]; + season_count: number; + series_launch_year: number; + subtitle_locales: Locale[]; + tenant_categories?: string[]; } export enum ItemType { - Episode = 'episode', - MovieListing = 'movie_listing', - Series = 'series', + Episode = 'episode', + MovieListing = 'movie_listing', + Series = 'series', } \ No newline at end of file diff --git a/@types/crunchyTypes.d.ts b/@types/crunchyTypes.d.ts index 0aa8960..a84d861 100644 --- a/@types/crunchyTypes.d.ts +++ b/@types/crunchyTypes.d.ts @@ -5,211 +5,211 @@ import { DownloadInfo } from './messageHandler'; import { CrunchyVideoPlayStreams, CrunchyAudioPlayStreams } from './enums'; export type CrunchyDownloadOptions = { - hslang: string, - // kstream: number, - cstream: keyof typeof CrunchyVideoPlayStreams, - vstream: keyof typeof CrunchyVideoPlayStreams, - astream: keyof typeof CrunchyAudioPlayStreams, - tsd?: boolean, - novids?: boolean, - noaudio?: boolean, - x: number, - q: number, - fileName: string, - numbers: number, - partsize: number, - callbackMaker?: (data: DownloadInfo) => HLSCallback, - timeout: number, - waittime: number, - fsRetryTime: number, - dlsubs: string[], - skipsubs: boolean, - nosubs?: boolean, - mp4: boolean, - override: string[], - videoTitle: string, - force: 'Y'|'y'|'N'|'n'|'C'|'c', - ffmpegOptions: string[], - mkvmergeOptions: string[], - defaultSub: LanguageItem, - defaultAudio: LanguageItem, - ccTag: string, - dlVideoOnce: boolean, - skipmux?: boolean, - syncTiming: boolean, - nocleanup: boolean, - chapters: boolean, - fontName: string | undefined, - originalFontSize: boolean, - fontSize: number, - dubLang: string[], + hslang: string, + // kstream: number, + cstream: keyof typeof CrunchyVideoPlayStreams, + vstream: keyof typeof CrunchyVideoPlayStreams, + astream: keyof typeof CrunchyAudioPlayStreams, + tsd?: boolean, + novids?: boolean, + noaudio?: boolean, + x: number, + q: number, + fileName: string, + numbers: number, + partsize: number, + callbackMaker?: (data: DownloadInfo) => HLSCallback, + timeout: number, + waittime: number, + fsRetryTime: number, + dlsubs: string[], + skipsubs: boolean, + nosubs?: boolean, + mp4: boolean, + override: string[], + videoTitle: string, + force: 'Y'|'y'|'N'|'n'|'C'|'c', + ffmpegOptions: string[], + mkvmergeOptions: string[], + defaultSub: LanguageItem, + defaultAudio: LanguageItem, + ccTag: string, + dlVideoOnce: boolean, + skipmux?: boolean, + syncTiming: boolean, + nocleanup: boolean, + chapters: boolean, + fontName: string | undefined, + originalFontSize: boolean, + fontSize: number, + dubLang: string[], } export type CrunchyMultiDownload = { - absolute?: boolean, - dubLang: string[], - all?: boolean, - but?: boolean, - e?: string, - s?: string + absolute?: boolean, + dubLang: string[], + all?: boolean, + but?: boolean, + e?: string, + s?: string } export type CrunchyMuxOptions = { - output: string, - skipSubMux?: boolean - keepAllVideos?: bolean - novids?: boolean, - mp4: boolean, - forceMuxer?: 'ffmpeg'|'mkvmerge', - nocleanup?: boolean, - videoTitle: string, - ffmpegOptions: string[], - mkvmergeOptions: string[], - defaultSub: LanguageItem, - defaultAudio: LanguageItem, - ccTag: string, - syncTiming: boolean, + output: string, + skipSubMux?: boolean + keepAllVideos?: bolean + novids?: boolean, + mp4: boolean, + forceMuxer?: 'ffmpeg'|'mkvmerge', + nocleanup?: boolean, + videoTitle: string, + ffmpegOptions: string[], + mkvmergeOptions: string[], + defaultSub: LanguageItem, + defaultAudio: LanguageItem, + ccTag: string, + syncTiming: boolean, } export type CrunchyEpMeta = { - data: { - mediaId: string, - lang?: LanguageItem, - playback?: string, - versions?: EpisodeVersion[] | null, - isSubbed: boolean, - isDubbed: boolean, - }[], - seriesTitle: string, - seasonTitle: string, - episodeNumber: string, - episodeTitle: string, - seasonID: string, - season: number, - showID: string, - e: string, - image: string, + data: { + mediaId: string, + lang?: LanguageItem, + playback?: string, + versions?: EpisodeVersion[] | null, + isSubbed: boolean, + isDubbed: boolean, + }[], + seriesTitle: string, + seasonTitle: string, + episodeNumber: string, + episodeTitle: string, + seasonID: string, + season: number, + showID: string, + e: string, + image: string, } export type DownloadedMedia = { - type: 'Video', - lang: LanguageItem, - path: string, - isPrimary?: boolean + type: 'Video', + lang: LanguageItem, + path: string, + isPrimary?: boolean } | { - type: 'Audio', - lang: LanguageItem, - path: string, - isPrimary?: boolean + type: 'Audio', + lang: LanguageItem, + path: string, + isPrimary?: boolean } | { - type: 'Chapters', - lang: LanguageItem, - path: string + type: 'Chapters', + lang: LanguageItem, + path: string } | ({ - type: 'Subtitle', - signs: boolean, - cc: boolean + type: 'Subtitle', + signs: boolean, + cc: boolean } & sxItem ) export type ParseItem = { - __class__?: string; - isSelected?: boolean, - type?: string, - id: string, - title: string, - playback?: string, - season_number?: number|string, - episode_number?: number|string, - season_count?: number|string, - is_premium_only?: boolean, - hide_metadata?: boolean, - seq_id?: string, - f_num?: string, - s_num?: string - external_id?: string, - ep_num?: string - last_public?: string, - subtitle_locales?: string[], - availability_notes?: string, - identifier?: string, - versions?: Version[] | null, - media_type?: string | null, - movie_release_year?: number | null, + __class__?: string; + isSelected?: boolean, + type?: string, + id: string, + title: string, + playback?: string, + season_number?: number|string, + episode_number?: number|string, + season_count?: number|string, + is_premium_only?: boolean, + hide_metadata?: boolean, + seq_id?: string, + f_num?: string, + s_num?: string + external_id?: string, + ep_num?: string + last_public?: string, + subtitle_locales?: string[], + availability_notes?: string, + identifier?: string, + versions?: Version[] | null, + media_type?: string | null, + movie_release_year?: number | null, } export interface SeriesSearch { - total: number; - data: SeriesSearchItem[]; - meta: Meta; + total: number; + data: SeriesSearchItem[]; + meta: Meta; } export interface SeriesSearchItem { - description: string; - seo_description: string; - number_of_episodes: number; - is_dubbed: boolean; - identifier: string; - channel_id: string; - slug_title: string; - season_sequence_number: number; - season_tags: string[]; - extended_maturity_rating: Record<unknown>; - is_mature: boolean; - audio_locale: string; - season_number: number; - images: Record<unknown>; - mature_blocked: boolean; - versions: Version[]; - title: string; - is_subbed: boolean; - id: string; - audio_locales: string[]; - subtitle_locales: string[]; - availability_notes: string; - series_id: string; - season_display_number: string; - is_complete: boolean; - keywords: any[]; - maturity_ratings: string[]; - is_simulcast: boolean; - seo_title: string; + description: string; + seo_description: string; + number_of_episodes: number; + is_dubbed: boolean; + identifier: string; + channel_id: string; + slug_title: string; + season_sequence_number: number; + season_tags: string[]; + extended_maturity_rating: Record<unknown>; + is_mature: boolean; + audio_locale: string; + season_number: number; + images: Record<unknown>; + mature_blocked: boolean; + versions: Version[]; + title: string; + is_subbed: boolean; + id: string; + audio_locales: string[]; + subtitle_locales: string[]; + availability_notes: string; + series_id: string; + season_display_number: string; + is_complete: boolean; + keywords: any[]; + maturity_ratings: string[]; + is_simulcast: boolean; + seo_title: string; } export interface Version { - audio_locale: Locale; - guid: string; - original: boolean; - variant: string; + audio_locale: Locale; + guid: string; + original: boolean; + variant: string; } export interface EpisodeVersion { - audio_locale: Locale; - guid: string; - is_premium_only: boolean; - media_guid: string; - original: boolean; - season_guid: string; - variant: string; + audio_locale: Locale; + guid: string; + is_premium_only: boolean; + media_guid: string; + original: boolean; + season_guid: string; + variant: string; } export enum Locale { - enUS = 'en-US', - esLA = 'es-LA', - es419 = 'es-419', - esES = 'es-ES', - ptBR = 'pt-BR', - frFR = 'fr-FR', - deDE = 'de-DE', - arME = 'ar-ME', - arSA = 'ar-SA', - itIT = 'it-IT', - ruRU = 'ru-RU', - trTR = 'tr-TR', - hiIN = 'hi-IN', - zhCN = 'zh-CN', - koKR = 'ko-KR', - jaJP = 'ja-JP', + enUS = 'en-US', + esLA = 'es-LA', + es419 = 'es-419', + esES = 'es-ES', + ptBR = 'pt-BR', + frFR = 'fr-FR', + deDE = 'de-DE', + arME = 'ar-ME', + arSA = 'ar-SA', + itIT = 'it-IT', + ruRU = 'ru-RU', + trTR = 'tr-TR', + hiIN = 'hi-IN', + zhCN = 'zh-CN', + koKR = 'ko-KR', + jaJP = 'ja-JP', } export interface Meta { - versions_considered: boolean; + versions_considered: boolean; } diff --git a/@types/downloadedFile.d.ts b/@types/downloadedFile.d.ts index 9627b0c..a4240a5 100644 --- a/@types/downloadedFile.d.ts +++ b/@types/downloadedFile.d.ts @@ -1,6 +1,6 @@ import { LanguageItem } from '../modules/module.langsData'; export type DownloadedFile = { - path: string, - lang: LanguageItem + path: string, + lang: LanguageItem } \ No newline at end of file diff --git a/@types/enums.ts b/@types/enums.ts index 310b510..31f337e 100644 --- a/@types/enums.ts +++ b/@types/enums.ts @@ -1,11 +1,11 @@ export enum CrunchyVideoPlayStreams { - 'androidtv' = 'tv/android_tv', - 'android' = 'android/phone', - 'androidtab'= 'android/tablet' + 'androidtv' = 'tv/android_tv', + 'android' = 'android/phone', + 'androidtab'= 'android/tablet' } export enum CrunchyAudioPlayStreams { - 'androidtv' = 'tv/android_tv', - 'android' = 'android/phone', - 'androidtab'= 'android/tablet' + 'androidtv' = 'tv/android_tv', + 'android' = 'android/phone', + 'androidtab'= 'android/tablet' } \ No newline at end of file diff --git a/@types/hidiveDashboard.d.ts b/@types/hidiveDashboard.d.ts index ec78de2..e6fab33 100644 --- a/@types/hidiveDashboard.d.ts +++ b/@types/hidiveDashboard.d.ts @@ -1,70 +1,70 @@ export interface HidiveDashboard { - Code: number; - Status: string; - Message: null; - Messages: object; - Data: Data; - Timestamp: string; - IPAddress: string; + Code: number; + Status: string; + Message: null; + Messages: object; + Data: Data; + Timestamp: string; + IPAddress: string; } export interface Data { - TitleRows: TitleRow[]; - LoadTime: number; + TitleRows: TitleRow[]; + LoadTime: number; } export interface TitleRow { - Name: string; - Titles: Title[]; - LoadTime: number; + Name: string; + Titles: Title[]; + LoadTime: number; } export interface Title { - Id: number; - Name: string; - ShortSynopsis: string; - MediumSynopsis: string; - LongSynopsis: string; - KeyArtUrl: string; - MasterArtUrl: string; - Rating: null | string; - OverallRating: number; - RatingCount: number; - MALScore: null; - UserRating: number; - RunTime: number | null; - ShowInfoTitle: string; - FirstPremiereDate: Date; - EpisodeCount: number; - SeasonName: string; - RokuHDArtUrl: string; - RokuSDArtUrl: string; - IsRateable: boolean; - InQueue: boolean; - IsFavorite: boolean; - IsContinueWatching: boolean; - ContinueWatching: ContinueWatching; - Episodes: any[]; - LoadTime: number; + Id: number; + Name: string; + ShortSynopsis: string; + MediumSynopsis: string; + LongSynopsis: string; + KeyArtUrl: string; + MasterArtUrl: string; + Rating: null | string; + OverallRating: number; + RatingCount: number; + MALScore: null; + UserRating: number; + RunTime: number | null; + ShowInfoTitle: string; + FirstPremiereDate: Date; + EpisodeCount: number; + SeasonName: string; + RokuHDArtUrl: string; + RokuSDArtUrl: string; + IsRateable: boolean; + InQueue: boolean; + IsFavorite: boolean; + IsContinueWatching: boolean; + ContinueWatching: ContinueWatching; + Episodes: any[]; + LoadTime: number; } export interface ContinueWatching { - Id: string; - ProfileId: number; - EpisodeId: number; - Status: Status | null; - CurrentTime: number; - UserId: number; - TitleId: number; - SeasonId: number; - VideoId: number; - TotalSeconds: number; - CreatedDT: Date; - ModifiedDT: Date | null; + Id: string; + ProfileId: number; + EpisodeId: number; + Status: Status | null; + CurrentTime: number; + UserId: number; + TitleId: number; + SeasonId: number; + VideoId: number; + TotalSeconds: number; + CreatedDT: Date; + ModifiedDT: Date | null; } export enum Status { - Paused = 'Paused', - Playing = 'Playing', - Watching = 'Watching', + Paused = 'Paused', + Playing = 'Playing', + Watching = 'Watching', } \ No newline at end of file diff --git a/@types/hidiveEpisodeList.d.ts b/@types/hidiveEpisodeList.d.ts index ce35e8e..b6de9ec 100644 --- a/@types/hidiveEpisodeList.d.ts +++ b/@types/hidiveEpisodeList.d.ts @@ -1,84 +1,84 @@ export interface HidiveEpisodeList { - Code: number; - Status: string; - Message: null; - Messages: Record<unknown, unknown>; - Data: Data; - Timestamp: string; - IPAddress: string; + Code: number; + Status: string; + Message: null; + Messages: Record<unknown, unknown>; + Data: Data; + Timestamp: string; + IPAddress: string; } export interface Data { - Title: HidiveTitle; + Title: HidiveTitle; } export interface HidiveTitle { - Id: number; - Name: string; - ShortSynopsis: string; - MediumSynopsis: string; - LongSynopsis: string; - KeyArtUrl: string; - MasterArtUrl: string; - Rating: string; - OverallRating: number; - RatingCount: number; - MALScore: null; - UserRating: number; - RunTime: number; - ShowInfoTitle: string; - FirstPremiereDate: Date; - EpisodeCount: number; - SeasonName: string; - RokuHDArtUrl: string; - RokuSDArtUrl: string; - IsRateable: boolean; - InQueue: boolean; - IsFavorite: boolean; - IsContinueWatching: boolean; - ContinueWatching: ContinueWatching; - Episodes: HidiveEpisode[]; - LoadTime: number; + Id: number; + Name: string; + ShortSynopsis: string; + MediumSynopsis: string; + LongSynopsis: string; + KeyArtUrl: string; + MasterArtUrl: string; + Rating: string; + OverallRating: number; + RatingCount: number; + MALScore: null; + UserRating: number; + RunTime: number; + ShowInfoTitle: string; + FirstPremiereDate: Date; + EpisodeCount: number; + SeasonName: string; + RokuHDArtUrl: string; + RokuSDArtUrl: string; + IsRateable: boolean; + InQueue: boolean; + IsFavorite: boolean; + IsContinueWatching: boolean; + ContinueWatching: ContinueWatching; + Episodes: HidiveEpisode[]; + LoadTime: number; } export interface ContinueWatching { - Id: string; - ProfileId: number; - EpisodeId: number; - Status: string; - CurrentTime: number; - UserId: number; - TitleId: number; - SeasonId: number; - VideoId: number; - TotalSeconds: number; - CreatedDT: Date; - ModifiedDT: Date; + Id: string; + ProfileId: number; + EpisodeId: number; + Status: string; + CurrentTime: number; + UserId: number; + TitleId: number; + SeasonId: number; + VideoId: number; + TotalSeconds: number; + CreatedDT: Date; + ModifiedDT: Date; } export interface HidiveEpisode { - Id: number; - Number: number; - Name: string; - Summary: string; - HIDIVEPremiereDate: Date; - ScreenShotSmallUrl: string; - ScreenShotCompressedUrl: string; - SeasonNumber: number; - TitleId: number; - SeasonNumberValue: number; - EpisodeNumberValue: number; - VideoKey: string; - DisplayNameLong: string; - PercentProgress: number; - LoadTime: number; + Id: number; + Number: number; + Name: string; + Summary: string; + HIDIVEPremiereDate: Date; + ScreenShotSmallUrl: string; + ScreenShotCompressedUrl: string; + SeasonNumber: number; + TitleId: number; + SeasonNumberValue: number; + EpisodeNumberValue: number; + VideoKey: string; + DisplayNameLong: string; + PercentProgress: number; + LoadTime: number; } export interface HidiveEpisodeExtra extends HidiveEpisode { - titleId: number; - epKey: string; - nameLong: string; - seriesTitle: string; - seriesId?: number; - isSelected: boolean; + titleId: number; + epKey: string; + nameLong: string; + seriesTitle: string; + seriesId?: number; + isSelected: boolean; } \ No newline at end of file diff --git a/@types/hidiveSearch.d.ts b/@types/hidiveSearch.d.ts index 2a6af57..32186d0 100644 --- a/@types/hidiveSearch.d.ts +++ b/@types/hidiveSearch.d.ts @@ -1,47 +1,47 @@ export interface HidiveSearch { - Code: number; - Status: string; - Message: null; - Messages: Record<unknown, unknown>; - Data: HidiveSearchData; - Timestamp: string; - IPAddress: string; + Code: number; + Status: string; + Message: null; + Messages: Record<unknown, unknown>; + Data: HidiveSearchData; + Timestamp: string; + IPAddress: string; } export interface HidiveSearchData { - Query: string; - Slug: string; - TitleResults: HidiveSearchItem[]; - SearchId: number; - IsSearchPinned: boolean; - IsPinnedSearchAvailable: boolean; + Query: string; + Slug: string; + TitleResults: HidiveSearchItem[]; + SearchId: number; + IsSearchPinned: boolean; + IsPinnedSearchAvailable: boolean; } export interface HidiveSearchItem { - Id: number; - Name: string; - ShortSynopsis: string; - MediumSynopsis: string; - LongSynopsis: string; - KeyArtUrl: string; - MasterArtUrl: string; - Rating: string; - OverallRating: number; - RatingCount: number; - MALScore: null; - UserRating: number; - RunTime: number | null; - ShowInfoTitle: string; - FirstPremiereDate: Date; - EpisodeCount: number; - SeasonName: string; - RokuHDArtUrl: string; - RokuSDArtUrl: string; - IsRateable: boolean; - InQueue: boolean; - IsFavorite: boolean; - IsContinueWatching: boolean; - ContinueWatching: null; - Episodes: any[]; - LoadTime: number; + Id: number; + Name: string; + ShortSynopsis: string; + MediumSynopsis: string; + LongSynopsis: string; + KeyArtUrl: string; + MasterArtUrl: string; + Rating: string; + OverallRating: number; + RatingCount: number; + MALScore: null; + UserRating: number; + RunTime: number | null; + ShowInfoTitle: string; + FirstPremiereDate: Date; + EpisodeCount: number; + SeasonName: string; + RokuHDArtUrl: string; + RokuSDArtUrl: string; + IsRateable: boolean; + InQueue: boolean; + IsFavorite: boolean; + IsContinueWatching: boolean; + ContinueWatching: null; + Episodes: any[]; + LoadTime: number; } \ No newline at end of file diff --git a/@types/hidiveTypes.d.ts b/@types/hidiveTypes.d.ts index 858c40d..4eca332 100644 --- a/@types/hidiveTypes.d.ts +++ b/@types/hidiveTypes.d.ts @@ -1,61 +1,61 @@ export interface HidiveVideoList { - Code: number; - Status: string; - Message: null; - Messages: Record<unknown, unknown>; - Data: HidiveVideo; - Timestamp: string; - IPAddress: string; + Code: number; + Status: string; + Message: null; + Messages: Record<unknown, unknown>; + Data: HidiveVideo; + Timestamp: string; + IPAddress: string; } export interface HidiveVideo { - ShowAds: boolean; - CaptionCssUrl: string; - FontSize: number; - FontScale: number; - CaptionLanguages: string[]; - CaptionLanguage: string; - CaptionVttUrls: Record<string, string>; - VideoLanguages: string[]; - VideoLanguage: string; - VideoUrls: Record<string, HidiveStreamList>; - FontColorName: string; - AutoPlayNextEpisode: boolean; - MaxStreams: number; - CurrentTime: number; - FontColorCode: string; - RunTime: number; - AdUrl: null; + ShowAds: boolean; + CaptionCssUrl: string; + FontSize: number; + FontScale: number; + CaptionLanguages: string[]; + CaptionLanguage: string; + CaptionVttUrls: Record<string, string>; + VideoLanguages: string[]; + VideoLanguage: string; + VideoUrls: Record<string, HidiveStreamList>; + FontColorName: string; + AutoPlayNextEpisode: boolean; + MaxStreams: number; + CurrentTime: number; + FontColorCode: string; + RunTime: number; + AdUrl: null; } export interface HidiveStreamList { - hls: string[]; - drm: string[]; - drmEnabled: boolean; + hls: string[]; + drm: string[]; + drmEnabled: boolean; } export interface HidiveStreamInfo extends HidiveStreamList { - language?: string; - episodeTitle?: string; - seriesTitle?: string; - season?: number; - episodeNumber?: number; - uncut?: boolean; - image?: string; + language?: string; + episodeTitle?: string; + seriesTitle?: string; + season?: number; + episodeNumber?: number; + uncut?: boolean; + image?: string; } export interface HidiveSubtitleInfo { - language: string; - cc: boolean; - url: string; + language: string; + cc: boolean; + url: string; } export type DownloadedMedia = { - type: 'Video', - lang: LanguageItem, - path: string, - uncut: boolean + type: 'Video', + lang: LanguageItem, + path: string, + uncut: boolean } | ({ - type: 'Subtitle', - cc: boolean + type: 'Subtitle', + cc: boolean } & sxItem ) \ No newline at end of file diff --git a/@types/iso639.d.ts b/@types/iso639.d.ts index 57f0f24..39ef820 100644 --- a/@types/iso639.d.ts +++ b/@types/iso639.d.ts @@ -1,9 +1,9 @@ declare module 'iso-639' { - export type iso639Type = { - [key: string]: { - '639-1'?: string, - '639-2'?: string - } - } - export const iso_639_2: iso639Type; + export type iso639Type = { + [key: string]: { + '639-1'?: string, + '639-2'?: string + } + } + export const iso_639_2: iso639Type; } \ No newline at end of file diff --git a/@types/items.d.ts b/@types/items.d.ts index cc3ae6e..548d17f 100644 --- a/@types/items.d.ts +++ b/@types/items.d.ts @@ -1,169 +1,169 @@ export interface Item { - // Added later - id: string, - id_split: (number|string)[] - // Added from the start - mostRecentSvodJpnUs: MostRecentSvodJpnUs; - synopsis: string; - mediaCategory: ContentType; - mostRecentSvodUsEndTimestamp: number; - quality: QualityClass; - genres: Genre[]; - titleImages: TitleImages; - engAllTerritoryAvail: EngAllTerritoryAvail; - thumb: string; - mostRecentSvodJpnAllTerrStartTimestamp: number; - title: string; - starRating: number; - primaryAvail: PrimaryAvail; - access: Access[]; - version: Version[]; - mostRecentSvodJpnAllTerrEndTimestamp: number; - itemId: number; - versionAudio: VersionAudio; - contentType: ContentType; - mostRecentSvodUsStartTimestamp: number; - poster: string; - mostRecentSvodEngAllTerrEndTimestamp: number; - mostRecentSvodJpnUsStartTimestamp: number; - mostRecentSvodJpnUsEndTimestamp: number; - mostRecentSvodStartTimestamp: number; - mostRecentSvod: MostRecent; - altAvail: AltAvail; - ids: IDs; - mostRecentSvodUs: MostRecent; - item: Item; - mostRecentSvodEngAllTerrStartTimestamp: number; - audio: string[]; - mostRecentAvod: MostRecent; + // Added later + id: string, + id_split: (number|string)[] + // Added from the start + mostRecentSvodJpnUs: MostRecentSvodJpnUs; + synopsis: string; + mediaCategory: ContentType; + mostRecentSvodUsEndTimestamp: number; + quality: QualityClass; + genres: Genre[]; + titleImages: TitleImages; + engAllTerritoryAvail: EngAllTerritoryAvail; + thumb: string; + mostRecentSvodJpnAllTerrStartTimestamp: number; + title: string; + starRating: number; + primaryAvail: PrimaryAvail; + access: Access[]; + version: Version[]; + mostRecentSvodJpnAllTerrEndTimestamp: number; + itemId: number; + versionAudio: VersionAudio; + contentType: ContentType; + mostRecentSvodUsStartTimestamp: number; + poster: string; + mostRecentSvodEngAllTerrEndTimestamp: number; + mostRecentSvodJpnUsStartTimestamp: number; + mostRecentSvodJpnUsEndTimestamp: number; + mostRecentSvodStartTimestamp: number; + mostRecentSvod: MostRecent; + altAvail: AltAvail; + ids: IDs; + mostRecentSvodUs: MostRecent; + item: Item; + mostRecentSvodEngAllTerrStartTimestamp: number; + audio: string[]; + mostRecentAvod: MostRecent; } export enum ContentType { - Episode = 'episode', - Ova = 'ova', + Episode = 'episode', + Ova = 'ova', } export interface IDs { - externalShowId: ID; - externalSeasonId: ExternalSeasonID; - externalEpisodeId: string; - externalAsianId?: string + externalShowId: ID; + externalSeasonId: ExternalSeasonID; + externalEpisodeId: string; + externalAsianId?: string } export interface Item { - seasonTitle: string; - seasonId: number; - episodeOrder: number; - episodeSlug: string; - created: Date; - titleSlug: string; - episodeNum: string; - episodeId: number; - titleId: number; - seasonNum: string; - ratings: Array<string[]>; - showImage: string; - titleName: string; - runtime: string; - episodeName: string; - seasonOrder: number; - titleExternalId: string; + seasonTitle: string; + seasonId: number; + episodeOrder: number; + episodeSlug: string; + created: Date; + titleSlug: string; + episodeNum: string; + episodeId: number; + titleId: number; + seasonNum: string; + ratings: Array<string[]>; + showImage: string; + titleName: string; + runtime: string; + episodeName: string; + seasonOrder: number; + titleExternalId: string; } export interface MostRecent { - image?: string; - siblingStartTimestamp?: string; - devices?: Device[]; - availId?: number; - distributor?: Distributor; - quality?: MostRecentAvodQuality; - endTimestamp?: string; - mediaCategory?: ContentType; - isPromo?: boolean; - siblingType?: Purchase; - version?: Version; - territory?: Territory; - startDate?: Date; - endDate?: Date; - versionId?: number; - tier?: Device | null; - purchase?: Purchase; - startTimestamp?: string; - language?: Audio; - itemTitle?: string; - ids?: MostRecentAvodIDS; - experience?: number; - siblingEndTimestamp?: string; - item?: Item; - subscriptionRequired?: boolean; - purchased?: boolean; + image?: string; + siblingStartTimestamp?: string; + devices?: Device[]; + availId?: number; + distributor?: Distributor; + quality?: MostRecentAvodQuality; + endTimestamp?: string; + mediaCategory?: ContentType; + isPromo?: boolean; + siblingType?: Purchase; + version?: Version; + territory?: Territory; + startDate?: Date; + endDate?: Date; + versionId?: number; + tier?: Device | null; + purchase?: Purchase; + startTimestamp?: string; + language?: Audio; + itemTitle?: string; + ids?: MostRecentAvodIDS; + experience?: number; + siblingEndTimestamp?: string; + item?: Item; + subscriptionRequired?: boolean; + purchased?: boolean; } export interface MostRecentAvodIDS { - externalSeasonId: ExternalSeasonID; - externalAsianId: null; - externalShowId: ID; - externalEpisodeId: string; - externalEnglishId: string; - externalAlphaId: string; + externalSeasonId: ExternalSeasonID; + externalAsianId: null; + externalShowId: ID; + externalEpisodeId: string; + externalEnglishId: string; + externalAlphaId: string; } export enum Purchase { - AVOD = 'A-VOD', - Dfov = 'DFOV', - Est = 'EST', - Svod = 'SVOD', + AVOD = 'A-VOD', + Dfov = 'DFOV', + Est = 'EST', + Svod = 'SVOD', } export enum Version { - Simulcast = 'Simulcast', - Uncut = 'Uncut', + Simulcast = 'Simulcast', + Uncut = 'Uncut', } export type MostRecentSvodJpnUs = Record<string, any> export interface QualityClass { - quality: QualityQuality; - height: number; + quality: QualityQuality; + height: number; } export enum QualityQuality { - HD = 'HD', - SD = 'SD', + HD = 'HD', + SD = 'SD', } export interface TitleImages { - showThumbnail: string; - showBackgroundSite: string; - showDetailHeaderDesktop: string; - continueWatchingDesktop: string; - showDetailHeroSite: string; - appleHorizontalBannerShow: string; - backgroundImageXbox_360: string; - applePosterCover: string; - showDetailBoxArtTablet: string; - featuredShowBackgroundTablet: string; - backgroundImageAppletvfiretv: string; - newShowDetailHero: string; - showDetailHeroDesktop: string; - showKeyart: string; - continueWatchingMobile: string; - featuredSpotlightShowPhone: string; - appleHorizontalBannerMovie: string; - featuredSpotlightShowTablet: string; - showDetailBoxArtPhone: string; - featuredShowBackgroundPhone: string; - appleSquareCover: string; - backgroundVideo: string; - showMasterKeyArt: string; - newShowDetailHeroPhone: string; - showDetailBoxArtXbox_360: string; - showDetailHeaderMobile: string; - showLogo: string; + showThumbnail: string; + showBackgroundSite: string; + showDetailHeaderDesktop: string; + continueWatchingDesktop: string; + showDetailHeroSite: string; + appleHorizontalBannerShow: string; + backgroundImageXbox_360: string; + applePosterCover: string; + showDetailBoxArtTablet: string; + featuredShowBackgroundTablet: string; + backgroundImageAppletvfiretv: string; + newShowDetailHero: string; + showDetailHeroDesktop: string; + showKeyart: string; + continueWatchingMobile: string; + featuredSpotlightShowPhone: string; + appleHorizontalBannerMovie: string; + featuredSpotlightShowTablet: string; + showDetailBoxArtPhone: string; + featuredShowBackgroundPhone: string; + appleSquareCover: string; + backgroundVideo: string; + showMasterKeyArt: string; + newShowDetailHeroPhone: string; + showDetailBoxArtXbox_360: string; + showDetailHeaderMobile: string; + showLogo: string; } export interface VersionAudio { - Uncut?: Audio[]; - Simulcast: Audio[]; + Uncut?: Audio[]; + Simulcast: Audio[]; } \ No newline at end of file diff --git a/@types/m3u8-parsed.d.ts b/@types/m3u8-parsed.d.ts index 7505adf..e42d532 100644 --- a/@types/m3u8-parsed.d.ts +++ b/@types/m3u8-parsed.d.ts @@ -1,49 +1,49 @@ declare module 'm3u8-parsed' { - export type M3U8 = { - allowCache: boolean, - discontinuityStarts: [], - segments: { - duration: number, - byterange?: { - length: number, - offset: number - }, - uri: string, - key: { - method: string, - uri: string, - }, - timeline: number - }[], - version: number, - mediaGroups: { - [type: string]: { - [index: string]: { - [language: string]: { - default: boolean, - autoselect: boolean, - language: string, - uri: string - } - } - } - }, - playlists: { - uri: string, - timeline: number, - attributes: { - 'CLOSED-CAPTIONS': string, - 'AUDIO': string, - 'FRAME-RATE': number, - 'RESOLUTION': { - width: number, - height: number - }, - 'CODECS': string, - 'AVERAGE-BANDWIDTH': string, - 'BANDWIDTH': number - } - }[], - } - export default function (data: string): M3U8; + export type M3U8 = { + allowCache: boolean, + discontinuityStarts: [], + segments: { + duration: number, + byterange?: { + length: number, + offset: number + }, + uri: string, + key: { + method: string, + uri: string, + }, + timeline: number + }[], + version: number, + mediaGroups: { + [type: string]: { + [index: string]: { + [language: string]: { + default: boolean, + autoselect: boolean, + language: string, + uri: string + } + } + } + }, + playlists: { + uri: string, + timeline: number, + attributes: { + 'CLOSED-CAPTIONS': string, + 'AUDIO': string, + 'FRAME-RATE': number, + 'RESOLUTION': { + width: number, + height: number + }, + 'CODECS': string, + 'AVERAGE-BANDWIDTH': string, + 'BANDWIDTH': number + } + }[], + } + export default function (data: string): M3U8; } \ No newline at end of file diff --git a/@types/messageHandler.d.ts b/@types/messageHandler.d.ts index 7e63da4..6f16ea9 100644 --- a/@types/messageHandler.d.ts +++ b/@types/messageHandler.d.ts @@ -4,97 +4,97 @@ import type { AvailableMuxer } from '../modules/module.args'; import { LanguageItem } from '../modules/module.langsData'; export interface MessageHandler { - name: string - auth: (data: AuthData) => Promise<AuthResponse>; - version: () => Promise<string>; - checkToken: () => Promise<CheckTokenResponse>; - search: (data: SearchData) => Promise<SearchResponse>, - availableDubCodes: () => Promise<string[]>, - availableSubCodes: () => Promise<string[]>, - handleDefault: (name: string) => Promise<any>, - resolveItems: (data: ResolveItemsData) => Promise<boolean>, - listEpisodes: (id: string) => Promise<EpisodeListResponse>, - downloadItem: (data: QueueItem) => void, - isDownloading: () => Promise<boolean>, - openFolder: (path: FolderTypes) => void, - openFile: (data: [FolderTypes, string]) => void, - openURL: (data: string) => void; - getQueue: () => Promise<QueueItem[]>, - removeFromQueue: (index: number) => void, - clearQueue: () => void, - setDownloadQueue: (data: boolean) => void, - getDownloadQueue: () => Promise<boolean> + name: string + auth: (data: AuthData) => Promise<AuthResponse>; + version: () => Promise<string>; + checkToken: () => Promise<CheckTokenResponse>; + search: (data: SearchData) => Promise<SearchResponse>, + availableDubCodes: () => Promise<string[]>, + availableSubCodes: () => Promise<string[]>, + handleDefault: (name: string) => Promise<any>, + resolveItems: (data: ResolveItemsData) => Promise<boolean>, + listEpisodes: (id: string) => Promise<EpisodeListResponse>, + downloadItem: (data: QueueItem) => void, + isDownloading: () => Promise<boolean>, + openFolder: (path: FolderTypes) => void, + openFile: (data: [FolderTypes, string]) => void, + openURL: (data: string) => void; + getQueue: () => Promise<QueueItem[]>, + removeFromQueue: (index: number) => void, + clearQueue: () => void, + setDownloadQueue: (data: boolean) => void, + getDownloadQueue: () => Promise<boolean> } export type FolderTypes = 'content' | 'config'; export type QueueItem = { - title: string, - episode: string, - fileName: string, - dlsubs: string[], - parent: { - title: string, - season: string - }, - q: number, - dlVideoOnce: boolean, - dubLang: string[], - image: string, + title: string, + episode: string, + fileName: string, + dlsubs: string[], + parent: { + title: string, + season: string + }, + q: number, + dlVideoOnce: boolean, + dubLang: string[], + image: string, } & ResolveItemsData export type ResolveItemsData = { - id: string, - dubLang: string[], - all: boolean, - but: boolean, - novids: boolean, - noaudio: boolean - dlVideoOnce: boolean, - e: string, - fileName: string, - q: number, - dlsubs: string[] + id: string, + dubLang: string[], + all: boolean, + but: boolean, + novids: boolean, + noaudio: boolean + dlVideoOnce: boolean, + e: string, + fileName: string, + q: number, + dlsubs: string[] } export type SearchResponseItem = { - image: string, - name: string, - desc?: string, - id: string, - lang?: string[], - rating: number + image: string, + name: string, + desc?: string, + id: string, + lang?: string[], + rating: number }; export type Episode = { - e: string, - lang: string[], - name: string, - season: string, - seasonTitle: string, - episode: string, - id: string, - img: string, - description: string, - time: string + e: string, + lang: string[], + name: string, + season: string, + seasonTitle: string, + episode: string, + id: string, + img: string, + description: string, + time: string } export type SearchResponse = ResponseBase<SearchResponseItem[]> export type EpisodeListResponse = ResponseBase<Episode[]> export type FuniEpisodeData = { - title: string, - episode: string, - epsiodeNumber: string, - episodeID: string, - seasonTitle: string, - seasonNumber: string, - ids: { - episode: string, - show: string, - season: string - }, - image: string + title: string, + episode: string, + epsiodeNumber: string, + episodeID: string, + seasonTitle: string, + seasonNumber: string, + ids: { + episode: string, + show: string, + season: string + }, + image: string }; export type AuthData = { username: string, password: string }; @@ -102,12 +102,12 @@ export type SearchData = { search: string, page?: number, 'search-type'?: string export type FuniGetShowData = { id: number, e?: string, but: boolean, all: boolean }; export type FuniGetEpisodeData = { subs: FuniSubsData, fnSlug: FuniEpisodeData, simul?: boolean; dubLang: string[], s: string } export type FuniStreamData = { force?: 'Y'|'y'|'N'|'n'|'C'|'c', callbackMaker?: (data: DownloadInfo) => HLSCallback, q: number, x: number, fileName: string, numbers: number, novids?: boolean, - timeout: number, partsize: number, fsRetryTime: number, noaudio?: boolean, mp4: boolean, ass: boolean, fontSize: number, fontName?: string, skipmux?: boolean, - forceMuxer: AvailableMuxer | undefined, simul: boolean, skipSubMux: boolean, nocleanup: boolean, override: string[], videoTitle: string, - ffmpegOptions: string[], mkvmergeOptions: string[], defaultAudio: LanguageItem, defaultSub: LanguageItem, ccTag: string } + timeout: number, partsize: number, fsRetryTime: number, noaudio?: boolean, mp4: boolean, ass: boolean, fontSize: number, fontName?: string, skipmux?: boolean, + forceMuxer: AvailableMuxer | undefined, simul: boolean, skipSubMux: boolean, nocleanup: boolean, override: string[], videoTitle: string, + 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>; @@ -118,44 +118,44 @@ export type CheckTokenResponse = ResponseBase<undefined>; export type ResponseBase<T> = ({ - isOk: true, - value: T + isOk: true, + value: T } | { - isOk: false, - reason: Error + isOk: false, + reason: Error }); export type ProgressData = { - total: number, - cur: number, - percent: number|string, - time: number, - downloadSpeed: number, - bytes: number + total: number, + cur: number, + percent: number|string, + time: number, + downloadSpeed: number, + bytes: number }; export type PossibleMessages = keyof ServiceHandler; export type DownloadInfo = { - image: string, - parent: { - title: string - }, - title: string, - language: LanguageItem, - fileName: string + image: string, + parent: { + title: string + }, + title: string, + language: LanguageItem, + fileName: string } export type ExtendedProgress = { - progress: ProgressData, - downloadInfo: DownloadInfo + progress: ProgressData, + downloadInfo: DownloadInfo } export type GuiState = { - setup: boolean, - services: Record<string, GuiStateService> + setup: boolean, + services: Record<string, GuiStateService> } export type GuiStateService = { - queue: QueueItem[] + queue: QueueItem[] } \ No newline at end of file diff --git a/@types/mpd-parser.d.ts b/@types/mpd-parser.d.ts index a45968f..54688a1 100644 --- a/@types/mpd-parser.d.ts +++ b/@types/mpd-parser.d.ts @@ -1,101 +1,101 @@ declare module 'mpd-parser' { - export type Segment = { - uri: string, - timeline: number, - duration: number, - resolvedUri: string, - map: { - uri: string, - resolvedUri: string, - byterange?: { - length: number, - offset: number - } - }, - byterange?: { - length: number, - offset: number - }, - number: number, - presentationTime: number - } + export type Segment = { + uri: string, + timeline: number, + duration: number, + resolvedUri: string, + map: { + uri: string, + resolvedUri: string, + byterange?: { + length: number, + offset: number + } + }, + byterange?: { + length: number, + offset: number + }, + number: number, + presentationTime: number + } - export type Sidx = { - uri: string, - resolvedUri: string, - byterange: { - length: number, - offset: number - }, - map: { - uri: string, - resolvedUri: string, - byterange: { - length: number, - offset: number - } - }, - duration: number, - timeline: number, - presentationTime: number, - number: number - } + export type Sidx = { + uri: string, + resolvedUri: string, + byterange: { + length: number, + offset: number + }, + map: { + uri: string, + resolvedUri: string, + byterange: { + length: number, + offset: number + } + }, + duration: number, + timeline: number, + presentationTime: number, + number: number + } - export type Playlist = { - attributes: { - NAME: string, - BANDWIDTH: number, - CODECS: string, - 'PROGRAM-ID': number, - // Following for video only - 'FRAME-RATE'?: number, - AUDIO?: string, // audio stream name - SUBTITLES?: string, - RESOLUTION?: { - width: number, - height: number - } - }, - uri: string, - endList: boolean, - timeline: number, - resolvedUri: string, - targetDuration: number, - discontinuitySequence: number, - discontinuityStarts: [], - timelineStarts: { - start: number, - timeline: number - }[], - mediaSequence: number, - contentProtection?: { - [type: string]: { - pssh?: Uint8Array - } - } - segments: Segment[] - sidx?: Sidx - } + export type Playlist = { + attributes: { + NAME: string, + BANDWIDTH: number, + CODECS: string, + 'PROGRAM-ID': number, + // Following for video only + 'FRAME-RATE'?: number, + AUDIO?: string, // audio stream name + SUBTITLES?: string, + RESOLUTION?: { + width: number, + height: number + } + }, + uri: string, + endList: boolean, + timeline: number, + resolvedUri: string, + targetDuration: number, + discontinuitySequence: number, + discontinuityStarts: [], + timelineStarts: { + start: number, + timeline: number + }[], + mediaSequence: number, + contentProtection?: { + [type: string]: { + pssh?: Uint8Array + } + } + segments: Segment[] + sidx?: Sidx + } - export type Manifest = { - allowCache: boolean, - discontinuityStarts: [], - segments: [], - endList: true, - duration: number, - playlists: Playlist[], - mediaGroups: { - AUDIO: { - audio: { - [name: string]: { - language: string, - autoselect: boolean, - default: boolean, - playlists: Playlist[] - } - } - } - } - } - export function parse(manifest: string): Manifest + export type Manifest = { + allowCache: boolean, + discontinuityStarts: [], + segments: [], + endList: true, + duration: number, + playlists: Playlist[], + mediaGroups: { + AUDIO: { + audio: { + [name: string]: { + language: string, + autoselect: boolean, + default: boolean, + playlists: Playlist[] + } + } + } + } + } + export function parse(manifest: string): Manifest } diff --git a/@types/newHidiveEpisode.d.ts b/@types/newHidiveEpisode.d.ts index 90fe2c7..9ccf705 100644 --- a/@types/newHidiveEpisode.d.ts +++ b/@types/newHidiveEpisode.d.ts @@ -1,43 +1,43 @@ export interface NewHidiveEpisode { - description: string; - duration: number; - title: string; - categories: string[]; - contentDownload: ContentDownload; - favourite: boolean; - subEvents: any[]; - thumbnailUrl: string; - longDescription: string; - posterUrl: string; - offlinePlaybackLanguages: string[]; - externalAssetId: string; - maxHeight: number; - rating: Rating; - episodeInformation: EpisodeInformation; - id: number; - accessLevel: string; - playerUrlCallback: string; - thumbnailsPreview: string; - displayableTags: any[]; - plugins: any[]; - watchStatus: string; - computedReleases: any[]; - licences: any[]; - type: string; + description: string; + duration: number; + title: string; + categories: string[]; + contentDownload: ContentDownload; + favourite: boolean; + subEvents: any[]; + thumbnailUrl: string; + longDescription: string; + posterUrl: string; + offlinePlaybackLanguages: string[]; + externalAssetId: string; + maxHeight: number; + rating: Rating; + episodeInformation: EpisodeInformation; + id: number; + accessLevel: string; + playerUrlCallback: string; + thumbnailsPreview: string; + displayableTags: any[]; + plugins: any[]; + watchStatus: string; + computedReleases: any[]; + licences: any[]; + type: string; } export interface ContentDownload { - permission: string; - period: string; + permission: string; + period: string; } export interface EpisodeInformation { - seasonNumber: number; - episodeNumber: number; - season: number; + seasonNumber: number; + episodeNumber: number; + season: number; } export interface Rating { - rating: string; - descriptors: any[]; + rating: string; + descriptors: any[]; } \ No newline at end of file diff --git a/@types/newHidivePlayback.d.ts b/@types/newHidivePlayback.d.ts index f25c863..3094c0c 100644 --- a/@types/newHidivePlayback.d.ts +++ b/@types/newHidivePlayback.d.ts @@ -1,33 +1,33 @@ export interface NewHidivePlayback { - watermark: null; - skipMarkers: any[]; - annotations: null; - dash: Format[]; - hls: Format[]; + watermark: null; + skipMarkers: any[]; + annotations: null; + dash: Format[]; + hls: Format[]; } export interface Format { - subtitles: Subtitle[]; - url: string; - drm: DRM; + subtitles: Subtitle[]; + url: string; + drm: DRM; } export interface DRM { - encryptionMode: string; - containerType: string; - jwtToken: string; - url: string; - keySystems: string[]; + encryptionMode: string; + containerType: string; + jwtToken: string; + url: string; + keySystems: string[]; } export interface Subtitle { - format: Formats; - language: string; - url: string; + format: Formats; + language: string; + url: string; } export enum Formats { - Scc = 'scc', - Srt = 'srt', - Vtt = 'vtt', + Scc = 'scc', + Srt = 'srt', + Vtt = 'vtt', } diff --git a/@types/newHidiveSearch.d.ts b/@types/newHidiveSearch.d.ts index 95b6acf..44cd903 100644 --- a/@types/newHidiveSearch.d.ts +++ b/@types/newHidiveSearch.d.ts @@ -1,88 +1,88 @@ export interface NewHidiveSearch { - results: Result[]; + results: Result[]; } export interface Result { - hits: Hit[]; - nbHits: number; - page: number; - nbPages: number; - hitsPerPage: number; - exhaustiveNbHits: boolean; - exhaustiveTypo: boolean; - exhaustive: Exhaustive; - query: string; - params: string; - index: string; - renderingContent: object; - processingTimeMS: number; - processingTimingsMS: ProcessingTimingsMS; - serverTimeMS: number; + hits: Hit[]; + nbHits: number; + page: number; + nbPages: number; + hitsPerPage: number; + exhaustiveNbHits: boolean; + exhaustiveTypo: boolean; + exhaustive: Exhaustive; + query: string; + params: string; + index: string; + renderingContent: object; + processingTimeMS: number; + processingTimingsMS: ProcessingTimingsMS; + serverTimeMS: number; } export interface Exhaustive { - nbHits: boolean; - typo: boolean; + nbHits: boolean; + typo: boolean; } export interface Hit { - type: string; - weight: number; - id: number; - name: string; - description: string; - meta: object; - coverUrl: string; - smallCoverUrl: string; - seasonsCount: number; - tags: string[]; - localisations: HitLocalisations; - ratings: Ratings; - objectID: string; - _highlightResult: HighlightResult; + type: string; + weight: number; + id: number; + name: string; + description: string; + meta: object; + coverUrl: string; + smallCoverUrl: string; + seasonsCount: number; + tags: string[]; + localisations: HitLocalisations; + ratings: Ratings; + objectID: string; + _highlightResult: HighlightResult; } export interface HighlightResult { - name: Description; - description: Description; - tags: Description[]; - localisations: HighlightResultLocalisations; + name: Description; + description: Description; + tags: Description[]; + localisations: HighlightResultLocalisations; } export interface Description { - value: string; - matchLevel: string; - matchedWords: string[]; - fullyHighlighted?: boolean; + value: string; + matchLevel: string; + matchedWords: string[]; + fullyHighlighted?: boolean; } export interface HighlightResultLocalisations { - en_US: PurpleEnUS; + en_US: PurpleEnUS; } export interface PurpleEnUS { - title: Description; - description: Description; + title: Description; + description: Description; } export interface HitLocalisations { - [language: string]: HitLocalization; + [language: string]: HitLocalization; } export interface HitLocalization { - title: string; - description: string; + title: string; + description: string; } export interface Ratings { - US: string[]; + US: string[]; } export interface ProcessingTimingsMS { - _request: Request; + _request: Request; } export interface Request { - queue: number; - roundTrip: number; + queue: number; + roundTrip: number; } diff --git a/@types/newHidiveSeason.d.ts b/@types/newHidiveSeason.d.ts index 0ed1b37..973c725 100644 --- a/@types/newHidiveSeason.d.ts +++ b/@types/newHidiveSeason.d.ts @@ -1,89 +1,89 @@ export interface NewHidiveSeason { - title: string; - description: string; - longDescription: string; - smallCoverUrl: string; - coverUrl: string; - titleUrl: string; - posterUrl: string; - seasonNumber: number; - episodeCount: number; - displayableTags: any[]; - rating: Rating; - contentRating: Rating; - id: number; - series: Series; - episodes: Episode[]; - paging: Paging; - licences: any[]; + title: string; + description: string; + longDescription: string; + smallCoverUrl: string; + coverUrl: string; + titleUrl: string; + posterUrl: string; + seasonNumber: number; + episodeCount: number; + displayableTags: any[]; + rating: Rating; + contentRating: Rating; + id: number; + series: Series; + episodes: Episode[]; + paging: Paging; + licences: any[]; } export interface Rating { - rating: string; - descriptors: any[]; + rating: string; + descriptors: any[]; } export interface Episode { - accessLevel: string; - availablePurchases?: any[]; - licenceIds?: any[]; - type: string; - id: number; - title: string; - description: string; - thumbnailUrl: string; - posterUrl: string; - duration: number; - favourite: boolean; - contentDownload: ContentDownload; - offlinePlaybackLanguages: string[]; - externalAssetId: string; - subEvents: any[]; - maxHeight: number; - thumbnailsPreview: string; - longDescription: string; - episodeInformation: EpisodeInformation; - categories: string[]; - displayableTags: any[]; - watchStatus: string; - computedReleases: any[]; + accessLevel: string; + availablePurchases?: any[]; + licenceIds?: any[]; + type: string; + id: number; + title: string; + description: string; + thumbnailUrl: string; + posterUrl: string; + duration: number; + favourite: boolean; + contentDownload: ContentDownload; + offlinePlaybackLanguages: string[]; + externalAssetId: string; + subEvents: any[]; + maxHeight: number; + thumbnailsPreview: string; + longDescription: string; + episodeInformation: EpisodeInformation; + categories: string[]; + displayableTags: any[]; + watchStatus: string; + computedReleases: any[]; } export interface ContentDownload { - permission: string; + permission: string; } export interface EpisodeInformation { - seasonNumber: number; - episodeNumber: number; - season: number; + seasonNumber: number; + episodeNumber: number; + season: number; } export interface Paging { - moreDataAvailable: boolean; - lastSeen: number; + moreDataAvailable: boolean; + lastSeen: number; } export interface Series { - seriesId: number; - title: string; - description: string; - longDescription: string; - displayableTags: any[]; - rating: Rating; - contentRating: Rating; + seriesId: number; + title: string; + description: string; + longDescription: string; + displayableTags: any[]; + rating: Rating; + contentRating: Rating; } export interface NewHidiveSeriesExtra extends Series { - season: NewHidiveSeason; + season: NewHidiveSeason; } export interface NewHidiveEpisodeExtra extends Episode { - titleId: number; - nameLong: string; - seasonTitle: string; - seriesTitle: string; - seriesId?: number; - isSelected: boolean; - jwtToken?: string; + titleId: number; + nameLong: string; + seasonTitle: string; + seriesTitle: string; + seriesId?: number; + isSelected: boolean; + jwtToken?: string; } \ No newline at end of file diff --git a/@types/newHidiveSeries.d.ts b/@types/newHidiveSeries.d.ts index 4391406..31935d2 100644 --- a/@types/newHidiveSeries.d.ts +++ b/@types/newHidiveSeries.d.ts @@ -1,35 +1,35 @@ export interface NewHidiveSeries { - id: number; - title: string; - description: string; - longDescription: string; - smallCoverUrl: string; - coverUrl: string; - titleUrl: string; - posterUrl: string; - seasons: Season[]; - rating: Rating; - contentRating: Rating; - displayableTags: any[]; - paging: Paging; + id: number; + title: string; + description: string; + longDescription: string; + smallCoverUrl: string; + coverUrl: string; + titleUrl: string; + posterUrl: string; + seasons: Season[]; + rating: Rating; + contentRating: Rating; + displayableTags: any[]; + paging: Paging; } export interface Rating { - rating: string; - descriptors: any[]; + rating: string; + descriptors: any[]; } export interface Paging { - moreDataAvailable: boolean; - lastSeen: number; + moreDataAvailable: boolean; + lastSeen: number; } export interface Season { - title: string; - description: string; - longDescription: string; - seasonNumber: number; - episodeCount: number; - displayableTags: any[]; - id: number; + title: string; + description: string; + longDescription: string; + seasonNumber: number; + episodeCount: number; + displayableTags: any[]; + id: number; } diff --git a/@types/objectInfo.d.ts b/@types/objectInfo.d.ts index 096e2c3..7a9a9e9 100644 --- a/@types/objectInfo.d.ts +++ b/@types/objectInfo.d.ts @@ -1,211 +1,211 @@ // Generated by https://quicktype.io export interface ObjectInfo { - total: number; - data: CrunchyObject[]; - meta: Record<unknown>; + total: number; + data: CrunchyObject[]; + meta: Record<unknown>; } export interface CrunchyObject { - __links__?: Links; - channel_id: string; - slug: string; - images: Images; - linked_resource_key: string; - description: string; - promo_description: string; - external_id: string; - title: string; - series_metadata?: SeriesMetadata; - id: string; - slug_title: string; - type: string; - promo_title: string; - movie_listing_metadata?: MovieListingMetadata; - movie_metadata?: MovieMetadata; - playback?: string; - episode_metadata?: EpisodeMetadata; - streams_link?: string; - season_metadata?: SeasonMetadata; - isSelected?: boolean; - f_num: string; - s_num: string; + __links__?: Links; + channel_id: string; + slug: string; + images: Images; + linked_resource_key: string; + description: string; + promo_description: string; + external_id: string; + title: string; + series_metadata?: SeriesMetadata; + id: string; + slug_title: string; + type: string; + promo_title: string; + movie_listing_metadata?: MovieListingMetadata; + movie_metadata?: MovieMetadata; + playback?: string; + episode_metadata?: EpisodeMetadata; + streams_link?: string; + season_metadata?: SeasonMetadata; + isSelected?: boolean; + f_num: string; + s_num: string; } export interface Links { - 'episode/season': LinkData; - 'episode/series': LinkData; - resource: LinkData; - 'resource/channel': LinkData; - streams: LinkData; + 'episode/season': LinkData; + 'episode/series': LinkData; + resource: LinkData; + 'resource/channel': LinkData; + streams: LinkData; } export interface LinkData { - href: string; + href: string; } export interface EpisodeMetadata { - audio_locale: Locale; - availability_ends: Date; - availability_notes: string; - availability_starts: Date; - available_date: null; - available_offline: boolean; - closed_captions_available: boolean; - duration_ms: number; - eligible_region: string; - episode: string; - episode_air_date: Date; - episode_number: number; - extended_maturity_rating: Record<unknown>; - free_available_date: Date; - identifier: string; - is_clip: boolean; - is_dubbed: boolean; - is_mature: boolean; - is_premium_only: boolean; - is_subbed: boolean; - mature_blocked: boolean; - maturity_ratings: string[]; - premium_available_date: Date; - premium_date: null; - season_id: string; - season_number: number; - season_slug_title: string; - season_title: string; - sequence_number: number; - series_id: string; - series_slug_title: string; - series_title: string; - subtitle_locales: Locale[]; - tenant_categories?: string[]; - upload_date: Date; - versions: EpisodeMetadataVersion[]; + audio_locale: Locale; + availability_ends: Date; + availability_notes: string; + availability_starts: Date; + available_date: null; + available_offline: boolean; + closed_captions_available: boolean; + duration_ms: number; + eligible_region: string; + episode: string; + episode_air_date: Date; + episode_number: number; + extended_maturity_rating: Record<unknown>; + free_available_date: Date; + identifier: string; + is_clip: boolean; + is_dubbed: boolean; + is_mature: boolean; + is_premium_only: boolean; + is_subbed: boolean; + mature_blocked: boolean; + maturity_ratings: string[]; + premium_available_date: Date; + premium_date: null; + season_id: string; + season_number: number; + season_slug_title: string; + season_title: string; + sequence_number: number; + series_id: string; + series_slug_title: string; + series_title: string; + subtitle_locales: Locale[]; + tenant_categories?: string[]; + upload_date: Date; + versions: EpisodeMetadataVersion[]; } export interface EpisodeMetadataVersion { - audio_locale: Locale; - guid: string; - is_premium_only: boolean; - media_guid: string; - original: boolean; - season_guid: string; - variant: string; + audio_locale: Locale; + guid: string; + is_premium_only: boolean; + media_guid: string; + original: boolean; + season_guid: string; + variant: string; } export interface Images { - poster_tall?: Array<Image[]>; - poster_wide?: Array<Image[]>; - promo_image?: Array<Image[]>; - thumbnail?: Array<Image[]>; + poster_tall?: Array<Image[]>; + poster_wide?: Array<Image[]>; + promo_image?: Array<Image[]>; + thumbnail?: Array<Image[]>; } export interface Image { - height: number; - source: string; - type: ImageType; - width: number; + height: number; + source: string; + type: ImageType; + width: number; } export enum ImageType { - PosterTall = 'poster_tall', - PosterWide = 'poster_wide', - PromoImage = 'promo_image', - Thumbnail = 'thumbnail', + PosterTall = 'poster_tall', + PosterWide = 'poster_wide', + PromoImage = 'promo_image', + Thumbnail = 'thumbnail', } export interface MovieListingMetadata { - availability_notes: string; - available_date: null; - available_offline: boolean; - duration_ms: number; - extended_description: string; - extended_maturity_rating: Record<unknown>; - first_movie_id: string; - free_available_date: Date; - is_dubbed: boolean; - is_mature: boolean; - is_premium_only: boolean; - is_subbed: boolean; - mature_blocked: boolean; - maturity_ratings: string[]; - movie_release_year: number; - premium_available_date: Date; - premium_date: null; - subtitle_locales: Locale[]; - tenant_categories: string[]; + availability_notes: string; + available_date: null; + available_offline: boolean; + duration_ms: number; + extended_description: string; + extended_maturity_rating: Record<unknown>; + first_movie_id: string; + free_available_date: Date; + is_dubbed: boolean; + is_mature: boolean; + is_premium_only: boolean; + is_subbed: boolean; + mature_blocked: boolean; + maturity_ratings: string[]; + movie_release_year: number; + premium_available_date: Date; + premium_date: null; + subtitle_locales: Locale[]; + tenant_categories: string[]; } export interface MovieMetadata { - availability_notes: string; - available_offline: boolean; - closed_captions_available: boolean; - duration_ms: number; - extended_maturity_rating: Record<unknown>; - is_dubbed: boolean; - is_mature: boolean; - is_premium_only: boolean; - is_subbed: boolean; - mature_blocked: boolean; - maturity_ratings: string[]; - movie_listing_id: string; - movie_listing_slug_title: string; - movie_listing_title: string; + availability_notes: string; + available_offline: boolean; + closed_captions_available: boolean; + duration_ms: number; + extended_maturity_rating: Record<unknown>; + is_dubbed: boolean; + is_mature: boolean; + is_premium_only: boolean; + is_subbed: boolean; + mature_blocked: boolean; + maturity_ratings: string[]; + movie_listing_id: string; + movie_listing_slug_title: string; + movie_listing_title: string; } export interface SeasonMetadata { - audio_locale: Locale; - audio_locales: Locale[]; - extended_maturity_rating: Record<unknown>; - identifier: string; - is_mature: boolean; - mature_blocked: boolean; - maturity_ratings: string[]; - season_display_number: string; - season_sequence_number: number; - subtitle_locales: Locale[]; - versions: SeasonMetadataVersion[]; + audio_locale: Locale; + audio_locales: Locale[]; + extended_maturity_rating: Record<unknown>; + identifier: string; + is_mature: boolean; + mature_blocked: boolean; + maturity_ratings: string[]; + season_display_number: string; + season_sequence_number: number; + subtitle_locales: Locale[]; + versions: SeasonMetadataVersion[]; } export interface SeasonMetadataVersion { - audio_locale: Locale; - guid: string; - original: boolean; - variant: string; + audio_locale: Locale; + guid: string; + original: boolean; + variant: string; } export interface SeriesMetadata { - audio_locales: Locale[]; - availability_notes: string; - episode_count: number; - extended_description: string; - extended_maturity_rating: Record<unknown>; - is_dubbed: boolean; - is_mature: boolean; - is_simulcast: boolean; - is_subbed: boolean; - mature_blocked: boolean; - maturity_ratings: string[]; - season_count: number; - series_launch_year: number; - subtitle_locales: Locale[]; - tenant_categories?: string[]; + audio_locales: Locale[]; + availability_notes: string; + episode_count: number; + extended_description: string; + extended_maturity_rating: Record<unknown>; + is_dubbed: boolean; + is_mature: boolean; + is_simulcast: boolean; + is_subbed: boolean; + mature_blocked: boolean; + maturity_ratings: string[]; + season_count: number; + series_launch_year: number; + subtitle_locales: Locale[]; + tenant_categories?: string[]; } export enum Locale { - enUS = 'en-US', - esLA = 'es-LA', - es419 = 'es-419', - esES = 'es-ES', - ptBR = 'pt-BR', - frFR = 'fr-FR', - deDE = 'de-DE', - arME = 'ar-ME', - arSA = 'ar-SA', - itIT = 'it-IT', - ruRU = 'ru-RU', - trTR = 'tr-TR', - hiIN = 'hi-IN', - zhCN = 'zh-CN', - koKR = 'ko-KR', - jaJP = 'ja-JP', + enUS = 'en-US', + esLA = 'es-LA', + es419 = 'es-419', + esES = 'es-ES', + ptBR = 'pt-BR', + frFR = 'fr-FR', + deDE = 'de-DE', + arME = 'ar-ME', + arSA = 'ar-SA', + itIT = 'it-IT', + ruRU = 'ru-RU', + trTR = 'tr-TR', + hiIN = 'hi-IN', + zhCN = 'zh-CN', + koKR = 'ko-KR', + jaJP = 'ja-JP', } \ No newline at end of file diff --git a/@types/pkg.d.ts b/@types/pkg.d.ts index b22e702..ba03e2a 100644 --- a/@types/pkg.d.ts +++ b/@types/pkg.d.ts @@ -1,3 +1,3 @@ declare module 'pkg' { - export async function exec(config: string[]); + export async function exec(config: string[]); } \ No newline at end of file diff --git a/@types/playbackData.d.ts b/@types/playbackData.d.ts index 015f41a..6223ab1 100644 --- a/@types/playbackData.d.ts +++ b/@types/playbackData.d.ts @@ -1,120 +1,120 @@ // Generated by https://quicktype.io export interface PlaybackData { - total: number; - vpb: { [key: string]: { [key: string]: StreamDetails } }; - apb: { [key: string]: { [key: string]: StreamDetails } }; - meta: Meta; + total: number; + vpb: { [key: string]: { [key: string]: StreamDetails } }; + apb: { [key: string]: { [key: string]: StreamDetails } }; + meta: Meta; } export interface StreamList { - download_hls: CrunchyStreams; - drm_adaptive_hls: CrunchyStreams; - multitrack_adaptive_hls_v2: CrunchyStreams; - vo_adaptive_hls: CrunchyStreams; - vo_drm_adaptive_hls: CrunchyStreams; - adaptive_hls: CrunchyStreams; - drm_download_dash: CrunchyStreams; - drm_download_hls: CrunchyStreams; - drm_multitrack_adaptive_hls_v2: CrunchyStreams; - vo_drm_adaptive_dash: CrunchyStreams; - adaptive_dash: CrunchyStreams; - urls: CrunchyStreams; - vo_adaptive_dash: CrunchyStreams; - download_dash: CrunchyStreams; - drm_adaptive_dash: CrunchyStreams; + download_hls: CrunchyStreams; + drm_adaptive_hls: CrunchyStreams; + multitrack_adaptive_hls_v2: CrunchyStreams; + vo_adaptive_hls: CrunchyStreams; + vo_drm_adaptive_hls: CrunchyStreams; + adaptive_hls: CrunchyStreams; + drm_download_dash: CrunchyStreams; + drm_download_hls: CrunchyStreams; + drm_multitrack_adaptive_hls_v2: CrunchyStreams; + vo_drm_adaptive_dash: CrunchyStreams; + adaptive_dash: CrunchyStreams; + urls: CrunchyStreams; + vo_adaptive_dash: CrunchyStreams; + download_dash: CrunchyStreams; + drm_adaptive_dash: CrunchyStreams; } export interface CrunchyStreams { - '': StreamDetails; - 'en-US'?: StreamDetails; - 'es-LA'?: StreamDetails; - 'es-419'?: StreamDetails; - 'es-ES'?: StreamDetails; - 'pt-BR'?: StreamDetails; - 'fr-FR'?: StreamDetails; - 'de-DE'?: StreamDetails; - 'ar-ME'?: StreamDetails; - 'ar-SA'?: StreamDetails; - 'it-IT'?: StreamDetails; - 'ru-RU'?: StreamDetails; - 'tr-TR'?: StreamDetails; - 'hi-IN'?: StreamDetails; - 'zh-CN'?: StreamDetails; - 'ko-KR'?: StreamDetails; - 'ja-JP'?: StreamDetails; - [string: string]: StreamDetails; + '': StreamDetails; + 'en-US'?: StreamDetails; + 'es-LA'?: StreamDetails; + 'es-419'?: StreamDetails; + 'es-ES'?: StreamDetails; + 'pt-BR'?: StreamDetails; + 'fr-FR'?: StreamDetails; + 'de-DE'?: StreamDetails; + 'ar-ME'?: StreamDetails; + 'ar-SA'?: StreamDetails; + 'it-IT'?: StreamDetails; + 'ru-RU'?: StreamDetails; + 'tr-TR'?: StreamDetails; + 'hi-IN'?: StreamDetails; + 'zh-CN'?: StreamDetails; + 'ko-KR'?: StreamDetails; + 'ja-JP'?: StreamDetails; + [string: string]: StreamDetails; } export interface StreamDetails { - //hardsub_locale: Locale; - hardsub_locale: string; - url: string; - hardsub_lang?: string; - audio_lang?: string; - type?: string; + //hardsub_locale: Locale; + hardsub_locale: string; + url: string; + hardsub_lang?: string; + audio_lang?: string; + type?: string; } export interface Meta { - media_id: string; - subtitles: Subtitles; - bifs: string[]; - versions: Version[]; - audio_locale: Locale; - closed_captions: Subtitles; - captions: Subtitles; + media_id: string; + subtitles: Subtitles; + bifs: string[]; + versions: Version[]; + audio_locale: Locale; + closed_captions: Subtitles; + captions: Subtitles; } export interface Subtitles { - ''?: SubtitleInfo; - 'en-US'?: SubtitleInfo; - 'es-LA'?: SubtitleInfo; - 'es-419'?: SubtitleInfo; - 'es-ES'?: SubtitleInfo; - 'pt-BR'?: SubtitleInfo; - 'fr-FR'?: SubtitleInfo; - 'de-DE'?: SubtitleInfo; - 'ar-ME'?: SubtitleInfo; - 'ar-SA'?: SubtitleInfo; - 'it-IT'?: SubtitleInfo; - 'ru-RU'?: SubtitleInfo; - 'tr-TR'?: SubtitleInfo; - 'hi-IN'?: SubtitleInfo; - 'zh-CN'?: SubtitleInfo; - 'ko-KR'?: SubtitleInfo; - 'ja-JP'?: SubtitleInfo; + ''?: SubtitleInfo; + 'en-US'?: SubtitleInfo; + 'es-LA'?: SubtitleInfo; + 'es-419'?: SubtitleInfo; + 'es-ES'?: SubtitleInfo; + 'pt-BR'?: SubtitleInfo; + 'fr-FR'?: SubtitleInfo; + 'de-DE'?: SubtitleInfo; + 'ar-ME'?: SubtitleInfo; + 'ar-SA'?: SubtitleInfo; + 'it-IT'?: SubtitleInfo; + 'ru-RU'?: SubtitleInfo; + 'tr-TR'?: SubtitleInfo; + 'hi-IN'?: SubtitleInfo; + 'zh-CN'?: SubtitleInfo; + 'ko-KR'?: SubtitleInfo; + 'ja-JP'?: SubtitleInfo; } export interface SubtitleInfo { - format: string; - locale: Locale; - url: string; + format: string; + locale: Locale; + url: string; } export interface Version { - audio_locale: Locale; - guid: string; - is_premium_only: boolean; - media_guid: string; - original: boolean; - season_guid: string; - variant: string; + audio_locale: Locale; + guid: string; + is_premium_only: boolean; + media_guid: string; + original: boolean; + season_guid: string; + variant: string; } export enum Locale { - default = '', - enUS = 'en-US', - esLA = 'es-LA', - es419 = 'es-419', - esES = 'es-ES', - ptBR = 'pt-BR', - frFR = 'fr-FR', - deDE = 'de-DE', - arME = 'ar-ME', - arSA = 'ar-SA', - itIT = 'it-IT', - ruRU = 'ru-RU', - trTR = 'tr-TR', - hiIN = 'hi-IN', - zhCN = 'zh-CN', - koKR = 'ko-KR', - jaJP = 'ja-JP', + default = '', + enUS = 'en-US', + esLA = 'es-LA', + es419 = 'es-419', + esES = 'es-ES', + ptBR = 'pt-BR', + frFR = 'fr-FR', + deDE = 'de-DE', + arME = 'ar-ME', + arSA = 'ar-SA', + itIT = 'it-IT', + ruRU = 'ru-RU', + trTR = 'tr-TR', + hiIN = 'hi-IN', + zhCN = 'zh-CN', + koKR = 'ko-KR', + jaJP = 'ja-JP', } \ No newline at end of file diff --git a/@types/randomEvents.d.ts b/@types/randomEvents.d.ts index 41ea519..357029f 100644 --- a/@types/randomEvents.d.ts +++ b/@types/randomEvents.d.ts @@ -1,15 +1,15 @@ import { ExtendedProgress, QueueItem } from './messageHandler'; export type RandomEvents = { - progress: ExtendedProgress, - finish: undefined, - queueChange: QueueItem[], - current: QueueItem|undefined + progress: ExtendedProgress, + finish: undefined, + queueChange: QueueItem[], + current: QueueItem|undefined } export interface RandomEvent<T extends keyof RandomEvents> { - name: T, - data: RandomEvents[T] + name: T, + data: RandomEvents[T] } export type Handler<T extends keyof RandomEvents> = (data: RandomEvent<T>) => unknown; \ No newline at end of file diff --git a/@types/removeNPMAbsolutePaths.d.ts b/@types/removeNPMAbsolutePaths.d.ts index b48935a..f141c00 100644 --- a/@types/removeNPMAbsolutePaths.d.ts +++ b/@types/removeNPMAbsolutePaths.d.ts @@ -1,3 +1,3 @@ declare module 'removeNPMAbsolutePaths' { - export default async function modulesCleanup(path: string); + export default async function modulesCleanup(path: string); } \ No newline at end of file diff --git a/@types/serviceClassInterface.d.ts b/@types/serviceClassInterface.d.ts index aefc890..4115bd6 100644 --- a/@types/serviceClassInterface.d.ts +++ b/@types/serviceClassInterface.d.ts @@ -1,3 +1,3 @@ export interface ServiceClass { - cli: () => Promise<boolean|undefined|void> + cli: () => Promise<boolean|undefined|void> } \ No newline at end of file diff --git a/@types/streamData.d.ts b/@types/streamData.d.ts index 351c96f..16ed2e7 100644 --- a/@types/streamData.d.ts +++ b/@types/streamData.d.ts @@ -1,28 +1,28 @@ // Generated by https://quicktype.io export interface StreamData { - items: Item[]; - watchHistorySaveInterval: number; - errors?: Error[] + items: Item[]; + watchHistorySaveInterval: number; + errors?: Error[] } export interface Error { - detail: string, - code: number + detail: string, + code: number } export interface Item { - src: string; - kind: string; - isPromo: boolean; - videoType: string; - aips: Aip[]; - experienceId: string; - showAds: boolean; - id: number; + src: string; + kind: string; + isPromo: boolean; + videoType: string; + aips: Aip[]; + experienceId: string; + showAds: boolean; + id: number; } export interface Aip { - out: number; - in: number; + out: number; + in: number; } diff --git a/@types/updateFile.d.ts b/@types/updateFile.d.ts index 7f00777..3960260 100644 --- a/@types/updateFile.d.ts +++ b/@types/updateFile.d.ts @@ -1,4 +1,4 @@ export type UpdateFile = { - lastCheck: number, - nextCheck: number + lastCheck: number, + nextCheck: number } \ No newline at end of file diff --git a/@types/ws.d.ts b/@types/ws.d.ts index 3669f28..1c85447 100644 --- a/@types/ws.d.ts +++ b/@types/ws.d.ts @@ -2,44 +2,44 @@ import { GUIConfig } from '../modules/module.cfg-loader'; import { AuthResponse, CheckTokenResponse, EpisodeListResponse, FolderTypes, QueueItem, ResolveItemsData, SearchData, SearchResponse } from './messageHandler'; export type WSMessage<T extends keyof MessageTypes, P extends 0|1 = 0> = { - name: T, - data: MessageTypes[T][P] + name: T, + data: MessageTypes[T][P] } export type WSMessageWithID<T extends keyof MessageTypes, P extends 0|1 = 0> = WSMessage<T, P> & { - id: string + id: string } export type UnknownWSMessage = { - name: keyof MessageTypes, - data: MessageTypes[keyof MessageTypes][0], - id: string + name: keyof MessageTypes, + data: MessageTypes[keyof MessageTypes][0], + id: string } export type MessageTypes = { - 'auth': [AuthData, AuthResponse], - 'version': [undefined, string], - 'checkToken': [undefined, CheckTokenResponse], - 'search': [SearchData, SearchResponse], - 'default': [string, unknown], - 'availableDubCodes': [undefined, string[]], - 'availableSubCodes': [undefined, string[]], - 'resolveItems': [ResolveItemsData, boolean], - 'listEpisodes': [string, EpisodeListResponse], - 'downloadItem': [QueueItem, undefined], - 'isDownloading': [undefined, boolean], - 'openFolder': [FolderTypes, undefined], - 'changeProvider': [undefined, boolean], - 'type': [undefined, 'crunchy'|'hidive'|'ao'|'adn'|undefined], - 'setup': ['crunchy'|'hidive'|'ao'|'adn'|undefined, undefined], - 'openFile': [[FolderTypes, string], undefined], - 'openURL': [string, undefined], - 'isSetup': [undefined, boolean], - 'setupServer': [GUIConfig, boolean], - 'requirePassword': [undefined, boolean], - 'getQueue': [undefined, QueueItem[]], - 'removeFromQueue': [number, undefined], - 'clearQueue': [undefined, undefined], - 'setDownloadQueue': [boolean, undefined], - 'getDownloadQueue': [undefined, boolean] + 'auth': [AuthData, AuthResponse], + 'version': [undefined, string], + 'checkToken': [undefined, CheckTokenResponse], + 'search': [SearchData, SearchResponse], + 'default': [string, unknown], + 'availableDubCodes': [undefined, string[]], + 'availableSubCodes': [undefined, string[]], + 'resolveItems': [ResolveItemsData, boolean], + 'listEpisodes': [string, EpisodeListResponse], + 'downloadItem': [QueueItem, undefined], + 'isDownloading': [undefined, boolean], + 'openFolder': [FolderTypes, undefined], + 'changeProvider': [undefined, boolean], + 'type': [undefined, 'crunchy'|'hidive'|'ao'|'adn'|undefined], + 'setup': ['crunchy'|'hidive'|'ao'|'adn'|undefined, undefined], + 'openFile': [[FolderTypes, string], undefined], + 'openURL': [string, undefined], + 'isSetup': [undefined, boolean], + 'setupServer': [GUIConfig, boolean], + 'requirePassword': [undefined, boolean], + 'getQueue': [undefined, QueueItem[]], + 'removeFromQueue': [number, undefined], + 'clearQueue': [undefined, undefined], + 'setDownloadQueue': [boolean, undefined], + 'getDownloadQueue': [undefined, boolean] } \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs index 95dc8d6..eed41c9 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -56,8 +56,10 @@ export default tseslint.config( files: ['gui/react/**/*'], rules: { 'no-console': 0, - // Disabled because ESLint bugs around on .tsx files somehow? - indent: 'off' + 'indent': [ + 'error', + 4 + ], } } ); \ No newline at end of file diff --git a/gui/react/.babelrc b/gui/react/.babelrc index 4c2774a..0761517 100644 --- a/gui/react/.babelrc +++ b/gui/react/.babelrc @@ -1,3 +1,3 @@ { - "presets": ["@babel/preset-env","@babel/preset-react", "@babel/preset-typescript"] + "presets": ["@babel/preset-env","@babel/preset-react", "@babel/preset-typescript"] } \ No newline at end of file diff --git a/gui/react/package.json b/gui/react/package.json index 77b45e6..a2b704c 100644 --- a/gui/react/package.json +++ b/gui/react/package.json @@ -1,57 +1,57 @@ { - "name": "anidl-gui", - "version": "1.0.0", - "private": true, - "dependencies": { - "@emotion/react": "^11.14.0", - "@emotion/styled": "^11.14.0", - "@mui/icons-material": "^7.1.2", - "@mui/lab": "7.0.0-beta.12", - "@mui/material": "^7.1.2", - "concurrently": "^9.2.0", - "notistack": "^3.0.2", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "typescript": "^5.8.3", - "uuid": "^11.1.0", - "ws": "^8.18.2" - }, - "devDependencies": { - "@babel/cli": "^7.27.2", - "@babel/core": "^7.27.4", - "@babel/preset-env": "^7.27.2", - "@babel/preset-react": "^7.27.1", - "@babel/preset-typescript": "^7.27.1", - "@types/node": "^22.15.32", - "@types/react": "^19.1.8", - "@types/react-dom": "^19.1.6", - "@types/uuid": "^10.0.0", - "babel-loader": "^10.0.0", - "css-loader": "^7.1.2", - "html-webpack-plugin": "^5.6.3", - "style-loader": "^4.0.0", - "ts-node": "^10.9.2", - "webpack": "^5.99.9", - "webpack-cli": "^6.0.1", - "webpack-dev-server": "^5.2.2" - }, - "proxy": "http://localhost:3000", - "scripts": { - "build": "npx tsc && npx webpack", - "start": "npx concurrently -k npm:frontend npm:backend", - "frontend": "npx webpack-dev-server", - "backend": "npx ts-node -T ../../gui.ts" - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - } -} + "name": "anidl-gui", + "version": "1.0.0", + "private": true, + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@mui/icons-material": "^7.1.2", + "@mui/lab": "7.0.0-beta.12", + "@mui/material": "^7.1.2", + "concurrently": "^9.2.0", + "notistack": "^3.0.2", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "typescript": "^5.8.3", + "uuid": "^11.1.0", + "ws": "^8.18.2" + }, + "devDependencies": { + "@babel/cli": "^7.27.2", + "@babel/core": "^7.27.4", + "@babel/preset-env": "^7.27.2", + "@babel/preset-react": "^7.27.1", + "@babel/preset-typescript": "^7.27.1", + "@types/node": "^22.15.32", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@types/uuid": "^10.0.0", + "babel-loader": "^10.0.0", + "css-loader": "^7.1.2", + "html-webpack-plugin": "^5.6.3", + "style-loader": "^4.0.0", + "ts-node": "^10.9.2", + "webpack": "^5.99.9", + "webpack-cli": "^6.0.1", + "webpack-dev-server": "^5.2.2" + }, + "proxy": "http://localhost:3000", + "scripts": { + "build": "npx tsc && npx webpack", + "start": "npx concurrently -k npm:frontend npm:backend", + "frontend": "npx webpack-dev-server", + "backend": "npx ts-node -T ../../gui.ts" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} \ No newline at end of file diff --git a/gui/react/public/index.html b/gui/react/public/index.html index e0f99bb..08d1831 100644 --- a/gui/react/public/index.html +++ b/gui/react/public/index.html @@ -1,15 +1,15 @@ <!DOCTYPE html> <html> - <head> - <title>Multi Downloader - - - - - -
- + + Multi Downloader + + + + + +
+ diff --git a/gui/react/src/@types/FC.d.ts b/gui/react/src/@types/FC.d.ts index 5ec3de1..11963df 100644 --- a/gui/react/src/@types/FC.d.ts +++ b/gui/react/src/@types/FC.d.ts @@ -1,3 +1,3 @@ type FCWithChildren = React.FC<{ - children?: React.ReactNode[]|React.ReactNode + children?: React.ReactNode[]|React.ReactNode } & T> \ No newline at end of file diff --git a/gui/react/src/App.tsx b/gui/react/src/App.tsx index 8f278f3..42b08bf 100644 --- a/gui/react/src/App.tsx +++ b/gui/react/src/App.tsx @@ -2,9 +2,9 @@ import React from 'react'; import Layout from './Layout'; const App: React.FC = () => { - return ( - - ); + return ( + + ); }; export default App; diff --git a/gui/react/src/Layout.tsx b/gui/react/src/Layout.tsx index 478ae91..d9b50f1 100644 --- a/gui/react/src/Layout.tsx +++ b/gui/react/src/Layout.tsx @@ -11,28 +11,28 @@ import MenuBar from './components/MenuBar/MenuBar'; const Layout: React.FC = () => { - const messageHandler = React.useContext(messageChannelContext); + const messageHandler = React.useContext(messageChannelContext); - return - - - - - - - - - - - ; + return + + + + + + + + + + + ; }; export default Layout; \ No newline at end of file diff --git a/gui/react/src/Style.tsx b/gui/react/src/Style.tsx index 1195472..643ba40 100644 --- a/gui/react/src/Style.tsx +++ b/gui/react/src/Style.tsx @@ -2,18 +2,18 @@ import React from 'react'; import { Container, Box, ThemeProvider, createTheme, Theme } from '@mui/material'; const makeTheme = (mode: 'dark'|'light') : Partial => { - return createTheme({ - palette: { - mode, - }, - }); + return createTheme({ + palette: { + mode, + }, + }); }; const Style: FCWithChildren = ({children}) => { - return - - {children} - ; + return + + {children} + ; }; export default Style; \ No newline at end of file diff --git a/gui/react/src/components/AddToQueue/AddToQueue.tsx b/gui/react/src/components/AddToQueue/AddToQueue.tsx index 6e19041..251f4d5 100644 --- a/gui/react/src/components/AddToQueue/AddToQueue.tsx +++ b/gui/react/src/components/AddToQueue/AddToQueue.tsx @@ -6,22 +6,22 @@ import EpisodeListing from './DownloadSelector/Listing/EpisodeListing'; import SearchBox from './SearchBox/SearchBox'; const AddToQueue: React.FC = () => { - const [isOpen, setOpen] = React.useState(false); + const [isOpen, setOpen] = React.useState(false); - return - - setOpen(false)} maxWidth='md' PaperProps={{ elevation:4 }}> - - - - setOpen(false)} /> - - - - ; + + ; }; export default AddToQueue; \ No newline at end of file diff --git a/gui/react/src/components/AddToQueue/DownloadSelector/DownloadSelector.tsx b/gui/react/src/components/AddToQueue/DownloadSelector/DownloadSelector.tsx index 586518b..bac174f 100644 --- a/gui/react/src/components/AddToQueue/DownloadSelector/DownloadSelector.tsx +++ b/gui/react/src/components/AddToQueue/DownloadSelector/DownloadSelector.tsx @@ -12,316 +12,316 @@ type DownloadSelectorProps = { } const DownloadSelector: React.FC = ({ onFinish }) => { - const messageHandler = React.useContext(messageChannelContext); - const [store, dispatch] = useStore(); - const [availableDubs, setAvailableDubs] = React.useState([]); - const [availableSubs, setAvailableSubs ] = React.useState([]); - const [ loading, setLoading ] = React.useState(false); - const { enqueueSnackbar } = useSnackbar(); - const ITEM_HEIGHT = 48; - const ITEM_PADDING_TOP = 8; + const messageHandler = React.useContext(messageChannelContext); + const [store, dispatch] = useStore(); + const [availableDubs, setAvailableDubs] = React.useState([]); + const [availableSubs, setAvailableSubs ] = React.useState([]); + const [ loading, setLoading ] = React.useState(false); + const { enqueueSnackbar } = useSnackbar(); + const ITEM_HEIGHT = 48; + const ITEM_PADDING_TOP = 8; - React.useEffect(() => { - (async () => { - /* If we don't wait the response is undefined? */ - await new Promise((resolve) => setTimeout(() => resolve(undefined), 100)); - const dubLang = messageHandler?.handleDefault('dubLang'); - const subLang = messageHandler?.handleDefault('dlsubs'); - const q = messageHandler?.handleDefault('q'); - const fileName = messageHandler?.handleDefault('fileName'); - const dlVideoOnce = messageHandler?.handleDefault('dlVideoOnce'); - const result = await Promise.all([dubLang, subLang, q, fileName, dlVideoOnce]); - dispatch({ - type: 'downloadOptions', - payload: { - ...store.downloadOptions, - dubLang: result[0], - dlsubs: result[1], - q: result[2], - fileName: result[3], - dlVideoOnce: result[4], + React.useEffect(() => { + (async () => { + /* If we don't wait the response is undefined? */ + await new Promise((resolve) => setTimeout(() => resolve(undefined), 100)); + const dubLang = messageHandler?.handleDefault('dubLang'); + const subLang = messageHandler?.handleDefault('dlsubs'); + const q = messageHandler?.handleDefault('q'); + const fileName = messageHandler?.handleDefault('fileName'); + const dlVideoOnce = messageHandler?.handleDefault('dlVideoOnce'); + const result = await Promise.all([dubLang, subLang, q, fileName, dlVideoOnce]); + dispatch({ + type: 'downloadOptions', + payload: { + ...store.downloadOptions, + dubLang: result[0], + dlsubs: result[1], + q: result[2], + fileName: result[3], + dlVideoOnce: result[4], + } + }); + setAvailableDubs(await messageHandler?.availableDubCodes() ?? []); + setAvailableSubs(await messageHandler?.availableSubCodes() ?? []); + })(); + }, []); + + const addToQueue = async () => { + setLoading(true); + const res = await messageHandler?.resolveItems(store.downloadOptions); + if (!res) + return enqueueSnackbar('The request failed. Please check if the ID is correct.', { + variant: 'error' + }); + setLoading(false); + if (onFinish) + onFinish(); + }; + + const listEpisodes = async () => { + if (!store.downloadOptions.id) { + return enqueueSnackbar('Please enter a ID', { + variant: 'error' + }); } - }); - setAvailableDubs(await messageHandler?.availableDubCodes() ?? []); - setAvailableSubs(await messageHandler?.availableSubCodes() ?? []); - })(); - }, []); - - const addToQueue = async () => { - setLoading(true); - const res = await messageHandler?.resolveItems(store.downloadOptions); - if (!res) - return enqueueSnackbar('The request failed. Please check if the ID is correct.', { - variant: 'error' - }); - setLoading(false); - if (onFinish) - onFinish(); - }; - - const listEpisodes = async () => { - if (!store.downloadOptions.id) { - return enqueueSnackbar('Please enter a ID', { - variant: 'error' - }); - } - setLoading(true); - const res = await messageHandler?.listEpisodes(store.downloadOptions.id); - if (!res || !res.isOk) { - setLoading(false); - return enqueueSnackbar('The request failed. Please check if the ID is correct.', { - variant: 'error' - }); - } else { - dispatch({ - type: 'episodeListing', - payload: res.value - }); - } - setLoading(false); - }; - - return - - - - - General Options - - { - dispatch({ - type: 'downloadOptions', - payload: { ...store.downloadOptions, id: e.target.value } + setLoading(true); + const res = await messageHandler?.listEpisodes(store.downloadOptions.id); + if (!res || !res.isOk) { + setLoading(false); + return enqueueSnackbar('The request failed. Please check if the ID is correct.', { + variant: 'error' }); - }} label='Show ID'/> - { - const parsed = parseInt(e.target.value); - if (isNaN(parsed) || parsed < 0 || parsed > 10) - return; + } else { dispatch({ - type: 'downloadOptions', - payload: { ...store.downloadOptions, q: parsed } + type: 'episodeListing', + payload: res.value }); - }} label='Quality Level (0 for max)'/> - - - - - - - Simulcast is only supported on Hidive - } - arrow placement='top' - > - - - - - - - - Episode Options - - + - - { - dispatch({ - type: 'downloadOptions', - payload: { ...store.downloadOptions, e: e.target.value } - }); - }} placeholder='Episode Select'/> - - List
Episodes
-
-
- - -
- - - Language Options - - { - dispatch({ - type: 'downloadOptions', - payload: { ...store.downloadOptions, dubLang: e } - }); - }} - allOption - /> - - { - dispatch({ - type: 'downloadOptions', - payload: { ...store.downloadOptions, dlsubs: e } - }); - }} - /> - - Hardsubs are only supported on Crunchyroll - - } - arrow placement='top'> - - - - Hardsub Language - - - + justifyContent: 'space-between', + //backgroundColor: '#ffffff30', + }}> + + + General Options + + { + dispatch({ + type: 'downloadOptions', + payload: { ...store.downloadOptions, id: e.target.value } + }); + }} label='Show ID'/> + { + const parsed = parseInt(e.target.value); + if (isNaN(parsed) || parsed < 0 || parsed > 10) + return; + dispatch({ + type: 'downloadOptions', + payload: { ...store.downloadOptions, q: parsed } + }); + }} label='Quality Level (0 for max)'/> + + + + + + + Simulcast is only supported on Hidive + } + arrow placement='top' + > + + + + + + + + Episode Options + + + + { + dispatch({ + type: 'downloadOptions', + payload: { ...store.downloadOptions, e: e.target.value } + }); + }} placeholder='Episode Select'/> + + List
Episodes
+
+
+ + +
+ + + Language Options + + { + dispatch({ + type: 'downloadOptions', + payload: { ...store.downloadOptions, dubLang: e } + }); + }} + allOption + /> + + { + dispatch({ + type: 'downloadOptions', + payload: { ...store.downloadOptions, dlsubs: e } + }); + }} + /> + + Hardsubs are only supported on Crunchyroll + + } + arrow placement='top'> + + + + + Hardsub Language + + + - + Downloads the hardsub version of the selected subtitle.
Subtitles are displayed PERMANENTLY!
You can choose only 1 subtitle per video! - - } arrow placement='top'> - -
+ + } arrow placement='top'> + +
+
+
+
-
-
-
- - - { - dispatch({ - type: 'downloadOptions', - payload: { ...store.downloadOptions, fileName: e.target.value } - }); - }} sx={{ width: '87%' }} label='Filename Overwrite' /> - + + + { + dispatch({ + type: 'downloadOptions', + payload: { ...store.downloadOptions, fileName: e.target.value } + }); + }} sx={{ width: '87%' }} label='Filename Overwrite' /> + Click here to see the documentation - - } arrow placement='top'> - - - - - - - + + } arrow placement='top'> + + + + + + + - Add to Queue + Add to Queue - ; +
; }; export default DownloadSelector; \ No newline at end of file diff --git a/gui/react/src/components/AddToQueue/DownloadSelector/Listing/EpisodeListing.tsx b/gui/react/src/components/AddToQueue/DownloadSelector/Listing/EpisodeListing.tsx index 2249068..a59cf77 100644 --- a/gui/react/src/components/AddToQueue/DownloadSelector/Listing/EpisodeListing.tsx +++ b/gui/react/src/components/AddToQueue/DownloadSelector/Listing/EpisodeListing.tsx @@ -7,185 +7,185 @@ import { useSnackbar } from 'notistack'; const EpisodeListing: React.FC = () => { - const [store, dispatch] = useStore(); + const [store, dispatch] = useStore(); - const [season, setSeason] = React.useState<'all'|string>('all'); - const { enqueueSnackbar } = useSnackbar(); + const [season, setSeason] = React.useState<'all'|string>('all'); + const { enqueueSnackbar } = useSnackbar(); - const seasons = React.useMemo(() => { - const s: string[] = []; - for (const {season} of store.episodeListing) { - if (s.includes(season)) - continue; - s.push(season); - } - return s; - }, [ store.episodeListing ]); + const seasons = React.useMemo(() => { + const s: string[] = []; + for (const {season} of store.episodeListing) { + if (s.includes(season)) + continue; + s.push(season); + } + return s; + }, [ store.episodeListing ]); - const [selected, setSelected] = React.useState([]); + const [selected, setSelected] = React.useState([]); - React.useEffect(() => { - setSelected(parseSelect(store.downloadOptions.e)); - }, [ store.episodeListing ]); + React.useEffect(() => { + setSelected(parseSelect(store.downloadOptions.e)); + }, [ store.episodeListing ]); - const close = () => { - dispatch({ - type: 'episodeListing', - payload: [] - }); - dispatch({ - type: 'downloadOptions', - payload: { - ...store.downloadOptions, - e: `${([...new Set([...parseSelect(store.downloadOptions.e), ...selected])]).join(',')}` - } - }); - }; + const close = () => { + dispatch({ + type: 'episodeListing', + payload: [] + }); + dispatch({ + type: 'downloadOptions', + payload: { + ...store.downloadOptions, + e: `${([...new Set([...parseSelect(store.downloadOptions.e), ...selected])]).join(',')}` + } + }); + }; - const getEpisodesForSeason = (season: string|'all') => { - return store.episodeListing.filter((a) => season === 'all' ? true : a.season === season); - }; + const getEpisodesForSeason = (season: string|'all') => { + return store.episodeListing.filter((a) => season === 'all' ? true : a.season === season); + }; - return 0} onClose={close} scroll='paper' maxWidth='xl' sx={{ p: 2 }}> - - + return 0} onClose={close} scroll='paper' maxWidth='xl' sx={{ p: 2 }}> + + Episodes - - - Season - - - - - - selected.includes(a.e)) && !store.episodeListing.every(a => selected.includes(a.e))} - checked={store.episodeListing.every(a => selected.includes(a.e))} - onChange={() => { - if (selected.length > 0) { - setSelected([]); - } else { - setSelected(getEpisodesForSeason(season).map(a => a.e)); - } - }} - /> - - {getEpisodesForSeason(season).map((item, index, { length }) => { - const e = isNaN(parseInt(item.e)) ? item.e : parseInt(item.e); - const idStr = `S${item.season}E${e}`; - const isSelected = selected.includes(e.toString()); - const imageRef = React.createRef(); - const summaryRef = React.createRef(); - return - { - let arr: string[] = []; - if (isSelected) { - arr = [...selected.filter(a => a !== e.toString())]; - } else { - arr = [...selected, e.toString()]; - } - setSelected(arr.filter(a => a.length > 0)); - }}> - { isSelected ? : } - - {idStr} - thumbnail - - - - {item.name} - - - {item.time.startsWith('00:') ? item.time.slice(3) : item.time} - - - - {item.description} - - - -
+ + Season + + +
+ + + selected.includes(a.e)) && !store.episodeListing.every(a => selected.includes(a.e))} + checked={store.episodeListing.every(a => selected.includes(a.e))} + onChange={() => { + if (selected.length > 0) { + setSelected([]); + } else { + setSelected(getEpisodesForSeason(season).map(a => a.e)); + } + }} + /> + + {getEpisodesForSeason(season).map((item, index, { length }) => { + const e = isNaN(parseInt(item.e)) ? item.e : parseInt(item.e); + const idStr = `S${item.season}E${e}`; + const isSelected = selected.includes(e.toString()); + const imageRef = React.createRef(); + const summaryRef = React.createRef(); + return + { + let arr: string[] = []; + if (isSelected) { + arr = [...selected.filter(a => a !== e.toString())]; + } else { + arr = [...selected, e.toString()]; + } + setSelected(arr.filter(a => a.length > 0)); + }}> + { isSelected ? : } + + {idStr} + + thumbnail + + + + {item.name} + + + {item.time.startsWith('00:') ? item.time.slice(3) : item.time} + + + + {item.description} + + + +
Available audio languages: {item.lang.join(', ')} -
-
-
-
- { - await navigator.clipboard.writeText(item.img); - enqueueSnackbar('Copied URL to clipboard', { - variant: 'info' - }); - }}, - { - text: 'Open image in new tab', - onClick: () => { - window.open(item.img); - } - } ]} popupItem={imageRef as RefObject} /> - { - await navigator.clipboard.writeText(item.description!); - enqueueSnackbar('Copied summary to clipboard', { - variant: 'info' - }); - }, - text: 'Copy summary to clipboard' - } - ]} popupItem={summaryRef as RefObject} /> - {index < length - 1 && } -
; - })} -
-
; +
+
+
+ + { + await navigator.clipboard.writeText(item.img); + enqueueSnackbar('Copied URL to clipboard', { + variant: 'info' + }); + }}, + { + text: 'Open image in new tab', + onClick: () => { + window.open(item.img); + } + } ]} popupItem={imageRef as RefObject} /> + { + await navigator.clipboard.writeText(item.description!); + enqueueSnackbar('Copied summary to clipboard', { + variant: 'info' + }); + }, + text: 'Copy summary to clipboard' + } + ]} popupItem={summaryRef as RefObject} /> + {index < length - 1 && } +
; + })} + + ; }; const parseSelect = (s: string): string[] => { - const ret: string[] = []; - s.split(',').forEach(item => { - if (item.includes('-')) { - const split = item.split('-'); - if (split.length !== 2) - return; - const match = split[0].match(/[A-Za-z]+/); - if (match && match.length > 0) { - if (match.index && match.index !== 0) { - return; - } - const letters = split[0].substring(0, match[0].length); - const number = parseInt(split[0].substring(match[0].length)); - const b = parseInt(split[1]); - if (isNaN(number) || isNaN(b)) { - return; - } - for (let i = number; i <= b; i++) { - ret.push(`${letters}${i}`); - } + const ret: string[] = []; + s.split(',').forEach(item => { + if (item.includes('-')) { + const split = item.split('-'); + if (split.length !== 2) + return; + const match = split[0].match(/[A-Za-z]+/); + if (match && match.length > 0) { + if (match.index && match.index !== 0) { + return; + } + const letters = split[0].substring(0, match[0].length); + const number = parseInt(split[0].substring(match[0].length)); + const b = parseInt(split[1]); + if (isNaN(number) || isNaN(b)) { + return; + } + for (let i = number; i <= b; i++) { + ret.push(`${letters}${i}`); + } - } else { - const a = parseInt(split[0]); - const b = parseInt(split[1]); - if (isNaN(a) || isNaN(b)) { - return; + } else { + const a = parseInt(split[0]); + const b = parseInt(split[1]); + if (isNaN(a) || isNaN(b)) { + return; + } + for (let i = a; i <= b; i++) { + ret.push(`${i}`); + } + } + } else { + ret.push(item); } - for (let i = a; i <= b; i++) { - ret.push(`${i}`); - } - } - } else { - ret.push(item); - } - }); - return [...new Set(ret)]; + }); + return [...new Set(ret)]; }; export default EpisodeListing; diff --git a/gui/react/src/components/AddToQueue/SearchBox/SearchBox.tsx b/gui/react/src/components/AddToQueue/SearchBox/SearchBox.tsx index 6c4e2e6..b494f33 100644 --- a/gui/react/src/components/AddToQueue/SearchBox/SearchBox.tsx +++ b/gui/react/src/components/AddToQueue/SearchBox/SearchBox.tsx @@ -8,112 +8,112 @@ import ContextMenu from '../../reusable/ContextMenu'; import { useSnackbar } from 'notistack'; const SearchBox: React.FC = () => { - const messageHandler = React.useContext(messageChannelContext); - const [store, dispatch] = useStore(); - const [search, setSearch] = React.useState(''); + const messageHandler = React.useContext(messageChannelContext); + const [store, dispatch] = useStore(); + const [search, setSearch] = React.useState(''); - const [focus, setFocus] = React.useState(false); + const [focus, setFocus] = React.useState(false); - const [searchResult, setSearchResult] = React.useState(); - const anchor = React.useRef(null); + const [searchResult, setSearchResult] = React.useState(); + const anchor = React.useRef(null); - const { enqueueSnackbar } = useSnackbar(); + const { enqueueSnackbar } = useSnackbar(); - const selectItem = (id: string) => { - dispatch({ - type: 'downloadOptions', - payload: { - ...store.downloadOptions, - id - } - }); - }; + const selectItem = (id: string) => { + dispatch({ + type: 'downloadOptions', + payload: { + ...store.downloadOptions, + id + } + }); + }; - React.useEffect(() => { - if (search.trim().length === 0) - return setSearchResult({ isOk: true, value: [] }); + React.useEffect(() => { + if (search.trim().length === 0) + return setSearchResult({ isOk: true, value: [] }); - const timeOutId = setTimeout(async () => { - if (search.trim().length > 3) { - const s = await messageHandler?.search({search}); - if (s && s.isOk) - s.value = s.value.slice(0, 10); - setSearchResult(s); - } - }, 500); - return () => clearTimeout(timeOutId); - }, [search]); + const timeOutId = setTimeout(async () => { + if (search.trim().length > 3) { + const s = await messageHandler?.search({search}); + if (s && s.isOk) + s.value = s.value.slice(0, 10); + setSearchResult(s); + } + }, 500); + return () => clearTimeout(timeOutId); + }, [search]); - const anchorBounding = anchor.current?.getBoundingClientRect(); - return setFocus(false)}> - - setFocus(true)} onChange={e => setSearch(e.target.value)} variant='outlined' label='Search' fullWidth /> - {searchResult !== undefined && searchResult.isOk && searchResult.value.length > 0 && focus && + const anchorBounding = anchor.current?.getBoundingClientRect(); + return setFocus(false)}> + + setFocus(true)} onChange={e => setSearch(e.target.value)} variant='outlined' label='Search' fullWidth /> + {searchResult !== undefined && searchResult.isOk && searchResult.value.length > 0 && focus && - - {searchResult && searchResult.isOk ? - searchResult.value.map((a, ind, arr) => { - const imageRef = React.createRef(); - const summaryRef = React.createRef(); - return - { - selectItem(a.id); - setFocus(false); - }}> - - - thumbnail - - - - {a.name} - - {a.desc && - {a.desc} - } - {a.lang && + left: anchorBounding?.x, top: (anchorBounding?.y ?? 0) + (anchorBounding?.height ?? 0), zIndex: 99, overflowY: 'scroll'}}> + + {searchResult && searchResult.isOk ? + searchResult.value.map((a, ind, arr) => { + const imageRef = React.createRef(); + const summaryRef = React.createRef(); + return + { + selectItem(a.id); + setFocus(false); + }}> + + + thumbnail + + + + {a.name} + + {a.desc && + {a.desc} + } + {a.lang && Languages: {a.lang.join(', ')} - } - + } + ID: {a.id} - - - - - { - await navigator.clipboard.writeText(a.image); - enqueueSnackbar('Copied URL to clipboard', { - variant: 'info' - }); - }}, - { - text: 'Open image in new tab', - onClick: () => { - window.open(a.image); - } - } ]} popupItem={imageRef as RefObject} /> - {a.desc && + + + + + { + await navigator.clipboard.writeText(a.image); + enqueueSnackbar('Copied URL to clipboard', { + variant: 'info' + }); + }}, + { + text: 'Open image in new tab', + onClick: () => { + window.open(a.image); + } + } ]} popupItem={imageRef as RefObject} /> + {a.desc && { - await navigator.clipboard.writeText(a.desc!); - enqueueSnackbar('Copied summary to clipboard', { - variant: 'info' - }); - }, - text: 'Copy summary to clipboard' - } + { + onClick: async () => { + await navigator.clipboard.writeText(a.desc!); + enqueueSnackbar('Copied summary to clipboard', { + variant: 'info' + }); + }, + text: 'Copy summary to clipboard' + } ]} popupItem={summaryRef as RefObject} /> - } - {(ind < arr.length - 1) && } - ; - }) - : <>} - + } + {(ind < arr.length - 1) && } + ; + }) + : <>} + } - - ; + + ; }; export default SearchBox; diff --git a/gui/react/src/components/AuthButton.tsx b/gui/react/src/components/AuthButton.tsx index 8f5d273..a218afb 100644 --- a/gui/react/src/components/AuthButton.tsx +++ b/gui/react/src/components/AuthButton.tsx @@ -6,107 +6,107 @@ import Require from './Require'; import { useSnackbar } from 'notistack'; const AuthButton: React.FC = () => { - const snackbar = useSnackbar(); + const snackbar = useSnackbar(); - const [open, setOpen] = React.useState(false); + const [open, setOpen] = React.useState(false); - const [username, setUsername] = React.useState(''); - const [password, setPassword] = React.useState(''); + const [username, setUsername] = React.useState(''); + const [password, setPassword] = React.useState(''); - const [usernameError, setUsernameError] = React.useState(false); - const [passwordError, setPasswordError] = React.useState(false); + const [usernameError, setUsernameError] = React.useState(false); + const [passwordError, setPasswordError] = React.useState(false); - const messageChannel = React.useContext(messageChannelContext); + const messageChannel = React.useContext(messageChannelContext); - const [loading, setLoading] = React.useState(false); - const [error, setError] = React.useState(undefined); - const [authed, setAuthed] = React.useState(false); + const [loading, setLoading] = React.useState(false); + const [error, setError] = React.useState(undefined); + const [authed, setAuthed] = React.useState(false); - const checkAuth = async () => { - setAuthed((await messageChannel?.checkToken())?.isOk ?? false); - }; + const checkAuth = async () => { + setAuthed((await messageChannel?.checkToken())?.isOk ?? false); + }; - React.useEffect(() => { checkAuth(); }, []); + React.useEffect(() => { checkAuth(); }, []); - const handleSubmit = async () => { - if (!messageChannel) - throw new Error('Invalid state'); //The components to confirm only render if the messageChannel is not undefinded - if (username.trim().length === 0) - return setUsernameError(true); - if (password.trim().length === 0) - return setPasswordError(true); - setUsernameError(false); - setPasswordError(false); - setLoading(true); + const handleSubmit = async () => { + if (!messageChannel) + throw new Error('Invalid state'); //The components to confirm only render if the messageChannel is not undefinded + if (username.trim().length === 0) + return setUsernameError(true); + if (password.trim().length === 0) + return setPasswordError(true); + setUsernameError(false); + setPasswordError(false); + setLoading(true); - const res = await messageChannel.auth({ username, password }); - if (res.isOk) { - setOpen(false); - snackbar.enqueueSnackbar('Logged in', { - variant: 'success' - }); - setUsername(''); - setPassword(''); - } else { - setError(res.reason); - } - await checkAuth(); - setLoading(false); - }; + const res = await messageChannel.auth({ username, password }); + if (res.isOk) { + setOpen(false); + snackbar.enqueueSnackbar('Logged in', { + variant: 'success' + }); + setUsername(''); + setPassword(''); + } else { + setError(res.reason); + } + await checkAuth(); + setLoading(false); + }; - return - - - Error during Authentication - - {error?.name} - {error?.message} - - - - - - Authentication - - + return + + + Error during Authentication + + {error?.name} + {error?.message} + + + + + + Authentication + + Here, you need to enter your username (most likely your Email) and your password.
These information are not stored anywhere and are only used to authenticate with the service once. -
- setUsername(e.target.value)} - disabled={loading} - /> - setPassword(e.target.value)} - disabled={loading} - /> -
- - {loading && } - - - -
- -
; +
+ setUsername(e.target.value)} + disabled={loading} + /> + setPassword(e.target.value)} + disabled={loading} + /> +
+ + {loading && } + + + +
+ +
; }; export default AuthButton; \ No newline at end of file diff --git a/gui/react/src/components/LogoutButton.tsx b/gui/react/src/components/LogoutButton.tsx index 916db7a..83283d7 100644 --- a/gui/react/src/components/LogoutButton.tsx +++ b/gui/react/src/components/LogoutButton.tsx @@ -6,31 +6,31 @@ import { messageChannelContext } from '../provider/MessageChannel'; import Require from './Require'; const LogoutButton: React.FC = () => { - const messageChannel = React.useContext(messageChannelContext); - const [, dispatch] = useStore(); + const messageChannel = React.useContext(messageChannelContext); + const [, dispatch] = useStore(); - const logout = async () => { - if (await messageChannel?.isDownloading()) - return alert('You are currently downloading. Please finish the download first.'); - if (await messageChannel?.logout()) - dispatch({ - type: 'service', - payload: undefined - }); - else - alert('Unable to change service'); - }; + const logout = async () => { + if (await messageChannel?.isDownloading()) + return alert('You are currently downloading. Please finish the download first.'); + if (await messageChannel?.logout()) + dispatch({ + type: 'service', + payload: undefined + }); + else + alert('Unable to change service'); + }; - return - - ; + + ; }; diff --git a/gui/react/src/components/MainFrame/DownloadManager/DownloadManager.tsx b/gui/react/src/components/MainFrame/DownloadManager/DownloadManager.tsx index 7917707..ee78ac5 100644 --- a/gui/react/src/components/MainFrame/DownloadManager/DownloadManager.tsx +++ b/gui/react/src/components/MainFrame/DownloadManager/DownloadManager.tsx @@ -4,37 +4,37 @@ import { RandomEvent } from '../../../../../../@types/randomEvents'; import { messageChannelContext } from '../../../provider/MessageChannel'; const useDownloadManager = () => { - const messageHandler = React.useContext(messageChannelContext); + const messageHandler = React.useContext(messageChannelContext); - const [progressData, setProgressData] = React.useState(); - const [current, setCurrent] = React.useState(); + const [progressData, setProgressData] = React.useState(); + const [current, setCurrent] = React.useState(); - React.useEffect(() => { - const handler = (ev: RandomEvent<'progress'>) => { - console.log(ev.data); - setProgressData(ev.data); - }; + React.useEffect(() => { + const handler = (ev: RandomEvent<'progress'>) => { + console.log(ev.data); + setProgressData(ev.data); + }; - const currentHandler = (ev: RandomEvent<'current'>) => { - setCurrent(ev.data); - }; + const currentHandler = (ev: RandomEvent<'current'>) => { + setCurrent(ev.data); + }; - const finishHandler = () => { - setProgressData(undefined); - }; + const finishHandler = () => { + setProgressData(undefined); + }; - messageHandler?.randomEvents.on('progress', handler); - messageHandler?.randomEvents.on('current', currentHandler); - messageHandler?.randomEvents.on('finish', finishHandler); + messageHandler?.randomEvents.on('progress', handler); + messageHandler?.randomEvents.on('current', currentHandler); + messageHandler?.randomEvents.on('finish', finishHandler); - return () => { - messageHandler?.randomEvents.removeListener('progress', handler); - messageHandler?.randomEvents.removeListener('finish', finishHandler); - messageHandler?.randomEvents.removeListener('current', currentHandler); - }; - }, [messageHandler]); + return () => { + messageHandler?.randomEvents.removeListener('progress', handler); + messageHandler?.randomEvents.removeListener('finish', finishHandler); + messageHandler?.randomEvents.removeListener('current', currentHandler); + }; + }, [messageHandler]); - return { data: progressData, current}; + return { data: progressData, current}; }; export default useDownloadManager; \ No newline at end of file diff --git a/gui/react/src/components/MainFrame/MainFrame.tsx b/gui/react/src/components/MainFrame/MainFrame.tsx index 425b321..1e387e5 100644 --- a/gui/react/src/components/MainFrame/MainFrame.tsx +++ b/gui/react/src/components/MainFrame/MainFrame.tsx @@ -3,9 +3,9 @@ import React from 'react'; import Queue from './Queue/Queue'; const MainFrame: React.FC = () => { - return - - ; + return + + ; }; export default MainFrame; \ No newline at end of file diff --git a/gui/react/src/components/MainFrame/Queue/Queue.tsx b/gui/react/src/components/MainFrame/Queue/Queue.tsx index 7707f74..96aa825 100644 --- a/gui/react/src/components/MainFrame/Queue/Queue.tsx +++ b/gui/react/src/components/MainFrame/Queue/Queue.tsx @@ -7,414 +7,414 @@ import DeleteIcon from '@mui/icons-material/Delete'; import useDownloadManager from '../DownloadManager/DownloadManager'; const Queue: React.FC = () => { - const { data, current } = useDownloadManager(); - const queue = React.useContext(queueContext); - const msg = React.useContext(messageChannelContext); + const { data, current } = useDownloadManager(); + const queue = React.useContext(queueContext); + const msg = React.useContext(messageChannelContext); - if (!msg) - return <>Never; + if (!msg) + return <>Never; - return data || queue.length > 0 ? <> - {data && <> - - - Thumbnail - + return data || queue.length > 0 ? <> + {data && <> - - - - {data.downloadInfo.parent.title} - - - {data.downloadInfo.title} - - - - - - Downloading: {data.downloadInfo.language.name} - - - - - - - - {data.progress.cur} / {(data.progress.total)} parts ({data.progress.percent}% | {formatTime(data.progress.time)} | {(data.progress.downloadSpeed / 1024 / 1024).toFixed(2)} MB/s | {(data.progress.bytes / 1024 / 1024).toFixed(2)}MB) - - - - - - - - } - { - current && !data && <> - - - Thumbnail - - - - - - {current.parent.title} - - - {current.title} - - - - - - - Downloading: - - - - - - - + + Thumbnail + + + + + + {data.downloadInfo.parent.title} + + + {data.downloadInfo.title} + + + + + + Downloading: {data.downloadInfo.language.name} + + + + + + + + {data.progress.cur} / {(data.progress.total)} parts ({data.progress.percent}% | {formatTime(data.progress.time)} | {(data.progress.downloadSpeed / 1024 / 1024).toFixed(2)} MB/s | {(data.progress.bytes / 1024 / 1024).toFixed(2)}MB) + + + + + + + + } + { + current && !data && <> + + + Thumbnail + + + + + + {current.parent.title} + + + {current.title} + + + + + + + Downloading: + + + + + + + + + + 0 / ? parts (0% | XX:XX | 0 MB/s | 0MB) + + + + + + + + } + {queue.map((queueItem, index, { length }) => { + return + - - + Thumbnail + - 0 / ? parts (0% | XX:XX | 0 MB/s | 0MB) - + + + {queueItem.parent.title} + + + S{queueItem.parent.season}E{queueItem.episode} + + + {queueItem.title} + + + + + Dub(s): {queueItem.dubLang.join(', ')} + + + Sub(s): {queueItem.dlsubs.join(', ')} + + + Quality: {queueItem.q} + + + + + { + msg.removeFromQueue(index); + }} + sx={{ + backgroundColor: '#ff573a25', + height: '40px', + transition: '250ms', + '&:hover' : { + backgroundColor: '#ff573a', + } + }}> + + + + + - - - - - } - {queue.map((queueItem, index, { length }) => { - return : - + - Thumbnail - - - - {queueItem.parent.title} - - - S{queueItem.parent.season}E{queueItem.episode} - - - {queueItem.title} - - - - - Dub(s): {queueItem.dubLang.join(', ')} - - - Sub(s): {queueItem.dlsubs.join(', ')} - - - Quality: {queueItem.q} - - - - - { - msg.removeFromQueue(index); - }} - sx={{ - backgroundColor: '#ff573a25', - height: '40px', - transition: '250ms', - '&:hover' : { - backgroundColor: '#ff573a', - } - }}> - - - - - - - - ; - })} - : - Selected episodes will be shown here - - - - - - - - - - - - - - - - ; + + + + + + + + + + + + + + + + ; }; const formatTime = (time: number) => { - time = Math.floor(time / 1000); - const minutes = Math.floor(time / 60); - time = time % 60; + time = Math.floor(time / 1000); + const minutes = Math.floor(time / 60); + time = time % 60; - return `${minutes.toFixed(0).length < 2 ? `0${minutes}` : minutes}m${time.toFixed(0).length < 2 ? `0${time}` : time}s`; + return `${minutes.toFixed(0).length < 2 ? `0${minutes}` : minutes}m${time.toFixed(0).length < 2 ? `0${time}` : time}s`; }; export default Queue; \ No newline at end of file diff --git a/gui/react/src/components/MenuBar/MenuBar.tsx b/gui/react/src/components/MenuBar/MenuBar.tsx index 00f5ed8..dc48ab7 100644 --- a/gui/react/src/components/MenuBar/MenuBar.tsx +++ b/gui/react/src/components/MenuBar/MenuBar.tsx @@ -5,120 +5,120 @@ import useStore from '../../hooks/useStore'; import { StoreState } from '../../provider/Store'; const MenuBar: React.FC = () => { - const [ openMenu, setMenuOpen ] = React.useState<'settings'|'help'|undefined>(); - const [anchorEl, setAnchorEl] = React.useState(null); - const [store, dispatch] = useStore(); + const [ openMenu, setMenuOpen ] = React.useState<'settings'|'help'|undefined>(); + const [anchorEl, setAnchorEl] = React.useState(null); + const [store, dispatch] = useStore(); - const messageChannel = React.useContext(messageChannelContext); + const messageChannel = React.useContext(messageChannelContext); - React.useEffect(() => { - (async () => { - if (!messageChannel || store.version !== '') - return; - dispatch({ - type: 'version', - payload: await messageChannel.version() - }); - })(); - }, [messageChannel]); + React.useEffect(() => { + (async () => { + if (!messageChannel || store.version !== '') + return; + dispatch({ + type: 'version', + payload: await messageChannel.version() + }); + })(); + }, [messageChannel]); - const transformService = (service: StoreState['service']) => { - switch(service) { - case 'crunchy': - return 'Crunchyroll'; - case 'hidive': - return 'Hidive'; - case 'ao': - return 'AnimeOnegai'; - case 'adn': - return 'AnimationDigitalNetwork'; - } - }; + const transformService = (service: StoreState['service']) => { + switch(service) { + case 'crunchy': + return 'Crunchyroll'; + case 'hidive': + return 'Hidive'; + case 'ao': + return 'AnimeOnegai'; + case 'adn': + return 'AnimationDigitalNetwork'; + } + }; - const msg = React.useContext(messageChannelContext); + const msg = React.useContext(messageChannelContext); - const handleClick = (event: React.MouseEvent, n: 'settings'|'help') => { - setAnchorEl(event.currentTarget); - setMenuOpen(n); - }; - const handleClose = () => { - setAnchorEl(null); - setMenuOpen(undefined); - }; + const handleClick = (event: React.MouseEvent, n: 'settings'|'help') => { + setAnchorEl(event.currentTarget); + setMenuOpen(n); + }; + const handleClose = () => { + setAnchorEl(null); + setMenuOpen(undefined); + }; - if (!msg) - return <>; + if (!msg) + return <>; - return - - - + - - - { - msg.openFolder('config'); - handleClose(); - }}> + + + + { + msg.openFolder('config'); + handleClose(); + }}> Open settings folder - - { - msg.openFile(['config', 'bin-path.yml']); - handleClose(); - }}> + + { + msg.openFile(['config', 'bin-path.yml']); + handleClose(); + }}> Open FFmpeg/Mkvmerge file - - { - msg.openFile(['config', 'cli-defaults.yml']); - handleClose(); - }}> + + { + msg.openFile(['config', 'cli-defaults.yml']); + handleClose(); + }}> Open advanced options - - { - msg.openFolder('content'); - handleClose(); - }}> + + { + msg.openFolder('content'); + handleClose(); + }}> Open output path - - - - { - msg.openURL('https://github.com/anidl/multi-downloader-nx'); - handleClose(); - }}> + + + + { + msg.openURL('https://github.com/anidl/multi-downloader-nx'); + handleClose(); + }}> GitHub - - { - msg.openURL('https://github.com/anidl/multi-downloader-nx/issues/new?assignees=AnimeDL,AnidlSupport&labels=bug&template=bug.yml&title=BUG'); - handleClose(); - }}> + + { + msg.openURL('https://github.com/anidl/multi-downloader-nx/issues/new?assignees=AnimeDL,AnidlSupport&labels=bug&template=bug.yml&title=BUG'); + handleClose(); + }}> Report a bug - - { - msg.openURL('https://github.com/anidl/multi-downloader-nx/graphs/contributors'); - handleClose(); - }}> + + { + msg.openURL('https://github.com/anidl/multi-downloader-nx/graphs/contributors'); + handleClose(); + }}> Contributors - - { - msg.openURL('https://discord.gg/qEpbWen5vq'); - handleClose(); - }}> + + { + msg.openURL('https://discord.gg/qEpbWen5vq'); + handleClose(); + }}> Discord - - { - handleClose(); - }}> + + { + handleClose(); + }}> Version: {store.version} - - - - {transformService(store.service)} - - ; + + + + {transformService(store.service)} + + ; }; export default MenuBar; diff --git a/gui/react/src/components/Require.tsx b/gui/react/src/components/Require.tsx index 847541f..8c2827d 100644 --- a/gui/react/src/components/Require.tsx +++ b/gui/react/src/components/Require.tsx @@ -6,9 +6,9 @@ export type RequireType = { } const Require = (props: React.PropsWithChildren>) => { - return props.value === undefined ? - - : {props.children}; + return props.value === undefined ? + + : {props.children}; }; export default Require; \ No newline at end of file diff --git a/gui/react/src/components/StartQueue.tsx b/gui/react/src/components/StartQueue.tsx index 05b630c..71eb834 100644 --- a/gui/react/src/components/StartQueue.tsx +++ b/gui/react/src/components/StartQueue.tsx @@ -5,37 +5,37 @@ import { messageChannelContext } from '../provider/MessageChannel'; import Require from './Require'; const StartQueueButton: React.FC = () => { - const messageChannel = React.useContext(messageChannelContext); - const [start, setStart] = React.useState(false); - const msg = React.useContext(messageChannelContext); + const messageChannel = React.useContext(messageChannelContext); + const [start, setStart] = React.useState(false); + const msg = React.useContext(messageChannelContext); - React.useEffect(() => { - (async () => { - if (!msg) - return alert('Invalid state: msg not found'); - setStart(await msg.getDownloadQueue()); - })(); - }, []); + React.useEffect(() => { + (async () => { + if (!msg) + return alert('Invalid state: msg not found'); + setStart(await msg.getDownloadQueue()); + })(); + }, []); - const change = async () => { - if (await messageChannel?.isDownloading()) - alert('The current download will be finished before the queue stops'); - msg?.setDownloadQueue(!start); - setStart(!start); - }; + const change = async () => { + if (await messageChannel?.isDownloading()) + alert('The current download will be finished before the queue stops'); + msg?.setDownloadQueue(!start); + setStart(!start); + }; - return - - ; + return + + ; }; diff --git a/gui/react/src/components/reusable/ContextMenu.tsx b/gui/react/src/components/reusable/ContextMenu.tsx index dc08610..41fa4a6 100644 --- a/gui/react/src/components/reusable/ContextMenu.tsx +++ b/gui/react/src/components/reusable/ContextMenu.tsx @@ -12,54 +12,54 @@ export type ContextMenuProps = { } const buttonSx: SxProps = { - '&:hover': { - background: 'rgb(0, 30, 60)' - }, - fontSize: '0.7rem', - minHeight: '30px', - justifyContent: 'center', - p: 0 + '&:hover': { + background: 'rgb(0, 30, 60)' + }, + fontSize: '0.7rem', + minHeight: '30px', + justifyContent: 'center', + p: 0 }; function ContextMenu(props: ContextMenuProps) { - const [anchor, setAnchor] = React.useState( { x: 0, y: 0 } ); + const [anchor, setAnchor] = React.useState( { x: 0, y: 0 } ); - const [show, setShow] = React.useState(false); + const [show, setShow] = React.useState(false); - React.useEffect(() => { - const { popupItem: ref } = props; - if (ref.current === null) - return; - const listener = (ev: MouseEvent) => { - ev.preventDefault(); - setAnchor({ x: ev.x + 10, y: ev.y + 10 }); - setShow(true); - }; - ref.current.addEventListener('contextmenu', listener); + React.useEffect(() => { + const { popupItem: ref } = props; + if (ref.current === null) + return; + const listener = (ev: MouseEvent) => { + ev.preventDefault(); + setAnchor({ x: ev.x + 10, y: ev.y + 10 }); + setShow(true); + }; + ref.current.addEventListener('contextmenu', listener); - return () => { - if (ref.current) - ref.current.removeEventListener('contextmenu', listener); - }; - }, [ props.popupItem ]); + return () => { + if (ref.current) + ref.current.removeEventListener('contextmenu', listener); + }; + }, [ props.popupItem ]); - return show ? - - {props.options.map((item, i) => { - return item === 'divider' ? : - ; - })} - - ; + })} + + - - : <>; + + + : <>; } export default ContextMenu; diff --git a/gui/react/src/components/reusable/LinearProgressWithLabel.tsx b/gui/react/src/components/reusable/LinearProgressWithLabel.tsx index 28ac451..295251b 100644 --- a/gui/react/src/components/reusable/LinearProgressWithLabel.tsx +++ b/gui/react/src/components/reusable/LinearProgressWithLabel.tsx @@ -7,18 +7,18 @@ import React from 'react'; export type LinearProgressWithLabelProps = LinearProgressProps & { value: number }; const LinearProgressWithLabel: React.FC = (props) => { - return ( - - - - - - {`${Math.round( - props.value, - )}%`} - - - ); + return ( + + + + + + {`${Math.round( + props.value, + )}%`} + + + ); }; export default LinearProgressWithLabel; \ No newline at end of file diff --git a/gui/react/src/components/reusable/MultiSelect.tsx b/gui/react/src/components/reusable/MultiSelect.tsx index 5e3b272..56ae3e7 100644 --- a/gui/react/src/components/reusable/MultiSelect.tsx +++ b/gui/react/src/components/reusable/MultiSelect.tsx @@ -12,63 +12,63 @@ export type MultiSelectProps = { const ITEM_HEIGHT = 48; const ITEM_PADDING_TOP = 8; const MenuProps = { - PaperProps: { - style: { - maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, - width: 250 + PaperProps: { + style: { + maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, + width: 250 + } } - } }; function getStyles(name: string, personName: readonly string[], theme: Theme) { - return { - fontWeight: + return { + fontWeight: (personName ?? []).indexOf(name) === -1 - ? theme.typography.fontWeightRegular - : theme.typography.fontWeightMedium - }; + ? theme.typography.fontWeightRegular + : theme.typography.fontWeightMedium + }; } const MultiSelect: React.FC = (props) => { - const theme = useTheme(); + const theme = useTheme(); - return
- - {props.title} - - -
; + return
+ + {props.title} + + +
; }; export default MultiSelect; \ No newline at end of file diff --git a/gui/react/src/hooks/useStore.tsx b/gui/react/src/hooks/useStore.tsx index 9d9b302..3c3699d 100644 --- a/gui/react/src/hooks/useStore.tsx +++ b/gui/react/src/hooks/useStore.tsx @@ -2,11 +2,11 @@ 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>]>); - if (!context) { - throw new Error('useStore must be used under Store'); - } - return context; + const context = React.useContext(StoreContext as unknown as React.Context<[StoreState, React.Dispatch>]>); + if (!context) { + throw new Error('useStore must be used under Store'); + } + return context; }; export default useStore; \ No newline at end of file diff --git a/gui/react/src/index.tsx b/gui/react/src/index.tsx index 42f0a07..ebaa1cd 100644 --- a/gui/react/src/index.tsx +++ b/gui/react/src/index.tsx @@ -17,35 +17,35 @@ document.body.style.justifyContent = 'center'; const notistackRef = React.createRef(); const onClickDismiss = (key: SnackbarKey | undefined) => () => { - if (notistackRef.current) - notistackRef.current.closeSnackbar(key); + if (notistackRef.current) + notistackRef.current.closeSnackbar(key); }; const container = document.getElementById('root'); const root = createRoot(container as HTMLElement); root.render( - - - ( - - - - )} - > - - - - + + + ( + + + + )} + > + + + + ); \ No newline at end of file diff --git a/gui/react/src/provider/ErrorHandler.tsx b/gui/react/src/provider/ErrorHandler.tsx index e2ae86f..e5135ba 100644 --- a/gui/react/src/provider/ErrorHandler.tsx +++ b/gui/react/src/provider/ErrorHandler.tsx @@ -10,30 +10,30 @@ export default class ErrorHandler extends React.Component<{ } }> { - constructor(props: { + constructor(props: { children: React.ReactNode|React.ReactNode[] }) { - super(props); - this.state = { error: undefined }; - } + super(props); + this.state = { error: undefined }; + } - componentDidCatch(er: Error, stack: React.ErrorInfo) { - this.setState({ error: { er, stack } }); - } + componentDidCatch(er: Error, stack: React.ErrorInfo) { + this.setState({ error: { er, stack } }); + } - render(): React.ReactNode { - return this.state.error ? - - - {`${this.state.error.er.name}: ${this.state.error.er.message}`} -
- {this.state.error.stack.componentStack?.split('\n').map(a => { - return <> - {a} -
- ; - })} -
-
: this.props.children; - } + render(): React.ReactNode { + return this.state.error ? + + + {`${this.state.error.er.name}: ${this.state.error.er.message}`} +
+ {this.state.error.stack.componentStack?.split('\n').map(a => { + return <> + {a} +
+ ; + })} +
+
: this.props.children; + } } \ No newline at end of file diff --git a/gui/react/src/provider/MessageChannel.tsx b/gui/react/src/provider/MessageChannel.tsx index 56e6147..f0f0768 100644 --- a/gui/react/src/provider/MessageChannel.tsx +++ b/gui/react/src/provider/MessageChannel.tsx @@ -12,233 +12,233 @@ import { GUIConfig } from '../../../../modules/module.cfg-loader'; export type FrontEndMessages = (MessageHandler & { randomEvents: RandomEventHandler, logout: () => Promise }); export class RandomEventHandler { - private handler: { + private handler: { [eventName in keyof RandomEvents]: Handler[] } = { - progress: [], - finish: [], - queueChange: [], - current: [] - }; + progress: [], + finish: [], + queueChange: [], + current: [] + }; - public on(name: T, listener: Handler) { - if (Object.prototype.hasOwnProperty.call(this.handler, name)) { - this.handler[name].push(listener as any); - } else { - this.handler[name] = [ listener as any ]; + public on(name: T, listener: Handler) { + if (Object.prototype.hasOwnProperty.call(this.handler, name)) { + this.handler[name].push(listener as any); + } else { + this.handler[name] = [ listener as any ]; + } } - } - public emit(name: keyof RandomEvents, data: RandomEvent) { - (this.handler[name] ?? []).forEach(handler => handler(data as any)); - } + public emit(name: keyof RandomEvents, data: RandomEvent) { + (this.handler[name] ?? []).forEach(handler => handler(data as any)); + } - public removeListener(name: T, listener: Handler) { - this.handler[name] = (this.handler[name] as Handler[]).filter(a => a !== listener) as any; - } + public removeListener(name: T, listener: Handler) { + this.handler[name] = (this.handler[name] as Handler[]).filter(a => a !== listener) as any; + } } export const messageChannelContext = React.createContext(undefined); async function messageAndResponse(socket: WebSocket, msg: WSMessage): Promise> { - const id = v4(); - const ret = new Promise>((resolve) => { - const handler = function({ data }: MessageEvent) { - const parsed = JSON.parse(data.toString()) as WSMessageWithID; - if (parsed.id === id) { - socket.removeEventListener('message', handler); - resolve(parsed); - } - }; - socket.addEventListener('message', handler); - }); - const toSend = msg as WSMessageWithID; - toSend.id = id; + const id = v4(); + const ret = new Promise>((resolve) => { + const handler = function({ data }: MessageEvent) { + const parsed = JSON.parse(data.toString()) as WSMessageWithID; + if (parsed.id === id) { + socket.removeEventListener('message', handler); + resolve(parsed); + } + }; + socket.addEventListener('message', handler); + }); + const toSend = msg as WSMessageWithID; + toSend.id = id; - socket.send(JSON.stringify(toSend)); - return ret; + socket.send(JSON.stringify(toSend)); + return ret; } const MessageChannelProvider: FCWithChildren = ({ children }) => { - const [store, dispatch] = useStore(); - const [socket, setSocket] = React.useState(); - const [publicWS, setPublicWS] = React.useState(); - const [usePassword, setUsePassword] = React.useState<'waiting'|'yes'|'no'>('waiting'); - const [isSetup, setIsSetup] = React.useState<'waiting'|'yes'|'no'>('waiting'); + const [store, dispatch] = useStore(); + const [socket, setSocket] = React.useState(); + const [publicWS, setPublicWS] = React.useState(); + const [usePassword, setUsePassword] = React.useState<'waiting'|'yes'|'no'>('waiting'); + const [isSetup, setIsSetup] = React.useState<'waiting'|'yes'|'no'>('waiting'); - const { enqueueSnackbar } = useSnackbar(); + 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`); - wss.addEventListener('open', () => { - setPublicWS(wss); - }); - wss.addEventListener('error', () => { - enqueueSnackbar('Unable to connect to server. Please reload the page to try again.', { variant: 'error' }); - }); - }, []); - - React.useEffect(() => { - (async () => { - if (!publicWS) - return; - setUsePassword((await messageAndResponse(publicWS, { name: 'requirePassword', data: undefined })).data ? 'yes' : 'no'); - setIsSetup((await messageAndResponse(publicWS, { name: 'isSetup', data: undefined })).data ? 'yes' : 'no'); - })(); - }, [publicWS]); - - const connect = (ev?: React.FormEvent) => { - let search = new URLSearchParams(); - if (ev) { - ev.preventDefault(); - const formData = new FormData(ev.currentTarget); - const password = formData.get('password')?.toString(); - if (!password) - return enqueueSnackbar('Please provide both a username and password', { - variant: 'error' + React.useEffect(() => { + const wss = new WebSocket(`${location.protocol == 'https:' ? 'wss' : 'ws'}://${process.env.NODE_ENV === 'development' ? 'localhost:3000' : window.location.host}/public`); + wss.addEventListener('open', () => { + setPublicWS(wss); }); - search = new URLSearchParams({ - password - }); - } + wss.addEventListener('error', () => { + enqueueSnackbar('Unable to connect to server. Please reload the page to try again.', { variant: 'error' }); + }); + }, []); - const wws = new WebSocket(`${location.protocol == 'https:' ? 'wss' : 'ws'}://${process.env.NODE_ENV === 'development' ? 'localhost:3000' : window.location.host}/private?${search}`, ); - wws.addEventListener('open', () => { - console.log('[INFO] [WS] Connected'); - setSocket(wws); - }); - wws.addEventListener('error', (er) => { - console.error('[ERROR] [WS]', er); - enqueueSnackbar('Unable to connect to server. Please check the password and try again.', { - variant: 'error' - }); - }); - }; + React.useEffect(() => { + (async () => { + if (!publicWS) + return; + setUsePassword((await messageAndResponse(publicWS, { name: 'requirePassword', data: undefined })).data ? 'yes' : 'no'); + setIsSetup((await messageAndResponse(publicWS, { name: 'isSetup', data: undefined })).data ? 'yes' : 'no'); + })(); + }, [publicWS]); - const setup = async (ev: React.FormEvent) => { - ev.preventDefault(); - if (!socket) - return enqueueSnackbar('Invalid state: socket not found', { variant: 'error' }); - const formData = new FormData(ev.currentTarget); - const password = formData.get('password'); - const data = { - port: parseInt(formData.get('port')?.toString() ?? '') ?? 3000, - password: password ? password.toString() : undefined - } as GUIConfig; - await messageAndResponse(socket, { name: 'setupServer', data }); - enqueueSnackbar(`The following settings have been set: Port=${data.port}, Password=${data.password ?? 'noPasswordRequired'}`, { - variant: 'success', - persist: true - }); - enqueueSnackbar('Please restart the server now.', { - variant: 'info', - persist: true - }); - }; + const connect = (ev?: React.FormEvent) => { + let search = new URLSearchParams(); + if (ev) { + ev.preventDefault(); + const formData = new FormData(ev.currentTarget); + const password = formData.get('password')?.toString(); + if (!password) + return enqueueSnackbar('Please provide both a username and password', { + variant: 'error' + }); + search = new URLSearchParams({ + password + }); + } - const randomEventHandler = React.useMemo(() => new RandomEventHandler(), []); - - React.useEffect(() => { - (async () => { - if (!socket) - return; - const currentService = await messageAndResponse(socket, { name: 'type', data: undefined }); - if (currentService.data !== undefined) - return dispatch({ type: 'service', payload: currentService.data }); - if (store.service !== currentService.data) - messageAndResponse(socket, { name: 'setup', data: store.service }); - })(); - }, [store.service, dispatch, socket]); - - React.useEffect(() => { - if (!socket) - return; - /* finish is a placeholder */ - const listener = (initalData: MessageEvent) => { - const data = JSON.parse(initalData.data) as RandomEvent<'finish'>; - randomEventHandler.emit(data.name, data); + const wws = new WebSocket(`${location.protocol == 'https:' ? 'wss' : 'ws'}://${process.env.NODE_ENV === 'development' ? 'localhost:3000' : window.location.host}/private?${search}`, ); + wws.addEventListener('open', () => { + console.log('[INFO] [WS] Connected'); + setSocket(wws); + }); + wws.addEventListener('error', (er) => { + console.error('[ERROR] [WS]', er); + enqueueSnackbar('Unable to connect to server. Please check the password and try again.', { + variant: 'error' + }); + }); }; - socket.addEventListener('message', listener); - return () => { - socket.removeEventListener('message', listener); + + const setup = async (ev: React.FormEvent) => { + ev.preventDefault(); + if (!socket) + return enqueueSnackbar('Invalid state: socket not found', { variant: 'error' }); + const formData = new FormData(ev.currentTarget); + const password = formData.get('password'); + const data = { + port: parseInt(formData.get('port')?.toString() ?? '') ?? 3000, + password: password ? password.toString() : undefined + } as GUIConfig; + await messageAndResponse(socket, { name: 'setupServer', data }); + enqueueSnackbar(`The following settings have been set: Port=${data.port}, Password=${data.password ?? 'noPasswordRequired'}`, { + variant: 'success', + persist: true + }); + enqueueSnackbar('Please restart the server now.', { + variant: 'info', + persist: true + }); }; - }, [ socket ]); - if (usePassword === 'waiting') - return <>; + const randomEventHandler = React.useMemo(() => new RandomEventHandler(), []); - if (socket === undefined) { - if (usePassword === 'no') { - connect(undefined); - return <>; - } - return - - - - + React.useEffect(() => { + (async () => { + if (!socket) + return; + const currentService = await messageAndResponse(socket, { name: 'type', data: undefined }); + if (currentService.data !== undefined) + return dispatch({ type: 'service', payload: currentService.data }); + if (store.service !== currentService.data) + messageAndResponse(socket, { name: 'setup', data: store.service }); + })(); + }, [store.service, dispatch, socket]); + + React.useEffect(() => { + if (!socket) + return; + /* finish is a placeholder */ + const listener = (initalData: MessageEvent) => { + const data = JSON.parse(initalData.data) as RandomEvent<'finish'>; + randomEventHandler.emit(data.name, data); + }; + socket.addEventListener('message', listener); + return () => { + socket.removeEventListener('message', listener); + }; + }, [ socket ]); + + if (usePassword === 'waiting') + return <>; + + if (socket === undefined) { + if (usePassword === 'no') { + connect(undefined); + return <>; + } + return + + + + Login - - - - - + + + + + You need to login in order to use this tool. - - - ; - } + + + ; + } - if (isSetup === 'no') { - return - - - - + if (isSetup === 'no') { + return + + + + Confirm - - - - - - + + + + + + Please enter data that will be set to use this tool. -
+
Leave blank to use no password (NOT RECOMMENDED)! -
-
-
; - } +
+
+ ; + } - const messageHandler: FrontEndMessages = { - name: 'default', - auth: async (data) => (await messageAndResponse(socket, { name: 'auth', data })).data, - version: async () => (await messageAndResponse(socket, { name: 'version', data: undefined })).data, - checkToken: async () => (await messageAndResponse(socket, { name: 'checkToken', data: undefined })).data, - search: async (data) => (await messageAndResponse(socket, { name: 'search', data })).data, - handleDefault: async (data) => (await messageAndResponse(socket, { name: 'default', data })).data, - availableDubCodes: async () => (await messageAndResponse(socket, { name: 'availableDubCodes', data: undefined})).data, - availableSubCodes: async () => (await messageAndResponse(socket, { name: 'availableSubCodes', data: undefined })).data, - resolveItems: async (data) => (await messageAndResponse(socket, { name: 'resolveItems', data })).data, - listEpisodes: async (data) => (await messageAndResponse(socket, { name: 'listEpisodes', data })).data, - randomEvents: randomEventHandler, - downloadItem: (data) => messageAndResponse(socket, { name: 'downloadItem', data }), - isDownloading: async () => (await messageAndResponse(socket, { name: 'isDownloading', data: undefined })).data, - openFolder: async (data) => messageAndResponse(socket, { name: 'openFolder', data }), - logout: async () => (await messageAndResponse(socket, { name: 'changeProvider', data: undefined })).data, - openFile: async (data) => await messageAndResponse(socket, { name: 'openFile', data }), - openURL: async (data) => await messageAndResponse(socket, { name: 'openURL', data }), - getQueue: async () => (await messageAndResponse(socket, { name: 'getQueue', data: undefined })).data, - removeFromQueue: async (data) => await messageAndResponse(socket, { name: 'removeFromQueue', data }), - clearQueue: async () => await messageAndResponse(socket, { name: 'clearQueue', data: undefined }), - setDownloadQueue: async (data) => await messageAndResponse(socket, { name: 'setDownloadQueue', data }), - getDownloadQueue: async () => (await messageAndResponse(socket, { name: 'getDownloadQueue', data: undefined })).data, - }; + const messageHandler: FrontEndMessages = { + name: 'default', + auth: async (data) => (await messageAndResponse(socket, { name: 'auth', data })).data, + version: async () => (await messageAndResponse(socket, { name: 'version', data: undefined })).data, + checkToken: async () => (await messageAndResponse(socket, { name: 'checkToken', data: undefined })).data, + search: async (data) => (await messageAndResponse(socket, { name: 'search', data })).data, + handleDefault: async (data) => (await messageAndResponse(socket, { name: 'default', data })).data, + availableDubCodes: async () => (await messageAndResponse(socket, { name: 'availableDubCodes', data: undefined})).data, + availableSubCodes: async () => (await messageAndResponse(socket, { name: 'availableSubCodes', data: undefined })).data, + resolveItems: async (data) => (await messageAndResponse(socket, { name: 'resolveItems', data })).data, + listEpisodes: async (data) => (await messageAndResponse(socket, { name: 'listEpisodes', data })).data, + randomEvents: randomEventHandler, + downloadItem: (data) => messageAndResponse(socket, { name: 'downloadItem', data }), + isDownloading: async () => (await messageAndResponse(socket, { name: 'isDownloading', data: undefined })).data, + openFolder: async (data) => messageAndResponse(socket, { name: 'openFolder', data }), + logout: async () => (await messageAndResponse(socket, { name: 'changeProvider', data: undefined })).data, + openFile: async (data) => await messageAndResponse(socket, { name: 'openFile', data }), + openURL: async (data) => await messageAndResponse(socket, { name: 'openURL', data }), + getQueue: async () => (await messageAndResponse(socket, { name: 'getQueue', data: undefined })).data, + removeFromQueue: async (data) => await messageAndResponse(socket, { name: 'removeFromQueue', data }), + clearQueue: async () => await messageAndResponse(socket, { name: 'clearQueue', data: undefined }), + setDownloadQueue: async (data) => await messageAndResponse(socket, { name: 'setDownloadQueue', data }), + getDownloadQueue: async () => (await messageAndResponse(socket, { name: 'getDownloadQueue', data: undefined })).data, + }; - return - {children} - ; + return + {children} + ; }; export default MessageChannelProvider; diff --git a/gui/react/src/provider/QueueProvider.tsx b/gui/react/src/provider/QueueProvider.tsx index dc31db9..833ff17 100644 --- a/gui/react/src/provider/QueueProvider.tsx +++ b/gui/react/src/provider/QueueProvider.tsx @@ -6,30 +6,30 @@ import { RandomEvent } from '../../../../@types/randomEvents'; export const queueContext = React.createContext([]); const QueueProvider: FCWithChildren = ({ children }) => { - const msg = React.useContext(messageChannelContext); + const msg = React.useContext(messageChannelContext); - const [ready, setReady] = React.useState(false); - const [queue, setQueue] = React.useState([]); + const [ready, setReady] = React.useState(false); + const [queue, setQueue] = React.useState([]); - React.useEffect(() => { - if (msg && !ready) { - msg.getQueue().then(data => { - setQueue(data); - setReady(true); - }); - } - const listener = (ev: RandomEvent<'queueChange'>) => { - setQueue(ev.data); - }; - msg?.randomEvents.on('queueChange', listener); - return () => { - msg?.randomEvents.removeListener('queueChange', listener); - }; - }, [ msg ]); + React.useEffect(() => { + if (msg && !ready) { + msg.getQueue().then(data => { + setQueue(data); + setReady(true); + }); + } + const listener = (ev: RandomEvent<'queueChange'>) => { + setQueue(ev.data); + }; + msg?.randomEvents.on('queueChange', listener); + return () => { + msg?.randomEvents.removeListener('queueChange', listener); + }; + }, [ msg ]); - return - {children} - ; + return + {children} + ; }; export default QueueProvider; \ No newline at end of file diff --git a/gui/react/src/provider/ServiceProvider.tsx b/gui/react/src/provider/ServiceProvider.tsx index 173aa3e..16cc5ca 100644 --- a/gui/react/src/provider/ServiceProvider.tsx +++ b/gui/react/src/provider/ServiceProvider.tsx @@ -8,28 +8,28 @@ type Services = 'crunchy'|'hidive'|'ao'|'adn'; export const serviceContext = React.createContext(undefined); const ServiceProvider: FCWithChildren = ({ children }) => { - const [ { service }, dispatch ] = useStore(); + const [ { service }, dispatch ] = useStore(); - const setService = (s: StoreState['service']) => { - dispatch({ - type: 'service', - payload: s - }); - }; + const setService = (s: StoreState['service']) => { + dispatch({ + type: 'service', + payload: s + }); + }; - return service === undefined ? - - Please select your service - - - - - - - - : - {children} - ; + return service === undefined ? + + Please select your service + + + + + + + + : + {children} + ; }; export default ServiceProvider; \ No newline at end of file diff --git a/gui/react/src/provider/Store.tsx b/gui/react/src/provider/Store.tsx index 15d50ec..2e0f8f7 100644 --- a/gui/react/src/provider/Store.tsx +++ b/gui/react/src/provider/Store.tsx @@ -32,35 +32,35 @@ export type StoreAction = { } const Reducer = (state: StoreState, action: StoreAction): StoreState => { - switch(action.type) { - default: - return { ...state, [action.type]: action.payload }; - } + switch(action.type) { + default: + return { ...state, [action.type]: action.payload }; + } }; const initialState: StoreState = { - downloadOptions: { - id: '', - q: 0, - e: '', - dubLang: [ 'jpn' ], - dlsubs: [ 'all' ], - fileName: '', - dlVideoOnce: false, - all: false, - but: false, - noaudio: false, - novids: false, - simul: false - }, - service: undefined, - episodeListing: [], - version: '', + downloadOptions: { + id: '', + q: 0, + e: '', + dubLang: [ 'jpn' ], + dlsubs: [ 'all' ], + fileName: '', + dlVideoOnce: false, + all: false, + but: false, + noaudio: false, + novids: false, + simul: false + }, + service: undefined, + episodeListing: [], + version: '', }; const Store: FCWithChildren = ({children}) => { - const [state, dispatch] = React.useReducer(Reducer, initialState); - /*React.useEffect(() => { + const [state, dispatch] = React.useReducer(Reducer, initialState); + /*React.useEffect(() => { if (!state.unsavedChanges.has) return; const unsavedChanges = (ev: BeforeUnloadEvent, lang: LanguageContextType) => { @@ -79,11 +79,11 @@ const Store: FCWithChildren = ({children}) => { return () => window.removeEventListener('beforeunload', windowListener); }, [state.unsavedChanges.has]);*/ - return ( - - {children} - - ); + return ( + + {children} + + ); }; /* Importent Notice -- The 'queue' generic will be overriden */ diff --git a/gui/react/tsconfig.json b/gui/react/tsconfig.json index b13020e..5eecbe8 100644 --- a/gui/react/tsconfig.json +++ b/gui/react/tsconfig.json @@ -1,29 +1,29 @@ { - "compilerOptions": { - "outDir": "./build", - "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], - "allowJs": true, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true, - "module": "CommonJS", - "moduleResolution": "node", - "resolveJsonModule": true, - "isolatedModules": true, - //"noEmit": true, - "jsx": "react-jsx", - "downlevelIteration": true - }, - "include": [ - "./src", - "./webpack.config.ts" - ] + "compilerOptions": { + "outDir": "./build", + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "CommonJS", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + //"noEmit": true, + "jsx": "react-jsx", + "downlevelIteration": true + }, + "include": [ + "./src", + "./webpack.config.ts" + ] } \ No newline at end of file diff --git a/gui/react/webpack.config.ts b/gui/react/webpack.config.ts index a5e485f..0901205 100644 --- a/gui/react/webpack.config.ts +++ b/gui/react/webpack.config.ts @@ -4,55 +4,55 @@ import path from 'path'; import type { Configuration as DevServerConfig } from 'webpack-dev-server'; const config: Configuration & DevServerConfig = { - devServer: { - proxy: [ - { - target: 'http://localhost:3000', - context: ['/public', '/private'], - ws: true - } - ], - }, - entry: './src/index.tsx', - mode: 'production', - output: { - path: path.resolve(process.cwd(), './build'), - filename: 'index.js', - }, - target: 'web', - resolve: { - extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'], - }, - performance: false, - module: { - rules: [ - { - test: /\.(ts|tsx)$/, - exclude: /node_modules/, - use: { - 'loader': 'babel-loader', - options: { - presets: [ - '@babel/typescript', - '@babel/preset-react', - ['@babel/preset-env', { - targets: 'defaults' - }] - ] - } - }, - }, - { - test: /\.css$/i, - use: ['style-loader', 'css-loader'], - }, - ], - }, - plugins: [ - new HtmlWebpackPlugin({ - template: path.join(process.cwd(), 'public', 'index.html') - }) - ] + devServer: { + proxy: [ + { + target: 'http://localhost:3000', + context: ['/public', '/private'], + ws: true + } + ], + }, + entry: './src/index.tsx', + mode: 'production', + output: { + path: path.resolve(process.cwd(), './build'), + filename: 'index.js', + }, + target: 'web', + resolve: { + extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'], + }, + performance: false, + module: { + rules: [ + { + test: /\.(ts|tsx)$/, + exclude: /node_modules/, + use: { + 'loader': 'babel-loader', + options: { + presets: [ + '@babel/typescript', + '@babel/preset-react', + ['@babel/preset-env', { + targets: 'defaults' + }] + ] + } + }, + }, + { + test: /\.css$/i, + use: ['style-loader', 'css-loader'], + }, + ], + }, + plugins: [ + new HtmlWebpackPlugin({ + template: path.join(process.cwd(), 'public', 'index.html') + }) + ] }; export default config; diff --git a/package.json b/package.json index d3b7ba8..25b1094 100644 --- a/package.json +++ b/package.json @@ -1,120 +1,120 @@ { - "name": "multi-downloader-nx", - "short_name": "aniDL", - "version": "5.5.6", - "description": "Downloader for Crunchyroll, Hidive, AnimeOnegai, and AnimationDigitalNetwork with CLI and GUI", - "keywords": [ - "download", - "downloader", - "hidive", - "crunchy", - "crunchyroll", - "util", - "utility", - "cli", - "gui" - ], - "engines": { - "node": ">=18", - "pnpm": ">=7" - }, - "author": "AnimeDL ", - "contributors": [ - { - "name": "AnimeDL " - }, - { - "name": "AniDL " - }, - { - "name": "AnidlSupport " - } - ], - "homepage": "https://github.com/anidl/multi-downloader-nx", - "repository": { - "type": "git", - "url": "https://github.com/anidl/multi-downloader-nx.git" - }, - "bugs": { - "url": "https://github.com/anidl/multi-downloader-nx/issues" - }, - "license": "MIT", - "dependencies": { - "@bufbuild/buf": "^1.57.2", - "@bufbuild/protobuf": "^2.8.0", - "@bufbuild/protoc-gen-es": "^2.8.0", - "@yao-pkg/pkg": "^6.6.0", - "binary-parser": "^2.2.1", - "binary-parser-encoder": "^1.5.3", - "bn.js": "^5.2.2", - "cors": "^2.8.5", - "elliptic": "^6.6.1", - "esbuild": "^0.25.10", - "express": "^5.1.0", - "fast-xml-parser": "^5.2.5", - "ffprobe": "^1.1.2", - "fs-extra": "^11.3.2", - "iso-639": "^0.2.2", - "leven": "^3.1.0", - "log4js": "^6.9.1", - "long": "^5.3.2", - "lookpath": "^1.2.3", - "m3u8-parsed": "^2.0.0", - "mpd-parser": "^1.3.1", - "node-forge": "^1.3.1", - "ofetch": "^1.4.1", - "open": "^8.4.2", - "protobufjs": "^7.5.4", - "puppeteer-real-browser": "^1.4.4", - "ws": "^8.18.3", - "yaml": "^2.8.1", - "yargs": "^17.7.2" - }, - "devDependencies": { - "@eslint/js": "^9.35.0", - "@types/bn.js": "^5.2.0", - "@types/cors": "^2.8.19", - "@types/elliptic": "^6.4.18", - "@types/express": "^5.0.3", - "@types/ffprobe": "^1.1.8", - "@types/fs-extra": "^11.0.4", - "@types/node": "^24.5.1", - "@types/node-forge": "^1.3.14", - "@types/ws": "^8.18.1", - "@types/yargs": "^17.0.33", - "@typescript-eslint/eslint-plugin": "^8.44.0", - "@typescript-eslint/parser": "^8.44.0", - "eslint": "^9.35.0", - "protoc": "^1.1.3", - "removeNPMAbsolutePaths": "^3.0.1", - "ts-node": "^10.9.2", - "typescript": "^5.9.2", - "typescript-eslint": "^8.44.0" - }, - "scripts": { - "prestart": "pnpm run tsc test", - "start": "pnpm prestart && cd lib && node gui.js", - "gui": "cd ./gui/react/ && pnpm start", - "docs": "ts-node modules/build-docs.ts", - "tsc": "ts-node tsc.ts", - "proto:compile": "protoc --plugin=protoc-gen-ts_proto=.\\node_modules\\.bin\\protoc-gen-ts_proto.cmd --ts_proto_opt=\"esModuleInterop=true\" --ts_proto_opt=\"forceLong=long\" --ts_proto_opt=\"env=node\" --ts_proto_out=. modules/*.proto", - "prebuild-cli": "pnpm run tsc false false", - "build-windows-cli": "pnpm run prebuild-cli && cd lib && node modules/build windows-x64", - "build-linux-cli": "pnpm run prebuild-cli && cd lib && node modules/build linuxstatic-x64", - "build-arm-cli": "pnpm run prebuild-cli && cd lib && node modules/build linux-arm64", - "build-macos-cli": "pnpm run prebuild-cli && cd lib && node modules/build macos-x64", - "build-alpine-cli": "pnpm run prebuild-cli && cd lib && node modules/build alpine-x64", - "build-android-cli": "pnpm run prebuild-cli && cd lib && node modules/build linuxstatic-armv7", - "prebuild-gui": "pnpm run tsc", - "build-windows-gui": "pnpm run prebuild-gui && cd lib && node modules/build windows-x64 true", - "build-linux-gui": "pnpm run prebuild-gui && cd lib && node modules/build linuxstatic-x64 true", - "build-arm-gui": "pnpm run prebuild-gui && cd lib && node modules/build linux-arm64 true", - "build-macos-gui": "pnpm run prebuild-gui && cd lib && node modules/build macos-x64 true", - "build-alpine-gui": "pnpm run prebuild-gui && cd lib && node modules/build alpine-x64 true", - "build-android-gui": "pnpm run prebuild-gui && cd lib && node modules/build linuxstatic-armv7 true", - "eslint": "npx eslint .", - "eslint-fix": "npx eslint . --fix", - "pretest": "pnpm run tsc", - "test": "pnpm run pretest && cd lib && node modules/build windows-x64 && node modules/build linuxstatic-x64 && node modules/build macos-x64" - } -} + "name": "multi-downloader-nx", + "short_name": "aniDL", + "version": "5.5.6", + "description": "Downloader for Crunchyroll, Hidive, AnimeOnegai, and AnimationDigitalNetwork with CLI and GUI", + "keywords": [ + "download", + "downloader", + "hidive", + "crunchy", + "crunchyroll", + "util", + "utility", + "cli", + "gui" + ], + "engines": { + "node": ">=18", + "pnpm": ">=7" + }, + "author": "AnimeDL ", + "contributors": [ + { + "name": "AnimeDL " + }, + { + "name": "AniDL " + }, + { + "name": "AnidlSupport " + } + ], + "homepage": "https://github.com/anidl/multi-downloader-nx", + "repository": { + "type": "git", + "url": "https://github.com/anidl/multi-downloader-nx.git" + }, + "bugs": { + "url": "https://github.com/anidl/multi-downloader-nx/issues" + }, + "license": "MIT", + "dependencies": { + "@bufbuild/buf": "^1.57.2", + "@bufbuild/protobuf": "^2.8.0", + "@bufbuild/protoc-gen-es": "^2.8.0", + "@yao-pkg/pkg": "^6.6.0", + "binary-parser": "^2.2.1", + "binary-parser-encoder": "^1.5.3", + "bn.js": "^5.2.2", + "cors": "^2.8.5", + "elliptic": "^6.6.1", + "esbuild": "^0.25.10", + "express": "^5.1.0", + "fast-xml-parser": "^5.2.5", + "ffprobe": "^1.1.2", + "fs-extra": "^11.3.2", + "iso-639": "^0.2.2", + "leven": "^3.1.0", + "log4js": "^6.9.1", + "long": "^5.3.2", + "lookpath": "^1.2.3", + "m3u8-parsed": "^2.0.0", + "mpd-parser": "^1.3.1", + "node-forge": "^1.3.1", + "ofetch": "^1.4.1", + "open": "^8.4.2", + "protobufjs": "^7.5.4", + "puppeteer-real-browser": "^1.4.4", + "ws": "^8.18.3", + "yaml": "^2.8.1", + "yargs": "^17.7.2" + }, + "devDependencies": { + "@eslint/js": "^9.35.0", + "@types/bn.js": "^5.2.0", + "@types/cors": "^2.8.19", + "@types/elliptic": "^6.4.18", + "@types/express": "^5.0.3", + "@types/ffprobe": "^1.1.8", + "@types/fs-extra": "^11.0.4", + "@types/node": "^24.5.1", + "@types/node-forge": "^1.3.14", + "@types/ws": "^8.18.1", + "@types/yargs": "^17.0.33", + "@typescript-eslint/eslint-plugin": "^8.44.0", + "@typescript-eslint/parser": "^8.44.0", + "eslint": "^9.35.0", + "protoc": "^1.1.3", + "removeNPMAbsolutePaths": "^3.0.1", + "ts-node": "^10.9.2", + "typescript": "^5.9.2", + "typescript-eslint": "^8.44.0" + }, + "scripts": { + "prestart": "pnpm run tsc test", + "start": "pnpm prestart && cd lib && node gui.js", + "gui": "cd ./gui/react/ && pnpm start", + "docs": "ts-node modules/build-docs.ts", + "tsc": "ts-node tsc.ts", + "proto:compile": "protoc --plugin=protoc-gen-ts_proto=.\\node_modules\\.bin\\protoc-gen-ts_proto.cmd --ts_proto_opt=\"esModuleInterop=true\" --ts_proto_opt=\"forceLong=long\" --ts_proto_opt=\"env=node\" --ts_proto_out=. modules/*.proto", + "prebuild-cli": "pnpm run tsc false false", + "build-windows-cli": "pnpm run prebuild-cli && cd lib && node modules/build windows-x64", + "build-linux-cli": "pnpm run prebuild-cli && cd lib && node modules/build linuxstatic-x64", + "build-arm-cli": "pnpm run prebuild-cli && cd lib && node modules/build linux-arm64", + "build-macos-cli": "pnpm run prebuild-cli && cd lib && node modules/build macos-x64", + "build-alpine-cli": "pnpm run prebuild-cli && cd lib && node modules/build alpine-x64", + "build-android-cli": "pnpm run prebuild-cli && cd lib && node modules/build linuxstatic-armv7", + "prebuild-gui": "pnpm run tsc", + "build-windows-gui": "pnpm run prebuild-gui && cd lib && node modules/build windows-x64 true", + "build-linux-gui": "pnpm run prebuild-gui && cd lib && node modules/build linuxstatic-x64 true", + "build-arm-gui": "pnpm run prebuild-gui && cd lib && node modules/build linux-arm64 true", + "build-macos-gui": "pnpm run prebuild-gui && cd lib && node modules/build macos-x64 true", + "build-alpine-gui": "pnpm run prebuild-gui && cd lib && node modules/build alpine-x64 true", + "build-android-gui": "pnpm run prebuild-gui && cd lib && node modules/build linuxstatic-armv7 true", + "eslint": "npx eslint .", + "eslint-fix": "npx eslint . --fix", + "pretest": "pnpm run tsc", + "test": "pnpm run pretest && cd lib && node modules/build windows-x64 && node modules/build linuxstatic-x64 && node modules/build macos-x64" + } +} \ No newline at end of file diff --git a/tsc.ts b/tsc.ts index 16e7d19..81f0cac 100644 --- a/tsc.ts +++ b/tsc.ts @@ -10,156 +10,156 @@ const isTest = argv.length > 0 && argv[0] === 'test'; const isGUI = !(argv.length > 1 && argv[1] === 'false'); if (!isTest) - buildIgnore = [ - '*/\\.env', - './config/setup.json' - ]; + buildIgnore = [ + '*/\\.env', + './config/setup.json' + ]; if (!isGUI) - buildIgnore = buildIgnore.concat([ - './gui*', - './build*', - 'gui.ts' - ]); + buildIgnore = buildIgnore.concat([ + './gui*', + './build*', + 'gui.ts' + ]); const ignore = [ - ...buildIgnore, - '*/\\.git*', - './lib*', - '*/@types*', - './out*', - './bin/mkvtoolnix*', - './config/token.yml$', - './config/updates.json$', - './config/*_token.yml$', - './config/*_sess.yml$', - './config/*_profile.yml$', - '*/\\.eslint*', - '*/*\\.tsx?$', - './fonts*', - './gui/react*', - './dev.js$', - '*/node_modules/*', - './widevine/*', - './playready/*', - './videos/*', - './logs/*', + ...buildIgnore, + '*/\\.git*', + './lib*', + '*/@types*', + './out*', + './bin/mkvtoolnix*', + './config/token.yml$', + './config/updates.json$', + './config/*_token.yml$', + './config/*_sess.yml$', + './config/*_profile.yml$', + '*/\\.eslint*', + '*/*\\.tsx?$', + './fonts*', + './gui/react*', + './dev.js$', + '*/node_modules/*', + './widevine/*', + './playready/*', + './videos/*', + './logs/*', ].map(a => a.replace(/\*/g, '[^]*').replace(/\.\//g, escapeRegExp(__dirname) + '/').replace(/\//g, path.sep === '\\' ? '\\\\' : '/')).map(a => new RegExp(a, 'i')); export { ignore }; (async () => { - const waitForProcess = async (proc: ChildProcess) => { - return new Promise((resolve, reject) => { - proc.stdout?.on('data', console.log); - proc.stderr?.on('data', console.error); - proc.on('close', resolve); - proc.on('error', reject); - }); - }; + const waitForProcess = async (proc: ChildProcess) => { + return new Promise((resolve, reject) => { + proc.stdout?.on('data', console.log); + proc.stderr?.on('data', console.error); + proc.on('close', resolve); + proc.on('error', reject); + }); + }; - process.stdout.write('Removing lib dir... '); - removeSync('lib'); - process.stdout.write('✓\nRunning tsc... '); - const tsc = exec('npx tsc'); + process.stdout.write('Removing lib dir... '); + removeSync('lib'); + process.stdout.write('✓\nRunning tsc... '); + const tsc = exec('npx tsc'); - await waitForProcess(tsc); - - if (!isGUI) { - fs.emptyDirSync(path.join('lib', 'gui')); - fs.rmdirSync(path.join('lib', 'gui')); - } + await waitForProcess(tsc); - if (!isTest && isGUI) { - process.stdout.write('✓\nBuilding react... '); + if (!isGUI) { + fs.emptyDirSync(path.join('lib', 'gui')); + fs.rmdirSync(path.join('lib', 'gui')); + } - const installReactDependencies = exec('pnpm install', { - cwd: path.join(__dirname, 'gui', 'react'), - }); + if (!isTest && isGUI) { + process.stdout.write('✓\nBuilding react... '); - await waitForProcess(installReactDependencies); - - const react = exec('pnpm run build', { - cwd: path.join(__dirname, 'gui', 'react'), - env: { - ...process.env, - CI: 'false' - } - }); - - await waitForProcess(react); - } + const installReactDependencies = exec('pnpm install', { + cwd: path.join(__dirname, 'gui', 'react'), + }); - process.stdout.write('✓\nCopying files... '); - if (!isTest && isGUI) { - copyDir(path.join(__dirname, 'gui', 'react', 'build'), path.join(__dirname, 'lib', 'gui', 'server', 'build')); - } + await waitForProcess(installReactDependencies); - const files = readDir(__dirname); - files.forEach(item => { - const itemPath = path.join(__dirname, 'lib', item.path.replace(__dirname, '')); - if (item.stats.isDirectory()) { - if (!fs.existsSync(itemPath)) - fs.mkdirSync(itemPath); - } else { - copyFileSync(item.path, itemPath); - } - }); + const react = exec('pnpm run build', { + cwd: path.join(__dirname, 'gui', 'react'), + env: { + ...process.env, + CI: 'false' + } + }); - process.stdout.write('✓\nInstalling dependencies... '); - if (!isTest) { - const dependencies = exec(`pnpm install ${isGUI ? '' : '-P'}`, { - cwd: path.join(__dirname, 'lib') - }); - await waitForProcess(dependencies); - } + await waitForProcess(react); + } - process.stdout.write('✓\n'); + process.stdout.write('✓\nCopying files... '); + if (!isTest && isGUI) { + copyDir(path.join(__dirname, 'gui', 'react', 'build'), path.join(__dirname, 'lib', 'gui', 'server', 'build')); + } + + const files = readDir(__dirname); + files.forEach(item => { + const itemPath = path.join(__dirname, 'lib', item.path.replace(__dirname, '')); + if (item.stats.isDirectory()) { + if (!fs.existsSync(itemPath)) + fs.mkdirSync(itemPath); + } else { + copyFileSync(item.path, itemPath); + } + }); + + process.stdout.write('✓\nInstalling dependencies... '); + if (!isTest) { + const dependencies = exec(`pnpm install ${isGUI ? '' : '-P'}`, { + cwd: path.join(__dirname, 'lib') + }); + await waitForProcess(dependencies); + } + + process.stdout.write('✓\n'); })(); -function readDir (dir: string): { - path: string, - stats: fs.Stats +function readDir(dir: string): { + path: string, + stats: fs.Stats }[] { - const items: { - path: string, - stats: fs.Stats - }[] = []; - const content = fs.readdirSync(dir); - itemLoop: for (const item of content) { - const itemPath = path.join(dir, item); - for (const ignoreItem of ignore) { - if (ignoreItem.test(itemPath)) - continue itemLoop; - } - const stats = fs.statSync(itemPath); - items.push({ - path: itemPath, - stats - }); - if (stats.isDirectory()) { - items.push(...readDir(itemPath)); - } - } - return items; + const items: { + path: string, + stats: fs.Stats + }[] = []; + const content = fs.readdirSync(dir); + itemLoop: for (const item of content) { + const itemPath = path.join(dir, item); + for (const ignoreItem of ignore) { + if (ignoreItem.test(itemPath)) + continue itemLoop; + } + const stats = fs.statSync(itemPath); + items.push({ + path: itemPath, + stats + }); + if (stats.isDirectory()) { + items.push(...readDir(itemPath)); + } + } + return items; } async function copyDir(src: string, dest: string) { - await fs.promises.mkdir(dest, { recursive: true }); - const entries = await fs.promises.readdir(src, { withFileTypes: true }); + await fs.promises.mkdir(dest, { recursive: true }); + const entries = await fs.promises.readdir(src, { withFileTypes: true }); - for (const entry of entries) { - const srcPath = path.join(src, entry.name); - const destPath = path.join(dest, entry.name); + for (const entry of entries) { + const srcPath = path.join(src, entry.name); + const destPath = path.join(dest, entry.name); - entry.isDirectory() ? - await copyDir(srcPath, destPath) : - await fs.promises.copyFile(srcPath, destPath); - } + entry.isDirectory() ? + await copyDir(srcPath, destPath) : + await fs.promises.copyFile(srcPath, destPath); + } } function escapeRegExp(string: string): string { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 43398a5..8a11d1f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,20 +1,20 @@ { - "compilerOptions": { - "target": "ES2020", - "module": "commonjs", - "outDir": "./lib", - "strict": true, - "esModuleInterop": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "downlevelIteration": true, - "jsx": "react" - }, - "exclude": [ - "./videos", - "./tsc.ts", - "lib/**/*", - "gui/react/**/*" - ] + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "outDir": "./lib", + "strict": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "downlevelIteration": true, + "jsx": "react" + }, + "exclude": [ + "./videos", + "./tsc.ts", + "lib/**/*", + "gui/react/**/*" + ] } \ No newline at end of file From a87628817b1bd7d0afbb82c52acfe519f04b3376 Mon Sep 17 00:00:00 2001 From: AnimeDL Date: Sat, 27 Sep 2025 10:53:57 -0700 Subject: [PATCH 3/4] re-disable eslint for react because of weird error --- eslint.config.mjs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index eed41c9..b4a7b34 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -56,10 +56,7 @@ export default tseslint.config( files: ['gui/react/**/*'], rules: { 'no-console': 0, - 'indent': [ - 'error', - 4 - ], + 'indent': 'off' } } ); \ No newline at end of file From 7a5b8e1a96406c65340c5da49f66ff13794fdb01 Mon Sep 17 00:00:00 2001 From: AnimeDL Date: Sat, 27 Sep 2025 12:31:07 -0700 Subject: [PATCH 4/4] Ignore whitespace for blame --- .git-blame-ignore-revs | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..5803095 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,3 @@ +# Ignore the whitespace changes in the following commits +460b4c1d0e12c88459aaff786fbe348c4c3d517a +a14466ec5d29accbe81b5ffac6e0a1373d04e356 \ No newline at end of file