added better dir and file creation and bugfixes

This commit is contained in:
Daniel Haller 2024-04-18 00:44:18 +02:00
parent e74c822f49
commit e59f808ae4
8 changed files with 219 additions and 100 deletions

View File

@ -5,6 +5,12 @@ A simple tool for downloading videos from Crunchyroll and ADN.
- Linux
## Credits
- This is literally just an improved version of [hama3254's](https://github.com/hama3254/Crunchyroll-Downloader-v3.0) [Crunchyroll-Downloader-v3.0](https://github.com/hama3254/Crunchyroll-Downloader-v3.0)
## To-Do
- ADN Downloader
- Download Pause/Delete
- Download Speed Display
- Settings
- Open Download Path
## User Interface
#### Home
![Screenshot 2024-04-06 180439](https://github.com/Junon401/CR-Downloader/assets/166554835/b45c5799-3716-49c9-8abf-5fc7c9fe08c9)

View File

@ -15,7 +15,7 @@
<div v-if="tab === 1" class="flex flex-col mt-5 gap-3.5 h-full" style="-webkit-app-region: no-drag">
<div class="relative flex flex-col">
<select v-model="service" name="service" class="bg-[#5c5b5b] focus:outline-none px-3 py-2 rounded-xl text-sm text-center cursor-pointer">
<option value="adn">ADN</option>
<!-- <option value="adn">ADN</option> -->
<option value="crunchyroll">Crunchyroll</option>
</select>
</div>
@ -64,7 +64,7 @@
<div class="relative flex flex-col">
<input v-model="url" type="text" name="text" placeholder="URL" class="bg-[#5c5b5b] focus:outline-none px-3 py-2 rounded-xl text-sm text-center" />
</div>
<div class="relative flex flex-col">
<!-- <div class="relative flex flex-col">
<input
@click="getFolderPath()"
v-model="path"
@ -74,7 +74,7 @@
class="bg-[#5c5b5b] focus:outline-none px-3 py-2 rounded-xl text-sm text-center cursor-pointer"
readonly
/>
</div>
</div> -->
<div class="relative flex flex-col mt-auto">
<button @click="switchToSeason" 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'">
@ -421,8 +421,20 @@ const toggleSub = (lang: { name: string | undefined; locale: string }) => {
const addToPlaylist = async () => {
if (!episodes.value) return
const startEpisodeIndex = episodes.value.findIndex(episode => episode === selectedStartEpisode.value);
const endEpisodeIndex = episodes.value.findIndex(episode => episode === selectedEndEpisode.value);
if (startEpisodeIndex === -1 || endEpisodeIndex === -1) {
console.error('Indexes not found.');
return;
}
const selectedEpisodes = episodes.value.slice(startEpisodeIndex, endEpisodeIndex + 1);
const data = {
episodes: [selectedStartEpisode.value],
episodes: selectedEpisodes,
dubs: selectedDubs.value,
subs: selectedSubs.value,
dir: path.value,

View File

@ -1,7 +1,10 @@
<template>
<div>
<MainHeader />
<div class="flex flex-col text-white">
<div class="flex flex-col text-white pt-16">
<button @click="deletePlaylist">
Delete Playlist
</button>
<div v-for="p in playlist" class="flex flex-row gap-4 h-40 p-5 bg-[#636363]">
<div class="flex min-w-52 w-52">
<img :src="p.media.images.thumbnail[0].find((p) => p.height === 1080)?.source" alt="Image" class="object-cover rounded-xl" />
@ -11,8 +14,12 @@
{{ p.status }}
</div>
<div class="text-base capitalize"> {{ p.media.series_title }} Season {{ p.media.season_number }} Episode {{ p.media.episode_number }} </div>
<div class="relative w-full min-h-5 bg-[#bdbbbb] mt-1 rounded">
<div v-if="p.partsleft && p.status === 'downloading'" class="w-full h-full rounded bg-[#4e422d] transition-all duration-300" :style="`width: calc((${p.partsdownloaded} / ${p.partsleft}) * 100%);`"></div>
<div class="relative w-full min-h-5 bg-[#bdbbbb] mt-1 rounded">
<div
v-if="p.partsleft && p.status === 'downloading'"
class="w-full h-full rounded bg-[#4e422d] transition-all duration-300"
:style="`width: calc((${p.partsdownloaded} / ${p.partsleft}) * 100%);`"
></div>
<div v-if="p.status === 'completed'" class="w-full h-full rounded bg-[#79ff77] transition-all duration-300"></div>
<div v-if="p.status === 'merging'" class="absolute top-0 w-20 h-full rounded bg-[#293129] transition-all duration-300 loading-a"></div>
</div>
@ -22,6 +29,7 @@
<div class="text-sm">Dubs: {{ p.dub.map((t) => t.name).join(', ') }}</div>
<div class="text-sm">Subs: {{ p.sub.map((t) => t.name).join(', ') }}</div>
<div v-if="p.partsleft && p.status === 'downloading'" class="text-sm">{{ p.partsdownloaded }}/{{ p.partsleft }}</div>
<div v-if="p.downloadspeed && p.status === 'downloading'" class="text-sm">{{ p.downloadspeed }} MB/s</div>
</div>
</div>
</div> </div
@ -39,9 +47,10 @@ const playlist = ref<
media: CrunchyEpisode
dub: Array<{ locale: string; name: string }>
sub: Array<{ locale: string; name: string }>
dir: string,
partsleft: number,
partsdownloaded: number
dir: string
partsleft: number
partsdownloaded: number
downloadspeed: number
}>
>()
@ -53,9 +62,10 @@ const getPlaylist = async () => {
media: CrunchyEpisode
dub: Array<{ locale: string; name: string }>
sub: Array<{ locale: string; name: string }>
dir: string,
partsleft: number,
dir: string
partsleft: number
partsdownloaded: number
downloadspeed: number
}>
>('http://localhost:8080/api/crunchyroll/playlist')
@ -71,6 +81,21 @@ const getPlaylist = async () => {
playlist.value = data.value
}
const deletePlaylist = async () => {
const { data, error } = await useFetch('http://localhost:8080/api/crunchyroll/playlist', {
method: "delete"
})
if (error.value) {
alert(error.value)
return
}
if (!data.value) {
return
}
}
onMounted(() => {
getPlaylist()
@ -79,7 +104,6 @@ onMounted(() => {
</script>
<style>
.loading-a {
animation: animation infinite 3s;
}
@ -97,5 +121,4 @@ onMounted(() => {
left: 0%;
}
}
</style>

View File

@ -34,8 +34,6 @@ interface PlaylistAttributes {
sub: Array<string>
hardsub: boolean,
dir: string,
partsdownloaded: number,
partsleft: number,
failedreason: string
}
@ -45,6 +43,7 @@ interface PlaylistCreateAttributes {
sub: Array<string>
dir: string,
hardsub: boolean,
status: 'waiting' | 'preparing' | 'downloading' | 'merging' | 'completed' | 'failed'
}
const Account: ModelDefined<AccountAttributes, AccountCreateAttributes> = sequelize.define('Accounts', {
@ -86,7 +85,6 @@ const Playlist: ModelDefined<PlaylistAttributes, PlaylistCreateAttributes> = seq
status: {
allowNull: false,
type: DataTypes.STRING,
defaultValue: 'waiting'
},
media: {
allowNull: false,
@ -108,16 +106,6 @@ const Playlist: ModelDefined<PlaylistAttributes, PlaylistCreateAttributes> = seq
allowNull: false,
type: DataTypes.BOOLEAN
},
partsdownloaded: {
allowNull: true,
type: DataTypes.BOOLEAN,
defaultValue: 0
},
partsleft: {
allowNull: true,
type: DataTypes.BOOLEAN,
defaultValue: 0
},
failedreason: {
allowNull: true,
type: DataTypes.STRING

View File

@ -1,5 +1,5 @@
import type { FastifyReply, FastifyRequest } from 'fastify'
import { crunchyLogin, checkIfLoggedInCR, safeLoginData, addEpisodeToPlaylist, getPlaylist } from './crunchyroll.service'
import { crunchyLogin, checkIfLoggedInCR, safeLoginData, addEpisodeToPlaylist, getPlaylist, getDownloading, deletePlaylist } from './crunchyroll.service'
import { dialog } from 'electron'
import { messageBox } from '../../../electron/background'
import { CrunchyEpisodes, CrunchySeason } from '../../types/crunchyroll'
@ -76,12 +76,22 @@ export async function addPlaylistController(
const body = request.body;
for (const e of body.episodes) {
await addEpisodeToPlaylist(e, body.subs, body.dubs, body.dir, body.hardsub)
await addEpisodeToPlaylist(e, body.subs, body.dubs, body.dir, body.hardsub, "waiting")
}
return reply.code(201).send()
}
export async function deleteCompletePlaylistController(
request: FastifyRequest,
reply: FastifyReply
) {
await deletePlaylist()
return reply.code(200).send()
}
export async function getPlaylistController(
request: FastifyRequest,
reply: FastifyReply
@ -89,6 +99,20 @@ export async function getPlaylistController(
const playlist = await getPlaylist()
for (const v of playlist) {
if (v.dataValues.status === 'downloading') {
const found = await getDownloading(v.dataValues.id)
if (found) {
(v as any).dataValues = {
...v.dataValues,
partsleft: found.partsToDownload,
partsdownloaded: found.downloadedParts,
downloadspeed: found.downloadSpeed.toFixed(2)
}
}
}
}
return reply.code(200).send(playlist.reverse())
}

View File

@ -1,5 +1,5 @@
import type { FastifyInstance } from 'fastify'
import { addPlaylistController, checkLoginController, getPlaylistController, loginController, loginLoginController } from './crunchyroll.controller'
import { addPlaylistController, checkLoginController, deleteCompletePlaylistController, getPlaylistController, loginController, loginLoginController } from './crunchyroll.controller'
async function crunchyrollRoutes(server: FastifyInstance) {
server.post(
@ -72,6 +72,21 @@ async function crunchyrollRoutes(server: FastifyInstance) {
},
getPlaylistController
)
server.delete(
'/playlist',
{
schema: {
response: {
'4xx': {
error: { type: 'string' },
message: { type: 'string' }
}
}
}
},
deleteCompletePlaylistController
)
}
export default crunchyrollRoutes

View File

@ -131,13 +131,21 @@ export async function safeLoginData(user: string, password: string, service: str
return login?.get()
}
export async function addEpisodeToPlaylist(e: CrunchyEpisode, s: Array<string>, d: Array<string>, dir: string, hardsub: boolean) {
export async function addEpisodeToPlaylist(
e: CrunchyEpisode,
s: Array<string>,
d: Array<string>,
dir: string,
hardsub: boolean,
status: 'waiting' | 'preparing' | 'downloading' | 'merging' | 'completed' | 'failed'
) {
const episode = await Playlist.create({
media: e,
sub: s,
dub: d,
dir: dir,
hardsub: hardsub
hardsub: hardsub,
status
})
return episode.get()
@ -149,44 +157,44 @@ export async function getPlaylist() {
return episodes
}
export async function deletePlaylist() {
await Playlist.truncate()
return true
}
export async function getDownloading(id: number) {
const found = downloading.find((i) => i.id === id)
if (found) return found
return null
}
export async function updatePlaylistByID(id: number, status: 'waiting' | 'preparing' | 'downloading' | 'completed' | 'merging' | 'failed') {
await Playlist.update({ status: status }, { where: { id: id } })
}
export async function updatePlaylistToDownloadPartsByID(id: number, parts: number) {
await Playlist.update({ partsleft: parts }, { where: { id: id } })
}
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 } })
updateTimeout = setTimeout(function () {
cooldown = false
updateTimeout = null
}, 2000)
}
var isDownloading: number = 0
async function checkPlaylists() {
const episodes = await Playlist.findAll({ where: { status: 'waiting' } })
const eps = await Playlist.findAll({ where: { status: 'waiting' } })
for (const e of episodes) {
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
)
for (const e of eps) {
if (isDownloading < 2 && e.dataValues.status === 'waiting') {
updatePlaylistByID(e.dataValues.id, 'preparing')
isDownloading++
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,
e.dataValues.media.series_title,
e.dataValues.media.season_number,
e.dataValues.media.episode_number
)
}
}
}
@ -249,6 +257,23 @@ async function createFolder() {
}
}
async function createFolderName(name: string) {
const folderPath = path.join(app.getPath('documents'), name)
try {
await fs.promises.access(folderPath)
return folderPath
} catch (error) {
try {
await fs.promises.mkdir(folderPath, { recursive: true })
return folderPath
} catch (mkdirError) {
console.error('Error creating season folder:', mkdirError)
throw mkdirError
}
}
}
async function deleteFolder(folderPath: string) {
fs.rmSync(folderPath, { recursive: true, force: true })
}
@ -285,11 +310,24 @@ export async function crunchyGetPlaylistMPD(q: string) {
}
}
export async function downloadPlaylist(e: string, dubs: Array<string>, subs: Array<string>, hardsub: boolean, downloadID: number) {
var playlist = await crunchyGetPlaylist(e)
var downloading: Array<{
id: number
downloadedParts: number
partsToDownload: number
downloadSpeed: number
}> = []
console.log(dubs)
console.log(subs)
export async function downloadPlaylist(e: string, dubs: Array<string>, subs: Array<string>, hardsub: boolean, downloadID: number, name: string, season: number, episode: number) {
downloading.push({
id: downloadID,
downloadedParts: 0,
partsToDownload: 0,
downloadSpeed: 0
})
await updatePlaylistByID(downloadID, 'downloading')
var playlist = await crunchyGetPlaylist(e)
if (!playlist) {
console.log('Playlist not found')
@ -312,6 +350,10 @@ export async function downloadPlaylist(e: string, dubs: Array<string>, subs: Arr
const audioFolder = await createFolder()
const videoFolder = await createFolder()
const seasonFolder = await createFolderName(`${name.replace(/[/\\?%*:|"<>]/g, '')} Season ${season}`)
const dubDownloadList: Array<{
audio_locale: string
guid: string
@ -443,20 +485,6 @@ export async function downloadPlaylist(e: string, dubs: Array<string>, subs: Arr
var mdp = await crunchyGetPlaylistMPD(play.url)
// if (hardsub) {
// const hardsuburl = play.hardSubs.find(h=> h.hlang === subs[0])?.url
// if (!hardsuburl) {
// console.error('No Hardsub stream found')
// return
// }
// mdp = await crunchyGetPlaylistMPD(hardsuburl)
// console.error('Hardsub stream found')
// } else {
// console.error('Hardsub is false')
// }
if (!mdp) return
var hq = mdp.playlists.find((i) => i.attributes.RESOLUTION?.width === 1920)
@ -477,11 +505,15 @@ export async function downloadPlaylist(e: string, dubs: Array<string>, subs: Arr
})
}
await updatePlaylistByID(downloadID, 'downloading')
// await updatePlaylistToDownloadPartsByID(downloadID, p.length)
await updatePlaylistToDownloadPartsByID(downloadID, p.length)
const dn = downloading.find((i) => i.id === downloadID)
const file = await downloadParts(p, downloadID)
if (dn) {
dn.partsToDownload = p.length
}
const file = await downloadParts(p, downloadID, videoFolder)
return file
}
@ -490,14 +522,13 @@ export async function downloadPlaylist(e: string, dubs: Array<string>, subs: Arr
if (!audios) return
await updatePlaylistByID(downloadID, 'merging')
await mergeFile(file as string, audios, subss, String(playlist.assetId), seasonFolder, `${name.replace(/[/\\?%*:|"<>]/g, '')} Season ${season} Episode ${episode}`)
await mergeFile(file as string, audios, subss, String(playlist.assetId))
await updatePlaylistByID(downloadID, 'completed')
await deleteFolder(subFolder)
await deleteFolder(audioFolder)
await updatePlaylistByID(downloadID, 'completed')
await deleteFolder(videoFolder)
return playlist
}
@ -534,9 +565,12 @@ async function fetchAndPipe(url: string, stream: fs.WriteStream, index: number)
})
}
async function downloadParts(parts: { filename: string; url: string }[], downloadID: number) {
var partsdownloaded = 0
async function downloadParts(parts: { filename: string; url: string }[], downloadID: number, dir: string) {
const path = await createFolder()
const dn = downloading.find((i) => i.id === downloadID)
let totalDownloadedBytes = 0
let startTime = Date.now()
for (const [index, part] of parts.entries()) {
let success = false
@ -544,10 +578,25 @@ async function downloadParts(parts: { filename: string; url: string }[], downloa
try {
const stream = fs.createWriteStream(`${path}/${part.filename}`)
const { body } = await fetch(part.url)
await finished(Readable.fromWeb(body as any).pipe(stream))
const readableStream = Readable.from(body as any)
let partDownloadedBytes = 0
readableStream.on('data', (chunk) => {
partDownloadedBytes += chunk.length
totalDownloadedBytes += chunk.length
})
await finished(readableStream.pipe(stream))
console.log(`Fragment ${index + 1} downloaded`)
partsdownloaded++
updatePlaylistToDownloadedPartsByID(downloadID, partsdownloaded, parts.length)
if (dn) {
dn.downloadedParts++
const endTime = Date.now()
const durationInSeconds = (endTime - startTime) / 1000
dn.downloadSpeed = totalDownloadedBytes / 1024 / 1024 / durationInSeconds
}
success = true
} catch (error) {
console.error(`Error occurred during download of fragment ${index + 1}:`, error)
@ -557,7 +606,7 @@ async function downloadParts(parts: { filename: string; url: string }[], downloa
}
}
return await mergeParts(parts, path)
return await mergeParts(parts, downloadID, path, dir)
}
async function downloadSub(
@ -631,12 +680,15 @@ async function concatenateTSFiles(inputFiles: Array<string>, outputFile: string)
})
}
async function mergeParts(parts: { filename: string; url: string }[], tmp: string) {
async function mergeParts(parts: { filename: string; url: string }[], downloadID: number, tmp: string, dir: string) {
const tempname = (Math.random() + 1).toString(36).substring(2)
try {
const list: Array<string> = []
await updatePlaylistByID(downloadID, 'merging')
isDownloading--
for (const [index, part] of parts.entries()) {
list.push(`${tmp}/${part.filename}`)
}
@ -648,11 +700,11 @@ async function mergeParts(parts: { filename: string; url: string }[], tmp: strin
Ffmpeg()
.input(concatenatedFile)
.outputOptions('-c copy')
.save(app.getPath('documents') + `/${tempname}.mp4`)
.save(dir + `/${tempname}.mp4`)
.on('end', async () => {
console.log('Merging finished')
await deleteFolder(tmp)
return resolve(app.getPath('documents') + `/${tempname}.mp4`)
return resolve(dir + `/${tempname}.mp4`)
})
})
} catch (error) {
@ -688,7 +740,7 @@ async function mergePartsAudio(parts: { filename: string; url: string }[], tmp:
}
}
async function mergeFile(video: string, audios: Array<string>, subs: Array<string>, name: string) {
async function mergeFile(video: string, audios: Array<string>, subs: Array<string>, name: string, path: string, filename: string) {
const locales: Array<{
locale: string
name: string
@ -796,7 +848,7 @@ async function mergeFile(video: string, audios: Array<string>, subs: Array<strin
output
.addOptions(options)
.saveToFile(app.getPath('documents') + `/${name}.mkv`)
.saveToFile(path + `/${filename}.mkv`)
.on('error', (error) => {
console.log(error)
reject(error)

View File

@ -65,7 +65,6 @@ function createWindow() {
if (singleInstance(app, mainWindow)) return
// Open the DevTools.
!isProduction &&
mainWindow.webContents.openDevTools({
mode: 'bottom'
})