added hardsub and dub subtitle download

This commit is contained in:
Daniel Haller 2024-04-17 17:46:08 +02:00
parent 33899b4d42
commit e74c822f49
7 changed files with 185 additions and 71 deletions

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="flex flex-row bg-[#111111] h-16" style="-webkit-app-region: drag"> <div class="fixed w-full flex flex-row bg-[#111111] h-16 z-10" style="-webkit-app-region: drag">
<div class="w-full flex gap-10 flex-row items-center justify-center px-"> <div class="w-full flex gap-10 flex-row items-center justify-center px-">
<button @click="openAddAnime" class="px-6 py-0.5 border-2 border-[#ce6104] rounded-xl" style="-webkit-app-region: no-drag"> <button @click="openAddAnime" class="px-6 py-0.5 border-2 border-[#ce6104] rounded-xl" style="-webkit-app-region: no-drag">
<Icon name="ph:plus-bold" class="h-7 w-7 text-[#ce6104]" /> <Icon name="ph:plus-bold" class="h-7 w-7 text-[#ce6104]" />
@ -56,7 +56,7 @@ async function openAddAnime() {
title: "Add Anime", title: "Add Anime",
url: isProduction ? 'http://localhost:8079/addanime' : 'http://localhost:3000/addanime', url: isProduction ? 'http://localhost:8079/addanime' : 'http://localhost:3000/addanime',
width: 700, width: 700,
height: 400, height: 450,
backgroundColor: "#111111" backgroundColor: "#111111"
}) })
} }

View File

@ -165,12 +165,34 @@
</button> </button>
</div> </div>
</div> </div>
<div class="relative flex flex-col">
<select v-model="hardsub" name="episode" class="bg-[#5c5b5b] focus:outline-none px-3 py-2 rounded-xl text-sm text-center cursor-pointer">
<option
:value="false"
class="text-sm text-slate-200"
>Hardsub: false</option
>
<option
:value="true"
class="text-sm text-slate-200"
>Hardsub: true</option
>
</select>
<div
class="absolute w-full h-9 bg-[#afadad] rounded-xl transition-all flex flex-row items-center justify-center gap-1 text-black"
:class="isFetchingEpisodes ? 'opacity-100' : 'opacity-0 pointer-events-none'"
>
<Icon name="mdi:loading" class="h-6 w-6 animate-spin" />
<div class="text-sm">Loading</div></div
>
</div>
<!-- {{ CRselectedShow?.Subs.map(s=> { return locales.find(l => l.locale === s)?.name }) }} <!-- {{ CRselectedShow?.Subs.map(s=> { return locales.find(l => l.locale === s)?.name }) }}
{{ CRselectedShow?.Dubs.map(s=> { return locales.find(l => l.locale === s)?.name }) }} --> {{ CRselectedShow?.Dubs.map(s=> { return locales.find(l => l.locale === s)?.name }) }} -->
<div class="relative flex flex-col mt-auto"> <div class="relative flex flex-col mt-auto">
<button @click="addToPlaylist" class="relative py-3 border-2 rounded-xl flex flex-row items-center justify-center"> <button @click="addToPlaylist" class="relative py-3 border-2 rounded-xl flex flex-row items-center justify-center">
<div class="flex flex-row items-center justify-center transition-all" :class="isFetchingSeasons ? 'opacity-0' : 'opacity-100'"> <div class="flex flex-row items-center justify-center transition-all" :class="isFetchingSeasons ? 'opacity-0' : 'opacity-100'">
<div class="text-xl">Add to Queue</div> <div class="text-xl">Add to Download</div>
</div> </div>
<div class="absolute flex flex-row items-center justify-center gap-1 transition-all" :class="isFetchingSeasons ? 'opacity-100' : 'opacity-0'"> <div class="absolute flex flex-row items-center justify-center gap-1 transition-all" :class="isFetchingSeasons ? 'opacity-100' : 'opacity-0'">
<Icon name="mdi:loading" class="h-6 w-6 animate-spin" /> <Icon name="mdi:loading" class="h-6 w-6 animate-spin" />
@ -236,6 +258,7 @@ const episodes = ref<CrunchyEpisodes>()
const selectedSeason = ref<CrunchySeason>() const selectedSeason = ref<CrunchySeason>()
const selectedStartEpisode = ref<CrunchyEpisode>() const selectedStartEpisode = ref<CrunchyEpisode>()
const selectedEndEpisode = ref<CrunchyEpisode>() const selectedEndEpisode = ref<CrunchyEpisode>()
const hardsub = ref<boolean>(false)
const isFetchingSeasons = ref<number>(0) const isFetchingSeasons = ref<number>(0)
const isFetchingEpisodes = ref<number>(0) const isFetchingEpisodes = ref<number>(0)
@ -402,7 +425,8 @@ const addToPlaylist = async () => {
episodes: [selectedStartEpisode.value], episodes: [selectedStartEpisode.value],
dubs: selectedDubs.value, dubs: selectedDubs.value,
subs: selectedSubs.value, subs: selectedSubs.value,
dir: path.value dir: path.value,
hardsub: hardsub.value
} }
const { error } = await useFetch('http://localhost:8080/api/crunchyroll/playlist', { const { error } = await useFetch('http://localhost:8080/api/crunchyroll/playlist', {

View File

@ -39,7 +39,7 @@ const openAddAnime = () => {
title: "Add Anime", title: "Add Anime",
url: isProduction ? 'http://localhost:8079/addanime' : 'http://localhost:3000/addanime', url: isProduction ? 'http://localhost:8079/addanime' : 'http://localhost:3000/addanime',
width: 700, width: 700,
height: 400, height: 450,
backgroundColor: "#111111" backgroundColor: "#111111"
}) })
} }

View File

@ -68,13 +68,13 @@ const getPlaylist = async () => {
return return
} }
playlist.value = data.value.reverse() playlist.value = data.value
} }
onMounted(() => { onMounted(() => {
getPlaylist() getPlaylist()
setInterval(getPlaylist, 1000) setInterval(getPlaylist, 2000)
}) })
</script> </script>

View File

@ -43,7 +43,8 @@ interface PlaylistCreateAttributes {
media: CrunchyEpisode media: CrunchyEpisode
dub: Array<string> dub: Array<string>
sub: Array<string> sub: Array<string>
dir: string dir: string,
hardsub: boolean,
} }
const Account: ModelDefined<AccountAttributes, AccountCreateAttributes> = sequelize.define('Accounts', { const Account: ModelDefined<AccountAttributes, AccountCreateAttributes> = sequelize.define('Accounts', {
@ -103,6 +104,10 @@ const Playlist: ModelDefined<PlaylistAttributes, PlaylistCreateAttributes> = seq
allowNull: false, allowNull: false,
type: DataTypes.STRING type: DataTypes.STRING
}, },
hardsub: {
allowNull: false,
type: DataTypes.BOOLEAN
},
partsdownloaded: { partsdownloaded: {
allowNull: true, allowNull: true,
type: DataTypes.BOOLEAN, type: DataTypes.BOOLEAN,

View File

@ -66,7 +66,8 @@ export async function addPlaylistController(
episodes: CrunchyEpisodes episodes: CrunchyEpisodes
dubs: Array<string> dubs: Array<string>
subs: Array<string> subs: Array<string>
dir: string dir: string,
hardsub: boolean
} }
}>, }>,
reply: FastifyReply reply: FastifyReply
@ -75,7 +76,7 @@ export async function addPlaylistController(
const body = request.body; const body = request.body;
for (const e of body.episodes) { for (const e of body.episodes) {
await addEpisodeToPlaylist(e, body.subs, body.dubs, body.dir) await addEpisodeToPlaylist(e, body.subs, body.dubs, body.dir, body.hardsub)
} }
return reply.code(201).send() return reply.code(201).send()
@ -88,6 +89,6 @@ export async function getPlaylistController(
const playlist = await getPlaylist() const playlist = await getPlaylist()
return reply.code(200).send(playlist) return reply.code(200).send(playlist.reverse())
} }

View File

@ -22,7 +22,8 @@ const crErrors = [
] ]
export async function crunchyLogin(user: string, passw: string) { export async function crunchyLogin(user: string, passw: string) {
const cachedData: { const cachedData:
| {
access_token: string access_token: string
refresh_token: string refresh_token: string
expires_in: number expires_in: number
@ -31,7 +32,8 @@ export async function crunchyLogin(user: string, passw: string) {
country: string country: string
account_id: string account_id: string
profile_id: string profile_id: string
} | undefined = server.CacheController.get('crtoken') }
| undefined = server.CacheController.get('crtoken')
if (!cachedData) { if (!cachedData) {
var { data, error } = await crunchyLoginFetch(user, passw) var { data, error } = await crunchyLoginFetch(user, passw)
@ -129,12 +131,13 @@ export async function safeLoginData(user: string, password: string, service: str
return login?.get() return login?.get()
} }
export async function addEpisodeToPlaylist(e: CrunchyEpisode, s: Array<string>, d: Array<string>, dir: string) { export async function addEpisodeToPlaylist(e: CrunchyEpisode, s: Array<string>, d: Array<string>, dir: string, hardsub: boolean) {
const episode = await Playlist.create({ const episode = await Playlist.create({
media: e, media: e,
sub: s, sub: s,
dub: d, dub: d,
dir: dir dir: dir,
hardsub: hardsub
}) })
return episode.get() return episode.get()
@ -154,8 +157,22 @@ export async function updatePlaylistToDownloadPartsByID(id: number, parts: numbe
await Playlist.update({ partsleft: parts }, { where: { id: id } }) await Playlist.update({ partsleft: parts }, { where: { id: id } })
} }
export async function updatePlaylistToDownloadedPartsByID(id: number, parts: number) { let updateTimeout: NodeJS.Timeout | null = null
let cooldown = false
export async function updatePlaylistToDownloadedPartsByID(id: number, parts: number, lenght: number) {
if (cooldown && parts !== lenght) {
return
}
cooldown = true
await Playlist.update({ partsdownloaded: parts }, { where: { id: id } }) await Playlist.update({ partsdownloaded: parts }, { where: { id: id } })
updateTimeout = setTimeout(function () {
cooldown = false
updateTimeout = null
}, 2000)
} }
async function checkPlaylists() { async function checkPlaylists() {
@ -163,11 +180,17 @@ async function checkPlaylists() {
for (const e of episodes) { for (const e of episodes) {
await updatePlaylistByID(e.dataValues.id, 'preparing') await updatePlaylistByID(e.dataValues.id, 'preparing')
await downloadPlaylist(e.dataValues.media.id, (e as any).dataValues.dub.map((s: { locale: any })=> s.locale), (e as any).dataValues.sub.map((s: { locale: any })=> s.locale), e.dataValues.hardsub, e.dataValues.id) await downloadPlaylist(
e.dataValues.media.id,
(e as any).dataValues.dub.map((s: { locale: any }) => s.locale),
(e as any).dataValues.sub.map((s: { locale: any }) => s.locale),
e.dataValues.hardsub,
e.dataValues.id
)
} }
} }
cron.schedule('* * * * * *', () => { cron.schedule('*/2 * * * * *', () => {
checkPlaylists() checkPlaylists()
}) })
@ -268,7 +291,10 @@ export async function downloadPlaylist(e: string, dubs: Array<string>, subs: Arr
console.log(dubs) console.log(dubs)
console.log(subs) console.log(subs)
if (!playlist) return if (!playlist) {
console.log('Playlist not found')
return
}
if (playlist.audioLocale !== subs[0]) { if (playlist.audioLocale !== subs[0]) {
const found = playlist.versions.find((v) => v.audio_locale === 'ja-JP') const found = playlist.versions.find((v) => v.audio_locale === 'ja-JP')
@ -277,7 +303,10 @@ export async function downloadPlaylist(e: string, dubs: Array<string>, subs: Arr
} }
} }
if (!playlist) return if (!playlist) {
console.log('Exact Playlist not found')
return
}
const subFolder = await createFolder() const subFolder = await createFolder()
@ -297,12 +326,29 @@ export async function downloadPlaylist(e: string, dubs: Array<string>, subs: Arr
format: string format: string
language: string language: string
url: string url: string
isDub: boolean
}> = [] }> = []
for (const s of subs) { for (const s of subs) {
const found = playlist.subtitles.find((sub) => sub.language === s) var subPlaylist
if (playlist.audioLocale !== 'ja-JP') {
const foundStream = playlist.versions.find((v) => v.audio_locale === 'ja-JP')
if (foundStream) {
subPlaylist = await crunchyGetPlaylist(foundStream.guid)
}
} else {
subPlaylist = playlist
}
if (!subPlaylist) {
console.log('Subtitle Playlist not found')
return
}
const found = subPlaylist.subtitles.find((sub) => sub.language === s)
if (found) { if (found) {
subDownloadList.push(found) subDownloadList.push({ ...found, isDub: false })
console.log(`Subtitle ${s}.ass found, adding to download`) console.log(`Subtitle ${s}.ass found, adding to download`)
} else { } else {
console.warn(`Subtitle ${s}.ass not found, skipping`) console.warn(`Subtitle ${s}.ass not found, skipping`)
@ -312,6 +358,15 @@ export async function downloadPlaylist(e: string, dubs: Array<string>, subs: Arr
for (const d of dubs) { for (const d of dubs) {
const found = playlist.versions.find((p) => p.audio_locale === d) const found = playlist.versions.find((p) => p.audio_locale === d)
if (found) { if (found) {
const list = await crunchyGetPlaylist(found.guid)
if (list) {
const foundSub = list.subtitles.find((sub) => sub.language === d)
if (foundSub) {
subDownloadList.push({ ...foundSub, isDub: true })
} else {
console.log(`No Dub Sub Found for ${d}`)
}
}
dubDownloadList.push(found) dubDownloadList.push(found)
console.log(`Audio ${d}.aac found, adding to download`) console.log(`Audio ${d}.aac found, adding to download`)
} else { } else {
@ -388,21 +443,19 @@ export async function downloadPlaylist(e: string, dubs: Array<string>, subs: Arr
var mdp = await crunchyGetPlaylistMPD(play.url) var mdp = await crunchyGetPlaylistMPD(play.url)
if (hardsub) { // if (hardsub) {
const findjpplaylist = playlist.versions.find((p) => p.audio_locale === 'ja-JP')?.guid // const hardsuburl = play.hardSubs.find(h=> h.hlang === subs[0])?.url
if (!findjpplaylist) return // if (!hardsuburl) {
// console.error('No Hardsub stream found')
// return
// }
const hsplaylist = await crunchyGetPlaylist(findjpplaylist) // mdp = await crunchyGetPlaylistMPD(hardsuburl)
// console.error('Hardsub stream found')
if (!hsplaylist) return // } else {
// console.error('Hardsub is false')
const hsurl = hsplaylist.hardSubs.find((h) => h.hlang === subs[0]) // }
if (hsurl) {
mdp = await crunchyGetPlaylistMPD(hsurl.url)
}
}
if (!mdp) return if (!mdp) return
@ -424,7 +477,7 @@ export async function downloadPlaylist(e: string, dubs: Array<string>, subs: Arr
}) })
} }
await updatePlaylistByID(downloadID, "downloading") await updatePlaylistByID(downloadID, 'downloading')
await updatePlaylistToDownloadPartsByID(downloadID, p.length) await updatePlaylistToDownloadPartsByID(downloadID, p.length)
@ -437,14 +490,14 @@ export async function downloadPlaylist(e: string, dubs: Array<string>, subs: Arr
if (!audios) return if (!audios) return
await updatePlaylistByID(downloadID, "merging") await updatePlaylistByID(downloadID, 'merging')
await mergeFile(file as string, audios, subss, String(playlist.assetId)) await mergeFile(file as string, audios, subss, String(playlist.assetId))
await deleteFolder(subFolder) await deleteFolder(subFolder)
await deleteFolder(audioFolder) await deleteFolder(audioFolder)
await updatePlaylistByID(downloadID, "completed") await updatePlaylistByID(downloadID, 'completed')
return playlist return playlist
} }
@ -486,7 +539,7 @@ async function downloadParts(parts: { filename: string; url: string }[], downloa
const path = await createFolder() const path = await createFolder()
for (const [index, part] of parts.entries()) { for (const [index, part] of parts.entries()) {
let success = false; let success = false
while (!success) { while (!success) {
try { try {
const stream = fs.createWriteStream(`${path}/${part.filename}`) const stream = fs.createWriteStream(`${path}/${part.filename}`)
@ -494,12 +547,12 @@ async function downloadParts(parts: { filename: string; url: string }[], downloa
await finished(Readable.fromWeb(body as any).pipe(stream)) await finished(Readable.fromWeb(body as any).pipe(stream))
console.log(`Fragment ${index + 1} downloaded`) console.log(`Fragment ${index + 1} downloaded`)
partsdownloaded++ partsdownloaded++
updatePlaylistToDownloadedPartsByID(downloadID, partsdownloaded) updatePlaylistToDownloadedPartsByID(downloadID, partsdownloaded, parts.length)
success = true; success = true
} catch (error) { } catch (error) {
console.error(`Error occurred during download of fragment ${index + 1}:`, error); console.error(`Error occurred during download of fragment ${index + 1}:`, error)
console.log(`Retrying download of fragment ${index + 1}...`); console.log(`Retrying download of fragment ${index + 1}...`)
await new Promise(resolve => setTimeout(resolve, 5000)); await new Promise((resolve) => setTimeout(resolve, 5000))
} }
} }
} }
@ -512,10 +565,11 @@ async function downloadSub(
format: string format: string
language: string language: string
url: string url: string
isDub: boolean
}, },
dir: string dir: string
) { ) {
const path = `${dir}/${sub.language}.${sub.format}` const path = `${dir}/${sub.language}${sub.isDub ? `-FORCED` : ''}.${sub.format}`
const stream = fs.createWriteStream(path) const stream = fs.createWriteStream(path)
const response = await fetch(sub.url) const response = await fetch(sub.url)
@ -666,7 +720,7 @@ async function mergeFile(video: string, audios: Array<string>, subs: Array<strin
var output = Ffmpeg() var output = Ffmpeg()
var ffindex = 1 var ffindex = 1
output.addInput(video) output.addInput(video)
var options = ['-c copy', '-map 0'] var options = ['-map_metadata -1', '-c copy', '-metadata:s:v:0 VARIANT_BITRATE=0', '-map 0']
for (const [index, a] of audios.entries()) { for (const [index, a] of audios.entries()) {
output.addInput(a) output.addInput(a)
@ -681,23 +735,33 @@ async function mergeFile(video: string, audios: Array<string>, subs: Array<strin
ffindex++ ffindex++
// Somehow not working options.push(
// options.push( `-metadata:s:a:${index} title=${
// `-metadata:s:a:${index} language="${ locales.find((l) => l.locale === a.split('/')[1].split('.aac')[0])
// locales.find( ? locales.find((l) => l.locale === a.split('/')[1].split('.aac')[0])?.title
// (l) => l.locale === a.split("/")[1].split(".aac")[0] : a.split('/')[1].split('.aac')[0]
// ) ? locales.find( }`
// (l) => l.locale === a.split("/")[1].split(".aac")[0] )
// )?.title : a.split("/")[1].split(".aac")[0]
// }"` options.push(`-metadata:s:a:${index} VARIANT_BITRATE=0`)
// );
} }
options.push(`-disposition:a:0 default`)
if (subs) { if (subs) {
for (const [index, s] of subs.entries()) { for (const [index, s] of subs.entries()) {
output.addInput(s) output.addInput(s)
options.push(`-map ${ffindex}:s`) options.push(`-map ${ffindex}:s`)
if (s.includes('-FORCED')) {
options.push(
`-metadata:s:s:${index} language=${
locales.find((l) => l.locale === s.split('/')[1].split('-FORCED.ass')[0])
? locales.find((l) => l.locale === s.split('/')[1].split('-FORCED.ass')[0])?.iso
: s.split('/')[1].split('-FORCED.ass')[0]
}`
)
} else {
options.push( options.push(
`-metadata:s:s:${index} language=${ `-metadata:s:s:${index} language=${
locales.find((l) => l.locale === s.split('/')[1].split('.ass')[0]) locales.find((l) => l.locale === s.split('/')[1].split('.ass')[0])
@ -705,9 +769,29 @@ async function mergeFile(video: string, audios: Array<string>, subs: Array<strin
: s.split('/')[1].split('.ass')[0] : s.split('/')[1].split('.ass')[0]
}` }`
) )
}
if (s.includes('-FORCED')) {
options.push(
`-metadata:s:s:${index} title=${
locales.find((l) => l.locale === s.split('/')[1].split('-FORCED.ass')[0])
? locales.find((l) => l.locale === s.split('/')[1].split('-FORCED.ass')[0])?.title
: s.split('/')[1].split('-FORCED.ass')[0]
}[FORCED]`
)
} else {
options.push(
`-metadata:s:s:${index} title=${
locales.find((l) => l.locale === s.split('/')[1].split('.ass')[0])
? locales.find((l) => l.locale === s.split('/')[1].split('.ass')[0])?.title
: s.split('/')[1].split('.ass')[0]
}`
)
}
ffindex++ ffindex++
} }
options.push(`-disposition:s:0 default`)
} }
output output