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 }
|
return { data, error }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkAccount() {
|
export async function checkAccount(service: string) {
|
||||||
const { data, error } = await useFetch<CrunchyLogin>('http://localhost:8080/api/crunchyroll/check', {
|
const { data, error } = await useFetch<CrunchyLogin>(`http://localhost:8080/api/service/check/${service}`, {
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
})
|
})
|
||||||
|
|
||||||
return { data, error }
|
return { data, error }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loginAccount(user: string, password: string) {
|
export async function loginAccount(user: string, password: string, service: string) {
|
||||||
const { data, error } = await useFetch<CrunchyLogin>('http://localhost:8080/api/crunchyroll/login/login', {
|
const { data, error } = await useFetch<CrunchyLogin>(`http://localhost:8080/api/service/login/${service}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: {
|
||||||
user: user,
|
user: user,
|
||||||
|
@ -39,18 +39,18 @@ async function openSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function openAddAnime() {
|
async function openAddAnime() {
|
||||||
const { data, error } = await checkAccount()
|
// const { data, error } = await checkAccount()
|
||||||
|
|
||||||
if (error.value) {
|
// if (error.value) {
|
||||||
(window as any).myAPI.openWindow({
|
// (window as any).myAPI.openWindow({
|
||||||
title: "Crunchyroll Login",
|
// title: "Crunchyroll Login",
|
||||||
url: isProduction ? 'http://localhost:8079/crunchylogin' : 'http://localhost:3000/crunchylogin',
|
// url: isProduction ? 'http://localhost:8079/crunchylogin' : 'http://localhost:3000/crunchylogin',
|
||||||
width: 600,
|
// width: 600,
|
||||||
height: 300,
|
// height: 300,
|
||||||
backgroundColor: "#111111"
|
// backgroundColor: "#111111"
|
||||||
})
|
// })
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
|
||||||
(window as any).myAPI.openWindow({
|
(window as any).myAPI.openWindow({
|
||||||
title: "Add Anime",
|
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 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">
|
<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">
|
<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="crunchyroll">Crunchyroll</option>
|
||||||
|
<option value="adn">ADN</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative flex flex-col">
|
<div v-if="isLoggedInCR && service === 'crunchyroll' || !isLoggedInCR && service === 'adn'" class="relative flex flex-col">
|
||||||
<input
|
<input
|
||||||
v-model="search"
|
v-model="search"
|
||||||
@input="handleInputChange"
|
@input="handleInputChange"
|
||||||
@ -61,10 +61,10 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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" />
|
<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>
|
||||||
<div class="relative flex flex-col">
|
<div v-if="isLoggedInCR && service === 'crunchyroll'" class="relative flex flex-col">
|
||||||
<input
|
<input
|
||||||
@click="getFolderPath()"
|
@click="getFolderPath()"
|
||||||
v-model="path"
|
v-model="path"
|
||||||
@ -75,6 +75,11 @@
|
|||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<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">
|
<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'">
|
<div class="flex flex-row items-center justify-center transition-all" :class="isFetchingSeasons ? 'opacity-0' : 'opacity-100'">
|
||||||
@ -165,7 +170,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row gap-5">
|
<div class="flex flex-row gap-3">
|
||||||
<div class="relative flex flex-col w-full">
|
<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">
|
<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>
|
<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>
|
<option :value="240" class="text-sm text-slate-200">240p</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- {{ CRselectedShow?.Subs.map(s=> { return locales.find(l => l.locale === s)?.name }) }}
|
<!-- {{ CRselectedShow?.Subs.map(s=> { return locales.find(l => l.locale === s)?.name }) }}
|
||||||
@ -226,6 +237,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { searchADN } from '~/components/ADN/ListAnimes'
|
import { searchADN } from '~/components/ADN/ListAnimes'
|
||||||
|
import { checkAccount } from '~/components/Crunchyroll/Account'
|
||||||
import { getCRSeries, searchCrunchy } from '~/components/Crunchyroll/ListAnimes'
|
import { getCRSeries, searchCrunchy } from '~/components/Crunchyroll/ListAnimes'
|
||||||
import { listEpisodeCrunchy } from '~/components/Crunchyroll/ListEpisodes'
|
import { listEpisodeCrunchy } from '~/components/Crunchyroll/ListEpisodes'
|
||||||
import { listSeasonCrunchy } from '~/components/Crunchyroll/ListSeasons'
|
import { listSeasonCrunchy } from '~/components/Crunchyroll/ListSeasons'
|
||||||
@ -257,6 +269,7 @@ const locales = ref<Array<{ locale: string; name: string }>>([
|
|||||||
{ locale: 'ko-KR', name: 'KO' }
|
{ locale: 'ko-KR', name: 'KO' }
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const isProduction = process.env.NODE_ENV !== 'development'
|
||||||
const selectDub = ref<boolean>(false)
|
const selectDub = ref<boolean>(false)
|
||||||
const selectedDubs = ref<Array<{ name: string | undefined; locale: string }>>([{ locale: 'ja-JP', name: 'JP' }])
|
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 added = ref<boolean>(false)
|
||||||
const isHardsubDisabled = ref<boolean>(true)
|
const isHardsubDisabled = ref<boolean>(true)
|
||||||
const quality = ref<1080 | 720 | 480 | 360 | 240>(1080)
|
const quality = ref<1080 | 720 | 480 | 360 | 240>(1080)
|
||||||
|
const format = ref<'mp4' | 'mkv'>('mkv')
|
||||||
|
|
||||||
const isFetchingSeasons = ref<number>(0)
|
const isFetchingSeasons = ref<number>(0)
|
||||||
const isFetchingEpisodes = ref<number>(0)
|
const isFetchingEpisodes = ref<number>(0)
|
||||||
const isFetchingResults = 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 () => {
|
const fetchSearch = async () => {
|
||||||
if (!search.value || search.value.length === 0) {
|
if (!search.value || search.value.length === 0) {
|
||||||
adnSearchResults.value = []
|
adnSearchResults.value = []
|
||||||
@ -343,7 +389,7 @@ const selectShow = async (show: any) => {
|
|||||||
|
|
||||||
if (service.value === 'crunchyroll') {
|
if (service.value === 'crunchyroll') {
|
||||||
CRselectedShow.value = show
|
CRselectedShow.value = show
|
||||||
url.value = show.Url
|
url.value = show.Url + '/'
|
||||||
}
|
}
|
||||||
|
|
||||||
search.value = ''
|
search.value = ''
|
||||||
@ -471,6 +517,18 @@ const addToPlaylist = async () => {
|
|||||||
return
|
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 selectedEpisodes = episodes.value.slice(startEpisodeIndex, endEpisodeIndex + 1)
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
@ -479,10 +537,12 @@ const addToPlaylist = async () => {
|
|||||||
subs: selectedSubs.value,
|
subs: selectedSubs.value,
|
||||||
dir: path.value,
|
dir: path.value,
|
||||||
hardsub: hardsub.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',
|
method: 'POST',
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
})
|
})
|
||||||
|
@ -34,16 +34,6 @@ const password = ref<string>()
|
|||||||
|
|
||||||
const isLoggingIn = ref<number>(0)
|
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 () => {
|
const login = async () => {
|
||||||
isLoggingIn.value++
|
isLoggingIn.value++
|
||||||
if (!username.value) {
|
if (!username.value) {
|
||||||
@ -55,14 +45,13 @@ const login = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, error } = await loginAccount(username.value, password.value)
|
const { data, error } = await loginAccount(username.value, password.value, 'CR')
|
||||||
|
|
||||||
if (error.value) {
|
if (error.value) {
|
||||||
isLoggingIn.value--
|
isLoggingIn.value--
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isLoggingIn.value--
|
isLoggingIn.value--
|
||||||
openAddAnime()
|
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
</script>
|
</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" />
|
<img :src="p.media.images.thumbnail[0].find((p) => p.height === 1080)?.source" alt="Image" class="object-cover rounded-xl" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
|
<div class="flex flex-row">
|
||||||
<div class="text-sm capitalize">
|
<div class="text-sm capitalize">
|
||||||
{{ p.status }}
|
{{ p.status }}
|
||||||
</div>
|
</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="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 class="relative w-full min-h-5 bg-[#bdbbbb] mt-1 rounded">
|
||||||
<div
|
<div
|
||||||
@ -26,9 +31,10 @@
|
|||||||
<div class="flex h-full"> </div>
|
<div class="flex h-full"> </div>
|
||||||
<div class="flex flex-row gap-2 mt-2">
|
<div class="flex flex-row gap-2 mt-2">
|
||||||
<div class="text-sm">{{ p.quality }}p</div>
|
<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">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 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 v-if="p.downloadspeed && p.status === 'downloading'" class="text-sm">{{ p.downloadspeed }} MB/s</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -52,6 +58,8 @@ const playlist = ref<
|
|||||||
partsdownloaded: number
|
partsdownloaded: number
|
||||||
downloadspeed: number
|
downloadspeed: number
|
||||||
quality: number
|
quality: number
|
||||||
|
service: string
|
||||||
|
format: string
|
||||||
}>
|
}>
|
||||||
>()
|
>()
|
||||||
|
|
||||||
@ -68,8 +76,10 @@ const getPlaylist = async () => {
|
|||||||
partsdownloaded: number
|
partsdownloaded: number
|
||||||
downloadspeed: number
|
downloadspeed: number
|
||||||
quality: number
|
quality: number
|
||||||
|
service: string
|
||||||
|
format: string
|
||||||
}>
|
}>
|
||||||
>('http://localhost:8080/api/crunchyroll/playlist')
|
>('http://localhost:8080/api/service/playlist')
|
||||||
|
|
||||||
if (error.value) {
|
if (error.value) {
|
||||||
alert(error.value)
|
alert(error.value)
|
||||||
@ -84,7 +94,7 @@ const getPlaylist = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deletePlaylist = 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'
|
method: 'delete'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import cors from "@fastify/cors";
|
|||||||
import NodeCache from "node-cache";
|
import NodeCache from "node-cache";
|
||||||
import crunchyrollRoutes from "./routes/crunchyroll/crunchyroll.route";
|
import crunchyrollRoutes from "./routes/crunchyroll/crunchyroll.route";
|
||||||
import { sequelize } from "./db/database";
|
import { sequelize } from "./db/database";
|
||||||
|
import serviceRoutes from "./routes/service/service.route";
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
@ -43,6 +44,7 @@ server.decorate("CacheController", CacheController);
|
|||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
server.register(crunchyrollRoutes, { prefix: 'api/crunchyroll' })
|
server.register(crunchyrollRoutes, { prefix: 'api/crunchyroll' })
|
||||||
|
server.register(serviceRoutes, { prefix: 'api/service' })
|
||||||
|
|
||||||
function startAPI() {
|
function startAPI() {
|
||||||
server.listen({ port: 8080 }, (err, address) => {
|
server.listen({ port: 8080 }, (err, address) => {
|
||||||
|
@ -4,7 +4,7 @@ import { CrunchyEpisode } from '../types/crunchyroll'
|
|||||||
|
|
||||||
const sequelize = new Sequelize({
|
const sequelize = new Sequelize({
|
||||||
dialect: 'sqlite',
|
dialect: 'sqlite',
|
||||||
storage: app.getPath('documents') + '/crd-dbv1.db'
|
storage: app.getPath('documents') + '/crd-dbv2.db'
|
||||||
})
|
})
|
||||||
|
|
||||||
interface AccountAttributes {
|
interface AccountAttributes {
|
||||||
@ -35,7 +35,9 @@ interface PlaylistAttributes {
|
|||||||
hardsub: boolean
|
hardsub: boolean
|
||||||
quality: 1080 | 720 | 480 | 360 | 240
|
quality: 1080 | 720 | 480 | 360 | 240
|
||||||
dir: string
|
dir: string
|
||||||
failedreason: string
|
failedreason: string,
|
||||||
|
service: 'CR' | 'ADN',
|
||||||
|
format: 'mp4' | 'mkv'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PlaylistCreateAttributes {
|
interface PlaylistCreateAttributes {
|
||||||
@ -45,7 +47,9 @@ interface PlaylistCreateAttributes {
|
|||||||
dir: string
|
dir: string
|
||||||
quality: 1080 | 720 | 480 | 360 | 240
|
quality: 1080 | 720 | 480 | 360 | 240
|
||||||
hardsub: boolean
|
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', {
|
const Account: ModelDefined<AccountAttributes, AccountCreateAttributes> = sequelize.define('Accounts', {
|
||||||
@ -116,6 +120,14 @@ const Playlist: ModelDefined<PlaylistAttributes, PlaylistCreateAttributes> = seq
|
|||||||
allowNull: true,
|
allowNull: true,
|
||||||
type: DataTypes.BOOLEAN
|
type: DataTypes.BOOLEAN
|
||||||
},
|
},
|
||||||
|
service: {
|
||||||
|
allowNull: true,
|
||||||
|
type: DataTypes.STRING
|
||||||
|
},
|
||||||
|
format: {
|
||||||
|
allowNull: true,
|
||||||
|
type: DataTypes.STRING
|
||||||
|
},
|
||||||
createdAt: {
|
createdAt: {
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
type: DataTypes.DATE
|
type: DataTypes.DATE
|
||||||
|
@ -2,376 +2,468 @@ import JSEncrypt from 'jsencrypt'
|
|||||||
import CryptoJS from 'crypto-js'
|
import CryptoJS from 'crypto-js'
|
||||||
import { server } from '../../api'
|
import { server } from '../../api'
|
||||||
import { ADNPlayerConfig } from '../../types/adn'
|
import { ADNPlayerConfig } from '../../types/adn'
|
||||||
|
import { messageBox } from '../../../electron/background'
|
||||||
|
import { useFetch } from '../useFetch'
|
||||||
|
|
||||||
export async function getShowADN(q: number) {
|
// export async function getShowADN(q: number) {
|
||||||
const cachedData = server.CacheController.get(`getshowadn-${q}`)
|
// const cachedData = server.CacheController.get(`getshowadn-${q}`)
|
||||||
|
|
||||||
if (cachedData) {
|
// if (cachedData) {
|
||||||
return cachedData
|
// return cachedData
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 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())
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export async function getEpisodesWithShowIdADN(q: number) {
|
||||||
|
// const cachedData = server.CacheController.get(`getepisodesadn-${q}`)
|
||||||
|
|
||||||
|
// 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'
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
// if (response.ok) {
|
||||||
|
// const data: {
|
||||||
|
// videos: Array<any>
|
||||||
|
// } = JSON.parse(await response.text())
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export async function getEpisodeADN(q: number) {
|
||||||
|
// const cachedData = server.CacheController.get(`getepisodeadn-${q}`)
|
||||||
|
|
||||||
|
// if (cachedData) {
|
||||||
|
// return cachedData
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 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())
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export async function searchADN(q: string) {
|
||||||
|
// const cachedData = server.CacheController.get(`searchadn-${q}`)
|
||||||
|
|
||||||
|
// 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'
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
// if (response.ok) {
|
||||||
|
// const data: {
|
||||||
|
// shows: Array<any>
|
||||||
|
// } = JSON.parse(await response.text())
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export async function loginADN() {
|
||||||
|
// const cachedData = server.CacheController.get('adnlogin')
|
||||||
|
// const cachedToken = server.CacheController.get('adntoken')
|
||||||
|
|
||||||
|
// if (cachedData) {
|
||||||
|
// return cachedData
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (cachedToken) {
|
||||||
|
// const data = await loginADNToken(cachedToken as string)
|
||||||
|
// return data
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// 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')
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 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())
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export async function getPlayerConfigADN() {
|
||||||
|
// const cachedData: ADNPlayerConfig | undefined = server.CacheController.get('adnplayerconfig')
|
||||||
|
|
||||||
|
// if (cachedData) {
|
||||||
|
// return cachedData
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 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}`
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
// if (response.ok) {
|
||||||
|
// const data: ADNPlayerConfig = JSON.parse(await response.text())
|
||||||
|
|
||||||
|
// server.CacheController.set('adnplayerconfig', data, 300)
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
|
||||||
|
// 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())
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// }
|
||||||
|
|
||||||
|
// async function getPlayerEncryptedToken() {
|
||||||
|
// const token = await getPlayerToken()
|
||||||
|
|
||||||
|
// var key = new JSEncrypt()
|
||||||
|
// var random = randomHexaString(16)
|
||||||
|
|
||||||
|
// key.setPublicKey(
|
||||||
|
// '-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCbQrCJBRmaXM4gJidDmcpWDssgnumHinCLHAgS4buMtdH7dEGGEUfBofLzoEdt1jqcrCDT6YNhM0aFCqbLOPFtx9cg/X2G/G5bPVu8cuFM0L+ehp8s6izK1kjx3OOPH/kWzvstM5tkqgJkNyNEvHdeJl6KhS+IFEqwvZqgbBpKuwIDAQAB-----END PUBLIC KEY-----'
|
||||||
|
// )
|
||||||
|
|
||||||
|
// const data = {
|
||||||
|
// k: random,
|
||||||
|
// t: String(token.token)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const finisheddata = JSON.stringify(data)
|
||||||
|
|
||||||
|
// const encryptedData = key.encrypt(finisheddata) || ''
|
||||||
|
|
||||||
|
// return { data: encryptedData, random: random }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
|
| undefined = server.CacheController.get('adntoken')
|
||||||
|
|
||||||
try {
|
if (!cachedData) {
|
||||||
const response = await fetch(`https://gw.api.animationdigitalnetwork.fr/show/${q}`, {
|
var { data, error } = await adnLoginFetch(user, passw)
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'x-target-distribution': 'de'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response.ok) {
|
if (error) {
|
||||||
const data: {
|
messageBox(
|
||||||
show: Array<any>
|
'error',
|
||||||
} = JSON.parse(await response.text())
|
['Cancel'],
|
||||||
|
2,
|
||||||
server.CacheController.set(`getshowadn-${q}`, data.show, 1000)
|
'Failed to login',
|
||||||
|
'Failed to login to ADN',
|
||||||
return data.show
|
(error.error as string)
|
||||||
} 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}`)
|
|
||||||
|
|
||||||
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'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data: {
|
|
||||||
videos: Array<any>
|
|
||||||
} = JSON.parse(await response.text())
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getEpisodeADN(q: number) {
|
|
||||||
const cachedData = server.CacheController.get(`getepisodeadn-${q}`)
|
|
||||||
|
|
||||||
if (cachedData) {
|
|
||||||
return cachedData
|
|
||||||
}
|
|
||||||
|
|
||||||
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())
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function searchADN(q: string) {
|
|
||||||
const cachedData = server.CacheController.get(`searchadn-${q}`)
|
|
||||||
|
|
||||||
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'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data: {
|
|
||||||
shows: Array<any>
|
|
||||||
} = JSON.parse(await response.text())
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loginADN() {
|
|
||||||
const cachedData = server.CacheController.get('adnlogin')
|
|
||||||
const cachedToken = server.CacheController.get('adntoken')
|
|
||||||
|
|
||||||
if (cachedData) {
|
|
||||||
return cachedData
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cachedToken) {
|
|
||||||
const data = await loginADNToken(cachedToken as string)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
|
|
||||||
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')
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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())
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getPlayerConfigADN() {
|
|
||||||
const cachedData: ADNPlayerConfig | undefined = server.CacheController.get('adnplayerconfig')
|
|
||||||
|
|
||||||
if (cachedData) {
|
|
||||||
return cachedData
|
|
||||||
}
|
|
||||||
|
|
||||||
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}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data: ADNPlayerConfig = JSON.parse(await response.text())
|
|
||||||
|
|
||||||
server.CacheController.set('adnplayerconfig', data, 300)
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
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())
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getPlayerEncryptedToken() {
|
|
||||||
const token = await getPlayerToken()
|
|
||||||
|
|
||||||
var key = new JSEncrypt()
|
|
||||||
var random = randomHexaString(16)
|
|
||||||
|
|
||||||
key.setPublicKey(
|
|
||||||
'-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCbQrCJBRmaXM4gJidDmcpWDssgnumHinCLHAgS4buMtdH7dEGGEUfBofLzoEdt1jqcrCDT6YNhM0aFCqbLOPFtx9cg/X2G/G5bPVu8cuFM0L+ehp8s6izK1kjx3OOPH/kWzvstM5tkqgJkNyNEvHdeJl6KhS+IFEqwvZqgbBpKuwIDAQAB-----END PUBLIC KEY-----'
|
|
||||||
)
|
)
|
||||||
|
return { data: null, error: error.error }
|
||||||
const data = {
|
|
||||||
k: random,
|
|
||||||
t: String(token.token)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const finisheddata = JSON.stringify(data)
|
if (!data) {
|
||||||
|
messageBox('error', ['Cancel'], 2, 'Failed to login', 'Failed to login to ADN', 'ADN returned null')
|
||||||
const encryptedData = key.encrypt(finisheddata) || ''
|
return { data: null, error: 'ADN returned null' }
|
||||||
|
|
||||||
return { data: encryptedData, random: random }
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data: cachedData, error: 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 (response.ok) {
|
if (error) {
|
||||||
const data: {
|
return { data: null, error: error }
|
||||||
links: {
|
|
||||||
subtitles: {
|
|
||||||
all: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return { data: null, error: null }
|
||||||
}
|
}
|
||||||
} = await JSON.parse(await response.text())
|
|
||||||
|
|
||||||
const subtitlelink = await fetch(data.links.subtitles.all, {
|
return { data: data, error: null }
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import type { FastifyReply, FastifyRequest } from 'fastify'
|
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 { dialog } from 'electron'
|
||||||
import { messageBox } from '../../../electron/background'
|
import { messageBox } from '../../../electron/background'
|
||||||
import { CrunchyEpisodes, CrunchySeason } from '../../types/crunchyroll'
|
import { CrunchyEpisodes, CrunchySeason } from '../../types/crunchyroll'
|
||||||
|
import { loggedInCheck } from '../service/service.service'
|
||||||
|
|
||||||
export async function loginController(request: FastifyRequest, reply: FastifyReply) {
|
export async function loginController(request: FastifyRequest, reply: FastifyReply) {
|
||||||
const account = await checkIfLoggedInCR('crunchyroll')
|
const account = await loggedInCheck('CR')
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return reply.code(401).send({ message: 'Not Logged in' })
|
return reply.code(401).send({ message: 'Not Logged in' })
|
||||||
@ -19,91 +20,3 @@ export async function loginController(request: FastifyRequest, reply: FastifyRep
|
|||||||
|
|
||||||
return reply.code(200).send(data)
|
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 type { FastifyInstance } from 'fastify'
|
||||||
import { addPlaylistController, checkLoginController, deleteCompletePlaylistController, getPlaylistController, loginController, loginLoginController } from './crunchyroll.controller'
|
import { loginController } from './crunchyroll.controller'
|
||||||
|
|
||||||
async function crunchyrollRoutes(server: FastifyInstance) {
|
async function crunchyrollRoutes(server: FastifyInstance) {
|
||||||
server.post(
|
server.post(
|
||||||
@ -15,77 +15,6 @@ async function crunchyrollRoutes(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
loginController
|
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 { messageBox } from '../../../electron/background'
|
||||||
import { server } from '../../api'
|
import { server } from '../../api'
|
||||||
import { Account, Playlist } from '../../db/database'
|
import { VideoPlaylist } from '../../types/crunchyroll'
|
||||||
import { CrunchyEpisode, VideoPlaylist } from '../../types/crunchyroll'
|
|
||||||
import { useFetch } from '../useFetch'
|
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 as mpdParse } from 'mpd-parser'
|
||||||
import { parse, stringify } from 'ass-compiler'
|
import { loggedInCheck } from '../service/service.service'
|
||||||
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')
|
|
||||||
|
|
||||||
const crErrors = [
|
const crErrors = [
|
||||||
{
|
{
|
||||||
@ -113,103 +102,8 @@ async function crunchyLoginFetch(user: string, passw: string) {
|
|||||||
return { data: data, error: null }
|
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) {
|
export async function crunchyGetPlaylist(q: string) {
|
||||||
const account = await checkIfLoggedInCR('crunchyroll')
|
const account = await loggedInCheck('CR')
|
||||||
|
|
||||||
if (!account) return
|
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) {
|
export async function crunchyGetPlaylistMPD(q: string) {
|
||||||
const account = await checkIfLoggedInCR('crunchyroll')
|
const account = await loggedInCheck('CR')
|
||||||
|
|
||||||
if (!account) return
|
if (!account) return
|
||||||
|
|
||||||
@ -334,613 +177,3 @@ export async function crunchyGetPlaylistMPD(q: string) {
|
|||||||
throw new Error(e as 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