Initial commit to add hidive
This commit is contained in:
parent
8414edfab9
commit
612bdff774
29 changed files with 3461 additions and 1757 deletions
3
.github/ISSUE_TEMPLATE/bug.yml
vendored
3
.github/ISSUE_TEMPLATE/bug.yml
vendored
|
|
@ -49,7 +49,8 @@ body:
|
|||
options:
|
||||
- Funimation
|
||||
- Crunchyroll
|
||||
- Both
|
||||
- Hidive
|
||||
- All
|
||||
- Irrelevant
|
||||
validations:
|
||||
required: true
|
||||
|
|
|
|||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -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
84
@types/hidiveEpisodeList.d.ts
vendored
Normal 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
47
@types/hidiveSearch.d.ts
vendored
Normal 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
60
@types/hidiveTypes.d.ts
vendored
Normal 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
4
@types/ws.d.ts
vendored
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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
2
dev.js
|
|
@ -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')
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
# Anime Downloader NX by AniDL
|
||||
|
||||
[](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
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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)> = {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
126
gui/server/services/hidive.ts
Normal file
126
gui/server/services/hidive.ts
Normal 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
792
hidive.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
15
index.ts
15
index.ts
|
|
@ -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();
|
||||
}
|
||||
})();
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}"`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
384
modules/module.vtt2ass.ts
Normal 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(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/&/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 };
|
||||
|
|
@ -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
3
tsc.ts
|
|
@ -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*',
|
||||
|
|
|
|||
Loading…
Reference in a new issue