added prettier

This commit is contained in:
Daniel Haller 2024-05-01 01:45:45 +02:00
parent 2e29f467d1
commit 9909e3b558
52 changed files with 5752 additions and 4129 deletions

View File

@ -11,7 +11,7 @@
"requirePragma": false, "requirePragma": false,
"semi": false, "semi": false,
"singleQuote": true, "singleQuote": true,
"tabWidth": 2, "tabWidth": 4,
"trailingComma": "none", "trailingComma": "none",
"useTabs": false, "useTabs": false,
"vueIndentScriptAndStyle": false, "vueIndentScriptAndStyle": false,

View File

@ -1,72 +0,0 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const builder = require('electron-builder')
const Platform = builder.Platform
/**
* @type {import('electron-builder').Configuration}
*/
const options = {
appId: 'com.stratum.crunchyrolldownloader',
productName: 'Crunchyroll Downloader',
compression: 'maximum',
removePackageScripts: true,
nodeGypRebuild: true,
buildDependenciesFromSource: true,
publish: {
provider: 'github',
releaseType: 'release'
},
directories: {
output: 'crunchyroll-downloader-output-${version}'
},
win: {
artifactName: 'crunchyroll-downloader-${version}-windows-installer.${ext}',
icon: 'public/favicon.ico',
target: [
{
target: 'nsis',
arch: ['x64', 'ia32']
}
]
},
nsis: {
deleteAppDataOnUninstall: true
},
mac: {
category: 'public.app-category.entertainment',
hardenedRuntime: false,
gatekeeperAssess: false,
target: [
{
target: 'default',
arch: ['x64', 'arm64']
}
]
},
linux: {
maintainer: 'Stratum',
desktop: {
StartupNotify: 'false',
Encoding: 'UTF-8',
MimeType: 'x-scheme-handler/deeplink'
},
target: ['AppImage', 'rpm', 'deb']
}
}
const platform = 'WINDOWS'
builder
.build({
targets: Platform[platform].createTarget(),
config: options
})
.then((result) => {
console.log('----------------------------')
console.log('Platform:', platform)
console.log('Output:', JSON.stringify(result, null, 2))
})

72
build.ts Normal file
View File

@ -0,0 +1,72 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const builder = require('electron-builder')
const Platform = builder.Platform
/**
* @type {import('electron-builder').Configuration}
*/
const options = {
appId: 'com.stratum.crunchyrolldownloader',
productName: 'Crunchyroll Downloader',
compression: 'maximum',
removePackageScripts: true,
nodeGypRebuild: true,
buildDependenciesFromSource: true,
publish: {
provider: 'github',
releaseType: 'release'
},
directories: {
output: 'crunchyroll-downloader-output-${version}'
},
win: {
artifactName: 'crunchyroll-downloader-${version}-windows-installer.${ext}',
icon: 'public/favicon.ico',
target: [
{
target: 'nsis',
arch: ['x64', 'ia32']
}
]
},
nsis: {
deleteAppDataOnUninstall: true
},
mac: {
category: 'public.app-category.entertainment',
hardenedRuntime: false,
gatekeeperAssess: false,
target: [
{
target: 'default',
arch: ['x64', 'arm64']
}
]
},
linux: {
maintainer: 'Stratum',
desktop: {
StartupNotify: 'false',
Encoding: 'UTF-8',
MimeType: 'x-scheme-handler/deeplink'
},
target: ['AppImage', 'rpm', 'deb']
}
}
const platform = 'WINDOWS'
builder
.build({
targets: Platform[platform].createTarget(),
config: options
})
.then((result: any) => {
console.log('----------------------------')
console.log('Platform:', platform)
console.log('Output:', JSON.stringify(result, null, 2))
})

View File

@ -1,19 +1,16 @@
import type { ADNSearchFetch } from "./Types"; import type { ADNSearchFetch } from './Types'
export async function searchADN(q: string) { export async function searchADN(q: string) {
const { data: deData, error: deError } = await useFetch<ADNSearchFetch>( const { data: deData, error: deError } = await useFetch<ADNSearchFetch>(`https://gw.api.animationdigitalnetwork.fr/show/catalog`, {
`https://gw.api.animationdigitalnetwork.fr/show/catalog`, method: 'GET',
{ headers: {
method: "GET", 'x-target-distribution': 'de'
headers: { },
"x-target-distribution": 'de', query: {
}, maxAgeCategory: '18',
query: { search: q
"maxAgeCategory": "18",
"search": q
}
} }
); })
if (deError.value) { if (deError.value) {
throw new Error(deError.value?.data.message as string) throw new Error(deError.value?.data.message as string)
@ -21,19 +18,16 @@ export async function searchADN(q: string) {
if (!deData.value) return if (!deData.value) return
const { data: frData, error: frError } = await useFetch<ADNSearchFetch>( const { data: frData, error: frError } = await useFetch<ADNSearchFetch>(`https://gw.api.animationdigitalnetwork.fr/show/catalog`, {
`https://gw.api.animationdigitalnetwork.fr/show/catalog`, method: 'GET',
{ headers: {
method: "GET", 'x-target-distribution': 'fr'
headers: { },
"x-target-distribution": 'fr', query: {
}, maxAgeCategory: '18',
query: { search: q
"maxAgeCategory": "18",
"search": q
}
} }
); })
if (frError.value) { if (frError.value) {
throw new Error(frError.value?.data.message as string) throw new Error(frError.value?.data.message as string)
@ -41,22 +35,24 @@ export async function searchADN(q: string) {
if (!frData.value) return if (!frData.value) return
const deShows = deData.value.shows; const deShows = deData.value.shows
const frShows = frData.value.shows; const frShows = frData.value.shows
const mergeLanguagesOfDuplicates = (shows: { const mergeLanguagesOfDuplicates = (
id: number shows: {
url: string id: number
title: string url: string
image2x: string title: string
episodeCount: number, image2x: string
languages: Array<string> episodeCount: number
}[]) => { languages: Array<string>
shows.forEach(show => { }[]
const existingShow = shows.find(s => s.id === show.id); ) => {
shows.forEach((show) => {
const existingShow = shows.find((s) => s.id === show.id)
if (existingShow) { if (existingShow) {
const existingShowIndex = shows.findIndex(s=> s === existingShow); const existingShowIndex = shows.findIndex((s) => s === existingShow)
const rawLanguages = [...show.languages, ...existingShow.languages]; const rawLanguages = [...show.languages, ...existingShow.languages]
const languages: Array<string> = [] const languages: Array<string> = []
for (const l of rawLanguages) { for (const l of rawLanguages) {
@ -66,13 +62,13 @@ export async function searchADN(q: string) {
} }
show.languages = languages show.languages = languages
} }
}); })
return shows; return shows
}; }
const allShows = mergeLanguagesOfDuplicates([...deShows, ...frShows]); const allShows = mergeLanguagesOfDuplicates([...deShows, ...frShows])
const unique = [...new Map(allShows.map((s) => [s.id, s])).values()]; const unique = [...new Map(allShows.map((s) => [s.id, s])).values()]
return unique return unique
} }

View File

@ -1,18 +1,18 @@
import type { ADNEpisodes, ADNEpisodesFetch } from './Types' import type { ADNEpisodes, ADNEpisodesFetch } from './Types'
export async function getEpisodesWithShowIdADN(id: number, lang: 'de' | 'fr') { export async function getEpisodesWithShowIdADN(id: number, lang: 'de' | 'fr') {
const { data, error } = await useFetch<ADNEpisodesFetch>(`https://gw.api.animationdigitalnetwork.fr/video/show/${id}?offset=0&limit=-1&order=asc`, { const { data, error } = await useFetch<ADNEpisodesFetch>(`https://gw.api.animationdigitalnetwork.fr/video/show/${id}?offset=0&limit=-1&order=asc`, {
method: 'GET', method: 'GET',
headers: { headers: {
"x-target-distribution": lang, 'x-target-distribution': lang
}, }
}) })
if (error.value || !data.value) { if (error.value || !data.value) {
console.log(error.value) console.log(error.value)
alert(error.value) alert(error.value)
return return
} }
return data.value.videos return data.value.videos
} }

View File

@ -1,85 +1,85 @@
export interface ADNSearchFetch { export interface ADNSearchFetch {
shows: Array<{ shows: Array<{
id: number id: number
url: string url: string
title: string title: string
image2x: string image2x: string
episodeCount: number, episodeCount: number
languages: Array<string> languages: Array<string>
}> }>
} }
export interface ADNEpisodesFetch { export interface ADNEpisodesFetch {
videos: Array<ADNEpisode> videos: Array<ADNEpisode>
} }
export interface ADNEpisode { export interface ADNEpisode {
id: number, id: number
title: string, title: string
name: string, name: string
number: string, number: string
shortNumber: string, shortNumber: string
season: string, season: string
reference: string, reference: string
type: string, type: string
order: number, order: number
image: string, image: string
image2x: string, image2x: string
summary: string, summary: string
releaseDate: string, releaseDate: string
duration: number, duration: number
url: string, url: string
urlPath: string, urlPath: string
embeddedUrl: string, embeddedUrl: string
languages: Array<string>, languages: Array<string>
qualities: Array<string>, qualities: Array<string>
rating: number, rating: number
ratingsCount: number, ratingsCount: number
commentsCount: number, commentsCount: number
available: boolean, available: boolean
download: boolean, download: boolean
free: boolean, free: boolean
freeWithAds: boolean, freeWithAds: boolean
show: { show: {
id: number, id: number
title: string, title: string
type: string, type: string
originalTitle: string, originalTitle: string
shortTitle: string, shortTitle: string
reference: string, reference: string
age: string, age: string
languages: Array<string>, languages: Array<string>
summary: string, summary: string
image: string, image: string
image2x: string, image2x: string
imageHorizontal: string, imageHorizontal: string
imageHorizontal2x: string, imageHorizontal2x: string
url: string, url: string
urlPath: string, urlPath: string
episodeCount: number, episodeCount: number
genres: Array<string>, genres: Array<string>
copyright: string, copyright: string
rating: number, rating: number
ratingsCount: number, ratingsCount: number
commentsCount: number, commentsCount: number
qualities: Array<string>, qualities: Array<string>
simulcast: boolean, simulcast: boolean
free: boolean, free: boolean
available: boolean, available: boolean
download: boolean, download: boolean
basedOn: string, basedOn: string
tagline: Array<string>, tagline: Array<string>
firstReleaseYear: string, firstReleaseYear: string
productionStudio: string, productionStudio: string
countryOfOrigin: string, countryOfOrigin: string
productionTeam: Array<{ productionTeam: Array<{
role: string, role: string
name: string, name: string
}>, }>
nextVideoReleaseDate: string, nextVideoReleaseDate: string
indexable: boolean
}
indexable: boolean indexable: boolean
}
indexable: boolean
} }
export interface ADNEpisodes extends Array<ADNEpisode> {} export interface ADNEpisodes extends Array<ADNEpisode> {}

View File

@ -1,29 +1,29 @@
import type { CrunchyLogin } from './Types' import type { CrunchyLogin } from './Types'
export async function crunchyLogin() { export async function crunchyLogin() {
const { data, error } = await useFetch<CrunchyLogin>('http://localhost:8080/api/crunchyroll/login', { const { data, error } = await useFetch<CrunchyLogin>('http://localhost:8080/api/crunchyroll/login', {
method: 'POST' method: 'POST'
}) })
return { data, error } return { data, error }
} }
export async function checkAccount(service: string) { export async function checkAccount(service: string) {
const { data, error } = await useFetch<CrunchyLogin>(`http://localhost:8080/api/service/check/${service}`, { const { data, error } = await useFetch<CrunchyLogin>(`http://localhost:8080/api/service/check/${service}`, {
method: 'GET' method: 'GET'
}) })
return { data, error } return { data, error }
} }
export async function loginAccount(user: string, password: string, service: string) { export async function loginAccount(user: string, password: string, service: string) {
const { data, error } = await useFetch<CrunchyLogin>(`http://localhost:8080/api/service/login/${service}`, { const { data, error } = await useFetch<CrunchyLogin>(`http://localhost:8080/api/service/login/${service}`, {
method: 'POST', method: 'POST',
body: { body: {
user: user, user: user,
password: password password: password
} }
}) })
return { data, error } return { data, error }
} }

View File

@ -3,87 +3,87 @@ import { crunchyLogin } from './Account'
import type { CrunchyAnimeFetch, CrunchySearchFetch } from './Types' import type { CrunchyAnimeFetch, CrunchySearchFetch } from './Types'
export async function searchCrunchy(q: string) { export async function searchCrunchy(q: string) {
const { data: token, error: tokenerror } = await crunchyLogin() const { data: token, error: tokenerror } = await crunchyLogin()
if (!token.value) { if (!token.value) {
return return
}
const { data, error } = await useFetch<CrunchySearchFetch>(`https://beta-api.crunchyroll.com/content/v2/discover/search`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token.value.access_token}`
},
query: {
q: q,
n: 100,
type: 'series',
ratings: false
} }
})
if (error.value) { const { data, error } = await useFetch<CrunchySearchFetch>(`https://beta-api.crunchyroll.com/content/v2/discover/search`, {
console.error(error.value) method: 'GET',
throw new Error(JSON.stringify(error.value)) headers: {
} Authorization: `Bearer ${token.value.access_token}`
},
if (!data.value) return query: {
q: q,
var results: CrunchyrollSearchResults = [] n: 100,
type: 'series',
for (const result of data.value.data[0].items) { ratings: false
results.push({ }
ID: result.id,
Url: `https://www.crunchyroll.com/series/${result.id}/${result.slug_title}`,
Title: result.title,
Description: result.description,
Dubs: result.series_metadata.audio_locales,
Subs: result.series_metadata.subtitle_locales,
Episodes: result.series_metadata.episode_count,
Seasons: result.series_metadata.season_count,
PEGI: result.series_metadata.maturity_ratings,
Year: result.series_metadata.series_launch_year,
Images: result.images
}) })
}
return results if (error.value) {
console.error(error.value)
throw new Error(JSON.stringify(error.value))
}
if (!data.value) return
var results: CrunchyrollSearchResults = []
for (const result of data.value.data[0].items) {
results.push({
ID: result.id,
Url: `https://www.crunchyroll.com/series/${result.id}/${result.slug_title}`,
Title: result.title,
Description: result.description,
Dubs: result.series_metadata.audio_locales,
Subs: result.series_metadata.subtitle_locales,
Episodes: result.series_metadata.episode_count,
Seasons: result.series_metadata.season_count,
PEGI: result.series_metadata.maturity_ratings,
Year: result.series_metadata.series_launch_year,
Images: result.images
})
}
return results
} }
export async function getCRSeries(q: string) { export async function getCRSeries(q: string) {
const { data: token, error: tokenerror } = await crunchyLogin() const { data: token, error: tokenerror } = await crunchyLogin()
if (!token.value) { if (!token.value) {
return return
}
const { data, error } = await useFetch<CrunchyAnimeFetch>(`https://beta-api.crunchyroll.com/content/v2/cms/series/${q}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token.value.access_token}`
} }
})
if (error.value) { const { data, error } = await useFetch<CrunchyAnimeFetch>(`https://beta-api.crunchyroll.com/content/v2/cms/series/${q}`, {
console.error(error.value) method: 'GET',
throw new Error(JSON.stringify(error.value)) headers: {
} Authorization: `Bearer ${token.value.access_token}`
}
})
if (!data.value) return if (error.value) {
console.error(error.value)
throw new Error(JSON.stringify(error.value))
}
const anime = data.value.data[0] if (!data.value) return
return { const anime = data.value.data[0]
ID: anime.id,
Url: `https://www.crunchyroll.com/series/${anime.id}/${anime.slug_title}`, return {
Title: anime.title, ID: anime.id,
Description: anime.description, Url: `https://www.crunchyroll.com/series/${anime.id}/${anime.slug_title}`,
Dubs: anime.audio_locales, Title: anime.title,
Subs: anime.subtitle_locales, Description: anime.description,
Episodes: anime.episode_count, Dubs: anime.audio_locales,
Seasons: anime.season_count, Subs: anime.subtitle_locales,
PEGI: anime.maturity_ratings, Episodes: anime.episode_count,
Year: anime.series_launch_year, Seasons: anime.season_count,
Images: anime.images PEGI: anime.maturity_ratings,
} Year: anime.series_launch_year,
Images: anime.images
}
} }

View File

@ -2,25 +2,25 @@ import { crunchyLogin } from './Account'
import type { CrunchyEpisodesFetch } from './Types' import type { CrunchyEpisodesFetch } from './Types'
export async function listEpisodeCrunchy(q: string) { export async function listEpisodeCrunchy(q: string) {
const { data: token, error: tokenerror } = await crunchyLogin() const { data: token, error: tokenerror } = await crunchyLogin()
if (!token.value) { if (!token.value) {
return return
}
const { data, error } = await useFetch<CrunchyEpisodesFetch>(`https://beta-api.crunchyroll.com/content/v2/cms/seasons/${q}/episodes`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token.value.access_token}`
} }
})
if (error.value) { const { data, error } = await useFetch<CrunchyEpisodesFetch>(`https://beta-api.crunchyroll.com/content/v2/cms/seasons/${q}/episodes`, {
console.error(error.value) method: 'GET',
throw new Error(JSON.stringify(error.value)) headers: {
} Authorization: `Bearer ${token.value.access_token}`
}
})
if (!data.value) return if (error.value) {
console.error(error.value)
throw new Error(JSON.stringify(error.value))
}
return data.value.data if (!data.value) return
return data.value.data
} }

View File

@ -2,27 +2,27 @@ import { crunchyLogin } from './Account'
import type { CrunchySeasonsFetch } from './Types' import type { CrunchySeasonsFetch } from './Types'
export async function listSeasonCrunchy(q: string) { export async function listSeasonCrunchy(q: string) {
const { data: token, error: tokenerror } = await crunchyLogin() const { data: token, error: tokenerror } = await crunchyLogin()
if (!token.value) { if (!token.value) {
return return
}
console.log(q)
const { data, error } = await useFetch<CrunchySeasonsFetch>(`https://beta-api.crunchyroll.com/content/v2/cms/series/${q}/seasons`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token.value.access_token}`
} }
})
if (error.value) { console.log(q)
console.error(error.value)
throw new Error(JSON.stringify(error.value))
}
if (!data.value) return const { data, error } = await useFetch<CrunchySeasonsFetch>(`https://beta-api.crunchyroll.com/content/v2/cms/series/${q}/seasons`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token.value.access_token}`
}
})
return data.value.data if (error.value) {
console.error(error.value)
throw new Error(JSON.stringify(error.value))
}
if (!data.value) return
return data.value.data
} }

View File

@ -1,18 +1,73 @@
export interface CrunchySearchFetch { export interface CrunchySearchFetch {
total: number total: number
data: Array<{ data: Array<{
type: string type: string
count: number count: number
items: Array<{ items: Array<{
promo_description: string promo_description: string
title: string title: string
promo_title: string promo_title: string
channel_id: string channel_id: string
slug_title: string slug_title: string
search_metadata: { search_metadata: {
score: number score: number
} }
series_metadata: { series_metadata: {
audio_locales: Array<string>
availability_notes: string
episode_count: number
extended_description: string
extended_maturity_rating: string
is_dubbed: boolean
is_mature: boolean
is_simulcast: boolean
is_subbed: boolean
mature_blocked: boolean
maturity_ratings: Array<string>
season_count: number
series_launch_year: number
subtitle_locales: Array<string>
}
id: string
slug: string
external_id: string
description: string
new: boolean
images: {
poster_tall: Array<
Array<{
height: number
source: string
type: string
width: number
}>
>
poster_wide: Array<
Array<{
height: number
source: string
type: string
width: number
}>
>
}
linked_resource_key: string
type: string
}>
}>
}
export interface CrunchyAnimeFetch {
total: number
data: Array<{
promo_description: string
title: string
promo_title: string
channel_id: string
slug_title: string
search_metadata: {
score: number
}
audio_locales: Array<string> audio_locales: Array<string>
availability_notes: string availability_notes: string
episode_count: number episode_count: number
@ -27,212 +82,157 @@ export interface CrunchySearchFetch {
season_count: number season_count: number
series_launch_year: number series_launch_year: number
subtitle_locales: Array<string> subtitle_locales: Array<string>
} id: string
id: string slug: string
slug: string external_id: string
external_id: string description: string
description: string new: boolean
new: boolean images: {
images: { poster_tall: Array<
poster_tall: Array< Array<{
Array<{ height: number
height: number source: string
source: string type: string
type: string width: number
width: number }>
}> >
> poster_wide: Array<
poster_wide: Array< Array<{
Array<{ height: number
height: number source: string
source: string type: string
type: string width: number
width: number }>
}> >
> }
} linked_resource_key: string
linked_resource_key: string type: string
type: string
}> }>
}>
}
export interface CrunchyAnimeFetch {
total: number
data: Array<{
promo_description: string
title: string
promo_title: string
channel_id: string
slug_title: string
search_metadata: {
score: number
}
audio_locales: Array<string>
availability_notes: string
episode_count: number
extended_description: string
extended_maturity_rating: string
is_dubbed: boolean
is_mature: boolean
is_simulcast: boolean
is_subbed: boolean
mature_blocked: boolean
maturity_ratings: Array<string>
season_count: number
series_launch_year: number
subtitle_locales: Array<string>
id: string
slug: string
external_id: string
description: string
new: boolean
images: {
poster_tall: Array<
Array<{
height: number
source: string
type: string
width: number
}>
>
poster_wide: Array<
Array<{
height: number
source: string
type: string
width: number
}>
>
}
linked_resource_key: string
type: string
}>
} }
export interface CrunchyLogin { export interface CrunchyLogin {
access_token: string access_token: string
refresh_token: string refresh_token: string
expires_in: number expires_in: number
token_type: string token_type: string
scope: string scope: string
country: string country: string
account_id: string account_id: string
profile_id: string profile_id: string
} }
export interface CrunchySeasonsFetch { export interface CrunchySeasonsFetch {
total: number total: number
data: Array<{ data: Array<{
identifier: string identifier: string
description: string description: string
is_simulcast: boolean is_simulcast: boolean
subtitle_locales: Array<string> subtitle_locales: Array<string>
series_id: string series_id: string
id: string id: string
audio_locales: Array<string> audio_locales: Array<string>
title: string title: string
versions: Array<{ versions: Array<{
audio_locale: string audio_locale: string
guid: string guid: string
original: boolean original: boolean
variant: string variant: string
}>
season_sequence_number: number
season_number: number
maturity_ratings: Array<string>
mature_blocked: boolean
channel_id: string
is_subbed: boolean
audio_locale: string
season_display_number: string
is_complete: boolean
season_tags: Array<string>
is_mature: boolean
is_dubbed: boolean
slug_title: string
availability_notes: string
number_of_episodes: boolean
}> }>
season_sequence_number: number meta: {
season_number: number versions_considered: boolean
maturity_ratings: Array<string> }
mature_blocked: boolean
channel_id: string
is_subbed: boolean
audio_locale: string
season_display_number: string
is_complete: boolean
season_tags: Array<string>
is_mature: boolean
is_dubbed: boolean
slug_title: string
availability_notes: string
number_of_episodes: boolean
}>
meta: {
versions_considered: boolean
}
} }
export interface CrunchyEpisodesFetch { export interface CrunchyEpisodesFetch {
total: number total: number
data: Array<{ data: Array<{
closed_captions_available: boolean closed_captions_available: boolean
availability_notes: string availability_notes: string
next_episode_title: string next_episode_title: string
upload_date: string upload_date: string
versions: Array<{ versions: Array<{
audio_locale: string audio_locale: string
guid: string guid: string
is_premium_only: boolean is_premium_only: boolean
media_guid: string media_guid: string
original: boolean original: boolean
season_guid: string season_guid: string
variant: string variant: string
}>
season_slug_title: string
series_title: string
season_title: string
sequence_number: number
maturity_ratings: Array<string>
slug_title: string
is_premium_only: boolean
availability_ends: string
identifier: string
recent_variant: string
free_available_date: string
subtitle_locales: Array<string>
series_id: string
mature_blocked: boolean
duration_ms: number
availability_starts: string
audio_locale: string
images: {
thumbnail: Array<
Array<{
height: number
source: string
type: string
width: number
}> }>
> season_slug_title: string
series_title: string
season_title: string
sequence_number: number
maturity_ratings: Array<string>
slug_title: string
is_premium_only: boolean
availability_ends: string
identifier: string
recent_variant: string
free_available_date: string
subtitle_locales: Array<string>
series_id: string
mature_blocked: boolean
duration_ms: number
availability_starts: string
audio_locale: string
images: {
thumbnail: Array<
Array<{
height: number
source: string
type: string
width: number
}>
>
}
season_sequence_number: number
season_id: string
episode_number: number
listing_id: string
available_date: string
channel_id: string
season_number: number
hd_flag: boolean
recent_audio_locale: string
available_offline: boolean
episode: string
is_subbed: boolean
media_type: string
is_clip: boolean
title: string
streams_link: string
slug: string
id: string
production_episode_id: string
is_dubbed: boolean
next_episode_id: string
series_slug_title: string
season_tags: Array<string>
premium_date: string
is_mature: boolean
premium_available_date: string
description: string
episode_air_date: string
eligible_region: string
}>
meta: {
versions_considered: boolean
} }
season_sequence_number: number
season_id: string
episode_number: number
listing_id: string
available_date: string
channel_id: string
season_number: number
hd_flag: boolean
recent_audio_locale: string
available_offline: boolean
episode: string
is_subbed: boolean
media_type: string
is_clip: boolean
title: string
streams_link: string
slug: string
id: string
production_episode_id: string
is_dubbed: boolean
next_episode_id: string
series_slug_title: string
season_tags: Array<string>
premium_date: string
is_mature: boolean
premium_available_date: string
description: string
episode_air_date: string
eligible_region: string
}>
meta: {
versions_considered: boolean
}
} }

View File

@ -1,70 +1,72 @@
export interface CrunchyEpisode { export interface CrunchyEpisode {
closed_captions_available: boolean, closed_captions_available: boolean
availability_notes: string, availability_notes: string
next_episode_title: string, next_episode_title: string
upload_date: string, upload_date: string
versions: Array<{ versions: Array<{
audio_locale: string, audio_locale: string
guid: string, guid: string
is_premium_only: boolean, is_premium_only: boolean
media_guid: string, media_guid: string
original: boolean, original: boolean
season_guid: string, season_guid: string
variant: string variant: string
}>, }>
season_slug_title: string, season_slug_title: string
series_title: string, series_title: string
season_title: string, season_title: string
sequence_number: number, sequence_number: number
maturity_ratings: Array<string>, maturity_ratings: Array<string>
slug_title: string, slug_title: string
is_premium_only: boolean, is_premium_only: boolean
availability_ends: string, availability_ends: string
identifier: string, identifier: string
recent_variant: string, recent_variant: string
free_available_date: string, free_available_date: string
subtitle_locales: Array<string>, subtitle_locales: Array<string>
series_id: string, series_id: string
mature_blocked: boolean, mature_blocked: boolean
duration_ms: number, duration_ms: number
availability_starts: string, availability_starts: string
audio_locale: string, audio_locale: string
images: { images: {
thumbnail: Array<Array<{ thumbnail: Array<
height: number, Array<{
source: string, height: number
type: string, source: string
width: number type: string
}>> width: number
}, }>
season_sequence_number: number, >
season_id: string, }
episode_number: number, season_sequence_number: number
listing_id: string, season_id: string
available_date: string, episode_number: number
channel_id: string, listing_id: string
season_number: number, available_date: string
hd_flag: boolean, channel_id: string
recent_audio_locale: string, season_number: number
available_offline: boolean, hd_flag: boolean
episode: string, recent_audio_locale: string
is_subbed: boolean, available_offline: boolean
media_type: string, episode: string
is_clip: boolean, is_subbed: boolean
title: string, media_type: string
streams_link: string, is_clip: boolean
slug: string, title: string
id: string, streams_link: string
production_episode_id: string, slug: string
is_dubbed: boolean, id: string
next_episode_id: string, production_episode_id: string
series_slug_title: string, is_dubbed: boolean
season_tags: Array<string>, next_episode_id: string
premium_date: string, series_slug_title: string
is_mature: boolean, season_tags: Array<string>
premium_available_date: string, premium_date: string
description: string, is_mature: boolean
episode_air_date: string, premium_available_date: string
description: string
episode_air_date: string
eligible_region: string eligible_region: string
} }

View File

@ -1,8 +1,8 @@
const isProduction = process.env.NODE_ENV !== 'development' const isProduction = process.env.NODE_ENV !== 'development'
export function openNewWindow(urlprod: string, urldev: string, w: string) { export function openNewWindow(urlprod: string, urldev: string, w: string) {
const newWindow = window.open(isProduction ? urlprod : urldev, '_blank', w) const newWindow = window.open(isProduction ? urlprod : urldev, '_blank', w)
if (newWindow) { if (newWindow) {
newWindow.focus() newWindow.focus()
} }
} }

View File

@ -1,32 +1,39 @@
<template> <template>
<div class="fixed w-full flex flex-row px-2 bg-[#11111189] h-14 z-10 gap-1" style="-webkit-app-region: drag"> <div class="fixed w-full flex flex-row px-2 bg-[#11111189] h-14 z-10 gap-1" style="-webkit-app-region: drag">
<div class="w-full flex gap-10 flex-row items-center justify-center px-5"> <div class="w-full flex gap-10 flex-row items-center justify-center px-5">
<button @click="openAddAnime" class="flex items-center justify-center px-2 py-2 gap-1 transition-all bg-[#ffffff16] hover:bg-[#ffffff25] rounded-lg select-none" style="-webkit-app-region: no-drag"> <button
<Icon name="ph:plus-bold" class="h-3.5 w-3.5 text-white" /> @click="openAddAnime"
<div class="text-[11px] text-white font-dm"> class="flex items-center justify-center px-2 py-2 gap-1 transition-all bg-[#ffffff16] hover:bg-[#ffffff25] rounded-lg select-none"
ADD DOWNLOAD style="-webkit-app-region: no-drag"
>
<Icon name="ph:plus-bold" class="h-3.5 w-3.5 text-white" />
<div class="text-[11px] text-white font-dm"> ADD DOWNLOAD </div>
</button>
</div> </div>
</button> <div class="w-full flex flex-row items-center justify-center">
</div> <img src="/logo.png" class="h-8" />
<div class="w-full flex flex-row items-center justify-center"> <div class="text-[10px] leading-[10px] text-opacity-90 text-white select-none"
<img src="/logo.png" class="h-8"> >Crunchyroll <br />
<div class="text-[10px] leading-[10px] text-opacity-90 text-white select-none">Crunchyroll <br> Downloader</div> Downloader</div
</div> >
<div class="w-full flex gap-2 flex-row items-center justify-center"> </div>
<!-- <button class="flex items-center justify-center px-2 py-2 gap-1 transition-all bg-[#ffffff16] hover:bg-[#ffffff25] rounded-lg select-none" style="-webkit-app-region: no-drag"> <div class="w-full flex gap-2 flex-row items-center justify-center">
<!-- <button class="flex items-center justify-center px-2 py-2 gap-1 transition-all bg-[#ffffff16] hover:bg-[#ffffff25] rounded-lg select-none" style="-webkit-app-region: no-drag">
<Icon name="iconamoon:playlist" class="h-3.5 w-3.5 text-white" /> <Icon name="iconamoon:playlist" class="h-3.5 w-3.5 text-white" />
<div class="text-[11px] text-white font-dm"> <div class="text-[11px] text-white font-dm">
PLAYLIST PLAYLIST
</div> </div>
</button> --> </button> -->
<button @click="openSettings" class="flex items-center justify-center px-2 py-2 gap-1 transition-all bg-[#ffffff16] hover:bg-[#ffffff25] rounded-lg select-none" style="-webkit-app-region: no-drag"> <button
<Icon name="ic:round-settings" class="h-3.5 w-3.5 text-white" /> @click="openSettings"
<div class="text-[11px] text-white font-dm"> class="flex items-center justify-center px-2 py-2 gap-1 transition-all bg-[#ffffff16] hover:bg-[#ffffff25] rounded-lg select-none"
SETTINGS style="-webkit-app-region: no-drag"
>
<Icon name="ic:round-settings" class="h-3.5 w-3.5 text-white" />
<div class="text-[11px] text-white font-dm"> SETTINGS </div>
</button>
</div> </div>
</button>
</div> </div>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -36,35 +43,35 @@ import { openNewWindow } from './Functions/WindowHandler'
const isProduction = process.env.NODE_ENV !== 'development' const isProduction = process.env.NODE_ENV !== 'development'
async function openSettings() { async function openSettings() {
(window as any).myAPI.openWindow({ ;(window as any).myAPI.openWindow({
title: "Settings", title: 'Settings',
url: isProduction ? 'http://localhost:8079/settings' : 'http://localhost:3000/settings', url: isProduction ? 'http://localhost:8079/settings' : 'http://localhost:3000/settings',
width: 600, width: 600,
height: 700, height: 700,
backgroundColor: "#111111" backgroundColor: '#111111'
}) })
} }
async function openAddAnime() { async function openAddAnime() {
// const { data, error } = await checkAccount() // const { data, error } = await checkAccount()
// if (error.value) { // if (error.value) {
// (window as any).myAPI.openWindow({ // (window as any).myAPI.openWindow({
// title: "Crunchyroll Login", // title: "Crunchyroll Login",
// url: isProduction ? 'http://localhost:8079/crunchylogin' : 'http://localhost:3000/crunchylogin', // url: isProduction ? 'http://localhost:8079/crunchylogin' : 'http://localhost:3000/crunchylogin',
// width: 600, // width: 600,
// height: 300, // height: 300,
// backgroundColor: "#111111" // backgroundColor: "#111111"
// }) // })
// return // return
// } // }
(window as any).myAPI.openWindow({ ;(window as any).myAPI.openWindow({
title: "Add Anime", title: 'Add Anime',
url: isProduction ? 'http://localhost:8079/addanime' : 'http://localhost:3000/addanime', url: isProduction ? 'http://localhost:8079/addanime' : 'http://localhost:3000/addanime',
width: 700, width: 700,
height: 450, height: 450,
backgroundColor: "#111111" backgroundColor: '#111111'
}) })
} }
</script> </script>

View File

@ -1,43 +1,43 @@
export interface CrunchyrollSearchResult { export interface CrunchyrollSearchResult {
ID: string ID: string
Url: string Url: string
Title: string Title: string
Description: string Description: string
Dubs: Array<string> Dubs: Array<string>
Subs: Array<string> Subs: Array<string>
Episodes: number Episodes: number
Seasons: number Seasons: number
PEGI: Array<string> PEGI: Array<string>
Year: number Year: number
Images: { Images: {
poster_tall: Array< poster_tall: Array<
Array<{ Array<{
height: number height: number
source: string source: string
type: string type: string
width: number width: number
}> }>
> >
poster_wide: Array< poster_wide: Array<
Array<{ Array<{
height: number height: number
source: string source: string
type: string type: string
width: number width: number
}> }>
> >
} }
} }
export interface CrunchyrollSearchResults extends Array<CrunchyrollSearchResult> {} export interface CrunchyrollSearchResults extends Array<CrunchyrollSearchResult> {}
export interface ADNSearchResult { export interface ADNSearchResult {
id: number id: number
url: string url: string
title: string title: string
image2x: string image2x: string
episodeCount: number episodeCount: number
languages: Array<string> languages: Array<string>
} }
export interface ADNSearchResults extends Array<ADNSearchResult> {} export interface ADNSearchResults extends Array<ADNSearchResult> {}

View File

@ -1,33 +1,33 @@
export interface CrunchySeason { export interface CrunchySeason {
identifier: string identifier: string
description: string description: string
is_simulcast: boolean is_simulcast: boolean
subtitle_locales: Array<string> subtitle_locales: Array<string>
series_id: string series_id: string
id: string id: string
audio_locales: Array<string> audio_locales: Array<string>
title: string title: string
versions: Array<{ versions: Array<{
audio_locale: string
guid: string
original: boolean
variant: string
}>
season_sequence_number: number
season_number: number
maturity_ratings: Array<string>
mature_blocked: boolean
channel_id: string
is_subbed: boolean
audio_locale: string audio_locale: string
guid: string season_display_number: string
original: boolean is_complete: boolean
variant: string season_tags: Array<string>
}> is_mature: boolean
season_sequence_number: number is_dubbed: boolean
season_number: number slug_title: string
maturity_ratings: Array<string> availability_notes: string
mature_blocked: boolean number_of_episodes: boolean
channel_id: string
is_subbed: boolean
audio_locale: string
season_display_number: string
is_complete: boolean
season_tags: Array<string>
is_mature: boolean
is_dubbed: boolean
slug_title: string
availability_notes: string
number_of_episodes: boolean
} }
export interface CrunchySeasons extends Array<CrunchySeason> {} export interface CrunchySeasons extends Array<CrunchySeason> {}

View File

@ -1,25 +1,29 @@
<template> <template>
<div <div
class="fixed bottom-3 right-5 p-3 flex flex-col bg-[#111111dc] w-80 min-h-24 rounded-xl font-dm text-white transition-all duration-300" class="fixed bottom-3 right-5 p-3 flex flex-col bg-[#111111dc] w-80 min-h-24 rounded-xl font-dm text-white transition-all duration-300"
:class="data?.status === 'update-available' && !ignoreUpdate || data?.status === 'downloading' || data?.status === 'update-downloaded' ? 'opacity-100' : 'opacity-0 pointer-events-none'" :class="
> (data?.status === 'update-available' && !ignoreUpdate) || data?.status === 'downloading' || data?.status === 'update-downloaded'
<button @click="ignoreUpdate = true" class="absolute right-3 top-2"> ? 'opacity-100'
<Icon name="akar-icons:cross" class="h-4 w-4 text-white" /> : 'opacity-0 pointer-events-none'
</button> "
<div class="text-base text-center"> Update available </div> >
<div class="text-sm text-center"> A new update is available </div> <button @click="ignoreUpdate = true" class="absolute right-3 top-2">
<div v-if="data && data.info && data.info.version" class="text-sm text-center"> v{{ data.info.version }} </div> <Icon name="akar-icons:cross" class="h-4 w-4 text-white" />
<button @click="startDownload" v-if="data && data.status === 'update-available'" class="text-sm py-3 bg-[#363434] mt-5 rounded-xl" :disabled="downloading"> </button>
Download Update <div class="text-base text-center"> Update available </div>
</button> <div class="text-sm text-center"> A new update is available </div>
<button v-if="data && data.status === 'downloading'" class="relative text-sm py-3 bg-[#363434] mt-5 rounded-xl overflow-hidden"> <div v-if="data && data.info && data.info.version" class="text-sm text-center"> v{{ data.info.version }} </div>
<div class="absolute top-0 left-0 w-full h-full bg-[#a1a1a141] transition-all duration-300" :style="`width: calc((${data.info.percent} / 100) * 100%);`"></div> <button @click="startDownload" v-if="data && data.status === 'update-available'" class="text-sm py-3 bg-[#363434] mt-5 rounded-xl" :disabled="downloading">
Downloading... Download Update
</button> </button>
<button @click="startInstall" v-if="data && data.status === 'update-downloaded'" class="text-sm py-3 bg-[#363434] mt-5 rounded-xl" :disabled="installing"> <button v-if="data && data.status === 'downloading'" class="relative text-sm py-3 bg-[#363434] mt-5 rounded-xl overflow-hidden">
Install Update <div class="absolute top-0 left-0 w-full h-full bg-[#a1a1a141] transition-all duration-300" :style="`width: calc((${data.info.percent} / 100) * 100%);`"></div>
</button> Downloading...
</div> </button>
<button @click="startInstall" v-if="data && data.status === 'update-downloaded'" class="text-sm py-3 bg-[#363434] mt-5 rounded-xl" :disabled="installing">
Install Update
</button>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -29,25 +33,25 @@ const installing = ref<boolean>(false)
const ignoreUpdate = ref<boolean>(false) const ignoreUpdate = ref<boolean>(false)
const checkUpdate = () => { const checkUpdate = () => {
;(window as any).myAPI.getUpdateStatus().then((result: any) => { ;(window as any).myAPI.getUpdateStatus().then((result: any) => {
data.value = result data.value = result
}) })
} }
const startDownload = () => { const startDownload = () => {
downloading.value = true downloading.value = true
;(window as any).myAPI.startUpdateDownload() ;(window as any).myAPI.startUpdateDownload()
} }
const startInstall = () => { const startInstall = () => {
installing.value = true installing.value = true
;(window as any).myAPI.startUpdateInstall() ;(window as any).myAPI.startUpdateInstall()
} }
onMounted(() => { onMounted(() => {
checkUpdate() checkUpdate()
setInterval(checkUpdate, 2000) setInterval(checkUpdate, 2000)
}) })
</script> </script>

View File

@ -1,67 +1,67 @@
{ {
"name": "crunchyroll-downloader", "name": "crunchyroll-downloader",
"author": "Stratum", "author": "Stratum",
"description": "Crunchyroll Downloader", "description": "Crunchyroll Downloader",
"version": "1.0.7", "version": "1.0.7",
"private": true, "private": true,
"main": ".output/src/electron/background.js", "main": ".output/src/electron/background.js",
"repository": "https://github.com/stratuma/Crunchyroll-Downloader-v4.0", "repository": "https://github.com/stratuma/Crunchyroll-Downloader-v4.0",
"scripts": { "scripts": {
"dev": "nuxt dev -o", "dev": "nuxt dev -o",
"build": "nuxt generate", "build": "nuxt generate",
"preview": "nuxt preview", "preview": "nuxt preview",
"postinstall": "nuxt prepare && electron-builder install-app-deps", "postinstall": "nuxt prepare && electron-builder install-app-deps",
"transpile-src": "tsc -p ./src --outDir .output/src", "transpile-src": "tsc -p ./src --outDir .output/src",
"dev:electron": "NODE_ENV=development concurrently --kill-others \"nuxt dev\" \"tsc-watch -p ./src --outDir .output/src --onSuccess 'electron ./.output/src/electron/background.js'\"", "dev:electron": "NODE_ENV=development concurrently --kill-others \"nuxt dev\" \"tsc-watch -p ./src --outDir .output/src --onSuccess 'electron ./.output/src/electron/background.js'\"",
"dev:electron:win": "set NODE_ENV=development& concurrently --kill-others \"nuxt dev\" \"tsc-watch -p ./src --outDir .output/src --onSuccess run.electron\"", "dev:electron:win": "set NODE_ENV=development& concurrently --kill-others \"nuxt dev\" \"tsc-watch -p ./src --outDir .output/src --onSuccess run.electron\"",
"build:electron": "pnpm build && pnpm transpile-src && node build.js" "build:electron": "pnpm build && pnpm transpile-src && node build.ts",
}, "prettier:fix": "pnpm prettier src --write && pnpm prettier components --write && pnpm prettier pages --write && pnpm prettier build.ts --write"
"devDependencies": { },
"7zip-bin": "^5.2.0", "devDependencies": {
"@nuxtjs/eslint-config-typescript": "^12.1.0", "7zip-bin": "^5.2.0",
"@nuxtjs/google-fonts": "^3.2.0", "@nuxtjs/eslint-config-typescript": "^12.1.0",
"@nuxtjs/tailwindcss": "^6.12.0", "@nuxtjs/google-fonts": "^3.2.0",
"@pinia/nuxt": "^0.4.11", "@nuxtjs/tailwindcss": "^6.12.0",
"@types/express": "^4.17.21", "@pinia/nuxt": "^0.4.11",
"concurrently": "^8.2.2", "@types/express": "^4.17.21",
"dotenv": "^16.4.5", "concurrently": "^8.2.2",
"electron": "^30.0.1", "dotenv": "^16.4.5",
"electron-builder": "^24.13.3", "electron": "^30.0.1",
"eslint": "^8.57.0", "electron-builder": "^24.13.3",
"eslint-config-prettier": "^8.10.0", "eslint-config-prettier": "^8.10.0",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"nuxt": "^3.11.2", "nuxt": "^3.11.2",
"nuxt-icon": "^0.6.10", "nuxt-icon": "^0.6.10",
"prettier": "^2.8.8", "prettier": "^3.2.5",
"sass": "^1.75.0", "sass": "^1.75.0",
"sass-loader": "^13.3.3", "sass-loader": "^13.3.3",
"tsc-watch": "^6.2.0", "tsc-watch": "^6.2.0",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"wait-on": "^7.2.0" "wait-on": "^7.2.0"
}, },
"dependencies": { "dependencies": {
"@fastify/cors": "^9.0.1", "@fastify/cors": "^9.0.1",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/fluent-ffmpeg": "^2.1.24", "@types/fluent-ffmpeg": "^2.1.24",
"@types/node-cron": "^3.0.11", "@types/node-cron": "^3.0.11",
"ass-compiler": "^0.1.11", "ass-compiler": "^0.1.11",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"electron-log": "^5.1.2", "electron-log": "^5.1.2",
"electron-settings": "^4.0.4", "electron-settings": "^4.0.4",
"electron-updater": "^6.1.8", "electron-updater": "^6.1.8",
"express": "^4.19.2", "express": "^4.19.2",
"fastify": "^4.26.2", "fastify": "^4.26.2",
"fluent-ffmpeg": "^2.1.2", "fluent-ffmpeg": "^2.1.2",
"jsencrypt": "^3.3.2", "jsencrypt": "^3.3.2",
"mpd-parser": "^1.3.0", "mpd-parser": "^1.3.0",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"node-cron": "^3.0.3", "node-cron": "^3.0.3",
"sequelize": "^6.37.3", "sequelize": "^6.37.3",
"sqlite3": "5.1.6" "sqlite3": "5.1.6"
}, },
"build": { "build": {
"extraResources": [ "extraResources": [
"./ffmpeg/**" "./ffmpeg/**"
] ]
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,28 +1,28 @@
<template> <template>
<div class="h-screen overflow-hidden bg-[#11111189] flex flex-col p-5 text-white font-dm" style="-webkit-app-region: drag"> <div class="h-screen overflow-hidden bg-[#11111189] flex flex-col p-5 text-white font-dm" style="-webkit-app-region: drag">
<div class="relative flex flex-row items-center justify-center"> <div class="relative flex flex-row items-center justify-center">
<div class="text-2xl">ADN Login</div> <div class="text-2xl">ADN Login</div>
</div>
<div class="flex flex-col mt-5 gap-3.5 h-full" style="-webkit-app-region: no-drag">
<div class="relative flex flex-col">
<input v-model="username" type="text" name="text" placeholder="Email" class="bg-[#5c5b5b] focus:outline-none px-3 py-3 rounded-xl text-sm text-center" />
</div>
<div class="relative flex flex-col">
<input v-model="password" type="password" name="text" placeholder="Password" class="bg-[#5c5b5b] focus:outline-none px-3 py-3 rounded-xl text-sm text-center" />
</div>
</div>
<div class="relative flex flex-col mt-auto">
<button @click="login" class="relative py-3 border-2 rounded-xl flex flex-row items-center justify-center" style="-webkit-app-region: no-drag">
<div class="flex flex-row items-center justify-center transition-all" :class="isLoggingIn ? 'opacity-0' : 'opacity-100'">
<div class="text-xl">Login</div>
</div> </div>
<div class="absolute flex flex-row items-center justify-center gap-1 transition-all" :class="isLoggingIn ? 'opacity-100' : 'opacity-0'"> <div class="flex flex-col mt-5 gap-3.5 h-full" style="-webkit-app-region: no-drag">
<Icon name="mdi:loading" class="h-6 w-6 animate-spin" /> <div class="relative flex flex-col">
<div class="text-xl">Logging in</div> <input v-model="username" type="text" name="text" placeholder="Email" class="bg-[#5c5b5b] focus:outline-none px-3 py-3 rounded-xl text-sm text-center" />
</div>
<div class="relative flex flex-col">
<input v-model="password" type="password" name="text" placeholder="Password" class="bg-[#5c5b5b] focus:outline-none px-3 py-3 rounded-xl text-sm text-center" />
</div>
</div>
<div class="relative flex flex-col mt-auto">
<button @click="login" class="relative py-3 border-2 rounded-xl flex flex-row items-center justify-center" style="-webkit-app-region: no-drag">
<div class="flex flex-row items-center justify-center transition-all" :class="isLoggingIn ? 'opacity-0' : 'opacity-100'">
<div class="text-xl">Login</div>
</div>
<div class="absolute flex flex-row items-center justify-center gap-1 transition-all" :class="isLoggingIn ? 'opacity-100' : 'opacity-0'">
<Icon name="mdi:loading" class="h-6 w-6 animate-spin" />
<div class="text-xl">Logging in</div>
</div>
</button>
</div> </div>
</button>
</div> </div>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -35,54 +35,54 @@ const password = ref<string>()
const isLoggingIn = ref<number>(0) const isLoggingIn = ref<number>(0)
const login = async () => { const login = async () => {
isLoggingIn.value++ isLoggingIn.value++
if (!username.value) { if (!username.value) {
isLoggingIn.value-- isLoggingIn.value--
return return
} }
if (!password.value) { if (!password.value) {
isLoggingIn.value-- isLoggingIn.value--
return return
} }
const { data, error } = await loginAccount(username.value, password.value, 'ADN') const { data, error } = await loginAccount(username.value, password.value, 'ADN')
if (error.value) { if (error.value) {
isLoggingIn.value--
return
}
isLoggingIn.value-- isLoggingIn.value--
return close()
}
isLoggingIn.value--
close()
} }
</script> </script>
<style> <style>
.font-dm { .font-dm {
font-family: "DM Sans", sans-serif; font-family: 'DM Sans', sans-serif;
} }
.font-protest { .font-protest {
font-family: "Protest Riot", sans-serif; font-family: 'Protest Riot', sans-serif;
font-weight: 400; font-weight: 400;
font-style: normal; font-style: normal;
} }
.font-dm-big { .font-dm-big {
font-family: "DM Sans", sans-serif; font-family: 'DM Sans', sans-serif;
font-weight: 1000; font-weight: 1000;
font-style: normal; font-style: normal;
} }
@keyframes fadein { @keyframes fadein {
from { from {
opacity: 0; opacity: 0;
} }
to { to {
opacity: 1; opacity: 1;
} }
} }
body { body {
animation: fadein 0.5s; animation: fadein 0.5s;
} }
</style> </style>

View File

@ -1,28 +1,28 @@
<template> <template>
<div class="h-screen overflow-hidden bg-[#11111189] flex flex-col p-5 text-white font-dm" style="-webkit-app-region: drag"> <div class="h-screen overflow-hidden bg-[#11111189] flex flex-col p-5 text-white font-dm" style="-webkit-app-region: drag">
<div class="relative flex flex-row items-center justify-center"> <div class="relative flex flex-row items-center justify-center">
<div class="text-2xl">Crunchyroll Login</div> <div class="text-2xl">Crunchyroll Login</div>
</div>
<div class="flex flex-col mt-5 gap-3.5 h-full" style="-webkit-app-region: no-drag">
<div class="relative flex flex-col">
<input v-model="username" type="text" name="text" placeholder="Email" class="bg-[#5c5b5b] focus:outline-none px-3 py-3 rounded-xl text-sm text-center" />
</div>
<div class="relative flex flex-col">
<input v-model="password" type="password" name="text" placeholder="Password" class="bg-[#5c5b5b] focus:outline-none px-3 py-3 rounded-xl text-sm text-center" />
</div>
</div>
<div class="relative flex flex-col mt-auto">
<button @click="login" class="relative py-3 border-2 rounded-xl flex flex-row items-center justify-center" style="-webkit-app-region: no-drag">
<div class="flex flex-row items-center justify-center transition-all" :class="isLoggingIn ? 'opacity-0' : 'opacity-100'">
<div class="text-xl">Login</div>
</div> </div>
<div class="absolute flex flex-row items-center justify-center gap-1 transition-all" :class="isLoggingIn ? 'opacity-100' : 'opacity-0'"> <div class="flex flex-col mt-5 gap-3.5 h-full" style="-webkit-app-region: no-drag">
<Icon name="mdi:loading" class="h-6 w-6 animate-spin" /> <div class="relative flex flex-col">
<div class="text-xl">Logging in</div> <input v-model="username" type="text" name="text" placeholder="Email" class="bg-[#5c5b5b] focus:outline-none px-3 py-3 rounded-xl text-sm text-center" />
</div>
<div class="relative flex flex-col">
<input v-model="password" type="password" name="text" placeholder="Password" class="bg-[#5c5b5b] focus:outline-none px-3 py-3 rounded-xl text-sm text-center" />
</div>
</div>
<div class="relative flex flex-col mt-auto">
<button @click="login" class="relative py-3 border-2 rounded-xl flex flex-row items-center justify-center" style="-webkit-app-region: no-drag">
<div class="flex flex-row items-center justify-center transition-all" :class="isLoggingIn ? 'opacity-0' : 'opacity-100'">
<div class="text-xl">Login</div>
</div>
<div class="absolute flex flex-row items-center justify-center gap-1 transition-all" :class="isLoggingIn ? 'opacity-100' : 'opacity-0'">
<Icon name="mdi:loading" class="h-6 w-6 animate-spin" />
<div class="text-xl">Logging in</div>
</div>
</button>
</div> </div>
</button>
</div> </div>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -35,54 +35,54 @@ const password = ref<string>()
const isLoggingIn = ref<number>(0) const isLoggingIn = ref<number>(0)
const login = async () => { const login = async () => {
isLoggingIn.value++ isLoggingIn.value++
if (!username.value) { if (!username.value) {
isLoggingIn.value-- isLoggingIn.value--
return return
} }
if (!password.value) { if (!password.value) {
isLoggingIn.value-- isLoggingIn.value--
return return
} }
const { data, error } = await loginAccount(username.value, password.value, 'CR') const { data, error } = await loginAccount(username.value, password.value, 'CR')
if (error.value) { if (error.value) {
isLoggingIn.value--
return
}
isLoggingIn.value-- isLoggingIn.value--
return close()
}
isLoggingIn.value--
close()
} }
</script> </script>
<style> <style>
.font-dm { .font-dm {
font-family: "DM Sans", sans-serif; font-family: 'DM Sans', sans-serif;
} }
.font-protest { .font-protest {
font-family: "Protest Riot", sans-serif; font-family: 'Protest Riot', sans-serif;
font-weight: 400; font-weight: 400;
font-style: normal; font-style: normal;
} }
.font-dm-big { .font-dm-big {
font-family: "DM Sans", sans-serif; font-family: 'DM Sans', sans-serif;
font-weight: 1000; font-weight: 1000;
font-style: normal; font-style: normal;
} }
@keyframes fadein { @keyframes fadein {
from { from {
opacity: 0; opacity: 0;
} }
to { to {
opacity: 1; opacity: 1;
} }
} }
body { body {
animation: fadein 0.5s; animation: fadein 0.5s;
} }
</style> </style>

View File

@ -1,70 +1,74 @@
<template> <template>
<div class="relative h-screen overflow-hidden"> <div class="relative h-screen overflow-hidden">
<Updater /> <Updater />
<MainHeader /> <MainHeader />
<div class="flex flex-col text-white gap-5 mt-14 p-5 overflow-y-scroll h-[calc(100vh-3.5rem)]"> <div class="flex flex-col text-white gap-5 mt-14 p-5 overflow-y-scroll h-[calc(100vh-3.5rem)]">
<!-- <button @click="deletePlaylist"> <!-- <button @click="deletePlaylist">
Delete Playlist Delete Playlist
</button> --> </button> -->
<div v-for="p in playlist" class="relative flex flex-row gap-4 min-h-36 bg-[#63636383] rounded-xl font-dm overflow-hidden"> <div v-for="p in playlist" class="relative flex flex-row gap-4 min-h-36 bg-[#63636383] rounded-xl font-dm overflow-hidden">
<div class="absolute top-0 left-0 w-full h-full bg-[#a1a1a141] transition-all duration-300" :style="`width: calc((${p.partsdownloaded} / ${p.partsleft}) * 100%);`"></div> <div
<div class="absolute h-full w-full flex flex-row gap-3 p-3.5"> class="absolute top-0 left-0 w-full h-full bg-[#a1a1a141] transition-all duration-300"
<div v-if="p.service === 'CR'" class="flex w-48 min-w-48"> :style="`width: calc((${p.partsdownloaded} / ${p.partsleft}) * 100%);`"
<img :src="(p.media as CrunchyEpisode).images.thumbnail[0].find((p) => p.height === 1080)?.source" alt="Image" class="object-cover rounded-lg" /> ></div>
</div> <div class="absolute h-full w-full flex flex-row gap-3 p-3.5">
<div v-if="p.service === 'ADN'" class="flex min-w-52 w-52"> <div v-if="p.service === 'CR'" class="flex w-48 min-w-48">
<img :src="(p.media as ADNEpisode).image2x" alt="Image" class="object-cover rounded-lg" /> <img :src="(p.media as CrunchyEpisode).images.thumbnail[0].find((p) => p.height === 1080)?.source" alt="Image" class="object-cover rounded-lg" />
</div> </div>
<div class="flex flex-col w-full"> <div v-if="p.service === 'ADN'" class="flex min-w-52 w-52">
<div class="flex flex-row h-full"> <img :src="(p.media as ADNEpisode).image2x" alt="Image" class="object-cover rounded-lg" />
<div class="flex flex-col"> </div>
<div v-if="p.status === 'waiting'" class="flex flex-row items-center justify-center gap-1 text-xs capitalize p-1.5 bg-[#866332] rounded-lg"> <div class="flex flex-col w-full">
<Icon name="mdi:clock" class="h-3.5 w-3.5 text-white" /> <div class="flex flex-row h-full">
{{ p.status }} <div class="flex flex-col">
<div v-if="p.status === 'waiting'" class="flex flex-row items-center justify-center gap-1 text-xs capitalize p-1.5 bg-[#866332] rounded-lg">
<Icon name="mdi:clock" class="h-3.5 w-3.5 text-white" />
{{ p.status }}
</div>
<div v-if="p.status === 'preparing'" class="flex flex-row items-center justify-center gap-1 text-xs capitalize p-1.5 bg-[#866332] rounded-lg">
<Icon name="mdi:clock" class="h-3.5 w-3.5 text-white" />
{{ p.status }}
</div>
<div v-if="p.status === 'downloading'" class="flex flex-row items-center justify-center gap-1 text-xs capitalize p-1.5 bg-[#60501b] rounded-lg">
<Icon name="mdi:loading" class="h-3.5 w-3.5 text-white animate-spin" />
{{ p.status }}
</div>
<div v-if="p.status === 'merging'" class="flex flex-row items-center justify-center gap-1 text-xs capitalize p-1.5 bg-[#866332] rounded-lg">
<Icon name="mdi:loading" class="h-3.5 w-3.5 text-white animate-spin" />
{{ p.status }}
</div>
<div v-if="p.status === 'completed'" class="flex flex-row items-center justify-center gap-1 text-xs capitalize p-1.5 bg-[#266326] rounded-lg">
<Icon name="material-symbols:check" class="h-3.5 w-3.5 text-white" />
{{ p.status }}
</div>
</div>
<div class="text-xs capitalize ml-auto">
{{ p.service === 'CR' ? 'Crunchyroll' : 'ADN' }}
</div>
</div>
<div v-if="p.service === 'CR'" class="text-base capitalize h-full flex items-center">
{{ (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 h-full">
{{ (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="flex flex-row gap-2 h-full items-end">
<div class="text-xs">{{ p.quality }}p</div>
<div class="text-xs uppercase">{{ p.format }}</div>
<div class="text-xs">Dubs: {{ p.dub.map((t) => t.name).join(', ') }}</div>
<div class="text-xs">Subs: {{ p.sub.length !== 0 ? p.sub.map((t) => t.name).join(', ') : '-' }}</div>
<div class="flex flex-col ml-auto gap-0.5">
<div v-if="p.partsleft && p.status === 'downloading'" class="text-xs ml-auto">{{ p.partsdownloaded }}/{{ p.partsleft }}</div>
<div v-if="p.downloadspeed && p.status === 'downloading'" class="text-xs">{{ p.downloadspeed }} MB/s</div>
</div>
</div>
</div>
</div> </div>
<div v-if="p.status === 'preparing'" class="flex flex-row items-center justify-center gap-1 text-xs capitalize p-1.5 bg-[#866332] rounded-lg">
<Icon name="mdi:clock" class="h-3.5 w-3.5 text-white" />
{{ p.status }}
</div>
<div v-if="p.status === 'downloading'" class="flex flex-row items-center justify-center gap-1 text-xs capitalize p-1.5 bg-[#60501b] rounded-lg">
<Icon name="mdi:loading" class="h-3.5 w-3.5 text-white animate-spin" />
{{ p.status }}
</div>
<div v-if="p.status === 'merging'" class="flex flex-row items-center justify-center gap-1 text-xs capitalize p-1.5 bg-[#866332] rounded-lg">
<Icon name="mdi:loading" class="h-3.5 w-3.5 text-white animate-spin" />
{{ p.status }}
</div>
<div v-if="p.status === 'completed'" class="flex flex-row items-center justify-center gap-1 text-xs capitalize p-1.5 bg-[#266326] rounded-lg">
<Icon name="material-symbols:check" class="h-3.5 w-3.5 text-white" />
{{ p.status }}
</div>
</div>
<div class="text-xs capitalize ml-auto">
{{ p.service === 'CR' ? 'Crunchyroll' : 'ADN' }}
</div>
</div> </div>
<div v-if="p.service === 'CR'" class="text-base capitalize h-full flex items-center">
{{ (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 h-full">
{{ (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="flex flex-row gap-2 h-full items-end">
<div class="text-xs">{{ p.quality }}p</div>
<div class="text-xs uppercase">{{ p.format }}</div>
<div class="text-xs">Dubs: {{ p.dub.map((t) => t.name).join(', ') }}</div>
<div class="text-xs">Subs: {{ p.sub.length !== 0 ? p.sub.map((t) => t.name).join(', ') : '-' }}</div>
<div class="flex flex-col ml-auto gap-0.5">
<div v-if="p.partsleft && p.status === 'downloading'" class="text-xs ml-auto">{{ p.partsdownloaded }}/{{ p.partsleft }}</div>
<div v-if="p.downloadspeed && p.status === 'downloading'" class="text-xs">{{ p.downloadspeed }} MB/s</div>
</div>
</div>
</div>
</div> </div>
</div>
</div> </div>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -73,144 +77,144 @@ import type { CrunchyEpisode } from '~/components/Episode/Types'
import Updater from '~/components/Updater.vue' import Updater from '~/components/Updater.vue'
const playlist = ref< const playlist = ref<
Array<{ Array<{
id: number id: number
status: string status: string
media: CrunchyEpisode | ADNEpisode media: CrunchyEpisode | ADNEpisode
dub: Array<{ locale: string; name: string }> dub: Array<{ locale: string; name: string }>
sub: Array<{ locale: string; name: string }> sub: Array<{ locale: string; name: string }>
dir: string dir: string
partsleft: number partsleft: number
partsdownloaded: number partsdownloaded: number
downloadspeed: number downloadspeed: number
quality: number quality: number
service: string service: string
format: string format: string
}> }>
>() >()
const getPlaylist = async () => { const getPlaylist = async () => {
const { data, error } = await useFetch< const { data, error } = await useFetch<
Array<{ Array<{
id: number id: number
status: string status: string
media: CrunchyEpisode | ADNEpisode media: CrunchyEpisode | ADNEpisode
dub: Array<{ locale: string; name: string }> dub: Array<{ locale: string; name: string }>
sub: Array<{ locale: string; name: string }> sub: Array<{ locale: string; name: string }>
dir: string dir: string
partsleft: number partsleft: number
partsdownloaded: number partsdownloaded: number
downloadspeed: number downloadspeed: number
quality: number quality: number
service: string service: string
format: string format: string
}> }>
>('http://localhost:8080/api/service/playlist') >('http://localhost:8080/api/service/playlist')
if (error.value) { if (error.value) {
alert(error.value) alert(error.value)
return return
} }
if (!data.value) { if (!data.value) {
return return
} }
playlist.value = data.value playlist.value = data.value
} }
const deletePlaylist = async () => { const deletePlaylist = async () => {
const { data, error } = await useFetch('http://localhost:8080/api/service/playlist', { const { data, error } = await useFetch('http://localhost:8080/api/service/playlist', {
method: 'delete' method: 'delete'
}) })
if (error.value) { if (error.value) {
alert(error.value) alert(error.value)
return return
} }
if (!data.value) { if (!data.value) {
return return
} }
} }
onMounted(() => { onMounted(() => {
getPlaylist() getPlaylist()
setInterval(getPlaylist, 1000) setInterval(getPlaylist, 1000)
}) })
</script> </script>
<style> <style>
body { body {
background: none; background: none;
background-color: none; background-color: none;
background: transparent; background: transparent;
background-color: transparent; background-color: transparent;
} }
.font-dm { .font-dm {
font-family: 'DM Sans', sans-serif; font-family: 'DM Sans', sans-serif;
} }
.font-protest { .font-protest {
font-family: 'Protest Riot', sans-serif; font-family: 'Protest Riot', sans-serif;
font-weight: 400; font-weight: 400;
font-style: normal; font-style: normal;
} }
.font-dm-big { .font-dm-big {
font-family: 'DM Sans', sans-serif; font-family: 'DM Sans', sans-serif;
font-weight: 1000; font-weight: 1000;
font-style: normal; font-style: normal;
} }
.loading-a { .loading-a {
animation: animation infinite 3s; animation: animation infinite 3s;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background: #383838; background: #383838;
} }
/* Handle */ /* Handle */
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #cac9c9; background: #cac9c9;
} }
/* Handle on hover */ /* Handle on hover */
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: #555; background: #555;
} }
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 10px; width: 10px;
} }
@keyframes animation { @keyframes animation {
0% { 0% {
left: 0%; left: 0%;
} }
50% { 50% {
left: 88%; left: 88%;
} }
100% { 100% {
left: 0%; left: 0%;
} }
} }
@keyframes fadein { @keyframes fadein {
from { from {
opacity: 0; opacity: 0;
} }
to { to {
opacity: 1; opacity: 1;
} }
} }
body { body {
animation: fadein 0.5s; animation: fadein 0.5s;
} }
</style> </style>

View File

@ -1,19 +1,19 @@
<template> <template>
<div class="h-screen bg-[#111111] flex flex-col p-5 text-white" style="-webkit-app-region: drag"> <div class="h-screen bg-[#111111] flex flex-col p-5 text-white" style="-webkit-app-region: drag">
<div class="flex flex-row items-center justify-center"> <div class="flex flex-row items-center justify-center">
<div class="text-2xl">Settings</div> <div class="text-2xl">Settings</div>
</div>
<div class="flex flex-row mt-2" style="-webkit-app-region: no-drag">
<button
v-for="(option, index) in options"
@click="activeIndex = index"
class="w-full flex items-center justify-center py-2 border-b-2 transition-all"
:class="activeIndex === index ? 'border-[#ce6104]' : 'border-[#ce620428]'"
>
{{ option }}
</button>
</div>
</div> </div>
<div class="flex flex-row mt-2" style="-webkit-app-region: no-drag">
<button
v-for="(option, index) in options"
@click="activeIndex = index"
class="w-full flex items-center justify-center py-2 border-b-2 transition-all"
:class="activeIndex === index ? 'border-[#ce6104]' : 'border-[#ce620428]'"
>
{{ option }}
</button>
</div>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -23,15 +23,15 @@ const activeIndex = ref(0)
<style> <style>
@keyframes fadein { @keyframes fadein {
from { from {
opacity: 0; opacity: 0;
} }
to { to {
opacity: 1; opacity: 1;
} }
} }
body { body {
animation: fadein 0.5s; animation: fadein 0.5s;
} }
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +0,0 @@
@echo off
wait-on http://localhost:3000 && electron ./.output/src/electron/background.js

View File

@ -1,46 +1,45 @@
import fastify from "fastify"; import fastify from 'fastify'
import cors from "@fastify/cors"; import cors from '@fastify/cors'
import NodeCache from "node-cache"; import NodeCache from 'node-cache'
import crunchyrollRoutes from "./routes/crunchyroll/crunchyroll.route"; import crunchyrollRoutes from './routes/crunchyroll/crunchyroll.route'
import { sequelize } from "./db/database"; import { sequelize } from './db/database'
import serviceRoutes from "./routes/service/service.route"; import serviceRoutes from './routes/service/service.route'
;(async () => {
(async () => {
try { try {
await sequelize.authenticate(); await sequelize.authenticate()
console.log("Connection has been established successfully."); console.log('Connection has been established successfully.')
} catch (error) { } catch (error) {
console.error("Unable to connect to the database:", error); console.error('Unable to connect to the database:', error)
} }
try { try {
await sequelize.sync(); await sequelize.sync()
console.log("All models were synchronized successfully."); console.log('All models were synchronized successfully.')
} catch (error) { } catch (error) {
console.log("Failed to synchronize Models"); console.log('Failed to synchronize Models')
} }
})(); })()
const CacheController = new NodeCache({ stdTTL: 100, checkperiod: 120 }); const CacheController = new NodeCache({ stdTTL: 100, checkperiod: 120 })
export const server = fastify(); export const server = fastify()
// Cors registration // Cors registration
server.register(cors, { server.register(cors, {
origin: "*", origin: '*',
methods: ["GET", "POST", "PUT", "DELETE"], methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ["Content-Type", "Authorization"], allowedHeaders: ['Content-Type', 'Authorization']
}); })
// Cache Controller Type // Cache Controller Type
declare module "fastify" { declare module 'fastify' {
interface FastifyInstance { interface FastifyInstance {
CacheController: NodeCache; CacheController: NodeCache
} }
} }
// Cache Controller // Cache Controller
server.decorate("CacheController", CacheController); server.decorate('CacheController', CacheController)
// Routes // Routes
server.register(crunchyrollRoutes, { prefix: 'api/crunchyroll' }) server.register(crunchyrollRoutes, { prefix: 'api/crunchyroll' })
@ -49,11 +48,11 @@ server.register(serviceRoutes, { prefix: 'api/service' })
function startAPI() { function startAPI() {
server.listen({ port: 8080 }, (err, address) => { server.listen({ port: 8080 }, (err, address) => {
if (err) { if (err) {
console.error(err); console.error(err)
return; return
} }
console.log(`Server is listening on ${address}`); console.log(`Server is listening on ${address}`)
}); })
} }
export default startAPI; export default startAPI

View File

@ -4,139 +4,139 @@ import { CrunchyEpisode } from '../types/crunchyroll'
import { ADNEpisode } from '../types/adn' import { ADNEpisode } from '../types/adn'
const sequelize = new Sequelize({ const sequelize = new Sequelize({
dialect: 'sqlite', dialect: 'sqlite',
storage: app.getPath('documents') + '/crd-dbv2.db' storage: app.getPath('documents') + '/crd-dbv2.db'
}) })
interface AccountAttributes { interface AccountAttributes {
id: number id: number
username: string username: string
password: string password: string
service: string service: string
} }
interface AccountCreateAttributes { interface AccountCreateAttributes {
username: string username: string
password: string password: string
service: string service: string
} }
interface AccountCreateAttributes { interface AccountCreateAttributes {
username: string username: string
password: string password: string
service: string service: string
} }
interface PlaylistAttributes { interface PlaylistAttributes {
id: number id: number
status: 'waiting' | 'preparing' | 'downloading' | 'merging' | 'completed' | 'failed' status: 'waiting' | 'preparing' | 'downloading' | 'merging' | 'completed' | 'failed'
media: CrunchyEpisode | ADNEpisode media: CrunchyEpisode | ADNEpisode
dub: Array<string> dub: Array<string>
sub: Array<string> sub: Array<string>
hardsub: boolean hardsub: boolean
quality: 1080 | 720 | 480 | 360 | 240 quality: 1080 | 720 | 480 | 360 | 240
dir: string dir: string
failedreason: string, failedreason: string
service: 'CR' | 'ADN', service: 'CR' | 'ADN'
format: 'mp4' | 'mkv' format: 'mp4' | 'mkv'
} }
interface PlaylistCreateAttributes { interface PlaylistCreateAttributes {
media: CrunchyEpisode | ADNEpisode media: CrunchyEpisode | ADNEpisode
dub: Array<string> dub: Array<string>
sub: Array<string> sub: Array<string>
dir: string dir: string
quality: 1080 | 720 | 480 | 360 | 240 quality: 1080 | 720 | 480 | 360 | 240
hardsub: boolean hardsub: boolean
status: 'waiting' | 'preparing' | 'downloading' | 'merging' | 'completed' | 'failed', status: 'waiting' | 'preparing' | 'downloading' | 'merging' | 'completed' | 'failed'
service: 'CR' | 'ADN', service: 'CR' | 'ADN'
format: 'mp4' | 'mkv' format: 'mp4' | 'mkv'
} }
const Account: ModelDefined<AccountAttributes, AccountCreateAttributes> = sequelize.define('Accounts', { const Account: ModelDefined<AccountAttributes, AccountCreateAttributes> = sequelize.define('Accounts', {
id: { id: {
allowNull: false, allowNull: false,
autoIncrement: true, autoIncrement: true,
primaryKey: true, primaryKey: true,
type: DataTypes.INTEGER type: DataTypes.INTEGER
}, },
username: { username: {
allowNull: false, allowNull: false,
type: DataTypes.STRING type: DataTypes.STRING
}, },
password: { password: {
allowNull: false, allowNull: false,
type: DataTypes.STRING type: DataTypes.STRING
}, },
service: { service: {
allowNull: false, allowNull: false,
type: DataTypes.STRING type: DataTypes.STRING
}, },
createdAt: { createdAt: {
allowNull: false, allowNull: false,
type: DataTypes.DATE type: DataTypes.DATE
}, },
updatedAt: { updatedAt: {
allowNull: false, allowNull: false,
type: DataTypes.DATE type: DataTypes.DATE
} }
}) })
const Playlist: ModelDefined<PlaylistAttributes, PlaylistCreateAttributes> = sequelize.define('Playlist', { const Playlist: ModelDefined<PlaylistAttributes, PlaylistCreateAttributes> = sequelize.define('Playlist', {
id: { id: {
allowNull: false, allowNull: false,
autoIncrement: true, autoIncrement: true,
primaryKey: true, primaryKey: true,
type: DataTypes.INTEGER type: DataTypes.INTEGER
}, },
status: { status: {
allowNull: false, allowNull: false,
type: DataTypes.STRING type: DataTypes.STRING
}, },
media: { media: {
allowNull: false, allowNull: false,
type: DataTypes.JSON type: DataTypes.JSON
}, },
dub: { dub: {
allowNull: false, allowNull: false,
type: DataTypes.JSON type: DataTypes.JSON
}, },
sub: { sub: {
allowNull: false, allowNull: false,
type: DataTypes.JSON type: DataTypes.JSON
}, },
dir: { dir: {
allowNull: false, allowNull: false,
type: DataTypes.STRING type: DataTypes.STRING
}, },
hardsub: { hardsub: {
allowNull: false, allowNull: false,
type: DataTypes.BOOLEAN type: DataTypes.BOOLEAN
}, },
failedreason: { failedreason: {
allowNull: true, allowNull: true,
type: DataTypes.STRING type: DataTypes.STRING
}, },
quality: { quality: {
allowNull: true, allowNull: true,
type: DataTypes.BOOLEAN type: DataTypes.BOOLEAN
}, },
service: { service: {
allowNull: true, allowNull: true,
type: DataTypes.STRING type: DataTypes.STRING
}, },
format: { format: {
allowNull: true, allowNull: true,
type: DataTypes.STRING type: DataTypes.STRING
}, },
createdAt: { createdAt: {
allowNull: false, allowNull: false,
type: DataTypes.DATE type: DataTypes.DATE
}, },
updatedAt: { updatedAt: {
allowNull: false, allowNull: false,
type: DataTypes.DATE type: DataTypes.DATE
} }
}) })
export { sequelize, Account, Playlist } export { sequelize, Account, Playlist }

View File

@ -8,302 +8,301 @@ import { loggedInCheck } from '../service/service.service'
import { parse as mpdParse, parse } from 'mpd-parser' import { parse as mpdParse, parse } from 'mpd-parser'
export async function adnLogin(user: string, passw: string) { export async function adnLogin(user: string, passw: string) {
const cachedData: const cachedData:
| { | {
accessToken: string accessToken: string
} }
| undefined = server.CacheController.get('adntoken') | undefined = server.CacheController.get('adntoken')
if (!cachedData) { if (!cachedData) {
var { data, error } = await adnLoginFetch(user, passw) var { data, error } = await adnLoginFetch(user, passw)
if (error) { if (error) {
messageBox('error', ['Cancel'], 2, 'Failed to login', 'Failed to login to ADN', error.error as string) messageBox('error', ['Cancel'], 2, 'Failed to login', 'Failed to login to ADN', error.error as string)
return { data: null, error: error.error } return { data: null, error: error.error }
}
if (!data) {
messageBox('error', ['Cancel'], 2, 'Failed to login', 'Failed to login to ADN', 'ADN returned null')
return { data: null, error: 'ADN returned null' }
}
if (!data.accessToken) {
messageBox('error', ['Cancel'], 2, 'Failed to login', 'Failed to login to ADN', 'ADN returned malformed data')
return { data: null, error: 'ADN returned malformed data' }
}
server.CacheController.set('adntoken', data, 300)
return { data: data, error: null }
} }
if (!data) { return { data: cachedData, error: null }
messageBox('error', ['Cancel'], 2, 'Failed to login', 'Failed to login to ADN', 'ADN returned null')
return { data: null, error: 'ADN returned null' }
}
if (!data.accessToken) {
messageBox('error', ['Cancel'], 2, 'Failed to login', 'Failed to login to ADN', 'ADN returned malformed data')
return { data: null, error: 'ADN returned malformed data' }
}
server.CacheController.set('adntoken', data, 300)
return { data: data, error: null }
}
return { data: cachedData, error: null }
} }
async function adnLoginFetch(user: string, passw: string) { async function adnLoginFetch(user: string, passw: string) {
const headers = { const headers = {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
const body = { const body = {
username: user, username: user,
password: passw, password: passw,
source: 'Web', source: 'Web',
rememberMe: true rememberMe: true
} }
const { data, error } = await useFetch<{ const { data, error } = await useFetch<{
accessToken: string accessToken: string
}>('https://gw.api.animationdigitalnetwork.fr/authentication/login', { }>('https://gw.api.animationdigitalnetwork.fr/authentication/login', {
type: 'POST', type: 'POST',
body: JSON.stringify(body), body: JSON.stringify(body),
header: headers header: headers
}) })
if (error) { if (error) {
return { data: null, error: error } return { data: null, error: error }
} }
if (!data) { if (!data) {
return { data: null, error: null } return { data: null, error: null }
} }
return { data: data, error: null } return { data: data, error: null }
} }
export async function getEpisodeADN(q: number) { export async function getEpisodeADN(q: number) {
const cachedData = server.CacheController.get(`getepisodeadn-${q}`) const cachedData = server.CacheController.get(`getepisodeadn-${q}`)
if (cachedData) { if (cachedData) {
return cachedData return cachedData
} }
try { try {
const response = await fetch(`https://gw.api.animationdigitalnetwork.fr/video/${q}/public`, { const response = await fetch(`https://gw.api.animationdigitalnetwork.fr/video/${q}/public`, {
method: 'GET', method: 'GET',
headers: { headers: {
'x-target-distribution': 'de' 'x-target-distribution': 'de'
} }
}) })
if (response.ok) { if (response.ok) {
const data: { const data: {
video: Array<any> video: Array<any>
} = JSON.parse(await response.text()) } = JSON.parse(await response.text())
server.CacheController.set(`getepisodeadn-${q}`, data.video, 1000) server.CacheController.set(`getepisodeadn-${q}`, data.video, 1000)
return data.video return data.video
} else { } else {
throw new Error('Failed to fetch ADN') throw new Error('Failed to fetch ADN')
}
} catch (e) {
throw new Error(e as string)
} }
} catch (e) {
throw new Error(e as string)
}
} }
export async function getPlayerConfigADN(id: number, geo: 'de' | 'fr') { export async function getPlayerConfigADN(id: number, geo: 'de' | 'fr') {
const account = await loggedInCheck('ADN')
const account = await loggedInCheck('ADN') if (!account) return
if (!account) return const token = await adnLogin(account.username, account.password)
const token = await adnLogin(account.username, account.password) if (!token.data?.accessToken) return
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': geo,
'Content-Type': 'application/json',
Authorization: `Bearer ${token.data.accessToken}`
}
})
try { if (response.ok) {
const response = await fetch(`https://gw.api.animationdigitalnetwork.fr/player/video/${id}/configuration`, { const data: ADNPlayerConfig = JSON.parse(await response.text())
method: 'GET',
headers: {
'x-target-distribution': geo,
'Content-Type': 'application/json',
Authorization: `Bearer ${token.data.accessToken}`
}
})
if (response.ok) { return data
const data: ADNPlayerConfig = JSON.parse(await response.text()) } else {
throw new Error('Failed to fetch ADN')
return data }
} else { } catch (e) {
throw new Error('Failed to fetch ADN') throw new Error(e as string)
} }
} catch (e) {
throw new Error(e as string)
}
} }
async function getPlayerToken(id: number, geo: 'de' | 'fr') { async function getPlayerToken(id: number, geo: 'de' | 'fr') {
const r = await getPlayerConfigADN(id, geo) const r = await getPlayerConfigADN(id, geo)
if (!r) return if (!r) return
try { try {
const response = await fetch(`https://gw.api.animationdigitalnetwork.fr/player/refresh/token`, { const response = await fetch(`https://gw.api.animationdigitalnetwork.fr/player/refresh/token`, {
method: 'POST', method: 'POST',
headers: { headers: {
'x-target-distribution': geo, 'x-target-distribution': geo,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-Player-Refresh-Token': r.player.options.user.refreshToken 'X-Player-Refresh-Token': r.player.options.user.refreshToken
} }
}) })
if (response.ok) { if (response.ok) {
const data: { const data: {
token: string token: string
accessToken: string accessToken: string
refreshToken: string refreshToken: string
} = JSON.parse(await response.text()) } = JSON.parse(await response.text())
return data return data
} else { } else {
throw new Error('Failed to fetch ADN') throw new Error('Failed to fetch ADN')
}
} catch (e) {
throw new Error(e as string)
} }
} catch (e) {
throw new Error(e as string)
}
} }
function randomHexaString(length: number) { function randomHexaString(length: number) {
const characters = '0123456789abcdef' const characters = '0123456789abcdef'
let result = '' let result = ''
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
result += characters[Math.floor(Math.random() * characters.length)] result += characters[Math.floor(Math.random() * characters.length)]
} }
return result return result
} }
async function getPlayerEncryptedToken(id: number, geo: 'de' | 'fr') { async function getPlayerEncryptedToken(id: number, geo: 'de' | 'fr') {
const token = await getPlayerToken(id, geo) const token = await getPlayerToken(id, geo)
if (!token) return if (!token) return
var key = new JSEncrypt() var key = new JSEncrypt()
var random = randomHexaString(16) var random = randomHexaString(16)
key.setPublicKey( key.setPublicKey(
'-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCbQrCJBRmaXM4gJidDmcpWDssgnumHinCLHAgS4buMtdH7dEGGEUfBofLzoEdt1jqcrCDT6YNhM0aFCqbLOPFtx9cg/X2G/G5bPVu8cuFM0L+ehp8s6izK1kjx3OOPH/kWzvstM5tkqgJkNyNEvHdeJl6KhS+IFEqwvZqgbBpKuwIDAQAB-----END PUBLIC KEY-----' '-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCbQrCJBRmaXM4gJidDmcpWDssgnumHinCLHAgS4buMtdH7dEGGEUfBofLzoEdt1jqcrCDT6YNhM0aFCqbLOPFtx9cg/X2G/G5bPVu8cuFM0L+ehp8s6izK1kjx3OOPH/kWzvstM5tkqgJkNyNEvHdeJl6KhS+IFEqwvZqgbBpKuwIDAQAB-----END PUBLIC KEY-----'
) )
const data = { const data = {
k: random, k: random,
t: String(token.token) t: String(token.token)
} }
const finisheddata = JSON.stringify(data) const finisheddata = JSON.stringify(data)
const encryptedData = key.encrypt(finisheddata) || '' const encryptedData = key.encrypt(finisheddata) || ''
return { data: encryptedData, random: random } return { data: encryptedData, random: random }
} }
export async function adnGetPlaylist(animeid: number, geo: 'de' | 'fr') { export async function adnGetPlaylist(animeid: number, geo: 'de' | 'fr') {
const token = await getPlayerEncryptedToken(animeid, geo) const token = await getPlayerEncryptedToken(animeid, geo)
if (!token) return if (!token) return
try { try {
const response = await fetch(`https://gw.api.animationdigitalnetwork.fr/player/video/${animeid}/link`, { const response = await fetch(`https://gw.api.animationdigitalnetwork.fr/player/video/${animeid}/link`, {
method: 'GET', method: 'GET',
headers: { headers: {
'x-target-distribution': geo, 'x-target-distribution': geo,
'X-Player-Token': token.data 'X-Player-Token': token.data
} }
}) })
if (response.ok) { if (response.ok) {
const data: ADNLink = await JSON.parse(await response.text()) const data: ADNLink = await JSON.parse(await response.text())
return { data: data, secret: token.random } return { data: data, secret: token.random }
} else { } else {
const data: { message: string, code: string, statusCode: string} = JSON.parse(await response.text()) const data: { message: string; code: string; statusCode: string } = JSON.parse(await response.text())
messageBox('error', ['Cancel'], 2, 'Failed to fetch Playlist', 'Failed to fetch ADN Playlist', `${data.message} - ${data.code}`) messageBox('error', ['Cancel'], 2, 'Failed to fetch Playlist', 'Failed to fetch ADN Playlist', `${data.message} - ${data.code}`)
return null return null
}
} catch (e) {
throw new Error(e as string)
} }
} catch (e) {
throw new Error(e as string)
}
} }
export async function adnGetM3U8Playlist(url: string) { export async function adnGetM3U8Playlist(url: string) {
try { try {
const response = await fetch(url, { const response = await fetch(url, {
method: 'GET', method: 'GET'
}) })
if (response.ok) { if (response.ok) {
const data: { location: string } = await JSON.parse(await response.text()) const data: { location: string } = await JSON.parse(await response.text())
const mu8 = await fetch(data.location, { const mu8 = await fetch(data.location, {
method: 'GET', method: 'GET'
}) })
const playlist = await mu8.text() const playlist = await mu8.text()
const url = await extractURLFromPlaylist(playlist) const url = await extractURLFromPlaylist(playlist)
const partsraw = await fetch(url, { const partsraw = await fetch(url, {
method: 'GET', method: 'GET'
}) })
const parts = await partsraw.text() const parts = await partsraw.text()
const baseurl = await extractBaseURL(url); const baseurl = await extractBaseURL(url)
const partsArray = await extractSequenceURLs(parts, baseurl) const partsArray = await extractSequenceURLs(parts, baseurl)
return partsArray return partsArray
}
} catch (e) {
throw new Error(e as string)
} }
} catch (e) {
throw new Error(e as string)
}
} }
async function extractURLFromPlaylist(playlist: string) { async function extractURLFromPlaylist(playlist: string) {
var startIndex = playlist.indexOf("http"); var startIndex = playlist.indexOf('http')
var endIndex = playlist.indexOf(" ", startIndex); var endIndex = playlist.indexOf(' ', startIndex)
var extractedURL = playlist.slice(startIndex, endIndex); var extractedURL = playlist.slice(startIndex, endIndex)
return extractedURL; return extractedURL
} }
async function extractBaseURL(playlistURL: string) { async function extractBaseURL(playlistURL: string) {
var baseURL = playlistURL.substring(0, playlistURL.lastIndexOf("/") + 1); var baseURL = playlistURL.substring(0, playlistURL.lastIndexOf('/') + 1)
return baseURL; return baseURL
} }
async function extractSequenceURLs(playlistText: string, baseURL: string) { async function extractSequenceURLs(playlistText: string, baseURL: string) {
var sequenceURLs: Array<{ filename: string, url: string }> = []; var sequenceURLs: Array<{ filename: string; url: string }> = []
var matches = playlistText.match(/sequence_\d+\.ts/g); var matches = playlistText.match(/sequence_\d+\.ts/g)
if (matches) { if (matches) {
matches.forEach(function(match) { matches.forEach(function (match) {
sequenceURLs.push({ filename: match, url: baseURL + match }); sequenceURLs.push({ filename: match, url: baseURL + match })
}); })
} }
return sequenceURLs; return sequenceURLs
} }
export async function parseSubs(url: string, secret: string) { export async function parseSubs(url: string, secret: string) {
const response = await fetch(url) const response = await fetch(url)
const data = await response.text() const data = await response.text()
var key = secret + '7fac1178830cfe0c' var key = secret + '7fac1178830cfe0c'
console.log(key) console.log(key)
var parsedSubtitle = CryptoJS.enc.Base64.parse(data.substring(0, 24)) var parsedSubtitle = CryptoJS.enc.Base64.parse(data.substring(0, 24))
var sec = CryptoJS.enc.Hex.parse(key) var sec = CryptoJS.enc.Hex.parse(key)
var som = data.substring(24) var som = data.substring(24)
try { try {
// Fuck You ADN // Fuck You ADN
var decrypted: any = CryptoJS.AES.decrypt(som, sec, { iv: parsedSubtitle }) var decrypted: any = CryptoJS.AES.decrypt(som, sec, { iv: parsedSubtitle })
decrypted = decrypted.toString(CryptoJS.enc.Utf8) decrypted = decrypted.toString(CryptoJS.enc.Utf8)
return decrypted return decrypted
} catch (error) { } catch (error) {
console.error('Error decrypting subtitles:', error) console.error('Error decrypting subtitles:', error)
return null return null
} }
} }

View File

@ -6,17 +6,17 @@ import { CrunchyEpisodes, CrunchySeason } from '../../types/crunchyroll'
import { loggedInCheck } from '../service/service.service' import { loggedInCheck } from '../service/service.service'
export async function loginController(request: FastifyRequest, reply: FastifyReply) { export async function loginController(request: FastifyRequest, reply: FastifyReply) {
const account = await loggedInCheck('CR') const account = await loggedInCheck('CR')
if (!account) { if (!account) {
return reply.code(401).send({ message: 'Not Logged in' }) return reply.code(401).send({ message: 'Not Logged in' })
} }
const { data, error } = await crunchyLogin(account.username, account.password) const { data, error } = await crunchyLogin(account.username, account.password)
if (error) { if (error) {
reply.code(400).send(error) reply.code(400).send(error)
} }
return reply.code(200).send(data) return reply.code(200).send(data)
} }

View File

@ -2,20 +2,20 @@ import type { FastifyInstance } from 'fastify'
import { loginController } from './crunchyroll.controller' import { loginController } from './crunchyroll.controller'
async function crunchyrollRoutes(server: FastifyInstance) { async function crunchyrollRoutes(server: FastifyInstance) {
server.post( server.post(
'/login', '/login',
{ {
schema: { schema: {
response: { response: {
'4xx': { '4xx': {
error: { type: 'string' }, error: { type: 'string' },
message: { type: 'string' } message: { type: 'string' }
} }
} }
} }
}, },
loginController loginController
) )
} }
export default crunchyrollRoutes export default crunchyrollRoutes

View File

@ -7,15 +7,76 @@ import { loggedInCheck } from '../service/service.service'
import { app } from 'electron' import { app } from 'electron'
const crErrors = [ const crErrors = [
{ {
error: 'invalid_grant', error: 'invalid_grant',
response: 'Email/Password is wrong' response: 'Email/Password is wrong'
} }
] ]
export async function crunchyLogin(user: string, passw: string) { export async function crunchyLogin(user: string, passw: string) {
const cachedData: const cachedData:
| { | {
access_token: string
refresh_token: string
expires_in: number
token_type: string
scope: string
country: string
account_id: string
profile_id: string
}
| undefined = server.CacheController.get('crtoken')
if (!cachedData) {
var { 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 (!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', data, data.expires_in - 30)
return { data: data, error: null }
}
return { data: cachedData, error: null }
}
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'
}
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 access_token: string
refresh_token: string refresh_token: string
expires_in: number expires_in: number
@ -24,157 +85,96 @@ export async function crunchyLogin(user: string, passw: string) {
country: string country: string
account_id: string account_id: string
profile_id: string profile_id: string
} }>('https://beta-api.crunchyroll.com/auth/v1/token', {
| undefined = server.CacheController.get('crtoken') type: 'POST',
body: new URLSearchParams(body).toString(),
if (!cachedData) { header: headers,
var { data, error } = await crunchyLoginFetch(user, passw) credentials: 'same-origin'
})
if (error) { if (error) {
messageBox( return { data: null, error: error }
'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) { if (!data) {
messageBox('error', ['Cancel'], 2, 'Failed to login', 'Failed to login to Crunchyroll', 'Crunchyroll returned null') return { data: null, error: 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', data, data.expires_in - 30)
return { data: data, error: null } return { data: data, error: null }
}
return { data: cachedData, error: null }
}
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'
}
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 }
} }
export async function crunchyGetPlaylist(q: string) { export async function crunchyGetPlaylist(q: string) {
const account = await loggedInCheck('CR') const account = await loggedInCheck('CR')
if (!account) return if (!account) return
const { data, error } = await crunchyLogin(account.username, account.password) const { data, error } = await crunchyLogin(account.username, account.password)
if (!data) return if (!data) return
const headers = { const headers = {
Authorization: `Bearer ${data.access_token}`, Authorization: `Bearer ${data.access_token}`,
'X-Cr-Disable-Drm': 'true' 'X-Cr-Disable-Drm': 'true'
} }
const query: any = { const query: any = {
q: q, q: q,
n: 100, n: 100,
type: 'series', type: 'series',
ratings: false, ratings: false,
locale: 'de-DE' locale: 'de-DE'
} }
try { try {
const response = await fetch(`https://cr-play-service.prd.crunchyrollsvc.com/v1/${q}/console/switch/play`, { const response = await fetch(`https://cr-play-service.prd.crunchyrollsvc.com/v1/${q}/console/switch/play`, {
method: 'GET', method: 'GET',
headers: headers headers: headers
}) })
if (response.ok) { if (response.ok) {
const data: VideoPlaylist = JSON.parse(await response.text()) const data: VideoPlaylist = JSON.parse(await response.text())
data.hardSubs = Object.values((data as any).hardSubs) data.hardSubs = Object.values((data as any).hardSubs)
data.subtitles = Object.values((data as any).subtitles) data.subtitles = Object.values((data as any).subtitles)
return data return data
} else { } else {
throw new Error(await response.text()) throw new Error(await response.text())
}
} catch (e) {
throw new Error(e as string)
} }
} catch (e) {
throw new Error(e as string)
}
} }
export async function crunchyGetPlaylistMPD(q: string) { export async function crunchyGetPlaylistMPD(q: string) {
const account = await loggedInCheck('CR') const account = await loggedInCheck('CR')
if (!account) return if (!account) return
const { data, error } = await crunchyLogin(account.username, account.password) const { data, error } = await crunchyLogin(account.username, account.password)
if (!data) return if (!data) return
const headers = { const headers = {
Authorization: `Bearer ${data.access_token}`, Authorization: `Bearer ${data.access_token}`,
'X-Cr-Disable-Drm': 'true' 'X-Cr-Disable-Drm': 'true'
} }
try { try {
const response = await fetch(q, { const response = await fetch(q, {
method: 'GET', method: 'GET',
headers: headers headers: headers
}) })
if (response.ok) { if (response.ok) {
const parsed = mpdParse(await response.text()) const parsed = mpdParse(await response.text())
return parsed return parsed
} else { } else {
throw new Error(await response.text()) throw new Error(await response.text())
}
} catch (e) {
throw new Error(e as string)
} }
} catch (e) {
throw new Error(e as string)
}
} }

View File

@ -1,110 +1,111 @@
import { FastifyReply, FastifyRequest } from "fastify" import { FastifyReply, FastifyRequest } from 'fastify'
import { crunchyLogin } from "../crunchyroll/crunchyroll.service" import { crunchyLogin } from '../crunchyroll/crunchyroll.service'
import { addEpisodeToPlaylist, getDownloading, getPlaylist, loggedInCheck, safeLoginData } from "./service.service" import { addEpisodeToPlaylist, getDownloading, getPlaylist, loggedInCheck, safeLoginData } from './service.service'
import { CrunchyEpisodes } from "../../types/crunchyroll" import { CrunchyEpisodes } from '../../types/crunchyroll'
import { adnLogin } from "../adn/adn.service" import { adnLogin } from '../adn/adn.service'
export async function checkLoginController(request: FastifyRequest<{ export async function checkLoginController(
Params: { request: FastifyRequest<{
id: string Params: {
id: string
}
}>,
reply: FastifyReply
) {
const account = await loggedInCheck(request.params.id)
if (!account) {
return reply.code(401).send({ message: 'Not Logged in' })
} }
}>, reply: FastifyReply) {
const account = await loggedInCheck(request.params.id)
if (!account) { return reply.code(200).send({ message: 'Logged in' })
return reply.code(401).send({ message: 'Not Logged in' })
}
return reply.code(200).send({ message: 'Logged in' })
} }
export async function loginController( export async function loginController(
request: FastifyRequest<{ request: FastifyRequest<{
Body: { Body: {
user: string user: string
password: string password: string
}, }
Params: { Params: {
id: string id: string
} }
}>, }>,
reply: FastifyReply reply: FastifyReply
) { ) {
const body = request.body const body = request.body
const params = request.params const params = request.params
const account = await loggedInCheck(params.id) const account = await loggedInCheck(params.id)
if (account) { if (account) {
return reply.code(404).send({ message: 'Already Logged In' }) return reply.code(404).send({ message: 'Already Logged In' })
} }
var responseData var responseData
var responseError var responseError
if (params.id === 'CR') { if (params.id === 'CR') {
const { data, error } = await crunchyLogin(body.user, body.password) const { data, error } = await crunchyLogin(body.user, body.password)
responseError = error, ;(responseError = error), (responseData = data)
responseData = data }
}
if (params.id === 'ADN') { if (params.id === 'ADN') {
const { data, error } = await adnLogin(body.user, body.password) const { data, error } = await adnLogin(body.user, body.password)
responseError = error, ;(responseError = error), (responseData = data)
responseData = data }
}
if (responseError || !responseData) { if (responseError || !responseData) {
return reply.code(404).send({ return reply.code(404).send({
message: 'Invalid Email or Password' message: 'Invalid Email or Password'
}) })
} }
await safeLoginData(body.user, body.password, params.id) await safeLoginData(body.user, body.password, params.id)
return reply.code(200).send() return reply.code(200).send()
} }
export async function addPlaylistController( export async function addPlaylistController(
request: FastifyRequest<{ request: FastifyRequest<{
Body: { Body: {
episodes: CrunchyEpisodes episodes: CrunchyEpisodes
dubs: Array<string> dubs: Array<string>
subs: Array<string> subs: Array<string>
dir: string dir: string
hardsub: boolean hardsub: boolean
quality: 1080 | 720 | 480 | 360 | 240 quality: 1080 | 720 | 480 | 360 | 240
service: 'CR' | 'ADN' service: 'CR' | 'ADN'
format: 'mp4' | 'mkv' format: 'mp4' | 'mkv'
} }
}>, }>,
reply: FastifyReply reply: FastifyReply
) { ) {
const body = request.body const body = request.body
for (const e of body.episodes) { for (const e of body.episodes) {
await addEpisodeToPlaylist(e, body.subs, body.dubs, body.dir, body.hardsub, 'waiting', body.quality, body.service, body.format) await addEpisodeToPlaylist(e, body.subs, body.dubs, body.dir, body.hardsub, 'waiting', body.quality, body.service, body.format)
} }
return reply.code(201).send() return reply.code(201).send()
} }
export async function getPlaylistController(request: FastifyRequest, reply: FastifyReply) { export async function getPlaylistController(request: FastifyRequest, reply: FastifyReply) {
const playlist = await getPlaylist() const playlist = await getPlaylist()
for (const v of playlist) { for (const v of playlist) {
if (v.dataValues.status === 'downloading') { if (v.dataValues.status === 'downloading') {
const found = await getDownloading(v.dataValues.id) const found = await getDownloading(v.dataValues.id)
if (found) { if (found) {
;(v as any).dataValues = { ;(v as any).dataValues = {
...v.dataValues, ...v.dataValues,
partsleft: found.partsToDownload, partsleft: found.partsToDownload,
partsdownloaded: found.downloadedParts, partsdownloaded: found.downloadedParts,
downloadspeed: found.downloadSpeed.toFixed(2) downloadspeed: found.downloadSpeed.toFixed(2)
}
}
} }
}
} }
}
return reply.code(200).send(playlist.reverse()) return reply.code(200).send(playlist.reverse())
} }

View File

@ -1,63 +1,63 @@
import { FastifyInstance } from "fastify" import { FastifyInstance } from 'fastify'
import { addPlaylistController, checkLoginController, getPlaylistController, loginController } from "./service.controller" import { addPlaylistController, checkLoginController, getPlaylistController, loginController } from './service.controller'
async function serviceRoutes(server: FastifyInstance) { async function serviceRoutes(server: FastifyInstance) {
server.post( server.post(
'/login/:id', '/login/:id',
{ {
schema: { schema: {
response: { response: {
'4xx': { '4xx': {
error: { type: 'string' }, error: { type: 'string' },
message: { type: 'string' } message: { type: 'string' }
}
}
} }
} },
} loginController
},
loginController
), ),
server.get(
'/check/:id',
{
schema: {
response: {
'4xx': {
error: { type: 'string' },
message: { type: 'string' }
}
}
}
},
checkLoginController
),
server.post(
'/playlist',
{
schema: {
response: {
'4xx': {
error: { type: 'string' },
message: { type: 'string' }
}
}
}
},
addPlaylistController
)
server.get( server.get(
'/check/:id', '/playlist',
{ {
schema: { schema: {
response: { response: {
'4xx': { '4xx': {
error: { type: 'string' }, error: { type: 'string' },
message: { type: 'string' } message: { type: 'string' }
}
}
} }
} },
} getPlaylistController
},
checkLoginController
),
server.post(
'/playlist',
{
schema: {
response: {
'4xx': {
error: { type: 'string' },
message: { type: 'string' }
}
}
}
},
addPlaylistController
) )
server.get( }
'/playlist',
{
schema: {
response: {
'4xx': {
error: { type: 'string' },
message: { type: 'string' }
}
}
}
},
getPlaylistController
)
}
export default serviceRoutes export default serviceRoutes

File diff suppressed because it is too large Load Diff

View File

@ -1,38 +1,38 @@
type ErrorType = { type ErrorType = {
error: string error: string
} | null } | null
export async function useFetch<T>( export async function useFetch<T>(
url: string, url: string,
options: { options: {
type: 'GET' | 'PUT' | 'POST' | 'DELETE' type: 'GET' | 'PUT' | 'POST' | 'DELETE'
header: HeadersInit header: HeadersInit
body: BodyInit body: BodyInit
query?: { [key: string]: string } query?: { [key: string]: string }
credentials?: RequestCredentials credentials?: RequestCredentials
}
): Promise<{ data: T | null; error: ErrorType }> {
const querystring = new URLSearchParams(options.query)
const raw = await fetch(`${url}${querystring ? querystring : ''}`, {
method: options.type,
headers: options.header,
body: options.body,
credentials: options.credentials
})
if (!raw.ok) {
const errorText = await raw.text()
let errorData: ErrorType = null
try {
errorData = JSON.parse(errorText)
} catch (error) {
console.error('Error parsing error text:', error)
} }
return { data: null, error: errorData } ): Promise<{ data: T | null; error: ErrorType }> {
} const querystring = new URLSearchParams(options.query)
const data = await raw.json() const raw = await fetch(`${url}${querystring ? querystring : ''}`, {
method: options.type,
headers: options.header,
body: options.body,
credentials: options.credentials
})
return { data: data, error: null } if (!raw.ok) {
const errorText = await raw.text()
let errorData: ErrorType = null
try {
errorData = JSON.parse(errorText)
} catch (error) {
console.error('Error parsing error text:', error)
}
return { data: null, error: errorData }
}
const data = await raw.json()
return { data: data, error: null }
} }

View File

@ -7,84 +7,84 @@ import { getFFMPEGPath } from './ffmpeg'
const ffmpegP = getFFMPEGPath() const ffmpegP = getFFMPEGPath()
export async function downloadMPDAudio(parts: { filename: string; url: string }[], dir: string, name: string) { export async function downloadMPDAudio(parts: { filename: string; url: string }[], dir: string, name: string) {
const path = await createFolder() const path = await createFolder()
const maxParallelDownloads = 5 const maxParallelDownloads = 5
const downloadPromises = [] const downloadPromises = []
for (const [index, part] of parts.entries()) { for (const [index, part] of parts.entries()) {
let retries = 0 let retries = 0
const downloadPromise = async () => { const downloadPromise = async () => {
let downloadSuccess = false let downloadSuccess = false
while (!downloadSuccess) { while (!downloadSuccess) {
try { try {
const stream = fs.createWriteStream(`${path}/${part.filename}`) const stream = fs.createWriteStream(`${path}/${part.filename}`)
await fetchAndPipe(part.url, stream, index + 1) await fetchAndPipe(part.url, stream, index + 1)
downloadSuccess = true downloadSuccess = true
} catch (error) { } catch (error) {
retries++ retries++
console.error(`Failed to download part ${part.filename}, retrying (${retries})...`) console.error(`Failed to download part ${part.filename}, retrying (${retries})...`)
await new Promise((resolve) => setTimeout(resolve, 1000)) await new Promise((resolve) => setTimeout(resolve, 1000))
}
}
}
downloadPromises.push(downloadPromise())
if (downloadPromises.length === maxParallelDownloads || index === parts.length - 1) {
await Promise.all(downloadPromises)
downloadPromises.length = 0
} }
}
} }
downloadPromises.push(downloadPromise()) return await mergePartsAudio(parts, path, dir, name)
if (downloadPromises.length === maxParallelDownloads || index === parts.length - 1) {
await Promise.all(downloadPromises)
downloadPromises.length = 0
}
}
return await mergePartsAudio(parts, path, dir, name)
} }
async function fetchAndPipe(url: string, stream: fs.WriteStream, index: number) { async function fetchAndPipe(url: string, stream: fs.WriteStream, index: number) {
const { body } = await fetch(url) const { body } = await fetch(url)
const readableStream = Readable.from(body as any) const readableStream = Readable.from(body as any)
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
readableStream readableStream
.pipe(stream) .pipe(stream)
.on('finish', () => { .on('finish', () => {
console.log(`Fragment ${index} downloaded`) console.log(`Fragment ${index} downloaded`)
resolve() resolve()
}) })
.on('error', (error) => { .on('error', (error) => {
reject(error) reject(error)
}) })
}) })
} }
async function mergePartsAudio(parts: { filename: string; url: string }[], tmp: string, dir: string, name: string) { async function mergePartsAudio(parts: { filename: string; url: string }[], tmp: string, dir: string, name: string) {
try { try {
const list: Array<string> = [] const list: Array<string> = []
for (const [index, part] of parts.entries()) { for (const [index, part] of parts.entries()) {
list.push(`${tmp}/${part.filename}`) list.push(`${tmp}/${part.filename}`)
} }
const concatenatedFile = `${tmp}/main.m4s` const concatenatedFile = `${tmp}/main.m4s`
await concatenateTSFiles(list, concatenatedFile) await concatenateTSFiles(list, concatenatedFile)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!ffmpegP.ffmpeg || !ffmpegP.ffprobe) return if (!ffmpegP.ffmpeg || !ffmpegP.ffprobe) return
Ffmpeg() Ffmpeg()
.setFfmpegPath(ffmpegP.ffmpeg) .setFfmpegPath(ffmpegP.ffmpeg)
.setFfprobePath(ffmpegP.ffprobe) .setFfprobePath(ffmpegP.ffprobe)
.input(concatenatedFile) .input(concatenatedFile)
.outputOptions('-c copy') .outputOptions('-c copy')
.save(`${dir}/${name}.aac`) .save(`${dir}/${name}.aac`)
.on('end', async () => { .on('end', async () => {
console.log('Merging finished') console.log('Merging finished')
await deleteFolder(tmp) await deleteFolder(tmp)
return resolve(`${dir}/${name}.aac`) return resolve(`${dir}/${name}.aac`)
})
}) })
}) } catch (error) {
} catch (error) { console.error('Error merging parts:', error)
console.error('Error merging parts:', error) }
}
} }

View File

@ -1,37 +1,37 @@
import fs from 'fs' import fs from 'fs'
export async function concatenateTSFiles(inputFiles: Array<string>, outputFile: string) { export async function concatenateTSFiles(inputFiles: Array<string>, outputFile: string) {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const writeStream = fs.createWriteStream(outputFile) const writeStream = fs.createWriteStream(outputFile)
writeStream.on('error', (error) => { writeStream.on('error', (error) => {
reject(error) reject(error)
})
writeStream.on('finish', () => {
console.log('TS files concatenated successfully!')
resolve()
})
const processNextFile = (index: number) => {
if (index >= inputFiles.length) {
writeStream.end()
return
}
const readStream = fs.createReadStream(inputFiles[index])
readStream.on('error', (error) => {
reject(error)
})
readStream.pipe(writeStream, { end: false })
readStream.on('end', () => {
processNextFile(index + 1)
})
}
processNextFile(0)
}) })
writeStream.on('finish', () => {
console.log('TS files concatenated successfully!')
resolve()
})
const processNextFile = (index: number) => {
if (index >= inputFiles.length) {
writeStream.end()
return
}
const readStream = fs.createReadStream(inputFiles[index])
readStream.on('error', (error) => {
reject(error)
})
readStream.pipe(writeStream, { end: false })
readStream.on('end', () => {
processNextFile(index + 1)
})
}
processNextFile(0)
})
} }

View File

@ -6,22 +6,22 @@ const appPath = app.getAppPath()
const resourcesPath = path.dirname(appPath) const resourcesPath = path.dirname(appPath)
const ffmpegPath = path.join(resourcesPath, 'ffmpeg') const ffmpegPath = path.join(resourcesPath, 'ffmpeg')
if (isDev) { if (isDev) {
require('dotenv').config() require('dotenv').config()
} }
export function getFFMPEGPath() { export function getFFMPEGPath() {
if (isDev) { if (isDev) {
const ffmpeg = process.env.FFMPEG_PATH const ffmpeg = process.env.FFMPEG_PATH
const ffprobe = process.env.FFPROBE_PATH const ffprobe = process.env.FFPROBE_PATH
console.log(ffmpeg) console.log(ffmpeg)
console.log(ffprobe) console.log(ffprobe)
return { ffmpeg: ffmpeg, ffprobe: ffprobe } return { ffmpeg: ffmpeg, ffprobe: ffprobe }
} else { } else {
const ffmpeg = path.join(ffmpegPath, 'ffmpeg.exe') const ffmpeg = path.join(ffmpegPath, 'ffmpeg.exe')
const ffprobe = path.join(ffmpegPath, 'ffprobe.exe') const ffprobe = path.join(ffmpegPath, 'ffprobe.exe')
return { ffmpeg: ffmpeg, ffprobe: ffprobe } return { ffmpeg: ffmpeg, ffprobe: ffprobe }
} }
} }

View File

@ -3,71 +3,71 @@ import { app } from 'electron'
import fs from 'fs' import fs from 'fs'
export async function createFolder() { export async function createFolder() {
const tempFolderPath = path.join(app.getPath('documents'), `crd-tmp-${(Math.random() + 1).toString(36).substring(2)}`) const tempFolderPath = path.join(app.getPath('documents'), `crd-tmp-${(Math.random() + 1).toString(36).substring(2)}`)
try { try {
await fs.promises.mkdir(tempFolderPath, { recursive: true }) await fs.promises.mkdir(tempFolderPath, { recursive: true })
return tempFolderPath return tempFolderPath
} catch (error) { } catch (error) {
console.error('Error creating temporary folder:', error) console.error('Error creating temporary folder:', error)
throw error throw error
} }
} }
export async function checkDirectoryExistence(dir: string) { export async function checkDirectoryExistence(dir: string) {
try { try {
await fs.promises.access(dir) await fs.promises.access(dir)
console.log(`Directory ${dir} exists.`) console.log(`Directory ${dir} exists.`)
return true return true
} catch (error) { } catch (error) {
console.log(`Directory ${dir} does not exist.`) console.log(`Directory ${dir} does not exist.`)
return false return false
} }
} }
export async function createFolderName(name: string, dir: string) { export async function createFolderName(name: string, dir: string) {
var folderPath var folderPath
const dirExists = await checkDirectoryExistence(dir) const dirExists = await checkDirectoryExistence(dir)
if (dirExists) { if (dirExists) {
folderPath = path.join(dir, name) folderPath = path.join(dir, name)
} else { } else {
folderPath = path.join(app.getPath('documents'), name) folderPath = path.join(app.getPath('documents'), name)
} }
try { try {
await fs.promises.access(folderPath) await fs.promises.access(folderPath)
return folderPath return folderPath
} catch (error) { } catch (error) {
try { try {
await fs.promises.mkdir(folderPath, { recursive: true }) await fs.promises.mkdir(folderPath, { recursive: true })
return folderPath return folderPath
} catch (mkdirError) { } catch (mkdirError) {
console.error('Error creating season folder:', mkdirError) console.error('Error creating season folder:', mkdirError)
throw mkdirError throw mkdirError
}
} }
}
} }
export async function deleteFolder(folderPath: string) { export async function deleteFolder(folderPath: string) {
fs.rmSync(folderPath, { recursive: true, force: true }) fs.rmSync(folderPath, { recursive: true, force: true })
} }
export async function deleteTemporaryFolders() { export async function deleteTemporaryFolders() {
const documentsPath = app.getPath('documents'); const documentsPath = app.getPath('documents')
const folderPrefix = 'crd-tmp-'; const folderPrefix = 'crd-tmp-'
try { try {
const files = await fs.promises.readdir(documentsPath); const files = await fs.promises.readdir(documentsPath)
const tempFolders = files.filter(file => file.startsWith(folderPrefix)); const tempFolders = files.filter((file) => file.startsWith(folderPrefix))
for (const folder of tempFolders) { for (const folder of tempFolders) {
const folderPath = path.join(documentsPath, folder); const folderPath = path.join(documentsPath, folder)
await deleteFolder(folderPath); await deleteFolder(folderPath)
console.log(`Temporary folder ${folder} deleted.`); console.log(`Temporary folder ${folder} deleted.`)
}
} catch (error) {
console.error('Error deleting temporary folders:', error)
throw error
} }
} catch (error) {
console.error('Error deleting temporary folders:', error);
throw error;
}
} }

View File

@ -5,144 +5,144 @@ import { finished } from 'stream/promises'
import CryptoJS from 'crypto-js' import CryptoJS from 'crypto-js'
export async function downloadCRSub( export async function downloadCRSub(
sub: { sub: {
format: string format: string
language: string language: string
url: string url: string
isDub: boolean isDub: boolean
}, },
dir: string, dir: string,
qual: 1080 | 720 | 480 | 360 | 240 qual: 1080 | 720 | 480 | 360 | 240
) { ) {
const path = `${dir}/${sub.language}${sub.isDub ? `-FORCED` : ''}.${sub.format}` const path = `${dir}/${sub.language}${sub.isDub ? `-FORCED` : ''}.${sub.format}`
var qualX var qualX
var qualY var qualY
switch (qual) { switch (qual) {
case 1080: case 1080:
qualX = 1920 qualX = 1920
qualY = 1080 qualY = 1080
break break
case 720: case 720:
qualX = 1280 qualX = 1280
qualY = 720 qualY = 720
break break
case 480: case 480:
qualX = 720 qualX = 720
qualY = 480 qualY = 480
break break
case 360: case 360:
qualX = 640 qualX = 640
qualY = 360 qualY = 360
break break
case 240: case 240:
qualX = 426 qualX = 426
qualY = 240 qualY = 240
break break
} }
const stream = fs.createWriteStream(path) const stream = fs.createWriteStream(path)
const response = await fetch(sub.url) const response = await fetch(sub.url)
var parsedASS = parse(await response.text()) var parsedASS = parse(await response.text())
parsedASS.info['Original Script'] = 'crd [https://github.com/stratuma/]' parsedASS.info['Original Script'] = 'crd [https://github.com/stratuma/]'
for (const s of parsedASS.styles.style) { for (const s of parsedASS.styles.style) {
;(s.Fontsize = String(Math.round((parseInt(s.Fontsize) / parseInt(parsedASS.info.PlayResY)) * qualY))), ;(s.Fontsize = String(Math.round((parseInt(s.Fontsize) / parseInt(parsedASS.info.PlayResY)) * qualY))),
(s.Outline = String(Math.round((parseInt(s.Outline) / parseInt(parsedASS.info.PlayResY)) * qualY))), (s.Outline = String(Math.round((parseInt(s.Outline) / parseInt(parsedASS.info.PlayResY)) * qualY))),
(s.MarginL = String(Math.round((parseInt(s.MarginL) / parseInt(parsedASS.info.PlayResY)) * qualY))), (s.MarginL = String(Math.round((parseInt(s.MarginL) / parseInt(parsedASS.info.PlayResY)) * qualY))),
(s.MarginR = String(Math.round((parseInt(s.MarginR) / parseInt(parsedASS.info.PlayResY)) * qualY))), (s.MarginR = String(Math.round((parseInt(s.MarginR) / parseInt(parsedASS.info.PlayResY)) * qualY))),
(s.MarginV = String(Math.round((parseInt(s.MarginV) / parseInt(parsedASS.info.PlayResY)) * qualY))) (s.MarginV = String(Math.round((parseInt(s.MarginV) / parseInt(parsedASS.info.PlayResY)) * qualY)))
} }
parsedASS.info.PlayResX = String(qualX) parsedASS.info.PlayResX = String(qualX)
parsedASS.info.PlayResY = String(qualY) parsedASS.info.PlayResY = String(qualY)
const fixed = stringify(parsedASS) const fixed = stringify(parsedASS)
const resampledSubs = resamplePOSSubtitle(fixed, 640, 360, qualX, qualY) const resampledSubs = resamplePOSSubtitle(fixed, 640, 360, qualX, qualY)
const readableStream = Readable.from([resampledSubs]) const readableStream = Readable.from([resampledSubs])
await finished(readableStream.pipe(stream)) await finished(readableStream.pipe(stream))
console.log(`Sub ${sub.language}.${sub.format} downloaded`) console.log(`Sub ${sub.language}.${sub.format} downloaded`)
return path return path
} }
function resamplePOSSubtitle(subtitle: string, ox: number, oy: number, nx: number, ny: number) { function resamplePOSSubtitle(subtitle: string, ox: number, oy: number, nx: number, ny: number) {
let lines = subtitle.split('\n') let lines = subtitle.split('\n')
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
let line = lines[i] let line = lines[i]
if (line.includes('pos(')) { if (line.includes('pos(')) {
let posMatches = line.matchAll(/pos\((-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)\)/g) let posMatches = line.matchAll(/pos\((-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)\)/g)
for (let posMatch of posMatches) { for (let posMatch of posMatches) {
let oldX = parseInt(posMatch[1]) let oldX = parseInt(posMatch[1])
let oldY = parseInt(posMatch[2]) let oldY = parseInt(posMatch[2])
let newX = Math.round((oldX / ox) * nx) let newX = Math.round((oldX / ox) * nx)
let newY = Math.round((oldY / oy) * ny) let newY = Math.round((oldY / oy) * ny)
let newPos = `pos(${newX},${newY})` let newPos = `pos(${newX},${newY})`
line = line.replace(posMatch[0], newPos) line = line.replace(posMatch[0], newPos)
} }
lines[i] = line lines[i] = line
}
if (line.includes('move(')) {
let posMatches = line.matchAll(/move\((-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)/g)
for (let posMatch of posMatches) {
let fromX = parseInt(posMatch[1])
let fromY = parseInt(posMatch[2])
let toX = parseInt(posMatch[3])
let toY = parseInt(posMatch[4])
let time1 = parseInt(posMatch[5])
let time2 = parseInt(posMatch[6])
let newFromX = Math.round((fromX / ox) * nx)
let newFromY = Math.round((fromY / oy) * ny)
let newToX = Math.round((toX / ox) * nx)
let newToY = Math.round((toY / oy) * ny)
let newMov = `move(${newFromX},${newFromY},${newToX},${newToY},${time1},${time2})`
line = line.replace(posMatch[0], newMov)
}
lines[i] = line
}
if (line.includes('\\fs')) {
let posMatches = line.matchAll(/\\fs(-?\d+(?:\.\d+)?)/g)
for (let posMatch of posMatches) {
let font = parseInt(posMatch[1])
let newFontSize = Math.round((font / oy) * ny)
let newFont = `\\fs${newFontSize}`
line = line.replace(posMatch[0], newFont)
}
lines[i] = line
}
if (line.includes('\\bord')) {
let posMatches = line.matchAll(/\\bord(-?\d+(?:\.\d+)?)/g)
for (let posMatch of posMatches) {
let oldBord = parseInt(posMatch[1])
let newBord = Math.round((oldBord / oy) * ny)
let bord = `\\bord${newBord}`
line = line.replace(posMatch[0], bord)
}
lines[i] = line
}
} }
if (line.includes('move(')) { return lines.join('\n')
let posMatches = line.matchAll(/move\((-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)/g)
for (let posMatch of posMatches) {
let fromX = parseInt(posMatch[1])
let fromY = parseInt(posMatch[2])
let toX = parseInt(posMatch[3])
let toY = parseInt(posMatch[4])
let time1 = parseInt(posMatch[5])
let time2 = parseInt(posMatch[6])
let newFromX = Math.round((fromX / ox) * nx)
let newFromY = Math.round((fromY / oy) * ny)
let newToX = Math.round((toX / ox) * nx)
let newToY = Math.round((toY / oy) * ny)
let newMov = `move(${newFromX},${newFromY},${newToX},${newToY},${time1},${time2})`
line = line.replace(posMatch[0], newMov)
}
lines[i] = line
}
if (line.includes('\\fs')) {
let posMatches = line.matchAll(/\\fs(-?\d+(?:\.\d+)?)/g)
for (let posMatch of posMatches) {
let font = parseInt(posMatch[1])
let newFontSize = Math.round((font / oy) * ny)
let newFont = `\\fs${newFontSize}`
line = line.replace(posMatch[0], newFont)
}
lines[i] = line
}
if (line.includes('\\bord')) {
let posMatches = line.matchAll(/\\bord(-?\d+(?:\.\d+)?)/g)
for (let posMatch of posMatches) {
let oldBord = parseInt(posMatch[1])
let newBord = Math.round((oldBord / oy) * ny)
let bord = `\\bord${newBord}`
line = line.replace(posMatch[0], bord)
}
lines[i] = line
}
}
return lines.join('\n')
} }
export async function downloadADNSub(link: string, dir: string, secret: string, language: string) { export async function downloadADNSub(link: string, dir: string, secret: string, language: string) {
var templateASS = `[Script Info] var templateASS = `[Script Info]
; Script generated by Aegisub 3.2.2 ; Script generated by Aegisub 3.2.2
; http://www.aegisub.org/ ; http://www.aegisub.org/
Title: Deutsch Title: Deutsch
@ -167,135 +167,135 @@ Style: Default,Arial,56,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,-1,0,0,0,100
[Events] [Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n` Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n`
const path = `${dir}/${language}.ass` const path = `${dir}/${language}.ass`
const stream = fs.createWriteStream(path) const stream = fs.createWriteStream(path)
const subURLFetch = await fetch(link) const subURLFetch = await fetch(link)
const subURL: { const subURL: {
location: string location: string
} = JSON.parse(await subURLFetch.text()) } = JSON.parse(await subURLFetch.text())
const rawSubsFetch = await fetch(subURL.location) const rawSubsFetch = await fetch(subURL.location)
const rawSubs = await rawSubsFetch.text() const rawSubs = await rawSubsFetch.text()
const subs = await ADNparseSub(rawSubs, secret) const subs = await ADNparseSub(rawSubs, secret)
const parsedSubs: { const parsedSubs: {
vde: Array<{ vde: Array<{
startTime: number startTime: number
endTime: number endTime: number
positionAligh: string positionAligh: string
lineAlign: string lineAlign: string
text: string text: string
}> }>
vostde: Array<{ vostde: Array<{
startTime: number startTime: number
endTime: number endTime: number
positionAligh: string positionAligh: string
lineAlign: string lineAlign: string
text: string text: string
}> }>
vostf: Array<{ vostf: Array<{
startTime: number startTime: number
endTime: number endTime: number
positionAligh: string positionAligh: string
lineAlign: string lineAlign: string
text: string text: string
}> }>
vf: Array<{ vf: Array<{
startTime: number startTime: number
endTime: number endTime: number
positionAligh: string positionAligh: string
lineAlign: string lineAlign: string
text: string text: string
}> }>
} = await JSON.parse(subs) } = await JSON.parse(subs)
// if (parsedSubs.vde) { // if (parsedSubs.vde) {
// for (const s of parsedSubs.vde) { // for (const s of parsedSubs.vde) {
// const convertedStart = convertToTimeFormat(s.startTime) // const convertedStart = convertToTimeFormat(s.startTime)
// const convertedEnd = convertToTimeFormat(s.endTime) // const convertedEnd = convertToTimeFormat(s.endTime)
// templateASS = // templateASS =
// templateASS + `Dialogue: 0,${convertedStart},${convertedEnd},Default,,0,0,0,,${s.text.replace('\n', '\\N').replace('<i>', '{\\i1}').replace('</i>', '{\\i0}')}\n` // templateASS + `Dialogue: 0,${convertedStart},${convertedEnd},Default,,0,0,0,,${s.text.replace('\n', '\\N').replace('<i>', '{\\i1}').replace('</i>', '{\\i0}')}\n`
// } // }
// } // }
if (parsedSubs.vostde) { if (parsedSubs.vostde) {
for (const s of parsedSubs.vostde) { for (const s of parsedSubs.vostde) {
const convertedStart = convertToTimeFormat(s.startTime) const convertedStart = convertToTimeFormat(s.startTime)
const convertedEnd = convertToTimeFormat(s.endTime) const convertedEnd = convertToTimeFormat(s.endTime)
templateASS = templateASS =
templateASS + `Dialogue: 0,${convertedStart},${convertedEnd},Default,,0,0,0,,${s.text.replace('\n', '\\N').replace('<i>', '{\\i1}').replace('</i>', '{\\i0}')}\n` templateASS + `Dialogue: 0,${convertedStart},${convertedEnd},Default,,0,0,0,,${s.text.replace('\n', '\\N').replace('<i>', '{\\i1}').replace('</i>', '{\\i0}')}\n`
}
} }
}
if (parsedSubs.vostf) { if (parsedSubs.vostf) {
for (const s of parsedSubs.vostf) { for (const s of parsedSubs.vostf) {
const convertedStart = convertToTimeFormat(s.startTime) const convertedStart = convertToTimeFormat(s.startTime)
const convertedEnd = convertToTimeFormat(s.endTime) const convertedEnd = convertToTimeFormat(s.endTime)
templateASS = templateASS =
templateASS + `Dialogue: 0,${convertedStart},${convertedEnd},Default,,0,0,0,,${s.text.replace('\n', '\\N').replace('<i>', '{\\i1}').replace('</i>', '{\\i0}')}\n` templateASS + `Dialogue: 0,${convertedStart},${convertedEnd},Default,,0,0,0,,${s.text.replace('\n', '\\N').replace('<i>', '{\\i1}').replace('</i>', '{\\i0}')}\n`
}
} }
}
// Disabling Changing ASS because still broken in vlc // Disabling Changing ASS because still broken in vlc
// parsedASS.info.PlayResX = "1920"; // parsedASS.info.PlayResX = "1920";
// parsedASS.info.PlayResY = "1080"; // parsedASS.info.PlayResY = "1080";
// for (const s of parsedASS.styles.style) { // for (const s of parsedASS.styles.style) {
// (s.Fontsize = "54"), (s.Outline = "4"); // (s.Fontsize = "54"), (s.Outline = "4");
// } // }
// const fixed = stringify(parsedASS) // const fixed = stringify(parsedASS)
const readableStream = Readable.from([templateASS]) const readableStream = Readable.from([templateASS])
await finished(readableStream.pipe(stream)) await finished(readableStream.pipe(stream))
console.log(`Sub downloaded`) console.log(`Sub downloaded`)
return path return path
} }
function convertToTimeFormat(time: number) { function convertToTimeFormat(time: number) {
var seconds: number | string = Math.floor(time) var seconds: number | string = Math.floor(time)
var milliseconds = Math.round((time - seconds) * 1000) var milliseconds = Math.round((time - seconds) * 1000)
var hours: number | string = Math.floor(seconds / 3600) var hours: number | string = Math.floor(seconds / 3600)
var minutes: number | string = Math.floor((seconds % 3600) / 60) var minutes: number | string = Math.floor((seconds % 3600) / 60)
seconds = seconds % 60 seconds = seconds % 60
hours = String(hours).padStart(2, '0') hours = String(hours).padStart(2, '0')
minutes = String(minutes).padStart(2, '0') minutes = String(minutes).padStart(2, '0')
seconds = String(seconds).padStart(2, '0') seconds = String(seconds).padStart(2, '0')
milliseconds = Math.round(milliseconds / 10) milliseconds = Math.round(milliseconds / 10)
var formattedMilliseconds = milliseconds < 10 ? '0' + milliseconds : milliseconds var formattedMilliseconds = milliseconds < 10 ? '0' + milliseconds : milliseconds
var formattedTime = hours + ':' + minutes + ':' + seconds + '.' + formattedMilliseconds var formattedTime = hours + ':' + minutes + ':' + seconds + '.' + formattedMilliseconds
return formattedTime return formattedTime
} }
export async function ADNparseSub(raw: string, secret: string) { export async function ADNparseSub(raw: string, secret: string) {
var key = secret + '7fac1178830cfe0c' var key = secret + '7fac1178830cfe0c'
console.log(key) console.log(key)
var parsedSubtitle = CryptoJS.enc.Base64.parse(raw.substring(0, 24)) var parsedSubtitle = CryptoJS.enc.Base64.parse(raw.substring(0, 24))
var sec = CryptoJS.enc.Hex.parse(key) var sec = CryptoJS.enc.Hex.parse(key)
var som = raw.substring(24) var som = raw.substring(24)
try { try {
// Fuck You ADN // Fuck You ADN
var decrypted: any = CryptoJS.AES.decrypt(som, sec, { iv: parsedSubtitle }) var decrypted: any = CryptoJS.AES.decrypt(som, sec, { iv: parsedSubtitle })
decrypted = decrypted.toString(CryptoJS.enc.Utf8) decrypted = decrypted.toString(CryptoJS.enc.Utf8)
return decrypted return decrypted
} catch (error) { } catch (error) {
console.error('Error decrypting subtitles:', error) console.error('Error decrypting subtitles:', error)
return null return null
} }
} }

View File

@ -1,145 +1,145 @@
export interface ADNPlayerConfig { export interface ADNPlayerConfig {
player: { player: {
image: string image: string
options: { options: {
user: { user: {
hasAccess: true hasAccess: true
profileId: number profileId: number
refreshToken: string refreshToken: string
refreshTokenUrl: string refreshTokenUrl: string
} }
chromecast: { chromecast: {
appId: string appId: string
refreshTokenUrl: string refreshTokenUrl: string
} }
ios: { ios: {
videoUrl: string videoUrl: string
appUrl: string appUrl: string
title: string title: string
} }
video: { video: {
startDate: string startDate: string
currentDate: string currentDate: string
available: boolean available: boolean
free: boolean free: boolean
url: string url: string
} }
dock: Array<string> dock: Array<string>
preference: { preference: {
quality: string quality: string
autoplay: boolean autoplay: boolean
language: string language: string
green: boolean green: boolean
} }
}
} }
}
} }
export interface ADNLink { export interface ADNLink {
links: { links: {
streaming: { streaming: {
vostde: { vostde: {
mobile: string mobile: string
sd: string sd: string
hd: string hd: string
fhd: string fhd: string
auto: string auto: string
} }
}
subtitles: {
all: string
}
history: string
nextVideoUrl: string
previousVideoUrl: string
} }
subtitles: { video: {
all: string 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
} }
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 { export interface ADNEpisode {
id: number, id: number
title: string, title: string
name: string, name: string
number: string, number: string
shortNumber: string, shortNumber: string
season: string, season: string
reference: string, reference: string
type: string, type: string
order: number, order: number
image: string, image: string
image2x: string, image2x: string
summary: string, summary: string
releaseDate: string, releaseDate: string
duration: number, duration: number
url: string, url: string
urlPath: string, urlPath: string
embeddedUrl: string, embeddedUrl: string
languages: Array<string>, languages: Array<string>
qualities: Array<string>, qualities: Array<string>
rating: number, rating: number
ratingsCount: number, ratingsCount: number
commentsCount: number, commentsCount: number
available: boolean, available: boolean
download: boolean, download: boolean
free: boolean, free: boolean
freeWithAds: boolean, freeWithAds: boolean
show: { show: {
id: number, id: number
title: string, title: string
type: string, type: string
originalTitle: string, originalTitle: string
shortTitle: string, shortTitle: string
reference: string, reference: string
age: string, age: string
languages: Array<string>, languages: Array<string>
summary: string, summary: string
image: string, image: string
image2x: string, image2x: string
imageHorizontal: string, imageHorizontal: string
imageHorizontal2x: string, imageHorizontal2x: string
url: string, url: string
urlPath: string, urlPath: string
episodeCount: number, episodeCount: number
genres: Array<string>, genres: Array<string>
copyright: string, copyright: string
rating: number, rating: number
ratingsCount: number, ratingsCount: number
commentsCount: number, commentsCount: number
qualities: Array<string>, qualities: Array<string>
simulcast: boolean, simulcast: boolean
free: boolean, free: boolean
available: boolean, available: boolean
download: boolean, download: boolean
basedOn: string, basedOn: string
tagline: Array<string>, tagline: Array<string>
firstReleaseYear: string, firstReleaseYear: string
productionStudio: string, productionStudio: string
countryOfOrigin: string, countryOfOrigin: string
productionTeam: Array<{ productionTeam: Array<{
role: string, role: string
name: string, name: string
}>, }>
nextVideoReleaseDate: string, nextVideoReleaseDate: string
indexable: boolean indexable: boolean
} }
indexable: boolean indexable: boolean
} }

View File

@ -1,146 +1,146 @@
export interface CrunchySeason { export interface CrunchySeason {
identifier: string identifier: string
description: string description: string
is_simulcast: boolean is_simulcast: boolean
subtitle_locales: Array<string> subtitle_locales: Array<string>
series_id: string series_id: string
id: string id: string
audio_locales: Array<string> audio_locales: Array<string>
title: string title: string
versions: Array<{ versions: Array<{
audio_locale: string
guid: string
original: boolean
variant: string
}>
season_sequence_number: number
season_number: number
maturity_ratings: Array<string>
mature_blocked: boolean
channel_id: string
is_subbed: boolean
audio_locale: string audio_locale: string
guid: string season_display_number: string
original: boolean is_complete: boolean
variant: string season_tags: Array<string>
}> is_mature: boolean
season_sequence_number: number is_dubbed: boolean
season_number: number slug_title: string
maturity_ratings: Array<string> availability_notes: string
mature_blocked: boolean number_of_episodes: boolean
channel_id: string
is_subbed: boolean
audio_locale: string
season_display_number: string
is_complete: boolean
season_tags: Array<string>
is_mature: boolean
is_dubbed: boolean
slug_title: string
availability_notes: string
number_of_episodes: boolean
} }
export interface CrunchySeasons extends Array<CrunchySeason> {} export interface CrunchySeasons extends Array<CrunchySeason> {}
export interface CrunchyEpisode { export interface CrunchyEpisode {
closed_captions_available: boolean closed_captions_available: boolean
availability_notes: string availability_notes: string
next_episode_title: string next_episode_title: string
upload_date: string upload_date: string
versions: Array<{ versions: Array<{
audio_locale: string audio_locale: string
guid: string guid: string
is_premium_only: boolean
media_guid: string
original: boolean
season_guid: string
variant: string
}>
season_slug_title: string
series_title: string
season_title: string
sequence_number: number
maturity_ratings: Array<string>
slug_title: string
is_premium_only: boolean is_premium_only: boolean
media_guid: string availability_ends: string
original: boolean identifier: string
season_guid: string recent_variant: string
variant: string free_available_date: string
}> subtitle_locales: Array<string>
season_slug_title: string series_id: string
series_title: string mature_blocked: boolean
season_title: string duration_ms: number
sequence_number: number availability_starts: string
maturity_ratings: Array<string> audio_locale: string
slug_title: string images: {
is_premium_only: boolean thumbnail: Array<
availability_ends: string Array<{
identifier: string height: number
recent_variant: string source: string
free_available_date: string type: string
subtitle_locales: Array<string> width: number
series_id: string }>
mature_blocked: boolean >
duration_ms: number }
availability_starts: string season_sequence_number: number
audio_locale: string season_id: string
images: { episode_number: number
thumbnail: Array< listing_id: string
Array<{ available_date: string
height: number channel_id: string
source: string season_number: number
type: string hd_flag: boolean
width: number recent_audio_locale: string
}> available_offline: boolean
> episode: string
} is_subbed: boolean
season_sequence_number: number media_type: string
season_id: string is_clip: boolean
episode_number: number title: string
listing_id: string streams_link: string
available_date: string slug: string
channel_id: string id: string
season_number: number production_episode_id: string
hd_flag: boolean is_dubbed: boolean
recent_audio_locale: string next_episode_id: string
available_offline: boolean series_slug_title: string
episode: string season_tags: Array<string>
is_subbed: boolean premium_date: string
media_type: string is_mature: boolean
is_clip: boolean premium_available_date: string
title: string description: string
streams_link: string episode_air_date: string
slug: string eligible_region: string
id: string
production_episode_id: string
is_dubbed: boolean
next_episode_id: string
series_slug_title: string
season_tags: Array<string>
premium_date: string
is_mature: boolean
premium_available_date: string
description: string
episode_air_date: string
eligible_region: string
} }
export interface CrunchyEpisodes extends Array<CrunchyEpisode> {} export interface CrunchyEpisodes extends Array<CrunchyEpisode> {}
export interface VideoPlaylist { export interface VideoPlaylist {
assetId: number assetId: number
audioLocale: string audioLocale: string
bifs: string bifs: string
burnedInLocale: string burnedInLocale: string
captions: string captions: string
hardSubs: Array<{ hardSubs: Array<{
hlang: string hlang: string
url: string
quality: string
}>
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
}>
token: string
url: string url: string
quality: string versions: Array<{
}> audio_locale: string
playbackType: string guid: string
session: { is_premium_only: boolean
renewSeconds: number media_guid: string
noNetworkRetryIntervalSeconds: number original: boolean
noNetworkTimeoutSeconds: number season_guid: string
maximumPauseSeconds: number variant: string
endOfVideoUnloadSeconds: number }>
sessionExpirationSeconds: number
usesStreamLimits: boolean
}
subtitles: Array<{
format: string
language: string
url: string
}>
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
}>
} }

View File

@ -1,71 +1,71 @@
declare module 'mpd-parser' { declare module 'mpd-parser' {
export type Segment = { export type Segment = {
uri: string uri: string
timeline: number timeline: number
duration: number duration: number
resolvedUri: string resolvedUri: string
map: { map: {
uri: string uri: string
resolvedUri: string resolvedUri: string
}
number: number
presentationTime: number
}
export type Playlist = {
attributes: {
NAME: string
BANDWIDTH: number
CODECS: string
'PROGRAM-ID': number
// Following for video only
'FRAME-RATE'?: number
AUDIO?: string // audio stream name
SUBTITLES?: string
RESOLUTION?: {
width: number
height: number
}
}
uri: string
endList: boolean
timeline: number
resolvedUri: string
targetDuration: number
discontinuitySequence: number
discontinuityStarts: []
timelineStarts: {
start: number
timeline: number
}[]
mediaSequence: number
contentProtection?: {
[type: string]: {
pssh?: Uint8Array
}
}
segments: Segment[]
}
export type Manifest = {
allowCache: boolean
discontinuityStarts: []
segments: []
endList: true
duration: number
playlists: Playlist[]
mediaGroups: {
AUDIO: {
audio: {
[name: string]: {
language: string
autoselect: boolean
default: boolean
playlists: Playlist[]
}
} }
} number: number
presentationTime: number
} }
}
export function parse(manifest: string): Manifest export type Playlist = {
attributes: {
NAME: string
BANDWIDTH: number
CODECS: string
'PROGRAM-ID': number
// Following for video only
'FRAME-RATE'?: number
AUDIO?: string // audio stream name
SUBTITLES?: string
RESOLUTION?: {
width: number
height: number
}
}
uri: string
endList: boolean
timeline: number
resolvedUri: string
targetDuration: number
discontinuitySequence: number
discontinuityStarts: []
timelineStarts: {
start: number
timeline: number
}[]
mediaSequence: number
contentProtection?: {
[type: string]: {
pssh?: Uint8Array
}
}
segments: Segment[]
}
export type Manifest = {
allowCache: boolean
discontinuityStarts: []
segments: []
endList: true
duration: number
playlists: Playlist[]
mediaGroups: {
AUDIO: {
audio: {
[name: string]: {
language: string
autoselect: boolean
default: boolean
playlists: Playlist[]
}
}
}
}
}
export function parse(manifest: string): Manifest
} }

View File

@ -17,197 +17,197 @@ const modules = [titleBarActionsModule, macMenuModule, updaterModule]
var mainWindow: BrowserWindow var mainWindow: BrowserWindow
function createWindow() { function createWindow() {
console.log('System info', { isProduction, platform, architucture }) console.log('System info', { isProduction, platform, architucture })
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
title: 'Crunchyroll Downloader', title: 'Crunchyroll Downloader',
icon: __dirname + '/icon/favicon.ico', icon: __dirname + '/icon/favicon.ico',
width: 950, width: 950,
height: 700, height: 700,
webPreferences: { webPreferences: {
devTools: true, devTools: true,
nodeIntegration: true, nodeIntegration: true,
contextIsolation: true, contextIsolation: true,
preload: path.join(__dirname, 'preload.js') preload: path.join(__dirname, 'preload.js')
}, },
titleBarStyle: 'hidden', titleBarStyle: 'hidden',
titleBarOverlay: { titleBarOverlay: {
color: 'rgba(0,0,0,0)', color: 'rgba(0,0,0,0)',
symbolColor: '#ffffff', symbolColor: '#ffffff',
height: 40 height: 40
}, },
resizable: false, resizable: false,
fullscreen: false, fullscreen: false,
maximizable: false, maximizable: false,
vibrancy: 'fullscreen-ui', vibrancy: 'fullscreen-ui',
// Not working when unfocusing the window somehow? // Not working when unfocusing the window somehow?
backgroundMaterial: 'acrylic', backgroundMaterial: 'acrylic',
show: false show: false
})
// Show window after loading page
mainWindow.once('ready-to-show', () => {
mainWindow.show()
})
// Closes all windows if mainwindow is being closed
mainWindow.on('closed', () => {
app.quit()
})
// Lock app to single instance
if (singleInstance(app, mainWindow)) return
// Open the DevTools.
!isProduction &&
mainWindow.webContents.openDevTools({
mode: 'bottom'
}) })
return mainWindow // Show window after loading page
mainWindow.once('ready-to-show', () => {
mainWindow.show()
})
// Closes all windows if mainwindow is being closed
mainWindow.on('closed', () => {
app.quit()
})
// Lock app to single instance
if (singleInstance(app, mainWindow)) return
// Open the DevTools.
!isProduction &&
mainWindow.webContents.openDevTools({
mode: 'bottom'
})
return mainWindow
} }
// App events // App events
// ========== // ==========
app.whenReady().then(async () => { app.whenReady().then(async () => {
startAPI() startAPI()
const mainWindow = createWindow() const mainWindow = createWindow()
if (!mainWindow) return if (!mainWindow) return
// Load renderer process // Load renderer process
await dynamicRenderer(mainWindow) await dynamicRenderer(mainWindow)
// Initialize modules // Initialize modules
console.log('-'.repeat(30) + '\n[+] Loading modules...') console.log('-'.repeat(30) + '\n[+] Loading modules...')
modules.forEach((module) => { modules.forEach((module) => {
try { try {
module(mainWindow) module(mainWindow)
} catch (err: any) { } catch (err: any) {
console.log('[!] Module error: ', err.message || err) console.log('[!] Module error: ', err.message || err)
} }
}) })
console.log('[!] Loading modules: Done.' + '\r\n' + '-'.repeat(30)) console.log('[!] Loading modules: Done.' + '\r\n' + '-'.repeat(30))
}) })
export async function messageBox( export async function messageBox(
type: 'none' | 'info' | 'error' | 'question' | 'warning' | undefined, type: 'none' | 'info' | 'error' | 'question' | 'warning' | undefined,
buttons: Array<'Cancel'>, buttons: Array<'Cancel'>,
defaultId: number, defaultId: number,
title: string, title: string,
message: string, message: string,
detail: string | undefined detail: string | undefined
) { ) {
const options = { const options = {
type: type as 'none' | 'info' | 'error' | 'question' | 'warning' | undefined, type: type as 'none' | 'info' | 'error' | 'question' | 'warning' | undefined,
buttons: buttons, buttons: buttons,
defaultId: defaultId, defaultId: defaultId,
title: title, title: title,
message: message, message: message,
detail: detail detail: detail
} }
const response = dialog.showMessageBox(options) const response = dialog.showMessageBox(options)
console.log(response) console.log(response)
} }
export async function setProgressBar(c: number) { export async function setProgressBar(c: number) {
mainWindow.setProgressBar(c) mainWindow.setProgressBar(c)
} }
ipcMain.handle('dialog:openDirectory', async () => { ipcMain.handle('dialog:openDirectory', async () => {
const window = BrowserWindow.getFocusedWindow() const window = BrowserWindow.getFocusedWindow()
if (!window) { if (!window) {
return return
} }
const { canceled, filePaths } = await dialog.showOpenDialog(window, { const { canceled, filePaths } = await dialog.showOpenDialog(window, {
properties: ['openDirectory'] properties: ['openDirectory']
}) })
if (canceled) { if (canceled) {
return await settings.get('downloadPath') return await settings.get('downloadPath')
} else { } else {
await settings.set('downloadPath', filePaths[0]) await settings.set('downloadPath', filePaths[0])
return filePaths[0] return filePaths[0]
} }
}) })
ipcMain.handle('dialog:defaultDirectory', async () => { ipcMain.handle('dialog:defaultDirectory', async () => {
const savedPath = await settings.get('downloadPath') const savedPath = await settings.get('downloadPath')
if (!savedPath) { if (!savedPath) {
const path = app.getPath('documents') const path = app.getPath('documents')
await settings.set('downloadPath', path) await settings.set('downloadPath', path)
return path return path
} }
return savedPath return savedPath
}) })
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
app.quit() app.quit()
} }
}) })
const openWindows = new Map() const openWindows = new Map()
// Open New Window // Open New Window
ipcMain.handle( ipcMain.handle(
'window:openNewWindow', 'window:openNewWindow',
async ( async (
events, events,
opt: { opt: {
title: string title: string
url: string url: string
width: number width: number
height: number height: number
}
) => {
if (openWindows.has(opt.title)) {
const existingWindow = openWindows.get(opt.title)
existingWindow.focus()
return
}
const newWindow = new BrowserWindow({
title: opt.title,
icon: __dirname + '/icon/favicon.ico',
width: opt.width,
height: opt.height,
webPreferences: {
devTools: true,
nodeIntegration: true,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
},
titleBarStyle: 'hidden',
titleBarOverlay: {
color: 'rgba(0,0,0,0)',
symbolColor: '#ffffff',
height: 40
},
resizable: false,
fullscreen: false,
maximizable: false,
vibrancy: 'fullscreen-ui',
backgroundMaterial: 'acrylic',
show: false
})
newWindow.once('ready-to-show', () => {
newWindow.show()
})
newWindow.loadURL(opt.url)
openWindows.set(opt.title, newWindow)
newWindow.on('closed', () => {
openWindows.delete(opt.title)
})
} }
) => {
if (openWindows.has(opt.title)) {
const existingWindow = openWindows.get(opt.title)
existingWindow.focus()
return
}
const newWindow = new BrowserWindow({
title: opt.title,
icon: __dirname + '/icon/favicon.ico',
width: opt.width,
height: opt.height,
webPreferences: {
devTools: true,
nodeIntegration: true,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
},
titleBarStyle: 'hidden',
titleBarOverlay: {
color: 'rgba(0,0,0,0)',
symbolColor: '#ffffff',
height: 40
},
resizable: false,
fullscreen: false,
maximizable: false,
vibrancy: 'fullscreen-ui',
backgroundMaterial: 'acrylic',
show: false
})
newWindow.once('ready-to-show', () => {
newWindow.show()
})
newWindow.loadURL(opt.url)
openWindows.set(opt.title, newWindow)
newWindow.on('closed', () => {
openWindows.delete(opt.title)
})
}
) )

View File

@ -12,12 +12,12 @@ const isProduction = process.env.NODE_ENV !== 'development'
// Dynamic Renderer // Dynamic Renderer
// ================ // ================
export default async function (mainWindow: BrowserWindow) { export default async function (mainWindow: BrowserWindow) {
if (!isProduction) return mainWindow.loadURL('http://localhost:3000/') if (!isProduction) return mainWindow.loadURL('http://localhost:3000/')
const app = express() const app = express()
app.use('/', serveStatic(path.join(__dirname, '../../public'))) app.use('/', serveStatic(path.join(__dirname, '../../public')))
const listener = app.listen(8079, 'localhost', () => { const listener = app.listen(8079, 'localhost', () => {
const port = (listener.address() as any).port const port = (listener.address() as any).port
console.log('Dynamic-Renderer Listening on', port) console.log('Dynamic-Renderer Listening on', port)
mainWindow.loadURL(`http://localhost:${port}`) mainWindow.loadURL(`http://localhost:${port}`)
}) })
} }

View File

@ -7,54 +7,54 @@ const template: (Electron.MenuItemConstructorOptions | Electron.MenuItem)[] = []
// Module // Module
// ====== // ======
export default (mainWindow: BrowserWindow) => { export default (mainWindow: BrowserWindow) => {
const isDevelopment = process.env.NODE_ENV === 'development' const isDevelopment = process.env.NODE_ENV === 'development'
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
// OS X // OS X
const name = 'electron-nuxt3' const name = 'electron-nuxt3'
template.unshift({ template.unshift({
label: name, label: name,
submenu: [ submenu: [
{ {
label: 'About ' + name, label: 'About ' + name,
role: 'about' role: 'about'
}, },
{ {
label: 'Quit', label: 'Quit',
accelerator: 'Command+Q', accelerator: 'Command+Q',
click() { click() {
app.quit() app.quit()
} }
}, },
{ {
label: 'Reload', label: 'Reload',
accelerator: 'Command+R', accelerator: 'Command+R',
click() { click() {
// Reload the current window // Reload the current window
if (mainWindow) { if (mainWindow) {
mainWindow.reload() mainWindow.reload()
} }
} }
}, },
...(isDevelopment ...(isDevelopment
? [ ? [
{ {
label: 'Toggle Developer Tools', label: 'Toggle Developer Tools',
accelerator: 'Alt+Command+I', accelerator: 'Alt+Command+I',
click() { click() {
// Open the DevTools. // Open the DevTools.
if (mainWindow) { if (mainWindow) {
mainWindow.webContents.toggleDevTools() mainWindow.webContents.toggleDevTools()
} }
} }
} }
]
: [])
] ]
: []) })
]
})
const menu = Menu.buildFromTemplate(template) const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu) Menu.setApplicationMenu(menu)
console.log('[-] MODULE::macMenu Initialized') console.log('[-] MODULE::macMenu Initialized')
} }
} }

View File

@ -3,48 +3,48 @@ import { ipcMain, BrowserWindow } from 'electron'
// Helpers // Helpers
// ======= // =======
const getWindowFromEvent = (event: Electron.IpcMainInvokeEvent) => { const getWindowFromEvent = (event: Electron.IpcMainInvokeEvent) => {
const webContents = event.sender const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents) const win = BrowserWindow.fromWebContents(webContents)
return win return win
} }
// Module // Module
// ====== // ======
export default (mainWindow: BrowserWindow) => { export default (mainWindow: BrowserWindow) => {
ipcMain.handle('isMaximized:app', (event) => { ipcMain.handle('isMaximized:app', (event) => {
const win = getWindowFromEvent(event) const win = getWindowFromEvent(event)
return win?.isMaximized() return win?.isMaximized()
}) })
ipcMain.handle('titlebar:action', (event, action: 'toggleMaximize' | 'minimize') => { ipcMain.handle('titlebar:action', (event, action: 'toggleMaximize' | 'minimize') => {
const win = getWindowFromEvent(event) const win = getWindowFromEvent(event)
if (!win) return if (!win) return
switch (action) { switch (action) {
case 'toggleMaximize': case 'toggleMaximize':
win.isMaximized() ? win.unmaximize() : win.maximize() win.isMaximized() ? win.unmaximize() : win.maximize()
break break
case 'minimize': case 'minimize':
win.minimize() win.minimize()
break break
} }
}) })
ipcMain.handle('close:app', (event) => { ipcMain.handle('close:app', (event) => {
const win = getWindowFromEvent(event) const win = getWindowFromEvent(event)
if (!win) return if (!win) return
win.close() win.close()
}) })
ipcMain.handle('get:windowVisible', (_event) => { ipcMain.handle('get:windowVisible', (_event) => {
return mainWindow.isVisible() return mainWindow.isVisible()
}) })
mainWindow.on('maximize', () => mainWindow.webContents.send('window:maximizeChanged', true)) mainWindow.on('maximize', () => mainWindow.webContents.send('window:maximizeChanged', true))
mainWindow.on('unmaximize', () => mainWindow.webContents.send('window:maximizeChanged', false)) mainWindow.on('unmaximize', () => mainWindow.webContents.send('window:maximizeChanged', false))
mainWindow.on('enter-full-screen', () => mainWindow.webContents.send('window:fullscreenChanged', true)) mainWindow.on('enter-full-screen', () => mainWindow.webContents.send('window:fullscreenChanged', true))
mainWindow.on('leave-full-screen', () => mainWindow.webContents.send('window:fullscreenChanged', false)) mainWindow.on('leave-full-screen', () => mainWindow.webContents.send('window:fullscreenChanged', false))
console.log('[-] MODULE::titleBarActions Initialized') console.log('[-] MODULE::titleBarActions Initialized')
} }
// https://www.electronjs.org/docs/latest/tutorial/ipc // https://www.electronjs.org/docs/latest/tutorial/ipc

View File

@ -2,7 +2,7 @@ import { BrowserWindow, ipcMain } from 'electron'
import { autoUpdater } from 'electron-updater' import { autoUpdater } from 'electron-updater'
import log from 'electron-log' import log from 'electron-log'
var status: { status: string, info: any } = { status: "", info: null } var status: { status: string; info: any } = { status: '', info: null }
autoUpdater.logger = log autoUpdater.logger = log
;(autoUpdater.logger as typeof log).transports.file.level = 'info' ;(autoUpdater.logger as typeof log).transports.file.level = 'info'
@ -11,54 +11,57 @@ autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = false autoUpdater.autoInstallOnAppQuit = false
ipcMain.handle('updater:getUpdateStatus', async () => { ipcMain.handle('updater:getUpdateStatus', async () => {
return status return status
}) })
export default (mainWindow: BrowserWindow) => { export default (mainWindow: BrowserWindow) => {
let readyToInstall = false let readyToInstall = false
function updateStatus(statusA: string, info?: any) { function updateStatus(statusA: string, info?: any) {
status = { status: statusA, info: info } status = { status: statusA, info: info }
} }
autoUpdater.on('checking-for-update', () => { autoUpdater.on('checking-for-update', () => {
updateStatus('check-for-update') updateStatus('check-for-update')
}) })
autoUpdater.on('update-available', (_info) => { autoUpdater.on('update-available', (_info) => {
updateStatus('update-available', _info) updateStatus('update-available', _info)
}) })
autoUpdater.on('update-not-available', (_info) => { autoUpdater.on('update-not-available', (_info) => {
updateStatus('update-not-available', _info) updateStatus('update-not-available', _info)
}) })
autoUpdater.on('error', (_err) => { autoUpdater.on('error', (_err) => {
updateStatus('update-error', _err) updateStatus('update-error', _err)
}) })
autoUpdater.on('download-progress', (progress) => { autoUpdater.on('download-progress', (progress) => {
updateStatus('downloading', progress) updateStatus('downloading', progress)
}) })
autoUpdater.on('update-downloaded', (_info) => { autoUpdater.on('update-downloaded', (_info) => {
updateStatus('update-downloaded', _info) updateStatus('update-downloaded', _info)
mainWindow.webContents.send('updater:readyToInstall') mainWindow.webContents.send('updater:readyToInstall')
readyToInstall = true readyToInstall = true
}) })
ipcMain.handle('updater:check', async (_event) => { ipcMain.handle('updater:check', async (_event) => {
return await autoUpdater.checkForUpdates() return await autoUpdater.checkForUpdates()
}) })
ipcMain.handle('updater:download', async (_event) => { ipcMain.handle('updater:download', async (_event) => {
return await autoUpdater.downloadUpdate() return await autoUpdater.downloadUpdate()
}) })
ipcMain.handle('updater:quitAndInstall', (_event) => { ipcMain.handle('updater:quitAndInstall', (_event) => {
if (!readyToInstall) return if (!readyToInstall) return
autoUpdater.quitAndInstall() autoUpdater.quitAndInstall()
}) })
autoUpdater.checkForUpdates()
setInterval(() => {
autoUpdater.checkForUpdates() autoUpdater.checkForUpdates()
}, 1000 * 60 * 60 * 2)
console.log('[-] MODULE::updater Initialized') setInterval(
() => {
autoUpdater.checkForUpdates()
},
1000 * 60 * 60 * 2
)
console.log('[-] MODULE::updater Initialized')
} }

View File

@ -1,16 +1,10 @@
import {contextBridge, ipcRenderer} from 'electron' import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('myAPI', { contextBridge.exposeInMainWorld('myAPI', {
selectFolder: () => ipcRenderer.invoke('dialog:openDirectory'), selectFolder: () => ipcRenderer.invoke('dialog:openDirectory'),
getFolder: () => ipcRenderer.invoke('dialog:defaultDirectory'), getFolder: () => ipcRenderer.invoke('dialog:defaultDirectory'),
openWindow: (opt: { openWindow: (opt: { title: string; url: string; width: number; height: number; backgroundColor: string }) => ipcRenderer.invoke('window:openNewWindow', opt),
title: string, getUpdateStatus: () => ipcRenderer.invoke('updater:getUpdateStatus'),
url: string, startUpdateDownload: () => ipcRenderer.invoke('updater:download'),
width: number, startUpdateInstall: () => ipcRenderer.invoke('updater:quitAndInstall')
height: number,
backgroundColor: string
}) => ipcRenderer.invoke('window:openNewWindow', opt),
getUpdateStatus: () => ipcRenderer.invoke('updater:getUpdateStatus'),
startUpdateDownload: () => ipcRenderer.invoke('updater:download'),
startUpdateInstall: () => ipcRenderer.invoke('updater:quitAndInstall'),
}) })

View File

@ -1,23 +1,23 @@
import { App, BrowserWindow } from 'electron' import { App, BrowserWindow } from 'electron'
export default (app: App, win: BrowserWindow) => { export default (app: App, win: BrowserWindow) => {
const gotTheLock = app.requestSingleInstanceLock() const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) { if (!gotTheLock) {
app.quit() app.quit()
return true return true
}
app.on('second-instance', (_, _argv) => {
if (win) {
win.show()
if (win.isMinimized()) win.restore()
win.focus()
} }
})
app.on('open-url', function (event, url) { app.on('second-instance', (_, _argv) => {
event.preventDefault() if (win) {
win.webContents.send('deeplink', url) win.show()
}) if (win.isMinimized()) win.restore()
win.focus()
}
})
app.on('open-url', function (event, url) {
event.preventDefault()
win.webContents.send('deeplink', url)
})
} }

View File

@ -1,103 +1,103 @@
{ {
"compilerOptions": { "compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */ /* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */ /* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */ /* Language and Environment */
"target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, "target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */ // "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */ /* Modules */
"module": "commonjs" /* Specify what module code is generated. */, "module": "commonjs" /* Specify what module code is generated. */,
// "rootDir": "./", /* Specify the root folder within your source files. */ // "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */ // "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "resolveJsonModule": true, /* Enable importing .json files. */ // "resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */ // "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */ /* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */ /* Emit */
"declaration": false, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ "declaration": false /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,
// "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "../.output/src" /* Specify an output folder for all emitted files. */, "outDir": "../.output/src" /* Specify an output folder for all emitted files. */,
// "removeComments": true, /* Disable emitting comments. */ // "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */ // "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */ // "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */ /* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */ /* Type Checking */
"strict": true /* Enable all strict type-checking options. */, "strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */ /* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */
} }
} }

View File

@ -1,9 +1,4 @@
{ {
// https://nuxt.com/docs/guide/concepts/typescript // https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json", "extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"types": [
"@nuxtjs/i18n",
]
}
} }