adn first full download support (adn subs to ass converter missing)
This commit is contained in:
parent
a9341a5877
commit
63080b70f9
@ -0,0 +1,18 @@
|
||||
import type { ADNEpisodes, ADNEpisodesFetch } from './Types'
|
||||
|
||||
export async function getEpisodesWithShowIdADN(id: number) {
|
||||
const { data, error } = await useFetch<ADNEpisodesFetch>(`https://gw.api.animationdigitalnetwork.fr/video/show/${id}?offset=0&limit=-1&order=asc`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
"x-target-distribution": "de",
|
||||
},
|
||||
})
|
||||
|
||||
if (error.value || !data.value) {
|
||||
console.log(error.value)
|
||||
alert(error.value)
|
||||
return
|
||||
}
|
||||
|
||||
return data.value.videos
|
||||
}
|
@ -7,3 +7,78 @@ export interface ADNSearchFetch {
|
||||
episodeCount: number
|
||||
}>
|
||||
}
|
||||
|
||||
export interface ADNEpisodesFetch {
|
||||
videos: Array<ADNEpisode>
|
||||
}
|
||||
|
||||
export interface ADNEpisode {
|
||||
id: number,
|
||||
title: string,
|
||||
name: string,
|
||||
number: string,
|
||||
shortNumber: string,
|
||||
season: string,
|
||||
reference: string,
|
||||
type: string,
|
||||
order: number,
|
||||
image: string,
|
||||
image2x: string,
|
||||
summary: string,
|
||||
releaseDate: string,
|
||||
duration: number,
|
||||
url: string,
|
||||
urlPath: string,
|
||||
embeddedUrl: string,
|
||||
languages: Array<string>,
|
||||
qualities: Array<string>,
|
||||
rating: number,
|
||||
ratingsCount: number,
|
||||
commentsCount: number,
|
||||
available: boolean,
|
||||
download: boolean,
|
||||
free: boolean,
|
||||
freeWithAds: boolean,
|
||||
show: {
|
||||
id: number,
|
||||
title: string,
|
||||
type: string,
|
||||
originalTitle: string,
|
||||
shortTitle: string,
|
||||
reference: string,
|
||||
age: string,
|
||||
languages: Array<string>,
|
||||
summary: string,
|
||||
image: string,
|
||||
image2x: string,
|
||||
imageHorizontal: string,
|
||||
imageHorizontal2x: string,
|
||||
url: string,
|
||||
urlPath: string,
|
||||
episodeCount: number,
|
||||
genres: Array<string>,
|
||||
copyright: string,
|
||||
rating: number,
|
||||
ratingsCount: number,
|
||||
commentsCount: number,
|
||||
qualities: Array<string>,
|
||||
simulcast: boolean,
|
||||
free: boolean,
|
||||
available: boolean,
|
||||
download: boolean,
|
||||
basedOn: string,
|
||||
tagline: Array<string>,
|
||||
firstReleaseYear: string,
|
||||
productionStudio: string,
|
||||
countryOfOrigin: string,
|
||||
productionTeam: Array<{
|
||||
role: string,
|
||||
name: string,
|
||||
}>,
|
||||
nextVideoReleaseDate: string,
|
||||
indexable: boolean
|
||||
}
|
||||
indexable: boolean
|
||||
}
|
||||
|
||||
export interface ADNEpisodes extends Array<ADNEpisode> {}
|
@ -3,7 +3,7 @@
|
||||
<div class="relative flex flex-row items-center justify-center">
|
||||
<button
|
||||
v-if="tab === 2"
|
||||
@click=";(tab = 1), (added = false), (CRselectedShow = null)"
|
||||
@click=";(tab = 1), (added = false), (CRselectedShow = null), (ADNselectedShow = null)"
|
||||
class="absolute left-0 bg-[#5c5b5b] py-1 px-3 rounded-xl flex flex-row items-center justify-center gap-0.5 hover:bg-[#4b4a4a] transition-all"
|
||||
style="-webkit-app-region: no-drag"
|
||||
>
|
||||
@ -19,7 +19,10 @@
|
||||
<option value="adn">ADN</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="isLoggedInCR && service === 'crunchyroll' || isLoggedInADN && service === 'adn'" class="relative flex flex-col">
|
||||
<div v-if="service === 'adn'" class="text-sm text-center">
|
||||
ADN downloader is still in beta and can only download ADN Germany
|
||||
</div>
|
||||
<div v-if="(isLoggedInCR && service === 'crunchyroll') || (isLoggedInADN && service === 'adn')" class="relative flex flex-col">
|
||||
<input
|
||||
v-model="search"
|
||||
@input="handleInputChange"
|
||||
@ -61,10 +64,10 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isLoggedInCR && service === 'crunchyroll' || isLoggedInADN && service === 'adn'" class="relative flex flex-col">
|
||||
<div v-if="(isLoggedInCR && service === 'crunchyroll') || (isLoggedInADN && service === 'adn')" class="relative flex flex-col">
|
||||
<input v-model="url" type="text" name="text" placeholder="URL" class="bg-[#5c5b5b] focus:outline-none px-3 py-2 rounded-xl text-sm text-center" />
|
||||
</div>
|
||||
<div v-if="isLoggedInCR && service === 'crunchyroll' || isLoggedInADN && service === 'adn'" class="relative flex flex-col">
|
||||
<div v-if="(isLoggedInCR && service === 'crunchyroll') || (isLoggedInADN && service === 'adn')" class="relative flex flex-col">
|
||||
<input
|
||||
@click="getFolderPath()"
|
||||
v-model="path"
|
||||
@ -76,14 +79,10 @@
|
||||
/>
|
||||
</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>
|
||||
<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 v-if="!isLoggedInADN && service === 'adn'" class="relative flex flex-col">
|
||||
<button @click="openADNLogin"
|
||||
class="bg-[#5c5b5b] focus:outline-none px-3 py-2 rounded-xl text-sm text-center cursor-pointer"
|
||||
>Click to Login</button>
|
||||
<button @click="openADNLogin" class="bg-[#5c5b5b] focus:outline-none px-3 py-2 rounded-xl text-sm text-center cursor-pointer">Click to Login</button>
|
||||
</div>
|
||||
<div class="relative flex flex-col mt-auto">
|
||||
<button @click="switchToSeason" class="relative py-3 border-2 rounded-xl flex flex-row items-center justify-center">
|
||||
@ -98,14 +97,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tab === 2" class="flex flex-col mt-5 gap-3.5 h-full" style="-webkit-app-region: no-drag">
|
||||
<div class="relative flex flex-col">
|
||||
<div v-if="service === 'crunchyroll'" class="relative flex flex-col">
|
||||
<select v-model="selectedSeason" name="seasons" class="bg-[#5c5b5b] focus:outline-none px-3 py-2 rounded-xl text-sm text-center cursor-pointer">
|
||||
<option v-for="season in seasons" :value="season" class="text-sm text-slate-200"
|
||||
>S{{ season.season_display_number ? season.season_display_number : season.season_number }} - {{ season.title }}</option
|
||||
>
|
||||
</select>
|
||||
</div>
|
||||
<div class="relative flex flex-col">
|
||||
<div v-if="service === 'crunchyroll'" class="relative flex flex-col">
|
||||
<select v-model="selectedStartEpisode" name="episode" class="bg-[#5c5b5b] focus:outline-none px-3 py-2 rounded-xl text-sm text-center cursor-pointer">
|
||||
<option v-for="episode in episodes" :value="episode" class="text-sm text-slate-200"
|
||||
>E{{ episode.episode_number ? episode.episode_number : episode.episode }} - {{ episode.title }}</option
|
||||
@ -119,7 +118,21 @@
|
||||
<div class="text-sm">Loading</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative flex flex-col">
|
||||
<div v-if="service === 'adn'" class="relative flex flex-col">
|
||||
<select v-model="selectedStartEpisodeADN" name="episode" class="bg-[#5c5b5b] focus:outline-none px-3 py-2 rounded-xl text-sm text-center cursor-pointer">
|
||||
<option v-for="episode in episodesADN" :value="episode" class="text-sm text-slate-200"
|
||||
>E{{ episode.shortNumber ? episode.shortNumber : episode.number }} - {{ episode.name }}</option
|
||||
>
|
||||
</select>
|
||||
<div
|
||||
class="absolute w-full h-9 bg-[#afadad] rounded-xl transition-all flex flex-row items-center justify-center gap-1 text-black"
|
||||
:class="isFetchingEpisodes ? 'opacity-100' : 'opacity-0 pointer-events-none'"
|
||||
>
|
||||
<Icon name="mdi:loading" class="h-6 w-6 animate-spin" />
|
||||
<div class="text-sm">Loading</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="service === 'crunchyroll'" class="relative flex flex-col">
|
||||
<select v-model="selectedEndEpisode" name="episode" class="bg-[#5c5b5b] focus:outline-none px-3 py-2 rounded-xl text-sm text-center cursor-pointer">
|
||||
<option
|
||||
v-if="episodes && selectedStartEpisode"
|
||||
@ -138,7 +151,26 @@
|
||||
<div class="text-sm">Loading</div></div
|
||||
>
|
||||
</div>
|
||||
<div class="relative flex flex-col select-none">
|
||||
<div v-if="service === 'adn'" class="relative flex flex-col">
|
||||
<select v-model="selectedEndEpisodeADN" name="episode" class="bg-[#5c5b5b] focus:outline-none px-3 py-2 rounded-xl text-sm text-center cursor-pointer">
|
||||
<option
|
||||
v-if="episodesADN && selectedEndEpisodeADN"
|
||||
v-for="(episode, index) in episodesADN"
|
||||
:value="episode"
|
||||
class="text-sm text-slate-200"
|
||||
:disabled="index < episodesADN.findIndex((i) => i.id === selectedStartEpisodeADN?.id)"
|
||||
>E{{ episode.shortNumber ? episode.shortNumber : episode.number }} - {{ episode.name }}</option
|
||||
>
|
||||
</select>
|
||||
<div
|
||||
class="absolute w-full h-9 bg-[#afadad] rounded-xl transition-all flex flex-row items-center justify-center gap-1 text-black"
|
||||
:class="isFetchingEpisodes ? 'opacity-100' : 'opacity-0 pointer-events-none'"
|
||||
>
|
||||
<Icon name="mdi:loading" class="h-6 w-6 animate-spin" />
|
||||
<div class="text-sm">Loading</div></div
|
||||
>
|
||||
</div>
|
||||
<div v-if="service === 'crunchyroll'" class="relative flex flex-col select-none">
|
||||
<div @click="selectDub ? (selectDub = false) : (selectDub = true)" class="bg-[#5c5b5b] focus:outline-none px-3 py-2 rounded-xl text-sm text-center cursor-pointer">
|
||||
Dubs:
|
||||
{{ selectedDubs.map((t) => t.name).join(', ') }}
|
||||
@ -156,7 +188,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative flex flex-col select-none">
|
||||
<div v-if="service === 'crunchyroll'" class="relative flex flex-col select-none">
|
||||
<div @click="selectSub ? (selectSub = false) : (selectSub = true)" class="bg-[#5c5b5b] focus:outline-none px-3 py-2 rounded-xl text-sm text-center cursor-pointer">
|
||||
Subs:
|
||||
{{ selectedSubs.length !== 0 ? selectedSubs.map((t) => t.name).join(', ') : 'No Subs selected' }}
|
||||
@ -175,7 +207,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-3">
|
||||
<div class="relative flex flex-col w-full">
|
||||
<div v-if="service === 'crunchyroll'" class="relative flex flex-col w-full">
|
||||
<select v-model="hardsub" name="episode" class="bg-[#5c5b5b] focus:outline-none px-3 py-2 rounded-xl text-sm text-center cursor-pointer" :disabled="isHardsubDisabled">
|
||||
<option :value="false" class="text-sm text-slate-200">Hardsub: false</option>
|
||||
<option :value="true" class="text-sm text-slate-200">Hardsub: true</option>
|
||||
@ -188,7 +220,7 @@
|
||||
<div class="text-sm">Loading</div></div
|
||||
>
|
||||
</div>
|
||||
<div class="relative flex flex-col w-full">
|
||||
<div v-if="service === 'crunchyroll'" class="relative flex flex-col w-full">
|
||||
<select v-model="quality" name="quality" class="bg-[#5c5b5b] focus:outline-none px-3 py-2 rounded-xl text-sm text-center cursor-pointer">
|
||||
<option :value="1080" class="text-sm text-slate-200">1080p</option>
|
||||
<option :value="720" class="text-sm text-slate-200">720p</option>
|
||||
@ -197,6 +229,13 @@
|
||||
<option :value="240" class="text-sm text-slate-200">240p</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="service === 'adn'" class="relative flex flex-col w-full">
|
||||
<select v-model="qualityADN" name="quality" class="bg-[#5c5b5b] focus:outline-none px-3 py-2 rounded-xl text-sm text-center cursor-pointer">
|
||||
<option :value="1080" class="text-sm text-slate-200">1080p</option>
|
||||
<option :value="720" class="text-sm text-slate-200">720p</option>
|
||||
<option :value="480" class="text-sm text-slate-200">480p</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="relative flex flex-col w-full">
|
||||
<select v-model="format" name="format" class="bg-[#5c5b5b] focus:outline-none px-3 py-2 rounded-xl text-sm text-center cursor-pointer">
|
||||
<option value="mp4" class="text-sm text-slate-200">MP4</option>
|
||||
@ -207,7 +246,7 @@
|
||||
|
||||
<!-- {{ CRselectedShow?.Subs.map(s=> { return locales.find(l => l.locale === s)?.name }) }}
|
||||
{{ CRselectedShow?.Dubs.map(s=> { return locales.find(l => l.locale === s)?.name }) }} -->
|
||||
<div v-if="!added" class="relative flex flex-col mt-auto">
|
||||
<div v-if="!added && service === 'crunchyroll'" class="relative flex flex-col mt-auto">
|
||||
<button @click="addToPlaylist" class="relative py-3 border-2 rounded-xl flex flex-row items-center justify-center">
|
||||
<div class="flex flex-row items-center justify-center transition-all" :class="isFetchingSeasons ? 'opacity-0' : 'opacity-100'">
|
||||
<div class="text-xl">Add to Download</div>
|
||||
@ -218,9 +257,20 @@
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="!added && service === 'adn'" class="relative flex flex-col mt-auto">
|
||||
<button @click="addToPlaylistADN" 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="text-xl">Add to Download</div>
|
||||
</div>
|
||||
<div class="absolute flex flex-row items-center justify-center gap-1 transition-all" :class="isFetchingSeasons ? 'opacity-100' : 'opacity-0'">
|
||||
<Icon name="mdi:loading" class="h-6 w-6 animate-spin" />
|
||||
<div class="text-xl">Loading</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="added" class="relative flex flex-row gap-5 mt-auto">
|
||||
<button
|
||||
@click=";(tab = 1), (added = false), (CRselectedShow = null)"
|
||||
@click=";(tab = 1), (added = false), (CRselectedShow = null), (ADNselectedShow = null)"
|
||||
class="relative py-3 border-2 rounded-xl flex flex-row items-center justify-center cursor-default w-full"
|
||||
>
|
||||
<div class="flex gap-1 flex-row items-center justify-center transition-all">
|
||||
@ -241,6 +291,8 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { searchADN } from '~/components/ADN/ListAnimes'
|
||||
import { getEpisodesWithShowIdADN } from '~/components/ADN/ListEpisodes'
|
||||
import type { ADNEpisode, ADNEpisodes } from '~/components/ADN/Types'
|
||||
import { checkAccount } from '~/components/Crunchyroll/Account'
|
||||
import { getCRSeries, searchCrunchy } from '~/components/Crunchyroll/ListAnimes'
|
||||
import { listEpisodeCrunchy } from '~/components/Crunchyroll/ListEpisodes'
|
||||
@ -305,10 +357,15 @@ const isFetchingSeasons = ref<number>(0)
|
||||
const isFetchingEpisodes = ref<number>(0)
|
||||
const isFetchingResults = ref<number>(0)
|
||||
|
||||
const episodesADN = ref<ADNEpisodes | undefined>()
|
||||
const selectedStartEpisodeADN = ref<ADNEpisode>()
|
||||
const selectedEndEpisodeADN = ref<ADNEpisode>()
|
||||
const qualityADN = ref<1080 | 720 | 480>(1080)
|
||||
|
||||
const isLoggedInCR = ref<boolean>(false)
|
||||
const isLoggedInADN = ref<boolean>(false)
|
||||
let intervalcr: NodeJS.Timeout;
|
||||
let intervaladn: NodeJS.Timeout;
|
||||
const isLoggedInADN = ref<boolean>(false)
|
||||
let intervalcr: NodeJS.Timeout
|
||||
let intervaladn: NodeJS.Timeout
|
||||
|
||||
const checkIfLoggedInCR = async () => {
|
||||
const { data, error } = await checkAccount('CR')
|
||||
@ -326,12 +383,12 @@ const checkIfLoggedInCR = async () => {
|
||||
}
|
||||
|
||||
const openCRLogin = () => {
|
||||
(window as any).myAPI.openWindow({
|
||||
title: "Crunchyroll Login",
|
||||
;(window as any).myAPI.openWindow({
|
||||
title: 'Crunchyroll Login',
|
||||
url: isProduction ? 'http://localhost:8079/crunchylogin' : 'http://localhost:3000/crunchylogin',
|
||||
width: 600,
|
||||
height: 300,
|
||||
backgroundColor: "#111111"
|
||||
backgroundColor: '#111111'
|
||||
})
|
||||
|
||||
intervalcr = setInterval(checkIfLoggedInCR, 1000)
|
||||
@ -353,18 +410,17 @@ const checkIfLoggedInADN = async () => {
|
||||
}
|
||||
|
||||
const openADNLogin = () => {
|
||||
(window as any).myAPI.openWindow({
|
||||
title: "ADN Login",
|
||||
;(window as any).myAPI.openWindow({
|
||||
title: 'ADN Login',
|
||||
url: isProduction ? 'http://localhost:8079/adnlogin' : 'http://localhost:3000/adnlogin',
|
||||
width: 600,
|
||||
height: 300,
|
||||
backgroundColor: "#111111"
|
||||
backgroundColor: '#111111'
|
||||
})
|
||||
|
||||
intervalcr = setInterval(checkIfLoggedInADN, 1000)
|
||||
}
|
||||
|
||||
|
||||
checkIfLoggedInCR()
|
||||
checkIfLoggedInADN()
|
||||
|
||||
@ -408,7 +464,7 @@ const handleInputChange = () => {
|
||||
|
||||
watch(search, () => {
|
||||
if (search.value.length === 0 || !search.value) {
|
||||
searchActive.value = false;
|
||||
searchActive.value = false
|
||||
}
|
||||
})
|
||||
|
||||
@ -451,9 +507,7 @@ watch(selectedSeason, () => {
|
||||
})
|
||||
|
||||
watch(service, () => {
|
||||
url.value = "",
|
||||
CRselectedShow.value = null,
|
||||
ADNselectedShow.value = null
|
||||
;(url.value = ''), (CRselectedShow.value = null), (ADNselectedShow.value = null)
|
||||
})
|
||||
|
||||
watch(selectedStartEpisode, () => {
|
||||
@ -468,6 +522,18 @@ watch(selectedStartEpisode, () => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(selectedStartEpisodeADN, () => {
|
||||
if (!selectedEndEpisodeADN.value) return
|
||||
if (!episodesADN.value) return
|
||||
|
||||
const indexA = episodesADN.value.findIndex((i) => i === selectedStartEpisodeADN.value)
|
||||
const indexE = episodesADN.value.findIndex((i) => i === selectedEndEpisodeADN.value)
|
||||
|
||||
if (indexA > indexE) {
|
||||
selectedEndEpisodeADN.value = episodesADN.value[indexA]
|
||||
}
|
||||
})
|
||||
|
||||
const refetchEpisodes = async () => {
|
||||
isFetchingEpisodes.value++
|
||||
if (!selectedSeason.value) {
|
||||
@ -490,6 +556,19 @@ const switchToSeason = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
if (ADNselectedShow.value) {
|
||||
episodesADN.value = await getEpisodesWithShowIdADN(ADNselectedShow.value.id)
|
||||
if (!episodesADN.value) {
|
||||
isFetchingSeasons.value--
|
||||
return
|
||||
}
|
||||
selectedStartEpisodeADN.value = episodesADN.value[0]
|
||||
selectedEndEpisodeADN.value = episodesADN.value[0]
|
||||
tab.value = 2
|
||||
selectedDubs.value = [{ locale: 'ja-JP', name: 'JP' }],
|
||||
selectedSubs.value = [{ locale: 'de-DE', name: 'DE' }]
|
||||
}
|
||||
|
||||
if (CRselectedShow.value) {
|
||||
seasons.value = await listSeasonCrunchy(CRselectedShow.value.ID)
|
||||
if (!seasons.value) {
|
||||
@ -503,6 +582,7 @@ const switchToSeason = async () => {
|
||||
selectedEndEpisode.value = episodes.value[0]
|
||||
}
|
||||
tab.value = 2
|
||||
selectedDubs.value = [{ locale: 'ja-JP', name: 'JP' }]
|
||||
}
|
||||
|
||||
if (url.value && url.value.includes('crunchyroll') && !CRselectedShow.value) {
|
||||
@ -520,9 +600,10 @@ const switchToSeason = async () => {
|
||||
selectedEndEpisode.value = episodes.value[0]
|
||||
}
|
||||
tab.value = 2
|
||||
selectedDubs.value = [{ locale: 'ja-JP', name: 'JP' }]
|
||||
}
|
||||
|
||||
;(selectedDubs.value = [{ locale: 'ja-JP', name: 'JP' }]), isFetchingSeasons.value--
|
||||
isFetchingSeasons.value--
|
||||
}
|
||||
|
||||
const toggleDub = (lang: { name: string | undefined; locale: string }) => {
|
||||
@ -571,18 +652,6 @@ const addToPlaylist = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
var selService
|
||||
|
||||
if (service.value === 'crunchyroll') {
|
||||
selService = 'CR'
|
||||
}
|
||||
|
||||
if (service.value === 'adn') {
|
||||
selService = 'ADN'
|
||||
}
|
||||
|
||||
if (!selService) return
|
||||
|
||||
const selectedEpisodes = episodes.value.slice(startEpisodeIndex, endEpisodeIndex + 1)
|
||||
|
||||
const data = {
|
||||
@ -592,7 +661,43 @@ const addToPlaylist = async () => {
|
||||
dir: path.value,
|
||||
hardsub: hardsub.value,
|
||||
quality: quality.value,
|
||||
service: selService,
|
||||
service: 'CR',
|
||||
format: format.value
|
||||
}
|
||||
|
||||
const { error } = await useFetch('http://localhost:8080/api/service/playlist', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
|
||||
if (error.value) {
|
||||
alert(error.value)
|
||||
}
|
||||
|
||||
added.value = true
|
||||
}
|
||||
|
||||
const addToPlaylistADN = async () => {
|
||||
if (!episodesADN.value) return
|
||||
|
||||
const startEpisodeIndex = episodesADN.value.findIndex((episode) => episode === selectedStartEpisodeADN.value)
|
||||
const endEpisodeIndex = episodesADN.value.findIndex((episode) => episode === selectedEndEpisodeADN.value)
|
||||
|
||||
if (startEpisodeIndex === -1 || endEpisodeIndex === -1) {
|
||||
console.error('Indexes not found.')
|
||||
return
|
||||
}
|
||||
|
||||
const selectedEpisodes = episodesADN.value.slice(startEpisodeIndex, endEpisodeIndex + 1)
|
||||
|
||||
const data = {
|
||||
episodes: selectedEpisodes,
|
||||
dubs: selectedDubs.value,
|
||||
subs: selectedSubs.value,
|
||||
dir: path.value,
|
||||
hardsub: false,
|
||||
quality: qualityADN.value,
|
||||
service: 'ADN',
|
||||
format: format.value
|
||||
}
|
||||
|
||||
|
@ -6,19 +6,27 @@
|
||||
Delete Playlist
|
||||
</button> -->
|
||||
<div v-for="p in playlist" class="flex flex-row gap-4 h-40 p-5 bg-[#636363] border-b-[1px] border-gray-400">
|
||||
<div class="flex min-w-52 w-52">
|
||||
<img :src="p.media.images.thumbnail[0].find((p) => p.height === 1080)?.source" alt="Image" class="object-cover rounded-xl" />
|
||||
<div v-if="p.service === 'CR'" class="flex min-w-52 w-52">
|
||||
<img :src="(p.media as CrunchyEpisode).images.thumbnail[0].find((p) => p.height === 1080)?.source" alt="Image" class="object-cover rounded-xl" />
|
||||
</div>
|
||||
<div v-if="p.service === 'ADN'" class="flex min-w-52 w-52">
|
||||
<img :src="(p.media as ADNEpisode).image2x" alt="Image" class="object-cover rounded-xl" />
|
||||
</div>
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex flex-row">
|
||||
<div class="text-sm capitalize">
|
||||
{{ p.status }}
|
||||
{{ p.status }}
|
||||
</div>
|
||||
<div class="text-sm capitalize ml-auto">
|
||||
{{ p.service === 'CR' ? 'Crunchyroll' : 'ADN' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm capitalize ml-auto">
|
||||
{{ p.service === 'CR' ? 'Crunchyroll' : 'ADN' }}
|
||||
<div v-if="p.service === 'CR'" class="text-base capitalize">
|
||||
{{ (p.media as CrunchyEpisode).series_title }} Season {{ (p.media as CrunchyEpisode).season_number }} Episode {{ (p.media as CrunchyEpisode).episode_number }}
|
||||
</div>
|
||||
<div v-if="p.service === 'ADN'" class="text-base capitalize">
|
||||
{{ (p.media as ADNEpisode).show.title }} Season {{ (p.media as ADNEpisode).season ? (p.media as ADNEpisode).season : 1 }} Episode {{ (p.media as ADNEpisode).shortNumber }}
|
||||
</div>
|
||||
<div class="text-base capitalize"> {{ p.media.series_title }} Season {{ p.media.season_number }} Episode {{ p.media.episode_number }} </div>
|
||||
<div class="relative w-full min-h-5 bg-[#bdbbbb] mt-1 rounded">
|
||||
<div
|
||||
v-if="p.partsleft && p.status === 'downloading'"
|
||||
@ -38,19 +46,20 @@
|
||||
<div v-if="p.downloadspeed && p.status === 'downloading'" class="text-sm">{{ p.downloadspeed }} MB/s</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> </div
|
||||
>s
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ADNEpisode } from '~/components/ADN/Types'
|
||||
import type { CrunchyEpisode } from '~/components/Episode/Types'
|
||||
|
||||
const playlist = ref<
|
||||
Array<{
|
||||
id: number
|
||||
status: string
|
||||
media: CrunchyEpisode
|
||||
media: CrunchyEpisode | ADNEpisode
|
||||
dub: Array<{ locale: string; name: string }>
|
||||
sub: Array<{ locale: string; name: string }>
|
||||
dir: string
|
||||
@ -68,7 +77,7 @@ const getPlaylist = async () => {
|
||||
Array<{
|
||||
id: number
|
||||
status: string
|
||||
media: CrunchyEpisode
|
||||
media: CrunchyEpisode | ADNEpisode
|
||||
dub: Array<{ locale: string; name: string }>
|
||||
sub: Array<{ locale: string; name: string }>
|
||||
dir: string
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { app } from 'electron'
|
||||
import { Sequelize, DataTypes, ModelDefined } from 'sequelize'
|
||||
import { CrunchyEpisode } from '../types/crunchyroll'
|
||||
import { ADNEpisode } from '../types/adn'
|
||||
|
||||
const sequelize = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
@ -29,7 +30,7 @@ interface AccountCreateAttributes {
|
||||
interface PlaylistAttributes {
|
||||
id: number
|
||||
status: 'waiting' | 'preparing' | 'downloading' | 'merging' | 'completed' | 'failed'
|
||||
media: CrunchyEpisode
|
||||
media: CrunchyEpisode | ADNEpisode
|
||||
dub: Array<string>
|
||||
sub: Array<string>
|
||||
hardsub: boolean
|
||||
@ -41,7 +42,7 @@ interface PlaylistAttributes {
|
||||
}
|
||||
|
||||
interface PlaylistCreateAttributes {
|
||||
media: CrunchyEpisode
|
||||
media: CrunchyEpisode | ADNEpisode
|
||||
dub: Array<string>
|
||||
sub: Array<string>
|
||||
dir: string
|
||||
|
@ -1,394 +1,16 @@
|
||||
import JSEncrypt from 'jsencrypt'
|
||||
import CryptoJS from 'crypto-js'
|
||||
import { server } from '../../api'
|
||||
import { ADNPlayerConfig } from '../../types/adn'
|
||||
import { ADNLink, ADNPlayerConfig } from '../../types/adn'
|
||||
import { messageBox } from '../../../electron/background'
|
||||
import { useFetch } from '../useFetch'
|
||||
|
||||
// export async function getShowADN(q: number) {
|
||||
// const cachedData = server.CacheController.get(`getshowadn-${q}`)
|
||||
|
||||
// if (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
|
||||
// }
|
||||
// }
|
||||
import { loggedInCheck } from '../service/service.service'
|
||||
import { parse as mpdParse, parse } from 'mpd-parser'
|
||||
|
||||
export async function adnLogin(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
|
||||
accessToken: string
|
||||
}
|
||||
| undefined = server.CacheController.get('adntoken')
|
||||
|
||||
@ -432,11 +54,11 @@ async function adnLoginFetch(user: string, passw: string) {
|
||||
}
|
||||
|
||||
const { data, error } = await useFetch<{
|
||||
accessToken: string;
|
||||
accessToken: string
|
||||
}>('https://gw.api.animationdigitalnetwork.fr/authentication/login', {
|
||||
type: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
header: headers,
|
||||
header: headers
|
||||
})
|
||||
|
||||
if (error) {
|
||||
@ -449,3 +71,238 @@ async function adnLoginFetch(user: string, passw: string) {
|
||||
|
||||
return { data: data, error: null }
|
||||
}
|
||||
|
||||
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 getPlayerConfigADN(id: number) {
|
||||
|
||||
const account = await loggedInCheck('ADN')
|
||||
|
||||
if (!account) return
|
||||
|
||||
const token = await adnLogin(account.username, account.password)
|
||||
|
||||
if (!token.data?.accessToken) return
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://gw.api.animationdigitalnetwork.fr/player/video/${id}/configuration`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'x-target-distribution': 'de',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token.data.accessToken}`
|
||||
}
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data: ADNPlayerConfig = JSON.parse(await response.text())
|
||||
|
||||
return data
|
||||
} else {
|
||||
throw new Error('Failed to fetch ADN')
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(e as string)
|
||||
}
|
||||
}
|
||||
|
||||
async function getPlayerToken(id: number) {
|
||||
const r = await getPlayerConfigADN(id)
|
||||
|
||||
if (!r) return
|
||||
|
||||
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(id: number) {
|
||||
const token = await getPlayerToken(id)
|
||||
|
||||
if (!token) return
|
||||
|
||||
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 adnGetPlaylist(animeid: number) {
|
||||
const token = await getPlayerEncryptedToken(animeid)
|
||||
|
||||
if (!token) return
|
||||
|
||||
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: ADNLink = await JSON.parse(await response.text())
|
||||
return { data: data, secret: token.random }
|
||||
} else {
|
||||
const data: ADNLink = JSON.parse(await response.text())
|
||||
|
||||
return { data: data, secret: token.random }
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(e as string)
|
||||
}
|
||||
}
|
||||
|
||||
export async function adnGetM3U8Playlist(url: string) {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data: { location: string } = await JSON.parse(await response.text())
|
||||
|
||||
const mu8 = await fetch(data.location, {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
const playlist = await mu8.text()
|
||||
|
||||
const url = await extractURLFromPlaylist(playlist)
|
||||
|
||||
const partsraw = await fetch(url, {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
const parts = await partsraw.text()
|
||||
|
||||
const baseurl = await extractBaseURL(url);
|
||||
|
||||
const partsArray = await extractSequenceURLs(parts, baseurl)
|
||||
|
||||
return partsArray
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(e as string)
|
||||
}
|
||||
}
|
||||
|
||||
async function extractURLFromPlaylist(playlist: string) {
|
||||
var startIndex = playlist.indexOf("http");
|
||||
var endIndex = playlist.indexOf(" ", startIndex);
|
||||
var extractedURL = playlist.slice(startIndex, endIndex);
|
||||
return extractedURL;
|
||||
}
|
||||
|
||||
async function extractBaseURL(playlistURL: string) {
|
||||
var baseURL = playlistURL.substring(0, playlistURL.lastIndexOf("/") + 1);
|
||||
return baseURL;
|
||||
}
|
||||
|
||||
async function extractSequenceURLs(playlistText: string, baseURL: string) {
|
||||
var sequenceURLs: Array<{ filename: string, url: string }> = [];
|
||||
var matches = playlistText.match(/sequence_\d+\.ts/g);
|
||||
if (matches) {
|
||||
matches.forEach(function(match) {
|
||||
sequenceURLs.push({ filename: match, url: baseURL + match });
|
||||
});
|
||||
}
|
||||
return sequenceURLs;
|
||||
}
|
||||
|
||||
export async function parseSubs(url: string, secret: string) {
|
||||
const response = await fetch(url)
|
||||
|
||||
const data = await response.text()
|
||||
|
||||
var key = secret + '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
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ 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 { downloadADNSub, downloadCRSub } from '../../services/subs'
|
||||
import { CrunchyEpisode } from '../../types/crunchyroll'
|
||||
import { crunchyGetPlaylist, crunchyGetPlaylistMPD } from '../crunchyroll/crunchyroll.service'
|
||||
import fs from 'fs'
|
||||
@ -10,6 +10,8 @@ var cron = require('node-cron')
|
||||
import { Readable } from 'stream'
|
||||
import { finished } from 'stream/promises'
|
||||
import Ffmpeg from 'fluent-ffmpeg'
|
||||
import { adnGetM3U8Playlist, adnGetPlaylist } from '../adn/adn.service'
|
||||
import { ADNEpisode } from '../../types/adn'
|
||||
const ffmpegPath = require('ffmpeg-static').replace('app.asar', 'app.asar.unpacked')
|
||||
const ffprobePath = require('ffprobe-static').path.replace('app.asar', 'app.asar.unpacked')
|
||||
|
||||
@ -104,19 +106,34 @@ async function checkPlaylists() {
|
||||
isDownloading++
|
||||
if (e.dataValues.service === 'CR') {
|
||||
downloadCrunchyrollPlaylist(
|
||||
e.dataValues.media.id,
|
||||
(e.dataValues.media as CrunchyEpisode).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.media as CrunchyEpisode).series_title,
|
||||
(e.dataValues.media as CrunchyEpisode).season_number,
|
||||
(e.dataValues.media as CrunchyEpisode).episode_number,
|
||||
e.dataValues.quality,
|
||||
e.dataValues.dir,
|
||||
e.dataValues.format
|
||||
)
|
||||
}
|
||||
if (e.dataValues.service === 'ADN') {
|
||||
downloadADNPlaylist(
|
||||
(e.dataValues.media as ADNEpisode).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.id,
|
||||
(e.dataValues.media as ADNEpisode).show.title,
|
||||
(e.dataValues.media as ADNEpisode).season,
|
||||
(e.dataValues.media as ADNEpisode).shortNumber,
|
||||
e.dataValues.quality,
|
||||
e.dataValues.dir,
|
||||
e.dataValues.format
|
||||
)
|
||||
console.log(e.dataValues.media)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -125,6 +142,98 @@ cron.schedule('*/2 * * * * *', () => {
|
||||
checkPlaylists()
|
||||
})
|
||||
|
||||
// Download ADN Playlist
|
||||
export async function downloadADNPlaylist(
|
||||
e: number,
|
||||
dubs: Array<string>,
|
||||
subs: Array<string>,
|
||||
downloadID: number,
|
||||
name: string,
|
||||
season: string,
|
||||
episode: string,
|
||||
quality: 1080 | 720 | 480 | 360 | 240,
|
||||
downloadPath: string,
|
||||
format: 'mp4' | 'mkv'
|
||||
) {
|
||||
downloading.push({
|
||||
id: downloadID,
|
||||
downloadedParts: 0,
|
||||
partsToDownload: 0,
|
||||
downloadSpeed: 0
|
||||
})
|
||||
|
||||
if (!season) {
|
||||
season = "1"
|
||||
}
|
||||
|
||||
await updatePlaylistByID(downloadID, 'downloading')
|
||||
|
||||
var playlist = await adnGetPlaylist(e)
|
||||
|
||||
const subFolder = await createFolder()
|
||||
|
||||
const videoFolder = await createFolder()
|
||||
|
||||
const seasonFolder = await createFolderName(`${name.replace(/[/\\?%*:|"<>]/g, '')} Season ${season}`, downloadPath)
|
||||
|
||||
const subDownload = async () => {
|
||||
if (!playlist) return
|
||||
const sbs: Array<string> = []
|
||||
const name = await downloadADNSub(playlist.data.links.subtitles.all, subFolder, playlist.secret)
|
||||
sbs.push(name)
|
||||
return sbs
|
||||
}
|
||||
|
||||
const downloadVideo = async () => {
|
||||
var code
|
||||
|
||||
if (!playlist) return
|
||||
|
||||
var link: string = '';
|
||||
|
||||
switch (quality) {
|
||||
case 1080:
|
||||
link = playlist.data.links.streaming.vostde.fhd
|
||||
break
|
||||
case 720:
|
||||
link = playlist.data.links.streaming.vostde.hd
|
||||
break
|
||||
case 480:
|
||||
link = playlist.data.links.streaming.vostde.sd
|
||||
break
|
||||
}
|
||||
|
||||
if (!link) return
|
||||
|
||||
var m3u8 = await adnGetM3U8Playlist(link)
|
||||
|
||||
if (!m3u8) return
|
||||
|
||||
const dn = downloading.find((i) => i.id === downloadID)
|
||||
|
||||
if (dn) {
|
||||
dn.partsToDownload = m3u8.length
|
||||
}
|
||||
|
||||
const file = await downloadParts(m3u8, downloadID, videoFolder)
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
const [subss, file] = await Promise.all([subDownload(), downloadVideo()])
|
||||
|
||||
if (!subss) return
|
||||
|
||||
await mergeVideoFile(file as string, [], [], String(playlist?.data.video.guid), seasonFolder, `${name.replace(/[/\\?%*:|"<>]/g, '')} Season ${season} Episode ${episode}`, format)
|
||||
|
||||
await updatePlaylistByID(downloadID, 'completed')
|
||||
|
||||
await deleteFolder(subFolder)
|
||||
await deleteFolder(videoFolder)
|
||||
|
||||
return playlist
|
||||
}
|
||||
|
||||
// Download Crunchyroll Playlist
|
||||
export async function downloadCrunchyrollPlaylist(
|
||||
e: string,
|
||||
@ -509,7 +618,7 @@ async function mergeVideoFile(video: string, audios: Array<string>, subs: Array<
|
||||
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')
|
||||
options.push('-c:s mov_text')
|
||||
}
|
||||
|
||||
for (const [index, a] of audios.entries()) {
|
||||
|
@ -2,6 +2,7 @@ import fs from 'fs'
|
||||
import { parse, stringify } from 'ass-compiler'
|
||||
import { Readable } from 'stream'
|
||||
import { finished } from 'stream/promises'
|
||||
import CryptoJS from 'crypto-js'
|
||||
|
||||
export async function downloadCRSub(
|
||||
sub: {
|
||||
@ -37,3 +38,60 @@ export async function downloadCRSub(
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
export async function downloadADNSub(
|
||||
link: string,
|
||||
dir: string,
|
||||
secret: string
|
||||
) {
|
||||
const path = `${dir}/de-DE.ass`
|
||||
|
||||
const stream = fs.createWriteStream(path)
|
||||
const subURLFetch = await fetch(link)
|
||||
const subURL: {
|
||||
location: string
|
||||
} = JSON.parse(await subURLFetch.text())
|
||||
|
||||
const rawSubsFetch = await fetch(subURL.location)
|
||||
const rawSubs = await rawSubsFetch.text()
|
||||
|
||||
const subs = await ADNparseSub(rawSubs, secret)
|
||||
|
||||
// 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([subs])
|
||||
|
||||
await finished(readableStream.pipe(stream))
|
||||
console.log(`Sub downloaded`)
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
export async function ADNparseSub(raw: string, secret: string) {
|
||||
var key = secret + '7fac1178830cfe0c'
|
||||
|
||||
console.log(key)
|
||||
|
||||
var parsedSubtitle = CryptoJS.enc.Base64.parse(raw.substring(0, 24))
|
||||
var sec = CryptoJS.enc.Hex.parse(key)
|
||||
var som = raw.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,37 +1,145 @@
|
||||
export interface ADNPlayerConfig {
|
||||
player: {
|
||||
image: string,
|
||||
options: {
|
||||
user: {
|
||||
hasAccess: true,
|
||||
profileId: number,
|
||||
refreshToken: string,
|
||||
refreshTokenUrl: string
|
||||
},
|
||||
chromecast: {
|
||||
appId: string,
|
||||
refreshTokenUrl: string
|
||||
},
|
||||
ios: {
|
||||
videoUrl: string,
|
||||
appUrl: string,
|
||||
title: string
|
||||
},
|
||||
video: {
|
||||
startDate: string,
|
||||
currentDate: string,
|
||||
available: boolean,
|
||||
free: boolean,
|
||||
url: string
|
||||
},
|
||||
dock: Array<string>,
|
||||
preference: {
|
||||
quality: string,
|
||||
autoplay: boolean,
|
||||
language: string,
|
||||
green: boolean
|
||||
}
|
||||
}
|
||||
player: {
|
||||
image: string
|
||||
options: {
|
||||
user: {
|
||||
hasAccess: true
|
||||
profileId: number
|
||||
refreshToken: string
|
||||
refreshTokenUrl: string
|
||||
}
|
||||
chromecast: {
|
||||
appId: string
|
||||
refreshTokenUrl: string
|
||||
}
|
||||
ios: {
|
||||
videoUrl: string
|
||||
appUrl: string
|
||||
title: string
|
||||
}
|
||||
video: {
|
||||
startDate: string
|
||||
currentDate: string
|
||||
available: boolean
|
||||
free: boolean
|
||||
url: string
|
||||
}
|
||||
dock: Array<string>
|
||||
preference: {
|
||||
quality: string
|
||||
autoplay: boolean
|
||||
language: string
|
||||
green: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
export interface ADNLink {
|
||||
links: {
|
||||
streaming: {
|
||||
vostde: {
|
||||
mobile: string
|
||||
sd: string
|
||||
hd: string
|
||||
fhd: string
|
||||
auto: string
|
||||
}
|
||||
}
|
||||
subtitles: {
|
||||
all: string
|
||||
}
|
||||
history: string
|
||||
nextVideoUrl: string
|
||||
previousVideoUrl: string
|
||||
}
|
||||
video: {
|
||||
guid: string
|
||||
id: number
|
||||
currentTime: number
|
||||
duration: number
|
||||
url: string
|
||||
image: string
|
||||
tcEpisodeStart: string
|
||||
tcEpisodeEnd: string
|
||||
tcIntroStart: string
|
||||
tcIntroEnd: string
|
||||
tcEndingStart: string
|
||||
tcEndingEnd: string
|
||||
}
|
||||
metadata: {
|
||||
title: string
|
||||
subtitle: string
|
||||
summary: string
|
||||
rating: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface ADNEpisode {
|
||||
id: number,
|
||||
title: string,
|
||||
name: string,
|
||||
number: string,
|
||||
shortNumber: string,
|
||||
season: string,
|
||||
reference: string,
|
||||
type: string,
|
||||
order: number,
|
||||
image: string,
|
||||
image2x: string,
|
||||
summary: string,
|
||||
releaseDate: string,
|
||||
duration: number,
|
||||
url: string,
|
||||
urlPath: string,
|
||||
embeddedUrl: string,
|
||||
languages: Array<string>,
|
||||
qualities: Array<string>,
|
||||
rating: number,
|
||||
ratingsCount: number,
|
||||
commentsCount: number,
|
||||
available: boolean,
|
||||
download: boolean,
|
||||
free: boolean,
|
||||
freeWithAds: boolean,
|
||||
show: {
|
||||
id: number,
|
||||
title: string,
|
||||
type: string,
|
||||
originalTitle: string,
|
||||
shortTitle: string,
|
||||
reference: string,
|
||||
age: string,
|
||||
languages: Array<string>,
|
||||
summary: string,
|
||||
image: string,
|
||||
image2x: string,
|
||||
imageHorizontal: string,
|
||||
imageHorizontal2x: string,
|
||||
url: string,
|
||||
urlPath: string,
|
||||
episodeCount: number,
|
||||
genres: Array<string>,
|
||||
copyright: string,
|
||||
rating: number,
|
||||
ratingsCount: number,
|
||||
commentsCount: number,
|
||||
qualities: Array<string>,
|
||||
simulcast: boolean,
|
||||
free: boolean,
|
||||
available: boolean,
|
||||
download: boolean,
|
||||
basedOn: string,
|
||||
tagline: Array<string>,
|
||||
firstReleaseYear: string,
|
||||
productionStudio: string,
|
||||
countryOfOrigin: string,
|
||||
productionTeam: Array<{
|
||||
role: string,
|
||||
name: string,
|
||||
}>,
|
||||
nextVideoReleaseDate: string,
|
||||
indexable: boolean
|
||||
}
|
||||
indexable: boolean
|
||||
}
|
||||
|
Reference in New Issue
Block a user