From 5e2d94703bc9524dd79fced8804a32d420d115e7 Mon Sep 17 00:00:00 2001 From: stratuma Date: Sun, 19 May 2024 02:44:04 +0200 Subject: [PATCH] added proxy and proxy result merging system --- components/Crunchyroll/Account.ts | 7 +- components/Crunchyroll/ListAnimes.ts | 96 ++++++- components/Crunchyroll/ListEpisodes.ts | 4 +- components/Crunchyroll/ListSeasons.ts | 25 +- components/Crunchyroll/Proxy.ts | 9 + components/Crunchyroll/Types.ts | 11 + components/Search/Types.ts | 1 + components/Settings/Main.vue | 4 +- components/Settings/Proxy.vue | 68 +++++ pages/addanime.vue | 13 +- pages/index.vue | 2 +- pages/settings.vue | 5 +- .../crunchyroll/crunchyroll.controller.ts | 11 +- .../routes/crunchyroll/crunchyroll.service.ts | 249 ++++++++++++++---- src/api/routes/service/service.controller.ts | 38 ++- src/api/routes/service/service.route.ts | 18 +- src/api/routes/service/service.service.ts | 40 ++- src/api/types/crunchyroll.ts | 45 ++++ 18 files changed, 561 insertions(+), 85 deletions(-) create mode 100644 components/Crunchyroll/Proxy.ts create mode 100644 components/Settings/Proxy.vue diff --git a/components/Crunchyroll/Account.ts b/components/Crunchyroll/Account.ts index 3f712f5..43aebaa 100644 --- a/components/Crunchyroll/Account.ts +++ b/components/Crunchyroll/Account.ts @@ -1,8 +1,11 @@ import type { CrunchyLogin } from './Types' -export async function crunchyLogin() { +export async function crunchyLogin(geo: string) { const { data, error } = await useFetch('http://localhost:9941/api/crunchyroll/login', { - method: 'POST' + method: 'POST', + query: { + geo: geo + } }) return { data, error } diff --git a/components/Crunchyroll/ListAnimes.ts b/components/Crunchyroll/ListAnimes.ts index 865c52c..19063e6 100644 --- a/components/Crunchyroll/ListAnimes.ts +++ b/components/Crunchyroll/ListAnimes.ts @@ -1,9 +1,13 @@ import type { CrunchyrollSearchResults } from '../Search/Types' import { crunchyLogin } from './Account' +import { getProxies } from './Proxy' import type { CrunchyAnimeFetch, CrunchySearchFetch } from './Types' export async function searchCrunchy(q: string) { - const { data: token, error: tokenerror } = await crunchyLogin() + + const { data: proxies } = await getProxies() + + const { data: token, error: tokenerror } = await crunchyLogin('LOCAL') if (!token.value) { return @@ -27,6 +31,56 @@ export async function searchCrunchy(q: string) { throw new Error(JSON.stringify(error.value)) } + if (proxies.value) { + for (const p of proxies.value) { + if (p.status !== 'offline') { + const { data: tokeng, error: tokenerrorg } = await crunchyLogin(p.code) + + if (!tokeng.value) { + return + } + + const { data: fdata, error: ferror } = await useFetch(`https://beta-api.crunchyroll.com/content/v2/discover/search`, { + method: 'GET', + headers: { + Authorization: `Bearer ${tokeng.value.access_token}` + }, + query: { + q: q, + n: 100, + type: 'series', + ratings: false + } + }) + + if (ferror.value) { + console.error(ferror.value) + throw new Error(JSON.stringify(ferror.value)) + } + + if (fdata.value) { + for (const r of fdata.value.data[0].items) { + if (!data.value?.data[0].items.find((d) => d.id === r.id)) { + r.geo = p.code + data.value?.data[0].items.push(r) + } else { + for (const l of r.series_metadata.audio_locales) { + if (!data.value.data[0].items.find(d => d.id === r.id)?.series_metadata.audio_locales.find(loc => loc === l)) { + data.value.data[0].items.find(d => d.id === r.id)?.series_metadata.audio_locales.push(l) + } + } + for (const l of r.series_metadata.subtitle_locales) { + if (!data.value.data[0].items.find(d => d.id === r.id)?.series_metadata.subtitle_locales.find(loc => loc === l)) { + data.value.data[0].items.find(d => d.id === r.id)?.series_metadata.subtitle_locales.push(l) + } + } + } + } + } + } + } + } + if (!data.value) return var results: CrunchyrollSearchResults = [] @@ -43,7 +97,8 @@ export async function searchCrunchy(q: string) { Seasons: result.series_metadata.season_count, PEGI: result.series_metadata.maturity_ratings, Year: result.series_metadata.series_launch_year, - Images: result.images + Images: result.images, + Geo: result.geo }) } @@ -51,7 +106,9 @@ export async function searchCrunchy(q: string) { } export async function getCRSeries(q: string) { - const { data: token, error: tokenerror } = await crunchyLogin() + const { data: proxies } = await getProxies() + + const { data: token, error: tokenerror } = await crunchyLogin('LOCAL') if (!token.value) { return @@ -69,6 +126,36 @@ export async function getCRSeries(q: string) { throw new Error(JSON.stringify(error.value)) } + if (!data.value && proxies.value) { + for (const p of proxies.value) { + if (p.status !== 'offline') { + const { data: tokeng, error: tokenerrorg } = await crunchyLogin(p.code) + + if (!tokeng.value) { + return + } + + const { data: fdata, error: ferror } = await useFetch(`https://beta-api.crunchyroll.com/content/v2/cms/series/${q}`, { + method: 'GET', + headers: { + Authorization: `Bearer ${tokeng.value.access_token}` + } + }) + + if (ferror.value) { + console.error(ferror.value) + throw new Error(JSON.stringify(ferror.value)) + } + + if (fdata.value) { + fdata.value.data[0].geo = p.code + } + + data.value = fdata.value + } + } + } + if (!data.value) return const anime = data.value.data[0] @@ -84,6 +171,7 @@ export async function getCRSeries(q: string) { Seasons: anime.season_count, PEGI: anime.maturity_ratings, Year: anime.series_launch_year, - Images: anime.images + Images: anime.images, + Geo: undefined } } diff --git a/components/Crunchyroll/ListEpisodes.ts b/components/Crunchyroll/ListEpisodes.ts index 0f70013..b8b0865 100644 --- a/components/Crunchyroll/ListEpisodes.ts +++ b/components/Crunchyroll/ListEpisodes.ts @@ -1,8 +1,8 @@ import { crunchyLogin } from './Account' import type { CrunchyEpisodesFetch } from './Types' -export async function listEpisodeCrunchy(q: string) { - const { data: token, error: tokenerror } = await crunchyLogin() +export async function listEpisodeCrunchy(q: string, geo: string | undefined) { + const { data: token, error: tokenerror } = await crunchyLogin(geo ? geo : 'LOCAL') if (!token.value) { return diff --git a/components/Crunchyroll/ListSeasons.ts b/components/Crunchyroll/ListSeasons.ts index e783c74..ad320fc 100644 --- a/components/Crunchyroll/ListSeasons.ts +++ b/components/Crunchyroll/ListSeasons.ts @@ -1,8 +1,11 @@ import { crunchyLogin } from './Account' +import { getProxies } from './Proxy' import type { CrunchySeasonsFetch } from './Types' -export async function listSeasonCrunchy(q: string) { - const { data: token, error: tokenerror } = await crunchyLogin() +export async function listSeasonCrunchy(q: string, geo: string | undefined) { + const { data: proxies } = await getProxies() + + const { data: token, error: tokenerror } = await crunchyLogin(geo ? geo : 'LOCAL') if (!token.value) { return @@ -22,5 +25,23 @@ export async function listSeasonCrunchy(q: string) { if (!data.value) return + if (proxies.value) { + for (const p of proxies.value) { + if (p.status !== 'offline') { + const { data: gdata, error: gerror } = await useFetch(`https://beta-api.crunchyroll.com/content/v2/cms/series/${q}/seasons`, { + method: 'GET', + headers: { + Authorization: `Bearer ${token.value.access_token}` + } + }) + + if (gerror.value) { + console.error(error.value) + throw new Error(JSON.stringify(error.value)) + } + } + } + } + return data.value.data } diff --git a/components/Crunchyroll/Proxy.ts b/components/Crunchyroll/Proxy.ts new file mode 100644 index 0000000..2120932 --- /dev/null +++ b/components/Crunchyroll/Proxy.ts @@ -0,0 +1,9 @@ +import type { Proxies } from "./Types" + +export async function getProxies() { + const { data, error } = await useFetch('http://localhost:9941/api/service/proxies', { + method: 'GET', + }) + + return { data, error } +} \ No newline at end of file diff --git a/components/Crunchyroll/Types.ts b/components/Crunchyroll/Types.ts index c590416..90c4318 100644 --- a/components/Crunchyroll/Types.ts +++ b/components/Crunchyroll/Types.ts @@ -53,6 +53,7 @@ export interface CrunchySearchFetch { } linked_resource_key: string type: string + geo: string | undefined }> }> } @@ -107,6 +108,7 @@ export interface CrunchyAnimeFetch { } linked_resource_key: string type: string + geo: string | undefined }> } @@ -236,3 +238,12 @@ export interface CrunchyEpisodesFetch { versions_considered: boolean } } + +export interface Proxy { + name: string + code: string + url: string + status: string +} + +export interface Proxies extends Array {} diff --git a/components/Search/Types.ts b/components/Search/Types.ts index 6830a4b..37b3eee 100644 --- a/components/Search/Types.ts +++ b/components/Search/Types.ts @@ -27,6 +27,7 @@ export interface CrunchyrollSearchResult { }> > } + Geo: string | undefined } export interface CrunchyrollSearchResults extends Array {} diff --git a/components/Settings/Main.vue b/components/Settings/Main.vue index 1cabf91..e5851d6 100644 --- a/components/Settings/Main.vue +++ b/components/Settings/Main.vue @@ -25,7 +25,7 @@ v-for="l in locales" @click="toggleDub(l)" class="flex flex-row items-center justify-center gap-3 py-2 rounded-xl text-sm" - :class="dubLocales.find((i) => i.locale === l.locale) ? 'bg-[#424242]' : 'hover:bg-[#747474]'" + :class="dubLocales && dubLocales.find((i) => i.locale === l.locale) ? 'bg-[#424242]' : 'hover:bg-[#747474]'" > {{ l.name }} @@ -38,7 +38,7 @@ v-for="l in locales" @click="toggleSub(l)" class="flex flex-row items-center justify-center gap-3 py-2 rounded-xl text-sm" - :class="subLocales.find((i) => i.locale === l.locale) ? 'bg-[#424242]' : 'hover:bg-[#747474]'" + :class="subLocales && subLocales.find((i) => i.locale === l.locale) ? 'bg-[#424242]' : 'hover:bg-[#747474]'" > {{ l.name }} diff --git a/components/Settings/Proxy.vue b/components/Settings/Proxy.vue new file mode 100644 index 0000000..287602d --- /dev/null +++ b/components/Settings/Proxy.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/pages/addanime.vue b/pages/addanime.vue index c02d4c1..54bfd1e 100644 --- a/pages/addanime.vue +++ b/pages/addanime.vue @@ -594,7 +594,9 @@ const refetchEpisodes = async () => { return } - episodes.value = await listEpisodeCrunchy(selectedSeason.value.id) + if (!CRselectedShow.value) return + + episodes.value = await listEpisodeCrunchy(selectedSeason.value.id, CRselectedShow.value.Geo) if (episodes.value) { selectedStartEpisode.value = episodes.value[0] selectedEndEpisode.value = episodes.value[0] @@ -623,13 +625,13 @@ const switchToSeason = async () => { } if (CRselectedShow.value) { - seasons.value = await listSeasonCrunchy(CRselectedShow.value.ID) + seasons.value = await listSeasonCrunchy(CRselectedShow.value.ID, CRselectedShow.value.Geo) if (!seasons.value) { isFetchingSeasons.value-- return } selectedSeason.value = seasons.value[0] - episodes.value = await listEpisodeCrunchy(selectedSeason.value.id) + episodes.value = await listEpisodeCrunchy(selectedSeason.value.id, CRselectedShow.value.Geo) if (episodes.value) { selectedStartEpisode.value = episodes.value[0] selectedEndEpisode.value = episodes.value[0] @@ -642,13 +644,14 @@ const switchToSeason = async () => { if (url.value && url.value.includes('crunchyroll') && !CRselectedShow.value) { const seriesID = url.value.split('/') CRselectedShow.value = await getCRSeries(seriesID[5]) - seasons.value = await listSeasonCrunchy(seriesID[5]) + if (!CRselectedShow.value) return + seasons.value = await listSeasonCrunchy(CRselectedShow.value.ID, CRselectedShow.value.Geo) if (!seasons.value) { isFetchingSeasons.value-- return } selectedSeason.value = seasons.value[0] - episodes.value = await listEpisodeCrunchy(selectedSeason.value.id) + episodes.value = await listEpisodeCrunchy(selectedSeason.value.id, CRselectedShow.value.Geo) if (episodes.value) { selectedStartEpisode.value = episodes.value[0] selectedEndEpisode.value = episodes.value[0] diff --git a/pages/index.vue b/pages/index.vue index 9a852fe..423b26f 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -66,7 +66,7 @@
{{ p.quality }}p
{{ p.format }}
Dubs: {{ p.dub.map((t) => t.name).join(', ') }}
-
Subs: {{ p.sub.length !== 0 ? p.sub.map((t) => t.name).join(', ') : '-' }}
+
Subs: {{ p.sub.length !== 0 ? p.sub.map((t) => t.name).join(', ') : '-' }}
{{ (p.totaldownloaded / Math.pow(1024, 2)).toFixed(2) }} MB
{{ p.partsdownloaded }}/{{ p.partsleft }}
diff --git a/pages/settings.vue b/pages/settings.vue index 6ec689d..5c44850 100644 --- a/pages/settings.vue +++ b/pages/settings.vue @@ -16,12 +16,13 @@ - + +
diff --git a/src/api/routes/crunchyroll/crunchyroll.controller.ts b/src/api/routes/crunchyroll/crunchyroll.controller.ts index 9bcede0..c4d1f25 100644 --- a/src/api/routes/crunchyroll/crunchyroll.controller.ts +++ b/src/api/routes/crunchyroll/crunchyroll.controller.ts @@ -2,14 +2,21 @@ import type { FastifyReply, FastifyRequest } from 'fastify' import { crunchyLogin } from './crunchyroll.service' import { loggedInCheck } from '../service/service.service' -export async function loginController(request: FastifyRequest, reply: FastifyReply) { +export async function loginController(request: FastifyRequest<{ + Querystring: { + geo: string + } +}>, reply: FastifyReply) { + + const query = request.query + const account = await loggedInCheck('CR') if (!account) { return reply.code(401).send({ message: 'Not Logged in' }) } - const { data, error } = await crunchyLogin(account.username, account.password) + const { data, error } = await crunchyLogin(account.username, account.password, query.geo) if (error) { reply.code(400).send(error) diff --git a/src/api/routes/crunchyroll/crunchyroll.service.ts b/src/api/routes/crunchyroll/crunchyroll.service.ts index 526cf14..157b321 100644 --- a/src/api/routes/crunchyroll/crunchyroll.service.ts +++ b/src/api/routes/crunchyroll/crunchyroll.service.ts @@ -1,6 +1,6 @@ import { messageBox } from '../../../electron/background' import { server } from '../../api' -import { VideoPlaylist } from '../../types/crunchyroll' +import { VideoPlaylist, VideoPlaylistNoGEO } from '../../types/crunchyroll' import { useFetch } from '../useFetch' import { parse as mpdParse } from 'mpd-parser' import { loggedInCheck } from '../service/service.service' @@ -14,8 +14,18 @@ const crErrors = [ } ] +// Login Proxies +const proxies: { name: string; code: string; url: string; status: string | undefined }[] = [ + { + name: 'US Proxy', + code: 'US', + url: 'https://us-proxy.crd.cx/', + status: undefined + } +] + // Crunchyroll Login Handler -export async function crunchyLogin(user: string, passw: string) { +export async function crunchyLogin(user: string, passw: string, geo: string) { const cachedData: | { access_token: string @@ -27,43 +37,79 @@ export async function crunchyLogin(user: string, passw: string) { account_id: string profile_id: string } - | undefined = server.CacheController.get('crtoken') + | undefined = server.CacheController.get(`crtoken-${geo}`) if (!cachedData) { - var { data, error } = await crunchyLoginFetch(user, passw) + if (geo === 'LOCAL') { + const { data, error } = await crunchyLoginFetch(user, passw) - if (error) { - messageBox( - 'error', - ['Cancel'], - 2, - 'Failed to login', - 'Failed to login to Crunchyroll', - crErrors.find((r) => r.error === (error?.error as string)) ? crErrors.find((r) => r.error === (error?.error as string))?.response : (error.error as string) - ) - return { data: null, error: error.error } + if (error) { + messageBox( + 'error', + ['Cancel'], + 2, + 'Failed to login', + 'Failed to login to Crunchyroll', + crErrors.find((r) => r.error === (error?.error as string)) ? crErrors.find((r) => r.error === (error?.error as string))?.response : (error.error as string) + ) + return { data: null, error: error.error } + } + + if (!data) { + messageBox('error', ['Cancel'], 2, 'Failed to login', 'Failed to login to Crunchyroll', 'Crunchyroll returned null') + return { data: null, error: 'Crunchyroll returned null' } + } + + if (!data.access_token) { + messageBox('error', ['Cancel'], 2, 'Failed to login', 'Failed to login to Crunchyroll', 'Crunchyroll returned malformed data') + return { data: null, error: 'Crunchyroll returned malformed data' } + } + + server.CacheController.set(`crtoken-${geo}`, data, data.expires_in - 30) + + return { data: data, error: null } } - if (!data) { - messageBox('error', ['Cancel'], 2, 'Failed to login', 'Failed to login to Crunchyroll', 'Crunchyroll returned null') - return { data: null, error: 'Crunchyroll returned null' } + if (geo !== 'LOCAL') { + const { data, error } = await crunchyLoginFetchProxy(user, passw, geo) + + if (error) { + messageBox( + 'error', + ['Cancel'], + 2, + 'Failed to login', + 'Failed to login to Crunchyroll', + crErrors.find((r) => r.error === (error?.error as string)) ? crErrors.find((r) => r.error === (error?.error as string))?.response : (error.error as string) + ) + return { data: null, error: error.error } + } + + if (!data) { + messageBox('error', ['Cancel'], 2, 'Failed to login', 'Failed to login to Crunchyroll', 'Crunchyroll returned null') + return { data: null, error: 'Crunchyroll returned null' } + } + + if (!data.access_token) { + messageBox('error', ['Cancel'], 2, 'Failed to login', 'Failed to login to Crunchyroll', 'Crunchyroll returned malformed data') + return { data: null, error: 'Crunchyroll returned malformed data' } + } + + server.CacheController.set(`crtoken-${geo}`, data, data.expires_in - 30) + + return { data: data, error: null } } - - if (!data.access_token) { - messageBox('error', ['Cancel'], 2, 'Failed to login', 'Failed to login to Crunchyroll', 'Crunchyroll returned malformed data') - return { data: null, error: 'Crunchyroll returned malformed data' } - } - - server.CacheController.set('crtoken', data, data.expires_in - 30) - - return { data: data, error: null } } return { data: cachedData, error: null } } -// Crunchyroll Login Fetch -async function crunchyLoginFetch(user: string, passw: string) { +// Crunchyroll Login Fetch Proxy +async function crunchyLoginFetchProxy(user: string, passw: string, geo: string) { + var host: string | undefined + + host = proxies.find((p) => p.code === geo)?.url + const headers = { Authorization: 'Basic dC1rZGdwMmg4YzNqdWI4Zm4wZnE6eWZMRGZNZnJZdktYaDRKWFMxTEVJMmNDcXUxdjVXYW4=', 'Content-Type': 'application/json', @@ -88,7 +134,7 @@ async function crunchyLoginFetch(user: string, passw: string) { country: string account_id: string profile_id: string - }>('https://crd.cx/auth/v1/token', { + }>(host + 'auth/v1/token', { type: 'POST', body: JSON.stringify(body), header: headers, @@ -108,19 +154,61 @@ async function crunchyLoginFetch(user: string, passw: string) { return { data: data, error: null } } -// Crunchyroll Playlist Fetch -export async function crunchyGetPlaylist(q: string) { +async function crunchyLoginFetch(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' + } - var endpoint = await settings.get('CREndpoint'); - const drmL3blob = await settings.get('l3blob'); - const drmL3key = await settings.get('l3key'); + const body: any = { + username: user, + password: passw, + grant_type: 'password', + scope: 'offline_access', + device_name: 'RMX2170', + device_type: 'realme RMX2170' + } + + const { data, error } = await useFetch<{ + access_token: string + refresh_token: string + expires_in: number + token_type: string + scope: string + country: string + account_id: string + profile_id: string + }>('https://beta-api.crunchyroll.com/auth/v1/token', { + type: 'POST', + body: new URLSearchParams(body).toString(), + header: headers, + credentials: 'same-origin' + }) + + if (error) { + return { data: null, error: error } + } + + if (!data) { + return { data: null, error: null } + } + + return { data: data, error: null } +} + +// Crunchyroll Playlist Fetch +export async function crunchyGetPlaylist(q: string, geo: string | undefined) { + var endpoint = await settings.get('CREndpoint') + const drmL3blob = await settings.get('l3blob') + const drmL3key = await settings.get('l3key') if (!drmL3blob || !drmL3key) { - await settings.set('CREndpoint', 1); + await settings.set('CREndpoint', 1) endpoint = 1 } - const endpoints: { id: number, name: string, url: string }[] = [ + const endpoints: { id: number; name: string; url: string }[] = [ { id: 1, name: 'Switch', @@ -185,28 +273,32 @@ export async function crunchyGetPlaylist(q: string) { id: 13, name: 'Samsung TV', url: `/tv/samsung/play` - }, + } ] const account = await loggedInCheck('CR') if (!account) return - const { data: login, error } = await crunchyLogin(account.username, account.password) + const { data: loginLocal, error } = await crunchyLogin(account.username, account.password, geo ? geo : 'LOCAL') - if (!login) return + if (!loginLocal) return - const headers = { - Authorization: `Bearer ${login.access_token}`, + const headersLoc = { + Authorization: `Bearer ${loginLocal.access_token}`, 'X-Cr-Disable-Drm': 'true' } + var playlist: VideoPlaylist + try { const response = await fetch( - `https://cr-play-service.prd.crunchyrollsvc.com/v1/${q}${endpoints.find(e=> e.id === endpoint) ? endpoints.find(e=> e.id === endpoint)?.url : '/console/switch/play'}`, + `https://cr-play-service.prd.crunchyrollsvc.com/v1/${q}${ + endpoints.find((e) => e.id === endpoint) ? endpoints.find((e) => e.id === endpoint)?.url : '/console/switch/play' + }`, { method: 'GET', - headers: headers + headers: headersLoc } ) @@ -217,9 +309,10 @@ export async function crunchyGetPlaylist(q: string) { data.subtitles = Object.values((data as any).subtitles) - return { data: data, account_id: login.account_id } - } else { + data.geo = geo + playlist = data + } else { const error = await response.text() messageBox('error', ['Cancel'], 2, 'Failed to get MPD Playlist', 'Failed to get MPD Playlist', error) @@ -228,6 +321,66 @@ export async function crunchyGetPlaylist(q: string) { } catch (e) { throw new Error(e as string) } + + for (const p of proxies) { + if (p.code !== loginLocal.country) { + const { data: login, error } = await crunchyLogin(account.username, account.password, p.code) + + if (!login) return + + const headers = { + Authorization: `Bearer ${login.access_token}`, + 'X-Cr-Disable-Drm': 'true' + } + + try { + const response = await fetch( + `https://cr-play-service.prd.crunchyrollsvc.com/v1/${q}${ + endpoints.find((e) => e.id === endpoint) ? endpoints.find((e) => e.id === endpoint)?.url : '/console/switch/play' + }`, + { + method: 'GET', + headers: headers + } + ) + + if (response.ok) { + const data: VideoPlaylistNoGEO = JSON.parse(await response.text()) + + data.hardSubs = Object.values((data as any).hardSubs) + + data.subtitles = Object.values((data as any).subtitles) + + for (const v of data.versions) { + if (!playlist.versions.find((ver) => ver.guid === v.guid)) { + playlist.versions.push({ ...v, geo: p.code }) + } + } + + for (const v of data.subtitles) { + if (!playlist.subtitles.find((ver) => ver.language === v.language)) { + playlist.subtitles.push({ ...v, geo: p.code }) + } + } + + for (const v of data.hardSubs) { + if (!playlist.hardSubs.find((ver) => ver.hlang === v.hlang)) { + playlist.hardSubs.push({ ...v, geo: p.code }) + } + } + } else { + const error = await response.text() + + messageBox('error', ['Cancel'], 2, 'Failed to get MPD Playlist', 'Failed to get MPD Playlist', error) + throw new Error(error) + } + } catch (e) { + throw new Error(e as string) + } + } + } + + return { data: playlist, account_id: loginLocal.account_id } } // Crunchyroll Delete Video Token Fetch @@ -236,7 +389,7 @@ export async function deleteVideoToken(content: string, token: string) { if (!account) return - const { data: login, error } = await crunchyLogin(account.username, account.password) + const { data: login, error } = await crunchyLogin(account.username, account.password, 'LOCAL') if (!login) return @@ -262,12 +415,12 @@ export async function deleteVideoToken(content: string, token: string) { } // Crunchyroll MPD Fetch -export async function crunchyGetPlaylistMPD(q: string) { +export async function crunchyGetPlaylistMPD(q: string, geo: string | undefined) { const account = await loggedInCheck('CR') if (!account) return - const { data } = await crunchyLogin(account.username, account.password) + const { data } = await crunchyLogin(account.username, account.password, geo ? geo : 'LOCAL') if (!data) return diff --git a/src/api/routes/service/service.controller.ts b/src/api/routes/service/service.controller.ts index 297854e..c98574e 100644 --- a/src/api/routes/service/service.controller.ts +++ b/src/api/routes/service/service.controller.ts @@ -3,6 +3,7 @@ import { crunchyLogin } from '../crunchyroll/crunchyroll.service' import { addEpisodeToPlaylist, deleteAccountID, getAllAccounts, getDownloading, getPlaylist, loggedInCheck, safeLoginData } from './service.service' import { CrunchyEpisodes } from '../../types/crunchyroll' import { adnLogin } from '../adn/adn.service' +import { server } from '../../api' export async function checkLoginController( request: FastifyRequest<{ @@ -46,7 +47,7 @@ export async function loginController( var responseError if (params.id === 'CR') { - const { data, error } = await crunchyLogin(body.user, body.password) + const { data, error } = await crunchyLogin(body.user, body.password, 'LOCAL') ;(responseError = error), (responseData = data) } @@ -133,3 +134,38 @@ export async function getPlaylistController(request: FastifyRequest, reply: Fast return reply.code(200).send(playlist.reverse()) } + +export async function checkProxiesController( + request: FastifyRequest, + reply: FastifyReply +) { + + const cachedData = server.CacheController.get('proxycheck'); + + if (!cachedData) { + const proxies: { name: string, code: string, url: string, status: string | undefined }[] = [{ + name: 'US Proxy', code: 'US', url: 'https://us-proxy.crd.cx/', status: undefined + }] + + for (const p of proxies) { + const response = await fetch( + p.url + 'health', + { + method: 'GET', + } + ) + + if (response.ok) { + p.status = 'online' + } else { + p.status = 'offline' + } + } + + server.CacheController.set('proxycheck', proxies, 60) + + return reply.code(200).send(proxies) + } + + return reply.code(200).send(cachedData) +} diff --git a/src/api/routes/service/service.route.ts b/src/api/routes/service/service.route.ts index 89c3bc0..d809eed 100644 --- a/src/api/routes/service/service.route.ts +++ b/src/api/routes/service/service.route.ts @@ -1,5 +1,5 @@ import { FastifyInstance } from 'fastify' -import { addPlaylistController, checkLoginController, deleteAccountHandler, getAllAccountsHandler, getPlaylistController, loginController } from './service.controller' +import { addPlaylistController, checkLoginController, checkProxiesController, deleteAccountHandler, getAllAccountsHandler, getPlaylistController, loginController } from './service.controller' async function serviceRoutes(server: FastifyInstance) { server.post( @@ -73,6 +73,7 @@ async function serviceRoutes(server: FastifyInstance) { }, getAllAccountsHandler ) + server.delete( '/account/:id', { @@ -87,6 +88,21 @@ async function serviceRoutes(server: FastifyInstance) { }, deleteAccountHandler ) + + server.get( + '/proxies', + { + schema: { + response: { + '4xx': { + error: { type: 'string' }, + message: { type: 'string' } + } + } + } + }, + checkProxiesController + ) } export default serviceRoutes diff --git a/src/api/routes/service/service.service.ts b/src/api/routes/service/service.service.ts index f7a290f..5509a37 100644 --- a/src/api/routes/service/service.service.ts +++ b/src/api/routes/service/service.service.ts @@ -81,8 +81,8 @@ async function deletePlaylistandTMP() { deletePlaylistandTMP() // Update Playlist Item -export async function updatePlaylistByID(id: number, status: 'waiting' | 'preparing' | 'downloading' | 'merging' | 'decrypting' | 'completed' | 'failed') { - await Playlist.update({ status: status }, { where: { id: id } }) +export async function updatePlaylistByID(id: number, status?: 'waiting' | 'preparing' | 'downloading' | 'merging' | 'decrypting' | 'completed' | 'failed', quality?: 1080 | 720 | 480 | 360 | 240) { + await Playlist.update({ status: status, quality: quality }, { where: { id: id } }) } // Add Episode to Playlist @@ -154,7 +154,8 @@ async function checkPlaylists() { (e.dataValues.media as CrunchyEpisode).episode_number, e.dataValues.quality, e.dataValues.dir, - e.dataValues.format + e.dataValues.format, + (e.dataValues.media as CrunchyEpisode).geo, ) } if (e.dataValues.service === 'ADN') { @@ -314,7 +315,8 @@ export async function downloadCrunchyrollPlaylist( episode: number, quality: 1080 | 720 | 480 | 360 | 240, downloadPath: string, - format: 'mp4' | 'mkv' + format: 'mp4' | 'mkv', + geo: string | undefined ) { downloading.push({ id: downloadID, @@ -326,7 +328,7 @@ export async function downloadCrunchyrollPlaylist( await updatePlaylistByID(downloadID, 'downloading') - var playlist = await crunchyGetPlaylist(e) + var playlist = await crunchyGetPlaylist(e, geo) if (!playlist) { await updatePlaylistByID(downloadID, 'failed') @@ -339,7 +341,7 @@ export async function downloadCrunchyrollPlaylist( const found = playlist.data.versions.find((v) => v.audio_locale === 'ja-JP') if (found) { await deleteVideoToken(episodeID, playlist.data.token) - playlist = await crunchyGetPlaylist(found.guid) + playlist = await crunchyGetPlaylist(found.guid, found.geo) } else { console.log('Exact Playlist not found, taking what crunchy gives.'), messageBox( @@ -381,6 +383,7 @@ export async function downloadCrunchyrollPlaylist( original: boolean season_guid: string variant: string + geo: string | undefined }> = [] const subDownloadList: Array<{ @@ -396,7 +399,7 @@ export async function downloadCrunchyrollPlaylist( if (playlist.data.audioLocale !== 'ja-JP') { const foundStream = playlist.data.versions.find((v) => v.audio_locale === 'ja-JP') if (foundStream) { - subPlaylist = await crunchyGetPlaylist(foundStream.guid) + subPlaylist = await crunchyGetPlaylist(foundStream.guid, foundStream.geo) } } else { subPlaylist = playlist @@ -426,7 +429,7 @@ export async function downloadCrunchyrollPlaylist( } if (found) { - const list = await crunchyGetPlaylist(found.guid) + const list = await crunchyGetPlaylist(found.guid, found.geo) if (list) { const foundSub = list.data.subtitles.find((sub) => sub.language === d) if (foundSub) { @@ -453,7 +456,8 @@ export async function downloadCrunchyrollPlaylist( media_guid: 'adas', original: false, season_guid: 'asdasd', - variant: 'asd' + variant: 'asd', + geo: undefined }) } else { console.warn(`Audio ${d}.aac not found, skipping`) @@ -481,11 +485,11 @@ export async function downloadCrunchyrollPlaylist( const audioDownload = async () => { const audios: Array = [] for (const v of dubDownloadList) { - const list = await crunchyGetPlaylist(v.guid) + const list = await crunchyGetPlaylist(v.guid, v.geo) if (!list) return - const playlist = await crunchyGetPlaylistMPD(list.data.url) + const playlist = await crunchyGetPlaylistMPD(list.data.url, list.data.geo) if (!playlist) return @@ -572,7 +576,7 @@ export async function downloadCrunchyrollPlaylist( return } - const play = await crunchyGetPlaylist(code) + const play = await crunchyGetPlaylist(code, geo) if (!play) { await updatePlaylistByID(downloadID, 'failed') @@ -582,22 +586,29 @@ export async function downloadCrunchyrollPlaylist( var downloadURL + var downloadGEO + if (hardsub) { const hardsubURL = play.data.hardSubs.find((h) => h.hlang === subs[0])?.url + const hardsubGEO = play.data.hardSubs.find((h) => h.hlang === subs[0])?.geo + if (hardsubURL) { downloadURL = hardsubURL + downloadGEO = hardsubGEO console.log('Hardsub Playlist found') } else { downloadURL = play.data.url + downloadGEO = play.data.geo console.log('Hardsub Playlist not found') } } else { downloadURL = play.data.url + downloadGEO = play.data.geo console.log('Hardsub disabled, skipping') } - var mdp = await crunchyGetPlaylistMPD(downloadURL) + var mdp = await crunchyGetPlaylistMPD(downloadURL, downloadGEO) if (!mdp) return @@ -615,6 +626,9 @@ export async function downloadCrunchyrollPlaylist( `Resolution ${quality}p not found`, `Resolution ${quality}p not found, using resolution ${mdp.playlists[0].attributes.RESOLUTION?.height}p instead` ) + + await updatePlaylistByID(downloadID, undefined, mdp.playlists[0].attributes.RESOLUTION?.height as 1080 | 720 | 480 | 360 | 240) + hq = mdp.playlists[0] } diff --git a/src/api/types/crunchyroll.ts b/src/api/types/crunchyroll.ts index 1c5e208..0d8a71f 100644 --- a/src/api/types/crunchyroll.ts +++ b/src/api/types/crunchyroll.ts @@ -102,11 +102,55 @@ export interface CrunchyEpisode { description: string episode_air_date: string eligible_region: string + geo: string | undefined } export interface CrunchyEpisodes extends Array {} export interface VideoPlaylist { + assetId: number + audioLocale: string + bifs: string + burnedInLocale: string + captions: string + hardSubs: Array<{ + hlang: string + url: string + quality: string, + geo: string | undefined + }> + playbackType: string + session: { + renewSeconds: number + noNetworkRetryIntervalSeconds: number + noNetworkTimeoutSeconds: number + maximumPauseSeconds: number + endOfVideoUnloadSeconds: number + sessionExpirationSeconds: number + usesStreamLimits: boolean + } + subtitles: Array<{ + format: string + language: string + url: string, + geo: string | undefined + }> + token: string + url: string + versions: Array<{ + audio_locale: string + guid: string + is_premium_only: boolean + media_guid: string + original: boolean + season_guid: string + variant: string, + geo: string | undefined + }> + geo: string | undefined +} + +export interface VideoPlaylistNoGEO { assetId: number audioLocale: string bifs: string @@ -143,4 +187,5 @@ export interface VideoPlaylist { season_guid: string variant: string }> + geo: string | undefined }