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 { messageBox } from '../../../electron/background'
import { server } from '../../api' import { server } from '../../api'
import { VideoPlaylist, VideoPlaylistNoGEO } from '../../types/crunchyroll' import { VideoMetadata, VideoPlaylist, VideoPlaylistNoGEO } from '../../types/crunchyroll'
import { useFetch } from '../useFetch' import { useFetch } from '../useFetch'
import { parse as mpdParse } from 'mpd-parser' import { parse as mpdParse } from 'mpd-parser'
import { checkProxies, loggedInCheck } from '../service/service.service' 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() { export async function getAccountInfo() {
const account = await loggedInCheck('CR') 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 { checkFileExistence, createFolder, createFolderName, deleteFolder, deleteTemporaryFolders } from '../../services/folder'
import { downloadADNSub, downloadCRSub } from '../../services/subs' import { downloadADNSub, downloadCRSub } from '../../services/subs'
import { CrunchyEpisode } from '../../types/crunchyroll' 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' import fs from 'fs'
var cron = require('node-cron') var cron = require('node-cron')
import { Readable } from 'stream' import { Readable } from 'stream'
@ -21,6 +21,7 @@ const mp4e = getMP4DecryptPath()
import util from 'util' import util from 'util'
import settings from 'electron-settings' import settings from 'electron-settings'
import { server } from '../../api' import { server } from '../../api'
import { createChapterFile } from '../../services/chapter'
const exec = util.promisify(require('child_process').exec) const exec = util.promisify(require('child_process').exec)
// Get All Accounts // Get All Accounts
@ -446,7 +447,7 @@ export async function downloadADNPlaylist(
return 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') await updatePlaylistByID(downloadID, 'completed')
@ -525,6 +526,8 @@ export async function downloadCrunchyrollPlaylist(
const videoFolder = await createFolder() const videoFolder = await createFolder()
const chapterFolder = await createFolder()
var seasonFolderNaming = (await settings.get('SeasonTemp')) as string var seasonFolderNaming = (await settings.get('SeasonTemp')) as string
if (!seasonFolderNaming) { if (!seasonFolderNaming) {
@ -695,6 +698,18 @@ export async function downloadCrunchyrollPlaylist(
await updatePlaylistByID(downloadID, 'downloading video') 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 subDownload = async () => {
const sbs: Array<string> = [] const sbs: Array<string> = []
for (const sub of subDownloadList) { for (const sub of subDownloadList) {
@ -1046,7 +1061,7 @@ export async function downloadCrunchyrollPlaylist(
return file 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 if (!audios) return
@ -1068,13 +1083,14 @@ export async function downloadCrunchyrollPlaylist(
.replace('{episodeNumberDD}', episode ? episode.toString().padStart(2, '0') : episode_string) .replace('{episodeNumberDD}', episode ? episode.toString().padStart(2, '0') : episode_string)
.replace('{quality}', quality.toString() + 'p') .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 updatePlaylistByID(downloadID, 'completed')
await deleteFolder(videoFolder) await deleteFolder(videoFolder)
await deleteFolder(subFolder) await deleteFolder(subFolder)
await deleteFolder(audioFolder) await deleteFolder(audioFolder)
await deleteFolder(chapterFolder)
return playlist 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<{ const locales: Array<{
locale: string locale: string
name: 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 output = Ffmpeg().setFfmpegPath(ffmpegP.ffmpeg).setFfprobePath(ffmpegP.ffprobe)
var ffindex = 1 var ffindex = 1
output.addInput(video) 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') { if (format === 'mp4') {
options.push('-c:s mov_text') 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 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
}