// Package Info import packageJson from './package.json'; // Node import path from 'path'; import fs from 'fs-extra'; // Plugins import shlp from 'sei-helper'; // Modules import * as fontsData from './modules/module.fontsData'; import * as langsData from './modules/module.langsData'; import * as yamlCfg from './modules/module.cfg-loader'; import * as yargs from './modules/module.app-args'; import * as reqModule from './modules/module.fetch'; import Merger, { Font, MergerInput, SubtitleInput } from './modules/module.merger'; import getKeys, { canDecrypt } from './modules/widevine'; import streamdl, { M3U8Json } from './modules/hls-download'; import { exec } from './modules/sei-helper-fixes'; import { console } from './modules/log'; import { domain } from './modules/module.api-urls'; import { downloaded } from './modules/module.downloadArchive'; import parseSelect from './modules/module.parseSelect'; import parseFileName, { Variable } from './modules/module.filename'; import { AvailableFilenameVars } from './modules/module.args'; import { parse } from './modules/module.transform-mpd'; // Types import type { ServiceClass } from './@types/serviceClassInterface'; import type { AuthData, AuthResponse, SearchData, SearchResponse, SearchResponseItem } from './@types/messageHandler'; import type { AOSearchResult, AnimeOnegaiSearch } from './@types/animeOnegaiSearch'; import type { AnimeOnegaiSeries } from './@types/animeOnegaiSeries'; import type { AnimeOnegaiSeasons, Episode } from './@types/animeOnegaiSeasons'; import type { DownloadedMedia, DownloadedMediaMap, sxItem } from './@types/downloaderTypes'; import type { AnimeOnegaiStream } from './@types/animeOnegaiStream'; type parsedMultiDubDownload = { data: { lang: string, videoId: string episode: Episode }[], seriesTitle: string, seasonTitle: string, episodeTitle: string, episodeNumber: number, seasonNumber: number, seriesID: number, seasonID: number, image: string, } 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 shlp.question('[Q] LOGIN/EMAIL'), password: argv.password ?? await shlp.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.'); } } 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 }; })}; } 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())) lang = 'ja'; else if (this.porStrings.includes(season.name.trim())) lang = 'pt'; else if (this.spaStrings.includes(season.name.trim())) 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))) lang = 'ja'; else if (this.porStrings.some(str => series.data.title.includes(str))) lang = 'pt'; else if (this.spaStrings.some(str => series.data.title.includes(str))) lang = 'es'; 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 }); } } 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(', ')})`); } 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, res.mediaMap, { ...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[], mediaMap: DownloadedMediaMap[], 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({ mediaMap, onlyVid: hasAudioStreams ? data.filter(a => a.type === 'Video').map((a) : MergerInput => { 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 => { 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 }; }), simul: data.filter(a => a.type === 'Video').map((a) : boolean => { 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 => { 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<{ data: DownloadedMedia[], mediaMap: DownloadedMediaMap[], 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[] = []; const mediaMap: DownloadedMediaMap[] = []; 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 fileMap: DownloadedMediaMap = { version: media.episode.ID.toString(), files: [] }; 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.warn('Decryption not enabled!'); } 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 split = outFile.split(path.sep).slice(0, -1); split.forEach((val, ind, arr) => { const isAbsolut = path.isAbsolute(outFile as string); if (!fs.existsSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val))) fs.mkdirSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val)); }); const videoJson: M3U8Json = { segments: chosenVideoSegments.segments }; try { const videoDownload = await new streamdl({ output: chosenVideoSegments.pssh ? `${tempTsFile}.video.enc.mp4` : `${tsFile}.video.mp4`, 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 split = outFile.split(path.sep).slice(0, -1); split.forEach((val, ind, arr) => { const isAbsolut = path.isAbsolute(outFile as string); if (!fs.existsSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val))) fs.mkdirSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val)); }); const audioJson: M3U8Json = { segments: chosenAudioSegments.segments }; try { const audioDownload = await new streamdl({ output: chosenAudioSegments.pssh ? `${tempTsFile}.audio.enc.mp4` : `${tsFile}.audio.mp4`, 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 || chosenAudioSegments.pssh) && (videoDownloaded || audioDownloaded)) { console.info('Decryption Needed, attempting to decrypt'); const encryptionKeys = await getKeys(chosenVideoSegments.pssh, streamData.widevine_proxy, {}); if (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) { const commandBase = `--show-progress --key ${encryptionKeys[1].kid}:${encryptionKeys[1].key} `; const commandVideo = commandBase+`"${tempTsFile}.video.enc.mp4" "${tempTsFile}.video.mp4"`; const commandAudio = commandBase+`"${tempTsFile}.audio.enc.mp4" "${tempTsFile}.audio.mp4"`; if (videoDownloaded) { console.info('Started decrypting video'); const decryptVideo = exec('mp4decrypt', `"${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.mp4`, `${tsFile}.video.enc.mp4`); return undefined; } else { console.info('Decryption done for video'); if (!options.nocleanup) { fs.removeSync(`${tempTsFile}.video.enc.mp4`); } fs.renameSync(`${tempTsFile}.video.mp4`, `${tsFile}.video.mp4`); files.push({ type: 'Video', path: `${tsFile}.video.mp4`, lang: lang }); fileMap.files.push({ type: 'Video', path: `${tsFile}.video.mp4`, lang: lang }); } } if (audioDownloaded) { console.info('Started decrypting audio'); const decryptAudio = exec('mp4decrypt', `"${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.mp4`, `${tsFile}.audio.enc.mp4`); return undefined; } else { if (!options.nocleanup) { fs.removeSync(`${tempTsFile}.audio.enc.mp4`); } fs.renameSync(`${tempTsFile}.audio.mp4`, `${tsFile}.audio.mp4`); files.push({ type: 'Audio', path: `${tsFile}.audio.mp4`, lang: lang }); fileMap.files.push({ type: 'Audio', path: `${tsFile}.audio.mp4`, lang: lang }); console.info('Decryption done for audio'); } } } else { console.warn('mp4decrypt not found, files need decryption. Decryption Keys:', encryptionKeys); } } else { if (videoDownloaded) { files.push({ type: 'Video', path: `${tsFile}.video.mp4`, lang: lang }); fileMap.files.push({ type: 'Video', path: `${tsFile}.video.mp4`, lang: lang }); } if (audioDownloaded) { files.push({ type: 'Audio', path: `${tsFile}.audio.mp4`, lang: lang }); fileMap.files.push({ type: 'Audio', path: `${tsFile}.audio.mp4`, lang: lang }); } } } } 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); sxData.path = path.join(this.cfg.dir.content, sxData.file); 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 }); fileMap.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!'); } mediaMap.push(fileMap); await this.sleep(options.waittime); } return { error: dlFailed, data: files, mediaMap, 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); }); } }