From e9e14aef2fc3fd81d09bc7ba65ae5e8e82f50af1 Mon Sep 17 00:00:00 2001 From: AnimeDL Date: Sat, 6 Apr 2024 21:05:04 -0700 Subject: [PATCH 1/6] Remove old HD API --- gui/server/services/hidive.ts | 148 ++---- hidive.ts | 868 +++------------------------------- modules/module.app-args.ts | 1 - modules/module.args.ts | 14 - 4 files changed, 110 insertions(+), 921 deletions(-) diff --git a/gui/server/services/hidive.ts b/gui/server/services/hidive.ts index 9f6827c..30eee45 100644 --- a/gui/server/services/hidive.ts +++ b/gui/server/services/hidive.ts @@ -13,12 +13,10 @@ class HidiveHandler extends Base implements MessageHandler { constructor(ws: WebSocketHandler) { super(ws); this.hidive = new Hidive(); - this.hidive.doInit(); this.initState(); } public async auth(data: AuthData) { - await this.getAPIVersion(); return this.hidive.doAuth(data); } @@ -27,13 +25,7 @@ class HidiveHandler extends Base implements MessageHandler { return { isOk: true, value: undefined }; } - public async getAPIVersion() { - const _default = yargs.appArgv(this.hidive.cfg.cli, true); - this.hidive.api = _default.hdapi; - } - public async search(data: SearchData): Promise { - await this.getAPIVersion(); console.debug(`Got search options: ${JSON.stringify(data)}`); const hidiveSearch = await this.hidive.doSearch(data); if (!hidiveSearch.isOk) { @@ -69,46 +61,24 @@ class HidiveHandler extends Base implements MessageHandler { if (isNaN(parse) || parse <= 0) return false; console.debug(`Got resolve options: ${JSON.stringify(data)}`); - await this.getAPIVersion(); - if (this.hidive.api == 'old') { - const res = await this.hidive.getShow(parseInt(data.id), data.e, data.but, data.all); - if (!res.isOk || !res.value) - return res.isOk; - this.addToQueue(res.value.map(item => { - return { - ...data, - ids: [item.Id], - title: item.Name, - parent: { - title: item.seriesTitle, - season: parseFloat(item.SeasonNumberValue+'')+'' - }, - image: item.ScreenShotSmallUrl, - e: parseFloat(item.EpisodeNumberValue+'')+'', - episode: parseFloat(item.EpisodeNumberValue+'')+'', - }; - })); - return true; - } else { - const res = await this.hidive.selectSeries(parseInt(data.id), data.e, data.but, data.all); - if (!res.isOk || !res.value) - return res.isOk; - this.addToQueue(res.value.map(item => { - return { - ...data, - ids: [item.id], - title: item.title, - parent: { - title: item.seriesTitle, - season: item.episodeInformation.seasonNumber+'' - }, - image: item.thumbnailUrl, - e: item.episodeInformation.episodeNumber+'', - episode: item.episodeInformation.episodeNumber+'', - }; - })); - return true; - } + const res = await this.hidive.selectSeries(parseInt(data.id), data.e, data.but, data.all); + if (!res.isOk || !res.value) + return res.isOk; + this.addToQueue(res.value.map(item => { + return { + ...data, + ids: [item.id], + title: item.title, + parent: { + title: item.seriesTitle, + season: item.episodeInformation.seasonNumber+'' + }, + image: item.thumbnailUrl, + e: item.episodeInformation.episodeNumber+'', + episode: item.episodeInformation.episodeNumber+'', + }; + })); + return true; } public async listEpisodes(id: string): Promise { @@ -116,73 +86,37 @@ class HidiveHandler extends Base implements MessageHandler { if (isNaN(parse) || parse <= 0) return { isOk: false, reason: new Error('The ID is invalid') }; - await this.getAPIVersion(); - if (this.hidive.api == 'old') { - const request = await this.hidive.listShow(parse); - if (!request.isOk || !request.value) - return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')}; + const request = await this.hidive.listSeries(parse); + if (!request.isOk || !request.value) + return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')}; - return { isOk: true, value: request.value.Episodes.map(function(item) { - const language = item.Summary.match(/^Audio: (.*)/m); - language?.shift(); - const description = item.Summary.split('\r\n'); - return { - e: parseFloat(item.EpisodeNumberValue+'')+'', - lang: language ? language[0].split(', ') : [], - name: item.Name, - season: parseFloat(item.SeasonNumberValue+'')+'', - seasonTitle: request.value.Name, - episode: parseFloat(item.EpisodeNumberValue+'')+'', - id: item.Id+'', - img: item.ScreenShotSmallUrl, - description: description ? description[0] : '', - time: '' - }; - })}; - } else { - const request = await this.hidive.listSeries(parse); - if (!request.isOk || !request.value) - return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')}; - - return { isOk: true, value: request.value.map(function(item) { - const description = item.description.split('\r\n'); - return { - e: item.episodeInformation.episodeNumber+'', - lang: [], - name: item.title, - season: item.episodeInformation.seasonNumber+'', - seasonTitle: request.series.seasons[item.episodeInformation.seasonNumber-1].title, - episode: item.episodeInformation.episodeNumber+'', - id: item.id+'', - img: item.thumbnailUrl, - description: description ? description[0] : '', - time: '' - }; - })}; - } + return { isOk: true, value: request.value.map(function(item) { + const description = item.description.split('\r\n'); + return { + e: item.episodeInformation.episodeNumber+'', + lang: [], + name: item.title, + season: item.episodeInformation.seasonNumber+'', + seasonTitle: request.series.seasons[item.episodeInformation.seasonNumber-1].title, + episode: item.episodeInformation.episodeNumber+'', + id: item.id+'', + img: item.thumbnailUrl, + description: description ? description[0] : '', + time: '' + }; + })}; } public async downloadItem(data: DownloadData) { this.setDownloading(true); console.debug(`Got download options: ${JSON.stringify(data)}`); const _default = yargs.appArgv(this.hidive.cfg.cli, true); - this.hidive.api = _default.hdapi; - if (this.hidive.api == 'old') { - const res = await this.hidive.getShow(parseInt(data.id), data.e, false, false); - if (!res.isOk || !res.showData) - return this.alertError(new Error('Download failed upstream, check for additional logs')); + const res = await this.hidive.selectSeries(parseInt(data.id), data.e, false, false); + if (!res.isOk || !res.showData) + return this.alertError(new Error('Download failed upstream, check for additional logs')); - for (const ep of res.value) { - await this.hidive.getEpisode(ep, {..._default, callbackMaker: this.makeProgressHandler.bind(this), dubLang: data.dubLang, dlsubs: data.dlsubs, fileName: data.fileName, q: data.q, force: 'y', noaudio: data.noaudio, novids: data.novids }); - } - } else { - const res = await this.hidive.selectSeries(parseInt(data.id), data.e, false, false); - if (!res.isOk || !res.showData) - return this.alertError(new Error('Download failed upstream, check for additional logs')); - - for (const ep of res.value) { - await this.hidive.downloadEpisode(ep, {..._default, callbackMaker: this.makeProgressHandler.bind(this), dubLang: data.dubLang, dlsubs: data.dlsubs, fileName: data.fileName, q: data.q, force: 'y', noaudio: data.noaudio, novids: data.novids }); - } + for (const ep of res.value) { + await this.hidive.downloadEpisode(ep, {..._default, callbackMaker: this.makeProgressHandler.bind(this), dubLang: data.dubLang, dlsubs: data.dlsubs, fileName: data.fileName, q: data.q, force: 'y', noaudio: data.noaudio, novids: data.novids }); } this.sendMessage({ name: 'finish', data: undefined }); this.setDownloading(false); diff --git a/hidive.ts b/hidive.ts index 6532dce..c82d9e6 100644 --- a/hidive.ts +++ b/hidive.ts @@ -1,7 +1,6 @@ // build-in import path from 'path'; import fs from 'fs-extra'; -import crypto from 'crypto'; // package program import packageJson from './package.json'; @@ -9,7 +8,6 @@ import packageJson from './package.json'; // plugins import { console } from './modules/log'; import shlp from 'sei-helper'; -import m3u8 from 'm3u8-parsed'; import streamdl, { M3U8Json } from './modules/hls-download'; // custom modules @@ -23,8 +21,7 @@ import vtt2ass from './modules/module.vtt2ass'; // load req import { domain, api } from './modules/module.api-urls'; import * as reqModule from './modules/module.req'; -import { HidiveEpisodeList, HidiveEpisodeExtra } from './@types/hidiveEpisodeList'; -import { HidiveVideoList, HidiveStreamInfo, DownloadedMedia, HidiveSubtitleInfo } from './@types/hidiveTypes'; +import { DownloadedMedia } from './@types/hidiveTypes'; import parseFileName, { Variable } from './modules/module.filename'; import { downloaded } from './modules/module.downloadArchive'; import parseSelect from './modules/module.parseSelect'; @@ -32,8 +29,6 @@ import { AvailableFilenameVars } from './modules/module.args'; import { AuthData, AuthResponse, SearchData, SearchResponse, SearchResponseItem } from './@types/messageHandler'; import { ServiceClass } from './@types/serviceClassInterface'; import { sxItem } from './crunchy'; -import { HidiveSearch } from './@types/hidiveSearch'; -import { HidiveDashboard } from './@types/hidiveDashboard'; import { Hit, NewHidiveSearch } from './@types/newHidiveSearch'; import { NewHidiveSeries } from './@types/newHidiveSeries'; import { Episode, NewHidiveEpisodeExtra, NewHidiveSeason, NewHidiveSeriesExtra } from './@types/newHidiveSeason'; @@ -46,59 +41,27 @@ import { KeyContainer } from './modules/license'; export default class Hidive implements ServiceClass { public cfg: yamlCfg.ConfigObject; - private session: Record; - private tokenOld: Record; private token: Record; private req: reqModule.Req; - public api: 'old' | 'new'; - private client: { - // base - ipAddress: string, - xNonce: string, - xSignature: string, - // personal - visitId: string, - // profile data - profile: { - userId: number, - profileId: number, - deviceId : string, - } - }; constructor(private debug = false) { this.cfg = yamlCfg.loadCfg(); - this.session = yamlCfg.loadHDSession(); - this.tokenOld = yamlCfg.loadHDToken(); this.token = yamlCfg.loadNewHDToken(); - this.client = yamlCfg.loadHDProfile() as {ipAddress: string, xNonce: string, xSignature: string, visitId: string, profile: {userId: number, profileId: number, deviceId : string}}; this.req = new reqModule.Req(domain, debug, false, 'hd'); - this.api = 'old'; } public async cli() { console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`); const argv = yargs.appArgv(this.cfg.cli); - this.api = argv.hdapi; if (argv.debug) this.debug = true; //below is for quickly testing API calls - /*const searchItems = await this.reqData('GetTitles', {'Filter': 'recently-added', 'Pager': {'Number': 1, 'Size': 30}, 'Sort': 'Date', 'Verbose': false}); - const searchItems = await this.reqData('GetTitles', {'Id': 492}); - if(!searchItems.ok || !searchItems.res){return;} - console.info(searchItems.res.body); - fs.writeFileSync('apitest.json', JSON.stringify(JSON.parse(searchItems.res.body), null, 2));*/ - - //new api testing - /*if (this.api == 'new') { - await this.doInit(); - const apiTest = await this.apiReq('/v4/season/18871', '', 'auth', 'GET'); + /*const apiTest = await this.apiReq('/v4/season/18871', '', 'auth', 'GET'); if(!apiTest.ok || !apiTest.res){return;} console.info(apiTest.res.body); fs.writeFileSync('apitest.json', JSON.stringify(JSON.parse(apiTest.res.body), null, 2)); - return console.info('test done'); - }*/ + return console.info('test done');*/ // load binaries this.cfg.bin = await yamlCfg.loadBinCfg(); @@ -106,42 +69,21 @@ export default class Hidive implements ServiceClass { argv.dubLang = langsData.dubLanguageCodes; } if (argv.auth) { - //Initilize session - await this.doInit(); //Authenticate await this.doAuth({ username: argv.username ?? await shlp.question('[Q] LOGIN/EMAIL'), password: argv.password ?? await shlp.question('[Q] PASSWORD ') }); } else if (argv.search && argv.search.length > 2){ - //Initilize session - await this.doInit(); - //Search await this.doSearch({ ...argv, search: argv.search as string }); } else if (argv.s && !isNaN(parseInt(argv.s,10)) && parseInt(argv.s,10) > 0) { - if (this.api == 'old') { - //Initilize session - await this.doInit(); - //get selected episodes - const selected = await this.getShow(parseInt(argv.s), argv.e, argv.but, argv.all); - if (selected.isOk && selected.showData) { - for (const select of selected.value) { - //download episode - if (!(await this.getEpisode(select, {...argv}))) { - console.error(`Unable to download selected episode ${parseFloat(select.EpisodeNumberValue+'')}`); - return false; - } - } - } - } else { - const selected = await this.selectSeason(parseInt(argv.s), argv.e, argv.but, argv.all); - if (selected.isOk && selected.showData) { - for (const select of selected.value) { - //download episode - if (!(await this.downloadEpisode(select, {...argv}))) { - console.error(`Unable to download selected episode ${select.episodeInformation.episodeNumber}`); - return false; - } + const selected = await this.selectSeason(parseInt(argv.s), argv.e, argv.but, argv.all); + if (selected.isOk && selected.showData) { + for (const select of selected.value) { + //download episode + if (!(await this.downloadEpisode(select, {...argv}))) { + console.error(`Unable to download selected episode ${select.episodeInformation.episodeNumber}`); + return false; } } } @@ -158,209 +100,17 @@ export default class Hidive implements ServiceClass { } } } else if (argv.new) { - if (this.api == 'old') { - //Initilize session - await this.doInit(); - //Get Newly Added - await this.getNewlyAdded(argv.page); - } else { - console.error('--new is not yet implemented in the new API'); - } + console.error('--new is not yet implemented in the new API'); } else if(argv.e) { - if (this.api == 'new') { - if (!(await this.downloadSingleEpisode(parseInt(argv.e), {...argv}))) { - console.error(`Unable to download selected episode ${argv.e}`); - return false; - } - } else { - console.error('-e is not supported in the old API'); + if (!(await this.downloadSingleEpisode(parseInt(argv.e), {...argv}))) { + console.error(`Unable to download selected episode ${argv.e}`); + return false; } } else { console.info('No option selected or invalid value entered. Try --help.'); } } - public async doInit() { - if (this.api == 'old') { - //get client ip - const newIp = await this.reqData('Ping', ''); - if (!newIp.ok || !newIp.res) return false; - this.client.ipAddress = JSON.parse(newIp.res.body).IPAddress; - //get device id - const newDevice = await this.reqData('InitDevice', { 'DeviceName': api.hd_devName }); - if (!newDevice.ok || !newDevice.res) return false; - this.client.profile = Object.assign(this.client.profile, { - deviceId: JSON.parse(newDevice.res.body).Data.DeviceId, - }); - //get visit id - const newVisitId = await this.reqData('InitVisit', {}); - if (!newVisitId.ok || !newVisitId.res) return false; - this.client.visitId = JSON.parse(newVisitId.res.body).Data.VisitId; - //save client - yamlCfg.saveHDProfile(this.client); - return true; - } else { - //this.refreshToken(); - return true; - } - } - - // Generate Nonce - public generateNonce(){ - const initDate = new Date(); - const nonceDate = [ - initDate.getUTCFullYear().toString().slice(-2), // yy - ('0'+(initDate.getUTCMonth()+1)).slice(-2), // MM - ('0'+initDate.getUTCDate()).slice(-2), // dd - ('0'+initDate.getUTCHours()).slice(-2), // HH - ('0'+initDate.getUTCMinutes()).slice(-2) // mm - ].join(''); // => "yyMMddHHmm" (UTC) - const nonceCleanStr = nonceDate + api.hd_apikey; - const nonceHash = crypto.createHash('sha256').update(nonceCleanStr).digest('hex'); - return nonceHash; - } - - // Generate Signature - public generateSignature(body: string|object, visitId: string, profile: Record) { - const sigCleanStr = [ - this.client.ipAddress, - api.hd_appId, - profile.deviceId, - visitId, - profile.userId, - profile.profileId, - body, - this.client.xNonce, - api.hd_apikey, - ].join(''); - return crypto.createHash('sha256').update(sigCleanStr).digest('hex'); - } - - public makeCookieList(data: Record, keys: Array) { - const res = []; - for (const key of keys) { - if (typeof data[key] !== 'object') continue; - res.push(`${key}=${data[key].value}`); - } - return res.join('; '); - } - - public async reqData(method: string, body: string | object, type = 'POST') { - const options = { - headers: {} as Record, - method: type as 'GET'|'POST', - url: '' as string, - body: body, - }; - // get request type - const isGet = type == 'GET' ? true : false; - // set request type, url, user agent, referrer, and origin - options.method = isGet ? 'GET' : 'POST'; - options.url = ( !isGet ? domain.hd_api + '/api/v1/' : '') + method; - options.headers['user-agent'] = isGet ? api.hd_clientExo : api.hd_clientWeb; - options.headers['referrer'] = 'https://www.hidive.com/'; - options.headers['origin'] = 'https://www.hidive.com'; - // set api data - if(!isGet){ - options.body = body == '' ? body : JSON.stringify(body); - // set api headers - if(method != 'Ping'){ - const visitId = this.client.visitId ? this.client.visitId : ''; - const vprofile = { - userId: this.client.profile.userId || 0, - profileId: this.client.profile.profileId || 0, - deviceId: this.client.profile.deviceId || '', - }; - this.client.xNonce = this.generateNonce(); - this.client.xSignature = this.generateSignature(options.body, visitId, vprofile); - options.headers = Object.assign(options.headers, { - 'X-VisitId' : visitId, - 'X-UserId' : vprofile.userId, - 'X-ProfileId' : vprofile.profileId, - 'X-DeviceId' : vprofile.deviceId, - 'X-Nonce' : this.client.xNonce, - 'X-Signature' : this.client.xSignature, - }); - } - options.headers = Object.assign({ - 'Content-Type' : 'application/x-www-form-urlencoded; charset=UTF-8', - 'X-ApplicationId': api.hd_appId, - }, options.headers); - // cookies - const cookiesList = Object.keys(this.session); - if(cookiesList.length > 0 && method != 'Ping') { - options.headers.Cookie = this.makeCookieList(this.session, cookiesList); - } - } else if(isGet && !options.url.match(/\?/)){ - this.client.xNonce = this.generateNonce(); - this.client.xSignature = this.generateSignature(options.body, this.client.visitId, this.client.profile); - options.url = options.url + '?' + (new URLSearchParams({ - 'X-ApplicationId': api.hd_appId, - 'X-DeviceId': this.client.profile.deviceId, - 'X-VisitId': this.client.visitId, - 'X-UserId': this.client.profile.userId+'', - 'X-ProfileId': this.client.profile.profileId+'', - 'X-Nonce': this.client.xNonce, - 'X-Signature': this.client.xSignature, - })).toString(); - } - try { - if (this.debug) { - console.debug('[DEBUG] Request params:'); - console.debug(options); - } - const apiReqOpts: reqModule.Params = { - method: options.method, - headers: options.headers as Record, - body: options.body as string - }; - const apiReq = await this.req.getData(options.url, apiReqOpts); - if(!apiReq.ok || !apiReq.res){ - console.error('API Request Failed!'); - return { - ok: false, - res: apiReq.res, - }; - } - - if (!isGet && apiReq.res.headers && apiReq.res.headers['set-cookie']) { - const newReqCookies = shlp.cookie.parse(apiReq.res.headers['set-cookie'] as unknown as Record); - this.session = Object.assign(this.session, newReqCookies); - yamlCfg.saveHDSession(this.session); - } - if (!isGet) { - const resJ = JSON.parse(apiReq.res.body); - if (resJ.Code > 0) { - console.error(`Code ${resJ.Code} (${resJ.Status}): ${resJ.Message}\n`); - if (resJ.Code == 81 || resJ.Code == 5) { - console.info('[NOTE] App was broken because of changes in official app.'); - console.info('[NOTE] See: https://github.com/anidl/hidive-downloader-nx/issues/1\n'); - } - if (resJ.Code == 55) { - console.info('[NOTE] You need premium account to view this video.'); - } - return { - ok: false, - res: apiReq.res, - }; - } - } - return { - ok: true, - res: apiReq.res, - }; - } catch (error: any) { - if (error.statusCode && error.statusMessage) { - console.error(`\n ${error.name} ${error.statusCode}: ${error.statusMessage}\n`); - } else { - console.error(`\n ${error.name}: ${error.code}\n`); - } - return { - ok: false, - error, - }; - } - } public async apiReq(endpoint: string, body: string | object = '', authType: 'refresh' | 'auth' | 'both' | 'other' | 'none' = 'none', method: 'GET' | 'POST' = 'POST', authHeader?: string) { const options = { @@ -447,43 +197,25 @@ export default class Hidive implements ServiceClass { } public async doAuth(data: AuthData): Promise { - if (this.api == 'old') { - const auth = await this.reqData('Authenticate', {'Email':data.username,'Password':data.password}); - if(!auth.ok || !auth.res) { - console.error('Authentication failed!'); - return { isOk: false, reason: new Error('Authentication failed') }; - } - const authData = JSON.parse(auth.res.body).Data; - this.client.profile = Object.assign(this.client.profile, { - userId: authData.User.Id, - profileId: authData.Profiles[0].Id, - }); - yamlCfg.saveHDProfile(this.client); - yamlCfg.saveHDToken(authData); - console.info('Auth complete!'); - console.info(`Service level for "${data.username}" is ${authData.User.ServiceLevel}`); - return { isOk: true, value: undefined }; - } else { - if (!this.token.refreshToken || !this.token.authorisationToken) { - await this.doAnonymousAuth(); - } - const authReq = await this.apiReq('/v2/login', { - id: data.username, - secret: data.password - }, 'auth'); - if(!authReq.ok || !authReq.res){ - console.error('Authentication failed!'); - return { isOk: false, reason: new Error('Authentication failed') }; - } - const tokens: Record = JSON.parse(authReq.res.body); - for (const token in tokens) { - this.token[token] = tokens[token]; - } - this.token.guest = false; - yamlCfg.saveNewHDToken(this.token); - console.info('Auth complete!'); - return { isOk: true, value: undefined }; + if (!this.token.refreshToken || !this.token.authorisationToken) { + await this.doAnonymousAuth(); } + const authReq = await this.apiReq('/v2/login', { + id: data.username, + secret: data.password + }, 'auth'); + if(!authReq.ok || !authReq.res){ + console.error('Authentication failed!'); + return { isOk: false, reason: new Error('Authentication failed') }; + } + const tokens: Record = JSON.parse(authReq.res.body); + for (const token in tokens) { + this.token[token] = tokens[token]; + } + this.token.guest = false; + yamlCfg.saveNewHDToken(this.token); + console.info('Auth complete!'); + return { isOk: true, value: undefined }; } public async doAnonymousAuth() { @@ -540,110 +272,48 @@ export default class Hidive implements ServiceClass { return true; } - public async genSubsUrl(type: string, file: string) { - return [ - `${domain.hd_api}/caption/${type}/`, - ( type == 'css' ? '?id=' : '' ), - `${file}.${type}` - ].join(''); - } - public async doSearch(data: SearchData): Promise { - if (this.api == 'old') { - const searchReq = await this.reqData('Search', {'Query':data.search}); - if(!searchReq.ok || !searchReq.res){ - console.error('Search FAILED!'); - return { isOk: false, reason: new Error('Search failed. No more information provided') }; - } - const searchData = JSON.parse(searchReq.res.body) as HidiveSearch; - const searchItems = searchData.Data.TitleResults; - if(searchItems.length>0) { - console.info('[INFO] Search Results:'); - for(let i=0;i { - return { - id: a.Id+'', - image: a.KeyArtUrl ?? '/notFound.png', - name: a.Name, - rating: a.OverallRating, - desc: a.LongSynopsis - }; - })}; - } else { - const searchReq = await this.req.getData('https://h99xldr8mj-dsn.algolia.net/1/indexes/*/queries?x-algolia-agent=Algolia%20for%20JavaScript%20(3.35.1)%3B%20Browser&x-algolia-application-id=H99XLDR8MJ&x-algolia-api-key=e55ccb3db0399eabe2bfc37a0314c346', { - method: 'POST', - body: JSON.stringify({'requests': - [ - {'indexName':'prod-dce.hidive-livestreaming-events','params':'query='+encodeURIComponent(data.search)+'&facetFilters=%5B%22type%3ALIVE_EVENT%22%5D&hitsPerPage=25'+(data.page ? '&page='+(data.page-1) : '')}, - {'indexName':'prod-dce.hidive-livestreaming-events','params':'query='+encodeURIComponent(data.search)+'&facetFilters=%5B%22type%3AVOD_VIDEO%22%5D&hitsPerPage=25'+(data.page ? '&page='+(data.page-1) : '')}, - {'indexName':'prod-dce.hidive-livestreaming-events','params':'query='+encodeURIComponent(data.search)+'&facetFilters=%5B%22type%3AVOD_PLAYLIST%22%5D&hitsPerPage=25'+(data.page ? '&page='+(data.page-1) : '')}, - {'indexName':'prod-dce.hidive-livestreaming-events','params':'query='+encodeURIComponent(data.search)+'&facetFilters=%5B%22type%3AVOD_SERIES%22%5D&hitsPerPage=25'+(data.page ? '&page='+(data.page-1) : '')} - ] - }) - }); - if(!searchReq.ok || !searchReq.res){ - console.error('Search FAILED!'); - return { isOk: false, reason: new Error('Search failed. No more information provided') }; - } - const searchData = JSON.parse(searchReq.res.body) as NewHidiveSearch; - const searchItems: Hit[] = []; - console.info('Search Results:'); - for (const category of searchData.results) { - for (const hit of category.hits) { - searchItems.push(hit); - let fullType: string; - if (hit.type == 'VOD_SERIES') { - fullType = `Z.${hit.id}`; - } else if (hit.type == 'VOD_VIDEO') { - fullType = `E.${hit.id}`; - } else { - fullType = `${hit.type} #${hit.id}`; - } - console.log(`[${fullType}] ${hit.name} ${hit.seasonsCount ? '('+hit.seasonsCount+' Seasons)' : ''}`); - } - } - return { isOk: true, value: searchItems.filter(a => a.type == 'VOD_SERIES').flatMap((a): SearchResponseItem => { - return { - id: a.id+'', - image: a.coverUrl ?? '/notFound.png', - name: a.name, - rating: -1, - desc: a.description - }; - })}; + const searchReq = await this.req.getData('https://h99xldr8mj-dsn.algolia.net/1/indexes/*/queries?x-algolia-agent=Algolia%20for%20JavaScript%20(3.35.1)%3B%20Browser&x-algolia-application-id=H99XLDR8MJ&x-algolia-api-key=e55ccb3db0399eabe2bfc37a0314c346', { + method: 'POST', + body: JSON.stringify({'requests': + [ + {'indexName':'prod-dce.hidive-livestreaming-events','params':'query='+encodeURIComponent(data.search)+'&facetFilters=%5B%22type%3ALIVE_EVENT%22%5D&hitsPerPage=25'+(data.page ? '&page='+(data.page-1) : '')}, + {'indexName':'prod-dce.hidive-livestreaming-events','params':'query='+encodeURIComponent(data.search)+'&facetFilters=%5B%22type%3AVOD_VIDEO%22%5D&hitsPerPage=25'+(data.page ? '&page='+(data.page-1) : '')}, + {'indexName':'prod-dce.hidive-livestreaming-events','params':'query='+encodeURIComponent(data.search)+'&facetFilters=%5B%22type%3AVOD_PLAYLIST%22%5D&hitsPerPage=25'+(data.page ? '&page='+(data.page-1) : '')}, + {'indexName':'prod-dce.hidive-livestreaming-events','params':'query='+encodeURIComponent(data.search)+'&facetFilters=%5B%22type%3AVOD_SERIES%22%5D&hitsPerPage=25'+(data.page ? '&page='+(data.page-1) : '')} + ] + }) + }); + if(!searchReq.ok || !searchReq.res){ + console.error('Search FAILED!'); + return { isOk: false, reason: new Error('Search failed. No more information provided') }; } - } - - public async getNewlyAdded(page?: number) { - const pageNum = page ? page : 1; - const dashboardReq = await this.reqData('GetDashboard', {'Pager': {'Number': pageNum, 'Size': 30}, 'Verbose': false}); - if(!dashboardReq.ok || !dashboardReq.res) { - console.error('Search for new episodes FAILED!'); - return; - } - - const dashboardData = JSON.parse(dashboardReq.res.body) as HidiveDashboard; - const dashboardItems = dashboardData.Data.TitleRows; - const recentlyAddedIndex = dashboardItems.findIndex(item => item.Name == 'Recently Added'); - const recentlyAdded = recentlyAddedIndex >= 0 ? dashboardItems[recentlyAddedIndex] : undefined; - if (recentlyAdded) { - const searchItems = recentlyAdded?.Titles; - if(searchItems.length>0) { - console.info('[INFO] Recently Added:'); - for(let i=0;i a.type == 'VOD_SERIES').flatMap((a): SearchResponseItem => { + return { + id: a.id+'', + image: a.coverUrl ?? '/notFound.png', + name: a.name, + rating: -1, + desc: a.description + }; + })}; } public async getSeries(id: number) { @@ -827,152 +497,6 @@ export default class Hidive implements ServiceClass { return { isOk: true, value: selEpsArr, showData: getShowData.series }; } - public async listShow(id: number) { - const getShowData = await this.reqData('GetTitle', { 'Id': id }); - if (!getShowData.ok || !getShowData.res) { - console.error('Failed to get show data'); - return { isOk: false }; - } - const rawShowData = JSON.parse(getShowData.res.body) as HidiveEpisodeList; - const showData = rawShowData.Data.Title; - console.info(`[#${showData.Id}] ${showData.Name} [${showData.ShowInfoTitle}]`); - return { isOk: true, value: showData }; - } - - async getShow(id: number, e: string | undefined, but: boolean, all: boolean) { - const getShowData = await this.listShow(id); - if (!getShowData.isOk || !getShowData.value) { - return { isOk: false, value: [] }; - } - const showData = getShowData.value; - const doEpsFilter = parseSelect(e as string); - // build selected episodes - const selEpsArr: HidiveEpisodeExtra[] = []; let ovaSeq = 1; let movieSeq = 1; - for (let i = 0; i < showData.Episodes.length; i++) { - const titleId = showData.Episodes[i].TitleId; - const epKey = showData.Episodes[i].VideoKey; - const seriesTitle = showData.Name; - let nameLong = showData.Episodes[i].DisplayNameLong; - if (nameLong.match(/OVA/i)) { - nameLong = 'ova' + (('0' + ovaSeq).slice(-2)); ovaSeq++; - } - else if (nameLong.match(/Theatrical/i)) { - nameLong = 'movie' + (('0' + movieSeq).slice(-2)); movieSeq++; - } - else { - nameLong = epKey; - } - let sumDub: string | RegExpMatchArray | null = showData.Episodes[i].Summary.match(/^Audio: (.*)/m); - sumDub = sumDub ? `\n - ${sumDub[0]}` : ''; - let sumSub: string | RegExpMatchArray | null = showData.Episodes[i].Summary.match(/^Subtitles: (.*)/m); - sumSub = sumSub ? `\n - ${sumSub[0]}` : ''; - let selMark = ''; - if (all || - but && !doEpsFilter.isSelected([parseFloat(showData.Episodes[i].EpisodeNumberValue+'')+'', showData.Episodes[i].Id+'']) || - !but && doEpsFilter.isSelected([parseFloat(showData.Episodes[i].EpisodeNumberValue+'')+'', showData.Episodes[i].Id+'']) - ) { - selEpsArr.push({ isSelected: true, titleId, epKey, nameLong, seriesTitle, ...showData.Episodes[i] }); - selMark = '✓ '; - } - //const epKeyTitle = !epKey.match(/e(\d+)$/) ? nameLong : epKey; - //const titleIdStr = (titleId != id ? `#${titleId}|` : '') + epKeyTitle; - //console.info(`[${titleIdStr}] ${showData.Episodes[i].Name}${selMark}${sumDub}${sumSub}`); - console.info('%s[%s] %s%s%s', - selMark, - 'S'+parseFloat(showData.Episodes[i].SeasonNumberValue+'')+'E'+parseFloat(showData.Episodes[i].EpisodeNumberValue+''), - showData.Episodes[i].Name, - sumDub, - sumSub - ); - } - return { isOk: true, value: selEpsArr, showData: showData }; - } - - public async getEpisode(selectedEpisode: HidiveEpisodeExtra, options: Record) { - const getVideoData = await this.reqData('GetVideos', { 'VideoKey': selectedEpisode.epKey, 'TitleId': selectedEpisode.titleId }); - if (getVideoData.ok && getVideoData.res) { - const videoData = JSON.parse(getVideoData.res.body) as HidiveVideoList; - const showTitle = `${selectedEpisode.seriesTitle} S${parseFloat(selectedEpisode.SeasonNumberValue+'')}`; - console.info(`[INFO] ${showTitle} - ${parseFloat(selectedEpisode.EpisodeNumberValue+'')}`); - const videoList = videoData.Data.VideoLanguages; - const subsList = videoData.Data.CaptionLanguages; - console.info('[INFO] Available dubs and subtitles:'); - console.info('\tVideos: ' + videoList.join('\n\t\t')); - console.info('\tSubs : ' + subsList.join('\n\t\t')); - console.info(`[INFO] Selected dub(s): ${options.dubLang.join(', ')}`); - const videoUrls = videoData.Data.VideoUrls; - const subsUrls = videoData.Data.CaptionVttUrls; - const fontSize = videoData.Data.FontSize ? videoData.Data.FontSize : options.fontSize; - const subsSel = subsList; - //Get Selected Video URLs - const videoSel = videoList.sort().filter(videoLanguage => - langsData.languages.find(a => - a.hd_locale ? videoLanguage.match(a.hd_locale) && - options.dubLang.includes(a.code) : false - ) - ); - //Prioritize Home Video, unless simul is used - videoSel.forEach(function(video, index) { - if (index > 0) { - const video1 = video.split(', '); - const video2 = videoSel[index - 1].split(', '); - if (video1[0] == video2[0]) { - if (video1[1] == 'Home Video' && video2[1] == 'Broadcast') { - options.simul ? videoSel.splice(index, 1) : videoSel.splice(index - 1, 1); - } - } - } - }); - if (videoSel.length === 0) { - console.error('No suitable videos(s) found for options!'); - } - //Build video array - const selectedVideoUrls: HidiveStreamInfo[] = []; - videoSel.forEach(function(video, index) { - const videodetails = videoSel[index].split(', '); - const videoinfo: HidiveStreamInfo = videoUrls[video]; - videoinfo.language = videodetails[0]; - videoinfo.episodeTitle = selectedEpisode.Name; - videoinfo.seriesTitle = selectedEpisode.seriesTitle; - videoinfo.season = parseFloat(selectedEpisode.SeasonNumberValue+''); - videoinfo.episodeNumber = parseFloat(selectedEpisode.EpisodeNumberValue+''); - videoinfo.uncut = videodetails[0] == 'Home Video' ? true : false; - videoinfo.image = selectedEpisode.ScreenShotSmallUrl; - console.info(`[INFO] Selected release: ${videodetails[0]} ${videodetails[1]}`); - selectedVideoUrls.push(videoinfo); - }); - //Build subtitle array - const selectedSubUrls: HidiveSubtitleInfo[] = []; - subsSel.forEach(function(sub, index) { - console.info(subsSel[index]); - const subinfo = { - url: subsUrls[sub], - cc: subsSel[index].includes('Caps'), - language: subsSel[index].replace(' Subs', '').replace(' Caps', '') - }; - selectedSubUrls.push(subinfo); - }); - //download media list - const res = await this.downloadMediaList(selectedVideoUrls, selectedSubUrls, fontSize, options); - if (res === undefined || res.error) { - console.error('Failed to download media list'); - return { isOk: false, reason: new Error('Failed to download media list') }; - } else { - if (!options.skipmux) { - await this.muxStreams(res.data, { ...options, output: res.fileName }); - } else { - console.info('Skipping mux'); - } - downloaded({ - service: 'hidive', - type: 's' - }, selectedEpisode.titleId+'', [selectedEpisode.EpisodeNumberValue+'']); - return { isOk: res, value: undefined }; - } - } - return { isOk: false, reason: new Error('Unknown download error') }; - } - public async downloadEpisode(selectedEpisode: NewHidiveEpisodeExtra, options: Record) { //Get Episode data const episodeDataReq = await this.apiReq(`/v4/vod/${selectedEpisode.id}?includePlaybackDetails=URL`, '', 'auth', 'GET'); @@ -1454,260 +978,6 @@ export default class Hidive implements ServiceClass { }; } - public async downloadMediaList(videoUrls: HidiveStreamInfo[], subUrls: HidiveSubtitleInfo[], fontSize: number, options: Record) { - let mediaName = '...'; - let fileName; - const files: DownloadedMedia[] = []; - const variables: Variable[] = []; - let dlFailed = false; - //let dlVideoOnce = false; // Variable to save if best selected video quality was downloaded - let subsMargin = 0; - let videoIndex = 0; - const chosenFontSize = options.originalFontSize ? fontSize : options.fontSize; - for (const videoData of videoUrls) { - if(videoData.seriesTitle && videoData.episodeNumber && videoData.episodeTitle){ - mediaName = `${videoData.seriesTitle} - ${videoData.episodeNumber} - ${videoData.episodeTitle}`; - } - if(!options.novids && !dlFailed) { - console.info(`Requesting: ${mediaName}`); - console.info('Playlists URL: %s', videoData.hls[0]); - const streamPlaylistsReq = await this.req.getData(videoData.hls[0]); - if(!streamPlaylistsReq.ok || !streamPlaylistsReq.res){ - console.error('CAN\'T FETCH VIDEO PLAYLISTS!'); - return { error: true, data: []}; - } - - variables.push(...([ - ['title', videoData.episodeTitle, true], - ['episode', isNaN(parseFloat(videoData.episodeNumber+'')) ? videoData.episodeNumber : parseFloat(videoData.episodeNumber+''), false], - ['service', 'HD', false], - ['showTitle', videoData.seriesTitle, true], - ['season', videoData.season, false] - ] as [AvailableFilenameVars, string|number, boolean][]).map((a): Variable => { - return { - name: a[0], - replaceWith: a[1], - type: typeof a[1], - sanitize: a[2] - } as Variable; - })); - - const streamPlaylists = m3u8(streamPlaylistsReq.res.body); - const plServerList: string[] = [], - plStreams: Record> = {}, - plQuality: { - str: string, - dim: string, - CODECS: 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}`; - // set codecs - const plCodecs = pl.attributes.CODECS; - // parse uri - const plUri = new URL(pl.uri); - let plServer = plUri.hostname; - // set server list - if (plUri.searchParams.get('cdn')) { - plServer += ` (${plUri.searchParams.get('cdn')})`; - } - if (!plServerList.includes(plServer)) { - plServerList.push(plServer); - } - // add to server - if (!Object.keys(plStreams).includes(plServer)) { - plStreams[plServer] = {}; - } - if ( - plStreams[plServer][plResolutionText] - && plStreams[plServer][plResolutionText] != pl.uri - && typeof plStreams[plServer][plResolutionText] != 'undefined' - ) { - console.error(`Non duplicate url for ${plServer} detected, please report to developer!`); - } - else { - plStreams[plServer][plResolutionText] = pl.uri; - } - // set plQualityStr - const plBandwidth = Math.round(pl.attributes.BANDWIDTH / 1024); - const qualityStrAdd = `${plResolutionText} (${plBandwidth}KiB/s)`; - const qualityStrRegx = new RegExp(qualityStrAdd.replace(/(:|\(|\)|\/)/g, '\\$1'), 'm'); - const qualityStrMatch = !plQuality.map(a => a.str).join('\r\n').match(qualityStrRegx); - if (qualityStrMatch) { - plQuality.push({ - str: qualityStrAdd, - dim: plResolutionText, - CODECS: plCodecs, - RESOLUTION: plResolution - }); - } - } - - options.x = options.x > plServerList.length ? 1 : options.x; - - const plSelectedServer = plServerList[options.x - 1]; - const plSelectedList = plStreams[plSelectedServer]; - plQuality.sort((a, b) => { - const aMatch: RegExpMatchArray | never[] = a.dim.match(/[0-9]+/) || []; - const bMatch: RegExpMatchArray | never[] = b.dim.match(/[0-9]+/) || []; - return parseInt(aMatch[0]) - parseInt(bMatch[0]); - }); - let quality = options.q === 0 ? plQuality.length : options.q; - if(quality > plQuality.length) { - console.warn(`The requested quality of ${options.q} is greater than the maximun ${plQuality.length}.\n[WARN] Therefor the maximum will be capped at ${plQuality.length}.`); - quality = plQuality.length; - } - const selPlUrl = plSelectedList[plQuality.map(a => a.dim)[quality - 1]] ? plSelectedList[plQuality.map(a => a.dim)[quality - 1]] : ''; - console.info(`Servers available:\n\t${plServerList.join('\n\t')}`); - console.info(`Available qualities:\n\t${plQuality.map((a, ind) => `[${ind+1}] ${a.str}`).join('\n\t')}`); - if(selPlUrl != '') { - variables.push({ - name: 'height', - type: 'number', - replaceWith: quality === 0 ? plQuality[plQuality.length - 1].RESOLUTION.height as number : plQuality[quality - 1].RESOLUTION.height - }, { - name: 'width', - type: 'number', - replaceWith: quality === 0 ? plQuality[plQuality.length - 1].RESOLUTION.width as number : plQuality[quality - 1].RESOLUTION.width - }); - } - const lang = langsData.languages.find(a => a.hd_locale === videoData.language); - if (!lang) { - console.error(`Unable to find language for code ${videoData.language}`); - return { error: true, data: [] }; - } - console.info(`Selected quality: ${Object.keys(plSelectedList).find(a => plSelectedList[a] === selPlUrl)} @ ${plSelectedServer}`); - console.info('Stream URL:', selPlUrl); - // TODO check filename - const outFile = parseFileName(options.fileName + '.' + lang.name + '.' + videoIndex, variables, options.numbers, options.override).join(path.sep); - fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); - console.info(`Output filename: ${outFile}`); - const chunkPage = await this.req.getData(selPlUrl); - if(!chunkPage.ok || !chunkPage.res){ - console.error('CAN\'T FETCH VIDEO PLAYLIST!'); - dlFailed = true; - } else { - const chunkPlaylist = m3u8(chunkPage.res.body); - //TODO: look into how to keep bumpers without the video being affected - if(chunkPlaylist.segments[0].uri.match(/\/bumpers\//) && options.removeBumpers){ - subsMargin = chunkPlaylist.segments[0].duration; - chunkPlaylist.segments.splice(0, 1); - } - const totalParts = chunkPlaylist.segments.length; - const mathParts = Math.ceil(totalParts / options.partsize); - const mathMsg = `(${mathParts}*${options.partsize})`; - console.info('Total parts in stream:', totalParts, mathMsg); - const tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); - const split = outFile.split(path.sep).slice(0, -1); - split.forEach((val, ind, arr) => { - const isAbsolut = path.isAbsolute(outFile as string); - if (!fs.existsSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val))) - fs.mkdirSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val)); - }); - const dlStreamByPl = await new streamdl({ - output: `${tsFile}.ts`, - timeout: options.timeout, - m3u8json: chunkPlaylist, - // baseurl: chunkPlaylist.baseUrl, - threads: options.partsize, - fsRetryTime: options.fsRetryTime * 1000, - override: options.force, - callback: options.callbackMaker ? options.callbackMaker({ - fileName: `${path.isAbsolute(outFile) ? outFile.slice(this.cfg.dir.content.length) : outFile}`, - image: videoData.image, - parent: { - title: videoData.seriesTitle - }, - title: videoData.episodeTitle, - language: lang - }) : undefined - }).download(); - if (!dlStreamByPl.ok) { - console.error(`DL Stats: ${JSON.stringify(dlStreamByPl.parts)}\n`); - dlFailed = true; - } - files.push({ - type: 'Video', - path: `${tsFile}.ts`, - lang: lang, - uncut: videoData.uncut - }); - //dlVideoOnce = true; - } - } else if(options.novids){ - fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); - console.info('Downloading skipped!'); - } - videoIndex++; - await this.sleep(options.waittime); - } - - if(options.dlsubs.indexOf('all') > -1){ - options.dlsubs = ['all']; - } - - if (options.nosubs) { - console.info('Subtitles downloading disabled from nosubs flag.'); - options.skipsubs = true; - } - - if(!options.skipsubs && options.dlsubs.indexOf('none') == -1) { - if(subUrls.length > 0) { - let subIndex = 0; - for(const sub of subUrls) { - const subLang = langsData.languages.find(a => a.hd_locale === sub.language); - if (!subLang) { - console.warn(`Language not found for subtitle language: ${sub.language}, Skipping`); - continue; - } - const sxData: Partial = {}; - sxData.file = langsData.subsFile(fileName as string, subIndex+'', subLang, sub.cc, options.ccTag); - sxData.path = path.join(this.cfg.dir.content, sxData.file); - sxData.language = subLang; - if(options.dlsubs.includes('all') || options.dlsubs.includes(subLang.locale)){ - const subs4XUrl = sub.url.split('/'); - const subsXUrl = subs4XUrl[subs4XUrl.length - 1].replace(/.vtt$/, ''); - const getCssContent = await this.req.getData(await this.genSubsUrl('css', subsXUrl)); - const getVttContent = await this.req.getData(await this.genSubsUrl('vtt', subsXUrl)); - if (getCssContent.ok && getVttContent.ok && getCssContent.res && getVttContent.res) { - console.info(`Subtitle Downloaded: ${await this.genSubsUrl('vtt', subsXUrl)}`); - //vttConvert(getVttContent.res.body, false, subLang.name, fontSize); - const sBody = vtt2ass(undefined, chosenFontSize, getVttContent.res.body, getCssContent.res.body, subsMargin, options.fontName, options.combineLines); - sxData.title = `${subLang.language} / ${sxData.title}`; - sxData.fonts = fontsData.assFonts(sBody) as Font[]; - fs.writeFileSync(sxData.path, sBody); - console.info(`Subtitle Converted: ${sxData.file}`); - files.push({ - type: 'Subtitle', - ...sxData as sxItem, - cc: sub.cc - }); - } else{ - console.warn(`Failed to download subtitle: ${sxData.file}`); - } - } - subIndex++; - } - } else{ - console.warn('Can\'t find urls for subtitles!'); - } - } else{ - console.info('Subtitles downloading skipped!'); - } - - return { - error: dlFailed, - data: files, - fileName: fileName ? (path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName)) || './unknown' : './unknown' - }; - } - public async muxStreams(data: DownloadedMedia[], options: Record, inverseTrackOrder: boolean = true) { this.cfg.bin = await yamlCfg.loadBinCfg(); let hasAudioStreams = false; diff --git a/modules/module.app-args.ts b/modules/module.app-args.ts index 00014c8..214e2e9 100644 --- a/modules/module.app-args.ts +++ b/modules/module.app-args.ts @@ -69,7 +69,6 @@ let argvC: { dlVideoOnce: boolean; chapters: boolean; crapi: 'android' | 'web'; - hdapi: 'old' | 'new'; removeBumpers: boolean; originalFontSize: boolean; keepAllVideos: boolean; diff --git a/modules/module.args.ts b/modules/module.args.ts index 0507576..7ffe506 100644 --- a/modules/module.args.ts +++ b/modules/module.args.ts @@ -230,20 +230,6 @@ const args: TAppArg[] = [ default: 'web' } }, - { - name: 'hdapi', - describe: 'Selects the API type for Hidive', - type: 'string', - group: 'dl', - service: ['hidive'], - docDescribe: 'If set to Old, it has lower quality, but Non-DRM streams, but some people can\'t use it,' - + '\nIf set to New, it has a higher quality stream, but everything is DRM.', - usage: '', - choices: ['old', 'new'], - default: { - default: 'new' - } - }, { name: 'removeBumpers', describe: 'Remove bumpers from final video', From 0a3b638c556803700af7f35437b4f15b3f70df8a Mon Sep 17 00:00:00 2001 From: AnimeDL Date: Sat, 6 Apr 2024 21:20:34 -0700 Subject: [PATCH 2/6] Start of Removing Funimation --- .github/ISSUE_TEMPLATE/bug.yml | 1 - @types/funiSearch.d.ts | 34 - @types/funiSubtitleRequest.d.ts | 75 -- @types/funiTypes.d.ts | 16 - @types/ws.d.ts | 4 +- docs/README.md | 4 +- funi.ts | 923 ------------------- gui/react/src/components/MenuBar/MenuBar.tsx | 2 - gui/react/src/provider/ServiceProvider.tsx | 3 +- gui/react/src/provider/Store.tsx | 2 +- gui/server/serviceHandler.ts | 7 +- gui/server/services/funimation.ts | 120 --- index.ts | 18 +- modules/build-docs.ts | 7 +- modules/module.app-args.ts | 2 +- modules/module.args.ts | 20 +- modules/module.cfg-loader.ts | 31 - modules/module.downloadArchive.ts | 22 +- modules/module.langsData.ts | 15 +- modules/module.req.ts | 4 +- package.json | 4 +- 21 files changed, 33 insertions(+), 1281 deletions(-) delete mode 100644 @types/funiSearch.d.ts delete mode 100644 @types/funiSubtitleRequest.d.ts delete mode 100644 @types/funiTypes.d.ts delete mode 100644 funi.ts delete mode 100644 gui/server/services/funimation.ts diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 5077179..2301e91 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -47,7 +47,6 @@ body: label: Service description: "Please tell us what service the bug occured in." options: - - Funimation - Crunchyroll - Hidive - All diff --git a/@types/funiSearch.d.ts b/@types/funiSearch.d.ts deleted file mode 100644 index 730985e..0000000 --- a/@types/funiSearch.d.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Generated by https://quicktype.io - -export interface FunimationSearch { - count: number; - items: Items; - limit: string; - offset: string; -} - -export interface Items { - hits: Hit[]; -} - -export interface Hit { - ratings: string; - description: string; - title: string; - image: { - showThumbnail: string, - [key: string]: string - }; - starRating: number; - slug: string; - languages: string[]; - synopsis: string; - quality: Quality; - id: string; - txDate: number; -} - -export interface Quality { - quality: string; - height: number; -} diff --git a/@types/funiSubtitleRequest.d.ts b/@types/funiSubtitleRequest.d.ts deleted file mode 100644 index e4b6490..0000000 --- a/@types/funiSubtitleRequest.d.ts +++ /dev/null @@ -1,75 +0,0 @@ -// Generated by https://quicktype.io - -export interface SubtitleRequest { - primary: Primary; - fallback: Primary[]; -} - -export interface Primary { - venueVideoId: string; - alphaPackageId: string; - versionContentId: VersionContentID; - manifestPath: string; - fileExt: PrimaryFileEXT; - subtitles: Subtitle[]; - accessType: AccessType; - sessionId: string; - audioLanguage: AudioLanguage; - version: Version; - aips: Aip[]; - drmToken: string; - drmType: string; -} - -export enum AccessType { - Subscription = 'subscription', -} - -export interface Aip { - in: number; - out: number; -} - -export enum AudioLanguage { - En = 'en', - Ja = 'ja', -} - -export enum PrimaryFileEXT { - M3U8 = 'm3u8', - Mp4 = 'mp4', -} - -export interface Subtitle { - filePath: string; - fileExt: SubtitleFileEXT; - contentType: ContentType; - languageCode: LanguageCode; -} - -export enum ContentType { - Cc = 'cc', - Full = 'full', -} - -export enum SubtitleFileEXT { - Dfxp = 'dfxp', - Srt = 'srt', - Vtt = 'vtt', -} - -export enum LanguageCode { - En = 'en', - Es = 'es', - Pt = 'pt', -} - -export enum Version { - Simulcast = 'simulcast', - Uncut = 'uncut', -} - -export enum VersionContentID { - Akusim0012 = 'AKUSIM0012', - Akuunc0012 = 'AKUUNC0012', -} diff --git a/@types/funiTypes.d.ts b/@types/funiTypes.d.ts deleted file mode 100644 index 5880011..0000000 --- a/@types/funiTypes.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { LanguageItem } from '../modules/module.langsData'; - -export type FunimationMediaDownload = { - id: string, - title: string, - showTitle: string, - image: string -} - -export type Subtitle = { - url: string, - lang: LanguageItem, - ext: string, - out?: string, - closedCaption?: boolean -} \ No newline at end of file diff --git a/@types/ws.d.ts b/@types/ws.d.ts index fab66b8..7f9b00c 100644 --- a/@types/ws.d.ts +++ b/@types/ws.d.ts @@ -30,8 +30,8 @@ export type MessageTypes = { 'isDownloading': [undefined, boolean], 'openFolder': [FolderTypes, undefined], 'changeProvider': [undefined, boolean], - 'type': [undefined, 'funi'|'crunchy'|'hidive'|undefined], - 'setup': ['funi'|'crunchy'|'hidive'|undefined, undefined], + 'type': [undefined, 'crunchy'|'hidive'|undefined], + 'setup': ['crunchy'|'hidive'|undefined, undefined], 'openFile': [[FolderTypes, string], undefined], 'openURL': [string, undefined], 'isSetup': [undefined, boolean], diff --git a/docs/README.md b/docs/README.md index dab0de0..8437606 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,11 +2,11 @@ [![Discord Shield](https://discord.com/api/guilds/884479461997805568/widget.png?style=banner2)](https://discord.gg/qEpbWen5vq) -This downloader can download anime from different sites. Currently supported are *Funimation*, *Crunchyroll*, and *Hidive*. +This downloader can download anime from different sites. Currently supported are *Crunchyroll*, and *Hidive*. ## Legal Warning -This application is not endorsed by or affiliated with *Funimation*, *Crunchyroll*, or *Hidive*. This application enables you to download videos for offline viewing which may be forbidden by law in your country. The usage of this application may also cause a violation of the *Terms of Service* between you and the stream provider. This tool is not responsible for your actions; please make an informed decision before using this application. +This application is not endorsed by or affiliated with *Crunchyroll*, or *Hidive*. This application enables you to download videos for offline viewing which may be forbidden by law in your country. The usage of this application may also cause a violation of the *Terms of Service* between you and the stream provider. This tool is not responsible for your actions; please make an informed decision before using this application. ## Dependencies diff --git a/funi.ts b/funi.ts deleted file mode 100644 index 6c3d314..0000000 --- a/funi.ts +++ /dev/null @@ -1,923 +0,0 @@ -// modules build-in -import fs from 'fs'; -import path from 'path'; - -// package json -import packageJson from './package.json'; - -// modules extra -import { console } from './modules/log'; -import * as shlp from 'sei-helper'; -import m3u8 from 'm3u8-parsed'; -import hlsDownload, { HLSCallback } from './modules/hls-download'; - -// extra -import * as appYargs from './modules/module.app-args'; -import * as yamlCfg from './modules/module.cfg-loader'; -import vttConvert from './modules/module.vttconvert'; - -// types -import type { Item } from './@types/items.js'; - -// params - -// Import modules after argv has been exported -import getData from './modules/module.getdata'; -import merger from './modules/module.merger'; -import parseSelect from './modules/module.parseSelect'; -import { EpisodeData, MediaChild } from './@types/episode'; -import { Subtitle } from './@types/funiTypes'; -import { StreamData } from './@types/streamData'; -import { DownloadedFile } from './@types/downloadedFile'; -import parseFileName, { Variable } from './modules/module.filename'; -import { downloaded } from './modules/module.downloadArchive'; -import { FunimationMediaDownload } from './@types/funiTypes'; -import * as langsData from './modules/module.langsData'; -import { TitleElement } from './@types/episode'; -import { AvailableFilenameVars } from './modules/module.args'; -import { AuthData, AuthResponse, CheckTokenResponse, FuniGetEpisodeData, FuniGetEpisodeResponse, FuniGetShowData, SearchData, FuniSearchReponse, FuniShowResponse, FuniStreamData, FuniSubsData, FuniEpisodeData, ResponseBase } from './@types/messageHandler'; -import { ServiceClass } from './@types/serviceClassInterface'; -import { SubtitleRequest } from './@types/funiSubtitleRequest'; - -// program name -const api_host = 'https://prod-api-funimationnow.dadcdigital.com/api'; -// check page - -// fn variables -let fnEpNum: string|number = 0, - fnOutput: string[] = [], - season = 0, - tsDlPath: { - path: string, - lang: langsData.LanguageItem - }[] = [], - stDlPath: Subtitle[] = []; - -export default class Funi implements ServiceClass { - public static epIdLen = 4; - public static typeIdLen = 0; - - public cfg: yamlCfg.ConfigObject; - private token: string | boolean; - - constructor(private debug = false) { - this.cfg = yamlCfg.loadCfg(); - this.token = yamlCfg.loadFuniToken(); - } - - public checkToken(): CheckTokenResponse { - const isOk = typeof this.token === 'string'; - return isOk ? { isOk, value: undefined } : { isOk, reason: new Error('Not authenticated') }; - } - - public async cli() : Promise { - const argv = appYargs.appArgv(this.cfg.cli); - if (argv.debug) - this.debug = true; - console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`); - if (argv.allDubs) { - argv.dubLang = langsData.dubLanguageCodes; - } - // select mode - if (argv.silentAuth && !argv.auth) { - const data: AuthData = { - username: argv.username ?? await shlp.question('[Q] LOGIN/EMAIL'), - password: argv.password ?? await shlp.question('[Q] PASSWORD ') - }; - await this.auth(data); - } - if(argv.auth){ - const data: AuthData = { - username: argv.username ?? await shlp.question('[Q] LOGIN/EMAIL'), - password: argv.password ?? await shlp.question('[Q] PASSWORD ') - }; - await this.auth(data); - } - else if(argv.search){ - this.searchShow(true, { search: argv.search }); - } - else if(argv.s && !isNaN(parseInt(argv.s)) && parseInt(argv.s) > 0){ - const data = await this.getShow(true, { id: parseInt(argv.s), but: argv.but, all: argv.all, e: argv.e }); - if (!data.isOk) { - console.error(`${data.reason.message}`); - return false; - } - let ok = true; - for (const episodeData of data.value) { - if ((await this.getEpisode(true, { subs: { dlsubs: argv.dlsubs, nosubs: argv.nosubs, sub: false, ccTag: argv.ccTag }, dubLang: argv.dubLang, fnSlug: episodeData, s: argv.s, simul: argv.simul }, { - ass: false, - ...argv - })).isOk !== true) - ok = false; - } - return ok; - } - else{ - console.info('No option selected or invalid value entered. Try --help.'); - } - } - public async auth(data: AuthData): Promise { - const authOpts = { - user: data.username, - pass: data.password - }; - const authData = await getData({ - baseUrl: api_host, - url: '/auth/login/', - auth: authOpts, - debug: this.debug, - }); - if(authData.ok && authData.res){ - const resJSON = JSON.parse(authData.res.body); - if(resJSON.token){ - console.info('Authentication success, your token: %s%s\n', resJSON.token.slice(0,8),'*'.repeat(32)); - yamlCfg.saveFuniToken({'token': resJSON.token}); - this.token = resJSON.token; - return { isOk: true, value: undefined }; - } else { - console.info('[ERROR]%s\n', ' No token found'); - if (this.debug) { - console.info(resJSON); - } - return { isOk: false, reason: new Error(resJSON) }; - } - } - return { isOk: false, reason: new Error('Login request failed') }; - } - - public async searchShow(log: boolean, data: SearchData): Promise { - const qs = {unique: true, limit: 100, q: data.search, offset: 0 }; - const searchData = await getData({ - baseUrl: api_host, - url: '/source/funimation/search/auto/', - querystring: qs, - token: this.token, - useToken: true, - debug: this.debug, - }); - if(!searchData.ok || !searchData.res){ - return { isOk: false, reason: new Error('Request is not ok') }; - } - const searchDataJSON = JSON.parse(searchData.res.body); - if(searchDataJSON.detail){ - console.error(`${searchDataJSON.detail}`); - return { isOk: false, reason: new Error(searchDataJSON.defail) }; - } - if(searchDataJSON.items && searchDataJSON.items.hits && log){ - const shows = searchDataJSON.items.hits; - console.info('Search Results:'); - for(const ssn in shows){ - console.info(`[#${shows[ssn].id}] ${shows[ssn].title}` + (shows[ssn].tx_date?` (${shows[ssn].tx_date})`:'')); - } - } - if (log) - console.info('Total shows found: %s\n',searchDataJSON.count); - return { isOk: true, value: searchDataJSON }; - } - - public async listShowItems(id: number) : Promise> { - const showData = await getData({ - baseUrl: api_host, - url: `/source/catalog/title/${id}`, - token: this.token, - useToken: true, - debug: this.debug, - }); - // check errors - if(!showData.ok || !showData.res){ return { isOk: false, reason: new Error('ShowData is not ok') }; } - const showDataJSON = JSON.parse(showData.res.body); - if(showDataJSON.status){ - console.error('Error #%d: %s\n', showDataJSON.status, showDataJSON.data.errors[0].detail); - return { isOk: false, reason: new Error(showDataJSON.data.errors[0].detail) }; - } - else if(!showDataJSON.items || showDataJSON.items.length<1){ - console.error('Show not found\n'); - return { isOk: false, reason: new Error('Show not found') }; - } - const showDataItem = showDataJSON.items[0]; - console.info('[#%s] %s (%s)',showDataItem.id,showDataItem.title,showDataItem.releaseYear); - // show episodes - const qs: { - limit: number, - sort: string, - sort_direction: string, - title_id: number, - language?: string - } = { limit: -1, sort: 'order', sort_direction: 'ASC', title_id: id }; - const episodesData = await getData({ - baseUrl: api_host, - url: '/funimation/episodes/', - querystring: qs, - token: this.token, - useToken: true, - debug: this.debug, - }); - if(!episodesData.ok || !episodesData.res){ return { isOk: false, reason: new Error('episodesData is not ok') }; } - - let epsDataArr: Item[] = JSON.parse(episodesData.res.body).items; - const epNumRegex = /^([A-Z0-9]*[A-Z])?(\d+)$/i; - - const parseEpStr = (epStr: string) => { - const match = epStr.match(epNumRegex); - if (!match) { - console.error('No match found'); - return ['', '']; - } - if(match.length > 2){ - const spliced = [...match].splice(1); - spliced[0] = spliced[0] ? spliced[0] : ''; - return spliced; - } - else return [ '', match[0] ]; - }; - - epsDataArr = epsDataArr.map(e => { - const baseId = e.ids.externalAsianId ? e.ids.externalAsianId : e.ids.externalEpisodeId; - e.id = baseId.replace(new RegExp('^' + e.ids.externalShowId), ''); - if(e.id.match(epNumRegex)){ - const epMatch = parseEpStr(e.id); - Funi.epIdLen = epMatch[1].length > Funi.epIdLen ? epMatch[1].length : Funi.epIdLen; - Funi.typeIdLen = epMatch[0].length > Funi.typeIdLen ? epMatch[0].length : Funi.typeIdLen; - e.id_split = epMatch; - } - else{ - Funi.typeIdLen = 3 > Funi.typeIdLen? 3 : Funi.typeIdLen; - console.error('FAILED TO PARSE: ', e.id); - e.id_split = [ 'ZZZ', 9999 ]; - } - return e; - }); - - epsDataArr.sort((a, b) => { - if (a.item.seasonOrder < b.item.seasonOrder && a.id.localeCompare(b.id) < 0) { - return -1; - } - if (a.item.seasonOrder > b.item.seasonOrder && a.id.localeCompare(b.id) > 0) { - return 1; - } - return 0; - }); - - return { isOk: true, value: epsDataArr }; - } - - public async getShow(log: boolean, data: FuniGetShowData) : Promise { - const showList = await this.listShowItems(data.id); - if (!showList.isOk) - return showList; - const eps = showList.value; - const epSelList = parseSelect(data.e as string, data.but); - const fnSlug: FuniEpisodeData[] = [], epSelEpsTxt: string[] = []; let is_selected = false; - - - for(const e in eps){ - eps[e].id_split[1] = parseInt(eps[e].id_split[1].toString()).toString().padStart(Funi.epIdLen, '0'); - let epStrId = eps[e].id_split.join(''); - // select - is_selected = false; - if (data.all || epSelList.isSelected(epStrId)) { - fnSlug.push({ - title:eps[e].item.titleSlug, - episode:eps[e].item.episodeSlug, - episodeID:epStrId, - epsiodeNumber: eps[e].item.episodeNum, - seasonTitle: eps[e].item.seasonTitle, - seasonNumber: eps[e].item.seasonNum, - ids: { - episode: eps[e].ids.externalEpisodeId, - season: eps[e].ids.externalSeasonId, - show: eps[e].ids.externalShowId - }, - image: eps[e].item.poster - }); - epSelEpsTxt.push(epStrId); - is_selected = true; - } - // console vars - const tx_snum = eps[e].item.seasonNum=='1'?'':` S${eps[e].item.seasonNum}`; - const tx_type = eps[e].mediaCategory != 'episode' ? eps[e].mediaCategory : ''; - const tx_enum = eps[e].item.episodeNum && eps[e].item.episodeNum !== '' ? - `#${(parseInt(eps[e].item.episodeNum) < 10 ? '0' : '')+eps[e].item.episodeNum}` : '#'+eps[e].item.episodeId; - const qua_str = eps[e].quality.height ? eps[e].quality.quality + eps[e].quality.height : 'UNK'; - const aud_str = eps[e].audio.length > 0 ? `, ${eps[e].audio.join(', ')}` : ''; - const rtm_str = eps[e].item.runtime !== '' ? eps[e].item.runtime : '??:??'; - // console string - eps[e].id_split[0] = eps[e].id_split[0].toString().padStart(Funi.typeIdLen, ' '); - epStrId = eps[e].id_split.join(''); - let conOut = `[${epStrId}] `; - conOut += `${eps[e].item.titleName+tx_snum} - ${tx_type+tx_enum} ${eps[e].item.episodeName} `; - conOut += `(${rtm_str}) [${qua_str+aud_str}]`; - conOut += is_selected ? ' (selected)' : ''; - conOut += eps.length-1 == parseInt(e) ? '\n' : ''; - console.info(conOut); - } - if(fnSlug.length < 1){ - if (log) - console.info('Episodes not selected!\n'); - return { isOk: true, value: [] } ; - } - else{ - if (log) - console.info('Selected Episodes: %s\n',epSelEpsTxt.join(', ')); - return { isOk: true, value: fnSlug }; - } - } - - public async getEpisode(log: boolean, data: FuniGetEpisodeData, downloadData: FuniStreamData) : Promise { - const episodeData = await getData({ - baseUrl: api_host, - url: `/source/catalog/episode/${data.fnSlug.title}/${data.fnSlug.episode}/`, - token: this.token, - useToken: true, - debug: this.debug, - }); - if(!episodeData.ok || !episodeData.res){return { isOk: false, reason: new Error('Unable to get episodeData') }; } - const ep = JSON.parse(episodeData.res.body).items[0] as EpisodeData, streamIds: { id: number, lang: langsData.LanguageItem }[] = []; - // build fn - season = parseInt(ep.parent.seasonNumber); - if(ep.mediaCategory != 'Episode'){ - ep.number = ep.number !== '' ? ep.mediaCategory+ep.number : ep.mediaCategory+'#'+ep.id; - } - fnEpNum = isNaN(parseInt(ep.number)) ? ep.number : parseInt(ep.number); - - // is uncut - const uncut = { - Japanese: false, - English: false - }; - - // end - if (log) { - console.info( - '%s - S%sE%s - %s', - ep.parent.title, - (ep.parent.seasonNumber ? ep.parent.seasonNumber : '?'), - (ep.number ? ep.number : '?'), - ep.title - ); - - console.info('Available streams (Non-Encrypted):'); - } - // map medias - const media = await Promise.all(ep.media.map(async (m) =>{ - if(m.mediaType == 'experience'){ - if(m.version.match(/uncut/i) && m.language){ - uncut[m.language] = true; - } - return { - id: m.id, - language: m.language, - version: m.version, - type: m.experienceType, - subtitles: await this.getSubsUrl(m.mediaChildren, m.language, data.subs, ep.ids.externalEpisodeId, data.subs.ccTag) - }; - } - else{ - return { id: 0, type: '' }; - } - })); - - // select - stDlPath = []; - for(const m of media){ - let selected = false; - if(m.id > 0 && m.type == 'Non-Encrypted'){ - const dub_type = m.language; - if (!dub_type) - continue; - let localSubs: Subtitle[] = []; - const selUncut = !data.simul && uncut[dub_type] && m.version?.match(/uncut/i) - ? true - : (!uncut[dub_type] || data.simul && m.version?.match(/simulcast/i) ? true : false); - for (const curDub of data.dubLang) { - const item = langsData.languages.find(a => a.code === curDub); - if(item && (dub_type === item.funi_name_lagacy || dub_type === (item.funi_name ?? item.name)) && selUncut){ - streamIds.push({ - id: m.id, - lang: item - }); - stDlPath.push(...m.subtitles); - localSubs = m.subtitles; - selected = true; - } - } - if (log) { - const subsToDisplay: langsData.LanguageItem[] = []; - localSubs.forEach(a => { - if (!subsToDisplay.includes(a.lang)) - subsToDisplay.push(a.lang); - }); - console.info(`[#${m.id}] ${dub_type} [${m.version}]${(selected?' (selected)':'')}${ - localSubs && localSubs.length > 0 && selected ? ` (using ${subsToDisplay.map(a => `'${a.name}'`).join(', ')} for subtitles)` : '' - }`); - } - } - } - - const already: string[] = []; - stDlPath = stDlPath.filter(a => { - if (already.includes(`${a.closedCaption ? 'cc' : ''}-${a.lang.code}`)) { - return false; - } else { - already.push(`${a.closedCaption ? 'cc' : ''}-${a.lang.code}`); - return true; - } - }); - if(streamIds.length < 1){ - if (log) - console.error('Track not selected\n'); - return { isOk: false, reason: new Error('Track not selected') }; - } - else{ - tsDlPath = []; - for (const streamId of streamIds) { - const streamData = await getData({ - baseUrl: api_host, - url: `/source/catalog/video/${streamId.id}/signed`, - token: this.token, - dinstid: 'uuid', - useToken: true, - debug: this.debug, - }); - if(!streamData.ok || !streamData.res){return { isOk: false, reason: new Error('Unable to get streamdata') };} - const streamDataRes = JSON.parse(streamData.res.body) as StreamData; - if(streamDataRes.errors){ - if (log) - console.info('Error #%s: %s\n',streamDataRes.errors[0].code,streamDataRes.errors[0].detail); - return { isOk: false, reason: new Error(streamDataRes.errors[0].detail) }; - } - else{ - for(const u in streamDataRes.items){ - if(streamDataRes.items[u].videoType == 'm3u8'){ - tsDlPath.push({ - path: streamDataRes.items[u].src, - lang: streamId.lang - }); - break; - } - } - } - } - if(tsDlPath.length < 1){ - if (log) - console.error('Unknown error\n'); - return { isOk: false, reason: new Error('Unknown error') }; - } - else{ - const res = await this.downloadStreams(true, { - id: data.fnSlug.episodeID, - title: ep.title, - showTitle: ep.parent.title, - image: ep.thumb - }, downloadData); - if (res === true) { - downloaded({ - service: 'funi', - type: 's' - }, data.s, [data.fnSlug.episodeID]); - return { isOk: res, value: undefined }; - } - return { isOk: false, reason: new Error('Unknown download error') }; - } - } - } - - public async downloadStreams(log: boolean, episode: FunimationMediaDownload, data: FuniStreamData): Promise { - - // req playlist - - const purvideo: DownloadedFile[] = []; - const puraudio: DownloadedFile[] = []; - const audioAndVideo: DownloadedFile[] = []; - for (const streamPath of tsDlPath) { - const plQualityReq = await getData({ - url: streamPath.path, - debug: this.debug, - }); - if(!plQualityReq.ok || !plQualityReq.res){return;} - - const plQualityLinkList = m3u8(plQualityReq.res.body); - - const mainServersList = [ - 'vmfst-api.prd.funimationsvc.com', - 'd33et77evd9bgg.cloudfront.net', - 'd132fumi6di1wa.cloudfront.net', - 'funiprod.akamaized.net', - ]; - - const plServerList: string[] = [], - plStreams: Record = {}, - plLayersStr: string[] = [], - plLayersRes: Record = {}; - let plMaxLayer = 1, - plNewIds = 1, - plAud: undefined|{ - uri: string - language: langsData.LanguageItem - }; - - // new uris - const vplReg = /streaming_video_(\d+)_(\d+)_(\d+)_index\.m3u8/; - if(plQualityLinkList.playlists[0].uri.match(vplReg)){ - const audioKey = Object.keys(plQualityLinkList.mediaGroups.AUDIO).pop(); - if (!audioKey) - return console.error('No audio key found'); - if(plQualityLinkList.mediaGroups.AUDIO[audioKey]){ - const audioDataParts = plQualityLinkList.mediaGroups.AUDIO[audioKey], - audioEl = Object.keys(audioDataParts); - const audioData = audioDataParts[audioEl[0]]; - let language = langsData.languages.find(a => a.code === audioData.language || a.locale === audioData.language); - if (!language) { - language = langsData.languages.find(a => a.funi_name_lagacy === audioEl[0] || ((a.funi_name ?? a.name) === audioEl[0])); - if (!language) { - if (log) - console.error(`Unable to find language for locale ${audioData.language} or name ${audioEl[0]}`); - return; - } - } - plAud = { - uri: audioData.uri, - language: language - }; - } - plQualityLinkList.playlists.sort((a, b) => { - const aMatch = a.uri.match(vplReg), bMatch = b.uri.match(vplReg); - if (!aMatch || !bMatch) { - console.info('Unable to match'); - return 0; - } - const av = parseInt(aMatch[3]); - const bv = parseInt(bMatch[3]); - if(av > bv){ - return 1; - } - if (av < bv) { - return -1; - } - return 0; - }); - } - - for(const s of plQualityLinkList.playlists){ - if(s.uri.match(/_Layer(\d+)\.m3u8/) || s.uri.match(vplReg)){ - // set layer and max layer - let plLayerId: number|string = 0; - const match = s.uri.match(/_Layer(\d+)\.m3u8/); - if(match){ - plLayerId = parseInt(match[1]); - } - else{ - plLayerId = plNewIds, plNewIds++; - } - plMaxLayer = plMaxLayer < plLayerId ? plLayerId : plMaxLayer; - // set urls and servers - const plUrlDl = s.uri; - const plServer = new URL(plUrlDl).host; - if(!plServerList.includes(plServer)){ - plServerList.push(plServer); - } - if(!Object.keys(plStreams).includes(plServer)){ - plStreams[plServer] = {}; - } - if(plStreams[plServer][plLayerId] && plStreams[plServer][plLayerId] != plUrlDl){ - console.warn(`Non duplicate url for ${plServer} detected, please report to developer!`); - } - else{ - plStreams[plServer][plLayerId] = plUrlDl; - } - // set plLayersStr - const plResolution = s.attributes.RESOLUTION; - plLayersRes[plLayerId] = plResolution; - const plBandwidth = Math.round(s.attributes.BANDWIDTH/1024); - if(plLayerId<10){ - plLayerId = plLayerId.toString().padStart(2,' '); - } - const qualityStrAdd = `${plLayerId}: ${plResolution.width}x${plResolution.height} (${plBandwidth}KiB/s)`; - const qualityStrRegx = new RegExp(qualityStrAdd.replace(/(:|\(|\)|\/)/g,'\\$1'),'m'); - const qualityStrMatch = !plLayersStr.join('\r\n').match(qualityStrRegx); - if(qualityStrMatch){ - plLayersStr.push(qualityStrAdd); - } - } - else { - console.info(s.uri); - } - } - - for(const s of mainServersList){ - if(plServerList.includes(s)){ - plServerList.splice(plServerList.indexOf(s), 1); - plServerList.unshift(s); - break; - } - } - - - const plSelectedServer = plServerList[data.x-1]; - const plSelectedList = plStreams[plSelectedServer]; - - plLayersStr.sort(); - if (log) { - console.info(`Servers available:\n\t${plServerList.join('\n\t')}`); - console.info(`Available qualities:\n\t${plLayersStr.join('\n\t')}`); - } - - const selectedQuality = data.q === 0 || data.q > Object.keys(plLayersRes).length - ? Object.keys(plLayersRes).pop() as string - : data.q; - const videoUrl = data.x < plServerList.length+1 && plSelectedList[selectedQuality] ? plSelectedList[selectedQuality] : ''; - - if(videoUrl != ''){ - if (log) { - console.info(`Selected layer: ${selectedQuality} (${plLayersRes[selectedQuality].width}x${plLayersRes[selectedQuality].height}) @ ${plSelectedServer}`); - console.info('Stream URL:',videoUrl); - } - - fnOutput = parseFileName(data.fileName, ([ - ['episode', isNaN(parseInt(fnEpNum as string)) ? fnEpNum : parseInt(fnEpNum as string), true], - ['title', episode.title, true], - ['showTitle', episode.showTitle, true], - ['season', season, false], - ['width', plLayersRes[selectedQuality].width, false], - ['height', plLayersRes[selectedQuality].height, false], - ['service', 'Funimation', false] - ] as [AvailableFilenameVars, string|number, boolean][]).map((a): Variable => { - return { - name: a[0], - replaceWith: a[1], - type: typeof a[1], - sanitize: a[2] - } as Variable; - }), data.numbers, data.override); - if (fnOutput.length < 1) - throw new Error(`Invalid path generated for input ${data.fileName}`); - if (log) - console.info(`Output filename: ${fnOutput.join(path.sep)}.ts`); - } - else if(data.x > plServerList.length){ - if (log) - console.error('Server not selected!\n'); - return; - } - else{ - if (log) - console.error('Layer not selected!\n'); - return; - } - - let dlFailed = false; - let dlFailedA = false; - - await fs.promises.mkdir(path.join(this.cfg.dir.content, ...fnOutput.slice(0, -1)), { recursive: true }); - - video: if (!data.novids) { - if (plAud && (purvideo.length > 0 || audioAndVideo.length > 0)) { - break video; - } else if (!plAud && (audioAndVideo.some(a => a.lang === streamPath.lang) || puraudio.some(a => a.lang === streamPath.lang))) { - break video; - } - // download video - const reqVideo = await getData({ - url: videoUrl, - debug: this.debug, - }); - if (!reqVideo.ok || !reqVideo.res) { break video; } - - const chunkList = m3u8(reqVideo.res.body); - - const tsFile = path.join(this.cfg.dir.content, ...fnOutput.slice(0, -1), `${fnOutput.slice(-1)}.video${(plAud?.uri ? '' : '.' + streamPath.lang.code )}`); - dlFailed = !await this.downloadFile(tsFile, chunkList, data.timeout, data.partsize, data.fsRetryTime, data.force, data.callbackMaker ? data.callbackMaker({ - fileName: `${fnOutput.slice(-1)}.video${(plAud?.uri ? '' : '.' + streamPath.lang.code )}.ts`, - parent: { - title: episode.showTitle - }, - title: episode.title, - image: episode.image, - language: streamPath.lang, - }) : undefined); - if (!dlFailed) { - if (plAud) { - purvideo.push({ - path: `${tsFile}.ts`, - lang: plAud.language - }); - } else { - audioAndVideo.push({ - path: `${tsFile}.ts`, - lang: streamPath.lang - }); - } - } - } - else{ - if (log) - console.info('Skip video downloading...\n'); - } - audio: if (plAud && !data.noaudio) { - // download audio - if (audioAndVideo.some(a => a.lang === plAud?.language) || puraudio.some(a => a.lang === plAud?.language)) - break audio; - const reqAudio = await getData({ - url: plAud.uri, - debug: this.debug, - }); - if (!reqAudio.ok || !reqAudio.res) { return; } - - const chunkListA = m3u8(reqAudio.res.body); - - const tsFileA = path.join(this.cfg.dir.content, ...fnOutput.slice(0, -1), `${fnOutput.slice(-1)}.audio.${plAud.language.code}`); - - dlFailedA = !await this.downloadFile(tsFileA, chunkListA, data.timeout, data.partsize, data.fsRetryTime, data.force, data.callbackMaker ? data.callbackMaker({ - fileName: `${fnOutput.slice(-1)}.audio.${plAud.language.code}.ts`, - parent: { - title: episode.showTitle - }, - title: episode.title, - image: episode.image, - language: plAud.language - }) : undefined); - if (!dlFailedA) - puraudio.push({ - path: `${tsFileA}.ts`, - lang: plAud.language - }); - - } - } - - // add subs - const subsExt = !data.mp4 || data.mp4 && data.ass ? '.ass' : '.srt'; - let addSubs = true; - - // download subtitles - if(stDlPath.length > 0){ - if (log) - console.info('Downloading subtitles...'); - for (const subObject of stDlPath) { - const subsSrc = await getData({ - url: subObject.url, - debug: this.debug, - }); - if(subsSrc.ok && subsSrc.res){ - const assData = vttConvert(subsSrc.res.body, (subsExt == '.srt' ? true : false), subObject.lang.name, data.fontSize, data.fontName); - subObject.out = path.join(this.cfg.dir.content, ...fnOutput.slice(0, -1), `${fnOutput.slice(-1)}.subtitle${subObject.ext}${subsExt}`); - fs.writeFileSync(subObject.out, assData); - } - else{ - if (log) - console.error('Failed to download subtitles!'); - addSubs = false; - break; - } - } - if (addSubs && log) - console.info('Subtitles downloaded!'); - } - - if((puraudio.length < 1 && audioAndVideo.length < 1) || (purvideo.length < 1 && audioAndVideo.length < 1)){ - if (log) - console.info('\nUnable to locate a video AND audio file\n'); - return; - } - - if(data.skipmux){ - if (log) - console.info('Skipping muxing...'); - return; - } - - // check exec - this.cfg.bin = await yamlCfg.loadBinCfg(); - const mergerBin = merger.checkMerger(this.cfg.bin, data.mp4, data.forceMuxer); - - if ( data.novids ){ - if (log) - console.info('Video not downloaded. Skip muxing video.'); - } - - const ffext = !data.mp4 ? 'mkv' : 'mp4'; - const mergeInstance = new merger({ - onlyAudio: puraudio, - onlyVid: purvideo, - output: `${path.join(this.cfg.dir.content, ...fnOutput)}.${ffext}`, - subtitles: stDlPath.map(a => { - return { - file: a.out as string, - language: a.lang, - title: a.lang.name, - closedCaption: a.closedCaption - }; - }), - videoAndAudio: audioAndVideo, - simul: data.simul, - skipSubMux: data.skipSubMux, - videoTitle: data.videoTitle, - options: { - ffmpeg: data.ffmpegOptions, - mkvmerge: data.mkvmergeOptions - }, - defaults: { - audio: data.defaultAudio, - sub: data.defaultSub - }, - ccTag: data.ccTag - }); - - if(mergerBin.MKVmerge){ - await mergeInstance.merge('mkvmerge', mergerBin.MKVmerge); - } - else if(mergerBin.FFmpeg){ - await mergeInstance.merge('ffmpeg', mergerBin.FFmpeg); - } - else{ - if (log) - console.info('\nDone!\n'); - return true; - } - if (data.nocleanup) { - return true; - } - - mergeInstance.cleanUp(); - if (log) - console.info('\nDone!\n'); - return true; - } - - public async downloadFile(filename: string, chunkList: { - segments: Record[], - }, timeout: number, partsize: number, fsRetryTime: number, override?: 'Y' | 'y' | 'N' | 'n' | 'C' | 'c', callback?: HLSCallback) { - const downloadStatus = await new hlsDownload({ - m3u8json: chunkList, - output: `${filename + '.ts'}`, - timeout: timeout, - threads: partsize, - fsRetryTime: fsRetryTime * 1000, - override, - callback - }).download(); - - return downloadStatus.ok; - } - - public async getSubsUrl(m: MediaChild[], parentLanguage: TitleElement|undefined, data: FuniSubsData, episodeID: string, ccTag: string) : Promise { - if((data.nosubs && !data.sub) || data.dlsubs.includes('none')){ - return []; - } - - const subs = await getData({ - baseUrl: 'https://playback.prd.funimationsvc.com/v1/play', - url: `/${episodeID}`, - token: this.token, - useToken: true, - debug: this.debug, - querystring: { deviceType: 'web' } - }); - if (!subs.ok || !subs.res || !subs.res.body) { - console.error('Subtitle Request failed.'); - return []; - } - const parsed: SubtitleRequest = JSON.parse(subs.res.body); - - const found: { - isCC: boolean; - url: string; - lang: langsData.LanguageItem; - }[] = parsed.primary.subtitles.filter(a => a.fileExt === 'vtt').map(subtitle => { - return { - isCC: subtitle.contentType === 'cc', - url: subtitle.filePath, - lang: langsData.languages.find(a => a.funi_locale === subtitle.languageCode || a.locale === subtitle.languageCode) - }; - }).concat(m.filter(a => a.filePath.split('.').pop() === 'vtt').map(media => { - const lang = langsData.languages.find(a => media.language === a.funi_name_lagacy || media.language === (a.funi_name || a.name)); - const pLang = langsData.languages.find(a => parentLanguage === a.funi_name_lagacy || (a.funi_name || a.name) === parentLanguage); - return { - isCC: pLang?.code === lang?.code, - url: media.filePath, - lang - }; - })).filter((a) => a.lang !== undefined) as { - isCC: boolean; - url: string; - lang: langsData.LanguageItem; - }[]; - - const ret = found.filter(item => { - return data.dlsubs.includes('all') || data.dlsubs.some(a => a === item.lang.locale); - }); - - return ret.map(a => ({ - ext: `.${a.lang.code}${a.isCC ? `.${ccTag}` : ''}`, - lang: a.lang, - url: a.url, - closedCaption: a.isCC - })); - } - -} diff --git a/gui/react/src/components/MenuBar/MenuBar.tsx b/gui/react/src/components/MenuBar/MenuBar.tsx index 23f5bb6..4edb3ac 100644 --- a/gui/react/src/components/MenuBar/MenuBar.tsx +++ b/gui/react/src/components/MenuBar/MenuBar.tsx @@ -23,8 +23,6 @@ const MenuBar: React.FC = () => { switch(service) { case 'crunchy': return 'Crunchyroll'; - case 'funi': - return 'Funimation'; case 'hidive': return 'Hidive'; } diff --git a/gui/react/src/provider/ServiceProvider.tsx b/gui/react/src/provider/ServiceProvider.tsx index eb770c5..9013407 100644 --- a/gui/react/src/provider/ServiceProvider.tsx +++ b/gui/react/src/provider/ServiceProvider.tsx @@ -3,7 +3,7 @@ import {Divider, Box, Button, Typography, Avatar} from '@mui/material'; import useStore from '../hooks/useStore'; import { StoreState } from './Store'; -type Services = 'funi'|'crunchy'|'hidive'; +type Services = 'crunchy'|'hidive'; export const serviceContext = React.createContext(undefined); @@ -21,7 +21,6 @@ const ServiceProvider: FCWithChildren = ({ children }) => { Please select your service - diff --git a/gui/react/src/provider/Store.tsx b/gui/react/src/provider/Store.tsx index c82ef92..eecb51c 100644 --- a/gui/react/src/provider/Store.tsx +++ b/gui/react/src/provider/Store.tsx @@ -21,7 +21,7 @@ export type DownloadOptions = { export type StoreState = { episodeListing: Episode[]; downloadOptions: DownloadOptions, - service: 'crunchy'|'funi'|'hidive'|undefined, + service: 'crunchy'|'hidive'|undefined, version: string, } diff --git a/gui/server/serviceHandler.ts b/gui/server/serviceHandler.ts index 9e8e365..87cdb92 100644 --- a/gui/server/serviceHandler.ts +++ b/gui/server/serviceHandler.ts @@ -4,7 +4,6 @@ import { IncomingMessage } from 'http'; import { MessageHandler, GuiState } from '../../@types/messageHandler'; import { setState, getState, writeYamlCfgFile } from '../../modules/module.cfg-loader'; import CrunchyHandler from './services/crunchyroll'; -import FunimationHandler from './services/funimation'; import HidiveHandler from './services/hidive'; import WebSocketHandler from './websocket'; import packageJson from '../../package.json'; @@ -31,9 +30,7 @@ export default class ServiceHandler { }); this.ws.events.on('setup', ({ data }) => { - if (data === 'funi') { - this.service = new FunimationHandler(this.ws); - } else if (data === 'crunchy') { + if (data === 'crunchy') { this.service = new CrunchyHandler(this.ws); } else if (data === 'hidive') { this.service = new HidiveHandler(this.ws); @@ -55,7 +52,7 @@ export default class ServiceHandler { this.ws.events.on('version', async (_, respond) => { respond(packageJson.version); }); - this.ws.events.on('type', async (_, respond) => respond(this.service === undefined ? undefined : this.service.name as 'hidive'|'crunchy'|'funi')); + this.ws.events.on('type', async (_, respond) => respond(this.service === undefined ? undefined : this.service.name as 'hidive'|'crunchy')); this.ws.events.on('checkToken', async (_, respond) => { if (this.service === undefined) return respond({ isOk: false, reason: new Error('No service selected') }); diff --git a/gui/server/services/funimation.ts b/gui/server/services/funimation.ts deleted file mode 100644 index 3bfc4eb..0000000 --- a/gui/server/services/funimation.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { AuthData, CheckTokenResponse, EpisodeListResponse, MessageHandler, QueueItem, ResolveItemsData, SearchData, SearchResponse } from '../../../@types/messageHandler'; -import Funimation from '../../../funi'; -import { getDefault } from '../../../modules/module.args'; -import { languages, subtitleLanguagesFilter } from '../../../modules/module.langsData'; -import WebSocketHandler from '../websocket'; -import Base from './base'; -import { console } from '../../../modules/log'; -import * as yargs from '../../../modules/module.app-args'; - -class FunimationHandler extends Base implements MessageHandler { - private funi: Funimation; - public name = 'funi'; - constructor(ws: WebSocketHandler) { - super(ws); - this.funi = new Funimation(); - this.initState(); - } - - public async listEpisodes (id: string) : Promise { - const parse = parseInt(id); - if (isNaN(parse) || parse <= 0) - return { isOk: false, reason: new Error('The ID is invalid') }; - const request = await this.funi.listShowItems(parse); - if (!request.isOk) - return request; - return { isOk: true, value: request.value.map(item => ({ - e: item.id_split.join(''), - lang: item.audio ?? [], - name: item.title, - season: item.seasonNum ?? item.seasonTitle ?? item.item.seasonNum ?? item.item.seasonTitle, - seasonTitle: item.seasonTitle, - episode: item.episodeNum, - id: item.id, - img: item.thumb, - description: item.synopsis, - time: item.runtime ?? item.item.runtime - })) }; - } - - public async handleDefault(name: string) { - return getDefault(name, this.funi.cfg.cli); - } - - public async availableDubCodes(): Promise { - const dubLanguageCodesArray: string[] = []; - for(const language of languages){ - if (language.funi_locale) - dubLanguageCodesArray.push(language.code); - } - return [...new Set(dubLanguageCodesArray)]; - } - - public async availableSubCodes(): Promise { - return subtitleLanguagesFilter; - } - - public async resolveItems(data: ResolveItemsData): Promise { - console.debug(`Got resolve options: ${JSON.stringify(data)}`); - const res = await this.funi.getShow(false, { ...data, id: parseInt(data.id) }); - if (!res.isOk) - return res.isOk; - this.addToQueue(res.value.map(a => { - return { - ...data, - ids: [a.episodeID], - title: a.title, - parent: { - title: a.seasonTitle, - season: a.seasonNumber - }, - image: a.image, - e: a.episodeID, - episode: a.epsiodeNumber - }; - })); - return true; - } - - public async search(data: SearchData): Promise { - console.debug(`Got search options: ${JSON.stringify(data)}`); - const funiSearch = await this.funi.searchShow(false, data); - if (!funiSearch.isOk) - return funiSearch; - return { isOk: true, value: funiSearch.value.items.hits.map(a => ({ - image: a.image.showThumbnail, - name: a.title, - desc: a.description, - id: a.id, - lang: a.languages, - rating: a.starRating - })) }; - } - - public async checkToken(): Promise { - return this.funi.checkToken(); - } - - public auth(data: AuthData) { - return this.funi.auth(data); - } - - public async downloadItem(data: QueueItem) { - this.setDownloading(true); - console.debug(`Got download options: ${JSON.stringify(data)}`); - const res = await this.funi.getShow(false, { all: false, but: false, id: parseInt(data.id), e: data.e }); - const _default = yargs.appArgv(this.funi.cfg.cli, true); - if (!res.isOk) - return this.alertError(res.reason); - - for (const ep of res.value) { - await this.funi.getEpisode(false, { dubLang: data.dubLang, fnSlug: ep, s: data.id, subs: { dlsubs: data.dlsubs, sub: false, ccTag: _default.ccTag } }, { ..._default, callbackMaker: this.makeProgressHandler.bind(this), ass: true, fileName: data.fileName, q: data.q, force: 'y', - noaudio: data.noaudio, novids: data.novids }); - } - this.sendMessage({ name: 'finish', data: undefined }); - this.setDownloading(false); - this.onFinish(); - } -} - -export default FunimationHandler; \ No newline at end of file diff --git a/index.ts b/index.ts index 2d6b91f..5b422c8 100644 --- a/index.ts +++ b/index.ts @@ -18,15 +18,7 @@ import update from './modules/module.updater'; } if (argv.addArchive) { - if (argv.service === 'funi') { - if (argv.s === undefined) - return console.error('`-s` not found'); - addToArchive({ - service: 'funi', - type: 's' - }, argv.s); - console.info('Added %s to the downloadArchive list', argv.s); - } else if (argv.service === 'crunchy') { + if (argv.service === 'crunchy') { if (argv.s === undefined && argv.series === undefined) return console.error('`-s` or `--srz` not found'); if (argv.s && argv.series) @@ -52,14 +44,11 @@ import update from './modules/module.updater'; overrideArguments(cfg.cli, id); /* Reimport module to override appArgv */ Object.keys(require.cache).forEach(key => { - if (key.endsWith('crunchy.js') || key.endsWith('funi.js') || key.endsWith('hidive.js')) + if (key.endsWith('crunchy.js') || key.endsWith('hidive.js')) delete require.cache[key]; }); let service: ServiceClass; switch(argv.service) { - case 'funi': - service = new (await import('./funi')).default; - break; case 'crunchy': service = new (await import('./crunchy')).default; break; @@ -75,9 +64,6 @@ import update from './modules/module.updater'; } else { let service: ServiceClass; switch(argv.service) { - case 'funi': - service = new (await import('./funi')).default; - break; case 'crunchy': service = new (await import('./crunchy')).default; break; diff --git a/modules/build-docs.ts b/modules/build-docs.ts index 5b8781b..94fe814 100644 --- a/modules/build-docs.ts +++ b/modules/build-docs.ts @@ -3,13 +3,10 @@ import fs from 'fs'; import path from 'path'; import { args, groups } from './module.args'; -const transformService = (str: Array<'funi'|'crunchy'|'hidive'|'all'>) => { +const transformService = (str: Array<'crunchy'|'hidive'|'all'>) => { const services: string[] = []; str.forEach(function(part) { switch(part) { - case 'funi': - services.push('Funimation'); - break; case 'crunchy': services.push('Crunchyroll'); break; @@ -30,7 +27,7 @@ If you find any bugs in this documentation or in the program itself please repor ## Legal Warning -This application is not endorsed by or affiliated with *Funimation*, *Hidive*, or *Crunchyroll*. +This application is not endorsed by or affiliated with *Crunchyroll* or *Hidive*. This application enables you to download videos for offline viewing which may be forbidden by law in your country. The usage of this application may also cause a violation of the *Terms of Service* between you and the stream provider. This tool is not responsible for your actions; please make an informed decision before using this application. diff --git a/modules/module.app-args.ts b/modules/module.app-args.ts index 00014c8..82cccda 100644 --- a/modules/module.app-args.ts +++ b/modules/module.app-args.ts @@ -61,7 +61,7 @@ let argvC: { debug: boolean | undefined; nocleanup: boolean; help: boolean | undefined; - service: 'funi' | 'crunchy' | 'hidive'; + service: 'crunchy' | 'hidive'; update: boolean; fontName: string | undefined; _: (string | number)[]; diff --git a/modules/module.args.ts b/modules/module.args.ts index 0507576..97f38b4 100644 --- a/modules/module.args.ts +++ b/modules/module.args.ts @@ -41,7 +41,7 @@ type TAppArg = { default: T|undefined, name?: string }, - service: Array<'funi'|'crunchy'|'hidive'|'all'>, + service: Array<'crunchy'|'hidive'|'all'>, usage: string // -(-)${name} will be added for each command, demandOption?: true, transformer?: (value: T) => K @@ -280,7 +280,7 @@ const args: TAppArg[] = [ type: 'number', alias: 'server', docDescribe: true, - service: ['crunchy','funi'], + service: ['crunchy'], usage: '${server}' }, { @@ -314,8 +314,7 @@ const args: TAppArg[] = [ name: 'dlsubs', group: 'dl', describe: 'Download subtitles by language tag (space-separated)' - + `\nFuni Only: ${languages.filter(a => a.funi_locale && !a.cr_locale).map(a => a.locale).join(', ')}` - + `\nCrunchy Only: ${languages.filter(a => a.cr_locale && !a.funi_locale).map(a => a.locale).join(', ')}`, + + `\nCrunchy Only: ${languages.filter(a => a.cr_locale).map(a => a.locale).join(', ')}`, docDescribe: true, service: ['all'], type: 'array', @@ -339,7 +338,7 @@ const args: TAppArg[] = [ group: 'dl', describe: 'Skip downloading audio', docDescribe: true, - service: ['funi'], + service: ['crunchy', 'hidive'], type: 'boolean', usage: '' }, @@ -355,8 +354,7 @@ const args: TAppArg[] = [ { name: 'dubLang', describe: 'Set the language to download: ' - + `\nFuni Only: ${languages.filter(a => a.funi_locale && !a.cr_locale).map(a => a.code).join(', ')}` - + `\nCrunchy Only: ${languages.filter(a => a.cr_locale && !a.funi_locale).map(a => a.code).join(', ')}`, + + `\nCrunchy Only: ${languages.filter(a => a.cr_locale).map(a => a.code).join(', ')}`, docDescribe: true, group: 'dl', choices: dubLanguageCodes, @@ -439,7 +437,7 @@ const args: TAppArg[] = [ group: 'dl', describe: 'Force downloading simulcast version instead of uncut version (if available).', docDescribe: true, - service: ['funi', 'hidive'], + service: ['hidive'], type: 'boolean', usage: '', default: { @@ -572,7 +570,7 @@ const args: TAppArg[] = [ group: 'util', service: ['all'], type: 'string', - choices: ['funi', 'crunchy', 'hidive'], + choices: ['crunchy', 'hidive'], usage: '${service}', default: { default: '' @@ -593,7 +591,7 @@ const args: TAppArg[] = [ group: 'fonts', describe: 'Set the font to use in subtiles', docDescribe: true, - service: ['funi', 'hidive'], + service: ['hidive'], type: 'string', usage: '${fontName}', }, @@ -677,7 +675,7 @@ const args: TAppArg[] = [ describe: 'Authenticate every time the script runs. Use at your own risk.', docDescribe: true, group: 'auth', - service: ['funi','crunchy'], + service: ['crunchy'], type: 'boolean', usage: '', default: { diff --git a/modules/module.cfg-loader.ts b/modules/module.cfg-loader.ts index 6f39d8c..49196d5 100644 --- a/modules/module.cfg-loader.ts +++ b/modules/module.cfg-loader.ts @@ -18,13 +18,11 @@ const guiCfgFile = path.join(workingDir, 'config', 'gui'); const cliCfgFile = path.join(workingDir, 'config', 'cli-defaults'); const hdPflCfgFile = path.join(workingDir, 'config', 'hd_profile'); const sessCfgFile = { - funi: path.join(workingDir, 'config', 'funi_sess'), cr: path.join(workingDir, 'config', 'cr_sess'), hd: path.join(workingDir, 'config', 'hd_sess') }; const stateFile = path.join(workingDir, 'config', 'guistate'); const tokenFile = { - funi: path.join(workingDir, 'config', 'funi_token'), cr: path.join(workingDir, 'config', 'cr_token'), hd: path.join(workingDir, 'config', 'hd_token'), hdNew: path.join(workingDir, 'config', 'hd_new_token') @@ -312,33 +310,6 @@ const saveNewHDToken = (data: Record) => { } }; -const loadFuniToken = () => { - const loadedToken = loadYamlCfgFile<{ - token?: string - }>(tokenFile.funi, true); - let token: false|string = false; - if (loadedToken && loadedToken.token) - token = loadedToken.token; - // info if token not set - if(!token){ - console.info('[INFO] Token not set!\n'); - } - return token; -}; - -const saveFuniToken = (data: { - token?: string -}) => { - const cfgFolder = path.dirname(tokenFile.funi); - try{ - fs.ensureDirSync(cfgFolder); - fs.writeFileSync(`${tokenFile.funi}.yml`, yaml.stringify(data)); - } - catch(e){ - console.error('Can\'t save token file to disk!'); - } -}; - const cfgDir = path.join(workingDir, 'config'); const getState = (): GuiState => { @@ -373,8 +344,6 @@ const setState = (state: GuiState) => { export { loadBinCfg, loadCfg, - loadFuniToken, - saveFuniToken, saveCRSession, loadCRSession, saveCRToken, diff --git a/modules/module.downloadArchive.ts b/modules/module.downloadArchive.ts index bdac33c..b324c2c 100644 --- a/modules/module.downloadArchive.ts +++ b/modules/module.downloadArchive.ts @@ -11,9 +11,6 @@ export type ItemType = { }[] export type DataType = { - funi: { - s: ItemType - }, hidive: { s: ItemType }, @@ -24,9 +21,6 @@ export type DataType = { } const addToArchive = (kind: { - service: 'funi', - type: 's' -} | { service: 'crunchy', type: 's'|'srz' } | { @@ -45,16 +39,7 @@ const addToArchive = (kind: { }); (data as any)[kind.service][kind.type] = items; } else { - if (kind.service === 'funi') { - data['funi'] = { - s: [ - { - id: ID, - already: [] - } - ] - }; - } else if (kind.service === 'crunchy') { + if (kind.service === 'crunchy') { data['crunchy'] = { s: ([] as ItemType).concat(kind.type === 's' ? { id: ID, @@ -80,9 +65,6 @@ const addToArchive = (kind: { }; const downloaded = (kind: { - service: 'funi', - type: 's' -} | { service: 'crunchy', type: 's'|'srz' } | { @@ -105,7 +87,7 @@ const downloaded = (kind: { fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4)); }; -const makeCommand = (service: 'funi'|'crunchy'|'hidive') : Partial[] => { +const makeCommand = (service: 'crunchy'|'hidive') : Partial[] => { const data = loadData(); const ret: Partial[] = []; const kind = data[service]; diff --git a/modules/module.langsData.ts b/modules/module.langsData.ts index d139229..af46eee 100644 --- a/modules/module.langsData.ts +++ b/modules/module.langsData.ts @@ -7,19 +7,16 @@ export type LanguageItem = { locale: string, code: string, name: string, - language?: string, - funi_locale?: string, - funi_name?: string, - funi_name_lagacy?: string + language?: string } const languages: LanguageItem[] = [ - { cr_locale: 'en-US', new_hd_locale: 'en-US', hd_locale: 'English', funi_locale: 'enUS', locale: 'en', code: 'eng', name: 'English' }, + { cr_locale: 'en-US', new_hd_locale: 'en-US', hd_locale: 'English', locale: 'en', code: 'eng', name: 'English' }, { cr_locale: 'en-IN', locale: 'en-IN', code: 'eng', name: 'English (India)', }, - { cr_locale: 'es-LA', new_hd_locale: 'es-MX', hd_locale: 'Spanish LatAm', funi_name: 'Spanish (LAS)', funi_name_lagacy: 'Spanish (Latin Am)', funi_locale: 'esLA', locale: 'es-419', code: 'spa', name: 'Spanish', language: 'Latin American Spanish' }, + { cr_locale: 'es-LA', new_hd_locale: 'es-MX', hd_locale: 'Spanish LatAm', locale: 'es-419', code: 'spa', name: 'Spanish', language: 'Latin American Spanish' }, { cr_locale: 'es-419',hd_locale: 'Spanish', locale: 'es-419', code: 'spa-419', name: 'Spanish', language: 'Latin American Spanish' }, { cr_locale: 'es-ES', new_hd_locale: 'es-ES', hd_locale: 'Spanish Europe', locale: 'es-ES', code: 'spa-ES', name: 'Castilian', language: 'European Spanish' }, - { cr_locale: 'pt-BR', new_hd_locale: 'pt-BR', hd_locale: 'Portuguese', funi_name: 'Portuguese (Brazil)', funi_locale: 'ptBR', locale: 'pt-BR', code: 'por', name: 'Portuguese', language: 'Brazilian Portuguese' }, + { cr_locale: 'pt-BR', new_hd_locale: 'pt-BR', hd_locale: 'Portuguese', locale: 'pt-BR', code: 'por', name: 'Portuguese', language: 'Brazilian Portuguese' }, { cr_locale: 'pt-PT', locale: 'pt-PT', code: 'por', name: 'Portuguese (Portugal)', language: 'Portugues (Portugal)' }, { cr_locale: 'fr-FR', hd_locale: 'French', locale: 'fr', code: 'fra', name: 'French' }, { cr_locale: 'de-DE', hd_locale: 'German', locale: 'de', code: 'deu', name: 'German' }, @@ -29,7 +26,7 @@ const languages: LanguageItem[] = [ { cr_locale: 'ru-RU', hd_locale: 'Russian', locale: 'ru', code: 'rus', name: 'Russian' }, { cr_locale: 'tr-TR', hd_locale: 'Turkish', locale: 'tr', code: 'tur', name: 'Turkish' }, { cr_locale: 'hi-IN', locale: 'hi', code: 'hin', name: 'Hindi' }, - { funi_locale: 'zhMN', locale: 'zh', code: 'cmn', name: 'Chinese (Mandarin, PRC)' }, + { locale: 'zh', code: 'cmn', name: 'Chinese (Mandarin, PRC)' }, { cr_locale: 'zh-CN', locale: 'zh-CN', code: 'zho', name: 'Chinese (Mainland China)' }, { cr_locale: 'zh-TW', locale: 'zh-TW', code: 'chi', name: 'Chinese (Taiwan)' }, { cr_locale: 'ko-KR', hd_locale: 'Korean', locale: 'ko', code: 'kor', name: 'Korean' }, @@ -41,7 +38,7 @@ const languages: LanguageItem[] = [ { cr_locale: 'vi-VN', locale: 'vi-VN', code: 'vie', name: 'Vietnamese', language: 'Tiếng Việt' }, { cr_locale: 'id-ID', locale: 'id-ID', code: 'ind', name: 'Indonesian', language: 'Bahasa Indonesia' }, { cr_locale: 'te-IN', locale: 'te-IN', code: 'tel', name: 'Telugu (India)', language: 'తెలుగు' }, - { cr_locale: 'ja-JP', hd_locale: 'Japanese', funi_locale: 'jaJP', locale: 'ja', code: 'jpn', name: 'Japanese' }, + { cr_locale: 'ja-JP', hd_locale: 'Japanese', locale: 'ja', code: 'jpn', name: 'Japanese' }, ]; // add en language names diff --git a/modules/module.req.ts b/modules/module.req.ts index 232abe0..aee4ffc 100644 --- a/modules/module.req.ts +++ b/modules/module.req.ts @@ -27,7 +27,7 @@ const usefulCookies = { // req class Req { private sessCfg: string; - private service: 'cr'|'funi'|'hd'; + private service: 'cr'|'hd'; private session: Record, private debug: boolean, private nosess = false, private type: 'cr'|'funi'|'hd') { + constructor(private domain: Record, private debug: boolean, private nosess = false, private type: 'cr'|'hd') { this.sessCfg = yamlCfg.sessCfgFile[type]; this.service = type; } diff --git a/package.json b/package.json index d93fddd..1a906ad 100644 --- a/package.json +++ b/package.json @@ -2,12 +2,10 @@ "name": "multi-downloader-nx", "short_name": "aniDL", "version": "4.6.3", - "description": "Downloader for Crunchyroll, Funimation, or Hidive via CLI or GUI", + "description": "Downloader for Crunchyroll and Hidive with CLI and GUI", "keywords": [ "download", "downloader", - "funimation", - "funimationnow", "hidive", "crunchy", "crunchyroll", From 9972a4836656a50c060c873d295984510878885e Mon Sep 17 00:00:00 2001 From: AnimeDL Date: Thu, 11 Apr 2024 08:09:17 -0700 Subject: [PATCH 3/6] [CR] Crunchy Hotfix --- modules/module.api-urls.ts | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/module.api-urls.ts b/modules/module.api-urls.ts index ad6b88a..29d74da 100644 --- a/modules/module.api-urls.ts +++ b/modules/module.api-urls.ts @@ -63,7 +63,7 @@ const api: APIType = { // beta api beta_auth: `${domain.api_beta}/auth/v1/token`, beta_authBasic: 'Basic bm9haWhkZXZtXzZpeWcwYThsMHE6', - beta_authBasicMob: 'Basic b2VkYXJteHN0bGgxanZhd2ltbnE6OWxFaHZIWkpEMzJqdVY1ZFc5Vk9TNTdkb3BkSnBnbzE=', + beta_authBasicMob: 'Basic bm12anNoZmtueW14eGtnN2ZiaDk6WllJVnJCV1VQYmNYRHRiRDIyVlNMYTZiNFdRb3Mzelg=', beta_profile: `${domain.api_beta}/accounts/v1/me/profile`, beta_cmsToken: `${domain.api_beta}/index/v2`, search: `${domain.api_beta}/content/v2/discover/search`, diff --git a/package.json b/package.json index 6bc43cf..c6ea795 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "multi-downloader-nx", "short_name": "aniDL", - "version": "4.7.0", + "version": "4.7.1", "description": "Downloader for Crunchyroll, Funimation, or Hidive via CLI or GUI", "keywords": [ "download", From 44381a04be4080a2a3557339a75e372404a157ea Mon Sep 17 00:00:00 2001 From: AnimeDL Date: Thu, 11 Apr 2024 15:10:19 +0000 Subject: [PATCH 4/6] [CR] Crunchy Hotfix + Documentation --- docs/DOCUMENTATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/DOCUMENTATION.md b/docs/DOCUMENTATION.md index 0e2e2fb..7b9b398 100644 --- a/docs/DOCUMENTATION.md +++ b/docs/DOCUMENTATION.md @@ -1,4 +1,4 @@ -# multi-downloader-nx (4.7.0v) +# multi-downloader-nx (4.7.1v) If you find any bugs in this documentation or in the program itself please report it [over on GitHub](https://github.com/anidl/multi-downloader-nx/issues). From 79fc6584d7a8f6833eef1835398fb5d8f3be1b9a Mon Sep 17 00:00:00 2001 From: AnimeDL Date: Thu, 11 Apr 2024 08:29:48 -0700 Subject: [PATCH 5/6] Make sure segments are only generated if none exist Makes sure that it only generates byterange segments for the mpd if there are non already present. --- modules/module.transform-mpd.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/module.transform-mpd.ts b/modules/module.transform-mpd.ts index 11d6a55..ab00cd9 100644 --- a/modules/module.transform-mpd.ts +++ b/modules/module.transform-mpd.ts @@ -61,7 +61,7 @@ export async function parse(manifest: string, language?: LanguageItem, url?: str ret[host] = { audio: [], video: [] }; - if (playlist.sidx) { + if (playlist.sidx && playlist.segments.length == 0) { const item = await fetch(playlist.sidx.uri, { 'method': 'head' }); @@ -130,7 +130,7 @@ export async function parse(manifest: string, language?: LanguageItem, url?: str if (!Object.prototype.hasOwnProperty.call(ret, host)) ret[host] = { audio: [], video: [] }; - if (playlist.sidx) { + if (playlist.sidx && playlist.segments.length == 0) { const item = await fetch(playlist.sidx.uri, { 'method': 'head' }); From e9c040ceb74c86e76183dab69b921b3e853c7660 Mon Sep 17 00:00:00 2001 From: AnimeDL Date: Thu, 11 Apr 2024 13:17:29 -0700 Subject: [PATCH 6/6] Update ignored token files --- .gitignore | 9 +++------ tsc.ts | 9 +++------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index a56e4c7..f6a14fe 100644 --- a/.gitignore +++ b/.gitignore @@ -21,12 +21,9 @@ token.yml lib test.* updates.json -funi_token.yml -cr_token.yml -hd_profile.yml -hd_sess.yml -hd_token.yml -hd_new_token.yml +*_token.yml +*_profile.yml +*_sess.yml archive.json guistate.json fonts diff --git a/tsc.ts b/tsc.ts index 8b2eedd..3c1ee1a 100644 --- a/tsc.ts +++ b/tsc.ts @@ -32,12 +32,9 @@ const ignore = [ './bin/mkvtoolnix*', './config/token.yml$', './config/updates.json$', - './config/cr_token.yml$', - './config/funi_token.yml$', - './config/new_hd_token.yml$', - './config/hd_token.yml$', - './config/hd_sess.yml$', - './config/hd_profile.yml$', + './config/*_token.yml$', + './config/*_sess.yml$', + './config/*_profile.yml$', '*/\\.eslint*', '*/*\\.tsx?$', './fonts*',