added chapter support
This commit is contained in:
parent
ce3f4dcfdc
commit
2e621a476a
@ -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')
|
||||
|
||||
|
@ -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')
|
||||
}
|
||||
|
63
src/api/services/chapter.ts
Normal file
63
src/api/services/chapter.ts
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
Reference in New Issue
Block a user