mirror of
https://github.com/anidl/multi-downloader-nx.git
synced 2026-03-11 17:45:30 +00:00
[CR] Initial commit to support chapters
TODO: 1) Add flag for chapters 2) Add ffmpeg merging for chapters 3) Add fallback to old CR API
This commit is contained in:
parent
7be22ec132
commit
02620ec5b5
4 changed files with 121 additions and 2 deletions
16
@types/crunchyChapters.d.ts
vendored
Normal file
16
@types/crunchyChapters.d.ts
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
export interface CrunchyChapters {
|
||||||
|
[key: string]: CrunchyChapter;
|
||||||
|
lastUpdate: Date;
|
||||||
|
mediaId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CrunchyChapter {
|
||||||
|
approverId: string;
|
||||||
|
distributionNumber: string;
|
||||||
|
end: number;
|
||||||
|
start: number;
|
||||||
|
title: string;
|
||||||
|
seriesId: string;
|
||||||
|
new: boolean;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
4
@types/crunchyTypes.d.ts
vendored
4
@types/crunchyTypes.d.ts
vendored
|
|
@ -90,6 +90,10 @@ export type DownloadedMedia = {
|
||||||
lang: LanguageItem,
|
lang: LanguageItem,
|
||||||
path: string,
|
path: string,
|
||||||
isPrimary?: boolean
|
isPrimary?: boolean
|
||||||
|
} | {
|
||||||
|
type: 'Chapters',
|
||||||
|
lang: LanguageItem,
|
||||||
|
path: string
|
||||||
} | ({
|
} | ({
|
||||||
type: 'Subtitle',
|
type: 'Subtitle',
|
||||||
cc: boolean
|
cc: boolean
|
||||||
|
|
|
||||||
91
crunchy.ts
91
crunchy.ts
|
|
@ -40,6 +40,7 @@ import { CrunchyAndroidStreams } from './@types/crunchyAndroidStreams';
|
||||||
import { CrunchyAndroidEpisodes } from './@types/crunchyAndroidEpisodes';
|
import { CrunchyAndroidEpisodes } from './@types/crunchyAndroidEpisodes';
|
||||||
import { parse } from './modules/module.transform-mpd';
|
import { parse } from './modules/module.transform-mpd';
|
||||||
import { CrunchyAndroidObject } from './@types/crunchyAndroidObject';
|
import { CrunchyAndroidObject } from './@types/crunchyAndroidObject';
|
||||||
|
import { CrunchyChapters, CrunchyChapter } from './@types/crunchyChapters';
|
||||||
|
|
||||||
export type sxItem = {
|
export type sxItem = {
|
||||||
language: langsData.LanguageItem,
|
language: langsData.LanguageItem,
|
||||||
|
|
@ -1174,7 +1175,55 @@ export default class Crunchy implements ServiceClass {
|
||||||
if (mediaId.includes(':'))
|
if (mediaId.includes(':'))
|
||||||
mediaId = mediaId.split(':')[1];
|
mediaId = mediaId.split(':')[1];
|
||||||
|
|
||||||
// /cms/v2/BUCKET/crunchyroll/videos/MEDIAID/streams
|
const chapterRequest = await this.req.getData(`https://static.crunchyroll.com/skip-events/production/${mMeta.mediaId}.json`);
|
||||||
|
let chapterData: CrunchyChapters;
|
||||||
|
const compiledChapters: string[] = [];
|
||||||
|
if(!chapterRequest.ok || !chapterRequest.res){
|
||||||
|
console.warn('Chapter request failed');
|
||||||
|
} else {
|
||||||
|
chapterData = JSON.parse(chapterRequest.res.body);
|
||||||
|
const chapters: CrunchyChapter[] = [];
|
||||||
|
for (const chapter in chapterData) {
|
||||||
|
if (typeof chapterData[chapter] == 'object') {
|
||||||
|
chapters.push(chapterData[chapter]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (chapters.length > 0) {
|
||||||
|
chapters.sort((a, b) => a.start - b.start);
|
||||||
|
for (const chapter of chapters) {
|
||||||
|
const startTime = new Date(0), endTime = new Date(0);
|
||||||
|
startTime.setSeconds(chapter.start);
|
||||||
|
endTime.setSeconds(chapter.end);
|
||||||
|
const startFormatted = startTime.toISOString().substring(11, 19)+'.00';
|
||||||
|
const endFormatted = endTime.toISOString().substring(11, 19)+'.00';
|
||||||
|
if (chapter.type == 'intro') {
|
||||||
|
if (chapter.start > 0) {
|
||||||
|
compiledChapters.push(
|
||||||
|
`CHAPTER${(compiledChapters.length/2)+1}=00:00:00.00`,
|
||||||
|
`CHAPTER${(compiledChapters.length/2)+1}NAME=Prologue`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
compiledChapters.push(
|
||||||
|
`CHAPTER${(compiledChapters.length/2)+1}=${startFormatted}`,
|
||||||
|
`CHAPTER${(compiledChapters.length/2)+1}NAME=Opening`
|
||||||
|
);
|
||||||
|
compiledChapters.push(
|
||||||
|
`CHAPTER${(compiledChapters.length/2)+1}=${endFormatted}`,
|
||||||
|
`CHAPTER${(compiledChapters.length/2)+1}NAME=Episode`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
compiledChapters.push(
|
||||||
|
`CHAPTER${(compiledChapters.length/2)+1}=${startFormatted}`,
|
||||||
|
`CHAPTER${(compiledChapters.length/2)+1}NAME=${chapter.type.charAt(0).toUpperCase() + chapter.type.slice(1)} Start`
|
||||||
|
);
|
||||||
|
compiledChapters.push(
|
||||||
|
`CHAPTER${(compiledChapters.length/2)+1}=${endFormatted}`,
|
||||||
|
`CHAPTER${(compiledChapters.length/2)+1}NAME=${chapter.type.charAt(0).toUpperCase() + chapter.type.slice(1)} End`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let pbData = { total: 0, data: {}, meta: {} } as PlaybackData;
|
let pbData = { total: 0, data: {}, meta: {} } as PlaybackData;
|
||||||
if (options.apiType == 'android') {
|
if (options.apiType == 'android') {
|
||||||
|
|
@ -1797,6 +1846,32 @@ export default class Crunchy implements ServiceClass {
|
||||||
console.info('Downloading skipped!');
|
console.info('Downloading skipped!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (compiledChapters.length > 0) {
|
||||||
|
try {
|
||||||
|
fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep);
|
||||||
|
const outFile = parseFileName(options.fileName + '.' + mMeta.lang?.name, variables, options.numbers, options.override).join(path.sep);
|
||||||
|
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);
|
||||||
|
if (!fs.existsSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val)))
|
||||||
|
fs.mkdirSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val));
|
||||||
|
});
|
||||||
|
const lang = langsData.languages.find(a => a.code === curStream?.audio_lang);
|
||||||
|
if (!lang) {
|
||||||
|
console.error(`Unable to find language for code ${curStream.audio_lang}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fs.writeFileSync(`${tsFile}.txt`, compiledChapters.join('\r\n'));
|
||||||
|
files.push({
|
||||||
|
path: `${tsFile}.txt`,
|
||||||
|
lang: lang,
|
||||||
|
type: 'Chapters'
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
console.error('Failed to write chapter file');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(options.dlsubs.indexOf('all') > -1){
|
if(options.dlsubs.indexOf('all') > -1){
|
||||||
options.dlsubs = ['all'];
|
options.dlsubs = ['all'];
|
||||||
|
|
@ -1912,6 +1987,8 @@ export default class Crunchy implements ServiceClass {
|
||||||
throw new Error('Never');
|
throw new Error('Never');
|
||||||
if (a.type === 'Audio')
|
if (a.type === 'Audio')
|
||||||
throw new Error('Never');
|
throw new Error('Never');
|
||||||
|
if (a.type === 'Chapters')
|
||||||
|
throw new Error('Never');
|
||||||
return {
|
return {
|
||||||
file: a.path,
|
file: a.path,
|
||||||
language: a.language,
|
language: a.language,
|
||||||
|
|
@ -1929,6 +2006,18 @@ export default class Crunchy implements ServiceClass {
|
||||||
path: a.path,
|
path: a.path,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
chapters: data.filter(a => a.type === 'Chapters').map((a) : MergerInput => {
|
||||||
|
if (a.type === 'Video')
|
||||||
|
throw new Error('Never');
|
||||||
|
if (a.type === 'Audio')
|
||||||
|
throw new Error('Never');
|
||||||
|
if (a.type === 'Subtitle')
|
||||||
|
throw new Error('Never');
|
||||||
|
return {
|
||||||
|
path: a.path,
|
||||||
|
lang: a.lang
|
||||||
|
};
|
||||||
|
}),
|
||||||
videoTitle: options.videoTitle,
|
videoTitle: options.videoTitle,
|
||||||
options: {
|
options: {
|
||||||
ffmpeg: options.ffmpegOptions,
|
ffmpeg: options.ffmpegOptions,
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ export type MergerOptions = {
|
||||||
onlyVid: MergerInput[],
|
onlyVid: MergerInput[],
|
||||||
onlyAudio: MergerInput[],
|
onlyAudio: MergerInput[],
|
||||||
subtitles: SubtitleInput[],
|
subtitles: SubtitleInput[],
|
||||||
|
chapters?: MergerInput[],
|
||||||
ccTag: string,
|
ccTag: string,
|
||||||
output: string,
|
output: string,
|
||||||
videoTitle?: string,
|
videoTitle?: string,
|
||||||
|
|
@ -162,7 +163,7 @@ class Merger {
|
||||||
args.push(`-i "${sub.file}"`);
|
args.push(`-i "${sub.file}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.output.split('.').pop() === 'mkv')
|
if (this.options.output.split('.').pop() === 'mkv') {
|
||||||
if (this.options.fonts) {
|
if (this.options.fonts) {
|
||||||
let fontIndex = 0;
|
let fontIndex = 0;
|
||||||
for (const font of this.options.fonts) {
|
for (const font of this.options.fonts) {
|
||||||
|
|
@ -170,6 +171,9 @@ class Merger {
|
||||||
fontIndex++;
|
fontIndex++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Make it possible for chapters to work with ffmpeg merging
|
||||||
|
|
||||||
args.push(...metaData);
|
args.push(...metaData);
|
||||||
args.push(...this.options.subtitles.map((_, subIndex) => `-map ${subIndex + index}`));
|
args.push(...this.options.subtitles.map((_, subIndex) => `-map ${subIndex + index}`));
|
||||||
|
|
@ -296,6 +300,7 @@ class Merger {
|
||||||
'--no-subtitles',
|
'--no-subtitles',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.fonts && this.options.fonts.length > 0) {
|
if (this.options.fonts && this.options.fonts.length > 0) {
|
||||||
for (const f of this.options.fonts) {
|
for (const f of this.options.fonts) {
|
||||||
args.push('--attachment-name', f.name);
|
args.push('--attachment-name', f.name);
|
||||||
|
|
@ -308,6 +313,10 @@ class Merger {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.options.chapters && this.options.chapters.length > 0) {
|
||||||
|
args.push(`--chapters "${this.options.chapters[0].path}"`);
|
||||||
|
}
|
||||||
|
|
||||||
return args.join(' ');
|
return args.join(' ');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -405,6 +414,7 @@ class Merger {
|
||||||
|
|
||||||
public cleanUp() {
|
public cleanUp() {
|
||||||
this.options.onlyAudio.concat(this.options.onlyVid).concat(this.options.videoAndAudio).forEach(a => fs.unlinkSync(a.path));
|
this.options.onlyAudio.concat(this.options.onlyVid).concat(this.options.videoAndAudio).forEach(a => fs.unlinkSync(a.path));
|
||||||
|
this.options.chapters?.forEach(a => fs.unlinkSync(a.path));
|
||||||
this.options.subtitles.forEach(a => fs.unlinkSync(a.file));
|
this.options.subtitles.forEach(a => fs.unlinkSync(a.file));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue