Compare commits
11 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40cfebb904 | ||
|
|
1d3822d307 | ||
|
|
a29e8a0d06 | ||
|
|
417656fd5f | ||
|
|
5b43fdcc77 | ||
|
|
e75aa80c3a | ||
|
|
ea85958191 | ||
|
|
0ce179b67a | ||
|
|
57fe5c289c | ||
|
|
8aff79244b | ||
|
|
e9c067c0b9 |
17 changed files with 507 additions and 208 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -33,3 +33,7 @@ gui/react/build/
|
|||
docker-compose.yml
|
||||
crunchyendpoints
|
||||
.vscode
|
||||
/logs
|
||||
/tmp/*/
|
||||
/videos/*/
|
||||
/tmp/*.*
|
||||
16
@types/crunchyTypes.d.ts
vendored
16
@types/crunchyTypes.d.ts
vendored
|
|
@ -31,6 +31,10 @@ export type CrunchyDownloadOptions = {
|
|||
dlVideoOnce: boolean,
|
||||
skipmux?: boolean,
|
||||
syncTiming: boolean,
|
||||
maxSecurityFrames: number,
|
||||
maxOffsetSeconds: number,
|
||||
targetLikeness: number,
|
||||
syncMethod: number,
|
||||
}
|
||||
|
||||
export type CurnchyMultiDownload = {
|
||||
|
|
@ -55,6 +59,10 @@ export type CrunchyMuxOptions = {
|
|||
defaultAudio: LanguageItem,
|
||||
ccTag: string,
|
||||
syncTiming: boolean,
|
||||
maxSecurityFrames: number;
|
||||
maxOffsetSeconds: number;
|
||||
targetLikeness: number;
|
||||
syncMethod: number;
|
||||
}
|
||||
|
||||
export type CrunchyEpMeta = {
|
||||
|
|
@ -84,7 +92,13 @@ export type DownloadedMedia = {
|
|||
isPrimary?: boolean
|
||||
} | ({
|
||||
type: 'Subtitle',
|
||||
cc: boolean
|
||||
cc: boolean,
|
||||
belongsToFile: {
|
||||
hasFile: false,
|
||||
} | {
|
||||
hasFile: true,
|
||||
file: string
|
||||
}
|
||||
} & sxItem )
|
||||
|
||||
export type ParseItem = {
|
||||
|
|
|
|||
10
@types/funiTypes.d.ts
vendored
10
@types/funiTypes.d.ts
vendored
|
|
@ -12,5 +12,11 @@ export type Subtitle = {
|
|||
lang: LanguageItem,
|
||||
ext: string,
|
||||
out?: string,
|
||||
closedCaption?: boolean
|
||||
}
|
||||
closedCaption?: boolean,
|
||||
belongsToFile: {
|
||||
hasFile: false,
|
||||
} | {
|
||||
hasFile: true,
|
||||
file: string
|
||||
}
|
||||
}
|
||||
|
|
|
|||
20
@types/hidiveTypes.d.ts
vendored
20
@types/hidiveTypes.d.ts
vendored
|
|
@ -1,3 +1,5 @@
|
|||
import { LanguageItem } from '../modules/module.langsData';
|
||||
|
||||
export interface HidiveVideoList {
|
||||
Code: number;
|
||||
Status: string;
|
||||
|
|
@ -50,12 +52,26 @@ export interface HidiveSubtitleInfo {
|
|||
url: string;
|
||||
}
|
||||
|
||||
export type sxItem = {
|
||||
language: LanguageItem,
|
||||
path: string,
|
||||
file: string
|
||||
title: string,
|
||||
fonts: Font[]
|
||||
}
|
||||
|
||||
export type DownloadedMedia = {
|
||||
type: 'Video',
|
||||
lang: LanguageItem,
|
||||
path: string,
|
||||
uncut: boolean
|
||||
uncut?: boolean
|
||||
} | ({
|
||||
type: 'Subtitle',
|
||||
cc: boolean
|
||||
cc: boolean,
|
||||
belongsToFile: {
|
||||
hasFile: false,
|
||||
} | {
|
||||
hasFile: true,
|
||||
file: string
|
||||
}
|
||||
} & sxItem )
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
ffmpeg: "ffmpeg.exe"
|
||||
mkvmerge: "mkvmerge.exe"
|
||||
ffprobe: "ffprobe.exe"
|
||||
|
|
|
|||
132
crunchy.ts
132
crunchy.ts
|
|
@ -47,7 +47,7 @@ export default class Crunchy implements ServiceClass {
|
|||
private token: Record<string, any>;
|
||||
private req: reqModule.Req;
|
||||
private cmsToken: {
|
||||
cms?: Record<string, string>
|
||||
cms?: Record<string, string>
|
||||
} = {};
|
||||
|
||||
constructor(private debug = false) {
|
||||
|
|
@ -65,7 +65,7 @@ export default class Crunchy implements ServiceClass {
|
|||
const argv = yargs.appArgv(this.cfg.cli);
|
||||
if (argv.debug)
|
||||
this.debug = true;
|
||||
|
||||
|
||||
// load binaries
|
||||
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||
if (argv.allDubs) {
|
||||
|
|
@ -491,7 +491,7 @@ export default class Crunchy implements ServiceClass {
|
|||
tMetadata = item.type + '_metadata',
|
||||
iMetadata = (Object.prototype.hasOwnProperty.call(item, tMetadata) ? item[tMetadata as keyof ParseItem] : item) as Record<string, any>,
|
||||
iTitle = [ item.title ];
|
||||
|
||||
|
||||
const audio_languages: string[] = [];
|
||||
|
||||
// set object booleans
|
||||
|
|
@ -581,7 +581,7 @@ export default class Crunchy implements ServiceClass {
|
|||
iTitle.join(' - '),
|
||||
showObjectMetadata ? ` (${oMetadata.join(', ')})` : '',
|
||||
showObjectBooleans ? ` [${oBooleans.join(', ')}]` : '',
|
||||
|
||||
|
||||
);
|
||||
if(item.last_public){
|
||||
console.info(''.padStart(pad+1, ' '), '- Last updated:', item.last_public);
|
||||
|
|
@ -704,7 +704,7 @@ export default class Crunchy implements ServiceClass {
|
|||
this.logObject(item, pad+2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async getNewlyAdded(page?: number){
|
||||
if(!this.token.access_token){
|
||||
console.error('Authentication required!');
|
||||
|
|
@ -751,7 +751,7 @@ export default class Crunchy implements ServiceClass {
|
|||
useProxy: true
|
||||
};
|
||||
|
||||
|
||||
|
||||
//get show info
|
||||
const showInfoReq = await this.req.getData(`${api.cms}/seasons/${id}?preferred_audio_language=ja-JP`, AuthHeaders);
|
||||
if(!showInfoReq.ok || !showInfoReq.res){
|
||||
|
|
@ -768,21 +768,21 @@ export default class Crunchy implements ServiceClass {
|
|||
return { isOk: false, reason: new Error('Episode List request failed. No more information provided.') };
|
||||
}
|
||||
const episodeList = JSON.parse(reqEpsList.res.body) as CrunchyEpisodeList;
|
||||
|
||||
|
||||
const epNumList: {
|
||||
ep: number[],
|
||||
sp: number
|
||||
} = { ep: [], sp: 0 };
|
||||
const epNumLen = numbers;
|
||||
|
||||
|
||||
if(episodeList.total < 1){
|
||||
console.info(' Season is empty!');
|
||||
return { isOk: true, value: [] };
|
||||
}
|
||||
|
||||
|
||||
const doEpsFilter = parseSelect(e as string);
|
||||
const selectedMedia: CrunchyEpMeta[] = [];
|
||||
|
||||
|
||||
episodeList.data.forEach((item) => {
|
||||
item.hide_season_title = true;
|
||||
if(item.season_title == '' && item.series_title != ''){
|
||||
|
|
@ -805,7 +805,7 @@ export default class Crunchy implements ServiceClass {
|
|||
epNumList.ep.push(parseInt(epNum, 10));
|
||||
}
|
||||
const selEpId = (
|
||||
isSpecial
|
||||
isSpecial
|
||||
? 'S' + epNumList.sp.toString().padStart(epNumLen, '0')
|
||||
: '' + parseInt(epNum, 10).toString().padStart(epNumLen, '0')
|
||||
);
|
||||
|
|
@ -850,12 +850,12 @@ export default class Crunchy implements ServiceClass {
|
|||
item.seq_id = selEpId;
|
||||
this.logObject(item);
|
||||
});
|
||||
|
||||
|
||||
// display
|
||||
if(selectedMedia.length < 1){
|
||||
console.info('\nEpisodes not selected!\n');
|
||||
}
|
||||
|
||||
|
||||
console.info('');
|
||||
return { isOk: true, value: selectedMedia };
|
||||
}
|
||||
|
|
@ -908,7 +908,7 @@ export default class Crunchy implements ServiceClass {
|
|||
'Key-Pair-Id': this.cmsToken.cms.key_pair_id,
|
||||
}),
|
||||
].join('');
|
||||
|
||||
|
||||
const extIdReq = await this.req.getData(extIdReqOpts);
|
||||
if (!extIdReq.ok || !extIdReq.res) {
|
||||
console.error('Objects Request FAILED!');
|
||||
|
|
@ -917,7 +917,7 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
const oldObjectInfo = JSON.parse(extIdReq.res.body) as Record<any, any>;
|
||||
for (const object of oldObjectInfo.items) {
|
||||
objectIds.push(object.id);
|
||||
|
|
@ -925,17 +925,17 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
convertedObjects = objectIds.join(',');
|
||||
}
|
||||
|
||||
|
||||
const doEpsFilter = parseSelect(convertedObjects ?? e as string);
|
||||
|
||||
|
||||
if(doEpsFilter.values.length < 1){
|
||||
console.info('\nObjects not selected!\n');
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
// node index.js --service crunchy -e G6497Z43Y,GRZXCMN1W,G62PEZ2E6,G25FVGDEK,GZ7UVPVX5
|
||||
console.info('Requested object ID: %s', doEpsFilter.values.join(', '));
|
||||
|
||||
|
||||
const AuthHeaders = {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token.access_token}`,
|
||||
|
|
@ -955,14 +955,14 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
const objectInfo = JSON.parse(objectReq.res.body) as ObjectInfo;
|
||||
if(earlyReturn){
|
||||
return objectInfo;
|
||||
}
|
||||
|
||||
|
||||
const selectedMedia: Partial<CrunchyEpMeta>[] = [];
|
||||
|
||||
|
||||
for(const item of objectInfo.data){
|
||||
if(item.type != 'episode' && item.type != 'movie'){
|
||||
await this.logObject(item, 2, true, false);
|
||||
|
|
@ -980,7 +980,7 @@ export default class Crunchy implements ServiceClass {
|
|||
isSubbed: item.episode_metadata.is_subbed,
|
||||
isDubbed: item.episode_metadata.is_dubbed
|
||||
}
|
||||
];
|
||||
];
|
||||
epMeta.seriesTitle = item.episode_metadata.series_title;
|
||||
epMeta.seasonTitle = item.episode_metadata.season_title;
|
||||
epMeta.episodeNumber = item.episode_metadata.episode;
|
||||
|
|
@ -1044,9 +1044,9 @@ export default class Crunchy implements ServiceClass {
|
|||
if(medias.seasonTitle && medias.episodeNumber && medias.episodeTitle){
|
||||
mediaName = `${medias.seasonTitle} - ${medias.episodeNumber} - ${medias.episodeTitle}`;
|
||||
}
|
||||
|
||||
|
||||
const files: DownloadedMedia[] = [];
|
||||
|
||||
|
||||
if(medias.data.every(a => !a.playback)){
|
||||
console.warn('Video not available!');
|
||||
return undefined;
|
||||
|
|
@ -1054,11 +1054,11 @@ export default class Crunchy implements ServiceClass {
|
|||
|
||||
let dlFailed = false;
|
||||
let dlVideoOnce = false; // Variable to save if best selected video quality was downloaded
|
||||
|
||||
|
||||
|
||||
for (const mMeta of medias.data) {
|
||||
console.info(`Requesting: [${mMeta.mediaId}] ${mediaName}`);
|
||||
|
||||
|
||||
//Make sure token is up to date
|
||||
await this.refreshToken(true, true);
|
||||
let currentVersion;
|
||||
|
|
@ -1095,9 +1095,9 @@ export default class Crunchy implements ServiceClass {
|
|||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const pbData = JSON.parse(playbackReq.res.body) as PlaybackData;
|
||||
|
||||
|
||||
variables.push(...([
|
||||
['title', medias.episodeTitle, true],
|
||||
['episode', isNaN(parseInt(medias.episodeNumber)) ? medias.episodeNumber : parseInt(medias.episodeNumber), false],
|
||||
|
|
@ -1113,7 +1113,7 @@ export default class Crunchy implements ServiceClass {
|
|||
sanitize: a[2]
|
||||
} as Variable;
|
||||
}));
|
||||
|
||||
|
||||
let streams: any[] = [];
|
||||
let hsLangs: string[] = [];
|
||||
const pbStreams = pbData.data[0];
|
||||
|
|
@ -1121,29 +1121,29 @@ export default class Crunchy implements ServiceClass {
|
|||
for(const s of Object.keys(pbStreams)){
|
||||
if(s.match(/hls/) && !s.match(/drm/) && !s.match(/trailer/)) {
|
||||
const pb = Object.values(pbStreams[s]).map(v => {
|
||||
v.hardsub_lang = v.hardsub_locale
|
||||
v.hardsub_lang = v.hardsub_locale
|
||||
? langsData.fixAndFindCrLC(v.hardsub_locale).locale
|
||||
: v.hardsub_locale;
|
||||
if(v.hardsub_lang && hsLangs.indexOf(v.hardsub_lang) < 0){
|
||||
hsLangs.push(v.hardsub_lang);
|
||||
}
|
||||
return {
|
||||
...v,
|
||||
return {
|
||||
...v,
|
||||
...{ format: s }
|
||||
};
|
||||
});
|
||||
streams.push(...pb);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(streams.length < 1){
|
||||
console.warn('No full streams found!');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
const audDub = langsData.findLang(langsData.fixLanguageTag(pbData.meta.audio_locale as string) || '').code;
|
||||
hsLangs = langsData.sortTags(hsLangs);
|
||||
|
||||
|
||||
streams = streams.map((s) => {
|
||||
s.audio_lang = audDub;
|
||||
s.hardsub_lang = s.hardsub_lang ? s.hardsub_lang : '-';
|
||||
|
|
@ -1157,7 +1157,7 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
|
||||
if(options.hslang != 'none'){
|
||||
if(hsLangs.indexOf(options.hslang) > -1){
|
||||
console.info('Selecting stream with %s hardsubs', langsData.locale2language(options.hslang).language);
|
||||
|
|
@ -1192,25 +1192,27 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
console.info('Selecting raw stream');
|
||||
}
|
||||
|
||||
|
||||
let curStream:
|
||||
undefined|typeof streams[0]
|
||||
= undefined;
|
||||
if(!dlFailed){
|
||||
options.kstream = typeof options.kstream == 'number' ? options.kstream : 1;
|
||||
options.kstream = options.kstream > streams.length ? 1 : options.kstream;
|
||||
|
||||
|
||||
streams.forEach((s, i) => {
|
||||
const isSelected = options.kstream == i + 1 ? '✓' : ' ';
|
||||
console.info('Full stream found! (%s%s: %s )', isSelected, i + 1, s.type);
|
||||
console.info('Full stream found! (%s%s: %s )', isSelected, i + 1, s.type);
|
||||
});
|
||||
|
||||
|
||||
console.info('Downloading video...');
|
||||
curStream = streams[options.kstream-1];
|
||||
|
||||
|
||||
console.info('Playlists URL: %s (%s)', curStream.url, curStream.type);
|
||||
}
|
||||
|
||||
|
||||
let tsFile = undefined;
|
||||
|
||||
if(!options.novids && !dlFailed && curStream !== undefined){
|
||||
const streamPlaylistsReq = await this.req.getData(curStream.url);
|
||||
if(!streamPlaylistsReq.ok || !streamPlaylistsReq.res){
|
||||
|
|
@ -1274,7 +1276,7 @@ export default class Crunchy implements ServiceClass {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
options.x = options.x > plServerList.length ? 1 : options.x;
|
||||
|
||||
const plSelectedServer = plServerList[options.x - 1];
|
||||
|
|
@ -1299,7 +1301,7 @@ export default class Crunchy implements ServiceClass {
|
|||
const selPlUrl = plSelectedList[plQuality.map(a => a.dim)[quality - 1]] ? plSelectedList[plQuality.map(a => a.dim)[quality - 1]] : '';
|
||||
console.info(`Servers available:\n\t${plServerList.join('\n\t')}`);
|
||||
console.info(`Available qualities:\n\t${plQuality.map((a, ind) => `[${ind+1}] ${a.str}`).join('\n\t')}`);
|
||||
|
||||
|
||||
if(selPlUrl != ''){
|
||||
variables.push({
|
||||
name: 'height',
|
||||
|
|
@ -1332,7 +1334,7 @@ export default class Crunchy implements ServiceClass {
|
|||
const mathParts = Math.ceil(totalParts / options.partsize);
|
||||
const mathMsg = `(${mathParts}*${options.partsize})`;
|
||||
console.info('Total parts in stream:', totalParts, mathMsg);
|
||||
const tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile);
|
||||
tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile);
|
||||
const split = outFile.split(path.sep).slice(0, -1);
|
||||
split.forEach((val, ind, arr) => {
|
||||
const isAbsolut = path.isAbsolute(outFile as string);
|
||||
|
|
@ -1380,12 +1382,12 @@ export default class Crunchy implements ServiceClass {
|
|||
fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep);
|
||||
console.info('Downloading skipped!');
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
if(options.dlsubs.indexOf('all') > -1){
|
||||
options.dlsubs = ['all'];
|
||||
}
|
||||
|
||||
|
||||
if(options.hslang != 'none'){
|
||||
console.warn('Subtitles downloading disabled for hardsubs streams.');
|
||||
options.skipsubs = true;
|
||||
|
|
@ -1395,7 +1397,7 @@ export default class Crunchy implements ServiceClass {
|
|||
console.info('Subtitles downloading disabled from nosubs flag.');
|
||||
options.skipsubs = true;
|
||||
}
|
||||
|
||||
|
||||
if(!options.skipsubs && options.dlsubs.indexOf('none') == -1){
|
||||
if(pbData.meta.subtitles && Object.values(pbData.meta.subtitles).length > 0){
|
||||
const subsData = Object.values(pbData.meta.subtitles);
|
||||
|
|
@ -1433,7 +1435,8 @@ export default class Crunchy implements ServiceClass {
|
|||
files.push({
|
||||
type: 'Subtitle',
|
||||
...sxData as sxItem,
|
||||
cc: isCC
|
||||
cc: isCC,
|
||||
belongsToFile: tsFile === undefined ? { hasFile: false } : { hasFile: true, file: `${tsFile}.ts`}
|
||||
});
|
||||
}
|
||||
else{
|
||||
|
|
@ -1473,7 +1476,8 @@ export default class Crunchy implements ServiceClass {
|
|||
return {
|
||||
file: a.path,
|
||||
language: a.language,
|
||||
closedCaption: a.cc
|
||||
closedCaption: a.cc,
|
||||
belongsToFile: a.belongsToFile
|
||||
};
|
||||
}),
|
||||
simul: false,
|
||||
|
|
@ -1503,7 +1507,7 @@ export default class Crunchy implements ServiceClass {
|
|||
// mergers
|
||||
let isMuxed = false;
|
||||
if (options.syncTiming) {
|
||||
await merger.createDelays();
|
||||
await merger.createDelays(options.syncMethod, options.targetLikeness, options.maxOffsetSeconds, options.maxSecurityFrames);
|
||||
}
|
||||
if (bin.MKVmerge) {
|
||||
await merger.merge('mkvmerge', bin.MKVmerge);
|
||||
|
|
@ -1573,7 +1577,7 @@ export default class Crunchy implements ServiceClass {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const itemIndexes = {
|
||||
sp: 1,
|
||||
no: 1
|
||||
|
|
@ -1585,11 +1589,11 @@ export default class Crunchy implements ServiceClass {
|
|||
episodes[`${isSpecial ? 'S' : 'E'}${itemIndexes[isSpecial ? 'sp' : 'no']}`] = item;
|
||||
if (isSpecial)
|
||||
itemIndexes.sp++;
|
||||
else
|
||||
else
|
||||
itemIndexes.no++;
|
||||
delete episodes[key];
|
||||
}
|
||||
|
||||
|
||||
for (const key of Object.keys(episodes)) {
|
||||
const item = episodes[key];
|
||||
console.info(`[${key}] ${
|
||||
|
|
@ -1648,9 +1652,9 @@ export default class Crunchy implements ServiceClass {
|
|||
langs: langsData.LanguageItem[]
|
||||
}>, dubLang: string[], but?: boolean, all?: boolean, e?: string, ) {
|
||||
const doEpsFilter = parseSelect(e as string);
|
||||
|
||||
|
||||
const ret: Record<string, CrunchyEpMeta> = {};
|
||||
|
||||
|
||||
for (const key of Object.keys(eps)) {
|
||||
const itemE = eps[key];
|
||||
itemE.items.forEach((item, index) => {
|
||||
|
|
@ -1677,7 +1681,7 @@ export default class Crunchy implements ServiceClass {
|
|||
isSubbed: item.is_subbed,
|
||||
isDubbed: item.is_dubbed
|
||||
}
|
||||
],
|
||||
],
|
||||
seriesTitle: itemE.items.find(a => !a.series_title.match(/\(\w+ Dub\)/))?.series_title ?? itemE.items[0].series_title.replace(/\(\w+ Dub\)/g, '').trimEnd(),
|
||||
seasonTitle: itemE.items.find(a => !a.season_title.match(/\(\w+ Dub\)/))?.season_title ?? itemE.items[0].season_title.replace(/\(\w+ Dub\)/g, '').trimEnd(),
|
||||
episodeNumber: item.episode,
|
||||
|
|
@ -1715,7 +1719,7 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
public parseSeriesResult (seasonsList: SeriesSearch) : Record<number, Record<string, SeriesSearchItem>> {
|
||||
const ret: Record<number, Record<string, SeriesSearchItem>> = {};
|
||||
let i = 0;
|
||||
|
|
@ -1743,7 +1747,7 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
public async parseSeriesById(id: string) {
|
||||
if(!this.cmsToken.cms){
|
||||
console.error('Authentication required!');
|
||||
|
|
@ -1771,7 +1775,7 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
return seasonsList;
|
||||
}
|
||||
|
||||
|
||||
public async getSeasonDataById(item: SeriesSearchItem, log = false){
|
||||
if(!this.cmsToken.cms){
|
||||
console.error('Authentication required!');
|
||||
|
|
@ -1801,7 +1805,7 @@ export default class Crunchy implements ServiceClass {
|
|||
return;
|
||||
}
|
||||
const episodeList = JSON.parse(reqEpsList.res.body) as CrunchyEpisodeList;
|
||||
|
||||
|
||||
if(episodeList.total < 1){
|
||||
console.info(' Season is empty!');
|
||||
return;
|
||||
|
|
@ -1809,4 +1813,4 @@ export default class Crunchy implements ServiceClass {
|
|||
return episodeList;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ This application is not endorsed by or affiliated with *Funimation*, *Crunchyrol
|
|||
By default this application uses the following paths to programs (main executables):
|
||||
|
||||
* `ffmpeg.exe` (From PATH)
|
||||
* `ffprobe.exe` (From PATH)
|
||||
* `mkvmerge.exe` (From PATH)
|
||||
|
||||
To change these paths you need to edit `bin-path.yml` in `./config/` directory.
|
||||
|
|
|
|||
112
funi.ts
112
funi.ts
|
|
@ -37,7 +37,7 @@ import { TitleElement } from './@types/episode';
|
|||
import { AvailableFilenameVars } from './modules/module.args';
|
||||
import { AuthData, AuthResponse, CheckTokenResponse, FuniGetEpisodeData, FuniGetEpisodeResponse, FuniGetShowData, SearchData, FuniSearchReponse, FuniShowResponse, FuniStreamData, FuniSubsData, FuniEpisodeData, ResponseBase } from './@types/messageHandler';
|
||||
import { ServiceClass } from './@types/serviceClassInterface';
|
||||
import { SubtitleRequest } from './@types/funiSubtitleRequest';
|
||||
import { SubtitleRequest } from './@types/funiSubtitleRequest';
|
||||
|
||||
// program name
|
||||
const api_host = 'https://prod-api-funimationnow.dadcdigital.com/api';
|
||||
|
|
@ -213,10 +213,10 @@ export default class Funi implements ServiceClass {
|
|||
debug: this.debug,
|
||||
});
|
||||
if(!episodesData.ok || !episodesData.res){ return { isOk: false, reason: new Error('episodesData is not ok') }; }
|
||||
|
||||
|
||||
let epsDataArr: Item[] = JSON.parse(episodesData.res.body).items;
|
||||
const epNumRegex = /^([A-Z0-9]*[A-Z])?(\d+)$/i;
|
||||
|
||||
|
||||
const parseEpStr = (epStr: string) => {
|
||||
const match = epStr.match(epNumRegex);
|
||||
if (!match) {
|
||||
|
|
@ -230,7 +230,7 @@ export default class Funi implements ServiceClass {
|
|||
}
|
||||
else return [ '', match[0] ];
|
||||
};
|
||||
|
||||
|
||||
epsDataArr = epsDataArr.map(e => {
|
||||
const baseId = e.ids.externalAsianId ? e.ids.externalAsianId : e.ids.externalEpisodeId;
|
||||
e.id = baseId.replace(new RegExp('^' + e.ids.externalShowId), '');
|
||||
|
|
@ -247,7 +247,7 @@ export default class Funi implements ServiceClass {
|
|||
}
|
||||
return e;
|
||||
});
|
||||
|
||||
|
||||
epsDataArr.sort((a, b) => {
|
||||
if (a.item.seasonOrder < b.item.seasonOrder && a.id.localeCompare(b.id) < 0) {
|
||||
return -1;
|
||||
|
|
@ -269,7 +269,7 @@ export default class Funi implements ServiceClass {
|
|||
const epSelList = parseSelect(data.e as string, data.but);
|
||||
const fnSlug: FuniEpisodeData[] = [], epSelEpsTxt: string[] = []; let is_selected = false;
|
||||
|
||||
|
||||
|
||||
for(const e in eps){
|
||||
eps[e].id_split[1] = parseInt(eps[e].id_split[1].toString()).toString().padStart(Funi.epIdLen, '0');
|
||||
let epStrId = eps[e].id_split.join('');
|
||||
|
|
@ -339,13 +339,13 @@ export default class Funi implements ServiceClass {
|
|||
ep.number = ep.number !== '' ? ep.mediaCategory+ep.number : ep.mediaCategory+'#'+ep.id;
|
||||
}
|
||||
fnEpNum = isNaN(parseInt(ep.number)) ? ep.number : parseInt(ep.number);
|
||||
|
||||
|
||||
// is uncut
|
||||
const uncut = {
|
||||
Japanese: false,
|
||||
English: false
|
||||
};
|
||||
|
||||
|
||||
// end
|
||||
if (log) {
|
||||
console.info(
|
||||
|
|
@ -355,7 +355,7 @@ export default class Funi implements ServiceClass {
|
|||
(ep.number ? ep.number : '?'),
|
||||
ep.title
|
||||
);
|
||||
|
||||
|
||||
console.info('Available streams (Non-Encrypted):');
|
||||
}
|
||||
// map medias
|
||||
|
|
@ -376,7 +376,7 @@ export default class Funi implements ServiceClass {
|
|||
return { id: 0, type: '' };
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
// select
|
||||
stDlPath = [];
|
||||
for(const m of media){
|
||||
|
|
@ -386,8 +386,8 @@ export default class Funi implements ServiceClass {
|
|||
if (!dub_type)
|
||||
continue;
|
||||
let localSubs: Subtitle[] = [];
|
||||
const selUncut = !data.simul && uncut[dub_type] && m.version?.match(/uncut/i)
|
||||
? true
|
||||
const selUncut = !data.simul && uncut[dub_type] && m.version?.match(/uncut/i)
|
||||
? true
|
||||
: (!uncut[dub_type] || data.simul && m.version?.match(/simulcast/i) ? true : false);
|
||||
for (const curDub of data.dubLang) {
|
||||
const item = langsData.languages.find(a => a.code === curDub);
|
||||
|
|
@ -413,7 +413,7 @@ export default class Funi implements ServiceClass {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const already: string[] = [];
|
||||
stDlPath = stDlPath.filter(a => {
|
||||
if (already.includes(`${a.closedCaption ? 'cc' : ''}-${a.lang.code}`)) {
|
||||
|
|
@ -481,33 +481,33 @@ export default class Funi implements ServiceClass {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async downloadStreams(log: boolean, episode: FunimationMediaDownload, data: FuniStreamData): Promise<boolean|void> {
|
||||
|
||||
|
||||
// req playlist
|
||||
|
||||
|
||||
const purvideo: DownloadedFile[] = [];
|
||||
const puraudio: DownloadedFile[] = [];
|
||||
const audioAndVideo: DownloadedFile[] = [];
|
||||
const audioAndVideo: DownloadedFile[] = [];
|
||||
for (const streamPath of tsDlPath) {
|
||||
const plQualityReq = await getData({
|
||||
url: streamPath.path,
|
||||
debug: this.debug,
|
||||
});
|
||||
if(!plQualityReq.ok || !plQualityReq.res){return;}
|
||||
|
||||
|
||||
const plQualityLinkList = m3u8(plQualityReq.res.body);
|
||||
|
||||
|
||||
const mainServersList = [
|
||||
'vmfst-api.prd.funimationsvc.com',
|
||||
'd33et77evd9bgg.cloudfront.net',
|
||||
'd132fumi6di1wa.cloudfront.net',
|
||||
'funiprod.akamaized.net',
|
||||
];
|
||||
|
||||
|
||||
const plServerList: string[] = [],
|
||||
plStreams: Record<string|number, {
|
||||
[key: string]: string
|
||||
[key: string]: string
|
||||
}> = {},
|
||||
plLayersStr: string[] = [],
|
||||
plLayersRes: Record<string|number, {
|
||||
|
|
@ -520,7 +520,7 @@ export default class Funi implements ServiceClass {
|
|||
uri: string
|
||||
language: langsData.LanguageItem
|
||||
};
|
||||
|
||||
|
||||
// new uris
|
||||
const vplReg = /streaming_video_(\d+)_(\d+)_(\d+)_index\.m3u8/;
|
||||
if(plQualityLinkList.playlists[0].uri.match(vplReg)){
|
||||
|
|
@ -562,7 +562,7 @@ export default class Funi implements ServiceClass {
|
|||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
for(const s of plQualityLinkList.playlists){
|
||||
if(s.uri.match(/_Layer(\d+)\.m3u8/) || s.uri.match(vplReg)){
|
||||
// set layer and max layer
|
||||
|
|
@ -608,7 +608,7 @@ export default class Funi implements ServiceClass {
|
|||
console.info(s.uri);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for(const s of mainServersList){
|
||||
if(plServerList.includes(s)){
|
||||
plServerList.splice(plServerList.indexOf(s), 1);
|
||||
|
|
@ -616,28 +616,28 @@ export default class Funi implements ServiceClass {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const plSelectedServer = plServerList[data.x-1];
|
||||
const plSelectedList = plStreams[plSelectedServer];
|
||||
|
||||
|
||||
plLayersStr.sort();
|
||||
if (log) {
|
||||
console.info(`Servers available:\n\t${plServerList.join('\n\t')}`);
|
||||
console.info(`Available qualities:\n\t${plLayersStr.join('\n\t')}`);
|
||||
}
|
||||
|
||||
|
||||
const selectedQuality = data.q === 0 || data.q > Object.keys(plLayersRes).length
|
||||
? Object.keys(plLayersRes).pop() as string
|
||||
: data.q;
|
||||
const videoUrl = data.x < plServerList.length+1 && plSelectedList[selectedQuality] ? plSelectedList[selectedQuality] : '';
|
||||
|
||||
|
||||
if(videoUrl != ''){
|
||||
if (log) {
|
||||
console.info(`Selected layer: ${selectedQuality} (${plLayersRes[selectedQuality].width}x${plLayersRes[selectedQuality].height}) @ ${plSelectedServer}`);
|
||||
console.info('Stream URL:',videoUrl);
|
||||
}
|
||||
|
||||
|
||||
fnOutput = parseFileName(data.fileName, ([
|
||||
['episode', isNaN(parseInt(fnEpNum as string)) ? fnEpNum : parseInt(fnEpNum as string), true],
|
||||
['title', episode.title, true],
|
||||
|
|
@ -669,12 +669,12 @@ export default class Funi implements ServiceClass {
|
|||
console.error('Layer not selected!\n');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let dlFailed = false;
|
||||
let dlFailedA = false;
|
||||
|
||||
|
||||
await fs.promises.mkdir(path.join(this.cfg.dir.content, ...fnOutput.slice(0, -1)), { recursive: true });
|
||||
|
||||
|
||||
video: if (!data.novids) {
|
||||
if (plAud && (purvideo.length > 0 || audioAndVideo.length > 0)) {
|
||||
break video;
|
||||
|
|
@ -687,9 +687,9 @@ export default class Funi implements ServiceClass {
|
|||
debug: this.debug,
|
||||
});
|
||||
if (!reqVideo.ok || !reqVideo.res) { break video; }
|
||||
|
||||
|
||||
const chunkList = m3u8(reqVideo.res.body);
|
||||
|
||||
|
||||
const tsFile = path.join(this.cfg.dir.content, ...fnOutput.slice(0, -1), `${fnOutput.slice(-1)}.video${(plAud?.uri ? '' : '.' + streamPath.lang.code )}`);
|
||||
dlFailed = !await this.downloadFile(tsFile, chunkList, data.timeout, data.partsize, data.fsRetryTime, data.force, data.callbackMaker ? data.callbackMaker({
|
||||
fileName: `${fnOutput.slice(-1)}.video${(plAud?.uri ? '' : '.' + streamPath.lang.code )}.ts`,
|
||||
|
|
@ -727,11 +727,11 @@ export default class Funi implements ServiceClass {
|
|||
debug: this.debug,
|
||||
});
|
||||
if (!reqAudio.ok || !reqAudio.res) { return; }
|
||||
|
||||
|
||||
const chunkListA = m3u8(reqAudio.res.body);
|
||||
|
||||
|
||||
const tsFileA = path.join(this.cfg.dir.content, ...fnOutput.slice(0, -1), `${fnOutput.slice(-1)}.audio.${plAud.language.code}`);
|
||||
|
||||
|
||||
dlFailedA = !await this.downloadFile(tsFileA, chunkListA, data.timeout, data.partsize, data.fsRetryTime, data.force, data.callbackMaker ? data.callbackMaker({
|
||||
fileName: `${fnOutput.slice(-1)}.audio.${plAud.language.code}.ts`,
|
||||
parent: {
|
||||
|
|
@ -746,14 +746,14 @@ export default class Funi implements ServiceClass {
|
|||
path: `${tsFileA}.ts`,
|
||||
lang: plAud.language
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// add subs
|
||||
const subsExt = !data.mp4 || data.mp4 && data.ass ? '.ass' : '.srt';
|
||||
let addSubs = true;
|
||||
|
||||
|
||||
// download subtitles
|
||||
if(stDlPath.length > 0){
|
||||
if (log)
|
||||
|
|
@ -778,28 +778,28 @@ export default class Funi implements ServiceClass {
|
|||
if (addSubs && log)
|
||||
console.info('Subtitles downloaded!');
|
||||
}
|
||||
|
||||
|
||||
if((puraudio.length < 1 && audioAndVideo.length < 1) || (purvideo.length < 1 && audioAndVideo.length < 1)){
|
||||
if (log)
|
||||
console.info('\nUnable to locate a video AND audio file\n');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if(data.skipmux){
|
||||
if (log)
|
||||
console.info('Skipping muxing...');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// check exec
|
||||
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||
const mergerBin = merger.checkMerger(this.cfg.bin, data.mp4, data.forceMuxer);
|
||||
|
||||
|
||||
if ( data.novids ){
|
||||
if (log)
|
||||
console.info('Video not downloaded. Skip muxing video.');
|
||||
}
|
||||
|
||||
|
||||
const ffext = !data.mp4 ? 'mkv' : 'mp4';
|
||||
const mergeInstance = new merger({
|
||||
onlyAudio: puraudio,
|
||||
|
|
@ -810,7 +810,8 @@ export default class Funi implements ServiceClass {
|
|||
file: a.out as string,
|
||||
language: a.lang,
|
||||
title: a.lang.name,
|
||||
closedCaption: a.closedCaption
|
||||
closedCaption: a.closedCaption,
|
||||
belongsToFile: a.belongsToFile
|
||||
};
|
||||
}),
|
||||
videoAndAudio: audioAndVideo,
|
||||
|
|
@ -827,7 +828,7 @@ export default class Funi implements ServiceClass {
|
|||
},
|
||||
ccTag: data.ccTag
|
||||
});
|
||||
|
||||
|
||||
if(mergerBin.MKVmerge){
|
||||
await mergeInstance.merge('mkvmerge', mergerBin.MKVmerge);
|
||||
}
|
||||
|
|
@ -842,13 +843,13 @@ export default class Funi implements ServiceClass {
|
|||
if (data.nocleanup) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
mergeInstance.cleanUp();
|
||||
if (log)
|
||||
console.info('\nDone!\n');
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public async downloadFile(filename: string, chunkList: {
|
||||
segments: Record<string, unknown>[],
|
||||
}, timeout: number, partsize: number, fsRetryTime: number, override?: 'Y' | 'y' | 'N' | 'n' | 'C' | 'c', callback?: HLSCallback) {
|
||||
|
|
@ -861,7 +862,7 @@ export default class Funi implements ServiceClass {
|
|||
override,
|
||||
callback
|
||||
}).download();
|
||||
|
||||
|
||||
return downloadStatus.ok;
|
||||
}
|
||||
|
||||
|
|
@ -869,7 +870,7 @@ export default class Funi implements ServiceClass {
|
|||
if((data.nosubs && !data.sub) || data.dlsubs.includes('none')){
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
const subs = await getData({
|
||||
baseUrl: 'https://playback.prd.funimationsvc.com/v1/play',
|
||||
url: `/${episodeID}`,
|
||||
|
|
@ -883,7 +884,7 @@ export default class Funi implements ServiceClass {
|
|||
return [];
|
||||
}
|
||||
const parsed: SubtitleRequest = JSON.parse(subs.res.body);
|
||||
|
||||
|
||||
const found: {
|
||||
isCC: boolean;
|
||||
url: string;
|
||||
|
|
@ -916,8 +917,9 @@ export default class Funi implements ServiceClass {
|
|||
ext: `.${a.lang.code}${a.isCC ? `.${ccTag}` : ''}`,
|
||||
lang: a.lang,
|
||||
url: a.url,
|
||||
closedCaption: a.isCC
|
||||
closedCaption: a.isCC,
|
||||
belongsToFile: { hasFile: false }
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
22
hidive.ts
22
hidive.ts
|
|
@ -257,6 +257,10 @@ export default class Hidive implements ServiceClass {
|
|||
};
|
||||
}
|
||||
|
||||
if (this.debug) {
|
||||
console.debug(apiReq.res.body);
|
||||
}
|
||||
|
||||
if (!isGet && apiReq.res.headers && apiReq.res.headers['set-cookie']) {
|
||||
const newReqCookies = shlp.cookie.parse(apiReq.res.headers['set-cookie'] as unknown as Record<string, string>);
|
||||
this.session = Object.assign(this.session, newReqCookies);
|
||||
|
|
@ -750,10 +754,21 @@ export default class Hidive implements ServiceClass {
|
|||
sxData.fonts = fontsData.assFonts(sBody) as Font[];
|
||||
fs.writeFileSync(sxData.path, sBody);
|
||||
console.info(`Subtitle downloaded: ${sxData.file}`);
|
||||
let tsFile;
|
||||
for (const file of files) {
|
||||
if (file.type == 'Video') {
|
||||
if (sub.cc && file.lang.code == 'eng') {
|
||||
tsFile = `${file.path}.ts`;
|
||||
} else if (!sub.cc && file.lang.code == 'jpn') {
|
||||
tsFile = `${file.path}.ts`;
|
||||
}
|
||||
}
|
||||
}
|
||||
files.push({
|
||||
type: 'Subtitle',
|
||||
...sxData as sxItem,
|
||||
cc: sub.cc
|
||||
cc: sub.cc,
|
||||
belongsToFile: typeof tsFile === 'undefined' ? { hasFile: false } : { hasFile: true, file: `${tsFile}.ts`}
|
||||
});
|
||||
} else{
|
||||
console.warn(`Failed to download subtitle: ${sxData.file}`);
|
||||
|
|
@ -792,7 +807,8 @@ export default class Hidive implements ServiceClass {
|
|||
return {
|
||||
file: a.path,
|
||||
language: a.language,
|
||||
closedCaption: a.cc
|
||||
closedCaption: a.cc,
|
||||
belongsToFile: a.belongsToFile
|
||||
};
|
||||
}),
|
||||
simul: data.filter(a => a.type === 'Video').map((a) : boolean => {
|
||||
|
|
@ -825,7 +841,7 @@ export default class Hidive implements ServiceClass {
|
|||
// mergers
|
||||
let isMuxed = false;
|
||||
if (options.syncTiming) {
|
||||
await merger.createDelays();
|
||||
await merger.createDelays(options.syncMethod, options.targetLikeness, options.maxOffsetSeconds, options.maxSecurityFrames);
|
||||
}
|
||||
if (bin.MKVmerge) {
|
||||
await merger.merge('mkvmerge', bin.MKVmerge);
|
||||
|
|
|
|||
|
|
@ -68,6 +68,10 @@ let argvC: {
|
|||
originalFontSize: boolean;
|
||||
keepAllVideos: boolean;
|
||||
syncTiming: boolean;
|
||||
maxSecurityFrames: number;
|
||||
maxOffsetSeconds: number;
|
||||
targetLikeness: number;
|
||||
syncMethod: number;
|
||||
};
|
||||
|
||||
export type ArgvType = typeof argvC;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ const groups = {
|
|||
'search': 'Search:',
|
||||
'dl': 'Downloading:',
|
||||
'mux': 'Muxing:',
|
||||
'timing': 'Timing Sync Options:',
|
||||
'fileName': 'Filename Template:',
|
||||
'debug': 'Debug:',
|
||||
'util': 'Utilities:',
|
||||
|
|
@ -420,8 +421,17 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
|||
}
|
||||
},
|
||||
{
|
||||
name: 'syncTiming',
|
||||
name: 'skipmux',
|
||||
describe: 'Skip muxing video, audio and subtitles',
|
||||
docDescribe: true,
|
||||
group: 'mux',
|
||||
service: ['all'],
|
||||
type: 'boolean',
|
||||
usage: ''
|
||||
},
|
||||
{
|
||||
name: 'syncTiming',
|
||||
group: 'timing',
|
||||
describe: 'Attempts to sync timing for multi-dub downloads EXPERIMENTAL',
|
||||
docDescribe: 'If enabled attempts to sync timing for multi-dub downloads.'
|
||||
+ '\nNOTE: This is currently experimental and syncs audio and subtitles, though subtitles has a lot of guesswork'
|
||||
|
|
@ -434,13 +444,59 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
|||
}
|
||||
},
|
||||
{
|
||||
name: 'skipmux',
|
||||
describe: 'Skip muxing video, audio and subtitles',
|
||||
docDescribe: true,
|
||||
group: 'mux',
|
||||
service: ['all'],
|
||||
name: 'maxSecurityFrames',
|
||||
group: 'timing',
|
||||
describe: 'Sets the maximum consecutive frames to check when syncing timing',
|
||||
docDescribe: 'Sets the maximum consecutive frames to check when syncing timing'
|
||||
+ '\nNOTE: The higher this is set, the longer it will take to process the timing differences.',
|
||||
service: ['crunchy','hidive'],
|
||||
type: 'boolean',
|
||||
usage: ''
|
||||
usage: '',
|
||||
default: {
|
||||
default: 10
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'maxOffsetSeconds',
|
||||
group: 'timing',
|
||||
describe: 'The maximum amount of time that should be scanned for sync timing before failing.',
|
||||
docDescribe: 'The maximum amount of time that should be scanned for sync timing before failing.'
|
||||
+ '\nNOTE: The higher this is set, the more temporary images will be stored in the temp folder.',
|
||||
service: ['crunchy','hidive'],
|
||||
type: 'boolean',
|
||||
usage: '',
|
||||
default: {
|
||||
default: 20
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'targetLikeness',
|
||||
group: 'timing',
|
||||
describe: 'The target for how like a frame should be to be considered a match.',
|
||||
docDescribe: 'The target for how like a frame should be to be considered a match.'
|
||||
+ '\nNOTE: Setting this too high or too low could lead to issues.',
|
||||
service: ['crunchy','hidive'],
|
||||
type: 'boolean',
|
||||
usage: '',
|
||||
default: {
|
||||
default: 0.95
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'syncMethod',
|
||||
choices: [1,2,3],
|
||||
group: 'timing',
|
||||
describe: 'Method for Syncing. Options: (1) Difference, (2) Frame Analysis, (3) Frame Analysis Slow',
|
||||
docDescribe: 'Method for Syncing, Options:'
|
||||
+ '\nDifference: Calculates the length difference and uses that (Fastest)'
|
||||
+ '\nFrame Analysis: Analyzes frames to find a match and uses that (Medium) [Default]'
|
||||
+ '\nFrame Analysis Slow: Analyzes frames using the netflix library (may not be available on every platform) for accuracy to find a match and uses that (Very Slow)',
|
||||
service: ['crunchy','hidive'],
|
||||
type: 'boolean',
|
||||
usage: '',
|
||||
default: {
|
||||
default: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'fileName',
|
||||
|
|
|
|||
|
|
@ -81,7 +81,8 @@ export type ConfigObject = {
|
|||
},
|
||||
bin: {
|
||||
ffmpeg?: string,
|
||||
mkvmerge?: string
|
||||
mkvmerge?: string,
|
||||
ffprobe?: string
|
||||
},
|
||||
cli: {
|
||||
[key: string]: any
|
||||
|
|
@ -143,8 +144,9 @@ const loadBinCfg = async () => {
|
|||
const binCfg = loadYamlCfgFile<ConfigObject['bin']>(binCfgFile);
|
||||
// binaries
|
||||
const defaultBin = {
|
||||
ffmpeg: '${wdir}/bin/ffmpeg/ffmpeg',
|
||||
mkvmerge: '${wdir}/bin/mkvtoolnix/mkvmerge',
|
||||
ffmpeg: 'ffmpeg',
|
||||
mkvmerge: 'mkvmerge',
|
||||
ffprobe: 'ffprobe'
|
||||
};
|
||||
const keys = Object.keys(defaultBin) as (keyof typeof defaultBin)[];
|
||||
for(const dir of keys){
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import * as iso639 from 'iso-639';
|
||||
import * as yamlCfg from './module.cfg-loader';
|
||||
import { fontFamilies, fontMime } from './module.fontsData';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
|
@ -7,7 +8,8 @@ import { AvailableMuxer } from './module.args';
|
|||
import { exec } from './sei-helper-fixes';
|
||||
import { console } from './log';
|
||||
import ffprobe from 'ffprobe';
|
||||
import ffprobeStatic from 'ffprobe-static';
|
||||
import lookssame from 'looks-same';
|
||||
import readline from 'readline';
|
||||
|
||||
export type MergerInput = {
|
||||
path: string,
|
||||
|
|
@ -15,13 +17,21 @@ export type MergerInput = {
|
|||
duration?: number,
|
||||
delay?: number,
|
||||
isPrimary?: boolean,
|
||||
frameRate?: number
|
||||
}
|
||||
|
||||
export type SubtitleInput = {
|
||||
language: LanguageItem,
|
||||
file: string,
|
||||
belongsToFile: {
|
||||
hasFile: false,
|
||||
} | {
|
||||
hasFile: true,
|
||||
file: string
|
||||
},
|
||||
closedCaption?: boolean,
|
||||
delay?: number
|
||||
delay?: number,
|
||||
frameRate?: number
|
||||
}
|
||||
|
||||
export type Font = keyof typeof fontFamilies;
|
||||
|
|
@ -55,8 +65,13 @@ export type MergerOptions = {
|
|||
}
|
||||
}
|
||||
|
||||
/* Defaults */
|
||||
const SECURITY_FRAMES = 10;
|
||||
const MAX_OFFSET_SEC = 20;
|
||||
const LIKENESS_TARGET = 0.95;
|
||||
|
||||
class Merger {
|
||||
|
||||
|
||||
constructor(private options: MergerOptions) {
|
||||
if (this.options.skipSubMux)
|
||||
this.options.subtitles = [];
|
||||
|
|
@ -64,44 +79,128 @@ class Merger {
|
|||
this.options.videoTitle = this.options.videoTitle.replace(/"/g, '\'');
|
||||
}
|
||||
|
||||
public async createDelays() {
|
||||
//Don't bother scanning it if there is only 1 vna stream
|
||||
public async createDelays(syncMethod: number, LIKENESS_TARGET: number, MAX_OFFSET_SEC: number, SECURITY_FRAMES: number) {
|
||||
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: ffprobeStatic.path });
|
||||
const streamInfo = await ffprobe(vna.path, { path: bin.ffprobe as string });
|
||||
const videoInfo = streamInfo.streams.filter(stream => stream.codec_type == 'video');
|
||||
vnas[vnaIndex].duration = videoInfo[0].duration;
|
||||
vnas[vnaIndex].frameRate = eval(videoInfo[0].avg_frame_rate) as number;
|
||||
}
|
||||
//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;
|
||||
}
|
||||
}
|
||||
|
||||
fs.mkdirSync('tmp/main-frames', { recursive: true });
|
||||
exec('ffmpeg', bin.ffmpeg as string, `-hide_banner -loglevel error -i "${vnas[0].path}" -t ${MAX_OFFSET_SEC} tmp/main-frames/%03d.png`, false, true);
|
||||
|
||||
const start = vnas[0];
|
||||
|
||||
console.info(`Using ${start.lang.code} as the base for syncing`);
|
||||
const items = fs.readdirSync('tmp/main-frames');
|
||||
console.info('Finding start frame from base...');
|
||||
let offset = 0;
|
||||
for (const [index, file] of items.entries()) {
|
||||
const result = await lookssame(`tmp/main-frames/${file}`, 'tmp/main-frames/001.png', {tolerance: 3});
|
||||
offset = index;
|
||||
if (!result.equal) break;
|
||||
}
|
||||
items.splice(0, offset);
|
||||
console.info(`Start frame from base is ${items[0]} - Generating differences`);
|
||||
const filesToRemove: string[] = [];
|
||||
itemLoop: for (const vna of vnas.slice(1)) {
|
||||
console.info(`Trying to find delay for ${vna.lang.code}...`);
|
||||
outer: for (let i = 1; i < (items.length); i++) {
|
||||
console.info(`Trying to match likeness with frame ${i+offset}`);
|
||||
const closeness = [];
|
||||
exec('ffmpeg', bin.ffmpeg as string, `-hide_banner -loglevel error -i tmp/main-frames/${items[i]} -i "${vna.path}" -t ${MAX_OFFSET_SEC} -lavfi "ssim=f=tmp/stats-${i}.log;[0:v][1:v]psnr" -f null -`, false, true);
|
||||
filesToRemove.push(`tmp/stats-${i}.log`);
|
||||
const fileStream = fs.createReadStream(`tmp/stats-${i}.log`);
|
||||
const rl = readline.createInterface({
|
||||
input: fileStream
|
||||
});
|
||||
for await (const line of rl) {
|
||||
const values = line.split(' ');
|
||||
closeness.push({
|
||||
frame: parseFloat(values[0].replace('n:', '')),
|
||||
Y: parseFloat(values[1].replace('Y:', '')),
|
||||
U: parseFloat(values[2].replace('U:', '')),
|
||||
V: parseFloat(values[3].replace('V:', '')),
|
||||
overall: parseFloat(values[4].replace('All:', '')),
|
||||
db: parseFloat(values[5].replace('(', '').replace(')', ''))
|
||||
});
|
||||
}
|
||||
if (closeness.length === 0) {
|
||||
console.info(`Failed to analyze time offset required for ${vna.lang.code}, check for addtional logs.`);
|
||||
continue itemLoop;
|
||||
}
|
||||
closeness.sort(function(a, b) {
|
||||
return b.overall - a.overall;
|
||||
});
|
||||
if (closeness[0].overall > LIKENESS_TARGET) {
|
||||
closeness.sort(function(a, b) {
|
||||
return a.frame - b.frame;
|
||||
});
|
||||
for (const frame of closeness) {
|
||||
if (frame.overall > LIKENESS_TARGET) {
|
||||
for (let b = i; b < Math.min(items.length, i + SECURITY_FRAMES); b++) {
|
||||
console.info(`Verifying Security Frame ${b}...`);
|
||||
exec('ffmpeg', bin.ffmpeg as string, `-hide_banner -loglevel error -i tmp/main-frames/${items[b]} -i "${vna.path}" -t ${MAX_OFFSET_SEC} -lavfi "ssim=f=tmp/stats-${i}-${b}.log;[0:v][1:v]psnr" -f null -`, false, true);
|
||||
filesToRemove.push(`tmp/stats-${i}-${b}.log`);
|
||||
const fileStream = fs.createReadStream(`tmp/stats-${i}-${b}.log`);
|
||||
const rl = readline.createInterface({
|
||||
input: fileStream
|
||||
});
|
||||
for await (const line of rl) {
|
||||
const values = line.split(' ');
|
||||
const securityframe = {
|
||||
frame: parseFloat(values[0].replace('n:', '')),
|
||||
Y: parseFloat(values[1].replace('Y:', '')),
|
||||
U: parseFloat(values[2].replace('U:', '')),
|
||||
V: parseFloat(values[3].replace('V:', '')),
|
||||
overall: parseFloat(values[4].replace('All:', '')),
|
||||
db: parseFloat(values[5].replace('(', '').replace(')', ''))
|
||||
};
|
||||
if (securityframe.frame === (frame.frame + b))
|
||||
if (!(securityframe.overall > LIKENESS_TARGET)) {
|
||||
console.info('Match failed, trying to find another match.');
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
console.info(`Security Frame ${b} verified.`);
|
||||
}
|
||||
console.info('Match Succesful');
|
||||
|
||||
vna.delay = frame.frame - offset;
|
||||
const subtitles = this.options.subtitles.filter(sub => {
|
||||
return sub.belongsToFile.hasFile && sub.belongsToFile.file === vna.path;
|
||||
});
|
||||
for (const sub of subtitles) {
|
||||
sub.delay = vna.delay;
|
||||
sub.frameRate = vna.frameRate;
|
||||
}
|
||||
console.info(`Found ${vna.delay} frames delay for ${vna.lang.code}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
continue outer;
|
||||
}
|
||||
continue itemLoop;
|
||||
}
|
||||
console.error(`Unable to find delay for ${vna.lang.code}`);
|
||||
}
|
||||
//Remove temp files
|
||||
fs.rmSync('tmp/main-frames/', { recursive: true, force: true });
|
||||
for (const file of filesToRemove) {
|
||||
fs.rmSync(file, { recursive: true, force: true });
|
||||
}
|
||||
console.info('Processed all files to find delays.');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -115,9 +214,13 @@ class Merger {
|
|||
|
||||
for (const vid of this.options.videoAndAudio) {
|
||||
if (vid.delay && hasVideo) {
|
||||
args.push(
|
||||
`-itsoffset -${Math.ceil(vid.delay*1000)}ms`
|
||||
);
|
||||
if (vid.frameRate) {
|
||||
args.push(
|
||||
`-ss ${Math.ceil(vid.delay * (1000 / vid.frameRate))}ms`
|
||||
);
|
||||
} else {
|
||||
console.error(`Missing framerate for video ${vid.lang.code}`);
|
||||
}
|
||||
}
|
||||
args.push(`-i "${vid.path}"`);
|
||||
if (!hasVideo || this.options.keepAllVideos) {
|
||||
|
|
@ -154,9 +257,13 @@ class Merger {
|
|||
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`
|
||||
);
|
||||
if (sub.frameRate) {
|
||||
args.push(
|
||||
`-itsoffset -${Math.ceil(sub.delay * (1000 / sub.frameRate))}ms`
|
||||
);
|
||||
} else {
|
||||
console.error(`Missing framerate for subtitle: ${JSON.stringify(sub)}`);
|
||||
}
|
||||
}
|
||||
args.push(`-i "${sub.file}"`);
|
||||
}
|
||||
|
|
@ -222,8 +329,12 @@ class Merger {
|
|||
const audioTrackNum = this.options.inverseTrackOrder ? '0' : '1';
|
||||
const videoTrackNum = this.options.inverseTrackOrder ? '1' : '0';
|
||||
if (vid.delay) {
|
||||
if (!vid.frameRate) {
|
||||
console.error(`Unable to find framerate for stream ${vid.lang.code}`);
|
||||
continue;
|
||||
}
|
||||
args.push(
|
||||
`--sync ${audioTrackNum}:-${Math.ceil(vid.delay*1000)}`
|
||||
`--sync ${audioTrackNum}:-${Math.ceil(vid.delay*(1000 / vid.frameRate))}`
|
||||
);
|
||||
}
|
||||
if (!hasVideo || this.options.keepAllVideos) {
|
||||
|
|
@ -276,9 +387,13 @@ class Merger {
|
|||
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 (subObj.frameRate) {
|
||||
args.push(
|
||||
`--sync 0:-${Math.ceil(subObj.delay*(1000 / subObj.frameRate))}`
|
||||
);
|
||||
} else {
|
||||
console.error(`Missing framerate for subtitle: ${JSON.stringify(subObj)}`);
|
||||
}
|
||||
}
|
||||
args.push('--track-name', `0:"${(subObj.language.language || subObj.language.name) + `${subObj.closedCaption === true ? ` ${this.options.ccTag}` : ''}`}"`);
|
||||
args.push('--language', `0:"${subObj.language.code}"`);
|
||||
|
|
@ -369,7 +484,7 @@ class Merger {
|
|||
path: fontPath,
|
||||
mime: mime,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -409,4 +524,4 @@ class Merger {
|
|||
|
||||
}
|
||||
|
||||
export default Merger;
|
||||
export default Merger;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import childProcess from 'child_process';
|
||||
import { console } from './log';
|
||||
|
||||
const exec = (pname: string, fpath: string, pargs: string, spc = false): {
|
||||
const exec = (pname: string, fpath: string, pargs: string, spc = false, quiet = false): {
|
||||
isOk: true
|
||||
} | {
|
||||
isOk: false,
|
||||
err: Error & { code: number }
|
||||
} => {
|
||||
pargs = pargs ? ' ' + pargs : '';
|
||||
console.info(`\n> "${pname}"${pargs}${spc ? '\n' : ''}`);
|
||||
if (quiet === false ){
|
||||
console.info(`\n> "${pname}"${pargs}${spc ? '\n' : ''}`);
|
||||
}
|
||||
try {
|
||||
childProcess.execSync((fpath + pargs), { stdio: 'inherit' });
|
||||
return {
|
||||
|
|
@ -26,4 +28,4 @@ const exec = (pname: string, fpath: string, pargs: string, spc = false): {
|
|||
}
|
||||
};
|
||||
|
||||
export { exec };
|
||||
export { exec };
|
||||
|
|
|
|||
|
|
@ -51,13 +51,13 @@
|
|||
"eslint-plugin-import": "^2.27.5",
|
||||
"express": "^4.18.2",
|
||||
"ffprobe": "^1.1.2",
|
||||
"ffprobe-static": "^3.1.0",
|
||||
"form-data": "^4.0.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"got": "^11.8.6",
|
||||
"iso-639": "^0.2.2",
|
||||
"log4js": "^6.9.1",
|
||||
"lookpath": "^1.2.2",
|
||||
"looks-same": "^8.2.1",
|
||||
"m3u8-parsed": "^1.3.0",
|
||||
"open": "^8.4.2",
|
||||
"sei-helper": "^3.3.0",
|
||||
|
|
|
|||
110
pnpm-lock.yaml
110
pnpm-lock.yaml
|
|
@ -32,9 +32,6 @@ dependencies:
|
|||
ffprobe:
|
||||
specifier: ^1.1.2
|
||||
version: 1.1.2
|
||||
ffprobe-static:
|
||||
specifier: ^3.1.0
|
||||
version: 3.1.0
|
||||
form-data:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
|
|
@ -53,6 +50,9 @@ dependencies:
|
|||
lookpath:
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.2
|
||||
looks-same:
|
||||
specifier: ^8.2.1
|
||||
version: 8.2.1
|
||||
m3u8-parsed:
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0
|
||||
|
|
@ -2629,7 +2629,6 @@ packages:
|
|||
|
||||
/chownr@1.1.4:
|
||||
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
||||
dev: true
|
||||
|
||||
/cliui@7.0.4:
|
||||
resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
|
||||
|
|
@ -2654,6 +2653,10 @@ packages:
|
|||
mimic-response: 1.0.1
|
||||
dev: false
|
||||
|
||||
/color-convert@0.5.3:
|
||||
resolution: {integrity: sha512-RwBeO/B/vZR3dfKL1ye/vx8MHZ40ugzpyfeVG5GsiuGnrlMWe2o8wxBbLCpw9CsxV+wHuzYlCiWnybrIA0ling==}
|
||||
dev: false
|
||||
|
||||
/color-convert@1.9.3:
|
||||
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
||||
dependencies:
|
||||
|
|
@ -2665,12 +2668,31 @@ packages:
|
|||
dependencies:
|
||||
color-name: 1.1.4
|
||||
|
||||
/color-diff@1.4.0:
|
||||
resolution: {integrity: sha512-4oDB/o78lNdppbaqrg0HjOp7pHmUc+dfCxWKWFnQg6AB/1dkjtBDop3RZht5386cq9xBUDRvDvSCA7WUlM9Jqw==}
|
||||
dev: false
|
||||
|
||||
/color-name@1.1.3:
|
||||
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
|
||||
|
||||
/color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
/color-string@1.9.1:
|
||||
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
simple-swizzle: 0.2.2
|
||||
dev: false
|
||||
|
||||
/color@4.2.3:
|
||||
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
|
||||
engines: {node: '>=12.5.0'}
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
color-string: 1.9.1
|
||||
dev: false
|
||||
|
||||
/combined-stream@1.0.8:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
|
@ -2837,7 +2859,6 @@ packages:
|
|||
/deep-extend@0.6.0:
|
||||
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
dev: true
|
||||
|
||||
/deep-is@0.1.4:
|
||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||
|
|
@ -2883,7 +2904,6 @@ packages:
|
|||
/detect-libc@2.0.1:
|
||||
resolution: {integrity: sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/diff@4.0.2:
|
||||
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
|
||||
|
|
@ -3458,7 +3478,6 @@ packages:
|
|||
/expand-template@2.0.3:
|
||||
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/express@4.18.2:
|
||||
resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==}
|
||||
|
|
@ -3523,10 +3542,6 @@ packages:
|
|||
dependencies:
|
||||
reusify: 1.0.4
|
||||
|
||||
/ffprobe-static@3.1.0:
|
||||
resolution: {integrity: sha512-Dvpa9uhVMOYivhHKWLGDoa512J751qN1WZAIO+Xw4L/mrUSPxS4DApzSUDbCFE/LUq2+xYnznEahTd63AqBSpA==}
|
||||
dev: false
|
||||
|
||||
/ffprobe@1.1.2:
|
||||
resolution: {integrity: sha512-a+oTbhyeM7Z8PRy+mpzmVUAnATZT7z4BO94HSKeqHupdmjiKZ1djzcZkyoyXA21zCOCG7oVRrsBMmvvtmzoz4g==}
|
||||
dependencies:
|
||||
|
|
@ -3612,7 +3627,6 @@ packages:
|
|||
|
||||
/fs-constants@1.0.0:
|
||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||
dev: true
|
||||
|
||||
/fs-extra@11.1.1:
|
||||
resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==}
|
||||
|
|
@ -3691,7 +3705,6 @@ packages:
|
|||
|
||||
/github-from-package@0.0.0:
|
||||
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
|
||||
dev: true
|
||||
|
||||
/glob-parent@5.1.2:
|
||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||
|
|
@ -3895,7 +3908,6 @@ packages:
|
|||
|
||||
/ini@1.3.8:
|
||||
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
|
||||
dev: true
|
||||
|
||||
/internal-slot@1.0.5:
|
||||
resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==}
|
||||
|
|
@ -3937,6 +3949,10 @@ packages:
|
|||
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
|
||||
dev: true
|
||||
|
||||
/is-arrayish@0.3.2:
|
||||
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
|
||||
dev: false
|
||||
|
||||
/is-bigint@1.0.4:
|
||||
resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
|
||||
dependencies:
|
||||
|
|
@ -4088,6 +4104,11 @@ packages:
|
|||
resolution: {integrity: sha512-t1ZNgV1kLZ4dBQGxtY4z5oysRFPANt0kqHjomasflQW0w+YdDRF+Wsl0gFftIeUiP508KhZxG3MXQ31Swd643Q==}
|
||||
dev: false
|
||||
|
||||
/js-graph-algorithms@1.0.18:
|
||||
resolution: {integrity: sha512-Gu1wtWzXBzGeye/j9BuyplGHscwqKRZodp/0M1vyBc19RJpblSwKGu099KwwaTx9cRIV+Qupk8xUMfEiGfFqSA==}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
|
|
@ -4200,7 +4221,6 @@ packages:
|
|||
|
||||
/lodash@4.17.21:
|
||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||
dev: true
|
||||
|
||||
/log4js@6.9.1:
|
||||
resolution: {integrity: sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==}
|
||||
|
|
@ -4221,6 +4241,19 @@ packages:
|
|||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/looks-same@8.2.1:
|
||||
resolution: {integrity: sha512-55u5dWPWc/9cIFRtDjqbH/J3h0nyar1gqFrDIDQSpqYDDd19dZUQtTMxLj0hvsxzp/qzQ0EV0HChtXRWylY2Pg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
dependencies:
|
||||
color-diff: 1.4.0
|
||||
fs-extra: 8.1.0
|
||||
js-graph-algorithms: 1.0.18
|
||||
lodash: 4.17.21
|
||||
nested-error-stacks: 2.1.1
|
||||
parse-color: 1.0.0
|
||||
sharp: 0.30.7
|
||||
dev: false
|
||||
|
||||
/loose-envify@1.4.0:
|
||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||
hasBin: true
|
||||
|
|
@ -4330,7 +4363,6 @@ packages:
|
|||
|
||||
/mkdirp-classic@0.5.3:
|
||||
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
|
||||
dev: true
|
||||
|
||||
/ms@2.0.0:
|
||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||
|
|
@ -4351,7 +4383,6 @@ packages:
|
|||
|
||||
/napi-build-utils@1.0.2:
|
||||
resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==}
|
||||
dev: true
|
||||
|
||||
/native-promise-only@0.8.1:
|
||||
resolution: {integrity: sha512-zkVhZUA3y8mbz652WrL5x0fB0ehrBkulWT3TomAQ9iDtyXZvzKeEA6GPxAItBYeNYl5yngKRX612qHOhvMkDeg==}
|
||||
|
|
@ -4369,12 +4400,19 @@ packages:
|
|||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/nested-error-stacks@2.1.1:
|
||||
resolution: {integrity: sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw==}
|
||||
dev: false
|
||||
|
||||
/node-abi@3.35.0:
|
||||
resolution: {integrity: sha512-jAlSOFR1Bls963NmFwxeQkNTzqjUF0NThm8Le7eRIRGzFUVJuMOFZDLv5Y30W/Oaw+KEebEJLAigwO9gQHoEmw==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
semver: 7.3.8
|
||||
dev: true
|
||||
|
||||
/node-addon-api@5.1.0:
|
||||
resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==}
|
||||
dev: false
|
||||
|
||||
/node-fetch@2.6.9:
|
||||
resolution: {integrity: sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==}
|
||||
|
|
@ -4527,6 +4565,12 @@ packages:
|
|||
dependencies:
|
||||
callsites: 3.1.0
|
||||
|
||||
/parse-color@1.0.0:
|
||||
resolution: {integrity: sha512-fuDHYgFHJGbpGMgw9skY/bj3HL/Jrn4l/5rSspy00DoT4RyLnDcRvPxdZ+r6OFwIsgAuhDh4I09tAId4mI12bw==}
|
||||
dependencies:
|
||||
color-convert: 0.5.3
|
||||
dev: false
|
||||
|
||||
/parse-json@5.2.0:
|
||||
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -4647,7 +4691,6 @@ packages:
|
|||
simple-get: 4.0.1
|
||||
tar-fs: 2.1.1
|
||||
tunnel-agent: 0.6.0
|
||||
dev: true
|
||||
|
||||
/prelude-ls@1.2.1:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
|
|
@ -4731,7 +4774,6 @@ packages:
|
|||
ini: 1.3.8
|
||||
minimist: 1.2.8
|
||||
strip-json-comments: 2.0.1
|
||||
dev: true
|
||||
|
||||
/react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
|
@ -4900,7 +4942,6 @@ packages:
|
|||
hasBin: true
|
||||
dependencies:
|
||||
lru-cache: 6.0.0
|
||||
dev: true
|
||||
|
||||
/semver@7.5.4:
|
||||
resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==}
|
||||
|
|
@ -4946,6 +4987,21 @@ packages:
|
|||
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
||||
dev: false
|
||||
|
||||
/sharp@0.30.7:
|
||||
resolution: {integrity: sha512-G+MY2YW33jgflKPTXXptVO28HvNOo9G3j0MybYAHeEmby+QuD2U98dT6ueht9cv/XDqZspSpIhoSW+BAKJ7Hig==}
|
||||
engines: {node: '>=12.13.0'}
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
color: 4.2.3
|
||||
detect-libc: 2.0.1
|
||||
node-addon-api: 5.1.0
|
||||
prebuild-install: 7.1.1
|
||||
semver: 7.5.4
|
||||
simple-get: 4.0.1
|
||||
tar-fs: 2.1.1
|
||||
tunnel-agent: 0.6.0
|
||||
dev: false
|
||||
|
||||
/shebang-command@2.0.0:
|
||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -4965,7 +5021,6 @@ packages:
|
|||
|
||||
/simple-concat@1.0.1:
|
||||
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
|
||||
dev: true
|
||||
|
||||
/simple-get@4.0.1:
|
||||
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
|
||||
|
|
@ -4973,7 +5028,12 @@ packages:
|
|||
decompress-response: 6.0.0
|
||||
once: 1.4.0
|
||||
simple-concat: 1.0.1
|
||||
dev: true
|
||||
|
||||
/simple-swizzle@0.2.2:
|
||||
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
|
||||
dependencies:
|
||||
is-arrayish: 0.3.2
|
||||
dev: false
|
||||
|
||||
/slash@3.0.0:
|
||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||
|
|
@ -5079,7 +5139,6 @@ packages:
|
|||
/strip-json-comments@2.0.1:
|
||||
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/strip-json-comments@3.1.1:
|
||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||
|
|
@ -5108,7 +5167,6 @@ packages:
|
|||
mkdirp-classic: 0.5.3
|
||||
pump: 3.0.0
|
||||
tar-stream: 2.2.0
|
||||
dev: true
|
||||
|
||||
/tar-stream@2.2.0:
|
||||
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
||||
|
|
@ -5119,7 +5177,6 @@ packages:
|
|||
fs-constants: 1.0.0
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
dev: true
|
||||
|
||||
/text-table@0.2.0:
|
||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||
|
|
@ -5212,7 +5269,6 @@ packages:
|
|||
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
dev: true
|
||||
|
||||
/type-check@0.4.0:
|
||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
|
||||
|
|
|
|||
0
tmp/.gitkeep
Normal file
0
tmp/.gitkeep
Normal file
Loading…
Reference in a new issue