[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:
AnimeDL 2024-01-29 22:47:56 -08:00
parent 7be22ec132
commit 02620ec5b5
4 changed files with 121 additions and 2 deletions

16
@types/crunchyChapters.d.ts vendored Normal file
View 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;
}

View file

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

View file

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

View file

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