Fixes for subtitles

This commit is contained in:
AnidlSupport 2023-08-22 12:32:55 -07:00
parent 0ce179b67a
commit ea85958191
6 changed files with 167 additions and 143 deletions

View file

@ -84,7 +84,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
View file

@ -12,5 +12,11 @@ export type Subtitle = {
lang: LanguageItem,
ext: string,
out?: string,
closedCaption?: boolean
}
closedCaption?: boolean,
belongsToFile: {
hasFile: false,
} | {
hasFile: true,
file: string
}
}

View file

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

112
funi.ts
View file

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

View file

@ -23,6 +23,12 @@ export type MergerInput = {
export type SubtitleInput = {
language: LanguageItem,
file: string,
belongsToFile: {
hasFile: false,
} | {
hasFile: true,
file: string
},
closedCaption?: boolean,
delay?: number,
frameRate?: number
@ -59,9 +65,9 @@ export type MergerOptions = {
}
}
const SECURITY_FRAMES = 10;
const SECURITY_FRAMES = 20;
const MAX_OFFSET_SEC = 20;
const LIKENESS_TARGET = 0.990;
const LIKENESS_TARGET = 0.95;
class Merger {
@ -89,7 +95,7 @@ class Merger {
});
fs.mkdirSync('tmp/main-frames', { recursive: true });
exec('ffmpeg', 'ffmpeg', `-hide_banner -loglevel error -i "${vnas[0].path}" -t ${MAX_OFFSET_SEC} tmp/main-frames/%03d.png`);
exec('ffmpeg', 'ffmpeg', `-hide_banner -loglevel error -i "${vnas[0].path}" -t ${MAX_OFFSET_SEC} tmp/main-frames/%03d.png`, false, true);
const start = vnas[0];
@ -109,7 +115,7 @@ class Merger {
console.info(`Trying to find delay for ${vna.lang.code}...`);
outer: for (let i = 1; i <= (items.length-offset); i++) {
const closeness = [];
exec('ffmpeg', 'ffmpeg', `-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 -`);
exec('ffmpeg', 'ffmpeg', `-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({
@ -123,7 +129,7 @@ class Merger {
U: parseFloat(values[2].replace('U:', '')),
V: parseFloat(values[3].replace('V:', '')),
overall: parseFloat(values[4].replace('All:', '')),
calc: parseFloat(values[5].replace('(', '').replace(')', ''))
db: parseFloat(values[5].replace('(', '').replace(')', ''))
});
}
closeness.sort(function(a, b) {
@ -136,8 +142,8 @@ class Merger {
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 match...');
exec('ffmpeg', 'ffmpeg', `-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 -`);
console.info(`Verifying Security Frame ${b}...`);
exec('ffmpeg', 'ffmpeg', `-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({
@ -151,7 +157,7 @@ class Merger {
U: parseFloat(values[2].replace('U:', '')),
V: parseFloat(values[3].replace('V:', '')),
overall: parseFloat(values[4].replace('All:', '')),
calc: parseFloat(values[5].replace('(', '').replace(')', ''))
db: parseFloat(values[5].replace('(', '').replace(')', ''))
};
if (securityframe.frame === (frame.frame + b))
if (!(securityframe.overall > LIKENESS_TARGET)) {
@ -159,19 +165,17 @@ class Merger {
continue outer;
}
}
console.info('Match Succesful');
console.info(`Security Frame ${b} verified.`);
}
console.info('Match Succesful');
vna.delay = frame.frame - offset;
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;
subtitles[subIndex].frameRate = vna.frameRate;
} else if (sub.closedCaption) {
subtitles[subIndex].delay = vna.delay;
subtitles[subIndex].frameRate = vna.frameRate;
}
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;
@ -248,7 +252,7 @@ class Merger {
if (sub.delay) {
if (sub.frameRate) {
args.push(
`-ss ${Math.ceil(sub.delay * (1000 / sub.frameRate))}ms`
`-itsoffset -${Math.ceil(sub.delay * (1000 / sub.frameRate))}ms`
);
} else {
console.error(`Missing framerate for subtitle: ${JSON.stringify(sub)}`);

View file

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