More funi.ts to class conversion
This commit is contained in:
parent
2e7271afcb
commit
52cff06388
5 changed files with 445 additions and 359 deletions
34
@types/funiSearch.d.ts
vendored
Normal file
34
@types/funiSearch.d.ts
vendored
Normal 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;
|
||||
}
|
||||
10
@types/hls-download.d.ts
vendored
10
@types/hls-download.d.ts
vendored
|
|
@ -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,
|
||||
|
|
|
|||
16
@types/messageHandler.d.ts
vendored
16
@types/messageHandler.d.ts
vendored
|
|
@ -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
741
funi.ts
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue