Convert main files to tabs

Still need to do .tsx files and type declaration files
This commit is contained in:
AnimeDL 2025-09-26 10:41:01 -07:00
parent c94268ef6a
commit 460b4c1d0e
49 changed files with 11559 additions and 11559 deletions

1650
adn.ts

File diff suppressed because it is too large Load diff

1502
ao.ts

File diff suppressed because it is too large Load diff

5652
crunchy.ts

File diff suppressed because it is too large Load diff

View file

@ -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'
}
}
);

View file

@ -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);

View file

@ -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));
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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');
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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}`);
});
}
}

1960
hidive.ts

File diff suppressed because it is too large Load diff

174
index.ts
View file

@ -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();
}
})();

View file

@ -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');
});

View file

@ -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;
}

View file

@ -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 [];
}
}

View file

@ -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;

View file

@ -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();

View file

@ -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 };

View file

@ -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

View file

@ -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
};

View file

@ -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;

View file

@ -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 };

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;

View file

@ -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;

View file

@ -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
}
};
}
};
}
}
}

View file

@ -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,
};

View file

@ -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));
}
}

View 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;

View file

@ -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');
}

View file

@ -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;
}
}

View file

@ -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(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/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(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&')
.replace(/<[^>]>/g, '')
.replace(/\\N$/, '')
.replace(/ +$/, '');
text = xtext;
return { style, text };
}
function convertTime(tm: string) {
const m = tm.match(/([\d:]*)\.?(\d*)/);
if (!m) return '0:00:00.00';
return toSubTime(m[0]);
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)
);
}

View file

@ -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(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/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(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/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;

View file

@ -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;
}
}

View file

@ -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}`);
}
}
}
}

View file

@ -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

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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');
}
}

View file

@ -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();
}
}

View file

@ -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,
];
}
}

View file

@ -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)

View file

@ -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),
]);
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}