mirror of
https://github.com/anidl/multi-downloader-nx.git
synced 2026-03-11 17:45:30 +00:00
Initial commit to add AnimeOnegai
This commit is contained in:
parent
b1bae92308
commit
4c4436814b
23 changed files with 1287 additions and 25 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -27,6 +27,7 @@ hd_profile.yml
|
||||||
hd_sess.yml
|
hd_sess.yml
|
||||||
hd_token.yml
|
hd_token.yml
|
||||||
hd_new_token.yml
|
hd_new_token.yml
|
||||||
|
ao_token.yml
|
||||||
archive.json
|
archive.json
|
||||||
guistate.json
|
guistate.json
|
||||||
fonts
|
fonts
|
||||||
|
|
|
||||||
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;
|
||||||
|
}
|
||||||
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'|undefined],
|
'type': [undefined, 'funi'|'crunchy'|'hidive'|'ao'|undefined],
|
||||||
'setup': ['funi'|'crunchy'|'hidive'|undefined, undefined],
|
'setup': ['funi'|'crunchy'|'hidive'|'ao'|undefined, undefined],
|
||||||
'openFile': [[FolderTypes, string], undefined],
|
'openFile': [[FolderTypes, string], undefined],
|
||||||
'openURL': [string, undefined],
|
'openURL': [string, undefined],
|
||||||
'isSetup': [undefined, boolean],
|
'isSetup': [undefined, boolean],
|
||||||
|
|
|
||||||
772
ao.ts
Normal file
772
ao.ts
Normal file
|
|
@ -0,0 +1,772 @@
|
||||||
|
// 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 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async cli() {
|
||||||
|
console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`);
|
||||||
|
const argv = yargs.appArgv(this.cfg.cli);
|
||||||
|
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)}`);
|
||||||
|
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}`);
|
||||||
|
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}`);
|
||||||
|
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 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}`, 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;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
let subIndex = 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*, and *Hidive*.
|
This downloader can download anime from different sites. Currently supported are *Funimation*, *Crunchyroll*, *Hidive*, and *AnimeOnegai*.
|
||||||
|
|
||||||
## Legal Warning
|
## Legal Warning
|
||||||
|
|
||||||
This application is not endorsed by or affiliated with *Funimation*, *Crunchyroll*, or *Hidive*. This application enables you to download videos for offline viewing which may be forbidden by law in your country. The usage of this application may also cause a violation of the *Terms of Service* between you and the stream provider. This tool is not responsible for your actions; please make an informed decision before using this application.
|
This application is not endorsed by or affiliated with *Funimation*, *Crunchyroll*, *Hidive*, or *AnimeOnegai*. 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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ const MenuBar: React.FC = () => {
|
||||||
return 'Funimation';
|
return 'Funimation';
|
||||||
case 'hidive':
|
case 'hidive':
|
||||||
return 'Hidive';
|
return 'Hidive';
|
||||||
|
case 'ao':
|
||||||
|
return 'AnimeOnegai';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 = 'funi'|'crunchy'|'hidive';
|
type Services = 'funi'|'crunchy'|'hidive'|'ao';
|
||||||
|
|
||||||
export const serviceContext = React.createContext<Services|undefined>(undefined);
|
export const serviceContext = React.createContext<Services|undefined>(undefined);
|
||||||
|
|
||||||
|
|
@ -24,6 +24,7 @@ const ServiceProvider: FCWithChildren = ({ children }) => {
|
||||||
<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('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>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
: <serviceContext.Provider value={service}>
|
: <serviceContext.Provider value={service}>
|
||||||
|
|
|
||||||
|
|
@ -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'|undefined,
|
service: 'crunchy'|'funi'|'hidive'|'ao'|undefined,
|
||||||
version: string,
|
version: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { setState, getState, writeYamlCfgFile } from '../../modules/module.cfg-l
|
||||||
import CrunchyHandler from './services/crunchyroll';
|
import CrunchyHandler from './services/crunchyroll';
|
||||||
import FunimationHandler from './services/funimation';
|
import FunimationHandler from './services/funimation';
|
||||||
import HidiveHandler from './services/hidive';
|
import HidiveHandler from './services/hidive';
|
||||||
|
import AnimeOnegaiHandler from './services/animeonegai';
|
||||||
import WebSocketHandler from './websocket';
|
import WebSocketHandler from './websocket';
|
||||||
import packageJson from '../../package.json';
|
import packageJson from '../../package.json';
|
||||||
|
|
||||||
|
|
@ -37,6 +38,8 @@ export default class ServiceHandler {
|
||||||
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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -55,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'));
|
this.ws.events.on('type', async (_, respond) => respond(this.service === undefined ? undefined : this.service.name as 'hidive'|'crunchy'|'funi'|'ao'));
|
||||||
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') });
|
||||||
|
|
|
||||||
144
gui/server/services/animeonegai.ts
Normal file
144
gui/server/services/animeonegai.ts
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
17
index.ts
17
index.ts
|
|
@ -45,6 +45,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,7 +61,7 @@ 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('funi.js') || key.endsWith('hidive.js') || key.endsWith('ao.js'))
|
||||||
delete require.cache[key];
|
delete require.cache[key];
|
||||||
});
|
});
|
||||||
let service: ServiceClass;
|
let service: ServiceClass;
|
||||||
|
|
@ -66,6 +75,9 @@ import update from './modules/module.updater';
|
||||||
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;
|
||||||
default:
|
default:
|
||||||
service = new (await import(`./${argv.service}`)).default;
|
service = new (await import(`./${argv.service}`)).default;
|
||||||
break;
|
break;
|
||||||
|
|
@ -84,6 +96,9 @@ import update from './modules/module.updater';
|
||||||
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;
|
||||||
default:
|
default:
|
||||||
service = new (await import(`./${argv.service}`)).default;
|
service = new (await import(`./${argv.service}`)).default;
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ 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'|'all'>) => {
|
const transformService = (str: Array<'funi'|'crunchy'|'hidive'|'ao'|'all'>) => {
|
||||||
const services: string[] = [];
|
const services: string[] = [];
|
||||||
str.forEach(function(part) {
|
str.forEach(function(part) {
|
||||||
switch(part) {
|
switch(part) {
|
||||||
|
|
@ -16,6 +16,9 @@ const transformService = (str: Array<'funi'|'crunchy'|'hidive'|'all'>) => {
|
||||||
case 'hidive':
|
case 'hidive':
|
||||||
services.push('Hidive');
|
services.push('Hidive');
|
||||||
break;
|
break;
|
||||||
|
case 'ao':
|
||||||
|
services.push('AnimeOnegai');
|
||||||
|
break;
|
||||||
case 'all':
|
case 'all':
|
||||||
services.push('All');
|
services.push('All');
|
||||||
break;
|
break;
|
||||||
|
|
@ -30,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*, or *Crunchyroll*.
|
This application is not endorsed by or affiliated with *Funimation*, *AnimeOnegai*, *Hidive*, or *Crunchyroll*.
|
||||||
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,6 +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 { DownloadInfo } from '../@types/messageHandler';
|
||||||
|
import { HLSCallback } from './hls-download';
|
||||||
|
|
||||||
let argvC: {
|
let argvC: {
|
||||||
[x: string]: unknown;
|
[x: string]: unknown;
|
||||||
|
|
@ -61,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';
|
service: 'funi' | 'crunchy' | 'hidive' | 'ao';
|
||||||
update: boolean;
|
update: boolean;
|
||||||
fontName: string | undefined;
|
fontName: string | undefined;
|
||||||
_: (string | number)[];
|
_: (string | number)[];
|
||||||
|
|
@ -74,6 +76,7 @@ let argvC: {
|
||||||
originalFontSize: boolean;
|
originalFontSize: boolean;
|
||||||
keepAllVideos: boolean;
|
keepAllVideos: boolean;
|
||||||
syncTiming: boolean;
|
syncTiming: boolean;
|
||||||
|
callbackMaker?: (data: DownloadInfo) => HLSCallback,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ArgvType = typeof argvC;
|
export type ArgvType = typeof argvC;
|
||||||
|
|
|
||||||
|
|
@ -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'|'all'>,
|
service: Array<'funi'|'crunchy'|'hidive'|'ao'|'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
|
||||||
|
|
@ -572,7 +572,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
||||||
group: 'util',
|
group: 'util',
|
||||||
service: ['all'],
|
service: ['all'],
|
||||||
type: 'string',
|
type: 'string',
|
||||||
choices: ['funi', 'crunchy', 'hidive'],
|
choices: ['funi', 'crunchy', 'hidive', 'ao'],
|
||||||
usage: '${service}',
|
usage: '${service}',
|
||||||
default: {
|
default: {
|
||||||
default: ''
|
default: ''
|
||||||
|
|
@ -689,7 +689,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: {
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,15 @@ const hdPflCfgFile = path.join(workingDir, 'config', 'hd_profile');
|
||||||
const sessCfgFile = {
|
const sessCfgFile = {
|
||||||
funi: path.join(workingDir, 'config', 'funi_sess'),
|
funi: path.join(workingDir, 'config', 'funi_sess'),
|
||||||
cr: path.join(workingDir, 'config', 'cr_sess'),
|
cr: path.join(workingDir, 'config', 'cr_sess'),
|
||||||
hd: path.join(workingDir, 'config', 'hd_sess')
|
hd: path.join(workingDir, 'config', 'hd_sess'),
|
||||||
|
ao: path.join(workingDir, 'config', 'ao_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'),
|
funi: path.join(workingDir, 'config', 'funi_token'),
|
||||||
cr: path.join(workingDir, 'config', 'cr_token'),
|
cr: path.join(workingDir, 'config', 'cr_token'),
|
||||||
hd: path.join(workingDir, 'config', 'hd_token'),
|
hd: path.join(workingDir, 'config', 'hd_token'),
|
||||||
|
ao: path.join(workingDir, 'config', 'ao_token'),
|
||||||
hdNew: path.join(workingDir, 'config', 'hd_new_token')
|
hdNew: path.join(workingDir, 'config', 'hd_new_token')
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -216,6 +218,25 @@ const saveCRToken = (data: Record<string, unknown>) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
@ -387,6 +408,8 @@ export {
|
||||||
loadNewHDToken,
|
loadNewHDToken,
|
||||||
saveHDProfile,
|
saveHDProfile,
|
||||||
loadHDProfile,
|
loadHDProfile,
|
||||||
|
saveAOToken,
|
||||||
|
loadAOToken,
|
||||||
getState,
|
getState,
|
||||||
setState,
|
setState,
|
||||||
writeYamlCfgFile,
|
writeYamlCfgFile,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,9 @@ export type DataType = {
|
||||||
hidive: {
|
hidive: {
|
||||||
s: ItemType
|
s: ItemType
|
||||||
},
|
},
|
||||||
|
ao: {
|
||||||
|
s: ItemType
|
||||||
|
},
|
||||||
crunchy: {
|
crunchy: {
|
||||||
srz: ItemType,
|
srz: ItemType,
|
||||||
s: ItemType
|
s: ItemType
|
||||||
|
|
@ -32,6 +35,9 @@ const addToArchive = (kind: {
|
||||||
} | {
|
} | {
|
||||||
service: 'hidive',
|
service: 'hidive',
|
||||||
type: 's'
|
type: 's'
|
||||||
|
} | {
|
||||||
|
service: 'ao',
|
||||||
|
type: 's'
|
||||||
}, ID: string) => {
|
}, ID: string) => {
|
||||||
const data = loadData();
|
const data = loadData();
|
||||||
|
|
||||||
|
|
@ -54,6 +60,15 @@ const addToArchive = (kind: {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
} else if (kind.service === 'ao') {
|
||||||
|
data['ao'] = {
|
||||||
|
s: [
|
||||||
|
{
|
||||||
|
id: ID,
|
||||||
|
already: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
} else if (kind.service === 'crunchy') {
|
} else if (kind.service === 'crunchy') {
|
||||||
data['crunchy'] = {
|
data['crunchy'] = {
|
||||||
s: ([] as ItemType).concat(kind.type === 's' ? {
|
s: ([] as ItemType).concat(kind.type === 's' ? {
|
||||||
|
|
@ -88,6 +103,9 @@ const downloaded = (kind: {
|
||||||
} | {
|
} | {
|
||||||
service: 'hidive',
|
service: 'hidive',
|
||||||
type: 's'
|
type: 's'
|
||||||
|
} | {
|
||||||
|
service: 'ao',
|
||||||
|
type: 's'
|
||||||
}, ID: string, episode: string[]) => {
|
}, ID: string, episode: string[]) => {
|
||||||
let data = loadData();
|
let data = loadData();
|
||||||
if (!Object.prototype.hasOwnProperty.call(data, kind.service) || !Object.prototype.hasOwnProperty.call(data[kind.service], kind.type)
|
if (!Object.prototype.hasOwnProperty.call(data, kind.service) || !Object.prototype.hasOwnProperty.call(data[kind.service], kind.type)
|
||||||
|
|
@ -105,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') : Partial<ArgvType>[] => {
|
const makeCommand = (service: 'funi'|'crunchy'|'hidive'|'ao') : 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';
|
private service: 'cr'|'funi'|'hd'|'ao';
|
||||||
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') {
|
constructor(private domain: Record<string, unknown>, private debug: boolean, private nosess = false, private type: 'cr'|'funi'|'hd'|'ao') {
|
||||||
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();
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ export type LanguageItem = {
|
||||||
cr_locale?: string,
|
cr_locale?: string,
|
||||||
hd_locale?: string,
|
hd_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,
|
||||||
|
|
@ -17,9 +18,9 @@ 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', funi_locale: 'enUS', 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', 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-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', funi_name: 'Portuguese (Brazil)', funi_locale: 'ptBR', 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', hd_locale: 'French', locale: 'fr', code: 'fra', name: 'French' },
|
{ cr_locale: 'fr-FR', hd_locale: 'French', locale: 'fr', code: 'fra', name: 'French' },
|
||||||
{ cr_locale: 'de-DE', hd_locale: 'German', locale: 'de', code: 'deu', name: 'German' },
|
{ cr_locale: 'de-DE', hd_locale: 'German', locale: 'de', code: 'deu', name: 'German' },
|
||||||
|
|
@ -41,7 +42,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', hd_locale: 'Japanese', funi_locale: 'jaJP', locale: 'ja', code: 'jpn', name: 'Japanese' },
|
{ cr_locale: 'ja-JP', ao_locale: 'ja', hd_locale: 'Japanese', funi_locale: 'jaJP', locale: 'ja', code: 'jpn', name: 'Japanese' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// add en language names
|
// add en language names
|
||||||
|
|
|
||||||
|
|
@ -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'|'funi'|'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'|'funi'|'hd'|'ao') {
|
||||||
this.sessCfg = yamlCfg.sessCfgFile[type];
|
this.sessCfg = yamlCfg.sessCfgFile[type];
|
||||||
this.service = type;
|
this.service = type;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
tsc.ts
1
tsc.ts
|
|
@ -34,6 +34,7 @@ const ignore = [
|
||||||
'./config/updates.json$',
|
'./config/updates.json$',
|
||||||
'./config/cr_token.yml$',
|
'./config/cr_token.yml$',
|
||||||
'./config/funi_token.yml$',
|
'./config/funi_token.yml$',
|
||||||
|
'./config/ao_token.yml$',
|
||||||
'./config/new_hd_token.yml$',
|
'./config/new_hd_token.yml$',
|
||||||
'./config/hd_token.yml$',
|
'./config/hd_token.yml$',
|
||||||
'./config/hd_sess.yml$',
|
'./config/hd_sess.yml$',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue