From 3c599cd48e113c8877a3649ff6560f18e4a9b400 Mon Sep 17 00:00:00 2001 From: Izuco Date: Mon, 1 Nov 2021 20:38:58 +0100 Subject: [PATCH] 100% --- .eslintignore | 3 +- @types/crunchyEpisodeList.d.ts | 45 +- @types/crunchySearch.d.ts | 34 +- @types/objectInfo.d.ts | 16 +- @types/playbackData.d.ts | 2 +- @types/sei-helper.d.ts | 2 +- crunchy-beta.ts | 1263 -------------------------------- crunchy.ts | 1189 ++++++++++++++++++++++++++++++ index.ts | 11 +- modules/module.api-urls.ts | 2 +- modules/module.app-args.ts | 477 ++++++------ modules/module.cfg-loader.ts | 2 +- modules/module.curl-req.ts | 24 +- modules/module.eps-filter.ts | 5 +- modules/module.filename.ts | 12 +- modules/module.fontsData.ts | 8 +- modules/module.langsData.ts | 12 +- modules/module.merger.ts | 51 +- modules/module.req.ts | 200 +++-- package.json | 2 +- 20 files changed, 1646 insertions(+), 1714 deletions(-) delete mode 100644 crunchy-beta.ts create mode 100644 crunchy.ts diff --git a/.eslintignore b/.eslintignore index 06f80bc..7bde27b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ lib -/videos/*.ts \ No newline at end of file +/videos/*.ts +crunchy \ No newline at end of file diff --git a/@types/crunchyEpisodeList.d.ts b/@types/crunchyEpisodeList.d.ts index 7a2b87b..14d9ce2 100644 --- a/@types/crunchyEpisodeList.d.ts +++ b/@types/crunchyEpisodeList.d.ts @@ -2,21 +2,18 @@ export interface CrunchyEpisodeList { __class__: string; __href__: string; __resource_key__: string; - __links__: Actions; - __actions__: Actions; + __links__: unknown; + __actions__: unknown; total: number; items: Item[]; } -export interface Actions { -} - export interface Item { __class__: Class; __href__: string; __resource_key__: string; __links__: Links; - __actions__: Actions; + __actions__: unknown; id: string; channel_id: ChannelID; series_id: string; @@ -64,15 +61,15 @@ export interface Item { } export enum Class { - Episode = "episode", + Episode = 'episode', } export interface Links { ads: Ads; - "episode/channel": Ads; - "episode/next_episode"?: Ads; - "episode/season": Ads; - "episode/series": Ads; + 'episode/channel': Ads; + 'episode/next_episode'?: Ads; + 'episode/season': Ads; + 'episode/series': Ads; streams?: Ads; } @@ -86,12 +83,12 @@ export interface AdBreak { } export enum AdBreakType { - Midroll = "midroll", - Preroll = "preroll", + Midroll = 'midroll', + Preroll = 'preroll', } export enum ChannelID { - Crunchyroll = "crunchyroll", + Crunchyroll = 'crunchyroll', } export interface Images { @@ -106,17 +103,17 @@ export interface Thumbnail { } export enum ThumbnailType { - Thumbnail = "thumbnail", + Thumbnail = 'thumbnail', } export enum SubtitleLocale { - ArSA = "ar-SA", - DeDE = "de-DE", - EnUS = "en-US", - Es419 = "es-419", - EsES = "es-ES", - FrFR = "fr-FR", - ItIT = "it-IT", - PtBR = "pt-BR", - RuRU = "ru-RU", + ArSA = 'ar-SA', + DeDE = 'de-DE', + EnUS = 'en-US', + Es419 = 'es-419', + EsES = 'es-ES', + FrFR = 'fr-FR', + ItIT = 'it-IT', + PtBR = 'pt-BR', + RuRU = 'ru-RU', } diff --git a/@types/crunchySearch.d.ts b/@types/crunchySearch.d.ts index 7bba856..19ff592 100644 --- a/@types/crunchySearch.d.ts +++ b/@types/crunchySearch.d.ts @@ -5,13 +5,11 @@ export interface CrunchySearch { __href__: string; __resource_key__: string; __links__: CrunchySearchLinks; - __actions__: Actions; + __actions__: unknown; total: number; items: CrunchySearchItem[]; } -export interface Actions { -} export interface CrunchySearchLinks { continuation?: Continuation; @@ -26,14 +24,14 @@ export interface CrunchySearchItem { __href__: string; __resource_key__: string; __links__: CrunchySearchLinks; - __actions__: Actions; + __actions__: unknown; type: string; total: number; items: ItemItem[]; } export interface ItemItem { - __actions__: Actions; + __actions__: unknown; __class__: Class; __href__: string; __links__: PurpleLinks; @@ -69,19 +67,19 @@ export interface ItemItem { } export enum Class { - Panel = "panel", + Panel = 'panel', } export interface PurpleLinks { resource: Continuation; - "resource/channel": Continuation; - "episode/season"?: Continuation; - "episode/series"?: Continuation; + 'resource/channel': Continuation; + 'episode/season'?: Continuation; + 'episode/series'?: Continuation; streams?: Continuation; } export enum ChannelID { - Crunchyroll = "crunchyroll", + Crunchyroll = 'crunchyroll', } export interface EpisodeMetadata { @@ -119,14 +117,14 @@ export interface AdBreak { } export enum AdBreakType { - Midroll = "midroll", - Preroll = "preroll", + Midroll = 'midroll', + Preroll = 'preroll', } export enum TenantCategory { - Action = "Action", - Drama = "Drama", - SciFi = "Sci-Fi", + Action = 'Action', + Drama = 'Drama', + SciFi = 'Sci-Fi', } export interface Images { @@ -143,9 +141,9 @@ export interface PosterTall { } export enum PosterTallType { - PosterTall = "poster_tall", - PosterWide = "poster_wide", - Thumbnail = "thumbnail", + PosterTall = 'poster_tall', + PosterWide = 'poster_wide', + Thumbnail = 'thumbnail', } export interface SearchMetadata { diff --git a/@types/objectInfo.d.ts b/@types/objectInfo.d.ts index 45d948f..c7eb6b2 100644 --- a/@types/objectInfo.d.ts +++ b/@types/objectInfo.d.ts @@ -4,20 +4,16 @@ export interface ObjectInfo { __class__: string; __href__: string; __resource_key__: string; - __links__: Actions; - __actions__: Actions; + __links__: unknown; + __actions__: unknown; total: number; items: Item[]; } - -export interface Actions { -} - export interface Item { __class__: string; __href__: string; __links__: Links; - __actions__: Actions; + __actions__: unknown; id: string; external_id: string; channel_id: string; @@ -43,10 +39,10 @@ export interface Item { } export interface Links { - "episode/season": EpisodeSeason; - "episode/series": EpisodeSeason; + 'episode/season': EpisodeSeason; + 'episode/series': EpisodeSeason; resource: EpisodeSeason; - "resource/channel": EpisodeSeason; + 'resource/channel': EpisodeSeason; streams: EpisodeSeason; } diff --git a/@types/playbackData.d.ts b/@types/playbackData.d.ts index 5181891..07068dd 100644 --- a/@types/playbackData.d.ts +++ b/@types/playbackData.d.ts @@ -24,7 +24,7 @@ export interface Stream { export enum Vcodec { - H264 = "h264", + H264 = 'h264', } export interface Subtitle { diff --git a/@types/sei-helper.d.ts b/@types/sei-helper.d.ts index 2fd29c1..41aa151 100644 --- a/@types/sei-helper.d.ts +++ b/@types/sei-helper.d.ts @@ -10,6 +10,6 @@ declare module 'sei-helper' { domain: string; secure: boolean; }> - } + }; export function formatTime(time: number): string } \ No newline at end of file diff --git a/crunchy-beta.ts b/crunchy-beta.ts deleted file mode 100644 index 295c1fc..0000000 --- a/crunchy-beta.ts +++ /dev/null @@ -1,1263 +0,0 @@ -#!/usr/bin/env node - -// build-in -import path from 'path'; -import fs from 'fs-extra'; - -// package program -import packageJson from './package.json'; -console.log(`\n=== Crunchyroll Beta Downloader NX ${packageJson.version} ===\n`); - -// plugins -import shlp from 'sei-helper'; -import m3u8 from 'm3u8-parsed'; -import streamdl from 'hls-download'; - -// custom 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 epsFilter from './modules/module.eps-filter'; -import Merger from './modules/module.merger'; - -// new-cfg paths -const cfg = yamlCfg.loadCfg(); -let token = yamlCfg.loadCRToken(); -let cmsToken: { - cms?: Record -} = {}; - -// args -const argv = yargs.appArgv(cfg.cli) -const appstore: { - fn: Variable[], - isBatch: boolean, - out?: string -} = { - fn: [], - isBatch: false -} - -// load req -import { domain, api } from './modules/module.api-urls'; -import * as reqModule from './modules/module.req'; -import { CrunchySearch } from './@types/crunchySearch'; -import { CrunchyEpisodeList } from './@types/crunchyEpisodeList'; -import { CrunchyEpMeta, ParseItem } from './@types/crunchyTypes'; -import { ObjectInfo } from './@types/objectInfo'; -import parseFileName, { Variable } from './modules/module.filename'; -import { PlaybackData } from './@types/playbackData'; -const req = new reqModule.Req(domain, argv); - -// select -(async () => { - // load binaries - cfg.bin = await yamlCfg.loadBinCfg(); - // select mode - if(argv.dlfonts){ - await getFonts(); - } - else if(argv.auth){ - await doAuth(); - } - else if(argv.cmsindex){ - await refreshToken(); - await getCmsData(); - } - else if(argv.new){ - await refreshToken(); - await getNewlyAdded(); - } - else if(argv.search && argv.search.length > 2){ - await refreshToken(); - await doSearch(); - } - else if(argv.series && argv.series.match(/^[0-9A-Z]{9}$/)){ - await refreshToken(); - await getSeriesById(); - } - else if(argv['movie-listing'] && argv['movie-listing'].match(/^[0-9A-Z]{9}$/)){ - await refreshToken(); - await getMovieListingById(); - } - else if(argv.s && argv.s.match(/^[0-9A-Z]{9}$/)){ - await refreshToken(); - await getSeasonById(); - } - else if(argv.e){ - await refreshToken(); - await getObjectById(); - } - else{ - yargs.showHelp(); - } -})(); - -// get cr fonts -async function getFonts(){ - console.log('[INFO] Downloading fonts...'); - for(const f of Object.keys(fontsData.fonts)){ - const fontFile = fontsData.fonts[f as fontsData.AvailableFonts]; - const fontLoc = path.join(cfg.dir.fonts, fontFile); - if(fs.existsSync(fontLoc) && fs.statSync(fontLoc).size != 0){ - console.log(`[INFO] ${f} (${fontFile}) 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){} - const fontUrl = fontsData.root + fontFile; - const getFont = await req.getData(fontUrl, { binary: true }); - if(getFont.ok && getFont.res){ - fs.writeFileSync(fontLoc, getFont.res.body); - console.log(`[INFO] Downloaded: ${f} (${fontFile})`); - } - else{ - console.log(`[WARN] Failed to download: ${f} (${fontFile})`); - } - } - } - console.log('[INFO] All required fonts downloaded!'); -} - -// auth method -async function doAuth(){ - const iLogin = await shlp.question('[Q] LOGIN/EMAIL'); - const iPsswd = await shlp.question('[Q] PASSWORD '); - const authData = new URLSearchParams({ - 'username': iLogin, - 'password': iPsswd, - 'grant_type': 'password', - 'scope': 'offline_access' - }).toString(); - const authReqOpts: reqModule.Params = { - method: 'POST', - headers: api.beta_authHeaderMob, - body: authData - }; - const authReq = await req.getData(api.beta_auth, authReqOpts); - if(!authReq.ok || !authReq.res){ - console.log('[ERROR] Authentication failed!'); - return; - } - token = JSON.parse(authReq.res.body); - token.expires = new Date(Date.now() + token.expires_in); - yamlCfg.saveCRToken(token); - await getProfile(); - console.log('[INFO] Your Country: %s', token.country); -} - -async function getProfile(){ - if(!token.access_token){ - console.log('[ERROR] No access token!'); - return; - } - const profileReqOptions = { - headers: { - Authorization: `Bearer ${token.access_token}`, - }, - useProxy: true - }; - const profileReq = await req.getData(api.beta_profile, profileReqOptions); - if(!profileReq.ok || !profileReq.res){ - console.log('[ERROR] Get profile failed!'); - return; - } - const profile = JSON.parse(profileReq.res.body); - console.log('[INFO] USER: %s (%s)', profile.username, profile.email); -} - -// auth method -async function doAnonymousAuth(){ - const authData = new URLSearchParams({ - 'grant_type': 'client_id', - 'scope': 'offline_access', - }).toString(); - const authReqOpts: reqModule.Params = { - method: 'POST', - headers: api.beta_authHeaderMob, - body: authData - }; - const authReq = await req.getData(api.beta_auth, authReqOpts); - if(!authReq.ok || !authReq.res){ - console.log('[ERROR] Authentication failed!'); - return; - } - token = JSON.parse(authReq.res.body); - token.expires = new Date(Date.now() + token.expires_in); - yamlCfg.saveCRToken(token); -} - -// refresh token -async function refreshToken(){ - if(!token.access_token && !token.refresh_token || token.access_token && !token.refresh_token){ - await doAnonymousAuth(); - } - else{ - if(Date.now() > new Date(token.expires).getTime()){ - // return; - } - const authData = new URLSearchParams({ - 'refresh_token': token.refresh_token, - 'grant_type': 'refresh_token', - 'scope': 'offline_access' - }).toString(); - const authReqOpts: reqModule.Params = { - method: 'POST', - headers: api.beta_authHeaderMob, - body: authData - }; - const authReq = await req.getData(api.beta_auth, authReqOpts); - if(!authReq.ok || !authReq.res){ - console.log('[ERROR] Authentication failed!'); - return; - } - token = JSON.parse(authReq.res.body); - token.expires = new Date(Date.now() + token.expires_in); - yamlCfg.saveCRToken(token); - } - if(token.refresh_token){ - await getProfile(); - } - else{ - console.log('[INFO] USER: Anonymous'); - } - await getCMStoken(); -} - -async function getCMStoken(){ - if(!token.access_token){ - console.log('[ERROR] No access token!'); - return; - } - const cmsTokenReqOpts = { - headers: { - Authorization: `Bearer ${token.access_token}`, - }, - useProxy: true - }; - const cmsTokenReq = await req.getData(api.beta_cmsToken, cmsTokenReqOpts); - if(!cmsTokenReq.ok || !cmsTokenReq.res){ - console.log('[ERROR] Authentication CMS token failed!'); - return; - } - cmsToken = JSON.parse(cmsTokenReq.res.body); - console.log('[INFO] Your Country: %s\n', cmsToken.cms?.bucket.split('/')[1]); -} - -async function getCmsData(){ - // check token - if(!cmsToken.cms){ - console.log('[ERROR] Authentication required!'); - return; - } - // opts - const indexReqOpts = [ - api.beta_cms, - cmsToken.cms.bucket, - '/index?', - new URLSearchParams({ - 'Policy': cmsToken.cms.policy, - 'Signature': cmsToken.cms.signature, - 'Key-Pair-Id': cmsToken.cms.key_pair_id, - }), - ].join(''); - const indexReq = await req.getData(indexReqOpts); - if(!indexReq.ok || ! indexReq.res){ - console.log('[ERROR] Get CMS index FAILED!'); - return; - } - console.log(JSON.parse(indexReq.res.body)); -} - -async function doSearch(){ - if(!token.access_token){ - console.log('[ERROR] Authentication required!'); - return; - } - const searchReqOpts = { - headers: { - Authorization: `Bearer ${token.access_token}`, - }, - useProxy: true - }; - const searchParams = new URLSearchParams({ - q: argv.search as string, - n: "5", - start: argv.page ? `${(argv.page-1)*5}` : "0", - type: argv['search-type'], - locale: argv['search-locale'], - }).toString(); - let searchReq = await req.getData(`${api.beta_search}?${searchParams}`, searchReqOpts); - if(!searchReq.ok || ! searchReq.res){ - console.log('[ERROR] Search FAILED!'); - return; - } - let searchResults = JSON.parse(searchReq.res.body) as CrunchySearch; - if(searchResults.total < 1){ - console.log('[INFO] Nothing Found!'); - return; - } - const searchTypesInfo = { - 'top_results': 'Top results', - 'series': 'Found series', - 'movie_listing': 'Found movie lists', - 'episode': 'Found episodes' - }; - for(let search_item of searchResults.items){ - console.log('[INFO] %s:', searchTypesInfo[search_item.type as keyof typeof searchTypesInfo]); - // calculate pages - let itemPad = parseInt(new URL(search_item.__href__, domain.api_beta).searchParams.get('start') || ''); - let pageCur = itemPad > 0 ? Math.ceil(itemPad/5) + 1 : 1; - let pageMax = Math.ceil(search_item.total/5); - // pages per category - if(search_item.total < 1){ - console.log(' [INFO] Nothing Found...'); - } - if(search_item.total > 0){ - if(pageCur > pageMax){ - console.log(' [INFO] Last page is %s...', pageMax); - continue; - } - for(let item of search_item.items){ - await parseObject(item); - } - console.log(` [INFO] Total results: ${search_item.total} (Page: ${pageCur}/${pageMax})`); - } - } -} - -async function parseObject(item: ParseItem, pad?: number, getSeries?: boolean, getMovieListing?: boolean){ - if(argv.debug){ - console.log(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__; - } - 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 = [], - oBooleans = [], - tMetadata = item.type + '_metadata', - iMetadata = (Object.prototype.hasOwnProperty.call(item, tMetadata) ? item[tMetadata as keyof ParseItem] : item) as Record, - iTitle = [ item.title ]; - // set object booleans - if(iMetadata.duration_ms){ - oBooleans.push(shlp.formatTime(iMetadata.duration_ms/1000)); - } - if(iMetadata.is_simulcast){ - oBooleans.push('SIMULCAST'); - } - if(iMetadata.is_mature){ - oBooleans.push('MATURE'); - } - 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 ? true : false; - const showObjectBooleans = oBooleans.length > 0 && !iMetadata.hide_metadata ? true : false; - // make obj ids - let objects_ids = []; - 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.log( - '%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.log(''.padStart(pad+1, ' '), '- Last updated:', item.last_public); - } - if(item.subtitle_locales){ - iMetadata.subtitle_locales = item.subtitle_locales; - } - if(iMetadata.subtitle_locales && iMetadata.subtitle_locales.length > 0){ - console.log( - '%s- Subtitles: %s', - ''.padStart(pad + 2, ' '), - langsData.parseSubtitlesArray(iMetadata.subtitle_locales) - ); - } - if(item.availability_notes && argv.shownotes){ - console.log( - '%s- Availability notes: %s', - ''.padStart(pad + 2, ' '), - item.availability_notes.replace(/\[[^\]]*\]?/gm, '') - ); - } - if(item.type == 'series' && getSeries){ - argv.series = item.id; - await getSeriesById(pad, true); - } - if(item.type == 'movie_listing' && getMovieListing){ - argv['movie-listing'] = item.id; - await getMovieListingById(pad+2); - } -} - -async function getSeriesById(pad?: number, hideSeriesTitle?: boolean){ - // parse - pad = pad || 0; - hideSeriesTitle = hideSeriesTitle !== undefined ? hideSeriesTitle : false; - // check token - if(!cmsToken.cms){ - console.log('[ERROR] Authentication required!'); - return; - } - // opts - const seriesReqOpts = [ - api.beta_cms, - cmsToken.cms.bucket, - '/series/', - argv.series, - '?', - new URLSearchParams({ - 'Policy': cmsToken.cms.policy, - 'Signature': cmsToken.cms.signature, - 'Key-Pair-Id': cmsToken.cms.key_pair_id, - }), - ].join(''); - const seriesSeasonListReqOpts = [ - api.beta_cms, - cmsToken.cms.bucket, - '/seasons?', - new URLSearchParams({ - 'series_id': argv.series as string, - 'Policy': cmsToken.cms.policy, - 'Signature': cmsToken.cms.signature, - 'Key-Pair-Id': cmsToken.cms.key_pair_id, - }), - ].join(''); - // reqs - if(!hideSeriesTitle){ - const seriesReq = await req.getData(seriesReqOpts); - if(!seriesReq.ok || !seriesReq.res){ - console.log('[ERROR] Series Request FAILED!'); - return; - } - const seriesData = JSON.parse(seriesReq.res.body); - await parseObject(seriesData, pad, false); - } - // seasons list - const seriesSeasonListReq = await req.getData(seriesSeasonListReqOpts); - if(!seriesSeasonListReq.ok || !seriesSeasonListReq.res){ - console.log('[ERROR] Series Request FAILED!'); - return; - } - // parse data - const seasonsList = JSON.parse(seriesSeasonListReq.res.body); - if(seasonsList.total < 1){ - console.log('[INFO] Series is empty!'); - return; - } - for(let item of seasonsList.items){ - await parseObject(item, pad+2); - } -} - -async function getMovieListingById(pad?: number){ - pad = pad || 2; - if(!cmsToken.cms){ - console.log('[ERROR] Authentication required!'); - return; - } - const movieListingReqOpts = [ - api.beta_cms, - cmsToken.cms.bucket, - '/movies?', - new URLSearchParams({ - 'movie_listing_id': argv['movie-listing'] as string, - 'Policy': cmsToken.cms.policy, - 'Signature': cmsToken.cms.signature, - 'Key-Pair-Id': cmsToken.cms.key_pair_id, - }), - ].join(''); - const movieListingReq = await req.getData(movieListingReqOpts); - if(!movieListingReq.ok || !movieListingReq.res){ - console.log('[ERROR] Movie Listing Request FAILED!'); - return; - } - let movieListing = JSON.parse(movieListingReq.res.body); - if(movieListing.total < 1){ - console.log('[INFO] Movie Listing is empty!'); - return; - } - for(let item of movieListing.items){ - parseObject(item, pad); - } -} - -async function getNewlyAdded(){ - if(!token.access_token){ - console.log('[ERROR] Authentication required!'); - return; - } - const newlyAddedReqOpts = { - headers: { - Authorization: `Bearer ${token.access_token}`, - }, - useProxy: true - }; - const newlyAddedParams = new URLSearchParams({ - sort_by: 'newly_added', - n: "25", - start: (argv.page ? (argv.page-1)*25 : 0).toString(), - }).toString(); - let newlyAddedReq = await req.getData(`${api.beta_browse}?${newlyAddedParams}`, newlyAddedReqOpts); - if(!newlyAddedReq.ok || !newlyAddedReq.res){ - console.log('[ERROR] Get newly added FAILED!'); - return; - } - let newlyAddedResults = JSON.parse(newlyAddedReq.res.body); - console.log('[INFO] Newly added:'); - for(const i of newlyAddedResults.items){ - await parseObject(i, 2); - } - // calculate pages - let itemPad = parseInt(new URL(newlyAddedResults.__href__, domain.api_beta).searchParams.get('start') as string); - let pageCur = itemPad > 0 ? Math.ceil(itemPad/5) + 1 : 1; - let pageMax = Math.ceil(newlyAddedResults.total/5); - console.log(` [INFO] Total results: ${newlyAddedResults.total} (Page: ${pageCur}/${pageMax})`); -} - -async function getSeasonById(){ - if(!cmsToken.cms){ - console.log('[ERROR] Authentication required!'); - return; - } - - const showInfoReqOpts = [ - api.beta_cms, - cmsToken.cms.bucket, - '/seasons/', - argv.s, - '?', - new URLSearchParams({ - 'Policy': cmsToken.cms.policy, - 'Signature': cmsToken.cms.signature, - 'Key-Pair-Id': cmsToken.cms.key_pair_id, - }), - ].join(''); - const showInfoReq = await req.getData(showInfoReqOpts); - if(!showInfoReq.ok || !showInfoReq.res){ - console.log('[ERROR] Show Request FAILED!'); - return; - } - let showInfo = JSON.parse(showInfoReq.res.body); - parseObject(showInfo, 0); - const reqEpsListOpts = [ - api.beta_cms, - cmsToken.cms.bucket, - '/episodes?', - new URLSearchParams({ - 'season_id': argv.s as string, - 'Policy': cmsToken.cms.policy, - 'Signature': cmsToken.cms.signature, - 'Key-Pair-Id': cmsToken.cms.key_pair_id, - }), - ].join(''); - const reqEpsList = await req.getData(reqEpsListOpts); - if(!reqEpsList.ok || !reqEpsList.res){ - console.log('[ERROR] Episode List Request FAILED!'); - return; - } - let episodeList = JSON.parse(reqEpsList.res.body) as CrunchyEpisodeList; - - const epNumList: { - ep: number[], - sp: number - } = { ep: [], sp: 0 }; - const epNumLen = epsFilter.epNumLen; - - if(episodeList.total < 1){ - console.log(' [INFO] Season is empty!'); - return; - } - - const doEpsFilter = new epsFilter.doFilter(); - const selEps = doEpsFilter.checkFilter(argv.e); - const selectedMedia: CrunchyEpMeta[] = []; - - episodeList.items.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'; - } - // set data - const epMeta: CrunchyEpMeta = { - mediaId: item.id, - seasonTitle: item.season_title, - episodeNumber: item.episode, - episodeTitle: item.title, - seasonID: item.season_id - }; - if(item.playback){ - epMeta.playback = item.playback; - } - // find episode numbers - let 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['S'], '0') - : '' + parseInt(epNum, 10).toString().padStart(epNumLen['E'], '0') - ); - if(selEps.indexOf(selEpId) > -1 && !item.isSelected && item.playback){ - selectedMedia.push(epMeta); - item.isSelected = true; - } - // show ep - item.seq_id = selEpId; - parseObject(item); - }); - - // display - if(selectedMedia.length < 1){ - console.log('\n[INFO] Episodes not selected!\n'); - return; - } - - if(selectedMedia.length > 1){ - appstore.isBatch = true; - } - - console.log(); - for(let media of selectedMedia){ - await getMedia(media); - } - -} - -async function getObjectById(returnData?: boolean){ - if(!cmsToken.cms){ - console.log('[ERROR] Authentication required!'); - return; - } - - const doEpsFilter = new epsFilter.doFilter(); - const inpMedia = doEpsFilter.checkBetaFilter(argv.e as string); - - if(inpMedia.length < 1){ - console.log('\n[INFO] Objects not selected!\n'); - return; - } - - // node crunchy-beta -e G6497Z43Y,GRZXCMN1W,G62PEZ2E6,G25FVGDEK,GZ7UVPVX5 - console.log('[INFO] Requested object ID: %s', inpMedia.join(', ')); - - const objectReqOpts = [ - api.beta_cms, - cmsToken.cms.bucket, - '/objects/', - inpMedia.join(','), - '?', - new URLSearchParams({ - 'Policy': cmsToken.cms.policy, - 'Signature': cmsToken.cms.signature, - 'Key-Pair-Id': cmsToken.cms.key_pair_id, - }), - ].join(''); - const objectReq = await req.getData(objectReqOpts); - if(!objectReq.ok || !objectReq.res){ - console.log('[ERROR] Objects Request FAILED!'); - if(objectReq.error && objectReq.error.res && objectReq.error.res.body){ - const objectInfo = JSON.parse(objectReq.error.res.body as string); - console.log('[INFO] Body:', JSON.stringify(objectInfo, null, '\t')); - objectInfo.error = true; - return objectInfo; - } - return { error: true }; - } - - const objectInfo = JSON.parse(objectReq.res.body) as ObjectInfo; - if(returnData){ - return objectInfo; - } - - const selectedMedia = []; - - for(const item of objectInfo.items){ - if(item.type != 'episode' && item.type != 'movie'){ - await parseObject(item, 2, true, false); - continue; - } - const epMeta: Partial = {}; - switch (item.type) { - case 'episode': - item.s_num = 'S:' + item.episode_metadata.season_id; - epMeta.mediaId = 'E:'+ item.id; - epMeta.seasonTitle = item.episode_metadata.season_title; - epMeta.episodeNumber = item.episode_metadata.episode; - epMeta.episodeTitle = item.title; - break; - case 'movie': - item.f_num = 'F:' + item.movie_metadata?.movie_listing_id; - epMeta.mediaId = 'M:'+ item.id; - epMeta.seasonTitle = item.movie_metadata?.movie_listing_title; - epMeta.episodeNumber = 'Movie'; - epMeta.episodeTitle = item.title; - break; - } - if(item.playback){ - epMeta.playback = item.playback; - selectedMedia.push(epMeta); - item.isSelected = true; - } - await parseObject(item, 2); - } - - if(selectedMedia.length > 1){ - appstore.isBatch = true; - } - - console.log(); - for(let media of selectedMedia){ - await getMedia(media as CrunchyEpMeta); - } - -} - -async function getMedia(mMeta: CrunchyEpMeta){ - - let mediaName = '...'; - if(mMeta.seasonTitle && mMeta.episodeNumber && mMeta.episodeTitle){ - mediaName = `${mMeta.seasonTitle} - ${mMeta.episodeNumber} - ${mMeta.episodeTitle}`; - } - - console.log(`[INFO] Requesting: [${mMeta.mediaId}] ${mediaName}`); - - if(!mMeta.playback){ - console.log('[WARN] Video not available!'); - return; - } - - let playbackReq = await req.getData(mMeta.playback); - - if(!playbackReq.ok || !playbackReq.res){ - console.log('[ERROR] Request Stream URLs FAILED!'); - return; - } - - let pbData = JSON.parse(playbackReq.res.body) as PlaybackData; - - appstore.fn = ([ - ['title', mMeta.episodeTitle], - ['episode', mMeta.episodeNumber], - ['service', 'Crunchyroll'], - ['showTitle', mMeta.seasonTitle], - ['season', mMeta.seasonID] - ] as [yargs.AvailableFilenameVars, string|number][]).map((a): Variable => { - return { - name: a[0], - replaceWith: a[1], - type: typeof a[1] - } as Variable - }); - - let streams = []; - let hsLangs: string[] = []; - let pbStreams = pbData.streams; - - for(let s of Object.keys(pbStreams)){ - if(s.match(/hls/) && !s.match(/drm/) && !s.match(/trailer/)){ - let pb = Object.values(pbStreams[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 } - }; - }); - streams.push(...pb); - } - } - - if(streams.length < 1){ - console.log('[WARN] No full streams found!'); - return; - } - - let audDub = langsData.findLang(langsData.fixLanguageTag(pbData.audio_locale)).code; - hsLangs = langsData.sortTags(hsLangs); - - streams = streams.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; - }); - - let dlFailed = false; - - if(argv.hslang != 'none'){ - if(hsLangs.indexOf(argv.hslang) > -1){ - console.log('[INFO] Selecting stream with %s hardsubs', langsData.locale2language(argv.hslang).language); - streams = streams.filter((s) => { - if(s.hardsub_lang == '-'){ - return false; - } - return s.hardsub_lang == argv.hslang ? true : false; - }); - } - else{ - console.log('[WARN] Selected stream with %s hardsubs not available', langsData.locale2language(argv.hslang).language); - if(hsLangs.length > 0){ - console.log('[WARN] Try other hardsubs stream:', hsLangs.join(', ')); - } - dlFailed = true; - } - } - else{ - streams = streams.filter((s) => { - if(s.hardsub_lang != '-'){ - return false; - } - return true; - }); - if(streams.length < 1){ - console.log('[WARN] Raw streams not available!'); - if(hsLangs.length > 0){ - console.log('[WARN] Try hardsubs stream:', hsLangs.join(', ')); - } - dlFailed = true; - } - console.log('[INFO] Selecting raw stream'); - } - - let curStream; - if(!dlFailed){ - argv.kstream = typeof argv.kstream == 'number' ? argv.kstream : 1; - argv.kstream = argv.kstream > streams.length ? 1 : argv.kstream; - - streams.map((s, i) => { - const isSelected = argv.kstream == i + 1 ? '✓' : ' '; - console.log('[INFO] Full stream found! (%s%s: %s )', isSelected, i + 1, s.type); - }); - - console.log('[INFO] Downloading video...'); - curStream = streams[argv.kstream-1]; - if(argv.dubLang != curStream.audio_lang){ - argv.dubLang = curStream.audio_lang as string; - console.log(`[INFO] audio language code detected, setted to ${curStream.audio_lang} for this episode`); - } - - const streamUrlTxt = argv['show-stream-url'] ? curStream.url : '[HIDDEN]'; - console.log('[INFO] Playlists URL: %s (%s)', streamUrlTxt, curStream.type); - } - - if(!argv.skipdl && !dlFailed && curStream){ - const streamPlaylistsReq = await req.getData(curStream.url); - if(!streamPlaylistsReq.ok || !streamPlaylistsReq.res){ - console.log('[ERROR] CAN\'T FETCH VIDEO PLAYLISTS!'); - dlFailed = true; - } - else{ - const streamPlaylists = m3u8(streamPlaylistsReq.res.body); - let plServerList: string[] = [], - plStreams: Record> = {}, - plQuality: { - str: string, - dim: string, - RESOLUTION: { - width: number, - height: number - } - }[] = [] - for(const pl of streamPlaylists.playlists){ - // set quality - let plResolution = pl.attributes.RESOLUTION; - let plResolutionText = `${plResolution.width}x${plResolution.height}`; - // parse uri - let 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.log(`[WARN] Non duplicate url for ${plServer} detected, please report to developer!`); - } - else{ - plStreams[plServer][plResolutionText] = pl.uri; - } - // set plQualityStr - let plBandwidth = Math.round(pl.attributes.BANDWIDTH/1024); - let qualityStrAdd = `${plResolutionText} (${plBandwidth}KiB/s)`; - let qualityStrRegx = new RegExp(qualityStrAdd.replace(/(:|\(|\)|\/)/g, '\\$1'), 'm'); - let qualityStrMatch = !plQuality.map(a => a.str).join('\r\n').match(qualityStrRegx); - if(qualityStrMatch){ - plQuality.push({ - str: qualityStrAdd, - dim: plResolutionText, - RESOLUTION: plResolution - }); - } - } - - argv.server = argv.x > plServerList.length ? 1 : argv.x; - - let plSelectedServer = plServerList[argv.x - 1]; - let plSelectedList = plStreams[plSelectedServer]; - plQuality.sort((a, b) => { - const aMatch = a.dim.match(/[0-9]+/) || [] - const bMatch = b.dim.match(/[0-9]+/) || [] - return parseInt(aMatch[0]) - parseInt(bMatch[0]) - }); - let quality = argv.q; - if (quality > plQuality.length) { - console.log(`[WARN] The requested quality of ${argv.q} is greater than the maximun ${plQuality.length}.\nTherefor the maximum will be capped at ${plQuality.length}.`) - quality = plQuality.length; - } - let selPlUrl = quality === 0 ? plSelectedList[plQuality.pop()?.dim as string] : - plSelectedList[plQuality.map(a => a.dim)[quality - 1]] ? plSelectedList[plQuality.map(a => a.dim)[quality - 1]] : ''; - console.log(`[INFO] Servers available:\n\t${plServerList.join('\n\t')}`); - console.log(`[INFO] Available qualities:\n\t${plQuality.map((a, ind) => `[${ind+1}] ${a.str}`).join('\n\t')}`); - - if(selPlUrl != ''){ - appstore.fn.push({ - name: 'height', - type: 'number', - replaceWith: quality === 0 ? plQuality.pop()?.RESOLUTION.height as number : plQuality[quality - 1].RESOLUTION.height - }, { - name: 'width', - type: 'number', - replaceWith: quality === 0 ? plQuality.pop()?.RESOLUTION.width as number : plQuality[quality - 1].RESOLUTION.width - }) - console.log(`[INFO] Selected quality: ${Object.keys(plSelectedList).find(a => plSelectedList[a] === selPlUrl)} @ ${plSelectedServer}`); - if(argv['show-stream-url']){ - console.log('[INFO] Stream URL:', selPlUrl); - } - // TODO check filename - appstore.out = parseFileName(argv.fileName, appstore.fn, argv.numbers).join(path.sep) - console.log(`[INFO] Output filename: ${appstore.out}`); - const chunkPage = await req.getData(selPlUrl); - if(!chunkPage.ok || !chunkPage.res){ - console.log('[ERROR] CAN\'T FETCH VIDEO PLAYLIST!'); - dlFailed = true; - } - else{ - const chunkPlaylist = m3u8(chunkPage.res.body); - let totalParts = chunkPlaylist.segments.length; - let mathParts = Math.ceil(totalParts / argv.partsize); - let mathMsg = `(${mathParts}*${argv.partsize})`; - console.log('[INFO] Total parts in stream:', totalParts, mathMsg); - let tsFile = path.join(cfg.dir.content, appstore.out); - const split = appstore.out.split(path.sep).slice(0, -1); - split.forEach((val, ind, arr) => { - let isAbsolut = path.isAbsolute(appstore.out as string); - if (!fs.existsSync(path.join(isAbsolut ? '' : cfg.dir.content, ...arr.slice(0, ind), val))) - fs.mkdirSync(path.join(isAbsolut ? '' : cfg.dir.content, ...arr.slice(0, ind), val)); - }) - let streamdlParams = { - fn: `${tsFile}.ts`, - m3u8json: chunkPlaylist, - // baseurl: chunkPlaylist.baseUrl, - pcount: argv.tsparts, - partsOffset: 0, - }; - let dlStreamByPl = await new streamdl(streamdlParams).download(); - if(!dlStreamByPl.ok){ - console.log(`[ERROR] DL Stats: ${JSON.stringify(dlStreamByPl.parts)}\n`); - dlFailed = true; - } - } - } - else{ - console.log('[ERROR] Quality not selected!\n'); - dlFailed = true; - } - } - } - else if(argv.skipdl){ - console.log('[INFO] Downloading skipped!'); - } - - // fix max quality for non streams - /* - if(argv.quality == 'max'){ - argv.quality = '1080p'; - argv.appstore.fn.out = fnOutputGen(); - } - - argv.appstore.sxList = []; - - if(argv.dlsubs.indexOf('all') > -1){ - argv.dlsubs = ['all']; - } - - if(argv.hslang != 'none'){ - console.log('[WARN] Subtitles downloading disabled for hardsubs streams.'); - argv.skipsubs = true; - } - - if(!argv.skipsubs && argv.dlsubs.indexOf('none') == -1){ - if(pbData.subtitles && Object.values(pbData.subtitles).length > 0){ - let subsData = Object.values(pbData.subtitles); - subsData = subsData.map((s) => { - const subLang = langsData.fixAndFindCrLC(s.locale); - s.locale = subLang; - s.language = subLang.locale; - s.title = subLang.language; - return s; - }); - const subsArr = langsData.sortSubtitles(subsData, 'language'); - for(let subsIndex in subsArr){ - const subsItem = subsArr[subsIndex]; - const langItem = subsItem.locale; - const sxData = {}; - sxData.language = langItem; - sxData.file = langsData.subsFile(argv.appstore.fn.out, subsIndex, langItem); - sxData.path = path.join(cfg.dir.content, sxData.file); - if(argv.dlsubs.includes('all') || argv.dlsubs.includes(langItem.locale)){ - const subsAssReq = await req.getData(subsItem.url, {useProxy: argv['use-proxy-streaming']}); - if(subsAssReq.ok){ - const sBody = '\ufeff' + subsAssReq.res.body; - sxData.title = sBody.split('\r\n')[1].replace(/^Title: /, ''); - sxData.title = `${langItem.language} / ${sxData.title}`; - sxData.fonts = fontsData.assFonts(sBody); - fs.writeFileSync(path.join(cfg.dir.content, sxData.file), sBody); - console.log(`[INFO] Subtitle downloaded: ${sxData.file}`); - argv.appstore.sxList.push(sxData); - } - else{ - console.log(`[WARN] Failed to download subtitle: ${sxData.file}`); - } - } - } - } - else{ - console.log('[WARN] Can\'t find urls for subtitles!'); - } - } - else{ - console.log('[INFO] Subtitles downloading skipped!'); - } - - // go to muxing - if(!argv.skipmux && !dlFailed){ - await muxStreams(); - } - else{ - console.log(); - } - */ - -} - -async function muxStreams(){ - /* - const merger = await appMux.checkMerger(cfg.bin, argv.mp4); - const muxFile = path.join(cfg.dir.content, argv.appstore.fn.out); - const sxList = argv.appstore.sxList; - const audioDub = argv.dub; - const addSubs = argv.muxsubs && sxList.length > 0 ? true : false; - // set vars - const vtag = appMux.constructVideoTag(argv['video-tag'], argv['group-tag'], argv.hslang); - const vlang = argv.hslang != 'none' ? argv.hslang : 'und'; - let setMainSubLang = argv.defsublang != 'none' ? argv.defsublang : false; - let isMuxed = false; - // skip if no ts - if(!appMux.checkTSFile(`${muxFile}.ts`)){ - console.log('[INFO] TS file not found, skip muxing video...\n'); - return; - } - // collect fonts info - const fontList = appMux.makeFontsList(cfg.dir.fonts, fontsData, sxList); - // mergers - if(!argv.mp4 && !merger.MKVmerge){ - console.log('[WARN] MKVMerge not found...'); - } - if(!merger.MKVmerge && !merger.FFmpeg || argv.mp4 && !merger.MKVmerge){ - console.log('[WARN] FFmpeg not found...'); - } - // muxers additional options - const muxOpts = { - audioDub, - addSubs, - vtag, - vlang, - setMainSubLang, - }; - // do mkvmerge - if(!argv.mp4 && merger.MKVmerge){ - const mkvmux = await appMux.buildCommandMkvMerge(muxFile, sxList, fontList, { - ...muxOpts, useBCP: argv['use-bcp-tags'], - }); - fs.writeFileSync(`${muxFile}.json`,JSON.stringify(mkvmux, null, ' ')); - try{ - shlp.exec('mkvmerge', `"${merger.MKVmerge}"`, `@"${muxFile}.json"`); - isMuxed = true; - } - catch(e){ - // okay.. - } - } - else if(merger.FFmpeg){ - const outputFormat = !argv.mp4 ? 'mkv' : 'mp4'; - const subsCodec = !argv.mp4 ? 'copy' : 'mov_text'; - const ffmux = await appMux.buildCommandFFmpeg(muxFile, sxList, fontList, { - ...muxOpts, outputFormat, subsCodec, - }); - try{ - shlp.exec('ffmpeg',`"${merger.FFmpeg}"`, ffmux); - isMuxed = true; - } - catch(e){ - // okay... - } - - } - else{ - console.log('\n[INFO] Done!\n'); - return; - } - - doCleanUp(isMuxed, muxFile, addSubs, sxList); - */ - -} - -function doCleanUp(isMuxed: boolean, muxFile: string, addSubs: boolean, sxList: { - file: string -}[]){ - // set output filename - const fnOut = appstore.out; - // check paths if same - if(path.join(cfg.dir.trash) == path.join(cfg.dir.content)){ - argv.notrashfolder = true; - } - if(argv.nocleanup && !fs.existsSync(cfg.dir.trash)){ - argv.notrashfolder = true; - } - // cleanup - if(argv.notrashfolder && argv.nocleanup){ - // don't move or delete temp files - } - else if(argv.nocleanup){ - if(isMuxed){ - const toTrashTS = path.join(cfg.dir.trash, `${fnOut}`); - fs.renameSync(`${muxFile}.ts`, toTrashTS + '.ts'); - if(fs.existsSync(`${muxFile}.json`) && !argv.jsonmuxdebug){ - fs.renameSync(`${muxFile}.json`, toTrashTS + '.json'); - } - if(addSubs){ - for(let t of sxList){ - let subsFile = path.join(cfg.dir.content, t.file); - let subsTrash = path.join(cfg.dir.trash, t.file); - fs.renameSync(subsFile, subsTrash); - } - } - } - } - else if(isMuxed){ - fs.unlinkSync(`${muxFile}.ts`); - if(fs.existsSync(`${muxFile}.json`) && !argv.jsonmuxdebug){ - fs.unlinkSync(`${muxFile}.json`); - } - if(addSubs){ - for(let t of sxList){ - let subsFile = path.join(cfg.dir.content, t.file); - fs.unlinkSync(subsFile); - } - } - } - // done - console.log('\n[INFO] Done!\n'); -} \ No newline at end of file diff --git a/crunchy.ts b/crunchy.ts new file mode 100644 index 0000000..42fd117 --- /dev/null +++ b/crunchy.ts @@ -0,0 +1,1189 @@ +#!/usr/bin/env node + +// build-in +import path from 'path'; +import fs from 'fs-extra'; + +// package program +import packageJson from './package.json'; +console.log(`\n=== Crunchyroll Beta Downloader NX ${packageJson.version} ===\n`); + +// plugins +import shlp from 'sei-helper'; +import m3u8 from 'm3u8-parsed'; +import streamdl from 'hls-download'; + +// custom 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 epsFilter from './modules/module.eps-filter'; +import Merger, { Font } from './modules/module.merger'; + +// new-cfg paths +const cfg = yamlCfg.loadCfg(); +let token = yamlCfg.loadCRToken(); +let cmsToken: { + cms?: Record +} = {}; + +export type sxItem = { + language: langsData.LanguageItem, + path: string, + file: string + title: string, + fonts: Font[] +} + +// args +const argv = yargs.appArgv(cfg.cli); +const appstore: { + fn: Variable[], + isBatch: boolean, + out?: string, + sxList: sxItem[], + lang?: string +} = { + fn: [], + isBatch: false, + sxList: [] +}; + +// load req +import { domain, api } from './modules/module.api-urls'; +import * as reqModule from './modules/module.req'; +import { CrunchySearch } from './@types/crunchySearch'; +import { CrunchyEpisodeList } from './@types/crunchyEpisodeList'; +import { CrunchyEpMeta, ParseItem } from './@types/crunchyTypes'; +import { ObjectInfo } from './@types/objectInfo'; +import parseFileName, { Variable } from './modules/module.filename'; +import { PlaybackData } from './@types/playbackData'; +const req = new reqModule.Req(domain, argv); + +// select +export default (async () => { + // load binaries + cfg.bin = await yamlCfg.loadBinCfg(); + // select mode + if(argv.dlfonts){ + await getFonts(); + } + else if(argv.auth){ + await doAuth(); + } + else if(argv.cmsindex){ + await refreshToken(); + await getCmsData(); + } + else if(argv.new){ + await refreshToken(); + await getNewlyAdded(); + } + else if(argv.search && argv.search.length > 2){ + await refreshToken(); + await doSearch(); + } + else if(argv.series && argv.series.match(/^[0-9A-Z]{9}$/)){ + await refreshToken(); + await getSeriesById(); + } + else if(argv['movie-listing'] && argv['movie-listing'].match(/^[0-9A-Z]{9}$/)){ + await refreshToken(); + await getMovieListingById(); + } + else if(argv.s && argv.s.match(/^[0-9A-Z]{9}$/)){ + await refreshToken(); + await getSeasonById(); + } + else if(argv.e){ + await refreshToken(); + await getObjectById(); + } + else{ + yargs.showHelp(); + } +}); + +// get cr fonts +async function getFonts(){ + console.log('[INFO] Downloading fonts...'); + for(const f of Object.keys(fontsData.fonts)){ + const fontFile = fontsData.fonts[f as fontsData.AvailableFonts]; + const fontLoc = path.join(cfg.dir.fonts, fontFile); + if(fs.existsSync(fontLoc) && fs.statSync(fontLoc).size != 0){ + console.log(`[INFO] ${f} (${fontFile}) 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.log(); + } + const fontUrl = fontsData.root + fontFile; + const getFont = await req.getData(fontUrl, { binary: true }); + if(getFont.ok && getFont.res){ + fs.writeFileSync(fontLoc, getFont.res.body); + console.log(`[INFO] Downloaded: ${f} (${fontFile})`); + } + else{ + console.log(`[WARN] Failed to download: ${f} (${fontFile})`); + } + } + } + console.log('[INFO] All required fonts downloaded!'); +} + +// auth method +async function doAuth(){ + const iLogin = await shlp.question('[Q] LOGIN/EMAIL'); + const iPsswd = await shlp.question('[Q] PASSWORD '); + const authData = new URLSearchParams({ + 'username': iLogin, + 'password': iPsswd, + 'grant_type': 'password', + 'scope': 'offline_access' + }).toString(); + const authReqOpts: reqModule.Params = { + method: 'POST', + headers: api.beta_authHeaderMob, + body: authData + }; + const authReq = await req.getData(api.beta_auth, authReqOpts); + if(!authReq.ok || !authReq.res){ + console.log('[ERROR] Authentication failed!'); + return; + } + token = JSON.parse(authReq.res.body); + token.expires = new Date(Date.now() + token.expires_in); + yamlCfg.saveCRToken(token); + await getProfile(); + console.log('[INFO] Your Country: %s', token.country); +} + +async function getProfile(){ + if(!token.access_token){ + console.log('[ERROR] No access token!'); + return; + } + const profileReqOptions = { + headers: { + Authorization: `Bearer ${token.access_token}`, + }, + useProxy: true + }; + const profileReq = await req.getData(api.beta_profile, profileReqOptions); + if(!profileReq.ok || !profileReq.res){ + console.log('[ERROR] Get profile failed!'); + return; + } + const profile = JSON.parse(profileReq.res.body); + console.log('[INFO] USER: %s (%s)', profile.username, profile.email); +} + +// auth method +async function doAnonymousAuth(){ + const authData = new URLSearchParams({ + 'grant_type': 'client_id', + 'scope': 'offline_access', + }).toString(); + const authReqOpts: reqModule.Params = { + method: 'POST', + headers: api.beta_authHeaderMob, + body: authData + }; + const authReq = await req.getData(api.beta_auth, authReqOpts); + if(!authReq.ok || !authReq.res){ + console.log('[ERROR] Authentication failed!'); + return; + } + token = JSON.parse(authReq.res.body); + token.expires = new Date(Date.now() + token.expires_in); + yamlCfg.saveCRToken(token); +} + +// refresh token +async function refreshToken(){ + if(!token.access_token && !token.refresh_token || token.access_token && !token.refresh_token){ + await doAnonymousAuth(); + } + else{ + if(Date.now() > new Date(token.expires).getTime()){ + // return; + } + const authData = new URLSearchParams({ + 'refresh_token': token.refresh_token, + 'grant_type': 'refresh_token', + 'scope': 'offline_access' + }).toString(); + const authReqOpts: reqModule.Params = { + method: 'POST', + headers: api.beta_authHeaderMob, + body: authData + }; + const authReq = await req.getData(api.beta_auth, authReqOpts); + if(!authReq.ok || !authReq.res){ + console.log('[ERROR] Authentication failed!'); + return; + } + token = JSON.parse(authReq.res.body); + token.expires = new Date(Date.now() + token.expires_in); + yamlCfg.saveCRToken(token); + } + if(token.refresh_token){ + await getProfile(); + } + else{ + console.log('[INFO] USER: Anonymous'); + } + await getCMStoken(); +} + +async function getCMStoken(){ + if(!token.access_token){ + console.log('[ERROR] No access token!'); + return; + } + const cmsTokenReqOpts = { + headers: { + Authorization: `Bearer ${token.access_token}`, + }, + useProxy: true + }; + const cmsTokenReq = await req.getData(api.beta_cmsToken, cmsTokenReqOpts); + if(!cmsTokenReq.ok || !cmsTokenReq.res){ + console.log('[ERROR] Authentication CMS token failed!'); + return; + } + cmsToken = JSON.parse(cmsTokenReq.res.body); + console.log('[INFO] Your Country: %s\n', cmsToken.cms?.bucket.split('/')[1]); +} + +async function getCmsData(){ + // check token + if(!cmsToken.cms){ + console.log('[ERROR] Authentication required!'); + return; + } + // opts + const indexReqOpts = [ + api.beta_cms, + cmsToken.cms.bucket, + '/index?', + new URLSearchParams({ + 'Policy': cmsToken.cms.policy, + 'Signature': cmsToken.cms.signature, + 'Key-Pair-Id': cmsToken.cms.key_pair_id, + }), + ].join(''); + const indexReq = await req.getData(indexReqOpts); + if(!indexReq.ok || ! indexReq.res){ + console.log('[ERROR] Get CMS index FAILED!'); + return; + } + console.log(JSON.parse(indexReq.res.body)); +} + +async function doSearch(){ + if(!token.access_token){ + console.log('[ERROR] Authentication required!'); + return; + } + const searchReqOpts = { + headers: { + Authorization: `Bearer ${token.access_token}`, + }, + useProxy: true + }; + const searchParams = new URLSearchParams({ + q: argv.search as string, + n: '5', + start: argv.page ? `${(argv.page-1)*5}` : '0', + type: argv['search-type'], + locale: argv['search-locale'], + }).toString(); + const searchReq = await req.getData(`${api.beta_search}?${searchParams}`, searchReqOpts); + if(!searchReq.ok || ! searchReq.res){ + console.log('[ERROR] Search FAILED!'); + return; + } + const searchResults = JSON.parse(searchReq.res.body) as CrunchySearch; + if(searchResults.total < 1){ + console.log('[INFO] Nothing Found!'); + return; + } + const searchTypesInfo = { + 'top_results': 'Top results', + 'series': 'Found series', + 'movie_listing': 'Found movie lists', + 'episode': 'Found episodes' + }; + for(const search_item of searchResults.items){ + console.log('[INFO] %s:', searchTypesInfo[search_item.type as keyof typeof searchTypesInfo]); + // calculate pages + const itemPad = parseInt(new URL(search_item.__href__, domain.api_beta).searchParams.get('start') || ''); + const pageCur = itemPad > 0 ? Math.ceil(itemPad/5) + 1 : 1; + const pageMax = Math.ceil(search_item.total/5); + // pages per category + if(search_item.total < 1){ + console.log(' [INFO] Nothing Found...'); + } + if(search_item.total > 0){ + if(pageCur > pageMax){ + console.log(' [INFO] Last page is %s...', pageMax); + continue; + } + for(const item of search_item.items){ + await parseObject(item); + } + console.log(` [INFO] Total results: ${search_item.total} (Page: ${pageCur}/${pageMax})`); + } + } +} + +async function parseObject(item: ParseItem, pad?: number, getSeries?: boolean, getMovieListing?: boolean){ + if(argv.debug){ + console.log(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__; + } + 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 = [], + oBooleans = [], + tMetadata = item.type + '_metadata', + iMetadata = (Object.prototype.hasOwnProperty.call(item, tMetadata) ? item[tMetadata as keyof ParseItem] : item) as Record, + iTitle = [ item.title ]; + // set object booleans + if(iMetadata.duration_ms){ + oBooleans.push(shlp.formatTime(iMetadata.duration_ms/1000)); + } + if(iMetadata.is_simulcast){ + oBooleans.push('SIMULCAST'); + } + if(iMetadata.is_mature){ + oBooleans.push('MATURE'); + } + 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 ? true : false; + const showObjectBooleans = oBooleans.length > 0 && !iMetadata.hide_metadata ? true : false; + // make obj ids + const objects_ids = []; + 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.log( + '%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.log(''.padStart(pad+1, ' '), '- Last updated:', item.last_public); + } + if(item.subtitle_locales){ + iMetadata.subtitle_locales = item.subtitle_locales; + } + if(iMetadata.subtitle_locales && iMetadata.subtitle_locales.length > 0){ + console.log( + '%s- Subtitles: %s', + ''.padStart(pad + 2, ' '), + langsData.parseSubtitlesArray(iMetadata.subtitle_locales) + ); + } + if(item.availability_notes && argv.shownotes){ + console.log( + '%s- Availability notes: %s', + ''.padStart(pad + 2, ' '), + item.availability_notes.replace(/\[[^\]]*\]?/gm, '') + ); + } + if(item.type == 'series' && getSeries){ + argv.series = item.id; + await getSeriesById(pad, true); + } + if(item.type == 'movie_listing' && getMovieListing){ + argv['movie-listing'] = item.id; + await getMovieListingById(pad+2); + } +} + +async function getSeriesById(pad?: number, hideSeriesTitle?: boolean){ + // parse + pad = pad || 0; + hideSeriesTitle = hideSeriesTitle !== undefined ? hideSeriesTitle : false; + // check token + if(!cmsToken.cms){ + console.log('[ERROR] Authentication required!'); + return; + } + // opts + const seriesReqOpts = [ + api.beta_cms, + cmsToken.cms.bucket, + '/series/', + argv.series, + '?', + new URLSearchParams({ + 'Policy': cmsToken.cms.policy, + 'Signature': cmsToken.cms.signature, + 'Key-Pair-Id': cmsToken.cms.key_pair_id, + }), + ].join(''); + const seriesSeasonListReqOpts = [ + api.beta_cms, + cmsToken.cms.bucket, + '/seasons?', + new URLSearchParams({ + 'series_id': argv.series as string, + 'Policy': cmsToken.cms.policy, + 'Signature': cmsToken.cms.signature, + 'Key-Pair-Id': cmsToken.cms.key_pair_id, + }), + ].join(''); + // reqs + if(!hideSeriesTitle){ + const seriesReq = await req.getData(seriesReqOpts); + if(!seriesReq.ok || !seriesReq.res){ + console.log('[ERROR] Series Request FAILED!'); + return; + } + const seriesData = JSON.parse(seriesReq.res.body); + await parseObject(seriesData, pad, false); + } + // seasons list + const seriesSeasonListReq = await req.getData(seriesSeasonListReqOpts); + if(!seriesSeasonListReq.ok || !seriesSeasonListReq.res){ + console.log('[ERROR] Series Request FAILED!'); + return; + } + // parse data + const seasonsList = JSON.parse(seriesSeasonListReq.res.body); + if(seasonsList.total < 1){ + console.log('[INFO] Series is empty!'); + return; + } + for(const item of seasonsList.items){ + await parseObject(item, pad+2); + } +} + +async function getMovieListingById(pad?: number){ + pad = pad || 2; + if(!cmsToken.cms){ + console.log('[ERROR] Authentication required!'); + return; + } + const movieListingReqOpts = [ + api.beta_cms, + cmsToken.cms.bucket, + '/movies?', + new URLSearchParams({ + 'movie_listing_id': argv['movie-listing'] as string, + 'Policy': cmsToken.cms.policy, + 'Signature': cmsToken.cms.signature, + 'Key-Pair-Id': cmsToken.cms.key_pair_id, + }), + ].join(''); + const movieListingReq = await req.getData(movieListingReqOpts); + if(!movieListingReq.ok || !movieListingReq.res){ + console.log('[ERROR] Movie Listing Request FAILED!'); + return; + } + const movieListing = JSON.parse(movieListingReq.res.body); + if(movieListing.total < 1){ + console.log('[INFO] Movie Listing is empty!'); + return; + } + for(const item of movieListing.items){ + parseObject(item, pad); + } +} + +async function getNewlyAdded(){ + if(!token.access_token){ + console.log('[ERROR] Authentication required!'); + return; + } + const newlyAddedReqOpts = { + headers: { + Authorization: `Bearer ${token.access_token}`, + }, + useProxy: true + }; + const newlyAddedParams = new URLSearchParams({ + sort_by: 'newly_added', + n: '25', + start: (argv.page ? (argv.page-1)*25 : 0).toString(), + }).toString(); + const newlyAddedReq = await req.getData(`${api.beta_browse}?${newlyAddedParams}`, newlyAddedReqOpts); + if(!newlyAddedReq.ok || !newlyAddedReq.res){ + console.log('[ERROR] Get newly added FAILED!'); + return; + } + const newlyAddedResults = JSON.parse(newlyAddedReq.res.body); + console.log('[INFO] Newly added:'); + for(const i of newlyAddedResults.items){ + await parseObject(i, 2); + } + // calculate pages + const itemPad = parseInt(new URL(newlyAddedResults.__href__, domain.api_beta).searchParams.get('start') as string); + const pageCur = itemPad > 0 ? Math.ceil(itemPad/5) + 1 : 1; + const pageMax = Math.ceil(newlyAddedResults.total/5); + console.log(` [INFO] Total results: ${newlyAddedResults.total} (Page: ${pageCur}/${pageMax})`); +} + +async function getSeasonById(){ + if(!cmsToken.cms){ + console.log('[ERROR] Authentication required!'); + return; + } + + const showInfoReqOpts = [ + api.beta_cms, + cmsToken.cms.bucket, + '/seasons/', + argv.s, + '?', + new URLSearchParams({ + 'Policy': cmsToken.cms.policy, + 'Signature': cmsToken.cms.signature, + 'Key-Pair-Id': cmsToken.cms.key_pair_id, + }), + ].join(''); + const showInfoReq = await req.getData(showInfoReqOpts); + if(!showInfoReq.ok || !showInfoReq.res){ + console.log('[ERROR] Show Request FAILED!'); + return; + } + const showInfo = JSON.parse(showInfoReq.res.body); + parseObject(showInfo, 0); + const reqEpsListOpts = [ + api.beta_cms, + cmsToken.cms.bucket, + '/episodes?', + new URLSearchParams({ + 'season_id': argv.s as string, + 'Policy': cmsToken.cms.policy, + 'Signature': cmsToken.cms.signature, + 'Key-Pair-Id': cmsToken.cms.key_pair_id, + }), + ].join(''); + const reqEpsList = await req.getData(reqEpsListOpts); + if(!reqEpsList.ok || !reqEpsList.res){ + console.log('[ERROR] Episode List Request FAILED!'); + return; + } + const episodeList = JSON.parse(reqEpsList.res.body) as CrunchyEpisodeList; + + const epNumList: { + ep: number[], + sp: number + } = { ep: [], sp: 0 }; + const epNumLen = epsFilter.epNumLen; + + if(episodeList.total < 1){ + console.log(' [INFO] Season is empty!'); + return; + } + + const doEpsFilter = new epsFilter.doFilter(); + const selEps = doEpsFilter.checkFilter(argv.e); + const selectedMedia: CrunchyEpMeta[] = []; + + episodeList.items.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'; + } + // set data + const epMeta: CrunchyEpMeta = { + mediaId: item.id, + seasonTitle: item.season_title, + episodeNumber: item.episode, + episodeTitle: item.title, + seasonID: item.season_id + }; + if(item.playback){ + epMeta.playback = item.playback; + } + // find episode numbers + 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['S'], '0') + : '' + parseInt(epNum, 10).toString().padStart(epNumLen['E'], '0') + ); + if(selEps.indexOf(selEpId) > -1 && !item.isSelected && item.playback){ + selectedMedia.push(epMeta); + item.isSelected = true; + } + // show ep + item.seq_id = selEpId; + parseObject(item); + }); + + // display + if(selectedMedia.length < 1){ + console.log('\n[INFO] Episodes not selected!\n'); + return; + } + + if(selectedMedia.length > 1){ + appstore.isBatch = true; + } + + console.log(); + for(const media of selectedMedia){ + await getMedia(media); + } + +} + +async function getObjectById(returnData?: boolean){ + if(!cmsToken.cms){ + console.log('[ERROR] Authentication required!'); + return; + } + + const doEpsFilter = new epsFilter.doFilter(); + const inpMedia = doEpsFilter.checkBetaFilter(argv.e as string); + + if(inpMedia.length < 1){ + console.log('\n[INFO] Objects not selected!\n'); + return; + } + + // node crunchy-beta -e G6497Z43Y,GRZXCMN1W,G62PEZ2E6,G25FVGDEK,GZ7UVPVX5 + console.log('[INFO] Requested object ID: %s', inpMedia.join(', ')); + + const objectReqOpts = [ + api.beta_cms, + cmsToken.cms.bucket, + '/objects/', + inpMedia.join(','), + '?', + new URLSearchParams({ + 'Policy': cmsToken.cms.policy, + 'Signature': cmsToken.cms.signature, + 'Key-Pair-Id': cmsToken.cms.key_pair_id, + }), + ].join(''); + const objectReq = await req.getData(objectReqOpts); + if(!objectReq.ok || !objectReq.res){ + console.log('[ERROR] Objects Request FAILED!'); + if(objectReq.error && objectReq.error.res && objectReq.error.res.body){ + const objectInfo = JSON.parse(objectReq.error.res.body as string); + console.log('[INFO] Body:', JSON.stringify(objectInfo, null, '\t')); + objectInfo.error = true; + return objectInfo; + } + return { error: true }; + } + + const objectInfo = JSON.parse(objectReq.res.body) as ObjectInfo; + if(returnData){ + return objectInfo; + } + + const selectedMedia = []; + + for(const item of objectInfo.items){ + if(item.type != 'episode' && item.type != 'movie'){ + await parseObject(item, 2, true, false); + continue; + } + const epMeta: Partial = {}; + switch (item.type) { + case 'episode': + item.s_num = 'S:' + item.episode_metadata.season_id; + epMeta.mediaId = 'E:'+ item.id; + epMeta.seasonTitle = item.episode_metadata.season_title; + epMeta.episodeNumber = item.episode_metadata.episode; + epMeta.episodeTitle = item.title; + break; + case 'movie': + item.f_num = 'F:' + item.movie_metadata?.movie_listing_id; + epMeta.mediaId = 'M:'+ item.id; + epMeta.seasonTitle = item.movie_metadata?.movie_listing_title; + epMeta.episodeNumber = 'Movie'; + epMeta.episodeTitle = item.title; + break; + } + if(item.playback){ + epMeta.playback = item.playback; + selectedMedia.push(epMeta); + item.isSelected = true; + } + await parseObject(item, 2); + } + + if(selectedMedia.length > 1){ + appstore.isBatch = true; + } + + console.log(); + for(const media of selectedMedia){ + await getMedia(media as CrunchyEpMeta); + } + +} + +async function getMedia(mMeta: CrunchyEpMeta){ + + let mediaName = '...'; + if(mMeta.seasonTitle && mMeta.episodeNumber && mMeta.episodeTitle){ + mediaName = `${mMeta.seasonTitle} - ${mMeta.episodeNumber} - ${mMeta.episodeTitle}`; + } + + console.log(`[INFO] Requesting: [${mMeta.mediaId}] ${mediaName}`); + + if(!mMeta.playback){ + console.log('[WARN] Video not available!'); + return; + } + + const playbackReq = await req.getData(mMeta.playback); + + if(!playbackReq.ok || !playbackReq.res){ + console.log('[ERROR] Request Stream URLs FAILED!'); + return; + } + + const pbData = JSON.parse(playbackReq.res.body) as PlaybackData; + + appstore.fn = ([ + ['title', mMeta.episodeTitle], + ['episode', mMeta.episodeNumber], + ['service', 'Crunchyroll'], + ['showTitle', mMeta.seasonTitle], + ['season', mMeta.seasonID] + ] as [yargs.AvailableFilenameVars, string|number][]).map((a): Variable => { + return { + name: a[0], + replaceWith: a[1], + type: typeof a[1] + } as Variable; + }); + + let streams = []; + let hsLangs: string[] = []; + const pbStreams = pbData.streams; + + for(const s of Object.keys(pbStreams)){ + if(s.match(/hls/) && !s.match(/drm/) && !s.match(/trailer/)){ + const pb = Object.values(pbStreams[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 } + }; + }); + streams.push(...pb); + } + } + + if(streams.length < 1){ + console.log('[WARN] No full streams found!'); + return; + } + + const audDub = langsData.findLang(langsData.fixLanguageTag(pbData.audio_locale)).code; + hsLangs = langsData.sortTags(hsLangs); + + streams = streams.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; + }); + + let dlFailed = false; + + if(argv.hslang != 'none'){ + if(hsLangs.indexOf(argv.hslang) > -1){ + console.log('[INFO] Selecting stream with %s hardsubs', langsData.locale2language(argv.hslang).language); + streams = streams.filter((s) => { + if(s.hardsub_lang == '-'){ + return false; + } + return s.hardsub_lang == argv.hslang ? true : false; + }); + } + else{ + console.log('[WARN] Selected stream with %s hardsubs not available', langsData.locale2language(argv.hslang).language); + if(hsLangs.length > 0){ + console.log('[WARN] Try other hardsubs stream:', hsLangs.join(', ')); + } + dlFailed = true; + } + } + else{ + streams = streams.filter((s) => { + if(s.hardsub_lang != '-'){ + return false; + } + return true; + }); + if(streams.length < 1){ + console.log('[WARN] Raw streams not available!'); + if(hsLangs.length > 0){ + console.log('[WARN] Try hardsubs stream:', hsLangs.join(', ')); + } + dlFailed = true; + } + console.log('[INFO] Selecting raw stream'); + } + + let curStream; + if(!dlFailed){ + argv.kstream = typeof argv.kstream == 'number' ? argv.kstream : 1; + argv.kstream = argv.kstream > streams.length ? 1 : argv.kstream; + + streams.forEach((s, i) => { + const isSelected = argv.kstream == i + 1 ? '✓' : ' '; + console.log('[INFO] Full stream found! (%s%s: %s )', isSelected, i + 1, s.type); + }); + + console.log('[INFO] Downloading video...'); + curStream = streams[argv.kstream-1]; + if(argv.dubLang != curStream.audio_lang){ + argv.dubLang = curStream.audio_lang as string; + console.log(`[INFO] audio language code detected, setted to ${curStream.audio_lang} for this episode`); + } + + console.log('[INFO] Playlists URL: %s (%s)', curStream.url, curStream.type); + } + + if(!argv.skipdl && !dlFailed && curStream){ + const streamPlaylistsReq = await req.getData(curStream.url); + if(!streamPlaylistsReq.ok || !streamPlaylistsReq.res){ + console.log('[ERROR] CAN\'T FETCH VIDEO PLAYLISTS!'); + dlFailed = true; + } + else{ + const streamPlaylists = m3u8(streamPlaylistsReq.res.body); + const plServerList: string[] = [], + plStreams: Record> = {}, + plQuality: { + str: string, + dim: string, + RESOLUTION: { + width: number, + height: number + } + }[] = []; + for(const pl of streamPlaylists.playlists){ + // set quality + const plResolution = pl.attributes.RESOLUTION; + const plResolutionText = `${plResolution.width}x${plResolution.height}`; + // 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.log(`[WARN] 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, + RESOLUTION: plResolution + }); + } + } + + argv.server = argv.x > plServerList.length ? 1 : argv.x; + + const plSelectedServer = plServerList[argv.x - 1]; + const plSelectedList = plStreams[plSelectedServer]; + plQuality.sort((a, b) => { + const aMatch = a.dim.match(/[0-9]+/) || []; + const bMatch = b.dim.match(/[0-9]+/) || []; + return parseInt(aMatch[0]) - parseInt(bMatch[0]); + }); + let quality = argv.q; + if (quality > plQuality.length) { + console.log(`[WARN] The requested quality of ${argv.q} is greater than the maximun ${plQuality.length}.\n[WARN] Therefor the maximum will be capped at ${plQuality.length}.`); + quality = plQuality.length; + } + const selPlUrl = quality === 0 ? plSelectedList[plQuality.pop()?.dim as string] : + plSelectedList[plQuality.map(a => a.dim)[quality - 1]] ? plSelectedList[plQuality.map(a => a.dim)[quality - 1]] : ''; + console.log(`[INFO] Servers available:\n\t${plServerList.join('\n\t')}`); + console.log(`[INFO] Available qualities:\n\t${plQuality.map((a, ind) => `[${ind+1}] ${a.str}`).join('\n\t')}`); + + if(selPlUrl != ''){ + appstore.fn.push({ + name: 'height', + type: 'number', + replaceWith: quality === 0 ? plQuality.pop()?.RESOLUTION.height as number : plQuality[quality - 1].RESOLUTION.height + }, { + name: 'width', + type: 'number', + replaceWith: quality === 0 ? plQuality.pop()?.RESOLUTION.width as number : plQuality[quality - 1].RESOLUTION.width + }); + appstore.lang = curStream.audio_lang; + console.log(`[INFO] Selected quality: ${Object.keys(plSelectedList).find(a => plSelectedList[a] === selPlUrl)} @ ${plSelectedServer}`); + if(argv['show-stream-url']){ + console.log('[INFO] Stream URL:', selPlUrl); + } + // TODO check filename + appstore.out = parseFileName(argv.fileName, appstore.fn, argv.numbers).join(path.sep); + console.log(`[INFO] Output filename: ${appstore.out}`); + const chunkPage = await req.getData(selPlUrl); + if(!chunkPage.ok || !chunkPage.res){ + console.log('[ERROR] CAN\'T FETCH VIDEO PLAYLIST!'); + dlFailed = true; + } + else{ + const chunkPlaylist = m3u8(chunkPage.res.body); + const totalParts = chunkPlaylist.segments.length; + const mathParts = Math.ceil(totalParts / argv.partsize); + const mathMsg = `(${mathParts}*${argv.partsize})`; + console.log('[INFO] Total parts in stream:', totalParts, mathMsg); + const tsFile = path.isAbsolute(appstore.out as string) ? appstore.out : path.join(cfg.dir.content, appstore.out); + const split = appstore.out.split(path.sep).slice(0, -1); + split.forEach((val, ind, arr) => { + const isAbsolut = path.isAbsolute(appstore.out as string); + if (!fs.existsSync(path.join(isAbsolut ? '' : cfg.dir.content, ...arr.slice(0, ind), val))) + fs.mkdirSync(path.join(isAbsolut ? '' : cfg.dir.content, ...arr.slice(0, ind), val)); + }); + const streamdlParams = { + fn: `${tsFile}.ts`, + m3u8json: chunkPlaylist, + // baseurl: chunkPlaylist.baseUrl, + pcount: argv.tsparts, + partsOffset: 0, + }; + const dlStreamByPl = await new streamdl(streamdlParams).download(); + if(!dlStreamByPl.ok){ + console.log(`[ERROR] DL Stats: ${JSON.stringify(dlStreamByPl.parts)}\n`); + dlFailed = true; + } + } + } + else{ + console.log('[ERROR] Quality not selected!\n'); + dlFailed = true; + } + } + } + else if(argv.skipdl){ + console.log('[INFO] Downloading skipped!'); + } + + appstore.sxList = []; + + if(argv.dlsubs.indexOf('all') > -1){ + argv.dlsubs = ['all']; + } + + if(argv.hslang != 'none'){ + console.log('[WARN] Subtitles downloading disabled for hardsubs streams.'); + argv.skipsubs = true; + } + + + if(!argv.skipsubs && argv.dlsubs.indexOf('none') == -1){ + if(pbData.subtitles && Object.values(pbData.subtitles).length > 0){ + const subsData = Object.values(pbData.subtitles); + const subsDataMapped = subsData.map((s) => { + const subLang = langsData.fixAndFindCrLC(s.locale); + return { + ...s, + locale: subLang, + language: subLang.locale, + titile: subLang.language + }; + }); + 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; + sxData.file = langsData.subsFile(appstore.out as string, subsIndex, langItem); + sxData.path = path.join(cfg.dir.content, sxData.file); + if(argv.dlsubs.includes('all') || argv.dlsubs.includes(langItem.locale)){ + const subsAssReq = await req.getData(subsItem.url); + if(subsAssReq.ok && subsAssReq.res){ + const sBody = '\ufeff' + subsAssReq.res.body; + sxData.title = sBody.split('\r\n')[1].replace(/^Title: /, ''); + sxData.title = `${langItem.language} / ${sxData.title}`; + sxData.fonts = fontsData.assFonts(sBody) as Font[]; + fs.writeFileSync(path.join(cfg.dir.content, sxData.file), sBody); + console.log(`[INFO] Subtitle downloaded: ${sxData.file}`); + appstore.sxList.push(sxData as sxItem); + } + else{ + console.log(`[WARN] Failed to download subtitle: ${sxData.file}`); + } + } + } + } + else{ + console.log('[WARN] Can\'t find urls for subtitles!'); + } + } + else{ + console.log('[INFO] Subtitles downloading skipped!'); + } + + // go to muxing + if(!argv.skipmux && !dlFailed){ + await muxStreams(); + } + else{ + console.log(); + } + +} + +async function muxStreams(){ + const merger = new Merger({ + onlyAudio: [], + onlyVid: [], + videoAndAudio: [{ + path: (path.isAbsolute(appstore.out as string) ? appstore.out as string : path.join(cfg.dir.content, appstore.out as string)) + '.ts', + lang: appstore.lang as string, + lookup: false + }], + subtitels: appstore.sxList.map(a => ({ + language: a.language.code, + file: a.path, + lookup: false, + title: a.title + })), + output: (path.isAbsolute(appstore.out as string) ? appstore.out as string : path.join(cfg.dir.content, appstore.out as string)) + '.' + (argv.mp4 ? 'mp4' : 'mkv'), + simul: false, + fonts: Merger.makeFontsList(cfg.dir.fonts, appstore.sxList) + }); + const bin = Merger.checkMerger(cfg.bin, argv.mp4); + // collect fonts info + // mergers + let isMuxed = false; + if (bin.MKVmerge) { + const command = merger.MkvMerge(); + shlp.exec('mkvmerge', `"${bin.MKVmerge}"`, command); + isMuxed = true; + } else if (bin.FFmpeg) { + const command = merger.FFmpeg(); + shlp.exec('ffmpeg', `"${bin.FFmpeg}"`, command); + isMuxed = true; + } else{ + console.log('\n[INFO] Done!\n'); + return; + } + if (isMuxed) + merger.cleanUp(); +} \ No newline at end of file diff --git a/index.ts b/index.ts index d46f088..7cf4856 100644 --- a/index.ts +++ b/index.ts @@ -1,7 +1,8 @@ -import { appArgv } from "./modules/module.app-args"; +import { appArgv } from './modules/module.app-args'; import * as yamlCfg from './modules/module.cfg-loader'; -import funimation from './funi' +import funimation from './funi'; +import crunchy from './crunchy'; (async () => { const cfg = yamlCfg.loadCfg(); @@ -9,9 +10,9 @@ import funimation from './funi' const argv = appArgv(cfg.cli); if (argv.service === 'funi') { - await funimation() + await funimation(); } else if (argv.service === 'crunchy') { - + await crunchy(); } -})() \ No newline at end of file +})(); \ No newline at end of file diff --git a/modules/module.api-urls.ts b/modules/module.api-urls.ts index fc42175..f0c0ce6 100644 --- a/modules/module.api-urls.ts +++ b/modules/module.api-urls.ts @@ -1,4 +1,4 @@ -import { Headers } from "got/dist/source"; +import { Headers } from 'got/dist/source'; // api domains const domain = { diff --git a/modules/module.app-args.ts b/modules/module.app-args.ts index 2eed1e4..b387f6c 100644 --- a/modules/module.app-args.ts +++ b/modules/module.app-args.ts @@ -1,5 +1,5 @@ import yargs from 'yargs'; -import * as langsData from "./module.langsData"; +import * as langsData from './module.langsData'; yargs(process.argv.slice(2)); @@ -12,7 +12,7 @@ const groups = { 'fileName': 'Filename Template:', 'debug': 'Debug:', 'util': 'Utilities:' -} +}; export type AvailableFilenameVars = 'title' | 'episode' | 'showTitle' | 'season' | 'width' | 'height' | 'service' @@ -46,240 +46,247 @@ const appArgv = (cfg: { }; const argv = yargs.parserConfiguration({ - "duplicate-arguments-array": false, - "camel-case-expansion": false + 'duplicate-arguments-array': false, + 'camel-case-expansion': false }) - .wrap(yargs.terminalWidth()) - .usage('Usage: $0 [options]') - .help(false).version(false) - .option('auth', { - group: groups.auth, - describe: 'Enter authentication mode', - type: 'boolean' - }) - .option('dlFonts', { - group: groups.fonts, - describe: 'Download all required fonts for mkv muxing', - type: 'boolean' - }) - .option('search', { - group: groups.search, - alias: 'f', - describe: 'Search for an anime', - type: 'string' - }) - .option('search-type', { - group: groups.search, - describe: 'Search type used for crunchyroll', - choices: [ '', 'top_results', 'series', 'movie_listing', 'episode' ], - default: '', - type: 'string', - }) - .option('page', { - group: groups.search, - alias: 'p', - describe: 'Page number for search results', - type: 'number', - }) - .option('search-locale', { - group: groups.search, - describe: 'Search locale used for crunchyroll', - choices: langsData.searchLocales, - default: '', - type: 'string', - }) - .option('new', { - group: groups.dl, - describe: 'Get last updated series list from crunchyroll', - type: 'boolean', - }) - .option('movie-listing', { - group: groups.dl, - alias: 'flm', - describe: 'Get video list by Movie Listing ID', - type: 'string', - }) - .option('series', { - group: groups.dl, - alias: 'srz', - describe: 'Get season list by Series ID', - type: 'string', - }) - .option('s', { - group: groups.dl, - describe: 'Set the season ID', - type: 'string' - }) - .option('e', { - group: groups.dl, - alias: 'episode', - describe: 'Sets the Episode Number/IDs (comma-separated, hyphen-sequence)', - type: 'string', - }) - .option('q', { - group: groups.dl, - describe: 'Set the quality layer. Use 0 to get the best quality.', - default: parseDefault('videoLayer', 7), - type: 'number' - }) - .option('x', { - group: groups.dl, - alias: 'server', - describe: 'Select server', - choices: [1, 2, 3, 4], - default: parseDefault('nServer', 1), - type: 'number', - }) - .option('kstream', { - group: groups.dl, - alias: 'k', - describe: 'Select specific stream for crunchyroll', - choices: [1, 2, 3, 4, 5, 6, 7], - default: parseDefault('kStream', 1), - type: 'number', - }) - .option('partsize', { - group: groups.dl, - describe: 'Set the amount of parts that should be downloaded in paralell', - type: 'number', - default: parseDefault('partsize', 10) - }) - .option('hslang', { - group: groups.dl, - describe: 'Download video with specific hardsubs', - choices: langsData.subtitleLanguagesFilter.slice(1), - default: parseDefault('hsLang', 'none'), - type: 'string', - }) - .option('subLang', { - group: groups.dl, - describe: 'Set the subtitles to download (Funi only)', - choices: subLang, - default: parseDefault('subLang', []), - type: 'array' - }) - .option('novids', { - group: groups.dl, - describe: 'Skip downloading videos', - type: 'boolean' - }) - .option('noaudio', { - group: groups.dl, - describe: 'Skip downloading audio', - type: 'boolean' - }) - .option('nosubs', { - group: groups.dl, - describe: 'Skip downloading subtitles', - type: 'boolean' - }) - .option('dub', { - group: groups.dl, - describe: 'Set languages to download (funi only)', - choices: dubLang, - default: parseDefault('dub', ['enUS']), - type: 'array' - }) - .option('dubLang', { - group: groups.dl, - describe: 'Set the language to download (Crunchy only)', - choices: langsData.dubLanguageCodes, - default: parseDefault('dubLanguage', langsData.dubLanguageCodes.slice(-1)[0]), - type: 'string', - }) - .option('all', { - group: groups.dl, - describe: 'Used to download all episodes from the show (Funi only)', - type: 'boolean', - default: parseDefault('all', false) - }) - .option('fontSize', { - group: groups.dl, - describe: 'Used to set the fontsize of the subtitles', - default: parseDefault('fontSize', 55), - type: 'number' - }) - .option('allSubs', { - group: groups.dl, - describe: 'If set to true, all available subs will get downloaded (Funi only)', - default: false, - type: 'boolean' - }) - .option('allDubs', { - group: groups.dl, - describe: 'If set to true, all available dubs will get downloaded (Funi only)', - default: false, - type: 'boolean' - }) - .option('timeout', { - group: groups.dl, - describe: 'Set the timeout of all download reqests. Set in millisecods', - type: 'number', - default: parseDefault('timeout', 60 * 1000) - }) - .option('simul', { - group: groups.dl, - describe: 'Force downloading simulcast ver. instead of uncut ver. (if uncut ver. available) (Funi only)', - default: parseDefault('forceSimul', false), - type: 'boolean', - }) - .option('mp4', { - group: groups.mux, - describe: 'Mux video into mp4', - default: parseDefault('mp4mux', false), - type: 'boolean' - }) - .option('skipmux', { - group: groups.mux, - describe: 'Skip muxing video and subtitles', - type: 'boolean' - }) - .option('fileName', { - group: groups.fileName, - describe: `Set the filename template. Use \${variable_name} to insert variables.\nYou may use ${availableFilenameVars - .map(a => `'${a}'`).join(', ')} as variables.`, - type: 'string', - default: parseDefault('fileName', '[${service}] ${showTitle} - ${episode} [${height}p]') - }) - .option('numbers', { - group: groups.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: parseDefault('numbers', 2) - }) - .option('nosess', { - group: groups.debug, - type: 'boolean', - default: 'Reset session cookie for testing purposes' - }) - .option('debug', { - group: groups.debug, - describe: 'Debug mode (tokens may be revield in the console output)', - type: 'boolean' - }) - .option('nocleanup', { - group: groups.util, - describe: 'Don\'t delete subtitles and videos after muxing', - default: parseDefault('noCleanUp', false), - type: 'boolean' - }) - .option('help', { - alias: 'h', - group: 'Help:', - describe: 'Show this help', - type: 'boolean' - }) - .option('service', { - group: groups.util, - describe: 'Set the service to use', - choices: ['funi', 'crunchy'], - demandOption: true, - default: parseDefault<'crunchy'|'funi'|undefined>('service', undefined) - }) - .parseSync(); + .wrap(yargs.terminalWidth()) + .usage('Usage: $0 [options]') + .help(false).version(false) + .option('auth', { + group: groups.auth, + describe: 'Enter authentication mode', + type: 'boolean' + }) + .option('dlFonts', { + group: groups.fonts, + describe: 'Download all required fonts for mkv muxing', + type: 'boolean' + }) + .option('search', { + group: groups.search, + alias: 'f', + describe: 'Search for an anime', + type: 'string' + }) + .option('search-type', { + group: groups.search, + describe: 'Search type used for crunchyroll', + choices: [ '', 'top_results', 'series', 'movie_listing', 'episode' ], + default: '', + type: 'string', + }) + .option('page', { + group: groups.search, + alias: 'p', + describe: 'Page number for search results', + type: 'number', + }) + .option('search-locale', { + group: groups.search, + describe: 'Search locale used for crunchyroll', + choices: langsData.searchLocales, + default: '', + type: 'string', + }) + .option('new', { + group: groups.dl, + describe: 'Get last updated series list from crunchyroll', + type: 'boolean', + }) + .option('movie-listing', { + group: groups.dl, + alias: 'flm', + describe: 'Get video list by Movie Listing ID', + type: 'string', + }) + .option('series', { + group: groups.dl, + alias: 'srz', + describe: 'Get season list by Series ID', + type: 'string', + }) + .option('s', { + group: groups.dl, + describe: 'Set the season ID', + type: 'string' + }) + .option('e', { + group: groups.dl, + alias: 'episode', + describe: 'Sets the Episode Number/IDs (comma-separated, hyphen-sequence)', + type: 'string', + }) + .option('q', { + group: groups.dl, + describe: 'Set the quality layer. Use 0 to get the best quality.', + default: parseDefault('videoLayer', 7), + type: 'number' + }) + .option('x', { + group: groups.dl, + alias: 'server', + describe: 'Select server', + choices: [1, 2, 3, 4], + default: parseDefault('nServer', 1), + type: 'number', + }) + .option('kstream', { + group: groups.dl, + alias: 'k', + describe: 'Select specific stream for crunchyroll', + choices: [1, 2, 3, 4, 5, 6, 7], + default: parseDefault('kStream', 1), + type: 'number', + }) + .option('partsize', { + group: groups.dl, + describe: 'Set the amount of parts that should be downloaded in paralell', + type: 'number', + default: parseDefault('partsize', 10) + }) + .option('hslang', { + group: groups.dl, + describe: 'Download video with specific hardsubs', + choices: langsData.subtitleLanguagesFilter.slice(1), + default: parseDefault('hsLang', 'none'), + type: 'string', + }) + .option('subLang', { + group: groups.dl, + describe: 'Set the subtitles to download (Funi only)', + choices: subLang, + default: parseDefault('subLang', []), + type: 'array' + }) + .option('dlsubs', { + group: groups.dl, + describe: 'Download subtitles by language tag (space-separated) (crunchy only)', + choices: langsData.subtitleLanguagesFilter, + default: parseDefault('dlSubs', 'all'), + type: 'array', + }) + .option('novids', { + group: groups.dl, + describe: 'Skip downloading videos', + type: 'boolean' + }) + .option('noaudio', { + group: groups.dl, + describe: 'Skip downloading audio', + type: 'boolean' + }) + .option('nosubs', { + group: groups.dl, + describe: 'Skip downloading subtitles', + type: 'boolean' + }) + .option('dub', { + group: groups.dl, + describe: 'Set languages to download (funi only)', + choices: dubLang, + default: parseDefault('dub', ['enUS']), + type: 'array' + }) + .option('dubLang', { + group: groups.dl, + describe: 'Set the language to download (Crunchy only)', + choices: langsData.dubLanguageCodes, + default: parseDefault('dubLanguage', langsData.dubLanguageCodes.slice(-1)[0]), + type: 'string', + }) + .option('all', { + group: groups.dl, + describe: 'Used to download all episodes from the show (Funi only)', + type: 'boolean', + default: parseDefault('all', false) + }) + .option('fontSize', { + group: groups.dl, + describe: 'Used to set the fontsize of the subtitles', + default: parseDefault('fontSize', 55), + type: 'number' + }) + .option('allSubs', { + group: groups.dl, + describe: 'If set to true, all available subs will get downloaded (Funi only)', + default: false, + type: 'boolean' + }) + .option('allDubs', { + group: groups.dl, + describe: 'If set to true, all available dubs will get downloaded (Funi only)', + default: false, + type: 'boolean' + }) + .option('timeout', { + group: groups.dl, + describe: 'Set the timeout of all download reqests. Set in millisecods', + type: 'number', + default: parseDefault('timeout', 60 * 1000) + }) + .option('simul', { + group: groups.dl, + describe: 'Force downloading simulcast ver. instead of uncut ver. (if uncut ver. available) (Funi only)', + default: parseDefault('forceSimul', false), + type: 'boolean', + }) + .option('mp4', { + group: groups.mux, + describe: 'Mux video into mp4', + default: parseDefault('mp4mux', false), + type: 'boolean' + }) + .option('skipmux', { + group: groups.mux, + describe: 'Skip muxing video and subtitles', + type: 'boolean' + }) + .option('fileName', { + group: groups.fileName, + describe: `Set the filename template. Use \${variable_name} to insert variables.\nYou may use ${availableFilenameVars + .map(a => `'${a}'`).join(', ')} as variables.`, + type: 'string', + default: parseDefault('fileName', '[${service}] ${showTitle} - ${episode} [${height}p]') + }) + .option('numbers', { + group: groups.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: parseDefault('numbers', 2) + }) + .option('nosess', { + group: groups.debug, + type: 'boolean', + default: 'Reset session cookie for testing purposes' + }) + .option('debug', { + group: groups.debug, + describe: 'Debug mode (tokens may be revield in the console output)', + type: 'boolean' + }) + .option('nocleanup', { + group: groups.util, + describe: 'Don\'t delete subtitles and videos after muxing', + default: parseDefault('noCleanUp', false), + type: 'boolean' + }) + .option('help', { + alias: 'h', + group: 'Help:', + describe: 'Show this help', + type: 'boolean' + }) + .option('service', { + group: groups.util, + describe: 'Set the service to use', + choices: ['funi', 'crunchy'], + demandOption: true, + default: parseDefault<'crunchy'|'funi'|undefined>('service', undefined) + }) + .parseSync(); return argv; -} +}; const showHelp = yargs.showHelp; @@ -289,4 +296,4 @@ export { availableFilenameVars, subLang, dubLang -} \ No newline at end of file +}; \ No newline at end of file diff --git a/modules/module.cfg-loader.ts b/modules/module.cfg-loader.ts index 8188b42..5463cd4 100644 --- a/modules/module.cfg-loader.ts +++ b/modules/module.cfg-loader.ts @@ -171,7 +171,7 @@ const loadFuniToken = () => { }>(tokenFile.funi, true); let token: false|string = false; if (loadedToken && loadedToken.token) - token = loadedToken.token; + token = loadedToken.token; // info if token not set if(!token){ console.log('[INFO] Token not set!\n'); diff --git a/modules/module.curl-req.ts b/modules/module.curl-req.ts index 2cbc1b5..0c43f79 100644 --- a/modules/module.curl-req.ts +++ b/modules/module.curl-req.ts @@ -27,7 +27,7 @@ export type Res = { // req const curlReq = async (curlBin: string, url: string, options: CurlOptions, cache: string) => { - let curlOpt = [ + const curlOpt = [ `"${curlBin}"`, `"${url}"`, ]; @@ -35,8 +35,8 @@ const curlReq = async (curlBin: string, url: string, options: CurlOptions, cache options = options || {}; if(options.headers && Object.keys(options.headers).length > 0){ - for(let h of Object.keys(options.headers)){ - let hC = options.headers[h]; + for(const h of Object.keys(options.headers)){ + const hC = options.headers[h]; curlOpt.push('-H', `"${h}: ${hC}"`); } } @@ -103,7 +103,7 @@ const curlReq = async (curlBin: string, url: string, options: CurlOptions, cache fs.unlinkSync(bodyFile); fs.unlinkSync(errFile); - let res: Res = { + const res: Res = { httpVersion: '', statusCode: '', statusMessage: '', @@ -113,20 +113,20 @@ const curlReq = async (curlBin: string, url: string, options: CurlOptions, cache body: rawBody.toString(), }; - let headersCont = rawHeaders.replace(/\r/g, '').split('\n'); + const headersCont = rawHeaders.replace(/\r/g, '').split('\n'); - for(let h of headersCont){ + for(const h of headersCont){ if( h == '' ){ continue; } if(!h.match(':')){ - let statusRes = h.split(' '); + const statusRes = h.split(' '); res.httpVersion = statusRes[0].split('/')[1]; res.statusCode = statusRes[1]; res.statusMessage = statusRes.slice(2).join(' '); } else{ - let resHeader = h.split(': '); - let resHeadName = resHeader[0].toLowerCase(); - let resHeadCont = resHeader.slice(1).join(': '); + const resHeader = h.split(': '); + const resHeadName = resHeader[0].toLowerCase(); + const resHeadCont = resHeader.slice(1).join(': '); if(resHeadName == 'set-cookie'){ if(!Object.prototype.hasOwnProperty.call(res.headers, resHeadName)){ res.headers[resHeadName] = []; @@ -140,7 +140,7 @@ const curlReq = async (curlBin: string, url: string, options: CurlOptions, cache } if(!res.statusCode.match(/^(2|3)\d\d$/)){ - let httpStatusMessage = res.statusMessage ? ` (${res.statusMessage})` : ''; + const httpStatusMessage = res.statusMessage ? ` (${res.statusMessage})` : ''; throw { name: 'HTTPError', message: `Response code ${res.statusCode}${httpStatusMessage}`, @@ -154,7 +154,7 @@ const curlReq = async (curlBin: string, url: string, options: CurlOptions, cache function uuidv4() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - let r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } diff --git a/modules/module.eps-filter.ts b/modules/module.eps-filter.ts index 4198c25..cb92503 100644 --- a/modules/module.eps-filter.ts +++ b/modules/module.eps-filter.ts @@ -7,7 +7,6 @@ const betaEpRegex = new RegExp (/^[0-9A-Z]{9}$/); const epLtReg = new RegExp (/(?:E|S|M)/); class doFilter { - constructor(){} ifMaxEp(type: keyof typeof epNumLen, num: number){ const maxEp = Math.pow(10, epNumLen[type]) - 1; return num > maxEp ? true : false; @@ -40,7 +39,7 @@ class doFilter { eRange[0] = this.ifMaxEp(epLetter, eRange[0]) ? this.powNum(epLetter) - 1 : eRange[0]; eRange[1] = eRange[1].toString().match(/^\d+$/) ? parseInt(eRange[1] as string) : 0; eRange[1] = this.ifMaxEp(epLetter, eRange[1]) ? this.powNum(epLetter) - 1 : eRange[1]; - console.log(eRange) + console.log(eRange); // check if correct range if (eRange[0] > eRange[1]){ const parsedEl = [ @@ -76,7 +75,7 @@ class doFilter { return ''; }); // end - const filteredArr1 = [...new Set(filteredArr.concat(inputEpsRange))] + const filteredArr1 = [...new Set(filteredArr.concat(inputEpsRange))]; const filteredArr2 = filteredArr1.indexOf('') > -1 ? filteredArr1.slice(1) : filteredArr1; return filteredArr2; } diff --git a/modules/module.filename.ts b/modules/module.filename.ts index 22284ef..9110d44 100644 --- a/modules/module.filename.ts +++ b/modules/module.filename.ts @@ -1,6 +1,6 @@ -import * as shlp from "sei-helper"; -import path from "path"; -import { AvailableFilenameVars } from "./module.app-args"; +import * as shlp from 'sei-helper'; +import path from 'path'; +import { AvailableFilenameVars } from './module.app-args'; export type Variable = ({ @@ -23,7 +23,7 @@ const parseFileName = (input: string, variables: Variable[], numbers: number): s const varName = type.slice(2, -1); const use = variables.find(a => a.name === varName); if (use === undefined) { - console.log(`[ERROR] Found variable '${type}' in fileName but no values was internally found!`) + console.log(`[ERROR] Found variable '${type}' in fileName but no values was internally found!`); continue; } @@ -32,10 +32,10 @@ const parseFileName = (input: string, variables: Variable[], numbers: number): s const replaceStr = len < numbers ? '0'.repeat(numbers - len) + use.replaceWith : use.replaceWith.toFixed(0); input = input.replace(type, replaceStr); } else { - input = input.replace(type, use.replaceWith) + input = input.replace(type, use.replaceWith); } } return input.split(path.sep).map(a => shlp.cleanupFilename(a)); -} +}; export default parseFileName; \ No newline at end of file diff --git a/modules/module.fontsData.ts b/modules/module.fontsData.ts index 9da695e..519e681 100644 --- a/modules/module.fontsData.ts +++ b/modules/module.fontsData.ts @@ -67,11 +67,11 @@ const fonts = { // collect styles from ass string function assFonts(ass: string){ - let strings = ass.replace(/\r/g,'').split('\n'); - let styles = []; - for(let s of strings){ + const strings = ass.replace(/\r/g,'').split('\n'); + const styles = []; + for(const s of strings){ if(s.match(/^Style: /)){ - let addStyle = s.split(','); + const addStyle = s.split(','); styles.push(addStyle[1]); } } diff --git a/modules/module.langsData.ts b/modules/module.langsData.ts index a103784..ffccf9c 100644 --- a/modules/module.langsData.ts +++ b/modules/module.langsData.ts @@ -26,7 +26,7 @@ const languages: LanguageItem[] = [ // add en language names (() =>{ - for(let languageIndex in languages){ + for(const languageIndex in languages){ if(!languages[languageIndex].language){ languages[languageIndex].language = languages[languageIndex].name; } @@ -99,16 +99,18 @@ const parseSubtitlesArray = (tags: string[]) => { }; // sort subtitles -const sortSubtitles = (data: Partial[], sortkey: keyof LanguageItem = 'locale') => { +const sortSubtitles = > (data: T[], sortkey?: keyof T) : T[] => { const idx: Record = {}; - sortkey = sortkey || 'locale'; + 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[sortkey] as string] ? idx[a[sortkey] as string] : 50; - const ib = idx[b[sortkey] as string] ? idx[b[sortkey] as string] : 50; + 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; diff --git a/modules/module.merger.ts b/modules/module.merger.ts index 094893c..beb6da9 100644 --- a/modules/module.merger.ts +++ b/modules/module.merger.ts @@ -1,18 +1,20 @@ import * as iso639 from 'iso-639'; -import { fonts, fontMime } from "./module.fontsData"; -import path from "path"; -import fs from "fs"; +import { fonts, fontMime } from './module.fontsData'; +import path from 'path'; +import fs from 'fs'; import { LanguageItem } from './module.langsData'; export type MergerInput = { path: string, lang: string, + lookup?: false, } export type SubtitleInput = { language: string, file: string, - fonts?: ParsedFont[] + title?: string + lookup?: false, } export type Font = keyof typeof fonts; @@ -30,6 +32,7 @@ export type MergerOptions = { subtitels: SubtitleInput[], output: string, simul?: boolean, + fonts?: ParsedFont[] } class Merger { @@ -131,7 +134,7 @@ class Merger { '--video-tracks 0', '--no-audio' ); - const trackName = this.subDict[vid.lang] + (this.options.simul ? ' [Simulcast]' : ' [Uncut]'); + const trackName = (vid.lookup === false ? vid.lang : this.subDict[vid.lang]) + (this.options.simul ? ' [Simulcast]' : ' [Uncut]'); args.push('--track-name', `0:"${trackName}"`); args.push(`--language 0:${Merger.getLanguageCode(vid.lang, vid.lang)}`); hasVideo = true; @@ -145,7 +148,7 @@ class Merger { '--video-tracks 0', '--audio-tracks 1' ); - const trackName = this.subDict[vid.lang] + (this.options.simul ? ' [Simulcast]' : ' [Uncut]'); + const trackName = (vid.lookup === false ? vid.lang : this.subDict[vid.lang]) + (this.options.simul ? ' [Simulcast]' : ' [Uncut]'); args.push('--track-name', `0:"${trackName}"`); args.push('--track-name', `1:"${trackName}"`); args.push(`--language 1:${Merger.getLanguageCode(vid.lang, vid.lang)}`); @@ -155,7 +158,7 @@ class Merger { '--no-video', '--audio-tracks 1' ); - const trackName = this.subDict[vid.lang] + (this.options.simul ? ' [Simulcast]' : ' [Uncut]'); + const trackName = (vid.lookup === false ? vid.lang : this.subDict[vid.lang]) + (this.options.simul ? ' [Simulcast]' : ' [Uncut]'); args.push('--track-name', `1:"${trackName}"`); args.push(`--language 1:${Merger.getLanguageCode(vid.lang, vid.lang)}`); } @@ -163,7 +166,7 @@ class Merger { } for (const aud of this.options.onlyAudio) { - const trackName = this.subDict[aud.lang] + (this.options.simul ? ' [Simulcast]' : ' [Uncut]'); + const trackName = (aud.lookup === false ? aud.lang : this.subDict[aud.lang]) + (this.options.simul ? ' [Simulcast]' : ' [Uncut]'); args.push('--track-name', `0:"${trackName}"`); args.push(`--language 0:${Merger.getLanguageCode(aud.lang, aud.lang)}`); args.push( @@ -175,26 +178,27 @@ class Merger { if (this.options.subtitels.length > 0) { for (const subObj of this.options.subtitels) { - const trackName = this.subDict[subObj.language] + (this.options.simul ? ' [Simulcast]' : ' [Uncut]'); - args.push('--track-name', `0:"${trackName}"`); - args.push('--language', `0:${Merger.getLanguageCode(subObj.language)}`); + args.push('--track-name', (subObj.title !== undefined ? `0:"${subObj.title}"` : `0:"${subObj.lookup === false ? subObj.language : Merger.getLanguageCode(subObj.language)}"`)); + args.push('--language', `0:"${subObj.lookup === false ? subObj.language : Merger.getLanguageCode(subObj.language)}"`); args.push(`"${subObj.file}"`); - if (subObj.fonts && subObj.fonts.length > 0) { - for (const f of subObj.fonts) { - args.push('--attachment-name', f.name); - args.push('--attachment-mime-type', f.mime); - args.push('--attach-file', f.path); - } - } } } 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(' '); }; @@ -225,7 +229,7 @@ class Merger { language: LanguageItem, fonts: Font[] }[]) : ParsedFont[] { - let fontsNameList: Font[] = [], fontsList = [], subsList = [], isNstr = true; + let fontsNameList: Font[] = []; const fontsList = [], subsList = []; let isNstr = true; for(const s of subs){ fontsNameList.push(...s.fonts); subsList.push(s.language.locale); @@ -253,7 +257,12 @@ class Merger { } } return fontsList; - }; + } + + public cleanUp() { + this.options.onlyAudio.concat(this.options.onlyVid).concat(this.options.videoAndAudio).forEach(a => fs.unlinkSync(a.path)); + this.options.subtitels.forEach(a => fs.unlinkSync(a.file)); + } } diff --git a/modules/module.req.ts b/modules/module.req.ts index 661cad1..4123b7b 100644 --- a/modules/module.req.ts +++ b/modules/module.req.ts @@ -1,6 +1,3 @@ -import path from 'path'; -import fs from 'fs-extra'; - import shlp from 'sei-helper'; import got, { Headers, Method, Options, ReadError, Response } from 'got'; import cookieFile from './module.cookieFile'; @@ -44,7 +41,7 @@ class Req { async getData (durl: string, params?: Params) { params = params || {}; // options - let options: Options & { + const options: Options & { minVersion?: string, maxVersion?: string curlDebug?: boolean @@ -83,7 +80,6 @@ class Req { } }*/ // if auth - let cookie = []; const loc = new URL(durl); // avoid cloudflare protection if(loc.origin == this.domain.www){ @@ -124,7 +120,7 @@ class Req { name: string } & ReadError & { res: Response - } + }; if(error.response && error.response.statusCode && error.response.statusMessage){ console.log(`[ERROR] ${error.name} ${error.response.statusCode}: ${error.response.statusMessage}`); } @@ -140,122 +136,122 @@ class Req { } if(error.res && error.res.body && error.response.statusCode && error.response.statusCode != 404 && error.response.statusCode != 403){ - console.log('[ERROR] Body:', error.res.body); - } - return { - ok: false, - error, - }; + console.log('[ERROR] Body:', error.res.body); + } + return { + ok: false, + error, + }; + } + } + setNewCookie(setCookie: Record, isAuth: boolean, fileData?: string){ + const cookieUpdated = []; let lastExp = 0; + console.trace('Type of setCookie:', typeof setCookie, setCookie); + const parsedCookie = fileData ? cookieFile(fileData) : shlp.cookie.parse(setCookie); + for(const cookieName of Object.keys(parsedCookie)){ + if(parsedCookie[cookieName] && parsedCookie[cookieName].value && parsedCookie[cookieName].value == 'deleted'){ + delete parsedCookie[cookieName]; } } - setNewCookie(setCookie: Record, isAuth: boolean, fileData?: string){ - let cookieUpdated = [], lastExp = 0; - console.trace('Type of setCookie:', typeof setCookie, setCookie) - const parsedCookie = fileData ? cookieFile(fileData) : shlp.cookie.parse(setCookie); - for(let cookieName of Object.keys(parsedCookie)){ - if(parsedCookie[cookieName] && parsedCookie[cookieName].value && parsedCookie[cookieName].value == 'deleted'){ - delete parsedCookie[cookieName]; - } + for(const uCookie of usefulCookies.auth){ + const cookieForceExp = 60*60*24*7; + const cookieExpCur = this.session[uCookie] ? this.session[uCookie] : { expires: 0 }; + const cookieExp = new Date(cookieExpCur.expires).getTime() - cookieForceExp; + if(cookieExp > lastExp){ + lastExp = cookieExp; } - for(let uCookie of usefulCookies.auth){ - const cookieForceExp = 60*60*24*7; - const cookieExpCur = this.session[uCookie] ? this.session[uCookie] : { expires: 0 }; - const cookieExp = new Date(cookieExpCur.expires).getTime() - cookieForceExp; - if(cookieExp > lastExp){ - lastExp = cookieExp; - } + } + for(const uCookie of usefulCookies.auth){ + if(!parsedCookie[uCookie]){ + continue; } - for(let uCookie of usefulCookies.auth){ - if(!parsedCookie[uCookie]){ - continue; - } - if(isAuth || parsedCookie[uCookie] && Date.now() > lastExp){ - this.session[uCookie] = parsedCookie[uCookie]; - cookieUpdated.push(uCookie); - } + if(isAuth || parsedCookie[uCookie] && Date.now() > lastExp){ + this.session[uCookie] = parsedCookie[uCookie]; + cookieUpdated.push(uCookie); } - for(let uCookie of usefulCookies.sess){ - if(!parsedCookie[uCookie]){ - continue; - } - if( - isAuth + } + for(const uCookie of usefulCookies.sess){ + if(!parsedCookie[uCookie]){ + continue; + } + if( + isAuth || this.argv.nosess && parsedCookie[uCookie] || parsedCookie[uCookie] && !this.checkSessId(this.session[uCookie]) - ){ - const sessionExp = 60*60; - this.session[uCookie] = parsedCookie[uCookie]; - this.session[uCookie].expires = new Date(Date.now() + sessionExp*1000); - this.session[uCookie]['Max-Age'] = sessionExp.toString(); - cookieUpdated.push(uCookie); - } - } - if(cookieUpdated.length > 0){ - if(this.argv.debug){ - console.log('[SAVING FILE]',`${this.sessCfg}.yml`); - } - yamlCfg.saveCRSession(this.session); - console.log(`[INFO] Cookies were updated! (${cookieUpdated.join(', ')})\n`); - } + ){ + const sessionExp = 60*60; + this.session[uCookie] = parsedCookie[uCookie]; + this.session[uCookie].expires = new Date(Date.now() + sessionExp*1000); + this.session[uCookie]['Max-Age'] = sessionExp.toString(); + cookieUpdated.push(uCookie); } - checkCookieVal(chcookie: Record){ - return chcookie + } + if(cookieUpdated.length > 0){ + if(this.argv.debug){ + console.log('[SAVING FILE]',`${this.sessCfg}.yml`); + } + yamlCfg.saveCRSession(this.session); + console.log(`[INFO] Cookies were updated! (${cookieUpdated.join(', ')})\n`); + } + } + checkCookieVal(chcookie: Record){ + return chcookie && chcookie.toString() == '[object Object]' && typeof chcookie.value == 'string' - ? true : false; - } - checkSessId(session_id: Record){ - if(session_id && typeof session_id.expires == 'string'){ - session_id.expires = new Date(session_id.expires); - } - return session_id + ? true : false; + } + checkSessId(session_id: Record){ + if(session_id && typeof session_id.expires == 'string'){ + session_id.expires = new Date(session_id.expires); + } + return session_id && session_id.toString() == '[object Object]' && typeof session_id.expires == 'object' && Date.now() < new Date(session_id.expires as any).getTime() && typeof session_id.value == 'string' - ? true : false; - } - uuidv4(){ - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - let r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); - } - }; + ? true : false; + } + uuidv4(){ + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } +} - function buildProxy(proxyBaseUrl: string, proxyAuth: string){ - if(!proxyBaseUrl.match(/^(https?|socks4|socks5):/)){ - proxyBaseUrl = 'http://' + proxyBaseUrl; - } +function buildProxy(proxyBaseUrl: string, proxyAuth: string){ + if(!proxyBaseUrl.match(/^(https?|socks4|socks5):/)){ + proxyBaseUrl = 'http://' + proxyBaseUrl; + } - let 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; +} - export { - buildProxy, - usefulCookies, - Req, - }; +export { + buildProxy, + usefulCookies, + Req, +}; \ No newline at end of file diff --git a/package.json b/package.json index 8ae23be..d34302f 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "url": "https://github.com/izu-co/funimation-downloader-nx/issues" }, "license": "MIT", - "main": "funi.js", + "main": "index.js", "dependencies": { "cheerio": "^1.0.0-rc.10", "form-data": "^4.0.0",