More funi.ts to class conversion

This commit is contained in:
Izuco 2022-01-24 20:19:30 +01:00
parent 2e7271afcb
commit 52cff06388
No known key found for this signature in database
GPG key ID: E9CBE9E4EF3A1BFA
5 changed files with 445 additions and 359 deletions

34
@types/funiSearch.d.ts vendored Normal file
View file

@ -0,0 +1,34 @@
// 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;
}

View file

@ -1,4 +1,11 @@
declare module 'hls-download' {
export type HLSCallback = (data: {
total: number,
cur: number,
percent: number|string,
time: number,
downloadSpeed: number
}) => unknown;
export default class hlsDownload {
constructor(options: {
m3u8json: {
@ -13,7 +20,8 @@ declare module 'hls-download' {
proxy?: string,
skipInit?: boolean,
timeout?: number,
fsRetryTime?: number
fsRetryTime?: number,
callback?: HLSCallback
})
async download() : Promise<{
ok: boolean,

View file

@ -1,9 +1,25 @@
import { HLSCallback } from 'hls-download';
import type { FunimationShow as FunimationSearch } from './funiSearch';
export interface MessageHandler {
auth: (data: AuthData) => Promise<AuthResponse>;
}
export type FuniEpisodeData = {
title: string,
episode: string,
episodeID: string
};
export type AuthData = { username: string, password: string };
export type FuniSearchData = { search: string };
export type FuniGetShowData = { id: number, e?: string, but: boolean, all: boolean };
export type FuniGetEpisodeData = { fnSlug: FuniEpisodeData, callback?: HLSCallback, simul?: boolean; dubLang: string[], s: string }
export type AuthResponse = ResponseBase<undefined>;
export type FuniSearchReponse = ResponseBase<FunimationSearch>;
export type FuniShowResponse = ResponseBase<FuniEpisodeData[]>;
export type FuniGetEpisodeResponse = ResponseBase<undefined>;
export type ResponseBase<T> = ({
isOk: true,

741
funi.ts
View file

@ -11,7 +11,7 @@ const api_host = 'https://prod-api-funimationnow.dadcdigital.com/api';
// modules extra
import * as shlp from 'sei-helper';
import m3u8 from 'm3u8-parsed';
import hlsDownload from 'hls-download';
import hlsDownload, { HLSCallback } from 'hls-download';
// extra
import * as appYargs from './modules/module.app-args';
@ -22,10 +22,7 @@ import vttConvert from './modules/module.vttconvert';
import { Item } from './@types/items';
// params
const cfg = yamlCfg.loadCfg();
const token = yamlCfg.loadFuniToken();
// cli
const argv = appYargs.appArgv(cfg.cli);
// Import modules after argv has been exported
import getData from './modules/module.getdata.js';
import merger from './modules/module.merger';
@ -40,9 +37,8 @@ import { FunimationMediaDownload } from './@types/funiTypes';
import * as langsData from './modules/module.langsData';
import { TitleElement } from './@types/episode';
import { AvailableFilenameVars } from './modules/module.args';
import { AuthResponse } from './@types/messageHandler';
import { AuthData, AuthResponse, FuniGetEpisodeData, FuniGetEpisodeResponse, FuniGetShowData, FuniSearchData, FuniSearchReponse, FuniShowResponse } from './@types/messageHandler';
// check page
argv.p = 1;
// fn variables
let fnEpNum: string|number = 0,
@ -54,375 +50,407 @@ let fnEpNum: string|number = 0,
}[] = [],
stDlPath: Subtitle[] = [];
// main
export default (async () => {
// load binaries
console.log(`\n=== Multi Downloader NX ${packageJson.version} ===\n`);
cfg.bin = await yamlCfg.loadBinCfg();
if (argv.allDubs) {
argv.dubLang = langsData.dubLanguageCodes;
}
// select mode
if (argv.silentAuth && !argv.auth) {
await auth(argv.username, argv.password);
}
if(argv.auth){
auth(argv.username, argv.password);
}
else if(argv.search){
searchShow();
}
else if(argv.s && !isNaN(parseInt(argv.s)) && parseInt(argv.s) > 0){
return getShow();
}
else{
appYargs.showHelp();
}
});
class Funi {
private cfg: yamlCfg.ConfigObject;
private token: string | boolean;
// auth
async function auth(username?: string, password?: string, ask = true): Promise<AuthResponse> {
const authOpts = {
user: username ? username : ask ? await shlp.question('[Q] LOGIN/EMAIL') : '',
pass: password ? password : ask ? await shlp.question('[Q] PASSWORD ') : ''
};
const authData = await getData({
baseUrl: api_host,
url: '/auth/login/',
auth: authOpts,
debug: argv.debug,
});
if(authData.ok && authData.res){
const resJSON = JSON.parse(authData.res.body);
if(resJSON.token){
console.log('[INFO] Authentication success, your token: %s%s\n', resJSON.token.slice(0,8),'*'.repeat(32));
yamlCfg.saveFuniToken({'token': resJSON.token});
return { isOk: true, value: undefined };
} else {
console.log('[ERROR]%s\n', ' No token found');
if (argv.debug) {
console.log(resJSON);
}
return { isOk: false, reason: new Error(resJSON) }
}
constructor(private debug = false) {
this.cfg = yamlCfg.loadCfg();
this.token = yamlCfg.loadFuniToken();
}
return { isOk: false, reason: new Error('Login request failed') }
}
// search show
async function searchShow(){
const qs = {unique: true, limit: 100, q: argv.search, offset: 0 };
const searchData = await getData({
baseUrl: api_host,
url: '/source/funimation/search/auto/',
querystring: qs,
token: token,
useToken: true,
debug: argv.debug,
});
if(!searchData.ok || !searchData.res){return;}
const searchDataJSON = JSON.parse(searchData.res.body);
if(searchDataJSON.detail){
console.log(`[ERROR] ${searchDataJSON.detail}`);
return;
public async init() {
this.cfg.bin = await yamlCfg.loadBinCfg();
}
if(searchDataJSON.items && searchDataJSON.items.hits){
const shows = searchDataJSON.items.hits;
console.log('[INFO] Search Results:');
for(const ssn in shows){
console.log(`[#${shows[ssn].id}] ${shows[ssn].title}` + (shows[ssn].tx_date?` (${shows[ssn].tx_date})`:''));
}
}
console.log('[INFO] Total shows found: %s\n',searchDataJSON.count);
}
// get show
async function getShow(){
let ok = true;
const showData = await getData({
baseUrl: api_host,
url: `/source/catalog/title/${argv.s}`,
token: token,
useToken: true,
debug: argv.debug,
});
// check errors
if(!showData.ok || !showData.res){return;}
const showDataJSON = JSON.parse(showData.res.body);
if(showDataJSON.status){
console.log('[ERROR] Error #%d: %s\n', showDataJSON.status, showDataJSON.data.errors[0].detail);
process.exit(1);
}
else if(!showDataJSON.items || showDataJSON.items.length<1){
console.log('[ERROR] Show not found\n');
process.exit(0);
}
const showDataItem = showDataJSON.items[0];
console.log('[#%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: parseInt(argv.s as string) };
if(argv.alt){ qs.language = 'English'; }
const episodesData = await getData({
baseUrl: api_host,
url: '/funimation/episodes/',
querystring: qs,
token: token,
useToken: true,
debug: argv.debug,
});
if(!episodesData.ok || !episodesData.res){return;}
let epsDataArr: Item[] = JSON.parse(episodesData.res.body).items;
const epNumRegex = /^([A-Z0-9]*[A-Z])?(\d+)$/i;
const epSelEpsTxt = []; let typeIdLen = 0, epIdLen = 4;
const parseEpStr = (epStr: string) => {
const match = epStr.match(epNumRegex);
if (!match) {
console.error('[ERROR] No match found');
return ['', ''];
public async cli() : Promise<boolean|undefined> {
const argv = appYargs.appArgv(this.cfg.cli);
if (argv.debug)
this.debug = true;
console.log(`\n=== Multi Downloader NX ${packageJson.version} ===\n`);
if (argv.allDubs) {
argv.dubLang = langsData.dubLanguageCodes;
}
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);
epIdLen = epMatch[1].length > epIdLen ? epMatch[1].length : epIdLen;
typeIdLen = epMatch[0].length > typeIdLen ? epMatch[0].length : typeIdLen;
e.id_split = epMatch;
}
else{
typeIdLen = 3 > typeIdLen? 3 : typeIdLen;
console.log('[ERROR] FAILED TO PARSE: ', e.id);
e.id_split = [ 'ZZZ', 9999 ];
}
return e;
});
const epSelList = parseSelect(argv.e as string, argv.but);
const fnSlug: {
title: string,
episode: string,
episodeID: string
}[] = []; let is_selected = false;
const eps = epsDataArr;
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;
});
for(const e in eps){
eps[e].id_split[1] = parseInt(eps[e].id_split[1].toString()).toString().padStart(epIdLen, '0');
let epStrId = eps[e].id_split.join('');
// select
is_selected = false;
if (argv.all || epSelList.isSelected(epStrId)) {
fnSlug.push({title:eps[e].item.titleSlug,episode:eps[e].item.episodeSlug, episodeID:epStrId});
epSelEpsTxt.push(epStrId);
is_selected = true;
}
else{
is_selected = false;
}
// 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(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.log(conOut);
}
if(fnSlug.length < 1){
console.log('[INFO] Episodes not selected!\n');
return;
}
else{
console.log('[INFO] Selected Episodes: %s\n',epSelEpsTxt.join(', '));
for(let fnEp=0;fnEp<fnSlug.length;fnEp++){
if (await getEpisode(fnSlug[fnEp]) !== true)
ok = false;
}
}
return ok;
}
async function getEpisode(fnSlug: {
title: string,
episode: string,
episodeID: string
}) {
const episodeData = await getData({
baseUrl: api_host,
url: `/source/catalog/episode/${fnSlug.title}/${fnSlug.episode}/`,
token: token,
useToken: true,
debug: argv.debug,
});
if(!episodeData.ok || !episodeData.res){return;}
const ep = JSON.parse(episodeData.res.body).items[0] as EpisodeData, streamIds = [];
// 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
console.log(
'[INFO] %s - S%sE%s - %s',
ep.parent.title,
(ep.parent.seasonNumber ? ep.parent.seasonNumber : '?'),
(ep.number ? ep.number : '?'),
ep.title
);
console.log('[INFO] Available streams (Non-Encrypted):');
// map medias
const media = ep.media.map(function(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: getSubsUrl(m.mediaChildren, m.language)
// 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.log(`[ERROR] ${data.reason.message}`);
return false;
}
let ok = true;
for (const episodeData of data.value) {
if ((await this.getEpisode(true, { dubLang: argv.dubLang, fnSlug: episodeData, s: argv.s, simul: argv.simul })).isOk !== true)
ok = false;
}
return ok;
}
else{
return { id: 0, type: '' };
appYargs.showHelp();
}
});
// 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 = !argv.simul && uncut[dub_type] && m.version?.match(/uncut/i)
? true
: (!uncut[dub_type] || argv.simul && m.version?.match(/simulcast/i) ? true : false);
for (const curDub of argv.dubLang) {
const item = langsData.languages.find(a => a.code === curDub);
if(item && dub_type == (item.funi_name || item.name) && selUncut){
streamIds.push({
id: m.id,
lang: item
});
stDlPath.push(...m.subtitles);
localSubs = m.subtitles;
selected = true;
}
public async auth(data: AuthData): Promise<AuthResponse> {
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.log('[INFO] Authentication success, your token: %s%s\n', resJSON.token.slice(0,8),'*'.repeat(32));
yamlCfg.saveFuniToken({'token': resJSON.token});
return { isOk: true, value: undefined };
} else {
console.log('[ERROR]%s\n', ' No token found');
if (this.debug) {
console.log(resJSON);
}
return { isOk: false, reason: new Error(resJSON) }
}
console.log(`[#${m.id}] ${dub_type} [${m.version}]${(selected?' (selected)':'')}${
localSubs && localSubs.length > 0 && selected ? ` (using ${localSubs.map(a => `'${a.lang.name}'`).join(', ')} for subtitles)` : ''
}`);
}
return { isOk: false, reason: new Error('Login request failed') }
}
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;
public async searchShow(log: boolean, data: FuniSearchData): Promise<FuniSearchReponse> {
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') }
}
});
if(streamIds.length < 1){
console.log('[ERROR] Track not selected\n');
return;
const searchDataJSON = JSON.parse(searchData.res.body);
if(searchDataJSON.detail){
console.log(`[ERROR] ${searchDataJSON.detail}`);
return { isOk: false, reason: new Error(searchDataJSON.defail) }
}
if(searchDataJSON.items && searchDataJSON.items.hits && log){
const shows = searchDataJSON.items.hits;
console.log('[INFO] Search Results:');
for(const ssn in shows){
console.log(`[#${shows[ssn].id}] ${shows[ssn].title}` + (shows[ssn].tx_date?` (${shows[ssn].tx_date})`:''));
}
}
if (log)
console.log('[INFO] Total shows found: %s\n',searchDataJSON.count);
return { isOk: true, value: searchDataJSON }
}
else{
tsDlPath = [];
for (const streamId of streamIds) {
const streamData = await getData({
baseUrl: api_host,
url: `/source/catalog/video/${streamId.id}/signed`,
token: token,
dinstid: 'uuid',
useToken: true,
debug: argv.debug,
});
if(!streamData.ok || !streamData.res){return;}
const streamDataRes = JSON.parse(streamData.res.body) as StreamData;
if(streamDataRes.errors){
console.log('[ERROR] Error #%s: %s\n',streamDataRes.errors[0].code,streamDataRes.errors[0].detail);
return;
public async getShow(log: boolean, data: FuniGetShowData) : Promise<FuniShowResponse> {
const showData = await getData({
baseUrl: api_host,
url: `/source/catalog/title/${data.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){
if (log)
console.log('[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.log('[ERROR] Show not found\n');
return { isOk: false, reason: new Error('Show not found') }
}
const showDataItem = showDataJSON.items[0];
if (log)
console.log('[#%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: data.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 epSelEpsTxt = []; let typeIdLen = 0, epIdLen = 4;
const parseEpStr = (epStr: string) => {
const match = epStr.match(epNumRegex);
if (!match) {
console.error('[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);
epIdLen = epMatch[1].length > epIdLen ? epMatch[1].length : epIdLen;
typeIdLen = epMatch[0].length > typeIdLen ? epMatch[0].length : typeIdLen;
e.id_split = epMatch;
}
else{
for(const u in streamDataRes.items){
if(streamDataRes.items[u].videoType == 'm3u8'){
tsDlPath.push({
path: streamDataRes.items[u].src,
lang: streamId.lang
typeIdLen = 3 > typeIdLen? 3 : typeIdLen;
console.log('[ERROR] FAILED TO PARSE: ', e.id);
e.id_split = [ 'ZZZ', 9999 ];
}
return e;
});
const epSelList = parseSelect(data.e as string, data.but);
const fnSlug: {
title: string,
episode: string,
episodeID: string
}[] = []; let is_selected = false;
const eps = epsDataArr;
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;
});
for(const e in eps){
eps[e].id_split[1] = parseInt(eps[e].id_split[1].toString()).toString().padStart(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});
epSelEpsTxt.push(epStrId);
is_selected = true;
}
else{
is_selected = false;
}
// 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(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.log(conOut);
}
if(fnSlug.length < 1){
if (log)
console.log('[INFO] Episodes not selected!\n');
return { isOk: true, value: [] } ;
}
else{
if (log)
console.log('[INFO] Selected Episodes: %s\n',epSelEpsTxt.join(', '));
return { isOk: true, value: fnSlug };
}
}
public async getEpisode(log: boolean, data: FuniGetEpisodeData) : Promise<FuniGetEpisodeResponse> {
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 = [];
// 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.log(
'[INFO] %s - S%sE%s - %s',
ep.parent.title,
(ep.parent.seasonNumber ? ep.parent.seasonNumber : '?'),
(ep.number ? ep.number : '?'),
ep.title
);
console.log('[INFO] Available streams (Non-Encrypted):');
}
// map medias
const media = ep.media.map(function(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: getSubsUrl(m.mediaChildren, m.language)
};
}
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 || item.name) && selUncut){
streamIds.push({
id: m.id,
lang: item
});
break;
stDlPath.push(...m.subtitles);
localSubs = m.subtitles;
selected = true;
}
}
if (log)
console.log(`[#${m.id}] ${dub_type} [${m.version}]${(selected?' (selected)':'')}${
localSubs && localSubs.length > 0 && selected ? ` (using ${localSubs.map(a => `'${a.lang.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.log('[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.log('[ERROR] 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){
console.log('[ERROR] Unknown error\n');
return;
}
else{
const res = await downloadStreams({
id: fnSlug.episodeID,
title: ep.title,
showTitle: ep.parent.title
});
if (res === true) {
downloaded({
service: 'funi',
type: 's'
}, argv.s as string, [fnSlug.episodeID]);
if(tsDlPath.length < 1){
if (log)
console.log('[ERROR] Unknown error\n');
return { isOk: false, reason: new Error('Unknown error') };
}
else{
const res = await downloadStreams({
id: data.fnSlug.episodeID,
title: ep.title,
showTitle: ep.parent.title
});
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') }
}
return res;
}
}
}
@ -781,13 +809,14 @@ async function downloadStreams(epsiode: FunimationMediaDownload){
async function downloadFile(filename: string, chunkList: {
segments: Record<string, unknown>[],
}) {
}, callback: HLSCallback, timeout: number, partsize: number, fsRetryTime: number) {
const downloadStatus = await new hlsDownload({
m3u8json: chunkList,
output: `${filename + '.ts'}`,
timeout: argv.timeout,
threads: argv.partsize,
fsRetryTime: argv.fsRetryTime * 1000
timeout: timeout,
threads: partsize,
fsRetryTime: fsRetryTime * 1000,
callback
}).download();
return downloadStatus.ok;

View file

@ -1,8 +1,6 @@
import yargs, { Choices } from 'yargs';
import { args, AvailableMuxer, groups } from './module.args';
yargs(process.argv.slice(2));
let argvC: { [x: string]: unknown; fsRetryTime: number, forceMuxer: AvailableMuxer|undefined; username: string|undefined, password: string|undefined, silentAuth: boolean, skipSubMux: boolean, downloadArchive: boolean, addArchive: boolean, but: boolean, auth: boolean | undefined; dlFonts: boolean | undefined; search: string | undefined; 'search-type': string; page: number | undefined; 'search-locale': string; new: boolean | undefined; 'movie-listing': string | undefined; series: string | undefined; s: string | undefined; e: string | undefined; q: number; x: number; kstream: number; partsize: number; hslang: string; dlsubs: string[]; novids: boolean | undefined; noaudio: boolean | undefined; nosubs: boolean | undefined; dubLang: string[]; all: boolean; fontSize: number; allDubs: boolean; timeout: number; simul: boolean; mp4: boolean; skipmux: boolean | undefined; fileName: string; numbers: number; nosess: string; debug: boolean | undefined; nocleanup: boolean; help: boolean | undefined; service: 'funi' | 'crunchy'; update: boolean; fontName: string | undefined; _: (string | number)[]; $0: string; };
export type ArgvType = typeof argvC;
@ -12,6 +10,7 @@ const appArgv = (cfg: {
}) => {
if (argvC)
return argvC;
yargs(process.argv.slice(2));
const argv = getArgv(cfg)
.parseSync();
argvC = argv;