Initial commit to add hidive

This commit is contained in:
AnimeDL 2023-07-04 08:37:43 -07:00
parent 8414edfab9
commit 612bdff774
29 changed files with 3461 additions and 1757 deletions

View file

@ -49,7 +49,8 @@ body:
options:
- Funimation
- Crunchyroll
- Both
- Hidive
- All
- Irrelevant
validations:
required: true

5
.gitignore vendored
View file

@ -21,6 +21,9 @@ test.*
updates.json
funi_token.yml
cr_token.yml
hd_profile.yml
hd_sess.yml
hd_token.yml
archive.json
fonts
.webpack/
@ -28,3 +31,5 @@ out/
dist/
gui/react/build/
docker-compose.yml
crunchyendpoints
.vscode

84
@types/hidiveEpisodeList.d.ts vendored Normal file
View file

@ -0,0 +1,84 @@
export interface HidiveEpisodeList {
Code: number;
Status: string;
Message: null;
Messages: Record<unknown, unknown>;
Data: Data;
Timestamp: string;
IPAddress: string;
}
export interface Data {
Title: HidiveTitle;
}
export interface HidiveTitle {
Id: number;
Name: string;
ShortSynopsis: string;
MediumSynopsis: string;
LongSynopsis: string;
KeyArtUrl: string;
MasterArtUrl: string;
Rating: string;
OverallRating: number;
RatingCount: number;
MALScore: null;
UserRating: number;
RunTime: number;
ShowInfoTitle: string;
FirstPremiereDate: Date;
EpisodeCount: number;
SeasonName: string;
RokuHDArtUrl: string;
RokuSDArtUrl: string;
IsRateable: boolean;
InQueue: boolean;
IsFavorite: boolean;
IsContinueWatching: boolean;
ContinueWatching: ContinueWatching;
Episodes: HidiveEpisode[];
LoadTime: number;
}
export interface ContinueWatching {
Id: string;
ProfileId: number;
EpisodeId: number;
Status: string;
CurrentTime: number;
UserId: number;
TitleId: number;
SeasonId: number;
VideoId: number;
TotalSeconds: number;
CreatedDT: Date;
ModifiedDT: Date;
}
export interface HidiveEpisode {
Id: number;
Number: number;
Name: string;
Summary: string;
HIDIVEPremiereDate: Date;
ScreenShotSmallUrl: string;
ScreenShotCompressedUrl: string;
SeasonNumber: number;
TitleId: number;
SeasonNumberValue: number;
EpisodeNumberValue: number;
VideoKey: string;
DisplayNameLong: string;
PercentProgress: number;
LoadTime: number;
}
export interface HidiveEpisodeExtra extends HidiveEpisode {
titleId: number;
epKey: string;
nameLong: string;
seriesTitle: string;
seriesId?: number;
isSelected: boolean;
}

47
@types/hidiveSearch.d.ts vendored Normal file
View file

@ -0,0 +1,47 @@
export interface HidiveSearch {
Code: number;
Status: string;
Message: null;
Messages: Record<unknown, unknown>;
Data: HidiveSearchData;
Timestamp: string;
IPAddress: string;
}
export interface HidiveSearchData {
Query: string;
Slug: string;
TitleResults: HidiveSearchItem[];
SearchId: number;
IsSearchPinned: boolean;
IsPinnedSearchAvailable: boolean;
}
export interface HidiveSearchItem {
Id: number;
Name: string;
ShortSynopsis: string;
MediumSynopsis: string;
LongSynopsis: string;
KeyArtUrl: string;
MasterArtUrl: string;
Rating: string;
OverallRating: number;
RatingCount: number;
MALScore: null;
UserRating: number;
RunTime: number | null;
ShowInfoTitle: string;
FirstPremiereDate: Date;
EpisodeCount: number;
SeasonName: string;
RokuHDArtUrl: string;
RokuSDArtUrl: string;
IsRateable: boolean;
InQueue: boolean;
IsFavorite: boolean;
IsContinueWatching: boolean;
ContinueWatching: null;
Episodes: any[];
LoadTime: number;
}

60
@types/hidiveTypes.d.ts vendored Normal file
View file

@ -0,0 +1,60 @@
export interface HidiveVideoList {
Code: number;
Status: string;
Message: null;
Messages: Record<unknown, unknown>;
Data: HidiveVideo;
Timestamp: string;
IPAddress: string;
}
export interface HidiveVideo {
ShowAds: boolean;
CaptionCssUrl: string;
FontSize: number;
FontScale: number;
CaptionLanguages: string[];
CaptionLanguage: string;
CaptionVttUrls: Record<string, string>;
VideoLanguages: string[];
VideoLanguage: string;
VideoUrls: Record<string, HidiveStreamList>;
FontColorName: string;
AutoPlayNextEpisode: boolean;
MaxStreams: number;
CurrentTime: number;
FontColorCode: string;
RunTime: number;
AdUrl: null;
}
export interface HidiveStreamList {
hls: string[];
drm: string[];
drmEnabled: boolean;
}
export interface HidiveStreamInfo extends HidiveStreamList {
language?: string;
episodeTitle?: string;
seriesTitle?: string;
season?: number;
episodeNumber?: number;
uncut?: boolean;
}
export interface HidiveSubtitleInfo {
language: string;
cc: boolean;
url: string;
}
export type DownloadedMedia = {
type: 'Video',
lang: LanguageItem,
path: string,
uncut: boolean
} | ({
type: 'Subtitle',
cc: boolean
} & sxItem )

4
@types/ws.d.ts vendored
View file

@ -29,8 +29,8 @@ export type MessageTypes = {
'isDownloading': [undefined, boolean],
'openFolder': [FolderTypes, undefined],
'changeProvider': [undefined, boolean],
'type': [undefined, 'funi'|'crunchy'|undefined],
'setup': ['funi'|'crunchy'|undefined, undefined],
'type': [undefined, 'funi'|'crunchy'|'hidive'|undefined],
'setup': ['funi'|'crunchy'|'hidive'|undefined, undefined],
'openFile': [[FolderTypes, string], undefined],
'openURL': [string, undefined],
'setuped': [undefined, boolean],

View file

@ -53,7 +53,7 @@ export default class Crunchy implements ServiceClass {
constructor(private debug = false) {
this.cfg = yamlCfg.loadCfg();
this.token = yamlCfg.loadCRToken();
this.req = new reqModule.Req(domain, false, false);
this.req = new reqModule.Req(domain, false, false, 'cr');
}
public checkToken(): boolean {

2
dev.js
View file

@ -14,7 +14,7 @@ const waitForProcess = async (proc) => {
(async () => {
await waitForProcess(exec('pnpm run tsc test false'));
for (let command of toRun) {
await waitForProcess(exec(`node index.js --service crunchy ${command}`, {
await waitForProcess(exec(`node index.js --service hidive ${command}`, {
cwd: path.join(__dirname, 'lib')
}));
}

View file

@ -1,11 +1,12 @@
# Anime Downloader NX by AniDL
[![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 *Funimation* and *Crunchyroll*.
This downloader can download anime from different sites. Currently supported are *Funimation*, *Crunchyroll*, and *Hidive*.
## Legal Warning
This application is not endorsed by or affiliated with *Funimation* or *Crunchyroll*. This application enables you to download videos for offline viewing which may be forbidden by law in your country. The usage of this application may also cause a violation of the *Terms of Service* between you and the stream provider. This tool is not responsible for your actions; please make an informed decision before using this application.
This application is not endorsed by or affiliated with *Funimation*, *Crunchyroll*, or *Hidive*. This application enables you to download videos for offline viewing which may be forbidden by law in your country. The usage of this application may also cause a violation of the *Terms of Service* between you and the stream provider. This tool is not responsible for your actions; please make an informed decision before using this application.
## Prerequisites
@ -27,6 +28,7 @@ To change these paths you need to edit `bin-path.yml` in `./config/` directory.
### Node Modules
After installing NodeJS with NPM go to directory with `package.json` file and type: `npm i`. Afterwards run `npm run tsc`. You can now find a lib folder containing the js code execute.
* [check dependencies](https://david-dm.org/anidl/funimation-downloader-nx)
## CLI Options

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,7 @@ import {Divider, Box, Button, Typography} from '@mui/material';
import useStore from '../hooks/useStore';
import { StoreState } from './Store';
type Services = 'funi'|'crunchy';
type Services = 'funi'|'crunchy'|'hidive';
export const serviceContext = React.createContext<Services|undefined>(undefined);
@ -24,6 +24,8 @@ const ServiceProvider: FCWithChildren = ({ children }) => {
<Button size='large' variant="contained" onClick={() => setService('funi')} >Funimation</Button>
<Divider orientation='vertical' flexItem />
<Button size='large' variant="contained" onClick={() => setService('crunchy')}>Crunchyroll</Button>
<Divider orientation='vertical' flexItem />
<Button size='large' variant="contained" onClick={() => setService('hidive')}>Hidive</Button>
</Box>
</Box>
: <serviceContext.Provider value={service}>

View file

@ -19,7 +19,7 @@ export type DownloadOptions = {
export type StoreState = {
episodeListing: Episode[];
downloadOptions: DownloadOptions,
service: 'crunchy'|'funi'|undefined,
service: 'crunchy'|'funi'|'hidive'|undefined,
}
export type StoreAction<T extends (keyof StoreState)> = {

View file

@ -6,6 +6,7 @@ import Funi from '../../funi';
import { setSetuped, writeYamlCfgFile } from '../../modules/module.cfg-loader';
import CrunchyHandler from './services/crunchyroll';
import FunimationHandler from './services/funimation';
import HidiveHandler from './services/hidive';
import WebSocketHandler from './websocket';
export default class ServiceHandler {
@ -31,6 +32,8 @@ export default class ServiceHandler {
this.service = new FunimationHandler(this.ws);
} else if (data === 'crunchy') {
this.service = new CrunchyHandler(this.ws);
} else if (data === 'hidive') {
this.service = new HidiveHandler(this.ws);
}
});

View file

@ -0,0 +1,126 @@
import { AuthData, CheckTokenResponse, DownloadData, EpisodeListResponse, MessageHandler, ResolveItemsData, SearchData, SearchResponse } from '../../../@types/messageHandler';
import Hidive from '../../../hidive';
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 HidiveHandler extends Base implements MessageHandler {
private hidive: Hidive;
constructor(ws: WebSocketHandler) {
super(ws);
this.hidive = new Hidive();
}
public auth(data: AuthData) {
return this.hidive.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 hidiveSearch = await this.hidive.doSearch(data);
if (!hidiveSearch.isOk) {
return hidiveSearch;
}
return { isOk: true, value: hidiveSearch.value };
}
public async handleDefault(name: string) {
return getDefault(name, this.hidive.cfg.cli);
}
public async availableDubCodes(): Promise<string[]> {
const dubLanguageCodesArray: string[] = [];
for(const language of languages){
if (language.hd_locale)
dubLanguageCodesArray.push(language.code);
}
return [...new Set(dubLanguageCodesArray)];
}
public async availableSubCodes(): Promise<string[]> {
const subLanguageCodesArray: string[] = [];
for(const language of languages){
if (language.hd_locale)
subLanguageCodesArray.push(language.locale);
}
return [...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.hidive.getShow(parseInt(data.id), data.e, data.but, data.all);
if (!res.isOk || !res.value)
return res.isOk;
this.addToQueue(res.value.map(item => {
return {
...data,
ids: [item.Id],
title: item.Name,
parent: {
title: item.seriesTitle,
season: parseFloat(item.SeasonNumberValue+'')+''
},
image: item.ScreenShotSmallUrl,
e: parseFloat(item.EpisodeNumberValue+'')+'',
episode: parseFloat(item.EpisodeNumberValue+'')+'',
};
}));
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.hidive.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.Episodes.map(function(item) {
const language = item.Summary.match(/^Audio: (.*)/m);
language?.shift();
const description = item.Summary.split('\r\n');
return {
e: parseFloat(item.EpisodeNumberValue+'')+'',
lang: language ? language[0].split(', ') : [],
name: item.Name,
season: parseFloat(item.SeasonNumberValue+'')+'',
seasonTitle: request.value.Name,
episode: parseFloat(item.EpisodeNumberValue+'')+'',
id: item.Id+'',
img: item.ScreenShotSmallUrl,
description: description ? description[0] : '',
time: ''
};
})};
}
public async downloadItem(data: DownloadData) {
this.setDownloading(true);
console.debug(`Got download options: ${JSON.stringify(data)}`);
const _default = yargs.appArgv(this.hidive.cfg.cli, true);
const res = await this.hidive.getShow(parseInt(data.id), data.e, false, false);
if (!res.isOk || !res.showData)
return this.alertError(new Error('Download failed upstream, check for additional logs'));
for (const ep of res.value) {
await this.hidive.getEpisode(ep, {..._default, callbackMaker: this.makeProgressHandler.bind(this), dubLang: data.dubLang, dlsubs: data.dlsubs, fileName: data.fileName, q: data.q, force: 'y', noaudio: data.noaudio, novids: data.novids});
}
this.sendMessage({ name: 'finish', data: undefined });
this.setDownloading(false);
this.onFinish();
}
}
export default HidiveHandler;

792
hidive.ts Normal file
View file

@ -0,0 +1,792 @@
// build-in
import path from 'path';
import fs from 'fs-extra';
import crypto from 'crypto';
// package program
import packageJson from './package.json';
// plugins
import { console } from './modules/log';
import shlp from 'sei-helper';
import m3u8 from 'm3u8-parsed';
import streamdl from './modules/hls-download';
// custom 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 Merger, { Font, MergerInput, SubtitleInput } from './modules/module.merger';
import { vtt } from './modules/module.vtt2ass';
// load req
import { domain, api } from './modules/module.api-urls';
import * as reqModule from './modules/module.req';
import { HidiveEpisodeList, HidiveEpisodeExtra } from './@types/hidiveEpisodeList';
import { HidiveVideoList, HidiveStreamInfo, DownloadedMedia, HidiveSubtitleInfo } from './@types/hidiveTypes';
import parseFileName, { Variable } from './modules/module.filename';
import { downloaded } from './modules/module.downloadArchive';
import parseSelect from './modules/module.parseSelect';
import { AvailableFilenameVars } from './modules/module.args';
import { AuthData, AuthResponse, SearchData, SearchResponse, SearchResponseItem } from './@types/messageHandler';
import { ServiceClass } from './@types/serviceClassInterface';
import { sxItem } from './crunchy';
import { HidiveSearch } from './@types/hidiveSearch';
export default class Hidive implements ServiceClass {
public cfg: yamlCfg.ConfigObject;
private session: Record<string, any>;
private token: Record<string, any>;
private req: reqModule.Req;
private client: {
// base
ipAddress: string,
xNonce: string,
xSignature: string,
// personal
visitId: string,
// profile data
profile: {
userId: number,
profileId: number,
deviceId : string,
}
};
constructor(private debug = false) {
this.cfg = yamlCfg.loadCfg();
this.session = yamlCfg.loadHDSession();
this.token = yamlCfg.loadHDToken();
this.client = yamlCfg.loadHDProfile() as {ipAddress: string, xNonce: string, xSignature: string, visitId: string, profile: {userId: number, profileId: number, deviceId : string}};
this.req = new reqModule.Req(domain, false, false, 'hd');
}
public async doInit() {
//get client ip
const newIp = await this.reqData('Ping', '');
if (!newIp.ok || !newIp.res) return false;
this.client.ipAddress = JSON.parse(newIp.res.body).IPAddress;
//get device id
const newDevice = await this.reqData('InitDevice', { 'DeviceName': api.hd_devName });
if (!newDevice.ok || !newDevice.res) return false;
this.client.profile = Object.assign(this.client.profile, {
deviceId: JSON.parse(newDevice.res.body).Data.DeviceId,
});
//get visit id
const newVisitId = await this.reqData('InitVisit', {});
if (!newVisitId.ok || !newVisitId.res) return false;
this.client.visitId = JSON.parse(newVisitId.res.body).Data.VisitId;
//save client
yamlCfg.saveHDProfile(this.client);
return true;
}
public async cli() {
console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`);
const argv = yargs.appArgv(this.cfg.cli);
//below is for quickly testing API calls
/*const searchItems = await this.reqData('Search', {'Query':''});
if(!searchItems.ok || !searchItems.res){return;}
console.info(searchItems.res.body);*/
// load binaries
this.cfg.bin = await yamlCfg.loadBinCfg();
if (argv.allDubs) {
argv.dubLang = langsData.dubLanguageCodes;
}
if (argv.auth) {
//Initilize session
await this.doInit();
//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){
//Initilize session
await this.doInit();
//Search
await this.doSearch({ ...argv, search: argv.search as string });
} else if (argv.s && !isNaN(parseInt(argv.s,10)) && parseInt(argv.s,10) > 0) {
//Initilize session
await this.doInit();
//get selected episodes
const selected = await this.getShow(parseInt(argv.s), argv.e, argv.but, argv.all);
if (selected.isOk && selected.showData) {
for (const select of selected.value) {
//download episode
if (!(await this.getEpisode(select, {...argv}))) {
console.error(`Unable to download selected episode ${parseFloat(select.EpisodeNumberValue+'')}`);
return false;
}
}
}
return true;
} else {
console.info('No option selected or invalid value entered. Try --help.');
}
}
// Generate Nonce
public generateNonce(){
const initDate = new Date();
const nonceDate = [
initDate.getUTCFullYear().toString().slice(-2), // yy
('0'+(initDate.getUTCMonth()+1)).slice(-2), // MM
('0'+initDate.getUTCDate()).slice(-2), // dd
('0'+initDate.getUTCHours()).slice(-2), // HH
('0'+initDate.getUTCMinutes()).slice(-2) // mm
].join(''); // => "yyMMddHHmm" (UTC)
const nonceCleanStr = nonceDate + api.hd_apikey;
const nonceHash = crypto.createHash('sha256').update(nonceCleanStr).digest('hex');
return nonceHash;
}
// Generate Signature
public generateSignature(body: string|object, visitId: string, profile: Record<string, any>) {
const sigCleanStr = [
this.client.ipAddress,
api.hd_appId,
profile.deviceId,
visitId,
profile.userId,
profile.profileId,
body,
this.client.xNonce,
api.hd_apikey,
].join('');
return crypto.createHash('sha256').update(sigCleanStr).digest('hex');
}
public makeCookieList(data: Record<string, any>, keys: Array<string>) {
const res = [];
for (const key of keys) {
if (typeof data[key] !== 'object') continue;
res.push(`${key}=${data[key].value}`);
}
return res.join('; ');
}
public async reqData(method: string, body: string | object, type = 'POST') {
const options = {
headers: {} as Record<string, unknown>,
method: type as 'GET'|'POST',
url: '' as string,
body: body,
};
// get request type
const isGet = type == 'GET' ? true : false;
// set request type, url, user agent, referrer, and origin
options.method = isGet ? 'GET' : 'POST';
options.url = ( !isGet ? domain.hd_api + '/api/v1/' : '') + method;
options.headers['user-agent'] = isGet ? api.hd_clientExo : api.hd_clientWeb;
options.headers['referrer'] = 'https://www.hidive.com/';
options.headers['origin'] = 'https://www.hidive.com';
// set api data
if(!isGet){
options.body = body == '' ? body : JSON.stringify(body);
// set api headers
if(method != 'Ping'){
const visitId = this.client.visitId ? this.client.visitId : '';
const vprofile = {
userId: this.client.profile.userId || 0,
profileId: this.client.profile.profileId || 0,
deviceId: this.client.profile.deviceId || '',
};
this.client.xNonce = this.generateNonce();
this.client.xSignature = this.generateSignature(options.body, visitId, vprofile);
options.headers = Object.assign(options.headers, {
'X-VisitId' : visitId,
'X-UserId' : vprofile.userId,
'X-ProfileId' : vprofile.profileId,
'X-DeviceId' : vprofile.deviceId,
'X-Nonce' : this.client.xNonce,
'X-Signature' : this.client.xSignature,
});
}
options.headers = Object.assign({
'Content-Type' : 'application/x-www-form-urlencoded; charset=UTF-8',
'X-ApplicationId': api.hd_appId,
}, options.headers);
// cookies
const cookiesList = Object.keys(this.session);
if(cookiesList.length > 0 && method != 'Ping') {
options.headers.Cookie = this.makeCookieList(this.session, cookiesList);
}
} else if(isGet && !options.url.match(/\?/)){
this.client.xNonce = this.generateNonce();
this.client.xSignature = this.generateSignature(options.body, this.client.visitId, this.client.profile);
options.url = options.url + '?' + (new URLSearchParams({
'X-ApplicationId': api.hd_appId,
'X-DeviceId': this.client.profile.deviceId,
'X-VisitId': this.client.visitId,
'X-UserId': this.client.profile.userId+'',
'X-ProfileId': this.client.profile.profileId+'',
'X-Nonce': this.client.xNonce,
'X-Signature': this.client.xSignature,
})).toString();
}
try {
if (this.debug) {
console.debug('[DEBUG] Request params:');
console.debug(options);
}
const apiReqOpts: reqModule.Params = {
method: options.method,
headers: options.headers as Record<string, string>,
body: options.body as string
};
const apiReq = await this.req.getData(options.url, apiReqOpts);
if(!apiReq.ok || !apiReq.res){
console.error('API Request Failed!');
return {
ok: false,
res: apiReq.res,
};
}
if (!isGet && apiReq.res.headers && apiReq.res.headers['set-cookie']) {
const newReqCookies = shlp.cookie.parse(apiReq.res.headers['set-cookie'] as unknown as Record<string, string>);
this.session = Object.assign(this.session, newReqCookies);
yamlCfg.saveHDSession(this.session);
}
if (!isGet) {
const resJ = JSON.parse(apiReq.res.body);
if (resJ.Code > 0) {
console.error(`[ERROR] Code ${resJ.Code} (${resJ.Status}): ${resJ.Message}\n`);
if (resJ.Code == 81 || resJ.Code == 5) {
console.info('[NOTE] App was broken because of changes in official app.');
console.info('[NOTE] See: https://github.com/anidl/hidive-downloader-nx/issues/1\n');
}
if (resJ.Code == 55) {
console.info('[NOTE] You need premium account to view this video.');
}
return {
ok: false,
res: apiReq.res,
};
}
}
return {
ok: true,
res: apiReq.res,
};
} catch (error: any) {
if (error.statusCode && error.statusMessage) {
console.error(`\n ${error.name} ${error.statusCode}: ${error.statusMessage}\n`);
} else {
console.error(`\n ${error.name}: ${error.code}\n`);
}
return {
ok: false,
error,
};
}
}
public async doAuth(data: AuthData): Promise<AuthResponse> {
const auth = await this.reqData('Authenticate', {'Email':data.username,'Password':data.password});
if(!auth.ok || !auth.res) {
console.error('Authentication failed!');
return { isOk: false, reason: new Error('Authentication failed') };
}
const authData = JSON.parse(auth.res.body).Data;
this.client.profile = Object.assign(this.client.profile, {
userId: authData.User.Id,
profileId: authData.Profiles[0].Id,
});
yamlCfg.saveHDProfile(this.client);
yamlCfg.saveHDToken(authData);
console.info('[INFO] Auth complete!');
console.info(`[INFO] Service level for "${data.username}" is ${authData.User.ServiceLevel}`);
return { isOk: true, value: undefined };
}
public async genSubsUrl(type: string, file: string) {
return [
`${domain.hd_www}/caption/${type}/`,
( type == 'css' ? '?id=' : '' ),
`${file}.${type}`
].join('');
}
public async doSearch(data: SearchData): Promise<SearchResponse> {
const searchReq = await this.reqData('Search', {'Query':data.search});
if(!searchReq.ok || !searchReq.res){
console.error('Search FAILED!');
return { isOk: false, reason: new Error('Search failed. No more information provided') };
}
const searchData = JSON.parse(searchReq.res.body) as HidiveSearch;
const searchItems = searchData.Data.TitleResults;
if(searchItems.length>0) {
console.info('[INFO] Search Results:');
for(let i=0;i<searchItems.length;i++){
console.info(`[#${searchItems[i].Id}] ${searchItems[i].Name} [${searchItems[i].ShowInfoTitle}]`);
}
} else{
console.warn('Nothing found!');
}
return { isOk: true, value: searchItems.map((a): SearchResponseItem => {
return {
id: a.Id+'',
image: a.KeyArtUrl ?? '/notFound.png',
name: a.Name,
rating: a.OverallRating,
desc: a.LongSynopsis
};
})};
}
public async listShow(id: number) {
const getShowData = await this.reqData('GetTitle', { 'Id': id });
if (!getShowData.ok || !getShowData.res) {
console.error('Failed to get show data');
return { isOk: false};
}
const rawShowData = JSON.parse(getShowData.res.body) as HidiveEpisodeList;
const showData = rawShowData.Data.Title;
console.info(`[#${showData.Id}] ${showData.Name} [${showData.ShowInfoTitle}]`);
return { isOk: true, value: showData };
}
async getShow(id: number, e: string | undefined, but: boolean, all: boolean) {
const getShowData = await this.listShow(id);
if (!getShowData.isOk || !getShowData.value) {
return { isOk: false, value: [] };
}
const showData = getShowData.value;
const doEpsFilter = parseSelect(e as string);
// build selected episodes
const selEpsArr: HidiveEpisodeExtra[] = []; let ovaSeq = 1; let movieSeq = 1;
for (let i = 0; i < showData.Episodes.length; i++) {
const titleId = showData.Episodes[i].TitleId;
const epKey = showData.Episodes[i].VideoKey;
const seriesTitle = showData.Name;
let nameLong = showData.Episodes[i].DisplayNameLong;
if (nameLong.match(/OVA/i)) {
nameLong = 'ova' + (('0' + ovaSeq).slice(-2)); ovaSeq++;
}
else if (nameLong.match(/Theatrical/i)) {
nameLong = 'movie' + (('0' + movieSeq).slice(-2)); movieSeq++;
}
else {
nameLong = epKey;
}
let sumDub: string | RegExpMatchArray | null = showData.Episodes[i].Summary.match(/^Audio: (.*)/m);
sumDub = sumDub ? `\n - ${sumDub[0]}` : '';
let sumSub: string | RegExpMatchArray | null = showData.Episodes[i].Summary.match(/^Subtitles: (.*)/m);
sumSub = sumSub ? `\n - ${sumSub[0]}` : '';
let selMark = '';
if (all ||
but && !doEpsFilter.isSelected([parseFloat(showData.Episodes[i].EpisodeNumberValue+'')+'', showData.Episodes[i].Id+'']) ||
!but && doEpsFilter.isSelected([parseFloat(showData.Episodes[i].EpisodeNumberValue+'')+'', showData.Episodes[i].Id+''])
) {
selEpsArr.push({ isSelected: true, titleId, epKey, nameLong, seriesTitle, ...showData.Episodes[i] });
selMark = '✓ ';
}
//const epKeyTitle = !epKey.match(/e(\d+)$/) ? nameLong : epKey;
//const titleIdStr = (titleId != id ? `#${titleId}|` : '') + epKeyTitle;
//console.info(`[${titleIdStr}] ${showData.Episodes[i].Name}${selMark}${sumDub}${sumSub}`);
console.info('%s[%s] %s%s%s',
selMark,
'S'+parseFloat(showData.Episodes[i].SeasonNumberValue+'')+'E'+parseFloat(showData.Episodes[i].EpisodeNumberValue+''),
showData.Episodes[i].Name,
sumDub,
sumSub
);
}
return { isOk: true, value: selEpsArr, showData: showData } ;
}
public async getEpisode(selectedEpisode: HidiveEpisodeExtra, options: Record<any, any>) {
const getVideoData = await this.reqData('GetVideos', { 'VideoKey': selectedEpisode.epKey, 'TitleId': selectedEpisode.titleId });
if (getVideoData.ok && getVideoData.res) {
const videoData = JSON.parse(getVideoData.res.body) as HidiveVideoList;
const showTitle = `${selectedEpisode.seriesTitle} S${parseFloat(selectedEpisode.SeasonNumberValue+'')}}`;
console.info(`[INFO] ${showTitle} - ${parseFloat(selectedEpisode.EpisodeNumberValue+'')}`);
const videoList = videoData.Data.VideoLanguages;
const subsList = videoData.Data.CaptionLanguages;
console.info('[INFO] Available dubs and subtitles:');
console.info('\tVideos: ' + videoList.join('\n\t\t'));
console.info('\tSubs : ' + subsList.join('\n\t\t'));
console.info(`[INFO] Selected dub(s): ${options.dubLang.join(', ')}`);
const videoUrls = videoData.Data.VideoUrls;
const subsUrls = videoData.Data.CaptionVttUrls;
const fontSize = videoData.Data.FontSize ? videoData.Data.FontSize : 34;
const subsSel = subsList;
//Get Selected Video URLs
const videoSel = videoList.sort().filter(videoLanguage =>
langsData.languages.find(a =>
a.hd_locale ? videoLanguage.match(a.hd_locale) &&
options.dubLang.includes(a.code) : false
)
);
//Prioritize Home Video, unless simul is used
videoSel.forEach(function(video, index) {
if (index > 0) {
const video1 = video.split(', ');
const video2 = videoSel[index - 1].split(', ');
if (video1[0] == video2[0]) {
if (video1[1] == 'Home Video' && video2[1] == 'Broadcast') {
options.simul ? videoSel.splice(index, 1) : videoSel.splice(index - 1, 1);
}
}
}
});
if (videoSel.length === 0) {
console.error('No suitable videos(s) found for options!');
}
//Build video array
const selectedVideoUrls: HidiveStreamInfo[] = [];
videoSel.forEach(function(video, index) {
const videodetails = videoSel[index].split(', ');
const videoinfo: HidiveStreamInfo = videoUrls[video];
videoinfo.language = videodetails[0];
videoinfo.episodeTitle = selectedEpisode.Name;
videoinfo.seriesTitle = selectedEpisode.seriesTitle;
videoinfo.season = parseFloat(selectedEpisode.SeasonNumberValue+'');
videoinfo.episodeNumber = parseFloat(selectedEpisode.EpisodeNumberValue+'');
videoinfo.uncut = videodetails[0] == 'Home Video' ? true : false;
console.info(`[INFO] Selected release: ${videodetails[0]} ${videodetails[1]}`);
selectedVideoUrls.push(videoinfo);
});
//Build subtitle array
const selectedSubUrls: HidiveSubtitleInfo[] = [];
subsSel.forEach(function(sub, index) {
console.info(subsSel[index]);
const subinfo = {
url: subsUrls[sub],
cc: subsSel[index].includes('Caps'),
language: subsSel[index].replace(' Subs', '').replace(' Caps', '')
};
selectedSubUrls.push(subinfo);
});
//download media list
const res = await this.downloadMediaList(selectedVideoUrls, selectedSubUrls, fontSize, options);
if (res === undefined || res.error) {
console.error('Failed to download media list');
return { isOk: false, reason: new Error('Failed to download media list') };
} else {
if (!options.skipmux) {
await this.muxStreams(res.data, { ...options, output: res.fileName });
} else {
console.info('Skipping mux');
}
downloaded({
service: 'hidive',
type: 's'
}, selectedEpisode.titleId+'', [selectedEpisode.EpisodeNumberValue+'']);
return { isOk: res, value: undefined };
}
}
return { isOk: false, reason: new Error('Unknown download error') };
}
public async downloadMediaList(videoUrls: HidiveStreamInfo[], subUrls: HidiveSubtitleInfo[], fontSize: number, options: Record<any, any>) {
let mediaName = '...';
let fileName;
const files: DownloadedMedia[] = [];
let dlFailed = false;
//let dlVideoOnce = false; // Variable to save if best selected video quality was downloaded
let subsMargin = 0;
const variables: Variable[] = [];
for (const videoData of videoUrls) {
if(videoData.seriesTitle && videoData.episodeNumber && videoData.episodeTitle){
mediaName = `${videoData.seriesTitle} - ${videoData.episodeNumber} - ${videoData.episodeTitle}`;
}
if(!options.novids && !dlFailed) {
console.info(`Requesting: ${mediaName}`);
console.info('Playlists URL: %s', videoData.hls[0]);
const streamPlaylistsReq = await this.req.getData(videoData.hls[0]);
if(!streamPlaylistsReq.ok || !streamPlaylistsReq.res){
console.error('CAN\'T FETCH VIDEO PLAYLISTS!');
return { error: true, data: []};
}
variables.push(...([
['title', videoData.episodeTitle, true],
['episode', isNaN(parseFloat(videoData.episodeNumber+'')) ? videoData.episodeNumber : parseFloat(videoData.episodeNumber+''), false],
['service', 'HD', false],
['showTitle', videoData.seriesTitle, true],
['season', videoData.season, false]
] as [AvailableFilenameVars, string|number, boolean][]).map((a): Variable => {
return {
name: a[0],
replaceWith: a[1],
type: typeof a[1],
sanitize: a[2]
} as Variable;
}));
const streamPlaylists = m3u8(streamPlaylistsReq.res.body);
const plServerList: string[] = [],
plStreams: Record<string, Record<string, string>> = {},
plQuality: {
str: string,
dim: string,
CODECS: string,
RESOLUTION: {
width: number,
height: number
}
}[] = [];
for (const pl of streamPlaylists.playlists) {
// set quality
const plResolution = pl.attributes.RESOLUTION;
const plResolutionText = `${plResolution.width}x${plResolution.height}`;
// set codecs
const plCodecs = pl.attributes.CODECS;
// parse uri
const plUri = new URL(pl.uri);
let plServer = plUri.hostname;
// set server list
if (plUri.searchParams.get('cdn')) {
plServer += ` (${plUri.searchParams.get('cdn')})`;
}
if (!plServerList.includes(plServer)) {
plServerList.push(plServer);
}
// add to server
if (!Object.keys(plStreams).includes(plServer)) {
plStreams[plServer] = {};
}
if (
plStreams[plServer][plResolutionText]
&& plStreams[plServer][plResolutionText] != pl.uri
&& typeof plStreams[plServer][plResolutionText] != 'undefined'
) {
console.error(`Non duplicate url for ${plServer} detected, please report to developer!`);
}
else {
plStreams[plServer][plResolutionText] = pl.uri;
}
// set plQualityStr
const plBandwidth = Math.round(pl.attributes.BANDWIDTH / 1024);
const qualityStrAdd = `${plResolutionText} (${plBandwidth}KiB/s)`;
const qualityStrRegx = new RegExp(qualityStrAdd.replace(/(:|\(|\)|\/)/g, '\\$1'), 'm');
const qualityStrMatch = !plQuality.map(a => a.str).join('\r\n').match(qualityStrRegx);
if (qualityStrMatch) {
plQuality.push({
str: qualityStrAdd,
dim: plResolutionText,
CODECS: plCodecs,
RESOLUTION: plResolution
});
}
}
options.x = options.x > plServerList.length ? 1 : options.x;
const plSelectedServer = plServerList[options.x - 1];
const plSelectedList = plStreams[plSelectedServer];
plQuality.sort((a, b) => {
const aMatch: RegExpMatchArray | never[] = a.dim.match(/[0-9]+/) || [];
const bMatch: RegExpMatchArray | never[] = b.dim.match(/[0-9]+/) || [];
return parseInt(aMatch[0]) - parseInt(bMatch[0]);
});
let quality = options.q === 0 ? plQuality.length : options.q;
if(quality > plQuality.length) {
console.warn(`The requested quality of ${options.q} is greater than the maximun ${plQuality.length}.\n[WARN] Therefor the maximum will be capped at ${plQuality.length}.`);
quality = plQuality.length;
}
const selPlUrl = plSelectedList[plQuality.map(a => a.dim)[quality - 1]] ? plSelectedList[plQuality.map(a => a.dim)[quality - 1]] : '';
console.info(`Servers available:\n\t${plServerList.join('\n\t')}`);
console.info(`Available qualities:\n\t${plQuality.map((a, ind) => `[${ind+1}] ${a.str}`).join('\n\t')}`);
if(selPlUrl != '') {
variables.push({
name: 'height',
type: 'number',
replaceWith: quality === 0 ? plQuality[plQuality.length - 1].RESOLUTION.height as number : plQuality[quality - 1].RESOLUTION.height
}, {
name: 'width',
type: 'number',
replaceWith: quality === 0 ? plQuality[plQuality.length - 1].RESOLUTION.width as number : plQuality[quality - 1].RESOLUTION.width
});
}
const lang = langsData.languages.find(a => a.hd_locale === videoData.language);
if (!lang) {
console.error(`Unable to find language for code ${videoData.language}`);
return { error: true, data: [] };
}
console.info(`Selected quality: ${Object.keys(plSelectedList).find(a => plSelectedList[a] === selPlUrl)} @ ${plSelectedServer}`);
console.info('Stream URL:', selPlUrl);
// TODO check filename
const outFile = parseFileName(options.fileName + '.' + lang.name, variables, options.numbers, options.override).join(path.sep);
fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep);
console.info(`Output filename: ${outFile}`);
const chunkPage = await this.req.getData(selPlUrl);
if(!chunkPage.ok || !chunkPage.res){
console.error('CAN\'T FETCH VIDEO PLAYLIST!');
dlFailed = true;
} else {
const chunkPlaylist = m3u8(chunkPage.res.body);
//TODO: look into how to keep bumpers without the video being affected
if(chunkPlaylist.segments[0].uri.match(/\/bumpers\//) && options.removeBumpers){
subsMargin = chunkPlaylist.segments[0].duration;
chunkPlaylist.segments.splice(0, 1);
}
const totalParts = chunkPlaylist.segments.length;
const mathParts = Math.ceil(totalParts / options.partsize);
const mathMsg = `(${mathParts}*${options.partsize})`;
console.info('Total parts in stream:', totalParts, mathMsg);
const tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile);
const split = outFile.split(path.sep).slice(0, -1);
split.forEach((val, ind, arr) => {
const isAbsolut = path.isAbsolute(outFile as string);
if (!fs.existsSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val)))
fs.mkdirSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val));
});
const dlStreamByPl = await new streamdl({
output: `${tsFile}.ts`,
timeout: options.timeout,
m3u8json: chunkPlaylist,
// baseurl: chunkPlaylist.baseUrl,
threads: options.partsize,
fsRetryTime: options.fsRetryTime * 1000,
override: options.force,
callback: options.callbackMaker ? options.callbackMaker({
fileName: `${path.isAbsolute(outFile) ? outFile.slice(this.cfg.dir.content.length) : outFile}`,
image: '',
parent: {
title: videoData.seriesTitle
},
title: videoData.episodeTitle,
language: lang
}) : undefined
}).download();
if (!dlStreamByPl.ok) {
console.error(`DL Stats: ${JSON.stringify(dlStreamByPl.parts)}\n`);
dlFailed = true;
}
files.push({
type: 'Video',
path: `${tsFile}.ts`,
lang: lang,
uncut: videoData.uncut
});
//dlVideoOnce = true;
}
} else if(options.novids){
fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep);
console.info('Downloading skipped!');
}
}
if(options.dlsubs.indexOf('all') > -1){
options.dlsubs = ['all'];
}
if(!options.skipsubs && options.dlsubs.indexOf('none') == -1) {
if(subUrls.length > 0) {
let subIndex = 0;
for(const sub of subUrls) {
const subLang = langsData.languages.find(a => a.hd_locale === sub.language);
if (!subLang) {
console.warn(`Language not found for subtitle language: ${sub.language}, Skipping`);
continue;
}
const sxData: Partial<sxItem> = {};
sxData.file = langsData.subsFile(fileName as string, subIndex+'', subLang, sub.cc, options.ccTag);
sxData.path = path.join(this.cfg.dir.content, sxData.file);
sxData.language = subLang;
if(options.dlsubs.includes('all') || options.dlsubs.includes(subLang.locale)){
const subs4XUrl = sub.url.split('/');
const subsXUrl = subs4XUrl[subs4XUrl.length - 1].replace(/.vtt$/, '');
const getCssContent = await this.req.getData(await this.genSubsUrl('css', subsXUrl));
const getVttContent = await this.req.getData(await this.genSubsUrl('vtt', subsXUrl));
if (getCssContent.ok && getVttContent.ok && getCssContent.res && getVttContent.res) {
//vttConvert(getVttContent.res.body, false, subLang.name, fontSize);
const sBody = vtt(undefined, fontSize, getVttContent.res.body, getCssContent.res.body, subsMargin);
sxData.title = `${subLang.language} / ${sxData.title}`;
sxData.fonts = fontsData.assFonts(sBody) as Font[];
fs.writeFileSync(sxData.path, sBody);
console.info(`Subtitle downloaded: ${sxData.file}`);
files.push({
type: 'Subtitle',
...sxData as sxItem,
cc: sub.cc
});
} else{
console.warn(`Failed to download subtitle: ${sxData.file}`);
}
}
subIndex++;
}
} else{
console.warn('Can\'t find urls for subtitles!');
}
} else{
console.info('Subtitles downloading skipped!');
}
return {
error: dlFailed,
data: files,
fileName: fileName ? (path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName)) || './unknown' : './unknown'
};
}
public async muxStreams(data: DownloadedMedia[], options: Record<any, any>) {
this.cfg.bin = await yamlCfg.loadBinCfg();
if (options.novids || data.filter(a => a.type === 'Video').length === 0)
return console.info('Skip muxing since no vids are downloaded');
const merger = new Merger({
onlyVid: [],
skipSubMux: options.skipSubMux,
inverseTrackOrder: true,
onlyAudio: [],
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');
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: 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 (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();
}
}

View file

@ -36,6 +36,15 @@ import update from './modules/module.updater';
type: argv.s === undefined ? 'srz' : '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.service === 'hidive') {
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);
@ -43,14 +52,14 @@ 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('funi.js'))
if (key.endsWith('crunchy.js') || key.endsWith('funi.js') || key.endsWith('hidive.js'))
delete require.cache[key];
});
const service = new (argv.service === 'funi' ? (await import('./funi')).default : (await import('./crunchy')).default)() as ServiceClass;
const service = new (argv.service === 'funi' ? (await import('./funi')).default : argv.service === 'hidive' ? (await import('./hidive')).default : (await import('./crunchy')).default)() as ServiceClass;
await service.cli();
}
} else {
const service = argv.service === 'funi' ? new (await import('./funi')).default() : new (await import('./crunchy')).default() as ServiceClass;
const service = new (argv.service === 'funi' ? (await import('./funi')).default : argv.service === 'hidive' ? (await import('./hidive')).default : (await import('./crunchy')).default)() as ServiceClass;
await service.cli();
}
})();

View file

@ -3,15 +3,8 @@ import fs from 'fs';
import path from 'path';
import { args, groups } from './module.args';
const transformService = (str: 'funi'|'crunchy'|'both') => {
switch (str) {
case 'both':
return 'Both';
case 'crunchy':
return 'Crunchyroll';
case 'funi':
return 'Funimation';
}
const transformService = (str: Array<'funi'|'crunchy'|'hidive'|'all'>) => {
return str.join(', ');
};
let docs = `# ${packageJSON.name} (${packageJSON.version}v)

View file

@ -417,6 +417,11 @@ const extFn = {
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';
}
// console.log(' - Req:', options.url.pathname);
}
],

View file

@ -6,6 +6,8 @@ const domain = {
api: 'https://api.crunchyroll.com',
www_beta: 'https://beta.crunchyroll.com',
api_beta: 'https://beta-api.crunchyroll.com',
hd_www: 'https://www.hidive.com',
hd_api: 'https://api.hidive.com'
};
export type APIType = {
@ -32,7 +34,13 @@ export type APIType = {
beta_browse: string
beta_cms: string,
beta_authHeader: Headers,
beta_authHeaderMob: Headers
beta_authHeaderMob: Headers,
hd_apikey: string,
hd_devName: string,
hd_appId: string,
hd_clientWeb: string,
hd_clientExo: string,
hd_api: string,
}
// api urls
@ -61,7 +69,14 @@ const api: APIType = {
beta_browse: `${domain.api_beta}/content/v1/browse`,
beta_cms: `${domain.api_beta}/cms/v2`,
beta_authHeader: {},
beta_authHeaderMob: {}
beta_authHeaderMob: {},
//hidive API
hd_apikey: '508efd7b42d546e19cc24f4d0b414e57e351ca73',
hd_devName: 'Android',
hd_appId: '24i-Android',
hd_clientWeb: 'okhttp/3.4.1',
hd_clientExo: 'smartexoplayer/1.6.0.R (Linux;Android 6.0) ExoPlayerLib/2.6.0',
hd_api: `${domain.hd_api}/api/v1`,
};
// set header

View file

@ -2,7 +2,68 @@ import yargs, { Choices } from 'yargs';
import { args, AvailableMuxer, groups } from './module.args';
import { LanguageItem } from './module.langsData';
let argvC: { [x: string]: unknown; ccTag: string, defaultAudio: LanguageItem, defaultSub: LanguageItem, ffmpegOptions: string[], mkvmergeOptions: string[], force: 'Y'|'y'|'N'|'n'|'C'|'c', skipUpdate: boolean, videoTitle: string, override: string[], fsRetryTime: number, forceMuxer: AvailableMuxer|undefined; username: string|undefined, password: string|undefined, silentAuth: boolean, skipSubMux: boolean, downloadArchive: boolean, addArchive: boolean, but: boolean, auth: boolean | undefined; dlFonts: boolean | undefined; search: string | undefined; 'search-type': string; page: number | undefined; 'search-locale': string; new: boolean | undefined; 'movie-listing': string | undefined; series: string | undefined; s: string | undefined; e: string | undefined; q: number; x: number; kstream: number; partsize: number; hslang: string; dlsubs: string[]; novids: boolean | undefined; noaudio: boolean | undefined; nosubs: boolean | undefined; dubLang: string[]; all: boolean; fontSize: number; allDubs: boolean; timeout: number; simul: boolean; mp4: boolean; skipmux: boolean | undefined; fileName: string; numbers: number; nosess: string; debug: boolean | undefined; nocleanup: boolean; help: boolean | undefined; service: 'funi' | 'crunchy'; update: boolean; fontName: string | undefined; _: (string | number)[]; $0: string; dlVideoOnce: boolean; };
let argvC: {
[x: string]: unknown;
ccTag: string,
defaultAudio: LanguageItem,
defaultSub: LanguageItem,
ffmpegOptions: string[],
mkvmergeOptions: string[],
force: 'Y'|'y'|'N'|'n'|'C'|'c',
skipUpdate: boolean,
videoTitle: string,
override: string[],
fsRetryTime: number,
forceMuxer: AvailableMuxer|undefined;
username: string|undefined,
password: string|undefined,
silentAuth: boolean,
skipSubMux: boolean,
downloadArchive: boolean,
addArchive: boolean,
but: boolean,
auth: boolean | undefined;
dlFonts: boolean | undefined;
search: string | undefined;
'search-type': string;
page: number | undefined;
'search-locale': string;
new: boolean | undefined;
'movie-listing': string | undefined;
series: string | undefined;
s: string | undefined;
e: string | undefined;
q: number;
x: number;
kstream: number;
partsize: number;
hslang: string;
dlsubs: string[];
novids: boolean | undefined;
noaudio: boolean | undefined;
nosubs: boolean | undefined;
dubLang: string[];
all: boolean;
fontSize: number;
allDubs: boolean;
timeout: number;
simul: boolean;
mp4: boolean;
skipmux: boolean | undefined;
fileName: string;
numbers: number;
nosess: string;
debug: boolean | undefined;
nocleanup: boolean;
help: boolean | undefined;
service: 'funi' | 'crunchy' | 'hidive';
update: boolean;
fontName: string | undefined;
_: (string | number)[];
$0: string;
dlVideoOnce: boolean;
removeBumpers: boolean;
};
export type ArgvType = typeof argvC;

View file

@ -40,7 +40,7 @@ type TAppArg<T extends boolean|string|number|unknown[], K = any> = {
default: T|undefined,
name?: string
},
service: 'funi'|'crunchy'|'both',
service: Array<'funi'|'crunchy'|'hidive'|'all'>,
usage: string // -(-)${name} will be added for each command,
demandOption?: true,
transformer?: (value: T) => K
@ -52,7 +52,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'Enter authentication mode',
type: 'boolean',
group: 'auth',
service: 'both',
service: ['all'],
docDescribe: 'Most of the shows on both services are only accessible if you payed for the service.'
+ '\nIn order for them to know who you are you are required to log in.'
+ '\nIf you trigger this command, you will be prompted for the username and password for the selected service',
@ -64,7 +64,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'Download all required fonts for mkv muxing',
docDescribe: 'Crunchyroll uses a variaty of fonts for the subtitles.'
+ '\nUse this command to download all the fonts and add them to the muxed **mkv** file.',
service: 'crunchy',
service: ['crunchy'],
type: 'boolean',
usage: ''
},
@ -75,7 +75,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'Search of an anime by the given string',
type: 'string',
docDescribe: true,
service: 'both',
service: ['all'],
usage: '${search}'
},
{
@ -83,7 +83,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'Search by type',
docDescribe: 'Search only for type of anime listings (e.g. episodes, series)',
group: 'search',
service: 'crunchy',
service: ['crunchy'],
type: 'string',
usage: '${type}',
choices: [ '', 'top_results', 'series', 'movie_listing', 'episode' ],
@ -97,7 +97,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'Set the page number for search results',
docDescribe: 'The output is organized in pages. Use this command to output the items for the given page',
group: 'search',
service: 'crunchy',
service: ['crunchy'],
type: 'number',
usage: '${page}'
},
@ -111,7 +111,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
default: ''
},
type: 'string',
service: 'crunchy',
service: ['crunchy'],
usage: '${locale}'
},
{
@ -119,7 +119,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
name: 'new',
describe: 'Get last updated series list',
docDescribe: true,
service: 'crunchy',
service: ['crunchy'],
type: 'boolean',
usage: '',
},
@ -129,7 +129,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
name: 'movie-listing',
describe: 'Get video list by Movie Listing ID',
docDescribe: true,
service: 'crunchy',
service: ['crunchy'],
type: 'string',
usage: '${ID}',
},
@ -140,7 +140,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'Get season list by series ID',
docDescribe: 'This command is used only for crunchyroll.'
+ '\n Requested is the ID of a show not a season.',
service: 'crunchy',
service: ['crunchy'],
type: 'string',
usage: '${ID}'
},
@ -150,7 +150,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
type: 'string',
describe: 'Set the season ID',
docDescribe: 'Used to set the season ID to download from',
service: 'both',
service: ['all'],
usage: '${ID}'
},
{
@ -160,7 +160,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
docDescribe: 'Set the episode(s) to download from any given show.'
+ '\nFor multiple selection: 1-4 OR 1,2,3,4 '
+ '\nFor special episodes: S1-4 OR S1,S2,S3,S4 where S is the special letter',
service: 'both',
service: ['all'],
type: 'string',
usage: '${selection}',
alias: 'episode'
@ -173,7 +173,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
default: 0
},
docDescribe: true,
service: 'both',
service: ['all'],
type: 'number',
usage: '${qualityLevel}'
},
@ -182,7 +182,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',
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.'
+ '\nBy the later merge of the videos, no quality difference will be present.'
@ -192,6 +192,19 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
default: false
}
},
{
name: 'removeBumpers',
describe: 'Remove bumpers from final video',
type: 'boolean',
group: 'dl',
service: ['hidive'],
docDescribe: 'If selected, it will remove the bumpers such as the hidive intro from the final file.'
+ '\nCurrently disabling this sometimes results in bugs such as video/audio desync',
usage: '',
default: {
default: true
}
},
{
name: 'x',
group: 'dl',
@ -203,7 +216,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
type: 'number',
alias: 'server',
docDescribe: true,
service: 'both',
service: ['crunchy','funi'],
usage: '${server}'
},
{
@ -216,7 +229,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
default: 1
},
docDescribe: true,
service: 'crunchy',
service: ['crunchy'],
type: 'number',
usage: '${stream}'
},
@ -231,7 +244,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
type: 'string',
usage: '${hslang}',
docDescribe: true,
service: 'crunchy'
service: ['crunchy']
},
{
name: 'dlsubs',
@ -240,7 +253,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
+ `\nFuni Only: ${languages.filter(a => a.funi_locale && !a.cr_locale).map(a => a.locale).join(', ')}`
+ `\nCrunchy Only: ${languages.filter(a => a.cr_locale && !a.funi_locale).map(a => a.locale).join(', ')}`,
docDescribe: true,
service: 'both',
service: ['all'],
type: 'array',
choices: subtitleLanguagesFilter,
default: {
@ -253,7 +266,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
group: 'dl',
describe: 'Skip downloading videos',
docDescribe: true,
service: 'both',
service: ['all'],
type: 'boolean',
usage: ''
},
@ -262,7 +275,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
group: 'dl',
describe: 'Skip downloading audio',
docDescribe: true,
service: 'both',
service: ['funi'],
type: 'boolean',
usage: ''
},
@ -271,7 +284,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
group: 'dl',
describe: 'Skip downloading subtitles',
docDescribe: true,
service: 'both',
service: ['all'],
type: 'boolean',
usage: ''
},
@ -286,7 +299,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
default: {
default: [dubLanguageCodes.slice(-1)[0]]
},
service: 'both',
service: ['all'],
type: 'array',
usage: '${dub1} ${dub2}',
},
@ -295,7 +308,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'Used to download all episodes from the show',
docDescribe: true,
group: 'dl',
service: 'both',
service: ['all'],
default: {
default: false
},
@ -310,7 +323,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
},
docDescribe: true,
group: 'dl',
service: 'both',
service: ['all'],
type: 'number',
usage: '${fontSize}'
},
@ -319,7 +332,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'If selected, all available dubs will get downloaded',
docDescribe: true,
group: 'dl',
service: 'both',
service: ['all'],
type: 'boolean',
usage: ''
},
@ -329,7 +342,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
type: 'number',
describe: 'Set the timeout of all download reqests. Set in millisecods',
docDescribe: true,
service: 'both',
service: ['all'],
usage: '${timeout}',
default: {
default: 15 * 1000
@ -340,7 +353,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
group: 'dl',
describe: 'Force downloading simulcast version instead of uncut version (if available).',
docDescribe: true,
service: 'funi',
service: ['funi', 'hidive'],
type: 'boolean',
usage: '',
default: {
@ -352,7 +365,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
group: 'mux',
describe: 'Mux video into mp4',
docDescribe: 'If selected, the output file will be an mp4 file (not recommended tho)',
service: 'both',
service: ['all'],
type: 'boolean',
usage: '',
default: {
@ -364,7 +377,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'Skip muxing video, audio and subtitles',
docDescribe: true,
group: 'mux',
service: 'both',
service: ['all'],
type: 'boolean',
usage: ''
},
@ -374,7 +387,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: `Set the filename template. Use \${variable_name} to insert variables.\nYou can also create folders by inserting a path seperator in the filename\nYou may use ${availableFilenameVars
.map(a => `'${a}'`).join(', ')} as variables.`,
docDescribe: true,
service: 'both',
service: ['all'],
type: 'string',
usage: '${fileName}',
default: {
@ -391,7 +404,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
default: 2
},
docDescribe: true,
service: 'both',
service: ['all'],
usage: '${number}'
},
{
@ -399,7 +412,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
group: 'debug',
describe: 'Reset session cookie for testing purposes',
docDescribe: true,
service: 'both',
service: ['all'],
type: 'boolean',
usage: '',
default: {
@ -411,7 +424,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
group: 'debug',
describe: 'Debug mode (tokens may be revealed in the console output)',
docDescribe: true,
service: 'both',
service: ['all'],
type: 'boolean',
usage: '',
default: {
@ -423,7 +436,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'Don\'t delete subtitle, audio and video files after muxing',
docDescribe: true,
group: 'mux',
service: 'both',
service: ['all'],
type: 'boolean',
default: {
default: false
@ -436,7 +449,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'Show the help output',
docDescribe: true,
group: 'help',
service: 'both',
service: ['all'],
type: 'boolean',
usage: ''
},
@ -445,9 +458,9 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'Set the service you want to use',
docDescribe: true,
group: 'util',
service: 'both',
service: ['all'],
type: 'string',
choices: ['funi', 'crunchy'],
choices: ['funi', 'crunchy', 'hidive'],
usage: '${service}',
default: {
default: ''
@ -459,7 +472,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
group: 'util',
describe: 'Force the tool to check for updates (code version only)',
docDescribe: true,
service: 'both',
service: ['all'],
type: 'boolean',
usage: ''
},
@ -468,7 +481,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
group: 'fonts',
describe: 'Set the font to use in subtiles',
docDescribe: true,
service: 'funi',
service: ['funi'],
type: 'string',
usage: '${fontName}',
},
@ -477,7 +490,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'Download everything but the -e selection',
docDescribe: true,
group: 'dl',
service: 'both',
service: ['all'],
type: 'boolean',
usage: ''
},
@ -486,7 +499,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'Used to download all archived shows',
group: 'dl',
docDescribe: true,
service: 'both',
service: ['all'],
type: 'boolean',
usage: ''
},
@ -495,7 +508,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'Used to add the `-s` and `--srz` to downloadArchive',
group: 'dl',
docDescribe: true,
service: 'both',
service: ['all'],
type: 'boolean',
usage: ''
},
@ -504,7 +517,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'Skip muxing the subtitles',
docDescribe: true,
group: 'mux',
service: 'both',
service: ['all'],
type: 'boolean',
usage: '',
default: {
@ -516,7 +529,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'Set the amount of parts to download at once',
docDescribe: 'Set the amount of parts to download at once\nIf you have a good connection try incresing this number to get a higher overall speed',
group: 'dl',
service: 'both',
service: ['all'],
type: 'number',
usage: '${amount}',
default: {
@ -528,7 +541,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'Set the username to use for the authentication. If not provided, you will be prompted for the input',
docDescribe: true,
group: 'auth',
service: 'both',
service: ['all'],
type: 'string',
usage: '${username}',
default: {
@ -540,7 +553,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'Set the password to use for the authentication. If not provided, you will be prompted for the input',
docDescribe: true,
group: 'auth',
service: 'both',
service: ['all'],
type: 'string',
usage: '${password}',
default: {
@ -552,7 +565,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'Authenticate every time the script runs. Use at your own risk.',
docDescribe: true,
group: 'auth',
service: 'both',
service: ['funi','crunchy'],
type: 'boolean',
usage: '',
default: {
@ -564,7 +577,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'Force the program to use said muxer or don\'t mux if the given muxer is not present',
docDescribe: true,
group: 'mux',
service: 'both',
service: ['all'],
type: 'string',
usage: '${muxer}',
choices: muxer,
@ -577,7 +590,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'Set the time the downloader waits before retrying if an error while writing the file occurs',
docDescribe: true,
group: 'dl',
service: 'both',
service: ['all'],
type: 'number',
usage: '${time in seconds}',
default: {
@ -589,7 +602,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'Override a template variable',
docDescribe: true,
group: 'fileName',
service: 'both',
service: ['all'],
type: 'array',
usage: '"${toOverride}=\'${value}\'"',
default: {
@ -601,7 +614,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'Set the video track name of the merged file',
docDescribe: true,
group: 'mux',
service: 'both',
service: ['all'],
type: 'string',
usage: '${title}'
},
@ -610,7 +623,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'If true, the tool won\'t check for updates',
docDescribe: true,
group: 'util',
service: 'both',
service: ['all'],
type: 'boolean',
usage: '',
default: {
@ -622,7 +635,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'Set the default option for the \'alredy exists\' prompt',
docDescribe: 'If a file already exists, the tool will ask you how to proceed. With this, you can answer in advance.',
group: 'dl',
service: 'both',
service: ['all'],
type: 'string',
usage: '${option}',
choices: [ 'y', 'Y', 'n', 'N', 'c', 'C' ]
@ -632,7 +645,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'Set the options given to mkvmerge',
docDescribe: true,
group: 'mux',
service: 'both',
service: ['all'],
type: 'array',
usage: '${args}',
default: {
@ -648,7 +661,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'Set the options given to ffmpeg',
docDescribe: true,
group: 'mux',
service: 'both',
service: ['all'],
type: 'array',
usage: '${args}',
default: {
@ -660,7 +673,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: `Set the default audio track by language code\nPossible Values: ${languages.map(a => a.code).join(', ')}`,
docDescribe: true,
group: 'mux',
service: 'both',
service: ['all'],
type: 'string',
usage: '${args}',
default: {
@ -679,7 +692,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: `Set the default subtitle track by language code\nPossible Values: ${languages.map(a => a.code).join(', ')}`,
docDescribe: true,
group: 'mux',
service: 'both',
service: ['all'],
type: 'string',
usage: '${args}',
default: {
@ -698,7 +711,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
describe: 'Used to set the name for subtitles that contain tranlations for none verbal communication (e.g. signs)',
docDescribe: true,
group: 'fileName',
service: 'both',
service: ['all'],
type: 'string',
usage: '${tag}',
default: {

View file

@ -15,11 +15,17 @@ const binCfgFile = path.join(workingDir, 'config', 'bin-path');
const dirCfgFile = path.join(workingDir, 'config', 'dir-path');
const guiCfgFile = path.join(workingDir, 'config', 'gui');
const cliCfgFile = path.join(workingDir, 'config', 'cli-defaults');
const sessCfgFile = path.join(workingDir, 'config', 'session');
const hdProfileCfgFile = path.join(workingDir, 'config', 'hd_profile');
const sessCfgFile = {
funi: path.join(workingDir, 'config', 'funi_sess'),
cr: path.join(workingDir, 'config', 'cr_sess'),
hd: path.join(workingDir, 'config', 'hd_sess')
};
const setupFile = path.join(workingDir, 'config', 'setup');
const tokenFile = {
funi: path.join(workingDir, 'config', 'funi_token'),
cr: path.join(workingDir, 'config', 'cr_token')
cr: path.join(workingDir, 'config', 'cr_token'),
hd: path.join(workingDir, 'config', 'hd_token')
};
export const ensureConfig = () => {
@ -162,7 +168,7 @@ const loadBinCfg = async () => {
};
const loadCRSession = () => {
let session = loadYamlCfgFile(sessCfgFile, true);
let session = loadYamlCfgFile(sessCfgFile.cr, true);
if(typeof session !== 'object' || session === null || Array.isArray(session)){
session = {};
}
@ -175,10 +181,10 @@ const loadCRSession = () => {
};
const saveCRSession = (data: Record<string, unknown>) => {
const cfgFolder = path.dirname(sessCfgFile);
const cfgFolder = path.dirname(sessCfgFile.cr);
try{
fs.ensureDirSync(cfgFolder);
fs.writeFileSync(`${sessCfgFile}.yml`, yaml.stringify(data));
fs.writeFileSync(`${sessCfgFile.cr}.yml`, yaml.stringify(data));
}
catch(e){
console.error('Can\'t save session file to disk!');
@ -204,6 +210,83 @@ const saveCRToken = (data: Record<string, unknown>) => {
}
};
const loadHDSession = () => {
let session = loadYamlCfgFile(sessCfgFile.hd, true);
if(typeof session !== 'object' || session === null || Array.isArray(session)){
session = {};
}
for(const cv of Object.keys(session)){
if(typeof session[cv] !== 'object' || session[cv] === null || Array.isArray(session[cv])){
session[cv] = {};
}
}
return session;
};
const saveHDSession = (data: Record<string, unknown>) => {
const cfgFolder = path.dirname(sessCfgFile.hd);
try{
fs.ensureDirSync(cfgFolder);
fs.writeFileSync(`${sessCfgFile.hd}.yml`, yaml.stringify(data));
}
catch(e){
console.error('Can\'t save session file to disk!');
}
};
const loadHDToken = () => {
let token = loadYamlCfgFile(tokenFile.cr, true);
if(typeof token !== 'object' || token === null || Array.isArray(token)){
token = {};
}
return token;
};
const saveHDToken = (data: Record<string, unknown>) => {
const cfgFolder = path.dirname(tokenFile.hd);
try{
fs.ensureDirSync(cfgFolder);
fs.writeFileSync(`${tokenFile.hd}.yml`, yaml.stringify(data));
}
catch(e){
console.error('Can\'t save token file to disk!');
}
};
const saveHDProfile = (data: Record<string, unknown>) => {
const cfgFolder = path.dirname(hdProfileCfgFile);
try{
fs.ensureDirSync(cfgFolder);
fs.writeFileSync(`${hdProfileCfgFile}.yml`, yaml.stringify(data));
}
catch(e){
console.error('Can\'t save profile file to disk!');
}
};
const loadHDProfile = () => {
let profile = loadYamlCfgFile(hdProfileCfgFile, true);
if(typeof profile !== 'object' || profile === null || Array.isArray(profile) || Object.keys(profile).length === 0){
profile = {
// base
ipAddress : '',
xNonce : '',
xSignature: '',
// personal
visitId : '',
// profile data
profile: {
userId : 0,
profileId: 0,
deviceId : '',
},
};
}
return profile;
};
const loadFuniToken = () => {
const loadedToken = loadYamlCfgFile<{
token?: string
@ -260,12 +343,19 @@ export {
loadFuniToken,
saveFuniToken,
saveCRSession,
loadCRSession,
saveCRToken,
loadCRToken,
loadCRSession,
saveHDSession,
loadHDSession,
saveHDToken,
loadHDToken,
saveHDProfile,
loadHDProfile,
isSetuped,
setSetuped,
writeYamlCfgFile,
sessCfgFile,
hdProfileCfgFile,
cfgDir
};

View file

@ -14,6 +14,9 @@ export type DataType = {
funi: {
s: ItemType
},
hidive: {
s: ItemType
},
crunchy: {
srz: ItemType,
s: ItemType
@ -26,6 +29,9 @@ const addToArchive = (kind: {
} | {
service: 'crunchy',
type: 's'|'srz'
} | {
service: 'hidive',
type: 's'
}, ID: string) => {
const data = loadData();
@ -48,7 +54,7 @@ const addToArchive = (kind: {
}
]
};
} else {
} else if (kind.service === 'crunchy') {
data['crunchy'] = {
s: ([] as ItemType).concat(kind.type === 's' ? {
id: ID,
@ -59,6 +65,15 @@ const addToArchive = (kind: {
already: [] as string[]
} : []),
};
} else {
data['hidive'] = {
s: [
{
id: ID,
already: []
}
]
};
}
}
fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4));
@ -70,6 +85,9 @@ const downloaded = (kind: {
} | {
service: 'crunchy',
type: 's'|'srz'
} | {
service: 'hidive',
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)
@ -81,7 +99,7 @@ const downloaded = (kind: {
fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4));
};
const makeCommand = (service: 'funi'|'crunchy') : Partial<ArgvType>[] => {
const makeCommand = (service: 'funi'|'crunchy'|'hidive') : Partial<ArgvType>[] => {
const data = loadData();
const ret: Partial<ArgvType>[] = [];
const kind = data[service];

View file

@ -2,6 +2,7 @@
export type LanguageItem = {
cr_locale?: string,
hd_locale?: string,
locale: string,
code: string,
name: string,
@ -12,25 +13,25 @@ export type LanguageItem = {
}
const languages: LanguageItem[] = [
{ cr_locale: 'en-US', funi_locale: 'enUS', locale: 'en', code: 'eng', name: 'English' },
{ cr_locale: 'en-US', hd_locale: 'English', funi_locale: 'enUS', locale: 'en', code: 'eng', name: 'English' },
{ cr_locale: 'en-IN', locale: 'en-IN', code: 'eng', name: 'English (India)', },
{ cr_locale: 'es-LA', funi_name: 'Spanish (LAS)', funi_name_lagacy: 'Spanish (Latin Am)', funi_locale: 'esLA', locale: 'es-419', code: 'spa', name: 'Spanish', language: 'Latin American Spanish' },
{ cr_locale: 'es-419', locale: 'es-419', code: 'spa-419', name: 'Spanish', language: 'Latin American Spanish' },
{ cr_locale: 'es-LA', hd_locale: 'Spanish LatAm', funi_name: 'Spanish (LAS)', funi_name_lagacy: 'Spanish (Latin Am)', funi_locale: 'esLA', locale: 'es-419', code: 'spa', name: 'Spanish', language: 'Latin American Spanish' },
{ cr_locale: 'es-419',hd_locale: 'Spanish', locale: 'es-419', code: 'spa-419', name: 'Spanish', language: 'Latin American Spanish' },
{ cr_locale: 'es-ES', locale: 'es-ES', code: 'spa-ES', name: 'Castilian', language: 'European Spanish' },
{ cr_locale: 'pt-BR', funi_name: 'Portuguese (Brazil)', funi_locale: 'ptBR', locale: 'pt-BR', code: 'por', name: 'Portuguese', language: 'Brazilian Portuguese' },
{ cr_locale: 'pt-BR', hd_locale: 'Portuguese', funi_name: 'Portuguese (Brazil)', funi_locale: 'ptBR', locale: 'pt-BR', code: 'por', name: 'Portuguese', language: 'Brazilian Portuguese' },
{ cr_locale: 'pt-PT', locale: 'pt-PT', code: 'por', name: 'Portuguese (Portugal)', language: 'Portugues (Portugal)' },
{ cr_locale: 'fr-FR', locale: 'fr', code: 'fra', name: 'French' },
{ cr_locale: 'de-DE', locale: 'de', code: 'deu', name: 'German' },
{ 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: 'ar-ME', locale: 'ar', code: 'ara-ME', name: 'Arabic' },
{ cr_locale: 'ar-SA', locale: 'ar', code: 'ara', name: 'Arabic (Saudi Arabia)' },
{ cr_locale: 'it-IT', locale: 'it', code: 'ita', name: 'Italian' },
{ cr_locale: 'it-IT', hd_locale: 'Italian', locale: 'it', code: 'ita', name: 'Italian' },
{ cr_locale: 'ru-RU', locale: 'ru', code: 'rus', name: 'Russian' },
{ cr_locale: 'tr-TR', locale: 'tr', code: 'tur', name: 'Turkish' },
{ cr_locale: 'tr-TR', hd_locale: 'Turkish', locale: 'tr', code: 'tur', name: 'Turkish' },
{ cr_locale: 'hi-IN', locale: 'hi', code: 'hin', name: 'Hindi' },
{ funi_locale: 'zhMN', locale: 'zh', code: 'cmn', name: 'Chinese (Mandarin, PRC)' },
{ cr_locale: 'zh-CN', locale: 'zh-CN', code: 'zho', name: 'Chinese (Mainland China)' },
{ cr_locale: 'ko-KR', locale: 'ko', code: 'kor', name: 'Korean' },
{ cr_locale: 'ja-JP', funi_locale: 'jaJP', locale: 'ja', code: 'jpn', name: 'Japanese' },
{ cr_locale: 'ko-KR', hd_locale: 'Korean', locale: 'ko', code: 'kor', name: 'Korean' },
{ cr_locale: 'ja-JP', hd_locale: 'Japanese', funi_locale: 'jaJP', locale: 'ja', code: 'jpn', name: 'Japanese' },
];
// add en language names

View file

@ -35,6 +35,7 @@ export type MergerOptions = {
output: string,
videoTitle?: string,
simul?: boolean,
inverseTrackOrder?: boolean,
fonts?: ParsedFont[],
skipSubMux?: boolean,
options: {
@ -160,49 +161,52 @@ class Merger {
}
for (const vid of this.options.videoAndAudio) {
const audioTrackNum = this.options.inverseTrackOrder ? '0' : '1';
const videoTrackNum = this.options.inverseTrackOrder ? '1' : '0';
if (!hasVideo) {
args.push(
'--video-tracks 0',
'--audio-tracks 1'
`--video-tracks ${videoTrackNum}`,
`--audio-tracks ${audioTrackNum}`
);
const trackName = ((this.options.videoTitle ?? vid.lang.name) + (this.options.simul ? ' [Simulcast]' : ' [Uncut]'));
args.push('--track-name', `0:"${trackName}"`);
//args.push('--track-name', `1:"${trackName}"`);
args.push(`--language 1:${vid.lang.code}`);
args.push(`--language ${audioTrackNum}:${vid.lang.code}`);
if (this.options.defaults.audio.code === vid.lang.code) {
args.push('--default-track 1');
args.push(`--default-track ${audioTrackNum}`);
} else {
args.push('--default-track 1:0');
args.push(`--default-track ${audioTrackNum}:0`);
}
hasVideo = true;
} else {
args.push(
'--no-video',
'--audio-tracks 1'
`--audio-tracks ${audioTrackNum}`
);
if (this.options.defaults.audio.code === vid.lang.code) {
args.push('--default-track 1');
args.push(`--default-track ${audioTrackNum}`);
} else {
args.push('--default-track 1:0');
args.push(`--default-track ${audioTrackNum}:0`);
}
args.push('--track-name', `1:"${vid.lang.name}"`);
args.push(`--language 1:${vid.lang.code}`);
args.push('--track-name', `${audioTrackNum}:"${vid.lang.name}"`);
args.push(`--language ${audioTrackNum}:${vid.lang.code}`);
}
args.push(`"${vid.path}"`);
}
for (const aud of this.options.onlyAudio) {
const trackName = aud.lang.name;
args.push('--track-name', `0:"${trackName}"`);
args.push(`--language 0:${aud.lang.code}`);
const trackNum = this.options.inverseTrackOrder ? '0' : '1';
args.push('--track-name', `${trackNum}:"${trackName}"`);
args.push(`--language ${trackNum}:${aud.lang.code}`);
args.push(
'--no-video',
'--audio-tracks 0'
`--audio-tracks ${trackNum}`
);
if (this.options.defaults.audio.code === aud.lang.code) {
args.push('--default-track 0');
args.push(`--default-track ${trackNum}`);
} else {
args.push('--default-track 0:0');
args.push(`--default-track ${trackNum}:0`);
}
args.push(`"${aud.path}"`);
}

View file

@ -26,7 +26,8 @@ const usefulCookies = {
// req
class Req {
private sessCfg = yamlCfg.sessCfgFile;
private sessCfg: string;
private service: 'cr'|'funi'|'hd';
private session: Record<string, {
value: string;
expires: Date;
@ -38,7 +39,10 @@ class Req {
private cfgDir = yamlCfg.cfgDir;
private curl: boolean|string = false;
constructor(private domain: Record<string, unknown>, private debug: boolean, private nosess = false) {}
constructor(private domain: Record<string, unknown>, private debug: boolean, private nosess = false, private type: 'cr'|'funi'|'hd') {
this.sessCfg = yamlCfg.sessCfgFile[type];
this.service = type;
}
async getData<T = string> (durl: string, params?: Params) {
params = params || {};
// options
@ -168,7 +172,11 @@ class Req {
if(this.debug){
console.info('[SAVING FILE]',`${this.sessCfg}.yml`);
}
yamlCfg.saveCRSession(this.session);
if (this.type === 'cr') {
yamlCfg.saveCRSession(this.session);
} else if (this.type === 'hd') {
yamlCfg.saveHDSession(this.session);
}
console.info(`Cookies were updated! (${cookieUpdated.join(', ')})\n`);
}
}

384
modules/module.vtt2ass.ts Normal file
View file

@ -0,0 +1,384 @@
// const
const cssPrefixRx = /\.rmp-container>\.rmp-content>\.rmp-cc-area>\.rmp-cc-container>\.rmp-cc-display>\.rmp-cc-cue /g;
import { console } from './log';
// colors
import colors from './module.colors.json';
const defaultStyleName = 'Default';
const defaultStyleFont = 'Arial';
// predefined
let relGroup = '';
let fontSize = 0;
let tmMrg = 0;
let rFont = '';
function loadCSS(cssStr: string) {
const css = cssStr.replace(cssPrefixRx, '').replace(/[\r\n]+/g, '\n').split('\n');
const defaultSFont = rFont == '' ? defaultStyleFont : rFont;
let defaultStyle = `${defaultSFont},40,&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,1,0,2,20,20,20,1`; //base for nonDialog
const styles = { [defaultStyleName]: { params: defaultStyle, list: [] } };
const classList = { [defaultStyleName]: 1 };
for (const i in css) {
let clx, clz, clzx, rgx;
const l = css[i];
if (l === '') continue;
const m = l.match(/^(.*)\{(.*)\}$/);
if (!m) {
console.error(`[WARN] VTT2ASS: Invalid css in line ${i}: ${l}`);
continue;
}
if (m[1] === '') {
const style = parseStyle(defaultStyleName, m[2], defaultStyle);
styles[defaultStyleName].params = style;
defaultStyle = style;
} else {
clx = m[1].replace(/\./g, '').split(',');
clz = clx[0].replace(/-C(\d+)_(\d+)$/i, '').replace(/-(\d+)$/i, '');
classList[clz] = (classList[clz] || 0) + 1;
rgx = classList[clz];
const classSubNum = rgx > 1 ? `-${rgx}` : '';
clzx = clz + classSubNum;
const style = parseStyle(clzx, m[2], defaultStyle);
styles[clzx] = { params: style, list: clx };
}
}
return styles;
}
function parseStyle(stylegroup: string, line: string, style: any) {
const defaultSFont = rFont == '' ? defaultStyleFont : rFont; //redeclare cause of let
if (stylegroup.startsWith('Subtitle') || stylegroup.startsWith('Song')) { //base for dialog, everything else use defaultStyle
style = `${defaultSFont},${fontSize},&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2.6,0,2,20,20,46,1`;
}
// Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour,
// BackColour, Bold, Italic, Underline, StrikeOut,
// ScaleX, ScaleY, Spacing, Angle, BorderStyle,
// Outline, Shadow, Alignment, MarginL, MarginR,
// MarginV, Encoding
style = style.split(',');
for (const s of line.split(';')) {
if (s == '') continue;
const st = s.trim().split(':');
let cl;
switch (st[0]) {
case 'font-family':
if (rFont != '') { //do rewrite if rFont is specified
if (stylegroup.startsWith('Subtitle') || stylegroup.startsWith('Song')) {
style[0] = rFont; //dialog to rFont
} else {
style[0] = defaultStyleFont; //non-dialog to Arial
}
} else { //otherwise keep default style
style[0] = st[1].match(/[\s"]*([^",]*)/)[1];
}
break;
case 'font-size':
style[1] = getPxSize(st[1], style[1]); //scale it based on input style size... so for dialog, this is the dialog font size set in config, for non dialog, it's 40 from default font size
break;
case 'color':
cl = getColor(st[1]);
if (cl !== null) {
if (cl == '&H0000FFFF') {
style[2] = style[3] = '&H00FFFFFF';
}
else {
style[2] = style[3] = cl;
}
}
break;
case 'font-weight':
if (stylegroup.startsWith('Subtitle') || stylegroup.startsWith('Song')) { //don't touch font-weight if dialog
break;
}
// console.info("Changing bold weight");
// console.info(stylegroup);
if (st[1] === 'bold') {
style[6] = -1;
break;
}
if (st[1] === 'normal') {
break;
}
break;
case 'font-style':
if (st[1] === 'italic') {
style[7] = -1;
break;
}
break;
case 'background':
if (st[1] === 'none') {
break;
}
break;
case 'text-shadow':
if (stylegroup.startsWith('Subtitle') || stylegroup.startsWith('Song')) { //don't touch shadow if dialog
break;
}
st[1] = st[1].split(',').map(r => r.trim());
st[1] = st[1].map(r => { return (r.split(' ').length > 3 ? r.replace(/(\d+)px black$/, '') : r.replace(/black$/, '')).trim(); });
st[1] = st[1].map(r => r.replace(/-/g, '').replace(/px/g, '').replace(/(^| )0( |$)/g, ' ').trim()).join(' ');
st[1] = st[1].split(' ');
if (st[1].length != 10) {
console.info(`[WARN] VTT2ASS: Can't properly parse text-shadow: ${s.trim()}`);
break;
}
st[1] = [...new Set(st[1])];
if (st[1].length > 1) {
console.info(`[WARN] VTT2ASS: Can't properly parse text-shadow: ${s.trim()}`);
break;
}
style[16] = st[1][0];
break;
default:
console.error(`[WARN] VTT2ASS: Unknown style: ${s.trim()}`);
}
}
return style.join(',');
}
function getPxSize(size_line: string, font_size: number) {
const m = size_line.trim().match(/([\d.]+)(.*)/);
if (!m) {
console.error(`[WARN] VTT2ASS: Unknown size: ${size_line}`);
return;
}
if (m[2] === 'em') m[1] *= font_size;
return Math.round(m[1]);
}
function getColor(c) {
if (c[0] !== '#') {
c = colors[c];
}
else if (c.length < 7) {
c = `#${c[1]}${c[1]}${c[2]}${c[2]}${c[3]}${c[3]}`;
}
const m = c.match(/#(..)(..)(..)/);
if (!m) return null;
return `&H00${m[3]}${m[2]}${m[1]}`.toUpperCase();
}
function loadVTT(vttStr: string) {
const rx = /^([\d:.]*) --> ([\d:.]*)\s?(.*?)\s*$/;
const lines = vttStr.replace(/\r?\n/g, '\n').split('\n');
const data = [];
let record = null;
let lineBuf = [];
for (const l of lines) {
const m = l.match(rx);
if (m) {
let caption = '';
if (lineBuf.length > 0) {
caption = lineBuf.pop();
}
if (caption !== '' && lineBuf.length > 0) {
lineBuf.pop();
}
if (record !== null) {
record.text = lineBuf.join('\n');
data.push(record);
}
record = {
caption,
time: {
start: m[1],
end: m[2],
ext: m[3].split(' ').map(x => x.split(':')).reduce((p, c) => (p[c[0]] = c[1]) && p, {}),
}
};
lineBuf = [];
continue;
}
lineBuf.push(l);
}
if (record !== null) {
if (lineBuf[lineBuf.length - 1] === '') {
lineBuf.pop();
}
record.text = lineBuf.join('\n');
data.push(record);
}
return data;
}
function convert(css, vtt) {
const stylesMap = {};
let ass = [
'\ufeff[Script Info]',
'Title: ' + relGroup,
'ScriptType: v4.00+',
'WrapStyle: 0',
'PlayResX: 1280',
'PlayResY: 720',
'ScaledBorderAndShadow: yes',
'',
'[V4+ Styles]',
'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding',
];
for (const s in css) {
ass.push(`Style: ${s},${css[s].params}`);
css[s].list.forEach(x => stylesMap[x] = s);
}
ass = ass.concat([
'',
'[Events]',
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text'
]);
const events = {
subtitle: [],
caption: [],
capt_pos: [],
song_cap: [],
};
const linesMap = {};
for (const l in vtt) {
const x = convertLine(stylesMap, vtt[l]);
if (x.ind !== '' && linesMap[x.ind] !== undefined) {
if (x.subInd > 1) {
const fx = convertLine(stylesMap, vtt[l - x.subInd + 1]);
if (x.style != fx.style) {
x.text = `{\\r${x.style}}${x.text}{\\r}`;
}
}
events[x.type][linesMap[x.ind]] += '\\N' + x.text;
}
else {
events[x.type].push(x.res);
if (x.ind !== '') {
linesMap[x.ind] = events[x.type].length - 1;
}
}
}
if (events.subtitle.length > 0) {
ass = ass.concat(
//`Comment: 0,0:00:00.00,0:00:00.00,${defaultStyleName},,0,0,0,,** Subtitles **`,
events.subtitle
);
}
if (events.caption.length > 0) {
ass = ass.concat(
//`Comment: 0,0:00:00.00,0:00:00.00,${defaultStyleName},,0,0,0,,** Captions **`,
events.caption
);
}
if (events.capt_pos.length > 0) {
ass = ass.concat(
//`Comment: 0,0:00:00.00,0:00:00.00,${defaultStyleName},,0,0,0,,** Captions with position **`,
events.capt_pos
);
}
if (events.song_cap.length > 0) {
ass = ass.concat(
//`Comment: 0,0:00:00.00,0:00:00.00,${defaultStyleName},,0,0,0,,** Song captions **`,
events.song_cap
);
}
return ass.join('\r\n') + '\r\n';
}
function convertLine(css: string, l: Record<any, any>) {
const start = convertTime(l.time.start);
const end = convertTime(l.time.end);
const txt = convertText(l.text);
let type = txt.style.match(/Caption/i) ? 'caption' : (txt.style.match(/SongCap/i) ? 'song_cap' : 'subtitle');
type = type == 'caption' && l.time.ext.position !== undefined ? 'capt_pos' : type;
if (l.time.ext.align === 'left') {
txt.text = `{\\an7}${txt.text}`;
}
let ind = '', subInd = 1;
const sMinus = 0; // (19.2 * 2);
if (l.time.ext.position !== undefined) {
const pos = parseInt(l.time.ext.position);
const PosX = pos < 0 ? (1280 / 100 * (100 - pos)) : ((1280 - sMinus) / 100 * pos);
const line = parseInt(l.time.ext.line) || 0;
const PosY = line < 0 ? (720 / 100 * (100 - line)) : ((720 - sMinus) / 100 * line);
txt.text = `{\\pos(${parseFloat(PosX.toFixed(3))},${parseFloat(PosY.toFixed(3))})}${txt.text}`;
}
else if (l.time.ext.line !== undefined && type == 'caption') {
const line = parseInt(l.time.ext.line);
const PosY = line < 0 ? (720 / 100 * (100 - line)) : ((720 - sMinus) / 100 * line);
txt.text = `{\\pos(640,${parseFloat(PosY.toFixed(3))})}${txt.text}`;
}
else {
const indregx = txt.style.match(/(.*)_(\d+)$/);
if (indregx !== null) {
ind = indregx[1];
subInd = parseInt(indregx[2]);
}
}
const style = css[txt.style as any] || defaultStyleName;
const res = `Dialogue: 0,${start},${end},${style},,0,0,0,,${txt.text}`;
return { type, ind, subInd, start, end, style, text: txt.text, res };
}
function convertText(text: string) {
const m = text.match(/<c\.([^>]*)>([\S\s]*)<\/c>/);
let style = '';
if (m) {
style = m[1];
text = m[2];
}
const xtext = text
// .replace(/<c[^>]*>[^<]*<\/c>/g, '')
// .replace(/<ruby[^>]*>[^<]*<\/ruby>/g, '')
.replace(/ \\N$/g, '\\N')
.replace(/<[^>]>/g, '')
.replace(/\\N$/, '')
.replace(/\r/g, '')
.replace(/\n/g, '\\N')
.replace(/\\N +/g, '\\N')
.replace(/ +\\N/g, '\\N')
.replace(/(\\N)+/g, '\\N')
.replace(/<b[^>]*>([^<]*)<\/b>/g, '{\\b1}$1{\\b0}')
.replace(/<i[^>]*>([^<]*)<\/i>/g, '{\\i1}$1{\\i0}')
.replace(/<u[^>]*>([^<]*)<\/u>/g, '{\\u1}$1{\\u0}')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&')
.replace(/<[^>]>/g, '')
.replace(/\\N$/, '')
.replace(/ +$/, '');
text = xtext;
return { style, text };
}
function convertTime(tm: string) {
const m = tm.match(/([\d:]*)\.?(\d*)/);
if (!m) return '0:00:00.00';
return toSubTime(m[0]);
}
function toSubTime(str: string) {
const n = [];
let sx;
const x: any[] = str.split(/[:.]/).map(x => Number(x));
x[3] = '0.' + ('00' + x[3]).slice(-3);
sx = (x[0] * 60 * 60 + x[1] * 60 + x[2] + Number(x[3]) - tmMrg).toFixed(2);
sx = sx.toString().split('.');
n.unshift(sx[1]);
sx = Number(sx[0]);
n.unshift(('0' + ((sx % 60).toString())).slice(-2));
n.unshift(('0' + ((Math.floor(sx / 60) % 60).toString())).slice(-2));
n.unshift((Math.floor(sx / 3600) % 60).toString());
return n.slice(0, 3).join(':') + '.' + n[3];
}
function vtt(group: string | undefined, xFontSize: number | undefined, vttStr: string, cssStr: string, timeMargin?: number, replaceFont?: string) {
relGroup = group ?? '';
fontSize = xFontSize && xFontSize > 0 ? xFontSize : 34; // 1em to pix
tmMrg = timeMargin ? timeMargin : 0; //
rFont = replaceFont ? replaceFont : rFont;
return convert(
loadCSS(cssStr),
loadVTT(vttStr)
);
}
export { vtt };

View file

@ -1,8 +1,8 @@
{
"name": "multi-downloader-nx",
"short_name": "aniDL",
"version": "3.4.4",
"description": "Download videos from Funimation or Crunchyroll via cli",
"version": "4.0.0",
"description": "Download videos from Funimation, Crunchyroll, or Hidive via cli",
"keywords": [
"download",
"downloader",

3
tsc.ts
View file

@ -34,6 +34,9 @@ const ignore = [
'./config/updates.json$',
'./config/cr_token.yml$',
'./config/funi_token.yml$',
'./config/hd_token.yml$',
'./config/hd_sess.yml$',
'./config/hd_profile.yml$',
'*/\\.eslint*',
'*/*\\.tsx?$',
'./fonts*',