added initial adn downloader functions

This commit is contained in:
Daniel Haller 2024-04-21 23:39:52 +02:00
parent f3e749ce79
commit 195b7aee15
6 changed files with 438 additions and 0 deletions

View File

@ -37,10 +37,12 @@
"dependencies": {
"@fastify/cors": "^9.0.1",
"@pinia/nuxt": "^0.4.11",
"@types/crypto-js": "^4.2.2",
"@types/fluent-ffmpeg": "^2.1.24",
"@types/node-cron": "^3.0.11",
"@types/uuid": "^9.0.8",
"ass-compiler": "^0.1.11",
"crypto-js": "^4.2.0",
"electron-log": "^5.1.2",
"electron-updater": "^5.3.0",
"express": "^4.19.2",
@ -48,6 +50,7 @@
"ffmpeg-static": "^5.2.0",
"ffprobe-static": "^3.1.0",
"fluent-ffmpeg": "^2.1.2",
"jsencrypt": "^3.3.2",
"mpd-parser": "^1.3.0",
"node-cache": "^5.1.2",
"node-cron": "^3.0.3",

21
pnpm-lock.yaml generated
View File

@ -11,6 +11,9 @@ dependencies:
'@pinia/nuxt':
specifier: ^0.4.11
version: 0.4.11(typescript@5.4.4)(vue@3.4.21)
'@types/crypto-js':
specifier: ^4.2.2
version: 4.2.2
'@types/fluent-ffmpeg':
specifier: ^2.1.24
version: 2.1.24
@ -23,6 +26,9 @@ dependencies:
ass-compiler:
specifier: ^0.1.11
version: 0.1.11
crypto-js:
specifier: ^4.2.0
version: 4.2.0
electron-log:
specifier: ^5.1.2
version: 5.1.2
@ -44,6 +50,9 @@ dependencies:
fluent-ffmpeg:
specifier: ^2.1.2
version: 2.1.2
jsencrypt:
specifier: ^3.3.2
version: 3.3.2
mpd-parser:
specifier: ^1.3.0
version: 1.3.0
@ -2068,6 +2077,10 @@ packages:
'@types/node': 20.12.5
dev: true
/@types/crypto-js@4.2.2:
resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
dev: false
/@types/debug@4.1.12:
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
dependencies:
@ -4338,6 +4351,10 @@ packages:
optional: true
dev: true
/crypto-js@4.2.0:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
dev: false
/css-declaration-sorter@7.2.0(postcss@8.4.38):
resolution: {integrity: sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==}
engines: {node: ^14 || ^16 || >=18}
@ -6954,6 +6971,10 @@ packages:
resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==}
requiresBuild: true
/jsencrypt@3.3.2:
resolution: {integrity: sha512-arQR1R1ESGdAxY7ZheWr12wCaF2yF47v5qpB76TtV64H1pyGudk9Hvw8Y9tb/FiTIaaTRUyaSnm5T/Y53Ghm/A==}
dev: false
/jsesc@2.5.2:
resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==}
engines: {node: '>=4'}

View File

View File

View File

@ -0,0 +1,377 @@
import JSEncrypt from 'jsencrypt'
import CryptoJS from 'crypto-js'
import { server } from '../../api'
import { ADNPlayerConfig } from '../../types/adn'
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
}
}

37
src/api/types/adn.ts Normal file
View File

@ -0,0 +1,37 @@
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
}
}
}
}