import { dubLanguageCodes, languages, searchLocales, subtitleLanguagesFilter } from './module.langsData'; 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:' }; export type AvailableFilenameVars = 'title' | 'episode' | 'showTitle' | 'seriesTitle' | 'season' | 'width' | 'height' | 'service'; const availableFilenameVars: AvailableFilenameVars[] = ['title', 'episode', 'showTitle', 'seriesTitle', 'season', 'width', 'height', 'service']; export type AvailableMuxer = 'ffmpeg' | 'mkvmerge'; export const muxer: AvailableMuxer[] = ['ffmpeg', 'mkvmerge']; export type TAppArg = { name: string; group: keyof typeof groups; type: 'boolean' | 'string' | 'number' | 'array'; choices?: T[]; alias?: string; describe: string; docDescribe: true | string; // true means use describe for the docs default?: | T | { default: T | undefined; name?: string; }; service: Array<'crunchy' | 'hidive' | 'adn' | 'all'>; usage: string; // -(-)${name} will be added for each command, demandOption?: true; transformer?: (value: T) => K; }; const args: TAppArg[] = [ { 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)] as string[], default: { default: 'en-US' }, type: 'string', service: ['crunchy', '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 }, 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'], 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 } }, { 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 }, type: 'number', alias: 'server', docDescribe: true, service: ['all'], usage: '${server}' }, { name: 'cstream', group: 'dl', alias: 'cs', service: ['crunchy'], type: 'string', describe: '(Please use --vstream and --astream instead, this will deprecate soon)', 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. androidtv provides the best video (CBR).', 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. android provides the best audio (192 kbps).', 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'] }, usage: '${sub1} ${sub2}' }, { name: 'skipMuxOnSubFail', group: 'dl', describe: 'Skips muxing when a subtitle download fails.', docDescribe: true, service: ['all'], type: 'boolean', usage: '', default: { default: false } }, { name: 'noASSConv', group: 'dl', describe: 'Disables VTT conversion to ASS.', docDescribe: true, service: ['crunchy', 'hidive'], type: 'boolean', usage: '', default: { default: false } }, { name: 'noSubFix', group: 'dl', describe: 'Disables all subtitle fixes and downloads raw subtitles.', docDescribe: true, service: ['crunchy'], type: 'boolean', usage: '', default: { default: false } }, { name: 'srtAssFix', group: 'dl', describe: 'Fixes the recently changed Crunchyroll subtitles provided by Closed Caption Converter.', docDescribe: true, service: ['crunchy'], type: 'boolean', usage: '', default: { default: true } }, { name: 'layoutResFix', group: 'dl', describe: 'Applies the LayoutRes Fix to all ASS subtitles.', docDescribe: true, service: ['crunchy'], type: 'boolean', usage: '', default: { default: true } }, { name: 'scaledBorderAndShadowFix', group: 'dl', describe: 'Applies the ScaledBorderAndShadow Fix to all ASS subtitles.', docDescribe: true, service: ['crunchy'], type: 'boolean', usage: '', default: { default: true } }, { name: 'scaledBorderAndShadow', group: 'dl', service: ['crunchy'], type: 'string', describe: 'Select if ScaledBorderAndShadow should be set to "yes" or "no".', choices: ['yes', 'no'], default: { default: 'yes' }, docDescribe: true, usage: '${yes/no}' }, { name: 'originalScriptFix', group: 'dl', describe: 'Removes the URL in the Original Script line of the ASS subtitles, it prevents from bricking the subs in VLC (Fonts not loading when url not returning 200).', docDescribe: true, service: ['crunchy'], type: 'boolean', usage: '', default: { default: true } }, { name: 'subtitleTimestampFix', group: 'dl', describe: 'Fixes subtitle dialogues that go over the video length (deletes dialogues where start is over video length and updates the end timestamp when end is over video length).', docDescribe: true, service: ['crunchy'], type: 'boolean', usage: '', default: { default: false } }, { 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]] }, 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 }, 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' + '\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.' + '\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 }, 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 }, 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', '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'], 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: 'proxy', describe: 'Uses Proxy on geo-restricted or geo-defining endpoints (e.g. https://127.0.0.1:1080 or http://127.0.0.1:1080)', docDescribe: true, group: 'util', service: ['all'], type: 'string', usage: '${proxy_url}', default: { default: '' } }, { name: 'proxyAll', describe: 'Proxies everything, not recommended. Proxy needs to be defined.', docDescribe: true, group: 'util', service: ['all'], type: 'boolean', usage: '', default: { default: false } } ]; const getDefault = (name: string, cfg: Record): 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]; } else { return (option.default as any).default as T; } } else { return option.default as T; } }; const buildDefault = () => { const data: Record = {}; 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 };