Merge branch 'v5' into adn-support
This commit is contained in:
commit
d260d8f3e6
30 changed files with 1426 additions and 2211 deletions
1
.github/ISSUE_TEMPLATE/bug.yml
vendored
1
.github/ISSUE_TEMPLATE/bug.yml
vendored
|
|
@ -47,7 +47,6 @@ body:
|
||||||
label: Service
|
label: Service
|
||||||
description: "Please tell us what service the bug occured in."
|
description: "Please tell us what service the bug occured in."
|
||||||
options:
|
options:
|
||||||
- Funimation
|
|
||||||
- Crunchyroll
|
- Crunchyroll
|
||||||
- Hidive
|
- Hidive
|
||||||
- All
|
- All
|
||||||
|
|
|
||||||
88
@types/animeOnegaiSearch.d.ts
vendored
Normal file
88
@types/animeOnegaiSearch.d.ts
vendored
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
export interface AnimeOnegaiSearch {
|
||||||
|
text: string;
|
||||||
|
list: AOSearchResult[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AOSearchResult {
|
||||||
|
/**
|
||||||
|
* Asset ID
|
||||||
|
*/
|
||||||
|
ID: number;
|
||||||
|
CreatedAt: Date;
|
||||||
|
UpdatedAt: Date;
|
||||||
|
DeletedAt: null;
|
||||||
|
title: string;
|
||||||
|
active: boolean;
|
||||||
|
excerpt: string;
|
||||||
|
description: string;
|
||||||
|
bg: string;
|
||||||
|
poster: string;
|
||||||
|
entry: string;
|
||||||
|
code_name: string;
|
||||||
|
/**
|
||||||
|
* The Video ID required to get the streams
|
||||||
|
*/
|
||||||
|
video_entry: string;
|
||||||
|
trailer: string;
|
||||||
|
year: number;
|
||||||
|
/**
|
||||||
|
* Asset Type, Known Possibilities
|
||||||
|
* * 1 - Video
|
||||||
|
* * 2 - Series
|
||||||
|
*/
|
||||||
|
asset_type: 1 | 2;
|
||||||
|
status: number;
|
||||||
|
permalink: string;
|
||||||
|
duration: string;
|
||||||
|
subtitles: boolean;
|
||||||
|
price: number;
|
||||||
|
rent_price: number;
|
||||||
|
rating: number;
|
||||||
|
color: number | null;
|
||||||
|
classification: number;
|
||||||
|
brazil_classification: null | string;
|
||||||
|
likes: number;
|
||||||
|
views: number;
|
||||||
|
button: string;
|
||||||
|
stream_url: string;
|
||||||
|
stream_url_backup: string;
|
||||||
|
copyright: null | string;
|
||||||
|
skip_intro: null | string;
|
||||||
|
ending: null | string;
|
||||||
|
bumper_intro: string;
|
||||||
|
ads: string;
|
||||||
|
age_restriction: boolean | null;
|
||||||
|
epg: null;
|
||||||
|
allow_languages: string[] | null;
|
||||||
|
allow_countries: string[] | null;
|
||||||
|
classification_text: string;
|
||||||
|
locked: boolean;
|
||||||
|
resign: boolean;
|
||||||
|
favorite: boolean;
|
||||||
|
actors_list: null;
|
||||||
|
voiceactors_list: null;
|
||||||
|
artdirectors_list: null;
|
||||||
|
audios_list: null;
|
||||||
|
awards_list: null;
|
||||||
|
companies_list: null;
|
||||||
|
countries_list: null;
|
||||||
|
directors_list: null;
|
||||||
|
edition_list: null;
|
||||||
|
genres_list: null;
|
||||||
|
music_list: null;
|
||||||
|
photograpy_list: null;
|
||||||
|
producer_list: null;
|
||||||
|
screenwriter_list: null;
|
||||||
|
season_list: null;
|
||||||
|
tags_list: null;
|
||||||
|
chapter_id: number;
|
||||||
|
chapter_entry: string;
|
||||||
|
chapter_poster: string;
|
||||||
|
progress_time: number;
|
||||||
|
progress_percent: number;
|
||||||
|
included_subscription: number;
|
||||||
|
paid_content: number;
|
||||||
|
rent_content: number;
|
||||||
|
objectID: string;
|
||||||
|
lang: string;
|
||||||
|
}
|
||||||
36
@types/animeOnegaiSeasons.d.ts
vendored
Normal file
36
@types/animeOnegaiSeasons.d.ts
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
export interface AnimeOnegaiSeasons {
|
||||||
|
ID: number;
|
||||||
|
CreatedAt: Date;
|
||||||
|
UpdatedAt: Date;
|
||||||
|
DeletedAt: null;
|
||||||
|
name: string;
|
||||||
|
number: number;
|
||||||
|
asset_id: number;
|
||||||
|
entry: string;
|
||||||
|
description: string;
|
||||||
|
active: boolean;
|
||||||
|
allow_languages: string[];
|
||||||
|
allow_countries: string[];
|
||||||
|
list: Episode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Episode {
|
||||||
|
ID: number;
|
||||||
|
CreatedAt: Date;
|
||||||
|
UpdatedAt: Date;
|
||||||
|
DeletedAt: null;
|
||||||
|
name: string;
|
||||||
|
number: number;
|
||||||
|
description: string;
|
||||||
|
thumbnail: string;
|
||||||
|
entry: string;
|
||||||
|
video_entry: string;
|
||||||
|
active: boolean;
|
||||||
|
season_id: number;
|
||||||
|
stream_url: string;
|
||||||
|
skip_intro: null;
|
||||||
|
ending: null;
|
||||||
|
open_free: boolean;
|
||||||
|
asset_id: number;
|
||||||
|
age_restriction: null;
|
||||||
|
}
|
||||||
111
@types/animeOnegaiSeries.d.ts
vendored
Normal file
111
@types/animeOnegaiSeries.d.ts
vendored
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
export interface AnimeOnegaiSeries {
|
||||||
|
ID: number;
|
||||||
|
CreatedAt: Date;
|
||||||
|
UpdatedAt: Date;
|
||||||
|
DeletedAt: null;
|
||||||
|
title: string;
|
||||||
|
active: boolean;
|
||||||
|
excerpt: string;
|
||||||
|
description: string;
|
||||||
|
bg: string;
|
||||||
|
poster: string;
|
||||||
|
entry: string;
|
||||||
|
code_name: string;
|
||||||
|
/**
|
||||||
|
* The Video ID required to get the streams
|
||||||
|
*/
|
||||||
|
video_entry: string;
|
||||||
|
trailer: string;
|
||||||
|
year: number;
|
||||||
|
asset_type: number;
|
||||||
|
status: number;
|
||||||
|
permalink: string;
|
||||||
|
duration: string;
|
||||||
|
subtitles: boolean;
|
||||||
|
price: number;
|
||||||
|
rent_price: number;
|
||||||
|
rating: number;
|
||||||
|
color: number;
|
||||||
|
classification: number;
|
||||||
|
brazil_classification: string;
|
||||||
|
likes: number;
|
||||||
|
views: number;
|
||||||
|
button: string;
|
||||||
|
stream_url: string;
|
||||||
|
stream_url_backup: string;
|
||||||
|
copyright: string;
|
||||||
|
skip_intro: null;
|
||||||
|
ending: null;
|
||||||
|
bumper_intro: string;
|
||||||
|
ads: string;
|
||||||
|
age_restriction: boolean;
|
||||||
|
epg: null;
|
||||||
|
allow_languages: string[];
|
||||||
|
allow_countries: string[];
|
||||||
|
classification_text: string;
|
||||||
|
locked: boolean;
|
||||||
|
resign: boolean;
|
||||||
|
favorite: boolean;
|
||||||
|
actors_list: CtorsList[];
|
||||||
|
voiceactors_list: CtorsList[];
|
||||||
|
artdirectors_list: any[];
|
||||||
|
audios_list: SList[];
|
||||||
|
awards_list: any[];
|
||||||
|
companies_list: any[];
|
||||||
|
countries_list: any[];
|
||||||
|
directors_list: CtorsList[];
|
||||||
|
edition_list: any[];
|
||||||
|
genres_list: SList[];
|
||||||
|
music_list: any[];
|
||||||
|
photograpy_list: any[];
|
||||||
|
producer_list: any[];
|
||||||
|
screenwriter_list: any[];
|
||||||
|
season_list: any[];
|
||||||
|
tags_list: TagsList[];
|
||||||
|
chapter_id: number;
|
||||||
|
chapter_entry: string;
|
||||||
|
chapter_poster: string;
|
||||||
|
progress_time: number;
|
||||||
|
progress_percent: number;
|
||||||
|
included_subscription: number;
|
||||||
|
paid_content: number;
|
||||||
|
rent_content: number;
|
||||||
|
objectID: string;
|
||||||
|
lang: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CtorsList {
|
||||||
|
ID: number;
|
||||||
|
CreatedAt: Date;
|
||||||
|
UpdatedAt: Date;
|
||||||
|
DeletedAt: null;
|
||||||
|
name: string;
|
||||||
|
Permalink?: string;
|
||||||
|
country: number | null;
|
||||||
|
year: number | null;
|
||||||
|
death: number | null;
|
||||||
|
image: string;
|
||||||
|
genre: null;
|
||||||
|
description: string;
|
||||||
|
permalink?: string;
|
||||||
|
background?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SList {
|
||||||
|
ID: number;
|
||||||
|
CreatedAt: Date;
|
||||||
|
UpdatedAt: Date;
|
||||||
|
DeletedAt: null;
|
||||||
|
name: string;
|
||||||
|
age_restriction?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TagsList {
|
||||||
|
ID: number;
|
||||||
|
CreatedAt: Date;
|
||||||
|
UpdatedAt: Date;
|
||||||
|
DeletedAt: null;
|
||||||
|
name: string;
|
||||||
|
position: number;
|
||||||
|
status: boolean;
|
||||||
|
}
|
||||||
41
@types/animeOnegaiStream.d.ts
vendored
Normal file
41
@types/animeOnegaiStream.d.ts
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
export interface AnimeOnegaiStream {
|
||||||
|
ID: number;
|
||||||
|
CreatedAt: Date;
|
||||||
|
UpdatedAt: Date;
|
||||||
|
DeletedAt: null;
|
||||||
|
name: string;
|
||||||
|
source_url: string;
|
||||||
|
backup_url: string;
|
||||||
|
live: boolean;
|
||||||
|
token_handler: number;
|
||||||
|
entry: string;
|
||||||
|
job: string;
|
||||||
|
drm: boolean;
|
||||||
|
transcoding_content_id: string;
|
||||||
|
transcoding_asset_id: string;
|
||||||
|
status: number;
|
||||||
|
thumbnail: string;
|
||||||
|
hls: string;
|
||||||
|
dash: string;
|
||||||
|
widevine_proxy: string;
|
||||||
|
playready_proxy: string;
|
||||||
|
apple_licence: string;
|
||||||
|
apple_certificate: string;
|
||||||
|
dpath: string;
|
||||||
|
dbin: string;
|
||||||
|
subtitles: Subtitle[];
|
||||||
|
origin: number;
|
||||||
|
offline_entry: string;
|
||||||
|
offline_status: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Subtitle {
|
||||||
|
ID: number;
|
||||||
|
CreatedAt: Date;
|
||||||
|
UpdatedAt: Date;
|
||||||
|
DeletedAt: null;
|
||||||
|
name: string;
|
||||||
|
lang: string;
|
||||||
|
entry_id: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
34
@types/funiSearch.d.ts
vendored
34
@types/funiSearch.d.ts
vendored
|
|
@ -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;
|
|
||||||
}
|
|
||||||
75
@types/funiSubtitleRequest.d.ts
vendored
75
@types/funiSubtitleRequest.d.ts
vendored
|
|
@ -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',
|
|
||||||
}
|
|
||||||
16
@types/funiTypes.d.ts
vendored
16
@types/funiTypes.d.ts
vendored
|
|
@ -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
|
|
||||||
}
|
|
||||||
4
@types/ws.d.ts
vendored
4
@types/ws.d.ts
vendored
|
|
@ -30,8 +30,8 @@ export type MessageTypes = {
|
||||||
'isDownloading': [undefined, boolean],
|
'isDownloading': [undefined, boolean],
|
||||||
'openFolder': [FolderTypes, undefined],
|
'openFolder': [FolderTypes, undefined],
|
||||||
'changeProvider': [undefined, boolean],
|
'changeProvider': [undefined, boolean],
|
||||||
'type': [undefined, 'funi'|'crunchy'|'hidive'|'adn'|undefined],
|
'type': [undefined, 'crunchy'|'hidive'|'ao'|'adn'|undefined],
|
||||||
'setup': ['funi'|'crunchy'|'hidive'|'adn'|undefined, undefined],
|
'setup': ['crunchy'|'hidive'|'ao'|'adn'|undefined, undefined],
|
||||||
'openFile': [[FolderTypes, string], undefined],
|
'openFile': [[FolderTypes, string], undefined],
|
||||||
'openURL': [string, undefined],
|
'openURL': [string, undefined],
|
||||||
'isSetup': [undefined, boolean],
|
'isSetup': [undefined, boolean],
|
||||||
|
|
|
||||||
779
ao.ts
Normal file
779
ao.ts
Normal file
|
|
@ -0,0 +1,779 @@
|
||||||
|
// Package Info
|
||||||
|
import packageJson from './package.json';
|
||||||
|
|
||||||
|
// Node
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
|
||||||
|
// Plugins
|
||||||
|
import shlp from 'sei-helper';
|
||||||
|
|
||||||
|
// Modules
|
||||||
|
import * as fontsData from './modules/module.fontsData';
|
||||||
|
import * as langsData from './modules/module.langsData';
|
||||||
|
import * as yamlCfg from './modules/module.cfg-loader';
|
||||||
|
import * as yargs from './modules/module.app-args';
|
||||||
|
import * as reqModule from './modules/module.fetch';
|
||||||
|
import Merger, { Font, MergerInput, SubtitleInput } from './modules/module.merger';
|
||||||
|
import getKeys, { canDecrypt } from './modules/widevine';
|
||||||
|
import streamdl, { M3U8Json } from './modules/hls-download';
|
||||||
|
import { exec } from './modules/sei-helper-fixes';
|
||||||
|
import { console } from './modules/log';
|
||||||
|
import { domain } from './modules/module.api-urls';
|
||||||
|
import { downloaded } from './modules/module.downloadArchive';
|
||||||
|
import parseSelect from './modules/module.parseSelect';
|
||||||
|
import parseFileName, { Variable } from './modules/module.filename';
|
||||||
|
import { AvailableFilenameVars } from './modules/module.args';
|
||||||
|
import { parse } from './modules/module.transform-mpd';
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import { ServiceClass } from './@types/serviceClassInterface';
|
||||||
|
import { AuthData, AuthResponse, SearchData, SearchResponse, SearchResponseItem } from './@types/messageHandler';
|
||||||
|
import { AOSearchResult, AnimeOnegaiSearch } from './@types/animeOnegaiSearch';
|
||||||
|
import { AnimeOnegaiSeries } from './@types/animeOnegaiSeries';
|
||||||
|
import { AnimeOnegaiSeasons, Episode } from './@types/animeOnegaiSeasons';
|
||||||
|
import { DownloadedMedia } from './@types/hidiveTypes';
|
||||||
|
import { AnimeOnegaiStream } from './@types/animeOnegaiStream';
|
||||||
|
import { sxItem } from './crunchy';
|
||||||
|
|
||||||
|
type parsedMultiDubDownload = {
|
||||||
|
data: {
|
||||||
|
lang: string,
|
||||||
|
videoId: string
|
||||||
|
episode: Episode
|
||||||
|
}[],
|
||||||
|
seriesTitle: string,
|
||||||
|
seasonTitle: string,
|
||||||
|
episodeTitle: string,
|
||||||
|
episodeNumber: number,
|
||||||
|
seasonNumber: number,
|
||||||
|
seriesID: number,
|
||||||
|
seasonID: number,
|
||||||
|
image: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class AnimeOnegai implements ServiceClass {
|
||||||
|
public cfg: yamlCfg.ConfigObject;
|
||||||
|
private token: Record<string, any>;
|
||||||
|
private req: reqModule.Req;
|
||||||
|
public locale: string;
|
||||||
|
public jpnStrings: string[] = [
|
||||||
|
'Japonés con Subtítulos en Español',
|
||||||
|
'Japonés con Subtítulos en Portugués',
|
||||||
|
'Japonês com legendas em espanhol',
|
||||||
|
'Japonês com legendas em português'
|
||||||
|
];
|
||||||
|
public spaStrings: string[] = [
|
||||||
|
'Doblaje en Español',
|
||||||
|
'Dublagem em espanhol'
|
||||||
|
];
|
||||||
|
public porStrings: string[] = [
|
||||||
|
'Doblaje en Portugués',
|
||||||
|
'Dublagem em português'
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(private debug = false) {
|
||||||
|
this.cfg = yamlCfg.loadCfg();
|
||||||
|
this.token = yamlCfg.loadAOToken();
|
||||||
|
this.req = new reqModule.Req(domain, debug, false, 'ao');
|
||||||
|
this.locale = 'es';
|
||||||
|
}
|
||||||
|
|
||||||
|
public async cli() {
|
||||||
|
console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`);
|
||||||
|
const argv = yargs.appArgv(this.cfg.cli);
|
||||||
|
this.locale = argv.locale;
|
||||||
|
if (argv.debug)
|
||||||
|
this.debug = true;
|
||||||
|
|
||||||
|
// load binaries
|
||||||
|
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||||
|
if (argv.allDubs) {
|
||||||
|
argv.dubLang = langsData.dubLanguageCodes;
|
||||||
|
}
|
||||||
|
if (argv.auth) {
|
||||||
|
//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) {
|
||||||
|
//Search
|
||||||
|
await this.doSearch({ ...argv, search: argv.search as string });
|
||||||
|
} else if (argv.s && !isNaN(parseInt(argv.s,10)) && parseInt(argv.s,10) > 0) {
|
||||||
|
const selected = await this.selectShow(parseInt(argv.s), argv.e, argv.but, argv.all, argv);
|
||||||
|
if (selected.isOk) {
|
||||||
|
for (const select of selected.value) {
|
||||||
|
if (!(await this.downloadEpisode(select, {...argv, skipsubs: false}))) {
|
||||||
|
console.error(`Unable to download selected episode ${select.episodeNumber}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else if (argv.token) {
|
||||||
|
this.token = {token: argv.token};
|
||||||
|
yamlCfg.saveAOToken(this.token);
|
||||||
|
console.info('Saved token');
|
||||||
|
} else {
|
||||||
|
console.info('No option selected or invalid value entered. Try --help.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async doSearch(data: SearchData): Promise<SearchResponse> {
|
||||||
|
const searchReq = await this.req.getData(`https://api.animeonegai.com/v1/search/algolia/${encodeURIComponent(data.search)}?lang=${this.locale}`);
|
||||||
|
if (!searchReq.ok || !searchReq.res) {
|
||||||
|
console.error('Search FAILED!');
|
||||||
|
return { isOk: false, reason: new Error('Search failed. No more information provided') };
|
||||||
|
}
|
||||||
|
const searchData = await searchReq.res.json() as AnimeOnegaiSearch;
|
||||||
|
const searchItems: AOSearchResult[] = [];
|
||||||
|
console.info('Search Results:');
|
||||||
|
for (const hit of searchData.list) {
|
||||||
|
searchItems.push(hit);
|
||||||
|
let fullType: string;
|
||||||
|
if (hit.asset_type == 2) {
|
||||||
|
fullType = `S.${hit.ID}`;
|
||||||
|
} else if (hit.asset_type == 1) {
|
||||||
|
fullType = `E.${hit.ID}`;
|
||||||
|
} else {
|
||||||
|
fullType = 'Unknown';
|
||||||
|
console.warn(`Unknown asset type ${hit.asset_type}, please report this.`);
|
||||||
|
}
|
||||||
|
console.log(`[${fullType}] ${hit.title}`);
|
||||||
|
}
|
||||||
|
return { isOk: true, value: searchItems.filter(a => a.asset_type == 2).flatMap((a): SearchResponseItem => {
|
||||||
|
return {
|
||||||
|
id: a.ID+'',
|
||||||
|
image: a.poster ?? '/notFound.png',
|
||||||
|
name: a.title,
|
||||||
|
rating: a.likes,
|
||||||
|
desc: a.description
|
||||||
|
};
|
||||||
|
})};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async doAuth(data: AuthData): Promise<AuthResponse> {
|
||||||
|
data;
|
||||||
|
console.error('Authentication not possible, manual authentication required due to recaptcha. In order to login use the --token flag. You can get the token by logging into the website, and opening the dev console and running the command "localStorage.ott_token"');
|
||||||
|
return { isOk: false, reason: new Error('Authentication not possible, manual authentication required do to recaptcha.') };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getShow(id: number) {
|
||||||
|
const getSeriesData = await this.req.getData(`https://api.animeonegai.com/v1/asset/${id}?lang=${this.locale}`);
|
||||||
|
if (!getSeriesData.ok || !getSeriesData.res) {
|
||||||
|
console.error('Failed to get Show Data');
|
||||||
|
return { isOk: false };
|
||||||
|
}
|
||||||
|
const seriesData = await getSeriesData.res.json() as AnimeOnegaiSeries;
|
||||||
|
|
||||||
|
const getSeasonData = await this.req.getData(`https://api.animeonegai.com/v1/asset/content/${id}?lang=${this.locale}`);
|
||||||
|
if (!getSeasonData.ok || !getSeasonData.res) {
|
||||||
|
console.error('Failed to get Show Data');
|
||||||
|
return { isOk: false };
|
||||||
|
}
|
||||||
|
const seasonData = await getSeasonData.res.json() as AnimeOnegaiSeasons[];
|
||||||
|
|
||||||
|
return { isOk: true, data: seriesData, seasons: seasonData };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async listShow(id: number, outputEpisode: boolean = true) {
|
||||||
|
const series = await this.getShow(id);
|
||||||
|
if (!series.isOk || !series.data) {
|
||||||
|
console.error('Failed to list series data: Failed to get series');
|
||||||
|
return { isOk: false };
|
||||||
|
}
|
||||||
|
console.info(`[S.${series.data.ID}] ${series.data.title} (${series.seasons.length} Seasons)`);
|
||||||
|
if (series.seasons.length === 0) {
|
||||||
|
console.info(' No Seasons found!');
|
||||||
|
return { isOk: false };
|
||||||
|
}
|
||||||
|
const episodes: { [key: number]: (Episode & { lang?: string })[] } = {};
|
||||||
|
for (const season of series.seasons) {
|
||||||
|
let lang: string | undefined = undefined;
|
||||||
|
if (this.jpnStrings.includes(season.name)) lang = 'ja';
|
||||||
|
if (this.porStrings.includes(season.name)) lang = 'pt';
|
||||||
|
if (this.spaStrings.includes(season.name)) lang = 'es';
|
||||||
|
for (const episode of season.list) {
|
||||||
|
if (!episodes[episode.number]) {
|
||||||
|
episodes[episode.number] = [];
|
||||||
|
}
|
||||||
|
/*if (!episodes[episode.number].find(a=>a.lang == lang))*/ episodes[episode.number].push({...episode, lang});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Enable to output episodes seperate from selection
|
||||||
|
if (outputEpisode) {
|
||||||
|
for (const episodeKey in episodes) {
|
||||||
|
const episode = episodes[episodeKey][0];
|
||||||
|
const langs = Array.from(new Set(episodes[episodeKey].map(a=>a.lang)));
|
||||||
|
console.info(` [E.${episode.ID}] E${episode.number} - ${episode.name} (${langs.map(a=>{
|
||||||
|
if (a) return langsData.languages.find(b=>b.ao_locale === a)?.name;
|
||||||
|
return 'Unknown';
|
||||||
|
}).join(', ')})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { isOk: true, value: episodes, series: series };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async selectShow(id: number, e: string | undefined, but: boolean, all: boolean, options: yargs.ArgvType) {
|
||||||
|
const getShowData = await this.listShow(id, false);
|
||||||
|
if (!getShowData.isOk || !getShowData.value) {
|
||||||
|
return { isOk: false, value: [] };
|
||||||
|
}
|
||||||
|
//const showData = getShowData.value;
|
||||||
|
const doEpsFilter = parseSelect(e as string);
|
||||||
|
// build selected episodes
|
||||||
|
const selEpsArr: parsedMultiDubDownload[] = [];
|
||||||
|
const episodes = getShowData.value;
|
||||||
|
const seasonNumberTitleParse = getShowData.series.data.title.match(/\d+$/);
|
||||||
|
const seasonNumber = seasonNumberTitleParse ? parseInt(seasonNumberTitleParse[0]) : 1;
|
||||||
|
for (const episodeKey in getShowData.value) {
|
||||||
|
const episode = episodes[episodeKey][0];
|
||||||
|
const selectedLangs: string[] = [];
|
||||||
|
const selected: {
|
||||||
|
lang: string,
|
||||||
|
videoId: string
|
||||||
|
episode: Episode
|
||||||
|
}[] = [];
|
||||||
|
for (const episode of episodes[episodeKey]) {
|
||||||
|
const lang = langsData.languages.find(a=>a.ao_locale === episode.lang);
|
||||||
|
let isSelected = false;
|
||||||
|
if (typeof selected.find(a=>a.lang == episode.lang) == 'undefined') {
|
||||||
|
if (options.dubLang.includes(lang?.code ?? 'Unknown')) {
|
||||||
|
if ((but && !doEpsFilter.isSelected([episode.number+'', episode.ID+''])) || all || (!but && doEpsFilter.isSelected([episode.number+'', episode.ID+'']))) {
|
||||||
|
isSelected = true;
|
||||||
|
selected.push({lang: episode.lang as string, videoId: episode.video_entry, episode: episode });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const selectedLang = isSelected ? `✓ ${lang?.name ?? 'Unknown'}` : `${lang?.name ?? 'Unknown'}`;
|
||||||
|
if (!selectedLangs.includes(selectedLang)) {
|
||||||
|
selectedLangs.push(selectedLang);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (selected.length > 0) {
|
||||||
|
selEpsArr.push({
|
||||||
|
'data': selected,
|
||||||
|
'seasonNumber': seasonNumber,
|
||||||
|
'episodeNumber': episode.number,
|
||||||
|
'episodeTitle': episode.name,
|
||||||
|
'image': episode.thumbnail,
|
||||||
|
'seasonID': episode.season_id,
|
||||||
|
'seasonTitle': getShowData.series.data.title,
|
||||||
|
'seriesTitle': getShowData.series.data.title,
|
||||||
|
'seriesID': getShowData.series.data.ID
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.info(` [S${seasonNumber}E${episode.number}] - ${episode.name} (${selectedLangs.join(', ')})`);
|
||||||
|
}
|
||||||
|
return { isOk: true, value: selEpsArr, showData: getShowData.series };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async downloadEpisode(data: parsedMultiDubDownload, options: yargs.ArgvType): Promise<boolean> {
|
||||||
|
const res = await this.downloadMediaList(data, options);
|
||||||
|
if (res === undefined || res.error) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
if (!options.skipmux) {
|
||||||
|
await this.muxStreams(res.data, { ...options, output: res.fileName });
|
||||||
|
} else {
|
||||||
|
console.info('Skipping mux');
|
||||||
|
}
|
||||||
|
downloaded({
|
||||||
|
service: 'ao',
|
||||||
|
type: 's'
|
||||||
|
}, data.seasonID+'', [data.episodeNumber+'']);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async muxStreams(data: DownloadedMedia[], options: yargs.ArgvType) {
|
||||||
|
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||||
|
let hasAudioStreams = false;
|
||||||
|
if (options.novids || data.filter(a => a.type === 'Video').length === 0)
|
||||||
|
return console.info('Skip muxing since no vids are downloaded');
|
||||||
|
if (data.some(a => a.type === 'Audio')) {
|
||||||
|
hasAudioStreams = true;
|
||||||
|
}
|
||||||
|
const merger = new Merger({
|
||||||
|
onlyVid: hasAudioStreams ? data.filter(a => a.type === 'Video').map((a) : MergerInput => {
|
||||||
|
if (a.type === 'Subtitle')
|
||||||
|
throw new Error('Never');
|
||||||
|
return {
|
||||||
|
lang: a.lang,
|
||||||
|
path: a.path,
|
||||||
|
};
|
||||||
|
}) : [],
|
||||||
|
skipSubMux: options.skipSubMux,
|
||||||
|
inverseTrackOrder: false,
|
||||||
|
keepAllVideos: options.keepAllVideos,
|
||||||
|
onlyAudio: hasAudioStreams ? data.filter(a => a.type === 'Audio').map((a) : MergerInput => {
|
||||||
|
if (a.type === 'Subtitle')
|
||||||
|
throw new Error('Never');
|
||||||
|
return {
|
||||||
|
lang: a.lang,
|
||||||
|
path: a.path,
|
||||||
|
};
|
||||||
|
}) : [],
|
||||||
|
output: `${options.output}.${options.mp4 ? 'mp4' : 'mkv'}`,
|
||||||
|
subtitles: data.filter(a => a.type === 'Subtitle').map((a) : SubtitleInput => {
|
||||||
|
if (a.type === 'Video')
|
||||||
|
throw new Error('Never');
|
||||||
|
if (a.type === 'Audio')
|
||||||
|
throw new Error('Never');
|
||||||
|
return {
|
||||||
|
file: a.path,
|
||||||
|
language: a.language,
|
||||||
|
closedCaption: a.cc
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
simul: data.filter(a => a.type === 'Video').map((a) : boolean => {
|
||||||
|
if (a.type === 'Subtitle')
|
||||||
|
throw new Error('Never');
|
||||||
|
return !a.uncut as boolean;
|
||||||
|
})[0],
|
||||||
|
fonts: Merger.makeFontsList(this.cfg.dir.fonts, data.filter(a => a.type === 'Subtitle') as sxItem[]),
|
||||||
|
videoAndAudio: hasAudioStreams ? [] : data.filter(a => a.type === 'Video').map((a) : MergerInput => {
|
||||||
|
if (a.type === 'Subtitle')
|
||||||
|
throw new Error('Never');
|
||||||
|
return {
|
||||||
|
lang: a.lang,
|
||||||
|
path: a.path,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
videoTitle: options.videoTitle,
|
||||||
|
options: {
|
||||||
|
ffmpeg: options.ffmpegOptions,
|
||||||
|
mkvmerge: options.mkvmergeOptions
|
||||||
|
},
|
||||||
|
defaults: {
|
||||||
|
audio: options.defaultAudio,
|
||||||
|
sub: options.defaultSub
|
||||||
|
},
|
||||||
|
ccTag: options.ccTag
|
||||||
|
});
|
||||||
|
const bin = Merger.checkMerger(this.cfg.bin, options.mp4, options.forceMuxer);
|
||||||
|
// collect fonts info
|
||||||
|
// mergers
|
||||||
|
let isMuxed = false;
|
||||||
|
if (options.syncTiming) {
|
||||||
|
await merger.createDelays();
|
||||||
|
}
|
||||||
|
if (bin.MKVmerge) {
|
||||||
|
await merger.merge('mkvmerge', bin.MKVmerge);
|
||||||
|
isMuxed = true;
|
||||||
|
} else if (bin.FFmpeg) {
|
||||||
|
await merger.merge('ffmpeg', bin.FFmpeg);
|
||||||
|
isMuxed = true;
|
||||||
|
} else{
|
||||||
|
console.info('\nDone!\n');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isMuxed && !options.nocleanup)
|
||||||
|
merger.cleanUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async downloadMediaList(medias: parsedMultiDubDownload, options: yargs.ArgvType) : Promise<{
|
||||||
|
data: DownloadedMedia[],
|
||||||
|
fileName: string,
|
||||||
|
error: boolean
|
||||||
|
} | undefined> {
|
||||||
|
if(!this.token.token){
|
||||||
|
console.error('Authentication required!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.cfg.bin.ffmpeg)
|
||||||
|
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||||
|
|
||||||
|
let mediaName = '...';
|
||||||
|
let fileName;
|
||||||
|
const variables: Variable[] = [];
|
||||||
|
if(medias.seasonTitle && medias.episodeNumber && medias.episodeTitle){
|
||||||
|
mediaName = `${medias.seasonTitle} - ${medias.episodeNumber} - ${medias.episodeTitle}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files: DownloadedMedia[] = [];
|
||||||
|
|
||||||
|
let subIndex = 0;
|
||||||
|
let dlFailed = false;
|
||||||
|
let dlVideoOnce = false; // Variable to save if best selected video quality was downloaded
|
||||||
|
|
||||||
|
for (const media of medias.data) {
|
||||||
|
console.info(`Requesting: [E.${media.episode.ID}] ${mediaName}`);
|
||||||
|
|
||||||
|
const AuthHeaders = {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.token.token}`,
|
||||||
|
'Referer': 'https://www.animeonegai.com/',
|
||||||
|
'Origin': 'https://www.animeonegai.com',
|
||||||
|
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const playbackReq = await this.req.getData(`https://api.animeonegai.com/v1/media/${media.videoId}?lang=${this.locale}`, AuthHeaders);
|
||||||
|
if(!playbackReq.ok || !playbackReq.res){
|
||||||
|
console.error('Request Stream URLs FAILED!');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const streamData = await playbackReq.res.json() as AnimeOnegaiStream;
|
||||||
|
|
||||||
|
variables.push(...([
|
||||||
|
['title', medias.episodeTitle, true],
|
||||||
|
['episode', isNaN(medias.episodeNumber) ? medias.episodeNumber : medias.episodeNumber, false],
|
||||||
|
['service', 'AO', false],
|
||||||
|
['seriesTitle', medias.seriesTitle, true],
|
||||||
|
['showTitle', medias.seasonTitle, true],
|
||||||
|
['season', medias.seasonNumber, 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;
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!canDecrypt) {
|
||||||
|
console.warn('Decryption not enabled!');
|
||||||
|
}
|
||||||
|
|
||||||
|
const lang = langsData.languages.find(a=>a.ao_locale == media.lang) as langsData.LanguageItem;
|
||||||
|
if (!lang) {
|
||||||
|
console.error(`Unable to find language for code ${media.lang}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let tsFile = undefined;
|
||||||
|
|
||||||
|
if (!streamData.dash) {
|
||||||
|
console.error('You don\'t have access to download this content');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info('Playlists URL: %s', streamData.dash);
|
||||||
|
|
||||||
|
if(!dlFailed && !(options.novids && options.noaudio)){
|
||||||
|
const streamPlaylistsReq = await this.req.getData(streamData.dash, AuthHeaders);
|
||||||
|
if(!streamPlaylistsReq.ok || !streamPlaylistsReq.res){
|
||||||
|
console.error('CAN\'T FETCH VIDEO PLAYLISTS!');
|
||||||
|
dlFailed = true;
|
||||||
|
} else {
|
||||||
|
if (!options.novids) {
|
||||||
|
const streamPlaylistBody = (await streamPlaylistsReq.res.text()).replace(/<BaseURL>(.*?)<\/BaseURL>/g, `<BaseURL>${streamData.dash.split('/dash/')[0]}/dash/$1</BaseURL>`);
|
||||||
|
fs.writeFileSync('test.mpd', streamPlaylistBody);
|
||||||
|
//Parse MPD Playlists
|
||||||
|
const streamPlaylists = await parse(streamPlaylistBody, lang as langsData.LanguageItem, streamData.dash.split('/dash/')[0]+'/dash/');
|
||||||
|
|
||||||
|
//Get name of CDNs/Servers
|
||||||
|
const streamServers = Object.keys(streamPlaylists);
|
||||||
|
|
||||||
|
options.x = options.x > streamServers.length ? 1 : options.x;
|
||||||
|
|
||||||
|
const selectedServer = streamServers[options.x - 1];
|
||||||
|
const selectedList = streamPlaylists[selectedServer];
|
||||||
|
|
||||||
|
//set Video Qualities
|
||||||
|
const videos = selectedList.video.map(item => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
resolutionText: `${item.quality.width}x${item.quality.height} (${Math.round(item.bandwidth/1024)}KiB/s)`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const audios = selectedList.audio.map(item => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
resolutionText: `${Math.round(item.bandwidth/1000)}kB/s`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
videos.sort((a, b) => {
|
||||||
|
return a.quality.width - b.quality.width;
|
||||||
|
});
|
||||||
|
|
||||||
|
audios.sort((a, b) => {
|
||||||
|
return a.bandwidth - b.bandwidth;
|
||||||
|
});
|
||||||
|
|
||||||
|
let chosenVideoQuality = options.q === 0 ? videos.length : options.q;
|
||||||
|
if(chosenVideoQuality > videos.length) {
|
||||||
|
console.warn(`The requested quality of ${options.q} is greater than the maximum ${videos.length}.\n[WARN] Therefor the maximum will be capped at ${videos.length}.`);
|
||||||
|
chosenVideoQuality = videos.length;
|
||||||
|
}
|
||||||
|
chosenVideoQuality--;
|
||||||
|
|
||||||
|
let chosenAudioQuality = options.q === 0 ? audios.length : options.q;
|
||||||
|
if(chosenAudioQuality > audios.length) {
|
||||||
|
chosenAudioQuality = audios.length;
|
||||||
|
}
|
||||||
|
chosenAudioQuality--;
|
||||||
|
|
||||||
|
const chosenVideoSegments = videos[chosenVideoQuality];
|
||||||
|
const chosenAudioSegments = audios[chosenAudioQuality];
|
||||||
|
|
||||||
|
console.info(`Servers available:\n\t${streamServers.join('\n\t')}`);
|
||||||
|
console.info(`Available Video Qualities:\n\t${videos.map((a, ind) => `[${ind+1}] ${a.resolutionText}`).join('\n\t')}`);
|
||||||
|
console.info(`Available Audio Qualities:\n\t${audios.map((a, ind) => `[${ind+1}] ${a.resolutionText}`).join('\n\t')}`);
|
||||||
|
|
||||||
|
variables.push({
|
||||||
|
name: 'height',
|
||||||
|
type: 'number',
|
||||||
|
replaceWith: chosenVideoSegments.quality.height
|
||||||
|
}, {
|
||||||
|
name: 'width',
|
||||||
|
type: 'number',
|
||||||
|
replaceWith: chosenVideoSegments.quality.width
|
||||||
|
});
|
||||||
|
|
||||||
|
console.info(`Selected quality: \n\tVideo: ${chosenVideoSegments.resolutionText}\n\tAudio: ${chosenAudioSegments.resolutionText}\n\tServer: ${selectedServer}`);
|
||||||
|
//console.info('Stream URL:', chosenVideoSegments.segments[0].uri);
|
||||||
|
// TODO check filename
|
||||||
|
fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep);
|
||||||
|
const outFile = parseFileName(options.fileName + '.' + lang.name, variables, options.numbers, options.override).join(path.sep);
|
||||||
|
const tempFile = parseFileName(`temp-${media.videoId}`, variables, options.numbers, options.override).join(path.sep);
|
||||||
|
const tempTsFile = path.isAbsolute(tempFile as string) ? tempFile : path.join(this.cfg.dir.content, tempFile);
|
||||||
|
|
||||||
|
let [audioDownloaded, videoDownloaded] = [false, false];
|
||||||
|
|
||||||
|
// When best selected video quality is already downloaded
|
||||||
|
if(dlVideoOnce && options.dlVideoOnce) {
|
||||||
|
console.info('Already downloaded video, skipping video download...');
|
||||||
|
} else if (options.novids) {
|
||||||
|
console.info('Skipping video download...');
|
||||||
|
} else {
|
||||||
|
//Download Video
|
||||||
|
const totalParts = chosenVideoSegments.segments.length;
|
||||||
|
const mathParts = Math.ceil(totalParts / options.partsize);
|
||||||
|
const mathMsg = `(${mathParts}*${options.partsize})`;
|
||||||
|
console.info('Total parts in video stream:', totalParts, mathMsg);
|
||||||
|
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 videoJson: M3U8Json = {
|
||||||
|
segments: chosenVideoSegments.segments
|
||||||
|
};
|
||||||
|
const videoDownload = await new streamdl({
|
||||||
|
output: chosenVideoSegments.pssh ? `${tempTsFile}.video.enc.mp4` : `${tsFile}.video.mp4`,
|
||||||
|
timeout: options.timeout,
|
||||||
|
m3u8json: videoJson,
|
||||||
|
// 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: medias.image,
|
||||||
|
parent: {
|
||||||
|
title: medias.seasonTitle
|
||||||
|
},
|
||||||
|
title: medias.episodeTitle,
|
||||||
|
language: lang
|
||||||
|
}) : undefined
|
||||||
|
}).download();
|
||||||
|
if(!videoDownload.ok){
|
||||||
|
console.error(`DL Stats: ${JSON.stringify(videoDownload.parts)}\n`);
|
||||||
|
dlFailed = true;
|
||||||
|
}
|
||||||
|
dlVideoOnce = true;
|
||||||
|
videoDownloaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chosenAudioSegments && !options.noaudio) {
|
||||||
|
//Download Audio (if available)
|
||||||
|
const totalParts = chosenAudioSegments.segments.length;
|
||||||
|
const mathParts = Math.ceil(totalParts / options.partsize);
|
||||||
|
const mathMsg = `(${mathParts}*${options.partsize})`;
|
||||||
|
console.info('Total parts in audio stream:', totalParts, mathMsg);
|
||||||
|
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 audioJson: M3U8Json = {
|
||||||
|
segments: chosenAudioSegments.segments
|
||||||
|
};
|
||||||
|
const audioDownload = await new streamdl({
|
||||||
|
output: chosenAudioSegments.pssh ? `${tempTsFile}.audio.enc.mp4` : `${tsFile}.audio.mp4`,
|
||||||
|
timeout: options.timeout,
|
||||||
|
m3u8json: audioJson,
|
||||||
|
// 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: medias.image,
|
||||||
|
parent: {
|
||||||
|
title: medias.seasonTitle
|
||||||
|
},
|
||||||
|
title: medias.episodeTitle,
|
||||||
|
language: lang
|
||||||
|
}) : undefined
|
||||||
|
}).download();
|
||||||
|
if(!audioDownload.ok){
|
||||||
|
console.error(`DL Stats: ${JSON.stringify(audioDownload.parts)}\n`);
|
||||||
|
dlFailed = true;
|
||||||
|
}
|
||||||
|
audioDownloaded = true;
|
||||||
|
} else if (options.noaudio) {
|
||||||
|
console.info('Skipping audio download...');
|
||||||
|
}
|
||||||
|
|
||||||
|
//Handle Decryption if needed
|
||||||
|
if ((chosenVideoSegments.pssh || chosenAudioSegments.pssh) && (videoDownloaded || audioDownloaded)) {
|
||||||
|
console.info('Decryption Needed, attempting to decrypt');
|
||||||
|
const encryptionKeys = await getKeys(chosenVideoSegments.pssh, streamData.widevine_proxy, {});
|
||||||
|
if (encryptionKeys.length == 0) {
|
||||||
|
console.error('Failed to get encryption keys');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
/*const keys = {} as Record<string, string>;
|
||||||
|
encryptionKeys.forEach(function(key) {
|
||||||
|
keys[key.kid] = key.key;
|
||||||
|
});*/
|
||||||
|
|
||||||
|
if (this.cfg.bin.mp4decrypt) {
|
||||||
|
const commandBase = `--show-progress --key ${encryptionKeys[1].kid}:${encryptionKeys[1].key} `;
|
||||||
|
const commandVideo = commandBase+`"${tempTsFile}.video.enc.mp4" "${tempTsFile}.video.mp4"`;
|
||||||
|
const commandAudio = commandBase+`"${tempTsFile}.audio.enc.mp4" "${tempTsFile}.audio.mp4"`;
|
||||||
|
|
||||||
|
if (videoDownloaded) {
|
||||||
|
console.info('Started decrypting video');
|
||||||
|
const decryptVideo = exec('mp4decrypt', `"${this.cfg.bin.mp4decrypt}"`, commandVideo);
|
||||||
|
if (!decryptVideo.isOk) {
|
||||||
|
console.error(decryptVideo.err);
|
||||||
|
console.error(`Decryption failed with exit code ${decryptVideo.err.code}`);
|
||||||
|
fs.renameSync(`${tempTsFile}.video.enc.mp4`, `${tsFile}.video.enc.mp4`);
|
||||||
|
return undefined;
|
||||||
|
} else {
|
||||||
|
console.info('Decryption done for video');
|
||||||
|
if (!options.nocleanup) {
|
||||||
|
fs.removeSync(`${tempTsFile}.video.enc.mp4`);
|
||||||
|
}
|
||||||
|
fs.renameSync(`${tempTsFile}.video.mp4`, `${tsFile}.video.mp4`);
|
||||||
|
files.push({
|
||||||
|
type: 'Video',
|
||||||
|
path: `${tsFile}.video.mp4`,
|
||||||
|
lang: lang
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioDownloaded) {
|
||||||
|
console.info('Started decrypting audio');
|
||||||
|
const decryptAudio = exec('mp4decrypt', `"${this.cfg.bin.mp4decrypt}"`, commandAudio);
|
||||||
|
if (!decryptAudio.isOk) {
|
||||||
|
console.error(decryptAudio.err);
|
||||||
|
console.error(`Decryption failed with exit code ${decryptAudio.err.code}`);
|
||||||
|
fs.renameSync(`${tempTsFile}.audio.enc.mp4`, `${tsFile}.audio.enc.mp4`);
|
||||||
|
return undefined;
|
||||||
|
} else {
|
||||||
|
if (!options.nocleanup) {
|
||||||
|
fs.removeSync(`${tempTsFile}.audio.enc.mp4`);
|
||||||
|
}
|
||||||
|
fs.renameSync(`${tempTsFile}.audio.mp4`, `${tsFile}.audio.mp4`);
|
||||||
|
files.push({
|
||||||
|
type: 'Audio',
|
||||||
|
path: `${tsFile}.audio.mp4`,
|
||||||
|
lang: lang
|
||||||
|
});
|
||||||
|
console.info('Decryption done for audio');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('mp4decrypt not found, files need decryption. Decryption Keys:', encryptionKeys);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (videoDownloaded) {
|
||||||
|
files.push({
|
||||||
|
type: 'Video',
|
||||||
|
path: `${tsFile}.video.mp4`,
|
||||||
|
lang: lang
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (audioDownloaded) {
|
||||||
|
files.push({
|
||||||
|
type: 'Audio',
|
||||||
|
path: `${tsFile}.audio.mp4`,
|
||||||
|
lang: lang
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep);
|
||||||
|
console.info('Downloading skipped!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (options.novids && options.noaudio) {
|
||||||
|
fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(streamData.subtitles.length > 0) {
|
||||||
|
for(const sub of streamData.subtitles) {
|
||||||
|
const subLang = langsData.languages.find(a => a.ao_locale === sub.lang);
|
||||||
|
if (!subLang) {
|
||||||
|
console.warn(`Language not found for subtitle language: ${sub.lang}, Skipping`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const sxData: Partial<sxItem> = {};
|
||||||
|
sxData.file = langsData.subsFile(fileName as string, subIndex+'', subLang, false, 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)) && sub.url.includes('.ass')) {
|
||||||
|
const getSubtitle = await this.req.getData(sub.url);
|
||||||
|
if (getSubtitle.ok && getSubtitle.res) {
|
||||||
|
console.info(`Subtitle Downloaded: ${sub.url}`);
|
||||||
|
const sBody = await getSubtitle.res.text();
|
||||||
|
sxData.title = `${subLang.language}`;
|
||||||
|
sxData.fonts = fontsData.assFonts(sBody) as Font[];
|
||||||
|
fs.writeFileSync(sxData.path, sBody);
|
||||||
|
files.push({
|
||||||
|
type: 'Subtitle',
|
||||||
|
...sxData as sxItem,
|
||||||
|
cc: false
|
||||||
|
});
|
||||||
|
} 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!');
|
||||||
|
}
|
||||||
|
await this.sleep(options.waittime);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
error: dlFailed,
|
||||||
|
data: files,
|
||||||
|
fileName: fileName ? (path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName)) || './unknown' : './unknown'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public sleep(ms: number) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
|
|
||||||
[](https://discord.gg/qEpbWen5vq)
|
[](https://discord.gg/qEpbWen5vq)
|
||||||
|
|
||||||
This downloader can download anime from different sites. Currently supported are *Funimation*, *Crunchyroll*, *AnimationDigitalNetwork*, and *Hidive*.
|
This downloader can download anime from different sites. Currently supported are *Crunchyroll*, *Hidive*, *AnimeOnegai*, and *AnimationDigitalNetwork*.
|
||||||
|
|
||||||
## Legal Warning
|
## Legal Warning
|
||||||
|
|
||||||
This application is not endorsed by or affiliated with *Funimation*, *Crunchyroll*, *AnimationDigitalNetwork*, 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*, *Hidive*, *AnimeOnegai*, or *AnimationDigitalNetwork*. 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
|
## Dependencies
|
||||||
|
|
||||||
|
|
@ -79,7 +79,6 @@ You can run the code from native TypeScript, this requires ts-node which you can
|
||||||
Afterwords, you can run the application like this:
|
Afterwords, you can run the application like this:
|
||||||
|
|
||||||
* CLI: `ts-node -T ./index.ts --help`
|
* CLI: `ts-node -T ./index.ts --help`
|
||||||
* GUI: `ts-node -T ./gui.ts`
|
|
||||||
|
|
||||||
### Run as JavaScript
|
### Run as JavaScript
|
||||||
|
|
||||||
|
|
|
||||||
923
funi.ts
923
funi.ts
|
|
@ -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<boolean|undefined> {
|
|
||||||
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<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.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<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') };
|
|
||||||
}
|
|
||||||
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<ResponseBase<Item[]>> {
|
|
||||||
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<FuniShowResponse> {
|
|
||||||
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<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: { 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<boolean|void> {
|
|
||||||
|
|
||||||
// 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<string|number, {
|
|
||||||
[key: string]: string
|
|
||||||
}> = {},
|
|
||||||
plLayersStr: string[] = [],
|
|
||||||
plLayersRes: Record<string|number, {
|
|
||||||
width: number,
|
|
||||||
height: number
|
|
||||||
}> = {};
|
|
||||||
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<string, unknown>[],
|
|
||||||
}, 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<Subtitle[]> {
|
|
||||||
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
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -23,12 +23,12 @@ const MenuBar: React.FC = () => {
|
||||||
switch(service) {
|
switch(service) {
|
||||||
case 'crunchy':
|
case 'crunchy':
|
||||||
return 'Crunchyroll';
|
return 'Crunchyroll';
|
||||||
case 'funi':
|
|
||||||
return 'Funimation';
|
|
||||||
case 'adn':
|
|
||||||
return 'AnimationDigitalNetwork';
|
|
||||||
case 'hidive':
|
case 'hidive':
|
||||||
return 'Hidive';
|
return 'Hidive';
|
||||||
|
case 'ao':
|
||||||
|
return 'AnimeOnegai';
|
||||||
|
case 'adn':
|
||||||
|
return 'AnimationDigitalNetwork';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import {Divider, Box, Button, Typography, Avatar} from '@mui/material';
|
||||||
import useStore from '../hooks/useStore';
|
import useStore from '../hooks/useStore';
|
||||||
import { StoreState } from './Store';
|
import { StoreState } from './Store';
|
||||||
|
|
||||||
type Services = 'adn'|'funi'|'crunchy'|'hidive';
|
type Services = 'crunchy'|'hidive'|'ao'|'adn';
|
||||||
|
|
||||||
export const serviceContext = React.createContext<Services|undefined>(undefined);
|
export const serviceContext = React.createContext<Services|undefined>(undefined);
|
||||||
|
|
||||||
|
|
@ -21,9 +21,9 @@ const ServiceProvider: FCWithChildren = ({ children }) => {
|
||||||
<Box sx={{ justifyContent: 'center', alignItems: 'center', display: 'flex', flexDirection: 'column', position: 'relative', top: '40vh'}}>
|
<Box sx={{ justifyContent: 'center', alignItems: 'center', display: 'flex', flexDirection: 'column', position: 'relative', top: '40vh'}}>
|
||||||
<Typography color="text.primary" variant='h3' sx={{ textAlign: 'center', mb: 5 }}>Please select your service</Typography>
|
<Typography color="text.primary" variant='h3' sx={{ textAlign: 'center', mb: 5 }}>Please select your service</Typography>
|
||||||
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'center' }}>
|
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'center' }}>
|
||||||
<Button size='large' variant="contained" onClick={() => setService('funi')} startIcon={<Avatar src={'https://static.funimation.com/static/img/favicon.ico'} />}>Funimation</Button>
|
|
||||||
<Button size='large' variant="contained" onClick={() => setService('crunchy')} startIcon={<Avatar src={'https://static.crunchyroll.com/cxweb/assets/img/favicons/favicon-32x32.png'} />}>Crunchyroll</Button>
|
<Button size='large' variant="contained" onClick={() => setService('crunchy')} startIcon={<Avatar src={'https://static.crunchyroll.com/cxweb/assets/img/favicons/favicon-32x32.png'} />}>Crunchyroll</Button>
|
||||||
<Button size='large' variant="contained" onClick={() => setService('hidive')} startIcon={<Avatar src={'https://static.diceplatform.com/prod/original/dce.hidive/settings/HIDIVE_AppLogo_1024x1024.0G0vK.jpg'} />}>Hidive</Button>
|
<Button size='large' variant="contained" onClick={() => setService('hidive')} startIcon={<Avatar src={'https://static.diceplatform.com/prod/original/dce.hidive/settings/HIDIVE_AppLogo_1024x1024.0G0vK.jpg'} />}>Hidive</Button>
|
||||||
|
<Button size='large' variant="contained" onClick={() => setService('ao')} startIcon={<Avatar src={'https://www.animeonegai.com/assets/img/anime/general/ao3-favicon.png'} />}>AnimeOnegai</Button>
|
||||||
<Button size='large' variant="contained" onClick={() => setService('adn')} startIcon={<Avatar src={'https://animationdigitalnetwork.de/favicon.ico'} />}>AnimationDigitalNetwork</Button>
|
<Button size='large' variant="contained" onClick={() => setService('adn')} startIcon={<Avatar src={'https://animationdigitalnetwork.de/favicon.ico'} />}>AnimationDigitalNetwork</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ export type DownloadOptions = {
|
||||||
export type StoreState = {
|
export type StoreState = {
|
||||||
episodeListing: Episode[];
|
episodeListing: Episode[];
|
||||||
downloadOptions: DownloadOptions,
|
downloadOptions: DownloadOptions,
|
||||||
service: 'crunchy'|'funi'|'hidive'|'adn'|undefined,
|
service: 'crunchy'|'hidive'|'ao'|'adn'|undefined,
|
||||||
version: string,
|
version: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ import { IncomingMessage } from 'http';
|
||||||
import { MessageHandler, GuiState } from '../../@types/messageHandler';
|
import { MessageHandler, GuiState } from '../../@types/messageHandler';
|
||||||
import { setState, getState, writeYamlCfgFile } from '../../modules/module.cfg-loader';
|
import { setState, getState, writeYamlCfgFile } from '../../modules/module.cfg-loader';
|
||||||
import CrunchyHandler from './services/crunchyroll';
|
import CrunchyHandler from './services/crunchyroll';
|
||||||
import FunimationHandler from './services/funimation';
|
|
||||||
import HidiveHandler from './services/hidive';
|
import HidiveHandler from './services/hidive';
|
||||||
|
import AnimeOnegaiHandler from './services/animeonegai';
|
||||||
import ADNHandler from './services/adn';
|
import ADNHandler from './services/adn';
|
||||||
import WebSocketHandler from './websocket';
|
import WebSocketHandler from './websocket';
|
||||||
import packageJson from '../../package.json';
|
import packageJson from '../../package.json';
|
||||||
|
|
@ -32,12 +32,12 @@ export default class ServiceHandler {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.ws.events.on('setup', ({ data }) => {
|
this.ws.events.on('setup', ({ data }) => {
|
||||||
if (data === 'funi') {
|
if (data === 'crunchy') {
|
||||||
this.service = new FunimationHandler(this.ws);
|
|
||||||
} else if (data === 'crunchy') {
|
|
||||||
this.service = new CrunchyHandler(this.ws);
|
this.service = new CrunchyHandler(this.ws);
|
||||||
} else if (data === 'hidive') {
|
} else if (data === 'hidive') {
|
||||||
this.service = new HidiveHandler(this.ws);
|
this.service = new HidiveHandler(this.ws);
|
||||||
|
} else if (data === 'ao') {
|
||||||
|
this.service = new AnimeOnegaiHandler(this.ws);
|
||||||
} else if (data === 'adn') {
|
} else if (data === 'adn') {
|
||||||
this.service = new ADNHandler(this.ws);
|
this.service = new ADNHandler(this.ws);
|
||||||
}
|
}
|
||||||
|
|
@ -58,7 +58,7 @@ export default class ServiceHandler {
|
||||||
this.ws.events.on('version', async (_, respond) => {
|
this.ws.events.on('version', async (_, respond) => {
|
||||||
respond(packageJson.version);
|
respond(packageJson.version);
|
||||||
});
|
});
|
||||||
this.ws.events.on('type', async (_, respond) => respond(this.service === undefined ? undefined : this.service.name as 'hidive'|'crunchy'|'funi'|'adn'));
|
this.ws.events.on('type', async (_, respond) => respond(this.service === undefined ? undefined : this.service.name as 'hidive'|'crunchy'|'ao'|'adn'));
|
||||||
this.ws.events.on('checkToken', async (_, respond) => {
|
this.ws.events.on('checkToken', async (_, respond) => {
|
||||||
if (this.service === undefined)
|
if (this.service === undefined)
|
||||||
return respond({ isOk: false, reason: new Error('No service selected') });
|
return respond({ isOk: false, reason: new Error('No service selected') });
|
||||||
|
|
|
||||||
150
gui/server/services/animeonegai.ts
Normal file
150
gui/server/services/animeonegai.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
import { AuthData, CheckTokenResponse, DownloadData, Episode, EpisodeListResponse, MessageHandler, ResolveItemsData, SearchData, SearchResponse } from '../../../@types/messageHandler';
|
||||||
|
import AnimeOnegai from '../../../ao';
|
||||||
|
import { getDefault } from '../../../modules/module.args';
|
||||||
|
import { languages } 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 AnimeOnegaiHandler extends Base implements MessageHandler {
|
||||||
|
private ao: AnimeOnegai;
|
||||||
|
public name = 'ao';
|
||||||
|
constructor(ws: WebSocketHandler) {
|
||||||
|
super(ws);
|
||||||
|
this.ao = new AnimeOnegai();
|
||||||
|
this.initState();
|
||||||
|
this.getDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getDefaults() {
|
||||||
|
const _default = yargs.appArgv(this.ao.cfg.cli, true);
|
||||||
|
this.ao.locale = _default.locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async auth(data: AuthData) {
|
||||||
|
return this.ao.doAuth(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async checkToken(): Promise<CheckTokenResponse> {
|
||||||
|
//TODO: implement proper method to check token
|
||||||
|
return { isOk: true, value: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async search(data: SearchData): Promise<SearchResponse> {
|
||||||
|
console.debug(`Got search options: ${JSON.stringify(data)}`);
|
||||||
|
const search = await this.ao.doSearch(data);
|
||||||
|
if (!search.isOk) {
|
||||||
|
return search;
|
||||||
|
}
|
||||||
|
return { isOk: true, value: search.value };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handleDefault(name: string) {
|
||||||
|
return getDefault(name, this.ao.cfg.cli);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async availableDubCodes(): Promise<string[]> {
|
||||||
|
const dubLanguageCodesArray: string[] = [];
|
||||||
|
for(const language of languages){
|
||||||
|
if (language.ao_locale)
|
||||||
|
dubLanguageCodesArray.push(language.code);
|
||||||
|
}
|
||||||
|
return [...new Set(dubLanguageCodesArray)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async availableSubCodes(): Promise<string[]> {
|
||||||
|
const subLanguageCodesArray: string[] = [];
|
||||||
|
for(const language of languages){
|
||||||
|
if (language.ao_locale)
|
||||||
|
subLanguageCodesArray.push(language.locale);
|
||||||
|
}
|
||||||
|
return ['all', 'none', ...new Set(subLanguageCodesArray)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async resolveItems(data: ResolveItemsData): Promise<boolean> {
|
||||||
|
const parse = parseInt(data.id);
|
||||||
|
if (isNaN(parse) || parse <= 0)
|
||||||
|
return false;
|
||||||
|
console.debug(`Got resolve options: ${JSON.stringify(data)}`);
|
||||||
|
const _default = yargs.appArgv(this.ao.cfg.cli, true);
|
||||||
|
const res = await this.ao.selectShow(parseInt(data.id), data.e, data.but, data.all, _default);
|
||||||
|
if (!res.isOk || !res.value)
|
||||||
|
return res.isOk;
|
||||||
|
this.addToQueue(res.value.map(a => {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
ids: a.data.map(a => a.videoId),
|
||||||
|
title: a.episodeTitle,
|
||||||
|
parent: {
|
||||||
|
title: a.seasonTitle,
|
||||||
|
season: a.seasonTitle
|
||||||
|
},
|
||||||
|
e: a.episodeNumber+'',
|
||||||
|
image: a.image,
|
||||||
|
episode: a.episodeNumber+''
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async listEpisodes(id: string): Promise<EpisodeListResponse> {
|
||||||
|
const parse = parseInt(id);
|
||||||
|
if (isNaN(parse) || parse <= 0)
|
||||||
|
return { isOk: false, reason: new Error('The ID is invalid') };
|
||||||
|
|
||||||
|
const request = await this.ao.listShow(parse);
|
||||||
|
if (!request.isOk || !request.value)
|
||||||
|
return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')};
|
||||||
|
|
||||||
|
const episodes: Episode[] = [];
|
||||||
|
const seasonNumberTitleParse = request.series.data.title.match(/\d+$/);
|
||||||
|
const seasonNumber = seasonNumberTitleParse ? parseInt(seasonNumberTitleParse[0]) : 1;
|
||||||
|
//request.value
|
||||||
|
for (const episodeKey in request.value) {
|
||||||
|
const episode = request.value[episodeKey][0];
|
||||||
|
const langs = Array.from(new Set(request.value[episodeKey].map(a=>a.lang)));
|
||||||
|
episodes.push({
|
||||||
|
e: episode.number+'',
|
||||||
|
lang: langs as string[],
|
||||||
|
name: episode.name,
|
||||||
|
season: seasonNumber+'',
|
||||||
|
seasonTitle: '',
|
||||||
|
episode: episode.number+'',
|
||||||
|
id: episode.video_entry+'',
|
||||||
|
img: episode.thumbnail,
|
||||||
|
description: episode.description,
|
||||||
|
time: ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { isOk: true, value: episodes };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async downloadItem(data: DownloadData) {
|
||||||
|
this.setDownloading(true);
|
||||||
|
console.debug(`Got download options: ${JSON.stringify(data)}`);
|
||||||
|
const _default = yargs.appArgv(this.ao.cfg.cli, true);
|
||||||
|
const res = await this.ao.selectShow(parseInt(data.id), data.e, false, false, {
|
||||||
|
..._default,
|
||||||
|
dubLang: data.dubLang,
|
||||||
|
e: data.e
|
||||||
|
});
|
||||||
|
if (res.isOk) {
|
||||||
|
for (const select of res.value) {
|
||||||
|
if (!(await this.ao.downloadEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y',
|
||||||
|
novids: data.novids, hslang: data.hslang || 'none' }))) {
|
||||||
|
const er = new Error(`Unable to download episode ${data.e} from ${data.id}`);
|
||||||
|
er.name = 'Download error';
|
||||||
|
this.alertError(er);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.alertError(new Error('Failed to download episode, check for additional logs.'));
|
||||||
|
}
|
||||||
|
this.sendMessage({ name: 'finish', data: undefined });
|
||||||
|
this.setDownloading(false);
|
||||||
|
this.onFinish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AnimeOnegaiHandler;
|
||||||
|
|
@ -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<EpisodeListResponse> {
|
|
||||||
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<string[]> {
|
|
||||||
const dubLanguageCodesArray: string[] = [];
|
|
||||||
for(const language of languages){
|
|
||||||
if (language.funi_locale)
|
|
||||||
dubLanguageCodesArray.push(language.code);
|
|
||||||
}
|
|
||||||
return [...new Set(dubLanguageCodesArray)];
|
|
||||||
}
|
|
||||||
|
|
||||||
public async availableSubCodes(): Promise<string[]> {
|
|
||||||
return subtitleLanguagesFilter;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async resolveItems(data: ResolveItemsData): Promise<boolean> {
|
|
||||||
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<SearchResponse> {
|
|
||||||
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<CheckTokenResponse> {
|
|
||||||
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;
|
|
||||||
|
|
@ -13,12 +13,10 @@ class HidiveHandler extends Base implements MessageHandler {
|
||||||
constructor(ws: WebSocketHandler) {
|
constructor(ws: WebSocketHandler) {
|
||||||
super(ws);
|
super(ws);
|
||||||
this.hidive = new Hidive();
|
this.hidive = new Hidive();
|
||||||
this.hidive.doInit();
|
|
||||||
this.initState();
|
this.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async auth(data: AuthData) {
|
public async auth(data: AuthData) {
|
||||||
await this.getAPIVersion();
|
|
||||||
return this.hidive.doAuth(data);
|
return this.hidive.doAuth(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,13 +25,7 @@ class HidiveHandler extends Base implements MessageHandler {
|
||||||
return { isOk: true, value: undefined };
|
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<SearchResponse> {
|
public async search(data: SearchData): Promise<SearchResponse> {
|
||||||
await this.getAPIVersion();
|
|
||||||
console.debug(`Got search options: ${JSON.stringify(data)}`);
|
console.debug(`Got search options: ${JSON.stringify(data)}`);
|
||||||
const hidiveSearch = await this.hidive.doSearch(data);
|
const hidiveSearch = await this.hidive.doSearch(data);
|
||||||
if (!hidiveSearch.isOk) {
|
if (!hidiveSearch.isOk) {
|
||||||
|
|
@ -69,46 +61,24 @@ class HidiveHandler extends Base implements MessageHandler {
|
||||||
if (isNaN(parse) || parse <= 0)
|
if (isNaN(parse) || parse <= 0)
|
||||||
return false;
|
return false;
|
||||||
console.debug(`Got resolve options: ${JSON.stringify(data)}`);
|
console.debug(`Got resolve options: ${JSON.stringify(data)}`);
|
||||||
await this.getAPIVersion();
|
const res = await this.hidive.selectSeries(parseInt(data.id), data.e, data.but, data.all);
|
||||||
if (this.hidive.api == 'old') {
|
if (!res.isOk || !res.value)
|
||||||
const res = await this.hidive.getShow(parseInt(data.id), data.e, data.but, data.all);
|
return res.isOk;
|
||||||
if (!res.isOk || !res.value)
|
this.addToQueue(res.value.map(item => {
|
||||||
return res.isOk;
|
return {
|
||||||
this.addToQueue(res.value.map(item => {
|
...data,
|
||||||
return {
|
ids: [item.id],
|
||||||
...data,
|
title: item.title,
|
||||||
ids: [item.Id],
|
parent: {
|
||||||
title: item.Name,
|
title: item.seriesTitle,
|
||||||
parent: {
|
season: item.episodeInformation.seasonNumber+''
|
||||||
title: item.seriesTitle,
|
},
|
||||||
season: parseFloat(item.SeasonNumberValue+'')+''
|
image: item.thumbnailUrl,
|
||||||
},
|
e: item.episodeInformation.episodeNumber+'',
|
||||||
image: item.ScreenShotSmallUrl,
|
episode: item.episodeInformation.episodeNumber+'',
|
||||||
e: parseFloat(item.EpisodeNumberValue+'')+'',
|
};
|
||||||
episode: parseFloat(item.EpisodeNumberValue+'')+'',
|
}));
|
||||||
};
|
return true;
|
||||||
}));
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async listEpisodes(id: string): Promise<EpisodeListResponse> {
|
public async listEpisodes(id: string): Promise<EpisodeListResponse> {
|
||||||
|
|
@ -116,73 +86,37 @@ class HidiveHandler extends Base implements MessageHandler {
|
||||||
if (isNaN(parse) || parse <= 0)
|
if (isNaN(parse) || parse <= 0)
|
||||||
return { isOk: false, reason: new Error('The ID is invalid') };
|
return { isOk: false, reason: new Error('The ID is invalid') };
|
||||||
|
|
||||||
await this.getAPIVersion();
|
const request = await this.hidive.listSeries(parse);
|
||||||
if (this.hidive.api == 'old') {
|
if (!request.isOk || !request.value)
|
||||||
const request = await this.hidive.listShow(parse);
|
return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')};
|
||||||
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) {
|
return { isOk: true, value: request.value.map(function(item) {
|
||||||
const language = item.Summary.match(/^Audio: (.*)/m);
|
const description = item.description.split('\r\n');
|
||||||
language?.shift();
|
return {
|
||||||
const description = item.Summary.split('\r\n');
|
e: item.episodeInformation.episodeNumber+'',
|
||||||
return {
|
lang: [],
|
||||||
e: parseFloat(item.EpisodeNumberValue+'')+'',
|
name: item.title,
|
||||||
lang: language ? language[0].split(', ') : [],
|
season: item.episodeInformation.seasonNumber+'',
|
||||||
name: item.Name,
|
seasonTitle: request.series.seasons[item.episodeInformation.seasonNumber-1].title,
|
||||||
season: parseFloat(item.SeasonNumberValue+'')+'',
|
episode: item.episodeInformation.episodeNumber+'',
|
||||||
seasonTitle: request.value.Name,
|
id: item.id+'',
|
||||||
episode: parseFloat(item.EpisodeNumberValue+'')+'',
|
img: item.thumbnailUrl,
|
||||||
id: item.Id+'',
|
description: description ? description[0] : '',
|
||||||
img: item.ScreenShotSmallUrl,
|
time: ''
|
||||||
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: ''
|
|
||||||
};
|
|
||||||
})};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async downloadItem(data: DownloadData) {
|
public async downloadItem(data: DownloadData) {
|
||||||
this.setDownloading(true);
|
this.setDownloading(true);
|
||||||
console.debug(`Got download options: ${JSON.stringify(data)}`);
|
console.debug(`Got download options: ${JSON.stringify(data)}`);
|
||||||
const _default = yargs.appArgv(this.hidive.cfg.cli, true);
|
const _default = yargs.appArgv(this.hidive.cfg.cli, true);
|
||||||
this.hidive.api = _default.hdapi;
|
const res = await this.hidive.selectSeries(parseInt(data.id), data.e, false, false);
|
||||||
if (this.hidive.api == 'old') {
|
if (!res.isOk || !res.showData)
|
||||||
const res = await this.hidive.getShow(parseInt(data.id), data.e, false, false);
|
return this.alertError(new Error('Download failed upstream, check for additional logs'));
|
||||||
if (!res.isOk || !res.showData)
|
|
||||||
return this.alertError(new Error('Download failed upstream, check for additional logs'));
|
|
||||||
|
|
||||||
for (const ep of res.value) {
|
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 });
|
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 });
|
||||||
}
|
|
||||||
} 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 });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.sendMessage({ name: 'finish', data: undefined });
|
this.sendMessage({ name: 'finish', data: undefined });
|
||||||
this.setDownloading(false);
|
this.setDownloading(false);
|
||||||
|
|
|
||||||
868
hidive.ts
868
hidive.ts
|
|
@ -1,7 +1,6 @@
|
||||||
// build-in
|
// build-in
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import crypto from 'crypto';
|
|
||||||
|
|
||||||
// package program
|
// package program
|
||||||
import packageJson from './package.json';
|
import packageJson from './package.json';
|
||||||
|
|
@ -9,7 +8,6 @@ import packageJson from './package.json';
|
||||||
// plugins
|
// plugins
|
||||||
import { console } from './modules/log';
|
import { console } from './modules/log';
|
||||||
import shlp from 'sei-helper';
|
import shlp from 'sei-helper';
|
||||||
import m3u8 from 'm3u8-parsed';
|
|
||||||
import streamdl, { M3U8Json } from './modules/hls-download';
|
import streamdl, { M3U8Json } from './modules/hls-download';
|
||||||
|
|
||||||
// custom modules
|
// custom modules
|
||||||
|
|
@ -23,8 +21,7 @@ import vtt2ass from './modules/module.vtt2ass';
|
||||||
// load req
|
// load req
|
||||||
import { domain, api } from './modules/module.api-urls';
|
import { domain, api } from './modules/module.api-urls';
|
||||||
import * as reqModule from './modules/module.req';
|
import * as reqModule from './modules/module.req';
|
||||||
import { HidiveEpisodeList, HidiveEpisodeExtra } from './@types/hidiveEpisodeList';
|
import { DownloadedMedia } from './@types/hidiveTypes';
|
||||||
import { HidiveVideoList, HidiveStreamInfo, DownloadedMedia, HidiveSubtitleInfo } from './@types/hidiveTypes';
|
|
||||||
import parseFileName, { Variable } from './modules/module.filename';
|
import parseFileName, { Variable } from './modules/module.filename';
|
||||||
import { downloaded } from './modules/module.downloadArchive';
|
import { downloaded } from './modules/module.downloadArchive';
|
||||||
import parseSelect from './modules/module.parseSelect';
|
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 { AuthData, AuthResponse, SearchData, SearchResponse, SearchResponseItem } from './@types/messageHandler';
|
||||||
import { ServiceClass } from './@types/serviceClassInterface';
|
import { ServiceClass } from './@types/serviceClassInterface';
|
||||||
import { sxItem } from './crunchy';
|
import { sxItem } from './crunchy';
|
||||||
import { HidiveSearch } from './@types/hidiveSearch';
|
|
||||||
import { HidiveDashboard } from './@types/hidiveDashboard';
|
|
||||||
import { Hit, NewHidiveSearch } from './@types/newHidiveSearch';
|
import { Hit, NewHidiveSearch } from './@types/newHidiveSearch';
|
||||||
import { NewHidiveSeries } from './@types/newHidiveSeries';
|
import { NewHidiveSeries } from './@types/newHidiveSeries';
|
||||||
import { Episode, NewHidiveEpisodeExtra, NewHidiveSeason, NewHidiveSeriesExtra } from './@types/newHidiveSeason';
|
import { Episode, NewHidiveEpisodeExtra, NewHidiveSeason, NewHidiveSeriesExtra } from './@types/newHidiveSeason';
|
||||||
|
|
@ -46,59 +41,27 @@ import { KeyContainer } from './modules/license';
|
||||||
|
|
||||||
export default class Hidive implements ServiceClass {
|
export default class Hidive implements ServiceClass {
|
||||||
public cfg: yamlCfg.ConfigObject;
|
public cfg: yamlCfg.ConfigObject;
|
||||||
private session: Record<string, any>;
|
|
||||||
private tokenOld: Record<string, any>;
|
|
||||||
private token: Record<string, any>;
|
private token: Record<string, any>;
|
||||||
private req: reqModule.Req;
|
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) {
|
constructor(private debug = false) {
|
||||||
this.cfg = yamlCfg.loadCfg();
|
this.cfg = yamlCfg.loadCfg();
|
||||||
this.session = yamlCfg.loadHDSession();
|
|
||||||
this.tokenOld = yamlCfg.loadHDToken();
|
|
||||||
this.token = yamlCfg.loadNewHDToken();
|
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.req = new reqModule.Req(domain, debug, false, 'hd');
|
||||||
this.api = 'old';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async cli() {
|
public async cli() {
|
||||||
console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`);
|
console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`);
|
||||||
const argv = yargs.appArgv(this.cfg.cli);
|
const argv = yargs.appArgv(this.cfg.cli);
|
||||||
this.api = argv.hdapi;
|
|
||||||
if (argv.debug)
|
if (argv.debug)
|
||||||
this.debug = true;
|
this.debug = true;
|
||||||
|
|
||||||
//below is for quickly testing API calls
|
//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 apiTest = await this.apiReq('/v4/season/18871', '', 'auth', 'GET');
|
||||||
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');
|
|
||||||
if(!apiTest.ok || !apiTest.res){return;}
|
if(!apiTest.ok || !apiTest.res){return;}
|
||||||
console.info(apiTest.res.body);
|
console.info(apiTest.res.body);
|
||||||
fs.writeFileSync('apitest.json', JSON.stringify(JSON.parse(apiTest.res.body), null, 2));
|
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
|
// load binaries
|
||||||
this.cfg.bin = await yamlCfg.loadBinCfg();
|
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||||
|
|
@ -106,42 +69,21 @@ export default class Hidive implements ServiceClass {
|
||||||
argv.dubLang = langsData.dubLanguageCodes;
|
argv.dubLang = langsData.dubLanguageCodes;
|
||||||
}
|
}
|
||||||
if (argv.auth) {
|
if (argv.auth) {
|
||||||
//Initilize session
|
|
||||||
await this.doInit();
|
|
||||||
//Authenticate
|
//Authenticate
|
||||||
await this.doAuth({
|
await this.doAuth({
|
||||||
username: argv.username ?? await shlp.question('[Q] LOGIN/EMAIL'),
|
username: argv.username ?? await shlp.question('[Q] LOGIN/EMAIL'),
|
||||||
password: argv.password ?? await shlp.question('[Q] PASSWORD ')
|
password: argv.password ?? await shlp.question('[Q] PASSWORD ')
|
||||||
});
|
});
|
||||||
} else if (argv.search && argv.search.length > 2){
|
} else if (argv.search && argv.search.length > 2){
|
||||||
//Initilize session
|
|
||||||
await this.doInit();
|
|
||||||
//Search
|
|
||||||
await this.doSearch({ ...argv, search: argv.search as string });
|
await this.doSearch({ ...argv, search: argv.search as string });
|
||||||
} else if (argv.s && !isNaN(parseInt(argv.s,10)) && parseInt(argv.s,10) > 0) {
|
} else if (argv.s && !isNaN(parseInt(argv.s,10)) && parseInt(argv.s,10) > 0) {
|
||||||
if (this.api == 'old') {
|
const selected = await this.selectSeason(parseInt(argv.s), argv.e, argv.but, argv.all);
|
||||||
//Initilize session
|
if (selected.isOk && selected.showData) {
|
||||||
await this.doInit();
|
for (const select of selected.value) {
|
||||||
//get selected episodes
|
//download episode
|
||||||
const selected = await this.getShow(parseInt(argv.s), argv.e, argv.but, argv.all);
|
if (!(await this.downloadEpisode(select, {...argv}))) {
|
||||||
if (selected.isOk && selected.showData) {
|
console.error(`Unable to download selected episode ${select.episodeInformation.episodeNumber}`);
|
||||||
for (const select of selected.value) {
|
return false;
|
||||||
//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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -158,209 +100,17 @@ export default class Hidive implements ServiceClass {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (argv.new) {
|
} else if (argv.new) {
|
||||||
if (this.api == 'old') {
|
console.error('--new is not yet implemented in the new API');
|
||||||
//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');
|
|
||||||
}
|
|
||||||
} else if(argv.e) {
|
} else if(argv.e) {
|
||||||
if (this.api == 'new') {
|
if (!(await this.downloadSingleEpisode(parseInt(argv.e), {...argv}))) {
|
||||||
if (!(await this.downloadSingleEpisode(parseInt(argv.e), {...argv}))) {
|
console.error(`Unable to download selected episode ${argv.e}`);
|
||||||
console.error(`Unable to download selected episode ${argv.e}`);
|
return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error('-e is not supported in the old API');
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.info('No option selected or invalid value entered. Try --help.');
|
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<string, any>) {
|
|
||||||
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<string, any>, keys: Array<string>) {
|
|
||||||
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<string, unknown>,
|
|
||||||
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<string, string>,
|
|
||||||
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<string, string>);
|
|
||||||
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) {
|
public async apiReq(endpoint: string, body: string | object = '', authType: 'refresh' | 'auth' | 'both' | 'other' | 'none' = 'none', method: 'GET' | 'POST' = 'POST', authHeader?: string) {
|
||||||
const options = {
|
const options = {
|
||||||
|
|
@ -447,43 +197,25 @@ export default class Hidive implements ServiceClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async doAuth(data: AuthData): Promise<AuthResponse> {
|
public async doAuth(data: AuthData): Promise<AuthResponse> {
|
||||||
if (this.api == 'old') {
|
if (!this.token.refreshToken || !this.token.authorisationToken) {
|
||||||
const auth = await this.reqData('Authenticate', {'Email':data.username,'Password':data.password});
|
await this.doAnonymousAuth();
|
||||||
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<string, string> = 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 };
|
|
||||||
}
|
}
|
||||||
|
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<string, string> = 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() {
|
public async doAnonymousAuth() {
|
||||||
|
|
@ -540,110 +272,48 @@ export default class Hidive implements ServiceClass {
|
||||||
return true;
|
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<SearchResponse> {
|
public async doSearch(data: SearchData): Promise<SearchResponse> {
|
||||||
if (this.api == 'old') {
|
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', {
|
||||||
const searchReq = await this.reqData('Search', {'Query':data.search});
|
method: 'POST',
|
||||||
if(!searchReq.ok || !searchReq.res){
|
body: JSON.stringify({'requests':
|
||||||
console.error('Search FAILED!');
|
[
|
||||||
return { isOk: false, reason: new Error('Search failed. No more information provided') };
|
{'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) : '')},
|
||||||
const searchData = JSON.parse(searchReq.res.body) as HidiveSearch;
|
{'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) : '')},
|
||||||
const searchItems = searchData.Data.TitleResults;
|
{'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(searchItems.length>0) {
|
]
|
||||||
console.info('[INFO] Search Results:');
|
})
|
||||||
for(let i=0;i<searchItems.length;i++){
|
});
|
||||||
console.info(`[#${searchItems[i].Id}] ${searchItems[i].Name} [${searchItems[i].ShowInfoTitle}]`);
|
if(!searchReq.ok || !searchReq.res){
|
||||||
}
|
console.error('Search FAILED!');
|
||||||
} else{
|
return { isOk: false, reason: new Error('Search failed. No more information provided') };
|
||||||
console.warn('Nothing found!');
|
|
||||||
}
|
|
||||||
return { isOk: true, value: searchItems.map((a): SearchResponseItem => {
|
|
||||||
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 searchData = JSON.parse(searchReq.res.body) as NewHidiveSearch;
|
||||||
|
const searchItems: Hit[] = [];
|
||||||
public async getNewlyAdded(page?: number) {
|
console.info('Search Results:');
|
||||||
const pageNum = page ? page : 1;
|
for (const category of searchData.results) {
|
||||||
const dashboardReq = await this.reqData('GetDashboard', {'Pager': {'Number': pageNum, 'Size': 30}, 'Verbose': false});
|
for (const hit of category.hits) {
|
||||||
if(!dashboardReq.ok || !dashboardReq.res) {
|
searchItems.push(hit);
|
||||||
console.error('Search for new episodes FAILED!');
|
let fullType: string;
|
||||||
return;
|
if (hit.type == 'VOD_SERIES') {
|
||||||
}
|
fullType = `Z.${hit.id}`;
|
||||||
|
} else if (hit.type == 'VOD_VIDEO') {
|
||||||
const dashboardData = JSON.parse(dashboardReq.res.body) as HidiveDashboard;
|
fullType = `E.${hit.id}`;
|
||||||
const dashboardItems = dashboardData.Data.TitleRows;
|
} else {
|
||||||
const recentlyAddedIndex = dashboardItems.findIndex(item => item.Name == 'Recently Added');
|
fullType = `${hit.type} #${hit.id}`;
|
||||||
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<searchItems.length;i++){
|
|
||||||
console.info(`[#${searchItems[i].Id}] ${searchItems[i].Name} [${searchItems[i].ShowInfoTitle}]`);
|
|
||||||
}
|
}
|
||||||
} else{
|
console.log(`[${fullType}] ${hit.name} ${hit.seasonsCount ? '('+hit.seasonsCount+' Seasons)' : ''}`);
|
||||||
console.warn('No new episodes found!');
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.warn('New episode category not found!');
|
|
||||||
}
|
}
|
||||||
|
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
|
||||||
|
};
|
||||||
|
})};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getSeries(id: number) {
|
public async getSeries(id: number) {
|
||||||
|
|
@ -827,152 +497,6 @@ export default class Hidive implements ServiceClass {
|
||||||
return { isOk: true, value: selEpsArr, showData: getShowData.series };
|
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<any, any>) {
|
|
||||||
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<any, any>) {
|
public async downloadEpisode(selectedEpisode: NewHidiveEpisodeExtra, options: Record<any, any>) {
|
||||||
//Get Episode data
|
//Get Episode data
|
||||||
const episodeDataReq = await this.apiReq(`/v4/vod/${selectedEpisode.id}?includePlaybackDetails=URL`, '', 'auth', 'GET');
|
const episodeDataReq = await this.apiReq(`/v4/vod/${selectedEpisode.id}?includePlaybackDetails=URL`, '', 'auth', 'GET');
|
||||||
|
|
@ -1462,260 +986,6 @@ export default class Hidive implements ServiceClass {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async downloadMediaList(videoUrls: HidiveStreamInfo[], subUrls: HidiveSubtitleInfo[], fontSize: number, options: Record<any, any>) {
|
|
||||||
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<string, Record<string, string>> = {},
|
|
||||||
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
|
|
||||||
fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep);
|
|
||||||
const outFile = parseFileName(options.fileName + '.' + lang.name + '.' + videoIndex, 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<sxItem> = {};
|
|
||||||
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<any, any>, inverseTrackOrder: boolean = true) {
|
public async muxStreams(data: DownloadedMedia[], options: Record<any, any>, inverseTrackOrder: boolean = true) {
|
||||||
this.cfg.bin = await yamlCfg.loadBinCfg();
|
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||||
let hasAudioStreams = false;
|
let hasAudioStreams = false;
|
||||||
|
|
|
||||||
33
index.ts
33
index.ts
|
|
@ -18,15 +18,7 @@ import update from './modules/module.updater';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (argv.addArchive) {
|
if (argv.addArchive) {
|
||||||
if (argv.service === 'funi') {
|
if (argv.service === 'crunchy') {
|
||||||
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.s === undefined && argv.series === undefined)
|
if (argv.s === undefined && argv.series === undefined)
|
||||||
return console.error('`-s` or `--srz` not found');
|
return console.error('`-s` or `--srz` not found');
|
||||||
if (argv.s && argv.series)
|
if (argv.s && argv.series)
|
||||||
|
|
@ -45,6 +37,15 @@ import update from './modules/module.updater';
|
||||||
type: 's'
|
type: 's'
|
||||||
}, (argv.s === undefined ? argv.series : argv.s) as string);
|
}, (argv.s === undefined ? argv.series : argv.s) as string);
|
||||||
console.info('Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s));
|
console.info('Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s));
|
||||||
|
} else if (argv.service === 'ao') {
|
||||||
|
if (argv.s === undefined)
|
||||||
|
return console.error('`-s` not found');
|
||||||
|
addToArchive({
|
||||||
|
service: 'hidive',
|
||||||
|
//type: argv.s === undefined ? 'srz' : 's'
|
||||||
|
type: 's'
|
||||||
|
}, (argv.s === undefined ? argv.series : argv.s) as string);
|
||||||
|
console.info('Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s));
|
||||||
}
|
}
|
||||||
} else if (argv.downloadArchive) {
|
} else if (argv.downloadArchive) {
|
||||||
const ids = makeCommand(argv.service);
|
const ids = makeCommand(argv.service);
|
||||||
|
|
@ -52,20 +53,20 @@ import update from './modules/module.updater';
|
||||||
overrideArguments(cfg.cli, id);
|
overrideArguments(cfg.cli, id);
|
||||||
/* Reimport module to override appArgv */
|
/* Reimport module to override appArgv */
|
||||||
Object.keys(require.cache).forEach(key => {
|
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') || key.endsWith('ao.js'))
|
||||||
delete require.cache[key];
|
delete require.cache[key];
|
||||||
});
|
});
|
||||||
let service: ServiceClass;
|
let service: ServiceClass;
|
||||||
switch(argv.service) {
|
switch(argv.service) {
|
||||||
case 'funi':
|
|
||||||
service = new (await import('./funi')).default;
|
|
||||||
break;
|
|
||||||
case 'crunchy':
|
case 'crunchy':
|
||||||
service = new (await import('./crunchy')).default;
|
service = new (await import('./crunchy')).default;
|
||||||
break;
|
break;
|
||||||
case 'hidive':
|
case 'hidive':
|
||||||
service = new (await import('./hidive')).default;
|
service = new (await import('./hidive')).default;
|
||||||
break;
|
break;
|
||||||
|
case 'ao':
|
||||||
|
service = new (await import('./ao')).default;
|
||||||
|
break;
|
||||||
case 'adn':
|
case 'adn':
|
||||||
service = new (await import('./adn')).default;
|
service = new (await import('./adn')).default;
|
||||||
break;
|
break;
|
||||||
|
|
@ -78,15 +79,15 @@ import update from './modules/module.updater';
|
||||||
} else {
|
} else {
|
||||||
let service: ServiceClass;
|
let service: ServiceClass;
|
||||||
switch(argv.service) {
|
switch(argv.service) {
|
||||||
case 'funi':
|
|
||||||
service = new (await import('./funi')).default;
|
|
||||||
break;
|
|
||||||
case 'crunchy':
|
case 'crunchy':
|
||||||
service = new (await import('./crunchy')).default;
|
service = new (await import('./crunchy')).default;
|
||||||
break;
|
break;
|
||||||
case 'hidive':
|
case 'hidive':
|
||||||
service = new (await import('./hidive')).default;
|
service = new (await import('./hidive')).default;
|
||||||
break;
|
break;
|
||||||
|
case 'ao':
|
||||||
|
service = new (await import('./ao')).default;
|
||||||
|
break;
|
||||||
case 'adn':
|
case 'adn':
|
||||||
service = new (await import('./adn')).default;
|
service = new (await import('./adn')).default;
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,19 @@ import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { args, groups } from './module.args';
|
import { args, groups } from './module.args';
|
||||||
|
|
||||||
const transformService = (str: Array<'funi'|'crunchy'|'hidive'|'adn'|'all'>) => {
|
const transformService = (str: Array<'crunchy'|'hidive'|'ao'|'adn'|'all'>) => {
|
||||||
const services: string[] = [];
|
const services: string[] = [];
|
||||||
str.forEach(function(part) {
|
str.forEach(function(part) {
|
||||||
switch(part) {
|
switch(part) {
|
||||||
case 'funi':
|
|
||||||
services.push('Funimation');
|
|
||||||
break;
|
|
||||||
case 'crunchy':
|
case 'crunchy':
|
||||||
services.push('Crunchyroll');
|
services.push('Crunchyroll');
|
||||||
break;
|
break;
|
||||||
case 'hidive':
|
case 'hidive':
|
||||||
services.push('Hidive');
|
services.push('Hidive');
|
||||||
break;
|
break;
|
||||||
|
case 'ao':
|
||||||
|
services.push('AnimeOnegai');
|
||||||
|
break;
|
||||||
case 'adn':
|
case 'adn':
|
||||||
services.push('AnimationDigitalNetwork');
|
services.push('AnimationDigitalNetwork');
|
||||||
break;
|
break;
|
||||||
|
|
@ -33,7 +33,7 @@ If you find any bugs in this documentation or in the program itself please repor
|
||||||
|
|
||||||
## Legal Warning
|
## Legal Warning
|
||||||
|
|
||||||
This application is not endorsed by or affiliated with *Funimation*, *Hidive*, *AnimationDigitalNetwork*, or *Crunchyroll*.
|
This application is not endorsed by or affiliated with *Crunchyroll*, *Hidive*, *AnimeOnegai*, or *AnimationDigitalNetwork*.
|
||||||
This application enables you to download videos for offline viewing which may be forbidden by law in your country.
|
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.
|
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 tool is not responsible for your actions; please make an informed decision before using this application.
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import yargs, { Choices } from 'yargs';
|
import yargs, { Choices } from 'yargs';
|
||||||
import { args, AvailableMuxer, groups } from './module.args';
|
import { args, AvailableMuxer, groups } from './module.args';
|
||||||
import { LanguageItem } from './module.langsData';
|
import { LanguageItem } from './module.langsData';
|
||||||
import { HLSCallback } from './hls-download';
|
|
||||||
import { DownloadInfo } from '../@types/messageHandler';
|
import { DownloadInfo } from '../@types/messageHandler';
|
||||||
|
import { HLSCallback } from './hls-download';
|
||||||
|
|
||||||
let argvC: {
|
let argvC: {
|
||||||
[x: string]: unknown;
|
[x: string]: unknown;
|
||||||
|
|
@ -63,7 +63,7 @@ let argvC: {
|
||||||
debug: boolean | undefined;
|
debug: boolean | undefined;
|
||||||
nocleanup: boolean;
|
nocleanup: boolean;
|
||||||
help: boolean | undefined;
|
help: boolean | undefined;
|
||||||
service: 'funi' | 'crunchy' | 'hidive' | 'adn';
|
service: 'crunchy' | 'hidive' | 'ao' | 'adn';
|
||||||
update: boolean;
|
update: boolean;
|
||||||
fontName: string | undefined;
|
fontName: string | undefined;
|
||||||
_: (string | number)[];
|
_: (string | number)[];
|
||||||
|
|
@ -71,7 +71,6 @@ let argvC: {
|
||||||
dlVideoOnce: boolean;
|
dlVideoOnce: boolean;
|
||||||
chapters: boolean;
|
chapters: boolean;
|
||||||
crapi: 'android' | 'web';
|
crapi: 'android' | 'web';
|
||||||
hdapi: 'old' | 'new';
|
|
||||||
removeBumpers: boolean;
|
removeBumpers: boolean;
|
||||||
originalFontSize: boolean;
|
originalFontSize: boolean;
|
||||||
keepAllVideos: boolean;
|
keepAllVideos: boolean;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { dubLanguageCodes, languages, searchLocales, subtitleLanguagesFilter } from './module.langsData';
|
import { aoSearchLocales, dubLanguageCodes, languages, searchLocales, subtitleLanguagesFilter } from './module.langsData';
|
||||||
|
|
||||||
const groups = {
|
const groups = {
|
||||||
'auth': 'Authentication:',
|
'auth': 'Authentication:',
|
||||||
|
|
@ -41,7 +41,7 @@ export type TAppArg<T extends boolean|string|number|unknown[], K = any> = {
|
||||||
default: T|undefined,
|
default: T|undefined,
|
||||||
name?: string
|
name?: string
|
||||||
},
|
},
|
||||||
service: Array<'funi'|'crunchy'|'hidive'|'adn'|'all'>,
|
service: Array<'crunchy'|'hidive'|'ao'|'adn'|'all'>,
|
||||||
usage: string // -(-)${name} will be added for each command,
|
usage: string // -(-)${name} will be added for each command,
|
||||||
demandOption?: true,
|
demandOption?: true,
|
||||||
transformer?: (value: T) => K
|
transformer?: (value: T) => K
|
||||||
|
|
@ -107,12 +107,12 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
||||||
describe: 'Set the service locale',
|
describe: 'Set the service locale',
|
||||||
docDescribe: 'Set the local that will be used for the API.',
|
docDescribe: 'Set the local that will be used for the API.',
|
||||||
group: 'search',
|
group: 'search',
|
||||||
choices: (searchLocales.filter(a => a !== undefined) as string[]),
|
choices: ([...searchLocales.filter(a => a !== undefined), ...aoSearchLocales.filter(a => a !== undefined)] as string[]),
|
||||||
default: {
|
default: {
|
||||||
default: 'en-US'
|
default: 'en-US'
|
||||||
},
|
},
|
||||||
type: 'string',
|
type: 'string',
|
||||||
service: ['crunchy', 'adn'],
|
service: ['crunchy', 'ao', 'adn'],
|
||||||
usage: '${locale}'
|
usage: '${locale}'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -230,20 +230,6 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
||||||
default: 'web'
|
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',
|
name: 'removeBumpers',
|
||||||
describe: 'Remove bumpers from final video',
|
describe: 'Remove bumpers from final video',
|
||||||
|
|
@ -280,7 +266,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
||||||
type: 'number',
|
type: 'number',
|
||||||
alias: 'server',
|
alias: 'server',
|
||||||
docDescribe: true,
|
docDescribe: true,
|
||||||
service: ['crunchy','funi'],
|
service: ['crunchy'],
|
||||||
usage: '${server}'
|
usage: '${server}'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -314,8 +300,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
||||||
name: 'dlsubs',
|
name: 'dlsubs',
|
||||||
group: 'dl',
|
group: 'dl',
|
||||||
describe: 'Download subtitles by language tag (space-separated)'
|
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).map(a => a.locale).join(', ')}`,
|
||||||
+ `\nCrunchy Only: ${languages.filter(a => a.cr_locale && !a.funi_locale).map(a => a.locale).join(', ')}`,
|
|
||||||
docDescribe: true,
|
docDescribe: true,
|
||||||
service: ['all'],
|
service: ['all'],
|
||||||
type: 'array',
|
type: 'array',
|
||||||
|
|
@ -339,7 +324,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
||||||
group: 'dl',
|
group: 'dl',
|
||||||
describe: 'Skip downloading audio',
|
describe: 'Skip downloading audio',
|
||||||
docDescribe: true,
|
docDescribe: true,
|
||||||
service: ['funi'],
|
service: ['crunchy', 'hidive'],
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
usage: ''
|
usage: ''
|
||||||
},
|
},
|
||||||
|
|
@ -355,8 +340,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
||||||
{
|
{
|
||||||
name: 'dubLang',
|
name: 'dubLang',
|
||||||
describe: 'Set the language to download: '
|
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).map(a => a.code).join(', ')}`,
|
||||||
+ `\nCrunchy Only: ${languages.filter(a => a.cr_locale && !a.funi_locale).map(a => a.code).join(', ')}`,
|
|
||||||
docDescribe: true,
|
docDescribe: true,
|
||||||
group: 'dl',
|
group: 'dl',
|
||||||
choices: dubLanguageCodes,
|
choices: dubLanguageCodes,
|
||||||
|
|
@ -439,7 +423,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
||||||
group: 'dl',
|
group: 'dl',
|
||||||
describe: 'Force downloading simulcast version instead of uncut version (if available).',
|
describe: 'Force downloading simulcast version instead of uncut version (if available).',
|
||||||
docDescribe: true,
|
docDescribe: true,
|
||||||
service: ['funi', 'hidive'],
|
service: ['hidive'],
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
usage: '',
|
usage: '',
|
||||||
default: {
|
default: {
|
||||||
|
|
@ -572,7 +556,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
||||||
group: 'util',
|
group: 'util',
|
||||||
service: ['all'],
|
service: ['all'],
|
||||||
type: 'string',
|
type: 'string',
|
||||||
choices: ['funi', 'crunchy', 'hidive', 'adn'],
|
choices: ['crunchy', 'hidive', 'ao', 'adn'],
|
||||||
usage: '${service}',
|
usage: '${service}',
|
||||||
default: {
|
default: {
|
||||||
default: ''
|
default: ''
|
||||||
|
|
@ -593,7 +577,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
||||||
group: 'fonts',
|
group: 'fonts',
|
||||||
describe: 'Set the font to use in subtiles',
|
describe: 'Set the font to use in subtiles',
|
||||||
docDescribe: true,
|
docDescribe: true,
|
||||||
service: ['funi', 'hidive', 'adn'],
|
service: ['hidive', 'adn'],
|
||||||
type: 'string',
|
type: 'string',
|
||||||
usage: '${fontName}',
|
usage: '${fontName}',
|
||||||
},
|
},
|
||||||
|
|
@ -677,7 +661,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
||||||
describe: 'Authenticate every time the script runs. Use at your own risk.',
|
describe: 'Authenticate every time the script runs. Use at your own risk.',
|
||||||
docDescribe: true,
|
docDescribe: true,
|
||||||
group: 'auth',
|
group: 'auth',
|
||||||
service: ['funi','crunchy'],
|
service: ['crunchy'],
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
usage: '',
|
usage: '',
|
||||||
default: {
|
default: {
|
||||||
|
|
@ -689,7 +673,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
||||||
describe: 'Allows you to login with your token (Example on crunchy is Refresh Token/etp-rt cookie)',
|
describe: 'Allows you to login with your token (Example on crunchy is Refresh Token/etp-rt cookie)',
|
||||||
docDescribe: true,
|
docDescribe: true,
|
||||||
group: 'auth',
|
group: 'auth',
|
||||||
service: ['crunchy'],
|
service: ['crunchy', 'ao'],
|
||||||
type: 'string',
|
type: 'string',
|
||||||
usage: '${token}',
|
usage: '${token}',
|
||||||
default: {
|
default: {
|
||||||
|
|
|
||||||
|
|
@ -18,18 +18,18 @@ const guiCfgFile = path.join(workingDir, 'config', 'gui');
|
||||||
const cliCfgFile = path.join(workingDir, 'config', 'cli-defaults');
|
const cliCfgFile = path.join(workingDir, 'config', 'cli-defaults');
|
||||||
const hdPflCfgFile = path.join(workingDir, 'config', 'hd_profile');
|
const hdPflCfgFile = path.join(workingDir, 'config', 'hd_profile');
|
||||||
const sessCfgFile = {
|
const sessCfgFile = {
|
||||||
funi: path.join(workingDir, 'config', 'funi_sess'),
|
|
||||||
cr: path.join(workingDir, 'config', 'cr_sess'),
|
cr: path.join(workingDir, 'config', 'cr_sess'),
|
||||||
adn: path.join(workingDir, 'config', 'adn_sess'),
|
hd: path.join(workingDir, 'config', 'hd_sess'),
|
||||||
hd: path.join(workingDir, 'config', 'hd_sess')
|
ao: path.join(workingDir, 'config', 'ao_sess'),
|
||||||
|
adn: path.join(workingDir, 'config', 'adn_sess')
|
||||||
};
|
};
|
||||||
const stateFile = path.join(workingDir, 'config', 'guistate');
|
const stateFile = path.join(workingDir, 'config', 'guistate');
|
||||||
const tokenFile = {
|
const tokenFile = {
|
||||||
funi: path.join(workingDir, 'config', 'funi_token'),
|
|
||||||
cr: path.join(workingDir, 'config', 'cr_token'),
|
cr: path.join(workingDir, 'config', 'cr_token'),
|
||||||
adn: path.join(workingDir, 'config', 'adn_token'),
|
|
||||||
hd: path.join(workingDir, 'config', 'hd_token'),
|
hd: path.join(workingDir, 'config', 'hd_token'),
|
||||||
hdNew: path.join(workingDir, 'config', 'hd_new_token')
|
hdNew:path.join(workingDir, 'config', 'hd_new_token'),
|
||||||
|
ao: path.join(workingDir, 'config', 'ao_token'),
|
||||||
|
adn: path.join(workingDir, 'config', 'adn_token')
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ensureConfig = () => {
|
export const ensureConfig = () => {
|
||||||
|
|
@ -217,7 +217,7 @@ const saveCRToken = (data: Record<string, unknown>) => {
|
||||||
console.error('Can\'t save token file to disk!');
|
console.error('Can\'t save token file to disk!');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadADNToken = () => {
|
const loadADNToken = () => {
|
||||||
let token = loadYamlCfgFile(tokenFile.adn, true);
|
let token = loadYamlCfgFile(tokenFile.adn, true);
|
||||||
if(typeof token !== 'object' || token === null || Array.isArray(token)){
|
if(typeof token !== 'object' || token === null || Array.isArray(token)){
|
||||||
|
|
@ -236,6 +236,25 @@ const saveADNToken = (data: Record<string, unknown>) => {
|
||||||
console.error('Can\'t save token file to disk!');
|
console.error('Can\'t save token file to disk!');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadAOToken = () => {
|
||||||
|
let token = loadYamlCfgFile(tokenFile.ao, true);
|
||||||
|
if(typeof token !== 'object' || token === null || Array.isArray(token)){
|
||||||
|
token = {};
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveAOToken = (data: Record<string, unknown>) => {
|
||||||
|
const cfgFolder = path.dirname(tokenFile.ao);
|
||||||
|
try{
|
||||||
|
fs.ensureDirSync(cfgFolder);
|
||||||
|
fs.writeFileSync(`${tokenFile.ao}.yml`, yaml.stringify(data));
|
||||||
|
}
|
||||||
|
catch(e){
|
||||||
|
console.error('Can\'t save token file to disk!');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const loadHDSession = () => {
|
const loadHDSession = () => {
|
||||||
let session = loadYamlCfgFile(sessCfgFile.hd, true);
|
let session = loadYamlCfgFile(sessCfgFile.hd, true);
|
||||||
|
|
@ -332,33 +351,6 @@ const saveNewHDToken = (data: Record<string, unknown>) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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 cfgDir = path.join(workingDir, 'config');
|
||||||
|
|
||||||
const getState = (): GuiState => {
|
const getState = (): GuiState => {
|
||||||
|
|
@ -393,8 +385,6 @@ const setState = (state: GuiState) => {
|
||||||
export {
|
export {
|
||||||
loadBinCfg,
|
loadBinCfg,
|
||||||
loadCfg,
|
loadCfg,
|
||||||
loadFuniToken,
|
|
||||||
saveFuniToken,
|
|
||||||
saveCRSession,
|
saveCRSession,
|
||||||
loadCRSession,
|
loadCRSession,
|
||||||
saveCRToken,
|
saveCRToken,
|
||||||
|
|
@ -409,6 +399,8 @@ export {
|
||||||
loadNewHDToken,
|
loadNewHDToken,
|
||||||
saveHDProfile,
|
saveHDProfile,
|
||||||
loadHDProfile,
|
loadHDProfile,
|
||||||
|
saveAOToken,
|
||||||
|
loadAOToken,
|
||||||
getState,
|
getState,
|
||||||
setState,
|
setState,
|
||||||
writeYamlCfgFile,
|
writeYamlCfgFile,
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,10 @@ export type ItemType = {
|
||||||
}[]
|
}[]
|
||||||
|
|
||||||
export type DataType = {
|
export type DataType = {
|
||||||
funi: {
|
hidive: {
|
||||||
s: ItemType
|
s: ItemType
|
||||||
},
|
},
|
||||||
hidive: {
|
ao: {
|
||||||
s: ItemType
|
s: ItemType
|
||||||
},
|
},
|
||||||
adn: {
|
adn: {
|
||||||
|
|
@ -27,14 +27,14 @@ export type DataType = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const addToArchive = (kind: {
|
const addToArchive = (kind: {
|
||||||
service: 'funi',
|
|
||||||
type: 's'
|
|
||||||
} | {
|
|
||||||
service: 'crunchy',
|
service: 'crunchy',
|
||||||
type: 's'|'srz'
|
type: 's'|'srz'
|
||||||
} | {
|
} | {
|
||||||
service: 'hidive',
|
service: 'hidive',
|
||||||
type: 's'
|
type: 's'
|
||||||
|
} | {
|
||||||
|
service: 'ao',
|
||||||
|
type: 's'
|
||||||
} | {
|
} | {
|
||||||
service: 'adn',
|
service: 'adn',
|
||||||
type: 's'
|
type: 's'
|
||||||
|
|
@ -51,8 +51,8 @@ const addToArchive = (kind: {
|
||||||
});
|
});
|
||||||
(data as any)[kind.service][kind.type] = items;
|
(data as any)[kind.service][kind.type] = items;
|
||||||
} else {
|
} else {
|
||||||
if (kind.service === 'funi') {
|
if (kind.service === 'ao') {
|
||||||
data['funi'] = {
|
data['ao'] = {
|
||||||
s: [
|
s: [
|
||||||
{
|
{
|
||||||
id: ID,
|
id: ID,
|
||||||
|
|
@ -95,14 +95,14 @@ const addToArchive = (kind: {
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloaded = (kind: {
|
const downloaded = (kind: {
|
||||||
service: 'funi',
|
|
||||||
type: 's'
|
|
||||||
} | {
|
|
||||||
service: 'crunchy',
|
service: 'crunchy',
|
||||||
type: 's'|'srz'
|
type: 's'|'srz'
|
||||||
} | {
|
} | {
|
||||||
service: 'hidive',
|
service: 'hidive',
|
||||||
type: 's'
|
type: 's'
|
||||||
|
} | {
|
||||||
|
service: 'ao',
|
||||||
|
type: 's'
|
||||||
} | {
|
} | {
|
||||||
service: 'adn',
|
service: 'adn',
|
||||||
type: 's'
|
type: 's'
|
||||||
|
|
@ -123,7 +123,7 @@ const downloaded = (kind: {
|
||||||
fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4));
|
fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4));
|
||||||
};
|
};
|
||||||
|
|
||||||
const makeCommand = (service: 'funi'|'crunchy'|'hidive'|'adn') : Partial<ArgvType>[] => {
|
const makeCommand = (service: 'crunchy'|'hidive'|'ao'|'adn') : Partial<ArgvType>[] => {
|
||||||
const data = loadData();
|
const data = loadData();
|
||||||
const ret: Partial<ArgvType>[] = [];
|
const ret: Partial<ArgvType>[] = [];
|
||||||
const kind = data[service];
|
const kind = data[service];
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export type Params = {
|
||||||
// req
|
// req
|
||||||
export class Req {
|
export class Req {
|
||||||
private sessCfg: string;
|
private sessCfg: string;
|
||||||
private service: 'cr'|'funi'|'hd'|'adn';
|
private service: 'cr'|'funi'|'hd'|'ao'|'adn';
|
||||||
private session: Record<string, {
|
private session: Record<string, {
|
||||||
value: string;
|
value: string;
|
||||||
expires: Date;
|
expires: Date;
|
||||||
|
|
@ -25,7 +25,7 @@ export class Req {
|
||||||
private cfgDir = yamlCfg.cfgDir;
|
private cfgDir = yamlCfg.cfgDir;
|
||||||
private curl: boolean|string = false;
|
private curl: boolean|string = false;
|
||||||
|
|
||||||
constructor(private domain: Record<string, unknown>, private debug: boolean, private nosess = false, private type: 'cr'|'funi'|'hd'|'adn') {
|
constructor(private domain: Record<string, unknown>, private debug: boolean, private nosess = false, private type: 'cr'|'funi'|'hd'|'ao'|'adn') {
|
||||||
this.sessCfg = yamlCfg.sessCfgFile[type];
|
this.sessCfg = yamlCfg.sessCfgFile[type];
|
||||||
this.service = type;
|
this.service = type;
|
||||||
}
|
}
|
||||||
|
|
@ -72,7 +72,7 @@ export class Req {
|
||||||
}
|
}
|
||||||
// try do request
|
// try do request
|
||||||
try {
|
try {
|
||||||
const res = await fetch(durl.toString(), options);
|
const res = await fetch(durl, options);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
console.error(`${res.status}: ${res.statusText}`);
|
console.error(`${res.status}: ${res.statusText}`);
|
||||||
const body = await res.text();
|
const body = await res.text();
|
||||||
|
|
|
||||||
|
|
@ -5,22 +5,20 @@ export type LanguageItem = {
|
||||||
hd_locale?: string,
|
hd_locale?: string,
|
||||||
adn_locale?: string,
|
adn_locale?: string,
|
||||||
new_hd_locale?: string,
|
new_hd_locale?: string,
|
||||||
|
ao_locale?: string,
|
||||||
locale: string,
|
locale: string,
|
||||||
code: string,
|
code: string,
|
||||||
name: string,
|
name: string,
|
||||||
language?: string,
|
language?: string
|
||||||
funi_locale?: string,
|
|
||||||
funi_name?: string,
|
|
||||||
funi_name_lagacy?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const languages: LanguageItem[] = [
|
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: '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-419',ao_locale: 'es',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: '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', ao_locale: 'pt',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: 'pt-PT', locale: 'pt-PT', code: 'por', name: 'Portuguese (Portugal)', language: 'Portugues (Portugal)' },
|
||||||
{ cr_locale: 'fr-FR', adn_locale: 'fr', hd_locale: 'French', locale: 'fr', code: 'fra', name: 'French' },
|
{ cr_locale: 'fr-FR', adn_locale: 'fr', hd_locale: 'French', locale: 'fr', code: 'fra', name: 'French' },
|
||||||
{ cr_locale: 'de-DE', adn_locale: 'de', hd_locale: 'German', locale: 'de', code: 'deu', name: 'German' },
|
{ cr_locale: 'de-DE', adn_locale: 'de', hd_locale: 'German', locale: 'de', code: 'deu', name: 'German' },
|
||||||
|
|
@ -30,7 +28,7 @@ const languages: LanguageItem[] = [
|
||||||
{ cr_locale: 'ru-RU', hd_locale: 'Russian', locale: 'ru', code: 'rus', name: 'Russian' },
|
{ 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: 'tr-TR', hd_locale: 'Turkish', locale: 'tr', code: 'tur', name: 'Turkish' },
|
||||||
{ cr_locale: 'hi-IN', locale: 'hi', code: 'hin', name: 'Hindi' },
|
{ 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-CN', locale: 'zh-CN', code: 'zho', name: 'Chinese (Mainland China)' },
|
||||||
{ cr_locale: 'zh-TW', locale: 'zh-TW', code: 'chi', name: 'Chinese (Taiwan)' },
|
{ 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' },
|
{ cr_locale: 'ko-KR', hd_locale: 'Korean', locale: 'ko', code: 'kor', name: 'Korean' },
|
||||||
|
|
@ -42,7 +40,7 @@ const languages: LanguageItem[] = [
|
||||||
{ cr_locale: 'vi-VN', locale: 'vi-VN', code: 'vie', name: 'Vietnamese', language: 'Tiếng Việt' },
|
{ 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: '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: 'te-IN', locale: 'te-IN', code: 'tel', name: 'Telugu (India)', language: 'తెలుగు' },
|
||||||
{ cr_locale: 'ja-JP', adn_locale: 'ja', hd_locale: 'Japanese', funi_locale: 'jaJP', locale: 'ja', code: 'jpn', name: 'Japanese' },
|
{ cr_locale: 'ja-JP', adn_locale: 'ja', ao_locale: 'ja', hd_locale: 'Japanese', locale: 'ja', code: 'jpn', name: 'Japanese' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// add en language names
|
// add en language names
|
||||||
|
|
@ -73,6 +71,10 @@ const searchLocales = (() => {
|
||||||
return ['', ...new Set(languages.map(l => { return l.cr_locale; }).slice(0, -1)), ...new Set(languages.map(l => { return l.adn_locale; }).slice(0, -1))];
|
return ['', ...new Set(languages.map(l => { return l.cr_locale; }).slice(0, -1)), ...new Set(languages.map(l => { return l.adn_locale; }).slice(0, -1))];
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
export const aoSearchLocales = (() => {
|
||||||
|
return ['', ...new Set(languages.map(l => { return l.ao_locale; }).slice(0, -1))];
|
||||||
|
})();
|
||||||
|
|
||||||
// convert
|
// convert
|
||||||
const fixLanguageTag = (tag: string) => {
|
const fixLanguageTag = (tag: string) => {
|
||||||
tag = typeof tag == 'string' ? tag : 'und';
|
tag = typeof tag == 'string' ? tag : 'und';
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ const usefulCookies = {
|
||||||
// req
|
// req
|
||||||
class Req {
|
class Req {
|
||||||
private sessCfg: string;
|
private sessCfg: string;
|
||||||
private service: 'cr'|'funi'|'hd';
|
private service: 'cr'|'hd'|'ao';
|
||||||
private session: Record<string, {
|
private session: Record<string, {
|
||||||
value: string;
|
value: string;
|
||||||
expires: Date;
|
expires: Date;
|
||||||
|
|
@ -39,7 +39,7 @@ class Req {
|
||||||
private cfgDir = yamlCfg.cfgDir;
|
private cfgDir = yamlCfg.cfgDir;
|
||||||
private curl: boolean|string = false;
|
private curl: boolean|string = false;
|
||||||
|
|
||||||
constructor(private domain: Record<string, unknown>, private debug: boolean, private nosess = false, private type: 'cr'|'funi'|'hd') {
|
constructor(private domain: Record<string, unknown>, private debug: boolean, private nosess = false, private type: 'cr'|'hd'|'ao') {
|
||||||
this.sessCfg = yamlCfg.sessCfgFile[type];
|
this.sessCfg = yamlCfg.sessCfgFile[type];
|
||||||
this.service = type;
|
this.service = type;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
{
|
{
|
||||||
"name": "multi-downloader-nx",
|
"name": "multi-downloader-nx",
|
||||||
"short_name": "aniDL",
|
"short_name": "aniDL",
|
||||||
"version": "4.7.1",
|
"version": "5.0.0a1",
|
||||||
"description": "Downloader for Crunchyroll, Funimation, or Hidive via CLI or GUI",
|
"description": "Downloader for Crunchyroll and Hidive with CLI and GUI",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"download",
|
"download",
|
||||||
"downloader",
|
"downloader",
|
||||||
"funimation",
|
|
||||||
"funimationnow",
|
|
||||||
"hidive",
|
"hidive",
|
||||||
"crunchy",
|
"crunchy",
|
||||||
"crunchyroll",
|
"crunchyroll",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue