removed AnimeOnegai

This commit is contained in:
stratumadev 2025-09-30 23:39:47 +02:00
parent 7e680f1117
commit 3fb4392b39
26 changed files with 81 additions and 2013 deletions

View file

@ -50,7 +50,6 @@ body:
- Crunchyroll
- Hidive
- AnimationDigitalNetwork
- AnimeOnegai
- All
- Irrelevant
validations:

View file

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

View file

@ -1,36 +0,0 @@
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: boolean;
}

View file

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

View file

@ -1,41 +0,0 @@
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
View file

@ -30,8 +30,8 @@ export type MessageTypes = {
isDownloading: [undefined, boolean];
openFolder: [FolderTypes, undefined];
changeProvider: [undefined, boolean];
type: [undefined, 'crunchy' | 'hidive' | 'ao' | 'adn' | undefined];
setup: ['crunchy' | 'hidive' | 'ao' | 'adn' | undefined, undefined];
type: [undefined, 'crunchy' | 'hidive' | 'adn' | undefined];
setup: ['crunchy' | 'hidive' | 'adn' | undefined, undefined];
openFile: [[FolderTypes, string], undefined];
openURL: [string, undefined];
isSetup: [undefined, boolean];

908
ao.ts
View file

@ -1,908 +0,0 @@
// Package Info
import packageJson from './package.json';
// Node
import path from 'path';
import fs from 'fs-extra';
// 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 { canDecrypt, getKeysWVD, cdm, getKeysPRD } from './modules/cdm';
import streamdl, { M3U8Json } from './modules/hls-download';
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';
import Helper from './modules/module.helper';
// 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',
'japonés'
];
public spaStrings: string[] = ['doblaje en español', 'dublagem em espanhol', 'español'];
public porStrings: string[] = ['doblaje en portugués', 'dublagem em português'];
private defaultOptions: RequestInit = {
headers: {
origin: 'https://www.animeonegai.com',
referer: 'https://www.animeonegai.com/'
}
};
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);
if (['pt', 'es'].includes(argv.locale)) 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 Helper.question('[Q] LOGIN/EMAIL: ')),
password: argv.password ?? (await Helper.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}`, this.defaultOptions);
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}`, this.defaultOptions);
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}`, this.defaultOptions);
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 && series.data.asset_type !== 1) {
console.info(' No Seasons found!');
return { isOk: false };
}
const episodes: { [key: string]: (Episode & { lang?: string })[] } = {};
for (const season of series.seasons) {
let lang: string | undefined = undefined;
if (this.jpnStrings.includes(season.name.trim().toLowerCase())) lang = 'ja';
else if (this.porStrings.includes(season.name.trim().toLowerCase())) lang = 'pt';
else if (this.spaStrings.includes(season.name.trim().toLowerCase())) lang = 'es';
else {
lang = 'unknown';
console.error(`Language (${season.name.trim()}) not known, please report this!`);
}
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 });
}
}
//Item is movie, lets define it manually
if (series.data.asset_type === 1 && series.seasons.length === 0) {
let lang: string | undefined;
if (this.jpnStrings.some((str) => series.data.title.includes(str.toLowerCase()))) lang = 'ja';
else if (this.porStrings.some((str) => series.data.title.includes(str.toLowerCase()))) lang = 'pt';
else if (this.spaStrings.some((str) => series.data.title.includes(str.toLowerCase()))) lang = 'es';
else {
lang = 'unknown';
console.error('Language could not be parsed from movie title, please report this!');
}
episodes[1] = [
{
video_entry: series.data.video_entry,
number: 1,
season_id: 1,
name: series.data.title,
ID: series.data.ID,
CreatedAt: series.data.CreatedAt,
DeletedAt: series.data.DeletedAt,
UpdatedAt: series.data.UpdatedAt,
active: series.data.active,
description: series.data.description,
age_restriction: series.data.age_restriction,
asset_id: series.data.ID,
ending: null,
entry: series.data.entry,
stream_url: series.data.stream_url,
skip_intro: null,
thumbnail: series.data.bg,
open_free: false,
lang
}
]; // as unknown as (Episode & { lang?: string })[];
// The above needs to be uncommented if the episode number should be M1 instead of 1
}
//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', 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.error('No valid Widevine or PlayReady CDM detected. Please ensure a supported and functional CDM is installed.');
return undefined;
}
if (!this.cfg.bin.mp4decrypt && !this.cfg.bin.shaka) {
console.error('Neither Shaka nor MP4Decrypt found. Please ensure at least one of them is installed.');
return undefined;
}
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 {
const streamPlaylistBody = (await streamPlaylistsReq.res.text()).replace(
/<BaseURL>(.*?)<\/BaseURL>/g,
`<BaseURL>${streamData.dash.split('/dash/')[0]}/dash/$1</BaseURL>`
);
//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 / 1024)}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 dirName = path.dirname(tsFile);
if (!fs.existsSync(dirName)) {
fs.mkdirSync(dirName, { recursive: true });
}
const videoJson: M3U8Json = {
segments: chosenVideoSegments.segments
};
try {
const videoDownload = await new streamdl({
output: chosenVideoSegments.pssh_wvd ? `${tempTsFile}.video.enc.m4s` : `${tsFile}.video.m4s`,
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;
} else {
dlVideoOnce = true;
videoDownloaded = true;
}
} catch (e) {
console.error(e);
dlFailed = 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 dirName = path.dirname(tsFile);
if (!fs.existsSync(dirName)) {
fs.mkdirSync(dirName, { recursive: true });
}
const audioJson: M3U8Json = {
segments: chosenAudioSegments.segments
};
try {
const audioDownload = await new streamdl({
output: chosenAudioSegments.pssh_wvd ? `${tempTsFile}.audio.enc.m4s` : `${tsFile}.audio.m4s`,
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;
} else {
audioDownloaded = true;
}
} catch (e) {
console.error(e);
dlFailed = true;
}
} else if (options.noaudio) {
console.info('Skipping audio download...');
}
//Handle Decryption if needed
if ((chosenVideoSegments.pssh_wvd || chosenAudioSegments.pssh_wvd) && (videoDownloaded || audioDownloaded)) {
console.info('Decryption Needed, attempting to decrypt');
let encryptionKeys;
if (cdm === 'widevine') {
encryptionKeys = await getKeysWVD(chosenVideoSegments.pssh_wvd, streamData.widevine_proxy, {});
}
if (cdm === 'playready') {
encryptionKeys = await getKeysPRD(chosenVideoSegments.pssh_prd, streamData.playready_proxy, {});
}
if (!encryptionKeys || 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 || this.cfg.bin.shaka) {
let commandBase = `--show-progress --key ${encryptionKeys[cdm === 'playready' ? 0 : 1].kid}:${encryptionKeys[cdm === 'playready' ? 0 : 1].key} `;
let commandVideo = commandBase + `"${tempTsFile}.video.enc.m4s" "${tempTsFile}.video.m4s"`;
let commandAudio = commandBase + `"${tempTsFile}.audio.enc.m4s" "${tempTsFile}.audio.m4s"`;
if (this.cfg.bin.shaka) {
commandBase = ` --enable_raw_key_decryption ${encryptionKeys.map((kb) => '--keys key_id=' + kb.kid + ':key=' + kb.key).join(' ')}`;
commandVideo = `input="${tempTsFile}.video.enc.m4s",stream=video,output="${tempTsFile}.video.m4s"` + commandBase;
commandAudio = `input="${tempTsFile}.audio.enc.m4s",stream=audio,output="${tempTsFile}.audio.m4s"` + commandBase;
}
if (videoDownloaded) {
console.info('Started decrypting video,', this.cfg.bin.shaka ? 'using shaka' : 'using mp4decrypt');
const decryptVideo = Helper.exec(
this.cfg.bin.shaka ? 'shaka-packager' : 'mp4decrypt',
this.cfg.bin.shaka ? `"${this.cfg.bin.shaka}"` : `"${this.cfg.bin.mp4decrypt}"`,
commandVideo
);
if (!decryptVideo.isOk) {
console.error(decryptVideo.err);
console.error(`Decryption failed with exit code ${decryptVideo.err.code}`);
if (this.cfg.bin.shaka) {
console.error(`Downgrade to Shaka-Packager v2.6.1 (https://github.com/shaka-project/shaka-packager/releases/tag/v2.6.1) and try again`);
}
fs.renameSync(`${tempTsFile}.video.enc.m4s`, `${tsFile}.video.enc.m4s`);
return undefined;
} else {
console.info('Decryption done for video');
if (!options.nocleanup) {
fs.removeSync(`${tempTsFile}.video.enc.m4s`);
}
fs.copyFileSync(`${tempTsFile}.video.m4s`, `${tsFile}.video.m4s`);
fs.unlinkSync(`${tempTsFile}.video.m4s`);
files.push({
type: 'Video',
path: `${tsFile}.video.m4s`,
lang: lang
});
}
}
if (audioDownloaded) {
console.info('Started decrypting audio,', this.cfg.bin.shaka ? 'using shaka' : 'using mp4decrypt');
const decryptAudio = Helper.exec(
this.cfg.bin.shaka ? 'shaka-packager' : 'mp4decrypt',
this.cfg.bin.shaka ? `"${this.cfg.bin.shaka}"` : `"${this.cfg.bin.mp4decrypt}"`,
commandAudio
);
if (!decryptAudio.isOk) {
console.error(decryptAudio.err);
console.error(`Decryption failed with exit code ${decryptAudio.err.code}`);
if (this.cfg.bin.shaka) {
console.error(`Downgrade to Shaka-Packager v2.6.1 (https://github.com/shaka-project/shaka-packager/releases/tag/v2.6.1) and try again`);
}
fs.renameSync(`${tempTsFile}.audio.enc.m4s`, `${tsFile}.audio.enc.m4s`);
return undefined;
} else {
if (!options.nocleanup) {
fs.removeSync(`${tempTsFile}.audio.enc.m4s`);
}
fs.copyFileSync(`${tempTsFile}.audio.m4s`, `${tsFile}.audio.m4s`);
fs.unlinkSync(`${tempTsFile}.audio.m4s`);
files.push({
type: 'Audio',
path: `${tsFile}.audio.m4s`,
lang: lang
});
console.info('Decryption done for audio');
}
}
} else {
console.warn('mp4decrypt/shaka not found, files need decryption. Decryption Keys:', encryptionKeys);
}
} else {
if (videoDownloaded) {
files.push({
type: 'Video',
path: `${tsFile}.video.m4s`,
lang: lang
});
}
if (audioDownloaded) {
files.push({
type: 'Audio',
path: `${tsFile}.audio.m4s`,
lang: lang
});
}
}
}
} 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);
if (path.isAbsolute(sxData.file)) {
sxData.path = sxData.file;
} else {
sxData.path = path.join(this.cfg.dir.content, sxData.file);
}
const dirName = path.dirname(sxData.path);
if (!fs.existsSync(dirName)) {
fs.mkdirSync(dirName, { recursive: true });
}
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, AuthHeaders);
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);
});
}
}

View file

@ -4,7 +4,7 @@ If you find any bugs in this documentation or in the program itself please repor
## Legal Warning
This application is not endorsed by or affiliated with *Crunchyroll*, *Hidive*, *AnimeOnegai*, or *AnimationDigitalNetwork*.
This application is not endorsed by or affiliated with *Crunchyroll*, *Hidive* 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.

View file

@ -2,11 +2,11 @@
[![Discord Shield](https://discord.com/api/guilds/884479461997805568/widget.png?style=banner2)](https://discord.gg/qEpbWen5vq)
This downloader can download anime from different sites. Currently supported are *Crunchyroll*, *Hidive*, *AnimeOnegai*, and *AnimationDigitalNetwork*.
This downloader can download anime from different sites. Currently supported are *Crunchyroll*, *Hidive* and *AnimationDigitalNetwork*.
## Legal Warning
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.
This application is not endorsed by or affiliated with *Crunchyroll*, *Hidive* 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

View file

@ -27,8 +27,6 @@ const MenuBar: React.FC = () => {
return 'Crunchyroll';
case 'hidive':
return 'Hidive';
case 'ao':
return 'AnimeOnegai';
case 'adn':
return 'AnimationDigitalNetwork';
}

View file

@ -3,7 +3,7 @@ import { Divider, Box, Button, Typography, Avatar } from '@mui/material';
import useStore from '../hooks/useStore';
import { StoreState } from './Store';
type Services = 'crunchy' | 'hidive' | 'ao' | 'adn';
type Services = 'crunchy' | 'hidive' | 'adn';
export const serviceContext = React.createContext<Services | undefined>(undefined);
@ -39,14 +39,6 @@ const ServiceProvider: FCWithChildren = ({ children }) => {
>
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.com/favicon.ico'} />}>
AnimationDigitalNetwork
</Button>

View file

@ -21,7 +21,7 @@ export type DownloadOptions = {
export type StoreState = {
episodeListing: Episode[];
downloadOptions: DownloadOptions;
service: 'crunchy' | 'hidive' | 'ao' | 'adn' | undefined;
service: 'crunchy' | 'hidive' | 'adn' | undefined;
version: string;
};

View file

@ -5,7 +5,6 @@ import { MessageHandler, GuiState } from '../../@types/messageHandler';
import { setState, getState, writeYamlCfgFile } from '../../modules/module.cfg-loader';
import CrunchyHandler from './services/crunchyroll';
import HidiveHandler from './services/hidive';
import AnimeOnegaiHandler from './services/animeonegai';
import ADNHandler from './services/adn';
import WebSocketHandler from './websocket';
import packageJson from '../../package.json';
@ -35,8 +34,6 @@ export default class ServiceHandler {
this.service = new CrunchyHandler(this.ws);
} else if (data === 'hidive') {
this.service = new HidiveHandler(this.ws);
} else if (data === 'ao') {
this.service = new AnimeOnegaiHandler(this.ws);
} else if (data === 'adn') {
this.service = new ADNHandler(this.ws);
}
@ -55,7 +52,7 @@ export default class ServiceHandler {
this.ws.events.on('version', async (_, respond) => {
respond(packageJson.version);
});
this.ws.events.on('type', async (_, respond) => respond(this.service === undefined ? undefined : (this.service.name as 'hidive' | 'crunchy' | 'ao' | 'adn')));
this.ws.events.on('type', async (_, respond) => respond(this.service === undefined ? undefined : (this.service.name as 'hidive' | 'crunchy' | 'adn')));
this.ws.events.on('checkToken', async (_, respond) => {
if (this.service === undefined) return respond({ isOk: false, reason: new Error('No service selected') });
respond(await this.service.checkToken());

View file

@ -1,170 +0,0 @@
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);
if (['es', 'pt'].includes(_default.locale)) 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,
noaudio: data.noaudio,
hslang: data.hslang || 'none',
dubLang: data.dubLang
}))
) {
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;

View file

@ -39,17 +39,6 @@ import update from './modules/module.updater';
(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.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) {
const ids = makeCommand(argv.service);
@ -57,7 +46,7 @@ import update from './modules/module.updater';
overrideArguments(cfg.cli, id);
/* Reimport module to override appArgv */
Object.keys(require.cache).forEach((key) => {
if (key.endsWith('crunchy.js') || key.endsWith('hidive.js') || key.endsWith('ao.js')) delete require.cache[key];
if (key.endsWith('crunchy.js') || key.endsWith('hidive.js')) delete require.cache[key];
});
let service: ServiceClass;
switch (argv.service) {
@ -67,9 +56,6 @@ import update from './modules/module.updater';
case 'hidive':
service = new (await import('./hidive')).default();
break;
case 'ao':
service = new (await import('./ao')).default();
break;
case 'adn':
service = new (await import('./adn')).default();
break;
@ -88,9 +74,6 @@ import update from './modules/module.updater';
case 'hidive':
service = new (await import('./hidive')).default();
break;
case 'ao':
service = new (await import('./ao')).default();
break;
case 'adn':
service = new (await import('./adn')).default();
break;

View file

@ -3,7 +3,7 @@ import fs from 'fs';
import path from 'path';
import { args, groups } from './module.args';
const transformService = (str: Array<'crunchy' | 'hidive' | 'ao' | 'adn' | 'all'>) => {
const transformService = (str: Array<'crunchy' | 'hidive' | 'adn' | 'all'>) => {
const services: string[] = [];
str.forEach(function (part) {
switch (part) {
@ -13,9 +13,6 @@ const transformService = (str: Array<'crunchy' | 'hidive' | 'ao' | 'adn' | 'all'
case 'hidive':
services.push('Hidive');
break;
case 'ao':
services.push('AnimeOnegai');
break;
case 'adn':
services.push('AnimationDigitalNetwork');
break;
@ -33,7 +30,7 @@ If you find any bugs in this documentation or in the program itself please repor
## Legal Warning
This application is not endorsed by or affiliated with *Crunchyroll*, *Hidive*, *AnimeOnegai*, or *AnimationDigitalNetwork*.
This application is not endorsed by or affiliated with *Crunchyroll*, *Hidive* 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.

View file

@ -1,457 +0,0 @@
// // build-in
// import crypto from 'crypto';
// import fs from 'fs';
// import url from 'url';
// import readline from 'readline/promises';
// import { stdin as input, stdout as output } from 'process';
// // extra
// import got, { Response } from 'got';
// import { console } from './log';
// import { ProgressData } from '../@types/messageHandler';
// import Helper from './module.helper';
// // The following function should fix an issue with downloading. For more information see https://github.com/sindresorhus/got/issues/1489
// const fixMiddleWare = (res: Response) => {
// const isResponseOk = (response: Response) => {
// const {statusCode} = response;
// const limitStatusCode = response.request.options.followRedirect ? 299 : 399;
// return (statusCode >= 200 && statusCode <= limitStatusCode) || statusCode === 304;
// };
// if (isResponseOk(res)) {
// res.request.destroy();
// }
// return res;
// };
// export type HLSCallback = (data: ProgressData) => unknown;
// export type M3U8Json = {
// segments: Record<string, unknown>[],
// mediaSequence?: number,
// }
// type Segment = {
// uri: string
// key: Key,
// byterange?: {
// offset: number,
// length: number
// }
// }
// type Key = {
// uri: string,
// iv: number[]
// }
// export type HLSOptions = {
// m3u8json: M3U8Json,
// output?: string,
// threads?: number,
// retries?: number,
// offset?: number,
// baseurl?: string,
// skipInit?: boolean,
// timeout?: number,
// fsRetryTime?: number,
// override?: 'Y'|'y'|'N'|'n'|'C'|'c'
// callback?: HLSCallback
// }
// type Data = {
// parts: {
// first: number,
// total: number,
// completed: number
// },
// m3u8json: M3U8Json,
// outputFile: string,
// threads: number,
// retries: number,
// offset: number,
// baseurl?: string
// skipInit?: boolean,
// keys: {
// [uri: string]: Buffer|string
// },
// timeout: number,
// checkPartLength: boolean,
// isResume: boolean,
// bytesDownloaded: number,
// waitTime: number,
// callback?: HLSCallback,
// override?: string,
// dateStart: number
// }
// // hls class
// class hlsDownload {
// private data: Data;
// constructor(options: HLSOptions){
// // check playlist
// if(
// !options
// || !options.m3u8json
// || !options.m3u8json.segments
// || options.m3u8json.segments.length === 0
// ){
// throw new Error('Playlist is empty!');
// }
// // init options
// this.data = {
// parts: {
// first: options.m3u8json.mediaSequence || 0,
// total: options.m3u8json.segments.length,
// completed: 0,
// },
// m3u8json: options.m3u8json,
// outputFile: options.output || 'stream.ts',
// threads: options.threads || 5,
// retries: options.retries || 4,
// offset: options.offset || 0,
// baseurl: options.baseurl,
// skipInit: options.skipInit,
// keys: {},
// timeout: options.timeout ? options.timeout : 60 * 1000,
// checkPartLength: true,
// isResume: options.offset ? options.offset > 0 : false,
// bytesDownloaded: 0,
// waitTime: options.fsRetryTime ?? 1000 * 5,
// callback: options.callback,
// override: options.override,
// dateStart: 0
// };
// }
// async download(){
// // set output
// const fn = this.data.outputFile;
// // try load resume file
// if(fs.existsSync(fn) && fs.existsSync(`${fn}.resume`) && this.data.offset < 1){
// try{
// console.info('Resume data found! Trying to resume...');
// const resumeData = JSON.parse(fs.readFileSync(`${fn}.resume`, 'utf-8'));
// if(
// resumeData.total == this.data.m3u8json.segments.length
// && resumeData.completed != resumeData.total
// && !isNaN(resumeData.completed)
// ){
// console.info('Resume data is ok!');
// this.data.offset = resumeData.completed;
// this.data.isResume = true;
// }
// else{
// console.warn(' Resume data is wrong!');
// console.warn({
// resume: { total: resumeData.total, dled: resumeData.completed },
// current: { total: this.data.m3u8json.segments.length },
// });
// }
// }
// catch(e){
// console.error('Resume failed, downloading will be not resumed!');
// console.error(e);
// }
// }
// // ask before rewrite file
// if (fs.existsSync(`${fn}`) && !this.data.isResume) {
// const rl = readline.createInterface({ input, output });
// let rwts = this.data.override ?? await rl.question(`[Q] File «${fn}» already exists! Rewrite? ([y]es/[N]o/[c]ontinue)`);
// rl.close();
// rwts = rwts || 'N';
// if (['Y', 'y'].includes(rwts[0])) {
// console.info(`Deleting «${fn}»...`);
// fs.unlinkSync(fn);
// }
// else if (['C', 'c'].includes(rwts[0])) {
// return { ok: true, parts: this.data.parts };
// }
// else {
// return { ok: false, parts: this.data.parts };
// }
// }
// // show output filename
// if (fs.existsSync(fn) && this.data.isResume) {
// console.info(`Adding content to «${fn}»...`);
// }
// else{
// console.info(`Saving stream to «${fn}»...`);
// }
// // start time
// this.data.dateStart = Date.now();
// let segments = this.data.m3u8json.segments;
// // download init part
// if (segments[0].map && this.data.offset === 0 && !this.data.skipInit) {
// console.info('Download and save init part...');
// const initSeg = segments[0].map as Segment;
// if(segments[0].key){
// initSeg.key = segments[0].key as Key;
// }
// try{
// const initDl = await this.downloadPart(initSeg, 0, 0);
// fs.writeFileSync(fn, initDl.dec, { flag: 'a' });
// fs.writeFileSync(`${fn}.resume`, JSON.stringify({
// completed: 0,
// total: this.data.m3u8json.segments.length
// }));
// console.info('Init part downloaded.');
// }
// catch(e: any){
// console.error(`Part init download error:\n\t${e.message}`);
// return { ok: false, parts: this.data.parts };
// }
// }
// else if(segments[0].map && this.data.offset === 0 && this.data.skipInit){
// console.warn('Skipping init part can lead to broken video!');
// }
// // resuming ...
// if(this.data.offset > 0){
// segments = segments.slice(this.data.offset);
// console.info(`Resuming download from part ${this.data.offset+1}...`);
// this.data.parts.completed = this.data.offset;
// }
// // dl process
// for (let p = 0; p < segments.length / this.data.threads; p++) {
// // set offsets
// const offset = p * this.data.threads;
// const dlOffset = offset + this.data.threads;
// // map download threads
// const krq = new Map(), prq = new Map();
// const res = [];
// let errcnt = 0;
// for (let px = offset; px < dlOffset && px < segments.length; px++){
// const curp = segments[px];
// const key = curp.key as Key;
// if(key && !krq.has(key.uri) && !this.data.keys[key.uri as string]){
// krq.set(key.uri, this.downloadKey(key, px, this.data.offset));
// }
// }
// try {
// await Promise.all(krq.values());
// } catch (er: any) {
// console.error(`Key ${er.p + 1} download error:\n\t${er.message}`);
// return { ok: false, parts: this.data.parts };
// }
// for (let px = offset; px < dlOffset && px < segments.length; px++){
// const curp = segments[px] as Segment;
// prq.set(px, this.downloadPart(curp, px, this.data.offset));
// }
// for (let i = prq.size; i--;) {
// try {
// const r = await Promise.race(prq.values());
// prq.delete(r.p);
// res[r.p - offset] = r.dec;
// }
// catch (error: any) {
// console.error('Part %s download error:\n\t%s',
// error.p + 1 + this.data.offset, error.message);
// prq.delete(error.p);
// errcnt++;
// }
// }
// // catch error
// if (errcnt > 0) {
// console.error(`${errcnt} parts not downloaded`);
// return { ok: false, parts: this.data.parts };
// }
// // write downloaded
// for (const r of res) {
// let error = 0;
// while (error < 3) {
// try {
// fs.writeFileSync(fn, r, { flag: 'a' });
// break;
// } catch (err) {
// console.error(err);
// console.error(`Unable to write to file '${fn}' (Attempt ${error+1}/3)`);
// console.info(`Waiting ${Math.round(this.data.waitTime / 1000)}s before retrying`);
// await new Promise<void>((resolve) => setTimeout(() => resolve(), this.data.waitTime));
// }
// error++;
// }
// if (error === 3) {
// console.error(`Unable to write content to '${fn}'.`);
// return { ok: false, parts: this.data.parts };
// }
// }
// // log downloaded
// const totalSeg = segments.length + this.data.offset; // Add the sliced lenght back so the resume data will be correct even if an resumed download fails
// const downloadedSeg = dlOffset < totalSeg ? dlOffset : totalSeg;
// this.data.parts.completed = downloadedSeg + this.data.offset;
// const data = extFn.getDownloadInfo(
// this.data.dateStart, downloadedSeg, totalSeg,
// this.data.bytesDownloaded
// );
// fs.writeFileSync(`${fn}.resume`, JSON.stringify({
// completed: this.data.parts.completed,
// total: totalSeg
// }));
// console.info(`${downloadedSeg} of ${totalSeg} parts downloaded [${data.percent}%] (${Helper.formatTime(parseInt((data.time / 1000).toFixed(0)))} | ${(data.downloadSpeed / 1000000).toPrecision(2)}Mb/s)`);
// if (this.data.callback)
// this.data.callback({ total: this.data.parts.total, cur: this.data.parts.completed, bytes: this.data.bytesDownloaded, percent: data.percent, time: data.time, downloadSpeed: data.downloadSpeed });
// }
// // return result
// fs.unlinkSync(`${fn}.resume`);
// return { ok: true, parts: this.data.parts };
// }
// async downloadPart(seg: Segment, segIndex: number, segOffset: number){
// const sURI = extFn.getURI(seg.uri, this.data.baseurl);
// let decipher, part, dec;
// const p = segIndex;
// try {
// if (seg.key != undefined) {
// decipher = await this.getKey(seg.key, p, segOffset);
// }
// part = await extFn.getData(p, sURI, {
// ...(seg.byterange ? {
// Range: `bytes=${seg.byterange.offset}-${seg.byterange.offset+seg.byterange.length-1}`
// } : {})
// }, segOffset, false, this.data.timeout, this.data.retries, [
// (res, retryWithMergedOptions) => {
// if(this.data.checkPartLength && res.headers['content-length']){
// if(!res.body || (res.body as any).length != res.headers['content-length']){
// // 'Part not fully downloaded'
// return retryWithMergedOptions();
// }
// }
// return res;
// }
// ]);
// if(this.data.checkPartLength && !(part as any).headers['content-length']){
// this.data.checkPartLength = false;
// console.warn(`Part ${segIndex+segOffset+1}: can't check parts size!`);
// }
// if (decipher == undefined) {
// this.data.bytesDownloaded += (part.body as Buffer).byteLength;
// return { dec: part.body, p };
// }
// dec = decipher.update(part.body as Buffer);
// dec = Buffer.concat([dec, decipher.final()]);
// this.data.bytesDownloaded += dec.byteLength;
// }
// catch (error: any) {
// error.p = p;
// throw error;
// }
// return { dec, p };
// }
// async downloadKey(key: Key, segIndex: number, segOffset: number){
// const kURI = extFn.getURI(key.uri, this.data.baseurl);
// if (!this.data.keys[kURI]) {
// try {
// const rkey = await extFn.getData(segIndex, kURI, {}, segOffset, true, this.data.timeout, this.data.retries, [
// (res, retryWithMergedOptions) => {
// if (!res || !res.body) {
// // 'Key get error'
// return retryWithMergedOptions();
// }
// if((res.body as any).length != 16){
// // 'Key not fully downloaded'
// return retryWithMergedOptions();
// }
// return res;
// }
// ]);
// return rkey;
// }
// catch (error: any) {
// error.p = segIndex;
// throw error;
// }
// }
// }
// async getKey(key: Key, segIndex: number, segOffset: number){
// const kURI = extFn.getURI(key.uri, this.data.baseurl);
// const p = segIndex;
// if (!this.data.keys[kURI]) {
// try{
// const rkey = await this.downloadKey(key, segIndex, segOffset);
// if (!rkey)
// throw new Error();
// this.data.keys[kURI] = rkey.body;
// }
// catch (error: any) {
// error.p = p;
// throw error;
// }
// }
// // get ivs
// const iv = Buffer.alloc(16);
// const ivs = key.iv ? key.iv : [0, 0, 0, p + 1];
// for (let i = 0; i < ivs.length; i++) {
// iv.writeUInt32BE(ivs[i], i * 4);
// }
// return crypto.createDecipheriv('aes-128-cbc', this.data.keys[kURI], iv);
// }
// }
// const extFn = {
// getURI: (uri: string, baseurl?: string) => {
// const httpURI = /^https{0,1}:/.test(uri);
// if (!baseurl && !httpURI) {
// throw new Error('No base and not http(s) uri');
// }
// else if (httpURI) {
// return uri;
// }
// return baseurl + uri;
// },
// getDownloadInfo: (dateStart: number, partsDL: number, partsTotal: number, downloadedBytes: number) => {
// const dateElapsed = Date.now() - dateStart;
// const percentFxd = parseInt((partsDL / partsTotal * 100).toFixed());
// const percent = percentFxd < 100 ? percentFxd : (partsTotal == partsDL ? 100 : 99);
// const revParts = dateElapsed * (partsTotal / partsDL - 1);
// const downloadSpeed = downloadedBytes / (dateElapsed / 1000); //Bytes per second
// return { percent, time: revParts, downloadSpeed };
// },
// getData: (partIndex: number, uri: string, headers: Record<string, string>, segOffset: number, isKey: boolean, timeout: number, retry: number, afterResponse: ((res: Response, retryWithMergedOptions: () => Response) => Response)[]) => {
// // get file if uri is local
// if (uri.startsWith('file://')) {
// return {
// body: fs.readFileSync(url.fileURLToPath(uri)),
// };
// }
// // base options
// headers = headers && typeof headers == 'object' ? headers : {};
// const options = { headers, retry, responseType: 'buffer', hooks: {
// beforeRequest: [
// (options: Record<string, Record<string, unknown>>) => {
// if(!options.headers['user-agent']){
// options.headers['user-agent'] = 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:70.0) Gecko/20100101 Firefox/70.0';
// }
// //TODO: implement fix for hidive properly
// if ((options.url.hostname as string).match('hidive')) {
// options.headers['referrer'] = 'https://www.hidive.com/';
// options.headers['origin'] = 'https://www.hidive.com';
// } else if ((options.url.hostname as string).includes('animecdn')) {
// options.headers = {
// origin: 'https://www.animeonegai.com',
// referer: 'https://www.animeonegai.com/',
// range: options.headers['range']
// };
// }
// // console.log(' - Req:', options.url.pathname);
// }
// ],
// afterResponse: [(fixMiddleWare as (r: Response, s: () => Response) => Response)].concat(afterResponse || []),
// beforeRetry: [
// (_: any, error: Error, retryCount: number) => {
// if(error){
// const partType = isKey ? 'Key': 'Part';
// const partIndx = partIndex + 1 + segOffset;
// console.warn('%s %s: %d attempt to retrieve data', partType, partIndx, retryCount + 1);
// console.error(`\t${error.message}`);
// }
// }
// ]
// }} as Record<string, unknown>;
// // proxy
// options.timeout = timeout;
// // do request
// return got(uri, options);
// }
// };
// export default hlsDownload;

View file

@ -75,7 +75,7 @@ let argvC: {
rawoutput: string;
nocleanup: boolean;
help: boolean | undefined;
service: 'crunchy' | 'hidive' | 'ao' | 'adn';
service: 'crunchy' | 'hidive' | 'adn';
update: boolean;
fontName: string | undefined;
_: (string | number)[];

View file

@ -1,4 +1,4 @@
import { aoSearchLocales, dubLanguageCodes, languages, searchLocales, subtitleLanguagesFilter } from './module.langsData';
import { dubLanguageCodes, languages, searchLocales, subtitleLanguagesFilter } from './module.langsData';
import { CrunchyVideoPlayStreams, CrunchyAudioPlayStreams } from '../@types/enums';
const groups = {
@ -35,7 +35,7 @@ export type TAppArg<T extends boolean | string | number | unknown[], K = any> =
default: T | undefined;
name?: string;
};
service: Array<'crunchy' | 'hidive' | 'ao' | 'adn' | 'all'>;
service: Array<'crunchy' | 'hidive' | 'adn' | 'all'>;
usage: string; // -(-)${name} will be added for each command,
demandOption?: true;
transformer?: (value: T) => K;
@ -110,12 +110,12 @@ const args: TAppArg<boolean | number | string | unknown[]>[] = [
describe: 'Set the service locale',
docDescribe: 'Set the local that will be used for the API.',
group: 'search',
choices: [...searchLocales.filter((a) => a !== undefined), ...aoSearchLocales.filter((a) => a !== undefined)] as string[],
choices: [...searchLocales.filter((a) => a !== undefined)] as string[],
default: {
default: 'en-US'
},
type: 'string',
service: ['crunchy', 'ao', 'adn'],
service: ['crunchy', 'adn'],
usage: '${locale}'
},
{
@ -226,7 +226,7 @@ const args: TAppArg<boolean | number | string | unknown[]>[] = [
describe: 'Download only once the video with the best selected quality',
type: 'boolean',
group: 'dl',
service: ['crunchy', 'ao'],
service: ['crunchy'],
docDescribe:
'If selected, the best selected quality will be downloaded only for the first language,' +
'\nthen the worst video quality with the same audio quality will be downloaded for every other language.' +
@ -689,7 +689,7 @@ const args: TAppArg<boolean | number | string | unknown[]>[] = [
group: 'util',
service: ['all'],
type: 'string',
choices: ['crunchy', 'hidive', 'ao', 'adn'],
choices: ['crunchy', 'hidive', 'adn'],
usage: '${service}',
default: {
default: ''
@ -806,7 +806,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)',
docDescribe: true,
group: 'auth',
service: ['crunchy', 'ao'],
service: ['crunchy'],
type: 'string',
usage: '${token}',
default: {

View file

@ -26,7 +26,6 @@ const hdPflCfgFile = path.join(workingDir, 'config', 'hd_profile');
const sessCfgFile = {
cr: path.join(workingDir, 'config', 'cr_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');
@ -34,7 +33,6 @@ const tokenFile = {
cr: path.join(workingDir, 'config', 'cr_token'),
hd: path.join(workingDir, 'config', 'hd_token'),
hdNew: path.join(workingDir, 'config', 'hd_new_token'),
ao: path.join(workingDir, 'config', 'ao_token'),
adn: path.join(workingDir, 'config', 'adn_token')
};
@ -237,24 +235,6 @@ const saveADNToken = (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 = () => {
let session = loadYamlCfgFile(sessCfgFile.hd, true);
if (typeof session !== 'object' || session === null || Array.isArray(session)) {
@ -392,8 +372,6 @@ export {
loadNewHDToken,
saveHDProfile,
loadHDProfile,
saveAOToken,
loadAOToken,
getState,
setState,
writeYamlCfgFile,

View file

@ -14,9 +14,6 @@ export type DataType = {
hidive: {
s: ItemType;
};
ao: {
s: ItemType;
};
adn: {
s: ItemType;
};
@ -36,10 +33,6 @@ const addToArchive = (
service: 'hidive';
type: 's';
}
| {
service: 'ao';
type: 's';
}
| {
service: 'adn';
type: 's';
@ -59,16 +52,7 @@ const addToArchive = (
});
(data as any)[kind.service][kind.type] = items;
} else {
if (kind.service === 'ao') {
data['ao'] = {
s: [
{
id: ID,
already: []
}
]
};
} else if (kind.service === 'crunchy') {
if (kind.service === 'crunchy') {
data['crunchy'] = {
s: ([] as ItemType).concat(
kind.type === 's'
@ -120,10 +104,6 @@ const downloaded = (
service: 'hidive';
type: 's';
}
| {
service: 'ao';
type: 's';
}
| {
service: 'adn';
type: 's';
@ -150,7 +130,7 @@ const downloaded = (
fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4));
};
const makeCommand = (service: 'crunchy' | 'hidive' | 'ao' | 'adn'): Partial<ArgvType>[] => {
const makeCommand = (service: 'crunchy' | 'hidive' | 'adn'): Partial<ArgvType>[] => {
const data = loadData();
const ret: Partial<ArgvType>[] = [];
const kind = data[service];

View file

@ -32,7 +32,7 @@ function hasDisplay(): boolean {
// req
export class Req {
private sessCfg: string;
private service: 'cr' | 'hd' | 'ao' | 'adn';
private service: 'cr' | 'hd' | 'adn';
private session: Record<
string,
{
@ -51,7 +51,7 @@ export class Req {
private domain: Record<string, unknown>,
private debug: boolean,
private nosess = false,
private type: 'cr' | 'hd' | 'ao' | 'adn'
private type: 'cr' | 'hd' | 'adn'
) {
this.sessCfg = yamlCfg.sessCfgFile[type];
this.service = type;

View file

@ -5,7 +5,6 @@ export type LanguageItem = {
hd_locale?: string;
adn_locale?: string;
new_hd_locale?: string;
ao_locale?: string;
locale: string;
code: string;
name: string;
@ -13,13 +12,13 @@ export type LanguageItem = {
};
const languages: LanguageItem[] = [
{ locale: 'un', code: 'und', name: 'Undetermined', language: 'Undetermined', new_hd_locale: 'und', cr_locale: 'und', adn_locale: 'und', ao_locale: 'und' },
{ locale: 'un', code: 'und', name: 'Undetermined', language: 'Undetermined', new_hd_locale: 'und', cr_locale: 'und', adn_locale: 'und' },
{ cr_locale: 'en-US', new_hd_locale: 'en-US', hd_locale: 'English', locale: 'en', code: 'eng', name: 'English' },
{ cr_locale: 'en-IN', locale: 'en-IN', code: 'eng', name: 'English (India)' },
{ cr_locale: 'es-LA', new_hd_locale: 'es-MX', hd_locale: 'Spanish LatAm', locale: 'es-419', code: 'spa', 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-419', hd_locale: 'Spanish', locale: 'es-419', code: 'spa-419', name: 'Spanish', language: 'Latin American Spanish' },
{ cr_locale: 'es-ES', new_hd_locale: 'es-ES', hd_locale: 'Spanish Europe', locale: 'es-ES', code: 'spa-ES', name: 'Castilian', language: 'European Spanish' },
{ cr_locale: 'pt-BR', ao_locale: 'pt', new_hd_locale: 'pt-BR', hd_locale: 'Portuguese', locale: 'pt-BR', code: 'por', name: 'Portuguese', language: 'Brazilian Portuguese' },
{ cr_locale: 'pt-BR', new_hd_locale: 'pt-BR', hd_locale: 'Portuguese', locale: 'pt-BR', code: 'por', name: 'Portuguese', language: 'Brazilian Portuguese' },
{ cr_locale: 'pt-PT', locale: 'pt-PT', code: 'por', name: 'Portuguese (Portugal)', language: 'Portugues (Portugal)' },
{ cr_locale: 'fr-FR', 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' },
@ -42,7 +41,7 @@ const languages: LanguageItem[] = [
{ cr_locale: 'vi-VN', locale: 'vi-VN', code: 'vie', name: 'Vietnamese', language: 'Tiếng Việt' },
{ cr_locale: 'id-ID', locale: 'id-ID', code: 'ind', name: 'Indonesian', language: 'Bahasa Indonesia' },
{ cr_locale: 'te-IN', locale: 'te-IN', code: 'tel', name: 'Telugu (India)', language: 'తెలుగు' },
{ cr_locale: 'ja-JP', adn_locale: 'ja', ao_locale: 'ja', hd_locale: 'Japanese', locale: 'ja', code: 'jpn', name: 'Japanese' }
{ cr_locale: 'ja-JP', adn_locale: 'ja', hd_locale: 'Japanese', locale: 'ja', code: 'jpn', name: 'Japanese' }
];
// add en language names
@ -96,19 +95,6 @@ const searchLocales = (() => {
];
})();
export const aoSearchLocales = (() => {
return [
'',
...new Set(
languages
.map((l) => {
return l.ao_locale;
})
.slice(0, -1)
)
];
})();
// convert
const fixLanguageTag = (tag: string) => {
tag = typeof tag == 'string' ? tag : 'und';

View file

@ -80,11 +80,6 @@ export async function parse(manifest: string, language?: LanguageItem, url?: str
const options: RequestInit = {
method: 'head'
};
if (playlist.sidx.uri.includes('animecdn'))
options.headers = {
origin: 'https://www.animeonegai.com',
referer: 'https://www.animeonegai.com/'
};
const item = await fetch(playlist.sidx.uri, options);
if (!item.ok)
console.warn(`${item.status}: ${item.statusText}, Unable to fetch byteLength for audio stream ${Math.round(playlist.attributes.BANDWIDTH / 1024)}KiB/s`);
@ -164,11 +159,6 @@ export async function parse(manifest: string, language?: LanguageItem, url?: str
const options: RequestInit = {
method: 'head'
};
if (playlist.sidx.uri.includes('animecdn'))
options.headers = {
origin: 'https://www.animeonegai.com',
referer: 'https://www.animeonegai.com/'
};
const item = await fetch(playlist.sidx.uri, options);
if (!item.ok)
console.warn(

View file

@ -2,7 +2,7 @@
"name": "multi-downloader-nx",
"short_name": "aniDL",
"version": "5.5.7",
"description": "Downloader for Crunchyroll, Hidive, AnimeOnegai, and AnimationDigitalNetwork with CLI and GUI",
"description": "Downloader for Crunchyroll, Hidive, and AnimationDigitalNetwork with CLI and GUI",
"keywords": [
"download",
"downloader",
@ -90,7 +90,7 @@
"protoc": "^1.1.3",
"removeNPMAbsolutePaths": "^3.0.1",
"ts-node": "^10.9.2",
"typescript": "^5.9.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.45.0"
},
"scripts": {

View file

@ -128,10 +128,10 @@ importers:
version: 17.0.33
'@typescript-eslint/eslint-plugin':
specifier: ^8.45.0
version: 8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2)
version: 8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.3))(eslint@9.36.0)(typescript@5.9.3)
'@typescript-eslint/parser':
specifier: ^8.45.0
version: 8.45.0(eslint@9.36.0)(typescript@5.9.2)
version: 8.45.0(eslint@9.36.0)(typescript@5.9.3)
'@yao-pkg/pkg':
specifier: ^6.7.0
version: 6.7.0
@ -152,13 +152,13 @@ importers:
version: 3.0.1
ts-node:
specifier: ^10.9.2
version: 10.9.2(@types/node@24.6.0)(typescript@5.9.2)
version: 10.9.2(@types/node@24.6.0)(typescript@5.9.3)
typescript:
specifier: ^5.9.2
version: 5.9.2
specifier: ^5.9.3
version: 5.9.3
typescript-eslint:
specifier: ^8.45.0
version: 8.45.0(eslint@9.36.0)(typescript@5.9.2)
version: 8.45.0(eslint@9.36.0)(typescript@5.9.3)
packages:
@ -763,14 +763,6 @@ packages:
resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==}
engines: {node: '>=4'}
async-function@1.0.0:
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
engines: {node: '>= 0.4'}
async-generator-function@1.0.0:
resolution: {integrity: sha512-+NAXNqgCrB95ya4Sr66i1CL2hqLVckAk7xwRYWdcm39/ELQ6YNn1aw5r0bdQtqNZgQpEWzc5yc/igXc7aL5SLA==}
engines: {node: '>= 0.4'}
b4a@1.7.3:
resolution: {integrity: sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==}
peerDependencies:
@ -1286,16 +1278,12 @@ packages:
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
generator-function@2.0.0:
resolution: {integrity: sha512-xPypGGincdfyl/AiSGa7GjXLkvld9V7GjZlowup9SHIJnQnHLFiLODCd/DqKOp0PBagbHJ68r1KJI9Mut7m4sA==}
engines: {node: '>= 0.4'}
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
get-intrinsic@1.3.1:
resolution: {integrity: sha512-fk1ZVEeOX9hVZ6QzoBNEC55+Ucqg4sTVwrVuigZhuRPESVFpMyXnd3sbXvPOwp7Y9riVyANiqhEuRF0G1aVSeQ==}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
get-proto@1.0.1:
@ -2137,8 +2125,8 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
typescript@5.9.2:
resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
@ -2673,41 +2661,41 @@ snapshots:
'@types/node': 24.6.0
optional: true
'@typescript-eslint/eslint-plugin@8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2)':
'@typescript-eslint/eslint-plugin@8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.3))(eslint@9.36.0)(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.1
'@typescript-eslint/parser': 8.45.0(eslint@9.36.0)(typescript@5.9.2)
'@typescript-eslint/parser': 8.45.0(eslint@9.36.0)(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.45.0
'@typescript-eslint/type-utils': 8.45.0(eslint@9.36.0)(typescript@5.9.2)
'@typescript-eslint/utils': 8.45.0(eslint@9.36.0)(typescript@5.9.2)
'@typescript-eslint/type-utils': 8.45.0(eslint@9.36.0)(typescript@5.9.3)
'@typescript-eslint/utils': 8.45.0(eslint@9.36.0)(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.45.0
eslint: 9.36.0
graphemer: 1.4.0
ignore: 7.0.5
natural-compare: 1.4.0
ts-api-utils: 2.1.0(typescript@5.9.2)
typescript: 5.9.2
ts-api-utils: 2.1.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.2)':
'@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.45.0
'@typescript-eslint/types': 8.45.0
'@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.2)
'@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.45.0
debug: 4.4.3
eslint: 9.36.0
typescript: 5.9.2
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.45.0(typescript@5.9.2)':
'@typescript-eslint/project-service@8.45.0(typescript@5.9.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.45.0(typescript@5.9.2)
'@typescript-eslint/tsconfig-utils': 8.45.0(typescript@5.9.3)
'@typescript-eslint/types': 8.45.0
debug: 4.4.3
typescript: 5.9.2
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@ -2716,28 +2704,28 @@ snapshots:
'@typescript-eslint/types': 8.45.0
'@typescript-eslint/visitor-keys': 8.45.0
'@typescript-eslint/tsconfig-utils@8.45.0(typescript@5.9.2)':
'@typescript-eslint/tsconfig-utils@8.45.0(typescript@5.9.3)':
dependencies:
typescript: 5.9.2
typescript: 5.9.3
'@typescript-eslint/type-utils@8.45.0(eslint@9.36.0)(typescript@5.9.2)':
'@typescript-eslint/type-utils@8.45.0(eslint@9.36.0)(typescript@5.9.3)':
dependencies:
'@typescript-eslint/types': 8.45.0
'@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.2)
'@typescript-eslint/utils': 8.45.0(eslint@9.36.0)(typescript@5.9.2)
'@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.3)
'@typescript-eslint/utils': 8.45.0(eslint@9.36.0)(typescript@5.9.3)
debug: 4.4.3
eslint: 9.36.0
ts-api-utils: 2.1.0(typescript@5.9.2)
typescript: 5.9.2
ts-api-utils: 2.1.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/types@8.45.0': {}
'@typescript-eslint/typescript-estree@8.45.0(typescript@5.9.2)':
'@typescript-eslint/typescript-estree@8.45.0(typescript@5.9.3)':
dependencies:
'@typescript-eslint/project-service': 8.45.0(typescript@5.9.2)
'@typescript-eslint/tsconfig-utils': 8.45.0(typescript@5.9.2)
'@typescript-eslint/project-service': 8.45.0(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.45.0(typescript@5.9.3)
'@typescript-eslint/types': 8.45.0
'@typescript-eslint/visitor-keys': 8.45.0
debug: 4.4.3
@ -2745,19 +2733,19 @@ snapshots:
is-glob: 4.0.3
minimatch: 9.0.5
semver: 7.7.2
ts-api-utils: 2.1.0(typescript@5.9.2)
typescript: 5.9.2
ts-api-utils: 2.1.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.45.0(eslint@9.36.0)(typescript@5.9.2)':
'@typescript-eslint/utils@8.45.0(eslint@9.36.0)(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0)
'@typescript-eslint/scope-manager': 8.45.0
'@typescript-eslint/types': 8.45.0
'@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.2)
'@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.3)
eslint: 9.36.0
typescript: 5.9.2
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@ -2869,10 +2857,6 @@ snapshots:
dependencies:
tslib: 2.8.1
async-function@1.0.0: {}
async-generator-function@1.0.0: {}
b4a@1.7.3: {}
balanced-match@1.0.2: {}
@ -2995,7 +2979,7 @@ snapshots:
call-bound@1.0.4:
dependencies:
call-bind-apply-helpers: 1.0.2
get-intrinsic: 1.3.1
get-intrinsic: 1.3.0
callsites@3.1.0: {}
@ -3445,20 +3429,15 @@ snapshots:
function-bind@1.1.2: {}
generator-function@2.0.0: {}
get-caller-file@2.0.5: {}
get-intrinsic@1.3.1:
get-intrinsic@1.3.0:
dependencies:
async-function: 1.0.0
async-generator-function: 1.0.0
call-bind-apply-helpers: 1.0.2
es-define-property: 1.0.1
es-errors: 1.3.0
es-object-atoms: 1.1.1
function-bind: 1.1.2
generator-function: 2.0.0
get-proto: 1.0.1
gopd: 1.2.0
has-symbols: 1.1.0
@ -4154,14 +4133,14 @@ snapshots:
dependencies:
call-bound: 1.0.4
es-errors: 1.3.0
get-intrinsic: 1.3.1
get-intrinsic: 1.3.0
object-inspect: 1.13.4
side-channel-weakmap@1.0.2:
dependencies:
call-bound: 1.0.4
es-errors: 1.3.0
get-intrinsic: 1.3.1
get-intrinsic: 1.3.0
object-inspect: 1.13.4
side-channel-map: 1.0.1
@ -4325,11 +4304,11 @@ snapshots:
tree-kill@1.2.2: {}
ts-api-utils@2.1.0(typescript@5.9.2):
ts-api-utils@2.1.0(typescript@5.9.3):
dependencies:
typescript: 5.9.2
typescript: 5.9.3
ts-node@10.9.2(@types/node@24.6.0)(typescript@5.9.2):
ts-node@10.9.2(@types/node@24.6.0)(typescript@5.9.3):
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.11
@ -4343,7 +4322,7 @@ snapshots:
create-require: 1.1.1
diff: 4.0.2
make-error: 1.3.6
typescript: 5.9.2
typescript: 5.9.3
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
@ -4365,20 +4344,20 @@ snapshots:
typed-query-selector@2.12.0: {}
typescript-eslint@8.45.0(eslint@9.36.0)(typescript@5.9.2):
typescript-eslint@8.45.0(eslint@9.36.0)(typescript@5.9.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2)
'@typescript-eslint/parser': 8.45.0(eslint@9.36.0)(typescript@5.9.2)
'@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.2)
'@typescript-eslint/utils': 8.45.0(eslint@9.36.0)(typescript@5.9.2)
'@typescript-eslint/eslint-plugin': 8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.3))(eslint@9.36.0)(typescript@5.9.3)
'@typescript-eslint/parser': 8.45.0(eslint@9.36.0)(typescript@5.9.3)
'@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.3)
'@typescript-eslint/utils': 8.45.0(eslint@9.36.0)(typescript@5.9.3)
eslint: 9.36.0
typescript: 5.9.2
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
typescript@5.4.5: {}
typescript@5.9.2: {}
typescript@5.9.3: {}
ufo@1.6.1: {}