added format selector and more adn stuff
This commit is contained in:
parent
195b7aee15
commit
8ece48d52a
@ -8,16 +8,16 @@ export async function crunchyLogin() {
|
||||
return { data, error }
|
||||
}
|
||||
|
||||
export async function checkAccount() {
|
||||
const { data, error } = await useFetch<CrunchyLogin>('http://localhost:8080/api/crunchyroll/check', {
|
||||
export async function checkAccount(service: string) {
|
||||
const { data, error } = await useFetch<CrunchyLogin>(`http://localhost:8080/api/service/check/${service}`, {
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
return { data, error }
|
||||
}
|
||||
|
||||
export async function loginAccount(user: string, password: string) {
|
||||
const { data, error } = await useFetch<CrunchyLogin>('http://localhost:8080/api/crunchyroll/login/login', {
|
||||
export async function loginAccount(user: string, password: string, service: string) {
|
||||
const { data, error } = await useFetch<CrunchyLogin>(`http://localhost:8080/api/service/login/${service}`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
user: user,
|
||||
|
@ -39,18 +39,18 @@ async function openSettings() {
|
||||
}
|
||||
|
||||
async function openAddAnime() {
|
||||
const { data, error } = await checkAccount()
|
||||
// const { data, error } = await checkAccount()
|
||||
|
||||
if (error.value) {
|
||||
(window as any).myAPI.openWindow({
|
||||
title: "Crunchyroll Login",
|
||||
url: isProduction ? 'http://localhost:8079/crunchylogin' : 'http://localhost:3000/crunchylogin',
|
||||
width: 600,
|
||||
height: 300,
|
||||
backgroundColor: "#111111"
|
||||
})
|
||||
return
|
||||
}
|
||||
// if (error.value) {
|
||||
// (window as any).myAPI.openWindow({
|
||||
// title: "Crunchyroll Login",
|
||||
// url: isProduction ? 'http://localhost:8079/crunchylogin' : 'http://localhost:3000/crunchylogin',
|
||||
// width: 600,
|
||||
// height: 300,
|
||||
// backgroundColor: "#111111"
|
||||
// })
|
||||
// return
|
||||
// }
|
||||
|
||||
(window as any).myAPI.openWindow({
|
||||
title: "Add Anime",
|
||||
|
@ -15,11 +15,11 @@
|
||||
<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="crunchyroll">Crunchyroll</option>
|
||||
<option value="adn">ADN</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="relative flex flex-col">
|
||||
<div v-if="isLoggedInCR && service === 'crunchyroll' || !isLoggedInCR && service === 'adn'" class="relative flex flex-col">
|
||||
<input
|
||||
v-model="search"
|
||||
@input="handleInputChange"
|
||||
@ -61,10 +61,10 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative flex flex-col">
|
||||
<div v-if="isLoggedInCR && service === 'crunchyroll'" 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 v-if="isLoggedInCR && service === 'crunchyroll'" class="relative flex flex-col">
|
||||
<input
|
||||
@click="getFolderPath()"
|
||||
v-model="path"
|
||||
@ -75,6 +75,11 @@
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!isLoggedInCR && service === 'crunchyroll'" class="relative flex flex-col">
|
||||
<button @click="openCRLogin"
|
||||
class="bg-[#5c5b5b] focus:outline-none px-3 py-2 rounded-xl text-sm text-center cursor-pointer"
|
||||
>Click to Login</button>
|
||||
</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'">
|
||||
@ -165,7 +170,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-5">
|
||||
<div class="flex flex-row gap-3">
|
||||
<div class="relative flex flex-col w-full">
|
||||
<select v-model="hardsub" name="episode" class="bg-[#5c5b5b] focus:outline-none px-3 py-2 rounded-xl text-sm text-center cursor-pointer" :disabled="isHardsubDisabled">
|
||||
<option :value="false" class="text-sm text-slate-200">Hardsub: false</option>
|
||||
@ -188,6 +193,12 @@
|
||||
<option :value="240" class="text-sm text-slate-200">240p</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="relative flex flex-col w-full">
|
||||
<select v-model="format" name="format" class="bg-[#5c5b5b] focus:outline-none px-3 py-2 rounded-xl text-sm text-center cursor-pointer">
|
||||
<option value="mp4" class="text-sm text-slate-200">MP4</option>
|
||||
<option value="mkv" class="text-sm text-slate-200">MKV</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- {{ CRselectedShow?.Subs.map(s=> { return locales.find(l => l.locale === s)?.name }) }}
|
||||
@ -226,6 +237,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { searchADN } from '~/components/ADN/ListAnimes'
|
||||
import { checkAccount } from '~/components/Crunchyroll/Account'
|
||||
import { getCRSeries, searchCrunchy } from '~/components/Crunchyroll/ListAnimes'
|
||||
import { listEpisodeCrunchy } from '~/components/Crunchyroll/ListEpisodes'
|
||||
import { listSeasonCrunchy } from '~/components/Crunchyroll/ListSeasons'
|
||||
@ -257,6 +269,7 @@ const locales = ref<Array<{ locale: string; name: string }>>([
|
||||
{ locale: 'ko-KR', name: 'KO' }
|
||||
])
|
||||
|
||||
const isProduction = process.env.NODE_ENV !== 'development'
|
||||
const selectDub = ref<boolean>(false)
|
||||
const selectedDubs = ref<Array<{ name: string | undefined; locale: string }>>([{ locale: 'ja-JP', name: 'JP' }])
|
||||
|
||||
@ -282,11 +295,44 @@ const hardsub = ref<boolean>(false)
|
||||
const added = ref<boolean>(false)
|
||||
const isHardsubDisabled = ref<boolean>(true)
|
||||
const quality = ref<1080 | 720 | 480 | 360 | 240>(1080)
|
||||
const format = ref<'mp4' | 'mkv'>('mkv')
|
||||
|
||||
const isFetchingSeasons = ref<number>(0)
|
||||
const isFetchingEpisodes = ref<number>(0)
|
||||
const isFetchingResults = ref<number>(0)
|
||||
|
||||
const isLoggedInCR = ref<boolean>(false)
|
||||
let interval: NodeJS.Timeout;
|
||||
|
||||
const checkIfLoggedInCR = async () => {
|
||||
const { data, error } = await checkAccount('CR')
|
||||
|
||||
if (error.value) {
|
||||
isLoggedInCR.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (interval) {
|
||||
clearInterval(interval)
|
||||
}
|
||||
|
||||
isLoggedInCR.value = true
|
||||
}
|
||||
|
||||
const openCRLogin = () => {
|
||||
(window as any).myAPI.openWindow({
|
||||
title: "Crunchyroll Login",
|
||||
url: isProduction ? 'http://localhost:8079/crunchylogin' : 'http://localhost:3000/crunchylogin',
|
||||
width: 600,
|
||||
height: 300,
|
||||
backgroundColor: "#111111"
|
||||
})
|
||||
|
||||
interval = setInterval(checkIfLoggedInCR, 1000)
|
||||
}
|
||||
|
||||
checkIfLoggedInCR()
|
||||
|
||||
const fetchSearch = async () => {
|
||||
if (!search.value || search.value.length === 0) {
|
||||
adnSearchResults.value = []
|
||||
@ -343,7 +389,7 @@ const selectShow = async (show: any) => {
|
||||
|
||||
if (service.value === 'crunchyroll') {
|
||||
CRselectedShow.value = show
|
||||
url.value = show.Url
|
||||
url.value = show.Url + '/'
|
||||
}
|
||||
|
||||
search.value = ''
|
||||
@ -471,6 +517,18 @@ const addToPlaylist = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
var selService
|
||||
|
||||
if (service.value === 'crunchyroll') {
|
||||
selService = 'CR'
|
||||
}
|
||||
|
||||
if (service.value === 'adn') {
|
||||
selService = 'ADN'
|
||||
}
|
||||
|
||||
if (!selService) return
|
||||
|
||||
const selectedEpisodes = episodes.value.slice(startEpisodeIndex, endEpisodeIndex + 1)
|
||||
|
||||
const data = {
|
||||
@ -479,10 +537,12 @@ const addToPlaylist = async () => {
|
||||
subs: selectedSubs.value,
|
||||
dir: path.value,
|
||||
hardsub: hardsub.value,
|
||||
quality: quality.value
|
||||
quality: quality.value,
|
||||
service: selService,
|
||||
format: format.value
|
||||
}
|
||||
|
||||
const { error } = await useFetch('http://localhost:8080/api/crunchyroll/playlist', {
|
||||
const { error } = await useFetch('http://localhost:8080/api/service/playlist', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
|
@ -34,16 +34,6 @@ const password = ref<string>()
|
||||
|
||||
const isLoggingIn = ref<number>(0)
|
||||
|
||||
const openAddAnime = () => {
|
||||
(window as any).myAPI.openWindow({
|
||||
title: "Add Anime",
|
||||
url: isProduction ? 'http://localhost:8079/addanime' : 'http://localhost:3000/addanime',
|
||||
width: 700,
|
||||
height: 450,
|
||||
backgroundColor: "#111111"
|
||||
})
|
||||
}
|
||||
|
||||
const login = async () => {
|
||||
isLoggingIn.value++
|
||||
if (!username.value) {
|
||||
@ -55,14 +45,13 @@ const login = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
const { data, error } = await loginAccount(username.value, password.value)
|
||||
const { data, error } = await loginAccount(username.value, password.value, 'CR')
|
||||
|
||||
if (error.value) {
|
||||
isLoggingIn.value--
|
||||
return
|
||||
}
|
||||
isLoggingIn.value--
|
||||
openAddAnime()
|
||||
close()
|
||||
}
|
||||
</script>
|
||||
|
@ -10,9 +10,14 @@
|
||||
<img :src="p.media.images.thumbnail[0].find((p) => p.height === 1080)?.source" alt="Image" class="object-cover rounded-xl" />
|
||||
</div>
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="text-sm capitalize">
|
||||
<div class="flex flex-row">
|
||||
<div class="text-sm capitalize">
|
||||
{{ p.status }}
|
||||
</div>
|
||||
<div class="text-sm capitalize ml-auto">
|
||||
{{ p.service === 'CR' ? 'Crunchyroll' : 'ADN' }}
|
||||
</div>
|
||||
</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
|
||||
@ -26,9 +31,10 @@
|
||||
<div class="flex h-full"> </div>
|
||||
<div class="flex flex-row gap-2 mt-2">
|
||||
<div class="text-sm">{{ p.quality }}p</div>
|
||||
<div class="text-sm uppercase">{{ p.format }}</div>
|
||||
<div class="text-sm">Dubs: {{ p.dub.map((t) => t.name).join(', ') }}</div>
|
||||
<div class="text-sm">Subs: {{ p.sub.length !== 0 ? 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.partsleft && p.status === 'downloading'" class="text-sm ml-auto">{{ p.partsdownloaded }}/{{ p.partsleft }}</div>
|
||||
<div v-if="p.downloadspeed && p.status === 'downloading'" class="text-sm">{{ p.downloadspeed }} MB/s</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -52,6 +58,8 @@ const playlist = ref<
|
||||
partsdownloaded: number
|
||||
downloadspeed: number
|
||||
quality: number
|
||||
service: string
|
||||
format: string
|
||||
}>
|
||||
>()
|
||||
|
||||
@ -68,8 +76,10 @@ const getPlaylist = async () => {
|
||||
partsdownloaded: number
|
||||
downloadspeed: number
|
||||
quality: number
|
||||
service: string
|
||||
format: string
|
||||
}>
|
||||
>('http://localhost:8080/api/crunchyroll/playlist')
|
||||
>('http://localhost:8080/api/service/playlist')
|
||||
|
||||
if (error.value) {
|
||||
alert(error.value)
|
||||
@ -84,7 +94,7 @@ const getPlaylist = async () => {
|
||||
}
|
||||
|
||||
const deletePlaylist = async () => {
|
||||
const { data, error } = await useFetch('http://localhost:8080/api/crunchyroll/playlist', {
|
||||
const { data, error } = await useFetch('http://localhost:8080/api/service/playlist', {
|
||||
method: 'delete'
|
||||
})
|
||||
|
||||
|
@ -3,6 +3,7 @@ import cors from "@fastify/cors";
|
||||
import NodeCache from "node-cache";
|
||||
import crunchyrollRoutes from "./routes/crunchyroll/crunchyroll.route";
|
||||
import { sequelize } from "./db/database";
|
||||
import serviceRoutes from "./routes/service/service.route";
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
@ -43,6 +44,7 @@ server.decorate("CacheController", CacheController);
|
||||
|
||||
// Routes
|
||||
server.register(crunchyrollRoutes, { prefix: 'api/crunchyroll' })
|
||||
server.register(serviceRoutes, { prefix: 'api/service' })
|
||||
|
||||
function startAPI() {
|
||||
server.listen({ port: 8080 }, (err, address) => {
|
||||
|
@ -4,7 +4,7 @@ import { CrunchyEpisode } from '../types/crunchyroll'
|
||||
|
||||
const sequelize = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
storage: app.getPath('documents') + '/crd-dbv1.db'
|
||||
storage: app.getPath('documents') + '/crd-dbv2.db'
|
||||
})
|
||||
|
||||
interface AccountAttributes {
|
||||
@ -35,7 +35,9 @@ interface PlaylistAttributes {
|
||||
hardsub: boolean
|
||||
quality: 1080 | 720 | 480 | 360 | 240
|
||||
dir: string
|
||||
failedreason: string
|
||||
failedreason: string,
|
||||
service: 'CR' | 'ADN',
|
||||
format: 'mp4' | 'mkv'
|
||||
}
|
||||
|
||||
interface PlaylistCreateAttributes {
|
||||
@ -45,7 +47,9 @@ interface PlaylistCreateAttributes {
|
||||
dir: string
|
||||
quality: 1080 | 720 | 480 | 360 | 240
|
||||
hardsub: boolean
|
||||
status: 'waiting' | 'preparing' | 'downloading' | 'merging' | 'completed' | 'failed'
|
||||
status: 'waiting' | 'preparing' | 'downloading' | 'merging' | 'completed' | 'failed',
|
||||
service: 'CR' | 'ADN',
|
||||
format: 'mp4' | 'mkv'
|
||||
}
|
||||
|
||||
const Account: ModelDefined<AccountAttributes, AccountCreateAttributes> = sequelize.define('Accounts', {
|
||||
@ -116,6 +120,14 @@ const Playlist: ModelDefined<PlaylistAttributes, PlaylistCreateAttributes> = seq
|
||||
allowNull: true,
|
||||
type: DataTypes.BOOLEAN
|
||||
},
|
||||
service: {
|
||||
allowNull: true,
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
format: {
|
||||
allowNull: true,
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
createdAt: {
|
||||
allowNull: false,
|
||||
type: DataTypes.DATE
|
||||
|
@ -2,376 +2,468 @@ import JSEncrypt from 'jsencrypt'
|
||||
import CryptoJS from 'crypto-js'
|
||||
import { server } from '../../api'
|
||||
import { ADNPlayerConfig } from '../../types/adn'
|
||||
import { messageBox } from '../../../electron/background'
|
||||
import { useFetch } from '../useFetch'
|
||||
|
||||
export async function getShowADN(q: number) {
|
||||
const cachedData = server.CacheController.get(`getshowadn-${q}`)
|
||||
// export async function getShowADN(q: number) {
|
||||
// const cachedData = server.CacheController.get(`getshowadn-${q}`)
|
||||
|
||||
if (cachedData) {
|
||||
return cachedData
|
||||
}
|
||||
// if (cachedData) {
|
||||
// return cachedData
|
||||
// }
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://gw.api.animationdigitalnetwork.fr/show/${q}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'x-target-distribution': 'de'
|
||||
}
|
||||
})
|
||||
// try {
|
||||
// const response = await fetch(`https://gw.api.animationdigitalnetwork.fr/show/${q}`, {
|
||||
// method: 'GET',
|
||||
// headers: {
|
||||
// 'x-target-distribution': 'de'
|
||||
// }
|
||||
// })
|
||||
|
||||
if (response.ok) {
|
||||
const data: {
|
||||
show: Array<any>
|
||||
} = JSON.parse(await response.text())
|
||||
// if (response.ok) {
|
||||
// const data: {
|
||||
// show: Array<any>
|
||||
// } = JSON.parse(await response.text())
|
||||
|
||||
server.CacheController.set(`getshowadn-${q}`, data.show, 1000)
|
||||
// server.CacheController.set(`getshowadn-${q}`, data.show, 1000)
|
||||
|
||||
return data.show
|
||||
} else {
|
||||
throw new Error('Failed to fetch ADN')
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(e as string)
|
||||
}
|
||||
}
|
||||
// return data.show
|
||||
// } else {
|
||||
// throw new Error('Failed to fetch ADN')
|
||||
// }
|
||||
// } catch (e) {
|
||||
// throw new Error(e as string)
|
||||
// }
|
||||
// }
|
||||
|
||||
export async function getEpisodesWithShowIdADN(q: number) {
|
||||
const cachedData = server.CacheController.get(`getepisodesadn-${q}`)
|
||||
// export async function getEpisodesWithShowIdADN(q: number) {
|
||||
// const cachedData = server.CacheController.get(`getepisodesadn-${q}`)
|
||||
|
||||
if (cachedData) {
|
||||
return cachedData
|
||||
}
|
||||
// if (cachedData) {
|
||||
// return cachedData
|
||||
// }
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://gw.api.animationdigitalnetwork.fr/video/show/${q}?offset=0&limit=-1&order=asc`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'x-target-distribution': 'de'
|
||||
}
|
||||
})
|
||||
// try {
|
||||
// const response = await fetch(`https://gw.api.animationdigitalnetwork.fr/video/show/${q}?offset=0&limit=-1&order=asc`, {
|
||||
// method: 'GET',
|
||||
// headers: {
|
||||
// 'x-target-distribution': 'de'
|
||||
// }
|
||||
// })
|
||||
|
||||
if (response.ok) {
|
||||
const data: {
|
||||
videos: Array<any>
|
||||
} = JSON.parse(await response.text())
|
||||
// if (response.ok) {
|
||||
// const data: {
|
||||
// videos: Array<any>
|
||||
// } = JSON.parse(await response.text())
|
||||
|
||||
server.CacheController.set(`getepisodesadn-${q}`, data.videos, 1000)
|
||||
// server.CacheController.set(`getepisodesadn-${q}`, data.videos, 1000)
|
||||
|
||||
return data.videos
|
||||
} else {
|
||||
throw new Error('Failed to fetch ADN')
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(e as string)
|
||||
}
|
||||
}
|
||||
// return data.videos
|
||||
// } else {
|
||||
// throw new Error('Failed to fetch ADN')
|
||||
// }
|
||||
// } catch (e) {
|
||||
// throw new Error(e as string)
|
||||
// }
|
||||
// }
|
||||
|
||||
export async function getEpisodeADN(q: number) {
|
||||
const cachedData = server.CacheController.get(`getepisodeadn-${q}`)
|
||||
// export async function getEpisodeADN(q: number) {
|
||||
// const cachedData = server.CacheController.get(`getepisodeadn-${q}`)
|
||||
|
||||
if (cachedData) {
|
||||
return cachedData
|
||||
}
|
||||
// if (cachedData) {
|
||||
// return cachedData
|
||||
// }
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://gw.api.animationdigitalnetwork.fr/video/${q}/public`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'x-target-distribution': 'de'
|
||||
}
|
||||
})
|
||||
// try {
|
||||
// const response = await fetch(`https://gw.api.animationdigitalnetwork.fr/video/${q}/public`, {
|
||||
// method: 'GET',
|
||||
// headers: {
|
||||
// 'x-target-distribution': 'de'
|
||||
// }
|
||||
// })
|
||||
|
||||
if (response.ok) {
|
||||
const data: {
|
||||
video: Array<any>
|
||||
} = JSON.parse(await response.text())
|
||||
// if (response.ok) {
|
||||
// const data: {
|
||||
// video: Array<any>
|
||||
// } = JSON.parse(await response.text())
|
||||
|
||||
server.CacheController.set(`getepisodeadn-${q}`, data.video, 1000)
|
||||
// server.CacheController.set(`getepisodeadn-${q}`, data.video, 1000)
|
||||
|
||||
return data.video
|
||||
} else {
|
||||
throw new Error('Failed to fetch ADN')
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(e as string)
|
||||
}
|
||||
}
|
||||
// return data.video
|
||||
// } else {
|
||||
// throw new Error('Failed to fetch ADN')
|
||||
// }
|
||||
// } catch (e) {
|
||||
// throw new Error(e as string)
|
||||
// }
|
||||
// }
|
||||
|
||||
export async function searchADN(q: string) {
|
||||
const cachedData = server.CacheController.get(`searchadn-${q}`)
|
||||
// export async function searchADN(q: string) {
|
||||
// const cachedData = server.CacheController.get(`searchadn-${q}`)
|
||||
|
||||
if (cachedData) {
|
||||
return cachedData
|
||||
}
|
||||
// if (cachedData) {
|
||||
// return cachedData
|
||||
// }
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://gw.api.animationdigitalnetwork.fr/show/catalog?maxAgeCategory=18&search=${q}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'x-target-distribution': 'de'
|
||||
}
|
||||
})
|
||||
// try {
|
||||
// const response = await fetch(`https://gw.api.animationdigitalnetwork.fr/show/catalog?maxAgeCategory=18&search=${q}`, {
|
||||
// method: 'GET',
|
||||
// headers: {
|
||||
// 'x-target-distribution': 'de'
|
||||
// }
|
||||
// })
|
||||
|
||||
if (response.ok) {
|
||||
const data: {
|
||||
shows: Array<any>
|
||||
} = JSON.parse(await response.text())
|
||||
// if (response.ok) {
|
||||
// const data: {
|
||||
// shows: Array<any>
|
||||
// } = JSON.parse(await response.text())
|
||||
|
||||
server.CacheController.set(`searchadn-${q}`, data.shows, 1000)
|
||||
// server.CacheController.set(`searchadn-${q}`, data.shows, 1000)
|
||||
|
||||
return data.shows
|
||||
} else {
|
||||
throw new Error('Failed to fetch ADN')
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(e as string)
|
||||
}
|
||||
}
|
||||
// return data.shows
|
||||
// } else {
|
||||
// throw new Error('Failed to fetch ADN')
|
||||
// }
|
||||
// } catch (e) {
|
||||
// throw new Error(e as string)
|
||||
// }
|
||||
// }
|
||||
|
||||
export async function loginADN() {
|
||||
const cachedData = server.CacheController.get('adnlogin')
|
||||
const cachedToken = server.CacheController.get('adntoken')
|
||||
// export async function loginADN() {
|
||||
// const cachedData = server.CacheController.get('adnlogin')
|
||||
// const cachedToken = server.CacheController.get('adntoken')
|
||||
|
||||
if (cachedData) {
|
||||
return cachedData
|
||||
}
|
||||
// if (cachedData) {
|
||||
// return cachedData
|
||||
// }
|
||||
|
||||
if (cachedToken) {
|
||||
const data = await loginADNToken(cachedToken as string)
|
||||
return data
|
||||
}
|
||||
// if (cachedToken) {
|
||||
// const data = await loginADNToken(cachedToken as string)
|
||||
// return data
|
||||
// }
|
||||
|
||||
const body = {
|
||||
source: 'Web',
|
||||
rememberMe: true
|
||||
}
|
||||
// const body = {
|
||||
// source: 'Web',
|
||||
// rememberMe: true
|
||||
// }
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://gw.api.animationdigitalnetwork.fr/authentication/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-target-distribution': 'de',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
// try {
|
||||
// const response = await fetch(`https://gw.api.animationdigitalnetwork.fr/authentication/login`, {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'x-target-distribution': 'de',
|
||||
// 'Content-Type': 'application/json'
|
||||
// },
|
||||
// body: JSON.stringify(body)
|
||||
// })
|
||||
|
||||
if (response.ok) {
|
||||
const data: {
|
||||
accessToken: string
|
||||
} = JSON.parse(await response.text())
|
||||
// if (response.ok) {
|
||||
// const data: {
|
||||
// accessToken: string
|
||||
// } = JSON.parse(await response.text())
|
||||
|
||||
server.CacheController.set('adnlogin', data, 100)
|
||||
server.CacheController.set('adntoken', data.accessToken, 300)
|
||||
server.CacheController.del('adnplayerconfig')
|
||||
// server.CacheController.set('adnlogin', data, 100)
|
||||
// server.CacheController.set('adntoken', data.accessToken, 300)
|
||||
// server.CacheController.del('adnplayerconfig')
|
||||
|
||||
return data
|
||||
} else {
|
||||
throw new Error('Failed to fetch ADN')
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(e as string)
|
||||
}
|
||||
}
|
||||
// return data
|
||||
// } else {
|
||||
// throw new Error('Failed to fetch ADN')
|
||||
// }
|
||||
// } catch (e) {
|
||||
// throw new Error(e as string)
|
||||
// }
|
||||
// }
|
||||
|
||||
async function loginADNToken(t: string) {
|
||||
const body = {
|
||||
source: 'Web',
|
||||
rememberMe: true
|
||||
}
|
||||
// async function loginADNToken(t: string) {
|
||||
// const body = {
|
||||
// source: 'Web',
|
||||
// rememberMe: true
|
||||
// }
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://gw.api.animationdigitalnetwork.fr/authentication/refresh`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-target-distribution': 'de',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${t}`,
|
||||
'X-Access-Token': t
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
// try {
|
||||
// const response = await fetch(`https://gw.api.animationdigitalnetwork.fr/authentication/refresh`, {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'x-target-distribution': 'de',
|
||||
// 'Content-Type': 'application/json',
|
||||
// Authorization: `Bearer ${t}`,
|
||||
// 'X-Access-Token': t
|
||||
// },
|
||||
// body: JSON.stringify(body)
|
||||
// })
|
||||
|
||||
if (response.ok) {
|
||||
const data: {
|
||||
accessToken: string
|
||||
} = JSON.parse(await response.text())
|
||||
// if (response.ok) {
|
||||
// const data: {
|
||||
// accessToken: string
|
||||
// } = JSON.parse(await response.text())
|
||||
|
||||
server.CacheController.set('adnlogin', data, 100)
|
||||
server.CacheController.set('adntoken', data.accessToken, 300)
|
||||
server.CacheController.del('adnplayerconfig')
|
||||
// server.CacheController.set('adnlogin', data, 100)
|
||||
// server.CacheController.set('adntoken', data.accessToken, 300)
|
||||
// server.CacheController.del('adnplayerconfig')
|
||||
|
||||
return data
|
||||
} else {
|
||||
console.log(await response.text())
|
||||
throw new Error('Failed to fetch ADN')
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(e as string)
|
||||
}
|
||||
}
|
||||
// return data
|
||||
// } else {
|
||||
// console.log(await response.text())
|
||||
// throw new Error('Failed to fetch ADN')
|
||||
// }
|
||||
// } catch (e) {
|
||||
// throw new Error(e as string)
|
||||
// }
|
||||
// }
|
||||
|
||||
export async function getPlayerConfigADN() {
|
||||
const cachedData: ADNPlayerConfig | undefined = server.CacheController.get('adnplayerconfig')
|
||||
// export async function getPlayerConfigADN() {
|
||||
// const cachedData: ADNPlayerConfig | undefined = server.CacheController.get('adnplayerconfig')
|
||||
|
||||
if (cachedData) {
|
||||
return cachedData
|
||||
}
|
||||
// if (cachedData) {
|
||||
// return cachedData
|
||||
// }
|
||||
|
||||
const token: any = await loginADN()
|
||||
// const token: any = await loginADN()
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://gw.api.animationdigitalnetwork.fr/player/video/19830/configuration`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'x-target-distribution': 'de',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token.accessToken}`
|
||||
}
|
||||
})
|
||||
// try {
|
||||
// const response = await fetch(`https://gw.api.animationdigitalnetwork.fr/player/video/19830/configuration`, {
|
||||
// method: 'GET',
|
||||
// headers: {
|
||||
// 'x-target-distribution': 'de',
|
||||
// 'Content-Type': 'application/json',
|
||||
// Authorization: `Bearer ${token.accessToken}`
|
||||
// }
|
||||
// })
|
||||
|
||||
if (response.ok) {
|
||||
const data: ADNPlayerConfig = JSON.parse(await response.text())
|
||||
// if (response.ok) {
|
||||
// const data: ADNPlayerConfig = JSON.parse(await response.text())
|
||||
|
||||
server.CacheController.set('adnplayerconfig', data, 300)
|
||||
// server.CacheController.set('adnplayerconfig', data, 300)
|
||||
|
||||
return data
|
||||
} else {
|
||||
throw new Error('Failed to fetch ADN')
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(e as string)
|
||||
}
|
||||
}
|
||||
// return data
|
||||
// } else {
|
||||
// throw new Error('Failed to fetch ADN')
|
||||
// }
|
||||
// } catch (e) {
|
||||
// throw new Error(e as string)
|
||||
// }
|
||||
// }
|
||||
|
||||
async function getPlayerToken() {
|
||||
const r = await getPlayerConfigADN()
|
||||
// async function getPlayerToken() {
|
||||
// const r = await getPlayerConfigADN()
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://gw.api.animationdigitalnetwork.fr/player/refresh/token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-target-distribution': 'de',
|
||||
'Content-Type': 'application/json',
|
||||
'X-Player-Refresh-Token': r.player.options.user.refreshToken
|
||||
}
|
||||
})
|
||||
// try {
|
||||
// const response = await fetch(`https://gw.api.animationdigitalnetwork.fr/player/refresh/token`, {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'x-target-distribution': 'de',
|
||||
// 'Content-Type': 'application/json',
|
||||
// 'X-Player-Refresh-Token': r.player.options.user.refreshToken
|
||||
// }
|
||||
// })
|
||||
|
||||
if (response.ok) {
|
||||
const data: {
|
||||
token: string
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
} = JSON.parse(await response.text())
|
||||
// if (response.ok) {
|
||||
// const data: {
|
||||
// token: string
|
||||
// accessToken: string
|
||||
// refreshToken: string
|
||||
// } = JSON.parse(await response.text())
|
||||
|
||||
return data
|
||||
} else {
|
||||
throw new Error('Failed to fetch ADN')
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(e as string)
|
||||
}
|
||||
}
|
||||
// return data
|
||||
// } else {
|
||||
// throw new Error('Failed to fetch ADN')
|
||||
// }
|
||||
// } catch (e) {
|
||||
// throw new Error(e as string)
|
||||
// }
|
||||
// }
|
||||
|
||||
function randomHexaString(length: number) {
|
||||
const characters = '0123456789abcdef'
|
||||
let result = ''
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters[Math.floor(Math.random() * characters.length)]
|
||||
}
|
||||
return result
|
||||
}
|
||||
// function randomHexaString(length: number) {
|
||||
// const characters = '0123456789abcdef'
|
||||
// let result = ''
|
||||
// for (let i = 0; i < length; i++) {
|
||||
// result += characters[Math.floor(Math.random() * characters.length)]
|
||||
// }
|
||||
// return result
|
||||
// }
|
||||
|
||||
async function getPlayerEncryptedToken() {
|
||||
const token = await getPlayerToken()
|
||||
// async function getPlayerEncryptedToken() {
|
||||
// const token = await getPlayerToken()
|
||||
|
||||
var key = new JSEncrypt()
|
||||
var random = randomHexaString(16)
|
||||
// var key = new JSEncrypt()
|
||||
// var random = randomHexaString(16)
|
||||
|
||||
key.setPublicKey(
|
||||
'-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCbQrCJBRmaXM4gJidDmcpWDssgnumHinCLHAgS4buMtdH7dEGGEUfBofLzoEdt1jqcrCDT6YNhM0aFCqbLOPFtx9cg/X2G/G5bPVu8cuFM0L+ehp8s6izK1kjx3OOPH/kWzvstM5tkqgJkNyNEvHdeJl6KhS+IFEqwvZqgbBpKuwIDAQAB-----END PUBLIC KEY-----'
|
||||
)
|
||||
// key.setPublicKey(
|
||||
// '-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCbQrCJBRmaXM4gJidDmcpWDssgnumHinCLHAgS4buMtdH7dEGGEUfBofLzoEdt1jqcrCDT6YNhM0aFCqbLOPFtx9cg/X2G/G5bPVu8cuFM0L+ehp8s6izK1kjx3OOPH/kWzvstM5tkqgJkNyNEvHdeJl6KhS+IFEqwvZqgbBpKuwIDAQAB-----END PUBLIC KEY-----'
|
||||
// )
|
||||
|
||||
const data = {
|
||||
k: random,
|
||||
t: String(token.token)
|
||||
}
|
||||
// const data = {
|
||||
// k: random,
|
||||
// t: String(token.token)
|
||||
// }
|
||||
|
||||
const finisheddata = JSON.stringify(data)
|
||||
// const finisheddata = JSON.stringify(data)
|
||||
|
||||
const encryptedData = key.encrypt(finisheddata) || ''
|
||||
// const encryptedData = key.encrypt(finisheddata) || ''
|
||||
|
||||
return { data: encryptedData, random: random }
|
||||
}
|
||||
// return { data: encryptedData, random: random }
|
||||
// }
|
||||
|
||||
export async function getPlayerPlaylists(animeid: number) {
|
||||
const token = await getPlayerEncryptedToken()
|
||||
// export async function getPlayerPlaylists(animeid: number) {
|
||||
// const token = await getPlayerEncryptedToken()
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://gw.api.animationdigitalnetwork.fr/player/video/${animeid}/link`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'x-target-distribution': 'de',
|
||||
'X-Player-Token': token.data
|
||||
}
|
||||
})
|
||||
// try {
|
||||
// const response = await fetch(`https://gw.api.animationdigitalnetwork.fr/player/video/${animeid}/link`, {
|
||||
// method: 'GET',
|
||||
// headers: {
|
||||
// 'x-target-distribution': 'de',
|
||||
// 'X-Player-Token': token.data
|
||||
// }
|
||||
// })
|
||||
|
||||
if (response.ok) {
|
||||
const data: {
|
||||
links: {
|
||||
subtitles: {
|
||||
all: string
|
||||
}
|
||||
// if (response.ok) {
|
||||
// const data: {
|
||||
// links: {
|
||||
// subtitles: {
|
||||
// all: string
|
||||
// }
|
||||
// }
|
||||
// } = await JSON.parse(await response.text())
|
||||
|
||||
// const subtitlelink = await fetch(data.links.subtitles.all, {
|
||||
// method: 'GET',
|
||||
// headers: {
|
||||
// 'x-target-distribution': 'de'
|
||||
// }
|
||||
// })
|
||||
|
||||
// const link: {
|
||||
// location: string
|
||||
// } = await JSON.parse(await subtitlelink.text())
|
||||
|
||||
// const subs = await parseSubs(link.location, token.random)
|
||||
|
||||
// return subs
|
||||
// } else {
|
||||
// const data: {
|
||||
// token: string
|
||||
// accessToken: string
|
||||
// refreshToken: string
|
||||
// } = JSON.parse(await response.text())
|
||||
|
||||
// return data
|
||||
// }
|
||||
// } catch (e) {
|
||||
// throw new Error(e as string)
|
||||
// }
|
||||
// }
|
||||
|
||||
// export async function parseSubs(url: string, grant: string) {
|
||||
// const response = await fetch(url)
|
||||
|
||||
// const data = await response.text()
|
||||
|
||||
// var key = grant + '7fac1178830cfe0c'
|
||||
|
||||
// console.log(key)
|
||||
|
||||
// var parsedSubtitle = CryptoJS.enc.Base64.parse(data.substring(0, 24))
|
||||
// var sec = CryptoJS.enc.Hex.parse(key)
|
||||
// var som = data.substring(24)
|
||||
|
||||
// try {
|
||||
// // Fuck You ADN
|
||||
// var decrypted: any = CryptoJS.AES.decrypt(som, sec, { iv: parsedSubtitle })
|
||||
// decrypted = decrypted.toString(CryptoJS.enc.Utf8)
|
||||
// return decrypted
|
||||
// } catch (error) {
|
||||
// console.error('Error decrypting subtitles:', error)
|
||||
// return null
|
||||
// }
|
||||
// }
|
||||
|
||||
export async function adnyLogin(user: string, passw: string) {
|
||||
const cachedData:
|
||||
| {
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
expires_in: number
|
||||
token_type: string
|
||||
scope: string
|
||||
country: string
|
||||
account_id: string
|
||||
profile_id: string
|
||||
}
|
||||
} = await JSON.parse(await response.text())
|
||||
|
||||
const subtitlelink = await fetch(data.links.subtitles.all, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'x-target-distribution': 'de'
|
||||
}
|
||||
})
|
||||
|
||||
const link: {
|
||||
location: string
|
||||
} = await JSON.parse(await subtitlelink.text())
|
||||
|
||||
const subs = await parseSubs(link.location, token.random)
|
||||
|
||||
return subs
|
||||
} else {
|
||||
const data: {
|
||||
token: string
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
} = JSON.parse(await response.text())
|
||||
|
||||
return data
|
||||
| undefined = server.CacheController.get('adntoken')
|
||||
|
||||
if (!cachedData) {
|
||||
var { data, error } = await adnLoginFetch(user, passw)
|
||||
|
||||
if (error) {
|
||||
messageBox(
|
||||
'error',
|
||||
['Cancel'],
|
||||
2,
|
||||
'Failed to login',
|
||||
'Failed to login to ADN',
|
||||
(error.error as string)
|
||||
)
|
||||
return { data: null, error: error.error }
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
messageBox('error', ['Cancel'], 2, 'Failed to login', 'Failed to login to ADN', 'ADN returned null')
|
||||
return { data: null, error: 'ADN returned null' }
|
||||
}
|
||||
|
||||
if (!data.access_token) {
|
||||
messageBox('error', ['Cancel'], 2, 'Failed to login', 'Failed to login to ADN', 'ADN returned malformed data')
|
||||
return { data: null, error: 'ADN returned malformed data' }
|
||||
}
|
||||
|
||||
server.CacheController.set('adntoken', data, data.expires_in - 30)
|
||||
|
||||
return { data: data, error: null }
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(e as string)
|
||||
|
||||
return { data: cachedData, error: null }
|
||||
}
|
||||
}
|
||||
|
||||
export async function parseSubs(url: string, grant: string) {
|
||||
const response = await fetch(url)
|
||||
|
||||
const data = await response.text()
|
||||
|
||||
var key = grant + '7fac1178830cfe0c'
|
||||
|
||||
console.log(key)
|
||||
|
||||
var parsedSubtitle = CryptoJS.enc.Base64.parse(data.substring(0, 24))
|
||||
var sec = CryptoJS.enc.Hex.parse(key)
|
||||
var som = data.substring(24)
|
||||
|
||||
try {
|
||||
// Fuck You ADN
|
||||
var decrypted: any = CryptoJS.AES.decrypt(som, sec, { iv: parsedSubtitle })
|
||||
decrypted = decrypted.toString(CryptoJS.enc.Utf8)
|
||||
return decrypted
|
||||
} catch (error) {
|
||||
console.error('Error decrypting subtitles:', error)
|
||||
return null
|
||||
|
||||
async function adnLoginFetch(user: string, passw: string) {
|
||||
const headers = {
|
||||
Authorization: 'Basic dC1rZGdwMmg4YzNqdWI4Zm4wZnE6eWZMRGZNZnJZdktYaDRKWFMxTEVJMmNDcXUxdjVXYW4=',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'User-Agent': 'Crunchyroll/3.46.2 Android/13 okhttp/4.12.0'
|
||||
}
|
||||
|
||||
const body: any = {
|
||||
username: user,
|
||||
password: passw,
|
||||
grant_type: 'password',
|
||||
scope: 'offline_access',
|
||||
device_name: 'RMX2170',
|
||||
device_type: 'realme RMX2170'
|
||||
}
|
||||
|
||||
const { data, error } = await useFetch<{
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
expires_in: number
|
||||
token_type: string
|
||||
scope: string
|
||||
country: string
|
||||
account_id: string
|
||||
profile_id: string
|
||||
}>('https://beta-api.crunchyroll.com/auth/v1/token', {
|
||||
type: 'POST',
|
||||
body: new URLSearchParams(body).toString(),
|
||||
header: headers,
|
||||
credentials: 'same-origin'
|
||||
})
|
||||
|
||||
if (error) {
|
||||
return { data: null, error: error }
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return { data: null, error: null }
|
||||
}
|
||||
|
||||
return { data: data, error: null }
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify'
|
||||
import { crunchyLogin, checkIfLoggedInCR, safeLoginData, addEpisodeToPlaylist, getPlaylist, getDownloading, deletePlaylist } from './crunchyroll.service'
|
||||
import { crunchyLogin } from './crunchyroll.service'
|
||||
import { dialog } from 'electron'
|
||||
import { messageBox } from '../../../electron/background'
|
||||
import { CrunchyEpisodes, CrunchySeason } from '../../types/crunchyroll'
|
||||
import { loggedInCheck } from '../service/service.service'
|
||||
|
||||
export async function loginController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const account = await checkIfLoggedInCR('crunchyroll')
|
||||
const account = await loggedInCheck('CR')
|
||||
|
||||
if (!account) {
|
||||
return reply.code(401).send({ message: 'Not Logged in' })
|
||||
@ -18,92 +19,4 @@ export async function loginController(request: FastifyRequest, reply: FastifyRep
|
||||
}
|
||||
|
||||
return reply.code(200).send(data)
|
||||
}
|
||||
|
||||
export async function checkLoginController(request: FastifyRequest, reply: FastifyReply) {
|
||||
const account = await checkIfLoggedInCR('crunchyroll')
|
||||
|
||||
if (!account) {
|
||||
return reply.code(401).send({ message: 'Not Logged in' })
|
||||
}
|
||||
|
||||
return reply.code(200).send({ message: 'Logged in' })
|
||||
}
|
||||
|
||||
export async function loginLoginController(
|
||||
request: FastifyRequest<{
|
||||
Body: {
|
||||
user: string
|
||||
password: string
|
||||
}
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
const body = request.body
|
||||
|
||||
const account = await checkIfLoggedInCR('crunchyroll')
|
||||
|
||||
if (account) {
|
||||
return reply.code(404).send({ message: 'Already Logged In' })
|
||||
}
|
||||
|
||||
const { data, error } = await crunchyLogin(body.user, body.password)
|
||||
|
||||
if (error || !data) {
|
||||
return reply.code(404).send({
|
||||
message: 'Invalid Email or Password'
|
||||
})
|
||||
}
|
||||
|
||||
await safeLoginData(body.user, body.password, 'crunchyroll')
|
||||
|
||||
return reply.code(200).send()
|
||||
}
|
||||
|
||||
export async function addPlaylistController(
|
||||
request: FastifyRequest<{
|
||||
Body: {
|
||||
episodes: CrunchyEpisodes
|
||||
dubs: Array<string>
|
||||
subs: Array<string>
|
||||
dir: string
|
||||
hardsub: boolean
|
||||
quality: 1080 | 720 | 480 | 360 | 240
|
||||
}
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
const body = request.body
|
||||
|
||||
for (const e of body.episodes) {
|
||||
await addEpisodeToPlaylist(e, body.subs, body.dubs, body.dir, body.hardsub, 'waiting', body.quality)
|
||||
}
|
||||
|
||||
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) {
|
||||
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())
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
import { addPlaylistController, checkLoginController, deleteCompletePlaylistController, getPlaylistController, loginController, loginLoginController } from './crunchyroll.controller'
|
||||
import { loginController } from './crunchyroll.controller'
|
||||
|
||||
async function crunchyrollRoutes(server: FastifyInstance) {
|
||||
server.post(
|
||||
@ -15,77 +15,6 @@ async function crunchyrollRoutes(server: FastifyInstance) {
|
||||
}
|
||||
},
|
||||
loginController
|
||||
),
|
||||
server.post(
|
||||
'/login/login',
|
||||
{
|
||||
schema: {
|
||||
response: {
|
||||
'4xx': {
|
||||
error: { type: 'string' },
|
||||
message: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
loginLoginController
|
||||
),
|
||||
server.get(
|
||||
'/check',
|
||||
{
|
||||
schema: {
|
||||
response: {
|
||||
'4xx': {
|
||||
error: { type: 'string' },
|
||||
message: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
checkLoginController
|
||||
),
|
||||
server.post(
|
||||
'/playlist',
|
||||
{
|
||||
schema: {
|
||||
response: {
|
||||
'4xx': {
|
||||
error: { type: 'string' },
|
||||
message: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
addPlaylistController
|
||||
)
|
||||
server.get(
|
||||
'/playlist',
|
||||
{
|
||||
schema: {
|
||||
response: {
|
||||
'4xx': {
|
||||
error: { type: 'string' },
|
||||
message: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
getPlaylistController
|
||||
)
|
||||
|
||||
server.delete(
|
||||
'/playlist',
|
||||
{
|
||||
schema: {
|
||||
response: {
|
||||
'4xx': {
|
||||
error: { type: 'string' },
|
||||
message: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteCompletePlaylistController
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,20 +1,9 @@
|
||||
import { messageBox } from '../../../electron/background'
|
||||
import { server } from '../../api'
|
||||
import { Account, Playlist } from '../../db/database'
|
||||
import { CrunchyEpisode, VideoPlaylist } from '../../types/crunchyroll'
|
||||
import { VideoPlaylist } from '../../types/crunchyroll'
|
||||
import { useFetch } from '../useFetch'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import Ffmpeg from 'fluent-ffmpeg'
|
||||
import { parse as mpdParse } from 'mpd-parser'
|
||||
import { parse, stringify } from 'ass-compiler'
|
||||
import { Readable } from 'stream'
|
||||
import { finished } from 'stream/promises'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { app } from 'electron'
|
||||
var cron = require('node-cron')
|
||||
const ffmpegPath = require('ffmpeg-static').replace('app.asar', 'app.asar.unpacked')
|
||||
const ffprobePath = require('ffprobe-static').path.replace('app.asar', 'app.asar.unpacked')
|
||||
import { loggedInCheck } from '../service/service.service'
|
||||
|
||||
const crErrors = [
|
||||
{
|
||||
@ -113,103 +102,8 @@ async function crunchyLoginFetch(user: string, passw: string) {
|
||||
return { data: data, error: null }
|
||||
}
|
||||
|
||||
export async function checkIfLoggedInCR(service: string) {
|
||||
const login = await Account.findOne({
|
||||
where: {
|
||||
service: service
|
||||
}
|
||||
})
|
||||
|
||||
return login?.get()
|
||||
}
|
||||
|
||||
export async function safeLoginData(user: string, password: string, service: string) {
|
||||
const login = await Account.create({
|
||||
username: user,
|
||||
password: password,
|
||||
service: service
|
||||
})
|
||||
|
||||
return login?.get()
|
||||
}
|
||||
|
||||
export async function addEpisodeToPlaylist(
|
||||
e: CrunchyEpisode,
|
||||
s: Array<string>,
|
||||
d: Array<string>,
|
||||
dir: string,
|
||||
hardsub: boolean,
|
||||
status: 'waiting' | 'preparing' | 'downloading' | 'merging' | 'completed' | 'failed',
|
||||
quality: 1080 | 720 | 480 | 360 | 240
|
||||
) {
|
||||
const episode = await Playlist.create({
|
||||
media: e,
|
||||
sub: s,
|
||||
dub: d,
|
||||
dir: dir,
|
||||
hardsub: hardsub,
|
||||
status: status,
|
||||
quality: quality
|
||||
})
|
||||
|
||||
return episode.get()
|
||||
}
|
||||
|
||||
export async function getPlaylist() {
|
||||
const episodes = await Playlist.findAll()
|
||||
|
||||
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 } })
|
||||
}
|
||||
|
||||
var isDownloading: number = 0
|
||||
|
||||
async function checkPlaylists() {
|
||||
const eps = await Playlist.findAll({ where: { status: 'waiting' } })
|
||||
|
||||
for (const e of eps) {
|
||||
if (isDownloading < 3 && 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,
|
||||
e.dataValues.quality,
|
||||
e.dataValues.dir
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cron.schedule('*/2 * * * * *', () => {
|
||||
checkPlaylists()
|
||||
})
|
||||
|
||||
export async function crunchyGetPlaylist(q: string) {
|
||||
const account = await checkIfLoggedInCR('crunchyroll')
|
||||
const account = await loggedInCheck('CR')
|
||||
|
||||
if (!account) return
|
||||
|
||||
@ -252,59 +146,8 @@ export async function crunchyGetPlaylist(q: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function createFolder() {
|
||||
const tempFolderPath = path.join(app.getPath('documents'), (Math.random() + 1).toString(36).substring(2))
|
||||
try {
|
||||
await fs.promises.mkdir(tempFolderPath, { recursive: true })
|
||||
return tempFolderPath
|
||||
} catch (error) {
|
||||
console.error('Error creating temporary folder:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function checkDirectoryExistence(dir: string) {
|
||||
try {
|
||||
await fs.promises.access(dir)
|
||||
console.log(`Directory ${dir} exists.`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.log(`Directory ${dir} does not exist.`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function createFolderName(name: string, dir: string) {
|
||||
var folderPath
|
||||
|
||||
const dirExists = await checkDirectoryExistence(dir)
|
||||
|
||||
if (dirExists) {
|
||||
folderPath = path.join(dir, name)
|
||||
} else {
|
||||
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 })
|
||||
}
|
||||
|
||||
export async function crunchyGetPlaylistMPD(q: string) {
|
||||
const account = await checkIfLoggedInCR('crunchyroll')
|
||||
const account = await loggedInCheck('CR')
|
||||
|
||||
if (!account) return
|
||||
|
||||
@ -334,613 +177,3 @@ export async function crunchyGetPlaylistMPD(q: string) {
|
||||
throw new Error(e as string)
|
||||
}
|
||||
}
|
||||
|
||||
var downloading: Array<{
|
||||
id: number
|
||||
downloadedParts: number
|
||||
partsToDownload: number
|
||||
downloadSpeed: number
|
||||
}> = []
|
||||
|
||||
export async function downloadPlaylist(
|
||||
e: string,
|
||||
dubs: Array<string>,
|
||||
subs: Array<string>,
|
||||
hardsub: boolean,
|
||||
downloadID: number,
|
||||
name: string,
|
||||
season: number,
|
||||
episode: number,
|
||||
quality: 1080 | 720 | 480 | 360 | 240,
|
||||
downloadPath: string
|
||||
) {
|
||||
downloading.push({
|
||||
id: downloadID,
|
||||
downloadedParts: 0,
|
||||
partsToDownload: 0,
|
||||
downloadSpeed: 0
|
||||
})
|
||||
|
||||
await updatePlaylistByID(downloadID, 'downloading')
|
||||
|
||||
var playlist = await crunchyGetPlaylist(e)
|
||||
|
||||
console.log(playlist)
|
||||
|
||||
if (!playlist) {
|
||||
console.log('Playlist not found')
|
||||
return
|
||||
}
|
||||
|
||||
if (playlist.versions && playlist.versions.length !== 0) {
|
||||
if (playlist.audioLocale !== subs[0]) {
|
||||
const found = playlist.versions.find((v) => v.audio_locale === 'ja-JP')
|
||||
if (found) {
|
||||
playlist = await crunchyGetPlaylist(found.guid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!playlist) {
|
||||
console.log('Exact Playlist not found')
|
||||
return
|
||||
}
|
||||
|
||||
const subFolder = await createFolder()
|
||||
|
||||
const audioFolder = await createFolder()
|
||||
|
||||
const videoFolder = await createFolder()
|
||||
|
||||
const seasonFolder = await createFolderName(`${name.replace(/[/\\?%*:|"<>]/g, '')} Season ${season}`, downloadPath)
|
||||
|
||||
const dubDownloadList: Array<{
|
||||
audio_locale: string
|
||||
guid: string
|
||||
is_premium_only: boolean
|
||||
media_guid: string
|
||||
original: boolean
|
||||
season_guid: string
|
||||
variant: string
|
||||
}> = []
|
||||
|
||||
const subDownloadList: Array<{
|
||||
format: string
|
||||
language: string
|
||||
url: string
|
||||
isDub: boolean
|
||||
}> = []
|
||||
|
||||
for (const s of subs) {
|
||||
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) {
|
||||
subDownloadList.push({ ...found, isDub: false })
|
||||
console.log(`Subtitle ${s}.ass found, adding to download`)
|
||||
} else {
|
||||
console.warn(`Subtitle ${s}.ass not found, skipping`)
|
||||
}
|
||||
}
|
||||
|
||||
for (const d of dubs) {
|
||||
var found
|
||||
if (playlist.versions) {
|
||||
found = playlist.versions.find((p) => p.audio_locale === d)
|
||||
}
|
||||
|
||||
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)
|
||||
console.log(`Audio ${d}.aac found, adding to download`)
|
||||
} else if (playlist.versions.length === 0) {
|
||||
const foundSub = playlist.subtitles.find((sub) => sub.language === d)
|
||||
if (foundSub) {
|
||||
subDownloadList.push({ ...foundSub, isDub: true })
|
||||
} else {
|
||||
console.log(`No Dub Sub Found for ${d}`)
|
||||
}
|
||||
dubDownloadList.push({
|
||||
audio_locale: 'ja-JP',
|
||||
guid: e,
|
||||
is_premium_only: true,
|
||||
media_guid: 'adas',
|
||||
original: false,
|
||||
season_guid: 'asdasd',
|
||||
variant: 'asd'
|
||||
})
|
||||
} else {
|
||||
console.warn(`Audio ${d}.aac not found, skipping`)
|
||||
}
|
||||
}
|
||||
|
||||
if (dubDownloadList.length === 0) {
|
||||
const jpVersion = playlist.versions.find((v) => v.audio_locale === 'ja-JP')
|
||||
|
||||
if (jpVersion) {
|
||||
console.log('Using ja-JP Audio because no Audio in download list')
|
||||
dubDownloadList.push(jpVersion)
|
||||
}
|
||||
}
|
||||
|
||||
const subDownload = async () => {
|
||||
const sbs: Array<string> = []
|
||||
for (const sub of subDownloadList) {
|
||||
const name = await downloadSub(sub, subFolder)
|
||||
sbs.push(name)
|
||||
}
|
||||
return sbs
|
||||
}
|
||||
|
||||
const audioDownload = async () => {
|
||||
const audios: Array<string> = []
|
||||
for (const v of dubDownloadList) {
|
||||
const list = await crunchyGetPlaylist(v.guid)
|
||||
|
||||
if (!list) return
|
||||
|
||||
const playlist = await crunchyGetPlaylistMPD(list.url)
|
||||
|
||||
if (!playlist) return
|
||||
|
||||
var p: { filename: string; url: string }[] = []
|
||||
|
||||
p.push({
|
||||
filename: (playlist.mediaGroups.AUDIO.audio.main.playlists[0].segments[0].map.uri.match(/([^\/]+)\?/) as RegExpMatchArray)[1],
|
||||
url: playlist.mediaGroups.AUDIO.audio.main.playlists[0].segments[0].map.resolvedUri
|
||||
})
|
||||
|
||||
for (const s of playlist.mediaGroups.AUDIO.audio.main.playlists[0].segments) {
|
||||
p.push({
|
||||
filename: (s.uri.match(/([^\/]+)\?/) as RegExpMatchArray)[1],
|
||||
url: s.resolvedUri
|
||||
})
|
||||
}
|
||||
|
||||
const path = await downloadAudio(p, audioFolder, list.audioLocale)
|
||||
|
||||
audios.push(path as string)
|
||||
}
|
||||
return audios
|
||||
}
|
||||
|
||||
const downloadVideo = async () => {
|
||||
var code
|
||||
|
||||
if (!playlist) return
|
||||
|
||||
if (playlist.versions && playlist.versions.length !== 0) {
|
||||
if (playlist.versions.find((p) => p.audio_locale === dubs[0])) {
|
||||
code = playlist.versions.find((p) => p.audio_locale === dubs[0])?.guid
|
||||
} else {
|
||||
code = playlist.versions.find((p) => p.audio_locale === 'ja-JP')?.guid
|
||||
}
|
||||
} else {
|
||||
code = e
|
||||
}
|
||||
|
||||
if (!code) return console.error('No clean stream found')
|
||||
|
||||
const play = await crunchyGetPlaylist(code)
|
||||
|
||||
if (!play) return
|
||||
|
||||
var downloadURL
|
||||
|
||||
if (hardsub) {
|
||||
const hardsubURL = play.hardSubs.find((h) => h.hlang === subs[0])?.url
|
||||
|
||||
if (hardsubURL) {
|
||||
downloadURL = hardsubURL
|
||||
console.log('Hardsub Playlist found')
|
||||
} else {
|
||||
downloadURL = play.url
|
||||
console.log('Hardsub Playlist not found')
|
||||
}
|
||||
} else {
|
||||
downloadURL = play.url
|
||||
console.log('Hardsub disabled, skipping')
|
||||
}
|
||||
|
||||
var mdp = await crunchyGetPlaylistMPD(downloadURL)
|
||||
|
||||
if (!mdp) return
|
||||
|
||||
var hq = mdp.playlists.find((i) => i.attributes.RESOLUTION?.height === quality)
|
||||
|
||||
if (!hq) return
|
||||
|
||||
var p: { filename: string; url: string }[] = []
|
||||
|
||||
p.push({
|
||||
filename: (hq.segments[0].map.uri.match(/([^\/]+)\?/) as RegExpMatchArray)[1],
|
||||
url: hq.segments[0].map.resolvedUri
|
||||
})
|
||||
|
||||
for (const s of hq.segments) {
|
||||
p.push({
|
||||
filename: (s.uri.match(/([^\/]+)\?/) as RegExpMatchArray)[1],
|
||||
url: s.resolvedUri
|
||||
})
|
||||
}
|
||||
|
||||
// await updatePlaylistToDownloadPartsByID(downloadID, p.length)
|
||||
|
||||
const dn = downloading.find((i) => i.id === downloadID)
|
||||
|
||||
if (dn) {
|
||||
dn.partsToDownload = p.length
|
||||
}
|
||||
|
||||
const file = await downloadParts(p, downloadID, videoFolder)
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
const [subss, audios, file] = await Promise.all([subDownload(), audioDownload(), downloadVideo()])
|
||||
|
||||
if (!audios) return
|
||||
|
||||
await mergeFile(file as string, audios, subss, String(playlist.assetId), seasonFolder, `${name.replace(/[/\\?%*:|"<>]/g, '')} Season ${season} Episode ${episode}`)
|
||||
|
||||
await updatePlaylistByID(downloadID, 'completed')
|
||||
|
||||
await deleteFolder(subFolder)
|
||||
await deleteFolder(audioFolder)
|
||||
await deleteFolder(videoFolder)
|
||||
|
||||
return playlist
|
||||
}
|
||||
|
||||
async function downloadAudio(parts: { filename: string; url: string }[], dir: string, name: string) {
|
||||
const path = await createFolder()
|
||||
const downloadPromises = []
|
||||
|
||||
for (const [index, part] of parts.entries()) {
|
||||
const stream = fs.createWriteStream(`${path}/${part.filename}`)
|
||||
const downloadPromise = fetchAndPipe(part.url, stream, index + 1)
|
||||
downloadPromises.push(downloadPromise)
|
||||
}
|
||||
|
||||
await Promise.all(downloadPromises)
|
||||
|
||||
return await mergePartsAudio(parts, path, dir, name)
|
||||
}
|
||||
|
||||
async function fetchAndPipe(url: string, stream: fs.WriteStream, index: number) {
|
||||
const { body } = await fetch(url)
|
||||
const readableStream = Readable.from(body as any)
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
readableStream
|
||||
.pipe(stream)
|
||||
.on('finish', () => {
|
||||
console.log(`Fragment ${index} downloaded`)
|
||||
resolve()
|
||||
})
|
||||
.on('error', (error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
while (!success) {
|
||||
try {
|
||||
const stream = fs.createWriteStream(`${path}/${part.filename}`)
|
||||
const { body } = await fetch(part.url)
|
||||
|
||||
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`)
|
||||
|
||||
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)
|
||||
console.log(`Retrying download of fragment ${index + 1}...`)
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await mergeParts(parts, downloadID, path, dir)
|
||||
}
|
||||
|
||||
async function downloadSub(
|
||||
sub: {
|
||||
format: string
|
||||
language: string
|
||||
url: string
|
||||
isDub: boolean
|
||||
},
|
||||
dir: string
|
||||
) {
|
||||
const path = `${dir}/${sub.language}${sub.isDub ? `-FORCED` : ''}.${sub.format}`
|
||||
|
||||
const stream = fs.createWriteStream(path)
|
||||
const response = await fetch(sub.url)
|
||||
|
||||
var parsedASS = parse(await response.text())
|
||||
|
||||
// Disabling Changing ASS because still broken in vlc
|
||||
|
||||
// parsedASS.info.PlayResX = "1920";
|
||||
// parsedASS.info.PlayResY = "1080";
|
||||
|
||||
// for (const s of parsedASS.styles.style) {
|
||||
// (s.Fontsize = "54"), (s.Outline = "4");
|
||||
// }
|
||||
|
||||
const fixed = stringify(parsedASS)
|
||||
|
||||
const readableStream = Readable.from([fixed])
|
||||
|
||||
await finished(readableStream.pipe(stream))
|
||||
console.log(`Sub ${sub.language}.${sub.format} downloaded`)
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
async function concatenateTSFiles(inputFiles: Array<string>, outputFile: string) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const writeStream = fs.createWriteStream(outputFile)
|
||||
|
||||
writeStream.on('error', (error) => {
|
||||
reject(error)
|
||||
})
|
||||
|
||||
writeStream.on('finish', () => {
|
||||
console.log('TS files concatenated successfully!')
|
||||
resolve()
|
||||
})
|
||||
|
||||
const processNextFile = (index: number) => {
|
||||
if (index >= inputFiles.length) {
|
||||
writeStream.end()
|
||||
return
|
||||
}
|
||||
|
||||
const readStream = fs.createReadStream(inputFiles[index])
|
||||
|
||||
readStream.on('error', (error) => {
|
||||
reject(error)
|
||||
})
|
||||
|
||||
readStream.pipe(writeStream, { end: false })
|
||||
|
||||
readStream.on('end', () => {
|
||||
processNextFile(index + 1)
|
||||
})
|
||||
}
|
||||
|
||||
processNextFile(0)
|
||||
})
|
||||
}
|
||||
|
||||
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}`)
|
||||
}
|
||||
|
||||
const concatenatedFile = `${tmp}/main.m4s`
|
||||
await concatenateTSFiles(list, concatenatedFile)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
Ffmpeg()
|
||||
.setFfmpegPath(ffmpegPath)
|
||||
.setFfprobePath(ffprobePath)
|
||||
.input(concatenatedFile)
|
||||
.outputOptions('-c copy')
|
||||
.save(dir + `/${tempname}.mp4`)
|
||||
.on('end', async () => {
|
||||
console.log('Merging finished')
|
||||
await deleteFolder(tmp)
|
||||
return resolve(dir + `/${tempname}.mp4`)
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error merging parts:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function mergePartsAudio(parts: { filename: string; url: string }[], tmp: string, dir: string, name: string) {
|
||||
try {
|
||||
const list: Array<string> = []
|
||||
|
||||
for (const [index, part] of parts.entries()) {
|
||||
list.push(`${tmp}/${part.filename}`)
|
||||
}
|
||||
|
||||
const concatenatedFile = `${tmp}/main.m4s`
|
||||
await concatenateTSFiles(list, concatenatedFile)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
Ffmpeg()
|
||||
.setFfmpegPath(ffmpegPath)
|
||||
.setFfprobePath(ffprobePath)
|
||||
.input(concatenatedFile)
|
||||
.outputOptions('-c copy')
|
||||
.save(`${dir}/${name}.aac`)
|
||||
.on('end', async () => {
|
||||
console.log('Merging finished')
|
||||
await deleteFolder(tmp)
|
||||
|
||||
return resolve(`${dir}/${name}.aac`)
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error merging parts:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function mergeFile(video: string, audios: Array<string>, subs: Array<string>, name: string, path: string, filename: string) {
|
||||
const locales: Array<{
|
||||
locale: string
|
||||
name: string
|
||||
iso: string
|
||||
title: string
|
||||
}> = [
|
||||
{ locale: 'ja-JP', name: 'JP', iso: 'jpn', title: 'Japanese' },
|
||||
{ locale: 'de-DE', name: 'DE', iso: 'deu', title: 'German' },
|
||||
{ locale: 'hi-IN', name: 'HI', iso: 'hin', title: 'Hindi' },
|
||||
{ locale: 'ru-RU', name: 'RU', iso: 'rus', title: 'Russian' },
|
||||
{ locale: 'en-US', name: 'EN', iso: 'eng', title: 'English' },
|
||||
{ locale: 'fr-FR', name: 'FR', iso: 'fra', title: 'French' },
|
||||
{ locale: 'pt-BR', name: 'PT', iso: 'por', title: 'Portugese' },
|
||||
{ locale: 'es-419', name: 'LA-ES', iso: 'spa', title: 'SpanishLatin' },
|
||||
{ locale: 'en-IN', name: 'EN-IN', iso: 'eng', title: 'IndianEnglish' },
|
||||
{ locale: 'it-IT', name: 'IT', iso: 'ita', title: 'Italian' },
|
||||
{ locale: 'es-ES', name: 'ES', iso: 'spa', title: 'Spanish' },
|
||||
{ locale: 'ta-IN', name: 'TA', iso: 'tam', title: 'Tamil' },
|
||||
{ locale: 'te-IN', name: 'TE', iso: 'tel', title: 'Telugu' },
|
||||
{ locale: 'ar-SA', name: 'AR', iso: 'ara', title: 'ArabicSA' },
|
||||
{ locale: 'ms-MY', name: 'MS', iso: 'msa', title: 'Malay' },
|
||||
{ locale: 'th-TH', name: 'TH', iso: 'tha', title: 'Thai' },
|
||||
{ locale: 'vi-VN', name: 'VI', iso: 'vie', title: 'Vietnamese' },
|
||||
{ locale: 'id-ID', name: 'ID', iso: 'ind', title: 'Indonesian' },
|
||||
{ locale: 'ko-KR', name: 'KO', iso: 'kor', title: 'Korean' }
|
||||
]
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
var output = Ffmpeg().setFfmpegPath(ffmpegPath).setFfprobePath(ffprobePath)
|
||||
var ffindex = 1
|
||||
output.addInput(video)
|
||||
var options = ['-map_metadata -1', '-c copy', '-metadata:s:v:0 VARIANT_BITRATE=0', '-map 0']
|
||||
|
||||
for (const [index, a] of audios.entries()) {
|
||||
output.addInput(a)
|
||||
options.push(`-map ${ffindex}:a:0`)
|
||||
options.push(
|
||||
`-metadata:s:a:${index} language=${
|
||||
locales.find((l) => l.locale === a.split('/')[1].split('.aac')[0])
|
||||
? locales.find((l) => l.locale === a.split('/')[1].split('.aac')[0])?.iso
|
||||
: a.split('/')[1].split('.aac')[0]
|
||||
}`
|
||||
)
|
||||
|
||||
ffindex++
|
||||
|
||||
options.push(
|
||||
`-metadata:s:a:${index} title=${
|
||||
locales.find((l) => l.locale === 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) {
|
||||
for (const [index, s] of subs.entries()) {
|
||||
output.addInput(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(
|
||||
`-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])?.iso
|
||||
: 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++
|
||||
}
|
||||
options.push(`-disposition:s:0 default`)
|
||||
}
|
||||
|
||||
output
|
||||
.addOptions(options)
|
||||
.saveToFile(path + `/${filename}.mkv`)
|
||||
.on('error', (error) => {
|
||||
console.log(error)
|
||||
reject(error)
|
||||
})
|
||||
.on('end', async () => {
|
||||
console.log('Download finished')
|
||||
return resolve('combined')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
109
src/api/routes/service/service.controller.ts
Normal file
109
src/api/routes/service/service.controller.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import { FastifyReply, FastifyRequest } from "fastify"
|
||||
import { crunchyLogin } from "../crunchyroll/crunchyroll.service"
|
||||
import { addEpisodeToPlaylist, getDownloading, getPlaylist, loggedInCheck, safeLoginData } from "./service.service"
|
||||
import { CrunchyEpisodes } from "../../types/crunchyroll"
|
||||
|
||||
export async function checkLoginController(request: FastifyRequest<{
|
||||
Params: {
|
||||
id: string
|
||||
}
|
||||
}>, reply: FastifyReply) {
|
||||
const account = await loggedInCheck(request.params.id)
|
||||
|
||||
if (!account) {
|
||||
return reply.code(401).send({ message: 'Not Logged in' })
|
||||
}
|
||||
|
||||
return reply.code(200).send({ message: 'Logged in' })
|
||||
}
|
||||
|
||||
export async function loginController(
|
||||
request: FastifyRequest<{
|
||||
Body: {
|
||||
user: string
|
||||
password: string
|
||||
},
|
||||
Params: {
|
||||
id: string
|
||||
}
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
const body = request.body
|
||||
const params = request.params
|
||||
|
||||
const account = await loggedInCheck(params.id)
|
||||
|
||||
if (account) {
|
||||
return reply.code(404).send({ message: 'Already Logged In' })
|
||||
}
|
||||
|
||||
var responseData
|
||||
var responseError
|
||||
|
||||
if (params.id === 'CR') {
|
||||
const { data, error } = await crunchyLogin(body.user, body.password)
|
||||
responseError = error,
|
||||
responseData = data
|
||||
}
|
||||
|
||||
if (params.id === 'ADN') {
|
||||
// const { data, error } = await adnLogin(body.user, body.password)
|
||||
// responseError = error,
|
||||
// responseData = data
|
||||
}
|
||||
|
||||
if (responseError || !responseData) {
|
||||
return reply.code(404).send({
|
||||
message: 'Invalid Email or Password'
|
||||
})
|
||||
}
|
||||
|
||||
await safeLoginData(body.user, body.password, params.id)
|
||||
|
||||
return reply.code(200).send()
|
||||
}
|
||||
|
||||
export async function addPlaylistController(
|
||||
request: FastifyRequest<{
|
||||
Body: {
|
||||
episodes: CrunchyEpisodes
|
||||
dubs: Array<string>
|
||||
subs: Array<string>
|
||||
dir: string
|
||||
hardsub: boolean
|
||||
quality: 1080 | 720 | 480 | 360 | 240
|
||||
service: 'CR' | 'ADN'
|
||||
format: 'mp4' | 'mkv'
|
||||
}
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
const body = request.body
|
||||
|
||||
for (const e of body.episodes) {
|
||||
await addEpisodeToPlaylist(e, body.subs, body.dubs, body.dir, body.hardsub, 'waiting', body.quality, body.service, body.format)
|
||||
}
|
||||
|
||||
return reply.code(201).send()
|
||||
}
|
||||
|
||||
export async function getPlaylistController(request: FastifyRequest, reply: FastifyReply) {
|
||||
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())
|
||||
}
|
63
src/api/routes/service/service.route.ts
Normal file
63
src/api/routes/service/service.route.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { FastifyInstance } from "fastify"
|
||||
import { addPlaylistController, checkLoginController, getPlaylistController, loginController } from "./service.controller"
|
||||
|
||||
async function serviceRoutes(server: FastifyInstance) {
|
||||
server.post(
|
||||
'/login/:id',
|
||||
{
|
||||
schema: {
|
||||
response: {
|
||||
'4xx': {
|
||||
error: { type: 'string' },
|
||||
message: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
loginController
|
||||
),
|
||||
server.get(
|
||||
'/check/:id',
|
||||
{
|
||||
schema: {
|
||||
response: {
|
||||
'4xx': {
|
||||
error: { type: 'string' },
|
||||
message: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
checkLoginController
|
||||
),
|
||||
server.post(
|
||||
'/playlist',
|
||||
{
|
||||
schema: {
|
||||
response: {
|
||||
'4xx': {
|
||||
error: { type: 'string' },
|
||||
message: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
addPlaylistController
|
||||
)
|
||||
server.get(
|
||||
'/playlist',
|
||||
{
|
||||
schema: {
|
||||
response: {
|
||||
'4xx': {
|
||||
error: { type: 'string' },
|
||||
message: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
getPlaylistController
|
||||
)
|
||||
}
|
||||
|
||||
export default serviceRoutes
|
599
src/api/routes/service/service.service.ts
Normal file
599
src/api/routes/service/service.service.ts
Normal file
@ -0,0 +1,599 @@
|
||||
import { Account, Playlist } from '../../db/database'
|
||||
import { downloadMPDAudio } from '../../services/audio'
|
||||
import { concatenateTSFiles } from '../../services/concatenate'
|
||||
import { createFolder, createFolderName, deleteFolder } from '../../services/folder'
|
||||
import { downloadCRSub } from '../../services/subs'
|
||||
import { CrunchyEpisode } from '../../types/crunchyroll'
|
||||
import { crunchyGetPlaylist, crunchyGetPlaylistMPD } from '../crunchyroll/crunchyroll.service'
|
||||
import fs from 'fs'
|
||||
var cron = require('node-cron')
|
||||
import { Readable } from 'stream'
|
||||
import { finished } from 'stream/promises'
|
||||
import Ffmpeg from 'fluent-ffmpeg'
|
||||
const ffmpegPath = require('ffmpeg-static').replace('app.asar', 'app.asar.unpacked')
|
||||
const ffprobePath = require('ffprobe-static').path.replace('app.asar', 'app.asar.unpacked')
|
||||
|
||||
// DB Account existence check
|
||||
export async function loggedInCheck(service: string) {
|
||||
const login = await Account.findOne({
|
||||
where: {
|
||||
service: service
|
||||
}
|
||||
})
|
||||
|
||||
return login?.get()
|
||||
}
|
||||
|
||||
// Save Login Data in DB
|
||||
export async function safeLoginData(user: string, password: string, service: string) {
|
||||
const login = await Account.create({
|
||||
username: user,
|
||||
password: password,
|
||||
service: service
|
||||
})
|
||||
|
||||
return login?.get()
|
||||
}
|
||||
|
||||
// Get Playlist
|
||||
export async function getPlaylist() {
|
||||
const episodes = await Playlist.findAll()
|
||||
|
||||
return episodes
|
||||
}
|
||||
|
||||
// Update Playlist Item
|
||||
export async function updatePlaylistByID(id: number, status: 'waiting' | 'preparing' | 'downloading' | 'completed' | 'merging' | 'failed') {
|
||||
await Playlist.update({ status: status }, { where: { id: id } })
|
||||
}
|
||||
|
||||
// Add Episode to Playlist
|
||||
export async function addEpisodeToPlaylist(
|
||||
e: CrunchyEpisode,
|
||||
s: Array<string>,
|
||||
d: Array<string>,
|
||||
dir: string,
|
||||
hardsub: boolean,
|
||||
status: 'waiting' | 'preparing' | 'downloading' | 'merging' | 'completed' | 'failed',
|
||||
quality: 1080 | 720 | 480 | 360 | 240,
|
||||
service: 'CR' | 'ADN',
|
||||
format: 'mp4' | 'mkv'
|
||||
) {
|
||||
const episode = await Playlist.create({
|
||||
media: e,
|
||||
sub: s,
|
||||
dub: d,
|
||||
dir: dir,
|
||||
hardsub: hardsub,
|
||||
status: status,
|
||||
quality: quality,
|
||||
service: service,
|
||||
format: format
|
||||
})
|
||||
|
||||
return episode.get()
|
||||
}
|
||||
|
||||
// Define Downloading Array
|
||||
var downloading: Array<{
|
||||
id: number
|
||||
downloadedParts: number
|
||||
partsToDownload: number
|
||||
downloadSpeed: number
|
||||
}> = []
|
||||
|
||||
// Get Downloading Episodes
|
||||
export async function getDownloading(id: number) {
|
||||
const found = downloading.find((i) => i.id === id)
|
||||
|
||||
if (found) return found
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Define IsDownloading Count
|
||||
var isDownloading: number = 0
|
||||
|
||||
// Check Playlist every 2 seconds for new items
|
||||
async function checkPlaylists() {
|
||||
const eps = await Playlist.findAll({ where: { status: 'waiting' } })
|
||||
|
||||
for (const e of eps) {
|
||||
if (isDownloading < 3 && e.dataValues.status === 'waiting') {
|
||||
updatePlaylistByID(e.dataValues.id, 'preparing')
|
||||
isDownloading++
|
||||
if (e.dataValues.service === 'CR') {
|
||||
downloadCrunchyrollPlaylist(
|
||||
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,
|
||||
e.dataValues.quality,
|
||||
e.dataValues.dir,
|
||||
e.dataValues.format
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cron.schedule('*/2 * * * * *', () => {
|
||||
checkPlaylists()
|
||||
})
|
||||
|
||||
// Download Crunchyroll Playlist
|
||||
export async function downloadCrunchyrollPlaylist(
|
||||
e: string,
|
||||
dubs: Array<string>,
|
||||
subs: Array<string>,
|
||||
hardsub: boolean,
|
||||
downloadID: number,
|
||||
name: string,
|
||||
season: number,
|
||||
episode: number,
|
||||
quality: 1080 | 720 | 480 | 360 | 240,
|
||||
downloadPath: string,
|
||||
format: 'mp4' | 'mkv'
|
||||
) {
|
||||
downloading.push({
|
||||
id: downloadID,
|
||||
downloadedParts: 0,
|
||||
partsToDownload: 0,
|
||||
downloadSpeed: 0
|
||||
})
|
||||
|
||||
await updatePlaylistByID(downloadID, 'downloading')
|
||||
|
||||
var playlist = await crunchyGetPlaylist(e)
|
||||
|
||||
console.log(playlist)
|
||||
|
||||
if (!playlist) {
|
||||
console.log('Playlist not found')
|
||||
return
|
||||
}
|
||||
|
||||
if (playlist.versions && playlist.versions.length !== 0) {
|
||||
if (playlist.audioLocale !== subs[0]) {
|
||||
const found = playlist.versions.find((v) => v.audio_locale === 'ja-JP')
|
||||
if (found) {
|
||||
playlist = await crunchyGetPlaylist(found.guid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!playlist) {
|
||||
console.log('Exact Playlist not found')
|
||||
return
|
||||
}
|
||||
|
||||
const subFolder = await createFolder()
|
||||
|
||||
const audioFolder = await createFolder()
|
||||
|
||||
const videoFolder = await createFolder()
|
||||
|
||||
const seasonFolder = await createFolderName(`${name.replace(/[/\\?%*:|"<>]/g, '')} Season ${season}`, downloadPath)
|
||||
|
||||
const dubDownloadList: Array<{
|
||||
audio_locale: string
|
||||
guid: string
|
||||
is_premium_only: boolean
|
||||
media_guid: string
|
||||
original: boolean
|
||||
season_guid: string
|
||||
variant: string
|
||||
}> = []
|
||||
|
||||
const subDownloadList: Array<{
|
||||
format: string
|
||||
language: string
|
||||
url: string
|
||||
isDub: boolean
|
||||
}> = []
|
||||
|
||||
for (const s of subs) {
|
||||
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) {
|
||||
subDownloadList.push({ ...found, isDub: false })
|
||||
console.log(`Subtitle ${s}.ass found, adding to download`)
|
||||
} else {
|
||||
console.warn(`Subtitle ${s}.ass not found, skipping`)
|
||||
}
|
||||
}
|
||||
|
||||
for (const d of dubs) {
|
||||
var found
|
||||
if (playlist.versions) {
|
||||
found = playlist.versions.find((p) => p.audio_locale === d)
|
||||
}
|
||||
|
||||
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)
|
||||
console.log(`Audio ${d}.aac found, adding to download`)
|
||||
} else if (playlist.versions.length === 0) {
|
||||
const foundSub = playlist.subtitles.find((sub) => sub.language === d)
|
||||
if (foundSub) {
|
||||
subDownloadList.push({ ...foundSub, isDub: true })
|
||||
} else {
|
||||
console.log(`No Dub Sub Found for ${d}`)
|
||||
}
|
||||
dubDownloadList.push({
|
||||
audio_locale: 'ja-JP',
|
||||
guid: e,
|
||||
is_premium_only: true,
|
||||
media_guid: 'adas',
|
||||
original: false,
|
||||
season_guid: 'asdasd',
|
||||
variant: 'asd'
|
||||
})
|
||||
} else {
|
||||
console.warn(`Audio ${d}.aac not found, skipping`)
|
||||
}
|
||||
}
|
||||
|
||||
if (dubDownloadList.length === 0) {
|
||||
const jpVersion = playlist.versions.find((v) => v.audio_locale === 'ja-JP')
|
||||
|
||||
if (jpVersion) {
|
||||
console.log('Using ja-JP Audio because no Audio in download list')
|
||||
dubDownloadList.push(jpVersion)
|
||||
}
|
||||
}
|
||||
|
||||
const subDownload = async () => {
|
||||
const sbs: Array<string> = []
|
||||
for (const sub of subDownloadList) {
|
||||
const name = await downloadCRSub(sub, subFolder)
|
||||
sbs.push(name)
|
||||
}
|
||||
return sbs
|
||||
}
|
||||
|
||||
const audioDownload = async () => {
|
||||
const audios: Array<string> = []
|
||||
for (const v of dubDownloadList) {
|
||||
const list = await crunchyGetPlaylist(v.guid)
|
||||
|
||||
if (!list) return
|
||||
|
||||
const playlist = await crunchyGetPlaylistMPD(list.url)
|
||||
|
||||
if (!playlist) return
|
||||
|
||||
var p: { filename: string; url: string }[] = []
|
||||
|
||||
p.push({
|
||||
filename: (playlist.mediaGroups.AUDIO.audio.main.playlists[0].segments[0].map.uri.match(/([^\/]+)\?/) as RegExpMatchArray)[1],
|
||||
url: playlist.mediaGroups.AUDIO.audio.main.playlists[0].segments[0].map.resolvedUri
|
||||
})
|
||||
|
||||
for (const s of playlist.mediaGroups.AUDIO.audio.main.playlists[0].segments) {
|
||||
p.push({
|
||||
filename: (s.uri.match(/([^\/]+)\?/) as RegExpMatchArray)[1],
|
||||
url: s.resolvedUri
|
||||
})
|
||||
}
|
||||
|
||||
const path = await downloadMPDAudio(p, audioFolder, list.audioLocale)
|
||||
|
||||
audios.push(path as string)
|
||||
}
|
||||
return audios
|
||||
}
|
||||
|
||||
const downloadVideo = async () => {
|
||||
var code
|
||||
|
||||
if (!playlist) return
|
||||
|
||||
if (playlist.versions && playlist.versions.length !== 0) {
|
||||
if (playlist.versions.find((p) => p.audio_locale === dubs[0])) {
|
||||
code = playlist.versions.find((p) => p.audio_locale === dubs[0])?.guid
|
||||
} else {
|
||||
code = playlist.versions.find((p) => p.audio_locale === 'ja-JP')?.guid
|
||||
}
|
||||
} else {
|
||||
code = e
|
||||
}
|
||||
|
||||
if (!code) return console.error('No clean stream found')
|
||||
|
||||
const play = await crunchyGetPlaylist(code)
|
||||
|
||||
if (!play) return
|
||||
|
||||
var downloadURL
|
||||
|
||||
if (hardsub) {
|
||||
const hardsubURL = play.hardSubs.find((h) => h.hlang === subs[0])?.url
|
||||
|
||||
if (hardsubURL) {
|
||||
downloadURL = hardsubURL
|
||||
console.log('Hardsub Playlist found')
|
||||
} else {
|
||||
downloadURL = play.url
|
||||
console.log('Hardsub Playlist not found')
|
||||
}
|
||||
} else {
|
||||
downloadURL = play.url
|
||||
console.log('Hardsub disabled, skipping')
|
||||
}
|
||||
|
||||
var mdp = await crunchyGetPlaylistMPD(downloadURL)
|
||||
|
||||
if (!mdp) return
|
||||
|
||||
var hq = mdp.playlists.find((i) => i.attributes.RESOLUTION?.height === quality)
|
||||
|
||||
if (!hq) return
|
||||
|
||||
var p: { filename: string; url: string }[] = []
|
||||
|
||||
p.push({
|
||||
filename: (hq.segments[0].map.uri.match(/([^\/]+)\?/) as RegExpMatchArray)[1],
|
||||
url: hq.segments[0].map.resolvedUri
|
||||
})
|
||||
|
||||
for (const s of hq.segments) {
|
||||
p.push({
|
||||
filename: (s.uri.match(/([^\/]+)\?/) as RegExpMatchArray)[1],
|
||||
url: s.resolvedUri
|
||||
})
|
||||
}
|
||||
|
||||
const dn = downloading.find((i) => i.id === downloadID)
|
||||
|
||||
if (dn) {
|
||||
dn.partsToDownload = p.length
|
||||
}
|
||||
|
||||
const file = await downloadParts(p, downloadID, videoFolder)
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
const [subss, audios, file] = await Promise.all([subDownload(), audioDownload(), downloadVideo()])
|
||||
|
||||
if (!audios) return
|
||||
|
||||
await mergeVideoFile(file as string, audios, subss, String(playlist.assetId), seasonFolder, `${name.replace(/[/\\?%*:|"<>]/g, '')} Season ${season} Episode ${episode}`, format)
|
||||
|
||||
await updatePlaylistByID(downloadID, 'completed')
|
||||
|
||||
await deleteFolder(subFolder)
|
||||
await deleteFolder(audioFolder)
|
||||
await deleteFolder(videoFolder)
|
||||
|
||||
return playlist
|
||||
}
|
||||
|
||||
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
|
||||
while (!success) {
|
||||
try {
|
||||
const stream = fs.createWriteStream(`${path}/${part.filename}`)
|
||||
const { body } = await fetch(part.url)
|
||||
|
||||
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`)
|
||||
|
||||
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)
|
||||
console.log(`Retrying download of fragment ${index + 1}...`)
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await mergeParts(parts, downloadID, path, dir)
|
||||
}
|
||||
|
||||
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}`)
|
||||
}
|
||||
|
||||
const concatenatedFile = `${tmp}/main.m4s`
|
||||
await concatenateTSFiles(list, concatenatedFile)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
Ffmpeg()
|
||||
.setFfmpegPath(ffmpegPath)
|
||||
.setFfprobePath(ffprobePath)
|
||||
.input(concatenatedFile)
|
||||
.outputOptions('-c copy')
|
||||
.save(dir + `/${tempname}.mp4`)
|
||||
.on('end', async () => {
|
||||
console.log('Merging finished')
|
||||
await deleteFolder(tmp)
|
||||
return resolve(dir + `/${tempname}.mp4`)
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error merging parts:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function mergeVideoFile(video: string, audios: Array<string>, subs: Array<string>, name: string, path: string, filename: string, format: 'mp4' | 'mkv') {
|
||||
const locales: Array<{
|
||||
locale: string
|
||||
name: string
|
||||
iso: string
|
||||
title: string
|
||||
}> = [
|
||||
{ locale: 'ja-JP', name: 'JP', iso: 'jpn', title: 'Japanese' },
|
||||
{ locale: 'de-DE', name: 'DE', iso: 'deu', title: 'German' },
|
||||
{ locale: 'hi-IN', name: 'HI', iso: 'hin', title: 'Hindi' },
|
||||
{ locale: 'ru-RU', name: 'RU', iso: 'rus', title: 'Russian' },
|
||||
{ locale: 'en-US', name: 'EN', iso: 'eng', title: 'English' },
|
||||
{ locale: 'fr-FR', name: 'FR', iso: 'fra', title: 'French' },
|
||||
{ locale: 'pt-BR', name: 'PT', iso: 'por', title: 'Portugese' },
|
||||
{ locale: 'es-419', name: 'LA-ES', iso: 'spa', title: 'SpanishLatin' },
|
||||
{ locale: 'en-IN', name: 'EN-IN', iso: 'eng', title: 'IndianEnglish' },
|
||||
{ locale: 'it-IT', name: 'IT', iso: 'ita', title: 'Italian' },
|
||||
{ locale: 'es-ES', name: 'ES', iso: 'spa', title: 'Spanish' },
|
||||
{ locale: 'ta-IN', name: 'TA', iso: 'tam', title: 'Tamil' },
|
||||
{ locale: 'te-IN', name: 'TE', iso: 'tel', title: 'Telugu' },
|
||||
{ locale: 'ar-SA', name: 'AR', iso: 'ara', title: 'ArabicSA' },
|
||||
{ locale: 'ms-MY', name: 'MS', iso: 'msa', title: 'Malay' },
|
||||
{ locale: 'th-TH', name: 'TH', iso: 'tha', title: 'Thai' },
|
||||
{ locale: 'vi-VN', name: 'VI', iso: 'vie', title: 'Vietnamese' },
|
||||
{ locale: 'id-ID', name: 'ID', iso: 'ind', title: 'Indonesian' },
|
||||
{ locale: 'ko-KR', name: 'KO', iso: 'kor', title: 'Korean' }
|
||||
]
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
var output = Ffmpeg().setFfmpegPath(ffmpegPath).setFfprobePath(ffprobePath)
|
||||
var ffindex = 1
|
||||
output.addInput(video)
|
||||
var options = ['-map_metadata -1', '-c copy', '-metadata:s:v:0 VARIANT_BITRATE=0', '-map 0']
|
||||
if (format === 'mp4') {
|
||||
options.push('-c:s mov_text')
|
||||
}
|
||||
|
||||
for (const [index, a] of audios.entries()) {
|
||||
output.addInput(a)
|
||||
options.push(`-map ${ffindex}:a:0`)
|
||||
options.push(
|
||||
`-metadata:s:a:${index} language=${
|
||||
locales.find((l) => l.locale === a.split('/')[1].split('.aac')[0])
|
||||
? locales.find((l) => l.locale === a.split('/')[1].split('.aac')[0])?.iso
|
||||
: a.split('/')[1].split('.aac')[0]
|
||||
}`
|
||||
)
|
||||
|
||||
ffindex++
|
||||
|
||||
options.push(
|
||||
`-metadata:s:a:${index} title=${
|
||||
locales.find((l) => l.locale === 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) {
|
||||
for (const [index, s] of subs.entries()) {
|
||||
output.addInput(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(
|
||||
`-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])?.iso
|
||||
: 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++
|
||||
}
|
||||
options.push(`-disposition:s:0 default`)
|
||||
}
|
||||
|
||||
output
|
||||
.addOptions(options)
|
||||
.saveToFile(path + `/${filename}.${format}`)
|
||||
.on('error', (error) => {
|
||||
console.log(error)
|
||||
reject(error)
|
||||
})
|
||||
.on('end', async () => {
|
||||
console.log('Download finished')
|
||||
return resolve('combined')
|
||||
})
|
||||
})
|
||||
}
|
69
src/api/services/audio.ts
Normal file
69
src/api/services/audio.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import fs from 'fs'
|
||||
import { Readable } from 'stream'
|
||||
import { createFolder, deleteFolder } from './folder'
|
||||
import { concatenateTSFiles } from './concatenate'
|
||||
import Ffmpeg from 'fluent-ffmpeg'
|
||||
const ffmpegPath = require('ffmpeg-static').replace('app.asar', 'app.asar.unpacked')
|
||||
const ffprobePath = require('ffprobe-static').path.replace('app.asar', 'app.asar.unpacked')
|
||||
|
||||
export async function downloadMPDAudio(parts: { filename: string; url: string }[], dir: string, name: string) {
|
||||
const path = await createFolder()
|
||||
const downloadPromises = []
|
||||
|
||||
for (const [index, part] of parts.entries()) {
|
||||
const stream = fs.createWriteStream(`${path}/${part.filename}`)
|
||||
const downloadPromise = fetchAndPipe(part.url, stream, index + 1)
|
||||
downloadPromises.push(downloadPromise)
|
||||
}
|
||||
|
||||
await Promise.all(downloadPromises)
|
||||
|
||||
return await mergePartsAudio(parts, path, dir, name)
|
||||
}
|
||||
|
||||
async function fetchAndPipe(url: string, stream: fs.WriteStream, index: number) {
|
||||
const { body } = await fetch(url)
|
||||
const readableStream = Readable.from(body as any)
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
readableStream
|
||||
.pipe(stream)
|
||||
.on('finish', () => {
|
||||
console.log(`Fragment ${index} downloaded`)
|
||||
resolve()
|
||||
})
|
||||
.on('error', (error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function mergePartsAudio(parts: { filename: string; url: string }[], tmp: string, dir: string, name: string) {
|
||||
try {
|
||||
const list: Array<string> = []
|
||||
|
||||
for (const [index, part] of parts.entries()) {
|
||||
list.push(`${tmp}/${part.filename}`)
|
||||
}
|
||||
|
||||
const concatenatedFile = `${tmp}/main.m4s`
|
||||
await concatenateTSFiles(list, concatenatedFile)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
Ffmpeg()
|
||||
.setFfmpegPath(ffmpegPath)
|
||||
.setFfprobePath(ffprobePath)
|
||||
.input(concatenatedFile)
|
||||
.outputOptions('-c copy')
|
||||
.save(`${dir}/${name}.aac`)
|
||||
.on('end', async () => {
|
||||
console.log('Merging finished')
|
||||
await deleteFolder(tmp)
|
||||
|
||||
return resolve(`${dir}/${name}.aac`)
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error merging parts:', error)
|
||||
}
|
||||
}
|
37
src/api/services/concatenate.ts
Normal file
37
src/api/services/concatenate.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import fs from 'fs'
|
||||
|
||||
export async function concatenateTSFiles(inputFiles: Array<string>, outputFile: string) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const writeStream = fs.createWriteStream(outputFile)
|
||||
|
||||
writeStream.on('error', (error) => {
|
||||
reject(error)
|
||||
})
|
||||
|
||||
writeStream.on('finish', () => {
|
||||
console.log('TS files concatenated successfully!')
|
||||
resolve()
|
||||
})
|
||||
|
||||
const processNextFile = (index: number) => {
|
||||
if (index >= inputFiles.length) {
|
||||
writeStream.end()
|
||||
return
|
||||
}
|
||||
|
||||
const readStream = fs.createReadStream(inputFiles[index])
|
||||
|
||||
readStream.on('error', (error) => {
|
||||
reject(error)
|
||||
})
|
||||
|
||||
readStream.pipe(writeStream, { end: false })
|
||||
|
||||
readStream.on('end', () => {
|
||||
processNextFile(index + 1)
|
||||
})
|
||||
}
|
||||
|
||||
processNextFile(0)
|
||||
})
|
||||
}
|
54
src/api/services/folder.ts
Normal file
54
src/api/services/folder.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import path from 'path'
|
||||
import { app } from 'electron'
|
||||
import fs from 'fs'
|
||||
|
||||
export async function createFolder() {
|
||||
const tempFolderPath = path.join(app.getPath('documents'), (Math.random() + 1).toString(36).substring(2))
|
||||
try {
|
||||
await fs.promises.mkdir(tempFolderPath, { recursive: true })
|
||||
return tempFolderPath
|
||||
} catch (error) {
|
||||
console.error('Error creating temporary folder:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkDirectoryExistence(dir: string) {
|
||||
try {
|
||||
await fs.promises.access(dir)
|
||||
console.log(`Directory ${dir} exists.`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.log(`Directory ${dir} does not exist.`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function createFolderName(name: string, dir: string) {
|
||||
var folderPath
|
||||
|
||||
const dirExists = await checkDirectoryExistence(dir)
|
||||
|
||||
if (dirExists) {
|
||||
folderPath = path.join(dir, name)
|
||||
} else {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteFolder(folderPath: string) {
|
||||
fs.rmSync(folderPath, { recursive: true, force: true })
|
||||
}
|
39
src/api/services/subs.ts
Normal file
39
src/api/services/subs.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import fs from 'fs'
|
||||
import { parse, stringify } from 'ass-compiler'
|
||||
import { Readable } from 'stream'
|
||||
import { finished } from 'stream/promises'
|
||||
|
||||
export async function downloadCRSub(
|
||||
sub: {
|
||||
format: string
|
||||
language: string
|
||||
url: string
|
||||
isDub: boolean
|
||||
},
|
||||
dir: string
|
||||
) {
|
||||
const path = `${dir}/${sub.language}${sub.isDub ? `-FORCED` : ''}.${sub.format}`
|
||||
|
||||
const stream = fs.createWriteStream(path)
|
||||
const response = await fetch(sub.url)
|
||||
|
||||
var parsedASS = parse(await response.text())
|
||||
|
||||
// Disabling Changing ASS because still broken in vlc
|
||||
|
||||
// parsedASS.info.PlayResX = "1920";
|
||||
// parsedASS.info.PlayResY = "1080";
|
||||
|
||||
// for (const s of parsedASS.styles.style) {
|
||||
// (s.Fontsize = "54"), (s.Outline = "4");
|
||||
// }
|
||||
|
||||
const fixed = stringify(parsedASS)
|
||||
|
||||
const readableStream = Readable.from([fixed])
|
||||
|
||||
await finished(readableStream.pipe(stream))
|
||||
console.log(`Sub ${sub.language}.${sub.format} downloaded`)
|
||||
|
||||
return path
|
||||
}
|
Reference in New Issue
Block a user