diff --git a/package.json b/package.json index de2b70e..a1967eb 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bce6856..430a4a4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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'} diff --git a/src/api/routes/adn/adn.controller.ts b/src/api/routes/adn/adn.controller.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/api/routes/adn/adn.route.ts b/src/api/routes/adn/adn.route.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/api/routes/adn/adn.service.ts b/src/api/routes/adn/adn.service.ts new file mode 100644 index 0000000..dd7d9c0 --- /dev/null +++ b/src/api/routes/adn/adn.service.ts @@ -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 + } = 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 + } = 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 + } = 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 + } = 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 + } +} diff --git a/src/api/types/adn.ts b/src/api/types/adn.ts new file mode 100644 index 0000000..eb2052a --- /dev/null +++ b/src/api/types/adn.ts @@ -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, + preference: { + quality: string, + autoplay: boolean, + language: string, + green: boolean + } + } + } + +} \ No newline at end of file