Compare commits
8 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4d673a183 | ||
|
|
868c886449 | ||
|
|
8bb64a6612 | ||
|
|
b623bf7fc0 | ||
|
|
b788bc0639 | ||
|
|
146c5f10a9 | ||
|
|
dfa0a31def | ||
|
|
561ccac6f1 |
11 changed files with 8287 additions and 6448 deletions
20
@types/crunchyTypes.d.ts
vendored
20
@types/crunchyTypes.d.ts
vendored
|
|
@ -87,26 +87,6 @@ export type CrunchyEpMeta = {
|
|||
image: string,
|
||||
}
|
||||
|
||||
export type DownloadedMedia = {
|
||||
type: 'Video',
|
||||
lang: LanguageItem,
|
||||
path: string,
|
||||
isPrimary?: boolean
|
||||
} | {
|
||||
type: 'Audio',
|
||||
lang: LanguageItem,
|
||||
path: string,
|
||||
isPrimary?: boolean
|
||||
} | {
|
||||
type: 'Chapters',
|
||||
lang: LanguageItem,
|
||||
path: string
|
||||
} | ({
|
||||
type: 'Subtitle',
|
||||
signs: boolean,
|
||||
cc: boolean
|
||||
} & sxItem )
|
||||
|
||||
export type ParseItem = {
|
||||
__class__?: string;
|
||||
isSelected?: boolean,
|
||||
|
|
|
|||
37
@types/downloaderTypes.d.ts
vendored
Normal file
37
@types/downloaderTypes.d.ts
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import type { Font, MergerInput, SubtitleInput } from './modules/module.merger';
|
||||
import type { LanguageItem } from '../modules/module.langsData';
|
||||
|
||||
export type sxItem = {
|
||||
language: LanguageItem,
|
||||
path: string,
|
||||
file: string
|
||||
title: string,
|
||||
fonts: Font[]
|
||||
}
|
||||
|
||||
export type DownloadedMedia = {
|
||||
type: 'Video',
|
||||
lang: LanguageItem,
|
||||
path: string,
|
||||
uncut?: boolean,
|
||||
isPrimary?: boolean
|
||||
} | {
|
||||
type: 'Audio',
|
||||
lang: LanguageItem,
|
||||
path: string,
|
||||
uncut?: boolean,
|
||||
isPrimary?: boolean
|
||||
} | {
|
||||
type: 'Chapters',
|
||||
lang: LanguageItem,
|
||||
path: string
|
||||
} | ({
|
||||
type: 'Subtitle',
|
||||
signs?: boolean,
|
||||
cc: boolean
|
||||
} & sxItem )
|
||||
|
||||
export type DownloadedMediaMap = {
|
||||
version: string;
|
||||
files: DownloadedMedia[];
|
||||
}
|
||||
12
@types/hidiveTypes.d.ts
vendored
12
@types/hidiveTypes.d.ts
vendored
|
|
@ -48,14 +48,4 @@ export interface HidiveSubtitleInfo {
|
|||
language: string;
|
||||
cc: boolean;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export type DownloadedMedia = {
|
||||
type: 'Video',
|
||||
lang: LanguageItem,
|
||||
path: string,
|
||||
uncut: boolean
|
||||
} | ({
|
||||
type: 'Subtitle',
|
||||
cc: boolean
|
||||
} & sxItem )
|
||||
}
|
||||
60
adn.ts
60
adn.ts
|
|
@ -26,15 +26,14 @@ import parseFileName, { Variable } from './modules/module.filename';
|
|||
import { AvailableFilenameVars } from './modules/module.args';
|
||||
|
||||
// Types
|
||||
import { ServiceClass } from './@types/serviceClassInterface';
|
||||
import { AuthData, AuthResponse, SearchData, SearchResponse, SearchResponseItem } from './@types/messageHandler';
|
||||
import { sxItem } from './crunchy';
|
||||
import { DownloadedMedia } from './@types/hidiveTypes';
|
||||
import { ADNSearch, ADNSearchShow } from './@types/adnSearch';
|
||||
import { ADNVideo, ADNVideos } from './@types/adnVideos';
|
||||
import { ADNPlayerConfig } from './@types/adnPlayerConfig';
|
||||
import { ADNStreams } from './@types/adnStreams';
|
||||
import { ADNSubtitles } from './@types/adnSubtitles';
|
||||
import type { ServiceClass } from './@types/serviceClassInterface';
|
||||
import type { AuthData, AuthResponse, SearchData, SearchResponse, SearchResponseItem } from './@types/messageHandler';
|
||||
import type { DownloadedMedia, DownloadedMediaMap, sxItem } from './@types/downloaderTypes';
|
||||
import type { ADNSearch, ADNSearchShow } from './@types/adnSearch';
|
||||
import type { ADNVideo, ADNVideos } from './@types/adnVideos';
|
||||
import type { ADNPlayerConfig } from './@types/adnPlayerConfig';
|
||||
import type { ADNStreams } from './@types/adnStreams';
|
||||
import type { ADNSubtitles } from './@types/adnSubtitles';
|
||||
|
||||
export default class AnimationDigitalNetwork implements ServiceClass {
|
||||
public cfg: yamlCfg.ConfigObject;
|
||||
|
|
@ -316,7 +315,7 @@ export default class AnimationDigitalNetwork implements ServiceClass {
|
|||
return { isOk: true, value: selEpsArr };
|
||||
}
|
||||
|
||||
public async muxStreams(data: DownloadedMedia[], options: yargs.ArgvType) {
|
||||
public async muxStreams(data: DownloadedMedia[], mediaMap: DownloadedMediaMap[], options: yargs.ArgvType) {
|
||||
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||
let hasAudioStreams = false;
|
||||
if (options.novids || data.filter(a => a.type === 'Video').length === 0)
|
||||
|
|
@ -325,9 +324,8 @@ export default class AnimationDigitalNetwork implements ServiceClass {
|
|||
hasAudioStreams = true;
|
||||
}
|
||||
const merger = new Merger({
|
||||
mediaMap,
|
||||
onlyVid: hasAudioStreams ? data.filter(a => a.type === 'Video').map((a) : MergerInput => {
|
||||
if (a.type === 'Subtitle')
|
||||
throw new Error('Never');
|
||||
return {
|
||||
lang: a.lang,
|
||||
path: a.path,
|
||||
|
|
@ -337,8 +335,6 @@ export default class AnimationDigitalNetwork implements ServiceClass {
|
|||
inverseTrackOrder: false,
|
||||
keepAllVideos: options.keepAllVideos,
|
||||
onlyAudio: hasAudioStreams ? data.filter(a => a.type === 'Audio').map((a) : MergerInput => {
|
||||
if (a.type === 'Subtitle')
|
||||
throw new Error('Never');
|
||||
return {
|
||||
lang: a.lang,
|
||||
path: a.path,
|
||||
|
|
@ -346,10 +342,6 @@ export default class AnimationDigitalNetwork implements ServiceClass {
|
|||
}) : [],
|
||||
output: `${options.output}.${options.mp4 ? 'mp4' : 'mkv'}`,
|
||||
subtitles: data.filter(a => a.type === 'Subtitle').map((a) : SubtitleInput => {
|
||||
if (a.type === 'Video')
|
||||
throw new Error('Never');
|
||||
if (a.type === 'Audio')
|
||||
throw new Error('Never');
|
||||
return {
|
||||
file: a.path,
|
||||
language: a.language,
|
||||
|
|
@ -357,14 +349,10 @@ export default class AnimationDigitalNetwork implements ServiceClass {
|
|||
};
|
||||
}),
|
||||
simul: data.filter(a => a.type === 'Video').map((a) : boolean => {
|
||||
if (a.type === 'Subtitle')
|
||||
throw new Error('Never');
|
||||
return !a.uncut as boolean;
|
||||
})[0],
|
||||
fonts: Merger.makeFontsList(this.cfg.dir.fonts, data.filter(a => a.type === 'Subtitle') as sxItem[]),
|
||||
videoAndAudio: hasAudioStreams ? [] : data.filter(a => a.type === 'Video').map((a) : MergerInput => {
|
||||
if (a.type === 'Subtitle')
|
||||
throw new Error('Never');
|
||||
return {
|
||||
lang: a.lang,
|
||||
path: a.path,
|
||||
|
|
@ -417,7 +405,7 @@ export default class AnimationDigitalNetwork implements ServiceClass {
|
|||
return { isOk: false, reason: new Error('Failed to download media list') };
|
||||
} else {
|
||||
if (!options.skipmux) {
|
||||
await this.muxStreams(res.data, { ...options, output: res.fileName });
|
||||
await this.muxStreams(res.data, res.mediaMap, { ...options, output: res.fileName });
|
||||
} else {
|
||||
console.info('Skipping mux');
|
||||
}
|
||||
|
|
@ -446,6 +434,12 @@ export default class AnimationDigitalNetwork implements ServiceClass {
|
|||
}
|
||||
|
||||
const files: DownloadedMedia[] = [];
|
||||
const mediaMap: DownloadedMediaMap[] = [];
|
||||
|
||||
const fileMap: DownloadedMediaMap = {
|
||||
version: data.id.toString(),
|
||||
files: []
|
||||
};
|
||||
|
||||
let dlFailed = false;
|
||||
let dlVideoOnce = false; // Variable to save if best selected video quality was downloaded
|
||||
|
|
@ -710,6 +704,11 @@ export default class AnimationDigitalNetwork implements ServiceClass {
|
|||
path: `${tsFile}.ts`,
|
||||
lang: audDub
|
||||
});
|
||||
fileMap.files.push({
|
||||
type: 'Video',
|
||||
path: `${tsFile}.ts`,
|
||||
lang: audDub
|
||||
});
|
||||
dlVideoOnce = true;
|
||||
}
|
||||
} else{
|
||||
|
|
@ -773,7 +772,12 @@ export default class AnimationDigitalNetwork implements ServiceClass {
|
|||
fs.writeFileSync(`${tsFile}.txt`, compiledChapters.join('\r\n'));
|
||||
files.push({
|
||||
path: `${tsFile}.txt`,
|
||||
lang: langsData.languages.find(a=>a.code=='jpn'),
|
||||
lang: langsData.languages.find(a=>a.code=='jpn')!,
|
||||
type: 'Chapters'
|
||||
});
|
||||
fileMap.files.push({
|
||||
path: `${tsFile}.txt`,
|
||||
lang: langsData.languages.find(a=>a.code=='jpn')!,
|
||||
type: 'Chapters'
|
||||
});
|
||||
} catch {
|
||||
|
|
@ -899,6 +903,11 @@ export default class AnimationDigitalNetwork implements ServiceClass {
|
|||
...sxData as sxItem,
|
||||
cc: false
|
||||
});
|
||||
fileMap.files.push({
|
||||
type: 'Subtitle',
|
||||
...sxData as sxItem,
|
||||
cc: false
|
||||
});
|
||||
}
|
||||
subIndex++;
|
||||
}
|
||||
|
|
@ -909,9 +918,12 @@ export default class AnimationDigitalNetwork implements ServiceClass {
|
|||
console.info('Subtitles downloading skipped!');
|
||||
}
|
||||
|
||||
mediaMap.push(fileMap);
|
||||
|
||||
return {
|
||||
error: dlFailed,
|
||||
data: files,
|
||||
mediaMap,
|
||||
fileName: fileName ? (path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName)) || './unknown' : './unknown'
|
||||
};
|
||||
}
|
||||
|
|
|
|||
66
ao.ts
66
ao.ts
|
|
@ -27,14 +27,13 @@ import { AvailableFilenameVars } from './modules/module.args';
|
|||
import { parse } from './modules/module.transform-mpd';
|
||||
|
||||
// Types
|
||||
import { ServiceClass } from './@types/serviceClassInterface';
|
||||
import { AuthData, AuthResponse, SearchData, SearchResponse, SearchResponseItem } from './@types/messageHandler';
|
||||
import { AOSearchResult, AnimeOnegaiSearch } from './@types/animeOnegaiSearch';
|
||||
import { AnimeOnegaiSeries } from './@types/animeOnegaiSeries';
|
||||
import { AnimeOnegaiSeasons, Episode } from './@types/animeOnegaiSeasons';
|
||||
import { DownloadedMedia } from './@types/hidiveTypes';
|
||||
import { AnimeOnegaiStream } from './@types/animeOnegaiStream';
|
||||
import { sxItem } from './crunchy';
|
||||
import type { ServiceClass } from './@types/serviceClassInterface';
|
||||
import type { AuthData, AuthResponse, SearchData, SearchResponse, SearchResponseItem } from './@types/messageHandler';
|
||||
import type { AOSearchResult, AnimeOnegaiSearch } from './@types/animeOnegaiSearch';
|
||||
import type { AnimeOnegaiSeries } from './@types/animeOnegaiSeries';
|
||||
import type { AnimeOnegaiSeasons, Episode } from './@types/animeOnegaiSeasons';
|
||||
import type { DownloadedMedia, DownloadedMediaMap, sxItem } from './@types/downloaderTypes';
|
||||
import type { AnimeOnegaiStream } from './@types/animeOnegaiStream';
|
||||
|
||||
type parsedMultiDubDownload = {
|
||||
data: {
|
||||
|
|
@ -315,7 +314,7 @@ export default class AnimeOnegai implements ServiceClass {
|
|||
return false;
|
||||
} else {
|
||||
if (!options.skipmux) {
|
||||
await this.muxStreams(res.data, { ...options, output: res.fileName });
|
||||
await this.muxStreams(res.data, res.mediaMap, { ...options, output: res.fileName });
|
||||
} else {
|
||||
console.info('Skipping mux');
|
||||
}
|
||||
|
|
@ -327,7 +326,7 @@ export default class AnimeOnegai implements ServiceClass {
|
|||
return true;
|
||||
}
|
||||
|
||||
public async muxStreams(data: DownloadedMedia[], options: yargs.ArgvType) {
|
||||
public async muxStreams(data: DownloadedMedia[], mediaMap: DownloadedMediaMap[], options: yargs.ArgvType) {
|
||||
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||
let hasAudioStreams = false;
|
||||
if (options.novids || data.filter(a => a.type === 'Video').length === 0)
|
||||
|
|
@ -336,9 +335,8 @@ export default class AnimeOnegai implements ServiceClass {
|
|||
hasAudioStreams = true;
|
||||
}
|
||||
const merger = new Merger({
|
||||
mediaMap,
|
||||
onlyVid: hasAudioStreams ? data.filter(a => a.type === 'Video').map((a) : MergerInput => {
|
||||
if (a.type === 'Subtitle')
|
||||
throw new Error('Never');
|
||||
return {
|
||||
lang: a.lang,
|
||||
path: a.path,
|
||||
|
|
@ -348,8 +346,6 @@ export default class AnimeOnegai implements ServiceClass {
|
|||
inverseTrackOrder: false,
|
||||
keepAllVideos: options.keepAllVideos,
|
||||
onlyAudio: hasAudioStreams ? data.filter(a => a.type === 'Audio').map((a) : MergerInput => {
|
||||
if (a.type === 'Subtitle')
|
||||
throw new Error('Never');
|
||||
return {
|
||||
lang: a.lang,
|
||||
path: a.path,
|
||||
|
|
@ -357,10 +353,6 @@ export default class AnimeOnegai implements ServiceClass {
|
|||
}) : [],
|
||||
output: `${options.output}.${options.mp4 ? 'mp4' : 'mkv'}`,
|
||||
subtitles: data.filter(a => a.type === 'Subtitle').map((a) : SubtitleInput => {
|
||||
if (a.type === 'Video')
|
||||
throw new Error('Never');
|
||||
if (a.type === 'Audio')
|
||||
throw new Error('Never');
|
||||
return {
|
||||
file: a.path,
|
||||
language: a.language,
|
||||
|
|
@ -368,14 +360,10 @@ export default class AnimeOnegai implements ServiceClass {
|
|||
};
|
||||
}),
|
||||
simul: data.filter(a => a.type === 'Video').map((a) : boolean => {
|
||||
if (a.type === 'Subtitle')
|
||||
throw new Error('Never');
|
||||
return !a.uncut as boolean;
|
||||
})[0],
|
||||
fonts: Merger.makeFontsList(this.cfg.dir.fonts, data.filter(a => a.type === 'Subtitle') as sxItem[]),
|
||||
videoAndAudio: hasAudioStreams ? [] : data.filter(a => a.type === 'Video').map((a) : MergerInput => {
|
||||
if (a.type === 'Subtitle')
|
||||
throw new Error('Never');
|
||||
return {
|
||||
lang: a.lang,
|
||||
path: a.path,
|
||||
|
|
@ -415,6 +403,7 @@ export default class AnimeOnegai implements ServiceClass {
|
|||
|
||||
public async downloadMediaList(medias: parsedMultiDubDownload, options: yargs.ArgvType) : Promise<{
|
||||
data: DownloadedMedia[],
|
||||
mediaMap: DownloadedMediaMap[],
|
||||
fileName: string,
|
||||
error: boolean
|
||||
} | undefined> {
|
||||
|
|
@ -434,6 +423,7 @@ export default class AnimeOnegai implements ServiceClass {
|
|||
}
|
||||
|
||||
const files: DownloadedMedia[] = [];
|
||||
const mediaMap: DownloadedMediaMap[] = [];
|
||||
|
||||
let subIndex = 0;
|
||||
let dlFailed = false;
|
||||
|
|
@ -441,6 +431,11 @@ export default class AnimeOnegai implements ServiceClass {
|
|||
|
||||
for (const media of medias.data) {
|
||||
console.info(`Requesting: [E.${media.episode.ID}] ${mediaName}`);
|
||||
|
||||
const fileMap: DownloadedMediaMap = {
|
||||
version: media.episode.ID.toString(),
|
||||
files: []
|
||||
};
|
||||
|
||||
const AuthHeaders = {
|
||||
headers: {
|
||||
|
|
@ -713,6 +708,11 @@ export default class AnimeOnegai implements ServiceClass {
|
|||
path: `${tsFile}.video.mp4`,
|
||||
lang: lang
|
||||
});
|
||||
fileMap.files.push({
|
||||
type: 'Video',
|
||||
path: `${tsFile}.video.mp4`,
|
||||
lang: lang
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -734,6 +734,11 @@ export default class AnimeOnegai implements ServiceClass {
|
|||
path: `${tsFile}.audio.mp4`,
|
||||
lang: lang
|
||||
});
|
||||
fileMap.files.push({
|
||||
type: 'Audio',
|
||||
path: `${tsFile}.audio.mp4`,
|
||||
lang: lang
|
||||
});
|
||||
console.info('Decryption done for audio');
|
||||
}
|
||||
}
|
||||
|
|
@ -747,6 +752,11 @@ export default class AnimeOnegai implements ServiceClass {
|
|||
path: `${tsFile}.video.mp4`,
|
||||
lang: lang
|
||||
});
|
||||
fileMap.files.push({
|
||||
type: 'Video',
|
||||
path: `${tsFile}.video.mp4`,
|
||||
lang: lang
|
||||
});
|
||||
}
|
||||
if (audioDownloaded) {
|
||||
files.push({
|
||||
|
|
@ -754,6 +764,11 @@ export default class AnimeOnegai implements ServiceClass {
|
|||
path: `${tsFile}.audio.mp4`,
|
||||
lang: lang
|
||||
});
|
||||
fileMap.files.push({
|
||||
type: 'Audio',
|
||||
path: `${tsFile}.audio.mp4`,
|
||||
lang: lang
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -795,6 +810,11 @@ export default class AnimeOnegai implements ServiceClass {
|
|||
...sxData as sxItem,
|
||||
cc: false
|
||||
});
|
||||
fileMap.files.push({
|
||||
type: 'Subtitle',
|
||||
...sxData as sxItem,
|
||||
cc: false
|
||||
});
|
||||
} else{
|
||||
console.warn(`Failed to download subtitle: ${sxData.file}`);
|
||||
}
|
||||
|
|
@ -808,11 +828,13 @@ export default class AnimeOnegai implements ServiceClass {
|
|||
else{
|
||||
console.info('Subtitles downloading skipped!');
|
||||
}
|
||||
mediaMap.push(fileMap);
|
||||
await this.sleep(options.waittime);
|
||||
}
|
||||
return {
|
||||
error: dlFailed,
|
||||
data: files,
|
||||
mediaMap,
|
||||
fileName: fileName ? (path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName)) || './unknown' : './unknown'
|
||||
};
|
||||
}
|
||||
|
|
|
|||
111
crunchy.ts
111
crunchy.ts
|
|
@ -1,18 +1,17 @@
|
|||
// build-in
|
||||
// Built In
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
// package program
|
||||
// Package
|
||||
import packageJson from './package.json';
|
||||
|
||||
// plugins
|
||||
import { console } from './modules/log';
|
||||
// Plugins
|
||||
import shlp from 'sei-helper';
|
||||
import m3u8 from 'm3u8-parsed';
|
||||
import streamdl, { M3U8Json } from './modules/hls-download';
|
||||
import { exec } from './modules/sei-helper-fixes';
|
||||
|
||||
// custom modules
|
||||
import { console } from './modules/log';
|
||||
import * as fontsData from './modules/module.fontsData';
|
||||
import * as langsData from './modules/module.langsData';
|
||||
import * as yamlCfg from './modules/module.cfg-loader';
|
||||
|
|
@ -20,40 +19,32 @@ import * as yargs from './modules/module.app-args';
|
|||
import Merger, { Font, MergerInput, SubtitleInput } from './modules/module.merger';
|
||||
import getKeys, { canDecrypt } from './modules/widevine';
|
||||
//import vttConvert from './modules/module.vttconvert';
|
||||
|
||||
// args
|
||||
|
||||
// load req
|
||||
import { domain, api } from './modules/module.api-urls';
|
||||
import * as reqModule from './modules/module.fetch';
|
||||
import { CrunchySearch } from './@types/crunchySearch';
|
||||
import { CrunchyEpisodeList, CrunchyEpisode } from './@types/crunchyEpisodeList';
|
||||
import { CrunchyDownloadOptions, CrunchyEpMeta, CrunchyMuxOptions, CrunchyMultiDownload, DownloadedMedia, ParseItem, SeriesSearch, SeriesSearchItem } from './@types/crunchyTypes';
|
||||
import { ObjectInfo } from './@types/objectInfo';
|
||||
import parseFileName, { Variable } from './modules/module.filename';
|
||||
import { CrunchyStreams, PlaybackData, Subtitles } from './@types/playbackData';
|
||||
import { downloaded } from './modules/module.downloadArchive';
|
||||
import parseSelect from './modules/module.parseSelect';
|
||||
import { AvailableFilenameVars, getDefault } from './modules/module.args';
|
||||
import { AuthData, AuthResponse, Episode, ResponseBase, SearchData, SearchResponse, SearchResponseItem } from './@types/messageHandler';
|
||||
import { ServiceClass } from './@types/serviceClassInterface';
|
||||
import { CrunchyAndroidStreams } from './@types/crunchyAndroidStreams';
|
||||
import { CrunchyAndroidEpisodes } from './@types/crunchyAndroidEpisodes';
|
||||
import { parse } from './modules/module.transform-mpd';
|
||||
import { CrunchyAndroidObject } from './@types/crunchyAndroidObject';
|
||||
import { CrunchyChapters, CrunchyChapter, CrunchyOldChapter } from './@types/crunchyChapters';
|
||||
import vtt2ass from './modules/module.vtt2ass';
|
||||
import { CrunchyPlayStream } from './@types/crunchyPlayStreams';
|
||||
import { CrunchyPlayStreams } from './@types/enums';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import streamdl, { M3U8Json } from './modules/hls-download';
|
||||
import { exec } from './modules/sei-helper-fixes';
|
||||
|
||||
export type sxItem = {
|
||||
language: langsData.LanguageItem,
|
||||
path: string,
|
||||
file: string
|
||||
title: string,
|
||||
fonts: Font[]
|
||||
}
|
||||
// Types
|
||||
import type { CrunchyDownloadOptions, CrunchyEpMeta, CrunchyMuxOptions, CrunchyMultiDownload, ParseItem, SeriesSearch, SeriesSearchItem } from './@types/crunchyTypes';
|
||||
import type { DownloadedMedia, DownloadedMediaMap, sxItem } from './@types/downloaderTypes';
|
||||
import type { CrunchySearch } from './@types/crunchySearch';
|
||||
import type { CrunchyEpisodeList, CrunchyEpisode } from './@types/crunchyEpisodeList';
|
||||
import type { ObjectInfo } from './@types/objectInfo';
|
||||
import type { AuthData, AuthResponse, Episode, ResponseBase, SearchData, SearchResponse, SearchResponseItem } from './@types/messageHandler';
|
||||
import type { ServiceClass } from './@types/serviceClassInterface';
|
||||
import type { CrunchyAndroidStreams } from './@types/crunchyAndroidStreams';
|
||||
import type { CrunchyAndroidEpisodes } from './@types/crunchyAndroidEpisodes';
|
||||
import type { CrunchyPlayStream } from './@types/crunchyPlayStreams';
|
||||
import type { CrunchyAndroidObject } from './@types/crunchyAndroidObject';
|
||||
import type { CrunchyChapters, CrunchyChapter, CrunchyOldChapter } from './@types/crunchyChapters';
|
||||
import type { CrunchyStreams, PlaybackData, Subtitles } from './@types/playbackData';
|
||||
import { CrunchyPlayStreams } from './@types/enums';
|
||||
|
||||
export default class Crunchy implements ServiceClass {
|
||||
public cfg: yamlCfg.ConfigObject;
|
||||
|
|
@ -985,7 +976,7 @@ export default class Crunchy implements ServiceClass {
|
|||
return false;
|
||||
} else {
|
||||
if (!options.skipmux) {
|
||||
await this.muxStreams(res.data, { ...options, output: res.fileName });
|
||||
await this.muxStreams(res.data, res.mediaMap, { ...options, output: res.fileName });
|
||||
} else {
|
||||
console.info('Skipping mux');
|
||||
}
|
||||
|
|
@ -1195,6 +1186,7 @@ export default class Crunchy implements ServiceClass {
|
|||
|
||||
public async downloadMediaList(medias: CrunchyEpMeta, options: CrunchyDownloadOptions) : Promise<{
|
||||
data: DownloadedMedia[],
|
||||
mediaMap: DownloadedMediaMap[],
|
||||
fileName: string,
|
||||
error: boolean
|
||||
} | undefined> {
|
||||
|
|
@ -1214,6 +1206,7 @@ export default class Crunchy implements ServiceClass {
|
|||
}
|
||||
|
||||
const files: DownloadedMedia[] = [];
|
||||
const mediaMap: DownloadedMediaMap[] = [];
|
||||
|
||||
if(medias.data.every(a => !a.playback)){
|
||||
console.warn('Video not available!');
|
||||
|
|
@ -1230,6 +1223,11 @@ export default class Crunchy implements ServiceClass {
|
|||
// Make sure we have a media id without a : in it
|
||||
const currentMediaId = (mMeta.mediaId.includes(':') ? mMeta.mediaId.split(':')[1] : mMeta.mediaId);
|
||||
|
||||
const fileMap: DownloadedMediaMap = {
|
||||
version: currentMediaId,
|
||||
files: []
|
||||
};
|
||||
|
||||
//Make sure token is up-to-date
|
||||
await this.refreshToken(true, true);
|
||||
let currentVersion;
|
||||
|
|
@ -1377,7 +1375,7 @@ export default class Crunchy implements ServiceClass {
|
|||
if (options.cstream !== 'none') {
|
||||
const playbackReq = await this.req.getData(`https://cr-play-service.prd.crunchyrollsvc.com/v1/${currentVersion ? currentVersion.guid : currentMediaId}/${CrunchyPlayStreams[options.cstream]}/play`, AuthHeaders);
|
||||
if (!playbackReq.ok || !playbackReq.res) {
|
||||
console.error('Non-DRM Request Stream URLs FAILED!');
|
||||
console.error('Request for Stream URLs FAILED!');
|
||||
} else {
|
||||
playStream = await playbackReq.res.json() as CrunchyPlayStream;
|
||||
const derivedPlaystreams = {} as CrunchyStreams;
|
||||
|
|
@ -1779,6 +1777,12 @@ export default class Crunchy implements ServiceClass {
|
|||
lang: lang,
|
||||
isPrimary: isPrimary
|
||||
});
|
||||
fileMap.files.push({
|
||||
type: 'Video',
|
||||
path: `${tsFile}.video.m4s`,
|
||||
lang: lang,
|
||||
isPrimary: isPrimary
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1801,6 +1805,12 @@ export default class Crunchy implements ServiceClass {
|
|||
lang: lang,
|
||||
isPrimary: isPrimary
|
||||
});
|
||||
fileMap.files.push({
|
||||
type: 'Audio',
|
||||
path: `${tsFile}.audio.m4s`,
|
||||
lang: lang,
|
||||
isPrimary: isPrimary
|
||||
});
|
||||
console.info('Decryption done for audio');
|
||||
}
|
||||
}
|
||||
|
|
@ -1815,6 +1825,12 @@ export default class Crunchy implements ServiceClass {
|
|||
lang: lang,
|
||||
isPrimary: isPrimary
|
||||
});
|
||||
fileMap.files.push({
|
||||
type: 'Video',
|
||||
path: `${tsFile}.video.m4s`,
|
||||
lang: lang,
|
||||
isPrimary: isPrimary
|
||||
});
|
||||
}
|
||||
if (audioDownloaded) {
|
||||
files.push({
|
||||
|
|
@ -1823,6 +1839,12 @@ export default class Crunchy implements ServiceClass {
|
|||
lang: lang,
|
||||
isPrimary: isPrimary
|
||||
});
|
||||
fileMap.files.push({
|
||||
type: 'Audio',
|
||||
path: `${tsFile}.audio.m4s`,
|
||||
lang: lang,
|
||||
isPrimary: isPrimary
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (!options.novids) {
|
||||
|
|
@ -1980,6 +2002,12 @@ export default class Crunchy implements ServiceClass {
|
|||
lang: lang,
|
||||
isPrimary: isPrimary
|
||||
});
|
||||
fileMap.files.push({
|
||||
type: 'Video',
|
||||
path: `${tsFile}.ts`,
|
||||
lang: lang,
|
||||
isPrimary: isPrimary
|
||||
});
|
||||
dlVideoOnce = true;
|
||||
}
|
||||
} else{
|
||||
|
|
@ -2017,6 +2045,11 @@ export default class Crunchy implements ServiceClass {
|
|||
lang: lang,
|
||||
type: 'Chapters'
|
||||
});
|
||||
fileMap.files.push({
|
||||
path: `${tsFile}.txt`,
|
||||
lang: lang,
|
||||
type: 'Chapters'
|
||||
});
|
||||
} catch {
|
||||
console.error('Failed to write chapter file');
|
||||
}
|
||||
|
|
@ -2104,6 +2137,12 @@ export default class Crunchy implements ServiceClass {
|
|||
cc: isCC,
|
||||
signs: isSigns,
|
||||
});
|
||||
fileMap.files.push({
|
||||
type: 'Subtitle',
|
||||
...sxData as sxItem,
|
||||
cc: isCC,
|
||||
signs: isSigns,
|
||||
});
|
||||
}
|
||||
else{
|
||||
console.warn(`Failed to download subtitle: ${sxData.file}`);
|
||||
|
|
@ -2118,16 +2157,19 @@ export default class Crunchy implements ServiceClass {
|
|||
console.info('Subtitles downloading skipped!');
|
||||
}
|
||||
|
||||
mediaMap.push(fileMap);
|
||||
await this.sleep(options.waittime);
|
||||
}
|
||||
|
||||
return {
|
||||
error: dlFailed,
|
||||
data: files,
|
||||
mediaMap,
|
||||
fileName: fileName ? (path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName)) || './unknown' : './unknown'
|
||||
};
|
||||
}
|
||||
|
||||
public async muxStreams(data: DownloadedMedia[], options: CrunchyMuxOptions) {
|
||||
public async muxStreams(data: DownloadedMedia[], mediaMap: DownloadedMediaMap[], options: CrunchyMuxOptions) {
|
||||
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||
let hasAudioStreams = false;
|
||||
if (options.novids || data.filter(a => a.type === 'Video').length === 0)
|
||||
|
|
@ -2136,6 +2178,7 @@ export default class Crunchy implements ServiceClass {
|
|||
hasAudioStreams = true;
|
||||
}
|
||||
const merger = new Merger({
|
||||
mediaMap,
|
||||
onlyVid: hasAudioStreams ? data.filter(a => a.type === 'Video').map((a) : MergerInput => {
|
||||
return {
|
||||
lang: a.lang,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
78
hidive.ts
78
hidive.ts
|
|
@ -1,44 +1,43 @@
|
|||
// build-in
|
||||
// Built In
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
// package program
|
||||
// Package
|
||||
import packageJson from './package.json';
|
||||
|
||||
// plugins
|
||||
import { console } from './modules/log';
|
||||
// Plugins
|
||||
import shlp from 'sei-helper';
|
||||
import streamdl, { M3U8Json } from './modules/hls-download';
|
||||
|
||||
// custom modules
|
||||
// Custom Modules
|
||||
import { console } from './modules/log';
|
||||
import streamdl, { M3U8Json } from './modules/hls-download';
|
||||
import * as fontsData from './modules/module.fontsData';
|
||||
import * as langsData from './modules/module.langsData';
|
||||
import * as yamlCfg from './modules/module.cfg-loader';
|
||||
import * as yargs from './modules/module.app-args';
|
||||
import Merger, { Font, MergerInput, SubtitleInput } from './modules/module.merger';
|
||||
import vtt2ass from './modules/module.vtt2ass';
|
||||
|
||||
// load req
|
||||
import { domain, api } from './modules/module.api-urls';
|
||||
import * as reqModule from './modules/module.req';
|
||||
import { DownloadedMedia } from './@types/hidiveTypes';
|
||||
import parseFileName, { Variable } from './modules/module.filename';
|
||||
import { downloaded } from './modules/module.downloadArchive';
|
||||
import parseSelect from './modules/module.parseSelect';
|
||||
import { AvailableFilenameVars } from './modules/module.args';
|
||||
import { AuthData, AuthResponse, SearchData, SearchResponse, SearchResponseItem } from './@types/messageHandler';
|
||||
import { ServiceClass } from './@types/serviceClassInterface';
|
||||
import { sxItem } from './crunchy';
|
||||
import { Hit, NewHidiveSearch } from './@types/newHidiveSearch';
|
||||
import { NewHidiveSeries } from './@types/newHidiveSeries';
|
||||
import { Episode, NewHidiveEpisodeExtra, NewHidiveSeason, NewHidiveSeriesExtra } from './@types/newHidiveSeason';
|
||||
import { NewHidiveEpisode } from './@types/newHidiveEpisode';
|
||||
import { NewHidivePlayback, Subtitle } from './@types/newHidivePlayback';
|
||||
import { MPDParsed, parse } from './modules/module.transform-mpd';
|
||||
import getKeys, { canDecrypt } from './modules/widevine';
|
||||
import { exec } from './modules/sei-helper-fixes';
|
||||
import { KeyContainer } from './modules/license';
|
||||
|
||||
// Types
|
||||
import type { AuthData, AuthResponse, SearchData, SearchResponse, SearchResponseItem } from './@types/messageHandler';
|
||||
import type { ServiceClass } from './@types/serviceClassInterface';
|
||||
import type { Hit, NewHidiveSearch } from './@types/newHidiveSearch';
|
||||
import type { NewHidiveSeries } from './@types/newHidiveSeries';
|
||||
import type { Episode, NewHidiveEpisodeExtra, NewHidiveSeason, NewHidiveSeriesExtra } from './@types/newHidiveSeason';
|
||||
import type { NewHidiveEpisode } from './@types/newHidiveEpisode';
|
||||
import type { NewHidivePlayback, Subtitle } from './@types/newHidivePlayback';
|
||||
import type { DownloadedMedia, DownloadedMediaMap, sxItem } from './@types/downloaderTypes';
|
||||
|
||||
export default class Hidive implements ServiceClass {
|
||||
public cfg: yamlCfg.ConfigObject;
|
||||
private token: Record<string, any>;
|
||||
|
|
@ -547,7 +546,7 @@ export default class Hidive implements ServiceClass {
|
|||
return { isOk: false, reason: new Error('Failed to download media list') };
|
||||
} else {
|
||||
if (!options.skipmux) {
|
||||
await this.muxStreams(res.data, { ...options, output: res.fileName }, false);
|
||||
await this.muxStreams(res.data, res.mediaMap, { ...options, output: res.fileName }, false);
|
||||
} else {
|
||||
console.info('Skipping mux');
|
||||
}
|
||||
|
|
@ -636,7 +635,7 @@ export default class Hidive implements ServiceClass {
|
|||
return { isOk: false, reason: new Error('Failed to download media list') };
|
||||
} else {
|
||||
if (!options.skipmux) {
|
||||
await this.muxStreams(res.data, { ...options, output: res.fileName }, false);
|
||||
await this.muxStreams(res.data, res.mediaMap, { ...options, output: res.fileName }, false);
|
||||
} else {
|
||||
console.info('Skipping mux');
|
||||
}
|
||||
|
|
@ -651,6 +650,7 @@ export default class Hidive implements ServiceClass {
|
|||
public async downloadMPD(streamPlaylists: MPDParsed, subs: Subtitle[], selectedEpisode: NewHidiveEpisodeExtra, options: Record<any, any>) {
|
||||
//let fileName: string;
|
||||
const files: DownloadedMedia[] = [];
|
||||
const mediaMap: DownloadedMediaMap[] = [];
|
||||
const variables: Variable[] = [];
|
||||
let dlFailed = false;
|
||||
const subsMargin = 0;
|
||||
|
|
@ -658,6 +658,11 @@ export default class Hidive implements ServiceClass {
|
|||
let encryptionKeys: KeyContainer[] = [];
|
||||
if (!canDecrypt) console.warn('Decryption not enabled!');
|
||||
|
||||
const fileMap: DownloadedMediaMap = {
|
||||
version: selectedEpisode.id.toString(),
|
||||
files: []
|
||||
};
|
||||
|
||||
if (!this.cfg.bin.ffmpeg)
|
||||
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||
|
||||
|
|
@ -839,6 +844,12 @@ export default class Hidive implements ServiceClass {
|
|||
lang: chosenAudios[0].language,
|
||||
isPrimary: true
|
||||
});
|
||||
fileMap.files.push({
|
||||
type: 'Video',
|
||||
path: `${tsFile}.video.m4s`,
|
||||
lang: chosenAudios[0].language,
|
||||
isPrimary: true
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.warn('mp4decrypt not found, files need decryption. Decryption Keys:', encryptionKeys);
|
||||
|
|
@ -920,6 +931,12 @@ export default class Hidive implements ServiceClass {
|
|||
lang: chosenAudioSegments.language,
|
||||
isPrimary: chosenAudioSegments.default
|
||||
});
|
||||
fileMap.files.push({
|
||||
type: 'Audio',
|
||||
path: `${tsFile}.audio.m4s`,
|
||||
lang: chosenAudioSegments.language,
|
||||
isPrimary: chosenAudioSegments.default
|
||||
});
|
||||
console.info('Decryption done for audio');
|
||||
}
|
||||
} else {
|
||||
|
|
@ -968,6 +985,11 @@ export default class Hidive implements ServiceClass {
|
|||
...sxData as sxItem,
|
||||
cc: false
|
||||
});
|
||||
fileMap.files.push({
|
||||
type: 'Subtitle',
|
||||
...sxData as sxItem,
|
||||
cc: false
|
||||
});
|
||||
} else{
|
||||
console.warn(`Failed to download subtitle: ${sxData.file}`);
|
||||
}
|
||||
|
|
@ -981,14 +1003,17 @@ export default class Hidive implements ServiceClass {
|
|||
console.info('Subtitles downloading skipped!');
|
||||
}
|
||||
|
||||
mediaMap.push(fileMap);
|
||||
|
||||
return {
|
||||
error: dlFailed,
|
||||
data: files,
|
||||
mediaMap,
|
||||
fileName: fileName ? (path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName)) || './unknown' : './unknown'
|
||||
};
|
||||
}
|
||||
|
||||
public async muxStreams(data: DownloadedMedia[], options: Record<any, any>, inverseTrackOrder: boolean = true) {
|
||||
public async muxStreams(data: DownloadedMedia[], mediaMap: DownloadedMediaMap[], options: Record<any, any>, inverseTrackOrder: boolean = true) {
|
||||
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||
let hasAudioStreams = false;
|
||||
if (options.novids || data.filter(a => a.type === 'Video').length === 0)
|
||||
|
|
@ -997,9 +1022,8 @@ export default class Hidive implements ServiceClass {
|
|||
hasAudioStreams = true;
|
||||
}
|
||||
const merger = new Merger({
|
||||
mediaMap,
|
||||
onlyVid: hasAudioStreams ? data.filter(a => a.type === 'Video').map((a) : MergerInput => {
|
||||
if (a.type === 'Subtitle')
|
||||
throw new Error('Never');
|
||||
return {
|
||||
lang: a.lang,
|
||||
path: a.path,
|
||||
|
|
@ -1009,8 +1033,6 @@ export default class Hidive implements ServiceClass {
|
|||
inverseTrackOrder: inverseTrackOrder,
|
||||
keepAllVideos: options.keepAllVideos,
|
||||
onlyAudio: hasAudioStreams ? data.filter(a => a.type === 'Audio').map((a) : MergerInput => {
|
||||
if (a.type === 'Subtitle')
|
||||
throw new Error('Never');
|
||||
return {
|
||||
lang: a.lang,
|
||||
path: a.path,
|
||||
|
|
@ -1018,10 +1040,6 @@ export default class Hidive implements ServiceClass {
|
|||
}) : [],
|
||||
output: `${options.output}.${options.mp4 ? 'mp4' : 'mkv'}`,
|
||||
subtitles: data.filter(a => a.type === 'Subtitle').map((a) : SubtitleInput => {
|
||||
if (a.type === 'Video')
|
||||
throw new Error('Never');
|
||||
if (a.type === 'Audio')
|
||||
throw new Error('Never');
|
||||
return {
|
||||
file: a.path,
|
||||
language: a.language,
|
||||
|
|
@ -1029,14 +1047,10 @@ export default class Hidive implements ServiceClass {
|
|||
};
|
||||
}),
|
||||
simul: data.filter(a => a.type === 'Video').map((a) : boolean => {
|
||||
if (a.type === 'Subtitle')
|
||||
throw new Error('Never');
|
||||
return !a.uncut as boolean;
|
||||
})[0],
|
||||
fonts: Merger.makeFontsList(this.cfg.dir.fonts, data.filter(a => a.type === 'Subtitle') as sxItem[]),
|
||||
videoAndAudio: hasAudioStreams ? [] : data.filter(a => a.type === 'Video').map((a) : MergerInput => {
|
||||
if (a.type === 'Subtitle')
|
||||
throw new Error('Never');
|
||||
return {
|
||||
lang: a.lang,
|
||||
path: a.path,
|
||||
|
|
|
|||
|
|
@ -1,20 +1,33 @@
|
|||
import * as iso639 from 'iso-639';
|
||||
import * as yamlCfg from './module.cfg-loader';
|
||||
import { fontFamilies, fontMime } from './module.fontsData';
|
||||
// Native
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { LanguageItem } from './module.langsData';
|
||||
import { AvailableMuxer } from './module.args';
|
||||
import { spawn } from 'child_process';
|
||||
import os from 'os';
|
||||
|
||||
// Plugins
|
||||
import * as iso639 from 'iso-639';
|
||||
import { exec } from './sei-helper-fixes';
|
||||
import { console } from './log';
|
||||
import ffprobe from 'ffprobe';
|
||||
import { OggOpusDecodedAudio, OggOpusDecoder } from 'ogg-opus-decoder';
|
||||
import SynAudio, { MultipleClipMatch, MultipleClipMatchFirst } from 'synaudio';
|
||||
|
||||
// Modules
|
||||
import { LanguageItem } from './module.langsData';
|
||||
import { AvailableMuxer } from './module.args';
|
||||
import * as yamlCfg from './module.cfg-loader';
|
||||
import { fontFamilies, fontMime } from './module.fontsData';
|
||||
|
||||
// Types
|
||||
import { DownloadedMediaMap } from '../@types/downloaderTypes';
|
||||
|
||||
export type MergerInput = {
|
||||
path: string,
|
||||
lang: LanguageItem,
|
||||
duration?: number,
|
||||
duration?: OggOpusDecodedAudio,
|
||||
delay?: number,
|
||||
isPrimary?: boolean,
|
||||
totalDuration?: number,
|
||||
}
|
||||
|
||||
export type SubtitleInput = {
|
||||
|
|
@ -34,6 +47,7 @@ export type ParsedFont = {
|
|||
}
|
||||
|
||||
export type MergerOptions = {
|
||||
mediaMap?: DownloadedMediaMap[],
|
||||
videoAndAudio: MergerInput[],
|
||||
onlyVid: MergerInput[],
|
||||
onlyAudio: MergerInput[],
|
||||
|
|
@ -58,7 +72,7 @@ export type MergerOptions = {
|
|||
}
|
||||
|
||||
class Merger {
|
||||
|
||||
|
||||
constructor(private options: MergerOptions) {
|
||||
if (this.options.skipSubMux)
|
||||
this.options.subtitles = [];
|
||||
|
|
@ -66,46 +80,146 @@ 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
|
||||
if (this.options.videoAndAudio.length > 1) {
|
||||
const bin = await yamlCfg.loadBinCfg();
|
||||
const vnas = this.options.videoAndAudio;
|
||||
//get and set durations on each videoAndAudio Stream
|
||||
for (const [vnaIndex, vna] of vnas.entries()) {
|
||||
const streamInfo = await ffprobe(vna.path, { path: bin.ffprobe as string });
|
||||
const videoInfo = streamInfo.streams.filter(stream => stream.codec_type == 'video');
|
||||
vnas[vnaIndex].duration = parseInt(videoInfo[0].duration as string);
|
||||
}
|
||||
//Sort videoAndAudio streams by duration (shortest first)
|
||||
vnas.sort((a,b) => {
|
||||
if (!a.duration || !b.duration) return -1;
|
||||
return a.duration - b.duration;
|
||||
async convertFile(path: string, ffmpeg: string): Promise<Uint8Array> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = [
|
||||
'-t',
|
||||
'60',
|
||||
'-i',
|
||||
path,
|
||||
'-vn',
|
||||
'-c:a',
|
||||
'libopus',
|
||||
'-filter:a',
|
||||
'loudnorm',
|
||||
'-f',
|
||||
'ogg',
|
||||
'pipe:1'
|
||||
];
|
||||
console.info(ffmpeg, options.join(' '));
|
||||
const ffmpegSpawn = spawn(ffmpeg, options);
|
||||
const data: number[] = [];
|
||||
|
||||
ffmpegSpawn.stdout.on('data', (chunk) => {
|
||||
data.push(...chunk);
|
||||
});
|
||||
//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;
|
||||
|
||||
ffmpegSpawn.stderr.on('data', (err) => {
|
||||
//console.debug(err.toString().trimEnd());
|
||||
});
|
||||
|
||||
ffmpegSpawn.on('close', (code) => {
|
||||
if (code !== 0) {
|
||||
console.error('FFmpeg exited with code: ', code);
|
||||
return reject();
|
||||
}
|
||||
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;
|
||||
resolve(new Uint8Array(data));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async decodeAudio(data: Uint8Array): Promise<OggOpusDecodedAudio> {
|
||||
const decoder = new OggOpusDecoder();
|
||||
await decoder.ready;
|
||||
|
||||
return decoder.decode(data);
|
||||
}
|
||||
|
||||
public async createDelays() {
|
||||
const bin = await yamlCfg.loadBinCfg();
|
||||
const allFiles = [...this.options.onlyAudio, ...this.options.videoAndAudio, ...this.options.onlyVid, ...this.options.subtitles];
|
||||
if (this.options.chapters)
|
||||
allFiles.push(...this.options.chapters);
|
||||
const audios = [...this.options.onlyAudio, ...this.options.videoAndAudio];
|
||||
if (audios.length < 2)
|
||||
return;
|
||||
|
||||
if (bin.ffmpeg === undefined || bin.ffmpeg.trim().length === 0)
|
||||
return console.error('Unable to sync timing without ffmpeg!');
|
||||
|
||||
for (const audio of audios) {
|
||||
try {
|
||||
const track = (
|
||||
await this.decodeAudio(
|
||||
await this.convertFile(audio.path, bin.ffmpeg)
|
||||
)
|
||||
);
|
||||
if (track.errors.length > 0) {
|
||||
console.error(`Unable to decode ${audio.path}: ${track.errors}`);
|
||||
return;
|
||||
}
|
||||
audio.duration = track;
|
||||
const streamInfo = await ffprobe(audio.path, { path: bin.ffprobe as string });
|
||||
const compare = streamInfo.streams.find(s => s.codec_type === 'video') || streamInfo.streams.find(s => s.codec_type === 'audio');
|
||||
audio.totalDuration = parseInt(compare!.duration!);
|
||||
} catch (e) {
|
||||
console.error(`Unable to generate sync timing because of file ${audio.path}: ${e}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
audios.sort((a, b) => a.totalDuration! - b.totalDuration!);
|
||||
|
||||
const synAudio = new SynAudio({
|
||||
correlationSampleSize: 50000,
|
||||
initialGranularity: 12,
|
||||
correlationThreshold: 0.5
|
||||
});
|
||||
|
||||
const audioArray = await synAudio.syncMultiple(
|
||||
audios.map((audio) => {
|
||||
return {
|
||||
name: audio.path,
|
||||
data: {
|
||||
channelData: audio.duration!.channelData,
|
||||
samplesDecoded: audio.duration!.samplesDecoded,
|
||||
}
|
||||
};
|
||||
}),
|
||||
os.cpus().length-1
|
||||
);
|
||||
|
||||
//console.debug(audioArray);
|
||||
|
||||
// Find max sampleOffset value
|
||||
const maxSampleOffset = Math.max(...audioArray[0].map(item => item.sampleOffset));
|
||||
|
||||
// Invert the sampleOffset values
|
||||
const invertedArray = audioArray[0].map(item => ({
|
||||
...item,
|
||||
sampleOffset: maxSampleOffset - item.sampleOffset
|
||||
}));
|
||||
|
||||
//Iterate over found matches
|
||||
invertedArray.forEach((clip: MultipleClipMatch | MultipleClipMatchFirst) => {
|
||||
const audio = audios.find(a => a.path === clip.name)!;
|
||||
audio.delay = (clip.sampleOffset || 0 ) / audio.duration!.sampleRate;
|
||||
|
||||
// Iterate over each DownloadedMediaMap in the array
|
||||
for (const mediaMap of this.options.mediaMap!) {
|
||||
// Iterate over the files array in each DownloadedMediaMap
|
||||
for (const media of mediaMap.files) {
|
||||
// Check if the path matches
|
||||
if (media.path === clip.name) {
|
||||
//console.debug('Matching path found:', media.path);
|
||||
|
||||
// Re-iterate over the files array where the match was found
|
||||
mediaMap.files.forEach((file) => {
|
||||
const mergerFile = allFiles.find(a =>
|
||||
('path' in a ? a.path : a.file) === file.path
|
||||
);
|
||||
if (mergerFile)
|
||||
mergerFile.delay = audio.delay;
|
||||
else
|
||||
console.error(`File ${file.path} was not found in allFiles array, this shouldn't happen`);
|
||||
});
|
||||
|
||||
// We only want the first match, so break out of the loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public FFmpeg() : string {
|
||||
|
|
@ -118,8 +232,9 @@ class Merger {
|
|||
|
||||
for (const vid of this.options.videoAndAudio) {
|
||||
if (vid.delay && hasVideo) {
|
||||
console.info('Inserting delay vid', vid.delay);
|
||||
args.push(
|
||||
`-itsoffset -${Math.ceil(vid.delay*1000)}ms`
|
||||
`-itsoffset ${Math.round(vid.delay * 1000) / 1000}`
|
||||
);
|
||||
}
|
||||
args.push(`-i "${vid.path}"`);
|
||||
|
|
@ -147,6 +262,10 @@ class Merger {
|
|||
}
|
||||
|
||||
for (const aud of this.options.onlyAudio) {
|
||||
if (aud.delay) {
|
||||
console.info('Inserting delay', aud.delay);
|
||||
args.push(`-itsoffset -${Math.ceil(aud.delay * 1000) / 1000}`);
|
||||
}
|
||||
args.push(`-i "${aud.path}"`);
|
||||
metaData.push(`-map ${index}`);
|
||||
metaData.push(`-metadata:s:a:${audioIndex} language=${aud.lang.code}`);
|
||||
|
|
@ -158,7 +277,7 @@ class Merger {
|
|||
const sub = this.options.subtitles[index];
|
||||
if (sub.delay) {
|
||||
args.push(
|
||||
`-itsoffset -${Math.ceil(sub.delay*1000)}ms`
|
||||
`-itsoffset -${Math.round(sub.delay*1000)}ms`
|
||||
);
|
||||
}
|
||||
args.push(`-i "${sub.file}"`);
|
||||
|
|
@ -229,7 +348,7 @@ class Merger {
|
|||
const videoTrackNum = this.options.inverseTrackOrder ? '1' : '0';
|
||||
if (vid.delay) {
|
||||
args.push(
|
||||
`--sync ${audioTrackNum}:-${Math.ceil(vid.delay*1000)}`
|
||||
`--sync ${audioTrackNum}:-${Math.round(vid.delay*1000)}`
|
||||
);
|
||||
}
|
||||
if (!hasVideo || this.options.keepAllVideos) {
|
||||
|
|
@ -264,6 +383,11 @@ class Merger {
|
|||
}
|
||||
|
||||
for (const aud of this.options.onlyAudio) {
|
||||
if (aud.delay) {
|
||||
args.push(
|
||||
`--sync 0:-${Math.round(aud.delay*1000)}`
|
||||
);
|
||||
}
|
||||
const trackName = aud.lang.name;
|
||||
args.push('--track-name', `0:"${trackName}"`);
|
||||
args.push(`--language 0:${aud.lang.code}`);
|
||||
|
|
@ -283,7 +407,7 @@ class Merger {
|
|||
for (const subObj of this.options.subtitles) {
|
||||
if (subObj.delay) {
|
||||
args.push(
|
||||
`--sync 0:-${Math.ceil(subObj.delay*1000)}`
|
||||
`--sync 0:-${Math.round(subObj.delay*1000)}`
|
||||
);
|
||||
}
|
||||
args.push('--track-name', `0:"${(subObj.language.language || subObj.language.name) + `${subObj.closedCaption === true ? ` ${this.options.ccTag}` : ''}` + `${subObj.signs === true ? ' Signs' : ''}`}"`);
|
||||
|
|
@ -380,7 +504,7 @@ class Merger {
|
|||
path: fontPath,
|
||||
mime: mime,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,9 +55,11 @@
|
|||
"lookpath": "^1.2.2",
|
||||
"m3u8-parsed": "^1.3.0",
|
||||
"mpd-parser": "^1.3.0",
|
||||
"ogg-opus-decoder": "^1.6.12",
|
||||
"open": "^8.4.2",
|
||||
"protobufjs": "^7.3.2",
|
||||
"sei-helper": "^3.3.0",
|
||||
"synaudio": "^0.3.5",
|
||||
"ws": "^8.17.1",
|
||||
"yaml": "^2.4.5",
|
||||
"yargs": "^17.7.2"
|
||||
|
|
|
|||
7474
pnpm-lock.yaml
7474
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue