mirror of
https://github.com/anidl/multi-downloader-nx.git
synced 2026-03-11 17:45:30 +00:00
Convert main files to tabs
Still need to do .tsx files and type declaration files
This commit is contained in:
parent
c94268ef6a
commit
460b4c1d0e
49 changed files with 11559 additions and 11559 deletions
5652
crunchy.ts
5652
crunchy.ts
File diff suppressed because it is too large
Load diff
|
|
@ -4,60 +4,60 @@ import eslint from '@eslint/js';
|
|||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
rules: {
|
||||
'no-console': 2,
|
||||
'react/prop-types': 0,
|
||||
'react-hooks/exhaustive-deps': 0,
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unsafe-declaration-merging': 'warn',
|
||||
'@typescript-eslint/no-unused-vars' : 'warn',
|
||||
'@typescript-eslint/no-unused-expressions': 'warn',
|
||||
'indent': [
|
||||
'error',
|
||||
2
|
||||
],
|
||||
'linebreak-style': [
|
||||
'warn',
|
||||
'windows'
|
||||
],
|
||||
'quotes': [
|
||||
'error',
|
||||
'single'
|
||||
],
|
||||
'semi': [
|
||||
'error',
|
||||
'always'
|
||||
]
|
||||
},
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
rules: {
|
||||
'no-console': 2,
|
||||
'react/prop-types': 0,
|
||||
'react-hooks/exhaustive-deps': 0,
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unsafe-declaration-merging': 'warn',
|
||||
'@typescript-eslint/no-unused-vars' : 'warn',
|
||||
'@typescript-eslint/no-unused-expressions': 'warn',
|
||||
'indent': [
|
||||
'error',
|
||||
4
|
||||
],
|
||||
'linebreak-style': [
|
||||
'warn',
|
||||
'windows'
|
||||
],
|
||||
'quotes': [
|
||||
'error',
|
||||
'single'
|
||||
],
|
||||
'semi': [
|
||||
'error',
|
||||
'always'
|
||||
]
|
||||
},
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module'
|
||||
},
|
||||
parser: tseslint.parser
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module'
|
||||
},
|
||||
parser: tseslint.parser
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
'**/lib',
|
||||
'**/videos/*.ts',
|
||||
'**/build',
|
||||
'dev.js',
|
||||
'tsc.ts'
|
||||
]
|
||||
},
|
||||
{
|
||||
files: ['gui/react/**/*'],
|
||||
rules: {
|
||||
'no-console': 0,
|
||||
// Disabled because ESLint bugs around on .tsx files somehow?
|
||||
indent: 'off'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
'**/lib',
|
||||
'**/videos/*.ts',
|
||||
'**/build',
|
||||
'dev.js',
|
||||
'tsc.ts'
|
||||
]
|
||||
},
|
||||
{
|
||||
files: ['gui/react/**/*'],
|
||||
rules: {
|
||||
'no-console': 0,
|
||||
// Disabled because ESLint bugs around on .tsx files somehow?
|
||||
indent: 'off'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -25,7 +25,7 @@ app.use(express.static(path.join(workingDir, 'gui', 'server', 'build'), { maxAge
|
|||
console.info(`\n=== Multi Downloader NX GUI ${packageJson.version} ===\n`);
|
||||
|
||||
const server = app.listen(cfg.gui.port, () => {
|
||||
console.info(`GUI server started on port ${cfg.gui.port}`);
|
||||
console.info(`GUI server started on port ${cfg.gui.port}`);
|
||||
});
|
||||
|
||||
new PublicWebSocket(server);
|
||||
|
|
|
|||
|
|
@ -12,123 +12,123 @@ import packageJson from '../../package.json';
|
|||
|
||||
export default class ServiceHandler {
|
||||
|
||||
private service: MessageHandler|undefined = undefined;
|
||||
private ws: WebSocketHandler;
|
||||
private state: GuiState;
|
||||
private service: MessageHandler|undefined = undefined;
|
||||
private ws: WebSocketHandler;
|
||||
private state: GuiState;
|
||||
|
||||
constructor(server: Server<typeof IncomingMessage, typeof ServerResponse>) {
|
||||
this.ws = new WebSocketHandler(server);
|
||||
this.handleMessages();
|
||||
this.state = getState();
|
||||
}
|
||||
constructor(server: Server<typeof IncomingMessage, typeof ServerResponse>) {
|
||||
this.ws = new WebSocketHandler(server);
|
||||
this.handleMessages();
|
||||
this.state = getState();
|
||||
}
|
||||
|
||||
private handleMessages() {
|
||||
this.ws.events.on('setupServer', ({ data }, respond) => {
|
||||
writeYamlCfgFile('gui', data);
|
||||
this.state.setup = true;
|
||||
setState(this.state);
|
||||
respond(true);
|
||||
process.exit(0);
|
||||
});
|
||||
private handleMessages() {
|
||||
this.ws.events.on('setupServer', ({ data }, respond) => {
|
||||
writeYamlCfgFile('gui', data);
|
||||
this.state.setup = true;
|
||||
setState(this.state);
|
||||
respond(true);
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
this.ws.events.on('setup', ({ data }) => {
|
||||
if (data === 'crunchy') {
|
||||
this.service = new CrunchyHandler(this.ws);
|
||||
} else if (data === 'hidive') {
|
||||
this.service = new HidiveHandler(this.ws);
|
||||
} else if (data === 'ao') {
|
||||
this.service = new AnimeOnegaiHandler(this.ws);
|
||||
} else if (data === 'adn') {
|
||||
this.service = new ADNHandler(this.ws);
|
||||
}
|
||||
});
|
||||
this.ws.events.on('setup', ({ data }) => {
|
||||
if (data === 'crunchy') {
|
||||
this.service = new CrunchyHandler(this.ws);
|
||||
} else if (data === 'hidive') {
|
||||
this.service = new HidiveHandler(this.ws);
|
||||
} else if (data === 'ao') {
|
||||
this.service = new AnimeOnegaiHandler(this.ws);
|
||||
} else if (data === 'adn') {
|
||||
this.service = new ADNHandler(this.ws);
|
||||
}
|
||||
});
|
||||
|
||||
this.ws.events.on('changeProvider', async (_, respond) => {
|
||||
if (await this.service?.isDownloading())
|
||||
return respond(false);
|
||||
this.service = undefined;
|
||||
respond(true);
|
||||
});
|
||||
this.ws.events.on('changeProvider', async (_, respond) => {
|
||||
if (await this.service?.isDownloading())
|
||||
return respond(false);
|
||||
this.service = undefined;
|
||||
respond(true);
|
||||
});
|
||||
|
||||
this.ws.events.on('auth', async ({ data }, respond) => {
|
||||
if (this.service === undefined)
|
||||
return respond({ isOk: false, reason: new Error('No service selected') });
|
||||
respond(await this.service.auth(data));
|
||||
});
|
||||
this.ws.events.on('version', async (_, respond) => {
|
||||
respond(packageJson.version);
|
||||
});
|
||||
this.ws.events.on('type', async (_, respond) => respond(this.service === undefined ? undefined : this.service.name as 'hidive'|'crunchy'|'ao'|'adn'));
|
||||
this.ws.events.on('checkToken', async (_, respond) => {
|
||||
if (this.service === undefined)
|
||||
return respond({ isOk: false, reason: new Error('No service selected') });
|
||||
respond(await this.service.checkToken());
|
||||
});
|
||||
this.ws.events.on('search', async ({ data }, respond) => {
|
||||
if (this.service === undefined)
|
||||
return respond({ isOk: false, reason: new Error('No service selected') });
|
||||
respond(await this.service.search(data));
|
||||
});
|
||||
this.ws.events.on('default', async ({ data }, respond) => {
|
||||
if (this.service === undefined)
|
||||
return respond({ isOk: false, reason: new Error('No service selected') });
|
||||
respond(await this.service.handleDefault(data));
|
||||
});
|
||||
this.ws.events.on('availableDubCodes', async (_, respond) => {
|
||||
if (this.service === undefined)
|
||||
return respond([]);
|
||||
respond(await this.service.availableDubCodes());
|
||||
});
|
||||
this.ws.events.on('availableSubCodes', async (_, respond) => {
|
||||
if (this.service === undefined)
|
||||
return respond([]);
|
||||
respond(await this.service.availableSubCodes());
|
||||
});
|
||||
this.ws.events.on('resolveItems', async ({ data }, respond) => {
|
||||
if (this.service === undefined)
|
||||
return respond(false);
|
||||
respond(await this.service.resolveItems(data));
|
||||
});
|
||||
this.ws.events.on('listEpisodes', async ({ data }, respond) => {
|
||||
if (this.service === undefined)
|
||||
return respond({ isOk: false, reason: new Error('No service selected') });
|
||||
respond(await this.service.listEpisodes(data));
|
||||
});
|
||||
this.ws.events.on('downloadItem', async ({ data }, respond) => {
|
||||
this.service?.downloadItem(data);
|
||||
respond(undefined);
|
||||
});
|
||||
this.ws.events.on('openFolder', async ({ data }, respond) => {
|
||||
this.service?.openFolder(data);
|
||||
respond(undefined);
|
||||
});
|
||||
this.ws.events.on('openFile', async ({ data }, respond) => {
|
||||
this.service?.openFile(data);
|
||||
respond(undefined);
|
||||
});
|
||||
this.ws.events.on('openURL', async ({ data }, respond) => {
|
||||
this.service?.openURL(data);
|
||||
respond(undefined);
|
||||
});
|
||||
this.ws.events.on('getQueue', async (_, respond) => {
|
||||
respond(await this.service?.getQueue() ?? []);
|
||||
});
|
||||
this.ws.events.on('removeFromQueue', async ({ data }, respond) => {
|
||||
this.service?.removeFromQueue(data);
|
||||
respond(undefined);
|
||||
});
|
||||
this.ws.events.on('clearQueue', async (_, respond) => {
|
||||
this.service?.clearQueue();
|
||||
respond(undefined);
|
||||
});
|
||||
this.ws.events.on('setDownloadQueue', async ({ data }, respond) => {
|
||||
this.service?.setDownloadQueue(data);
|
||||
respond(undefined);
|
||||
});
|
||||
this.ws.events.on('getDownloadQueue', async (_, respond) => {
|
||||
respond(await this.service?.getDownloadQueue() ?? false);
|
||||
});
|
||||
this.ws.events.on('isDownloading', async (_, respond) => respond(await this.service?.isDownloading() ?? false));
|
||||
}
|
||||
this.ws.events.on('auth', async ({ data }, respond) => {
|
||||
if (this.service === undefined)
|
||||
return respond({ isOk: false, reason: new Error('No service selected') });
|
||||
respond(await this.service.auth(data));
|
||||
});
|
||||
this.ws.events.on('version', async (_, respond) => {
|
||||
respond(packageJson.version);
|
||||
});
|
||||
this.ws.events.on('type', async (_, respond) => respond(this.service === undefined ? undefined : this.service.name as 'hidive'|'crunchy'|'ao'|'adn'));
|
||||
this.ws.events.on('checkToken', async (_, respond) => {
|
||||
if (this.service === undefined)
|
||||
return respond({ isOk: false, reason: new Error('No service selected') });
|
||||
respond(await this.service.checkToken());
|
||||
});
|
||||
this.ws.events.on('search', async ({ data }, respond) => {
|
||||
if (this.service === undefined)
|
||||
return respond({ isOk: false, reason: new Error('No service selected') });
|
||||
respond(await this.service.search(data));
|
||||
});
|
||||
this.ws.events.on('default', async ({ data }, respond) => {
|
||||
if (this.service === undefined)
|
||||
return respond({ isOk: false, reason: new Error('No service selected') });
|
||||
respond(await this.service.handleDefault(data));
|
||||
});
|
||||
this.ws.events.on('availableDubCodes', async (_, respond) => {
|
||||
if (this.service === undefined)
|
||||
return respond([]);
|
||||
respond(await this.service.availableDubCodes());
|
||||
});
|
||||
this.ws.events.on('availableSubCodes', async (_, respond) => {
|
||||
if (this.service === undefined)
|
||||
return respond([]);
|
||||
respond(await this.service.availableSubCodes());
|
||||
});
|
||||
this.ws.events.on('resolveItems', async ({ data }, respond) => {
|
||||
if (this.service === undefined)
|
||||
return respond(false);
|
||||
respond(await this.service.resolveItems(data));
|
||||
});
|
||||
this.ws.events.on('listEpisodes', async ({ data }, respond) => {
|
||||
if (this.service === undefined)
|
||||
return respond({ isOk: false, reason: new Error('No service selected') });
|
||||
respond(await this.service.listEpisodes(data));
|
||||
});
|
||||
this.ws.events.on('downloadItem', async ({ data }, respond) => {
|
||||
this.service?.downloadItem(data);
|
||||
respond(undefined);
|
||||
});
|
||||
this.ws.events.on('openFolder', async ({ data }, respond) => {
|
||||
this.service?.openFolder(data);
|
||||
respond(undefined);
|
||||
});
|
||||
this.ws.events.on('openFile', async ({ data }, respond) => {
|
||||
this.service?.openFile(data);
|
||||
respond(undefined);
|
||||
});
|
||||
this.ws.events.on('openURL', async ({ data }, respond) => {
|
||||
this.service?.openURL(data);
|
||||
respond(undefined);
|
||||
});
|
||||
this.ws.events.on('getQueue', async (_, respond) => {
|
||||
respond(await this.service?.getQueue() ?? []);
|
||||
});
|
||||
this.ws.events.on('removeFromQueue', async ({ data }, respond) => {
|
||||
this.service?.removeFromQueue(data);
|
||||
respond(undefined);
|
||||
});
|
||||
this.ws.events.on('clearQueue', async (_, respond) => {
|
||||
this.service?.clearQueue();
|
||||
respond(undefined);
|
||||
});
|
||||
this.ws.events.on('setDownloadQueue', async ({ data }, respond) => {
|
||||
this.service?.setDownloadQueue(data);
|
||||
respond(undefined);
|
||||
});
|
||||
this.ws.events.on('getDownloadQueue', async (_, respond) => {
|
||||
respond(await this.service?.getDownloadQueue() ?? false);
|
||||
});
|
||||
this.ws.events.on('isDownloading', async (_, respond) => respond(await this.service?.isDownloading() ?? false));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -8,132 +8,132 @@ import { console } from '../../../modules/log';
|
|||
import * as yargs from '../../../modules/module.app-args';
|
||||
|
||||
class ADNHandler extends Base implements MessageHandler {
|
||||
private adn: AnimationDigitalNetwork;
|
||||
public name = 'adn';
|
||||
constructor(ws: WebSocketHandler) {
|
||||
super(ws);
|
||||
this.adn = new AnimationDigitalNetwork();
|
||||
this.initState();
|
||||
this.getDefaults();
|
||||
}
|
||||
private adn: AnimationDigitalNetwork;
|
||||
public name = 'adn';
|
||||
constructor(ws: WebSocketHandler) {
|
||||
super(ws);
|
||||
this.adn = new AnimationDigitalNetwork();
|
||||
this.initState();
|
||||
this.getDefaults();
|
||||
}
|
||||
|
||||
public getDefaults() {
|
||||
const _default = yargs.appArgv(this.adn.cfg.cli, true);
|
||||
if (['fr', 'de'].includes(_default.locale))
|
||||
this.adn.locale = _default.locale;
|
||||
}
|
||||
public getDefaults() {
|
||||
const _default = yargs.appArgv(this.adn.cfg.cli, true);
|
||||
if (['fr', 'de'].includes(_default.locale))
|
||||
this.adn.locale = _default.locale;
|
||||
}
|
||||
|
||||
public async auth(data: AuthData) {
|
||||
return this.adn.doAuth(data);
|
||||
}
|
||||
public async auth(data: AuthData) {
|
||||
return this.adn.doAuth(data);
|
||||
}
|
||||
|
||||
public async checkToken(): Promise<CheckTokenResponse> {
|
||||
public async checkToken(): Promise<CheckTokenResponse> {
|
||||
//TODO: implement proper method to check token
|
||||
return { isOk: true, value: undefined };
|
||||
}
|
||||
|
||||
public async search(data: SearchData): Promise<SearchResponse> {
|
||||
console.debug(`Got search options: ${JSON.stringify(data)}`);
|
||||
const search = await this.adn.doSearch(data);
|
||||
if (!search.isOk) {
|
||||
return search;
|
||||
return { isOk: true, value: undefined };
|
||||
}
|
||||
return { isOk: true, value: search.value };
|
||||
}
|
||||
|
||||
public async handleDefault(name: string) {
|
||||
return getDefault(name, this.adn.cfg.cli);
|
||||
}
|
||||
|
||||
public async availableDubCodes(): Promise<string[]> {
|
||||
const dubLanguageCodesArray: string[] = [];
|
||||
for(const language of languages){
|
||||
if (language.adn_locale)
|
||||
dubLanguageCodesArray.push(language.code);
|
||||
}
|
||||
return [...new Set(dubLanguageCodesArray)];
|
||||
}
|
||||
|
||||
public async availableSubCodes(): Promise<string[]> {
|
||||
const subLanguageCodesArray: string[] = [];
|
||||
for(const language of languages){
|
||||
if (language.adn_locale)
|
||||
subLanguageCodesArray.push(language.locale);
|
||||
}
|
||||
return ['all', 'none', ...new Set(subLanguageCodesArray)];
|
||||
}
|
||||
|
||||
public async resolveItems(data: ResolveItemsData): Promise<boolean> {
|
||||
const parse = parseInt(data.id);
|
||||
if (isNaN(parse) || parse <= 0)
|
||||
return false;
|
||||
console.debug(`Got resolve options: ${JSON.stringify(data)}`);
|
||||
const res = await this.adn.selectShow(parseInt(data.id), data.e, data.but, data.all);
|
||||
if (!res.isOk || !res.value)
|
||||
return res.isOk;
|
||||
this.addToQueue(res.value.map(a => {
|
||||
return {
|
||||
...data,
|
||||
ids: [a.id],
|
||||
title: a.title,
|
||||
parent: {
|
||||
title: a.show.shortTitle,
|
||||
season: a.season
|
||||
},
|
||||
e: a.shortNumber,
|
||||
image: a.image,
|
||||
episode: a.shortNumber
|
||||
};
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
public async listEpisodes(id: string): Promise<EpisodeListResponse> {
|
||||
const parse = parseInt(id);
|
||||
if (isNaN(parse) || parse <= 0)
|
||||
return { isOk: false, reason: new Error('The ID is invalid') };
|
||||
|
||||
const request = await this.adn.listShow(parse);
|
||||
if (!request.isOk || !request.value)
|
||||
return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')};
|
||||
|
||||
return { isOk: true, value: request.value.videos.map(function(item) {
|
||||
return {
|
||||
e: item.shortNumber,
|
||||
lang: [],
|
||||
name: item.title,
|
||||
season: item.season,
|
||||
seasonTitle: item.show.title,
|
||||
episode: item.shortNumber,
|
||||
id: item.id+'',
|
||||
img: item.image,
|
||||
description: item.summary,
|
||||
time: item.duration+''
|
||||
};
|
||||
})};
|
||||
}
|
||||
|
||||
public async downloadItem(data: DownloadData) {
|
||||
this.setDownloading(true);
|
||||
console.debug(`Got download options: ${JSON.stringify(data)}`);
|
||||
const _default = yargs.appArgv(this.adn.cfg.cli, true);
|
||||
const res = await this.adn.selectShow(parseInt(data.id), data.e, false, false);
|
||||
if (res.isOk) {
|
||||
for (const select of res.value) {
|
||||
if (!(await this.adn.getEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y',
|
||||
novids: data.novids, noaudio: data.noaudio, hslang: data.hslang || 'none', dubLang: data.dubLang }))) {
|
||||
const er = new Error(`Unable to download episode ${data.e} from ${data.id}`);
|
||||
er.name = 'Download error';
|
||||
this.alertError(er);
|
||||
public async search(data: SearchData): Promise<SearchResponse> {
|
||||
console.debug(`Got search options: ${JSON.stringify(data)}`);
|
||||
const search = await this.adn.doSearch(data);
|
||||
if (!search.isOk) {
|
||||
return search;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.alertError(new Error('Failed to download episode, check for additional logs.'));
|
||||
return { isOk: true, value: search.value };
|
||||
}
|
||||
|
||||
public async handleDefault(name: string) {
|
||||
return getDefault(name, this.adn.cfg.cli);
|
||||
}
|
||||
|
||||
public async availableDubCodes(): Promise<string[]> {
|
||||
const dubLanguageCodesArray: string[] = [];
|
||||
for(const language of languages){
|
||||
if (language.adn_locale)
|
||||
dubLanguageCodesArray.push(language.code);
|
||||
}
|
||||
return [...new Set(dubLanguageCodesArray)];
|
||||
}
|
||||
|
||||
public async availableSubCodes(): Promise<string[]> {
|
||||
const subLanguageCodesArray: string[] = [];
|
||||
for(const language of languages){
|
||||
if (language.adn_locale)
|
||||
subLanguageCodesArray.push(language.locale);
|
||||
}
|
||||
return ['all', 'none', ...new Set(subLanguageCodesArray)];
|
||||
}
|
||||
|
||||
public async resolveItems(data: ResolveItemsData): Promise<boolean> {
|
||||
const parse = parseInt(data.id);
|
||||
if (isNaN(parse) || parse <= 0)
|
||||
return false;
|
||||
console.debug(`Got resolve options: ${JSON.stringify(data)}`);
|
||||
const res = await this.adn.selectShow(parseInt(data.id), data.e, data.but, data.all);
|
||||
if (!res.isOk || !res.value)
|
||||
return res.isOk;
|
||||
this.addToQueue(res.value.map(a => {
|
||||
return {
|
||||
...data,
|
||||
ids: [a.id],
|
||||
title: a.title,
|
||||
parent: {
|
||||
title: a.show.shortTitle,
|
||||
season: a.season
|
||||
},
|
||||
e: a.shortNumber,
|
||||
image: a.image,
|
||||
episode: a.shortNumber
|
||||
};
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
public async listEpisodes(id: string): Promise<EpisodeListResponse> {
|
||||
const parse = parseInt(id);
|
||||
if (isNaN(parse) || parse <= 0)
|
||||
return { isOk: false, reason: new Error('The ID is invalid') };
|
||||
|
||||
const request = await this.adn.listShow(parse);
|
||||
if (!request.isOk || !request.value)
|
||||
return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')};
|
||||
|
||||
return { isOk: true, value: request.value.videos.map(function(item) {
|
||||
return {
|
||||
e: item.shortNumber,
|
||||
lang: [],
|
||||
name: item.title,
|
||||
season: item.season,
|
||||
seasonTitle: item.show.title,
|
||||
episode: item.shortNumber,
|
||||
id: item.id+'',
|
||||
img: item.image,
|
||||
description: item.summary,
|
||||
time: item.duration+''
|
||||
};
|
||||
})};
|
||||
}
|
||||
|
||||
public async downloadItem(data: DownloadData) {
|
||||
this.setDownloading(true);
|
||||
console.debug(`Got download options: ${JSON.stringify(data)}`);
|
||||
const _default = yargs.appArgv(this.adn.cfg.cli, true);
|
||||
const res = await this.adn.selectShow(parseInt(data.id), data.e, false, false);
|
||||
if (res.isOk) {
|
||||
for (const select of res.value) {
|
||||
if (!(await this.adn.getEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y',
|
||||
novids: data.novids, noaudio: data.noaudio, hslang: data.hslang || 'none', dubLang: data.dubLang }))) {
|
||||
const er = new Error(`Unable to download episode ${data.e} from ${data.id}`);
|
||||
er.name = 'Download error';
|
||||
this.alertError(er);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.alertError(new Error('Failed to download episode, check for additional logs.'));
|
||||
}
|
||||
this.sendMessage({ name: 'finish', data: undefined });
|
||||
this.setDownloading(false);
|
||||
this.onFinish();
|
||||
}
|
||||
this.sendMessage({ name: 'finish', data: undefined });
|
||||
this.setDownloading(false);
|
||||
this.onFinish();
|
||||
}
|
||||
}
|
||||
|
||||
export default ADNHandler;
|
||||
|
|
@ -8,144 +8,144 @@ import { console } from '../../../modules/log';
|
|||
import * as yargs from '../../../modules/module.app-args';
|
||||
|
||||
class AnimeOnegaiHandler extends Base implements MessageHandler {
|
||||
private ao: AnimeOnegai;
|
||||
public name = 'ao';
|
||||
constructor(ws: WebSocketHandler) {
|
||||
super(ws);
|
||||
this.ao = new AnimeOnegai();
|
||||
this.initState();
|
||||
this.getDefaults();
|
||||
}
|
||||
private ao: AnimeOnegai;
|
||||
public name = 'ao';
|
||||
constructor(ws: WebSocketHandler) {
|
||||
super(ws);
|
||||
this.ao = new AnimeOnegai();
|
||||
this.initState();
|
||||
this.getDefaults();
|
||||
}
|
||||
|
||||
public getDefaults() {
|
||||
const _default = yargs.appArgv(this.ao.cfg.cli, true);
|
||||
if (['es', 'pt'].includes(_default.locale))
|
||||
this.ao.locale = _default.locale;
|
||||
}
|
||||
public getDefaults() {
|
||||
const _default = yargs.appArgv(this.ao.cfg.cli, true);
|
||||
if (['es', 'pt'].includes(_default.locale))
|
||||
this.ao.locale = _default.locale;
|
||||
}
|
||||
|
||||
public async auth(data: AuthData) {
|
||||
return this.ao.doAuth(data);
|
||||
}
|
||||
public async auth(data: AuthData) {
|
||||
return this.ao.doAuth(data);
|
||||
}
|
||||
|
||||
public async checkToken(): Promise<CheckTokenResponse> {
|
||||
public async checkToken(): Promise<CheckTokenResponse> {
|
||||
//TODO: implement proper method to check token
|
||||
return { isOk: true, value: undefined };
|
||||
}
|
||||
|
||||
public async search(data: SearchData): Promise<SearchResponse> {
|
||||
console.debug(`Got search options: ${JSON.stringify(data)}`);
|
||||
const search = await this.ao.doSearch(data);
|
||||
if (!search.isOk) {
|
||||
return search;
|
||||
return { isOk: true, value: undefined };
|
||||
}
|
||||
return { isOk: true, value: search.value };
|
||||
}
|
||||
|
||||
public async handleDefault(name: string) {
|
||||
return getDefault(name, this.ao.cfg.cli);
|
||||
}
|
||||
|
||||
public async availableDubCodes(): Promise<string[]> {
|
||||
const dubLanguageCodesArray: string[] = [];
|
||||
for(const language of languages){
|
||||
if (language.ao_locale)
|
||||
dubLanguageCodesArray.push(language.code);
|
||||
}
|
||||
return [...new Set(dubLanguageCodesArray)];
|
||||
}
|
||||
|
||||
public async availableSubCodes(): Promise<string[]> {
|
||||
const subLanguageCodesArray: string[] = [];
|
||||
for(const language of languages){
|
||||
if (language.ao_locale)
|
||||
subLanguageCodesArray.push(language.locale);
|
||||
}
|
||||
return ['all', 'none', ...new Set(subLanguageCodesArray)];
|
||||
}
|
||||
|
||||
public async resolveItems(data: ResolveItemsData): Promise<boolean> {
|
||||
const parse = parseInt(data.id);
|
||||
if (isNaN(parse) || parse <= 0)
|
||||
return false;
|
||||
console.debug(`Got resolve options: ${JSON.stringify(data)}`);
|
||||
const _default = yargs.appArgv(this.ao.cfg.cli, true);
|
||||
const res = await this.ao.selectShow(parseInt(data.id), data.e, data.but, data.all, _default);
|
||||
if (!res.isOk || !res.value)
|
||||
return res.isOk;
|
||||
this.addToQueue(res.value.map(a => {
|
||||
return {
|
||||
...data,
|
||||
ids: a.data.map(a => a.videoId),
|
||||
title: a.episodeTitle,
|
||||
parent: {
|
||||
title: a.seasonTitle,
|
||||
season: a.seasonTitle
|
||||
},
|
||||
e: a.episodeNumber+'',
|
||||
image: a.image,
|
||||
episode: a.episodeNumber+''
|
||||
};
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
public async listEpisodes(id: string): Promise<EpisodeListResponse> {
|
||||
const parse = parseInt(id);
|
||||
if (isNaN(parse) || parse <= 0)
|
||||
return { isOk: false, reason: new Error('The ID is invalid') };
|
||||
|
||||
const request = await this.ao.listShow(parse);
|
||||
if (!request.isOk || !request.value)
|
||||
return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')};
|
||||
|
||||
const episodes: Episode[] = [];
|
||||
const seasonNumberTitleParse = request.series.data.title.match(/\d+$/);
|
||||
const seasonNumber = seasonNumberTitleParse ? parseInt(seasonNumberTitleParse[0]) : 1;
|
||||
//request.value
|
||||
for (const episodeKey in request.value) {
|
||||
const episode = request.value[episodeKey][0];
|
||||
const langs = Array.from(new Set(request.value[episodeKey].map(a=>a.lang)));
|
||||
episodes.push({
|
||||
e: episode.number+'',
|
||||
lang: langs as string[],
|
||||
name: episode.name,
|
||||
season: seasonNumber+'',
|
||||
seasonTitle: '',
|
||||
episode: episode.number+'',
|
||||
id: episode.video_entry+'',
|
||||
img: episode.thumbnail,
|
||||
description: episode.description,
|
||||
time: ''
|
||||
});
|
||||
}
|
||||
return { isOk: true, value: episodes };
|
||||
}
|
||||
|
||||
public async downloadItem(data: DownloadData) {
|
||||
this.setDownloading(true);
|
||||
console.debug(`Got download options: ${JSON.stringify(data)}`);
|
||||
const _default = yargs.appArgv(this.ao.cfg.cli, true);
|
||||
const res = await this.ao.selectShow(parseInt(data.id), data.e, false, false, {
|
||||
..._default,
|
||||
dubLang: data.dubLang,
|
||||
e: data.e
|
||||
});
|
||||
if (res.isOk) {
|
||||
for (const select of res.value) {
|
||||
if (!(await this.ao.downloadEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y',
|
||||
novids: data.novids, noaudio: data.noaudio, hslang: data.hslang || 'none', dubLang: data.dubLang }))) {
|
||||
const er = new Error(`Unable to download episode ${data.e} from ${data.id}`);
|
||||
er.name = 'Download error';
|
||||
this.alertError(er);
|
||||
public async search(data: SearchData): Promise<SearchResponse> {
|
||||
console.debug(`Got search options: ${JSON.stringify(data)}`);
|
||||
const search = await this.ao.doSearch(data);
|
||||
if (!search.isOk) {
|
||||
return search;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.alertError(new Error('Failed to download episode, check for additional logs.'));
|
||||
return { isOk: true, value: search.value };
|
||||
}
|
||||
|
||||
public async handleDefault(name: string) {
|
||||
return getDefault(name, this.ao.cfg.cli);
|
||||
}
|
||||
|
||||
public async availableDubCodes(): Promise<string[]> {
|
||||
const dubLanguageCodesArray: string[] = [];
|
||||
for(const language of languages){
|
||||
if (language.ao_locale)
|
||||
dubLanguageCodesArray.push(language.code);
|
||||
}
|
||||
return [...new Set(dubLanguageCodesArray)];
|
||||
}
|
||||
|
||||
public async availableSubCodes(): Promise<string[]> {
|
||||
const subLanguageCodesArray: string[] = [];
|
||||
for(const language of languages){
|
||||
if (language.ao_locale)
|
||||
subLanguageCodesArray.push(language.locale);
|
||||
}
|
||||
return ['all', 'none', ...new Set(subLanguageCodesArray)];
|
||||
}
|
||||
|
||||
public async resolveItems(data: ResolveItemsData): Promise<boolean> {
|
||||
const parse = parseInt(data.id);
|
||||
if (isNaN(parse) || parse <= 0)
|
||||
return false;
|
||||
console.debug(`Got resolve options: ${JSON.stringify(data)}`);
|
||||
const _default = yargs.appArgv(this.ao.cfg.cli, true);
|
||||
const res = await this.ao.selectShow(parseInt(data.id), data.e, data.but, data.all, _default);
|
||||
if (!res.isOk || !res.value)
|
||||
return res.isOk;
|
||||
this.addToQueue(res.value.map(a => {
|
||||
return {
|
||||
...data,
|
||||
ids: a.data.map(a => a.videoId),
|
||||
title: a.episodeTitle,
|
||||
parent: {
|
||||
title: a.seasonTitle,
|
||||
season: a.seasonTitle
|
||||
},
|
||||
e: a.episodeNumber+'',
|
||||
image: a.image,
|
||||
episode: a.episodeNumber+''
|
||||
};
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
public async listEpisodes(id: string): Promise<EpisodeListResponse> {
|
||||
const parse = parseInt(id);
|
||||
if (isNaN(parse) || parse <= 0)
|
||||
return { isOk: false, reason: new Error('The ID is invalid') };
|
||||
|
||||
const request = await this.ao.listShow(parse);
|
||||
if (!request.isOk || !request.value)
|
||||
return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')};
|
||||
|
||||
const episodes: Episode[] = [];
|
||||
const seasonNumberTitleParse = request.series.data.title.match(/\d+$/);
|
||||
const seasonNumber = seasonNumberTitleParse ? parseInt(seasonNumberTitleParse[0]) : 1;
|
||||
//request.value
|
||||
for (const episodeKey in request.value) {
|
||||
const episode = request.value[episodeKey][0];
|
||||
const langs = Array.from(new Set(request.value[episodeKey].map(a=>a.lang)));
|
||||
episodes.push({
|
||||
e: episode.number+'',
|
||||
lang: langs as string[],
|
||||
name: episode.name,
|
||||
season: seasonNumber+'',
|
||||
seasonTitle: '',
|
||||
episode: episode.number+'',
|
||||
id: episode.video_entry+'',
|
||||
img: episode.thumbnail,
|
||||
description: episode.description,
|
||||
time: ''
|
||||
});
|
||||
}
|
||||
return { isOk: true, value: episodes };
|
||||
}
|
||||
|
||||
public async downloadItem(data: DownloadData) {
|
||||
this.setDownloading(true);
|
||||
console.debug(`Got download options: ${JSON.stringify(data)}`);
|
||||
const _default = yargs.appArgv(this.ao.cfg.cli, true);
|
||||
const res = await this.ao.selectShow(parseInt(data.id), data.e, false, false, {
|
||||
..._default,
|
||||
dubLang: data.dubLang,
|
||||
e: data.e
|
||||
});
|
||||
if (res.isOk) {
|
||||
for (const select of res.value) {
|
||||
if (!(await this.ao.downloadEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y',
|
||||
novids: data.novids, noaudio: data.noaudio, hslang: data.hslang || 'none', dubLang: data.dubLang }))) {
|
||||
const er = new Error(`Unable to download episode ${data.e} from ${data.id}`);
|
||||
er.name = 'Download error';
|
||||
this.alertError(er);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.alertError(new Error('Failed to download episode, check for additional logs.'));
|
||||
}
|
||||
this.sendMessage({ name: 'finish', data: undefined });
|
||||
this.setDownloading(false);
|
||||
this.onFinish();
|
||||
}
|
||||
this.sendMessage({ name: 'finish', data: undefined });
|
||||
this.setDownloading(false);
|
||||
this.onFinish();
|
||||
}
|
||||
}
|
||||
|
||||
export default AnimeOnegaiHandler;
|
||||
|
|
@ -9,140 +9,140 @@ import { getState, setState } from '../../../modules/module.cfg-loader';
|
|||
import packageJson from '../../../package.json';
|
||||
|
||||
export default class Base {
|
||||
private state: GuiState;
|
||||
public name = 'default';
|
||||
constructor(private ws: WebSocketHandler) {
|
||||
this.state = getState();
|
||||
}
|
||||
|
||||
private downloading = false;
|
||||
|
||||
private queue: QueueItem[] = [];
|
||||
private workOnQueue = false;
|
||||
|
||||
version(): Promise<string> {
|
||||
return new Promise(() => {
|
||||
return packageJson.version;
|
||||
});
|
||||
}
|
||||
|
||||
initState() {
|
||||
if (this.state.services[this.name]) {
|
||||
this.queue = this.state.services[this.name].queue;
|
||||
this.queueChange();
|
||||
} else {
|
||||
this.state.services[this.name] = {
|
||||
'queue': []
|
||||
};
|
||||
private state: GuiState;
|
||||
public name = 'default';
|
||||
constructor(private ws: WebSocketHandler) {
|
||||
this.state = getState();
|
||||
}
|
||||
}
|
||||
|
||||
setDownloading(downloading: boolean) {
|
||||
this.downloading = downloading;
|
||||
}
|
||||
private downloading = false;
|
||||
|
||||
getDownloading() {
|
||||
return this.downloading;
|
||||
}
|
||||
private queue: QueueItem[] = [];
|
||||
private workOnQueue = false;
|
||||
|
||||
alertError(error: Error) {
|
||||
console.error(`${error}`);
|
||||
}
|
||||
version(): Promise<string> {
|
||||
return new Promise(() => {
|
||||
return packageJson.version;
|
||||
});
|
||||
}
|
||||
|
||||
makeProgressHandler(videoInfo: DownloadInfo) {
|
||||
return ((data: ProgressData) => {
|
||||
this.sendMessage({
|
||||
name: 'progress',
|
||||
data: {
|
||||
downloadInfo: videoInfo,
|
||||
progress: data
|
||||
initState() {
|
||||
if (this.state.services[this.name]) {
|
||||
this.queue = this.state.services[this.name].queue;
|
||||
this.queueChange();
|
||||
} else {
|
||||
this.state.services[this.name] = {
|
||||
'queue': []
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
sendMessage<T extends keyof RandomEvents>(data: RandomEvent<T>) {
|
||||
this.ws.sendMessage(data);
|
||||
}
|
||||
|
||||
async isDownloading() {
|
||||
return this.downloading;
|
||||
}
|
||||
|
||||
async openFolder(folderType: FolderTypes) {
|
||||
switch (folderType) {
|
||||
case 'content':
|
||||
open(cfg.dir.content);
|
||||
break;
|
||||
case 'config':
|
||||
open(cfg.dir.config);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async openFile(data: [FolderTypes, string]) {
|
||||
switch (data[0]) {
|
||||
case 'config':
|
||||
open(path.join(cfg.dir.config, data[1]));
|
||||
break;
|
||||
case 'content':
|
||||
throw new Error('No subfolders');
|
||||
setDownloading(downloading: boolean) {
|
||||
this.downloading = downloading;
|
||||
}
|
||||
}
|
||||
|
||||
async openURL(data: string) {
|
||||
open(data);
|
||||
}
|
||||
|
||||
public async getQueue(): Promise<QueueItem[]> {
|
||||
return this.queue;
|
||||
}
|
||||
|
||||
public async removeFromQueue(index: number) {
|
||||
this.queue.splice(index, 1);
|
||||
this.queueChange();
|
||||
}
|
||||
|
||||
public async clearQueue() {
|
||||
this.queue = [];
|
||||
this.queueChange();
|
||||
}
|
||||
|
||||
public addToQueue(data: QueueItem[]) {
|
||||
this.queue = this.queue.concat(...data);
|
||||
this.queueChange();
|
||||
}
|
||||
|
||||
public setDownloadQueue(data: boolean) {
|
||||
this.workOnQueue = data;
|
||||
this.queueChange();
|
||||
}
|
||||
|
||||
public async getDownloadQueue(): Promise<boolean> {
|
||||
return this.workOnQueue;
|
||||
}
|
||||
|
||||
private async queueChange() {
|
||||
this.sendMessage({ name: 'queueChange', data: this.queue });
|
||||
if (this.workOnQueue && this.queue.length > 0 && !await this.isDownloading()) {
|
||||
this.setDownloading(true);
|
||||
this.sendMessage({ name: 'current', data: this.queue[0] });
|
||||
this.downloadItem(this.queue[0]);
|
||||
this.queue = this.queue.slice(1);
|
||||
this.queueChange();
|
||||
getDownloading() {
|
||||
return this.downloading;
|
||||
}
|
||||
this.state.services[this.name].queue = this.queue;
|
||||
setState(this.state);
|
||||
}
|
||||
|
||||
public async onFinish() {
|
||||
this.sendMessage({ name: 'current', data: undefined });
|
||||
this.queueChange();
|
||||
}
|
||||
alertError(error: Error) {
|
||||
console.error(`${error}`);
|
||||
}
|
||||
|
||||
//Overriten
|
||||
// eslint-disable-next-line
|
||||
makeProgressHandler(videoInfo: DownloadInfo) {
|
||||
return ((data: ProgressData) => {
|
||||
this.sendMessage({
|
||||
name: 'progress',
|
||||
data: {
|
||||
downloadInfo: videoInfo,
|
||||
progress: data
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
sendMessage<T extends keyof RandomEvents>(data: RandomEvent<T>) {
|
||||
this.ws.sendMessage(data);
|
||||
}
|
||||
|
||||
async isDownloading() {
|
||||
return this.downloading;
|
||||
}
|
||||
|
||||
async openFolder(folderType: FolderTypes) {
|
||||
switch (folderType) {
|
||||
case 'content':
|
||||
open(cfg.dir.content);
|
||||
break;
|
||||
case 'config':
|
||||
open(cfg.dir.config);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async openFile(data: [FolderTypes, string]) {
|
||||
switch (data[0]) {
|
||||
case 'config':
|
||||
open(path.join(cfg.dir.config, data[1]));
|
||||
break;
|
||||
case 'content':
|
||||
throw new Error('No subfolders');
|
||||
}
|
||||
}
|
||||
|
||||
async openURL(data: string) {
|
||||
open(data);
|
||||
}
|
||||
|
||||
public async getQueue(): Promise<QueueItem[]> {
|
||||
return this.queue;
|
||||
}
|
||||
|
||||
public async removeFromQueue(index: number) {
|
||||
this.queue.splice(index, 1);
|
||||
this.queueChange();
|
||||
}
|
||||
|
||||
public async clearQueue() {
|
||||
this.queue = [];
|
||||
this.queueChange();
|
||||
}
|
||||
|
||||
public addToQueue(data: QueueItem[]) {
|
||||
this.queue = this.queue.concat(...data);
|
||||
this.queueChange();
|
||||
}
|
||||
|
||||
public setDownloadQueue(data: boolean) {
|
||||
this.workOnQueue = data;
|
||||
this.queueChange();
|
||||
}
|
||||
|
||||
public async getDownloadQueue(): Promise<boolean> {
|
||||
return this.workOnQueue;
|
||||
}
|
||||
|
||||
private async queueChange() {
|
||||
this.sendMessage({ name: 'queueChange', data: this.queue });
|
||||
if (this.workOnQueue && this.queue.length > 0 && !await this.isDownloading()) {
|
||||
this.setDownloading(true);
|
||||
this.sendMessage({ name: 'current', data: this.queue[0] });
|
||||
this.downloadItem(this.queue[0]);
|
||||
this.queue = this.queue.slice(1);
|
||||
this.queueChange();
|
||||
}
|
||||
this.state.services[this.name].queue = this.queue;
|
||||
setState(this.state);
|
||||
}
|
||||
|
||||
public async onFinish() {
|
||||
this.sendMessage({ name: 'current', data: undefined });
|
||||
this.queueChange();
|
||||
}
|
||||
|
||||
//Overriten
|
||||
// eslint-disable-next-line
|
||||
public async downloadItem(_: QueueItem) {
|
||||
throw new Error('downloadItem not overriden');
|
||||
}
|
||||
throw new Error('downloadItem not overriden');
|
||||
}
|
||||
}
|
||||
|
|
@ -8,120 +8,120 @@ import { console } from '../../../modules/log';
|
|||
import * as yargs from '../../../modules/module.app-args';
|
||||
|
||||
class CrunchyHandler extends Base implements MessageHandler {
|
||||
private crunchy: Crunchy;
|
||||
public name = 'crunchy';
|
||||
constructor(ws: WebSocketHandler) {
|
||||
super(ws);
|
||||
this.crunchy = new Crunchy();
|
||||
this.crunchy.refreshToken();
|
||||
this.initState();
|
||||
this.getDefaults();
|
||||
}
|
||||
private crunchy: Crunchy;
|
||||
public name = 'crunchy';
|
||||
constructor(ws: WebSocketHandler) {
|
||||
super(ws);
|
||||
this.crunchy = new Crunchy();
|
||||
this.crunchy.refreshToken();
|
||||
this.initState();
|
||||
this.getDefaults();
|
||||
}
|
||||
|
||||
public getDefaults() {
|
||||
const _default = yargs.appArgv(this.crunchy.cfg.cli, true);
|
||||
this.crunchy.locale = _default.locale;
|
||||
}
|
||||
public getDefaults() {
|
||||
const _default = yargs.appArgv(this.crunchy.cfg.cli, true);
|
||||
this.crunchy.locale = _default.locale;
|
||||
}
|
||||
|
||||
public async listEpisodes (id: string): Promise<EpisodeListResponse> {
|
||||
this.getDefaults();
|
||||
await this.crunchy.refreshToken(true);
|
||||
return { isOk: true, value: (await this.crunchy.listSeriesID(id)).list };
|
||||
}
|
||||
public async listEpisodes (id: string): Promise<EpisodeListResponse> {
|
||||
this.getDefaults();
|
||||
await this.crunchy.refreshToken(true);
|
||||
return { isOk: true, value: (await this.crunchy.listSeriesID(id)).list };
|
||||
}
|
||||
|
||||
public async handleDefault(name: string) {
|
||||
return getDefault(name, this.crunchy.cfg.cli);
|
||||
}
|
||||
|
||||
public async availableDubCodes(): Promise<string[]> {
|
||||
const dubLanguageCodesArray: string[] = [];
|
||||
for(const language of languages){
|
||||
if (language.cr_locale)
|
||||
dubLanguageCodesArray.push(language.code);
|
||||
public async handleDefault(name: string) {
|
||||
return getDefault(name, this.crunchy.cfg.cli);
|
||||
}
|
||||
return [...new Set(dubLanguageCodesArray)];
|
||||
}
|
||||
|
||||
public async availableSubCodes(): Promise<string[]> {
|
||||
return subtitleLanguagesFilter;
|
||||
}
|
||||
|
||||
public async resolveItems(data: ResolveItemsData): Promise<boolean> {
|
||||
this.getDefaults();
|
||||
await this.crunchy.refreshToken(true);
|
||||
console.debug(`Got resolve options: ${JSON.stringify(data)}`);
|
||||
const res = await this.crunchy.downloadFromSeriesID(data.id, data);
|
||||
if (!res.isOk)
|
||||
return res.isOk;
|
||||
this.addToQueue(res.value.map(a => {
|
||||
return {
|
||||
...data,
|
||||
|
||||
ids: a.data.map(a => a.mediaId),
|
||||
title: a.episodeTitle,
|
||||
parent: {
|
||||
title: a.seasonTitle,
|
||||
season: a.season.toString()
|
||||
},
|
||||
e: a.e,
|
||||
image: a.image,
|
||||
episode: a.episodeNumber
|
||||
};
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
public async search(data: SearchData): Promise<SearchResponse> {
|
||||
this.getDefaults();
|
||||
await this.crunchy.refreshToken(true);
|
||||
if (!data['search-type']) data['search-type'] = 'series';
|
||||
console.debug(`Got search options: ${JSON.stringify(data)}`);
|
||||
const crunchySearch = await this.crunchy.doSearch(data);
|
||||
if (!crunchySearch.isOk) {
|
||||
this.crunchy.refreshToken();
|
||||
return crunchySearch;
|
||||
}
|
||||
return { isOk: true, value: crunchySearch.value };
|
||||
}
|
||||
|
||||
public async checkToken(): Promise<CheckTokenResponse> {
|
||||
if (await this.crunchy.getProfile()) {
|
||||
return { isOk: true, value: undefined };
|
||||
} else {
|
||||
return { isOk: false, reason: new Error('') };
|
||||
}
|
||||
}
|
||||
|
||||
public auth(data: AuthData) {
|
||||
return this.crunchy.doAuth(data);
|
||||
}
|
||||
|
||||
public async downloadItem(data: DownloadData) {
|
||||
this.getDefaults();
|
||||
await this.crunchy.refreshToken(true);
|
||||
console.debug(`Got download options: ${JSON.stringify(data)}`);
|
||||
this.setDownloading(true);
|
||||
const _default = yargs.appArgv(this.crunchy.cfg.cli, true);
|
||||
const res = await this.crunchy.downloadFromSeriesID(data.id, {
|
||||
dubLang: data.dubLang,
|
||||
e: data.e
|
||||
});
|
||||
if (res.isOk) {
|
||||
for (const select of res.value) {
|
||||
if (!(await this.crunchy.downloadEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y',
|
||||
novids: data.novids, noaudio: data.noaudio, hslang: data.hslang || 'none' }))) {
|
||||
const er = new Error(`Unable to download episode ${data.e} from ${data.id}`);
|
||||
er.name = 'Download error';
|
||||
this.alertError(er);
|
||||
public async availableDubCodes(): Promise<string[]> {
|
||||
const dubLanguageCodesArray: string[] = [];
|
||||
for(const language of languages){
|
||||
if (language.cr_locale)
|
||||
dubLanguageCodesArray.push(language.code);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.alertError(res.reason);
|
||||
return [...new Set(dubLanguageCodesArray)];
|
||||
}
|
||||
|
||||
public async availableSubCodes(): Promise<string[]> {
|
||||
return subtitleLanguagesFilter;
|
||||
}
|
||||
|
||||
public async resolveItems(data: ResolveItemsData): Promise<boolean> {
|
||||
this.getDefaults();
|
||||
await this.crunchy.refreshToken(true);
|
||||
console.debug(`Got resolve options: ${JSON.stringify(data)}`);
|
||||
const res = await this.crunchy.downloadFromSeriesID(data.id, data);
|
||||
if (!res.isOk)
|
||||
return res.isOk;
|
||||
this.addToQueue(res.value.map(a => {
|
||||
return {
|
||||
...data,
|
||||
|
||||
ids: a.data.map(a => a.mediaId),
|
||||
title: a.episodeTitle,
|
||||
parent: {
|
||||
title: a.seasonTitle,
|
||||
season: a.season.toString()
|
||||
},
|
||||
e: a.e,
|
||||
image: a.image,
|
||||
episode: a.episodeNumber
|
||||
};
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
public async search(data: SearchData): Promise<SearchResponse> {
|
||||
this.getDefaults();
|
||||
await this.crunchy.refreshToken(true);
|
||||
if (!data['search-type']) data['search-type'] = 'series';
|
||||
console.debug(`Got search options: ${JSON.stringify(data)}`);
|
||||
const crunchySearch = await this.crunchy.doSearch(data);
|
||||
if (!crunchySearch.isOk) {
|
||||
this.crunchy.refreshToken();
|
||||
return crunchySearch;
|
||||
}
|
||||
return { isOk: true, value: crunchySearch.value };
|
||||
}
|
||||
|
||||
public async checkToken(): Promise<CheckTokenResponse> {
|
||||
if (await this.crunchy.getProfile()) {
|
||||
return { isOk: true, value: undefined };
|
||||
} else {
|
||||
return { isOk: false, reason: new Error('') };
|
||||
}
|
||||
}
|
||||
|
||||
public auth(data: AuthData) {
|
||||
return this.crunchy.doAuth(data);
|
||||
}
|
||||
|
||||
public async downloadItem(data: DownloadData) {
|
||||
this.getDefaults();
|
||||
await this.crunchy.refreshToken(true);
|
||||
console.debug(`Got download options: ${JSON.stringify(data)}`);
|
||||
this.setDownloading(true);
|
||||
const _default = yargs.appArgv(this.crunchy.cfg.cli, true);
|
||||
const res = await this.crunchy.downloadFromSeriesID(data.id, {
|
||||
dubLang: data.dubLang,
|
||||
e: data.e
|
||||
});
|
||||
if (res.isOk) {
|
||||
for (const select of res.value) {
|
||||
if (!(await this.crunchy.downloadEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y',
|
||||
novids: data.novids, noaudio: data.noaudio, hslang: data.hslang || 'none' }))) {
|
||||
const er = new Error(`Unable to download episode ${data.e} from ${data.id}`);
|
||||
er.name = 'Download error';
|
||||
this.alertError(er);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.alertError(res.reason);
|
||||
}
|
||||
this.sendMessage({ name: 'finish', data: undefined });
|
||||
this.setDownloading(false);
|
||||
this.onFinish();
|
||||
}
|
||||
this.sendMessage({ name: 'finish', data: undefined });
|
||||
this.setDownloading(false);
|
||||
this.onFinish();
|
||||
}
|
||||
}
|
||||
|
||||
export default CrunchyHandler;
|
||||
|
|
@ -8,120 +8,120 @@ import { console } from '../../../modules/log';
|
|||
import * as yargs from '../../../modules/module.app-args';
|
||||
|
||||
class HidiveHandler extends Base implements MessageHandler {
|
||||
private hidive: Hidive;
|
||||
public name = 'hidive';
|
||||
constructor(ws: WebSocketHandler) {
|
||||
super(ws);
|
||||
this.hidive = new Hidive();
|
||||
this.initState();
|
||||
}
|
||||
private hidive: Hidive;
|
||||
public name = 'hidive';
|
||||
constructor(ws: WebSocketHandler) {
|
||||
super(ws);
|
||||
this.hidive = new Hidive();
|
||||
this.initState();
|
||||
}
|
||||
|
||||
public async auth(data: AuthData) {
|
||||
return this.hidive.doAuth(data);
|
||||
}
|
||||
public async auth(data: AuthData) {
|
||||
return this.hidive.doAuth(data);
|
||||
}
|
||||
|
||||
public async checkToken(): Promise<CheckTokenResponse> {
|
||||
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: undefined };
|
||||
}
|
||||
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.new_hd_locale)
|
||||
dubLanguageCodesArray.push(language.code);
|
||||
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 };
|
||||
}
|
||||
return [...new Set(dubLanguageCodesArray)];
|
||||
}
|
||||
|
||||
public async availableSubCodes(): Promise<string[]> {
|
||||
const subLanguageCodesArray: string[] = [];
|
||||
for(const language of languages){
|
||||
if (language.new_hd_locale)
|
||||
subLanguageCodesArray.push(language.locale);
|
||||
public async handleDefault(name: string) {
|
||||
return getDefault(name, this.hidive.cfg.cli);
|
||||
}
|
||||
return ['all', 'none', ...new Set(subLanguageCodesArray)];
|
||||
}
|
||||
|
||||
public async resolveItems(data: ResolveItemsData): Promise<boolean> {
|
||||
const parse = parseInt(data.id);
|
||||
if (isNaN(parse) || parse <= 0)
|
||||
return false;
|
||||
console.debug(`Got resolve options: ${JSON.stringify(data)}`);
|
||||
const res = await this.hidive.selectSeries(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.title,
|
||||
parent: {
|
||||
title: item.seriesTitle,
|
||||
season: item.episodeInformation.seasonNumber+''
|
||||
},
|
||||
image: item.thumbnailUrl,
|
||||
e: item.episodeInformation.episodeNumber+'',
|
||||
episode: item.episodeInformation.episodeNumber+'',
|
||||
};
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
public async listEpisodes(id: string): Promise<EpisodeListResponse> {
|
||||
const parse = parseInt(id);
|
||||
if (isNaN(parse) || parse <= 0)
|
||||
return { isOk: false, reason: new Error('The ID is invalid') };
|
||||
|
||||
const request = await this.hidive.listSeries(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.map(function(item) {
|
||||
const description = item.description.split('\r\n');
|
||||
return {
|
||||
e: item.episodeInformation.episodeNumber+'',
|
||||
lang: [],
|
||||
name: item.title,
|
||||
season: item.episodeInformation.seasonNumber+'',
|
||||
seasonTitle: request.series.seasons[item.episodeInformation.seasonNumber-1]?.title ?? request.series.title,
|
||||
episode: item.episodeInformation.episodeNumber+'',
|
||||
id: item.id+'',
|
||||
img: item.thumbnailUrl,
|
||||
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.selectSeries(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.downloadEpisode(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 });
|
||||
public async availableDubCodes(): Promise<string[]> {
|
||||
const dubLanguageCodesArray: string[] = [];
|
||||
for(const language of languages){
|
||||
if (language.new_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.new_hd_locale)
|
||||
subLanguageCodesArray.push(language.locale);
|
||||
}
|
||||
return ['all', 'none', ...new Set(subLanguageCodesArray)];
|
||||
}
|
||||
|
||||
public async resolveItems(data: ResolveItemsData): Promise<boolean> {
|
||||
const parse = parseInt(data.id);
|
||||
if (isNaN(parse) || parse <= 0)
|
||||
return false;
|
||||
console.debug(`Got resolve options: ${JSON.stringify(data)}`);
|
||||
const res = await this.hidive.selectSeries(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.title,
|
||||
parent: {
|
||||
title: item.seriesTitle,
|
||||
season: item.episodeInformation.seasonNumber+''
|
||||
},
|
||||
image: item.thumbnailUrl,
|
||||
e: item.episodeInformation.episodeNumber+'',
|
||||
episode: item.episodeInformation.episodeNumber+'',
|
||||
};
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
public async listEpisodes(id: string): Promise<EpisodeListResponse> {
|
||||
const parse = parseInt(id);
|
||||
if (isNaN(parse) || parse <= 0)
|
||||
return { isOk: false, reason: new Error('The ID is invalid') };
|
||||
|
||||
const request = await this.hidive.listSeries(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.map(function(item) {
|
||||
const description = item.description.split('\r\n');
|
||||
return {
|
||||
e: item.episodeInformation.episodeNumber+'',
|
||||
lang: [],
|
||||
name: item.title,
|
||||
season: item.episodeInformation.seasonNumber+'',
|
||||
seasonTitle: request.series.seasons[item.episodeInformation.seasonNumber-1]?.title ?? request.series.title,
|
||||
episode: item.episodeInformation.episodeNumber+'',
|
||||
id: item.id+'',
|
||||
img: item.thumbnailUrl,
|
||||
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.selectSeries(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.downloadEpisode(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();
|
||||
}
|
||||
this.sendMessage({ name: 'finish', data: undefined });
|
||||
this.setDownloading(false);
|
||||
this.onFinish();
|
||||
}
|
||||
}
|
||||
|
||||
export default HidiveHandler;
|
||||
|
|
@ -16,108 +16,108 @@ class ExternalEvent extends EventEmitter {}
|
|||
|
||||
export default class WebSocketHandler {
|
||||
|
||||
private wsServer: ws.Server;
|
||||
private wsServer: ws.Server;
|
||||
|
||||
public events: ExternalEvent = new ExternalEvent();
|
||||
public events: ExternalEvent = new ExternalEvent();
|
||||
|
||||
constructor(server: Server) {
|
||||
this.wsServer = new ws.WebSocketServer({ noServer: true, path: '/private' });
|
||||
constructor(server: Server) {
|
||||
this.wsServer = new ws.WebSocketServer({ noServer: true, path: '/private' });
|
||||
|
||||
this.wsServer.on('connection', (socket, req) => {
|
||||
console.info(`[WS] Connection from '${req.socket.remoteAddress}'`);
|
||||
socket.on('error', (er) => console.error(`[WS] ${er}`));
|
||||
socket.on('message', (data) => {
|
||||
const json = JSON.parse(data.toString()) as UnknownWSMessage;
|
||||
this.events.emit(json.name, json as any, (data) => {
|
||||
this.wsServer.clients.forEach(client => {
|
||||
if (client.readyState !== WebSocket.OPEN)
|
||||
return;
|
||||
client.send(JSON.stringify({
|
||||
data,
|
||||
id: json.id,
|
||||
name: json.name
|
||||
}), (er) => {
|
||||
if (er)
|
||||
console.error(`[WS] ${er}`);
|
||||
this.wsServer.on('connection', (socket, req) => {
|
||||
console.info(`[WS] Connection from '${req.socket.remoteAddress}'`);
|
||||
socket.on('error', (er) => console.error(`[WS] ${er}`));
|
||||
socket.on('message', (data) => {
|
||||
const json = JSON.parse(data.toString()) as UnknownWSMessage;
|
||||
this.events.emit(json.name, json as any, (data) => {
|
||||
this.wsServer.clients.forEach(client => {
|
||||
if (client.readyState !== WebSocket.OPEN)
|
||||
return;
|
||||
client.send(JSON.stringify({
|
||||
data,
|
||||
id: json.id,
|
||||
name: json.name
|
||||
}), (er) => {
|
||||
if (er)
|
||||
console.error(`[WS] ${er}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
if (!this.wsServer.shouldHandle(request))
|
||||
return;
|
||||
if (!this.authenticate(request)) {
|
||||
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
||||
socket.destroy();
|
||||
console.info(`[WS] ${request.socket.remoteAddress} tried to connect but used a wrong password.`);
|
||||
return;
|
||||
}
|
||||
this.wsServer.handleUpgrade(request, socket, head, socket => {
|
||||
this.wsServer.emit('connection', socket, request);
|
||||
});
|
||||
});
|
||||
}
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
if (!this.wsServer.shouldHandle(request))
|
||||
return;
|
||||
if (!this.authenticate(request)) {
|
||||
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
||||
socket.destroy();
|
||||
console.info(`[WS] ${request.socket.remoteAddress} tried to connect but used a wrong password.`);
|
||||
return;
|
||||
}
|
||||
this.wsServer.handleUpgrade(request, socket, head, socket => {
|
||||
this.wsServer.emit('connection', socket, request);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public sendMessage<T extends keyof RandomEvents>(data: RandomEvent<T>) {
|
||||
this.wsServer.clients.forEach(client => {
|
||||
if (client.readyState !== WebSocket.OPEN)
|
||||
return;
|
||||
client.send(JSON.stringify(data), (er) => {
|
||||
if (er)
|
||||
console.error(`[WS] ${er}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
public sendMessage<T extends keyof RandomEvents>(data: RandomEvent<T>) {
|
||||
this.wsServer.clients.forEach(client => {
|
||||
if (client.readyState !== WebSocket.OPEN)
|
||||
return;
|
||||
client.send(JSON.stringify(data), (er) => {
|
||||
if (er)
|
||||
console.error(`[WS] ${er}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private authenticate(request: IncomingMessage): boolean {
|
||||
const search = new URL(`http://${request.headers.host}${request.url}`).searchParams;
|
||||
return cfg.gui.password === (search.get('password') ?? undefined);
|
||||
}
|
||||
private authenticate(request: IncomingMessage): boolean {
|
||||
const search = new URL(`http://${request.headers.host}${request.url}`).searchParams;
|
||||
return cfg.gui.password === (search.get('password') ?? undefined);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class PublicWebSocket {
|
||||
private wsServer: ws.Server;
|
||||
private wsServer: ws.Server;
|
||||
|
||||
private state = getState();
|
||||
constructor(server: Server) {
|
||||
this.wsServer = new ws.WebSocketServer({ noServer: true, path: '/public' });
|
||||
private state = getState();
|
||||
constructor(server: Server) {
|
||||
this.wsServer = new ws.WebSocketServer({ noServer: true, path: '/public' });
|
||||
|
||||
this.wsServer.on('connection', (socket, req) => {
|
||||
console.info(`[WS] Connection to public ws from '${req.socket.remoteAddress}'`);
|
||||
socket.on('error', (er) => console.error(`[WS] ${er}`));
|
||||
socket.on('message', (msg) => {
|
||||
const data = JSON.parse(msg.toString()) as UnknownWSMessage;
|
||||
switch (data.name) {
|
||||
case 'isSetup':
|
||||
this.send(socket, data.id, data.name, this.state.setup);
|
||||
break;
|
||||
case 'requirePassword':
|
||||
this.send(socket, data.id, data.name, cfg.gui.password !== undefined);
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
this.wsServer.on('connection', (socket, req) => {
|
||||
console.info(`[WS] Connection to public ws from '${req.socket.remoteAddress}'`);
|
||||
socket.on('error', (er) => console.error(`[WS] ${er}`));
|
||||
socket.on('message', (msg) => {
|
||||
const data = JSON.parse(msg.toString()) as UnknownWSMessage;
|
||||
switch (data.name) {
|
||||
case 'isSetup':
|
||||
this.send(socket, data.id, data.name, this.state.setup);
|
||||
break;
|
||||
case 'requirePassword':
|
||||
this.send(socket, data.id, data.name, cfg.gui.password !== undefined);
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
if (!this.wsServer.shouldHandle(request))
|
||||
return;
|
||||
this.wsServer.handleUpgrade(request, socket, head, socket => {
|
||||
this.wsServer.emit('connection', socket, request);
|
||||
});
|
||||
});
|
||||
}
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
if (!this.wsServer.shouldHandle(request))
|
||||
return;
|
||||
this.wsServer.handleUpgrade(request, socket, head, socket => {
|
||||
this.wsServer.emit('connection', socket, request);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private send(client: ws.WebSocket, id: string, name: string, data: any) {
|
||||
client.send(JSON.stringify({
|
||||
data,
|
||||
id,
|
||||
name
|
||||
}), (er) => {
|
||||
if (er)
|
||||
console.error(`[WS] ${er}`);
|
||||
});
|
||||
}
|
||||
private send(client: ws.WebSocket, id: string, name: string, data: any) {
|
||||
client.send(JSON.stringify({
|
||||
data,
|
||||
id,
|
||||
name
|
||||
}), (er) => {
|
||||
if (er)
|
||||
console.error(`[WS] ${er}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
174
index.ts
174
index.ts
|
|
@ -7,94 +7,94 @@ import { makeCommand, addToArchive } from './modules/module.downloadArchive';
|
|||
import update from './modules/module.updater';
|
||||
|
||||
(async () => {
|
||||
const cfg = yamlCfg.loadCfg();
|
||||
const argv = appArgv(cfg.cli);
|
||||
if (!argv.skipUpdate)
|
||||
await update(argv.update);
|
||||
const cfg = yamlCfg.loadCfg();
|
||||
const argv = appArgv(cfg.cli);
|
||||
if (!argv.skipUpdate)
|
||||
await update(argv.update);
|
||||
|
||||
if (argv.all && argv.but) {
|
||||
console.error('--all and --but exclude each other!');
|
||||
return;
|
||||
}
|
||||
if (argv.all && argv.but) {
|
||||
console.error('--all and --but exclude each other!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (argv.addArchive) {
|
||||
if (argv.service === 'crunchy') {
|
||||
if (argv.s === undefined && argv.series === undefined)
|
||||
return console.error('`-s` or `--srz` not found');
|
||||
if (argv.s && argv.series)
|
||||
return console.error('Both `-s` and `--srz` found');
|
||||
addToArchive({
|
||||
service: 'crunchy',
|
||||
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.service === 'ao') {
|
||||
if (argv.s === undefined)
|
||||
return console.error('`-s` not found');
|
||||
addToArchive({
|
||||
service: 'hidive',
|
||||
//type: argv.s === undefined ? 'srz' : 's'
|
||||
type: 's'
|
||||
}, (argv.s === undefined ? argv.series : argv.s) as string);
|
||||
console.info('Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s));
|
||||
if (argv.addArchive) {
|
||||
if (argv.service === 'crunchy') {
|
||||
if (argv.s === undefined && argv.series === undefined)
|
||||
return console.error('`-s` or `--srz` not found');
|
||||
if (argv.s && argv.series)
|
||||
return console.error('Both `-s` and `--srz` found');
|
||||
addToArchive({
|
||||
service: 'crunchy',
|
||||
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.service === 'ao') {
|
||||
if (argv.s === undefined)
|
||||
return console.error('`-s` not found');
|
||||
addToArchive({
|
||||
service: 'hidive',
|
||||
//type: argv.s === undefined ? 'srz' : 's'
|
||||
type: 's'
|
||||
}, (argv.s === undefined ? argv.series : argv.s) as string);
|
||||
console.info('Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s));
|
||||
}
|
||||
} else if (argv.downloadArchive) {
|
||||
const ids = makeCommand(argv.service);
|
||||
for (const id of ids) {
|
||||
overrideArguments(cfg.cli, id);
|
||||
/* Reimport module to override appArgv */
|
||||
Object.keys(require.cache).forEach(key => {
|
||||
if (key.endsWith('crunchy.js') || key.endsWith('hidive.js') || key.endsWith('ao.js'))
|
||||
delete require.cache[key];
|
||||
});
|
||||
let service: ServiceClass;
|
||||
switch(argv.service) {
|
||||
case 'crunchy':
|
||||
service = new (await import('./crunchy')).default;
|
||||
break;
|
||||
case 'hidive':
|
||||
service = new (await import('./hidive')).default;
|
||||
break;
|
||||
case 'ao':
|
||||
service = new (await import('./ao')).default;
|
||||
break;
|
||||
case 'adn':
|
||||
service = new (await import('./adn')).default;
|
||||
break;
|
||||
default:
|
||||
service = new (await import(`./${argv.service}`)).default;
|
||||
break;
|
||||
}
|
||||
await service.cli();
|
||||
}
|
||||
} else {
|
||||
let service: ServiceClass;
|
||||
switch(argv.service) {
|
||||
case 'crunchy':
|
||||
service = new (await import('./crunchy')).default;
|
||||
break;
|
||||
case 'hidive':
|
||||
service = new (await import('./hidive')).default;
|
||||
break;
|
||||
case 'ao':
|
||||
service = new (await import('./ao')).default;
|
||||
break;
|
||||
case 'adn':
|
||||
service = new (await import('./adn')).default;
|
||||
break;
|
||||
default:
|
||||
service = new (await import(`./${argv.service}`)).default;
|
||||
break;
|
||||
}
|
||||
await service.cli();
|
||||
}
|
||||
} else if (argv.downloadArchive) {
|
||||
const ids = makeCommand(argv.service);
|
||||
for (const id of ids) {
|
||||
overrideArguments(cfg.cli, id);
|
||||
/* Reimport module to override appArgv */
|
||||
Object.keys(require.cache).forEach(key => {
|
||||
if (key.endsWith('crunchy.js') || key.endsWith('hidive.js') || key.endsWith('ao.js'))
|
||||
delete require.cache[key];
|
||||
});
|
||||
let service: ServiceClass;
|
||||
switch(argv.service) {
|
||||
case 'crunchy':
|
||||
service = new (await import('./crunchy')).default;
|
||||
break;
|
||||
case 'hidive':
|
||||
service = new (await import('./hidive')).default;
|
||||
break;
|
||||
case 'ao':
|
||||
service = new (await import('./ao')).default;
|
||||
break;
|
||||
case 'adn':
|
||||
service = new (await import('./adn')).default;
|
||||
break;
|
||||
default:
|
||||
service = new (await import(`./${argv.service}`)).default;
|
||||
break;
|
||||
}
|
||||
await service.cli();
|
||||
}
|
||||
} else {
|
||||
let service: ServiceClass;
|
||||
switch(argv.service) {
|
||||
case 'crunchy':
|
||||
service = new (await import('./crunchy')).default;
|
||||
break;
|
||||
case 'hidive':
|
||||
service = new (await import('./hidive')).default;
|
||||
break;
|
||||
case 'ao':
|
||||
service = new (await import('./ao')).default;
|
||||
break;
|
||||
case 'adn':
|
||||
service = new (await import('./adn')).default;
|
||||
break;
|
||||
default:
|
||||
service = new (await import(`./${argv.service}`)).default;
|
||||
break;
|
||||
}
|
||||
await service.cli();
|
||||
}
|
||||
})();
|
||||
|
|
@ -4,27 +4,27 @@ import path from 'path';
|
|||
import { args, groups } from './module.args';
|
||||
|
||||
const transformService = (str: Array<'crunchy'|'hidive'|'ao'|'adn'|'all'>) => {
|
||||
const services: string[] = [];
|
||||
str.forEach(function(part) {
|
||||
switch(part) {
|
||||
case 'crunchy':
|
||||
services.push('Crunchyroll');
|
||||
break;
|
||||
case 'hidive':
|
||||
services.push('Hidive');
|
||||
break;
|
||||
case 'ao':
|
||||
services.push('AnimeOnegai');
|
||||
break;
|
||||
case 'adn':
|
||||
services.push('AnimationDigitalNetwork');
|
||||
break;
|
||||
case 'all':
|
||||
services.push('All');
|
||||
break;
|
||||
}
|
||||
});
|
||||
return services.join(', ');
|
||||
const services: string[] = [];
|
||||
str.forEach(function(part) {
|
||||
switch(part) {
|
||||
case 'crunchy':
|
||||
services.push('Crunchyroll');
|
||||
break;
|
||||
case 'hidive':
|
||||
services.push('Hidive');
|
||||
break;
|
||||
case 'ao':
|
||||
services.push('AnimeOnegai');
|
||||
break;
|
||||
case 'adn':
|
||||
services.push('AnimationDigitalNetwork');
|
||||
break;
|
||||
case 'all':
|
||||
services.push('All');
|
||||
break;
|
||||
}
|
||||
});
|
||||
return services.join(', ');
|
||||
};
|
||||
|
||||
let docs = `# ${packageJSON.name} (v${packageJSON.version})
|
||||
|
|
@ -45,30 +45,30 @@ This tool is not responsible for your actions; please make an informed decision
|
|||
`;
|
||||
|
||||
Object.entries(groups).forEach(([key, value]) => {
|
||||
docs += `\n### ${value.slice(0, -1)}\n`;
|
||||
docs += `\n### ${value.slice(0, -1)}\n`;
|
||||
|
||||
docs += args.filter(a => a.group === key).map(argument => {
|
||||
return [`#### \`${argument.name.length > 1 ? '--' : '-'}${argument.name}\``,
|
||||
`| **Service** | **Usage** | **Type** | **Required** | **Alias** | ${argument.choices ? '**Choices** |' : ''} ${argument.default ? '**Default** |' : ''}**cli-default Entry**`,
|
||||
`| --- | --- | --- | --- | --- | ${argument.choices ? '--- | ' : ''}${argument.default ? '--- | ' : ''}---| `,
|
||||
`| ${transformService(argument.service)} | \`${argument.name.length > 1 ? '--' : '-'}${argument.name} ${argument.usage}\` | \`${argument.type}\` | \`${argument.demandOption ? 'Yes' : 'No'}\`|`
|
||||
docs += args.filter(a => a.group === key).map(argument => {
|
||||
return [`#### \`${argument.name.length > 1 ? '--' : '-'}${argument.name}\``,
|
||||
`| **Service** | **Usage** | **Type** | **Required** | **Alias** | ${argument.choices ? '**Choices** |' : ''} ${argument.default ? '**Default** |' : ''}**cli-default Entry**`,
|
||||
`| --- | --- | --- | --- | --- | ${argument.choices ? '--- | ' : ''}${argument.default ? '--- | ' : ''}---| `,
|
||||
`| ${transformService(argument.service)} | \`${argument.name.length > 1 ? '--' : '-'}${argument.name} ${argument.usage}\` | \`${argument.type}\` | \`${argument.demandOption ? 'Yes' : 'No'}\`|`
|
||||
+ ` \`${(argument.alias ? `${argument.alias.length > 1 ? '--' : '-'}${argument.alias}` : undefined) ?? 'NaN'}\` |`
|
||||
+ `${argument.choices ? ` [${argument.choices.map(a => `\`${a || '\'\''}\``).join(', ')}] |` : ''}`
|
||||
+ `${argument.default ? ` \`${
|
||||
typeof argument.default === 'object'
|
||||
? Array.isArray(argument.default)
|
||||
? JSON.stringify(argument.default)
|
||||
: (argument.default as any).default
|
||||
: argument.default
|
||||
typeof argument.default === 'object'
|
||||
? Array.isArray(argument.default)
|
||||
? JSON.stringify(argument.default)
|
||||
: (argument.default as any).default
|
||||
: argument.default
|
||||
}\`|` : ''}`
|
||||
+ ` ${typeof argument.default === 'object' && !Array.isArray(argument.default)
|
||||
? `\`${argument.default.name || argument.name}: \``
|
||||
: '`NaN`'
|
||||
? `\`${argument.default.name || argument.name}: \``
|
||||
: '`NaN`'
|
||||
} |`,
|
||||
'',
|
||||
argument.docDescribe === true ? argument.describe : argument.docDescribe
|
||||
].join('\n');
|
||||
}).join('\n');
|
||||
'',
|
||||
argument.docDescribe === true ? argument.describe : argument.docDescribe
|
||||
].join('\n');
|
||||
}).join('\n');
|
||||
});
|
||||
|
||||
|
||||
|
|
|
|||
176
modules/build.ts
176
modules/build.ts
|
|
@ -15,104 +15,104 @@ const nodeVer = 'node20-';
|
|||
type BuildTypes = `${'windows'|'macos'|'linux'|'linuxstatic'|'alpine'}-${'x64'|'arm64'}`|'linuxstatic-armv7'
|
||||
|
||||
(async () => {
|
||||
const buildType = process.argv[2] as BuildTypes;
|
||||
const isGUI = process.argv[3] === 'true';
|
||||
const buildType = process.argv[2] as BuildTypes;
|
||||
const isGUI = process.argv[3] === 'true';
|
||||
|
||||
buildBinary(buildType, isGUI);
|
||||
buildBinary(buildType, isGUI);
|
||||
})();
|
||||
|
||||
// main
|
||||
async function buildBinary(buildType: BuildTypes, gui: boolean) {
|
||||
const buildStr = 'multi-downloader-nx';
|
||||
const acceptablePlatforms = ['windows','linux','linuxstatic','macos','alpine'];
|
||||
const acceptableArchs = ['x64','arm64'];
|
||||
const acceptableBuilds: string[] = ['linuxstatic-armv7'];
|
||||
for (const platform of acceptablePlatforms) {
|
||||
for (const arch of acceptableArchs) {
|
||||
acceptableBuilds.push(platform+'-'+arch);
|
||||
const buildStr = 'multi-downloader-nx';
|
||||
const acceptablePlatforms = ['windows','linux','linuxstatic','macos','alpine'];
|
||||
const acceptableArchs = ['x64','arm64'];
|
||||
const acceptableBuilds: string[] = ['linuxstatic-armv7'];
|
||||
for (const platform of acceptablePlatforms) {
|
||||
for (const arch of acceptableArchs) {
|
||||
acceptableBuilds.push(platform+'-'+arch);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!acceptableBuilds.includes(buildType)){
|
||||
console.error('Unknown build type!');
|
||||
process.exit(1);
|
||||
}
|
||||
await modulesCleanup('.');
|
||||
if(!fs.existsSync(buildsDir)){
|
||||
fs.mkdirSync(buildsDir);
|
||||
}
|
||||
const buildFull = `${buildStr}-${getFriendlyName(buildType)}-${gui ? 'gui' : 'cli'}`;
|
||||
const buildDir = `${buildsDir}/${buildFull}`;
|
||||
if(fs.existsSync(buildDir)){
|
||||
fs.removeSync(buildDir);
|
||||
}
|
||||
fs.mkdirSync(buildDir);
|
||||
console.info('Running esbuild');
|
||||
if(!acceptableBuilds.includes(buildType)){
|
||||
console.error('Unknown build type!');
|
||||
process.exit(1);
|
||||
}
|
||||
await modulesCleanup('.');
|
||||
if(!fs.existsSync(buildsDir)){
|
||||
fs.mkdirSync(buildsDir);
|
||||
}
|
||||
const buildFull = `${buildStr}-${getFriendlyName(buildType)}-${gui ? 'gui' : 'cli'}`;
|
||||
const buildDir = `${buildsDir}/${buildFull}`;
|
||||
if(fs.existsSync(buildDir)){
|
||||
fs.removeSync(buildDir);
|
||||
}
|
||||
fs.mkdirSync(buildDir);
|
||||
console.info('Running esbuild');
|
||||
|
||||
const build = await esbuild.build({
|
||||
entryPoints: [
|
||||
gui ? 'gui.js' : 'index.js',
|
||||
],
|
||||
sourceRoot: './',
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
treeShaking: true,
|
||||
// External source map for debugging
|
||||
sourcemap: true,
|
||||
// Minify and keep the original names
|
||||
minify: true,
|
||||
keepNames: true,
|
||||
outfile: path.join(buildsDir, 'index.cjs'),
|
||||
metafile: true,
|
||||
external: ['cheerio', 'sleep', ...builtinModules]
|
||||
});
|
||||
const build = await esbuild.build({
|
||||
entryPoints: [
|
||||
gui ? 'gui.js' : 'index.js',
|
||||
],
|
||||
sourceRoot: './',
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
treeShaking: true,
|
||||
// External source map for debugging
|
||||
sourcemap: true,
|
||||
// Minify and keep the original names
|
||||
minify: true,
|
||||
keepNames: true,
|
||||
outfile: path.join(buildsDir, 'index.cjs'),
|
||||
metafile: true,
|
||||
external: ['cheerio', 'sleep', ...builtinModules]
|
||||
});
|
||||
|
||||
if (build.errors?.length > 0) console.error(build.errors);
|
||||
if (build.warnings?.length > 0) console.warn(build.warnings);
|
||||
if (build.errors?.length > 0) console.error(build.errors);
|
||||
if (build.warnings?.length > 0) console.warn(build.warnings);
|
||||
|
||||
const buildConfig = [
|
||||
`${buildsDir}/index.cjs`,
|
||||
'--target', nodeVer + buildType,
|
||||
'--output', `${buildDir}/${pkg.short_name}`,
|
||||
'--compress', 'GZip'
|
||||
];
|
||||
console.info(`[Build] Build configuration: ${buildFull}`);
|
||||
try {
|
||||
await exec(buildConfig);
|
||||
}
|
||||
catch(e){
|
||||
console.info(e);
|
||||
process.exit(1);
|
||||
}
|
||||
fs.mkdirSync(`${buildDir}/config`);
|
||||
fs.mkdirSync(`${buildDir}/videos`);
|
||||
fs.mkdirSync(`${buildDir}/widevine`);
|
||||
fs.mkdirSync(`${buildDir}/playready`);
|
||||
fs.copySync('./config/bin-path.yml', `${buildDir}/config/bin-path.yml`);
|
||||
fs.copySync('./config/cli-defaults.yml', `${buildDir}/config/cli-defaults.yml`);
|
||||
fs.copySync('./config/dir-path.yml', `${buildDir}/config/dir-path.yml`);
|
||||
fs.copySync('./config/gui.yml', `${buildDir}/config/gui.yml`);
|
||||
fs.copySync('./modules/cmd-here.bat', `${buildDir}/cmd-here.bat`);
|
||||
fs.copySync('./modules/NotoSans-Regular.ttf', `${buildDir}/NotoSans-Regular.ttf`);
|
||||
fs.copySync('./package.json', `${buildDir}/package.json`);
|
||||
fs.copySync('./docs/', `${buildDir}/docs/`);
|
||||
fs.copySync('./LICENSE.md', `${buildDir}/docs/LICENSE.md`);
|
||||
if (gui) {
|
||||
fs.copySync('./gui', `${buildDir}/gui`);
|
||||
fs.copySync('./node_modules/open/xdg-open', `${buildDir}/xdg-open`);
|
||||
}
|
||||
if(fs.existsSync(`${buildsDir}/${buildFull}.7z`)){
|
||||
fs.removeSync(`${buildsDir}/${buildFull}.7z`);
|
||||
}
|
||||
execSync(`7z a -t7z "${buildsDir}/${buildFull}.7z" "${buildDir}"`,{stdio:[0,1,2]});
|
||||
const buildConfig = [
|
||||
`${buildsDir}/index.cjs`,
|
||||
'--target', nodeVer + buildType,
|
||||
'--output', `${buildDir}/${pkg.short_name}`,
|
||||
'--compress', 'GZip'
|
||||
];
|
||||
console.info(`[Build] Build configuration: ${buildFull}`);
|
||||
try {
|
||||
await exec(buildConfig);
|
||||
}
|
||||
catch(e){
|
||||
console.info(e);
|
||||
process.exit(1);
|
||||
}
|
||||
fs.mkdirSync(`${buildDir}/config`);
|
||||
fs.mkdirSync(`${buildDir}/videos`);
|
||||
fs.mkdirSync(`${buildDir}/widevine`);
|
||||
fs.mkdirSync(`${buildDir}/playready`);
|
||||
fs.copySync('./config/bin-path.yml', `${buildDir}/config/bin-path.yml`);
|
||||
fs.copySync('./config/cli-defaults.yml', `${buildDir}/config/cli-defaults.yml`);
|
||||
fs.copySync('./config/dir-path.yml', `${buildDir}/config/dir-path.yml`);
|
||||
fs.copySync('./config/gui.yml', `${buildDir}/config/gui.yml`);
|
||||
fs.copySync('./modules/cmd-here.bat', `${buildDir}/cmd-here.bat`);
|
||||
fs.copySync('./modules/NotoSans-Regular.ttf', `${buildDir}/NotoSans-Regular.ttf`);
|
||||
fs.copySync('./package.json', `${buildDir}/package.json`);
|
||||
fs.copySync('./docs/', `${buildDir}/docs/`);
|
||||
fs.copySync('./LICENSE.md', `${buildDir}/docs/LICENSE.md`);
|
||||
if (gui) {
|
||||
fs.copySync('./gui', `${buildDir}/gui`);
|
||||
fs.copySync('./node_modules/open/xdg-open', `${buildDir}/xdg-open`);
|
||||
}
|
||||
if(fs.existsSync(`${buildsDir}/${buildFull}.7z`)){
|
||||
fs.removeSync(`${buildsDir}/${buildFull}.7z`);
|
||||
}
|
||||
execSync(`7z a -t7z "${buildsDir}/${buildFull}.7z" "${buildDir}"`,{stdio:[0,1,2]});
|
||||
}
|
||||
|
||||
function getFriendlyName(buildString: string): string {
|
||||
if (buildString.includes('armv7')) {
|
||||
return 'android';
|
||||
}
|
||||
if (buildString.includes('linuxstatic')) {
|
||||
buildString = buildString.replace('linuxstatic', 'linux');
|
||||
}
|
||||
return buildString;
|
||||
if (buildString.includes('armv7')) {
|
||||
return 'android';
|
||||
}
|
||||
if (buildString.includes('linuxstatic')) {
|
||||
buildString = buildString.replace('linuxstatic', 'linux');
|
||||
}
|
||||
return buildString;
|
||||
}
|
||||
290
modules/cdm.ts
290
modules/cdm.ts
|
|
@ -10,176 +10,176 @@ import { ofetch } from 'ofetch';
|
|||
|
||||
//read cdm files located in the same directory
|
||||
let privateKey: Buffer = Buffer.from([]),
|
||||
identifierBlob: Buffer = Buffer.from([]),
|
||||
prd: Buffer = Buffer.from([]),
|
||||
prd_cdm: Cdm | undefined;
|
||||
identifierBlob: Buffer = Buffer.from([]),
|
||||
prd: Buffer = Buffer.from([]),
|
||||
prd_cdm: Cdm | undefined;
|
||||
export let cdm: 'widevine' | 'playready';
|
||||
export let canDecrypt: boolean;
|
||||
try {
|
||||
const files_prd = fs.readdirSync(path.join(workingDir, 'playready'));
|
||||
const prd_file_found = files_prd.find((f) => f.includes('.prd'));
|
||||
try {
|
||||
if (prd_file_found) {
|
||||
const file_prd = path.join(workingDir, 'playready', prd_file_found);
|
||||
const stats = fs.statSync(file_prd);
|
||||
if (stats.size < 1024 * 8 && stats.isFile()) {
|
||||
const fileContents = fs.readFileSync(file_prd, {
|
||||
encoding: 'utf8'
|
||||
});
|
||||
if (fileContents.includes('CERT')) {
|
||||
prd = fs.readFileSync(file_prd);
|
||||
const device = Device.loads(prd);
|
||||
prd_cdm = Cdm.fromDevice(device);
|
||||
const files_prd = fs.readdirSync(path.join(workingDir, 'playready'));
|
||||
const prd_file_found = files_prd.find((f) => f.includes('.prd'));
|
||||
try {
|
||||
if (prd_file_found) {
|
||||
const file_prd = path.join(workingDir, 'playready', prd_file_found);
|
||||
const stats = fs.statSync(file_prd);
|
||||
if (stats.size < 1024 * 8 && stats.isFile()) {
|
||||
const fileContents = fs.readFileSync(file_prd, {
|
||||
encoding: 'utf8'
|
||||
});
|
||||
if (fileContents.includes('CERT')) {
|
||||
prd = fs.readFileSync(file_prd);
|
||||
const device = Device.loads(prd);
|
||||
prd_cdm = Cdm.fromDevice(device);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading Playready CDM, ensure the CDM is provisioned as a V3 Device and not malformed. For more informations read the readme.');
|
||||
prd = Buffer.from([]);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading Playready CDM, ensure the CDM is provisioned as a V3 Device and not malformed. For more informations read the readme.');
|
||||
prd = Buffer.from([]);
|
||||
}
|
||||
|
||||
const files_wvd = fs.readdirSync(path.join(workingDir, 'widevine'));
|
||||
try {
|
||||
files_wvd.forEach(function (file) {
|
||||
file = path.join(workingDir, 'widevine', file);
|
||||
const stats = fs.statSync(file);
|
||||
if (stats.size < 1024 * 8 && stats.isFile()) {
|
||||
const fileContents = fs.readFileSync(file, { encoding: 'utf8' });
|
||||
if ((fileContents.startsWith('-----BEGIN RSA PRIVATE KEY-----') && fileContents.endsWith('-----END RSA PRIVATE KEY-----')) || (fileContents.startsWith('-----BEGIN PRIVATE KEY-----') && fileContents.endsWith('-----END PRIVATE KEY-----'))) {
|
||||
privateKey = fs.readFileSync(file);
|
||||
}
|
||||
if (fileContents.includes('widevine_cdm_version') && fileContents.includes('oem_crypto_security_patch_level') && !fileContents.startsWith('WVD')) {
|
||||
identifierBlob = fs.readFileSync(file);
|
||||
}
|
||||
if (fileContents.startsWith('WVD')) {
|
||||
console.warn('Found WVD file in folder, AniDL currently only supports device_client_id_blob and device_private_key, make sure to have them in the widevine folder.');
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error loading Widevine CDM, malformed client blob or private key.');
|
||||
privateKey = Buffer.from([]);
|
||||
identifierBlob = Buffer.from([]);
|
||||
}
|
||||
const files_wvd = fs.readdirSync(path.join(workingDir, 'widevine'));
|
||||
try {
|
||||
files_wvd.forEach(function (file) {
|
||||
file = path.join(workingDir, 'widevine', file);
|
||||
const stats = fs.statSync(file);
|
||||
if (stats.size < 1024 * 8 && stats.isFile()) {
|
||||
const fileContents = fs.readFileSync(file, { encoding: 'utf8' });
|
||||
if ((fileContents.startsWith('-----BEGIN RSA PRIVATE KEY-----') && fileContents.endsWith('-----END RSA PRIVATE KEY-----')) || (fileContents.startsWith('-----BEGIN PRIVATE KEY-----') && fileContents.endsWith('-----END PRIVATE KEY-----'))) {
|
||||
privateKey = fs.readFileSync(file);
|
||||
}
|
||||
if (fileContents.includes('widevine_cdm_version') && fileContents.includes('oem_crypto_security_patch_level') && !fileContents.startsWith('WVD')) {
|
||||
identifierBlob = fs.readFileSync(file);
|
||||
}
|
||||
if (fileContents.startsWith('WVD')) {
|
||||
console.warn('Found WVD file in folder, AniDL currently only supports device_client_id_blob and device_private_key, make sure to have them in the widevine folder.');
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error loading Widevine CDM, malformed client blob or private key.');
|
||||
privateKey = Buffer.from([]);
|
||||
identifierBlob = Buffer.from([]);
|
||||
}
|
||||
|
||||
if (privateKey.length !== 0 && identifierBlob.length !== 0) {
|
||||
cdm = 'widevine';
|
||||
canDecrypt = true;
|
||||
} else if (prd.length !== 0) {
|
||||
cdm = 'playready';
|
||||
canDecrypt = true;
|
||||
} else if (privateKey.length === 0 && identifierBlob.length !== 0) {
|
||||
console.warn('Private key missing');
|
||||
canDecrypt = false;
|
||||
} else if (identifierBlob.length === 0 && privateKey.length !== 0) {
|
||||
console.warn('Identifier blob missing');
|
||||
canDecrypt = false;
|
||||
} else if (prd.length == 0) {
|
||||
canDecrypt = false;
|
||||
} else {
|
||||
canDecrypt = false;
|
||||
}
|
||||
if (privateKey.length !== 0 && identifierBlob.length !== 0) {
|
||||
cdm = 'widevine';
|
||||
canDecrypt = true;
|
||||
} else if (prd.length !== 0) {
|
||||
cdm = 'playready';
|
||||
canDecrypt = true;
|
||||
} else if (privateKey.length === 0 && identifierBlob.length !== 0) {
|
||||
console.warn('Private key missing');
|
||||
canDecrypt = false;
|
||||
} else if (identifierBlob.length === 0 && privateKey.length !== 0) {
|
||||
console.warn('Identifier blob missing');
|
||||
canDecrypt = false;
|
||||
} else if (prd.length == 0) {
|
||||
canDecrypt = false;
|
||||
} else {
|
||||
canDecrypt = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
canDecrypt = false;
|
||||
console.error(e);
|
||||
canDecrypt = false;
|
||||
}
|
||||
|
||||
export async function getKeysWVD(pssh: string | undefined, licenseServer: string, authData: Record<string, string>): Promise<KeyContainer[]> {
|
||||
if (!pssh || !canDecrypt) return [];
|
||||
//pssh found in the mpd manifest
|
||||
const psshBuffer = Buffer.from(pssh, 'base64');
|
||||
if (!pssh || !canDecrypt) return [];
|
||||
//pssh found in the mpd manifest
|
||||
const psshBuffer = Buffer.from(pssh, 'base64');
|
||||
|
||||
//Create a new widevine session
|
||||
const session = new Session({ privateKey, identifierBlob }, psshBuffer);
|
||||
//Create a new widevine session
|
||||
const session = new Session({ privateKey, identifierBlob }, psshBuffer);
|
||||
|
||||
//Generate license
|
||||
const data = await ofetch(licenseServer, {
|
||||
method: 'POST',
|
||||
body: session.createLicenseRequest(),
|
||||
headers: authData,
|
||||
responseType: 'arrayBuffer'
|
||||
}).catch((error) => {
|
||||
if (error.status && error.statusText) {
|
||||
console.error(`${error.name} ${error.status}: ${error.statusText}`);
|
||||
} else {
|
||||
console.error(`${error.name}: ${error.message}`);
|
||||
}
|
||||
//Generate license
|
||||
const data = await ofetch(licenseServer, {
|
||||
method: 'POST',
|
||||
body: session.createLicenseRequest(),
|
||||
headers: authData,
|
||||
responseType: 'arrayBuffer'
|
||||
}).catch((error) => {
|
||||
if (error.status && error.statusText) {
|
||||
console.error(`${error.name} ${error.status}: ${error.statusText}`);
|
||||
} else {
|
||||
console.error(`${error.name}: ${error.message}`);
|
||||
}
|
||||
|
||||
if (!error.data) return;
|
||||
const data = error.data instanceof ArrayBuffer ? new TextDecoder().decode(error.data) : error.data;
|
||||
if (data) {
|
||||
const docTitle = data.match(/<title>(.*)<\/title>/);
|
||||
if (docTitle) {
|
||||
console.error(docTitle[1]);
|
||||
}
|
||||
if (error.status && error.status != 404 && error.status != 403) {
|
||||
console.error('Body:', data);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!error.data) return;
|
||||
const data = error.data instanceof ArrayBuffer ? new TextDecoder().decode(error.data) : error.data;
|
||||
if (data) {
|
||||
const docTitle = data.match(/<title>(.*)<\/title>/);
|
||||
if (docTitle) {
|
||||
console.error(docTitle[1]);
|
||||
}
|
||||
if (error.status && error.status != 404 && error.status != 403) {
|
||||
console.error('Body:', data);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (data) {
|
||||
//Parse License and return keys
|
||||
const text = new TextDecoder().decode(data);
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
return session.parseLicense(Buffer.from(json['license'], 'base64')) as KeyContainer[];
|
||||
} catch {
|
||||
return session.parseLicense(Buffer.from(new Uint8Array(data))) as KeyContainer[];
|
||||
const text = new TextDecoder().decode(data);
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
return session.parseLicense(Buffer.from(json['license'], 'base64')) as KeyContainer[];
|
||||
} catch {
|
||||
return session.parseLicense(Buffer.from(new Uint8Array(data))) as KeyContainer[];
|
||||
}
|
||||
} else {
|
||||
console.error('License request failed');
|
||||
return [];
|
||||
}
|
||||
} else {
|
||||
console.error('License request failed');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getKeysPRD(pssh: string | undefined, licenseServer: string, authData: Record<string, string>): Promise<KeyContainer[]> {
|
||||
if (!pssh || !canDecrypt || !prd_cdm) return [];
|
||||
const pssh_parsed = new PSSH(pssh);
|
||||
if (!pssh || !canDecrypt || !prd_cdm) return [];
|
||||
const pssh_parsed = new PSSH(pssh);
|
||||
|
||||
//Create a new playready session
|
||||
const session = prd_cdm.getLicenseChallenge(pssh_parsed.get_wrm_headers(true)[0]);
|
||||
//Create a new playready session
|
||||
const session = prd_cdm.getLicenseChallenge(pssh_parsed.get_wrm_headers(true)[0]);
|
||||
|
||||
//Generate license
|
||||
const data = await ofetch(licenseServer, {
|
||||
method: 'POST',
|
||||
body: session,
|
||||
headers: authData,
|
||||
responseType: 'text'
|
||||
}).catch((error) => {
|
||||
if (error && error.status && error.statusText) {
|
||||
console.error(`${error.name} ${error.status}: ${error.statusText}`);
|
||||
} else {
|
||||
console.error(`${error.name}: ${error.message}`);
|
||||
}
|
||||
//Generate license
|
||||
const data = await ofetch(licenseServer, {
|
||||
method: 'POST',
|
||||
body: session,
|
||||
headers: authData,
|
||||
responseType: 'text'
|
||||
}).catch((error) => {
|
||||
if (error && error.status && error.statusText) {
|
||||
console.error(`${error.name} ${error.status}: ${error.statusText}`);
|
||||
} else {
|
||||
console.error(`${error.name}: ${error.message}`);
|
||||
}
|
||||
|
||||
if (!error.data) return;
|
||||
const docTitle = error.data.match(/<title>(.*)<\/title>/);
|
||||
if (docTitle) {
|
||||
console.error(docTitle[1]);
|
||||
}
|
||||
if (error.status && error.status != 404 && error.status != 403) {
|
||||
console.error('Body:', error.data);
|
||||
}
|
||||
});
|
||||
if (!error.data) return;
|
||||
const docTitle = error.data.match(/<title>(.*)<\/title>/);
|
||||
if (docTitle) {
|
||||
console.error(docTitle[1]);
|
||||
}
|
||||
if (error.status && error.status != 404 && error.status != 403) {
|
||||
console.error('Body:', error.data);
|
||||
}
|
||||
});
|
||||
|
||||
if (data) {
|
||||
if (data) {
|
||||
//Parse License and return keys
|
||||
try {
|
||||
const keys = prd_cdm.parseLicense(data);
|
||||
try {
|
||||
const keys = prd_cdm.parseLicense(data);
|
||||
|
||||
return keys.map((k) => {
|
||||
return {
|
||||
kid: k.key_id,
|
||||
key: k.key
|
||||
};
|
||||
});
|
||||
} catch {
|
||||
console.error('License parsing failed');
|
||||
return [];
|
||||
return keys.map((k) => {
|
||||
return {
|
||||
kid: k.key_id,
|
||||
key: k.key
|
||||
};
|
||||
});
|
||||
} catch {
|
||||
console.error('License parsing failed');
|
||||
return [];
|
||||
}
|
||||
} else {
|
||||
console.error('License request failed');
|
||||
return [];
|
||||
}
|
||||
} else {
|
||||
console.error('License request failed');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,352 +72,352 @@ type Data = {
|
|||
|
||||
// hls class
|
||||
class hlsDownload {
|
||||
private data: Data;
|
||||
constructor(options: HLSOptions) {
|
||||
private data: Data;
|
||||
constructor(options: HLSOptions) {
|
||||
// check playlist
|
||||
if (!options || !options.m3u8json || !options.m3u8json.segments || options.m3u8json.segments.length === 0) {
|
||||
throw new Error('Playlist is empty!');
|
||||
if (!options || !options.m3u8json || !options.m3u8json.segments || options.m3u8json.segments.length === 0) {
|
||||
throw new Error('Playlist is empty!');
|
||||
}
|
||||
// init options
|
||||
this.data = {
|
||||
parts: {
|
||||
first: options.m3u8json.mediaSequence || 0,
|
||||
total: options.m3u8json.segments.length,
|
||||
completed: 0
|
||||
},
|
||||
m3u8json: options.m3u8json,
|
||||
outputFile: options.output || 'stream.ts',
|
||||
threads: options.threads || 5,
|
||||
retries: options.retries || 4,
|
||||
offset: options.offset || 0,
|
||||
baseurl: options.baseurl,
|
||||
skipInit: options.skipInit,
|
||||
keys: {},
|
||||
timeout: options.timeout ? options.timeout : 60 * 1000,
|
||||
checkPartLength: false,
|
||||
isResume: options.offset ? options.offset > 0 : false,
|
||||
bytesDownloaded: 0,
|
||||
waitTime: options.fsRetryTime ?? 1000 * 5,
|
||||
callback: options.callback,
|
||||
override: options.override,
|
||||
dateStart: 0
|
||||
};
|
||||
}
|
||||
// init options
|
||||
this.data = {
|
||||
parts: {
|
||||
first: options.m3u8json.mediaSequence || 0,
|
||||
total: options.m3u8json.segments.length,
|
||||
completed: 0
|
||||
},
|
||||
m3u8json: options.m3u8json,
|
||||
outputFile: options.output || 'stream.ts',
|
||||
threads: options.threads || 5,
|
||||
retries: options.retries || 4,
|
||||
offset: options.offset || 0,
|
||||
baseurl: options.baseurl,
|
||||
skipInit: options.skipInit,
|
||||
keys: {},
|
||||
timeout: options.timeout ? options.timeout : 60 * 1000,
|
||||
checkPartLength: false,
|
||||
isResume: options.offset ? options.offset > 0 : false,
|
||||
bytesDownloaded: 0,
|
||||
waitTime: options.fsRetryTime ?? 1000 * 5,
|
||||
callback: options.callback,
|
||||
override: options.override,
|
||||
dateStart: 0
|
||||
};
|
||||
}
|
||||
async download() {
|
||||
async download() {
|
||||
// set output
|
||||
const fn = this.data.outputFile;
|
||||
// try load resume file
|
||||
if (fsp.existsSync(fn) && fsp.existsSync(`${fn}.resume`) && this.data.offset < 1) {
|
||||
try {
|
||||
console.info('Resume data found! Trying to resume...');
|
||||
const resumeData = JSON.parse(await fs.readFile(`${fn}.resume`, 'utf-8'));
|
||||
if (resumeData.total == this.data.m3u8json.segments.length && resumeData.completed != resumeData.total && !isNaN(resumeData.completed)) {
|
||||
console.info('Resume data is ok!');
|
||||
this.data.offset = resumeData.completed;
|
||||
this.data.isResume = true;
|
||||
} else {
|
||||
console.warn(' Resume data is wrong!');
|
||||
console.warn({
|
||||
resume: { total: resumeData.total, dled: resumeData.completed },
|
||||
current: { total: this.data.m3u8json.segments.length }
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Resume failed, downloading will be not resumed!');
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
// ask before rewrite file
|
||||
if (fsp.existsSync(`${fn}`) && !this.data.isResume) {
|
||||
let rwts = this.data.override ?? (await Helper.question(`[Q] File «${fn}» already exists! Rewrite? ([y]es/[N]o/[c]ontinue)`));
|
||||
rwts = rwts || 'N';
|
||||
if (['Y', 'y'].includes(rwts[0])) {
|
||||
console.info(`Deleting «${fn}»...`);
|
||||
await fs.unlink(fn);
|
||||
} else if (['C', 'c'].includes(rwts[0])) {
|
||||
return { ok: true, parts: this.data.parts };
|
||||
} else {
|
||||
return { ok: false, parts: this.data.parts };
|
||||
}
|
||||
}
|
||||
// show output filename
|
||||
if (fsp.existsSync(fn) && this.data.isResume) {
|
||||
console.info(`Adding content to «${fn}»...`);
|
||||
} else {
|
||||
console.info(`Saving stream to «${fn}»...`);
|
||||
}
|
||||
// start time
|
||||
this.data.dateStart = Date.now();
|
||||
let segments = this.data.m3u8json.segments;
|
||||
// download init part
|
||||
if (segments[0].map && this.data.offset === 0 && !this.data.skipInit) {
|
||||
console.info('Download and save init part...');
|
||||
const initSeg = segments[0].map as Segment;
|
||||
if (segments[0].key) {
|
||||
initSeg.key = segments[0].key as Key;
|
||||
}
|
||||
try {
|
||||
const initDl = await this.downloadPart(initSeg, 0, 0);
|
||||
await fs.writeFile(fn, initDl.dec, { flag: 'a' });
|
||||
await fs.writeFile(
|
||||
`${fn}.resume`,
|
||||
JSON.stringify({
|
||||
completed: 0,
|
||||
total: this.data.m3u8json.segments.length
|
||||
})
|
||||
);
|
||||
console.info('Init part downloaded.');
|
||||
} catch (e: any) {
|
||||
console.error(`Part init download error:\n\t${e.message}`);
|
||||
return { ok: false, parts: this.data.parts };
|
||||
}
|
||||
} else if (segments[0].map && this.data.offset === 0 && this.data.skipInit) {
|
||||
console.warn('Skipping init part can lead to broken video!');
|
||||
}
|
||||
// resuming ...
|
||||
if (this.data.offset > 0) {
|
||||
segments = segments.slice(this.data.offset);
|
||||
console.info(`Resuming download from part ${this.data.offset + 1}...`);
|
||||
this.data.parts.completed = this.data.offset;
|
||||
}
|
||||
// dl process
|
||||
for (let p = 0; p < segments.length / this.data.threads; p++) {
|
||||
// set offsets
|
||||
const offset = p * this.data.threads;
|
||||
const dlOffset = offset + this.data.threads;
|
||||
// map download threads
|
||||
const krq = new Map(),
|
||||
prq = new Map();
|
||||
const res: any[] = [];
|
||||
let errcnt = 0;
|
||||
for (let px = offset; px < dlOffset && px < segments.length; px++) {
|
||||
const curp = segments[px];
|
||||
const key = curp.key as Key;
|
||||
if (key && !krq.has(key.uri) && !this.data.keys[key.uri as string]) {
|
||||
krq.set(key.uri, this.downloadKey(key, px, this.data.offset));
|
||||
}
|
||||
}
|
||||
try {
|
||||
await Promise.all(krq.values());
|
||||
} catch (er: any) {
|
||||
console.error(`Key ${er.p + 1} download error:\n\t${er.message}`);
|
||||
return { ok: false, parts: this.data.parts };
|
||||
}
|
||||
for (let px = offset; px < dlOffset && px < segments.length; px++) {
|
||||
const curp = segments[px] as Segment;
|
||||
prq.set(px, () => this.downloadPart(curp, px, this.data.offset));
|
||||
}
|
||||
// Parallelized part download with retry logic and optional concurrency limit
|
||||
const maxConcurrency = this.data.threads;
|
||||
const partEntries = [...prq.entries()];
|
||||
let index = 0;
|
||||
|
||||
async function worker(this: hlsDownload) {
|
||||
while (index < partEntries.length) {
|
||||
const i = index++;
|
||||
const [px, downloadFn] = partEntries[i];
|
||||
|
||||
let retriesLeft = this.data.retries;
|
||||
let success = false;
|
||||
while (retriesLeft > 0 && !success) {
|
||||
const fn = this.data.outputFile;
|
||||
// try load resume file
|
||||
if (fsp.existsSync(fn) && fsp.existsSync(`${fn}.resume`) && this.data.offset < 1) {
|
||||
try {
|
||||
const r = await downloadFn();
|
||||
res[px - offset] = r.dec;
|
||||
success = true;
|
||||
console.info('Resume data found! Trying to resume...');
|
||||
const resumeData = JSON.parse(await fs.readFile(`${fn}.resume`, 'utf-8'));
|
||||
if (resumeData.total == this.data.m3u8json.segments.length && resumeData.completed != resumeData.total && !isNaN(resumeData.completed)) {
|
||||
console.info('Resume data is ok!');
|
||||
this.data.offset = resumeData.completed;
|
||||
this.data.isResume = true;
|
||||
} else {
|
||||
console.warn(' Resume data is wrong!');
|
||||
console.warn({
|
||||
resume: { total: resumeData.total, dled: resumeData.completed },
|
||||
current: { total: this.data.m3u8json.segments.length }
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Resume failed, downloading will be not resumed!');
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
// ask before rewrite file
|
||||
if (fsp.existsSync(`${fn}`) && !this.data.isResume) {
|
||||
let rwts = this.data.override ?? (await Helper.question(`[Q] File «${fn}» already exists! Rewrite? ([y]es/[N]o/[c]ontinue)`));
|
||||
rwts = rwts || 'N';
|
||||
if (['Y', 'y'].includes(rwts[0])) {
|
||||
console.info(`Deleting «${fn}»...`);
|
||||
await fs.unlink(fn);
|
||||
} else if (['C', 'c'].includes(rwts[0])) {
|
||||
return { ok: true, parts: this.data.parts };
|
||||
} else {
|
||||
return { ok: false, parts: this.data.parts };
|
||||
}
|
||||
}
|
||||
// show output filename
|
||||
if (fsp.existsSync(fn) && this.data.isResume) {
|
||||
console.info(`Adding content to «${fn}»...`);
|
||||
} else {
|
||||
console.info(`Saving stream to «${fn}»...`);
|
||||
}
|
||||
// start time
|
||||
this.data.dateStart = Date.now();
|
||||
let segments = this.data.m3u8json.segments;
|
||||
// download init part
|
||||
if (segments[0].map && this.data.offset === 0 && !this.data.skipInit) {
|
||||
console.info('Download and save init part...');
|
||||
const initSeg = segments[0].map as Segment;
|
||||
if (segments[0].key) {
|
||||
initSeg.key = segments[0].key as Key;
|
||||
}
|
||||
try {
|
||||
const initDl = await this.downloadPart(initSeg, 0, 0);
|
||||
await fs.writeFile(fn, initDl.dec, { flag: 'a' });
|
||||
await fs.writeFile(
|
||||
`${fn}.resume`,
|
||||
JSON.stringify({
|
||||
completed: 0,
|
||||
total: this.data.m3u8json.segments.length
|
||||
})
|
||||
);
|
||||
console.info('Init part downloaded.');
|
||||
} catch (e: any) {
|
||||
console.error(`Part init download error:\n\t${e.message}`);
|
||||
return { ok: false, parts: this.data.parts };
|
||||
}
|
||||
} else if (segments[0].map && this.data.offset === 0 && this.data.skipInit) {
|
||||
console.warn('Skipping init part can lead to broken video!');
|
||||
}
|
||||
// resuming ...
|
||||
if (this.data.offset > 0) {
|
||||
segments = segments.slice(this.data.offset);
|
||||
console.info(`Resuming download from part ${this.data.offset + 1}...`);
|
||||
this.data.parts.completed = this.data.offset;
|
||||
}
|
||||
// dl process
|
||||
for (let p = 0; p < segments.length / this.data.threads; p++) {
|
||||
// set offsets
|
||||
const offset = p * this.data.threads;
|
||||
const dlOffset = offset + this.data.threads;
|
||||
// map download threads
|
||||
const krq = new Map(),
|
||||
prq = new Map();
|
||||
const res: any[] = [];
|
||||
let errcnt = 0;
|
||||
for (let px = offset; px < dlOffset && px < segments.length; px++) {
|
||||
const curp = segments[px];
|
||||
const key = curp.key as Key;
|
||||
if (key && !krq.has(key.uri) && !this.data.keys[key.uri as string]) {
|
||||
krq.set(key.uri, this.downloadKey(key, px, this.data.offset));
|
||||
}
|
||||
}
|
||||
try {
|
||||
await Promise.all(krq.values());
|
||||
} catch (er: any) {
|
||||
console.error(`Key ${er.p + 1} download error:\n\t${er.message}`);
|
||||
return { ok: false, parts: this.data.parts };
|
||||
}
|
||||
for (let px = offset; px < dlOffset && px < segments.length; px++) {
|
||||
const curp = segments[px] as Segment;
|
||||
prq.set(px, () => this.downloadPart(curp, px, this.data.offset));
|
||||
}
|
||||
// Parallelized part download with retry logic and optional concurrency limit
|
||||
const maxConcurrency = this.data.threads;
|
||||
const partEntries = [...prq.entries()];
|
||||
let index = 0;
|
||||
|
||||
async function worker(this: hlsDownload) {
|
||||
while (index < partEntries.length) {
|
||||
const i = index++;
|
||||
const [px, downloadFn] = partEntries[i];
|
||||
|
||||
let retriesLeft = this.data.retries;
|
||||
let success = false;
|
||||
while (retriesLeft > 0 && !success) {
|
||||
try {
|
||||
const r = await downloadFn();
|
||||
res[px - offset] = r.dec;
|
||||
success = true;
|
||||
} catch (error: any) {
|
||||
retriesLeft--;
|
||||
console.warn(`Retrying part ${error.p + 1 + this.data.offset} (${this.data.retries - retriesLeft}/${this.data.retries})`);
|
||||
if (retriesLeft > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
} else {
|
||||
console.error(`Part ${error.p + 1 + this.data.offset} download failed after ${this.data.retries} retries:\n\t${error.message}`);
|
||||
errcnt++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const workers = [];
|
||||
for (let i = 0; i < maxConcurrency; i++) {
|
||||
workers.push(worker.call(this));
|
||||
}
|
||||
await Promise.all(workers);
|
||||
|
||||
// catch error
|
||||
if (errcnt > 0) {
|
||||
console.error(`${errcnt} parts not downloaded`);
|
||||
return { ok: false, parts: this.data.parts };
|
||||
}
|
||||
// write downloaded
|
||||
for (const r of res) {
|
||||
let error = 0;
|
||||
while (error < 3) {
|
||||
try {
|
||||
await fs.writeFile(fn, r, { flag: 'a' });
|
||||
break;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
console.error(`Unable to write to file '${fn}' (Attempt ${error + 1}/3)`);
|
||||
console.info(`Waiting ${Math.round(this.data.waitTime / 1000)}s before retrying`);
|
||||
await new Promise<void>((resolve) => setTimeout(() => resolve(), this.data.waitTime));
|
||||
}
|
||||
error++;
|
||||
}
|
||||
if (error === 3) {
|
||||
console.error(`Unable to write content to '${fn}'.`);
|
||||
return { ok: false, parts: this.data.parts };
|
||||
}
|
||||
}
|
||||
// log downloaded
|
||||
const totalSeg = segments.length + this.data.offset; // Add the sliced lenght back so the resume data will be correct even if an resumed download fails
|
||||
const downloadedSeg = dlOffset < totalSeg ? dlOffset : totalSeg;
|
||||
this.data.parts.completed = downloadedSeg + this.data.offset;
|
||||
const data = extFn.getDownloadInfo(this.data.dateStart, downloadedSeg, totalSeg, this.data.bytesDownloaded);
|
||||
await fs.writeFile(
|
||||
`${fn}.resume`,
|
||||
JSON.stringify({
|
||||
completed: this.data.parts.completed,
|
||||
total: totalSeg
|
||||
})
|
||||
);
|
||||
function formatDLSpeedB(s: number) {
|
||||
if (s < 1000000) return `${(s / 1000).toFixed(2)} KB/s`;
|
||||
if (s < 1000000000) return `${(s / 1000000).toFixed(2)} MB/s`;
|
||||
return `${(s / 1000000000).toFixed(2)} GB/s`;
|
||||
}
|
||||
function formatDLSpeedBit(s: number) {
|
||||
if (s * 8 < 1000000) return `${(s * 8 / 1000).toFixed(2)} KBit/s`;
|
||||
if (s * 8 < 1000000000) return `${(s * 8 / 1000000).toFixed(2)} MBit/s`;
|
||||
return `${(s * 8 / 1000000000).toFixed(2)} GBit/s`;
|
||||
}
|
||||
console.info(
|
||||
`${downloadedSeg} of ${totalSeg} parts downloaded [${data.percent}%] (${Helper.formatTime(parseInt((data.time / 1000).toFixed(0)))} | ${formatDLSpeedB(data.downloadSpeed)} / ${formatDLSpeedBit(data.downloadSpeed)})`
|
||||
);
|
||||
if (this.data.callback)
|
||||
this.data.callback({
|
||||
total: this.data.parts.total,
|
||||
cur: this.data.parts.completed,
|
||||
bytes: this.data.bytesDownloaded,
|
||||
percent: data.percent,
|
||||
time: data.time,
|
||||
downloadSpeed: data.downloadSpeed
|
||||
});
|
||||
}
|
||||
// return result
|
||||
await fs.unlink(`${fn}.resume`);
|
||||
return { ok: true, parts: this.data.parts };
|
||||
}
|
||||
async downloadPart(seg: Segment, segIndex: number, segOffset: number) {
|
||||
const sURI = extFn.getURI(seg.uri, this.data.baseurl);
|
||||
let decipher, part, dec;
|
||||
const p = segIndex;
|
||||
try {
|
||||
if (seg.key != undefined) {
|
||||
decipher = await this.getKey(seg.key, p, segOffset);
|
||||
}
|
||||
part = await extFn.getData(
|
||||
p,
|
||||
sURI,
|
||||
{
|
||||
...(seg.byterange
|
||||
? {
|
||||
Range: `bytes=${seg.byterange.offset}-${seg.byterange.offset + seg.byterange.length - 1}`
|
||||
}
|
||||
: {})
|
||||
},
|
||||
segOffset,
|
||||
false
|
||||
);
|
||||
// if (this.data.checkPartLength) {
|
||||
// this.data.checkPartLength = false;
|
||||
// console.warn(`Part ${segIndex + segOffset + 1}: can't check parts size!`);
|
||||
// }
|
||||
if (decipher == undefined) {
|
||||
this.data.bytesDownloaded += Buffer.from(part).byteLength;
|
||||
return { dec: Buffer.from(part), p };
|
||||
}
|
||||
dec = decipher.update(Buffer.from(part));
|
||||
dec = Buffer.concat([dec, decipher.final()]);
|
||||
this.data.bytesDownloaded += dec.byteLength;
|
||||
} catch (error: any) {
|
||||
error.p = p;
|
||||
throw error;
|
||||
}
|
||||
return { dec, p };
|
||||
}
|
||||
async downloadKey(key: Key, segIndex: number, segOffset: number) {
|
||||
const kURI = extFn.getURI(key.uri, this.data.baseurl);
|
||||
if (!this.data.keys[kURI]) {
|
||||
try {
|
||||
const rkey = await extFn.getData(segIndex, kURI, {}, segOffset, true);
|
||||
return rkey;
|
||||
} catch (error: any) {
|
||||
retriesLeft--;
|
||||
console.warn(`Retrying part ${error.p + 1 + this.data.offset} (${this.data.retries - retriesLeft}/${this.data.retries})`);
|
||||
if (retriesLeft > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
} else {
|
||||
console.error(`Part ${error.p + 1 + this.data.offset} download failed after ${this.data.retries} retries:\n\t${error.message}`);
|
||||
errcnt++;
|
||||
}
|
||||
error.p = segIndex;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const workers = [];
|
||||
for (let i = 0; i < maxConcurrency; i++) {
|
||||
workers.push(worker.call(this));
|
||||
}
|
||||
await Promise.all(workers);
|
||||
|
||||
// catch error
|
||||
if (errcnt > 0) {
|
||||
console.error(`${errcnt} parts not downloaded`);
|
||||
return { ok: false, parts: this.data.parts };
|
||||
}
|
||||
// write downloaded
|
||||
for (const r of res) {
|
||||
let error = 0;
|
||||
while (error < 3) {
|
||||
try {
|
||||
await fs.writeFile(fn, r, { flag: 'a' });
|
||||
break;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
console.error(`Unable to write to file '${fn}' (Attempt ${error + 1}/3)`);
|
||||
console.info(`Waiting ${Math.round(this.data.waitTime / 1000)}s before retrying`);
|
||||
await new Promise<void>((resolve) => setTimeout(() => resolve(), this.data.waitTime));
|
||||
}
|
||||
error++;
|
||||
}
|
||||
if (error === 3) {
|
||||
console.error(`Unable to write content to '${fn}'.`);
|
||||
return { ok: false, parts: this.data.parts };
|
||||
}
|
||||
}
|
||||
// log downloaded
|
||||
const totalSeg = segments.length + this.data.offset; // Add the sliced lenght back so the resume data will be correct even if an resumed download fails
|
||||
const downloadedSeg = dlOffset < totalSeg ? dlOffset : totalSeg;
|
||||
this.data.parts.completed = downloadedSeg + this.data.offset;
|
||||
const data = extFn.getDownloadInfo(this.data.dateStart, downloadedSeg, totalSeg, this.data.bytesDownloaded);
|
||||
await fs.writeFile(
|
||||
`${fn}.resume`,
|
||||
JSON.stringify({
|
||||
completed: this.data.parts.completed,
|
||||
total: totalSeg
|
||||
})
|
||||
);
|
||||
function formatDLSpeedB(s: number) {
|
||||
if (s < 1000000) return `${(s / 1000).toFixed(2)} KB/s`;
|
||||
if (s < 1000000000) return `${(s / 1000000).toFixed(2)} MB/s`;
|
||||
return `${(s / 1000000000).toFixed(2)} GB/s`;
|
||||
}
|
||||
function formatDLSpeedBit(s: number) {
|
||||
if (s * 8 < 1000000) return `${(s * 8 / 1000).toFixed(2)} KBit/s`;
|
||||
if (s * 8 < 1000000000) return `${(s * 8 / 1000000).toFixed(2)} MBit/s`;
|
||||
return `${(s * 8 / 1000000000).toFixed(2)} GBit/s`;
|
||||
}
|
||||
console.info(
|
||||
`${downloadedSeg} of ${totalSeg} parts downloaded [${data.percent}%] (${Helper.formatTime(parseInt((data.time / 1000).toFixed(0)))} | ${formatDLSpeedB(data.downloadSpeed)} / ${formatDLSpeedBit(data.downloadSpeed)})`
|
||||
);
|
||||
if (this.data.callback)
|
||||
this.data.callback({
|
||||
total: this.data.parts.total,
|
||||
cur: this.data.parts.completed,
|
||||
bytes: this.data.bytesDownloaded,
|
||||
percent: data.percent,
|
||||
time: data.time,
|
||||
downloadSpeed: data.downloadSpeed
|
||||
});
|
||||
}
|
||||
// return result
|
||||
await fs.unlink(`${fn}.resume`);
|
||||
return { ok: true, parts: this.data.parts };
|
||||
}
|
||||
async downloadPart(seg: Segment, segIndex: number, segOffset: number) {
|
||||
const sURI = extFn.getURI(seg.uri, this.data.baseurl);
|
||||
let decipher, part, dec;
|
||||
const p = segIndex;
|
||||
try {
|
||||
if (seg.key != undefined) {
|
||||
decipher = await this.getKey(seg.key, p, segOffset);
|
||||
}
|
||||
part = await extFn.getData(
|
||||
p,
|
||||
sURI,
|
||||
{
|
||||
...(seg.byterange
|
||||
? {
|
||||
Range: `bytes=${seg.byterange.offset}-${seg.byterange.offset + seg.byterange.length - 1}`
|
||||
async getKey(key: Key, segIndex: number, segOffset: number) {
|
||||
const kURI = extFn.getURI(key.uri, this.data.baseurl);
|
||||
const p = segIndex;
|
||||
if (!this.data.keys[kURI]) {
|
||||
try {
|
||||
const rkey = await this.downloadKey(key, segIndex, segOffset);
|
||||
if (!rkey) throw new Error();
|
||||
this.data.keys[kURI] = Buffer.from(rkey);
|
||||
} catch (error: any) {
|
||||
error.p = p;
|
||||
throw error;
|
||||
}
|
||||
: {})
|
||||
},
|
||||
segOffset,
|
||||
false
|
||||
);
|
||||
// if (this.data.checkPartLength) {
|
||||
// this.data.checkPartLength = false;
|
||||
// console.warn(`Part ${segIndex + segOffset + 1}: can't check parts size!`);
|
||||
// }
|
||||
if (decipher == undefined) {
|
||||
this.data.bytesDownloaded += Buffer.from(part).byteLength;
|
||||
return { dec: Buffer.from(part), p };
|
||||
}
|
||||
dec = decipher.update(Buffer.from(part));
|
||||
dec = Buffer.concat([dec, decipher.final()]);
|
||||
this.data.bytesDownloaded += dec.byteLength;
|
||||
} catch (error: any) {
|
||||
error.p = p;
|
||||
throw error;
|
||||
}
|
||||
// get ivs
|
||||
const iv = Buffer.alloc(16);
|
||||
const ivs = key.iv ? key.iv : [0, 0, 0, p + 1];
|
||||
for (let i = 0; i < ivs.length; i++) {
|
||||
iv.writeUInt32BE(ivs[i], i * 4);
|
||||
}
|
||||
return crypto.createDecipheriv('aes-128-cbc', this.data.keys[kURI], iv);
|
||||
}
|
||||
return { dec, p };
|
||||
}
|
||||
async downloadKey(key: Key, segIndex: number, segOffset: number) {
|
||||
const kURI = extFn.getURI(key.uri, this.data.baseurl);
|
||||
if (!this.data.keys[kURI]) {
|
||||
try {
|
||||
const rkey = await extFn.getData(segIndex, kURI, {}, segOffset, true);
|
||||
return rkey;
|
||||
} catch (error: any) {
|
||||
error.p = segIndex;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
async getKey(key: Key, segIndex: number, segOffset: number) {
|
||||
const kURI = extFn.getURI(key.uri, this.data.baseurl);
|
||||
const p = segIndex;
|
||||
if (!this.data.keys[kURI]) {
|
||||
try {
|
||||
const rkey = await this.downloadKey(key, segIndex, segOffset);
|
||||
if (!rkey) throw new Error();
|
||||
this.data.keys[kURI] = Buffer.from(rkey);
|
||||
} catch (error: any) {
|
||||
error.p = p;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// get ivs
|
||||
const iv = Buffer.alloc(16);
|
||||
const ivs = key.iv ? key.iv : [0, 0, 0, p + 1];
|
||||
for (let i = 0; i < ivs.length; i++) {
|
||||
iv.writeUInt32BE(ivs[i], i * 4);
|
||||
}
|
||||
return crypto.createDecipheriv('aes-128-cbc', this.data.keys[kURI], iv);
|
||||
}
|
||||
}
|
||||
|
||||
const extFn = {
|
||||
getURI: (uri: string, baseurl?: string) => {
|
||||
const httpURI = /^https{0,1}:/.test(uri);
|
||||
if (!baseurl && !httpURI) {
|
||||
throw new Error('No base and not http(s) uri');
|
||||
} else if (httpURI) {
|
||||
return uri;
|
||||
}
|
||||
return baseurl + uri;
|
||||
},
|
||||
getDownloadInfo: (dateStart: number, partsDL: number, partsTotal: number, downloadedBytes: number) => {
|
||||
const dateElapsed = Date.now() - dateStart;
|
||||
const percentFxd = parseInt(((partsDL / partsTotal) * 100).toFixed());
|
||||
const percent = percentFxd < 100 ? percentFxd : partsTotal == partsDL ? 100 : 99;
|
||||
const revParts = dateElapsed * (partsTotal / partsDL - 1);
|
||||
const downloadSpeed = downloadedBytes / (dateElapsed / 1000); //Bytes per second
|
||||
return { percent, time: revParts, downloadSpeed };
|
||||
},
|
||||
getData: async (partIndex: number, uri: string, headers: Record<string, string>, segOffset: number, isKey: boolean) => {
|
||||
getURI: (uri: string, baseurl?: string) => {
|
||||
const httpURI = /^https{0,1}:/.test(uri);
|
||||
if (!baseurl && !httpURI) {
|
||||
throw new Error('No base and not http(s) uri');
|
||||
} else if (httpURI) {
|
||||
return uri;
|
||||
}
|
||||
return baseurl + uri;
|
||||
},
|
||||
getDownloadInfo: (dateStart: number, partsDL: number, partsTotal: number, downloadedBytes: number) => {
|
||||
const dateElapsed = Date.now() - dateStart;
|
||||
const percentFxd = parseInt(((partsDL / partsTotal) * 100).toFixed());
|
||||
const percent = percentFxd < 100 ? percentFxd : partsTotal == partsDL ? 100 : 99;
|
||||
const revParts = dateElapsed * (partsTotal / partsDL - 1);
|
||||
const downloadSpeed = downloadedBytes / (dateElapsed / 1000); //Bytes per second
|
||||
return { percent, time: revParts, downloadSpeed };
|
||||
},
|
||||
getData: async (partIndex: number, uri: string, headers: Record<string, string>, segOffset: number, isKey: boolean) => {
|
||||
// get file if uri is local
|
||||
if (uri.startsWith('file://')) {
|
||||
const buffer = await fs.readFile(url.fileURLToPath(uri));
|
||||
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
||||
if (uri.startsWith('file://')) {
|
||||
const buffer = await fs.readFile(url.fileURLToPath(uri));
|
||||
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
||||
}
|
||||
// do request
|
||||
return await ofetch(uri, {
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
responseType: 'arrayBuffer',
|
||||
retry: 0,
|
||||
async onRequestError({ error }) {
|
||||
const partType = isKey ? 'Key' : 'Part';
|
||||
const partIndx = partIndex + 1 + segOffset;
|
||||
console.warn(`%s %s: ${error.message}`, partType, partIndx);
|
||||
}
|
||||
});
|
||||
}
|
||||
// do request
|
||||
return await ofetch(uri, {
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
responseType: 'arrayBuffer',
|
||||
retry: 0,
|
||||
async onRequestError({ error }) {
|
||||
const partType = isKey ? 'Key' : 'Part';
|
||||
const partIndx = partIndex + 1 + segOffset;
|
||||
console.warn(`%s %s: ${error.message}`, partType, partIndx);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default hlsDownload;
|
||||
|
|
|
|||
|
|
@ -7,63 +7,63 @@ const logFolder = path.join(workingDir, 'logs');
|
|||
const latest = path.join(logFolder, 'latest.log');
|
||||
|
||||
const makeLogFolder = () => {
|
||||
if (!fs.existsSync(logFolder))
|
||||
fs.mkdirSync(logFolder);
|
||||
if (fs.existsSync(latest)) {
|
||||
const stats = fs.statSync(latest);
|
||||
fs.renameSync(latest, path.join(logFolder, `${stats.mtimeMs}.log`));
|
||||
}
|
||||
if (!fs.existsSync(logFolder))
|
||||
fs.mkdirSync(logFolder);
|
||||
if (fs.existsSync(latest)) {
|
||||
const stats = fs.statSync(latest);
|
||||
fs.renameSync(latest, path.join(logFolder, `${stats.mtimeMs}.log`));
|
||||
}
|
||||
};
|
||||
|
||||
const makeLogger = () => {
|
||||
global.console.log =
|
||||
global.console.log =
|
||||
global.console.info =
|
||||
global.console.warn =
|
||||
global.console.error =
|
||||
global.console.debug = (...data: any[]) => {
|
||||
console.info((data.length >= 1 ? data.shift() : ''), ...data);
|
||||
console.info((data.length >= 1 ? data.shift() : ''), ...data);
|
||||
};
|
||||
makeLogFolder();
|
||||
log4js.configure({
|
||||
appenders: {
|
||||
console: {
|
||||
type: 'console', layout: {
|
||||
type: 'pattern',
|
||||
pattern: process.env.isGUI === 'true' ? '%[%x{info}%m%]' : '%x{info}%m',
|
||||
tokens: {
|
||||
info: (ev) => {
|
||||
return ev.level.levelStr === 'INFO' ? '' : `[${ev.level.levelStr}] `;
|
||||
makeLogFolder();
|
||||
log4js.configure({
|
||||
appenders: {
|
||||
console: {
|
||||
type: 'console', layout: {
|
||||
type: 'pattern',
|
||||
pattern: process.env.isGUI === 'true' ? '%[%x{info}%m%]' : '%x{info}%m',
|
||||
tokens: {
|
||||
info: (ev) => {
|
||||
return ev.level.levelStr === 'INFO' ? '' : `[${ev.level.levelStr}] `;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
file: {
|
||||
type: 'file',
|
||||
filename: latest,
|
||||
layout: {
|
||||
type: 'pattern',
|
||||
pattern: '%x{info}%m',
|
||||
tokens: {
|
||||
info: (ev) => {
|
||||
return ev.level.levelStr === 'INFO' ? '' : `[${ev.level.levelStr}] `;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
file: {
|
||||
type: 'file',
|
||||
filename: latest,
|
||||
layout: {
|
||||
type: 'pattern',
|
||||
pattern: '%x{info}%m',
|
||||
tokens: {
|
||||
info: (ev) => {
|
||||
return ev.level.levelStr === 'INFO' ? '' : `[${ev.level.levelStr}] `;
|
||||
},
|
||||
categories: {
|
||||
default: {
|
||||
appenders: ['console', 'file'],
|
||||
level: 'all',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
categories: {
|
||||
default: {
|
||||
appenders: ['console', 'file'],
|
||||
level: 'all',
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const getLogger = () => {
|
||||
if (!log4js.isConfigured())
|
||||
makeLogger();
|
||||
return log4js.getLogger();
|
||||
if (!log4js.isConfigured())
|
||||
makeLogger();
|
||||
return log4js.getLogger();
|
||||
};
|
||||
|
||||
export const console = getLogger();
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
// api domains
|
||||
const domain = {
|
||||
cr_www: 'https://www.crunchyroll.com',
|
||||
cr_api: 'https://api.crunchyroll.com',
|
||||
hd_www: 'https://www.hidive.com',
|
||||
hd_api: 'https://api.hidive.com',
|
||||
hd_new: 'https://dce-frontoffice.imggaming.com'
|
||||
cr_www: 'https://www.crunchyroll.com',
|
||||
cr_api: 'https://api.crunchyroll.com',
|
||||
hd_www: 'https://www.hidive.com',
|
||||
hd_api: 'https://api.hidive.com',
|
||||
hd_new: 'https://dce-frontoffice.imggaming.com'
|
||||
};
|
||||
|
||||
export type APIType = {
|
||||
|
|
@ -42,63 +42,63 @@ export type APIType = {
|
|||
};
|
||||
|
||||
const api: APIType = {
|
||||
//
|
||||
//
|
||||
// Crunchyroll
|
||||
// Vilos bundle.js (where we can extract the basic token thats needed for the initial auth)
|
||||
bundlejs: 'https://static.crunchyroll.com/vilos-v2/web/vilos/js/bundle.js',
|
||||
//
|
||||
// Crunchyroll API
|
||||
basic_auth_token: 'Y2I5bnpybWh0MzJ2Z3RleHlna286S1V3bU1qSlh4eHVyc0hJVGQxenZsMkMyeVFhUW84TjQ=',
|
||||
auth: `${domain.cr_www}/auth/v1/token`,
|
||||
me: `${domain.cr_www}/accounts/v1/me`,
|
||||
profile: `${domain.cr_www}/accounts/v1/me/profile`,
|
||||
search: `${domain.cr_www}/content/v2/discover/search`,
|
||||
content_cms: `${domain.cr_www}/content/v2/cms`,
|
||||
browse: `${domain.cr_www}/content/v1/browse`,
|
||||
browse_all_series: `${domain.cr_www}/content/v2/discover/browse`,
|
||||
streaming_sessions: `${domain.cr_www}/playback/v1/sessions/streaming`,
|
||||
drm_widevine: `${domain.cr_www}/license/v1/license/widevine`,
|
||||
drm_playready: `${domain.cr_www}/license/v1/license/playReady`,
|
||||
//
|
||||
// Crunchyroll Bucket
|
||||
cms_bucket: `${domain.cr_www}/cms/v2`,
|
||||
cms_auth: `${domain.cr_www}/index/v2`,
|
||||
//
|
||||
// Crunchyroll Headers
|
||||
crunchyDefUserAgent: 'Crunchyroll/ANDROIDTV/3.45.2_22274 (Android 12; en-US; SHIELD Android TV Build/SR1A.211012.001)',
|
||||
crunchyDefHeader: {},
|
||||
crunchyAuthHeader: {},
|
||||
//
|
||||
//
|
||||
// Hidive
|
||||
// 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`,
|
||||
// Hidive New API
|
||||
hd_new_api: `${domain.hd_new}/api`,
|
||||
hd_new_apiKey: '857a1e5d-e35e-4fdf-805b-a87b6f8364bf',
|
||||
hd_new_version: '6.0.1.bbf09a2'
|
||||
//
|
||||
//
|
||||
// Crunchyroll
|
||||
// Vilos bundle.js (where we can extract the basic token thats needed for the initial auth)
|
||||
bundlejs: 'https://static.crunchyroll.com/vilos-v2/web/vilos/js/bundle.js',
|
||||
//
|
||||
// Crunchyroll API
|
||||
basic_auth_token: 'Y2I5bnpybWh0MzJ2Z3RleHlna286S1V3bU1qSlh4eHVyc0hJVGQxenZsMkMyeVFhUW84TjQ=',
|
||||
auth: `${domain.cr_www}/auth/v1/token`,
|
||||
me: `${domain.cr_www}/accounts/v1/me`,
|
||||
profile: `${domain.cr_www}/accounts/v1/me/profile`,
|
||||
search: `${domain.cr_www}/content/v2/discover/search`,
|
||||
content_cms: `${domain.cr_www}/content/v2/cms`,
|
||||
browse: `${domain.cr_www}/content/v1/browse`,
|
||||
browse_all_series: `${domain.cr_www}/content/v2/discover/browse`,
|
||||
streaming_sessions: `${domain.cr_www}/playback/v1/sessions/streaming`,
|
||||
drm_widevine: `${domain.cr_www}/license/v1/license/widevine`,
|
||||
drm_playready: `${domain.cr_www}/license/v1/license/playReady`,
|
||||
//
|
||||
// Crunchyroll Bucket
|
||||
cms_bucket: `${domain.cr_www}/cms/v2`,
|
||||
cms_auth: `${domain.cr_www}/index/v2`,
|
||||
//
|
||||
// Crunchyroll Headers
|
||||
crunchyDefUserAgent: 'Crunchyroll/ANDROIDTV/3.45.2_22274 (Android 12; en-US; SHIELD Android TV Build/SR1A.211012.001)',
|
||||
crunchyDefHeader: {},
|
||||
crunchyAuthHeader: {},
|
||||
//
|
||||
//
|
||||
// Hidive
|
||||
// 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`,
|
||||
// Hidive New API
|
||||
hd_new_api: `${domain.hd_new}/api`,
|
||||
hd_new_apiKey: '857a1e5d-e35e-4fdf-805b-a87b6f8364bf',
|
||||
hd_new_version: '6.0.1.bbf09a2'
|
||||
};
|
||||
|
||||
api.crunchyDefHeader = {
|
||||
'User-Agent': api.crunchyDefUserAgent,
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip',
|
||||
Connection: 'Keep-Alive',
|
||||
Host: 'www.crunchyroll.com'
|
||||
'User-Agent': api.crunchyDefUserAgent,
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip',
|
||||
Connection: 'Keep-Alive',
|
||||
Host: 'www.crunchyroll.com'
|
||||
};
|
||||
|
||||
// set header
|
||||
api.crunchyAuthHeader = {
|
||||
Authorization: `Basic ${api.basic_auth_token}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
|
||||
'Request-Type': 'SignIn',
|
||||
...api.crunchyDefHeader
|
||||
Authorization: `Basic ${api.basic_auth_token}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
|
||||
'Request-Type': 'SignIn',
|
||||
...api.crunchyDefHeader
|
||||
};
|
||||
|
||||
export { domain, api };
|
||||
|
|
|
|||
|
|
@ -94,91 +94,91 @@ export type ArgvType = typeof argvC;
|
|||
const appArgv = (cfg: {
|
||||
[key: string]: unknown
|
||||
}, isGUI = false) => {
|
||||
if (argvC)
|
||||
return argvC;
|
||||
yargs(process.argv.slice(2));
|
||||
const argv = getArgv(cfg, isGUI)
|
||||
.parseSync();
|
||||
argvC = argv;
|
||||
return argv;
|
||||
if (argvC)
|
||||
return argvC;
|
||||
yargs(process.argv.slice(2));
|
||||
const argv = getArgv(cfg, isGUI)
|
||||
.parseSync();
|
||||
argvC = argv;
|
||||
return argv;
|
||||
};
|
||||
|
||||
|
||||
const overrideArguments = (cfg: { [key:string]: unknown }, override: Partial<typeof argvC>, isGUI = false) => {
|
||||
const argv = getArgv(cfg, isGUI).middleware((ar) => {
|
||||
for (const key of Object.keys(override)) {
|
||||
ar[key] = override[key];
|
||||
}
|
||||
}).parseSync();
|
||||
argvC = argv;
|
||||
const argv = getArgv(cfg, isGUI).middleware((ar) => {
|
||||
for (const key of Object.keys(override)) {
|
||||
ar[key] = override[key];
|
||||
}
|
||||
}).parseSync();
|
||||
argvC = argv;
|
||||
};
|
||||
|
||||
export {
|
||||
appArgv,
|
||||
overrideArguments
|
||||
appArgv,
|
||||
overrideArguments
|
||||
};
|
||||
|
||||
const getArgv = (cfg: { [key:string]: unknown }, isGUI: boolean) => {
|
||||
const parseDefault = <T = unknown>(key: string, _default: T) : T=> {
|
||||
if (Object.prototype.hasOwnProperty.call(cfg, key)) {
|
||||
return cfg[key] as T;
|
||||
} else
|
||||
return _default;
|
||||
};
|
||||
const argv = yargs.parserConfiguration({
|
||||
'duplicate-arguments-array': false,
|
||||
'camel-case-expansion': false,
|
||||
})
|
||||
.wrap(yargs.terminalWidth())
|
||||
.usage('Usage: $0 [options]')
|
||||
.help(true);
|
||||
const parseDefault = <T = unknown>(key: string, _default: T) : T=> {
|
||||
if (Object.prototype.hasOwnProperty.call(cfg, key)) {
|
||||
return cfg[key] as T;
|
||||
} else
|
||||
return _default;
|
||||
};
|
||||
const argv = yargs.parserConfiguration({
|
||||
'duplicate-arguments-array': false,
|
||||
'camel-case-expansion': false,
|
||||
})
|
||||
.wrap(yargs.terminalWidth())
|
||||
.usage('Usage: $0 [options]')
|
||||
.help(true);
|
||||
//.strictOptions()
|
||||
const data = args.map(a => {
|
||||
return {
|
||||
...a,
|
||||
demandOption: !isGUI && a.demandOption,
|
||||
group: groups[a.group],
|
||||
default: typeof a.default === 'object' && !Array.isArray(a.default) ?
|
||||
parseDefault((a.default as any).name || a.name, (a.default as any).default) : a.default
|
||||
};
|
||||
});
|
||||
for (const item of data)
|
||||
argv.option(item.name, {
|
||||
...item,
|
||||
coerce: (value) => {
|
||||
if (item.transformer) {
|
||||
return item.transformer(value);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
},
|
||||
choices: item.name === 'service' && isGUI ? undefined : item.choices as unknown as Choices
|
||||
const data = args.map(a => {
|
||||
return {
|
||||
...a,
|
||||
demandOption: !isGUI && a.demandOption,
|
||||
group: groups[a.group],
|
||||
default: typeof a.default === 'object' && !Array.isArray(a.default) ?
|
||||
parseDefault((a.default as any).name || a.name, (a.default as any).default) : a.default
|
||||
};
|
||||
});
|
||||
for (const item of data)
|
||||
argv.option(item.name, {
|
||||
...item,
|
||||
coerce: (value) => {
|
||||
if (item.transformer) {
|
||||
return item.transformer(value);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
},
|
||||
choices: item.name === 'service' && isGUI ? undefined : item.choices as unknown as Choices
|
||||
});
|
||||
|
||||
// Custom logic for suggesting corrections for misspelled options
|
||||
argv.middleware((argv: Record<string, any>) => {
|
||||
// Custom logic for suggesting corrections for misspelled options
|
||||
argv.middleware((argv: Record<string, any>) => {
|
||||
// List of valid options
|
||||
const validOptions = [
|
||||
...args.map(a => a.name),
|
||||
...args.map(a => a.alias).filter(alias => alias !== undefined) as string[]
|
||||
];
|
||||
const unknownOptions = Object.keys(argv).filter(key => !validOptions.includes(key) && key !== '_' && key !== '$0'); // Filter out known options
|
||||
const validOptions = [
|
||||
...args.map(a => a.name),
|
||||
...args.map(a => a.alias).filter(alias => alias !== undefined) as string[]
|
||||
];
|
||||
const unknownOptions = Object.keys(argv).filter(key => !validOptions.includes(key) && key !== '_' && key !== '$0'); // Filter out known options
|
||||
|
||||
const suggestedOptions: Record<string, boolean> = {};
|
||||
unknownOptions.forEach(actualOption => {
|
||||
const closestOption = validOptions.find(option => {
|
||||
const levenVal = leven(option, actualOption);
|
||||
return levenVal <= 2 && levenVal > 0;
|
||||
});
|
||||
const suggestedOptions: Record<string, boolean> = {};
|
||||
unknownOptions.forEach(actualOption => {
|
||||
const closestOption = validOptions.find(option => {
|
||||
const levenVal = leven(option, actualOption);
|
||||
return levenVal <= 2 && levenVal > 0;
|
||||
});
|
||||
|
||||
if (closestOption && !suggestedOptions[closestOption]) {
|
||||
suggestedOptions[closestOption] = true;
|
||||
console.info(`Unknown option ${actualOption}, did you mean ${closestOption}?`);
|
||||
} else if (!suggestedOptions[actualOption]) {
|
||||
suggestedOptions[actualOption] = true;
|
||||
console.info(`Unknown option ${actualOption}`);
|
||||
}
|
||||
if (closestOption && !suggestedOptions[closestOption]) {
|
||||
suggestedOptions[closestOption] = true;
|
||||
console.info(`Unknown option ${actualOption}, did you mean ${closestOption}?`);
|
||||
} else if (!suggestedOptions[actualOption]) {
|
||||
suggestedOptions[actualOption] = true;
|
||||
console.info(`Unknown option ${actualOption}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
return argv as unknown as yargs.Argv<typeof argvC>;
|
||||
return argv as unknown as yargs.Argv<typeof argvC>;
|
||||
};
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -18,45 +18,45 @@ const guiCfgFile = path.join(workingDir, 'config', 'gui');
|
|||
const cliCfgFile = path.join(workingDir, 'config', 'cli-defaults');
|
||||
const hdPflCfgFile = path.join(workingDir, 'config', 'hd_profile');
|
||||
const sessCfgFile = {
|
||||
cr: path.join(workingDir, 'config', 'cr_sess'),
|
||||
hd: path.join(workingDir, 'config', 'hd_sess'),
|
||||
ao: path.join(workingDir, 'config', 'ao_sess'),
|
||||
adn: path.join(workingDir, 'config', 'adn_sess')
|
||||
cr: path.join(workingDir, 'config', 'cr_sess'),
|
||||
hd: path.join(workingDir, 'config', 'hd_sess'),
|
||||
ao: path.join(workingDir, 'config', 'ao_sess'),
|
||||
adn: path.join(workingDir, 'config', 'adn_sess')
|
||||
};
|
||||
const stateFile = path.join(workingDir, 'config', 'guistate');
|
||||
const tokenFile = {
|
||||
cr: path.join(workingDir, 'config', 'cr_token'),
|
||||
hd: path.join(workingDir, 'config', 'hd_token'),
|
||||
hdNew:path.join(workingDir, 'config', 'hd_new_token'),
|
||||
ao: path.join(workingDir, 'config', 'ao_token'),
|
||||
adn: path.join(workingDir, 'config', 'adn_token')
|
||||
cr: path.join(workingDir, 'config', 'cr_token'),
|
||||
hd: path.join(workingDir, 'config', 'hd_token'),
|
||||
hdNew:path.join(workingDir, 'config', 'hd_new_token'),
|
||||
ao: path.join(workingDir, 'config', 'ao_token'),
|
||||
adn: path.join(workingDir, 'config', 'adn_token')
|
||||
};
|
||||
|
||||
export const ensureConfig = () => {
|
||||
if (!fs.existsSync(path.join(workingDir, 'config')))
|
||||
fs.mkdirSync(path.join(workingDir, 'config'));
|
||||
if (process.env.contentDirectory)
|
||||
[binCfgFile, dirCfgFile, cliCfgFile, guiCfgFile].forEach(a => {
|
||||
if (!fs.existsSync(`${a}.yml`))
|
||||
fs.copyFileSync(path.join(__dirname, '..', 'config', `${path.basename(a)}.yml`), `${a}.yml`);
|
||||
});
|
||||
if (!fs.existsSync(path.join(workingDir, 'config')))
|
||||
fs.mkdirSync(path.join(workingDir, 'config'));
|
||||
if (process.env.contentDirectory)
|
||||
[binCfgFile, dirCfgFile, cliCfgFile, guiCfgFile].forEach(a => {
|
||||
if (!fs.existsSync(`${a}.yml`))
|
||||
fs.copyFileSync(path.join(__dirname, '..', 'config', `${path.basename(a)}.yml`), `${a}.yml`);
|
||||
});
|
||||
};
|
||||
|
||||
const loadYamlCfgFile = <T extends Record<string, any>>(file: string, isSess?: boolean): T => {
|
||||
if(fs.existsSync(`${file}.user.yml`) && !isSess){
|
||||
file += '.user';
|
||||
}
|
||||
file += '.yml';
|
||||
if(fs.existsSync(file)){
|
||||
try{
|
||||
return yaml.parse(fs.readFileSync(file, 'utf8'));
|
||||
if(fs.existsSync(`${file}.user.yml`) && !isSess){
|
||||
file += '.user';
|
||||
}
|
||||
catch(e){
|
||||
console.error('[ERROR]', e);
|
||||
return {} as T;
|
||||
file += '.yml';
|
||||
if(fs.existsSync(file)){
|
||||
try{
|
||||
return yaml.parse(fs.readFileSync(file, 'utf8'));
|
||||
}
|
||||
catch(e){
|
||||
console.error('[ERROR]', e);
|
||||
return {} as T;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {} as T;
|
||||
return {} as T;
|
||||
};
|
||||
|
||||
export type WriteObjects = {
|
||||
|
|
@ -64,10 +64,10 @@ export type WriteObjects = {
|
|||
}
|
||||
|
||||
const writeYamlCfgFile = <T extends keyof WriteObjects>(file: T, data: WriteObjects[T]) => {
|
||||
const fn = path.join(workingDir, 'config', `${file}.yml`);
|
||||
if (fs.existsSync(fn))
|
||||
fs.removeSync(fn);
|
||||
fs.writeFileSync(fn, yaml.stringify(data));
|
||||
const fn = path.join(workingDir, 'config', `${file}.yml`);
|
||||
if (fs.existsSync(fn))
|
||||
fs.removeSync(fn);
|
||||
fs.writeFileSync(fn, yaml.stringify(data));
|
||||
};
|
||||
|
||||
export type GUIConfig = {
|
||||
|
|
@ -96,317 +96,317 @@ export type ConfigObject = {
|
|||
}
|
||||
|
||||
const loadCfg = () : ConfigObject => {
|
||||
// load cfgs
|
||||
const defaultCfg: ConfigObject = {
|
||||
bin: {},
|
||||
dir: loadYamlCfgFile<{
|
||||
// load cfgs
|
||||
const defaultCfg: ConfigObject = {
|
||||
bin: {},
|
||||
dir: loadYamlCfgFile<{
|
||||
content: string,
|
||||
trash: string,
|
||||
fonts: string
|
||||
config: string
|
||||
}>(dirCfgFile),
|
||||
cli: loadYamlCfgFile<{
|
||||
cli: loadYamlCfgFile<{
|
||||
[key: string]: any
|
||||
}>(cliCfgFile),
|
||||
gui: loadYamlCfgFile<GUIConfig>(guiCfgFile)
|
||||
};
|
||||
const defaultDirs = {
|
||||
fonts: '${wdir}/fonts/',
|
||||
content: '${wdir}/videos/',
|
||||
trash: '${wdir}/videos/_trash/',
|
||||
config: '${wdir}/config'
|
||||
};
|
||||
if (typeof defaultCfg.dir !== 'object' || defaultCfg.dir === null || Array.isArray(defaultCfg.dir)) {
|
||||
defaultCfg.dir = defaultDirs;
|
||||
}
|
||||
gui: loadYamlCfgFile<GUIConfig>(guiCfgFile)
|
||||
};
|
||||
const defaultDirs = {
|
||||
fonts: '${wdir}/fonts/',
|
||||
content: '${wdir}/videos/',
|
||||
trash: '${wdir}/videos/_trash/',
|
||||
config: '${wdir}/config'
|
||||
};
|
||||
if (typeof defaultCfg.dir !== 'object' || defaultCfg.dir === null || Array.isArray(defaultCfg.dir)) {
|
||||
defaultCfg.dir = defaultDirs;
|
||||
}
|
||||
|
||||
const keys = Object.keys(defaultDirs) as (keyof typeof defaultDirs)[];
|
||||
for (const key of keys) {
|
||||
if (!Object.prototype.hasOwnProperty.call(defaultCfg.dir, key) || typeof defaultCfg.dir[key] !== 'string') {
|
||||
defaultCfg.dir[key] = defaultDirs[key];
|
||||
const keys = Object.keys(defaultDirs) as (keyof typeof defaultDirs)[];
|
||||
for (const key of keys) {
|
||||
if (!Object.prototype.hasOwnProperty.call(defaultCfg.dir, key) || typeof defaultCfg.dir[key] !== 'string') {
|
||||
defaultCfg.dir[key] = defaultDirs[key];
|
||||
}
|
||||
if (!path.isAbsolute(defaultCfg.dir[key])) {
|
||||
defaultCfg.dir[key] = path.join(workingDir, defaultCfg.dir[key].replace(/^\${wdir}/, ''));
|
||||
}
|
||||
}
|
||||
if (!path.isAbsolute(defaultCfg.dir[key])) {
|
||||
defaultCfg.dir[key] = path.join(workingDir, defaultCfg.dir[key].replace(/^\${wdir}/, ''));
|
||||
if(!fs.existsSync(defaultCfg.dir.content)){
|
||||
try{
|
||||
fs.ensureDirSync(defaultCfg.dir.content);
|
||||
}
|
||||
catch(e){
|
||||
console.error('Content directory not accessible!');
|
||||
return defaultCfg;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!fs.existsSync(defaultCfg.dir.content)){
|
||||
try{
|
||||
fs.ensureDirSync(defaultCfg.dir.content);
|
||||
if(!fs.existsSync(defaultCfg.dir.trash)){
|
||||
defaultCfg.dir.trash = defaultCfg.dir.content;
|
||||
}
|
||||
catch(e){
|
||||
console.error('Content directory not accessible!');
|
||||
return defaultCfg;
|
||||
}
|
||||
}
|
||||
if(!fs.existsSync(defaultCfg.dir.trash)){
|
||||
defaultCfg.dir.trash = defaultCfg.dir.content;
|
||||
}
|
||||
// output
|
||||
return defaultCfg;
|
||||
// output
|
||||
return defaultCfg;
|
||||
};
|
||||
|
||||
const loadBinCfg = async () => {
|
||||
const binCfg = loadYamlCfgFile<ConfigObject['bin']>(binCfgFile);
|
||||
// binaries
|
||||
const defaultBin = {
|
||||
ffmpeg: 'ffmpeg',
|
||||
mkvmerge: 'mkvmerge',
|
||||
ffprobe: 'ffprobe',
|
||||
mp4decrypt: 'mp4decrypt',
|
||||
shaka: 'shaka-packager'
|
||||
};
|
||||
const keys = Object.keys(defaultBin) as (keyof typeof defaultBin)[];
|
||||
for(const dir of keys){
|
||||
if(!Object.prototype.hasOwnProperty.call(binCfg, dir) || typeof binCfg[dir] != 'string'){
|
||||
binCfg[dir] = defaultBin[dir];
|
||||
const binCfg = loadYamlCfgFile<ConfigObject['bin']>(binCfgFile);
|
||||
// binaries
|
||||
const defaultBin = {
|
||||
ffmpeg: 'ffmpeg',
|
||||
mkvmerge: 'mkvmerge',
|
||||
ffprobe: 'ffprobe',
|
||||
mp4decrypt: 'mp4decrypt',
|
||||
shaka: 'shaka-packager'
|
||||
};
|
||||
const keys = Object.keys(defaultBin) as (keyof typeof defaultBin)[];
|
||||
for(const dir of keys){
|
||||
if(!Object.prototype.hasOwnProperty.call(binCfg, dir) || typeof binCfg[dir] != 'string'){
|
||||
binCfg[dir] = defaultBin[dir];
|
||||
}
|
||||
if ((binCfg[dir] as string).match(/^\${wdir}/)) {
|
||||
binCfg[dir] = (binCfg[dir] as string).replace(/^\${wdir}/, '');
|
||||
binCfg[dir] = path.join(workingDir, binCfg[dir] as string);
|
||||
}
|
||||
if (!path.isAbsolute(binCfg[dir] as string)){
|
||||
binCfg[dir] = path.join(workingDir, binCfg[dir] as string);
|
||||
}
|
||||
binCfg[dir] = await lookpath(binCfg[dir] as string);
|
||||
binCfg[dir] = binCfg[dir] ? binCfg[dir] : undefined;
|
||||
if(!binCfg[dir]){
|
||||
const binFile = await lookpath(path.basename(defaultBin[dir]));
|
||||
binCfg[dir] = binFile ? binFile : binCfg[dir];
|
||||
}
|
||||
}
|
||||
if ((binCfg[dir] as string).match(/^\${wdir}/)) {
|
||||
binCfg[dir] = (binCfg[dir] as string).replace(/^\${wdir}/, '');
|
||||
binCfg[dir] = path.join(workingDir, binCfg[dir] as string);
|
||||
}
|
||||
if (!path.isAbsolute(binCfg[dir] as string)){
|
||||
binCfg[dir] = path.join(workingDir, binCfg[dir] as string);
|
||||
}
|
||||
binCfg[dir] = await lookpath(binCfg[dir] as string);
|
||||
binCfg[dir] = binCfg[dir] ? binCfg[dir] : undefined;
|
||||
if(!binCfg[dir]){
|
||||
const binFile = await lookpath(path.basename(defaultBin[dir]));
|
||||
binCfg[dir] = binFile ? binFile : binCfg[dir];
|
||||
}
|
||||
}
|
||||
return binCfg;
|
||||
return binCfg;
|
||||
};
|
||||
|
||||
const loadCRSession = () => {
|
||||
let session = loadYamlCfgFile(sessCfgFile.cr, 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] = {};
|
||||
let session = loadYamlCfgFile(sessCfgFile.cr, true);
|
||||
if(typeof session !== 'object' || session === null || Array.isArray(session)){
|
||||
session = {};
|
||||
}
|
||||
}
|
||||
return 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 saveCRSession = (data: Record<string, unknown>) => {
|
||||
const cfgFolder = path.dirname(sessCfgFile.cr);
|
||||
try{
|
||||
fs.ensureDirSync(cfgFolder);
|
||||
fs.writeFileSync(`${sessCfgFile.cr}.yml`, yaml.stringify(data));
|
||||
}
|
||||
catch(e){
|
||||
console.error('Can\'t save session file to disk!');
|
||||
}
|
||||
const cfgFolder = path.dirname(sessCfgFile.cr);
|
||||
try{
|
||||
fs.ensureDirSync(cfgFolder);
|
||||
fs.writeFileSync(`${sessCfgFile.cr}.yml`, yaml.stringify(data));
|
||||
}
|
||||
catch(e){
|
||||
console.error('Can\'t save session file to disk!');
|
||||
}
|
||||
};
|
||||
|
||||
const loadCRToken = () => {
|
||||
let token = loadYamlCfgFile(tokenFile.cr, true);
|
||||
if(typeof token !== 'object' || token === null || Array.isArray(token)){
|
||||
token = {};
|
||||
}
|
||||
return token;
|
||||
let token = loadYamlCfgFile(tokenFile.cr, true);
|
||||
if(typeof token !== 'object' || token === null || Array.isArray(token)){
|
||||
token = {};
|
||||
}
|
||||
return token;
|
||||
};
|
||||
|
||||
const saveCRToken = (data: Record<string, unknown>) => {
|
||||
const cfgFolder = path.dirname(tokenFile.cr);
|
||||
try{
|
||||
fs.ensureDirSync(cfgFolder);
|
||||
fs.writeFileSync(`${tokenFile.cr}.yml`, yaml.stringify(data));
|
||||
}
|
||||
catch(e){
|
||||
console.error('Can\'t save token file to disk!');
|
||||
}
|
||||
const cfgFolder = path.dirname(tokenFile.cr);
|
||||
try{
|
||||
fs.ensureDirSync(cfgFolder);
|
||||
fs.writeFileSync(`${tokenFile.cr}.yml`, yaml.stringify(data));
|
||||
}
|
||||
catch(e){
|
||||
console.error('Can\'t save token file to disk!');
|
||||
}
|
||||
};
|
||||
|
||||
const loadADNToken = () => {
|
||||
let token = loadYamlCfgFile(tokenFile.adn, true);
|
||||
if(typeof token !== 'object' || token === null || Array.isArray(token)){
|
||||
token = {};
|
||||
}
|
||||
return token;
|
||||
let token = loadYamlCfgFile(tokenFile.adn, true);
|
||||
if(typeof token !== 'object' || token === null || Array.isArray(token)){
|
||||
token = {};
|
||||
}
|
||||
return token;
|
||||
};
|
||||
|
||||
const saveADNToken = (data: Record<string, unknown>) => {
|
||||
const cfgFolder = path.dirname(tokenFile.adn);
|
||||
try{
|
||||
fs.ensureDirSync(cfgFolder);
|
||||
fs.writeFileSync(`${tokenFile.adn}.yml`, yaml.stringify(data));
|
||||
}
|
||||
catch(e){
|
||||
console.error('Can\'t save token file to disk!');
|
||||
}
|
||||
const cfgFolder = path.dirname(tokenFile.adn);
|
||||
try{
|
||||
fs.ensureDirSync(cfgFolder);
|
||||
fs.writeFileSync(`${tokenFile.adn}.yml`, yaml.stringify(data));
|
||||
}
|
||||
catch(e){
|
||||
console.error('Can\'t save token file to disk!');
|
||||
}
|
||||
};
|
||||
|
||||
const loadAOToken = () => {
|
||||
let token = loadYamlCfgFile(tokenFile.ao, true);
|
||||
if(typeof token !== 'object' || token === null || Array.isArray(token)){
|
||||
token = {};
|
||||
}
|
||||
return token;
|
||||
let token = loadYamlCfgFile(tokenFile.ao, true);
|
||||
if(typeof token !== 'object' || token === null || Array.isArray(token)){
|
||||
token = {};
|
||||
}
|
||||
return token;
|
||||
};
|
||||
|
||||
const saveAOToken = (data: Record<string, unknown>) => {
|
||||
const cfgFolder = path.dirname(tokenFile.ao);
|
||||
try{
|
||||
fs.ensureDirSync(cfgFolder);
|
||||
fs.writeFileSync(`${tokenFile.ao}.yml`, yaml.stringify(data));
|
||||
}
|
||||
catch(e){
|
||||
console.error('Can\'t save token file to disk!');
|
||||
}
|
||||
const cfgFolder = path.dirname(tokenFile.ao);
|
||||
try{
|
||||
fs.ensureDirSync(cfgFolder);
|
||||
fs.writeFileSync(`${tokenFile.ao}.yml`, yaml.stringify(data));
|
||||
}
|
||||
catch(e){
|
||||
console.error('Can\'t save token file to disk!');
|
||||
}
|
||||
};
|
||||
|
||||
const loadHDSession = () => {
|
||||
let session = loadYamlCfgFile(sessCfgFile.hd, true);
|
||||
if(typeof session !== 'object' || session === null || Array.isArray(session)){
|
||||
session = {};
|
||||
}
|
||||
for(const cv of Object.keys(session)){
|
||||
if(typeof session[cv] !== 'object' || session[cv] === null || Array.isArray(session[cv])){
|
||||
session[cv] = {};
|
||||
let session = loadYamlCfgFile(sessCfgFile.hd, true);
|
||||
if(typeof session !== 'object' || session === null || Array.isArray(session)){
|
||||
session = {};
|
||||
}
|
||||
}
|
||||
return 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 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.hd, true);
|
||||
if(typeof token !== 'object' || token === null || Array.isArray(token)){
|
||||
token = {};
|
||||
}
|
||||
return token;
|
||||
let token = loadYamlCfgFile(tokenFile.hd, 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 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(hdPflCfgFile);
|
||||
try{
|
||||
fs.ensureDirSync(cfgFolder);
|
||||
fs.writeFileSync(`${hdPflCfgFile}.yml`, yaml.stringify(data));
|
||||
}
|
||||
catch(e){
|
||||
console.error('Can\'t save profile file to disk!');
|
||||
}
|
||||
const cfgFolder = path.dirname(hdPflCfgFile);
|
||||
try{
|
||||
fs.ensureDirSync(cfgFolder);
|
||||
fs.writeFileSync(`${hdPflCfgFile}.yml`, yaml.stringify(data));
|
||||
}
|
||||
catch(e){
|
||||
console.error('Can\'t save profile file to disk!');
|
||||
}
|
||||
};
|
||||
|
||||
const loadHDProfile = () => {
|
||||
let profile = loadYamlCfgFile(hdPflCfgFile, 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;
|
||||
let profile = loadYamlCfgFile(hdPflCfgFile, 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 loadNewHDToken = () => {
|
||||
let token = loadYamlCfgFile(tokenFile.hdNew, true);
|
||||
if(typeof token !== 'object' || token === null || Array.isArray(token)){
|
||||
token = {};
|
||||
}
|
||||
return token;
|
||||
let token = loadYamlCfgFile(tokenFile.hdNew, true);
|
||||
if(typeof token !== 'object' || token === null || Array.isArray(token)){
|
||||
token = {};
|
||||
}
|
||||
return token;
|
||||
};
|
||||
|
||||
const saveNewHDToken = (data: Record<string, unknown>) => {
|
||||
const cfgFolder = path.dirname(tokenFile.hdNew);
|
||||
try{
|
||||
fs.ensureDirSync(cfgFolder);
|
||||
fs.writeFileSync(`${tokenFile.hdNew}.yml`, yaml.stringify(data));
|
||||
}
|
||||
catch(e){
|
||||
console.error('Can\'t save token file to disk!');
|
||||
}
|
||||
const cfgFolder = path.dirname(tokenFile.hdNew);
|
||||
try{
|
||||
fs.ensureDirSync(cfgFolder);
|
||||
fs.writeFileSync(`${tokenFile.hdNew}.yml`, yaml.stringify(data));
|
||||
}
|
||||
catch(e){
|
||||
console.error('Can\'t save token file to disk!');
|
||||
}
|
||||
};
|
||||
|
||||
const cfgDir = path.join(workingDir, 'config');
|
||||
|
||||
const getState = (): GuiState => {
|
||||
const fn = `${stateFile}.json`;
|
||||
if (!fs.existsSync(fn)) {
|
||||
return {
|
||||
'setup': false,
|
||||
'services': {}
|
||||
};
|
||||
}
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(fn).toString());
|
||||
} catch(e) {
|
||||
console.error('Invalid state file, regenerating');
|
||||
return {
|
||||
'setup': false,
|
||||
'services': {}
|
||||
};
|
||||
}
|
||||
const fn = `${stateFile}.json`;
|
||||
if (!fs.existsSync(fn)) {
|
||||
return {
|
||||
'setup': false,
|
||||
'services': {}
|
||||
};
|
||||
}
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(fn).toString());
|
||||
} catch(e) {
|
||||
console.error('Invalid state file, regenerating');
|
||||
return {
|
||||
'setup': false,
|
||||
'services': {}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const setState = (state: GuiState) => {
|
||||
const fn = `${stateFile}.json`;
|
||||
try {
|
||||
fs.writeFileSync(fn, JSON.stringify(state, null, 2));
|
||||
} catch(e) {
|
||||
console.error('Failed to write state file.');
|
||||
}
|
||||
const fn = `${stateFile}.json`;
|
||||
try {
|
||||
fs.writeFileSync(fn, JSON.stringify(state, null, 2));
|
||||
} catch(e) {
|
||||
console.error('Failed to write state file.');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export {
|
||||
loadBinCfg,
|
||||
loadCfg,
|
||||
saveCRSession,
|
||||
loadCRSession,
|
||||
saveCRToken,
|
||||
loadCRToken,
|
||||
saveADNToken,
|
||||
loadADNToken,
|
||||
saveHDSession,
|
||||
loadHDSession,
|
||||
saveHDToken,
|
||||
loadHDToken,
|
||||
saveNewHDToken,
|
||||
loadNewHDToken,
|
||||
saveHDProfile,
|
||||
loadHDProfile,
|
||||
saveAOToken,
|
||||
loadAOToken,
|
||||
getState,
|
||||
setState,
|
||||
writeYamlCfgFile,
|
||||
sessCfgFile,
|
||||
hdPflCfgFile,
|
||||
cfgDir
|
||||
loadBinCfg,
|
||||
loadCfg,
|
||||
saveCRSession,
|
||||
loadCRSession,
|
||||
saveCRToken,
|
||||
loadCRToken,
|
||||
saveADNToken,
|
||||
loadADNToken,
|
||||
saveHDSession,
|
||||
loadHDSession,
|
||||
saveHDToken,
|
||||
loadHDToken,
|
||||
saveNewHDToken,
|
||||
loadNewHDToken,
|
||||
saveHDProfile,
|
||||
loadHDProfile,
|
||||
saveAOToken,
|
||||
loadAOToken,
|
||||
getState,
|
||||
setState,
|
||||
writeYamlCfgFile,
|
||||
sessCfgFile,
|
||||
hdPflCfgFile,
|
||||
cfgDir
|
||||
};
|
||||
|
|
@ -1,26 +1,26 @@
|
|||
const parse = (data: string) => {
|
||||
const res: Record<string, {
|
||||
const res: Record<string, {
|
||||
value: string,
|
||||
expires: Date,
|
||||
path: string,
|
||||
domain: string,
|
||||
secure: boolean
|
||||
}> = {};
|
||||
const split = data.replace(/\r/g,'').split('\n');
|
||||
for (const line of split) {
|
||||
const c = line.split('\t');
|
||||
if(c.length < 7){
|
||||
continue;
|
||||
const split = data.replace(/\r/g,'').split('\n');
|
||||
for (const line of split) {
|
||||
const c = line.split('\t');
|
||||
if(c.length < 7){
|
||||
continue;
|
||||
}
|
||||
res[c[5]] = {
|
||||
value: c[6],
|
||||
expires: new Date(parseInt(c[4])*1000),
|
||||
path: c[2],
|
||||
domain: c[0].replace(/^\./,''),
|
||||
secure: c[3] == 'TRUE' ? true : false
|
||||
};
|
||||
}
|
||||
res[c[5]] = {
|
||||
value: c[6],
|
||||
expires: new Date(parseInt(c[4])*1000),
|
||||
path: c[2],
|
||||
domain: c[0].replace(/^\./,''),
|
||||
secure: c[3] == 'TRUE' ? true : false
|
||||
};
|
||||
}
|
||||
return res;
|
||||
return res;
|
||||
};
|
||||
|
||||
export default parse;
|
||||
|
|
|
|||
|
|
@ -39,59 +39,59 @@ const addToArchive = (kind: {
|
|||
service: 'adn',
|
||||
type: 's'
|
||||
}, ID: string) => {
|
||||
const data = loadData();
|
||||
const data = loadData();
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(data, kind.service)) {
|
||||
const items = kind.service === 'crunchy' ? data[kind.service][kind.type] : data[kind.service][kind.type];
|
||||
if (items.findIndex(a => a.id === ID) >= 0) // Prevent duplicate
|
||||
return;
|
||||
items.push({
|
||||
id: ID,
|
||||
already: []
|
||||
});
|
||||
(data as any)[kind.service][kind.type] = items;
|
||||
} else {
|
||||
if (kind.service === 'ao') {
|
||||
data['ao'] = {
|
||||
s: [
|
||||
{
|
||||
if (Object.prototype.hasOwnProperty.call(data, kind.service)) {
|
||||
const items = kind.service === 'crunchy' ? data[kind.service][kind.type] : data[kind.service][kind.type];
|
||||
if (items.findIndex(a => a.id === ID) >= 0) // Prevent duplicate
|
||||
return;
|
||||
items.push({
|
||||
id: ID,
|
||||
already: []
|
||||
}
|
||||
]
|
||||
};
|
||||
} else if (kind.service === 'crunchy') {
|
||||
data['crunchy'] = {
|
||||
s: ([] as ItemType).concat(kind.type === 's' ? {
|
||||
id: ID,
|
||||
already: [] as string[]
|
||||
} : []),
|
||||
srz: ([] as ItemType).concat(kind.type === 'srz' ? {
|
||||
id: ID,
|
||||
already: [] as string[]
|
||||
} : []),
|
||||
};
|
||||
} else if (kind.service === 'adn') {
|
||||
data['adn'] = {
|
||||
s: [
|
||||
{
|
||||
id: ID,
|
||||
already: []
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
(data as any)[kind.service][kind.type] = items;
|
||||
} else {
|
||||
data['hidive'] = {
|
||||
s: [
|
||||
{
|
||||
id: ID,
|
||||
already: []
|
||||
}
|
||||
]
|
||||
};
|
||||
if (kind.service === 'ao') {
|
||||
data['ao'] = {
|
||||
s: [
|
||||
{
|
||||
id: ID,
|
||||
already: []
|
||||
}
|
||||
]
|
||||
};
|
||||
} else if (kind.service === 'crunchy') {
|
||||
data['crunchy'] = {
|
||||
s: ([] as ItemType).concat(kind.type === 's' ? {
|
||||
id: ID,
|
||||
already: [] as string[]
|
||||
} : []),
|
||||
srz: ([] as ItemType).concat(kind.type === 'srz' ? {
|
||||
id: ID,
|
||||
already: [] as string[]
|
||||
} : []),
|
||||
};
|
||||
} else if (kind.service === 'adn') {
|
||||
data['adn'] = {
|
||||
s: [
|
||||
{
|
||||
id: ID,
|
||||
already: []
|
||||
}
|
||||
]
|
||||
};
|
||||
} else {
|
||||
data['hidive'] = {
|
||||
s: [
|
||||
{
|
||||
id: ID,
|
||||
already: []
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4));
|
||||
fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4));
|
||||
};
|
||||
|
||||
const downloaded = (kind: {
|
||||
|
|
@ -107,49 +107,49 @@ const downloaded = (kind: {
|
|||
service: 'adn',
|
||||
type: 's'
|
||||
}, ID: string, episode: string[]) => {
|
||||
let data = loadData();
|
||||
if (!Object.prototype.hasOwnProperty.call(data, kind.service) || !Object.prototype.hasOwnProperty.call(data[kind.service], kind.type)
|
||||
let data = loadData();
|
||||
if (!Object.prototype.hasOwnProperty.call(data, kind.service) || !Object.prototype.hasOwnProperty.call(data[kind.service], kind.type)
|
||||
|| !Object.prototype.hasOwnProperty.call((data as any)[kind.service][kind.type], ID)) {
|
||||
addToArchive(kind, ID);
|
||||
data = loadData(); // Load updated version
|
||||
}
|
||||
addToArchive(kind, ID);
|
||||
data = loadData(); // Load updated version
|
||||
}
|
||||
|
||||
const archivedata = (kind.service == 'crunchy' ? data[kind.service][kind.type] : data[kind.service][kind.type]);
|
||||
const alreadyData = archivedata.find(a => a.id === ID)?.already;
|
||||
for (const ep of episode) {
|
||||
if (alreadyData?.includes(ep)) continue;
|
||||
alreadyData?.push(ep);
|
||||
}
|
||||
fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4));
|
||||
const archivedata = (kind.service == 'crunchy' ? data[kind.service][kind.type] : data[kind.service][kind.type]);
|
||||
const alreadyData = archivedata.find(a => a.id === ID)?.already;
|
||||
for (const ep of episode) {
|
||||
if (alreadyData?.includes(ep)) continue;
|
||||
alreadyData?.push(ep);
|
||||
}
|
||||
fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4));
|
||||
};
|
||||
|
||||
const makeCommand = (service: 'crunchy'|'hidive'|'ao'|'adn') : Partial<ArgvType>[] => {
|
||||
const data = loadData();
|
||||
const ret: Partial<ArgvType>[] = [];
|
||||
const kind = data[service];
|
||||
for (const type of Object.keys(kind)) {
|
||||
const item = kind[type as 's']; // 'srz' is also possible but will be ignored for the compiler
|
||||
item.forEach(i => ret.push({
|
||||
but: true,
|
||||
all: false,
|
||||
service,
|
||||
e: i.already.join(','),
|
||||
...(type === 's' ? {
|
||||
s: i.id,
|
||||
series: undefined
|
||||
} : {
|
||||
series: i.id,
|
||||
s: undefined
|
||||
})
|
||||
}));
|
||||
}
|
||||
return ret;
|
||||
const data = loadData();
|
||||
const ret: Partial<ArgvType>[] = [];
|
||||
const kind = data[service];
|
||||
for (const type of Object.keys(kind)) {
|
||||
const item = kind[type as 's']; // 'srz' is also possible but will be ignored for the compiler
|
||||
item.forEach(i => ret.push({
|
||||
but: true,
|
||||
all: false,
|
||||
service,
|
||||
e: i.already.join(','),
|
||||
...(type === 's' ? {
|
||||
s: i.id,
|
||||
series: undefined
|
||||
} : {
|
||||
series: i.id,
|
||||
s: undefined
|
||||
})
|
||||
}));
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
const loadData = () : DataType => {
|
||||
if (fs.existsSync(archiveFile))
|
||||
return JSON.parse(fs.readFileSync(archiveFile).toString()) as DataType;
|
||||
return {} as DataType;
|
||||
if (fs.existsSync(archiveFile))
|
||||
return JSON.parse(fs.readFileSync(archiveFile).toString()) as DataType;
|
||||
return {} as DataType;
|
||||
};
|
||||
|
||||
export { addToArchive, downloaded, makeCommand };
|
||||
|
|
@ -22,18 +22,18 @@ type GetDataResponse = {
|
|||
};
|
||||
|
||||
function hasDisplay(): boolean {
|
||||
if (process.platform === 'linux') {
|
||||
return !!process.env.DISPLAY || !!process.env.WAYLAND_DISPLAY;
|
||||
}
|
||||
// Win and Mac true by default
|
||||
return true;
|
||||
if (process.platform === 'linux') {
|
||||
return !!process.env.DISPLAY || !!process.env.WAYLAND_DISPLAY;
|
||||
}
|
||||
// Win and Mac true by default
|
||||
return true;
|
||||
}
|
||||
|
||||
// req
|
||||
export class Req {
|
||||
private sessCfg: string;
|
||||
private service: 'cr' | 'hd' | 'ao' | 'adn';
|
||||
private session: Record<
|
||||
private sessCfg: string;
|
||||
private service: 'cr' | 'hd' | 'ao' | 'adn';
|
||||
private session: Record<
|
||||
string,
|
||||
{
|
||||
value: string;
|
||||
|
|
@ -44,133 +44,133 @@ export class Req {
|
|||
'Max-Age'?: string;
|
||||
}
|
||||
> = {};
|
||||
private cfgDir = yamlCfg.cfgDir;
|
||||
private curl: boolean | string = false;
|
||||
private cfgDir = yamlCfg.cfgDir;
|
||||
private curl: boolean | string = false;
|
||||
|
||||
constructor(private domain: Record<string, unknown>, private debug: boolean, private nosess = false, private type: 'cr' | 'hd' | 'ao' | 'adn') {
|
||||
this.sessCfg = yamlCfg.sessCfgFile[type];
|
||||
this.service = type;
|
||||
}
|
||||
|
||||
async getData(durl: string, params?: RequestInit): Promise<GetDataResponse> {
|
||||
params = params || {};
|
||||
// options
|
||||
const options: RequestInit = {
|
||||
method: params.method ? params.method : 'GET'
|
||||
};
|
||||
// additional params
|
||||
if (params.headers) {
|
||||
options.headers = params.headers;
|
||||
constructor(private domain: Record<string, unknown>, private debug: boolean, private nosess = false, private type: 'cr' | 'hd' | 'ao' | 'adn') {
|
||||
this.sessCfg = yamlCfg.sessCfgFile[type];
|
||||
this.service = type;
|
||||
}
|
||||
if (params.body) {
|
||||
options.body = params.body;
|
||||
}
|
||||
if (typeof params.redirect == 'string') {
|
||||
options.redirect = params.redirect;
|
||||
}
|
||||
// debug
|
||||
if (this.debug) {
|
||||
console.debug('[DEBUG] FETCH OPTIONS:');
|
||||
console.debug(options);
|
||||
}
|
||||
// try do request
|
||||
try {
|
||||
const res = await fetch(durl, options);
|
||||
if (!res.ok) {
|
||||
console.error(`${res.status}: ${res.statusText}`);
|
||||
const body = await res.text();
|
||||
const docTitle = body.match(/<title>(.*)<\/title>/);
|
||||
if (body && docTitle) {
|
||||
if (docTitle[1] === 'Just a moment...' && durl.includes('crunchyroll') && hasDisplay()) {
|
||||
console.warn('Cloudflare triggered, trying to get cookies...');
|
||||
|
||||
const { page } = await connect({
|
||||
headless: false,
|
||||
turnstile: true
|
||||
});
|
||||
|
||||
await page.goto('https://www.crunchyroll.com/', {
|
||||
waitUntil: 'networkidle2'
|
||||
});
|
||||
|
||||
await page.waitForRequest('https://www.crunchyroll.com/auth/v1/token');
|
||||
|
||||
const cookies = await page.cookies();
|
||||
|
||||
await page.close();
|
||||
|
||||
params.headers = {
|
||||
...params.headers,
|
||||
Cookie: cookies.map((c) => `${c.name}=${c.value}`).join('; '),
|
||||
'Set-Cookie': cookies.map((c) => `${c.name}=${c.value}`).join('; ')
|
||||
};
|
||||
|
||||
(params as any).headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36';
|
||||
|
||||
return await this.getData(durl, params);
|
||||
} else {
|
||||
console.error(docTitle[1]);
|
||||
}
|
||||
} else {
|
||||
console.error(body);
|
||||
async getData(durl: string, params?: RequestInit): Promise<GetDataResponse> {
|
||||
params = params || {};
|
||||
// options
|
||||
const options: RequestInit = {
|
||||
method: params.method ? params.method : 'GET'
|
||||
};
|
||||
// additional params
|
||||
if (params.headers) {
|
||||
options.headers = params.headers;
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: res.ok,
|
||||
res,
|
||||
headers: params.headers as Record<string, string>
|
||||
};
|
||||
} catch (_error) {
|
||||
const error = _error as {
|
||||
if (params.body) {
|
||||
options.body = params.body;
|
||||
}
|
||||
if (typeof params.redirect == 'string') {
|
||||
options.redirect = params.redirect;
|
||||
}
|
||||
// debug
|
||||
if (this.debug) {
|
||||
console.debug('[DEBUG] FETCH OPTIONS:');
|
||||
console.debug(options);
|
||||
}
|
||||
// try do request
|
||||
try {
|
||||
const res = await fetch(durl, options);
|
||||
if (!res.ok) {
|
||||
console.error(`${res.status}: ${res.statusText}`);
|
||||
const body = await res.text();
|
||||
const docTitle = body.match(/<title>(.*)<\/title>/);
|
||||
if (body && docTitle) {
|
||||
if (docTitle[1] === 'Just a moment...' && durl.includes('crunchyroll') && hasDisplay()) {
|
||||
console.warn('Cloudflare triggered, trying to get cookies...');
|
||||
|
||||
const { page } = await connect({
|
||||
headless: false,
|
||||
turnstile: true
|
||||
});
|
||||
|
||||
await page.goto('https://www.crunchyroll.com/', {
|
||||
waitUntil: 'networkidle2'
|
||||
});
|
||||
|
||||
await page.waitForRequest('https://www.crunchyroll.com/auth/v1/token');
|
||||
|
||||
const cookies = await page.cookies();
|
||||
|
||||
await page.close();
|
||||
|
||||
params.headers = {
|
||||
...params.headers,
|
||||
Cookie: cookies.map((c) => `${c.name}=${c.value}`).join('; '),
|
||||
'Set-Cookie': cookies.map((c) => `${c.name}=${c.value}`).join('; ')
|
||||
};
|
||||
|
||||
(params as any).headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36';
|
||||
|
||||
return await this.getData(durl, params);
|
||||
} else {
|
||||
console.error(docTitle[1]);
|
||||
}
|
||||
} else {
|
||||
console.error(body);
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: res.ok,
|
||||
res,
|
||||
headers: params.headers as Record<string, string>
|
||||
};
|
||||
} catch (_error) {
|
||||
const error = _error as {
|
||||
name: string;
|
||||
} & TypeError & {
|
||||
res: Response;
|
||||
};
|
||||
if (error.res && error.res.status && error.res.statusText) {
|
||||
console.error(`${error.name} ${error.res.status}: ${error.res.statusText}`);
|
||||
} else {
|
||||
console.error(`${error.name}: ${error.res?.statusText || error.message}`);
|
||||
}
|
||||
if (error.res) {
|
||||
const body = await error.res.text();
|
||||
const docTitle = body.match(/<title>(.*)<\/title>/);
|
||||
if (body && docTitle) {
|
||||
console.error(docTitle[1]);
|
||||
if (error.res && error.res.status && error.res.statusText) {
|
||||
console.error(`${error.name} ${error.res.status}: ${error.res.statusText}`);
|
||||
} else {
|
||||
console.error(`${error.name}: ${error.res?.statusText || error.message}`);
|
||||
}
|
||||
if (error.res) {
|
||||
const body = await error.res.text();
|
||||
const docTitle = body.match(/<title>(.*)<\/title>/);
|
||||
if (body && docTitle) {
|
||||
console.error(docTitle[1]);
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
error
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
error
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function buildProxy(proxyBaseUrl: string, proxyAuth: string) {
|
||||
if (!proxyBaseUrl.match(/^(https?|socks4|socks5):/)) {
|
||||
proxyBaseUrl = 'http://' + proxyBaseUrl;
|
||||
}
|
||||
if (!proxyBaseUrl.match(/^(https?|socks4|socks5):/)) {
|
||||
proxyBaseUrl = 'http://' + proxyBaseUrl;
|
||||
}
|
||||
|
||||
const proxyCfg = new URL(proxyBaseUrl);
|
||||
let proxyStr = `${proxyCfg.protocol}//`;
|
||||
const proxyCfg = new URL(proxyBaseUrl);
|
||||
let proxyStr = `${proxyCfg.protocol}//`;
|
||||
|
||||
if (typeof proxyCfg.hostname != 'string' || proxyCfg.hostname == '') {
|
||||
throw new Error('[ERROR] Hostname and port required for proxy!');
|
||||
}
|
||||
if (typeof proxyCfg.hostname != 'string' || proxyCfg.hostname == '') {
|
||||
throw new Error('[ERROR] Hostname and port required for proxy!');
|
||||
}
|
||||
|
||||
if (proxyAuth && typeof proxyAuth == 'string' && proxyAuth.match(':')) {
|
||||
proxyCfg.username = proxyAuth.split(':')[0];
|
||||
proxyCfg.password = proxyAuth.split(':')[1];
|
||||
proxyStr += `${proxyCfg.username}:${proxyCfg.password}@`;
|
||||
}
|
||||
if (proxyAuth && typeof proxyAuth == 'string' && proxyAuth.match(':')) {
|
||||
proxyCfg.username = proxyAuth.split(':')[0];
|
||||
proxyCfg.password = proxyAuth.split(':')[1];
|
||||
proxyStr += `${proxyCfg.username}:${proxyCfg.password}@`;
|
||||
}
|
||||
|
||||
proxyStr += proxyCfg.hostname;
|
||||
proxyStr += proxyCfg.hostname;
|
||||
|
||||
if (!proxyCfg.port && proxyCfg.protocol == 'http:') {
|
||||
proxyStr += ':80';
|
||||
} else if (!proxyCfg.port && proxyCfg.protocol == 'https:') {
|
||||
proxyStr += ':443';
|
||||
}
|
||||
if (!proxyCfg.port && proxyCfg.protocol == 'http:') {
|
||||
proxyStr += ':80';
|
||||
} else if (!proxyCfg.port && proxyCfg.protocol == 'https:') {
|
||||
proxyStr += ':443';
|
||||
}
|
||||
|
||||
return proxyStr;
|
||||
return proxyStr;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,51 +1,51 @@
|
|||
import fs from 'fs';
|
||||
|
||||
export function convertChaptersToFFmpegFormat(inputFilePath: string): string {
|
||||
const content = fs.readFileSync(inputFilePath, 'utf-8');
|
||||
const content = fs.readFileSync(inputFilePath, 'utf-8');
|
||||
|
||||
const chapterMatches = Array.from(content.matchAll(/CHAPTER(\d+)=([\d:.]+)/g));
|
||||
const nameMatches = Array.from(content.matchAll(/CHAPTER(\d+)NAME=([^\n]+)/g));
|
||||
const chapterMatches = Array.from(content.matchAll(/CHAPTER(\d+)=([\d:.]+)/g));
|
||||
const nameMatches = Array.from(content.matchAll(/CHAPTER(\d+)NAME=([^\n]+)/g));
|
||||
|
||||
const chapters = chapterMatches.map((m) => ({
|
||||
index: parseInt(m[1], 10),
|
||||
time: m[2],
|
||||
})).sort((a, b) => a.index - b.index);
|
||||
const chapters = chapterMatches.map((m) => ({
|
||||
index: parseInt(m[1], 10),
|
||||
time: m[2],
|
||||
})).sort((a, b) => a.index - b.index);
|
||||
|
||||
const nameDict: Record<number, string> = {};
|
||||
nameMatches.forEach((m) => {
|
||||
nameDict[parseInt(m[1], 10)] = m[2];
|
||||
});
|
||||
const nameDict: Record<number, string> = {};
|
||||
nameMatches.forEach((m) => {
|
||||
nameDict[parseInt(m[1], 10)] = m[2];
|
||||
});
|
||||
|
||||
let ffmpegContent = ';FFMETADATA1\n';
|
||||
let startTimeInNs = 0;
|
||||
let ffmpegContent = ';FFMETADATA1\n';
|
||||
let startTimeInNs = 0;
|
||||
|
||||
for (let i = 0; i < chapters.length; i++) {
|
||||
const chapterStartTime = timeToNanoSeconds(chapters[i].time);
|
||||
const chapterEndTime = (i + 1 < chapters.length)
|
||||
? timeToNanoSeconds(chapters[i + 1].time)
|
||||
: chapterStartTime + 1000000000;
|
||||
for (let i = 0; i < chapters.length; i++) {
|
||||
const chapterStartTime = timeToNanoSeconds(chapters[i].time);
|
||||
const chapterEndTime = (i + 1 < chapters.length)
|
||||
? timeToNanoSeconds(chapters[i + 1].time)
|
||||
: chapterStartTime + 1000000000;
|
||||
|
||||
const chapterName = nameDict[chapters[i].index] || `Chapter ${chapters[i].index}`;
|
||||
const chapterName = nameDict[chapters[i].index] || `Chapter ${chapters[i].index}`;
|
||||
|
||||
ffmpegContent += '[CHAPTER]\n';
|
||||
ffmpegContent += 'TIMEBASE=1/1000000000\n';
|
||||
ffmpegContent += `START=${startTimeInNs}\n`;
|
||||
ffmpegContent += `END=${chapterEndTime}\n`;
|
||||
ffmpegContent += `title=${chapterName}\n`;
|
||||
ffmpegContent += '[CHAPTER]\n';
|
||||
ffmpegContent += 'TIMEBASE=1/1000000000\n';
|
||||
ffmpegContent += `START=${startTimeInNs}\n`;
|
||||
ffmpegContent += `END=${chapterEndTime}\n`;
|
||||
ffmpegContent += `title=${chapterName}\n`;
|
||||
|
||||
startTimeInNs = chapterEndTime;
|
||||
}
|
||||
startTimeInNs = chapterEndTime;
|
||||
}
|
||||
|
||||
return ffmpegContent;
|
||||
return ffmpegContent;
|
||||
}
|
||||
|
||||
export function timeToNanoSeconds(time: string): number {
|
||||
const parts = time.split(':');
|
||||
const hours = parseInt(parts[0], 10);
|
||||
const minutes = parseInt(parts[1], 10);
|
||||
const secondsAndMs = parts[2].split('.');
|
||||
const seconds = parseInt(secondsAndMs[0], 10);
|
||||
const milliseconds = parseInt(secondsAndMs[1], 10);
|
||||
const parts = time.split(':');
|
||||
const hours = parseInt(parts[0], 10);
|
||||
const minutes = parseInt(parts[1], 10);
|
||||
const secondsAndMs = parts[2].split('.');
|
||||
const seconds = parseInt(secondsAndMs[0], 10);
|
||||
const milliseconds = parseInt(secondsAndMs[1], 10);
|
||||
|
||||
return (hours * 3600 + minutes * 60 + seconds) * 1000000000 + milliseconds * 1000000;
|
||||
return (hours * 3600 + minutes * 60 + seconds) * 1000000000 + milliseconds * 1000000;
|
||||
}
|
||||
|
|
@ -15,77 +15,77 @@ export type Variable<T extends string = AvailableFilenameVars> = ({
|
|||
}
|
||||
|
||||
const parseFileName = (input: string, variables: Variable[], numbers: number, override: string[]): string[] => {
|
||||
const varRegex = /\${[A-Za-z1-9]+}/g;
|
||||
const vars = input.match(varRegex);
|
||||
const overridenVars = parseOverride(variables, override);
|
||||
if (!vars)
|
||||
return [input];
|
||||
for (let i = 0; i < vars.length; i++) {
|
||||
const type = vars[i];
|
||||
const varName = type.slice(2, -1);
|
||||
let use = overridenVars.find(a => a.name === varName);
|
||||
if (use === undefined && type === '${height}') {
|
||||
use = { type: 'number', replaceWith: 0 } as Variable<string>;
|
||||
}
|
||||
if (use === undefined) {
|
||||
console.info(`[ERROR] Found variable '${type}' in fileName but no values was internally found!`);
|
||||
continue;
|
||||
}
|
||||
const varRegex = /\${[A-Za-z1-9]+}/g;
|
||||
const vars = input.match(varRegex);
|
||||
const overridenVars = parseOverride(variables, override);
|
||||
if (!vars)
|
||||
return [input];
|
||||
for (let i = 0; i < vars.length; i++) {
|
||||
const type = vars[i];
|
||||
const varName = type.slice(2, -1);
|
||||
let use = overridenVars.find(a => a.name === varName);
|
||||
if (use === undefined && type === '${height}') {
|
||||
use = { type: 'number', replaceWith: 0 } as Variable<string>;
|
||||
}
|
||||
if (use === undefined) {
|
||||
console.info(`[ERROR] Found variable '${type}' in fileName but no values was internally found!`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (use.type === 'number') {
|
||||
const len = use.replaceWith.toFixed(0).length;
|
||||
const replaceStr = len < numbers ? '0'.repeat(numbers - len) + use.replaceWith : use.replaceWith+'';
|
||||
input = input.replace(type, replaceStr);
|
||||
} else {
|
||||
if (use.sanitize)
|
||||
use.replaceWith = Helper.cleanupFilename(use.replaceWith);
|
||||
input = input.replace(type, use.replaceWith);
|
||||
if (use.type === 'number') {
|
||||
const len = use.replaceWith.toFixed(0).length;
|
||||
const replaceStr = len < numbers ? '0'.repeat(numbers - len) + use.replaceWith : use.replaceWith+'';
|
||||
input = input.replace(type, replaceStr);
|
||||
} else {
|
||||
if (use.sanitize)
|
||||
use.replaceWith = Helper.cleanupFilename(use.replaceWith);
|
||||
input = input.replace(type, use.replaceWith);
|
||||
}
|
||||
}
|
||||
}
|
||||
return input.split(path.sep).map(a => Helper.cleanupFilename(a));
|
||||
return input.split(path.sep).map(a => Helper.cleanupFilename(a));
|
||||
};
|
||||
|
||||
const parseOverride = (variables: Variable[], override: string[]): Variable<string>[] => {
|
||||
const vars: Variable<string>[] = variables;
|
||||
override.forEach(item => {
|
||||
const index = item.indexOf('=');
|
||||
if (index === -1)
|
||||
return logError(item, 'invalid');
|
||||
const parts = [ item.slice(0, index), item.slice(index + 1) ];
|
||||
if (!(parts[1].startsWith('\'') && parts[1].endsWith('\'') && parts[1].length >= 2))
|
||||
return logError(item, 'invalid');
|
||||
parts[1] = parts[1].slice(1, -1);
|
||||
const already = vars.findIndex(a => a.name === parts[0]);
|
||||
if (already > -1) {
|
||||
if (vars[already].type === 'number') {
|
||||
if (isNaN(parseFloat(parts[1])))
|
||||
return logError(item, 'wrongType');
|
||||
vars[already].replaceWith = parseFloat(parts[1]);
|
||||
} else {
|
||||
vars[already].replaceWith = parts[1];
|
||||
}
|
||||
} else {
|
||||
const isNumber = !isNaN(parseFloat(parts[1]));
|
||||
vars.push({
|
||||
name: parts[0],
|
||||
replaceWith: isNumber ? parseFloat(parts[1]) : parts[1],
|
||||
type: isNumber ? 'number' : 'string'
|
||||
} as Variable<string>);
|
||||
}
|
||||
});
|
||||
const vars: Variable<string>[] = variables;
|
||||
override.forEach(item => {
|
||||
const index = item.indexOf('=');
|
||||
if (index === -1)
|
||||
return logError(item, 'invalid');
|
||||
const parts = [ item.slice(0, index), item.slice(index + 1) ];
|
||||
if (!(parts[1].startsWith('\'') && parts[1].endsWith('\'') && parts[1].length >= 2))
|
||||
return logError(item, 'invalid');
|
||||
parts[1] = parts[1].slice(1, -1);
|
||||
const already = vars.findIndex(a => a.name === parts[0]);
|
||||
if (already > -1) {
|
||||
if (vars[already].type === 'number') {
|
||||
if (isNaN(parseFloat(parts[1])))
|
||||
return logError(item, 'wrongType');
|
||||
vars[already].replaceWith = parseFloat(parts[1]);
|
||||
} else {
|
||||
vars[already].replaceWith = parts[1];
|
||||
}
|
||||
} else {
|
||||
const isNumber = !isNaN(parseFloat(parts[1]));
|
||||
vars.push({
|
||||
name: parts[0],
|
||||
replaceWith: isNumber ? parseFloat(parts[1]) : parts[1],
|
||||
type: isNumber ? 'number' : 'string'
|
||||
} as Variable<string>);
|
||||
}
|
||||
});
|
||||
|
||||
return variables;
|
||||
return variables;
|
||||
};
|
||||
|
||||
const logError = (override: string, reason: 'invalid'|'wrongType') => {
|
||||
switch (reason) {
|
||||
case 'wrongType':
|
||||
console.error(`[ERROR] Invalid type on \`${override}\`. Expected number but found string. It has been ignored`);
|
||||
break;
|
||||
case 'invalid':
|
||||
default:
|
||||
console.error(`[ERROR] Invalid override \`${override}\`. It has been ignored`);
|
||||
}
|
||||
switch (reason) {
|
||||
case 'wrongType':
|
||||
console.error(`[ERROR] Invalid type on \`${override}\`. Expected number but found string. It has been ignored`);
|
||||
break;
|
||||
case 'invalid':
|
||||
default:
|
||||
console.error(`[ERROR] Invalid override \`${override}\`. It has been ignored`);
|
||||
}
|
||||
};
|
||||
|
||||
export default parseFileName;
|
||||
|
|
@ -3,99 +3,99 @@ const root = 'https://static.crunchyroll.com/vilos-v2/web/vilos/assets/libass-fo
|
|||
|
||||
// file list
|
||||
const fontFamilies = {
|
||||
'Adobe Arabic': ['AdobeArabic-Bold.otf'],
|
||||
'Andale Mono': ['andalemo.ttf'],
|
||||
'Arial': ['arial.ttf'],
|
||||
'Arial Black': ['ariblk.ttf'],
|
||||
'Arial Bold': ['arialbd.ttf'],
|
||||
'Arial Bold Italic': ['arialbi.ttf'],
|
||||
'Arial Italic': ['ariali.ttf'],
|
||||
'Arial Unicode MS': ['arialuni.ttf'],
|
||||
'Comic Sans MS': ['comic.ttf'],
|
||||
'Comic Sans MS Bold': ['comicbd.ttf'],
|
||||
'Courier New': ['cour.ttf'],
|
||||
'Courier New Bold': ['courbd.ttf'],
|
||||
'Courier New Bold Italic': ['courbi.ttf'],
|
||||
'Courier New Italic': ['couri.ttf'],
|
||||
'DejaVu LGC Sans Mono': ['DejaVuLGCSansMono.ttf'],
|
||||
'DejaVu LGC Sans Mono Bold': ['DejaVuLGCSansMono-Bold.ttf'],
|
||||
'DejaVu LGC Sans Mono Bold Oblique': ['DejaVuLGCSansMono-BoldOblique.ttf'],
|
||||
'DejaVu LGC Sans Mono Oblique': ['DejaVuLGCSansMono-Oblique.ttf'],
|
||||
'DejaVu Sans': ['DejaVuSans.ttf'],
|
||||
'DejaVu Sans Bold': ['DejaVuSans-Bold.ttf'],
|
||||
'DejaVu Sans Bold Oblique': ['DejaVuSans-BoldOblique.ttf'],
|
||||
'DejaVu Sans Condensed': ['DejaVuSansCondensed.ttf'],
|
||||
'DejaVu Sans Condensed Bold': ['DejaVuSansCondensed-Bold.ttf'],
|
||||
'DejaVu Sans Condensed Bold Oblique': ['DejaVuSansCondensed-BoldOblique.ttf'],
|
||||
'DejaVu Sans Condensed Oblique': ['DejaVuSansCondensed-Oblique.ttf'],
|
||||
'DejaVu Sans ExtraLight': ['DejaVuSans-ExtraLight.ttf'],
|
||||
'DejaVu Sans Mono': ['DejaVuSansMono.ttf'],
|
||||
'DejaVu Sans Mono Bold': ['DejaVuSansMono-Bold.ttf'],
|
||||
'DejaVu Sans Mono Bold Oblique': ['DejaVuSansMono-BoldOblique.ttf'],
|
||||
'DejaVu Sans Mono Oblique': ['DejaVuSansMono-Oblique.ttf'],
|
||||
'DejaVu Sans Oblique': ['DejaVuSans-Oblique.ttf'],
|
||||
'Gautami': ['gautami.ttf'],
|
||||
'Georgia': ['georgia.ttf'],
|
||||
'Georgia Bold': ['georgiab.ttf'],
|
||||
'Georgia Bold Italic': ['georgiaz.ttf'],
|
||||
'Georgia Italic': ['georgiai.ttf'],
|
||||
'Impact': ['impact.ttf'],
|
||||
'Meera Inimai': ['MeeraInimai-Regular.ttf'],
|
||||
'Noto Sans Thai': ['NotoSansThai.ttf'],
|
||||
'Rubik': ['Rubik-Regular.ttf'],
|
||||
'Rubik Black': ['Rubik-Black.ttf'],
|
||||
'Rubik Black Italic': ['Rubik-BlackItalic.ttf'],
|
||||
'Rubik Bold': ['Rubik-Bold.ttf'],
|
||||
'Rubik Bold Italic': ['Rubik-BoldItalic.ttf'],
|
||||
'Rubik Italic': ['Rubik-Italic.ttf'],
|
||||
'Rubik Light': ['Rubik-Light.ttf'],
|
||||
'Rubik Light Italic': ['Rubik-LightItalic.ttf'],
|
||||
'Rubik Medium': ['Rubik-Medium.ttf'],
|
||||
'Rubik Medium Italic': ['Rubik-MediumItalic.ttf'],
|
||||
'Tahoma': ['tahoma.ttf'],
|
||||
'Times New Roman': ['times.ttf'],
|
||||
'Times New Roman Bold': ['timesbd.ttf'],
|
||||
'Times New Roman Bold Italic': ['timesbi.ttf'],
|
||||
'Times New Roman Italic': ['timesi.ttf'],
|
||||
'Trebuchet MS': ['trebuc.ttf'],
|
||||
'Trebuchet MS Bold': ['trebucbd.ttf'],
|
||||
'Trebuchet MS Bold Italic': ['trebucbi.ttf'],
|
||||
'Trebuchet MS Italic': ['trebucit.ttf'],
|
||||
'Verdana': ['verdana.ttf'],
|
||||
'Verdana Bold': ['verdanab.ttf'],
|
||||
'Verdana Bold Italic': ['verdanaz.ttf'],
|
||||
'Verdana Italic': ['verdanai.ttf'],
|
||||
'Vrinda': ['vrinda.ttf'],
|
||||
'Vrinda Bold': ['vrindab.ttf'],
|
||||
'Webdings': ['webdings.ttf'],
|
||||
'Adobe Arabic': ['AdobeArabic-Bold.otf'],
|
||||
'Andale Mono': ['andalemo.ttf'],
|
||||
'Arial': ['arial.ttf'],
|
||||
'Arial Black': ['ariblk.ttf'],
|
||||
'Arial Bold': ['arialbd.ttf'],
|
||||
'Arial Bold Italic': ['arialbi.ttf'],
|
||||
'Arial Italic': ['ariali.ttf'],
|
||||
'Arial Unicode MS': ['arialuni.ttf'],
|
||||
'Comic Sans MS': ['comic.ttf'],
|
||||
'Comic Sans MS Bold': ['comicbd.ttf'],
|
||||
'Courier New': ['cour.ttf'],
|
||||
'Courier New Bold': ['courbd.ttf'],
|
||||
'Courier New Bold Italic': ['courbi.ttf'],
|
||||
'Courier New Italic': ['couri.ttf'],
|
||||
'DejaVu LGC Sans Mono': ['DejaVuLGCSansMono.ttf'],
|
||||
'DejaVu LGC Sans Mono Bold': ['DejaVuLGCSansMono-Bold.ttf'],
|
||||
'DejaVu LGC Sans Mono Bold Oblique': ['DejaVuLGCSansMono-BoldOblique.ttf'],
|
||||
'DejaVu LGC Sans Mono Oblique': ['DejaVuLGCSansMono-Oblique.ttf'],
|
||||
'DejaVu Sans': ['DejaVuSans.ttf'],
|
||||
'DejaVu Sans Bold': ['DejaVuSans-Bold.ttf'],
|
||||
'DejaVu Sans Bold Oblique': ['DejaVuSans-BoldOblique.ttf'],
|
||||
'DejaVu Sans Condensed': ['DejaVuSansCondensed.ttf'],
|
||||
'DejaVu Sans Condensed Bold': ['DejaVuSansCondensed-Bold.ttf'],
|
||||
'DejaVu Sans Condensed Bold Oblique': ['DejaVuSansCondensed-BoldOblique.ttf'],
|
||||
'DejaVu Sans Condensed Oblique': ['DejaVuSansCondensed-Oblique.ttf'],
|
||||
'DejaVu Sans ExtraLight': ['DejaVuSans-ExtraLight.ttf'],
|
||||
'DejaVu Sans Mono': ['DejaVuSansMono.ttf'],
|
||||
'DejaVu Sans Mono Bold': ['DejaVuSansMono-Bold.ttf'],
|
||||
'DejaVu Sans Mono Bold Oblique': ['DejaVuSansMono-BoldOblique.ttf'],
|
||||
'DejaVu Sans Mono Oblique': ['DejaVuSansMono-Oblique.ttf'],
|
||||
'DejaVu Sans Oblique': ['DejaVuSans-Oblique.ttf'],
|
||||
'Gautami': ['gautami.ttf'],
|
||||
'Georgia': ['georgia.ttf'],
|
||||
'Georgia Bold': ['georgiab.ttf'],
|
||||
'Georgia Bold Italic': ['georgiaz.ttf'],
|
||||
'Georgia Italic': ['georgiai.ttf'],
|
||||
'Impact': ['impact.ttf'],
|
||||
'Meera Inimai': ['MeeraInimai-Regular.ttf'],
|
||||
'Noto Sans Thai': ['NotoSansThai.ttf'],
|
||||
'Rubik': ['Rubik-Regular.ttf'],
|
||||
'Rubik Black': ['Rubik-Black.ttf'],
|
||||
'Rubik Black Italic': ['Rubik-BlackItalic.ttf'],
|
||||
'Rubik Bold': ['Rubik-Bold.ttf'],
|
||||
'Rubik Bold Italic': ['Rubik-BoldItalic.ttf'],
|
||||
'Rubik Italic': ['Rubik-Italic.ttf'],
|
||||
'Rubik Light': ['Rubik-Light.ttf'],
|
||||
'Rubik Light Italic': ['Rubik-LightItalic.ttf'],
|
||||
'Rubik Medium': ['Rubik-Medium.ttf'],
|
||||
'Rubik Medium Italic': ['Rubik-MediumItalic.ttf'],
|
||||
'Tahoma': ['tahoma.ttf'],
|
||||
'Times New Roman': ['times.ttf'],
|
||||
'Times New Roman Bold': ['timesbd.ttf'],
|
||||
'Times New Roman Bold Italic': ['timesbi.ttf'],
|
||||
'Times New Roman Italic': ['timesi.ttf'],
|
||||
'Trebuchet MS': ['trebuc.ttf'],
|
||||
'Trebuchet MS Bold': ['trebucbd.ttf'],
|
||||
'Trebuchet MS Bold Italic': ['trebucbi.ttf'],
|
||||
'Trebuchet MS Italic': ['trebucit.ttf'],
|
||||
'Verdana': ['verdana.ttf'],
|
||||
'Verdana Bold': ['verdanab.ttf'],
|
||||
'Verdana Bold Italic': ['verdanaz.ttf'],
|
||||
'Verdana Italic': ['verdanai.ttf'],
|
||||
'Vrinda': ['vrinda.ttf'],
|
||||
'Vrinda Bold': ['vrindab.ttf'],
|
||||
'Webdings': ['webdings.ttf'],
|
||||
};
|
||||
|
||||
// collect styles from ass string
|
||||
function assFonts(ass: string){
|
||||
const strings = ass.replace(/\r/g,'').split('\n');
|
||||
const styles: string[] = [];
|
||||
for(const s of strings){
|
||||
if(s.match(/^Style: /)){
|
||||
const addStyle = s.split(',');
|
||||
styles.push(addStyle[1]);
|
||||
const strings = ass.replace(/\r/g,'').split('\n');
|
||||
const styles: string[] = [];
|
||||
for(const s of strings){
|
||||
if(s.match(/^Style: /)){
|
||||
const addStyle = s.split(',');
|
||||
styles.push(addStyle[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
const fontMatches = ass.matchAll(/\\fn([^\\}]+)/g);
|
||||
for (const match of fontMatches) {
|
||||
styles.push(match[1]);
|
||||
}
|
||||
return [...new Set(styles)];
|
||||
const fontMatches = ass.matchAll(/\\fn([^\\}]+)/g);
|
||||
for (const match of fontMatches) {
|
||||
styles.push(match[1]);
|
||||
}
|
||||
return [...new Set(styles)];
|
||||
}
|
||||
|
||||
// font mime type
|
||||
function fontMime(fontFile: string){
|
||||
if(fontFile.match(/\.otf$/)){
|
||||
return 'application/vnd.ms-opentype';
|
||||
}
|
||||
if(fontFile.match(/\.ttf$/)){
|
||||
return 'application/x-truetype-font';
|
||||
}
|
||||
return 'application/octet-stream';
|
||||
if(fontFile.match(/\.otf$/)){
|
||||
return 'application/vnd.ms-opentype';
|
||||
}
|
||||
if(fontFile.match(/\.ttf$/)){
|
||||
return 'application/x-truetype-font';
|
||||
}
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
|
||||
export type AvailableFonts = keyof typeof fontFamilies;
|
||||
|
|
|
|||
|
|
@ -5,46 +5,46 @@ import childProcess from 'child_process';
|
|||
import { console } from './log';
|
||||
|
||||
export default class Helper {
|
||||
static async question(q: string) {
|
||||
const rl = readline.createInterface({ input, output });
|
||||
const a = await rl.question(q);
|
||||
rl.close();
|
||||
return a;
|
||||
}
|
||||
static formatTime(t: number) {
|
||||
const days = Math.floor(t / 86400);
|
||||
const hours = Math.floor((t % 86400) / 3600);
|
||||
const minutes = Math.floor(((t % 86400) % 3600) / 60);
|
||||
const seconds = t % 60;
|
||||
const daysS = days > 0 ? `${days}d` : '';
|
||||
const hoursS = daysS || hours ? `${daysS}${daysS && hours < 10 ? '0' : ''}${hours}h` : '';
|
||||
const minutesS = minutes || hoursS ? `${hoursS}${hoursS && minutes < 10 ? '0' : ''}${minutes}m` : '';
|
||||
const secondsS = `${minutesS}${minutesS && seconds < 10 ? '0' : ''}${seconds}s`;
|
||||
return secondsS;
|
||||
}
|
||||
static async question(q: string) {
|
||||
const rl = readline.createInterface({ input, output });
|
||||
const a = await rl.question(q);
|
||||
rl.close();
|
||||
return a;
|
||||
}
|
||||
static formatTime(t: number) {
|
||||
const days = Math.floor(t / 86400);
|
||||
const hours = Math.floor((t % 86400) / 3600);
|
||||
const minutes = Math.floor(((t % 86400) % 3600) / 60);
|
||||
const seconds = t % 60;
|
||||
const daysS = days > 0 ? `${days}d` : '';
|
||||
const hoursS = daysS || hours ? `${daysS}${daysS && hours < 10 ? '0' : ''}${hours}h` : '';
|
||||
const minutesS = minutes || hoursS ? `${hoursS}${hoursS && minutes < 10 ? '0' : ''}${minutes}m` : '';
|
||||
const secondsS = `${minutesS}${minutesS && seconds < 10 ? '0' : ''}${seconds}s`;
|
||||
return secondsS;
|
||||
}
|
||||
|
||||
static cleanupFilename(n: string) {
|
||||
/* eslint-disable no-extra-boolean-cast, no-useless-escape, no-control-regex */
|
||||
const fixingChar = '_';
|
||||
const illegalRe = /[\/\?<>\\:\*\|":]/g;
|
||||
const controlRe = /[\x00-\x1f\x80-\x9f]/g;
|
||||
const reservedRe = /^\.+$/;
|
||||
const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
|
||||
const windowsTrailingRe = /[\. ]+$/;
|
||||
return n
|
||||
.replace(illegalRe, fixingChar)
|
||||
.replace(controlRe, fixingChar)
|
||||
.replace(reservedRe, fixingChar)
|
||||
.replace(windowsReservedRe, fixingChar)
|
||||
.replace(windowsTrailingRe, fixingChar);
|
||||
}
|
||||
static cleanupFilename(n: string) {
|
||||
/* eslint-disable no-useless-escape, no-control-regex */
|
||||
const fixingChar = '_';
|
||||
const illegalRe = /[\/\?<>\\:\*\|":]/g;
|
||||
const controlRe = /[\x00-\x1f\x80-\x9f]/g;
|
||||
const reservedRe = /^\.+$/;
|
||||
const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
|
||||
const windowsTrailingRe = /[\. ]+$/;
|
||||
return n
|
||||
.replace(illegalRe, fixingChar)
|
||||
.replace(controlRe, fixingChar)
|
||||
.replace(reservedRe, fixingChar)
|
||||
.replace(windowsReservedRe, fixingChar)
|
||||
.replace(windowsTrailingRe, fixingChar);
|
||||
}
|
||||
|
||||
static exec(
|
||||
pname: string,
|
||||
fpath: string,
|
||||
pargs: string,
|
||||
spc = false
|
||||
):
|
||||
static exec(
|
||||
pname: string,
|
||||
fpath: string,
|
||||
pargs: string,
|
||||
spc = false
|
||||
):
|
||||
| {
|
||||
isOk: true;
|
||||
}
|
||||
|
|
@ -52,26 +52,26 @@ export default class Helper {
|
|||
isOk: false;
|
||||
err: Error & { code: number };
|
||||
} {
|
||||
pargs = pargs ? ' ' + pargs : '';
|
||||
console.info(`\n> "${pname}"${pargs}${spc ? '\n' : ''}`);
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
childProcess.execSync('& ' + fpath + pargs, { stdio: 'inherit', shell: 'powershell.exe', windowsHide: true });
|
||||
} else {
|
||||
childProcess.execSync(fpath + pargs, { stdio: 'inherit' });
|
||||
}
|
||||
return {
|
||||
isOk: true
|
||||
};
|
||||
} catch (er) {
|
||||
const err = er as Error & { status: number };
|
||||
return {
|
||||
isOk: false,
|
||||
err: {
|
||||
...err,
|
||||
code: err.status
|
||||
pargs = pargs ? ' ' + pargs : '';
|
||||
console.info(`\n> "${pname}"${pargs}${spc ? '\n' : ''}`);
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
childProcess.execSync('& ' + fpath + pargs, { stdio: 'inherit', shell: 'powershell.exe', windowsHide: true });
|
||||
} else {
|
||||
childProcess.execSync(fpath + pargs, { stdio: 'inherit' });
|
||||
}
|
||||
return {
|
||||
isOk: true
|
||||
};
|
||||
} catch (er) {
|
||||
const err = er as Error & { status: number };
|
||||
return {
|
||||
isOk: false,
|
||||
err: {
|
||||
...err,
|
||||
code: err.status
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,155 +13,155 @@ export type LanguageItem = {
|
|||
}
|
||||
|
||||
const languages: LanguageItem[] = [
|
||||
{ locale: 'un', code: 'und', name: 'Undetermined', language: 'Undetermined', new_hd_locale: 'und', cr_locale: 'und', adn_locale: 'und', ao_locale: 'und' },
|
||||
{ cr_locale: 'en-US', new_hd_locale: 'en-US', hd_locale: 'English', locale: 'en', code: 'eng', name: 'English' },
|
||||
{ cr_locale: 'en-IN', locale: 'en-IN', code: 'eng', name: 'English (India)', },
|
||||
{ cr_locale: 'es-LA', new_hd_locale: 'es-MX', hd_locale: 'Spanish LatAm', locale: 'es-419', code: 'spa', name: 'Spanish', language: 'Latin American Spanish' },
|
||||
{ cr_locale: 'es-419',ao_locale: 'es',hd_locale: 'Spanish', locale: 'es-419', code: 'spa-419', name: 'Spanish', language: 'Latin American Spanish' },
|
||||
{ cr_locale: 'es-ES', new_hd_locale: 'es-ES', hd_locale: 'Spanish Europe', locale: 'es-ES', code: 'spa-ES', name: 'Castilian', language: 'European Spanish' },
|
||||
{ cr_locale: 'pt-BR', ao_locale: 'pt',new_hd_locale: 'pt-BR', hd_locale: 'Portuguese', locale: 'pt-BR', code: 'por', name: 'Portuguese', language: 'Brazilian Portuguese' },
|
||||
{ cr_locale: 'pt-PT', locale: 'pt-PT', code: 'por', name: 'Portuguese (Portugal)', language: 'Portugues (Portugal)' },
|
||||
{ cr_locale: 'fr-FR', adn_locale: 'fr', hd_locale: 'French', locale: 'fr', code: 'fra', name: 'French' },
|
||||
{ cr_locale: 'de-DE', adn_locale: 'de', hd_locale: 'German', locale: 'de', code: 'deu', name: 'German' },
|
||||
{ cr_locale: 'ar-ME', locale: 'ar', code: 'ara-ME', name: 'Arabic' },
|
||||
{ cr_locale: 'ar-SA', hd_locale: 'Arabic', locale: 'ar', code: 'ara', name: 'Arabic (Saudi Arabia)' },
|
||||
{ cr_locale: 'it-IT', hd_locale: 'Italian', locale: 'it', code: 'ita', name: 'Italian' },
|
||||
{ cr_locale: 'ru-RU', hd_locale: 'Russian', locale: 'ru', code: 'rus', name: 'Russian' },
|
||||
{ cr_locale: 'tr-TR', hd_locale: 'Turkish', locale: 'tr', code: 'tur', name: 'Turkish' },
|
||||
{ cr_locale: 'hi-IN', locale: 'hi', code: 'hin', name: 'Hindi' },
|
||||
{ locale: 'zh', code: 'cmn', name: 'Chinese (Mandarin, PRC)' },
|
||||
{ cr_locale: 'zh-CN', locale: 'zh-CN', code: 'zho', name: 'Chinese (Mainland China)' },
|
||||
{ cr_locale: 'zh-TW', locale: 'zh-TW', code: 'chi', name: 'Chinese (Taiwan)' },
|
||||
{ cr_locale: 'zh-HK', locale: 'zh-HK', code: 'zh-HK', name: 'Chinese (Hong-Kong)', language: '中文 (粵語)' },
|
||||
{ cr_locale: 'ko-KR', hd_locale: 'Korean', locale: 'ko', code: 'kor', name: 'Korean' },
|
||||
{ cr_locale: 'ca-ES', locale: 'ca-ES', code: 'cat', name: 'Catalan' },
|
||||
{ cr_locale: 'pl-PL', locale: 'pl-PL', code: 'pol', name: 'Polish' },
|
||||
{ cr_locale: 'th-TH', locale: 'th-TH', code: 'tha', name: 'Thai', language: 'ไทย' },
|
||||
{ cr_locale: 'ta-IN', locale: 'ta-IN', code: 'tam', name: 'Tamil (India)', language: 'தமிழ்' },
|
||||
{ cr_locale: 'ms-MY', locale: 'ms-MY', code: 'may', name: 'Malay (Malaysia)', language: 'Bahasa Melayu' },
|
||||
{ cr_locale: 'vi-VN', locale: 'vi-VN', code: 'vie', name: 'Vietnamese', language: 'Tiếng Việt' },
|
||||
{ cr_locale: 'id-ID', locale: 'id-ID', code: 'ind', name: 'Indonesian', language: 'Bahasa Indonesia' },
|
||||
{ cr_locale: 'te-IN', locale: 'te-IN', code: 'tel', name: 'Telugu (India)', language: 'తెలుగు' },
|
||||
{ cr_locale: 'ja-JP', adn_locale: 'ja', ao_locale: 'ja', hd_locale: 'Japanese', locale: 'ja', code: 'jpn', name: 'Japanese' },
|
||||
{ locale: 'un', code: 'und', name: 'Undetermined', language: 'Undetermined', new_hd_locale: 'und', cr_locale: 'und', adn_locale: 'und', ao_locale: 'und' },
|
||||
{ cr_locale: 'en-US', new_hd_locale: 'en-US', hd_locale: 'English', locale: 'en', code: 'eng', name: 'English' },
|
||||
{ cr_locale: 'en-IN', locale: 'en-IN', code: 'eng', name: 'English (India)', },
|
||||
{ cr_locale: 'es-LA', new_hd_locale: 'es-MX', hd_locale: 'Spanish LatAm', locale: 'es-419', code: 'spa', name: 'Spanish', language: 'Latin American Spanish' },
|
||||
{ cr_locale: 'es-419',ao_locale: 'es',hd_locale: 'Spanish', locale: 'es-419', code: 'spa-419', name: 'Spanish', language: 'Latin American Spanish' },
|
||||
{ cr_locale: 'es-ES', new_hd_locale: 'es-ES', hd_locale: 'Spanish Europe', locale: 'es-ES', code: 'spa-ES', name: 'Castilian', language: 'European Spanish' },
|
||||
{ cr_locale: 'pt-BR', ao_locale: 'pt',new_hd_locale: 'pt-BR', hd_locale: 'Portuguese', locale: 'pt-BR', code: 'por', name: 'Portuguese', language: 'Brazilian Portuguese' },
|
||||
{ cr_locale: 'pt-PT', locale: 'pt-PT', code: 'por', name: 'Portuguese (Portugal)', language: 'Portugues (Portugal)' },
|
||||
{ cr_locale: 'fr-FR', adn_locale: 'fr', hd_locale: 'French', locale: 'fr', code: 'fra', name: 'French' },
|
||||
{ cr_locale: 'de-DE', adn_locale: 'de', hd_locale: 'German', locale: 'de', code: 'deu', name: 'German' },
|
||||
{ cr_locale: 'ar-ME', locale: 'ar', code: 'ara-ME', name: 'Arabic' },
|
||||
{ cr_locale: 'ar-SA', hd_locale: 'Arabic', locale: 'ar', code: 'ara', name: 'Arabic (Saudi Arabia)' },
|
||||
{ cr_locale: 'it-IT', hd_locale: 'Italian', locale: 'it', code: 'ita', name: 'Italian' },
|
||||
{ cr_locale: 'ru-RU', hd_locale: 'Russian', locale: 'ru', code: 'rus', name: 'Russian' },
|
||||
{ cr_locale: 'tr-TR', hd_locale: 'Turkish', locale: 'tr', code: 'tur', name: 'Turkish' },
|
||||
{ cr_locale: 'hi-IN', locale: 'hi', code: 'hin', name: 'Hindi' },
|
||||
{ locale: 'zh', code: 'cmn', name: 'Chinese (Mandarin, PRC)' },
|
||||
{ cr_locale: 'zh-CN', locale: 'zh-CN', code: 'zho', name: 'Chinese (Mainland China)' },
|
||||
{ cr_locale: 'zh-TW', locale: 'zh-TW', code: 'chi', name: 'Chinese (Taiwan)' },
|
||||
{ cr_locale: 'zh-HK', locale: 'zh-HK', code: 'zh-HK', name: 'Chinese (Hong-Kong)', language: '中文 (粵語)' },
|
||||
{ cr_locale: 'ko-KR', hd_locale: 'Korean', locale: 'ko', code: 'kor', name: 'Korean' },
|
||||
{ cr_locale: 'ca-ES', locale: 'ca-ES', code: 'cat', name: 'Catalan' },
|
||||
{ cr_locale: 'pl-PL', locale: 'pl-PL', code: 'pol', name: 'Polish' },
|
||||
{ cr_locale: 'th-TH', locale: 'th-TH', code: 'tha', name: 'Thai', language: 'ไทย' },
|
||||
{ cr_locale: 'ta-IN', locale: 'ta-IN', code: 'tam', name: 'Tamil (India)', language: 'தமிழ்' },
|
||||
{ cr_locale: 'ms-MY', locale: 'ms-MY', code: 'may', name: 'Malay (Malaysia)', language: 'Bahasa Melayu' },
|
||||
{ cr_locale: 'vi-VN', locale: 'vi-VN', code: 'vie', name: 'Vietnamese', language: 'Tiếng Việt' },
|
||||
{ cr_locale: 'id-ID', locale: 'id-ID', code: 'ind', name: 'Indonesian', language: 'Bahasa Indonesia' },
|
||||
{ cr_locale: 'te-IN', locale: 'te-IN', code: 'tel', name: 'Telugu (India)', language: 'తెలుగు' },
|
||||
{ cr_locale: 'ja-JP', adn_locale: 'ja', ao_locale: 'ja', hd_locale: 'Japanese', locale: 'ja', code: 'jpn', name: 'Japanese' },
|
||||
];
|
||||
|
||||
// add en language names
|
||||
(() =>{
|
||||
for(const languageIndex in languages){
|
||||
if(!languages[languageIndex].language){
|
||||
languages[languageIndex].language = languages[languageIndex].name;
|
||||
for(const languageIndex in languages){
|
||||
if(!languages[languageIndex].language){
|
||||
languages[languageIndex].language = languages[languageIndex].name;
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
// construct dub language codes
|
||||
const dubLanguageCodes = (() => {
|
||||
const dubLanguageCodesArray: string[] = [];
|
||||
for(const language of languages){
|
||||
dubLanguageCodesArray.push(language.code);
|
||||
}
|
||||
return [...new Set(dubLanguageCodesArray)];
|
||||
const dubLanguageCodesArray: string[] = [];
|
||||
for(const language of languages){
|
||||
dubLanguageCodesArray.push(language.code);
|
||||
}
|
||||
return [...new Set(dubLanguageCodesArray)];
|
||||
})();
|
||||
|
||||
// construct subtitle languages filter
|
||||
const subtitleLanguagesFilter = (() => {
|
||||
const subtitleLanguagesExtraParameters = ['all', 'none'];
|
||||
return [...subtitleLanguagesExtraParameters, ...new Set(languages.map(l => { return l.locale; }))];
|
||||
const subtitleLanguagesExtraParameters = ['all', 'none'];
|
||||
return [...subtitleLanguagesExtraParameters, ...new Set(languages.map(l => { return l.locale; }))];
|
||||
})();
|
||||
|
||||
const searchLocales = (() => {
|
||||
return ['', ...new Set(languages.map(l => { return l.cr_locale; }).slice(0, -1)), ...new Set(languages.map(l => { return l.adn_locale; }).slice(0, -1))];
|
||||
return ['', ...new Set(languages.map(l => { return l.cr_locale; }).slice(0, -1)), ...new Set(languages.map(l => { return l.adn_locale; }).slice(0, -1))];
|
||||
})();
|
||||
|
||||
export const aoSearchLocales = (() => {
|
||||
return ['', ...new Set(languages.map(l => { return l.ao_locale; }).slice(0, -1))];
|
||||
return ['', ...new Set(languages.map(l => { return l.ao_locale; }).slice(0, -1))];
|
||||
})();
|
||||
|
||||
// convert
|
||||
const fixLanguageTag = (tag: string) => {
|
||||
tag = typeof tag == 'string' ? tag : 'und';
|
||||
const tagLangLC = tag.match(/^(\w{2})-?(\w{2})$/);
|
||||
if(tagLangLC){
|
||||
const tagLang = `${tagLangLC[1]}-${tagLangLC[2].toUpperCase()}`;
|
||||
if(findLang(tagLang).cr_locale != 'und'){
|
||||
return findLang(tagLang).cr_locale;
|
||||
tag = typeof tag == 'string' ? tag : 'und';
|
||||
const tagLangLC = tag.match(/^(\w{2})-?(\w{2})$/);
|
||||
if(tagLangLC){
|
||||
const tagLang = `${tagLangLC[1]}-${tagLangLC[2].toUpperCase()}`;
|
||||
if(findLang(tagLang).cr_locale != 'und'){
|
||||
return findLang(tagLang).cr_locale;
|
||||
}
|
||||
else{
|
||||
return tagLang;
|
||||
}
|
||||
}
|
||||
else{
|
||||
return tagLang;
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
else{
|
||||
return tag;
|
||||
}
|
||||
};
|
||||
|
||||
// find lang by cr_locale
|
||||
const findLang = (cr_locale: string) => {
|
||||
const lang = languages.find(l => { return l.cr_locale == cr_locale; });
|
||||
return lang ? lang : languages.find(l => l.code === 'und') || { cr_locale: 'und', locale: 'un', code: 'und', name: 'Undetermined', language: 'Undetermined' };
|
||||
const lang = languages.find(l => { return l.cr_locale == cr_locale; });
|
||||
return lang ? lang : languages.find(l => l.code === 'und') || { cr_locale: 'und', locale: 'un', code: 'und', name: 'Undetermined', language: 'Undetermined' };
|
||||
};
|
||||
|
||||
const fixAndFindCrLC = (cr_locale: string) => {
|
||||
const str = fixLanguageTag(cr_locale);
|
||||
return findLang(str || '');
|
||||
const str = fixLanguageTag(cr_locale);
|
||||
return findLang(str || '');
|
||||
};
|
||||
|
||||
// rss subs lang parser
|
||||
const parseRssSubtitlesString = (subs: string) => {
|
||||
const splitMap = subs.replace(/\s/g, '').split(',').map((s) => {
|
||||
return fixAndFindCrLC(s).locale;
|
||||
});
|
||||
const sort = sortTags(splitMap);
|
||||
return sort.join(', ');
|
||||
const splitMap = subs.replace(/\s/g, '').split(',').map((s) => {
|
||||
return fixAndFindCrLC(s).locale;
|
||||
});
|
||||
const sort = sortTags(splitMap);
|
||||
return sort.join(', ');
|
||||
};
|
||||
|
||||
|
||||
// parse subtitles Array
|
||||
const parseSubtitlesArray = (tags: string[]) => {
|
||||
const sort = sortSubtitles(tags.map((t) => {
|
||||
return { locale: fixAndFindCrLC(t).locale };
|
||||
}));
|
||||
return sort.map((t) => { return t.locale; }).join(', ');
|
||||
const sort = sortSubtitles(tags.map((t) => {
|
||||
return { locale: fixAndFindCrLC(t).locale };
|
||||
}));
|
||||
return sort.map((t) => { return t.locale; }).join(', ');
|
||||
};
|
||||
|
||||
// sort subtitles
|
||||
const sortSubtitles = <T extends {
|
||||
[key: string]: unknown
|
||||
} = Record<string, string>> (data: T[], sortkey?: keyof T) : T[] => {
|
||||
const idx: Record<string, number> = {};
|
||||
const key = sortkey || 'locale' as keyof T;
|
||||
const tags = [...new Set(Object.values(languages).map(e => e.locale))];
|
||||
for(const l of tags){
|
||||
idx[l] = Object.keys(idx).length + 1;
|
||||
}
|
||||
data.sort((a, b) => {
|
||||
const ia = idx[a[key] as string] ? idx[a[key] as string] : 50;
|
||||
const ib = idx[b[key] as string] ? idx[b[key] as string] : 50;
|
||||
return ia - ib;
|
||||
});
|
||||
return data;
|
||||
const idx: Record<string, number> = {};
|
||||
const key = sortkey || 'locale' as keyof T;
|
||||
const tags = [...new Set(Object.values(languages).map(e => e.locale))];
|
||||
for(const l of tags){
|
||||
idx[l] = Object.keys(idx).length + 1;
|
||||
}
|
||||
data.sort((a, b) => {
|
||||
const ia = idx[a[key] as string] ? idx[a[key] as string] : 50;
|
||||
const ib = idx[b[key] as string] ? idx[b[key] as string] : 50;
|
||||
return ia - ib;
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
const sortTags = (data: string[]) => {
|
||||
const retData = data.map(e => { return { locale: e }; });
|
||||
const sort = sortSubtitles(retData);
|
||||
return sort.map(e => e.locale as string);
|
||||
const retData = data.map(e => { return { locale: e }; });
|
||||
const sort = sortSubtitles(retData);
|
||||
return sort.map(e => e.locale as string);
|
||||
};
|
||||
|
||||
const subsFile = (fnOutput:string, subsIndex: string, langItem: LanguageItem, isCC: boolean, ccTag: string, isSigns?: boolean, format?: string) => {
|
||||
subsIndex = (parseInt(subsIndex) + 1).toString().padStart(2, '0');
|
||||
return `${fnOutput}.${subsIndex}.${langItem.code}.${langItem.language}${isCC ? `.${ccTag}` : ''}${isSigns ? '.signs' : ''}.${format ? format : 'ass'}`;
|
||||
subsIndex = (parseInt(subsIndex) + 1).toString().padStart(2, '0');
|
||||
return `${fnOutput}.${subsIndex}.${langItem.code}.${langItem.language}${isCC ? `.${ccTag}` : ''}${isSigns ? '.signs' : ''}.${format ? format : 'ass'}`;
|
||||
};
|
||||
|
||||
// construct dub langs const
|
||||
const dubLanguages = (() => {
|
||||
const dubDb: Record<string, string> = {};
|
||||
for(const lang of languages){
|
||||
if(!Object.keys(dubDb).includes(lang.name)){
|
||||
dubDb[lang.name] = lang.code;
|
||||
const dubDb: Record<string, string> = {};
|
||||
for(const lang of languages){
|
||||
if(!Object.keys(dubDb).includes(lang.name)){
|
||||
dubDb[lang.name] = lang.code;
|
||||
}
|
||||
}
|
||||
}
|
||||
return dubDb;
|
||||
return dubDb;
|
||||
})();
|
||||
|
||||
// dub regex
|
||||
|
|
@ -171,34 +171,34 @@ const dubRegExp = new RegExp(dubRegExpStr);
|
|||
|
||||
// code to lang name
|
||||
const langCode2name = (code: string) => {
|
||||
const codeIdx = dubLanguageCodes.indexOf(code);
|
||||
return Object.keys(dubLanguages)[codeIdx];
|
||||
const codeIdx = dubLanguageCodes.indexOf(code);
|
||||
return Object.keys(dubLanguages)[codeIdx];
|
||||
};
|
||||
|
||||
// locale to lang name
|
||||
const locale2language = (locale: string) => {
|
||||
const filteredLocale = languages.filter(l => {
|
||||
return l.locale == locale;
|
||||
});
|
||||
return filteredLocale[0];
|
||||
const filteredLocale = languages.filter(l => {
|
||||
return l.locale == locale;
|
||||
});
|
||||
return filteredLocale[0];
|
||||
};
|
||||
|
||||
// output
|
||||
export {
|
||||
languages,
|
||||
dubLanguageCodes,
|
||||
dubLanguages,
|
||||
langCode2name,
|
||||
locale2language,
|
||||
dubRegExp,
|
||||
subtitleLanguagesFilter,
|
||||
searchLocales,
|
||||
fixLanguageTag,
|
||||
findLang,
|
||||
fixAndFindCrLC,
|
||||
parseRssSubtitlesString,
|
||||
parseSubtitlesArray,
|
||||
sortSubtitles,
|
||||
sortTags,
|
||||
subsFile,
|
||||
languages,
|
||||
dubLanguageCodes,
|
||||
dubLanguages,
|
||||
langCode2name,
|
||||
locale2language,
|
||||
dubRegExp,
|
||||
subtitleLanguagesFilter,
|
||||
searchLocales,
|
||||
fixLanguageTag,
|
||||
findLang,
|
||||
fixAndFindCrLC,
|
||||
parseRssSubtitlesString,
|
||||
parseSubtitlesArray,
|
||||
sortSubtitles,
|
||||
sortTags,
|
||||
subsFile,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -60,370 +60,370 @@ export type MergerOptions = {
|
|||
|
||||
class Merger {
|
||||
|
||||
constructor(private options: MergerOptions) {
|
||||
if (this.options.skipSubMux)
|
||||
this.options.subtitles = [];
|
||||
if (this.options.videoTitle)
|
||||
this.options.videoTitle = this.options.videoTitle.replace(/"/g, '\'');
|
||||
}
|
||||
constructor(private options: MergerOptions) {
|
||||
if (this.options.skipSubMux)
|
||||
this.options.subtitles = [];
|
||||
if (this.options.videoTitle)
|
||||
this.options.videoTitle = this.options.videoTitle.replace(/"/g, '\'');
|
||||
}
|
||||
|
||||
public async createDelays() {
|
||||
public async createDelays() {
|
||||
//Don't bother scanning it if there is only 1 vna stream
|
||||
if (this.options.videoAndAudio.length > 1) {
|
||||
const bin = await yamlCfg.loadBinCfg();
|
||||
const vnas = this.options.videoAndAudio;
|
||||
//get and set durations on each videoAndAudio Stream
|
||||
for (const [vnaIndex, vna] of vnas.entries()) {
|
||||
const streamInfo = await ffprobe(vna.path, { path: bin.ffprobe as string });
|
||||
const videoInfo = streamInfo.streams.filter(stream => stream.codec_type == 'video');
|
||||
vnas[vnaIndex].duration = parseInt(videoInfo[0].duration as string);
|
||||
}
|
||||
//Sort videoAndAudio streams by duration (shortest first)
|
||||
vnas.sort((a,b) => {
|
||||
if (!a.duration || !b.duration) return -1;
|
||||
return a.duration - b.duration;
|
||||
});
|
||||
//Set Delays
|
||||
const shortestDuration = vnas[0].duration;
|
||||
for (const [vnaIndex, vna] of vnas.entries()) {
|
||||
//Don't calculate the shortestDuration track
|
||||
if (vnaIndex == 0) {
|
||||
if (!vna.isPrimary && vna.isPrimary !== undefined)
|
||||
console.warn('Shortest video isn\'t primary, this might lead to problems with subtitles. Please report on github or discord if you experience issues.');
|
||||
continue;
|
||||
if (this.options.videoAndAudio.length > 1) {
|
||||
const bin = await yamlCfg.loadBinCfg();
|
||||
const vnas = this.options.videoAndAudio;
|
||||
//get and set durations on each videoAndAudio Stream
|
||||
for (const [vnaIndex, vna] of vnas.entries()) {
|
||||
const streamInfo = await ffprobe(vna.path, { path: bin.ffprobe as string });
|
||||
const videoInfo = streamInfo.streams.filter(stream => stream.codec_type == 'video');
|
||||
vnas[vnaIndex].duration = parseInt(videoInfo[0].duration as string);
|
||||
}
|
||||
//Sort videoAndAudio streams by duration (shortest first)
|
||||
vnas.sort((a,b) => {
|
||||
if (!a.duration || !b.duration) return -1;
|
||||
return a.duration - b.duration;
|
||||
});
|
||||
//Set Delays
|
||||
const shortestDuration = vnas[0].duration;
|
||||
for (const [vnaIndex, vna] of vnas.entries()) {
|
||||
//Don't calculate the shortestDuration track
|
||||
if (vnaIndex == 0) {
|
||||
if (!vna.isPrimary && vna.isPrimary !== undefined)
|
||||
console.warn('Shortest video isn\'t primary, this might lead to problems with subtitles. Please report on github or discord if you experience issues.');
|
||||
continue;
|
||||
}
|
||||
if (vna.duration && shortestDuration) {
|
||||
//Calculate the tracks delay
|
||||
vna.delay = Math.ceil((vna.duration-shortestDuration) * 1000) / 1000;
|
||||
//TODO: set primary language for audio so it can be used to determine which track needs the delay
|
||||
//The above is a problem in the event that it isn't the dub that needs the delay, but rather the sub.
|
||||
//Alternatively: Might not work: it could be checked if there are multiple of the same video language, and if there is
|
||||
//more than 1 of the same video language, then do the subtitle delay on CC, else normal language.
|
||||
const subtitles = this.options.subtitles.filter(sub => sub.language.code == vna.lang.code);
|
||||
for (const [subIndex, sub] of subtitles.entries()) {
|
||||
if (vna.isPrimary) subtitles[subIndex].delay = vna.delay;
|
||||
else if (sub.closedCaption) subtitles[subIndex].delay = vna.delay;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (vna.duration && shortestDuration) {
|
||||
//Calculate the tracks delay
|
||||
vna.delay = Math.ceil((vna.duration-shortestDuration) * 1000) / 1000;
|
||||
//TODO: set primary language for audio so it can be used to determine which track needs the delay
|
||||
//The above is a problem in the event that it isn't the dub that needs the delay, but rather the sub.
|
||||
//Alternatively: Might not work: it could be checked if there are multiple of the same video language, and if there is
|
||||
//more than 1 of the same video language, then do the subtitle delay on CC, else normal language.
|
||||
const subtitles = this.options.subtitles.filter(sub => sub.language.code == vna.lang.code);
|
||||
for (const [subIndex, sub] of subtitles.entries()) {
|
||||
if (vna.isPrimary) subtitles[subIndex].delay = vna.delay;
|
||||
else if (sub.closedCaption) subtitles[subIndex].delay = vna.delay;
|
||||
}
|
||||
}
|
||||
|
||||
public FFmpeg() : string {
|
||||
const args: string[] = [];
|
||||
const metaData: string[] = [];
|
||||
|
||||
let index = 0;
|
||||
let audioIndex = 0;
|
||||
let hasVideo = false;
|
||||
|
||||
for (const vid of this.options.videoAndAudio) {
|
||||
if (vid.delay && hasVideo) {
|
||||
args.push(
|
||||
`-itsoffset -${Math.ceil(vid.delay*1000)}ms`
|
||||
);
|
||||
}
|
||||
args.push(`-i "${vid.path}"`);
|
||||
if (!hasVideo || this.options.keepAllVideos) {
|
||||
metaData.push(`-map ${index}:a -map ${index}:v`);
|
||||
metaData.push(`-metadata:s:a:${audioIndex} language=${vid.lang.code}`);
|
||||
metaData.push(`-metadata:s:v:${index} title="${this.options.videoTitle}"`);
|
||||
hasVideo = true;
|
||||
} else {
|
||||
metaData.push(`-map ${index}:a`);
|
||||
metaData.push(`-metadata:s:a:${audioIndex} language=${vid.lang.code}`);
|
||||
}
|
||||
audioIndex++;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FFmpeg() : string {
|
||||
const args: string[] = [];
|
||||
const metaData: string[] = [];
|
||||
|
||||
let index = 0;
|
||||
let audioIndex = 0;
|
||||
let hasVideo = false;
|
||||
|
||||
for (const vid of this.options.videoAndAudio) {
|
||||
if (vid.delay && hasVideo) {
|
||||
args.push(
|
||||
`-itsoffset -${Math.ceil(vid.delay*1000)}ms`
|
||||
);
|
||||
}
|
||||
args.push(`-i "${vid.path}"`);
|
||||
if (!hasVideo || this.options.keepAllVideos) {
|
||||
metaData.push(`-map ${index}:a -map ${index}:v`);
|
||||
metaData.push(`-metadata:s:a:${audioIndex} language=${vid.lang.code}`);
|
||||
metaData.push(`-metadata:s:v:${index} title="${this.options.videoTitle}"`);
|
||||
hasVideo = true;
|
||||
} else {
|
||||
metaData.push(`-map ${index}:a`);
|
||||
metaData.push(`-metadata:s:a:${audioIndex} language=${vid.lang.code}`);
|
||||
}
|
||||
audioIndex++;
|
||||
index++;
|
||||
}
|
||||
|
||||
for (const vid of this.options.onlyVid) {
|
||||
if (!hasVideo || this.options.keepAllVideos) {
|
||||
args.push(`-i "${vid.path}"`);
|
||||
metaData.push(`-map ${index} -map -${index}:a`);
|
||||
metaData.push(`-metadata:s:v:${index} title="${this.options.videoTitle}"`);
|
||||
hasVideo = true;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
for (const aud of this.options.onlyAudio) {
|
||||
args.push(`-i "${aud.path}"`);
|
||||
metaData.push(`-map ${index}`);
|
||||
metaData.push(`-metadata:s:a:${audioIndex} language=${aud.lang.code}`);
|
||||
index++;
|
||||
audioIndex++;
|
||||
}
|
||||
|
||||
for (const index in this.options.subtitles) {
|
||||
const sub = this.options.subtitles[index];
|
||||
if (sub.delay) {
|
||||
args.push(
|
||||
`-itsoffset -${Math.ceil(sub.delay*1000)}ms`
|
||||
);
|
||||
}
|
||||
args.push(`-i "${sub.file}"`);
|
||||
}
|
||||
|
||||
if (this.options.chapters && this.options.chapters.length > 0) {
|
||||
const chapterFilePath = this.options.chapters[0].path;
|
||||
const chapterData = convertChaptersToFFmpegFormat(this.options.chapters[0].path);
|
||||
fs.writeFileSync(chapterFilePath, chapterData, 'utf-8');
|
||||
args.push(`-i "${chapterFilePath}" -map_metadata 1`);
|
||||
}
|
||||
|
||||
if (this.options.output.split('.').pop() === 'mkv') {
|
||||
if (this.options.fonts) {
|
||||
let fontIndex = 0;
|
||||
for (const font of this.options.fonts) {
|
||||
args.push(`-attach ${font.path} -metadata:s:t:${fontIndex} mimetype=${font.mime} -metadata:s:t:${fontIndex} filename=${font.name}`);
|
||||
fontIndex++;
|
||||
for (const vid of this.options.onlyVid) {
|
||||
if (!hasVideo || this.options.keepAllVideos) {
|
||||
args.push(`-i "${vid.path}"`);
|
||||
metaData.push(`-map ${index} -map -${index}:a`);
|
||||
metaData.push(`-metadata:s:v:${index} title="${this.options.videoTitle}"`);
|
||||
hasVideo = true;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const aud of this.options.onlyAudio) {
|
||||
args.push(`-i "${aud.path}"`);
|
||||
metaData.push(`-map ${index}`);
|
||||
metaData.push(`-metadata:s:a:${audioIndex} language=${aud.lang.code}`);
|
||||
index++;
|
||||
audioIndex++;
|
||||
}
|
||||
|
||||
for (const index in this.options.subtitles) {
|
||||
const sub = this.options.subtitles[index];
|
||||
if (sub.delay) {
|
||||
args.push(
|
||||
`-itsoffset -${Math.ceil(sub.delay*1000)}ms`
|
||||
);
|
||||
}
|
||||
args.push(`-i "${sub.file}"`);
|
||||
}
|
||||
|
||||
if (this.options.chapters && this.options.chapters.length > 0) {
|
||||
const chapterFilePath = this.options.chapters[0].path;
|
||||
const chapterData = convertChaptersToFFmpegFormat(this.options.chapters[0].path);
|
||||
fs.writeFileSync(chapterFilePath, chapterData, 'utf-8');
|
||||
args.push(`-i "${chapterFilePath}" -map_metadata 1`);
|
||||
}
|
||||
|
||||
if (this.options.output.split('.').pop() === 'mkv') {
|
||||
if (this.options.fonts) {
|
||||
let fontIndex = 0;
|
||||
for (const font of this.options.fonts) {
|
||||
args.push(`-attach ${font.path} -metadata:s:t:${fontIndex} mimetype=${font.mime} -metadata:s:t:${fontIndex} filename=${font.name}`);
|
||||
fontIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
args.push(...metaData);
|
||||
args.push(...this.options.subtitles.map((_, subIndex) => `-map ${subIndex + index}`));
|
||||
args.push(
|
||||
'-c:v copy',
|
||||
'-c:a copy',
|
||||
this.options.output.split('.').pop()?.toLowerCase() === 'mp4' ? '-c:s mov_text' : '-c:s ass',
|
||||
...this.options.subtitles.map((sub, subindex) => `-metadata:s:s:${subindex} title="${
|
||||
(sub.language.language || sub.language.name) + `${sub.closedCaption === true ? ` ${this.options.ccTag}` : ''}` + `${sub.signs === true ? ' Signs' : ''}`
|
||||
}" -metadata:s:s:${subindex} language=${sub.language.code}`)
|
||||
);
|
||||
args.push(...this.options.options.ffmpeg);
|
||||
args.push(`"${this.options.output}"`);
|
||||
return args.join(' ');
|
||||
}
|
||||
|
||||
args.push(...metaData);
|
||||
args.push(...this.options.subtitles.map((_, subIndex) => `-map ${subIndex + index}`));
|
||||
args.push(
|
||||
'-c:v copy',
|
||||
'-c:a copy',
|
||||
this.options.output.split('.').pop()?.toLowerCase() === 'mp4' ? '-c:s mov_text' : '-c:s ass',
|
||||
...this.options.subtitles.map((sub, subindex) => `-metadata:s:s:${subindex} title="${
|
||||
(sub.language.language || sub.language.name) + `${sub.closedCaption === true ? ` ${this.options.ccTag}` : ''}` + `${sub.signs === true ? ' Signs' : ''}`
|
||||
}" -metadata:s:s:${subindex} language=${sub.language.code}`)
|
||||
);
|
||||
args.push(...this.options.options.ffmpeg);
|
||||
args.push(`"${this.options.output}"`);
|
||||
return args.join(' ');
|
||||
}
|
||||
public static getLanguageCode = (from: string, _default = 'eng'): string => {
|
||||
if (from === 'cmn') return 'chi';
|
||||
for (const lang in iso639.iso_639_2) {
|
||||
const langObj = iso639.iso_639_2[lang];
|
||||
if (Object.prototype.hasOwnProperty.call(langObj, '639-1') && langObj['639-1'] === from) {
|
||||
return langObj['639-2'] as string;
|
||||
}
|
||||
}
|
||||
return _default;
|
||||
};
|
||||
|
||||
public static getLanguageCode = (from: string, _default = 'eng'): string => {
|
||||
if (from === 'cmn') return 'chi';
|
||||
for (const lang in iso639.iso_639_2) {
|
||||
const langObj = iso639.iso_639_2[lang];
|
||||
if (Object.prototype.hasOwnProperty.call(langObj, '639-1') && langObj['639-1'] === from) {
|
||||
return langObj['639-2'] as string;
|
||||
}
|
||||
}
|
||||
return _default;
|
||||
};
|
||||
public MkvMerge = () => {
|
||||
const args: string[] = [];
|
||||
|
||||
public MkvMerge = () => {
|
||||
const args: string[] = [];
|
||||
let hasVideo = false;
|
||||
|
||||
let hasVideo = false;
|
||||
args.push(`-o "${this.options.output}"`);
|
||||
args.push(...this.options.options.mkvmerge);
|
||||
|
||||
args.push(`-o "${this.options.output}"`);
|
||||
args.push(...this.options.options.mkvmerge);
|
||||
for (const vid of this.options.onlyVid) {
|
||||
if (!hasVideo || this.options.keepAllVideos) {
|
||||
args.push(
|
||||
'--video-tracks 0',
|
||||
'--no-audio'
|
||||
);
|
||||
const trackName = ((this.options.videoTitle ?? vid.lang.name) + (this.options.simul ? ' [Simulcast]' : ' [Uncut]'));
|
||||
args.push('--track-name', `0:"${trackName}"`);
|
||||
args.push(`--language 0:${vid.lang.code}`);
|
||||
hasVideo = true;
|
||||
args.push(`"${vid.path}"`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const vid of this.options.onlyVid) {
|
||||
if (!hasVideo || this.options.keepAllVideos) {
|
||||
args.push(
|
||||
'--video-tracks 0',
|
||||
'--no-audio'
|
||||
);
|
||||
const trackName = ((this.options.videoTitle ?? vid.lang.name) + (this.options.simul ? ' [Simulcast]' : ' [Uncut]'));
|
||||
args.push('--track-name', `0:"${trackName}"`);
|
||||
args.push(`--language 0:${vid.lang.code}`);
|
||||
hasVideo = true;
|
||||
args.push(`"${vid.path}"`);
|
||||
}
|
||||
}
|
||||
for (const vid of this.options.videoAndAudio) {
|
||||
const audioTrackNum = this.options.inverseTrackOrder ? '0' : '1';
|
||||
const videoTrackNum = this.options.inverseTrackOrder ? '1' : '0';
|
||||
if (vid.delay) {
|
||||
args.push(
|
||||
`--sync ${audioTrackNum}:-${Math.ceil(vid.delay*1000)}`
|
||||
);
|
||||
}
|
||||
if (!hasVideo || this.options.keepAllVideos) {
|
||||
args.push(
|
||||
`--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 ${audioTrackNum}:${vid.lang.code}`);
|
||||
if (this.options.defaults.audio.code === vid.lang.code) {
|
||||
args.push(`--default-track ${audioTrackNum}`);
|
||||
} else {
|
||||
args.push(`--default-track ${audioTrackNum}:0`);
|
||||
}
|
||||
hasVideo = true;
|
||||
} else {
|
||||
args.push(
|
||||
'--no-video',
|
||||
`--audio-tracks ${audioTrackNum}`
|
||||
);
|
||||
if (this.options.defaults.audio.code === vid.lang.code) {
|
||||
args.push(`--default-track ${audioTrackNum}`);
|
||||
} else {
|
||||
args.push(`--default-track ${audioTrackNum}:0`);
|
||||
}
|
||||
args.push('--track-name', `${audioTrackNum}:"${vid.lang.name}"`);
|
||||
args.push(`--language ${audioTrackNum}:${vid.lang.code}`);
|
||||
}
|
||||
args.push(`"${vid.path}"`);
|
||||
}
|
||||
|
||||
for (const vid of this.options.videoAndAudio) {
|
||||
const audioTrackNum = this.options.inverseTrackOrder ? '0' : '1';
|
||||
const videoTrackNum = this.options.inverseTrackOrder ? '1' : '0';
|
||||
if (vid.delay) {
|
||||
args.push(
|
||||
`--sync ${audioTrackNum}:-${Math.ceil(vid.delay*1000)}`
|
||||
);
|
||||
}
|
||||
if (!hasVideo || this.options.keepAllVideos) {
|
||||
args.push(
|
||||
`--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 ${audioTrackNum}:${vid.lang.code}`);
|
||||
if (this.options.defaults.audio.code === vid.lang.code) {
|
||||
args.push(`--default-track ${audioTrackNum}`);
|
||||
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}`);
|
||||
args.push(
|
||||
'--no-video',
|
||||
'--audio-tracks 0'
|
||||
);
|
||||
if (this.options.defaults.audio.code === aud.lang.code) {
|
||||
args.push('--default-track 0');
|
||||
} else {
|
||||
args.push('--default-track 0:0');
|
||||
}
|
||||
args.push(`"${aud.path}"`);
|
||||
}
|
||||
|
||||
if (this.options.subtitles.length > 0) {
|
||||
for (const subObj of this.options.subtitles) {
|
||||
if (subObj.delay) {
|
||||
args.push(
|
||||
`--sync 0:-${Math.ceil(subObj.delay*1000)}`
|
||||
);
|
||||
}
|
||||
args.push('--track-name', `0:"${(subObj.language.language || subObj.language.name) + `${subObj.closedCaption === true ? ` ${this.options.ccTag}` : ''}` + `${subObj.signs === true ? ' Signs' : ''}`}"`);
|
||||
args.push('--language', `0:"${subObj.language.code}"`);
|
||||
//TODO: look into making Closed Caption default if it's the only sub of the default language downloaded
|
||||
if (this.options.defaults.sub.code === subObj.language.code && !subObj.closedCaption) {
|
||||
args.push('--default-track 0');
|
||||
} else {
|
||||
args.push('--default-track 0:0');
|
||||
}
|
||||
args.push(`"${subObj.file}"`);
|
||||
}
|
||||
} else {
|
||||
args.push(`--default-track ${audioTrackNum}:0`);
|
||||
args.push(
|
||||
'--no-subtitles',
|
||||
);
|
||||
}
|
||||
hasVideo = true;
|
||||
} else {
|
||||
args.push(
|
||||
'--no-video',
|
||||
`--audio-tracks ${audioTrackNum}`
|
||||
);
|
||||
if (this.options.defaults.audio.code === vid.lang.code) {
|
||||
args.push(`--default-track ${audioTrackNum}`);
|
||||
|
||||
if (this.options.fonts && this.options.fonts.length > 0) {
|
||||
for (const f of this.options.fonts) {
|
||||
args.push('--attachment-name', f.name);
|
||||
args.push('--attachment-mime-type', f.mime);
|
||||
args.push('--attach-file', `"${f.path}"`);
|
||||
}
|
||||
} else {
|
||||
args.push(`--default-track ${audioTrackNum}:0`);
|
||||
args.push(
|
||||
'--no-attachments'
|
||||
);
|
||||
}
|
||||
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}`);
|
||||
args.push(
|
||||
'--no-video',
|
||||
'--audio-tracks 0'
|
||||
);
|
||||
if (this.options.defaults.audio.code === aud.lang.code) {
|
||||
args.push('--default-track 0');
|
||||
} else {
|
||||
args.push('--default-track 0:0');
|
||||
}
|
||||
args.push(`"${aud.path}"`);
|
||||
}
|
||||
|
||||
if (this.options.subtitles.length > 0) {
|
||||
for (const subObj of this.options.subtitles) {
|
||||
if (subObj.delay) {
|
||||
args.push(
|
||||
`--sync 0:-${Math.ceil(subObj.delay*1000)}`
|
||||
);
|
||||
if (this.options.chapters && this.options.chapters.length > 0) {
|
||||
args.push(`--chapters "${this.options.chapters[0].path}"`);
|
||||
}
|
||||
args.push('--track-name', `0:"${(subObj.language.language || subObj.language.name) + `${subObj.closedCaption === true ? ` ${this.options.ccTag}` : ''}` + `${subObj.signs === true ? ' Signs' : ''}`}"`);
|
||||
args.push('--language', `0:"${subObj.language.code}"`);
|
||||
//TODO: look into making Closed Caption default if it's the only sub of the default language downloaded
|
||||
if (this.options.defaults.sub.code === subObj.language.code && !subObj.closedCaption) {
|
||||
args.push('--default-track 0');
|
||||
} else {
|
||||
args.push('--default-track 0:0');
|
||||
}
|
||||
args.push(`"${subObj.file}"`);
|
||||
}
|
||||
} else {
|
||||
args.push(
|
||||
'--no-subtitles',
|
||||
);
|
||||
}
|
||||
|
||||
if (this.options.fonts && this.options.fonts.length > 0) {
|
||||
for (const f of this.options.fonts) {
|
||||
args.push('--attachment-name', f.name);
|
||||
args.push('--attachment-mime-type', f.mime);
|
||||
args.push('--attach-file', `"${f.path}"`);
|
||||
}
|
||||
} else {
|
||||
args.push(
|
||||
'--no-attachments'
|
||||
);
|
||||
}
|
||||
return args.join(' ');
|
||||
};
|
||||
|
||||
if (this.options.chapters && this.options.chapters.length > 0) {
|
||||
args.push(`--chapters "${this.options.chapters[0].path}"`);
|
||||
}
|
||||
|
||||
return args.join(' ');
|
||||
};
|
||||
|
||||
public static checkMerger(bin: {
|
||||
public static checkMerger(bin: {
|
||||
mkvmerge?: string,
|
||||
ffmpeg?: string,
|
||||
}, useMP4format: boolean, force: AvailableMuxer|undefined) : {
|
||||
MKVmerge?: string,
|
||||
FFmpeg?: string
|
||||
} {
|
||||
if (force && bin[force]) {
|
||||
return {
|
||||
FFmpeg: force === 'ffmpeg' ? bin.ffmpeg : undefined,
|
||||
MKVmerge: force === 'mkvmerge' ? bin.mkvmerge : undefined
|
||||
};
|
||||
if (force && bin[force]) {
|
||||
return {
|
||||
FFmpeg: force === 'ffmpeg' ? bin.ffmpeg : undefined,
|
||||
MKVmerge: force === 'mkvmerge' ? bin.mkvmerge : undefined
|
||||
};
|
||||
}
|
||||
if (useMP4format && bin.ffmpeg) {
|
||||
return {
|
||||
FFmpeg: bin.ffmpeg
|
||||
};
|
||||
} else if (!useMP4format && (bin.mkvmerge || bin.ffmpeg)) {
|
||||
return {
|
||||
MKVmerge: bin.mkvmerge,
|
||||
FFmpeg: bin.ffmpeg
|
||||
};
|
||||
} else if (useMP4format) {
|
||||
console.warn('FFmpeg not found, skip muxing...');
|
||||
} else if (!bin.mkvmerge) {
|
||||
console.warn('MKVMerge not found, skip muxing...');
|
||||
}
|
||||
return {};
|
||||
}
|
||||
if (useMP4format && bin.ffmpeg) {
|
||||
return {
|
||||
FFmpeg: bin.ffmpeg
|
||||
};
|
||||
} else if (!useMP4format && (bin.mkvmerge || bin.ffmpeg)) {
|
||||
return {
|
||||
MKVmerge: bin.mkvmerge,
|
||||
FFmpeg: bin.ffmpeg
|
||||
};
|
||||
} else if (useMP4format) {
|
||||
console.warn('FFmpeg not found, skip muxing...');
|
||||
} else if (!bin.mkvmerge) {
|
||||
console.warn('MKVMerge not found, skip muxing...');
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
public static makeFontsList (fontsDir: string, subs: {
|
||||
public static makeFontsList (fontsDir: string, subs: {
|
||||
language: LanguageItem,
|
||||
fonts: Font[]
|
||||
}[]) : ParsedFont[] {
|
||||
let fontsNameList: Font[] = []; const fontsList: { name: string, path: string, mime: string }[] = [], subsList: string[] = []; let isNstr = true;
|
||||
for(const s of subs){
|
||||
fontsNameList.push(...s.fonts);
|
||||
subsList.push(s.language.locale);
|
||||
}
|
||||
fontsNameList = [...new Set(fontsNameList)];
|
||||
if(subsList.length > 0){
|
||||
console.info('\nSubtitles: %s (Total: %s)', subsList.join(', '), subsList.length);
|
||||
isNstr = false;
|
||||
}
|
||||
if(fontsNameList.length > 0){
|
||||
console.info((isNstr ? '\n' : '') + 'Required fonts: %s (Total: %s)', fontsNameList.join(', '), fontsNameList.length);
|
||||
}
|
||||
for(const f of fontsNameList){
|
||||
const fontFiles = fontFamilies[f];
|
||||
if(fontFiles){
|
||||
for (const fontFile of fontFiles) {
|
||||
const fontPath = path.join(fontsDir, fontFile);
|
||||
const mime = fontMime(fontFile);
|
||||
if(fs.existsSync(fontPath) && fs.statSync(fontPath).size != 0){
|
||||
fontsList.push({
|
||||
name: fontFile,
|
||||
path: fontPath,
|
||||
mime: mime,
|
||||
});
|
||||
}
|
||||
let fontsNameList: Font[] = []; const fontsList: { name: string, path: string, mime: string }[] = [], subsList: string[] = []; let isNstr = true;
|
||||
for(const s of subs){
|
||||
fontsNameList.push(...s.fonts);
|
||||
subsList.push(s.language.locale);
|
||||
}
|
||||
}
|
||||
fontsNameList = [...new Set(fontsNameList)];
|
||||
if(subsList.length > 0){
|
||||
console.info('\nSubtitles: %s (Total: %s)', subsList.join(', '), subsList.length);
|
||||
isNstr = false;
|
||||
}
|
||||
if(fontsNameList.length > 0){
|
||||
console.info((isNstr ? '\n' : '') + 'Required fonts: %s (Total: %s)', fontsNameList.join(', '), fontsNameList.length);
|
||||
}
|
||||
for(const f of fontsNameList){
|
||||
const fontFiles = fontFamilies[f];
|
||||
if(fontFiles){
|
||||
for (const fontFile of fontFiles) {
|
||||
const fontPath = path.join(fontsDir, fontFile);
|
||||
const mime = fontMime(fontFile);
|
||||
if(fs.existsSync(fontPath) && fs.statSync(fontPath).size != 0){
|
||||
fontsList.push({
|
||||
name: fontFile,
|
||||
path: fontPath,
|
||||
mime: mime,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return fontsList;
|
||||
}
|
||||
return fontsList;
|
||||
}
|
||||
|
||||
public async merge(type: 'ffmpeg'|'mkvmerge', bin: string) {
|
||||
let command: string|undefined = undefined;
|
||||
switch (type) {
|
||||
case 'ffmpeg':
|
||||
command = this.FFmpeg();
|
||||
break;
|
||||
case 'mkvmerge':
|
||||
command = this.MkvMerge();
|
||||
break;
|
||||
public async merge(type: 'ffmpeg'|'mkvmerge', bin: string) {
|
||||
let command: string|undefined = undefined;
|
||||
switch (type) {
|
||||
case 'ffmpeg':
|
||||
command = this.FFmpeg();
|
||||
break;
|
||||
case 'mkvmerge':
|
||||
command = this.MkvMerge();
|
||||
break;
|
||||
}
|
||||
if (command === undefined) {
|
||||
console.warn('Unable to merge files.');
|
||||
return;
|
||||
}
|
||||
console.info(`[${type}] Started merging`);
|
||||
const res = Helper.exec(type, `"${bin}"`, command);
|
||||
if (!res.isOk && type === 'mkvmerge' && res.err.code === 1) {
|
||||
console.info(`[${type}] Mkvmerge finished with at least one warning`);
|
||||
} else if (!res.isOk) {
|
||||
console.error(res.err);
|
||||
console.error(`[${type}] Merging failed with exit code ${res.err.code}`);
|
||||
} else {
|
||||
console.info(`[${type} Done]`);
|
||||
}
|
||||
}
|
||||
if (command === undefined) {
|
||||
console.warn('Unable to merge files.');
|
||||
return;
|
||||
}
|
||||
console.info(`[${type}] Started merging`);
|
||||
const res = Helper.exec(type, `"${bin}"`, command);
|
||||
if (!res.isOk && type === 'mkvmerge' && res.err.code === 1) {
|
||||
console.info(`[${type}] Mkvmerge finished with at least one warning`);
|
||||
} else if (!res.isOk) {
|
||||
console.error(res.err);
|
||||
console.error(`[${type}] Merging failed with exit code ${res.err.code}`);
|
||||
} else {
|
||||
console.info(`[${type} Done]`);
|
||||
}
|
||||
}
|
||||
|
||||
public cleanUp() {
|
||||
this.options.onlyAudio.concat(this.options.onlyVid).concat(this.options.videoAndAudio).forEach(a => fs.unlinkSync(a.path));
|
||||
this.options.chapters?.forEach(a => fs.unlinkSync(a.path));
|
||||
this.options.subtitles.forEach(a => fs.unlinkSync(a.file));
|
||||
}
|
||||
public cleanUp() {
|
||||
this.options.onlyAudio.concat(this.options.onlyVid).concat(this.options.videoAndAudio).forEach(a => fs.unlinkSync(a.path));
|
||||
this.options.chapters?.forEach(a => fs.unlinkSync(a.path));
|
||||
this.options.subtitles.forEach(a => fs.unlinkSync(a.file));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,107 +4,107 @@ const parseSelect = (selectString: string, but = false) : {
|
|||
isSelected: (val: string|string[]) => boolean,
|
||||
values: string[]
|
||||
} => {
|
||||
if (!selectString)
|
||||
return {
|
||||
values: [],
|
||||
isSelected: () => but
|
||||
};
|
||||
const parts = selectString.split(',');
|
||||
const select: string[] = [];
|
||||
if (!selectString)
|
||||
return {
|
||||
values: [],
|
||||
isSelected: () => but
|
||||
};
|
||||
const parts = selectString.split(',');
|
||||
const select: string[] = [];
|
||||
|
||||
parts.forEach(part => {
|
||||
if (part.includes('-')) {
|
||||
const splits = part.split('-');
|
||||
if (splits.length !== 2) {
|
||||
console.warn(`[WARN] Unable to parse input "${part}"`);
|
||||
return;
|
||||
}
|
||||
parts.forEach(part => {
|
||||
if (part.includes('-')) {
|
||||
const splits = part.split('-');
|
||||
if (splits.length !== 2) {
|
||||
console.warn(`[WARN] Unable to parse input "${part}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
const firstPart = splits[0];
|
||||
const match = firstPart.match(/[A-Za-z]+/);
|
||||
if (match && match.length > 0) {
|
||||
if (match.index && match.index !== 0) {
|
||||
console.warn(`[WARN] Unable to parse input "${part}"`);
|
||||
return;
|
||||
}
|
||||
const letters = firstPart.substring(0, match[0].length);
|
||||
const number = parseFloat(firstPart.substring(match[0].length));
|
||||
const b = parseFloat(splits[1]);
|
||||
if (isNaN(number) || isNaN(b)) {
|
||||
console.warn(`[WARN] Unable to parse input "${part}"`);
|
||||
return;
|
||||
}
|
||||
for (let i = number; i <= b; i++) {
|
||||
select.push(`${letters}${i}`);
|
||||
}
|
||||
const firstPart = splits[0];
|
||||
const match = firstPart.match(/[A-Za-z]+/);
|
||||
if (match && match.length > 0) {
|
||||
if (match.index && match.index !== 0) {
|
||||
console.warn(`[WARN] Unable to parse input "${part}"`);
|
||||
return;
|
||||
}
|
||||
const letters = firstPart.substring(0, match[0].length);
|
||||
const number = parseFloat(firstPart.substring(match[0].length));
|
||||
const b = parseFloat(splits[1]);
|
||||
if (isNaN(number) || isNaN(b)) {
|
||||
console.warn(`[WARN] Unable to parse input "${part}"`);
|
||||
return;
|
||||
}
|
||||
for (let i = number; i <= b; i++) {
|
||||
select.push(`${letters}${i}`);
|
||||
}
|
||||
|
||||
} else {
|
||||
const a = parseFloat(firstPart);
|
||||
const b = parseFloat(splits[1]);
|
||||
if (isNaN(a) || isNaN(b)) {
|
||||
console.warn(`[WARN] Unable to parse input "${part}"`);
|
||||
return;
|
||||
}
|
||||
for (let i = a; i <= b; i++) {
|
||||
select.push(`${i}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const a = parseFloat(firstPart);
|
||||
const b = parseFloat(splits[1]);
|
||||
if (isNaN(a) || isNaN(b)) {
|
||||
console.warn(`[WARN] Unable to parse input "${part}"`);
|
||||
return;
|
||||
}
|
||||
for (let i = a; i <= b; i++) {
|
||||
select.push(`${i}`);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
if (part.match(/[0-9A-Z]{9}/)) {
|
||||
select.push(part);
|
||||
return;
|
||||
} else if (part.match(/[A-Z]{3}\.[0-9]*/)) {
|
||||
select.push(part);
|
||||
return;
|
||||
}
|
||||
const match = part.match(/[A-Za-z]+/);
|
||||
if (match && match.length > 0) {
|
||||
if (match.index && match.index !== 0) {
|
||||
console.warn(`[WARN] Unable to parse input "${part}"`);
|
||||
return;
|
||||
}
|
||||
const letters = part.substring(0, match[0].length);
|
||||
const number = parseFloat(part.substring(match[0].length));
|
||||
if (isNaN(number)) {
|
||||
console.warn(`[WARN] Unable to parse input "${part}"`);
|
||||
return;
|
||||
}
|
||||
select.push(`${letters}${number}`);
|
||||
} else {
|
||||
select.push(`${parseFloat(part)}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
values: select,
|
||||
isSelected: (st) => {
|
||||
if (typeof st === 'string')
|
||||
st = [st];
|
||||
return st.some(st => {
|
||||
const match = st.match(/[A-Za-z]+/);
|
||||
if (st.match(/[0-9A-Z]{9}/)) {
|
||||
const included = select.includes(st);
|
||||
return but ? !included : included;
|
||||
} else if (match && match.length > 0) {
|
||||
if (match.index && match.index !== 0) {
|
||||
return false;
|
||||
}
|
||||
const letter = st.substring(0, match[0].length);
|
||||
const number = parseFloat(st.substring(match[0].length));
|
||||
if (isNaN(number)) {
|
||||
return false;
|
||||
}
|
||||
const included = select.includes(`${letter}${number}`);
|
||||
return but ? !included : included;
|
||||
} else {
|
||||
const included = select.includes(`${parseFloat(st)}`);
|
||||
return but ? !included : included;
|
||||
if (part.match(/[0-9A-Z]{9}/)) {
|
||||
select.push(part);
|
||||
return;
|
||||
} else if (part.match(/[A-Z]{3}\.[0-9]*/)) {
|
||||
select.push(part);
|
||||
return;
|
||||
}
|
||||
const match = part.match(/[A-Za-z]+/);
|
||||
if (match && match.length > 0) {
|
||||
if (match.index && match.index !== 0) {
|
||||
console.warn(`[WARN] Unable to parse input "${part}"`);
|
||||
return;
|
||||
}
|
||||
const letters = part.substring(0, match[0].length);
|
||||
const number = parseFloat(part.substring(match[0].length));
|
||||
if (isNaN(number)) {
|
||||
console.warn(`[WARN] Unable to parse input "${part}"`);
|
||||
return;
|
||||
}
|
||||
select.push(`${letters}${number}`);
|
||||
} else {
|
||||
select.push(`${parseFloat(part)}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
values: select,
|
||||
isSelected: (st) => {
|
||||
if (typeof st === 'string')
|
||||
st = [st];
|
||||
return st.some(st => {
|
||||
const match = st.match(/[A-Za-z]+/);
|
||||
if (st.match(/[0-9A-Z]{9}/)) {
|
||||
const included = select.includes(st);
|
||||
return but ? !included : included;
|
||||
} else if (match && match.length > 0) {
|
||||
if (match.index && match.index !== 0) {
|
||||
return false;
|
||||
}
|
||||
const letter = st.substring(0, match[0].length);
|
||||
const number = parseFloat(st.substring(match[0].length));
|
||||
if (isNaN(number)) {
|
||||
return false;
|
||||
}
|
||||
const included = select.includes(`${letter}${number}`);
|
||||
return but ? !included : included;
|
||||
} else {
|
||||
const included = select.includes(`${parseFloat(st)}`);
|
||||
return but ? !included : included;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default parseSelect;
|
||||
|
|
@ -49,212 +49,212 @@ export type MPDParsed = {
|
|||
}
|
||||
|
||||
function extractPSSH(
|
||||
manifest: string,
|
||||
schemeIdUri: string,
|
||||
psshTagNames: string[]
|
||||
manifest: string,
|
||||
schemeIdUri: string,
|
||||
psshTagNames: string[]
|
||||
): string | null {
|
||||
const regex = new RegExp(
|
||||
`<ContentProtection[^>]*schemeIdUri=["']${schemeIdUri}["'][^>]*>([\\s\\S]*?)</ContentProtection>`,
|
||||
'i'
|
||||
);
|
||||
const match = regex.exec(manifest);
|
||||
if (match && match[1]) {
|
||||
const innerContent = match[1];
|
||||
for (const tagName of psshTagNames) {
|
||||
const psshRegex = new RegExp(`<${tagName}[^>]*>([\\s\\S]*?)</${tagName}>`, 'i');
|
||||
const psshMatch = psshRegex.exec(innerContent);
|
||||
if (psshMatch && psshMatch[1]) {
|
||||
return psshMatch[1].trim();
|
||||
}
|
||||
const regex = new RegExp(
|
||||
`<ContentProtection[^>]*schemeIdUri=["']${schemeIdUri}["'][^>]*>([\\s\\S]*?)</ContentProtection>`,
|
||||
'i'
|
||||
);
|
||||
const match = regex.exec(manifest);
|
||||
if (match && match[1]) {
|
||||
const innerContent = match[1];
|
||||
for (const tagName of psshTagNames) {
|
||||
const psshRegex = new RegExp(`<${tagName}[^>]*>([\\s\\S]*?)</${tagName}>`, 'i');
|
||||
const psshMatch = psshRegex.exec(innerContent);
|
||||
if (psshMatch && psshMatch[1]) {
|
||||
return psshMatch[1].trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function parse(manifest: string, language?: LanguageItem, url?: string) {
|
||||
if (!manifest.includes('BaseURL') && url) {
|
||||
manifest = manifest.replace(/(<MPD*\b[^>]*>)/gm, `$1<BaseURL>${url}</BaseURL>`);
|
||||
}
|
||||
const parsed = mpdParse(manifest);
|
||||
const ret: MPDParsed = {};
|
||||
if (!manifest.includes('BaseURL') && url) {
|
||||
manifest = manifest.replace(/(<MPD*\b[^>]*>)/gm, `$1<BaseURL>${url}</BaseURL>`);
|
||||
}
|
||||
const parsed = mpdParse(manifest);
|
||||
const ret: MPDParsed = {};
|
||||
|
||||
// Audio Loop
|
||||
for (const item of Object.values(parsed.mediaGroups.AUDIO.audio)){
|
||||
for (const playlist of item.playlists) {
|
||||
const host = new URL(playlist.resolvedUri).hostname;
|
||||
if (!Object.prototype.hasOwnProperty.call(ret, host))
|
||||
ret[host] = { audio: [], video: [] };
|
||||
// Audio Loop
|
||||
for (const item of Object.values(parsed.mediaGroups.AUDIO.audio)){
|
||||
for (const playlist of item.playlists) {
|
||||
const host = new URL(playlist.resolvedUri).hostname;
|
||||
if (!Object.prototype.hasOwnProperty.call(ret, host))
|
||||
ret[host] = { audio: [], video: [] };
|
||||
|
||||
|
||||
if (playlist.sidx && playlist.segments.length == 0) {
|
||||
const options: RequestInit = {
|
||||
method: 'head'
|
||||
};
|
||||
if (playlist.sidx.uri.includes('animecdn')) options.headers = {
|
||||
'origin': 'https://www.animeonegai.com',
|
||||
'referer': 'https://www.animeonegai.com/',
|
||||
};
|
||||
const item = await fetch(playlist.sidx.uri, options);
|
||||
if (!item.ok) console.warn(`${item.status}: ${item.statusText}, Unable to fetch byteLength for audio stream ${Math.round(playlist.attributes.BANDWIDTH/1024)}KiB/s`);
|
||||
const byteLength = parseInt(item.headers.get('content-length') as string);
|
||||
let currentByte = playlist.sidx.map.byterange.length;
|
||||
while (currentByte <= byteLength) {
|
||||
playlist.segments.push({
|
||||
'duration': 0,
|
||||
'map': {
|
||||
'uri': playlist.resolvedUri,
|
||||
'resolvedUri': playlist.resolvedUri,
|
||||
'byterange': playlist.sidx.map.byterange
|
||||
},
|
||||
'uri': playlist.resolvedUri,
|
||||
'resolvedUri': playlist.resolvedUri,
|
||||
'byterange': {
|
||||
'length': 500000,
|
||||
'offset': currentByte
|
||||
},
|
||||
timeline: 0,
|
||||
number: 0,
|
||||
presentationTime: 0
|
||||
});
|
||||
currentByte = currentByte + 500000;
|
||||
}
|
||||
}
|
||||
if (playlist.sidx && playlist.segments.length == 0) {
|
||||
const options: RequestInit = {
|
||||
method: 'head'
|
||||
};
|
||||
if (playlist.sidx.uri.includes('animecdn')) options.headers = {
|
||||
'origin': 'https://www.animeonegai.com',
|
||||
'referer': 'https://www.animeonegai.com/',
|
||||
};
|
||||
const item = await fetch(playlist.sidx.uri, options);
|
||||
if (!item.ok) console.warn(`${item.status}: ${item.statusText}, Unable to fetch byteLength for audio stream ${Math.round(playlist.attributes.BANDWIDTH/1024)}KiB/s`);
|
||||
const byteLength = parseInt(item.headers.get('content-length') as string);
|
||||
let currentByte = playlist.sidx.map.byterange.length;
|
||||
while (currentByte <= byteLength) {
|
||||
playlist.segments.push({
|
||||
'duration': 0,
|
||||
'map': {
|
||||
'uri': playlist.resolvedUri,
|
||||
'resolvedUri': playlist.resolvedUri,
|
||||
'byterange': playlist.sidx.map.byterange
|
||||
},
|
||||
'uri': playlist.resolvedUri,
|
||||
'resolvedUri': playlist.resolvedUri,
|
||||
'byterange': {
|
||||
'length': 500000,
|
||||
'offset': currentByte
|
||||
},
|
||||
timeline: 0,
|
||||
number: 0,
|
||||
presentationTime: 0
|
||||
});
|
||||
currentByte = currentByte + 500000;
|
||||
}
|
||||
}
|
||||
|
||||
//Find and add audio language if it is found in the MPD
|
||||
let audiolang: LanguageItem;
|
||||
const foundlanguage = findLang(languages.find(a => a.code === item.language)?.cr_locale ?? 'unknown');
|
||||
if (item.language) {
|
||||
audiolang = foundlanguage;
|
||||
} else {
|
||||
audiolang = language ? language : foundlanguage;
|
||||
}
|
||||
const pItem: AudioPlayList = {
|
||||
bandwidth: playlist.attributes.BANDWIDTH,
|
||||
language: audiolang,
|
||||
default: item.default,
|
||||
segments: playlist.segments.map((segment): Segment => {
|
||||
const uri = segment.resolvedUri;
|
||||
const map_uri = segment.map.resolvedUri;
|
||||
return {
|
||||
duration: segment.duration,
|
||||
map: { uri: map_uri, byterange: segment.map.byterange },
|
||||
number: segment.number,
|
||||
presentationTime: segment.presentationTime,
|
||||
timeline: segment.timeline,
|
||||
byterange: segment.byterange,
|
||||
uri
|
||||
};
|
||||
})
|
||||
};
|
||||
//Find and add audio language if it is found in the MPD
|
||||
let audiolang: LanguageItem;
|
||||
const foundlanguage = findLang(languages.find(a => a.code === item.language)?.cr_locale ?? 'unknown');
|
||||
if (item.language) {
|
||||
audiolang = foundlanguage;
|
||||
} else {
|
||||
audiolang = language ? language : foundlanguage;
|
||||
}
|
||||
const pItem: AudioPlayList = {
|
||||
bandwidth: playlist.attributes.BANDWIDTH,
|
||||
language: audiolang,
|
||||
default: item.default,
|
||||
segments: playlist.segments.map((segment): Segment => {
|
||||
const uri = segment.resolvedUri;
|
||||
const map_uri = segment.map.resolvedUri;
|
||||
return {
|
||||
duration: segment.duration,
|
||||
map: { uri: map_uri, byterange: segment.map.byterange },
|
||||
number: segment.number,
|
||||
presentationTime: segment.presentationTime,
|
||||
timeline: segment.timeline,
|
||||
byterange: segment.byterange,
|
||||
uri
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
const playreadyPssh = extractPSSH(
|
||||
manifest,
|
||||
'urn:uuid:9A04F079-9840-4286-AB92-E65BE0885F95',
|
||||
['cenc:pssh', 'mspr:pro']
|
||||
);
|
||||
const playreadyPssh = extractPSSH(
|
||||
manifest,
|
||||
'urn:uuid:9A04F079-9840-4286-AB92-E65BE0885F95',
|
||||
['cenc:pssh', 'mspr:pro']
|
||||
);
|
||||
|
||||
const widevinePssh = extractPSSH(
|
||||
manifest,
|
||||
'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed',
|
||||
['cenc:pssh']
|
||||
);
|
||||
const widevinePssh = extractPSSH(
|
||||
manifest,
|
||||
'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed',
|
||||
['cenc:pssh']
|
||||
);
|
||||
|
||||
if (widevinePssh) {
|
||||
pItem.pssh_wvd = widevinePssh;
|
||||
}
|
||||
if (widevinePssh) {
|
||||
pItem.pssh_wvd = widevinePssh;
|
||||
}
|
||||
|
||||
if (playreadyPssh) {
|
||||
pItem.pssh_prd = playreadyPssh;
|
||||
}
|
||||
if (playreadyPssh) {
|
||||
pItem.pssh_prd = playreadyPssh;
|
||||
}
|
||||
|
||||
ret[host].audio.push(pItem);
|
||||
}
|
||||
}
|
||||
|
||||
// Video Loop
|
||||
for (const playlist of parsed.playlists) {
|
||||
const host = new URL(playlist.resolvedUri).hostname;
|
||||
if (!Object.prototype.hasOwnProperty.call(ret, host))
|
||||
ret[host] = { audio: [], video: [] };
|
||||
|
||||
if (playlist.sidx && playlist.segments.length == 0) {
|
||||
const options: RequestInit = {
|
||||
method: 'head'
|
||||
};
|
||||
if (playlist.sidx.uri.includes('animecdn')) options.headers = {
|
||||
'origin': 'https://www.animeonegai.com',
|
||||
'referer': 'https://www.animeonegai.com/',
|
||||
};
|
||||
const item = await fetch(playlist.sidx.uri, options);
|
||||
if (!item.ok) console.warn(`${item.status}: ${item.statusText}, Unable to fetch byteLength for video stream ${playlist.attributes.RESOLUTION?.height}x${playlist.attributes.RESOLUTION?.width}@${Math.round(playlist.attributes.BANDWIDTH/1024)}KiB/s`);
|
||||
const byteLength = parseInt(item.headers.get('content-length') as string);
|
||||
let currentByte = playlist.sidx.map.byterange.length;
|
||||
while (currentByte <= byteLength) {
|
||||
playlist.segments.push({
|
||||
'duration': 0,
|
||||
'map': {
|
||||
'uri': playlist.resolvedUri,
|
||||
'resolvedUri': playlist.resolvedUri,
|
||||
'byterange': playlist.sidx.map.byterange
|
||||
},
|
||||
'uri': playlist.resolvedUri,
|
||||
'resolvedUri': playlist.resolvedUri,
|
||||
'byterange': {
|
||||
'length': 2000000,
|
||||
'offset': currentByte
|
||||
},
|
||||
timeline: 0,
|
||||
number: 0,
|
||||
presentationTime: 0
|
||||
});
|
||||
currentByte = currentByte + 2000000;
|
||||
}
|
||||
ret[host].audio.push(pItem);
|
||||
}
|
||||
}
|
||||
|
||||
const pItem: VideoPlayList = {
|
||||
bandwidth: playlist.attributes.BANDWIDTH,
|
||||
quality: playlist.attributes.RESOLUTION!,
|
||||
segments: playlist.segments.map((segment): Segment => {
|
||||
const uri = segment.resolvedUri;
|
||||
const map_uri = segment.map.resolvedUri;
|
||||
return {
|
||||
duration: segment.duration,
|
||||
map: { uri: map_uri, byterange: segment.map.byterange },
|
||||
number: segment.number,
|
||||
presentationTime: segment.presentationTime,
|
||||
timeline: segment.timeline,
|
||||
byterange: segment.byterange,
|
||||
uri
|
||||
// Video Loop
|
||||
for (const playlist of parsed.playlists) {
|
||||
const host = new URL(playlist.resolvedUri).hostname;
|
||||
if (!Object.prototype.hasOwnProperty.call(ret, host))
|
||||
ret[host] = { audio: [], video: [] };
|
||||
|
||||
if (playlist.sidx && playlist.segments.length == 0) {
|
||||
const options: RequestInit = {
|
||||
method: 'head'
|
||||
};
|
||||
if (playlist.sidx.uri.includes('animecdn')) options.headers = {
|
||||
'origin': 'https://www.animeonegai.com',
|
||||
'referer': 'https://www.animeonegai.com/',
|
||||
};
|
||||
const item = await fetch(playlist.sidx.uri, options);
|
||||
if (!item.ok) console.warn(`${item.status}: ${item.statusText}, Unable to fetch byteLength for video stream ${playlist.attributes.RESOLUTION?.height}x${playlist.attributes.RESOLUTION?.width}@${Math.round(playlist.attributes.BANDWIDTH/1024)}KiB/s`);
|
||||
const byteLength = parseInt(item.headers.get('content-length') as string);
|
||||
let currentByte = playlist.sidx.map.byterange.length;
|
||||
while (currentByte <= byteLength) {
|
||||
playlist.segments.push({
|
||||
'duration': 0,
|
||||
'map': {
|
||||
'uri': playlist.resolvedUri,
|
||||
'resolvedUri': playlist.resolvedUri,
|
||||
'byterange': playlist.sidx.map.byterange
|
||||
},
|
||||
'uri': playlist.resolvedUri,
|
||||
'resolvedUri': playlist.resolvedUri,
|
||||
'byterange': {
|
||||
'length': 2000000,
|
||||
'offset': currentByte
|
||||
},
|
||||
timeline: 0,
|
||||
number: 0,
|
||||
presentationTime: 0
|
||||
});
|
||||
currentByte = currentByte + 2000000;
|
||||
}
|
||||
}
|
||||
|
||||
const pItem: VideoPlayList = {
|
||||
bandwidth: playlist.attributes.BANDWIDTH,
|
||||
quality: playlist.attributes.RESOLUTION!,
|
||||
segments: playlist.segments.map((segment): Segment => {
|
||||
const uri = segment.resolvedUri;
|
||||
const map_uri = segment.map.resolvedUri;
|
||||
return {
|
||||
duration: segment.duration,
|
||||
map: { uri: map_uri, byterange: segment.map.byterange },
|
||||
number: segment.number,
|
||||
presentationTime: segment.presentationTime,
|
||||
timeline: segment.timeline,
|
||||
byterange: segment.byterange,
|
||||
uri
|
||||
};
|
||||
})
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
const playreadyPssh = extractPSSH(
|
||||
manifest,
|
||||
'urn:uuid:9A04F079-9840-4286-AB92-E65BE0885F95',
|
||||
['cenc:pssh', 'mspr:pro']
|
||||
);
|
||||
const playreadyPssh = extractPSSH(
|
||||
manifest,
|
||||
'urn:uuid:9A04F079-9840-4286-AB92-E65BE0885F95',
|
||||
['cenc:pssh', 'mspr:pro']
|
||||
);
|
||||
|
||||
const widevinePssh = extractPSSH(
|
||||
manifest,
|
||||
'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed',
|
||||
['cenc:pssh']
|
||||
);
|
||||
const widevinePssh = extractPSSH(
|
||||
manifest,
|
||||
'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed',
|
||||
['cenc:pssh']
|
||||
);
|
||||
|
||||
if (widevinePssh) {
|
||||
pItem.pssh_wvd = widevinePssh;
|
||||
if (widevinePssh) {
|
||||
pItem.pssh_wvd = widevinePssh;
|
||||
}
|
||||
|
||||
if (playreadyPssh) {
|
||||
pItem.pssh_prd = playreadyPssh;
|
||||
}
|
||||
|
||||
ret[host].video.push(pItem);
|
||||
}
|
||||
|
||||
if (playreadyPssh) {
|
||||
pItem.pssh_prd = playreadyPssh;
|
||||
}
|
||||
|
||||
ret[host].video.push(pItem);
|
||||
}
|
||||
|
||||
return ret;
|
||||
return ret;
|
||||
}
|
||||
|
||||
function arrayBufferToBase64(buffer: Uint8Array): string {
|
||||
return Buffer.from(buffer).toString('base64');
|
||||
return Buffer.from(buffer).toString('base64');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,17 +12,17 @@ import Helper from './module.helper';
|
|||
const updateFilePlace = path.join(workingDir, 'config', 'updates.json');
|
||||
|
||||
const updateIgnore = [
|
||||
'*.d.ts',
|
||||
'.git',
|
||||
'lib',
|
||||
'node_modules',
|
||||
'@types',
|
||||
path.join('bin', 'mkvtoolnix'),
|
||||
path.join('config', 'token.yml'),
|
||||
'.eslint',
|
||||
'tsconfig.json',
|
||||
'updates.json',
|
||||
'tsc.ts'
|
||||
'*.d.ts',
|
||||
'.git',
|
||||
'lib',
|
||||
'node_modules',
|
||||
'@types',
|
||||
path.join('bin', 'mkvtoolnix'),
|
||||
path.join('config', 'token.yml'),
|
||||
'.eslint',
|
||||
'tsconfig.json',
|
||||
'updates.json',
|
||||
'tsc.ts'
|
||||
];
|
||||
|
||||
const askBeforeUpdate = ['*.yml'];
|
||||
|
|
@ -40,158 +40,158 @@ export type ApplyItem = {
|
|||
};
|
||||
|
||||
export default async (force = false) => {
|
||||
const isPackaged = (
|
||||
const isPackaged = (
|
||||
process as NodeJS.Process & {
|
||||
pkg?: unknown;
|
||||
}
|
||||
).pkg
|
||||
? true
|
||||
: !!process.env.contentDirectory;
|
||||
if (isPackaged) {
|
||||
return;
|
||||
}
|
||||
let updateFile: UpdateFile | undefined;
|
||||
if (fs.existsSync(updateFilePlace)) {
|
||||
updateFile = JSON.parse(fs.readFileSync(updateFilePlace).toString()) as UpdateFile;
|
||||
if (new Date() < new Date(updateFile.nextCheck) && !force) {
|
||||
return;
|
||||
).pkg
|
||||
? true
|
||||
: !!process.env.contentDirectory;
|
||||
if (isPackaged) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
console.info('Checking for updates...');
|
||||
const tagRequest = await fetch('https://api.github.com/repos/anidl/multi-downloader-nx/tags');
|
||||
const tags = JSON.parse(await tagRequest.text()) as GithubTag[];
|
||||
|
||||
if (tags.length > 0) {
|
||||
const newer = tags.filter((a) => {
|
||||
return isNewer(packageJson.version, a.name);
|
||||
});
|
||||
console.info(`Found ${tags.length} release tags and ${newer.length} that are new.`);
|
||||
|
||||
if (newer.length < 1) {
|
||||
console.info('No new tags found');
|
||||
return done();
|
||||
let updateFile: UpdateFile | undefined;
|
||||
if (fs.existsSync(updateFilePlace)) {
|
||||
updateFile = JSON.parse(fs.readFileSync(updateFilePlace).toString()) as UpdateFile;
|
||||
if (new Date() < new Date(updateFile.nextCheck) && !force) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const newest = newer.sort((a, b) => (a.name < b.name ? 1 : a.name > b.name ? -1 : 0))[0];
|
||||
const compareRequest = await fetch(`https://api.github.com/repos/anidl/multi-downloader-nx/compare/${packageJson.version}...${newest.name}`);
|
||||
console.info('Checking for updates...');
|
||||
const tagRequest = await fetch('https://api.github.com/repos/anidl/multi-downloader-nx/tags');
|
||||
const tags = JSON.parse(await tagRequest.text()) as GithubTag[];
|
||||
|
||||
const compareJSON = JSON.parse(await compareRequest.text()) as TagCompare;
|
||||
if (tags.length > 0) {
|
||||
const newer = tags.filter((a) => {
|
||||
return isNewer(packageJson.version, a.name);
|
||||
});
|
||||
console.info(`Found ${tags.length} release tags and ${newer.length} that are new.`);
|
||||
|
||||
console.info(`You are behind by ${compareJSON.ahead_by} releases!`);
|
||||
const changedFiles = compareJSON.files
|
||||
.map((a) => ({
|
||||
...a,
|
||||
filename: path.join(...a.filename.split('/'))
|
||||
}))
|
||||
.filter((a) => {
|
||||
return !updateIgnore.some((_filter) => matchString(_filter, a.filename));
|
||||
});
|
||||
if (changedFiles.length < 1) {
|
||||
console.info('No file changes found... updating package.json. If you think this is an error please get the newst version yourself.');
|
||||
return done(newest.name);
|
||||
if (newer.length < 1) {
|
||||
console.info('No new tags found');
|
||||
return done();
|
||||
}
|
||||
const newest = newer.sort((a, b) => (a.name < b.name ? 1 : a.name > b.name ? -1 : 0))[0];
|
||||
const compareRequest = await fetch(`https://api.github.com/repos/anidl/multi-downloader-nx/compare/${packageJson.version}...${newest.name}`);
|
||||
|
||||
const compareJSON = JSON.parse(await compareRequest.text()) as TagCompare;
|
||||
|
||||
console.info(`You are behind by ${compareJSON.ahead_by} releases!`);
|
||||
const changedFiles = compareJSON.files
|
||||
.map((a) => ({
|
||||
...a,
|
||||
filename: path.join(...a.filename.split('/'))
|
||||
}))
|
||||
.filter((a) => {
|
||||
return !updateIgnore.some((_filter) => matchString(_filter, a.filename));
|
||||
});
|
||||
if (changedFiles.length < 1) {
|
||||
console.info('No file changes found... updating package.json. If you think this is an error please get the newst version yourself.');
|
||||
return done(newest.name);
|
||||
}
|
||||
console.info(`Found file changes: \n${changedFiles.map((a) => ` [${a.status === 'modified' ? '*' : a.status === 'added' ? '+' : '-'}] ${a.filename}`).join('\n')}`);
|
||||
|
||||
const remove: string[] = [];
|
||||
|
||||
for (const a of changedFiles.filter((a) => a.status !== 'added')) {
|
||||
if (!askBeforeUpdate.some((pattern) => matchString(pattern, a.filename))) continue;
|
||||
const answer = await Helper.question(
|
||||
`The developer decided that the file '${a.filename}' may contain information you changed yourself. Should they be overriden to be updated? [y/N]`
|
||||
);
|
||||
if (answer.toLowerCase() === 'y') remove.push(a.sha);
|
||||
}
|
||||
|
||||
const changesToApply = await Promise.all(
|
||||
changedFiles
|
||||
.filter((a) => !remove.includes(a.sha))
|
||||
.map(async (a): Promise<ApplyItem> => {
|
||||
if (a.filename.endsWith('.ts') || a.filename.endsWith('tsx')) {
|
||||
const isTSX = a.filename.endsWith('tsx');
|
||||
const ret = {
|
||||
path: a.filename.slice(0, isTSX ? -3 : -2) + `js${isTSX ? 'x' : ''}`,
|
||||
content: transpileModule(await (await fetch(a.raw_url)).text(), {
|
||||
compilerOptions: tsConfig.compilerOptions as unknown as CompilerOptions
|
||||
}).outputText,
|
||||
type: a.status === 'modified' ? ApplyType.UPDATE : a.status === 'added' ? ApplyType.ADD : ApplyType.DELETE
|
||||
};
|
||||
console.info('✓ Transpiled %s', ret.path);
|
||||
return ret;
|
||||
} else {
|
||||
const ret = {
|
||||
path: a.filename,
|
||||
content: await (await fetch(a.raw_url)).text(),
|
||||
type: a.status === 'modified' ? ApplyType.UPDATE : a.status === 'added' ? ApplyType.ADD : ApplyType.DELETE
|
||||
};
|
||||
console.info('✓ Got %s', ret.path);
|
||||
return ret;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
changesToApply.forEach((a) => {
|
||||
try {
|
||||
fsextra.ensureDirSync(path.dirname(a.path));
|
||||
fs.writeFileSync(path.join(__dirname, '..', a.path), a.content);
|
||||
console.info('✓ Written %s', a.path);
|
||||
} catch (er) {
|
||||
console.info('✗ Error while writing %s', a.path);
|
||||
}
|
||||
});
|
||||
|
||||
console.info('Done');
|
||||
return done();
|
||||
}
|
||||
console.info(`Found file changes: \n${changedFiles.map((a) => ` [${a.status === 'modified' ? '*' : a.status === 'added' ? '+' : '-'}] ${a.filename}`).join('\n')}`);
|
||||
|
||||
const remove: string[] = [];
|
||||
|
||||
for (const a of changedFiles.filter((a) => a.status !== 'added')) {
|
||||
if (!askBeforeUpdate.some((pattern) => matchString(pattern, a.filename))) continue;
|
||||
const answer = await Helper.question(
|
||||
`The developer decided that the file '${a.filename}' may contain information you changed yourself. Should they be overriden to be updated? [y/N]`
|
||||
);
|
||||
if (answer.toLowerCase() === 'y') remove.push(a.sha);
|
||||
}
|
||||
|
||||
const changesToApply = await Promise.all(
|
||||
changedFiles
|
||||
.filter((a) => !remove.includes(a.sha))
|
||||
.map(async (a): Promise<ApplyItem> => {
|
||||
if (a.filename.endsWith('.ts') || a.filename.endsWith('tsx')) {
|
||||
const isTSX = a.filename.endsWith('tsx');
|
||||
const ret = {
|
||||
path: a.filename.slice(0, isTSX ? -3 : -2) + `js${isTSX ? 'x' : ''}`,
|
||||
content: transpileModule(await (await fetch(a.raw_url)).text(), {
|
||||
compilerOptions: tsConfig.compilerOptions as unknown as CompilerOptions
|
||||
}).outputText,
|
||||
type: a.status === 'modified' ? ApplyType.UPDATE : a.status === 'added' ? ApplyType.ADD : ApplyType.DELETE
|
||||
};
|
||||
console.info('✓ Transpiled %s', ret.path);
|
||||
return ret;
|
||||
} else {
|
||||
const ret = {
|
||||
path: a.filename,
|
||||
content: await (await fetch(a.raw_url)).text(),
|
||||
type: a.status === 'modified' ? ApplyType.UPDATE : a.status === 'added' ? ApplyType.ADD : ApplyType.DELETE
|
||||
};
|
||||
console.info('✓ Got %s', ret.path);
|
||||
return ret;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
changesToApply.forEach((a) => {
|
||||
try {
|
||||
fsextra.ensureDirSync(path.dirname(a.path));
|
||||
fs.writeFileSync(path.join(__dirname, '..', a.path), a.content);
|
||||
console.info('✓ Written %s', a.path);
|
||||
} catch (er) {
|
||||
console.info('✗ Error while writing %s', a.path);
|
||||
}
|
||||
});
|
||||
|
||||
console.info('Done');
|
||||
return done();
|
||||
}
|
||||
};
|
||||
|
||||
function done(newVersion?: string) {
|
||||
const next = new Date(Date.now() + 1000 * 60 * 60 * 24);
|
||||
fs.writeFileSync(
|
||||
updateFilePlace,
|
||||
JSON.stringify(
|
||||
const next = new Date(Date.now() + 1000 * 60 * 60 * 24);
|
||||
fs.writeFileSync(
|
||||
updateFilePlace,
|
||||
JSON.stringify(
|
||||
{
|
||||
lastCheck: Date.now(),
|
||||
nextCheck: next.getTime()
|
||||
lastCheck: Date.now(),
|
||||
nextCheck: next.getTime()
|
||||
} as UpdateFile,
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
if (newVersion) {
|
||||
fs.writeFileSync(
|
||||
'../package.json',
|
||||
JSON.stringify(
|
||||
{
|
||||
...packageJson,
|
||||
version: newVersion
|
||||
},
|
||||
null,
|
||||
4
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
console.info('[INFO] Searching for update finished. Next time running on the ' + next.toLocaleDateString() + ' at ' + next.toLocaleTimeString() + '.');
|
||||
if (newVersion) {
|
||||
fs.writeFileSync(
|
||||
'../package.json',
|
||||
JSON.stringify(
|
||||
{
|
||||
...packageJson,
|
||||
version: newVersion
|
||||
},
|
||||
null,
|
||||
4
|
||||
)
|
||||
);
|
||||
}
|
||||
console.info('[INFO] Searching for update finished. Next time running on the ' + next.toLocaleDateString() + ' at ' + next.toLocaleTimeString() + '.');
|
||||
}
|
||||
|
||||
function isNewer(curr: string, compare: string): boolean {
|
||||
const currParts = curr.split('.').map((a) => parseInt(a));
|
||||
const compareParts = compare.split('.').map((a) => parseInt(a));
|
||||
const currParts = curr.split('.').map((a) => parseInt(a));
|
||||
const compareParts = compare.split('.').map((a) => parseInt(a));
|
||||
|
||||
for (let i = 0; i < Math.max(currParts.length, compareParts.length); i++) {
|
||||
if (currParts.length <= i) return true;
|
||||
if (compareParts.length <= i) return false;
|
||||
if (currParts[i] !== compareParts[i]) return compareParts[i] > currParts[i];
|
||||
}
|
||||
for (let i = 0; i < Math.max(currParts.length, compareParts.length); i++) {
|
||||
if (currParts.length <= i) return true;
|
||||
if (compareParts.length <= i) return false;
|
||||
if (currParts[i] !== compareParts[i]) return compareParts[i] > currParts[i];
|
||||
}
|
||||
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
|
||||
function matchString(pattern: string, toMatch: string): boolean {
|
||||
const filter = path.join('..', pattern);
|
||||
if (pattern.startsWith('*')) {
|
||||
return toMatch.endsWith(pattern.slice(1));
|
||||
} else if (filter.split(path.sep).pop()?.indexOf('.') === -1) {
|
||||
return toMatch.startsWith(filter);
|
||||
} else {
|
||||
return toMatch.split(path.sep).pop() === pattern;
|
||||
}
|
||||
const filter = path.join('..', pattern);
|
||||
if (pattern.startsWith('*')) {
|
||||
return toMatch.endsWith(pattern.slice(1));
|
||||
} else if (filter.split(path.sep).pop()?.indexOf('.') === -1) {
|
||||
return toMatch.startsWith(filter);
|
||||
} else {
|
||||
return toMatch.split(path.sep).pop() === pattern;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,498 +31,498 @@ type Vtt = {
|
|||
};
|
||||
|
||||
function loadCSS(cssStr: string): Css {
|
||||
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: Record<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: Record<string, {
|
||||
params: string,
|
||||
list: string[]
|
||||
}> = { [defaultStyleName]: { params: defaultStyle, list: [] } };
|
||||
const classList: Record<string, number> = { [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(`VTT2ASS: Invalid css in line ${i}: ${l}`);
|
||||
continue;
|
||||
}
|
||||
const classList: Record<string, number> = { [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(`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 };
|
||||
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;
|
||||
return styles;
|
||||
}
|
||||
|
||||
function parseStyle(stylegroup: string, line: string, style: any) {
|
||||
const defaultSFont = rFont == '' ? defaultStyleFont : rFont; //redeclare cause of let
|
||||
const defaultSFont = rFont == '' ? defaultStyleFont : rFont; //redeclare cause of let
|
||||
|
||||
if (stylegroup.startsWith('Subtitle') || stylegroup.startsWith('Song') || stylegroup.startsWith('Q') || stylegroup.startsWith('Default')) { //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(':');
|
||||
if (st[0]) st[0] = st[0].trim();
|
||||
if (st[1]) st[1] = st[1].trim();
|
||||
let cl, arr, transformed_str;
|
||||
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 'text-decoration':
|
||||
if (st[1] === 'underline') {
|
||||
style[8] = -1;
|
||||
} else {
|
||||
console.warn(`vtt2ass: Unknown text-decoration value: ${st[1]}`);
|
||||
}
|
||||
break;
|
||||
case 'right':
|
||||
style[17] = 3;
|
||||
break;
|
||||
case 'left':
|
||||
style[17] = 1;
|
||||
break;
|
||||
case 'font-style':
|
||||
if (st[1] === 'italic') {
|
||||
style[7] = -1;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'background-color':
|
||||
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;
|
||||
}
|
||||
arr = transformed_str = st[1].split(',').map(r => r.trim());
|
||||
arr = arr.map(r => { return (r.split(' ').length > 3 ? r.replace(/(\d+)px black$/, '') : r.replace(/black$/, '')).trim(); });
|
||||
transformed_str[1] = arr.map(r => r.replace(/-/g, '').replace(/px/g, '').replace(/(^| )0( |$)/g, ' ').trim()).join(' ');
|
||||
arr = transformed_str[1].split(' ');
|
||||
if (arr.length != 10) {
|
||||
console.warn(`VTT2ASS: Can't properly parse text-shadow: ${s.trim()}`);
|
||||
break;
|
||||
}
|
||||
arr = [...new Set(arr)];
|
||||
if (arr.length > 1) {
|
||||
console.warn(`VTT2ASS: Can't properly parse text-shadow: ${s.trim()}`);
|
||||
break;
|
||||
}
|
||||
style[16] = arr[0];
|
||||
break;
|
||||
default:
|
||||
console.error(`VTT2ASS: Unknown style: ${s.trim()}`);
|
||||
if (stylegroup.startsWith('Subtitle') || stylegroup.startsWith('Song') || stylegroup.startsWith('Q') || stylegroup.startsWith('Default')) { //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`;
|
||||
}
|
||||
}
|
||||
return style.join(',');
|
||||
|
||||
// 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(':');
|
||||
if (st[0]) st[0] = st[0].trim();
|
||||
if (st[1]) st[1] = st[1].trim();
|
||||
let cl, arr, transformed_str;
|
||||
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 'text-decoration':
|
||||
if (st[1] === 'underline') {
|
||||
style[8] = -1;
|
||||
} else {
|
||||
console.warn(`vtt2ass: Unknown text-decoration value: ${st[1]}`);
|
||||
}
|
||||
break;
|
||||
case 'right':
|
||||
style[17] = 3;
|
||||
break;
|
||||
case 'left':
|
||||
style[17] = 1;
|
||||
break;
|
||||
case 'font-style':
|
||||
if (st[1] === 'italic') {
|
||||
style[7] = -1;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'background-color':
|
||||
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;
|
||||
}
|
||||
arr = transformed_str = st[1].split(',').map(r => r.trim());
|
||||
arr = arr.map(r => { return (r.split(' ').length > 3 ? r.replace(/(\d+)px black$/, '') : r.replace(/black$/, '')).trim(); });
|
||||
transformed_str[1] = arr.map(r => r.replace(/-/g, '').replace(/px/g, '').replace(/(^| )0( |$)/g, ' ').trim()).join(' ');
|
||||
arr = transformed_str[1].split(' ');
|
||||
if (arr.length != 10) {
|
||||
console.warn(`VTT2ASS: Can't properly parse text-shadow: ${s.trim()}`);
|
||||
break;
|
||||
}
|
||||
arr = [...new Set(arr)];
|
||||
if (arr.length > 1) {
|
||||
console.warn(`VTT2ASS: Can't properly parse text-shadow: ${s.trim()}`);
|
||||
break;
|
||||
}
|
||||
style[16] = arr[0];
|
||||
break;
|
||||
default:
|
||||
console.error(`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(`VTT2ASS: Unknown size: ${size_line}`);
|
||||
return;
|
||||
}
|
||||
let size = parseFloat(m[1]);
|
||||
if (m[2] === 'em') size *= font_size;
|
||||
return Math.round(size);
|
||||
const m = size_line.trim().match(/([\d.]+)(.*)/);
|
||||
if (!m) {
|
||||
console.error(`VTT2ASS: Unknown size: ${size_line}`);
|
||||
return;
|
||||
}
|
||||
let size = parseFloat(m[1]);
|
||||
if (m[2] === 'em') size *= font_size;
|
||||
return Math.round(size);
|
||||
}
|
||||
|
||||
function getColor(c: string) {
|
||||
if (c[0] !== '#') {
|
||||
c = colors[c as keyof typeof colors];
|
||||
} else if (c.length < 7 || 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();
|
||||
if (c[0] !== '#') {
|
||||
c = colors[c as keyof typeof colors];
|
||||
} else if (c.length < 7 || 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): Vtt[] {
|
||||
const rx = /^([\d:.]*) --> ([\d:.]*)\s?(.*?)\s*$/;
|
||||
const lines = vttStr.replace(/\r?\n/g, '\n').split('\n');
|
||||
const data = [];
|
||||
let record: null|Vtt = 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) {
|
||||
const rx = /^([\d:.]*) --> ([\d:.]*)\s?(.*?)\s*$/;
|
||||
const lines = vttStr.replace(/\r?\n/g, '\n').split('\n');
|
||||
const data = [];
|
||||
let record: null|Vtt = 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 as any)[c[0]] = c[1] ?? 'invalid-input') && p, {}),
|
||||
}
|
||||
};
|
||||
lineBuf = [];
|
||||
continue;
|
||||
}
|
||||
lineBuf.push(l);
|
||||
}
|
||||
if (record !== null) {
|
||||
if (lineBuf[lineBuf.length - 1] === '') {
|
||||
lineBuf.pop();
|
||||
}
|
||||
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 as any)[c[0]] = c[1] ?? 'invalid-input') && 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;
|
||||
return data;
|
||||
}
|
||||
|
||||
function timestampToCentiseconds(timestamp: string) {
|
||||
const timestamp_split = timestamp.split(':');
|
||||
const timestamp_sec_split = timestamp_split[2].split('.');
|
||||
const hour = parseInt(timestamp_split[0]);
|
||||
const minute = parseInt(timestamp_split[1]);
|
||||
const second = parseInt(timestamp_sec_split[0]);
|
||||
const centisecond = parseInt(timestamp_sec_split[1]);
|
||||
const timestamp_split = timestamp.split(':');
|
||||
const timestamp_sec_split = timestamp_split[2].split('.');
|
||||
const hour = parseInt(timestamp_split[0]);
|
||||
const minute = parseInt(timestamp_split[1]);
|
||||
const second = parseInt(timestamp_sec_split[0]);
|
||||
const centisecond = parseInt(timestamp_sec_split[1]);
|
||||
|
||||
return 360000 * hour + 6000 * minute + 100 * second + centisecond;
|
||||
return 360000 * hour + 6000 * minute + 100 * second + centisecond;
|
||||
}
|
||||
|
||||
function combineLines(events: string[]): string[] {
|
||||
if (!doCombineLines) {
|
||||
return events;
|
||||
}
|
||||
// This function is for combining adjacent lines with same information
|
||||
const newLines: string[] = [];
|
||||
for (const currentLine of events) {
|
||||
let hasCombined: boolean = false;
|
||||
// Check previous 7 elements, arbritary lookback amount
|
||||
for (let j = 1; j < 8 && j < newLines.length; j++) {
|
||||
const checkLine = newLines[newLines.length - j];
|
||||
const checkLineSplit = checkLine.split(',');
|
||||
const currentLineSplit = currentLine.split(',');
|
||||
// 1 = start, 2 = end, 3 = style, 9+ = text
|
||||
if (checkLineSplit.slice(9).join(',') == currentLineSplit.slice(9).join(',') &&
|
||||
if (!doCombineLines) {
|
||||
return events;
|
||||
}
|
||||
// This function is for combining adjacent lines with same information
|
||||
const newLines: string[] = [];
|
||||
for (const currentLine of events) {
|
||||
let hasCombined: boolean = false;
|
||||
// Check previous 7 elements, arbritary lookback amount
|
||||
for (let j = 1; j < 8 && j < newLines.length; j++) {
|
||||
const checkLine = newLines[newLines.length - j];
|
||||
const checkLineSplit = checkLine.split(',');
|
||||
const currentLineSplit = currentLine.split(',');
|
||||
// 1 = start, 2 = end, 3 = style, 9+ = text
|
||||
if (checkLineSplit.slice(9).join(',') == currentLineSplit.slice(9).join(',') &&
|
||||
checkLineSplit[3] == currentLineSplit[3] &&
|
||||
checkLineSplit[2] == currentLineSplit[1]
|
||||
) {
|
||||
checkLineSplit[2] = currentLineSplit[2];
|
||||
newLines[newLines.length - j] = checkLineSplit.join(',');
|
||||
hasCombined = true;
|
||||
break;
|
||||
}
|
||||
) {
|
||||
checkLineSplit[2] = currentLineSplit[2];
|
||||
newLines[newLines.length - j] = checkLineSplit.join(',');
|
||||
hasCombined = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasCombined) {
|
||||
newLines.push(currentLine);
|
||||
}
|
||||
}
|
||||
if (!hasCombined) {
|
||||
newLines.push(currentLine);
|
||||
}
|
||||
}
|
||||
return newLines;
|
||||
return newLines;
|
||||
}
|
||||
|
||||
function pushBuffer(buffer: ReturnType<typeof convertLine>[], events: string[]) {
|
||||
buffer.reverse();
|
||||
const bufferStrings: string[] = buffer.map(line =>
|
||||
`Dialogue: 1,${line.start},${line.end},${line.style},,0,0,0,,${line.text}`);
|
||||
events.push(...bufferStrings);
|
||||
buffer.splice(0,buffer.length);
|
||||
buffer.reverse();
|
||||
const bufferStrings: string[] = buffer.map(line =>
|
||||
`Dialogue: 1,${line.start},${line.end},${line.style},,0,0,0,,${line.text}`);
|
||||
events.push(...bufferStrings);
|
||||
buffer.splice(0,buffer.length);
|
||||
}
|
||||
|
||||
function convert(css: Css, vtt: Vtt[]) {
|
||||
const stylesMap: Record<string, string> = {};
|
||||
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: {
|
||||
const stylesMap: Record<string, string> = {};
|
||||
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: string[],
|
||||
caption: string[],
|
||||
capt_pos: string[],
|
||||
song_cap: string[],
|
||||
} = {
|
||||
subtitle: [],
|
||||
caption: [],
|
||||
capt_pos: [],
|
||||
song_cap: [],
|
||||
subtitle: [],
|
||||
caption: [],
|
||||
capt_pos: [],
|
||||
song_cap: [],
|
||||
};
|
||||
const linesMap: Record<string, number> = {};
|
||||
const buffer: ReturnType<typeof convertLine>[] = [];
|
||||
const captionsBuffer: string[] = [];
|
||||
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[parseInt(l) - x.subInd + 1]);
|
||||
if (x.style != fx.style) {
|
||||
x.text = `{\\r${x.style}}${x.text}{\\r}`;
|
||||
const linesMap: Record<string, number> = {};
|
||||
const buffer: ReturnType<typeof convertLine>[] = [];
|
||||
const captionsBuffer: string[] = [];
|
||||
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[parseInt(l) - x.subInd + 1]);
|
||||
if (x.style != fx.style) {
|
||||
x.text = `{\\r${x.style}}${x.text}{\\r}`;
|
||||
}
|
||||
}
|
||||
events[x.type as keyof typeof events][linesMap[x.ind]] += '\\N' + x.text;
|
||||
}
|
||||
}
|
||||
events[x.type as keyof typeof events][linesMap[x.ind]] += '\\N' + x.text;
|
||||
}
|
||||
else {
|
||||
events[x.type as keyof typeof events].push(x.res);
|
||||
if (x.ind !== '') {
|
||||
linesMap[x.ind] = events[x.type as keyof typeof events].length - 1;
|
||||
}
|
||||
}
|
||||
/**
|
||||
else {
|
||||
events[x.type as keyof typeof events].push(x.res);
|
||||
if (x.ind !== '') {
|
||||
linesMap[x.ind] = events[x.type as keyof typeof events].length - 1;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* What cursed code have I brought upon this land?
|
||||
* This handles making lines multi-line when neccesary and reverses
|
||||
* order of subtitles so that they display correctly
|
||||
*/
|
||||
if (x.type != 'subtitle') {
|
||||
// Do nothing
|
||||
} else if (x.text.includes('\\pos')) {
|
||||
events['subtitle'].pop();
|
||||
captionsBuffer.push(x.res);
|
||||
} else if (buffer.length > 0) {
|
||||
const previousBufferLine = buffer[buffer.length - 1];
|
||||
const previousStart = timestampToCentiseconds(previousBufferLine.start);
|
||||
const currentStart = timestampToCentiseconds(x.start);
|
||||
events['subtitle'].pop();
|
||||
if ((currentStart - previousStart) <= 2) {
|
||||
x.start = previousBufferLine.start;
|
||||
if (previousBufferLine.style == x.style) {
|
||||
buffer.pop();
|
||||
x.text = previousBufferLine.text + '\\N' + x.text;
|
||||
if (x.type != 'subtitle') {
|
||||
// Do nothing
|
||||
} else if (x.text.includes('\\pos')) {
|
||||
events['subtitle'].pop();
|
||||
captionsBuffer.push(x.res);
|
||||
} else if (buffer.length > 0) {
|
||||
const previousBufferLine = buffer[buffer.length - 1];
|
||||
const previousStart = timestampToCentiseconds(previousBufferLine.start);
|
||||
const currentStart = timestampToCentiseconds(x.start);
|
||||
events['subtitle'].pop();
|
||||
if ((currentStart - previousStart) <= 2) {
|
||||
x.start = previousBufferLine.start;
|
||||
if (previousBufferLine.style == x.style) {
|
||||
buffer.pop();
|
||||
x.text = previousBufferLine.text + '\\N' + x.text;
|
||||
}
|
||||
} else {
|
||||
pushBuffer(buffer, events['subtitle']);
|
||||
}
|
||||
buffer.push(x);
|
||||
}
|
||||
else {
|
||||
events['subtitle'].pop();
|
||||
buffer.push(x);
|
||||
}
|
||||
} else {
|
||||
pushBuffer(buffer, events['subtitle']);
|
||||
}
|
||||
buffer.push(x);
|
||||
}
|
||||
else {
|
||||
events['subtitle'].pop();
|
||||
buffer.push(x);
|
||||
|
||||
pushBuffer(buffer, events['subtitle']);
|
||||
events['subtitle'].push(...captionsBuffer);
|
||||
events['subtitle'] = combineLines(events['subtitle']);
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pushBuffer(buffer, events['subtitle']);
|
||||
events['subtitle'].push(...captionsBuffer);
|
||||
events['subtitle'] = combineLines(events['subtitle']);
|
||||
|
||||
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';
|
||||
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: Record<string, 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 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}`;
|
||||
}
|
||||
}
|
||||
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 };
|
||||
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>/);
|
||||
const m = text.match(/<(?:c\.|)([^>]*)>([\S\s]*)<\/(?:c|Default)>/);
|
||||
let style = '';
|
||||
if (m) {
|
||||
style = m[1];
|
||||
text = m[2];
|
||||
}
|
||||
const xtext = text
|
||||
//const m = text.match(/<c\.([^>]*)>([\S\s]*)<\/c>/);
|
||||
const m = text.match(/<(?:c\.|)([^>]*)>([\S\s]*)<\/(?:c|Default)>/);
|
||||
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(/ \\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 };
|
||||
.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]);
|
||||
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];
|
||||
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];
|
||||
}
|
||||
|
||||
export default function vtt2ass(group: string | undefined, xFontSize: number | undefined, vttStr: string, cssStr: string, timeMargin?: number, replaceFont?: string, combineLines?: boolean) {
|
||||
relGroup = group ?? '';
|
||||
fontSize = xFontSize && xFontSize > 0 ? xFontSize : 34; // 1em to pix
|
||||
tmMrg = timeMargin ? timeMargin : 0; //
|
||||
rFont = replaceFont ? replaceFont : rFont;
|
||||
doCombineLines = combineLines ? combineLines : doCombineLines;
|
||||
if (vttStr.match(/::cue(?:.(.+)\) *)?{([^}]+)}/g)) {
|
||||
const cssLines = [];
|
||||
let defaultCss = '';
|
||||
const cssGroups = vttStr.matchAll(/::cue(?:.(.+)\) *)?{([^}]+)}/g);
|
||||
for (const cssGroup of cssGroups) {
|
||||
//Below code will bulldoze defined sizes for custom ones
|
||||
/*if (!options.originalFontSize) {
|
||||
relGroup = group ?? '';
|
||||
fontSize = xFontSize && xFontSize > 0 ? xFontSize : 34; // 1em to pix
|
||||
tmMrg = timeMargin ? timeMargin : 0; //
|
||||
rFont = replaceFont ? replaceFont : rFont;
|
||||
doCombineLines = combineLines ? combineLines : doCombineLines;
|
||||
if (vttStr.match(/::cue(?:.(.+)\) *)?{([^}]+)}/g)) {
|
||||
const cssLines = [];
|
||||
let defaultCss = '';
|
||||
const cssGroups = vttStr.matchAll(/::cue(?:.(.+)\) *)?{([^}]+)}/g);
|
||||
for (const cssGroup of cssGroups) {
|
||||
//Below code will bulldoze defined sizes for custom ones
|
||||
/*if (!options.originalFontSize) {
|
||||
cssGroup[2] = cssGroup[2].replace(/( font-size:.+?;)/g, '').replace(/(font-size:.+?;)/g, '');
|
||||
}*/
|
||||
if (cssGroup[1]) {
|
||||
cssLines.push(`${cssGroup[1]}{${defaultCss}${cssGroup[2].replace(/(\r\n|\n|\r)/gm, '')}}`);
|
||||
} else {
|
||||
defaultCss = cssGroup[2].replace(/(\r\n|\n|\r)/gm, '');
|
||||
//cssLines.push(`{${defaultCss}}`);
|
||||
}
|
||||
if (cssGroup[1]) {
|
||||
cssLines.push(`${cssGroup[1]}{${defaultCss}${cssGroup[2].replace(/(\r\n|\n|\r)/gm, '')}}`);
|
||||
} else {
|
||||
defaultCss = cssGroup[2].replace(/(\r\n|\n|\r)/gm, '');
|
||||
//cssLines.push(`{${defaultCss}}`);
|
||||
}
|
||||
}
|
||||
cssStr += cssLines.join('\r\n');
|
||||
}
|
||||
cssStr += cssLines.join('\r\n');
|
||||
}
|
||||
return convert(
|
||||
loadCSS(cssStr),
|
||||
loadVTT(vttStr)
|
||||
);
|
||||
return convert(
|
||||
loadCSS(cssStr),
|
||||
loadVTT(vttStr)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,167 +8,167 @@ export type Record = {
|
|||
export type NullRecord = Record | null;
|
||||
|
||||
function loadVtt(vttStr: string) {
|
||||
const rx = /^([\d:.]*) --> ([\d:.]*)\s?(.*?)\s*$/;
|
||||
const lines = vttStr.replace(/\r?\n/g, '\n').split('\n');
|
||||
const data: Record[] = []; let lineBuf: string[] = [], record: NullRecord = null;
|
||||
// check lines
|
||||
for (const l of lines) {
|
||||
const m = l.match(rx);
|
||||
if (m) {
|
||||
if (lineBuf.length > 0) {
|
||||
lineBuf.pop();
|
||||
}
|
||||
if (record !== null) {
|
||||
const rx = /^([\d:.]*) --> ([\d:.]*)\s?(.*?)\s*$/;
|
||||
const lines = vttStr.replace(/\r?\n/g, '\n').split('\n');
|
||||
const data: Record[] = []; let lineBuf: string[] = [], record: NullRecord = null;
|
||||
// check lines
|
||||
for (const l of lines) {
|
||||
const m = l.match(rx);
|
||||
if (m) {
|
||||
if (lineBuf.length > 0) {
|
||||
lineBuf.pop();
|
||||
}
|
||||
if (record !== null) {
|
||||
record.text = lineBuf.join('\n');
|
||||
data.push(record);
|
||||
}
|
||||
record = {
|
||||
time_start: m[1],
|
||||
time_end: m[2],
|
||||
ext_param: m[3].split(' ').map(x => x.split(':')).reduce((p: any, c: any) => (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);
|
||||
}
|
||||
record = {
|
||||
time_start: m[1],
|
||||
time_end: m[2],
|
||||
ext_param: m[3].split(' ').map(x => x.split(':')).reduce((p: any, c: any) => (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;
|
||||
return data;
|
||||
}
|
||||
|
||||
// ass specific
|
||||
function convertToAss(vttStr: string, lang: string, fontSize: number, fontName?: string){
|
||||
let ass = [
|
||||
'\ufeff[Script Info]',
|
||||
`Title: ${lang}`,
|
||||
'ScriptType: v4.00+',
|
||||
'PlayResX: 1280',
|
||||
'PlayResY: 720',
|
||||
'WrapStyle: 0',
|
||||
'ScaledBorderAndShadow: yes',
|
||||
'',
|
||||
'[V4+ Styles]',
|
||||
'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, '
|
||||
let ass = [
|
||||
'\ufeff[Script Info]',
|
||||
`Title: ${lang}`,
|
||||
'ScriptType: v4.00+',
|
||||
'PlayResX: 1280',
|
||||
'PlayResY: 720',
|
||||
'WrapStyle: 0',
|
||||
'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',
|
||||
`Style: Main,${fontName || 'Noto Sans'},${fontSize},&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,10,10,10,1`,
|
||||
`Style: MainTop,${fontName || 'Noto Sans'},${fontSize},&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,3,0,8,10,10,10,1`,
|
||||
'',
|
||||
'[Events]',
|
||||
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text',
|
||||
];
|
||||
`Style: Main,${fontName || 'Noto Sans'},${fontSize},&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,10,10,10,1`,
|
||||
`Style: MainTop,${fontName || 'Noto Sans'},${fontSize},&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,3,0,8,10,10,10,1`,
|
||||
'',
|
||||
'[Events]',
|
||||
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text',
|
||||
];
|
||||
|
||||
const vttData = loadVtt(vttStr);
|
||||
for (const l of vttData) {
|
||||
const line = convertToAssLine(l, 'Main');
|
||||
ass = ass.concat(line);
|
||||
}
|
||||
const vttData = loadVtt(vttStr);
|
||||
for (const l of vttData) {
|
||||
const line = convertToAssLine(l, 'Main');
|
||||
ass = ass.concat(line);
|
||||
}
|
||||
|
||||
return ass.join('\r\n') + '\r\n';
|
||||
return ass.join('\r\n') + '\r\n';
|
||||
}
|
||||
|
||||
function convertToAssLine(l: Record, style: string) {
|
||||
const start = convertTime(l.time_start as string);
|
||||
const end = convertTime(l.time_end as string);
|
||||
const text = convertToAssText(l.text as string);
|
||||
const start = convertTime(l.time_start as string);
|
||||
const end = convertTime(l.time_end as string);
|
||||
const text = convertToAssText(l.text as string);
|
||||
|
||||
// debugger
|
||||
if ((l.ext_param as any).line === '7%') {
|
||||
style = 'MainTop';
|
||||
}
|
||||
// debugger
|
||||
if ((l.ext_param as any).line === '7%') {
|
||||
style = 'MainTop';
|
||||
}
|
||||
|
||||
if ((l.ext_param as any).line === '10%') {
|
||||
style = 'MainTop';
|
||||
}
|
||||
if ((l.ext_param as any).line === '10%') {
|
||||
style = 'MainTop';
|
||||
}
|
||||
|
||||
return `Dialogue: 0,${start},${end},${style},,0,0,0,,${text}`;
|
||||
return `Dialogue: 0,${start},${end},${style},,0,0,0,,${text}`;
|
||||
}
|
||||
|
||||
function convertToAssText(text: string) {
|
||||
text = text
|
||||
.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(/ +$/, '');
|
||||
return text;
|
||||
text = text
|
||||
.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(/ +$/, '');
|
||||
return text;
|
||||
}
|
||||
|
||||
// srt specific
|
||||
function convertToSrt(vttStr: string){
|
||||
let srt: string[] = [], srtLineIdx = 0;
|
||||
let srt: string[] = [], srtLineIdx = 0;
|
||||
|
||||
const vttData = loadVtt(vttStr);
|
||||
for (const l of vttData) {
|
||||
srtLineIdx++;
|
||||
const line = convertToSrtLine(l, srtLineIdx);
|
||||
srt = srt.concat(line);
|
||||
}
|
||||
const vttData = loadVtt(vttStr);
|
||||
for (const l of vttData) {
|
||||
srtLineIdx++;
|
||||
const line = convertToSrtLine(l, srtLineIdx);
|
||||
srt = srt.concat(line);
|
||||
}
|
||||
|
||||
return srt.join('\r\n') + '\r\n';
|
||||
return srt.join('\r\n') + '\r\n';
|
||||
}
|
||||
|
||||
function convertToSrtLine(l: Record, idx: number) : string {
|
||||
const bom = idx == 1 ? '\ufeff' : '';
|
||||
const start = convertTime(l.time_start as string, true);
|
||||
const end = convertTime(l.time_end as string, true);
|
||||
const text = l.text;
|
||||
return `${bom}${idx}\r\n${start} --> ${end}\r\n${text}\r\n`;
|
||||
const bom = idx == 1 ? '\ufeff' : '';
|
||||
const start = convertTime(l.time_start as string, true);
|
||||
const end = convertTime(l.time_end as string, true);
|
||||
const text = l.text;
|
||||
return `${bom}${idx}\r\n${start} --> ${end}\r\n${text}\r\n`;
|
||||
}
|
||||
|
||||
// time parser
|
||||
function convertTime(time: string, srtFormat = false) {
|
||||
const mTime = time.match(/([\d:]*)\.?(\d*)/);
|
||||
if (!mTime){
|
||||
return srtFormat ? '00:00:00,000' : '0:00:00.00';
|
||||
}
|
||||
return toSubsTime(mTime[0], srtFormat);
|
||||
const mTime = time.match(/([\d:]*)\.?(\d*)/);
|
||||
if (!mTime){
|
||||
return srtFormat ? '00:00:00,000' : '0:00:00.00';
|
||||
}
|
||||
return toSubsTime(mTime[0], srtFormat);
|
||||
}
|
||||
|
||||
function toSubsTime(str: string, srtFormat: boolean) : string {
|
||||
|
||||
const n: string[] = [], x: (string|number)[] = str.split(/[:.]/).map(x => Number(x)); let sx;
|
||||
const n: string[] = [], x: (string|number)[] = str.split(/[:.]/).map(x => Number(x)); let sx;
|
||||
|
||||
const msLen = srtFormat ? 3 : 2;
|
||||
const hLen = srtFormat ? 2 : 1;
|
||||
const msLen = srtFormat ? 3 : 2;
|
||||
const hLen = srtFormat ? 2 : 1;
|
||||
|
||||
x[3] = '0.' + ('' + x[3]).padStart(3, '0');
|
||||
sx = (x[0] as number)*60*60 + (x[1] as number)*60 + (x[2] as number) + Number(x[3]);
|
||||
sx = sx.toFixed(msLen).split('.');
|
||||
x[3] = '0.' + ('' + x[3]).padStart(3, '0');
|
||||
sx = (x[0] as number)*60*60 + (x[1] as number)*60 + (x[2] as number) + Number(x[3]);
|
||||
sx = sx.toFixed(msLen).split('.');
|
||||
|
||||
|
||||
n.unshift(padTimeNum((srtFormat ? ',' : '.'), sx[1], msLen));
|
||||
sx = Number(sx[0]);
|
||||
n.unshift(padTimeNum((srtFormat ? ',' : '.'), sx[1], msLen));
|
||||
sx = Number(sx[0]);
|
||||
|
||||
n.unshift(padTimeNum(':', sx%60, 2));
|
||||
n.unshift(padTimeNum(':', Math.floor(sx/60)%60, 2));
|
||||
n.unshift(padTimeNum('', Math.floor(sx/3600)%60, hLen));
|
||||
n.unshift(padTimeNum(':', sx%60, 2));
|
||||
n.unshift(padTimeNum(':', Math.floor(sx/60)%60, 2));
|
||||
n.unshift(padTimeNum('', Math.floor(sx/3600)%60, hLen));
|
||||
|
||||
return n.join('');
|
||||
return n.join('');
|
||||
}
|
||||
|
||||
function padTimeNum(sep: string, input: string|number , pad:number){
|
||||
return sep + ('' + input).padStart(pad, '0');
|
||||
return sep + ('' + input).padStart(pad, '0');
|
||||
}
|
||||
|
||||
// export module
|
||||
const _default = (vttStr: string, toSrt: boolean, lang = 'English', fontSize: number, fontName?: string) => {
|
||||
const convert = toSrt ? convertToSrt : convertToAss;
|
||||
return convert(vttStr, lang, fontSize, fontName);
|
||||
const convert = toSrt ? convertToSrt : convertToAss;
|
||||
return convert(vttStr, lang, fontSize, fontName);
|
||||
};
|
||||
export default _default;
|
||||
|
|
|
|||
|
|
@ -5,474 +5,474 @@ import ECCKey from './ecc_key';
|
|||
import { console } from '../log';
|
||||
|
||||
function alignUp(length: number, alignment: number): number {
|
||||
return Math.ceil(length / alignment) * alignment;
|
||||
return Math.ceil(length / alignment) * alignment;
|
||||
}
|
||||
|
||||
export class BCertStructs {
|
||||
static DrmBCertBasicInfo = new Parser()
|
||||
.buffer('cert_id', { length: 16 })
|
||||
.uint32be('security_level')
|
||||
.uint32be('flags')
|
||||
.uint32be('cert_type')
|
||||
.buffer('public_key_digest', { length: 32 })
|
||||
.uint32be('expiration_date')
|
||||
.buffer('client_id', { length: 16 });
|
||||
static DrmBCertBasicInfo = new Parser()
|
||||
.buffer('cert_id', { length: 16 })
|
||||
.uint32be('security_level')
|
||||
.uint32be('flags')
|
||||
.uint32be('cert_type')
|
||||
.buffer('public_key_digest', { length: 32 })
|
||||
.uint32be('expiration_date')
|
||||
.buffer('client_id', { length: 16 });
|
||||
|
||||
static DrmBCertDomainInfo = new Parser()
|
||||
.buffer('service_id', { length: 16 })
|
||||
.buffer('account_id', { length: 16 })
|
||||
.uint32be('revision_timestamp')
|
||||
.uint32be('domain_url_length')
|
||||
.buffer('domain_url', {
|
||||
length: function () {
|
||||
return alignUp((this as any).domain_url_length, 4);
|
||||
},
|
||||
});
|
||||
static DrmBCertDomainInfo = new Parser()
|
||||
.buffer('service_id', { length: 16 })
|
||||
.buffer('account_id', { length: 16 })
|
||||
.uint32be('revision_timestamp')
|
||||
.uint32be('domain_url_length')
|
||||
.buffer('domain_url', {
|
||||
length: function () {
|
||||
return alignUp((this as any).domain_url_length, 4);
|
||||
},
|
||||
});
|
||||
|
||||
static DrmBCertPCInfo = new Parser().uint32be('security_version');
|
||||
static DrmBCertPCInfo = new Parser().uint32be('security_version');
|
||||
|
||||
static DrmBCertDeviceInfo = new Parser()
|
||||
.uint32be('max_license')
|
||||
.uint32be('max_header')
|
||||
.uint32be('max_chain_depth');
|
||||
static DrmBCertDeviceInfo = new Parser()
|
||||
.uint32be('max_license')
|
||||
.uint32be('max_header')
|
||||
.uint32be('max_chain_depth');
|
||||
|
||||
static DrmBCertFeatureInfo = new Parser()
|
||||
.uint32be('feature_count')
|
||||
.array('features', {
|
||||
type: 'uint32be',
|
||||
length: 'feature_count',
|
||||
});
|
||||
static DrmBCertFeatureInfo = new Parser()
|
||||
.uint32be('feature_count')
|
||||
.array('features', {
|
||||
type: 'uint32be',
|
||||
length: 'feature_count',
|
||||
});
|
||||
|
||||
static CertKey = new Parser()
|
||||
.uint16be('type')
|
||||
.uint16be('length')
|
||||
.uint32be('flags')
|
||||
.buffer('key', {
|
||||
length: function () {
|
||||
return (this as any).length / 8;
|
||||
},
|
||||
})
|
||||
.uint32be('usages_count')
|
||||
.array('usages', {
|
||||
type: 'uint32be',
|
||||
length: 'usages_count',
|
||||
});
|
||||
static CertKey = new Parser()
|
||||
.uint16be('type')
|
||||
.uint16be('length')
|
||||
.uint32be('flags')
|
||||
.buffer('key', {
|
||||
length: function () {
|
||||
return (this as any).length / 8;
|
||||
},
|
||||
})
|
||||
.uint32be('usages_count')
|
||||
.array('usages', {
|
||||
type: 'uint32be',
|
||||
length: 'usages_count',
|
||||
});
|
||||
|
||||
static DrmBCertKeyInfo = new Parser()
|
||||
.uint32be('key_count')
|
||||
.array('cert_keys', {
|
||||
type: BCertStructs.CertKey,
|
||||
length: 'key_count',
|
||||
});
|
||||
static DrmBCertKeyInfo = new Parser()
|
||||
.uint32be('key_count')
|
||||
.array('cert_keys', {
|
||||
type: BCertStructs.CertKey,
|
||||
length: 'key_count',
|
||||
});
|
||||
|
||||
static DrmBCertManufacturerInfo = new Parser()
|
||||
.uint32be('flags')
|
||||
.uint32be('manufacturer_name_length')
|
||||
.buffer('manufacturer_name', {
|
||||
length: function () {
|
||||
return alignUp((this as any).manufacturer_name_length, 4);
|
||||
},
|
||||
})
|
||||
.uint32be('model_name_length')
|
||||
.buffer('model_name', {
|
||||
length: function () {
|
||||
return alignUp((this as any).model_name_length, 4);
|
||||
},
|
||||
})
|
||||
.uint32be('model_number_length')
|
||||
.buffer('model_number', {
|
||||
length: function () {
|
||||
return alignUp((this as any).model_number_length, 4);
|
||||
},
|
||||
});
|
||||
static DrmBCertManufacturerInfo = new Parser()
|
||||
.uint32be('flags')
|
||||
.uint32be('manufacturer_name_length')
|
||||
.buffer('manufacturer_name', {
|
||||
length: function () {
|
||||
return alignUp((this as any).manufacturer_name_length, 4);
|
||||
},
|
||||
})
|
||||
.uint32be('model_name_length')
|
||||
.buffer('model_name', {
|
||||
length: function () {
|
||||
return alignUp((this as any).model_name_length, 4);
|
||||
},
|
||||
})
|
||||
.uint32be('model_number_length')
|
||||
.buffer('model_number', {
|
||||
length: function () {
|
||||
return alignUp((this as any).model_number_length, 4);
|
||||
},
|
||||
});
|
||||
|
||||
static DrmBCertSignatureInfo = new Parser()
|
||||
.uint16be('signature_type')
|
||||
.uint16be('signature_size')
|
||||
.buffer('signature', { length: 'signature_size' })
|
||||
.uint32be('signature_key_size')
|
||||
.buffer('signature_key', {
|
||||
length: function () {
|
||||
return (this as any).signature_key_size / 8;
|
||||
},
|
||||
});
|
||||
static DrmBCertSignatureInfo = new Parser()
|
||||
.uint16be('signature_type')
|
||||
.uint16be('signature_size')
|
||||
.buffer('signature', { length: 'signature_size' })
|
||||
.uint32be('signature_key_size')
|
||||
.buffer('signature_key', {
|
||||
length: function () {
|
||||
return (this as any).signature_key_size / 8;
|
||||
},
|
||||
});
|
||||
|
||||
static DrmBCertSilverlightInfo = new Parser()
|
||||
.uint32be('security_version')
|
||||
.uint32be('platform_identifier');
|
||||
static DrmBCertSilverlightInfo = new Parser()
|
||||
.uint32be('security_version')
|
||||
.uint32be('platform_identifier');
|
||||
|
||||
static DrmBCertMeteringInfo = new Parser()
|
||||
.buffer('metering_id', { length: 16 })
|
||||
.uint32be('metering_url_length')
|
||||
.buffer('metering_url', {
|
||||
length: function () {
|
||||
return alignUp((this as any).metering_url_length, 4);
|
||||
},
|
||||
});
|
||||
static DrmBCertMeteringInfo = new Parser()
|
||||
.buffer('metering_id', { length: 16 })
|
||||
.uint32be('metering_url_length')
|
||||
.buffer('metering_url', {
|
||||
length: function () {
|
||||
return alignUp((this as any).metering_url_length, 4);
|
||||
},
|
||||
});
|
||||
|
||||
static DrmBCertExtDataSignKeyInfo = new Parser()
|
||||
.uint16be('key_type')
|
||||
.uint16be('key_length')
|
||||
.uint32be('flags')
|
||||
.buffer('key', {
|
||||
length: function () {
|
||||
return (this as any).length / 8;
|
||||
},
|
||||
});
|
||||
static DrmBCertExtDataSignKeyInfo = new Parser()
|
||||
.uint16be('key_type')
|
||||
.uint16be('key_length')
|
||||
.uint32be('flags')
|
||||
.buffer('key', {
|
||||
length: function () {
|
||||
return (this as any).length / 8;
|
||||
},
|
||||
});
|
||||
|
||||
static BCertExtDataRecord = new Parser()
|
||||
.uint32be('data_size')
|
||||
.buffer('data', {
|
||||
length: 'data_size',
|
||||
});
|
||||
static BCertExtDataRecord = new Parser()
|
||||
.uint32be('data_size')
|
||||
.buffer('data', {
|
||||
length: 'data_size',
|
||||
});
|
||||
|
||||
static DrmBCertExtDataSignature = new Parser()
|
||||
.uint16be('signature_type')
|
||||
.uint16be('signature_size')
|
||||
.buffer('signature', {
|
||||
length: 'signature_size',
|
||||
});
|
||||
static DrmBCertExtDataSignature = new Parser()
|
||||
.uint16be('signature_type')
|
||||
.uint16be('signature_size')
|
||||
.buffer('signature', {
|
||||
length: 'signature_size',
|
||||
});
|
||||
|
||||
static BCertExtDataContainer = new Parser()
|
||||
.uint32be('record_count')
|
||||
.array('records', {
|
||||
length: 'record_count',
|
||||
type: BCertStructs.BCertExtDataRecord,
|
||||
})
|
||||
.nest('signature', {
|
||||
type: BCertStructs.DrmBCertExtDataSignature,
|
||||
});
|
||||
static BCertExtDataContainer = new Parser()
|
||||
.uint32be('record_count')
|
||||
.array('records', {
|
||||
length: 'record_count',
|
||||
type: BCertStructs.BCertExtDataRecord,
|
||||
})
|
||||
.nest('signature', {
|
||||
type: BCertStructs.DrmBCertExtDataSignature,
|
||||
});
|
||||
|
||||
static DrmBCertServerInfo = new Parser().uint32be('warning_days');
|
||||
static DrmBCertServerInfo = new Parser().uint32be('warning_days');
|
||||
|
||||
static DrmBcertSecurityVersion = new Parser()
|
||||
.uint32be('security_version')
|
||||
.uint32be('platform_identifier');
|
||||
static DrmBcertSecurityVersion = new Parser()
|
||||
.uint32be('security_version')
|
||||
.uint32be('platform_identifier');
|
||||
|
||||
static Attribute = new Parser()
|
||||
.uint16be('flags')
|
||||
.uint16be('tag')
|
||||
.uint32be('length')
|
||||
.choice('attribute', {
|
||||
tag: 'tag',
|
||||
choices: {
|
||||
1: BCertStructs.DrmBCertBasicInfo,
|
||||
2: BCertStructs.DrmBCertDomainInfo,
|
||||
3: BCertStructs.DrmBCertPCInfo,
|
||||
4: BCertStructs.DrmBCertDeviceInfo,
|
||||
5: BCertStructs.DrmBCertFeatureInfo,
|
||||
6: BCertStructs.DrmBCertKeyInfo,
|
||||
7: BCertStructs.DrmBCertManufacturerInfo,
|
||||
8: BCertStructs.DrmBCertSignatureInfo,
|
||||
9: BCertStructs.DrmBCertSilverlightInfo,
|
||||
10: BCertStructs.DrmBCertMeteringInfo,
|
||||
11: BCertStructs.DrmBCertExtDataSignKeyInfo,
|
||||
12: BCertStructs.BCertExtDataContainer,
|
||||
13: BCertStructs.DrmBCertExtDataSignature,
|
||||
14: new Parser().buffer('data', {
|
||||
length: function () {
|
||||
return (this as any).length - 8;
|
||||
},
|
||||
}),
|
||||
15: BCertStructs.DrmBCertServerInfo,
|
||||
16: BCertStructs.DrmBcertSecurityVersion,
|
||||
17: BCertStructs.DrmBcertSecurityVersion,
|
||||
},
|
||||
defaultChoice: new Parser().buffer('data', {
|
||||
length: function () {
|
||||
return (this as any).length - 8;
|
||||
},
|
||||
}),
|
||||
});
|
||||
static Attribute = new Parser()
|
||||
.uint16be('flags')
|
||||
.uint16be('tag')
|
||||
.uint32be('length')
|
||||
.choice('attribute', {
|
||||
tag: 'tag',
|
||||
choices: {
|
||||
1: BCertStructs.DrmBCertBasicInfo,
|
||||
2: BCertStructs.DrmBCertDomainInfo,
|
||||
3: BCertStructs.DrmBCertPCInfo,
|
||||
4: BCertStructs.DrmBCertDeviceInfo,
|
||||
5: BCertStructs.DrmBCertFeatureInfo,
|
||||
6: BCertStructs.DrmBCertKeyInfo,
|
||||
7: BCertStructs.DrmBCertManufacturerInfo,
|
||||
8: BCertStructs.DrmBCertSignatureInfo,
|
||||
9: BCertStructs.DrmBCertSilverlightInfo,
|
||||
10: BCertStructs.DrmBCertMeteringInfo,
|
||||
11: BCertStructs.DrmBCertExtDataSignKeyInfo,
|
||||
12: BCertStructs.BCertExtDataContainer,
|
||||
13: BCertStructs.DrmBCertExtDataSignature,
|
||||
14: new Parser().buffer('data', {
|
||||
length: function () {
|
||||
return (this as any).length - 8;
|
||||
},
|
||||
}),
|
||||
15: BCertStructs.DrmBCertServerInfo,
|
||||
16: BCertStructs.DrmBcertSecurityVersion,
|
||||
17: BCertStructs.DrmBcertSecurityVersion,
|
||||
},
|
||||
defaultChoice: new Parser().buffer('data', {
|
||||
length: function () {
|
||||
return (this as any).length - 8;
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
static BCert = new Parser()
|
||||
.string('signature', { length: 4, assert: 'CERT' })
|
||||
.int32be('version')
|
||||
.int32be('total_length')
|
||||
.int32be('certificate_length')
|
||||
.array('attributes', {
|
||||
type: BCertStructs.Attribute,
|
||||
lengthInBytes: function () {
|
||||
return (this as any).total_length - 16;
|
||||
},
|
||||
});
|
||||
static BCert = new Parser()
|
||||
.string('signature', { length: 4, assert: 'CERT' })
|
||||
.int32be('version')
|
||||
.int32be('total_length')
|
||||
.int32be('certificate_length')
|
||||
.array('attributes', {
|
||||
type: BCertStructs.Attribute,
|
||||
lengthInBytes: function () {
|
||||
return (this as any).total_length - 16;
|
||||
},
|
||||
});
|
||||
|
||||
static BCertChain = new Parser()
|
||||
.string('signature', { length: 4, assert: 'CHAI' })
|
||||
.int32be('version')
|
||||
.int32be('total_length')
|
||||
.int32be('flags')
|
||||
.int32be('certificate_count')
|
||||
.array('certificates', {
|
||||
type: BCertStructs.BCert,
|
||||
length: 'certificate_count',
|
||||
});
|
||||
static BCertChain = new Parser()
|
||||
.string('signature', { length: 4, assert: 'CHAI' })
|
||||
.int32be('version')
|
||||
.int32be('total_length')
|
||||
.int32be('flags')
|
||||
.int32be('certificate_count')
|
||||
.array('certificates', {
|
||||
type: BCertStructs.BCert,
|
||||
length: 'certificate_count',
|
||||
});
|
||||
}
|
||||
|
||||
export class Certificate {
|
||||
parsed: any;
|
||||
_BCERT: Parser;
|
||||
parsed: any;
|
||||
_BCERT: Parser;
|
||||
|
||||
constructor(parsed_bcert: any, bcert_obj: Parser = BCertStructs.BCert) {
|
||||
this.parsed = parsed_bcert;
|
||||
this._BCERT = bcert_obj;
|
||||
}
|
||||
constructor(parsed_bcert: any, bcert_obj: Parser = BCertStructs.BCert) {
|
||||
this.parsed = parsed_bcert;
|
||||
this._BCERT = bcert_obj;
|
||||
}
|
||||
|
||||
// UNSTABLE
|
||||
static new_leaf_cert(
|
||||
cert_id: Buffer,
|
||||
security_level: number,
|
||||
client_id: Buffer,
|
||||
signing_key: ECCKey,
|
||||
encryption_key: ECCKey,
|
||||
group_key: ECCKey,
|
||||
parent: CertificateChain,
|
||||
expiry: number = 0xffffffff,
|
||||
max_license: number = 10240,
|
||||
max_header: number = 15360,
|
||||
max_chain_depth: number = 2
|
||||
): Certificate {
|
||||
const basic_info = {
|
||||
cert_id: cert_id,
|
||||
security_level: security_level,
|
||||
flags: 0,
|
||||
cert_type: 2,
|
||||
public_key_digest: signing_key.publicSha256Digest(),
|
||||
expiration_date: expiry,
|
||||
client_id: client_id,
|
||||
};
|
||||
const basic_info_attribute = {
|
||||
flags: 1,
|
||||
tag: 1,
|
||||
length: BCertStructs.DrmBCertBasicInfo.encode(basic_info).length + 8,
|
||||
attribute: basic_info,
|
||||
};
|
||||
// UNSTABLE
|
||||
static new_leaf_cert(
|
||||
cert_id: Buffer,
|
||||
security_level: number,
|
||||
client_id: Buffer,
|
||||
signing_key: ECCKey,
|
||||
encryption_key: ECCKey,
|
||||
group_key: ECCKey,
|
||||
parent: CertificateChain,
|
||||
expiry: number = 0xffffffff,
|
||||
max_license: number = 10240,
|
||||
max_header: number = 15360,
|
||||
max_chain_depth: number = 2
|
||||
): Certificate {
|
||||
const basic_info = {
|
||||
cert_id: cert_id,
|
||||
security_level: security_level,
|
||||
flags: 0,
|
||||
cert_type: 2,
|
||||
public_key_digest: signing_key.publicSha256Digest(),
|
||||
expiration_date: expiry,
|
||||
client_id: client_id,
|
||||
};
|
||||
const basic_info_attribute = {
|
||||
flags: 1,
|
||||
tag: 1,
|
||||
length: BCertStructs.DrmBCertBasicInfo.encode(basic_info).length + 8,
|
||||
attribute: basic_info,
|
||||
};
|
||||
|
||||
const device_info = {
|
||||
max_license: max_license,
|
||||
max_header: max_header,
|
||||
max_chain_depth: max_chain_depth,
|
||||
};
|
||||
const device_info = {
|
||||
max_license: max_license,
|
||||
max_header: max_header,
|
||||
max_chain_depth: max_chain_depth,
|
||||
};
|
||||
|
||||
const device_info_attribute = {
|
||||
flags: 1,
|
||||
tag: 4,
|
||||
length: BCertStructs.DrmBCertDeviceInfo.encode(device_info).length + 8,
|
||||
attribute: device_info,
|
||||
};
|
||||
const device_info_attribute = {
|
||||
flags: 1,
|
||||
tag: 4,
|
||||
length: BCertStructs.DrmBCertDeviceInfo.encode(device_info).length + 8,
|
||||
attribute: device_info,
|
||||
};
|
||||
|
||||
const feature = {
|
||||
feature_count: 3,
|
||||
features: [4, 9, 13],
|
||||
};
|
||||
const feature_attribute = {
|
||||
flags: 1,
|
||||
tag: 5,
|
||||
length: BCertStructs.DrmBCertFeatureInfo.encode(feature).length + 8,
|
||||
attribute: feature,
|
||||
};
|
||||
const feature = {
|
||||
feature_count: 3,
|
||||
features: [4, 9, 13],
|
||||
};
|
||||
const feature_attribute = {
|
||||
flags: 1,
|
||||
tag: 5,
|
||||
length: BCertStructs.DrmBCertFeatureInfo.encode(feature).length + 8,
|
||||
attribute: feature,
|
||||
};
|
||||
|
||||
const cert_key_sign = {
|
||||
type: 1,
|
||||
length: 512, // bits
|
||||
flags: 0,
|
||||
key: signing_key.privateBytes(),
|
||||
usages_count: 1,
|
||||
usages: [1],
|
||||
};
|
||||
const cert_key_encrypt = {
|
||||
type: 1,
|
||||
length: 512, // bits
|
||||
flags: 0,
|
||||
key: encryption_key.privateBytes(),
|
||||
usages_count: 1,
|
||||
usages: [2],
|
||||
};
|
||||
const key_info = {
|
||||
key_count: 2,
|
||||
cert_keys: [cert_key_sign, cert_key_encrypt],
|
||||
};
|
||||
const key_info_attribute = {
|
||||
flags: 1,
|
||||
tag: 6,
|
||||
length: BCertStructs.DrmBCertKeyInfo.encode(key_info).length + 8,
|
||||
attribute: key_info,
|
||||
};
|
||||
const cert_key_sign = {
|
||||
type: 1,
|
||||
length: 512, // bits
|
||||
flags: 0,
|
||||
key: signing_key.privateBytes(),
|
||||
usages_count: 1,
|
||||
usages: [1],
|
||||
};
|
||||
const cert_key_encrypt = {
|
||||
type: 1,
|
||||
length: 512, // bits
|
||||
flags: 0,
|
||||
key: encryption_key.privateBytes(),
|
||||
usages_count: 1,
|
||||
usages: [2],
|
||||
};
|
||||
const key_info = {
|
||||
key_count: 2,
|
||||
cert_keys: [cert_key_sign, cert_key_encrypt],
|
||||
};
|
||||
const key_info_attribute = {
|
||||
flags: 1,
|
||||
tag: 6,
|
||||
length: BCertStructs.DrmBCertKeyInfo.encode(key_info).length + 8,
|
||||
attribute: key_info,
|
||||
};
|
||||
|
||||
const manufacturer_info = parent.get_certificate(0).get_attribute(7);
|
||||
const manufacturer_info = parent.get_certificate(0).get_attribute(7);
|
||||
|
||||
const new_bcert_container = {
|
||||
signature: 'CERT',
|
||||
version: 1,
|
||||
total_length: 0,
|
||||
certificate_length: 0,
|
||||
attributes: [
|
||||
basic_info_attribute,
|
||||
device_info_attribute,
|
||||
feature_attribute,
|
||||
key_info_attribute,
|
||||
manufacturer_info,
|
||||
],
|
||||
};
|
||||
const new_bcert_container = {
|
||||
signature: 'CERT',
|
||||
version: 1,
|
||||
total_length: 0,
|
||||
certificate_length: 0,
|
||||
attributes: [
|
||||
basic_info_attribute,
|
||||
device_info_attribute,
|
||||
feature_attribute,
|
||||
key_info_attribute,
|
||||
manufacturer_info,
|
||||
],
|
||||
};
|
||||
|
||||
let payload = BCertStructs.BCert.encode(new_bcert_container);
|
||||
new_bcert_container.certificate_length = payload.length;
|
||||
new_bcert_container.total_length = payload.length + 144;
|
||||
payload = BCertStructs.BCert.encode(new_bcert_container);
|
||||
let payload = BCertStructs.BCert.encode(new_bcert_container);
|
||||
new_bcert_container.certificate_length = payload.length;
|
||||
new_bcert_container.total_length = payload.length + 144;
|
||||
payload = BCertStructs.BCert.encode(new_bcert_container);
|
||||
|
||||
const hash = createHash('sha256');
|
||||
hash.update(payload);
|
||||
const digest = hash.digest();
|
||||
const hash = createHash('sha256');
|
||||
hash.update(payload);
|
||||
const digest = hash.digest();
|
||||
|
||||
const signatureObj = group_key.keyPair.sign(digest);
|
||||
const r = Buffer.from(signatureObj.r.toArray('be', 32));
|
||||
const s = Buffer.from(signatureObj.s.toArray('be', 32));
|
||||
const signature = Buffer.concat([r, s]);
|
||||
const signatureObj = group_key.keyPair.sign(digest);
|
||||
const r = Buffer.from(signatureObj.r.toArray('be', 32));
|
||||
const s = Buffer.from(signatureObj.s.toArray('be', 32));
|
||||
const signature = Buffer.concat([r, s]);
|
||||
|
||||
const signature_info = {
|
||||
signature_type: 1,
|
||||
signature_size: 64,
|
||||
signature: signature,
|
||||
signature_key_size: 512, // bits
|
||||
signature_key: group_key.publicBytes(),
|
||||
};
|
||||
const signature_info_attribute = {
|
||||
flags: 1,
|
||||
tag: 8,
|
||||
length:
|
||||
const signature_info = {
|
||||
signature_type: 1,
|
||||
signature_size: 64,
|
||||
signature: signature,
|
||||
signature_key_size: 512, // bits
|
||||
signature_key: group_key.publicBytes(),
|
||||
};
|
||||
const signature_info_attribute = {
|
||||
flags: 1,
|
||||
tag: 8,
|
||||
length:
|
||||
BCertStructs.DrmBCertSignatureInfo.encode(signature_info).length + 8,
|
||||
attribute: signature_info,
|
||||
};
|
||||
new_bcert_container.attributes.push(signature_info_attribute);
|
||||
attribute: signature_info,
|
||||
};
|
||||
new_bcert_container.attributes.push(signature_info_attribute);
|
||||
|
||||
return new Certificate(new_bcert_container);
|
||||
}
|
||||
|
||||
static loads(data: string | Buffer): Certificate {
|
||||
if (typeof data === 'string') {
|
||||
data = Buffer.from(data, 'base64');
|
||||
}
|
||||
if (!Buffer.isBuffer(data)) {
|
||||
throw new Error(`Expecting Bytes or Base64 input, got ${data}`);
|
||||
return new Certificate(new_bcert_container);
|
||||
}
|
||||
|
||||
const cert = BCertStructs.BCert;
|
||||
const parsed_bcert = cert.parse(data);
|
||||
return new Certificate(parsed_bcert, cert);
|
||||
}
|
||||
static loads(data: string | Buffer): Certificate {
|
||||
if (typeof data === 'string') {
|
||||
data = Buffer.from(data, 'base64');
|
||||
}
|
||||
if (!Buffer.isBuffer(data)) {
|
||||
throw new Error(`Expecting Bytes or Base64 input, got ${data}`);
|
||||
}
|
||||
|
||||
static load(filePath: string): Certificate {
|
||||
const data = fs.readFileSync(filePath);
|
||||
return Certificate.loads(data);
|
||||
}
|
||||
|
||||
get_attribute(type_: number) {
|
||||
for (const attribute of this.parsed.attributes) {
|
||||
if (attribute.tag === type_) {
|
||||
return attribute;
|
||||
}
|
||||
const cert = BCertStructs.BCert;
|
||||
const parsed_bcert = cert.parse(data);
|
||||
return new Certificate(parsed_bcert, cert);
|
||||
}
|
||||
}
|
||||
|
||||
get_security_level(): number {
|
||||
const basic_info_attribute = this.get_attribute(1);
|
||||
if (basic_info_attribute) {
|
||||
return basic_info_attribute.attribute.security_level;
|
||||
static load(filePath: string): Certificate {
|
||||
const data = fs.readFileSync(filePath);
|
||||
return Certificate.loads(data);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static _unpad(name: Buffer): string {
|
||||
return name.toString('utf8').replace(/\0+$/, '');
|
||||
}
|
||||
|
||||
get_name(): string {
|
||||
const manufacturer_info_attribute = this.get_attribute(7);
|
||||
if (manufacturer_info_attribute) {
|
||||
const manufacturer_info = manufacturer_info_attribute.attribute;
|
||||
const manufacturer_name = Certificate._unpad(
|
||||
manufacturer_info.manufacturer_name
|
||||
);
|
||||
const model_name = Certificate._unpad(manufacturer_info.model_name);
|
||||
const model_number = Certificate._unpad(manufacturer_info.model_number);
|
||||
return `${manufacturer_name} ${model_name} ${model_number}`;
|
||||
get_attribute(type_: number) {
|
||||
for (const attribute of this.parsed.attributes) {
|
||||
if (attribute.tag === type_) {
|
||||
return attribute;
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
dumps(): Buffer {
|
||||
return this._BCERT.encode(this.parsed);
|
||||
}
|
||||
get_security_level(): number {
|
||||
const basic_info_attribute = this.get_attribute(1);
|
||||
if (basic_info_attribute) {
|
||||
return basic_info_attribute.attribute.security_level;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct(): Parser {
|
||||
return this._BCERT;
|
||||
}
|
||||
private static _unpad(name: Buffer): string {
|
||||
return name.toString('utf8').replace(/\0+$/, '');
|
||||
}
|
||||
|
||||
get_name(): string {
|
||||
const manufacturer_info_attribute = this.get_attribute(7);
|
||||
if (manufacturer_info_attribute) {
|
||||
const manufacturer_info = manufacturer_info_attribute.attribute;
|
||||
const manufacturer_name = Certificate._unpad(
|
||||
manufacturer_info.manufacturer_name
|
||||
);
|
||||
const model_name = Certificate._unpad(manufacturer_info.model_name);
|
||||
const model_number = Certificate._unpad(manufacturer_info.model_number);
|
||||
return `${manufacturer_name} ${model_name} ${model_number}`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
dumps(): Buffer {
|
||||
return this._BCERT.encode(this.parsed);
|
||||
}
|
||||
|
||||
struct(): Parser {
|
||||
return this._BCERT;
|
||||
}
|
||||
}
|
||||
|
||||
export class CertificateChain {
|
||||
parsed: any;
|
||||
_BCERT_CHAIN: Parser;
|
||||
parsed: any;
|
||||
_BCERT_CHAIN: Parser;
|
||||
|
||||
constructor(
|
||||
parsed_bcert_chain: any,
|
||||
bcert_chain_obj: Parser = BCertStructs.BCertChain
|
||||
) {
|
||||
this.parsed = parsed_bcert_chain;
|
||||
this._BCERT_CHAIN = bcert_chain_obj;
|
||||
}
|
||||
|
||||
static loads(data: string | Buffer): CertificateChain {
|
||||
if (typeof data === 'string') {
|
||||
data = Buffer.from(data, 'base64');
|
||||
}
|
||||
if (!Buffer.isBuffer(data)) {
|
||||
throw new Error(`Expecting Bytes or Base64 input, got ${data}`);
|
||||
constructor(
|
||||
parsed_bcert_chain: any,
|
||||
bcert_chain_obj: Parser = BCertStructs.BCertChain
|
||||
) {
|
||||
this.parsed = parsed_bcert_chain;
|
||||
this._BCERT_CHAIN = bcert_chain_obj;
|
||||
}
|
||||
|
||||
const cert_chain = BCertStructs.BCertChain;
|
||||
try {
|
||||
const parsed_bcert_chain = cert_chain.parse(data);
|
||||
return new CertificateChain(parsed_bcert_chain, cert_chain);
|
||||
} catch (error) {
|
||||
console.error('Error during parsing:', error);
|
||||
throw error;
|
||||
static loads(data: string | Buffer): CertificateChain {
|
||||
if (typeof data === 'string') {
|
||||
data = Buffer.from(data, 'base64');
|
||||
}
|
||||
if (!Buffer.isBuffer(data)) {
|
||||
throw new Error(`Expecting Bytes or Base64 input, got ${data}`);
|
||||
}
|
||||
|
||||
const cert_chain = BCertStructs.BCertChain;
|
||||
try {
|
||||
const parsed_bcert_chain = cert_chain.parse(data);
|
||||
return new CertificateChain(parsed_bcert_chain, cert_chain);
|
||||
} catch (error) {
|
||||
console.error('Error during parsing:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static load(filePath: string): CertificateChain {
|
||||
const data = fs.readFileSync(filePath);
|
||||
return CertificateChain.loads(data);
|
||||
}
|
||||
static load(filePath: string): CertificateChain {
|
||||
const data = fs.readFileSync(filePath);
|
||||
return CertificateChain.loads(data);
|
||||
}
|
||||
|
||||
dumps(): Buffer {
|
||||
return this._BCERT_CHAIN.encode(this.parsed);
|
||||
}
|
||||
dumps(): Buffer {
|
||||
return this._BCERT_CHAIN.encode(this.parsed);
|
||||
}
|
||||
|
||||
struct(): Parser {
|
||||
return this._BCERT_CHAIN;
|
||||
}
|
||||
struct(): Parser {
|
||||
return this._BCERT_CHAIN;
|
||||
}
|
||||
|
||||
get_certificate(index: number): Certificate {
|
||||
return new Certificate(this.parsed.certificates[index]);
|
||||
}
|
||||
get_certificate(index: number): Certificate {
|
||||
return new Certificate(this.parsed.certificates[index]);
|
||||
}
|
||||
|
||||
get_security_level(): number {
|
||||
return this.get_certificate(0).get_security_level();
|
||||
}
|
||||
get_security_level(): number {
|
||||
return this.get_certificate(0).get_security_level();
|
||||
}
|
||||
|
||||
get_name(): string {
|
||||
return this.get_certificate(0).get_name();
|
||||
}
|
||||
get_name(): string {
|
||||
return this.get_certificate(0).get_name();
|
||||
}
|
||||
|
||||
append(bcert: Certificate): void {
|
||||
this.parsed.certificate_count += 1;
|
||||
this.parsed.certificates.push(bcert.parsed);
|
||||
this.parsed.total_length += bcert.dumps().length;
|
||||
}
|
||||
append(bcert: Certificate): void {
|
||||
this.parsed.certificate_count += 1;
|
||||
this.parsed.certificates.push(bcert.parsed);
|
||||
this.parsed.total_length += bcert.dumps().length;
|
||||
}
|
||||
|
||||
prepend(bcert: Certificate): void {
|
||||
this.parsed.certificate_count += 1;
|
||||
this.parsed.certificates.unshift(bcert.parsed);
|
||||
this.parsed.total_length += bcert.dumps().length;
|
||||
}
|
||||
prepend(bcert: Certificate): void {
|
||||
this.parsed.certificate_count += 1;
|
||||
this.parsed.certificates.unshift(bcert.parsed);
|
||||
this.parsed.total_length += bcert.dumps().length;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,99 +12,99 @@ import { Device } from './device';
|
|||
import { XMLParser } from 'fast-xml-parser';
|
||||
|
||||
export default class Cdm {
|
||||
security_level: number;
|
||||
certificate_chain: CertificateChain;
|
||||
encryption_key: ECCKey;
|
||||
signing_key: ECCKey;
|
||||
client_version: string;
|
||||
la_version: number;
|
||||
security_level: number;
|
||||
certificate_chain: CertificateChain;
|
||||
encryption_key: ECCKey;
|
||||
signing_key: ECCKey;
|
||||
client_version: string;
|
||||
la_version: number;
|
||||
|
||||
curve: elliptic.ec;
|
||||
elgamal: ElGamal;
|
||||
curve: elliptic.ec;
|
||||
elgamal: ElGamal;
|
||||
|
||||
private wmrm_key: elliptic.ec.KeyPair;
|
||||
private xml_key: XmlKey;
|
||||
private wmrm_key: elliptic.ec.KeyPair;
|
||||
private xml_key: XmlKey;
|
||||
|
||||
constructor(
|
||||
security_level: number,
|
||||
certificate_chain: CertificateChain,
|
||||
encryption_key: ECCKey,
|
||||
signing_key: ECCKey,
|
||||
client_version: string = '2.4.117.27',
|
||||
la_version: number = 1
|
||||
) {
|
||||
this.security_level = security_level;
|
||||
this.certificate_chain = certificate_chain;
|
||||
this.encryption_key = encryption_key;
|
||||
this.signing_key = signing_key;
|
||||
this.client_version = client_version;
|
||||
this.la_version = la_version;
|
||||
constructor(
|
||||
security_level: number,
|
||||
certificate_chain: CertificateChain,
|
||||
encryption_key: ECCKey,
|
||||
signing_key: ECCKey,
|
||||
client_version: string = '2.4.117.27',
|
||||
la_version: number = 1
|
||||
) {
|
||||
this.security_level = security_level;
|
||||
this.certificate_chain = certificate_chain;
|
||||
this.encryption_key = encryption_key;
|
||||
this.signing_key = signing_key;
|
||||
this.client_version = client_version;
|
||||
this.la_version = la_version;
|
||||
|
||||
this.curve = new elliptic.ec('p256');
|
||||
this.elgamal = new ElGamal(this.curve);
|
||||
this.curve = new elliptic.ec('p256');
|
||||
this.elgamal = new ElGamal(this.curve);
|
||||
|
||||
const x =
|
||||
const x =
|
||||
'c8b6af16ee941aadaa5389b4af2c10e356be42af175ef3face93254e7b0b3d9b';
|
||||
const y =
|
||||
const y =
|
||||
'982b27b5cb2341326e56aa857dbfd5c634ce2cf9ea74fca8f2af5957efeea562';
|
||||
this.wmrm_key = this.curve.keyFromPublic({ x, y }, 'hex');
|
||||
this.xml_key = new XmlKey();
|
||||
}
|
||||
this.wmrm_key = this.curve.keyFromPublic({ x, y }, 'hex');
|
||||
this.xml_key = new XmlKey();
|
||||
}
|
||||
|
||||
static fromDevice(device: Device): Cdm {
|
||||
return new Cdm(
|
||||
device.security_level,
|
||||
device.group_certificate,
|
||||
device.encryption_key,
|
||||
device.signing_key
|
||||
);
|
||||
}
|
||||
static fromDevice(device: Device): Cdm {
|
||||
return new Cdm(
|
||||
device.security_level,
|
||||
device.group_certificate,
|
||||
device.encryption_key,
|
||||
device.signing_key
|
||||
);
|
||||
}
|
||||
|
||||
private getKeyData(): Buffer {
|
||||
const messagePoint = this.xml_key.getPoint(this.elgamal.curve);
|
||||
const [point1, point2] = this.elgamal.encrypt(
|
||||
messagePoint,
|
||||
private getKeyData(): Buffer {
|
||||
const messagePoint = this.xml_key.getPoint(this.elgamal.curve);
|
||||
const [point1, point2] = this.elgamal.encrypt(
|
||||
messagePoint,
|
||||
this.wmrm_key.getPublic() as Point
|
||||
);
|
||||
);
|
||||
|
||||
const bufferArray = Buffer.concat([
|
||||
ElGamal.toBytes(point1.getX()),
|
||||
ElGamal.toBytes(point1.getY()),
|
||||
ElGamal.toBytes(point2.getX()),
|
||||
ElGamal.toBytes(point2.getY()),
|
||||
]);
|
||||
const bufferArray = Buffer.concat([
|
||||
ElGamal.toBytes(point1.getX()),
|
||||
ElGamal.toBytes(point1.getY()),
|
||||
ElGamal.toBytes(point2.getX()),
|
||||
ElGamal.toBytes(point2.getY()),
|
||||
]);
|
||||
|
||||
return bufferArray;
|
||||
}
|
||||
return bufferArray;
|
||||
}
|
||||
|
||||
private getCipherData(): Buffer {
|
||||
const b64_chain = this.certificate_chain.dumps().toString('base64');
|
||||
const body = `<Data><CertificateChains><CertificateChain>${b64_chain}</CertificateChain></CertificateChains><Features><Feature Name="AESCBC"></Feature></Features></Data>`;
|
||||
private getCipherData(): Buffer {
|
||||
const b64_chain = this.certificate_chain.dumps().toString('base64');
|
||||
const body = `<Data><CertificateChains><CertificateChain>${b64_chain}</CertificateChain></CertificateChains><Features><Feature Name="AESCBC"></Feature></Features></Data>`;
|
||||
|
||||
const cipher = crypto.createCipheriv(
|
||||
'aes-128-cbc',
|
||||
this.xml_key.aesKey,
|
||||
this.xml_key.aesIv
|
||||
);
|
||||
const cipher = crypto.createCipheriv(
|
||||
'aes-128-cbc',
|
||||
this.xml_key.aesKey,
|
||||
this.xml_key.aesIv
|
||||
);
|
||||
|
||||
const ciphertext = Buffer.concat([
|
||||
cipher.update(Buffer.from(body, 'utf-8')),
|
||||
cipher.final(),
|
||||
]);
|
||||
const ciphertext = Buffer.concat([
|
||||
cipher.update(Buffer.from(body, 'utf-8')),
|
||||
cipher.final(),
|
||||
]);
|
||||
|
||||
return Buffer.concat([this.xml_key.aesIv, ciphertext]);
|
||||
}
|
||||
return Buffer.concat([this.xml_key.aesIv, ciphertext]);
|
||||
}
|
||||
|
||||
private buildDigestContent(
|
||||
content_header: string,
|
||||
nonce: string,
|
||||
wmrm_cipher: string,
|
||||
cert_cipher: string
|
||||
): string {
|
||||
const clientTime = Math.floor(Date.now() / 1000);
|
||||
private buildDigestContent(
|
||||
content_header: string,
|
||||
nonce: string,
|
||||
wmrm_cipher: string,
|
||||
cert_cipher: string
|
||||
): string {
|
||||
const clientTime = Math.floor(Date.now() / 1000);
|
||||
|
||||
return (
|
||||
'<LA xmlns="http://schemas.microsoft.com/DRM/2007/03/protocols" Id="SignedData" xml:space="preserve">' +
|
||||
return (
|
||||
'<LA xmlns="http://schemas.microsoft.com/DRM/2007/03/protocols" Id="SignedData" xml:space="preserve">' +
|
||||
'<Version>4</Version>' +
|
||||
`<ContentHeader>${content_header}</ContentHeader>` +
|
||||
'<CLIENTINFO>' +
|
||||
|
|
@ -130,12 +130,12 @@ export default class Cdm {
|
|||
'</CipherData>' +
|
||||
'</EncryptedData>' +
|
||||
'</LA>'
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static buildSignedInfo(digest_value: string): string {
|
||||
return (
|
||||
'<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">' +
|
||||
private static buildSignedInfo(digest_value: string): string {
|
||||
return (
|
||||
'<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">' +
|
||||
'<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></CanonicalizationMethod>' +
|
||||
'<SignatureMethod Algorithm="http://schemas.microsoft.com/DRM/2007/03/protocols#ecdsa-sha256"></SignatureMethod>' +
|
||||
'<Reference URI="#SignedData">' +
|
||||
|
|
@ -143,43 +143,43 @@ export default class Cdm {
|
|||
`<DigestValue>${digest_value}</DigestValue>` +
|
||||
'</Reference>' +
|
||||
'</SignedInfo>'
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getLicenseChallenge(content_header: string): string {
|
||||
const nonce = randomBytes(16).toString('base64');
|
||||
const wmrm_cipher = this.getKeyData().toString('base64');
|
||||
const cert_cipher = this.getCipherData().toString('base64');
|
||||
getLicenseChallenge(content_header: string): string {
|
||||
const nonce = randomBytes(16).toString('base64');
|
||||
const wmrm_cipher = this.getKeyData().toString('base64');
|
||||
const cert_cipher = this.getCipherData().toString('base64');
|
||||
|
||||
const la_content = this.buildDigestContent(
|
||||
content_header,
|
||||
nonce,
|
||||
wmrm_cipher,
|
||||
cert_cipher
|
||||
);
|
||||
const la_content = this.buildDigestContent(
|
||||
content_header,
|
||||
nonce,
|
||||
wmrm_cipher,
|
||||
cert_cipher
|
||||
);
|
||||
|
||||
const la_hash = createHash('sha256').update(la_content, 'utf-8').digest();
|
||||
const la_hash = createHash('sha256').update(la_content, 'utf-8').digest();
|
||||
|
||||
const signed_info = Cdm.buildSignedInfo(la_hash.toString('base64'));
|
||||
const signed_info_digest = createHash('sha256')
|
||||
.update(signed_info, 'utf-8')
|
||||
.digest();
|
||||
const signed_info = Cdm.buildSignedInfo(la_hash.toString('base64'));
|
||||
const signed_info_digest = createHash('sha256')
|
||||
.update(signed_info, 'utf-8')
|
||||
.digest();
|
||||
|
||||
const signatureObj = this.signing_key.keyPair.sign(signed_info_digest);
|
||||
const signatureObj = this.signing_key.keyPair.sign(signed_info_digest);
|
||||
|
||||
const r = signatureObj.r.toArrayLike(Buffer, 'be', 32);
|
||||
const s = signatureObj.s.toArrayLike(Buffer, 'be', 32);
|
||||
const r = signatureObj.r.toArrayLike(Buffer, 'be', 32);
|
||||
const s = signatureObj.s.toArrayLike(Buffer, 'be', 32);
|
||||
|
||||
const rawSignature = Buffer.concat([r, s]);
|
||||
const signatureValue = rawSignature.toString('base64');
|
||||
const rawSignature = Buffer.concat([r, s]);
|
||||
const signatureValue = rawSignature.toString('base64');
|
||||
|
||||
const publicKeyBytes = this.signing_key.keyPair
|
||||
.getPublic()
|
||||
.encode('array', false);
|
||||
const publicKeyBuffer = Buffer.from(publicKeyBytes);
|
||||
const publicKeyBase64 = publicKeyBuffer.toString('base64');
|
||||
const publicKeyBytes = this.signing_key.keyPair
|
||||
.getPublic()
|
||||
.encode('array', false);
|
||||
const publicKeyBuffer = Buffer.from(publicKeyBytes);
|
||||
const publicKeyBase64 = publicKeyBuffer.toString('base64');
|
||||
|
||||
const main_body =
|
||||
const main_body =
|
||||
'<?xml version="1.0" encoding="utf-8"?>' +
|
||||
'<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' +
|
||||
'xmlns:xsd="http://www.w3.org/2001/XMLSchema" ' +
|
||||
|
|
@ -206,73 +206,73 @@ export default class Cdm {
|
|||
'</soap:Body>' +
|
||||
'</soap:Envelope>';
|
||||
|
||||
return main_body;
|
||||
}
|
||||
return main_body;
|
||||
}
|
||||
|
||||
private decryptEcc256Key(encrypted_key: Buffer): Buffer {
|
||||
const point1 = this.curve.curve.point(
|
||||
encrypted_key.subarray(0, 32).toString('hex'),
|
||||
encrypted_key.subarray(32, 64).toString('hex')
|
||||
);
|
||||
const point2 = this.curve.curve.point(
|
||||
encrypted_key.subarray(64, 96).toString('hex'),
|
||||
encrypted_key.subarray(96, 128).toString('hex')
|
||||
);
|
||||
private decryptEcc256Key(encrypted_key: Buffer): Buffer {
|
||||
const point1 = this.curve.curve.point(
|
||||
encrypted_key.subarray(0, 32).toString('hex'),
|
||||
encrypted_key.subarray(32, 64).toString('hex')
|
||||
);
|
||||
const point2 = this.curve.curve.point(
|
||||
encrypted_key.subarray(64, 96).toString('hex'),
|
||||
encrypted_key.subarray(96, 128).toString('hex')
|
||||
);
|
||||
|
||||
const decrypted = ElGamal.decrypt(
|
||||
[point1, point2],
|
||||
this.encryption_key.keyPair.getPrivate()
|
||||
);
|
||||
const decryptedBytes = decrypted.getX().toArray('be', 32).slice(16, 32);
|
||||
const decrypted = ElGamal.decrypt(
|
||||
[point1, point2],
|
||||
this.encryption_key.keyPair.getPrivate()
|
||||
);
|
||||
const decryptedBytes = decrypted.getX().toArray('be', 32).slice(16, 32);
|
||||
|
||||
return Buffer.from(decryptedBytes);
|
||||
}
|
||||
return Buffer.from(decryptedBytes);
|
||||
}
|
||||
|
||||
parseLicense(license: string | Buffer): {
|
||||
parseLicense(license: string | Buffer): {
|
||||
key_id: string;
|
||||
key_type: number;
|
||||
cipher_type: number;
|
||||
key_length: number;
|
||||
key: string;
|
||||
}[] {
|
||||
try {
|
||||
const parser = new XMLParser({
|
||||
removeNSPrefix: true,
|
||||
});
|
||||
const result = parser.parse(license);
|
||||
try {
|
||||
const parser = new XMLParser({
|
||||
removeNSPrefix: true,
|
||||
});
|
||||
const result = parser.parse(license);
|
||||
|
||||
let licenses =
|
||||
let licenses =
|
||||
result['Envelope']['Body']['AcquireLicenseResponse'][
|
||||
'AcquireLicenseResult'
|
||||
'AcquireLicenseResult'
|
||||
]['Response']['LicenseResponse']['Licenses']['License'];
|
||||
|
||||
if (!Array.isArray(licenses)) {
|
||||
licenses = [licenses];
|
||||
}
|
||||
if (!Array.isArray(licenses)) {
|
||||
licenses = [licenses];
|
||||
}
|
||||
|
||||
const keys = [];
|
||||
const keys = [];
|
||||
|
||||
for (const licenseElement of licenses) {
|
||||
const keyMaterial = XmrUtil.parse(Buffer.from(licenseElement, 'base64'))
|
||||
.license.license.keyMaterial;
|
||||
for (const licenseElement of licenses) {
|
||||
const keyMaterial = XmrUtil.parse(Buffer.from(licenseElement, 'base64'))
|
||||
.license.license.keyMaterial;
|
||||
|
||||
if (!keyMaterial || !keyMaterial.contentKey)
|
||||
throw new Error('No Content Keys retrieved');
|
||||
if (!keyMaterial || !keyMaterial.contentKey)
|
||||
throw new Error('No Content Keys retrieved');
|
||||
|
||||
keys.push(
|
||||
new Key(
|
||||
keyMaterial.contentKey.kid,
|
||||
keyMaterial.contentKey.keyType,
|
||||
keyMaterial.contentKey.ciphertype,
|
||||
keyMaterial.contentKey.length,
|
||||
this.decryptEcc256Key(keyMaterial.contentKey.value)
|
||||
)
|
||||
);
|
||||
}
|
||||
keys.push(
|
||||
new Key(
|
||||
keyMaterial.contentKey.kid,
|
||||
keyMaterial.contentKey.keyType,
|
||||
keyMaterial.contentKey.ciphertype,
|
||||
keyMaterial.contentKey.length,
|
||||
this.decryptEcc256Key(keyMaterial.contentKey.value)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return keys;
|
||||
} catch (error) {
|
||||
throw new Error(`Unable to parse license, ${error}`);
|
||||
return keys;
|
||||
} catch (error) {
|
||||
throw new Error(`Unable to parse license, ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,89 +13,89 @@ type RawDeviceV2 = {
|
|||
};
|
||||
|
||||
class DeviceStructs {
|
||||
static magic = 'PRD';
|
||||
static magic = 'PRD';
|
||||
|
||||
static v1 = new Parser()
|
||||
.string('signature', { length: 3, assert: DeviceStructs.magic })
|
||||
.uint8('version')
|
||||
.uint32('group_key_length')
|
||||
.buffer('group_key', { length: 'group_key_length' })
|
||||
.uint32('group_certificate_length')
|
||||
.buffer('group_certificate', { length: 'group_certificate_length' });
|
||||
static v1 = new Parser()
|
||||
.string('signature', { length: 3, assert: DeviceStructs.magic })
|
||||
.uint8('version')
|
||||
.uint32('group_key_length')
|
||||
.buffer('group_key', { length: 'group_key_length' })
|
||||
.uint32('group_certificate_length')
|
||||
.buffer('group_certificate', { length: 'group_certificate_length' });
|
||||
|
||||
static v2 = new Parser()
|
||||
.string('signature', { length: 3, assert: DeviceStructs.magic })
|
||||
.uint8('version')
|
||||
.uint32('group_certificate_length')
|
||||
.buffer('group_certificate', { length: 'group_certificate_length' })
|
||||
.buffer('encryption_key', { length: 96 })
|
||||
.buffer('signing_key', { length: 96 });
|
||||
static v2 = new Parser()
|
||||
.string('signature', { length: 3, assert: DeviceStructs.magic })
|
||||
.uint8('version')
|
||||
.uint32('group_certificate_length')
|
||||
.buffer('group_certificate', { length: 'group_certificate_length' })
|
||||
.buffer('encryption_key', { length: 96 })
|
||||
.buffer('signing_key', { length: 96 });
|
||||
|
||||
static v3 = new Parser()
|
||||
.string('signature', { length: 3, assert: DeviceStructs.magic })
|
||||
.uint8('version')
|
||||
.buffer('group_key', { length: 96 })
|
||||
.buffer('encryption_key', { length: 96 })
|
||||
.buffer('signing_key', { length: 96 })
|
||||
.uint32('group_certificate_length')
|
||||
.buffer('group_certificate', { length: 'group_certificate_length' });
|
||||
static v3 = new Parser()
|
||||
.string('signature', { length: 3, assert: DeviceStructs.magic })
|
||||
.uint8('version')
|
||||
.buffer('group_key', { length: 96 })
|
||||
.buffer('encryption_key', { length: 96 })
|
||||
.buffer('signing_key', { length: 96 })
|
||||
.uint32('group_certificate_length')
|
||||
.buffer('group_certificate', { length: 'group_certificate_length' });
|
||||
}
|
||||
|
||||
export class Device {
|
||||
static CURRENT_STRUCT = DeviceStructs.v3;
|
||||
static CURRENT_STRUCT = DeviceStructs.v3;
|
||||
|
||||
group_certificate: CertificateChain;
|
||||
encryption_key: ECCKey;
|
||||
signing_key: ECCKey;
|
||||
security_level: number;
|
||||
group_certificate: CertificateChain;
|
||||
encryption_key: ECCKey;
|
||||
signing_key: ECCKey;
|
||||
security_level: number;
|
||||
|
||||
constructor(parsedData: RawDeviceV2) {
|
||||
this.group_certificate = CertificateChain.loads(
|
||||
parsedData.group_certificate
|
||||
);
|
||||
this.encryption_key = ECCKey.loads(parsedData.encryption_key);
|
||||
this.signing_key = ECCKey.loads(parsedData.signing_key);
|
||||
this.security_level = this.group_certificate.get_security_level();
|
||||
}
|
||||
constructor(parsedData: RawDeviceV2) {
|
||||
this.group_certificate = CertificateChain.loads(
|
||||
parsedData.group_certificate
|
||||
);
|
||||
this.encryption_key = ECCKey.loads(parsedData.encryption_key);
|
||||
this.signing_key = ECCKey.loads(parsedData.signing_key);
|
||||
this.security_level = this.group_certificate.get_security_level();
|
||||
}
|
||||
|
||||
static loads(data: Buffer): Device {
|
||||
const parsedData = Device.CURRENT_STRUCT.parse(data);
|
||||
return new Device(parsedData);
|
||||
}
|
||||
static loads(data: Buffer): Device {
|
||||
const parsedData = Device.CURRENT_STRUCT.parse(data);
|
||||
return new Device(parsedData);
|
||||
}
|
||||
|
||||
static load(filePath: string): Device {
|
||||
const data = fs.readFileSync(filePath);
|
||||
return Device.loads(data);
|
||||
}
|
||||
static load(filePath: string): Device {
|
||||
const data = fs.readFileSync(filePath);
|
||||
return Device.loads(data);
|
||||
}
|
||||
|
||||
dumps(): Buffer {
|
||||
const groupCertBytes = this.group_certificate.dumps();
|
||||
const encryptionKeyBytes = this.encryption_key.dumps();
|
||||
const signingKeyBytes = this.signing_key.dumps();
|
||||
dumps(): Buffer {
|
||||
const groupCertBytes = this.group_certificate.dumps();
|
||||
const encryptionKeyBytes = this.encryption_key.dumps();
|
||||
const signingKeyBytes = this.signing_key.dumps();
|
||||
|
||||
const buildData = {
|
||||
signature: DeviceStructs.magic,
|
||||
version: 2,
|
||||
group_certificate_length: groupCertBytes.length,
|
||||
group_certificate: groupCertBytes,
|
||||
encryption_key: encryptionKeyBytes,
|
||||
signing_key: signingKeyBytes,
|
||||
};
|
||||
const buildData = {
|
||||
signature: DeviceStructs.magic,
|
||||
version: 2,
|
||||
group_certificate_length: groupCertBytes.length,
|
||||
group_certificate: groupCertBytes,
|
||||
encryption_key: encryptionKeyBytes,
|
||||
signing_key: signingKeyBytes,
|
||||
};
|
||||
|
||||
return Device.CURRENT_STRUCT.encode(buildData);
|
||||
}
|
||||
return Device.CURRENT_STRUCT.encode(buildData);
|
||||
}
|
||||
|
||||
dump(filePath: string): void {
|
||||
const data = this.dumps();
|
||||
fs.writeFileSync(filePath, data);
|
||||
}
|
||||
dump(filePath: string): void {
|
||||
const data = this.dumps();
|
||||
fs.writeFileSync(filePath, data);
|
||||
}
|
||||
|
||||
get_name(): string {
|
||||
const name = `${this.group_certificate.get_name()}_sl${
|
||||
this.security_level
|
||||
}`;
|
||||
return name.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase();
|
||||
}
|
||||
get_name(): string {
|
||||
const name = `${this.group_certificate.get_name()}_sl${
|
||||
this.security_level
|
||||
}`;
|
||||
return name.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
// Device V2 disabled because unstable provisioning
|
||||
|
|
|
|||
|
|
@ -3,91 +3,91 @@ import { createHash } from 'crypto';
|
|||
import * as fs from 'fs';
|
||||
|
||||
export default class ECCKey {
|
||||
keyPair: elliptic.ec.KeyPair;
|
||||
keyPair: elliptic.ec.KeyPair;
|
||||
|
||||
constructor(keyPair: elliptic.ec.KeyPair) {
|
||||
this.keyPair = keyPair;
|
||||
}
|
||||
|
||||
static generate(): ECCKey {
|
||||
const EC = new elliptic.ec('p256');
|
||||
const keyPair = EC.genKeyPair();
|
||||
return new ECCKey(keyPair);
|
||||
}
|
||||
|
||||
static construct(privateKey: Buffer | string | number): ECCKey {
|
||||
if (Buffer.isBuffer(privateKey)) {
|
||||
privateKey = privateKey.toString('hex');
|
||||
} else if (typeof privateKey === 'number') {
|
||||
privateKey = privateKey.toString(16);
|
||||
constructor(keyPair: elliptic.ec.KeyPair) {
|
||||
this.keyPair = keyPair;
|
||||
}
|
||||
|
||||
const EC = new elliptic.ec('p256');
|
||||
const keyPair = EC.keyFromPrivate(privateKey, 'hex');
|
||||
|
||||
return new ECCKey(keyPair);
|
||||
}
|
||||
|
||||
static loads(data: string | Buffer): ECCKey {
|
||||
if (typeof data === 'string') {
|
||||
data = Buffer.from(data, 'base64');
|
||||
}
|
||||
if (!Buffer.isBuffer(data)) {
|
||||
throw new Error(`Expecting Bytes or Base64 input, got ${data}`);
|
||||
static generate(): ECCKey {
|
||||
const EC = new elliptic.ec('p256');
|
||||
const keyPair = EC.genKeyPair();
|
||||
return new ECCKey(keyPair);
|
||||
}
|
||||
|
||||
if (data.length !== 96 && data.length !== 32) {
|
||||
throw new Error(
|
||||
`Invalid data length. Expecting 96 or 32 bytes, got ${data.length}`
|
||||
);
|
||||
static construct(privateKey: Buffer | string | number): ECCKey {
|
||||
if (Buffer.isBuffer(privateKey)) {
|
||||
privateKey = privateKey.toString('hex');
|
||||
} else if (typeof privateKey === 'number') {
|
||||
privateKey = privateKey.toString(16);
|
||||
}
|
||||
|
||||
const EC = new elliptic.ec('p256');
|
||||
const keyPair = EC.keyFromPrivate(privateKey, 'hex');
|
||||
|
||||
return new ECCKey(keyPair);
|
||||
}
|
||||
|
||||
const privateKey = data.subarray(0, 32);
|
||||
return ECCKey.construct(privateKey);
|
||||
}
|
||||
static loads(data: string | Buffer): ECCKey {
|
||||
if (typeof data === 'string') {
|
||||
data = Buffer.from(data, 'base64');
|
||||
}
|
||||
if (!Buffer.isBuffer(data)) {
|
||||
throw new Error(`Expecting Bytes or Base64 input, got ${data}`);
|
||||
}
|
||||
|
||||
static load(filePath: string): ECCKey {
|
||||
const data = fs.readFileSync(filePath);
|
||||
return ECCKey.loads(data);
|
||||
}
|
||||
if (data.length !== 96 && data.length !== 32) {
|
||||
throw new Error(
|
||||
`Invalid data length. Expecting 96 or 32 bytes, got ${data.length}`
|
||||
);
|
||||
}
|
||||
|
||||
dumps(): Buffer {
|
||||
return Buffer.concat([this.privateBytes(), this.publicBytes()]);
|
||||
}
|
||||
const privateKey = data.subarray(0, 32);
|
||||
return ECCKey.construct(privateKey);
|
||||
}
|
||||
|
||||
dump(filePath: string): void {
|
||||
fs.writeFileSync(filePath, this.dumps());
|
||||
}
|
||||
static load(filePath: string): ECCKey {
|
||||
const data = fs.readFileSync(filePath);
|
||||
return ECCKey.loads(data);
|
||||
}
|
||||
|
||||
getPoint(): { x: string; y: string } {
|
||||
const publicKey = this.keyPair.getPublic();
|
||||
return {
|
||||
x: publicKey.getX().toString('hex'),
|
||||
y: publicKey.getY().toString('hex'),
|
||||
};
|
||||
}
|
||||
dumps(): Buffer {
|
||||
return Buffer.concat([this.privateBytes(), this.publicBytes()]);
|
||||
}
|
||||
|
||||
privateBytes(): Buffer {
|
||||
const privateKey = this.keyPair.getPrivate();
|
||||
return Buffer.from(privateKey.toArray('be', 32));
|
||||
}
|
||||
dump(filePath: string): void {
|
||||
fs.writeFileSync(filePath, this.dumps());
|
||||
}
|
||||
|
||||
privateSha256Digest(): Buffer {
|
||||
const hash = createHash('sha256');
|
||||
hash.update(this.privateBytes());
|
||||
return hash.digest();
|
||||
}
|
||||
getPoint(): { x: string; y: string } {
|
||||
const publicKey = this.keyPair.getPublic();
|
||||
return {
|
||||
x: publicKey.getX().toString('hex'),
|
||||
y: publicKey.getY().toString('hex'),
|
||||
};
|
||||
}
|
||||
|
||||
publicBytes(): Buffer {
|
||||
const publicKey = this.keyPair.getPublic();
|
||||
const x = publicKey.getX().toArray('be', 32);
|
||||
const y = publicKey.getY().toArray('be', 32);
|
||||
return Buffer.concat([Buffer.from(x), Buffer.from(y)]);
|
||||
}
|
||||
privateBytes(): Buffer {
|
||||
const privateKey = this.keyPair.getPrivate();
|
||||
return Buffer.from(privateKey.toArray('be', 32));
|
||||
}
|
||||
|
||||
publicSha256Digest(): Buffer {
|
||||
const hash = createHash('sha256');
|
||||
hash.update(this.publicBytes());
|
||||
return hash.digest();
|
||||
}
|
||||
privateSha256Digest(): Buffer {
|
||||
const hash = createHash('sha256');
|
||||
hash.update(this.privateBytes());
|
||||
return hash.digest();
|
||||
}
|
||||
|
||||
publicBytes(): Buffer {
|
||||
const publicKey = this.keyPair.getPublic();
|
||||
const x = publicKey.getX().toArray('be', 32);
|
||||
const y = publicKey.getY().toArray('be', 32);
|
||||
return Buffer.concat([Buffer.from(x), Buffer.from(y)]);
|
||||
}
|
||||
|
||||
publicSha256Digest(): Buffer {
|
||||
const hash = createHash('sha256');
|
||||
hash.update(this.publicBytes());
|
||||
return hash.digest();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,35 +11,35 @@ export interface Point {
|
|||
}
|
||||
|
||||
export default class ElGamal {
|
||||
curve: EC;
|
||||
curve: EC;
|
||||
|
||||
constructor(curve: EC) {
|
||||
this.curve = curve;
|
||||
}
|
||||
|
||||
static toBytes(n: BN): Uint8Array {
|
||||
const byteArray = n.toString(16).padStart(2, '0');
|
||||
if (byteArray.length % 2 !== 0) {
|
||||
return Uint8Array.from(Buffer.from('0' + byteArray, 'hex'));
|
||||
constructor(curve: EC) {
|
||||
this.curve = curve;
|
||||
}
|
||||
return Uint8Array.from(Buffer.from(byteArray, 'hex'));
|
||||
}
|
||||
|
||||
encrypt(messagePoint: Point, publicKey: Point): [Point, Point] {
|
||||
const ephemeralKey = new BN(randomBytes(32).toString('hex'), 16).mod(
|
||||
static toBytes(n: BN): Uint8Array {
|
||||
const byteArray = n.toString(16).padStart(2, '0');
|
||||
if (byteArray.length % 2 !== 0) {
|
||||
return Uint8Array.from(Buffer.from('0' + byteArray, 'hex'));
|
||||
}
|
||||
return Uint8Array.from(Buffer.from(byteArray, 'hex'));
|
||||
}
|
||||
|
||||
encrypt(messagePoint: Point, publicKey: Point): [Point, Point] {
|
||||
const ephemeralKey = new BN(randomBytes(32).toString('hex'), 16).mod(
|
||||
this.curve.n!
|
||||
);
|
||||
const ephemeralKeyBigInt = BigInt(ephemeralKey.toString(10));
|
||||
const point1 = this.curve.g.mul(ephemeralKeyBigInt);
|
||||
const point2 = messagePoint.add(publicKey.mul(ephemeralKeyBigInt));
|
||||
);
|
||||
const ephemeralKeyBigInt = BigInt(ephemeralKey.toString(10));
|
||||
const point1 = this.curve.g.mul(ephemeralKeyBigInt);
|
||||
const point2 = messagePoint.add(publicKey.mul(ephemeralKeyBigInt));
|
||||
|
||||
return [point1, point2];
|
||||
}
|
||||
return [point1, point2];
|
||||
}
|
||||
|
||||
static decrypt(encrypted: [Point, Point], privateKey: BN): Point {
|
||||
const [point1, point2] = encrypted;
|
||||
const sharedSecret = point1.mul(privateKey);
|
||||
const decryptedMessage = point2.add(sharedSecret.neg());
|
||||
return decryptedMessage;
|
||||
}
|
||||
static decrypt(encrypted: [Point, Point], privateKey: BN): Point {
|
||||
const [point1, point2] = encrypted;
|
||||
const sharedSecret = point1.mul(privateKey);
|
||||
const decryptedMessage = point2.add(sharedSecret.neg());
|
||||
return decryptedMessage;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,17 +9,17 @@ enum KeyType {
|
|||
}
|
||||
|
||||
function getKeyType(value: number): KeyType {
|
||||
switch (value) {
|
||||
case KeyType.Invalid:
|
||||
case KeyType.AES128CTR:
|
||||
case KeyType.RC4:
|
||||
case KeyType.AES128ECB:
|
||||
case KeyType.Cocktail:
|
||||
case KeyType.AESCBC:
|
||||
return value;
|
||||
default:
|
||||
return KeyType.UNKNOWN;
|
||||
}
|
||||
switch (value) {
|
||||
case KeyType.Invalid:
|
||||
case KeyType.AES128CTR:
|
||||
case KeyType.RC4:
|
||||
case KeyType.AES128ECB:
|
||||
case KeyType.Cocktail:
|
||||
case KeyType.AESCBC:
|
||||
return value;
|
||||
default:
|
||||
return KeyType.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
enum CipherType {
|
||||
|
|
@ -33,37 +33,37 @@ enum CipherType {
|
|||
}
|
||||
|
||||
function getCipherType(value: number): CipherType {
|
||||
switch (value) {
|
||||
case CipherType.Invalid:
|
||||
case CipherType.RSA128:
|
||||
case CipherType.ChainedLicense:
|
||||
case CipherType.ECC256:
|
||||
case CipherType.ECCforScalableLicenses:
|
||||
case CipherType.Scalable:
|
||||
return value;
|
||||
default:
|
||||
return CipherType.UNKNOWN;
|
||||
}
|
||||
switch (value) {
|
||||
case CipherType.Invalid:
|
||||
case CipherType.RSA128:
|
||||
case CipherType.ChainedLicense:
|
||||
case CipherType.ECC256:
|
||||
case CipherType.ECCforScalableLicenses:
|
||||
case CipherType.Scalable:
|
||||
return value;
|
||||
default:
|
||||
return CipherType.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
export class Key {
|
||||
key_id: string;
|
||||
key_type: KeyType;
|
||||
cipher_type: CipherType;
|
||||
key_length: number;
|
||||
key: string;
|
||||
key_id: string;
|
||||
key_type: KeyType;
|
||||
cipher_type: CipherType;
|
||||
key_length: number;
|
||||
key: string;
|
||||
|
||||
constructor(
|
||||
key_id: string,
|
||||
key_type: number,
|
||||
cipher_type: number,
|
||||
key_length: number,
|
||||
key: Buffer
|
||||
) {
|
||||
this.key_id = key_id;
|
||||
this.key_type = getKeyType(key_type);
|
||||
this.cipher_type = getCipherType(cipher_type);
|
||||
this.key_length = key_length;
|
||||
this.key = key.toString('hex');
|
||||
}
|
||||
constructor(
|
||||
key_id: string,
|
||||
key_type: number,
|
||||
cipher_type: number,
|
||||
key_length: number,
|
||||
key: Buffer
|
||||
) {
|
||||
this.key_id = key_id;
|
||||
this.key_type = getKeyType(key_type);
|
||||
this.cipher_type = getCipherType(cipher_type);
|
||||
this.key_length = key_length;
|
||||
this.key = key.toString('hex');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,125 +5,125 @@ import WRMHeader from './wrmheader';
|
|||
const SYSTEM_ID = Buffer.from('9a04f07998404286ab92e65be0885f95', 'hex');
|
||||
|
||||
const PSSHBox = new Parser()
|
||||
.uint32('length')
|
||||
.string('pssh', { length: 4, assert: 'pssh' })
|
||||
.uint32('fullbox')
|
||||
.buffer('system_id', { length: 16 })
|
||||
.uint32('data_length')
|
||||
.buffer('data', {
|
||||
length: 'data_length',
|
||||
});
|
||||
.uint32('length')
|
||||
.string('pssh', { length: 4, assert: 'pssh' })
|
||||
.uint32('fullbox')
|
||||
.buffer('system_id', { length: 16 })
|
||||
.uint32('data_length')
|
||||
.buffer('data', {
|
||||
length: 'data_length',
|
||||
});
|
||||
|
||||
const PlayreadyObject = new Parser()
|
||||
.useContextVars()
|
||||
.uint16('type')
|
||||
.uint16('length')
|
||||
.choice('data', {
|
||||
tag: 'type',
|
||||
choices: {
|
||||
1: new Parser().string('data', {
|
||||
length: function () {
|
||||
return (this as any).$parent.length;
|
||||
.useContextVars()
|
||||
.uint16('type')
|
||||
.uint16('length')
|
||||
.choice('data', {
|
||||
tag: 'type',
|
||||
choices: {
|
||||
1: new Parser().string('data', {
|
||||
length: function () {
|
||||
return (this as any).$parent.length;
|
||||
},
|
||||
encoding: 'utf16le',
|
||||
}),
|
||||
},
|
||||
encoding: 'utf16le',
|
||||
}),
|
||||
},
|
||||
defaultChoice: new Parser().buffer('data', {
|
||||
length: function () {
|
||||
return (this as any).$parent.length;
|
||||
},
|
||||
}),
|
||||
});
|
||||
defaultChoice: new Parser().buffer('data', {
|
||||
length: function () {
|
||||
return (this as any).$parent.length;
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const PlayreadyHeader = new Parser()
|
||||
.uint32('length')
|
||||
.uint16('record_count')
|
||||
.array('records', {
|
||||
length: 'record_count',
|
||||
type: PlayreadyObject,
|
||||
});
|
||||
.uint32('length')
|
||||
.uint16('record_count')
|
||||
.array('records', {
|
||||
length: 'record_count',
|
||||
type: PlayreadyObject,
|
||||
});
|
||||
|
||||
function isPlayreadyPsshBox(data: Buffer): boolean {
|
||||
if (data.length < 28) return false;
|
||||
return data.subarray(12, 28).equals(SYSTEM_ID);
|
||||
if (data.length < 28) return false;
|
||||
return data.subarray(12, 28).equals(SYSTEM_ID);
|
||||
}
|
||||
|
||||
function isUtf16(data: Buffer): boolean {
|
||||
for (let i = 1; i < data.length; i += 2) {
|
||||
if (data[i] !== 0) {
|
||||
return false;
|
||||
for (let i = 1; i < data.length; i += 2) {
|
||||
if (data[i] !== 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return true;
|
||||
}
|
||||
|
||||
function* getWrmHeaders(wrm_header: any): IterableIterator<string> {
|
||||
for (const record of wrm_header.records) {
|
||||
if (record.type === 1 && typeof record.data === 'string') {
|
||||
yield record.data;
|
||||
for (const record of wrm_header.records) {
|
||||
if (record.type === 1 && typeof record.data === 'string') {
|
||||
yield record.data;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class PSSH {
|
||||
public wrm_headers: string[];
|
||||
public wrm_headers: string[];
|
||||
|
||||
constructor(data: string | Buffer) {
|
||||
if (!data) {
|
||||
throw new Error('Data must not be empty');
|
||||
}
|
||||
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
data = Buffer.from(data, 'base64');
|
||||
} catch (e) {
|
||||
throw new Error(`Could not decode data as Base64: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (isPlayreadyPsshBox(data)) {
|
||||
const pssh_box = PSSHBox.parse(data);
|
||||
const psshData = pssh_box.data;
|
||||
|
||||
if (isUtf16(psshData)) {
|
||||
this.wrm_headers = [psshData.toString('utf16le')];
|
||||
} else if (isUtf16(psshData.subarray(6))) {
|
||||
this.wrm_headers = [psshData.subarray(6).toString('utf16le')];
|
||||
} else if (isUtf16(psshData.subarray(10))) {
|
||||
this.wrm_headers = [psshData.subarray(10).toString('utf16le')];
|
||||
} else {
|
||||
const playready_header = PlayreadyHeader.parse(psshData);
|
||||
this.wrm_headers = Array.from(getWrmHeaders(playready_header));
|
||||
constructor(data: string | Buffer) {
|
||||
if (!data) {
|
||||
throw new Error('Data must not be empty');
|
||||
}
|
||||
} else {
|
||||
if (isUtf16(data)) {
|
||||
this.wrm_headers = [data.toString('utf16le')];
|
||||
} else if (isUtf16(data.subarray(6))) {
|
||||
this.wrm_headers = [data.subarray(6).toString('utf16le')];
|
||||
} else if (isUtf16(data.subarray(10))) {
|
||||
this.wrm_headers = [data.subarray(10).toString('utf16le')];
|
||||
} else {
|
||||
const playready_header = PlayreadyHeader.parse(data);
|
||||
this.wrm_headers = Array.from(getWrmHeaders(playready_header));
|
||||
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
data = Buffer.from(data, 'base64');
|
||||
} catch (e) {
|
||||
throw new Error(`Could not decode data as Base64: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (isPlayreadyPsshBox(data)) {
|
||||
const pssh_box = PSSHBox.parse(data);
|
||||
const psshData = pssh_box.data;
|
||||
|
||||
if (isUtf16(psshData)) {
|
||||
this.wrm_headers = [psshData.toString('utf16le')];
|
||||
} else if (isUtf16(psshData.subarray(6))) {
|
||||
this.wrm_headers = [psshData.subarray(6).toString('utf16le')];
|
||||
} else if (isUtf16(psshData.subarray(10))) {
|
||||
this.wrm_headers = [psshData.subarray(10).toString('utf16le')];
|
||||
} else {
|
||||
const playready_header = PlayreadyHeader.parse(psshData);
|
||||
this.wrm_headers = Array.from(getWrmHeaders(playready_header));
|
||||
}
|
||||
} else {
|
||||
if (isUtf16(data)) {
|
||||
this.wrm_headers = [data.toString('utf16le')];
|
||||
} else if (isUtf16(data.subarray(6))) {
|
||||
this.wrm_headers = [data.subarray(6).toString('utf16le')];
|
||||
} else if (isUtf16(data.subarray(10))) {
|
||||
this.wrm_headers = [data.subarray(10).toString('utf16le')];
|
||||
} else {
|
||||
const playready_header = PlayreadyHeader.parse(data);
|
||||
this.wrm_headers = Array.from(getWrmHeaders(playready_header));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
'Could not parse data as a PSSH Box nor a PlayReadyHeader'
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
'Could not parse data as a PSSH Box nor a PlayReadyHeader'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Header downgrade
|
||||
public get_wrm_headers(downgrade_to_v4: boolean = false): string[] {
|
||||
return this.wrm_headers.map(
|
||||
downgrade_to_v4 ? this.downgradePSSH : (_) => _
|
||||
);
|
||||
}
|
||||
// Header downgrade
|
||||
public get_wrm_headers(downgrade_to_v4: boolean = false): string[] {
|
||||
return this.wrm_headers.map(
|
||||
downgrade_to_v4 ? this.downgradePSSH : (_) => _
|
||||
);
|
||||
}
|
||||
|
||||
private downgradePSSH(wrm_header: string): string {
|
||||
const header = new WRMHeader(wrm_header);
|
||||
return header.to_v4_0_0_0();
|
||||
}
|
||||
private downgradePSSH(wrm_header: string): string {
|
||||
const header = new WRMHeader(wrm_header);
|
||||
return header.to_v4_0_0_0();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { XMLParser } from 'fast-xml-parser';
|
||||
|
||||
export class SignedKeyID {
|
||||
constructor(
|
||||
constructor(
|
||||
public alg_id: string,
|
||||
public value: string,
|
||||
public checksum?: string
|
||||
) {}
|
||||
) {}
|
||||
}
|
||||
|
||||
export type Version = '4.0.0.0' | '4.1.0.0' | '4.2.0.0' | '4.3.0.0' | 'UNKNOWN';
|
||||
|
|
@ -25,88 +25,88 @@ interface ParsedWRMHeader {
|
|||
}
|
||||
|
||||
export default class WRMHeader {
|
||||
private header: ParsedWRMHeader['WRMHEADER'];
|
||||
version: Version;
|
||||
private header: ParsedWRMHeader['WRMHEADER'];
|
||||
version: Version;
|
||||
|
||||
constructor(data: string) {
|
||||
if (!data) throw new Error('Data must not be empty');
|
||||
constructor(data: string) {
|
||||
if (!data) throw new Error('Data must not be empty');
|
||||
|
||||
const parser = new XMLParser({
|
||||
ignoreAttributes: false,
|
||||
removeNSPrefix: true,
|
||||
attributeNamePrefix: '@_',
|
||||
});
|
||||
const parsed = parser.parse(data) as ParsedWRMHeader;
|
||||
const parser = new XMLParser({
|
||||
ignoreAttributes: false,
|
||||
removeNSPrefix: true,
|
||||
attributeNamePrefix: '@_',
|
||||
});
|
||||
const parsed = parser.parse(data) as ParsedWRMHeader;
|
||||
|
||||
if (!parsed.WRMHEADER) throw new Error('Data is not a valid WRMHEADER');
|
||||
if (!parsed.WRMHEADER) throw new Error('Data is not a valid WRMHEADER');
|
||||
|
||||
this.header = parsed.WRMHEADER;
|
||||
this.version = WRMHeader.fromString(this.header['@_version']);
|
||||
}
|
||||
|
||||
private static fromString(value: string): Version {
|
||||
if (['4.0.0.0', '4.1.0.0', '4.2.0.0', '4.3.0.0'].includes(value)) {
|
||||
return value as Version;
|
||||
this.header = parsed.WRMHEADER;
|
||||
this.version = WRMHeader.fromString(this.header['@_version']);
|
||||
}
|
||||
return 'UNKNOWN';
|
||||
}
|
||||
|
||||
to_v4_0_0_0(): string {
|
||||
const [key_ids, la_url, lui_url, ds_id] = this.readAttributes();
|
||||
if (key_ids.length === 0) throw new Error('No Key IDs available');
|
||||
const key_id = key_ids[0];
|
||||
return `<WRMHEADER xmlns="http://schemas.microsoft.com/DRM/2007/03/PlayReadyHeader" version="4.0.0.0"><DATA><PROTECTINFO><KEYLEN>16</KEYLEN><ALGID>AESCTR</ALGID></PROTECTINFO><KID>${
|
||||
key_id.value
|
||||
}</KID>${la_url ? `<LA_URL>${la_url}</LA_URL>` : ''}${
|
||||
lui_url ? `<LUI_URL>${lui_url}</LUI_URL>` : ''
|
||||
}${ds_id ? `<DS_ID>${ds_id}</DS_ID>` : ''}${
|
||||
key_id.checksum ? `<CHECKSUM>${key_id.checksum}</CHECKSUM>` : ''
|
||||
}</DATA></WRMHEADER>`;
|
||||
}
|
||||
|
||||
readAttributes(): ReturnStructure {
|
||||
const data = this.header.DATA;
|
||||
if (!data)
|
||||
throw new Error(
|
||||
'Not a valid PlayReady Header Record, WRMHEADER/DATA required'
|
||||
);
|
||||
switch (this.version) {
|
||||
case '4.0.0.0':
|
||||
return WRMHeader.read_v4(data);
|
||||
case '4.1.0.0':
|
||||
case '4.2.0.0':
|
||||
case '4.3.0.0':
|
||||
return WRMHeader.read_vX(data);
|
||||
default:
|
||||
throw new Error(`Unsupported version: ${this.version}`);
|
||||
private static fromString(value: string): Version {
|
||||
if (['4.0.0.0', '4.1.0.0', '4.2.0.0', '4.3.0.0'].includes(value)) {
|
||||
return value as Version;
|
||||
}
|
||||
return 'UNKNOWN';
|
||||
}
|
||||
}
|
||||
|
||||
private static read_v4(data: any): ReturnStructure {
|
||||
const protectInfo = data.PROTECTINFO;
|
||||
return [
|
||||
[new SignedKeyID(protectInfo.ALGID, data.KID, data.CHECKSUM)],
|
||||
data.LA_URL || null,
|
||||
data.LUI_URL || null,
|
||||
data.DS_ID || null,
|
||||
];
|
||||
}
|
||||
to_v4_0_0_0(): string {
|
||||
const [key_ids, la_url, lui_url, ds_id] = this.readAttributes();
|
||||
if (key_ids.length === 0) throw new Error('No Key IDs available');
|
||||
const key_id = key_ids[0];
|
||||
return `<WRMHEADER xmlns="http://schemas.microsoft.com/DRM/2007/03/PlayReadyHeader" version="4.0.0.0"><DATA><PROTECTINFO><KEYLEN>16</KEYLEN><ALGID>AESCTR</ALGID></PROTECTINFO><KID>${
|
||||
key_id.value
|
||||
}</KID>${la_url ? `<LA_URL>${la_url}</LA_URL>` : ''}${
|
||||
lui_url ? `<LUI_URL>${lui_url}</LUI_URL>` : ''
|
||||
}${ds_id ? `<DS_ID>${ds_id}</DS_ID>` : ''}${
|
||||
key_id.checksum ? `<CHECKSUM>${key_id.checksum}</CHECKSUM>` : ''
|
||||
}</DATA></WRMHEADER>`;
|
||||
}
|
||||
|
||||
private static read_vX(data: any): ReturnStructure {
|
||||
const protectInfo = data.PROTECTINFO;
|
||||
readAttributes(): ReturnStructure {
|
||||
const data = this.header.DATA;
|
||||
if (!data)
|
||||
throw new Error(
|
||||
'Not a valid PlayReady Header Record, WRMHEADER/DATA required'
|
||||
);
|
||||
switch (this.version) {
|
||||
case '4.0.0.0':
|
||||
return WRMHeader.read_v4(data);
|
||||
case '4.1.0.0':
|
||||
case '4.2.0.0':
|
||||
case '4.3.0.0':
|
||||
return WRMHeader.read_vX(data);
|
||||
default:
|
||||
throw new Error(`Unsupported version: ${this.version}`);
|
||||
}
|
||||
}
|
||||
|
||||
const signedKeyID: SignedKeyID | undefined = protectInfo.KIDS.KID
|
||||
? new SignedKeyID(
|
||||
protectInfo.KIDS.KID['@_ALGID'] || '',
|
||||
protectInfo.KIDS.KID['@_VALUE'],
|
||||
protectInfo.KIDS.KID['@_CHECKSUM']
|
||||
)
|
||||
: undefined;
|
||||
return [
|
||||
signedKeyID ? [signedKeyID] : [],
|
||||
data.LA_URL || null,
|
||||
data.LUI_URL || null,
|
||||
data.DS_ID || null,
|
||||
];
|
||||
}
|
||||
private static read_v4(data: any): ReturnStructure {
|
||||
const protectInfo = data.PROTECTINFO;
|
||||
return [
|
||||
[new SignedKeyID(protectInfo.ALGID, data.KID, data.CHECKSUM)],
|
||||
data.LA_URL || null,
|
||||
data.LUI_URL || null,
|
||||
data.DS_ID || null,
|
||||
];
|
||||
}
|
||||
|
||||
private static read_vX(data: any): ReturnStructure {
|
||||
const protectInfo = data.PROTECTINFO;
|
||||
|
||||
const signedKeyID: SignedKeyID | undefined = protectInfo.KIDS.KID
|
||||
? new SignedKeyID(
|
||||
protectInfo.KIDS.KID['@_ALGID'] || '',
|
||||
protectInfo.KIDS.KID['@_VALUE'],
|
||||
protectInfo.KIDS.KID['@_CHECKSUM']
|
||||
)
|
||||
: undefined;
|
||||
return [
|
||||
signedKeyID ? [signedKeyID] : [],
|
||||
data.LA_URL || null,
|
||||
data.LUI_URL || null,
|
||||
data.DS_ID || null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,25 +4,25 @@ import ECCKey from './ecc_key';
|
|||
import ElGamal, { Point } from './elgamal';
|
||||
|
||||
export default class XmlKey {
|
||||
private _sharedPoint: ECCKey;
|
||||
public sharedKeyX: BN;
|
||||
public sharedKeyY: BN;
|
||||
public _shared_key_x_bytes: Uint8Array;
|
||||
public aesIv: Uint8Array;
|
||||
public aesKey: Uint8Array;
|
||||
private _sharedPoint: ECCKey;
|
||||
public sharedKeyX: BN;
|
||||
public sharedKeyY: BN;
|
||||
public _shared_key_x_bytes: Uint8Array;
|
||||
public aesIv: Uint8Array;
|
||||
public aesKey: Uint8Array;
|
||||
|
||||
constructor() {
|
||||
this._sharedPoint = ECCKey.generate();
|
||||
this.sharedKeyX = this._sharedPoint.keyPair.getPublic().getX();
|
||||
this.sharedKeyY = this._sharedPoint.keyPair.getPublic().getY();
|
||||
this._shared_key_x_bytes = ElGamal.toBytes(this.sharedKeyX);
|
||||
this.aesIv = this._shared_key_x_bytes.subarray(0, 16);
|
||||
this.aesKey = this._shared_key_x_bytes.subarray(16, 32);
|
||||
}
|
||||
constructor() {
|
||||
this._sharedPoint = ECCKey.generate();
|
||||
this.sharedKeyX = this._sharedPoint.keyPair.getPublic().getX();
|
||||
this.sharedKeyY = this._sharedPoint.keyPair.getPublic().getY();
|
||||
this._shared_key_x_bytes = ElGamal.toBytes(this.sharedKeyX);
|
||||
this.aesIv = this._shared_key_x_bytes.subarray(0, 16);
|
||||
this.aesKey = this._shared_key_x_bytes.subarray(16, 32);
|
||||
}
|
||||
|
||||
getPoint(curve: EC): Point {
|
||||
return curve.curve.point(this.sharedKeyX, this.sharedKeyY);
|
||||
}
|
||||
getPoint(curve: EC): Point {
|
||||
return curve.curve.point(this.sharedKeyX, this.sharedKeyY);
|
||||
}
|
||||
}
|
||||
|
||||
// Make it more undetectable (not working right now)
|
||||
|
|
|
|||
|
|
@ -44,57 +44,57 @@ type ParsedLicense = {
|
|||
};
|
||||
|
||||
export class XMRLicenseStructsV2 {
|
||||
static CONTENT_KEY = new Parser()
|
||||
.buffer('kid', { length: 16 })
|
||||
.uint16('keytype')
|
||||
.uint16('ciphertype')
|
||||
.uint16('length')
|
||||
.buffer('value', {
|
||||
length: 'length',
|
||||
});
|
||||
static CONTENT_KEY = new Parser()
|
||||
.buffer('kid', { length: 16 })
|
||||
.uint16('keytype')
|
||||
.uint16('ciphertype')
|
||||
.uint16('length')
|
||||
.buffer('value', {
|
||||
length: 'length',
|
||||
});
|
||||
|
||||
static ECC_KEY = new Parser()
|
||||
.uint16('curve')
|
||||
.uint16('length')
|
||||
.buffer('value', {
|
||||
length: 'length',
|
||||
});
|
||||
static ECC_KEY = new Parser()
|
||||
.uint16('curve')
|
||||
.uint16('length')
|
||||
.buffer('value', {
|
||||
length: 'length',
|
||||
});
|
||||
|
||||
static FTLV = new Parser()
|
||||
.uint16('flags')
|
||||
.uint16('type')
|
||||
.uint32('length')
|
||||
.buffer('value', {
|
||||
length: function () {
|
||||
return (this as any).length - 8;
|
||||
},
|
||||
});
|
||||
static FTLV = new Parser()
|
||||
.uint16('flags')
|
||||
.uint16('type')
|
||||
.uint32('length')
|
||||
.buffer('value', {
|
||||
length: function () {
|
||||
return (this as any).length - 8;
|
||||
},
|
||||
});
|
||||
|
||||
static AUXILIARY_LOCATIONS = new Parser()
|
||||
.uint32('location')
|
||||
.buffer('value', { length: 16 });
|
||||
static AUXILIARY_LOCATIONS = new Parser()
|
||||
.uint32('location')
|
||||
.buffer('value', { length: 16 });
|
||||
|
||||
static AUXILIARY_KEY_OBJECT = new Parser()
|
||||
.uint16('count')
|
||||
.array('locations', {
|
||||
length: 'count',
|
||||
type: XMRLicenseStructsV2.AUXILIARY_LOCATIONS,
|
||||
});
|
||||
static AUXILIARY_KEY_OBJECT = new Parser()
|
||||
.uint16('count')
|
||||
.array('locations', {
|
||||
length: 'count',
|
||||
type: XMRLicenseStructsV2.AUXILIARY_LOCATIONS,
|
||||
});
|
||||
|
||||
static SIGNATURE = new Parser()
|
||||
.uint16('type')
|
||||
.uint16('siglength')
|
||||
.buffer('signature', {
|
||||
length: 'siglength',
|
||||
});
|
||||
static SIGNATURE = new Parser()
|
||||
.uint16('type')
|
||||
.uint16('siglength')
|
||||
.buffer('signature', {
|
||||
length: 'siglength',
|
||||
});
|
||||
|
||||
static XMR = new Parser()
|
||||
.string('constant', { length: 4, assert: 'XMR\x00' })
|
||||
.int32('version')
|
||||
.buffer('rightsid', { length: 16 })
|
||||
.nest('data', {
|
||||
type: XMRLicenseStructsV2.FTLV,
|
||||
});
|
||||
static XMR = new Parser()
|
||||
.string('constant', { length: 4, assert: 'XMR\x00' })
|
||||
.int32('version')
|
||||
.buffer('rightsid', { length: 16 })
|
||||
.nest('data', {
|
||||
type: XMRLicenseStructsV2.FTLV,
|
||||
});
|
||||
}
|
||||
|
||||
enum XMRTYPE {
|
||||
|
|
@ -117,135 +117,135 @@ enum XMRTYPE {
|
|||
}
|
||||
|
||||
export class XmrUtil {
|
||||
public data: Buffer;
|
||||
public license: ParsedLicense;
|
||||
public data: Buffer;
|
||||
public license: ParsedLicense;
|
||||
|
||||
constructor(data: Buffer, license: ParsedLicense) {
|
||||
this.data = data;
|
||||
this.license = license;
|
||||
}
|
||||
|
||||
static parse(license: Buffer) {
|
||||
const xmr = XMRLicenseStructsV2.XMR.parse(license);
|
||||
|
||||
const parsed_license: ParsedLicense = {
|
||||
version: xmr.version,
|
||||
rights: Buffer.from(xmr.rightsid).toString('hex'),
|
||||
length: license.length,
|
||||
license: {
|
||||
length: xmr.data.length,
|
||||
},
|
||||
};
|
||||
const container = parsed_license.license;
|
||||
const data = xmr.data;
|
||||
|
||||
let pos = 0;
|
||||
while (pos < data.length - 16) {
|
||||
const value = XMRLicenseStructsV2.FTLV.parse(data.value.slice(pos));
|
||||
|
||||
// XMR_SIGNATURE_OBJECT
|
||||
if (value.type === XMRTYPE.XMR_SIGNATURE_OBJECT) {
|
||||
const signature = XMRLicenseStructsV2.SIGNATURE.parse(value.value);
|
||||
|
||||
container.signature = {
|
||||
length: value.length,
|
||||
type: signature.type,
|
||||
value: Buffer.from(signature.signature).toString('hex'),
|
||||
};
|
||||
}
|
||||
|
||||
// XMRTYPE.XMR_GLOBAL_POLICY_CONTAINER
|
||||
if (value.type === XMRTYPE.XMR_GLOBAL_POLICY_CONTAINER) {
|
||||
container.global_container = {};
|
||||
|
||||
let index = 0;
|
||||
while (index < value.length - 16) {
|
||||
const data = XMRLicenseStructsV2.FTLV.parse(value.value.slice(index));
|
||||
|
||||
// XMRTYPE.XMR_REVOCATION_INFORMATION_VERSION
|
||||
if (data.type === XMRTYPE.XMR_REVOCATION_INFORMATION_VERSION) {
|
||||
container.global_container.revocationInfo = {
|
||||
version: data.value.readUInt32BE(0),
|
||||
};
|
||||
}
|
||||
|
||||
// XMRTYPE.XMR_SECURITY_LEVEL
|
||||
if (data.type === XMRTYPE.XMR_SECURITY_LEVEL) {
|
||||
container.global_container.securityLevel = {
|
||||
level: data.value.readUInt16BE(0),
|
||||
};
|
||||
}
|
||||
|
||||
index += data.length;
|
||||
}
|
||||
}
|
||||
|
||||
// XMRTYPE.XMR_KEY_MATERIAL_CONTAINER
|
||||
if (value.type === XMRTYPE.XMR_KEY_MATERIAL_CONTAINER) {
|
||||
container.keyMaterial = {};
|
||||
|
||||
let index = 0;
|
||||
while (index < value.length - 16) {
|
||||
const data = XMRLicenseStructsV2.FTLV.parse(value.value.slice(index));
|
||||
|
||||
// XMRTYPE.XMR_CONTENT_KEY_OBJECT
|
||||
if (data.type === XMRTYPE.XMR_CONTENT_KEY_OBJECT) {
|
||||
const content_key = XMRLicenseStructsV2.CONTENT_KEY.parse(
|
||||
data.value
|
||||
);
|
||||
|
||||
container.keyMaterial.contentKey = {
|
||||
kid: XmrUtil.fixUUID(content_key.kid).toString('hex'),
|
||||
keyType: content_key.keytype,
|
||||
ciphertype: content_key.ciphertype,
|
||||
length: content_key.length,
|
||||
value: content_key.value,
|
||||
};
|
||||
}
|
||||
|
||||
// XMRTYPE.XMR_ECC_KEY_OBJECT
|
||||
if (data.type === XMRTYPE.XMR_ECC_KEY_OBJECT) {
|
||||
const ecc_key = XMRLicenseStructsV2.ECC_KEY.parse(data.value);
|
||||
|
||||
container.keyMaterial.encryptionKey = {
|
||||
curve: ecc_key.curve,
|
||||
length: ecc_key.length,
|
||||
value: Buffer.from(ecc_key.value).toString('hex'),
|
||||
};
|
||||
}
|
||||
|
||||
// XMRTYPE.XMR_AUXILIARY_KEY_OBJECT
|
||||
if (data.type === XMRTYPE.XMR_AUXILIARY_KEY_OBJECT) {
|
||||
const aux_keys = XMRLicenseStructsV2.AUXILIARY_KEY_OBJECT.parse(
|
||||
data.value
|
||||
);
|
||||
|
||||
container.keyMaterial.auxKeys = {
|
||||
count: aux_keys.count,
|
||||
value: aux_keys.locations.map((a: any) => {
|
||||
return {
|
||||
location: a.location,
|
||||
value: Buffer.from(a.value).toString('hex'),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
index += data.length;
|
||||
}
|
||||
}
|
||||
|
||||
pos += value.length;
|
||||
constructor(data: Buffer, license: ParsedLicense) {
|
||||
this.data = data;
|
||||
this.license = license;
|
||||
}
|
||||
|
||||
return new XmrUtil(license, parsed_license);
|
||||
}
|
||||
static parse(license: Buffer) {
|
||||
const xmr = XMRLicenseStructsV2.XMR.parse(license);
|
||||
|
||||
static fixUUID(data: Buffer): Buffer {
|
||||
return Buffer.concat([
|
||||
Buffer.from(data.subarray(0, 4).reverse()),
|
||||
Buffer.from(data.subarray(4, 6).reverse()),
|
||||
Buffer.from(data.subarray(6, 8).reverse()),
|
||||
data.subarray(8, 16),
|
||||
]);
|
||||
}
|
||||
const parsed_license: ParsedLicense = {
|
||||
version: xmr.version,
|
||||
rights: Buffer.from(xmr.rightsid).toString('hex'),
|
||||
length: license.length,
|
||||
license: {
|
||||
length: xmr.data.length,
|
||||
},
|
||||
};
|
||||
const container = parsed_license.license;
|
||||
const data = xmr.data;
|
||||
|
||||
let pos = 0;
|
||||
while (pos < data.length - 16) {
|
||||
const value = XMRLicenseStructsV2.FTLV.parse(data.value.slice(pos));
|
||||
|
||||
// XMR_SIGNATURE_OBJECT
|
||||
if (value.type === XMRTYPE.XMR_SIGNATURE_OBJECT) {
|
||||
const signature = XMRLicenseStructsV2.SIGNATURE.parse(value.value);
|
||||
|
||||
container.signature = {
|
||||
length: value.length,
|
||||
type: signature.type,
|
||||
value: Buffer.from(signature.signature).toString('hex'),
|
||||
};
|
||||
}
|
||||
|
||||
// XMRTYPE.XMR_GLOBAL_POLICY_CONTAINER
|
||||
if (value.type === XMRTYPE.XMR_GLOBAL_POLICY_CONTAINER) {
|
||||
container.global_container = {};
|
||||
|
||||
let index = 0;
|
||||
while (index < value.length - 16) {
|
||||
const data = XMRLicenseStructsV2.FTLV.parse(value.value.slice(index));
|
||||
|
||||
// XMRTYPE.XMR_REVOCATION_INFORMATION_VERSION
|
||||
if (data.type === XMRTYPE.XMR_REVOCATION_INFORMATION_VERSION) {
|
||||
container.global_container.revocationInfo = {
|
||||
version: data.value.readUInt32BE(0),
|
||||
};
|
||||
}
|
||||
|
||||
// XMRTYPE.XMR_SECURITY_LEVEL
|
||||
if (data.type === XMRTYPE.XMR_SECURITY_LEVEL) {
|
||||
container.global_container.securityLevel = {
|
||||
level: data.value.readUInt16BE(0),
|
||||
};
|
||||
}
|
||||
|
||||
index += data.length;
|
||||
}
|
||||
}
|
||||
|
||||
// XMRTYPE.XMR_KEY_MATERIAL_CONTAINER
|
||||
if (value.type === XMRTYPE.XMR_KEY_MATERIAL_CONTAINER) {
|
||||
container.keyMaterial = {};
|
||||
|
||||
let index = 0;
|
||||
while (index < value.length - 16) {
|
||||
const data = XMRLicenseStructsV2.FTLV.parse(value.value.slice(index));
|
||||
|
||||
// XMRTYPE.XMR_CONTENT_KEY_OBJECT
|
||||
if (data.type === XMRTYPE.XMR_CONTENT_KEY_OBJECT) {
|
||||
const content_key = XMRLicenseStructsV2.CONTENT_KEY.parse(
|
||||
data.value
|
||||
);
|
||||
|
||||
container.keyMaterial.contentKey = {
|
||||
kid: XmrUtil.fixUUID(content_key.kid).toString('hex'),
|
||||
keyType: content_key.keytype,
|
||||
ciphertype: content_key.ciphertype,
|
||||
length: content_key.length,
|
||||
value: content_key.value,
|
||||
};
|
||||
}
|
||||
|
||||
// XMRTYPE.XMR_ECC_KEY_OBJECT
|
||||
if (data.type === XMRTYPE.XMR_ECC_KEY_OBJECT) {
|
||||
const ecc_key = XMRLicenseStructsV2.ECC_KEY.parse(data.value);
|
||||
|
||||
container.keyMaterial.encryptionKey = {
|
||||
curve: ecc_key.curve,
|
||||
length: ecc_key.length,
|
||||
value: Buffer.from(ecc_key.value).toString('hex'),
|
||||
};
|
||||
}
|
||||
|
||||
// XMRTYPE.XMR_AUXILIARY_KEY_OBJECT
|
||||
if (data.type === XMRTYPE.XMR_AUXILIARY_KEY_OBJECT) {
|
||||
const aux_keys = XMRLicenseStructsV2.AUXILIARY_KEY_OBJECT.parse(
|
||||
data.value
|
||||
);
|
||||
|
||||
container.keyMaterial.auxKeys = {
|
||||
count: aux_keys.count,
|
||||
value: aux_keys.locations.map((a: any) => {
|
||||
return {
|
||||
location: a.location,
|
||||
value: Buffer.from(a.value).toString('hex'),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
index += data.length;
|
||||
}
|
||||
}
|
||||
|
||||
pos += value.length;
|
||||
}
|
||||
|
||||
return new XmrUtil(license, parsed_license);
|
||||
}
|
||||
|
||||
static fixUUID(data: Buffer): Buffer {
|
||||
return Buffer.concat([
|
||||
Buffer.from(data.subarray(0, 4).reverse()),
|
||||
Buffer.from(data.subarray(4, 6).reverse()),
|
||||
Buffer.from(data.subarray(6, 8).reverse()),
|
||||
data.subarray(8, 16),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,112 +2,112 @@
|
|||
import crypto from 'crypto';
|
||||
|
||||
export class AES_CMAC {
|
||||
private readonly BLOCK_SIZE = 16;
|
||||
private readonly XOR_RIGHT = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87]);
|
||||
private readonly EMPTY_BLOCK_SIZE_BUFFER = Buffer.alloc(this.BLOCK_SIZE) as Buffer;
|
||||
private readonly BLOCK_SIZE = 16;
|
||||
private readonly XOR_RIGHT = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87]);
|
||||
private readonly EMPTY_BLOCK_SIZE_BUFFER = Buffer.alloc(this.BLOCK_SIZE) as Buffer;
|
||||
|
||||
private _key: Buffer;
|
||||
private _subkeys: { first: Buffer; second: Buffer };
|
||||
private _key: Buffer;
|
||||
private _subkeys: { first: Buffer; second: Buffer };
|
||||
|
||||
public constructor(key: Buffer) {
|
||||
if (![16, 24, 32].includes(key.length)) {
|
||||
throw new Error('Key size must be 128, 192, or 256 bits.');
|
||||
}
|
||||
this._key = key;
|
||||
this._subkeys = this._generateSubkeys();
|
||||
}
|
||||
|
||||
public calculate(message: Buffer): Buffer {
|
||||
const blockCount = this._getBlockCount(message);
|
||||
|
||||
let x = this.EMPTY_BLOCK_SIZE_BUFFER;
|
||||
let y;
|
||||
|
||||
for (let i = 0; i < blockCount - 1; i++) {
|
||||
const from = i * this.BLOCK_SIZE;
|
||||
const block = message.subarray(from, from + this.BLOCK_SIZE);
|
||||
y = this._xor(x, block);
|
||||
x = this._aes(y);
|
||||
public constructor(key: Buffer) {
|
||||
if (![16, 24, 32].includes(key.length)) {
|
||||
throw new Error('Key size must be 128, 192, or 256 bits.');
|
||||
}
|
||||
this._key = key;
|
||||
this._subkeys = this._generateSubkeys();
|
||||
}
|
||||
|
||||
y = this._xor(x, this._getLastBlock(message));
|
||||
x = this._aes(y);
|
||||
public calculate(message: Buffer): Buffer {
|
||||
const blockCount = this._getBlockCount(message);
|
||||
|
||||
return x;
|
||||
}
|
||||
let x = this.EMPTY_BLOCK_SIZE_BUFFER;
|
||||
let y;
|
||||
|
||||
private _generateSubkeys(): { first: Buffer; second: Buffer } {
|
||||
const l = this._aes(this.EMPTY_BLOCK_SIZE_BUFFER);
|
||||
for (let i = 0; i < blockCount - 1; i++) {
|
||||
const from = i * this.BLOCK_SIZE;
|
||||
const block = message.subarray(from, from + this.BLOCK_SIZE);
|
||||
y = this._xor(x, block);
|
||||
x = this._aes(y);
|
||||
}
|
||||
|
||||
let first = this._bitShiftLeft(l);
|
||||
if (l[0] & 0x80) {
|
||||
first = this._xor(first, this.XOR_RIGHT);
|
||||
y = this._xor(x, this._getLastBlock(message));
|
||||
x = this._aes(y);
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
let second = this._bitShiftLeft(first);
|
||||
if (first[0] & 0x80) {
|
||||
second = this._xor(second, this.XOR_RIGHT);
|
||||
private _generateSubkeys(): { first: Buffer; second: Buffer } {
|
||||
const l = this._aes(this.EMPTY_BLOCK_SIZE_BUFFER);
|
||||
|
||||
let first = this._bitShiftLeft(l);
|
||||
if (l[0] & 0x80) {
|
||||
first = this._xor(first, this.XOR_RIGHT);
|
||||
}
|
||||
|
||||
let second = this._bitShiftLeft(first);
|
||||
if (first[0] & 0x80) {
|
||||
second = this._xor(second, this.XOR_RIGHT);
|
||||
}
|
||||
|
||||
return { first: first, second: second };
|
||||
}
|
||||
|
||||
return { first: first, second: second };
|
||||
}
|
||||
|
||||
private _getBlockCount(message: Buffer): number {
|
||||
const blockCount = Math.ceil(message.length / this.BLOCK_SIZE);
|
||||
return blockCount === 0 ? 1 : blockCount;
|
||||
}
|
||||
|
||||
private _aes(message: Buffer): Buffer {
|
||||
const cipher = crypto.createCipheriv(`aes-${this._key.length * 8}-cbc`, this._key, Buffer.alloc(this.BLOCK_SIZE));
|
||||
const result = cipher.update(message).subarray(0, 16);
|
||||
cipher.destroy();
|
||||
return result;
|
||||
}
|
||||
|
||||
private _getLastBlock(message: Buffer): Buffer {
|
||||
const blockCount = this._getBlockCount(message);
|
||||
const paddedBlock = this._padding(message, blockCount - 1);
|
||||
|
||||
let complete = false;
|
||||
if (message.length > 0) {
|
||||
complete = message.length % this.BLOCK_SIZE === 0;
|
||||
private _getBlockCount(message: Buffer): number {
|
||||
const blockCount = Math.ceil(message.length / this.BLOCK_SIZE);
|
||||
return blockCount === 0 ? 1 : blockCount;
|
||||
}
|
||||
|
||||
const key = complete ? this._subkeys.first : this._subkeys.second;
|
||||
return this._xor(paddedBlock, key);
|
||||
}
|
||||
|
||||
private _padding(message: Buffer, blockIndex: number): Buffer {
|
||||
const block = Buffer.alloc(this.BLOCK_SIZE);
|
||||
|
||||
const from = blockIndex * this.BLOCK_SIZE;
|
||||
|
||||
const slice = message.subarray(from, from + this.BLOCK_SIZE);
|
||||
block.set(slice);
|
||||
|
||||
if (slice.length !== this.BLOCK_SIZE) {
|
||||
block[slice.length] = 0x80;
|
||||
private _aes(message: Buffer): Buffer {
|
||||
const cipher = crypto.createCipheriv(`aes-${this._key.length * 8}-cbc`, this._key, Buffer.alloc(this.BLOCK_SIZE));
|
||||
const result = cipher.update(message).subarray(0, 16);
|
||||
cipher.destroy();
|
||||
return result;
|
||||
}
|
||||
|
||||
return block;
|
||||
}
|
||||
private _getLastBlock(message: Buffer): Buffer {
|
||||
const blockCount = this._getBlockCount(message);
|
||||
const paddedBlock = this._padding(message, blockCount - 1);
|
||||
|
||||
private _bitShiftLeft(input: Buffer): Buffer {
|
||||
const output = Buffer.alloc(input.length);
|
||||
let overflow = 0;
|
||||
for (let i = input.length - 1; i >= 0; i--) {
|
||||
output[i] = (input[i] << 1) | overflow;
|
||||
overflow = input[i] & 0x80 ? 1 : 0;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
let complete = false;
|
||||
if (message.length > 0) {
|
||||
complete = message.length % this.BLOCK_SIZE === 0;
|
||||
}
|
||||
|
||||
private _xor(a: Buffer, b: Buffer): Buffer {
|
||||
const length = Math.min(a.length, b.length);
|
||||
const output = Buffer.alloc(length);
|
||||
for (let i = 0; i < length; i++) {
|
||||
output[i] = a[i] ^ b[i];
|
||||
const key = complete ? this._subkeys.first : this._subkeys.second;
|
||||
return this._xor(paddedBlock, key);
|
||||
}
|
||||
|
||||
private _padding(message: Buffer, blockIndex: number): Buffer {
|
||||
const block = Buffer.alloc(this.BLOCK_SIZE);
|
||||
|
||||
const from = blockIndex * this.BLOCK_SIZE;
|
||||
|
||||
const slice = message.subarray(from, from + this.BLOCK_SIZE);
|
||||
block.set(slice);
|
||||
|
||||
if (slice.length !== this.BLOCK_SIZE) {
|
||||
block[slice.length] = 0x80;
|
||||
}
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
private _bitShiftLeft(input: Buffer): Buffer {
|
||||
const output = Buffer.alloc(input.length);
|
||||
let overflow = 0;
|
||||
for (let i = input.length - 1; i >= 0; i--) {
|
||||
output[i] = (input[i] << 1) | overflow;
|
||||
overflow = input[i] & 0x80 ? 1 : 0;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
private _xor(a: Buffer, b: Buffer): Buffer {
|
||||
const length = Math.min(a.length, b.length);
|
||||
const output = Buffer.alloc(length);
|
||||
for (let i = 0; i < length; i++) {
|
||||
output[i] = a[i] ^ b[i];
|
||||
}
|
||||
return output;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,76 +3,76 @@
|
|||
import { AES_CMAC } from './cmac';
|
||||
import forge from 'node-forge';
|
||||
import {
|
||||
ClientIdentification,
|
||||
ClientIdentificationSchema,
|
||||
DrmCertificateSchema,
|
||||
EncryptedClientIdentification,
|
||||
EncryptedClientIdentificationSchema,
|
||||
LicenseRequest,
|
||||
LicenseRequest_ContentIdentification_WidevinePsshDataSchema,
|
||||
LicenseRequest_ContentIdentificationSchema,
|
||||
LicenseRequest_RequestType,
|
||||
LicenseRequestSchema,
|
||||
LicenseSchema,
|
||||
LicenseType,
|
||||
ProtocolVersion,
|
||||
SignedDrmCertificate,
|
||||
SignedDrmCertificateSchema,
|
||||
SignedMessage,
|
||||
SignedMessage_MessageType,
|
||||
SignedMessageSchema,
|
||||
WidevinePsshData,
|
||||
WidevinePsshDataSchema
|
||||
ClientIdentification,
|
||||
ClientIdentificationSchema,
|
||||
DrmCertificateSchema,
|
||||
EncryptedClientIdentification,
|
||||
EncryptedClientIdentificationSchema,
|
||||
LicenseRequest,
|
||||
LicenseRequest_ContentIdentification_WidevinePsshDataSchema,
|
||||
LicenseRequest_ContentIdentificationSchema,
|
||||
LicenseRequest_RequestType,
|
||||
LicenseRequestSchema,
|
||||
LicenseSchema,
|
||||
LicenseType,
|
||||
ProtocolVersion,
|
||||
SignedDrmCertificate,
|
||||
SignedDrmCertificateSchema,
|
||||
SignedMessage,
|
||||
SignedMessage_MessageType,
|
||||
SignedMessageSchema,
|
||||
WidevinePsshData,
|
||||
WidevinePsshDataSchema
|
||||
} from './license_protocol_pb3';
|
||||
import { create, fromBinary, toBinary } from '@bufbuild/protobuf';
|
||||
|
||||
const WIDEVINE_SYSTEM_ID = new Uint8Array([0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, 0xd5, 0x1d, 0x21, 0xed]);
|
||||
|
||||
const WIDEVINE_ROOT_PUBLIC_KEY = new Uint8Array([
|
||||
0x30, 0x82, 0x01, 0x8a, 0x02, 0x82, 0x01, 0x81, 0x00, 0xb4, 0xfe, 0x39, 0xc3, 0x65, 0x90, 0x03, 0xdb, 0x3c, 0x11, 0x97, 0x09, 0xe8, 0x68, 0xcd, 0xf2, 0xc3, 0x5e, 0x9b, 0xf2,
|
||||
0xe7, 0x4d, 0x23, 0xb1, 0x10, 0xdb, 0x87, 0x65, 0xdf, 0xdc, 0xfb, 0x9f, 0x35, 0xa0, 0x57, 0x03, 0x53, 0x4c, 0xf6, 0x6d, 0x35, 0x7d, 0xa6, 0x78, 0xdb, 0xb3, 0x36, 0xd2, 0x3f,
|
||||
0x9c, 0x40, 0xa9, 0x95, 0x26, 0x72, 0x7f, 0xb8, 0xbe, 0x66, 0xdf, 0xc5, 0x21, 0x98, 0x78, 0x15, 0x16, 0x68, 0x5d, 0x2f, 0x46, 0x0e, 0x43, 0xcb, 0x8a, 0x84, 0x39, 0xab, 0xfb,
|
||||
0xb0, 0x35, 0x80, 0x22, 0xbe, 0x34, 0x23, 0x8b, 0xab, 0x53, 0x5b, 0x72, 0xec, 0x4b, 0xb5, 0x48, 0x69, 0x53, 0x3e, 0x47, 0x5f, 0xfd, 0x09, 0xfd, 0xa7, 0x76, 0x13, 0x8f, 0x0f,
|
||||
0x92, 0xd6, 0x4c, 0xdf, 0xae, 0x76, 0xa9, 0xba, 0xd9, 0x22, 0x10, 0xa9, 0x9d, 0x71, 0x45, 0xd6, 0xd7, 0xe1, 0x19, 0x25, 0x85, 0x9c, 0x53, 0x9a, 0x97, 0xeb, 0x84, 0xd7, 0xcc,
|
||||
0xa8, 0x88, 0x82, 0x20, 0x70, 0x26, 0x20, 0xfd, 0x7e, 0x40, 0x50, 0x27, 0xe2, 0x25, 0x93, 0x6f, 0xbc, 0x3e, 0x72, 0xa0, 0xfa, 0xc1, 0xbd, 0x29, 0xb4, 0x4d, 0x82, 0x5c, 0xc1,
|
||||
0xb4, 0xcb, 0x9c, 0x72, 0x7e, 0xb0, 0xe9, 0x8a, 0x17, 0x3e, 0x19, 0x63, 0xfc, 0xfd, 0x82, 0x48, 0x2b, 0xb7, 0xb2, 0x33, 0xb9, 0x7d, 0xec, 0x4b, 0xba, 0x89, 0x1f, 0x27, 0xb8,
|
||||
0x9b, 0x88, 0x48, 0x84, 0xaa, 0x18, 0x92, 0x0e, 0x65, 0xf5, 0xc8, 0x6c, 0x11, 0xff, 0x6b, 0x36, 0xe4, 0x74, 0x34, 0xca, 0x8c, 0x33, 0xb1, 0xf9, 0xb8, 0x8e, 0xb4, 0xe6, 0x12,
|
||||
0xe0, 0x02, 0x98, 0x79, 0x52, 0x5e, 0x45, 0x33, 0xff, 0x11, 0xdc, 0xeb, 0xc3, 0x53, 0xba, 0x7c, 0x60, 0x1a, 0x11, 0x3d, 0x00, 0xfb, 0xd2, 0xb7, 0xaa, 0x30, 0xfa, 0x4f, 0x5e,
|
||||
0x48, 0x77, 0x5b, 0x17, 0xdc, 0x75, 0xef, 0x6f, 0xd2, 0x19, 0x6d, 0xdc, 0xbe, 0x7f, 0xb0, 0x78, 0x8f, 0xdc, 0x82, 0x60, 0x4c, 0xbf, 0xe4, 0x29, 0x06, 0x5e, 0x69, 0x8c, 0x39,
|
||||
0x13, 0xad, 0x14, 0x25, 0xed, 0x19, 0xb2, 0xf2, 0x9f, 0x01, 0x82, 0x0d, 0x56, 0x44, 0x88, 0xc8, 0x35, 0xec, 0x1f, 0x11, 0xb3, 0x24, 0xe0, 0x59, 0x0d, 0x37, 0xe4, 0x47, 0x3c,
|
||||
0xea, 0x4b, 0x7f, 0x97, 0x31, 0x1c, 0x81, 0x7c, 0x94, 0x8a, 0x4c, 0x7d, 0x68, 0x15, 0x84, 0xff, 0xa5, 0x08, 0xfd, 0x18, 0xe7, 0xe7, 0x2b, 0xe4, 0x47, 0x27, 0x12, 0x11, 0xb8,
|
||||
0x23, 0xec, 0x58, 0x93, 0x3c, 0xac, 0x12, 0xd2, 0x88, 0x6d, 0x41, 0x3d, 0xc5, 0xfe, 0x1c, 0xdc, 0xb9, 0xf8, 0xd4, 0x51, 0x3e, 0x07, 0xe5, 0x03, 0x6f, 0xa7, 0x12, 0xe8, 0x12,
|
||||
0xf7, 0xb5, 0xce, 0xa6, 0x96, 0x55, 0x3f, 0x78, 0xb4, 0x64, 0x82, 0x50, 0xd2, 0x33, 0x5f, 0x91, 0x02, 0x03, 0x01, 0x00, 0x01
|
||||
0x30, 0x82, 0x01, 0x8a, 0x02, 0x82, 0x01, 0x81, 0x00, 0xb4, 0xfe, 0x39, 0xc3, 0x65, 0x90, 0x03, 0xdb, 0x3c, 0x11, 0x97, 0x09, 0xe8, 0x68, 0xcd, 0xf2, 0xc3, 0x5e, 0x9b, 0xf2,
|
||||
0xe7, 0x4d, 0x23, 0xb1, 0x10, 0xdb, 0x87, 0x65, 0xdf, 0xdc, 0xfb, 0x9f, 0x35, 0xa0, 0x57, 0x03, 0x53, 0x4c, 0xf6, 0x6d, 0x35, 0x7d, 0xa6, 0x78, 0xdb, 0xb3, 0x36, 0xd2, 0x3f,
|
||||
0x9c, 0x40, 0xa9, 0x95, 0x26, 0x72, 0x7f, 0xb8, 0xbe, 0x66, 0xdf, 0xc5, 0x21, 0x98, 0x78, 0x15, 0x16, 0x68, 0x5d, 0x2f, 0x46, 0x0e, 0x43, 0xcb, 0x8a, 0x84, 0x39, 0xab, 0xfb,
|
||||
0xb0, 0x35, 0x80, 0x22, 0xbe, 0x34, 0x23, 0x8b, 0xab, 0x53, 0x5b, 0x72, 0xec, 0x4b, 0xb5, 0x48, 0x69, 0x53, 0x3e, 0x47, 0x5f, 0xfd, 0x09, 0xfd, 0xa7, 0x76, 0x13, 0x8f, 0x0f,
|
||||
0x92, 0xd6, 0x4c, 0xdf, 0xae, 0x76, 0xa9, 0xba, 0xd9, 0x22, 0x10, 0xa9, 0x9d, 0x71, 0x45, 0xd6, 0xd7, 0xe1, 0x19, 0x25, 0x85, 0x9c, 0x53, 0x9a, 0x97, 0xeb, 0x84, 0xd7, 0xcc,
|
||||
0xa8, 0x88, 0x82, 0x20, 0x70, 0x26, 0x20, 0xfd, 0x7e, 0x40, 0x50, 0x27, 0xe2, 0x25, 0x93, 0x6f, 0xbc, 0x3e, 0x72, 0xa0, 0xfa, 0xc1, 0xbd, 0x29, 0xb4, 0x4d, 0x82, 0x5c, 0xc1,
|
||||
0xb4, 0xcb, 0x9c, 0x72, 0x7e, 0xb0, 0xe9, 0x8a, 0x17, 0x3e, 0x19, 0x63, 0xfc, 0xfd, 0x82, 0x48, 0x2b, 0xb7, 0xb2, 0x33, 0xb9, 0x7d, 0xec, 0x4b, 0xba, 0x89, 0x1f, 0x27, 0xb8,
|
||||
0x9b, 0x88, 0x48, 0x84, 0xaa, 0x18, 0x92, 0x0e, 0x65, 0xf5, 0xc8, 0x6c, 0x11, 0xff, 0x6b, 0x36, 0xe4, 0x74, 0x34, 0xca, 0x8c, 0x33, 0xb1, 0xf9, 0xb8, 0x8e, 0xb4, 0xe6, 0x12,
|
||||
0xe0, 0x02, 0x98, 0x79, 0x52, 0x5e, 0x45, 0x33, 0xff, 0x11, 0xdc, 0xeb, 0xc3, 0x53, 0xba, 0x7c, 0x60, 0x1a, 0x11, 0x3d, 0x00, 0xfb, 0xd2, 0xb7, 0xaa, 0x30, 0xfa, 0x4f, 0x5e,
|
||||
0x48, 0x77, 0x5b, 0x17, 0xdc, 0x75, 0xef, 0x6f, 0xd2, 0x19, 0x6d, 0xdc, 0xbe, 0x7f, 0xb0, 0x78, 0x8f, 0xdc, 0x82, 0x60, 0x4c, 0xbf, 0xe4, 0x29, 0x06, 0x5e, 0x69, 0x8c, 0x39,
|
||||
0x13, 0xad, 0x14, 0x25, 0xed, 0x19, 0xb2, 0xf2, 0x9f, 0x01, 0x82, 0x0d, 0x56, 0x44, 0x88, 0xc8, 0x35, 0xec, 0x1f, 0x11, 0xb3, 0x24, 0xe0, 0x59, 0x0d, 0x37, 0xe4, 0x47, 0x3c,
|
||||
0xea, 0x4b, 0x7f, 0x97, 0x31, 0x1c, 0x81, 0x7c, 0x94, 0x8a, 0x4c, 0x7d, 0x68, 0x15, 0x84, 0xff, 0xa5, 0x08, 0xfd, 0x18, 0xe7, 0xe7, 0x2b, 0xe4, 0x47, 0x27, 0x12, 0x11, 0xb8,
|
||||
0x23, 0xec, 0x58, 0x93, 0x3c, 0xac, 0x12, 0xd2, 0x88, 0x6d, 0x41, 0x3d, 0xc5, 0xfe, 0x1c, 0xdc, 0xb9, 0xf8, 0xd4, 0x51, 0x3e, 0x07, 0xe5, 0x03, 0x6f, 0xa7, 0x12, 0xe8, 0x12,
|
||||
0xf7, 0xb5, 0xce, 0xa6, 0x96, 0x55, 0x3f, 0x78, 0xb4, 0x64, 0x82, 0x50, 0xd2, 0x33, 0x5f, 0x91, 0x02, 0x03, 0x01, 0x00, 0x01
|
||||
]);
|
||||
|
||||
export const SERVICE_CERTIFICATE_CHALLENGE = new Uint8Array([0x08, 0x04]);
|
||||
|
||||
const COMMON_SERVICE_CERTIFICATE = new Uint8Array([
|
||||
0x08, 0x05, 0x12, 0xc7, 0x05, 0x0a, 0xc1, 0x02, 0x08, 0x03, 0x12, 0x10, 0x17, 0x05, 0xb9, 0x17, 0xcc, 0x12, 0x04, 0x86, 0x8b, 0x06, 0x33, 0x3a, 0x2f, 0x77, 0x2a, 0x8c, 0x18,
|
||||
0x82, 0xb4, 0x82, 0x92, 0x05, 0x22, 0x8e, 0x02, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0x99, 0xed, 0x5b, 0x3b, 0x32, 0x7d, 0xab, 0x5e, 0x24, 0xef, 0xc3, 0xb6,
|
||||
0x2a, 0x95, 0xb5, 0x98, 0x52, 0x0a, 0xd5, 0xbc, 0xcb, 0x37, 0x50, 0x3e, 0x06, 0x45, 0xb8, 0x14, 0xd8, 0x76, 0xb8, 0xdf, 0x40, 0x51, 0x04, 0x41, 0xad, 0x8c, 0xe3, 0xad, 0xb1,
|
||||
0x1b, 0xb8, 0x8c, 0x4e, 0x72, 0x5a, 0x5e, 0x4a, 0x9e, 0x07, 0x95, 0x29, 0x1d, 0x58, 0x58, 0x40, 0x23, 0xa7, 0xe1, 0xaf, 0x0e, 0x38, 0xa9, 0x12, 0x79, 0x39, 0x30, 0x08, 0x61,
|
||||
0x0b, 0x6f, 0x15, 0x8c, 0x87, 0x8c, 0x7e, 0x21, 0xbf, 0xfb, 0xfe, 0xea, 0x77, 0xe1, 0x01, 0x9e, 0x1e, 0x57, 0x81, 0xe8, 0xa4, 0x5f, 0x46, 0x26, 0x3d, 0x14, 0xe6, 0x0e, 0x80,
|
||||
0x58, 0xa8, 0x60, 0x7a, 0xdc, 0xe0, 0x4f, 0xac, 0x84, 0x57, 0xb1, 0x37, 0xa8, 0xd6, 0x7c, 0xcd, 0xeb, 0x33, 0x70, 0x5d, 0x98, 0x3a, 0x21, 0xfb, 0x4e, 0xec, 0xbd, 0x4a, 0x10,
|
||||
0xca, 0x47, 0x49, 0x0c, 0xa4, 0x7e, 0xaa, 0x5d, 0x43, 0x82, 0x18, 0xdd, 0xba, 0xf1, 0xca, 0xde, 0x33, 0x92, 0xf1, 0x3d, 0x6f, 0xfb, 0x64, 0x42, 0xfd, 0x31, 0xe1, 0xbf, 0x40,
|
||||
0xb0, 0xc6, 0x04, 0xd1, 0xc4, 0xba, 0x4c, 0x95, 0x20, 0xa4, 0xbf, 0x97, 0xee, 0xbd, 0x60, 0x92, 0x9a, 0xfc, 0xee, 0xf5, 0x5b, 0xba, 0xf5, 0x64, 0xe2, 0xd0, 0xe7, 0x6c, 0xd7,
|
||||
0xc5, 0x5c, 0x73, 0xa0, 0x82, 0xb9, 0x96, 0x12, 0x0b, 0x83, 0x59, 0xed, 0xce, 0x24, 0x70, 0x70, 0x82, 0x68, 0x0d, 0x6f, 0x67, 0xc6, 0xd8, 0x2c, 0x4a, 0xc5, 0xf3, 0x13, 0x44,
|
||||
0x90, 0xa7, 0x4e, 0xec, 0x37, 0xaf, 0x4b, 0x2f, 0x01, 0x0c, 0x59, 0xe8, 0x28, 0x43, 0xe2, 0x58, 0x2f, 0x0b, 0x6b, 0x9f, 0x5d, 0xb0, 0xfc, 0x5e, 0x6e, 0xdf, 0x64, 0xfb, 0xd3,
|
||||
0x08, 0xb4, 0x71, 0x1b, 0xcf, 0x12, 0x50, 0x01, 0x9c, 0x9f, 0x5a, 0x09, 0x02, 0x03, 0x01, 0x00, 0x01, 0x3a, 0x14, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x2e, 0x77, 0x69,
|
||||
0x64, 0x65, 0x76, 0x69, 0x6e, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x12, 0x80, 0x03, 0xae, 0x34, 0x73, 0x14, 0xb5, 0xa8, 0x35, 0x29, 0x7f, 0x27, 0x13, 0x88, 0xfb, 0x7b, 0xb8, 0xcb,
|
||||
0x52, 0x77, 0xd2, 0x49, 0x82, 0x3c, 0xdd, 0xd1, 0xda, 0x30, 0xb9, 0x33, 0x39, 0x51, 0x1e, 0xb3, 0xcc, 0xbd, 0xea, 0x04, 0xb9, 0x44, 0xb9, 0x27, 0xc1, 0x21, 0x34, 0x6e, 0xfd,
|
||||
0xbd, 0xea, 0xc9, 0xd4, 0x13, 0x91, 0x7e, 0x6e, 0xc1, 0x76, 0xa1, 0x04, 0x38, 0x46, 0x0a, 0x50, 0x3b, 0xc1, 0x95, 0x2b, 0x9b, 0xa4, 0xe4, 0xce, 0x0f, 0xc4, 0xbf, 0xc2, 0x0a,
|
||||
0x98, 0x08, 0xaa, 0xaf, 0x4b, 0xfc, 0xd1, 0x9c, 0x1d, 0xcf, 0xcd, 0xf5, 0x74, 0xcc, 0xac, 0x28, 0xd1, 0xb4, 0x10, 0x41, 0x6c, 0xf9, 0xde, 0x88, 0x04, 0x30, 0x1c, 0xbd, 0xb3,
|
||||
0x34, 0xca, 0xfc, 0xd0, 0xd4, 0x09, 0x78, 0x42, 0x3a, 0x64, 0x2e, 0x54, 0x61, 0x3d, 0xf0, 0xaf, 0xcf, 0x96, 0xca, 0x4a, 0x92, 0x49, 0xd8, 0x55, 0xe4, 0x2b, 0x3a, 0x70, 0x3e,
|
||||
0xf1, 0x76, 0x7f, 0x6a, 0x9b, 0xd3, 0x6d, 0x6b, 0xf8, 0x2b, 0xe7, 0x6b, 0xbf, 0x0c, 0xba, 0x4f, 0xde, 0x59, 0xd2, 0xab, 0xcc, 0x76, 0xfe, 0xb6, 0x42, 0x47, 0xb8, 0x5c, 0x43,
|
||||
0x1f, 0xbc, 0xa5, 0x22, 0x66, 0xb6, 0x19, 0xfc, 0x36, 0x97, 0x95, 0x43, 0xfc, 0xa9, 0xcb, 0xbd, 0xbb, 0xfa, 0xfa, 0x0e, 0x1a, 0x55, 0xe7, 0x55, 0xa3, 0xc7, 0xbc, 0xe6, 0x55,
|
||||
0xf9, 0x64, 0x6f, 0x58, 0x2a, 0xb9, 0xcf, 0x70, 0xaa, 0x08, 0xb9, 0x79, 0xf8, 0x67, 0xf6, 0x3a, 0x0b, 0x2b, 0x7f, 0xdb, 0x36, 0x2c, 0x5b, 0xc4, 0xec, 0xd5, 0x55, 0xd8, 0x5b,
|
||||
0xca, 0xa9, 0xc5, 0x93, 0xc3, 0x83, 0xc8, 0x57, 0xd4, 0x9d, 0xaa, 0xb7, 0x7e, 0x40, 0xb7, 0x85, 0x1d, 0xdf, 0xd2, 0x49, 0x98, 0x80, 0x8e, 0x35, 0xb2, 0x58, 0xe7, 0x5d, 0x78,
|
||||
0xea, 0xc0, 0xca, 0x16, 0xf7, 0x04, 0x73, 0x04, 0xc2, 0x0d, 0x93, 0xed, 0xe4, 0xe8, 0xff, 0x1c, 0x6f, 0x17, 0xe6, 0x24, 0x3e, 0x3f, 0x3d, 0xa8, 0xfc, 0x17, 0x09, 0x87, 0x0e,
|
||||
0xc4, 0x5f, 0xba, 0x82, 0x3a, 0x26, 0x3f, 0x0c, 0xef, 0xa1, 0xf7, 0x09, 0x3b, 0x19, 0x09, 0x92, 0x83, 0x26, 0x33, 0x37, 0x05, 0x04, 0x3a, 0x29, 0xbd, 0xa6, 0xf9, 0xb4, 0x34,
|
||||
0x2c, 0xc8, 0xdf, 0x54, 0x3c, 0xb1, 0xa1, 0x18, 0x2f, 0x7c, 0x5f, 0xff, 0x33, 0xf1, 0x04, 0x90, 0xfa, 0xca, 0x5b, 0x25, 0x36, 0x0b, 0x76, 0x01, 0x5e, 0x9c, 0x5a, 0x06, 0xab,
|
||||
0x8e, 0xe0, 0x2f, 0x00, 0xd2, 0xe8, 0xd5, 0x98, 0x61, 0x04, 0xaa, 0xcc, 0x4d, 0xd4, 0x75, 0xfd, 0x96, 0xee, 0x9c, 0xe4, 0xe3, 0x26, 0xf2, 0x1b, 0x83, 0xc7, 0x05, 0x85, 0x77,
|
||||
0xb3, 0x87, 0x32, 0xcd, 0xda, 0xbc, 0x6a, 0x6b, 0xed, 0x13, 0xfb, 0x0d, 0x49, 0xd3, 0x8a, 0x45, 0xeb, 0x87, 0xa5, 0xf4
|
||||
0x08, 0x05, 0x12, 0xc7, 0x05, 0x0a, 0xc1, 0x02, 0x08, 0x03, 0x12, 0x10, 0x17, 0x05, 0xb9, 0x17, 0xcc, 0x12, 0x04, 0x86, 0x8b, 0x06, 0x33, 0x3a, 0x2f, 0x77, 0x2a, 0x8c, 0x18,
|
||||
0x82, 0xb4, 0x82, 0x92, 0x05, 0x22, 0x8e, 0x02, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0x99, 0xed, 0x5b, 0x3b, 0x32, 0x7d, 0xab, 0x5e, 0x24, 0xef, 0xc3, 0xb6,
|
||||
0x2a, 0x95, 0xb5, 0x98, 0x52, 0x0a, 0xd5, 0xbc, 0xcb, 0x37, 0x50, 0x3e, 0x06, 0x45, 0xb8, 0x14, 0xd8, 0x76, 0xb8, 0xdf, 0x40, 0x51, 0x04, 0x41, 0xad, 0x8c, 0xe3, 0xad, 0xb1,
|
||||
0x1b, 0xb8, 0x8c, 0x4e, 0x72, 0x5a, 0x5e, 0x4a, 0x9e, 0x07, 0x95, 0x29, 0x1d, 0x58, 0x58, 0x40, 0x23, 0xa7, 0xe1, 0xaf, 0x0e, 0x38, 0xa9, 0x12, 0x79, 0x39, 0x30, 0x08, 0x61,
|
||||
0x0b, 0x6f, 0x15, 0x8c, 0x87, 0x8c, 0x7e, 0x21, 0xbf, 0xfb, 0xfe, 0xea, 0x77, 0xe1, 0x01, 0x9e, 0x1e, 0x57, 0x81, 0xe8, 0xa4, 0x5f, 0x46, 0x26, 0x3d, 0x14, 0xe6, 0x0e, 0x80,
|
||||
0x58, 0xa8, 0x60, 0x7a, 0xdc, 0xe0, 0x4f, 0xac, 0x84, 0x57, 0xb1, 0x37, 0xa8, 0xd6, 0x7c, 0xcd, 0xeb, 0x33, 0x70, 0x5d, 0x98, 0x3a, 0x21, 0xfb, 0x4e, 0xec, 0xbd, 0x4a, 0x10,
|
||||
0xca, 0x47, 0x49, 0x0c, 0xa4, 0x7e, 0xaa, 0x5d, 0x43, 0x82, 0x18, 0xdd, 0xba, 0xf1, 0xca, 0xde, 0x33, 0x92, 0xf1, 0x3d, 0x6f, 0xfb, 0x64, 0x42, 0xfd, 0x31, 0xe1, 0xbf, 0x40,
|
||||
0xb0, 0xc6, 0x04, 0xd1, 0xc4, 0xba, 0x4c, 0x95, 0x20, 0xa4, 0xbf, 0x97, 0xee, 0xbd, 0x60, 0x92, 0x9a, 0xfc, 0xee, 0xf5, 0x5b, 0xba, 0xf5, 0x64, 0xe2, 0xd0, 0xe7, 0x6c, 0xd7,
|
||||
0xc5, 0x5c, 0x73, 0xa0, 0x82, 0xb9, 0x96, 0x12, 0x0b, 0x83, 0x59, 0xed, 0xce, 0x24, 0x70, 0x70, 0x82, 0x68, 0x0d, 0x6f, 0x67, 0xc6, 0xd8, 0x2c, 0x4a, 0xc5, 0xf3, 0x13, 0x44,
|
||||
0x90, 0xa7, 0x4e, 0xec, 0x37, 0xaf, 0x4b, 0x2f, 0x01, 0x0c, 0x59, 0xe8, 0x28, 0x43, 0xe2, 0x58, 0x2f, 0x0b, 0x6b, 0x9f, 0x5d, 0xb0, 0xfc, 0x5e, 0x6e, 0xdf, 0x64, 0xfb, 0xd3,
|
||||
0x08, 0xb4, 0x71, 0x1b, 0xcf, 0x12, 0x50, 0x01, 0x9c, 0x9f, 0x5a, 0x09, 0x02, 0x03, 0x01, 0x00, 0x01, 0x3a, 0x14, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x2e, 0x77, 0x69,
|
||||
0x64, 0x65, 0x76, 0x69, 0x6e, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x12, 0x80, 0x03, 0xae, 0x34, 0x73, 0x14, 0xb5, 0xa8, 0x35, 0x29, 0x7f, 0x27, 0x13, 0x88, 0xfb, 0x7b, 0xb8, 0xcb,
|
||||
0x52, 0x77, 0xd2, 0x49, 0x82, 0x3c, 0xdd, 0xd1, 0xda, 0x30, 0xb9, 0x33, 0x39, 0x51, 0x1e, 0xb3, 0xcc, 0xbd, 0xea, 0x04, 0xb9, 0x44, 0xb9, 0x27, 0xc1, 0x21, 0x34, 0x6e, 0xfd,
|
||||
0xbd, 0xea, 0xc9, 0xd4, 0x13, 0x91, 0x7e, 0x6e, 0xc1, 0x76, 0xa1, 0x04, 0x38, 0x46, 0x0a, 0x50, 0x3b, 0xc1, 0x95, 0x2b, 0x9b, 0xa4, 0xe4, 0xce, 0x0f, 0xc4, 0xbf, 0xc2, 0x0a,
|
||||
0x98, 0x08, 0xaa, 0xaf, 0x4b, 0xfc, 0xd1, 0x9c, 0x1d, 0xcf, 0xcd, 0xf5, 0x74, 0xcc, 0xac, 0x28, 0xd1, 0xb4, 0x10, 0x41, 0x6c, 0xf9, 0xde, 0x88, 0x04, 0x30, 0x1c, 0xbd, 0xb3,
|
||||
0x34, 0xca, 0xfc, 0xd0, 0xd4, 0x09, 0x78, 0x42, 0x3a, 0x64, 0x2e, 0x54, 0x61, 0x3d, 0xf0, 0xaf, 0xcf, 0x96, 0xca, 0x4a, 0x92, 0x49, 0xd8, 0x55, 0xe4, 0x2b, 0x3a, 0x70, 0x3e,
|
||||
0xf1, 0x76, 0x7f, 0x6a, 0x9b, 0xd3, 0x6d, 0x6b, 0xf8, 0x2b, 0xe7, 0x6b, 0xbf, 0x0c, 0xba, 0x4f, 0xde, 0x59, 0xd2, 0xab, 0xcc, 0x76, 0xfe, 0xb6, 0x42, 0x47, 0xb8, 0x5c, 0x43,
|
||||
0x1f, 0xbc, 0xa5, 0x22, 0x66, 0xb6, 0x19, 0xfc, 0x36, 0x97, 0x95, 0x43, 0xfc, 0xa9, 0xcb, 0xbd, 0xbb, 0xfa, 0xfa, 0x0e, 0x1a, 0x55, 0xe7, 0x55, 0xa3, 0xc7, 0xbc, 0xe6, 0x55,
|
||||
0xf9, 0x64, 0x6f, 0x58, 0x2a, 0xb9, 0xcf, 0x70, 0xaa, 0x08, 0xb9, 0x79, 0xf8, 0x67, 0xf6, 0x3a, 0x0b, 0x2b, 0x7f, 0xdb, 0x36, 0x2c, 0x5b, 0xc4, 0xec, 0xd5, 0x55, 0xd8, 0x5b,
|
||||
0xca, 0xa9, 0xc5, 0x93, 0xc3, 0x83, 0xc8, 0x57, 0xd4, 0x9d, 0xaa, 0xb7, 0x7e, 0x40, 0xb7, 0x85, 0x1d, 0xdf, 0xd2, 0x49, 0x98, 0x80, 0x8e, 0x35, 0xb2, 0x58, 0xe7, 0x5d, 0x78,
|
||||
0xea, 0xc0, 0xca, 0x16, 0xf7, 0x04, 0x73, 0x04, 0xc2, 0x0d, 0x93, 0xed, 0xe4, 0xe8, 0xff, 0x1c, 0x6f, 0x17, 0xe6, 0x24, 0x3e, 0x3f, 0x3d, 0xa8, 0xfc, 0x17, 0x09, 0x87, 0x0e,
|
||||
0xc4, 0x5f, 0xba, 0x82, 0x3a, 0x26, 0x3f, 0x0c, 0xef, 0xa1, 0xf7, 0x09, 0x3b, 0x19, 0x09, 0x92, 0x83, 0x26, 0x33, 0x37, 0x05, 0x04, 0x3a, 0x29, 0xbd, 0xa6, 0xf9, 0xb4, 0x34,
|
||||
0x2c, 0xc8, 0xdf, 0x54, 0x3c, 0xb1, 0xa1, 0x18, 0x2f, 0x7c, 0x5f, 0xff, 0x33, 0xf1, 0x04, 0x90, 0xfa, 0xca, 0x5b, 0x25, 0x36, 0x0b, 0x76, 0x01, 0x5e, 0x9c, 0x5a, 0x06, 0xab,
|
||||
0x8e, 0xe0, 0x2f, 0x00, 0xd2, 0xe8, 0xd5, 0x98, 0x61, 0x04, 0xaa, 0xcc, 0x4d, 0xd4, 0x75, 0xfd, 0x96, 0xee, 0x9c, 0xe4, 0xe3, 0x26, 0xf2, 0x1b, 0x83, 0xc7, 0x05, 0x85, 0x77,
|
||||
0xb3, 0x87, 0x32, 0xcd, 0xda, 0xbc, 0x6a, 0x6b, 0xed, 0x13, 0xfb, 0x0d, 0x49, 0xd3, 0x8a, 0x45, 0xeb, 0x87, 0xa5, 0xf4
|
||||
]);
|
||||
|
||||
export type KeyContainer = {
|
||||
|
|
@ -86,216 +86,216 @@ export type ContentDecryptionModule = {
|
|||
};
|
||||
|
||||
export class Session {
|
||||
private _devicePrivateKey: forge.pki.rsa.PrivateKey;
|
||||
private _identifierBlob: ClientIdentification;
|
||||
private _pssh: Buffer;
|
||||
private _rawLicenseRequest?: Buffer;
|
||||
private _serviceCertificate?: SignedDrmCertificate;
|
||||
private _devicePrivateKey: forge.pki.rsa.PrivateKey;
|
||||
private _identifierBlob: ClientIdentification;
|
||||
private _pssh: Buffer;
|
||||
private _rawLicenseRequest?: Buffer;
|
||||
private _serviceCertificate?: SignedDrmCertificate;
|
||||
|
||||
constructor(contentDecryptionModule: ContentDecryptionModule, pssh: Buffer) {
|
||||
this._devicePrivateKey = forge.pki.privateKeyFromPem(contentDecryptionModule.privateKey.toString('binary'));
|
||||
constructor(contentDecryptionModule: ContentDecryptionModule, pssh: Buffer) {
|
||||
this._devicePrivateKey = forge.pki.privateKeyFromPem(contentDecryptionModule.privateKey.toString('binary'));
|
||||
|
||||
this._identifierBlob = fromBinary(ClientIdentificationSchema, contentDecryptionModule.identifierBlob);
|
||||
this._pssh = pssh;
|
||||
}
|
||||
|
||||
async setDefaultServiceCertificate() {
|
||||
await this.setServiceCertificate(Buffer.from(COMMON_SERVICE_CERTIFICATE));
|
||||
}
|
||||
|
||||
async setServiceCertificateFromMessage(rawSignedMessage: Buffer) {
|
||||
const signedMessage: SignedMessage = fromBinary(SignedMessageSchema, rawSignedMessage);
|
||||
if (!signedMessage.msg) {
|
||||
throw new Error('the service certificate message does not contain a message');
|
||||
}
|
||||
await this.setServiceCertificate(Buffer.from(signedMessage.msg));
|
||||
}
|
||||
|
||||
async setServiceCertificate(serviceCertificate: Buffer) {
|
||||
const signedServiceCertificate: SignedDrmCertificate = fromBinary(SignedDrmCertificateSchema, serviceCertificate);
|
||||
if (!(await this._verifyServiceCertificate(signedServiceCertificate))) {
|
||||
throw new Error('Service certificate is not signed by the Widevine root certificate');
|
||||
}
|
||||
this._serviceCertificate = signedServiceCertificate;
|
||||
}
|
||||
|
||||
createLicenseRequest(licenseType: LicenseType = LicenseType.STREAMING, android: boolean = false): Buffer {
|
||||
if (!this._pssh.subarray(12, 28).equals(Buffer.from(WIDEVINE_SYSTEM_ID))) {
|
||||
throw new Error('the pssh is not an actuall pssh');
|
||||
this._identifierBlob = fromBinary(ClientIdentificationSchema, contentDecryptionModule.identifierBlob);
|
||||
this._pssh = pssh;
|
||||
}
|
||||
|
||||
const pssh = this._parsePSSH(this._pssh);
|
||||
if (!pssh) {
|
||||
throw new Error('pssh is invalid');
|
||||
async setDefaultServiceCertificate() {
|
||||
await this.setServiceCertificate(Buffer.from(COMMON_SERVICE_CERTIFICATE));
|
||||
}
|
||||
|
||||
const licenseRequest: LicenseRequest = create(LicenseRequestSchema, {
|
||||
type: LicenseRequest_RequestType.NEW,
|
||||
contentId: create(LicenseRequest_ContentIdentificationSchema, {
|
||||
contentIdVariant: {
|
||||
case: 'widevinePsshData',
|
||||
value: create(LicenseRequest_ContentIdentification_WidevinePsshDataSchema, {
|
||||
psshData: [this._pssh.subarray(32)],
|
||||
licenseType: licenseType,
|
||||
requestId: android ? this._generateAndroidIdentifier() : this._generateGenericIdentifier()
|
||||
})
|
||||
async setServiceCertificateFromMessage(rawSignedMessage: Buffer) {
|
||||
const signedMessage: SignedMessage = fromBinary(SignedMessageSchema, rawSignedMessage);
|
||||
if (!signedMessage.msg) {
|
||||
throw new Error('the service certificate message does not contain a message');
|
||||
}
|
||||
}),
|
||||
requestTime: BigInt(Date.now()) / BigInt(1000),
|
||||
protocolVersion: ProtocolVersion.VERSION_2_1,
|
||||
keyControlNonce: Math.floor(Math.random() * 2 ** 31)
|
||||
});
|
||||
|
||||
if (this._serviceCertificate) {
|
||||
const encryptedClientIdentification = this._encryptClientIdentification(this._identifierBlob, this._serviceCertificate);
|
||||
licenseRequest.encryptedClientId = encryptedClientIdentification;
|
||||
} else {
|
||||
licenseRequest.clientId = this._identifierBlob;
|
||||
await this.setServiceCertificate(Buffer.from(signedMessage.msg));
|
||||
}
|
||||
|
||||
this._rawLicenseRequest = Buffer.from(toBinary(LicenseRequestSchema, licenseRequest));
|
||||
|
||||
const pss: forge.pss.PSS = forge.pss.create({ md: forge.md.sha1.create(), mgf: forge.mgf.mgf1.create(forge.md.sha1.create()), saltLength: 20 });
|
||||
const md = forge.md.sha1.create();
|
||||
md.update(this._rawLicenseRequest.toString('binary'), 'raw');
|
||||
const signature = Buffer.from(this._devicePrivateKey.sign(md, pss), 'binary');
|
||||
|
||||
const signedLicenseRequest: SignedMessage = create(SignedMessageSchema, {
|
||||
type: SignedMessage_MessageType.LICENSE_REQUEST,
|
||||
msg: this._rawLicenseRequest,
|
||||
signature: signature
|
||||
});
|
||||
|
||||
return Buffer.from(toBinary(SignedMessageSchema, signedLicenseRequest));
|
||||
}
|
||||
|
||||
parseLicense(rawLicense: Buffer) {
|
||||
if (!this._rawLicenseRequest) {
|
||||
throw new Error('please request a license first');
|
||||
async setServiceCertificate(serviceCertificate: Buffer) {
|
||||
const signedServiceCertificate: SignedDrmCertificate = fromBinary(SignedDrmCertificateSchema, serviceCertificate);
|
||||
if (!(await this._verifyServiceCertificate(signedServiceCertificate))) {
|
||||
throw new Error('Service certificate is not signed by the Widevine root certificate');
|
||||
}
|
||||
this._serviceCertificate = signedServiceCertificate;
|
||||
}
|
||||
|
||||
const signedLicense = fromBinary(SignedMessageSchema, rawLicense);
|
||||
if (!signedLicense.sessionKey) {
|
||||
throw new Error('the license does not contain a session key');
|
||||
}
|
||||
if (!signedLicense.msg) {
|
||||
throw new Error('the license does not contain a message');
|
||||
}
|
||||
if (!signedLicense.signature) {
|
||||
throw new Error('the license does not contain a signature');
|
||||
createLicenseRequest(licenseType: LicenseType = LicenseType.STREAMING, android: boolean = false): Buffer {
|
||||
if (!this._pssh.subarray(12, 28).equals(Buffer.from(WIDEVINE_SYSTEM_ID))) {
|
||||
throw new Error('the pssh is not an actuall pssh');
|
||||
}
|
||||
|
||||
const pssh = this._parsePSSH(this._pssh);
|
||||
if (!pssh) {
|
||||
throw new Error('pssh is invalid');
|
||||
}
|
||||
|
||||
const licenseRequest: LicenseRequest = create(LicenseRequestSchema, {
|
||||
type: LicenseRequest_RequestType.NEW,
|
||||
contentId: create(LicenseRequest_ContentIdentificationSchema, {
|
||||
contentIdVariant: {
|
||||
case: 'widevinePsshData',
|
||||
value: create(LicenseRequest_ContentIdentification_WidevinePsshDataSchema, {
|
||||
psshData: [this._pssh.subarray(32)],
|
||||
licenseType: licenseType,
|
||||
requestId: android ? this._generateAndroidIdentifier() : this._generateGenericIdentifier()
|
||||
})
|
||||
}
|
||||
}),
|
||||
requestTime: BigInt(Date.now()) / BigInt(1000),
|
||||
protocolVersion: ProtocolVersion.VERSION_2_1,
|
||||
keyControlNonce: Math.floor(Math.random() * 2 ** 31)
|
||||
});
|
||||
|
||||
if (this._serviceCertificate) {
|
||||
const encryptedClientIdentification = this._encryptClientIdentification(this._identifierBlob, this._serviceCertificate);
|
||||
licenseRequest.encryptedClientId = encryptedClientIdentification;
|
||||
} else {
|
||||
licenseRequest.clientId = this._identifierBlob;
|
||||
}
|
||||
|
||||
this._rawLicenseRequest = Buffer.from(toBinary(LicenseRequestSchema, licenseRequest));
|
||||
|
||||
const pss: forge.pss.PSS = forge.pss.create({ md: forge.md.sha1.create(), mgf: forge.mgf.mgf1.create(forge.md.sha1.create()), saltLength: 20 });
|
||||
const md = forge.md.sha1.create();
|
||||
md.update(this._rawLicenseRequest.toString('binary'), 'raw');
|
||||
const signature = Buffer.from(this._devicePrivateKey.sign(md, pss), 'binary');
|
||||
|
||||
const signedLicenseRequest: SignedMessage = create(SignedMessageSchema, {
|
||||
type: SignedMessage_MessageType.LICENSE_REQUEST,
|
||||
msg: this._rawLicenseRequest,
|
||||
signature: signature
|
||||
});
|
||||
|
||||
return Buffer.from(toBinary(SignedMessageSchema, signedLicenseRequest));
|
||||
}
|
||||
|
||||
const sessionKey = this._devicePrivateKey.decrypt(Buffer.from(signedLicense.sessionKey).toString('binary'), 'RSA-OAEP', {
|
||||
md: forge.md.sha1.create()
|
||||
});
|
||||
parseLicense(rawLicense: Buffer) {
|
||||
if (!this._rawLicenseRequest) {
|
||||
throw new Error('please request a license first');
|
||||
}
|
||||
|
||||
const cmac = new AES_CMAC(Buffer.from(sessionKey, 'binary'));
|
||||
const signedLicense = fromBinary(SignedMessageSchema, rawLicense);
|
||||
if (!signedLicense.sessionKey) {
|
||||
throw new Error('the license does not contain a session key');
|
||||
}
|
||||
if (!signedLicense.msg) {
|
||||
throw new Error('the license does not contain a message');
|
||||
}
|
||||
if (!signedLicense.signature) {
|
||||
throw new Error('the license does not contain a signature');
|
||||
}
|
||||
|
||||
const encKeyBase = Buffer.concat([Buffer.from('ENCRYPTION'), Buffer.from('\x00', 'ascii'), this._rawLicenseRequest, Buffer.from('\x00\x00\x00\x80', 'ascii')]);
|
||||
const authKeyBase = Buffer.concat([Buffer.from('AUTHENTICATION'), Buffer.from('\x00', 'ascii'), this._rawLicenseRequest, Buffer.from('\x00\x00\x02\x00', 'ascii')]);
|
||||
const sessionKey = this._devicePrivateKey.decrypt(Buffer.from(signedLicense.sessionKey).toString('binary'), 'RSA-OAEP', {
|
||||
md: forge.md.sha1.create()
|
||||
});
|
||||
|
||||
const encKey = cmac.calculate(Buffer.concat([Buffer.from('\x01'), encKeyBase]));
|
||||
const serverKey = Buffer.concat([cmac.calculate(Buffer.concat([Buffer.from('\x01'), authKeyBase])), cmac.calculate(Buffer.concat([Buffer.from('\x02'), authKeyBase]))]);
|
||||
/*const clientKey = Buffer.concat([
|
||||
const cmac = new AES_CMAC(Buffer.from(sessionKey, 'binary'));
|
||||
|
||||
const encKeyBase = Buffer.concat([Buffer.from('ENCRYPTION'), Buffer.from('\x00', 'ascii'), this._rawLicenseRequest, Buffer.from('\x00\x00\x00\x80', 'ascii')]);
|
||||
const authKeyBase = Buffer.concat([Buffer.from('AUTHENTICATION'), Buffer.from('\x00', 'ascii'), this._rawLicenseRequest, Buffer.from('\x00\x00\x02\x00', 'ascii')]);
|
||||
|
||||
const encKey = cmac.calculate(Buffer.concat([Buffer.from('\x01'), encKeyBase]));
|
||||
const serverKey = Buffer.concat([cmac.calculate(Buffer.concat([Buffer.from('\x01'), authKeyBase])), cmac.calculate(Buffer.concat([Buffer.from('\x02'), authKeyBase]))]);
|
||||
/*const clientKey = Buffer.concat([
|
||||
cmac.calculate(Buffer.concat([Buffer.from("\x03"), authKeyBase])),
|
||||
cmac.calculate(Buffer.concat([Buffer.from("\x04"), authKeyBase]))
|
||||
]);*/
|
||||
|
||||
const hmac = forge.hmac.create();
|
||||
hmac.start(forge.md.sha256.create(), serverKey.toString('binary'));
|
||||
hmac.update(Buffer.from(signedLicense.msg).toString('binary'));
|
||||
const calculatedSignature = Buffer.from(hmac.digest().data, 'binary');
|
||||
const hmac = forge.hmac.create();
|
||||
hmac.start(forge.md.sha256.create(), serverKey.toString('binary'));
|
||||
hmac.update(Buffer.from(signedLicense.msg).toString('binary'));
|
||||
const calculatedSignature = Buffer.from(hmac.digest().data, 'binary');
|
||||
|
||||
if (!calculatedSignature.equals(signedLicense.signature)) {
|
||||
throw new Error('signatures do not match');
|
||||
if (!calculatedSignature.equals(signedLicense.signature)) {
|
||||
throw new Error('signatures do not match');
|
||||
}
|
||||
|
||||
const license = fromBinary(LicenseSchema, signedLicense.msg);
|
||||
|
||||
const keyContainers = license.key.map((keyContainer) => {
|
||||
if (keyContainer.type && keyContainer.key && keyContainer.iv) {
|
||||
const keyId = keyContainer.id ? Buffer.from(keyContainer.id).toString('hex') : '00000000000000000000000000000000';
|
||||
const decipher = forge.cipher.createDecipher('AES-CBC', encKey.toString('binary'));
|
||||
decipher.start({ iv: Buffer.from(keyContainer.iv).toString('binary') });
|
||||
decipher.update(forge.util.createBuffer(new Uint8Array(keyContainer.key)));
|
||||
decipher.finish();
|
||||
const decryptedKey = Buffer.from(decipher.output.data, 'binary');
|
||||
const key: KeyContainer = {
|
||||
kid: keyId,
|
||||
key: decryptedKey.toString('hex')
|
||||
};
|
||||
return key;
|
||||
}
|
||||
});
|
||||
if (keyContainers.filter((container) => !!container).length < 1) {
|
||||
throw new Error('there was not a single valid key in the response');
|
||||
}
|
||||
return keyContainers;
|
||||
}
|
||||
|
||||
const license = fromBinary(LicenseSchema, signedLicense.msg);
|
||||
private _encryptClientIdentification(clientIdentification: ClientIdentification, signedServiceCertificate: SignedDrmCertificate): EncryptedClientIdentification {
|
||||
if (!signedServiceCertificate.drmCertificate) {
|
||||
throw new Error('the service certificate does not contain an actual certificate');
|
||||
}
|
||||
|
||||
const keyContainers = license.key.map((keyContainer) => {
|
||||
if (keyContainer.type && keyContainer.key && keyContainer.iv) {
|
||||
const keyId = keyContainer.id ? Buffer.from(keyContainer.id).toString('hex') : '00000000000000000000000000000000';
|
||||
const decipher = forge.cipher.createDecipher('AES-CBC', encKey.toString('binary'));
|
||||
decipher.start({ iv: Buffer.from(keyContainer.iv).toString('binary') });
|
||||
decipher.update(forge.util.createBuffer(new Uint8Array(keyContainer.key)));
|
||||
decipher.finish();
|
||||
const decryptedKey = Buffer.from(decipher.output.data, 'binary');
|
||||
const key: KeyContainer = {
|
||||
kid: keyId,
|
||||
key: decryptedKey.toString('hex')
|
||||
};
|
||||
return key;
|
||||
}
|
||||
});
|
||||
if (keyContainers.filter((container) => !!container).length < 1) {
|
||||
throw new Error('there was not a single valid key in the response');
|
||||
}
|
||||
return keyContainers;
|
||||
}
|
||||
const serviceCertificate = fromBinary(DrmCertificateSchema, signedServiceCertificate.drmCertificate);
|
||||
if (!serviceCertificate.publicKey) {
|
||||
throw new Error('the service certificate does not contain a public key');
|
||||
}
|
||||
|
||||
private _encryptClientIdentification(clientIdentification: ClientIdentification, signedServiceCertificate: SignedDrmCertificate): EncryptedClientIdentification {
|
||||
if (!signedServiceCertificate.drmCertificate) {
|
||||
throw new Error('the service certificate does not contain an actual certificate');
|
||||
const key = forge.random.getBytesSync(16);
|
||||
const iv = forge.random.getBytesSync(16);
|
||||
const cipher = forge.cipher.createCipher('AES-CBC', key);
|
||||
cipher.start({ iv: iv });
|
||||
cipher.update(forge.util.createBuffer(toBinary(ClientIdentificationSchema, clientIdentification)));
|
||||
cipher.finish();
|
||||
const rawEncryptedClientIdentification = Buffer.from(cipher.output.data, 'binary');
|
||||
|
||||
const publicKey = forge.pki.publicKeyFromAsn1(forge.asn1.fromDer(Buffer.from(serviceCertificate.publicKey).toString('binary')));
|
||||
const encryptedKey = publicKey.encrypt(key, 'RSA-OAEP', { md: forge.md.sha1.create() });
|
||||
|
||||
const encryptedClientIdentification: EncryptedClientIdentification = create(EncryptedClientIdentificationSchema, {
|
||||
encryptedClientId: rawEncryptedClientIdentification,
|
||||
encryptedClientIdIv: Buffer.from(iv, 'binary'),
|
||||
encryptedPrivacyKey: Buffer.from(encryptedKey, 'binary'),
|
||||
providerId: serviceCertificate.providerId,
|
||||
serviceCertificateSerialNumber: serviceCertificate.serialNumber
|
||||
});
|
||||
return encryptedClientIdentification;
|
||||
}
|
||||
|
||||
const serviceCertificate = fromBinary(DrmCertificateSchema, signedServiceCertificate.drmCertificate);
|
||||
if (!serviceCertificate.publicKey) {
|
||||
throw new Error('the service certificate does not contain a public key');
|
||||
private async _verifyServiceCertificate(signedServiceCertificate: SignedDrmCertificate): Promise<boolean> {
|
||||
if (!signedServiceCertificate.drmCertificate) {
|
||||
throw new Error('the service certificate does not contain an actual certificate');
|
||||
}
|
||||
if (!signedServiceCertificate.signature) {
|
||||
throw new Error('the service certificate does not contain a signature');
|
||||
}
|
||||
|
||||
const publicKey = forge.pki.publicKeyFromAsn1(forge.asn1.fromDer(Buffer.from(WIDEVINE_ROOT_PUBLIC_KEY).toString('binary')));
|
||||
const pss: forge.pss.PSS = forge.pss.create({ md: forge.md.sha1.create(), mgf: forge.mgf.mgf1.create(forge.md.sha1.create()), saltLength: 20 });
|
||||
const sha1 = forge.md.sha1.create();
|
||||
sha1.update(Buffer.from(signedServiceCertificate.drmCertificate).toString('binary'), 'raw');
|
||||
return publicKey.verify(sha1.digest().bytes(), Buffer.from(signedServiceCertificate.signature).toString('binary'), pss);
|
||||
}
|
||||
|
||||
const key = forge.random.getBytesSync(16);
|
||||
const iv = forge.random.getBytesSync(16);
|
||||
const cipher = forge.cipher.createCipher('AES-CBC', key);
|
||||
cipher.start({ iv: iv });
|
||||
cipher.update(forge.util.createBuffer(toBinary(ClientIdentificationSchema, clientIdentification)));
|
||||
cipher.finish();
|
||||
const rawEncryptedClientIdentification = Buffer.from(cipher.output.data, 'binary');
|
||||
|
||||
const publicKey = forge.pki.publicKeyFromAsn1(forge.asn1.fromDer(Buffer.from(serviceCertificate.publicKey).toString('binary')));
|
||||
const encryptedKey = publicKey.encrypt(key, 'RSA-OAEP', { md: forge.md.sha1.create() });
|
||||
|
||||
const encryptedClientIdentification: EncryptedClientIdentification = create(EncryptedClientIdentificationSchema, {
|
||||
encryptedClientId: rawEncryptedClientIdentification,
|
||||
encryptedClientIdIv: Buffer.from(iv, 'binary'),
|
||||
encryptedPrivacyKey: Buffer.from(encryptedKey, 'binary'),
|
||||
providerId: serviceCertificate.providerId,
|
||||
serviceCertificateSerialNumber: serviceCertificate.serialNumber
|
||||
});
|
||||
return encryptedClientIdentification;
|
||||
}
|
||||
|
||||
private async _verifyServiceCertificate(signedServiceCertificate: SignedDrmCertificate): Promise<boolean> {
|
||||
if (!signedServiceCertificate.drmCertificate) {
|
||||
throw new Error('the service certificate does not contain an actual certificate');
|
||||
}
|
||||
if (!signedServiceCertificate.signature) {
|
||||
throw new Error('the service certificate does not contain a signature');
|
||||
private _parsePSSH(pssh: Buffer): WidevinePsshData | null {
|
||||
try {
|
||||
return fromBinary(WidevinePsshDataSchema, pssh.subarray(32));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const publicKey = forge.pki.publicKeyFromAsn1(forge.asn1.fromDer(Buffer.from(WIDEVINE_ROOT_PUBLIC_KEY).toString('binary')));
|
||||
const pss: forge.pss.PSS = forge.pss.create({ md: forge.md.sha1.create(), mgf: forge.mgf.mgf1.create(forge.md.sha1.create()), saltLength: 20 });
|
||||
const sha1 = forge.md.sha1.create();
|
||||
sha1.update(Buffer.from(signedServiceCertificate.drmCertificate).toString('binary'), 'raw');
|
||||
return publicKey.verify(sha1.digest().bytes(), Buffer.from(signedServiceCertificate.signature).toString('binary'), pss);
|
||||
}
|
||||
|
||||
private _parsePSSH(pssh: Buffer): WidevinePsshData | null {
|
||||
try {
|
||||
return fromBinary(WidevinePsshDataSchema, pssh.subarray(32));
|
||||
} catch {
|
||||
return null;
|
||||
private _generateAndroidIdentifier(): Buffer {
|
||||
return Buffer.from(`${forge.util.bytesToHex(forge.random.getBytesSync(8))}${'01'}${'00000000000000'}`);
|
||||
}
|
||||
}
|
||||
|
||||
private _generateAndroidIdentifier(): Buffer {
|
||||
return Buffer.from(`${forge.util.bytesToHex(forge.random.getBytesSync(8))}${'01'}${'00000000000000'}`);
|
||||
}
|
||||
private _generateGenericIdentifier(): Buffer {
|
||||
return Buffer.from(forge.random.getBytesSync(16), 'binary');
|
||||
}
|
||||
|
||||
private _generateGenericIdentifier(): Buffer {
|
||||
return Buffer.from(forge.random.getBytesSync(16), 'binary');
|
||||
}
|
||||
|
||||
get pssh(): Buffer {
|
||||
return this._pssh;
|
||||
}
|
||||
get pssh(): Buffer {
|
||||
return this._pssh;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue