commit
f39645166e
21 changed files with 1276 additions and 25 deletions
50
@types/adnPlayerConfig.d.ts
vendored
Normal file
50
@types/adnPlayerConfig.d.ts
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
export interface ADNPlayerConfig {
|
||||
player: Player;
|
||||
}
|
||||
|
||||
export interface Player {
|
||||
image: string;
|
||||
options: Options;
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
user: User;
|
||||
chromecast: Chromecast;
|
||||
ios: Ios;
|
||||
video: Video;
|
||||
dock: any[];
|
||||
preference: Preference;
|
||||
}
|
||||
|
||||
export interface Chromecast {
|
||||
appId: string;
|
||||
refreshTokenUrl: string;
|
||||
}
|
||||
|
||||
export interface Ios {
|
||||
videoUrl: string;
|
||||
appUrl: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface Preference {
|
||||
quality: string;
|
||||
autoplay: boolean;
|
||||
language: string;
|
||||
green: boolean;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
hasAccess: boolean;
|
||||
profileId: number;
|
||||
refreshToken: string;
|
||||
refreshTokenUrl: string;
|
||||
}
|
||||
|
||||
export interface Video {
|
||||
startDate: null;
|
||||
currentDate: Date;
|
||||
available: boolean;
|
||||
free: boolean;
|
||||
url: string;
|
||||
}
|
||||
46
@types/adnSearch.d.ts
vendored
Normal file
46
@types/adnSearch.d.ts
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
export interface ADNSearch {
|
||||
shows: ADNSearchShow[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ADNSearchShow {
|
||||
id: number;
|
||||
title: string;
|
||||
type: string;
|
||||
originalTitle: string;
|
||||
shortTitle: string;
|
||||
reference: string;
|
||||
age: string;
|
||||
languages: string[];
|
||||
summary: string;
|
||||
image: string;
|
||||
image2x: string;
|
||||
imageHorizontal: string;
|
||||
imageHorizontal2x: string;
|
||||
url: string;
|
||||
urlPath: string;
|
||||
episodeCount: number;
|
||||
genres: string[];
|
||||
copyright: string;
|
||||
rating: number;
|
||||
ratingsCount: number;
|
||||
commentsCount: number;
|
||||
qualities: string[];
|
||||
simulcast: boolean;
|
||||
free: boolean;
|
||||
available: boolean;
|
||||
download: boolean;
|
||||
basedOn: string;
|
||||
tagline: null;
|
||||
firstReleaseYear: string;
|
||||
productionStudio: string;
|
||||
countryOfOrigin: string;
|
||||
productionTeam: ProductionTeam[];
|
||||
nextVideoReleaseDate: null;
|
||||
indexable: boolean;
|
||||
}
|
||||
|
||||
export interface ProductionTeam {
|
||||
role: string;
|
||||
name: string;
|
||||
}
|
||||
51
@types/adnStreams.d.ts
vendored
Normal file
51
@types/adnStreams.d.ts
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
export interface ADNStreams {
|
||||
links: Links;
|
||||
video: Video;
|
||||
metadata: Metadata;
|
||||
}
|
||||
|
||||
export interface Links {
|
||||
streaming: Streaming;
|
||||
subtitles: Subtitles;
|
||||
history: string;
|
||||
nextVideoUrl: string;
|
||||
previousVideoUrl: string;
|
||||
}
|
||||
|
||||
export interface Streaming {
|
||||
[streams: string]: Streams;
|
||||
}
|
||||
|
||||
export interface Streams {
|
||||
mobile: string;
|
||||
sd: string;
|
||||
hd: string;
|
||||
fhd: string;
|
||||
auto: string;
|
||||
}
|
||||
|
||||
export interface Subtitles {
|
||||
all: string;
|
||||
}
|
||||
|
||||
export interface Metadata {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
summary: null;
|
||||
rating: number;
|
||||
}
|
||||
|
||||
export interface Video {
|
||||
guid: string;
|
||||
id: number;
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
url: string;
|
||||
image: string;
|
||||
tcEpisodeStart: string;
|
||||
tcEpisodeEnd: string;
|
||||
tcIntroStart: string;
|
||||
tcIntroEnd: string;
|
||||
tcEndingStart: string;
|
||||
tcEndingEnd: string;
|
||||
}
|
||||
11
@types/adnSubtitles.d.ts
vendored
Normal file
11
@types/adnSubtitles.d.ts
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export interface ADNSubtitles {
|
||||
[subtitleLang: string]: Subtitle[];
|
||||
}
|
||||
|
||||
export interface Subtitle {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
positionAlign: string;
|
||||
lineAlign: string;
|
||||
text: string;
|
||||
}
|
||||
77
@types/adnVideos.d.ts
vendored
Normal file
77
@types/adnVideos.d.ts
vendored
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
export interface ADNVideos {
|
||||
videos: ADNVideo[];
|
||||
}
|
||||
|
||||
export interface ADNVideo {
|
||||
id: number;
|
||||
title: string;
|
||||
name: string;
|
||||
number: string;
|
||||
shortNumber: string;
|
||||
season: string;
|
||||
reference: string;
|
||||
type: string;
|
||||
order: number;
|
||||
image: string;
|
||||
image2x: string;
|
||||
summary: string;
|
||||
releaseDate: Date;
|
||||
duration: number;
|
||||
url: string;
|
||||
urlPath: string;
|
||||
embeddedUrl: string;
|
||||
languages: string[];
|
||||
qualities: string[];
|
||||
rating: number;
|
||||
ratingsCount: number;
|
||||
commentsCount: number;
|
||||
available: boolean;
|
||||
download: boolean;
|
||||
free: boolean;
|
||||
freeWithAds: boolean;
|
||||
show: Show;
|
||||
indexable: boolean;
|
||||
isSelected?: boolean;
|
||||
}
|
||||
|
||||
export interface Show {
|
||||
id: number;
|
||||
title: string;
|
||||
type: string;
|
||||
originalTitle: string;
|
||||
shortTitle: string;
|
||||
reference: string;
|
||||
age: string;
|
||||
languages: string[];
|
||||
summary: string;
|
||||
image: string;
|
||||
image2x: string;
|
||||
imageHorizontal: string;
|
||||
imageHorizontal2x: string;
|
||||
url: string;
|
||||
urlPath: string;
|
||||
episodeCount: number;
|
||||
genres: string[];
|
||||
copyright: string;
|
||||
rating: number;
|
||||
ratingsCount: number;
|
||||
commentsCount: number;
|
||||
qualities: string[];
|
||||
simulcast: boolean;
|
||||
free: boolean;
|
||||
available: boolean;
|
||||
download: boolean;
|
||||
basedOn: string;
|
||||
tagline: string;
|
||||
firstReleaseYear: string;
|
||||
productionStudio: string;
|
||||
countryOfOrigin: string;
|
||||
productionTeam: ProductionTeam[];
|
||||
nextVideoReleaseDate: Date;
|
||||
indexable: boolean;
|
||||
}
|
||||
|
||||
export interface ProductionTeam {
|
||||
role: string;
|
||||
name: string;
|
||||
}
|
||||
4
@types/ws.d.ts
vendored
4
@types/ws.d.ts
vendored
|
|
@ -30,8 +30,8 @@ export type MessageTypes = {
|
|||
'isDownloading': [undefined, boolean],
|
||||
'openFolder': [FolderTypes, undefined],
|
||||
'changeProvider': [undefined, boolean],
|
||||
'type': [undefined, 'crunchy'|'hidive'|'ao'||undefined],
|
||||
'setup': ['crunchy'|'hidive'|'ao'|undefined, undefined],
|
||||
'type': [undefined, 'crunchy'|'hidive'|'ao'|'adn'|undefined],
|
||||
'setup': ['crunchy'|'hidive'|'ao'|'adn'|undefined, undefined],
|
||||
'openFile': [[FolderTypes, string], undefined],
|
||||
'openURL': [string, undefined],
|
||||
'isSetup': [undefined, boolean],
|
||||
|
|
|
|||
822
adn.ts
Normal file
822
adn.ts
Normal file
|
|
@ -0,0 +1,822 @@
|
|||
// Package Info
|
||||
import packageJson from './package.json';
|
||||
|
||||
// Node
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import crypto from 'crypto';
|
||||
|
||||
// Plugins
|
||||
import shlp from 'sei-helper';
|
||||
import m3u8 from 'm3u8-parsed';
|
||||
|
||||
// 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 streamdl 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';
|
||||
|
||||
// Types
|
||||
import { ServiceClass } from './@types/serviceClassInterface';
|
||||
import { AuthData, AuthResponse, SearchData, SearchResponse, SearchResponseItem } from './@types/messageHandler';
|
||||
import { sxItem } from './crunchy';
|
||||
import { DownloadedMedia } from './@types/hidiveTypes';
|
||||
import { ADNSearch, ADNSearchShow } from './@types/adnSearch';
|
||||
import { ADNVideo, ADNVideos } from './@types/adnVideos';
|
||||
import { ADNPlayerConfig } from './@types/adnPlayerConfig';
|
||||
import { ADNStreams } from './@types/adnStreams';
|
||||
import { ADNSubtitles } from './@types/adnSubtitles';
|
||||
|
||||
export default class AnimationDigitalNetwork implements ServiceClass {
|
||||
public cfg: yamlCfg.ConfigObject;
|
||||
public locale: string;
|
||||
private token: Record<string, any>;
|
||||
private req: reqModule.Req;
|
||||
private posAlignMap: { [key: string]: number } = {
|
||||
'start': 1,
|
||||
'end': 3
|
||||
};
|
||||
private lineAlignMap: { [key: string]: number } = {
|
||||
'middle': 8,
|
||||
'end': 4
|
||||
};
|
||||
private jpnStrings: string[] = [
|
||||
'vostf',
|
||||
'vostde'
|
||||
];
|
||||
private deuStrings: string[] = [
|
||||
'vde'
|
||||
];
|
||||
private fraStrings: string[] = [
|
||||
'vf'
|
||||
];
|
||||
private deuSubStrings: string[] = [
|
||||
'vde',
|
||||
'vostde'
|
||||
];
|
||||
private fraSubStrings: string[] = [
|
||||
'vf',
|
||||
'vostf'
|
||||
];
|
||||
|
||||
constructor(private debug = false) {
|
||||
this.cfg = yamlCfg.loadCfg();
|
||||
this.token = yamlCfg.loadADNToken();
|
||||
this.req = new reqModule.Req(domain, debug, false, 'adn');
|
||||
this.locale = 'fr';
|
||||
}
|
||||
|
||||
public async cli() {
|
||||
console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`);
|
||||
const argv = yargs.appArgv(this.cfg.cli);
|
||||
this.locale = argv.locale;
|
||||
if (argv.debug)
|
||||
this.debug = true;
|
||||
|
||||
// load binaries
|
||||
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||
if (argv.allDubs) {
|
||||
argv.dubLang = langsData.dubLanguageCodes;
|
||||
}
|
||||
if (argv.auth) {
|
||||
//Authenticate
|
||||
await this.doAuth({
|
||||
username: argv.username ?? await shlp.question('[Q] LOGIN/EMAIL'),
|
||||
password: argv.password ?? await shlp.question('[Q] PASSWORD ')
|
||||
});
|
||||
} else if (argv.search && argv.search.length > 2) {
|
||||
//Search
|
||||
await this.doSearch({ ...argv, search: argv.search as string });
|
||||
} else if (argv.s && !isNaN(parseInt(argv.s,10)) && parseInt(argv.s,10) > 0) {
|
||||
const selected = await this.selectShow(parseInt(argv.s), argv.e, argv.but, argv.all);
|
||||
if (selected.isOk) {
|
||||
for (const select of selected.value) {
|
||||
if (!(await this.getEpisode(select, {...argv, skipsubs: false}))) {
|
||||
console.error(`Unable to download selected episode ${select.shortNumber}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
console.info('No option selected or invalid value entered. Try --help.');
|
||||
}
|
||||
}
|
||||
|
||||
private generateRandomString(length: number) {
|
||||
const characters = '0123456789abcdef';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private parseCookies(cookiesString: string | null): Record<string, string> {
|
||||
const cookies: Record<string, string> = {};
|
||||
if (cookiesString) {
|
||||
cookiesString.split(';').forEach(cookie => {
|
||||
const parts = cookie.split('=');
|
||||
const name = parts.shift()?.trim();
|
||||
const value = decodeURIComponent(parts.join('='));
|
||||
if (name) {
|
||||
cookies[name] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
return cookies;
|
||||
}
|
||||
|
||||
private convertToSSATimestamp(timestamp: number): string {
|
||||
const seconds = Math.floor(timestamp);
|
||||
const centiseconds = Math.round((timestamp - seconds) * 100);
|
||||
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
public async doSearch(data: SearchData): Promise<SearchResponse> {
|
||||
const limit = 12;
|
||||
const offset = data.page ? data.page * limit : 0;
|
||||
const searchReq = await this.req.getData(`https://gw.api.animationdigitalnetwork.fr/show/catalog?maxAgeCategory=18&offset=${offset}&limit=${limit}&search=${encodeURIComponent(data.search)}`, {
|
||||
'headers': {
|
||||
'X-Target-Distribution': this.locale
|
||||
}
|
||||
});
|
||||
if (!searchReq.ok || !searchReq.res) {
|
||||
console.error('Search FAILED!');
|
||||
return { isOk: false, reason: new Error('Search failed. No more information provided') };
|
||||
}
|
||||
const searchData = await searchReq.res.json() as ADNSearch;
|
||||
const searchItems: ADNSearchShow[] = [];
|
||||
console.info('Search Results:');
|
||||
for (const show of searchData.shows) {
|
||||
searchItems.push(show);
|
||||
let fullType: string;
|
||||
if (show.type == 'EPS') {
|
||||
fullType = `S.${show.id}`;
|
||||
} else if (show.type == 'MOV' || show.type == 'OAV') {
|
||||
fullType = `E.${show.id}`;
|
||||
} else {
|
||||
fullType = 'Unknown';
|
||||
console.warn(`Unknown type ${show.type}, please report this.`);
|
||||
}
|
||||
console.log(`[${fullType}] ${show.title}`);
|
||||
}
|
||||
return { isOk: true, value: searchItems.flatMap((a): SearchResponseItem => {
|
||||
return {
|
||||
id: a.id+'',
|
||||
image: a.image ?? '/notFound.png',
|
||||
name: a.title,
|
||||
rating: a.rating,
|
||||
desc: a.summary
|
||||
};
|
||||
})};
|
||||
}
|
||||
|
||||
public async doAuth(data: AuthData): Promise<AuthResponse> {
|
||||
const authData = new URLSearchParams({
|
||||
'username': data.username,
|
||||
'password': data.password,
|
||||
'source': 'Web',
|
||||
'rememberMe': 'true'
|
||||
}).toString();
|
||||
const authReqOpts: reqModule.Params = {
|
||||
method: 'POST',
|
||||
body: authData
|
||||
};
|
||||
const authReq = await this.req.getData('https://gw.api.animationdigitalnetwork.fr/authentication/login', authReqOpts);
|
||||
if(!authReq.ok || !authReq.res){
|
||||
console.error('Authentication failed!');
|
||||
return { isOk: false, reason: new Error('Authentication failed') };
|
||||
}
|
||||
this.token = await authReq.res.json();
|
||||
const cookies = this.parseCookies(authReq.res.headers.get('Set-Cookie'));
|
||||
this.token.refreshToken = cookies.adnrt;
|
||||
yamlCfg.saveADNToken(this.token);
|
||||
console.info('Authentication Success');
|
||||
return { isOk: true, value: undefined };
|
||||
}
|
||||
|
||||
public async refreshToken() {
|
||||
const authReq = await this.req.getData('https://gw.api.animationdigitalnetwork.fr/authentication/refresh', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token.accessToken}`,
|
||||
'Cookie': `adnrt=${this.token.refreshToken}`,
|
||||
'X-Access-Token': this.token.accessToken
|
||||
},
|
||||
body: '{}'
|
||||
});
|
||||
if(!authReq.ok || !authReq.res){
|
||||
console.error('Token refresh failed!');
|
||||
return { isOk: false, reason: new Error('Token refresh failed') };
|
||||
}
|
||||
this.token = await authReq.res.json();
|
||||
const cookies = this.parseCookies(authReq.res.headers.get('Set-Cookie'));
|
||||
//this.token.refreshtoken = this.token.refreshToken;
|
||||
this.token.refreshToken = cookies.adnrt;
|
||||
yamlCfg.saveADNToken(this.token);
|
||||
return { isOk: true, value: undefined };
|
||||
}
|
||||
|
||||
public async getShow(id: number) {
|
||||
const getShowData = await this.req.getData(`https://gw.api.animationdigitalnetwork.fr/video/show/${id}?maxAgeCategory=18&limit=-1&order=asc`, {
|
||||
'headers': {
|
||||
'X-Target-Distribution': this.locale
|
||||
}
|
||||
});
|
||||
if (!getShowData.ok || !getShowData.res) {
|
||||
console.error('Failed to get Series Data');
|
||||
return { isOk: false };
|
||||
}
|
||||
const showData = await getShowData.res.json() as ADNVideos;
|
||||
return { isOk: true, value: showData };
|
||||
}
|
||||
|
||||
public async listShow(id: number) {
|
||||
const show = await this.getShow(id);
|
||||
if (!show.isOk || !show.value) {
|
||||
console.error('Failed to list show data: Failed to get show');
|
||||
return { isOk: false };
|
||||
}
|
||||
const showData = show.value.videos[0].show;
|
||||
console.info(`[S.${showData.id}] ${showData.title}`);
|
||||
const specials: ADNVideo[] = [];
|
||||
let episodeIndex = 0, specialIndex = 0;
|
||||
for (const episode of show.value.videos) {
|
||||
const episodeNumber = parseInt(episode.shortNumber);
|
||||
if (!episodeNumber) {
|
||||
specialIndex++;
|
||||
const special = show.value.videos.splice(episodeIndex, 1);
|
||||
special[0].shortNumber = 'S'+specialIndex;
|
||||
specials.push(...special);
|
||||
episodeIndex--;
|
||||
} else {
|
||||
console.info(` [E${episode.shortNumber}] ${episode.number} - ${episode.name}`);
|
||||
}
|
||||
episodeIndex++;
|
||||
}
|
||||
for (const special of specials) {
|
||||
console.info(` [${special.shortNumber}] ${special.number} - ${special.name}`);
|
||||
}
|
||||
show.value.videos.push(...specials);
|
||||
return { isOk: true, value: show.value };
|
||||
}
|
||||
|
||||
public async selectShow(id: number, e: string | undefined, but: boolean, all: boolean) {
|
||||
const getShowData = await this.listShow(id);
|
||||
if (!getShowData.isOk || !getShowData.value) {
|
||||
return { isOk: false, value: [] };
|
||||
}
|
||||
console.info('');
|
||||
console.info('-'.repeat(30));
|
||||
console.info('');
|
||||
const showData = getShowData.value;
|
||||
const doEpsFilter = parseSelect(e as string);
|
||||
const selEpsArr: ADNVideo[] = [];
|
||||
for (const episode of showData.videos) {
|
||||
if (
|
||||
all ||
|
||||
but && !doEpsFilter.isSelected([episode.shortNumber, episode.id+'']) ||
|
||||
!but && doEpsFilter.isSelected([episode.shortNumber, episode.id+''])
|
||||
) {
|
||||
selEpsArr.push({ isSelected: true, ...episode });
|
||||
console.info('%s[%s] %s',
|
||||
'✓ ',
|
||||
episode.shortNumber,
|
||||
episode.name,
|
||||
);
|
||||
}
|
||||
}
|
||||
return { isOk: true, value: selEpsArr };
|
||||
}
|
||||
|
||||
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 getEpisode(data: ADNVideo, options: yargs.ArgvType) {
|
||||
//TODO: Move all the requests for getting the m3u8 here
|
||||
const res = await this.downloadEpisode(data, options);
|
||||
if (res === undefined || res.error) {
|
||||
console.error('Failed to download media list');
|
||||
return { isOk: false, reason: new Error('Failed to download media list') };
|
||||
} else {
|
||||
if (!options.skipmux) {
|
||||
await this.muxStreams(res.data, { ...options, output: res.fileName });
|
||||
} else {
|
||||
console.info('Skipping mux');
|
||||
}
|
||||
downloaded({
|
||||
service: 'adn',
|
||||
type: 's'
|
||||
}, data.id+'', [data.shortNumber]);
|
||||
return { isOk: res, value: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
public async downloadEpisode(data: ADNVideo, options: yargs.ArgvType) {
|
||||
if(!this.token.accessToken){
|
||||
console.error('Authentication required!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.cfg.bin.ffmpeg)
|
||||
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||
|
||||
let mediaName = '...';
|
||||
let fileName;
|
||||
const variables: Variable[] = [];
|
||||
if(data.show.title && data.shortNumber && data.title){
|
||||
mediaName = `${data.show.shortTitle} - ${data.shortNumber} - ${data.title}`;
|
||||
}
|
||||
|
||||
const files: DownloadedMedia[] = [];
|
||||
|
||||
let dlFailed = false;
|
||||
let dlVideoOnce = false; // Variable to save if best selected video quality was downloaded
|
||||
|
||||
const refreshToken = await this.refreshToken();
|
||||
if (!refreshToken.isOk) {
|
||||
console.error('Failed to refresh token');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const configReq = await this.req.getData(`https://gw.api.animationdigitalnetwork.fr/player/video/${data.id}/configuration`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token.accessToken}`
|
||||
}
|
||||
});
|
||||
if(!configReq.ok || !configReq.res){
|
||||
console.error('Player Config Request failed!');
|
||||
return undefined;
|
||||
}
|
||||
const configuration = await configReq.res.json() as ADNPlayerConfig;
|
||||
if (!configuration.player.options.user.hasAccess) {
|
||||
console.error('You don\'t have access to this video!');
|
||||
return undefined;
|
||||
}
|
||||
const tokenReq = await this.req.getData(configuration.player.options.user.refreshTokenUrl || 'https://gw.api.animationdigitalnetwork.fr/player/refresh/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Player-Refresh-Token': `${configuration.player.options.user.refreshToken}`
|
||||
}
|
||||
});
|
||||
if(!tokenReq.ok || !tokenReq.res){
|
||||
console.error('Player Token Request failed!');
|
||||
return undefined;
|
||||
}
|
||||
const token = await tokenReq.res.json() as {
|
||||
refreshToken: string,
|
||||
accessToken: string,
|
||||
token: string
|
||||
};
|
||||
|
||||
const linksUrl = configuration.player.options.video.url || `https://gw.api.animationdigitalnetwork.fr/player/video/${data.id}/link`;
|
||||
const key = this.generateRandomString(16);
|
||||
const decryptionKey = key + '7fac1178830cfe0c';
|
||||
|
||||
const authorization = crypto.publicEncrypt({
|
||||
'key': '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCbQrCJBRmaXM4gJidDmcpWDssg\nnumHinCLHAgS4buMtdH7dEGGEUfBofLzoEdt1jqcrCDT6YNhM0aFCqbLOPFtx9cg\n/X2G/G5bPVu8cuFM0L+ehp8s6izK1kjx3OOPH/kWzvstM5tkqgJkNyNEvHdeJl6\nKhS+IFEqwvZqgbBpKuwIDAQAB\n-----END PUBLIC KEY-----',
|
||||
padding: crypto.constants.RSA_PKCS1_PADDING
|
||||
}, Buffer.from(JSON.stringify({
|
||||
k: key,
|
||||
t: token.token
|
||||
}), 'utf-8')).toString('base64');
|
||||
|
||||
//TODO: Add chapter support
|
||||
const streamsRequest = await this.req.getData(linksUrl+'?freeWithAds=true&adaptive=true&withMetadata=true&source=Web', {
|
||||
'headers': {
|
||||
'X-Player-Token': authorization,
|
||||
'X-Target-Distribution': this.locale
|
||||
}
|
||||
});
|
||||
if(!streamsRequest.ok || !streamsRequest.res){
|
||||
if (streamsRequest.error?.res.status == 403 || streamsRequest.res?.status == 403) {
|
||||
console.error('Georestricted!');
|
||||
} else {
|
||||
console.error('Streams request failed!');
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
const streams = await streamsRequest.res.json() as ADNStreams;
|
||||
for (const streamName in streams.links.streaming) {
|
||||
let audDub: langsData.LanguageItem;
|
||||
if (this.jpnStrings.includes(streamName)) {
|
||||
audDub = langsData.languages.find(a=>a.code == 'jpn') as langsData.LanguageItem;
|
||||
} else if (this.deuStrings.includes(streamName)) {
|
||||
audDub = langsData.languages.find(a=>a.code == 'deu') as langsData.LanguageItem;
|
||||
} else if (this.fraStrings.includes(streamName)) {
|
||||
audDub = langsData.languages.find(a=>a.code == 'fra') as langsData.LanguageItem;
|
||||
} else {
|
||||
console.error(`Language ${streamName} not recognized, please report this.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!options.dubLang.includes(audDub.code)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
console.info(`Requesting: [${data.id}] ${mediaName} (${audDub.name})`);
|
||||
|
||||
variables.push(...([
|
||||
['title', data.title, true],
|
||||
['episode', data.shortNumber, false],
|
||||
['service', 'ADN', false],
|
||||
['seriesTitle', data.show.shortTitle, true],
|
||||
['showTitle', data.show.title, true],
|
||||
['season', data.season, false]
|
||||
] as [AvailableFilenameVars, string|number, boolean][]).map((a): Variable => {
|
||||
return {
|
||||
name: a[0],
|
||||
replaceWith: a[1],
|
||||
type: typeof a[1],
|
||||
sanitize: a[2]
|
||||
} as Variable;
|
||||
}));
|
||||
|
||||
console.info('Playlists URL: %s', streams.links.streaming[streamName].auto);
|
||||
|
||||
let tsFile = undefined;
|
||||
|
||||
if (!dlFailed && !options.novids) {
|
||||
const streamPlaylistsLocationReq = await this.req.getData(streams.links.streaming[streamName].auto);
|
||||
if (!streamPlaylistsLocationReq.ok || !streamPlaylistsLocationReq.res) {
|
||||
console.error('CAN\'T FETCH VIDEO PLAYLIST LOCATION!');
|
||||
return undefined;
|
||||
}
|
||||
const streamPlaylistLocation = await streamPlaylistsLocationReq.res.json() as {'location': string};
|
||||
const streamPlaylistsReq = await this.req.getData(streamPlaylistLocation.location);
|
||||
if (!streamPlaylistsReq.ok || !streamPlaylistsReq.res) {
|
||||
console.error('CAN\'T FETCH VIDEO PLAYLISTS!');
|
||||
dlFailed = true;
|
||||
} else {
|
||||
const streamPlaylistBody = await streamPlaylistsReq.res.text();
|
||||
if (!options.novids) {
|
||||
const streamPlaylists = m3u8(streamPlaylistBody);
|
||||
const plServerList: string[] = [],
|
||||
plStreams: Record<string, Record<string, string>> = {},
|
||||
plQuality: {
|
||||
str: string,
|
||||
dim: string,
|
||||
CODECS: string,
|
||||
RESOLUTION: {
|
||||
width: number,
|
||||
height: number
|
||||
}
|
||||
}[] = [];
|
||||
for(const pl of streamPlaylists.playlists){
|
||||
// set quality
|
||||
const plResolution = pl.attributes.RESOLUTION;
|
||||
const plResolutionText = `${plResolution.width}x${plResolution.height}`;
|
||||
// set codecs
|
||||
const plCodecs = pl.attributes.CODECS;
|
||||
// parse uri
|
||||
const plUri = new URL(pl.uri);
|
||||
let plServer = plUri.hostname;
|
||||
// set server list
|
||||
if (plUri.searchParams.get('cdn')){
|
||||
plServer += ` (${plUri.searchParams.get('cdn')})`;
|
||||
}
|
||||
if (!plServerList.includes(plServer)){
|
||||
plServerList.push(plServer);
|
||||
}
|
||||
// add to server
|
||||
if (!Object.keys(plStreams).includes(plServer)){
|
||||
plStreams[plServer] = {};
|
||||
}
|
||||
if(
|
||||
plStreams[plServer][plResolutionText]
|
||||
&& plStreams[plServer][plResolutionText] != pl.uri
|
||||
&& typeof plStreams[plServer][plResolutionText] != 'undefined'
|
||||
) {
|
||||
console.error(`Non duplicate url for ${plServer} detected, please report to developer!`);
|
||||
} else{
|
||||
plStreams[plServer][plResolutionText] = pl.uri;
|
||||
}
|
||||
// set plQualityStr
|
||||
const plBandwidth = Math.round(pl.attributes.BANDWIDTH/1024);
|
||||
const qualityStrAdd = `${plResolutionText} (${plBandwidth}KiB/s)`;
|
||||
const qualityStrRegx = new RegExp(qualityStrAdd.replace(/(:|\(|\)|\/)/g, '\\$1'), 'm');
|
||||
const qualityStrMatch = !plQuality.map(a => a.str).join('\r\n').match(qualityStrRegx);
|
||||
if(qualityStrMatch){
|
||||
plQuality.push({
|
||||
str: qualityStrAdd,
|
||||
dim: plResolutionText,
|
||||
CODECS: plCodecs,
|
||||
RESOLUTION: plResolution
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
options.x = options.x > plServerList.length ? 1 : options.x;
|
||||
|
||||
const plSelectedServer = plServerList[options.x - 1];
|
||||
const plSelectedList = plStreams[plSelectedServer];
|
||||
plQuality.sort((a, b) => {
|
||||
const aMatch: RegExpMatchArray | never[] = a.dim.match(/[0-9]+/) || [];
|
||||
const bMatch: RegExpMatchArray | never[] = b.dim.match(/[0-9]+/) || [];
|
||||
return parseInt(aMatch[0]) - parseInt(bMatch[0]);
|
||||
});
|
||||
let quality = options.q === 0 ? plQuality.length : options.q;
|
||||
if(quality > plQuality.length) {
|
||||
console.warn(`The requested quality of ${options.q} is greater than the maximum ${plQuality.length}.\n[WARN] Therefor the maximum will be capped at ${plQuality.length}.`);
|
||||
quality = plQuality.length;
|
||||
}
|
||||
// When best selected video quality is already downloaded
|
||||
if(dlVideoOnce && options.dlVideoOnce) {
|
||||
// Select the lowest resolution with the same codecs
|
||||
while(quality !=1 && plQuality[quality - 1].CODECS == plQuality[quality - 2].CODECS) {
|
||||
quality--;
|
||||
}
|
||||
}
|
||||
const selPlUrl = plSelectedList[plQuality.map(a => a.dim)[quality - 1]] ? plSelectedList[plQuality.map(a => a.dim)[quality - 1]] : '';
|
||||
console.info(`Servers available:\n\t${plServerList.join('\n\t')}`);
|
||||
console.info(`Available qualities:\n\t${plQuality.map((a, ind) => `[${ind+1}] ${a.str}`).join('\n\t')}`);
|
||||
|
||||
if(selPlUrl != ''){
|
||||
variables.push({
|
||||
name: 'height',
|
||||
type: 'number',
|
||||
replaceWith: quality === 0 ? plQuality[plQuality.length - 1].RESOLUTION.height as number : plQuality[quality - 1].RESOLUTION.height
|
||||
}, {
|
||||
name: 'width',
|
||||
type: 'number',
|
||||
replaceWith: quality === 0 ? plQuality[plQuality.length - 1].RESOLUTION.width as number : plQuality[quality - 1].RESOLUTION.width
|
||||
});
|
||||
|
||||
console.info(`Selected quality: ${Object.keys(plSelectedList).find(a => plSelectedList[a] === selPlUrl)} @ ${plSelectedServer}`);
|
||||
console.info('Stream URL:', selPlUrl);
|
||||
// TODO check filename
|
||||
fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep);
|
||||
const outFile = parseFileName(options.fileName + '.' + audDub.name, variables, options.numbers, options.override).join(path.sep);
|
||||
console.info(`Output filename: ${outFile}`);
|
||||
const chunkPage = await this.req.getData(selPlUrl);
|
||||
if(!chunkPage.ok || !chunkPage.res){
|
||||
console.error('CAN\'T FETCH VIDEO PLAYLIST!');
|
||||
dlFailed = true;
|
||||
} else {
|
||||
const chunkPageBody = await chunkPage.res.text();
|
||||
const chunkPlaylist = m3u8(chunkPageBody);
|
||||
const totalParts = chunkPlaylist.segments.length;
|
||||
const mathParts = Math.ceil(totalParts / options.partsize);
|
||||
const mathMsg = `(${mathParts}*${options.partsize})`;
|
||||
console.info('Total parts in stream:', totalParts, mathMsg);
|
||||
tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile);
|
||||
const split = outFile.split(path.sep).slice(0, -1);
|
||||
split.forEach((val, ind, arr) => {
|
||||
const isAbsolut = path.isAbsolute(outFile as string);
|
||||
if (!fs.existsSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val)))
|
||||
fs.mkdirSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val));
|
||||
});
|
||||
const dlStreamByPl = await new streamdl({
|
||||
output: `${tsFile}.ts`,
|
||||
timeout: options.timeout,
|
||||
m3u8json: chunkPlaylist,
|
||||
baseurl: selPlUrl.replace('playlist.m3u8',''),
|
||||
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: data.image,
|
||||
parent: {
|
||||
title: data.show.title
|
||||
},
|
||||
title: data.title,
|
||||
language: audDub
|
||||
}) : undefined
|
||||
}).download();
|
||||
if (!dlStreamByPl.ok) {
|
||||
console.error(`DL Stats: ${JSON.stringify(dlStreamByPl.parts)}\n`);
|
||||
dlFailed = true;
|
||||
}
|
||||
files.push({
|
||||
type: 'Video',
|
||||
path: `${tsFile}.ts`,
|
||||
lang: audDub
|
||||
});
|
||||
dlVideoOnce = true;
|
||||
}
|
||||
} else{
|
||||
console.error('Quality not selected!\n');
|
||||
dlFailed = true;
|
||||
}
|
||||
} else if (options.novids) {
|
||||
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);
|
||||
}
|
||||
await this.sleep(options.waittime);
|
||||
}
|
||||
|
||||
if(options.dlsubs.indexOf('all') > -1){
|
||||
options.dlsubs = ['all'];
|
||||
}
|
||||
|
||||
if (options.nosubs) {
|
||||
console.info('Subtitles downloading disabled from nosubs flag.');
|
||||
options.skipsubs = true;
|
||||
}
|
||||
|
||||
if(!options.skipsubs && options.dlsubs.indexOf('none') == -1) {
|
||||
if (Object.keys(streams.links.subtitles).length !== 0) {
|
||||
const subtitlesUrlReq = await this.req.getData(streams.links.subtitles.all);
|
||||
if(!subtitlesUrlReq.ok || !subtitlesUrlReq.res){
|
||||
console.error('Subtitle location request failed!');
|
||||
return undefined;
|
||||
}
|
||||
const subtitleUrl = await subtitlesUrlReq.res.json() as {'location': string};
|
||||
const encryptedSubtitlesReq = await this.req.getData(subtitleUrl.location);
|
||||
if(!encryptedSubtitlesReq.ok || !encryptedSubtitlesReq.res){
|
||||
console.error('Subtitle request failed!');
|
||||
return undefined;
|
||||
}
|
||||
const encryptedSubtitles = await encryptedSubtitlesReq.res.text();
|
||||
const iv = Buffer.from(encryptedSubtitles.slice(0, 24), 'base64');
|
||||
const derivedKey = Buffer.from(decryptionKey, 'hex');
|
||||
const encryptedData = Buffer.from(encryptedSubtitles.slice(24), 'base64');
|
||||
const decipher = crypto.createDecipheriv('aes-128-cbc', derivedKey, iv);
|
||||
const decryptedData = Buffer.concat([decipher.update(encryptedData), decipher.final()]).toString('utf8');
|
||||
|
||||
let subIndex = 0;
|
||||
const subtitles = JSON.parse(decryptedData) as ADNSubtitles;
|
||||
if (Object.keys(subtitles).length === 0) {
|
||||
console.warn('No subtitles found.');
|
||||
}
|
||||
for (const subName in subtitles) {
|
||||
console.debug(subName);
|
||||
let subLang: langsData.LanguageItem;
|
||||
if (this.deuSubStrings.includes(subName)) {
|
||||
subLang = langsData.languages.find(a=>a.code == 'deu') as langsData.LanguageItem;
|
||||
} else if (this.fraSubStrings.includes(subName)) {
|
||||
subLang = langsData.languages.find(a=>a.code == 'fra') as langsData.LanguageItem;
|
||||
} else {
|
||||
console.error(`Language ${subName} not recognized, please report this.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!options.dlsubs.includes(subLang.locale) && !options.dlsubs.includes('all')) {
|
||||
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);
|
||||
const split = sxData.path.split(path.sep).slice(0, -1);
|
||||
split.forEach((val, ind, arr) => {
|
||||
const isAbsolut = path.isAbsolute(sxData.path 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));
|
||||
});
|
||||
sxData.language = subLang;
|
||||
if(options.dlsubs.includes('all') || options.dlsubs.includes(subLang.locale)) {
|
||||
let subBody = '[Script Info]'
|
||||
+ '\nScriptType:V4.00'
|
||||
+ '\nWrapStyle: 0'
|
||||
+ '\nPlayResX: 1280'
|
||||
+ '\nPlayResY: 720'
|
||||
+ '\nScaledBorderAndShadow: yes'
|
||||
+ '\n[V4 Styles]'
|
||||
+ '\nFormat: Name,Fontname,Fontsize,PrimaryColour,SecondaryColour,TertiaryColour,BackColour,Bold,Italic,BorderStyle,Outline,Shadow,Alignment,MarginL,MarginR,MarginV,AlphaLevel,Encoding'
|
||||
+ `\nStyle: Default,${options.fontName ?? 'Arial'},${options.fontSize ?? 50},16777215,16777215,16777215,0,-1,0,1,1,0,2,20,20,20,0,0`
|
||||
+ '\n[Events]'
|
||||
+ '\nFormat: Marked,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text';
|
||||
|
||||
for (const sub of subtitles[subName]) {
|
||||
const [start, end, text, lineAlign, positionAlign] =
|
||||
[sub.startTime, sub.endTime, sub.text, sub.lineAlign, sub.positionAlign];
|
||||
const alignment = (this.posAlignMap[positionAlign] || 2) + (this.lineAlignMap[lineAlign] || 0);
|
||||
subBody += `\nDialogue: Marked=0,${this.convertToSSATimestamp(start)},${this.convertToSSATimestamp(end)},Default,,0,0,0,,${(alignment !== 2 ? `{\\a${alignment}}` : '')}${text.replace('\n', '\\N').replace('<i>', '{\\i1}').replace('</i>', '{\\i0}')}`;
|
||||
}
|
||||
sxData.title = `${subLang.language}`;
|
||||
sxData.fonts = fontsData.assFonts(subBody) as Font[];
|
||||
fs.writeFileSync(sxData.path, subBody);
|
||||
console.info(`Subtitle converted: ${sxData.file}`);
|
||||
files.push({
|
||||
type: 'Subtitle',
|
||||
...sxData as sxItem,
|
||||
cc: false
|
||||
});
|
||||
}
|
||||
subIndex++;
|
||||
}
|
||||
} else {
|
||||
console.warn('Couldn\'t find subtitles.');
|
||||
}
|
||||
} else{
|
||||
console.info('Subtitles downloading skipped!');
|
||||
}
|
||||
|
||||
return {
|
||||
error: dlFailed,
|
||||
data: files,
|
||||
fileName: fileName ? (path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName)) || './unknown' : './unknown'
|
||||
};
|
||||
}
|
||||
|
||||
public sleep(ms: number) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
[](https://discord.gg/qEpbWen5vq)
|
||||
|
||||
This downloader can download anime from different sites. Currently supported are *Crunchyroll*, *Hidive*, and *AnimeOnegai*.
|
||||
This downloader can download anime from different sites. Currently supported are *Crunchyroll*, *Hidive*, *AnimeOnegai*, and *AnimationDigitalNetwork*.
|
||||
|
||||
## Legal Warning
|
||||
|
||||
This application is not endorsed by or affiliated with *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.
|
||||
This application is not endorsed by or affiliated with *Crunchyroll*, *Hidive*, *AnimeOnegai*, or *AnimationDigitalNetwork*. This application enables you to download videos for offline viewing which may be forbidden by law in your country. The usage of this application may also cause a violation of the *Terms of Service* between you and the stream provider. This tool is not responsible for your actions; please make an informed decision before using this application.
|
||||
|
||||
## Dependencies
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ const MenuBar: React.FC = () => {
|
|||
return 'Hidive';
|
||||
case 'ao':
|
||||
return 'AnimeOnegai';
|
||||
case 'adn':
|
||||
return 'AnimationDigitalNetwork';
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import {Divider, Box, Button, Typography, Avatar} from '@mui/material';
|
|||
import useStore from '../hooks/useStore';
|
||||
import { StoreState } from './Store';
|
||||
|
||||
type Services = 'crunchy'|'hidive'|'ao';
|
||||
type Services = 'crunchy'|'hidive'|'ao'|'adn';
|
||||
|
||||
export const serviceContext = React.createContext<Services|undefined>(undefined);
|
||||
|
||||
|
|
@ -24,6 +24,7 @@ const ServiceProvider: FCWithChildren = ({ children }) => {
|
|||
<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('ao')} startIcon={<Avatar src={'https://www.animeonegai.com/assets/img/anime/general/ao3-favicon.png'} />}>AnimeOnegai</Button>
|
||||
<Button size='large' variant="contained" onClick={() => setService('adn')} startIcon={<Avatar src={'https://animationdigitalnetwork.de/favicon.ico'} />}>AnimationDigitalNetwork</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
: <serviceContext.Provider value={service}>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export type DownloadOptions = {
|
|||
export type StoreState = {
|
||||
episodeListing: Episode[];
|
||||
downloadOptions: DownloadOptions,
|
||||
service: 'crunchy'|'hidive'|'ao'|undefined,
|
||||
service: 'crunchy'|'hidive'|'ao'|'adn'|undefined,
|
||||
version: string,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { setState, getState, writeYamlCfgFile } from '../../modules/module.cfg-l
|
|||
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';
|
||||
|
||||
|
|
@ -37,6 +38,8 @@ export default class ServiceHandler {
|
|||
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 +58,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'));
|
||||
this.ws.events.on('type', async (_, respond) => respond(this.service === undefined ? undefined : this.service.name as 'hidive'|'crunchy'|'ao'|'adn'));
|
||||
this.ws.events.on('checkToken', async (_, respond) => {
|
||||
if (this.service === undefined)
|
||||
return respond({ isOk: false, reason: new Error('No service selected') });
|
||||
|
|
|
|||
138
gui/server/services/adn.ts
Normal file
138
gui/server/services/adn.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import { AuthData, CheckTokenResponse, DownloadData, EpisodeListResponse, MessageHandler, ResolveItemsData, SearchData, SearchResponse } from '../../../@types/messageHandler';
|
||||
import AnimationDigitalNetwork from '../../../adn';
|
||||
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 ADNHandler extends Base implements MessageHandler {
|
||||
private adn: AnimationDigitalNetwork;
|
||||
public name = 'adn';
|
||||
constructor(ws: WebSocketHandler) {
|
||||
super(ws);
|
||||
this.adn = new AnimationDigitalNetwork();
|
||||
this.initState();
|
||||
this.getDefaults();
|
||||
}
|
||||
|
||||
public getDefaults() {
|
||||
const _default = yargs.appArgv(this.adn.cfg.cli, true);
|
||||
this.adn.locale = _default.locale;
|
||||
}
|
||||
|
||||
public async auth(data: AuthData) {
|
||||
return this.adn.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.adn.doSearch(data);
|
||||
if (!search.isOk) {
|
||||
return search;
|
||||
}
|
||||
return { isOk: true, value: search.value };
|
||||
}
|
||||
|
||||
public async handleDefault(name: string) {
|
||||
return getDefault(name, this.adn.cfg.cli);
|
||||
}
|
||||
|
||||
public async availableDubCodes(): Promise<string[]> {
|
||||
const dubLanguageCodesArray: string[] = [];
|
||||
for(const language of languages){
|
||||
if (language.adn_locale)
|
||||
dubLanguageCodesArray.push(language.code);
|
||||
}
|
||||
return [...new Set(dubLanguageCodesArray)];
|
||||
}
|
||||
|
||||
public async availableSubCodes(): Promise<string[]> {
|
||||
const subLanguageCodesArray: string[] = [];
|
||||
for(const language of languages){
|
||||
if (language.adn_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 res = await this.adn.selectShow(parseInt(data.id), data.e, data.but, data.all);
|
||||
if (!res.isOk || !res.value)
|
||||
return res.isOk;
|
||||
this.addToQueue(res.value.map(a => {
|
||||
return {
|
||||
...data,
|
||||
ids: [a.id],
|
||||
title: a.title,
|
||||
parent: {
|
||||
title: a.show.shortTitle,
|
||||
season: a.season
|
||||
},
|
||||
e: a.shortNumber,
|
||||
image: a.image,
|
||||
episode: a.shortNumber
|
||||
};
|
||||
}));
|
||||
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.adn.listShow(parse);
|
||||
if (!request.isOk || !request.value)
|
||||
return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')};
|
||||
|
||||
return { isOk: true, value: request.value.videos.map(function(item) {
|
||||
return {
|
||||
e: item.shortNumber,
|
||||
lang: [],
|
||||
name: item.title,
|
||||
season: item.season,
|
||||
seasonTitle: item.show.title,
|
||||
episode: item.shortNumber,
|
||||
id: item.id+'',
|
||||
img: item.image,
|
||||
description: item.summary,
|
||||
time: item.duration+''
|
||||
};
|
||||
})};
|
||||
}
|
||||
|
||||
public async downloadItem(data: DownloadData) {
|
||||
this.setDownloading(true);
|
||||
console.debug(`Got download options: ${JSON.stringify(data)}`);
|
||||
const _default = yargs.appArgv(this.adn.cfg.cli, true);
|
||||
const res = await this.adn.selectShow(parseInt(data.id), data.e, false, false);
|
||||
if (res.isOk) {
|
||||
for (const select of res.value) {
|
||||
if (!(await this.adn.getEpisode(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 ADNHandler;
|
||||
6
index.ts
6
index.ts
|
|
@ -67,6 +67,9 @@ import update from './modules/module.updater';
|
|||
case 'ao':
|
||||
service = new (await import('./ao')).default;
|
||||
break;
|
||||
case 'adn':
|
||||
service = new (await import('./adn')).default;
|
||||
break;
|
||||
default:
|
||||
service = new (await import(`./${argv.service}`)).default;
|
||||
break;
|
||||
|
|
@ -85,6 +88,9 @@ import update from './modules/module.updater';
|
|||
case 'ao':
|
||||
service = new (await import('./ao')).default;
|
||||
break;
|
||||
case 'adn':
|
||||
service = new (await import('./adn')).default;
|
||||
break;
|
||||
default:
|
||||
service = new (await import(`./${argv.service}`)).default;
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -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'|'all'>) => {
|
||||
const transformService = (str: Array<'crunchy'|'hidive'|'ao'|'adn'|'all'>) => {
|
||||
const services: string[] = [];
|
||||
str.forEach(function(part) {
|
||||
switch(part) {
|
||||
|
|
@ -16,6 +16,9 @@ const transformService = (str: Array<'crunchy'|'hidive'|'ao'|'all'>) => {
|
|||
case 'ao':
|
||||
services.push('AnimeOnegai');
|
||||
break;
|
||||
case 'adn':
|
||||
services.push('AnimationDigitalNetwork');
|
||||
break;
|
||||
case 'all':
|
||||
services.push('All');
|
||||
break;
|
||||
|
|
@ -30,7 +33,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*, or *AnimeOnegai*.
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ let argvC: {
|
|||
debug: boolean | undefined;
|
||||
nocleanup: boolean;
|
||||
help: boolean | undefined;
|
||||
service: 'crunchy' | 'hidive' | 'ao';
|
||||
service: 'crunchy' | 'hidive' | 'ao' | 'adn';
|
||||
update: boolean;
|
||||
fontName: string | undefined;
|
||||
_: (string | number)[];
|
||||
|
|
@ -75,7 +75,7 @@ let argvC: {
|
|||
originalFontSize: boolean;
|
||||
keepAllVideos: boolean;
|
||||
syncTiming: boolean;
|
||||
callbackMaker?: (data: DownloadInfo) => HLSCallback,
|
||||
callbackMaker?: (data: DownloadInfo) => HLSCallback;
|
||||
};
|
||||
|
||||
export type ArgvType = typeof argvC;
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export type TAppArg<T extends boolean|string|number|unknown[], K = any> = {
|
|||
default: T|undefined,
|
||||
name?: string
|
||||
},
|
||||
service: Array<'crunchy'|'hidive'|'ao'|'all'>,
|
||||
service: Array<'crunchy'|'hidive'|'ao'|'adn'|'all'>,
|
||||
usage: string // -(-)${name} will be added for each command,
|
||||
demandOption?: true,
|
||||
transformer?: (value: T) => K
|
||||
|
|
@ -112,7 +112,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
|||
default: 'en-US'
|
||||
},
|
||||
type: 'string',
|
||||
service: ['crunchy', 'ao'],
|
||||
service: ['crunchy', 'ao', 'adn'],
|
||||
usage: '${locale}'
|
||||
},
|
||||
{
|
||||
|
|
@ -556,7 +556,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
|||
group: 'util',
|
||||
service: ['all'],
|
||||
type: 'string',
|
||||
choices: ['crunchy', 'hidive', 'ao'],
|
||||
choices: ['crunchy', 'hidive', 'ao', 'adn'],
|
||||
usage: '${service}',
|
||||
default: {
|
||||
default: ''
|
||||
|
|
@ -577,7 +577,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
|||
group: 'fonts',
|
||||
describe: 'Set the font to use in subtiles',
|
||||
docDescribe: true,
|
||||
service: ['hidive'],
|
||||
service: ['hidive', 'adn'],
|
||||
type: 'string',
|
||||
usage: '${fontName}',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -20,14 +20,16 @@ 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')
|
||||
ao: path.join(workingDir, 'config', 'ao_sess'),
|
||||
adn: path.join(workingDir, 'config', 'adn_sess')
|
||||
};
|
||||
const stateFile = path.join(workingDir, 'config', 'guistate');
|
||||
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'),
|
||||
hdNew: path.join(workingDir, 'config', 'hd_new_token')
|
||||
adn: path.join(workingDir, 'config', 'adn_token')
|
||||
};
|
||||
|
||||
export const ensureConfig = () => {
|
||||
|
|
@ -215,7 +217,26 @@ const saveCRToken = (data: Record<string, unknown>) => {
|
|||
console.error('Can\'t save token file to disk!');
|
||||
}
|
||||
};
|
||||
|
||||
const loadADNToken = () => {
|
||||
let token = loadYamlCfgFile(tokenFile.adn, true);
|
||||
if(typeof token !== 'object' || token === null || Array.isArray(token)){
|
||||
token = {};
|
||||
}
|
||||
return token;
|
||||
};
|
||||
|
||||
const saveADNToken = (data: Record<string, unknown>) => {
|
||||
const cfgFolder = path.dirname(tokenFile.adn);
|
||||
try{
|
||||
fs.ensureDirSync(cfgFolder);
|
||||
fs.writeFileSync(`${tokenFile.adn}.yml`, yaml.stringify(data));
|
||||
}
|
||||
catch(e){
|
||||
console.error('Can\'t save token file to disk!');
|
||||
}
|
||||
};
|
||||
|
||||
const loadAOToken = () => {
|
||||
let token = loadYamlCfgFile(tokenFile.ao, true);
|
||||
if(typeof token !== 'object' || token === null || Array.isArray(token)){
|
||||
|
|
@ -235,7 +256,6 @@ const saveAOToken = (data: Record<string, unknown>) => {
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
const loadHDSession = () => {
|
||||
let session = loadYamlCfgFile(sessCfgFile.hd, true);
|
||||
if(typeof session !== 'object' || session === null || Array.isArray(session)){
|
||||
|
|
@ -369,6 +389,8 @@ export {
|
|||
loadCRSession,
|
||||
saveCRToken,
|
||||
loadCRToken,
|
||||
saveADNToken,
|
||||
loadADNToken,
|
||||
saveHDSession,
|
||||
loadHDSession,
|
||||
saveHDToken,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ export type DataType = {
|
|||
ao: {
|
||||
s: ItemType
|
||||
},
|
||||
adn: {
|
||||
s: ItemType
|
||||
},
|
||||
crunchy: {
|
||||
srz: ItemType,
|
||||
s: ItemType
|
||||
|
|
@ -32,6 +35,9 @@ const addToArchive = (kind: {
|
|||
} | {
|
||||
service: 'ao',
|
||||
type: 's'
|
||||
} | {
|
||||
service: 'adn',
|
||||
type: 's'
|
||||
}, ID: string) => {
|
||||
const data = loadData();
|
||||
|
||||
|
|
@ -65,6 +71,15 @@ const addToArchive = (kind: {
|
|||
already: [] as string[]
|
||||
} : []),
|
||||
};
|
||||
} else if (kind.service === 'adn') {
|
||||
data['adn'] = {
|
||||
s: [
|
||||
{
|
||||
id: ID,
|
||||
already: []
|
||||
}
|
||||
]
|
||||
};
|
||||
} else {
|
||||
data['hidive'] = {
|
||||
s: [
|
||||
|
|
@ -88,6 +103,9 @@ const downloaded = (kind: {
|
|||
} | {
|
||||
service: 'ao',
|
||||
type: 's'
|
||||
} | {
|
||||
service: 'adn',
|
||||
type: 's'
|
||||
}, ID: string, episode: string[]) => {
|
||||
let data = loadData();
|
||||
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));
|
||||
};
|
||||
|
||||
const makeCommand = (service: 'crunchy'|'hidive'|'ao') : Partial<ArgvType>[] => {
|
||||
const makeCommand = (service: 'crunchy'|'hidive'|'ao'|'adn') : Partial<ArgvType>[] => {
|
||||
const data = loadData();
|
||||
const ret: Partial<ArgvType>[] = [];
|
||||
const kind = data[service];
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export type Params = {
|
|||
// req
|
||||
export class Req {
|
||||
private sessCfg: string;
|
||||
private service: 'cr'|'funi'|'hd'|'ao';
|
||||
private service: 'cr'|'funi'|'hd'|'ao'|'adn';
|
||||
private session: Record<string, {
|
||||
value: string;
|
||||
expires: Date;
|
||||
|
|
@ -25,7 +25,7 @@ export class Req {
|
|||
private cfgDir = yamlCfg.cfgDir;
|
||||
private curl: boolean|string = false;
|
||||
|
||||
constructor(private domain: Record<string, unknown>, private debug: boolean, private nosess = false, private type: 'cr'|'funi'|'hd'|'ao') {
|
||||
constructor(private domain: Record<string, unknown>, private debug: boolean, private nosess = false, private type: 'cr'|'funi'|'hd'|'ao'|'adn') {
|
||||
this.sessCfg = yamlCfg.sessCfgFile[type];
|
||||
this.service = type;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
export type LanguageItem = {
|
||||
cr_locale?: string,
|
||||
hd_locale?: string,
|
||||
adn_locale?: string,
|
||||
new_hd_locale?: string,
|
||||
ao_locale?: string,
|
||||
locale: string,
|
||||
|
|
@ -19,8 +20,8 @@ const languages: LanguageItem[] = [
|
|||
{ 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-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: 'de-DE', hd_locale: 'German', locale: 'de', code: 'deu', name: 'German' },
|
||||
{ cr_locale: 'fr-FR', adn_locale: 'fr', hd_locale: 'French', locale: 'fr', code: 'fra', name: 'French' },
|
||||
{ cr_locale: 'de-DE', adn_locale: 'de', hd_locale: 'German', locale: 'de', code: 'deu', name: 'German' },
|
||||
{ cr_locale: 'ar-ME', locale: 'ar', code: 'ara-ME', name: 'Arabic' },
|
||||
{ cr_locale: 'ar-SA', hd_locale: 'Arabic', locale: 'ar', code: 'ara', name: 'Arabic (Saudi Arabia)' },
|
||||
{ cr_locale: 'it-IT', hd_locale: 'Italian', locale: 'it', code: 'ita', name: 'Italian' },
|
||||
|
|
@ -39,7 +40,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', ao_locale: 'ja', hd_locale: 'Japanese', locale: 'ja', code: 'jpn', name: 'Japanese' },
|
||||
{ cr_locale: 'ja-JP', adn_locale: 'ja', ao_locale: 'ja', hd_locale: 'Japanese', locale: 'ja', code: 'jpn', name: 'Japanese' },
|
||||
];
|
||||
|
||||
// add en language names
|
||||
|
|
@ -67,7 +68,7 @@ const subtitleLanguagesFilter = (() => {
|
|||
})();
|
||||
|
||||
const searchLocales = (() => {
|
||||
return ['', ...new Set(languages.map(l => { return l.cr_locale; }).slice(0, -1))];
|
||||
return ['', ...new Set(languages.map(l => { return l.cr_locale; }).slice(0, -1)), ...new Set(languages.map(l => { return l.adn_locale; }).slice(0, -1))];
|
||||
})();
|
||||
|
||||
export const aoSearchLocales = (() => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue