added chapter support

This commit is contained in:
stratuma 2024-06-20 15:16:42 +02:00
parent ce3f4dcfdc
commit 2e621a476a
4 changed files with 164 additions and 7 deletions

View File

@ -1,6 +1,6 @@
import { messageBox } from '../../../electron/background'
import { server } from '../../api'
import { VideoPlaylist, VideoPlaylistNoGEO } from '../../types/crunchyroll'
import { VideoMetadata, VideoPlaylist, VideoPlaylistNoGEO } from '../../types/crunchyroll'
import { useFetch } from '../useFetch'
import { parse as mpdParse } from 'mpd-parser'
import { checkProxies, loggedInCheck } from '../service/service.service'
@ -799,6 +799,38 @@ export async function crunchyGetPlaylistMPD(q: string, geo: string | undefined)
}
}
// Crunchyroll Metadata Fetch
export async function crunchyGetMetadata(q: string) {
const headers = {
'User-Agent': 'Crunchyroll/1.8.0 Nintendo Switch/12.3.12.0 UE4/4.27'
}
try {
const response = await fetch(`https://static.crunchyroll.com/skip-events/production/${q}.json`, {
method: 'GET',
headers: headers
})
if (response.ok) {
return await JSON.parse(await response.text()) as VideoMetadata
} else {
const error = await response.text()
messageBox('error', ['Cancel'], 2, 'Failed to get Crunchyroll Metadata', 'Failed to get Crunchyroll Metadata', error)
server.logger.log({
level: 'error',
message: 'Failed to get Crunchyroll Metadata',
error: error,
timestamp: new Date().toISOString(),
section: 'metadataCrunchyrollFetch'
})
throw new Error(error)
}
} catch (e) {
throw new Error(e as string)
}
}
export async function getAccountInfo() {
const account = await loggedInCheck('CR')

View File

@ -4,7 +4,7 @@ import { concatenateTSFiles } from '../../services/concatenate'
import { checkFileExistence, createFolder, createFolderName, deleteFolder, deleteTemporaryFolders } from '../../services/folder'
import { downloadADNSub, downloadCRSub } from '../../services/subs'
import { CrunchyEpisode } from '../../types/crunchyroll'
import { checkAccountMaxStreams, crunchyGetPlaylist, crunchyGetPlaylistMPD } from '../crunchyroll/crunchyroll.service'
import { checkAccountMaxStreams, crunchyGetMetadata, crunchyGetPlaylist, crunchyGetPlaylistMPD } from '../crunchyroll/crunchyroll.service'
import fs from 'fs'
var cron = require('node-cron')
import { Readable } from 'stream'
@ -21,6 +21,7 @@ const mp4e = getMP4DecryptPath()
import util from 'util'
import settings from 'electron-settings'
import { server } from '../../api'
import { createChapterFile } from '../../services/chapter'
const exec = util.promisify(require('child_process').exec)
// Get All Accounts
@ -446,7 +447,7 @@ export async function downloadADNPlaylist(
return
}
await mergeVideoFile(file as string, [], subss, seasonFolder, `${name.replace(/[/\\?%*:|"<>]/g, '')} Season ${season} Episode ${episode}`, format, downloadID)
await mergeVideoFile(file as string, null, [], subss, seasonFolder, `${name.replace(/[/\\?%*:|"<>]/g, '')} Season ${season} Episode ${episode}`, format, downloadID)
await updatePlaylistByID(downloadID, 'completed')
@ -525,6 +526,8 @@ export async function downloadCrunchyrollPlaylist(
const videoFolder = await createFolder()
const chapterFolder = await createFolder()
var seasonFolderNaming = (await settings.get('SeasonTemp')) as string
if (!seasonFolderNaming) {
@ -695,6 +698,18 @@ export async function downloadCrunchyrollPlaylist(
await updatePlaylistByID(downloadID, 'downloading video')
const chapterDownload = async () => {
const metadata = await crunchyGetMetadata(e)
if (!metadata.intro && !metadata.credits && !metadata.preview && !metadata.recap) {
return null
}
const chapterPath = await createChapterFile(metadata, chapterFolder)
return chapterPath
}
const subDownload = async () => {
const sbs: Array<string> = []
for (const sub of subDownloadList) {
@ -1046,7 +1061,7 @@ export async function downloadCrunchyrollPlaylist(
return file
}
const [subss, audios, file] = await Promise.all([subDownload(), audioDownload(), downloadVideo()])
const [chapter, subss, audios, file] = await Promise.all([chapterDownload(), subDownload(), audioDownload(), downloadVideo()])
if (!audios) return
@ -1068,13 +1083,14 @@ export async function downloadCrunchyrollPlaylist(
.replace('{episodeNumberDD}', episode ? episode.toString().padStart(2, '0') : episode_string)
.replace('{quality}', quality.toString() + 'p')
await mergeVideoFile(file as string, audios, subss, seasonFolder, episodeNaming, format, downloadID)
await mergeVideoFile(file as string, chapter, audios, subss, seasonFolder, episodeNaming, format, downloadID)
await updatePlaylistByID(downloadID, 'completed')
await deleteFolder(videoFolder)
await deleteFolder(subFolder)
await deleteFolder(audioFolder)
await deleteFolder(chapterFolder)
return playlist
}
@ -1257,7 +1273,7 @@ async function mergeParts(parts: { filename: string; url: string }[], downloadID
}
}
async function mergeVideoFile(video: string, audios: Array<string>, subs: Array<string>, path: string, filename: string, format: 'mp4' | 'mkv', downloadID: number) {
async function mergeVideoFile(video: string, chapter: string | null, audios: Array<string>, subs: Array<string>, path: string, filename: string, format: 'mp4' | 'mkv', downloadID: number) {
const locales: Array<{
locale: string
name: string
@ -1290,7 +1306,11 @@ async function mergeVideoFile(video: string, audios: Array<string>, subs: Array<
var output = Ffmpeg().setFfmpegPath(ffmpegP.ffmpeg).setFfprobePath(ffmpegP.ffprobe)
var ffindex = 1
output.addInput(video)
var options = ['-map_metadata -1', '-metadata:s:v:0 VENDOR_ID=', '-metadata:s:v:0 language=', '-c copy', '-map 0']
if (chapter) {
output.addInput(chapter)
ffindex++
}
var options = ['-map_metadata 1', '-metadata:s:v:0 VENDOR_ID=', '-metadata:s:v:0 language=', '-c copy', '-map 0']
if (format === 'mp4') {
options.push('-c:s mov_text')
}

View File

@ -0,0 +1,63 @@
import path from 'path'
import fs from 'fs/promises'
import { VideoMetadata } from '../types/crunchyroll'
import { server } from '../api'
function formatTimeFFMPEG(seconds: number) {
return seconds * 1000
}
export async function createChapterFile(rawchapters: VideoMetadata, dir: string) {
const filepath = path.join(dir, 'chapters.txt')
var chapters: string[] = []
chapters.push(';FFMETADATA1')
if (rawchapters.intro && rawchapters.intro.type && rawchapters.intro.start && rawchapters.intro.end) {
chapters.push('[CHAPTER]')
chapters.push('TIMEBASE=1/1000')
chapters.push(`START=${formatTimeFFMPEG(rawchapters.intro.start)}`)
chapters.push(`END=${formatTimeFFMPEG(rawchapters.intro.end) - 1}`)
chapters.push('title=Intro')
}
if (rawchapters.credits && rawchapters.credits.type && rawchapters.credits.start && rawchapters.credits.end) {
chapters.push('[CHAPTER]')
chapters.push('TIMEBASE=1/1000')
chapters.push(`START=${formatTimeFFMPEG(rawchapters.credits.start)}`)
chapters.push(`END=${formatTimeFFMPEG(rawchapters.credits.end) - 1}`)
chapters.push('title=Credits')
}
if (rawchapters.preview && rawchapters.preview.type && rawchapters.preview.start && rawchapters.preview.end) {
chapters.push('[CHAPTER]')
chapters.push('TIMEBASE=1/1000')
chapters.push(`START=${formatTimeFFMPEG(rawchapters.preview.start)}`)
chapters.push(`END=${formatTimeFFMPEG(rawchapters.preview.end) - 1}`)
chapters.push('title=Preview')
}
if (rawchapters.recap && rawchapters.recap.type && rawchapters.recap.start && rawchapters.recap.end) {
chapters.push('[CHAPTER]')
chapters.push('TIMEBASE=1/1000')
chapters.push(`START=${formatTimeFFMPEG(rawchapters.recap.start)}`)
chapters.push(`END=${formatTimeFFMPEG(rawchapters.recap.end) - 1}`)
chapters.push('title=Recap')
}
try {
await fs.writeFile(filepath, chapters.join('\r\n'), 'utf-8')
} catch (e) {
console.error(`Error occurred during chapter writing:`, e)
server.logger.log({
level: 'error',
message: `Error occurred during chapter writing:`,
error: e,
timestamp: new Date().toISOString(),
section: 'crunchyrollWritingProcessChapter'
})
}
return filepath
}

View File

@ -189,3 +189,45 @@ export interface VideoPlaylistNoGEO {
}>
geo: string | undefined
}
export interface VideoMetadata {
intro: {
approverId: string,
distributionNumber: string,
end: number,
seriesId: string,
start: number,
title: string,
type: string
}
credits: {
approverId: string,
distributionNumber: string,
end: number,
seriesId: string,
start: number,
title: string,
type: string
}
preview: {
approverId: string,
distributionNumber: string,
end: number,
seriesId: string,
start: number,
title: string,
type: string
}
recap: {
approverId: string,
distributionNumber: string,
end: number,
seriesId: string,
start: number,
title: string,
type: string
}
lastUpdated: Date,
mediaId: string
}