diff --git a/@types/crunchyChapters.d.ts b/@types/crunchyChapters.d.ts index 6a4ad37..5aafdb3 100644 --- a/@types/crunchyChapters.d.ts +++ b/@types/crunchyChapters.d.ts @@ -14,3 +14,13 @@ export interface CrunchyChapter { new: boolean; type: string; } + +export interface CrunchyOldChapter { + media_id: string; + startTime: number; + endTime: number; + duration: number; + comparedWith: string; + ordering: string; + last_updated: Date; +} \ No newline at end of file diff --git a/@types/crunchyTypes.d.ts b/@types/crunchyTypes.d.ts index cd96708..d4a42f0 100644 --- a/@types/crunchyTypes.d.ts +++ b/@types/crunchyTypes.d.ts @@ -32,6 +32,7 @@ export type CrunchyDownloadOptions = { skipmux?: boolean, syncTiming: boolean, nocleanup: boolean, + chapters: boolean, apiType: 'web' | 'android' } diff --git a/crunchy.ts b/crunchy.ts index 3b9e846..772f15c 100644 --- a/crunchy.ts +++ b/crunchy.ts @@ -40,7 +40,7 @@ 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 } from './@types/crunchyChapters'; +import { CrunchyChapters, CrunchyChapter, CrunchyOldChapter } from './@types/crunchyChapters'; export type sxItem = { language: langsData.LanguageItem, @@ -1175,51 +1175,93 @@ export default class Crunchy implements ServiceClass { if (mediaId.includes(':')) mediaId = mediaId.split(':')[1]; - 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) { + if (options.chapters) { + //Make Chapter Request + const chapterRequest = await this.req.getData(`https://static.crunchyroll.com/skip-events/production/${mMeta.mediaId}.json`); + if(!chapterRequest.ok || !chapterRequest.res){ + //Old Chapter Request Fallback + console.warn('Chapter request failed, attempting old API'); + const oldChapterRequest = await this.req.getData(`https://static.crunchyroll.com/datalab-intro-v2/${mMeta.mediaId}.json`); + if(!oldChapterRequest.ok || !oldChapterRequest.res) { + console.warn('Old Chapter API request failed'); + } else { + console.info('Old Chapter request successful'); + const chapterData = JSON.parse(oldChapterRequest.res.body) as CrunchyOldChapter; + + //Generate Timestamps 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) { + startTime.setSeconds(chapterData.startTime); + endTime.setSeconds(chapterData.endTime); + const startFormatted = startTime.toISOString().substring(11, 19)+'.'+String(chapterData.startTime).split('.')[1]; + const endFormatted = endTime.toISOString().substring(11, 19)+'.'+String(chapterData.endTime).split('.')[1]; + + //Push Generated Chapters + if (chapterData.startTime > 1) { + 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 { + //Chapter request succeeded, now let's parse them + console.info('Chapter request successful'); + const chapterData = JSON.parse(chapterRequest.res.body) as CrunchyChapters; + const chapters: CrunchyChapter[] = []; + + //Make a format more usable for the crunchy chapters + 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); + //Loop through all the chapters + for (const chapter of chapters) { + //Generate timestamps + 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'; + + //Push generated chapters + 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}=00:00:00.00`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=Prologue` + `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` ); } - 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` - ); } } } diff --git a/modules/module.app-args.ts b/modules/module.app-args.ts index 8047cc1..a06d0ae 100644 --- a/modules/module.app-args.ts +++ b/modules/module.app-args.ts @@ -64,6 +64,7 @@ let argvC: { _: (string | number)[]; $0: string; dlVideoOnce: boolean; + chapters: boolean; crapi: 'android' | 'web'; removeBumpers: boolean; originalFontSize: boolean; diff --git a/modules/module.args.ts b/modules/module.args.ts index 07b728f..ad988e5 100644 --- a/modules/module.args.ts +++ b/modules/module.args.ts @@ -203,6 +203,19 @@ const args: TAppArg[] = [ default: false } }, + { + name: 'chapters', + describe: 'Will fetch the chapters and add them into the final video', + type: 'boolean', + group: 'dl', + service: ['crunchy'], + docDescribe: 'Will fetch the chapters and add them into the final video.' + + '\nCurrently only works with mkvmerge.', + usage: '', + default: { + default: false + } + }, { name: 'crapi', describe: 'Selects the API type for Crunchyroll',