Merge pull request #1078 from anidl/tabs

Convert to Tabs (4 space) instead of 2 space
This commit is contained in:
Stratuma 2025-09-30 09:48:29 +02:00 committed by GitHub
commit 8365e9d9e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
125 changed files with 15868 additions and 15866 deletions

3
.git-blame-ignore-revs Normal file
View file

@ -0,0 +1,3 @@
# Ignore the whitespace changes in the following commits
460b4c1d0e12c88459aaff786fbe348c4c3d517a
a14466ec5d29accbe81b5ffac6e0a1373d04e356

View file

@ -11,9 +11,9 @@
"requirePragma": false,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"tabWidth": 4,
"trailingComma": "none",
"useTabs": false,
"useTabs": true,
"vueIndentScriptAndStyle": false,
"printWidth": 180,
"endOfLine": "auto"

View file

@ -1,50 +1,50 @@
export interface ADNPlayerConfig {
player: Player;
player: Player;
}
export interface Player {
image: string;
options: Options;
image: string;
options: Options;
}
export interface Options {
user: User;
chromecast: Chromecast;
ios: Ios;
video: Video;
dock: any[];
preference: Preference;
user: User;
chromecast: Chromecast;
ios: Ios;
video: Video;
dock: any[];
preference: Preference;
}
export interface Chromecast {
appId: string;
refreshTokenUrl: string;
appId: string;
refreshTokenUrl: string;
}
export interface Ios {
videoUrl: string;
appUrl: string;
title: string;
videoUrl: string;
appUrl: string;
title: string;
}
export interface Preference {
quality: string;
autoplay: boolean;
language: string;
green: boolean;
quality: string;
autoplay: boolean;
language: string;
green: boolean;
}
export interface User {
hasAccess: boolean;
profileId: number;
refreshToken: string;
refreshTokenUrl: string;
hasAccess: boolean;
profileId: number;
refreshToken: string;
refreshTokenUrl: string;
}
export interface Video {
startDate: null;
currentDate: Date;
available: boolean;
free: boolean;
url: string;
startDate: null;
currentDate: Date;
available: boolean;
free: boolean;
url: string;
}

76
@types/adnSearch.d.ts vendored
View file

@ -1,46 +1,46 @@
export interface ADNSearch {
shows: ADNSearchShow[];
total: number;
shows: ADNSearchShow[];
total: number;
}
export interface ADNSearchShow {
id: number;
title: string;
type: string;
originalTitle: string;
shortTitle: string;
reference: string;
age: string;
languages: string[];
summary: string;
image: string;
image2x: string;
imageHorizontal: string;
imageHorizontal2x: string;
url: string;
urlPath: string;
episodeCount: number;
genres: string[];
copyright: string;
rating: number;
ratingsCount: number;
commentsCount: number;
qualities: string[];
simulcast: boolean;
free: boolean;
available: boolean;
download: boolean;
basedOn: string;
tagline: null;
firstReleaseYear: string;
productionStudio: string;
countryOfOrigin: string;
productionTeam: ProductionTeam[];
nextVideoReleaseDate: null;
indexable: boolean;
id: number;
title: string;
type: string;
originalTitle: string;
shortTitle: string;
reference: string;
age: string;
languages: string[];
summary: string;
image: string;
image2x: string;
imageHorizontal: string;
imageHorizontal2x: string;
url: string;
urlPath: string;
episodeCount: number;
genres: string[];
copyright: string;
rating: number;
ratingsCount: number;
commentsCount: number;
qualities: string[];
simulcast: boolean;
free: boolean;
available: boolean;
download: boolean;
basedOn: string;
tagline: null;
firstReleaseYear: string;
productionStudio: string;
countryOfOrigin: string;
productionTeam: ProductionTeam[];
nextVideoReleaseDate: null;
indexable: boolean;
}
export interface ProductionTeam {
role: string;
name: string;
role: string;
name: string;
}

View file

@ -1,51 +1,51 @@
export interface ADNStreams {
links: Links;
video: Video;
metadata: Metadata;
links: Links;
video: Video;
metadata: Metadata;
}
export interface Links {
streaming: Streaming;
subtitles: Subtitles;
history: string;
nextVideoUrl: string;
previousVideoUrl: string;
streaming: Streaming;
subtitles: Subtitles;
history: string;
nextVideoUrl: string;
previousVideoUrl: string;
}
export interface Streaming {
[streams: string]: Streams;
[streams: string]: Streams;
}
export interface Streams {
mobile: string;
sd: string;
hd: string;
fhd: string;
auto: string;
mobile: string;
sd: string;
hd: string;
fhd: string;
auto: string;
}
export interface Subtitles {
all: string;
all: string;
}
export interface Metadata {
title: string;
subtitle: string;
summary: null;
rating: number;
title: string;
subtitle: string;
summary: null;
rating: number;
}
export interface 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;
guid: string;
id: number;
currentTime: number;
duration: number;
url: string;
image: string;
tcEpisodeStart?:string;
tcEpisodeEnd?: string;
tcIntroStart?: string;
tcIntroEnd?: string;
tcEndingStart?: string;
tcEndingEnd?: string;
}

View file

@ -1,11 +1,11 @@
export interface ADNSubtitles {
[subtitleLang: string]: Subtitle[];
[subtitleLang: string]: Subtitle[];
}
export interface Subtitle {
startTime: number;
endTime: number;
positionAlign: string;
lineAlign: string;
text: string;
startTime: number;
endTime: number;
positionAlign: string;
lineAlign: string;
text: string;
}

132
@types/adnVideos.d.ts vendored
View file

@ -1,77 +1,77 @@
export interface ADNVideos {
videos: ADNVideo[];
videos: ADNVideo[];
}
export interface ADNVideo {
id: number;
title: string;
name: string;
number: string;
shortNumber: string;
season: string;
reference: string;
type: string;
order: number;
image: string;
image2x: string;
summary: string;
releaseDate: Date;
duration: number;
url: string;
urlPath: string;
embeddedUrl: string;
languages: string[];
qualities: string[];
rating: number;
ratingsCount: number;
commentsCount: number;
available: boolean;
download: boolean;
free: boolean;
freeWithAds: boolean;
show: Show;
indexable: boolean;
isSelected?: boolean;
id: number;
title: string;
name: string;
number: string;
shortNumber: string;
season: string;
reference: string;
type: string;
order: number;
image: string;
image2x: string;
summary: string;
releaseDate: Date;
duration: number;
url: string;
urlPath: string;
embeddedUrl: string;
languages: string[];
qualities: string[];
rating: number;
ratingsCount: number;
commentsCount: number;
available: boolean;
download: boolean;
free: boolean;
freeWithAds: boolean;
show: Show;
indexable: boolean;
isSelected?: boolean;
}
export interface Show {
id: number;
title: string;
type: string;
originalTitle: string;
shortTitle: string;
reference: string;
age: string;
languages: string[];
summary: string;
image: string;
image2x: string;
imageHorizontal: string;
imageHorizontal2x: string;
url: string;
urlPath: string;
episodeCount: number;
genres: string[];
copyright: string;
rating: number;
ratingsCount: number;
commentsCount: number;
qualities: string[];
simulcast: boolean;
free: boolean;
available: boolean;
download: boolean;
basedOn: string;
tagline: string;
firstReleaseYear: string;
productionStudio: string;
countryOfOrigin: string;
productionTeam: ProductionTeam[];
nextVideoReleaseDate: Date;
indexable: boolean;
id: number;
title: string;
type: string;
originalTitle: string;
shortTitle: string;
reference: string;
age: string;
languages: string[];
summary: string;
image: string;
image2x: string;
imageHorizontal: string;
imageHorizontal2x: string;
url: string;
urlPath: string;
episodeCount: number;
genres: string[];
copyright: string;
rating: number;
ratingsCount: number;
commentsCount: number;
qualities: string[];
simulcast: boolean;
free: boolean;
available: boolean;
download: boolean;
basedOn: string;
tagline: string;
firstReleaseYear: string;
productionStudio: string;
countryOfOrigin: string;
productionTeam: ProductionTeam[];
nextVideoReleaseDate: Date;
indexable: boolean;
}
export interface ProductionTeam {
role: string;
name: string;
role: string;
name: string;
}

View file

@ -1,88 +1,88 @@
export interface AnimeOnegaiSearch {
text: string;
list: AOSearchResult[];
text: string;
list: AOSearchResult[];
}
export interface AOSearchResult {
/**
* Asset ID
*/
ID: number;
CreatedAt: Date;
UpdatedAt: Date;
DeletedAt: null;
title: string;
active: boolean;
excerpt: string;
description: string;
bg: string;
poster: string;
entry: string;
code_name: string;
/**
* The Video ID required to get the streams
*/
video_entry: string;
trailer: string;
year: number;
/**
* Asset Type, Known Possibilities
* * 1 - Video
* * 2 - Series
*/
asset_type: 1 | 2;
status: number;
permalink: string;
duration: string;
subtitles: boolean;
price: number;
rent_price: number;
rating: number;
color: number | null;
classification: number;
brazil_classification: null | string;
likes: number;
views: number;
button: string;
stream_url: string;
stream_url_backup: string;
copyright: null | string;
skip_intro: null | string;
ending: null | string;
bumper_intro: string;
ads: string;
age_restriction: boolean | null;
epg: null;
allow_languages: string[] | null;
allow_countries: string[] | null;
classification_text: string;
locked: boolean;
resign: boolean;
favorite: boolean;
actors_list: null;
voiceactors_list: null;
artdirectors_list: null;
audios_list: null;
awards_list: null;
companies_list: null;
countries_list: null;
directors_list: null;
edition_list: null;
genres_list: null;
music_list: null;
photograpy_list: null;
producer_list: null;
screenwriter_list: null;
season_list: null;
tags_list: null;
chapter_id: number;
chapter_entry: string;
chapter_poster: string;
progress_time: number;
progress_percent: number;
included_subscription: number;
paid_content: number;
rent_content: number;
objectID: string;
lang: string;
/**
* Asset ID
*/
ID: number;
CreatedAt: Date;
UpdatedAt: Date;
DeletedAt: null;
title: string;
active: boolean;
excerpt: string;
description: string;
bg: string;
poster: string;
entry: string;
code_name: string;
/**
* The Video ID required to get the streams
*/
video_entry: string;
trailer: string;
year: number;
/**
* Asset Type, Known Possibilities
* * 1 - Video
* * 2 - Series
*/
asset_type: 1 | 2;
status: number;
permalink: string;
duration: string;
subtitles: boolean;
price: number;
rent_price: number;
rating: number;
color: number | null;
classification: number;
brazil_classification: null | string;
likes: number;
views: number;
button: string;
stream_url: string;
stream_url_backup: string;
copyright: null | string;
skip_intro: null | string;
ending: null | string;
bumper_intro: string;
ads: string;
age_restriction: boolean | null;
epg: null;
allow_languages: string[] | null;
allow_countries: string[] | null;
classification_text: string;
locked: boolean;
resign: boolean;
favorite: boolean;
actors_list: null;
voiceactors_list: null;
artdirectors_list: null;
audios_list: null;
awards_list: null;
companies_list: null;
countries_list: null;
directors_list: null;
edition_list: null;
genres_list: null;
music_list: null;
photograpy_list: null;
producer_list: null;
screenwriter_list: null;
season_list: null;
tags_list: null;
chapter_id: number;
chapter_entry: string;
chapter_poster: string;
progress_time: number;
progress_percent: number;
included_subscription: number;
paid_content: number;
rent_content: number;
objectID: string;
lang: string;
}

View file

@ -1,36 +1,36 @@
export interface AnimeOnegaiSeasons {
ID: number;
CreatedAt: Date;
UpdatedAt: Date;
DeletedAt: null;
name: string;
number: number;
asset_id: number;
entry: string;
description: string;
active: boolean;
allow_languages: string[];
allow_countries: string[];
list: Episode[];
ID: number;
CreatedAt: Date;
UpdatedAt: Date;
DeletedAt: null;
name: string;
number: number;
asset_id: number;
entry: string;
description: string;
active: boolean;
allow_languages: string[];
allow_countries: string[];
list: Episode[];
}
export interface Episode {
ID: number;
CreatedAt: Date;
UpdatedAt: Date;
DeletedAt: null;
name: string;
number: number;
description: string;
thumbnail: string;
entry: string;
video_entry: string;
active: boolean;
season_id: number;
stream_url: string;
skip_intro: null;
ending: null;
open_free: boolean;
asset_id: number;
age_restriction: boolean;
ID: number;
CreatedAt: Date;
UpdatedAt: Date;
DeletedAt: null;
name: string;
number: number;
description: string;
thumbnail: string;
entry: string;
video_entry: string;
active: boolean;
season_id: number;
stream_url: string;
skip_intro: null;
ending: null;
open_free: boolean;
asset_id: number;
age_restriction: boolean;
}

View file

@ -1,111 +1,111 @@
export interface AnimeOnegaiSeries {
ID: number;
CreatedAt: Date;
UpdatedAt: Date;
DeletedAt: null;
title: string;
active: boolean;
excerpt: string;
description: string;
bg: string;
poster: string;
entry: string;
code_name: string;
/**
* The Video ID required to get the streams
*/
video_entry: string;
trailer: string;
year: number;
asset_type: number;
status: number;
permalink: string;
duration: string;
subtitles: boolean;
price: number;
rent_price: number;
rating: number;
color: number;
classification: number;
brazil_classification: string;
likes: number;
views: number;
button: string;
stream_url: string;
stream_url_backup: string;
copyright: string;
skip_intro: null;
ending: null;
bumper_intro: string;
ads: string;
age_restriction: boolean;
epg: null;
allow_languages: string[];
allow_countries: string[];
classification_text: string;
locked: boolean;
resign: boolean;
favorite: boolean;
actors_list: CtorsList[];
voiceactors_list: CtorsList[];
artdirectors_list: any[];
audios_list: SList[];
awards_list: any[];
companies_list: any[];
countries_list: any[];
directors_list: CtorsList[];
edition_list: any[];
genres_list: SList[];
music_list: any[];
photograpy_list: any[];
producer_list: any[];
screenwriter_list: any[];
season_list: any[];
tags_list: TagsList[];
chapter_id: number;
chapter_entry: string;
chapter_poster: string;
progress_time: number;
progress_percent: number;
included_subscription: number;
paid_content: number;
rent_content: number;
objectID: string;
lang: string;
ID: number;
CreatedAt: Date;
UpdatedAt: Date;
DeletedAt: null;
title: string;
active: boolean;
excerpt: string;
description: string;
bg: string;
poster: string;
entry: string;
code_name: string;
/**
* The Video ID required to get the streams
*/
video_entry: string;
trailer: string;
year: number;
asset_type: number;
status: number;
permalink: string;
duration: string;
subtitles: boolean;
price: number;
rent_price: number;
rating: number;
color: number;
classification: number;
brazil_classification: string;
likes: number;
views: number;
button: string;
stream_url: string;
stream_url_backup: string;
copyright: string;
skip_intro: null;
ending: null;
bumper_intro: string;
ads: string;
age_restriction: boolean;
epg: null;
allow_languages: string[];
allow_countries: string[];
classification_text: string;
locked: boolean;
resign: boolean;
favorite: boolean;
actors_list: CtorsList[];
voiceactors_list: CtorsList[];
artdirectors_list: any[];
audios_list: SList[];
awards_list: any[];
companies_list: any[];
countries_list: any[];
directors_list: CtorsList[];
edition_list: any[];
genres_list: SList[];
music_list: any[];
photograpy_list: any[];
producer_list: any[];
screenwriter_list: any[];
season_list: any[];
tags_list: TagsList[];
chapter_id: number;
chapter_entry: string;
chapter_poster: string;
progress_time: number;
progress_percent: number;
included_subscription: number;
paid_content: number;
rent_content: number;
objectID: string;
lang: string;
}
export interface CtorsList {
ID: number;
CreatedAt: Date;
UpdatedAt: Date;
DeletedAt: null;
name: string;
Permalink?: string;
country: number | null;
year: number | null;
death: number | null;
image: string;
genre: null;
description: string;
permalink?: string;
background?: string;
ID: number;
CreatedAt: Date;
UpdatedAt: Date;
DeletedAt: null;
name: string;
Permalink?: string;
country: number | null;
year: number | null;
death: number | null;
image: string;
genre: null;
description: string;
permalink?: string;
background?: string;
}
export interface SList {
ID: number;
CreatedAt: Date;
UpdatedAt: Date;
DeletedAt: null;
name: string;
age_restriction?: number;
ID: number;
CreatedAt: Date;
UpdatedAt: Date;
DeletedAt: null;
name: string;
age_restriction?: number;
}
export interface TagsList {
ID: number;
CreatedAt: Date;
UpdatedAt: Date;
DeletedAt: null;
name: string;
position: number;
status: boolean;
ID: number;
CreatedAt: Date;
UpdatedAt: Date;
DeletedAt: null;
name: string;
position: number;
status: boolean;
}

View file

@ -1,41 +1,41 @@
export interface AnimeOnegaiStream {
ID: number;
CreatedAt: Date;
UpdatedAt: Date;
DeletedAt: null;
name: string;
source_url: string;
backup_url: string;
live: boolean;
token_handler: number;
entry: string;
job: string;
drm: boolean;
transcoding_content_id: string;
transcoding_asset_id: string;
status: number;
thumbnail: string;
hls: string;
dash: string;
widevine_proxy: string;
playready_proxy: string;
apple_licence: string;
apple_certificate: string;
dpath: string;
dbin: string;
subtitles: Subtitle[];
origin: number;
offline_entry: string;
offline_status: boolean;
ID: number;
CreatedAt: Date;
UpdatedAt: Date;
DeletedAt: null;
name: string;
source_url: string;
backup_url: string;
live: boolean;
token_handler: number;
entry: string;
job: string;
drm: boolean;
transcoding_content_id: string;
transcoding_asset_id: string;
status: number;
thumbnail: string;
hls: string;
dash: string;
widevine_proxy: string;
playready_proxy: string;
apple_licence: string;
apple_certificate: string;
dpath: string;
dbin: string;
subtitles: Subtitle[];
origin: number;
offline_entry: string;
offline_status: boolean;
}
export interface Subtitle {
ID: number;
CreatedAt: Date;
UpdatedAt: Date;
DeletedAt: null;
name: string;
lang: string;
entry_id: string;
url: string;
ID: number;
CreatedAt: Date;
UpdatedAt: Date;
DeletedAt: null;
name: string;
lang: string;
entry_id: string;
url: string;
}

View file

@ -1,136 +1,136 @@
import { Images } from './crunchyEpisodeList';
export interface CrunchyAndroidEpisodes {
__class__: string;
__href__: string;
__resource_key__: string;
__links__: object;
__actions__: object;
total: number;
items: CrunchyAndroidEpisode[];
__class__: string;
__href__: string;
__resource_key__: string;
__links__: object;
__actions__: object;
total: number;
items: CrunchyAndroidEpisode[];
}
export interface CrunchyAndroidEpisode {
__class__: string;
__href__: string;
__resource_key__: string;
__links__: Links;
__actions__: Actions;
playback: string;
id: string;
channel_id: ChannelID;
series_id: string;
series_title: string;
series_slug_title: string;
season_id: string;
season_title: string;
season_slug_title: string;
season_number: number;
episode: string;
episode_number: number;
sequence_number: number;
production_episode_id: string;
title: string;
slug_title: string;
description: string;
next_episode_id: string;
next_episode_title: string;
hd_flag: boolean;
maturity_ratings: MaturityRating[];
extended_maturity_rating: Actions;
is_mature: boolean;
mature_blocked: boolean;
episode_air_date: Date;
upload_date: Date;
availability_starts: Date;
availability_ends: Date;
eligible_region: string;
available_date: Date;
free_available_date: Date;
premium_date: Date;
premium_available_date: Date;
is_subbed: boolean;
is_dubbed: boolean;
is_clip: boolean;
seo_title: string;
seo_description: string;
season_tags: string[];
available_offline: boolean;
subtitle_locales: Locale[];
availability_notes: string;
audio_locale: Locale;
versions: Version[];
closed_captions_available: boolean;
identifier: string;
media_type: MediaType;
slug: string;
images: Images;
duration_ms: number;
is_premium_only: boolean;
listing_id: string;
hide_season_title?: boolean;
hide_season_number?: boolean;
isSelected?: boolean;
seq_id: string;
__class__: string;
__href__: string;
__resource_key__: string;
__links__: Links;
__actions__: Actions;
playback: string;
id: string;
channel_id: ChannelID;
series_id: string;
series_title: string;
series_slug_title: string;
season_id: string;
season_title: string;
season_slug_title: string;
season_number: number;
episode: string;
episode_number: number;
sequence_number: number;
production_episode_id: string;
title: string;
slug_title: string;
description: string;
next_episode_id: string;
next_episode_title: string;
hd_flag: boolean;
maturity_ratings: MaturityRating[];
extended_maturity_rating: Actions;
is_mature: boolean;
mature_blocked: boolean;
episode_air_date: Date;
upload_date: Date;
availability_starts: Date;
availability_ends: Date;
eligible_region: string;
available_date: Date;
free_available_date: Date;
premium_date: Date;
premium_available_date: Date;
is_subbed: boolean;
is_dubbed: boolean;
is_clip: boolean;
seo_title: string;
seo_description: string;
season_tags: string[];
available_offline: boolean;
subtitle_locales: Locale[];
availability_notes: string;
audio_locale: Locale;
versions: Version[];
closed_captions_available: boolean;
identifier: string;
media_type: MediaType;
slug: string;
images: Images;
duration_ms: number;
is_premium_only: boolean;
listing_id: string;
hide_season_title?: boolean;
hide_season_number?: boolean;
isSelected?: boolean;
seq_id: string;
}
export interface Links {
'episode/channel': Link;
'episode/next_episode': Link;
'episode/season': Link;
'episode/series': Link;
streams: Link;
'episode/channel': Link;
'episode/next_episode': Link;
'episode/season': Link;
'episode/series': Link;
streams: Link;
}
export interface Link {
href: string;
href: string;
}
export interface Thumbnail {
width: number;
height: number;
type: string;
source: string;
width: number;
height: number;
type: string;
source: string;
}
export enum Locale {
enUS = 'en-US',
esLA = 'es-LA',
es419 = 'es-419',
esES = 'es-ES',
ptBR = 'pt-BR',
frFR = 'fr-FR',
deDE = 'de-DE',
arME = 'ar-ME',
arSA = 'ar-SA',
itIT = 'it-IT',
ruRU = 'ru-RU',
trTR = 'tr-TR',
hiIN = 'hi-IN',
zhCN = 'zh-CN',
koKR = 'ko-KR',
jaJP = 'ja-JP',
enUS = 'en-US',
esLA = 'es-LA',
es419 = 'es-419',
esES = 'es-ES',
ptBR = 'pt-BR',
frFR = 'fr-FR',
deDE = 'de-DE',
arME = 'ar-ME',
arSA = 'ar-SA',
itIT = 'it-IT',
ruRU = 'ru-RU',
trTR = 'tr-TR',
hiIN = 'hi-IN',
zhCN = 'zh-CN',
koKR = 'ko-KR',
jaJP = 'ja-JP',
}
export enum MediaType {
Episode = 'episode',
Episode = 'episode',
}
export enum ChannelID {
Crunchyroll = 'crunchyroll',
Crunchyroll = 'crunchyroll',
}
export enum MaturityRating {
Tv14 = 'TV-14',
Tv14 = 'TV-14',
}
export interface Version {
audio_locale: Locale;
guid: string;
original: boolean;
variant: string;
season_guid: string;
media_guid: string;
is_premium_only: boolean;
audio_locale: Locale;
guid: string;
original: boolean;
variant: string;
season_guid: string;
media_guid: string;
is_premium_only: boolean;
}

View file

@ -1,186 +1,186 @@
import { ImageType, Images, Image } from './objectInfo';
export interface CrunchyAndroidObject {
__class__: string;
__href__: string;
__resource_key__: string;
__links__: object;
__actions__: object;
total: number;
items: AndroidObject[];
__class__: string;
__href__: string;
__resource_key__: string;
__links__: object;
__actions__: object;
total: number;
items: AndroidObject[];
}
export interface AndroidObject {
__class__: string;
__href__: string;
__links__: Links;
__actions__: Actions;
id: string;
external_id: string;
channel_id: string;
title: string;
description: string;
promo_title: string;
promo_description: string;
type: string;
slug: string;
slug_title: string;
images: Images;
movie_listing_metadata?: MovieListingMetadata;
movie_metadata?: MovieMetadata;
playback?: string;
episode_metadata?: EpisodeMetadata;
streams_link?: string;
season_metadata?: SeasonMetadata;
linked_resource_key: string;
isSelected?: boolean;
f_num: string;
s_num: string;
__class__: string;
__href__: string;
__links__: Links;
__actions__: Actions;
id: string;
external_id: string;
channel_id: string;
title: string;
description: string;
promo_title: string;
promo_description: string;
type: string;
slug: string;
slug_title: string;
images: Images;
movie_listing_metadata?: MovieListingMetadata;
movie_metadata?: MovieMetadata;
playback?: string;
episode_metadata?: EpisodeMetadata;
streams_link?: string;
season_metadata?: SeasonMetadata;
linked_resource_key: string;
isSelected?: boolean;
f_num: string;
s_num: string;
}
export interface Links {
'episode/season': LinkData;
'episode/series': LinkData;
resource: LinkData;
'resource/channel': LinkData;
streams: LinkData;
'episode/season': LinkData;
'episode/series': LinkData;
resource: LinkData;
'resource/channel': LinkData;
streams: LinkData;
}
export interface LinkData {
href: string;
href: string;
}
export interface EpisodeMetadata {
audio_locale: Locale;
availability_ends: Date;
availability_notes: string;
availability_starts: Date;
available_date: null;
available_offline: boolean;
closed_captions_available: boolean;
duration_ms: number;
eligible_region: string;
episode: string;
episode_air_date: Date;
episode_number: number;
extended_maturity_rating: Record<unknown>;
free_available_date: Date;
identifier: string;
is_clip: boolean;
is_dubbed: boolean;
is_mature: boolean;
is_premium_only: boolean;
is_subbed: boolean;
mature_blocked: boolean;
maturity_ratings: string[];
premium_available_date: Date;
premium_date: null;
season_id: string;
season_number: number;
season_slug_title: string;
season_title: string;
sequence_number: number;
series_id: string;
series_slug_title: string;
series_title: string;
subtitle_locales: Locale[];
tenant_categories?: string[];
upload_date: Date;
versions: EpisodeMetadataVersion[];
audio_locale: Locale;
availability_ends: Date;
availability_notes: string;
availability_starts: Date;
available_date: null;
available_offline: boolean;
closed_captions_available: boolean;
duration_ms: number;
eligible_region: string;
episode: string;
episode_air_date: Date;
episode_number: number;
extended_maturity_rating: Record<unknown>;
free_available_date: Date;
identifier: string;
is_clip: boolean;
is_dubbed: boolean;
is_mature: boolean;
is_premium_only: boolean;
is_subbed: boolean;
mature_blocked: boolean;
maturity_ratings: string[];
premium_available_date: Date;
premium_date: null;
season_id: string;
season_number: number;
season_slug_title: string;
season_title: string;
sequence_number: number;
series_id: string;
series_slug_title: string;
series_title: string;
subtitle_locales: Locale[];
tenant_categories?: string[];
upload_date: Date;
versions: EpisodeMetadataVersion[];
}
export interface MovieListingMetadata {
availability_notes: string;
available_date: null;
available_offline: boolean;
duration_ms: number;
extended_description: string;
extended_maturity_rating: Record<unknown>;
first_movie_id: string;
free_available_date: Date;
is_dubbed: boolean;
is_mature: boolean;
is_premium_only: boolean;
is_subbed: boolean;
mature_blocked: boolean;
maturity_ratings: string[];
movie_release_year: number;
premium_available_date: Date;
premium_date: null;
subtitle_locales: Locale[];
tenant_categories: string[];
availability_notes: string;
available_date: null;
available_offline: boolean;
duration_ms: number;
extended_description: string;
extended_maturity_rating: Record<unknown>;
first_movie_id: string;
free_available_date: Date;
is_dubbed: boolean;
is_mature: boolean;
is_premium_only: boolean;
is_subbed: boolean;
mature_blocked: boolean;
maturity_ratings: string[];
movie_release_year: number;
premium_available_date: Date;
premium_date: null;
subtitle_locales: Locale[];
tenant_categories: string[];
}
export interface MovieMetadata {
availability_notes: string;
available_offline: boolean;
closed_captions_available: boolean;
duration_ms: number;
extended_maturity_rating: Record<unknown>;
is_dubbed: boolean;
is_mature: boolean;
is_premium_only: boolean;
is_subbed: boolean;
mature_blocked: boolean;
maturity_ratings: string[];
movie_listing_id: string;
movie_listing_slug_title: string;
movie_listing_title: string;
availability_notes: string;
available_offline: boolean;
closed_captions_available: boolean;
duration_ms: number;
extended_maturity_rating: Record<unknown>;
is_dubbed: boolean;
is_mature: boolean;
is_premium_only: boolean;
is_subbed: boolean;
mature_blocked: boolean;
maturity_ratings: string[];
movie_listing_id: string;
movie_listing_slug_title: string;
movie_listing_title: string;
}
export interface SeasonMetadata {
audio_locale: Locale;
audio_locales: Locale[];
extended_maturity_rating: Record<unknown>;
identifier: string;
is_mature: boolean;
mature_blocked: boolean;
maturity_ratings: string[];
season_display_number: string;
season_sequence_number: number;
subtitle_locales: Locale[];
versions: SeasonMetadataVersion[];
audio_locale: Locale;
audio_locales: Locale[];
extended_maturity_rating: Record<unknown>;
identifier: string;
is_mature: boolean;
mature_blocked: boolean;
maturity_ratings: string[];
season_display_number: string;
season_sequence_number: number;
subtitle_locales: Locale[];
versions: SeasonMetadataVersion[];
}
export interface SeasonMetadataVersion {
audio_locale: Locale;
guid: string;
original: boolean;
variant: string;
audio_locale: Locale;
guid: string;
original: boolean;
variant: string;
}
export interface SeriesMetadata {
audio_locales: Locale[];
availability_notes: string;
episode_count: number;
extended_description: string;
extended_maturity_rating: Record<unknown>;
is_dubbed: boolean;
is_mature: boolean;
is_simulcast: boolean;
is_subbed: boolean;
mature_blocked: boolean;
maturity_ratings: string[];
season_count: number;
series_launch_year: number;
subtitle_locales: Locale[];
tenant_categories?: string[];
audio_locales: Locale[];
availability_notes: string;
episode_count: number;
extended_description: string;
extended_maturity_rating: Record<unknown>;
is_dubbed: boolean;
is_mature: boolean;
is_simulcast: boolean;
is_subbed: boolean;
mature_blocked: boolean;
maturity_ratings: string[];
season_count: number;
series_launch_year: number;
subtitle_locales: Locale[];
tenant_categories?: string[];
}
export enum Locale {
enUS = 'en-US',
esLA = 'es-LA',
es419 = 'es-419',
esES = 'es-ES',
ptBR = 'pt-BR',
frFR = 'fr-FR',
deDE = 'de-DE',
arME = 'ar-ME',
arSA = 'ar-SA',
itIT = 'it-IT',
ruRU = 'ru-RU',
trTR = 'tr-TR',
hiIN = 'hi-IN',
zhCN = 'zh-CN',
koKR = 'ko-KR',
jaJP = 'ja-JP',
enUS = 'en-US',
esLA = 'es-LA',
es419 = 'es-419',
esES = 'es-ES',
ptBR = 'pt-BR',
frFR = 'fr-FR',
deDE = 'de-DE',
arME = 'ar-ME',
arSA = 'ar-SA',
itIT = 'it-IT',
ruRU = 'ru-RU',
trTR = 'tr-TR',
hiIN = 'hi-IN',
zhCN = 'zh-CN',
koKR = 'ko-KR',
jaJP = 'ja-JP',
}

View file

@ -1,93 +1,93 @@
export interface CrunchyAndroidStreams {
__class__: string;
__href__: string;
__resource_key__: string;
__links__: Links;
__actions__: Record<unknown, unknown>;
media_id: string;
audio_locale: Locale;
subtitles: Subtitles;
closed_captions: Subtitles;
streams: Streams;
bifs: string[];
versions: Version[];
captions: Record<unknown, unknown>;
__class__: string;
__href__: string;
__resource_key__: string;
__links__: Links;
__actions__: Record<unknown, unknown>;
media_id: string;
audio_locale: Locale;
subtitles: Subtitles;
closed_captions: Subtitles;
streams: Streams;
bifs: string[];
versions: Version[];
captions: Record<unknown, unknown>;
}
export interface Subtitles {
'': Subtitle;
'en-US'?: Subtitle;
'es-LA'?: Subtitle;
'es-419'?: Subtitle;
'es-ES'?: Subtitle;
'pt-BR'?: Subtitle;
'fr-FR'?: Subtitle;
'de-DE'?: Subtitle;
'ar-ME'?: Subtitle;
'ar-SA'?: Subtitle;
'it-IT'?: Subtitle;
'ru-RU'?: Subtitle;
'tr-TR'?: Subtitle;
'hi-IN'?: Subtitle;
'zh-CN'?: Subtitle;
'ko-KR'?: Subtitle;
'ja-JP'?: Subtitle;
'': Subtitle;
'en-US'?: Subtitle;
'es-LA'?: Subtitle;
'es-419'?: Subtitle;
'es-ES'?: Subtitle;
'pt-BR'?: Subtitle;
'fr-FR'?: Subtitle;
'de-DE'?: Subtitle;
'ar-ME'?: Subtitle;
'ar-SA'?: Subtitle;
'it-IT'?: Subtitle;
'ru-RU'?: Subtitle;
'tr-TR'?: Subtitle;
'hi-IN'?: Subtitle;
'zh-CN'?: Subtitle;
'ko-KR'?: Subtitle;
'ja-JP'?: Subtitle;
}
export interface Links {
resource: Resource;
resource: Resource;
}
export interface Resource {
href: string;
href: string;
}
export interface Streams {
[key: string]: { [key: string]: Download };
[key: string]: { [key: string]: Download };
}
export interface Download {
hardsub_locale: Locale;
hardsub_lang?: string;
url: string;
hardsub_locale: Locale;
hardsub_lang?: string;
url: string;
}
export interface Urls {
'': Download;
'': Download;
}
export interface Subtitle {
locale: Locale;
url: string;
format: string;
locale: Locale;
url: string;
format: string;
}
export interface Version {
audio_locale: Locale;
guid: string;
original: boolean;
variant: string;
season_guid: string;
media_guid: string;
is_premium_only: boolean;
audio_locale: Locale;
guid: string;
original: boolean;
variant: string;
season_guid: string;
media_guid: string;
is_premium_only: boolean;
}
export enum Locale {
default = '',
enUS = 'en-US',
esLA = 'es-LA',
es419 = 'es-419',
esES = 'es-ES',
ptBR = 'pt-BR',
frFR = 'fr-FR',
deDE = 'de-DE',
arME = 'ar-ME',
arSA = 'ar-SA',
itIT = 'it-IT',
ruRU = 'ru-RU',
trTR = 'tr-TR',
hiIN = 'hi-IN',
zhCN = 'zh-CN',
koKR = 'ko-KR',
jaJP = 'ja-JP',
default = '',
enUS = 'en-US',
esLA = 'es-LA',
es419 = 'es-419',
esES = 'es-ES',
ptBR = 'pt-BR',
frFR = 'fr-FR',
deDE = 'de-DE',
arME = 'ar-ME',
arSA = 'ar-SA',
itIT = 'it-IT',
ruRU = 'ru-RU',
trTR = 'tr-TR',
hiIN = 'hi-IN',
zhCN = 'zh-CN',
koKR = 'ko-KR',
jaJP = 'ja-JP',
}

View file

@ -1,134 +1,134 @@
import { Links } from './crunchyAndroidEpisodes';
export interface CrunchyEpisodeList {
total: number;
data: CrunchyEpisode[];
meta: Meta;
total: number;
data: CrunchyEpisode[];
meta: Meta;
}
export interface CrunchyEpisode {
next_episode_id: string;
series_id: string;
season_number: number;
next_episode_title: string;
availability_notes: string;
duration_ms: number;
series_slug_title: string;
series_title: string;
is_dubbed: boolean;
versions: Version[] | null;
identifier: string;
sequence_number: number;
eligible_region: Record<unknown>;
availability_starts: Date;
images: Images;
season_id: string;
seo_title: string;
is_premium_only: boolean;
extended_maturity_rating: Record<unknown>;
title: string;
production_episode_id: string;
premium_available_date: Date;
season_title: string;
seo_description: string;
audio_locale: Locale;
id: string;
media_type: MediaType;
availability_ends: Date;
free_available_date: Date;
playback: string;
channel_id: ChannelID;
episode: string;
is_mature: boolean;
listing_id: string;
episode_air_date: Date;
slug: string;
available_date: Date;
subtitle_locales: Locale[];
slug_title: string;
available_offline: boolean;
description: string;
is_subbed: boolean;
premium_date: Date;
upload_date: Date;
season_slug_title: string;
closed_captions_available: boolean;
episode_number: number;
season_tags: any[];
maturity_ratings: MaturityRating[];
streams_link?: string;
mature_blocked: boolean;
is_clip: boolean;
hd_flag: boolean;
hide_season_title?: boolean;
hide_season_number?: boolean;
isSelected?: boolean;
seq_id: string;
__links__?: Links;
next_episode_id: string;
series_id: string;
season_number: number;
next_episode_title: string;
availability_notes: string;
duration_ms: number;
series_slug_title: string;
series_title: string;
is_dubbed: boolean;
versions: Version[] | null;
identifier: string;
sequence_number: number;
eligible_region: Record<unknown>;
availability_starts: Date;
images: Images;
season_id: string;
seo_title: string;
is_premium_only: boolean;
extended_maturity_rating: Record<unknown>;
title: string;
production_episode_id: string;
premium_available_date: Date;
season_title: string;
seo_description: string;
audio_locale: Locale;
id: string;
media_type: MediaType;
availability_ends: Date;
free_available_date: Date;
playback: string;
channel_id: ChannelID;
episode: string;
is_mature: boolean;
listing_id: string;
episode_air_date: Date;
slug: string;
available_date: Date;
subtitle_locales: Locale[];
slug_title: string;
available_offline: boolean;
description: string;
is_subbed: boolean;
premium_date: Date;
upload_date: Date;
season_slug_title: string;
closed_captions_available: boolean;
episode_number: number;
season_tags: any[];
maturity_ratings: MaturityRating[];
streams_link?: string;
mature_blocked: boolean;
is_clip: boolean;
hd_flag: boolean;
hide_season_title?: boolean;
hide_season_number?: boolean;
isSelected?: boolean;
seq_id: string;
__links__?: Links;
}
export enum Locale {
enUS = 'en-US',
esLA = 'es-LA',
es419 = 'es-419',
esES = 'es-ES',
ptBR = 'pt-BR',
frFR = 'fr-FR',
deDE = 'de-DE',
arME = 'ar-ME',
arSA = 'ar-SA',
itIT = 'it-IT',
ruRU = 'ru-RU',
trTR = 'tr-TR',
hiIN = 'hi-IN',
zhCN = 'zh-CN',
koKR = 'ko-KR',
jaJP = 'ja-JP',
enUS = 'en-US',
esLA = 'es-LA',
es419 = 'es-419',
esES = 'es-ES',
ptBR = 'pt-BR',
frFR = 'fr-FR',
deDE = 'de-DE',
arME = 'ar-ME',
arSA = 'ar-SA',
itIT = 'it-IT',
ruRU = 'ru-RU',
trTR = 'tr-TR',
hiIN = 'hi-IN',
zhCN = 'zh-CN',
koKR = 'ko-KR',
jaJP = 'ja-JP',
}
export enum ChannelID {
Crunchyroll = 'crunchyroll',
Crunchyroll = 'crunchyroll',
}
export interface Images {
poster_tall?: Array<Image[]>;
poster_wide?: Array<Image[]>;
promo_image?: Array<Image[]>;
thumbnail?: Array<Image[]>;
poster_tall?: Array<Image[]>;
poster_wide?: Array<Image[]>;
promo_image?: Array<Image[]>;
thumbnail?: Array<Image[]>;
}
export interface Image {
height: number;
source: string;
type: ImageType;
width: number;
height: number;
source: string;
type: ImageType;
width: number;
}
export enum ImageType {
PosterTall = 'poster_tall',
PosterWide = 'poster_wide',
PromoImage = 'promo_image',
Thumbnail = 'thumbnail',
PosterTall = 'poster_tall',
PosterWide = 'poster_wide',
PromoImage = 'promo_image',
Thumbnail = 'thumbnail',
}
export enum MaturityRating {
Tv14 = 'TV-14',
Tv14 = 'TV-14',
}
export enum MediaType {
Episode = 'episode',
Episode = 'episode',
}
export interface Version {
audio_locale: Locale;
guid: string;
is_premium_only: boolean;
media_guid: string;
original: boolean;
season_guid: string;
variant: string;
audio_locale: Locale;
guid: string;
is_premium_only: boolean;
media_guid: string;
original: boolean;
season_guid: string;
variant: string;
}
export interface Meta {
versions_considered?: boolean;
versions_considered?: boolean;
}

View file

@ -1,183 +1,183 @@
// Generated by https://quicktype.io
export interface CrunchySearch {
total: number;
data: CrunchySearchData[];
meta: Record<string, unknown>;
total: number;
data: CrunchySearchData[];
meta: Record<string, unknown>;
}
export interface CrunchySearchData {
type: string;
count: number;
items: CrunchySearchItem[];
type: string;
count: number;
items: CrunchySearchItem[];
}
export interface CrunchySearchItem {
title: string;
images: Images;
series_metadata?: SeriesMetadata;
promo_description: string;
external_id: string;
slug: string;
new: boolean;
slug_title: string;
channel_id: ChannelID;
description: string;
linked_resource_key: string;
type: ItemType;
id: string;
promo_title: string;
search_metadata: SearchMetadata;
movie_listing_metadata?: MovieListingMetadata;
playback?: string;
streams_link?: string;
episode_metadata?: EpisodeMetadata;
title: string;
images: Images;
series_metadata?: SeriesMetadata;
promo_description: string;
external_id: string;
slug: string;
new: boolean;
slug_title: string;
channel_id: ChannelID;
description: string;
linked_resource_key: string;
type: ItemType;
id: string;
promo_title: string;
search_metadata: SearchMetadata;
movie_listing_metadata?: MovieListingMetadata;
playback?: string;
streams_link?: string;
episode_metadata?: EpisodeMetadata;
}
export enum ChannelID {
Crunchyroll = 'crunchyroll',
Crunchyroll = 'crunchyroll',
}
export interface EpisodeMetadata {
audio_locale: Locale;
availability_ends: Date;
availability_notes: string;
availability_starts: Date;
available_date: null;
available_offline: boolean;
closed_captions_available: boolean;
duration_ms: number;
eligible_region: string[];
episode: string;
episode_air_date: Date;
episode_number: number;
extended_maturity_rating: Record<unknown>;
free_available_date: Date;
identifier: string;
is_clip: boolean;
is_dubbed: boolean;
is_mature: boolean;
is_premium_only: boolean;
is_subbed: boolean;
mature_blocked: boolean;
maturity_ratings: MaturityRating[];
premium_available_date: Date;
premium_date: null;
season_id: string;
season_number: number;
season_slug_title: string;
season_title: string;
sequence_number: number;
series_id: string;
series_slug_title: string;
series_title: string;
subtitle_locales: Locale[];
upload_date: Date;
versions: Version[] | null;
tenant_categories?: string[];
audio_locale: Locale;
availability_ends: Date;
availability_notes: string;
availability_starts: Date;
available_date: null;
available_offline: boolean;
closed_captions_available: boolean;
duration_ms: number;
eligible_region: string[];
episode: string;
episode_air_date: Date;
episode_number: number;
extended_maturity_rating: Record<unknown>;
free_available_date: Date;
identifier: string;
is_clip: boolean;
is_dubbed: boolean;
is_mature: boolean;
is_premium_only: boolean;
is_subbed: boolean;
mature_blocked: boolean;
maturity_ratings: MaturityRating[];
premium_available_date: Date;
premium_date: null;
season_id: string;
season_number: number;
season_slug_title: string;
season_title: string;
sequence_number: number;
series_id: string;
series_slug_title: string;
series_title: string;
subtitle_locales: Locale[];
upload_date: Date;
versions: Version[] | null;
tenant_categories?: string[];
}
export enum Locale {
enUS = 'en-US',
esLA = 'es-LA',
es419 = 'es-419',
esES = 'es-ES',
ptBR = 'pt-BR',
frFR = 'fr-FR',
deDE = 'de-DE',
arME = 'ar-ME',
arSA = 'ar-SA',
itIT = 'it-IT',
ruRU = 'ru-RU',
trTR = 'tr-TR',
hiIN = 'hi-IN',
zhCN = 'zh-CN',
koKR = 'ko-KR',
jaJP = 'ja-JP',
enUS = 'en-US',
esLA = 'es-LA',
es419 = 'es-419',
esES = 'es-ES',
ptBR = 'pt-BR',
frFR = 'fr-FR',
deDE = 'de-DE',
arME = 'ar-ME',
arSA = 'ar-SA',
itIT = 'it-IT',
ruRU = 'ru-RU',
trTR = 'tr-TR',
hiIN = 'hi-IN',
zhCN = 'zh-CN',
koKR = 'ko-KR',
jaJP = 'ja-JP',
}
export enum MaturityRating {
Tv14 = 'TV-14',
TvMa = 'TV-MA',
Tv14 = 'TV-14',
TvMa = 'TV-MA',
}
export interface Version {
audio_locale: Locale;
guid: string;
is_premium_only: boolean;
media_guid: string;
original: boolean;
season_guid: string;
variant: string;
audio_locale: Locale;
guid: string;
is_premium_only: boolean;
media_guid: string;
original: boolean;
season_guid: string;
variant: string;
}
export interface Images {
poster_tall?: Array<Image[]>;
poster_wide?: Array<Image[]>;
promo_image?: Array<Image[]>;
thumbnail?: Array<Image[]>;
poster_tall?: Array<Image[]>;
poster_wide?: Array<Image[]>;
promo_image?: Array<Image[]>;
thumbnail?: Array<Image[]>;
}
export interface Image {
height: number;
source: string;
type: ImageType;
width: number;
height: number;
source: string;
type: ImageType;
width: number;
}
export enum ImageType {
PosterTall = 'poster_tall',
PosterWide = 'poster_wide',
PromoImage = 'promo_image',
Thumbnail = 'thumbnail',
PosterTall = 'poster_tall',
PosterWide = 'poster_wide',
PromoImage = 'promo_image',
Thumbnail = 'thumbnail',
}
export interface MovieListingMetadata {
availability_notes: string;
available_date: null;
available_offline: boolean;
duration_ms: number;
extended_description: string;
extended_maturity_rating: Record<unknown>;
first_movie_id: string;
free_available_date: Date;
is_dubbed: boolean;
is_mature: boolean;
is_premium_only: boolean;
is_subbed: boolean;
mature_blocked: boolean;
maturity_ratings: string[];
movie_release_year: number;
premium_available_date: Date;
premium_date: null;
subtitle_locales: any[];
tenant_categories: string[];
availability_notes: string;
available_date: null;
available_offline: boolean;
duration_ms: number;
extended_description: string;
extended_maturity_rating: Record<unknown>;
first_movie_id: string;
free_available_date: Date;
is_dubbed: boolean;
is_mature: boolean;
is_premium_only: boolean;
is_subbed: boolean;
mature_blocked: boolean;
maturity_ratings: string[];
movie_release_year: number;
premium_available_date: Date;
premium_date: null;
subtitle_locales: any[];
tenant_categories: string[];
}
export interface SearchMetadata {
score: number;
score: number;
}
export interface SeriesMetadata {
audio_locales: Locale[];
availability_notes: string;
episode_count: number;
extended_description: string;
extended_maturity_rating: Record<unknown>;
is_dubbed: boolean;
is_mature: boolean;
is_simulcast: boolean;
is_subbed: boolean;
mature_blocked: boolean;
maturity_ratings: MaturityRating[];
season_count: number;
series_launch_year: number;
subtitle_locales: Locale[];
tenant_categories?: string[];
audio_locales: Locale[];
availability_notes: string;
episode_count: number;
extended_description: string;
extended_maturity_rating: Record<unknown>;
is_dubbed: boolean;
is_mature: boolean;
is_simulcast: boolean;
is_subbed: boolean;
mature_blocked: boolean;
maturity_ratings: MaturityRating[];
season_count: number;
series_launch_year: number;
subtitle_locales: Locale[];
tenant_categories?: string[];
}
export enum ItemType {
Episode = 'episode',
MovieListing = 'movie_listing',
Series = 'series',
Episode = 'episode',
MovieListing = 'movie_listing',
Series = 'series',
}

View file

@ -5,211 +5,211 @@ import { DownloadInfo } from './messageHandler';
import { CrunchyVideoPlayStreams, CrunchyAudioPlayStreams } from './enums';
export type CrunchyDownloadOptions = {
hslang: string,
// kstream: number,
cstream: keyof typeof CrunchyVideoPlayStreams,
vstream: keyof typeof CrunchyVideoPlayStreams,
astream: keyof typeof CrunchyAudioPlayStreams,
tsd?: boolean,
novids?: boolean,
noaudio?: boolean,
x: number,
q: number,
fileName: string,
numbers: number,
partsize: number,
callbackMaker?: (data: DownloadInfo) => HLSCallback,
timeout: number,
waittime: number,
fsRetryTime: number,
dlsubs: string[],
skipsubs: boolean,
nosubs?: boolean,
mp4: boolean,
override: string[],
videoTitle: string,
force: 'Y'|'y'|'N'|'n'|'C'|'c',
ffmpegOptions: string[],
mkvmergeOptions: string[],
defaultSub: LanguageItem,
defaultAudio: LanguageItem,
ccTag: string,
dlVideoOnce: boolean,
skipmux?: boolean,
syncTiming: boolean,
nocleanup: boolean,
chapters: boolean,
fontName: string | undefined,
originalFontSize: boolean,
fontSize: number,
dubLang: string[],
hslang: string,
// kstream: number,
cstream: keyof typeof CrunchyVideoPlayStreams,
vstream: keyof typeof CrunchyVideoPlayStreams,
astream: keyof typeof CrunchyAudioPlayStreams,
tsd?: boolean,
novids?: boolean,
noaudio?: boolean,
x: number,
q: number,
fileName: string,
numbers: number,
partsize: number,
callbackMaker?: (data: DownloadInfo) => HLSCallback,
timeout: number,
waittime: number,
fsRetryTime: number,
dlsubs: string[],
skipsubs: boolean,
nosubs?: boolean,
mp4: boolean,
override: string[],
videoTitle: string,
force: 'Y'|'y'|'N'|'n'|'C'|'c',
ffmpegOptions: string[],
mkvmergeOptions: string[],
defaultSub: LanguageItem,
defaultAudio: LanguageItem,
ccTag: string,
dlVideoOnce: boolean,
skipmux?: boolean,
syncTiming: boolean,
nocleanup: boolean,
chapters: boolean,
fontName: string | undefined,
originalFontSize: boolean,
fontSize: number,
dubLang: string[],
}
export type CrunchyMultiDownload = {
absolute?: boolean,
dubLang: string[],
all?: boolean,
but?: boolean,
e?: string,
s?: string
absolute?: boolean,
dubLang: string[],
all?: boolean,
but?: boolean,
e?: string,
s?: string
}
export type CrunchyMuxOptions = {
output: string,
skipSubMux?: boolean
keepAllVideos?: bolean
novids?: boolean,
mp4: boolean,
forceMuxer?: 'ffmpeg'|'mkvmerge',
nocleanup?: boolean,
videoTitle: string,
ffmpegOptions: string[],
mkvmergeOptions: string[],
defaultSub: LanguageItem,
defaultAudio: LanguageItem,
ccTag: string,
syncTiming: boolean,
output: string,
skipSubMux?: boolean
keepAllVideos?: bolean
novids?: boolean,
mp4: boolean,
forceMuxer?: 'ffmpeg'|'mkvmerge',
nocleanup?: boolean,
videoTitle: string,
ffmpegOptions: string[],
mkvmergeOptions: string[],
defaultSub: LanguageItem,
defaultAudio: LanguageItem,
ccTag: string,
syncTiming: boolean,
}
export type CrunchyEpMeta = {
data: {
mediaId: string,
lang?: LanguageItem,
playback?: string,
versions?: EpisodeVersion[] | null,
isSubbed: boolean,
isDubbed: boolean,
}[],
seriesTitle: string,
seasonTitle: string,
episodeNumber: string,
episodeTitle: string,
seasonID: string,
season: number,
showID: string,
e: string,
image: string,
data: {
mediaId: string,
lang?: LanguageItem,
playback?: string,
versions?: EpisodeVersion[] | null,
isSubbed: boolean,
isDubbed: boolean,
}[],
seriesTitle: string,
seasonTitle: string,
episodeNumber: string,
episodeTitle: string,
seasonID: string,
season: number,
showID: string,
e: string,
image: string,
}
export type DownloadedMedia = {
type: 'Video',
lang: LanguageItem,
path: string,
isPrimary?: boolean
type: 'Video',
lang: LanguageItem,
path: string,
isPrimary?: boolean
} | {
type: 'Audio',
lang: LanguageItem,
path: string,
isPrimary?: boolean
type: 'Audio',
lang: LanguageItem,
path: string,
isPrimary?: boolean
} | {
type: 'Chapters',
lang: LanguageItem,
path: string
type: 'Chapters',
lang: LanguageItem,
path: string
} | ({
type: 'Subtitle',
signs: boolean,
cc: boolean
type: 'Subtitle',
signs: boolean,
cc: boolean
} & sxItem )
export type ParseItem = {
__class__?: string;
isSelected?: boolean,
type?: string,
id: string,
title: string,
playback?: string,
season_number?: number|string,
episode_number?: number|string,
season_count?: number|string,
is_premium_only?: boolean,
hide_metadata?: boolean,
seq_id?: string,
f_num?: string,
s_num?: string
external_id?: string,
ep_num?: string
last_public?: string,
subtitle_locales?: string[],
availability_notes?: string,
identifier?: string,
versions?: Version[] | null,
media_type?: string | null,
movie_release_year?: number | null,
__class__?: string;
isSelected?: boolean,
type?: string,
id: string,
title: string,
playback?: string,
season_number?: number|string,
episode_number?: number|string,
season_count?: number|string,
is_premium_only?: boolean,
hide_metadata?: boolean,
seq_id?: string,
f_num?: string,
s_num?: string
external_id?: string,
ep_num?: string
last_public?: string,
subtitle_locales?: string[],
availability_notes?: string,
identifier?: string,
versions?: Version[] | null,
media_type?: string | null,
movie_release_year?: number | null,
}
export interface SeriesSearch {
total: number;
data: SeriesSearchItem[];
meta: Meta;
total: number;
data: SeriesSearchItem[];
meta: Meta;
}
export interface SeriesSearchItem {
description: string;
seo_description: string;
number_of_episodes: number;
is_dubbed: boolean;
identifier: string;
channel_id: string;
slug_title: string;
season_sequence_number: number;
season_tags: string[];
extended_maturity_rating: Record<unknown>;
is_mature: boolean;
audio_locale: string;
season_number: number;
images: Record<unknown>;
mature_blocked: boolean;
versions: Version[];
title: string;
is_subbed: boolean;
id: string;
audio_locales: string[];
subtitle_locales: string[];
availability_notes: string;
series_id: string;
season_display_number: string;
is_complete: boolean;
keywords: any[];
maturity_ratings: string[];
is_simulcast: boolean;
seo_title: string;
description: string;
seo_description: string;
number_of_episodes: number;
is_dubbed: boolean;
identifier: string;
channel_id: string;
slug_title: string;
season_sequence_number: number;
season_tags: string[];
extended_maturity_rating: Record<unknown>;
is_mature: boolean;
audio_locale: string;
season_number: number;
images: Record<unknown>;
mature_blocked: boolean;
versions: Version[];
title: string;
is_subbed: boolean;
id: string;
audio_locales: string[];
subtitle_locales: string[];
availability_notes: string;
series_id: string;
season_display_number: string;
is_complete: boolean;
keywords: any[];
maturity_ratings: string[];
is_simulcast: boolean;
seo_title: string;
}
export interface Version {
audio_locale: Locale;
guid: string;
original: boolean;
variant: string;
audio_locale: Locale;
guid: string;
original: boolean;
variant: string;
}
export interface EpisodeVersion {
audio_locale: Locale;
guid: string;
is_premium_only: boolean;
media_guid: string;
original: boolean;
season_guid: string;
variant: string;
audio_locale: Locale;
guid: string;
is_premium_only: boolean;
media_guid: string;
original: boolean;
season_guid: string;
variant: string;
}
export enum Locale {
enUS = 'en-US',
esLA = 'es-LA',
es419 = 'es-419',
esES = 'es-ES',
ptBR = 'pt-BR',
frFR = 'fr-FR',
deDE = 'de-DE',
arME = 'ar-ME',
arSA = 'ar-SA',
itIT = 'it-IT',
ruRU = 'ru-RU',
trTR = 'tr-TR',
hiIN = 'hi-IN',
zhCN = 'zh-CN',
koKR = 'ko-KR',
jaJP = 'ja-JP',
enUS = 'en-US',
esLA = 'es-LA',
es419 = 'es-419',
esES = 'es-ES',
ptBR = 'pt-BR',
frFR = 'fr-FR',
deDE = 'de-DE',
arME = 'ar-ME',
arSA = 'ar-SA',
itIT = 'it-IT',
ruRU = 'ru-RU',
trTR = 'tr-TR',
hiIN = 'hi-IN',
zhCN = 'zh-CN',
koKR = 'ko-KR',
jaJP = 'ja-JP',
}
export interface Meta {
versions_considered: boolean;
versions_considered: boolean;
}

View file

@ -1,6 +1,6 @@
import { LanguageItem } from '../modules/module.langsData';
export type DownloadedFile = {
path: string,
lang: LanguageItem
path: string,
lang: LanguageItem
}

View file

@ -1,11 +1,11 @@
export enum CrunchyVideoPlayStreams {
'androidtv' = 'tv/android_tv',
'android' = 'android/phone',
'androidtab'= 'android/tablet'
'androidtv' = 'tv/android_tv',
'android' = 'android/phone',
'androidtab'= 'android/tablet'
}
export enum CrunchyAudioPlayStreams {
'androidtv' = 'tv/android_tv',
'android' = 'android/phone',
'androidtab'= 'android/tablet'
'androidtv' = 'tv/android_tv',
'android' = 'android/phone',
'androidtab'= 'android/tablet'
}

View file

@ -1,70 +1,70 @@
export interface HidiveDashboard {
Code: number;
Status: string;
Message: null;
Messages: object;
Data: Data;
Timestamp: string;
IPAddress: string;
Code: number;
Status: string;
Message: null;
Messages: object;
Data: Data;
Timestamp: string;
IPAddress: string;
}
export interface Data {
TitleRows: TitleRow[];
LoadTime: number;
TitleRows: TitleRow[];
LoadTime: number;
}
export interface TitleRow {
Name: string;
Titles: Title[];
LoadTime: number;
Name: string;
Titles: Title[];
LoadTime: number;
}
export interface Title {
Id: number;
Name: string;
ShortSynopsis: string;
MediumSynopsis: string;
LongSynopsis: string;
KeyArtUrl: string;
MasterArtUrl: string;
Rating: null | string;
OverallRating: number;
RatingCount: number;
MALScore: null;
UserRating: number;
RunTime: number | null;
ShowInfoTitle: string;
FirstPremiereDate: Date;
EpisodeCount: number;
SeasonName: string;
RokuHDArtUrl: string;
RokuSDArtUrl: string;
IsRateable: boolean;
InQueue: boolean;
IsFavorite: boolean;
IsContinueWatching: boolean;
ContinueWatching: ContinueWatching;
Episodes: any[];
LoadTime: number;
Id: number;
Name: string;
ShortSynopsis: string;
MediumSynopsis: string;
LongSynopsis: string;
KeyArtUrl: string;
MasterArtUrl: string;
Rating: null | string;
OverallRating: number;
RatingCount: number;
MALScore: null;
UserRating: number;
RunTime: number | null;
ShowInfoTitle: string;
FirstPremiereDate: Date;
EpisodeCount: number;
SeasonName: string;
RokuHDArtUrl: string;
RokuSDArtUrl: string;
IsRateable: boolean;
InQueue: boolean;
IsFavorite: boolean;
IsContinueWatching: boolean;
ContinueWatching: ContinueWatching;
Episodes: any[];
LoadTime: number;
}
export interface ContinueWatching {
Id: string;
ProfileId: number;
EpisodeId: number;
Status: Status | null;
CurrentTime: number;
UserId: number;
TitleId: number;
SeasonId: number;
VideoId: number;
TotalSeconds: number;
CreatedDT: Date;
ModifiedDT: Date | null;
Id: string;
ProfileId: number;
EpisodeId: number;
Status: Status | null;
CurrentTime: number;
UserId: number;
TitleId: number;
SeasonId: number;
VideoId: number;
TotalSeconds: number;
CreatedDT: Date;
ModifiedDT: Date | null;
}
export enum Status {
Paused = 'Paused',
Playing = 'Playing',
Watching = 'Watching',
Paused = 'Paused',
Playing = 'Playing',
Watching = 'Watching',
}

View file

@ -1,84 +1,84 @@
export interface HidiveEpisodeList {
Code: number;
Status: string;
Message: null;
Messages: Record<unknown, unknown>;
Data: Data;
Timestamp: string;
IPAddress: string;
Code: number;
Status: string;
Message: null;
Messages: Record<unknown, unknown>;
Data: Data;
Timestamp: string;
IPAddress: string;
}
export interface Data {
Title: HidiveTitle;
Title: HidiveTitle;
}
export interface HidiveTitle {
Id: number;
Name: string;
ShortSynopsis: string;
MediumSynopsis: string;
LongSynopsis: string;
KeyArtUrl: string;
MasterArtUrl: string;
Rating: string;
OverallRating: number;
RatingCount: number;
MALScore: null;
UserRating: number;
RunTime: number;
ShowInfoTitle: string;
FirstPremiereDate: Date;
EpisodeCount: number;
SeasonName: string;
RokuHDArtUrl: string;
RokuSDArtUrl: string;
IsRateable: boolean;
InQueue: boolean;
IsFavorite: boolean;
IsContinueWatching: boolean;
ContinueWatching: ContinueWatching;
Episodes: HidiveEpisode[];
LoadTime: number;
Id: number;
Name: string;
ShortSynopsis: string;
MediumSynopsis: string;
LongSynopsis: string;
KeyArtUrl: string;
MasterArtUrl: string;
Rating: string;
OverallRating: number;
RatingCount: number;
MALScore: null;
UserRating: number;
RunTime: number;
ShowInfoTitle: string;
FirstPremiereDate: Date;
EpisodeCount: number;
SeasonName: string;
RokuHDArtUrl: string;
RokuSDArtUrl: string;
IsRateable: boolean;
InQueue: boolean;
IsFavorite: boolean;
IsContinueWatching: boolean;
ContinueWatching: ContinueWatching;
Episodes: HidiveEpisode[];
LoadTime: number;
}
export interface ContinueWatching {
Id: string;
ProfileId: number;
EpisodeId: number;
Status: string;
CurrentTime: number;
UserId: number;
TitleId: number;
SeasonId: number;
VideoId: number;
TotalSeconds: number;
CreatedDT: Date;
ModifiedDT: Date;
Id: string;
ProfileId: number;
EpisodeId: number;
Status: string;
CurrentTime: number;
UserId: number;
TitleId: number;
SeasonId: number;
VideoId: number;
TotalSeconds: number;
CreatedDT: Date;
ModifiedDT: Date;
}
export interface HidiveEpisode {
Id: number;
Number: number;
Name: string;
Summary: string;
HIDIVEPremiereDate: Date;
ScreenShotSmallUrl: string;
ScreenShotCompressedUrl: string;
SeasonNumber: number;
TitleId: number;
SeasonNumberValue: number;
EpisodeNumberValue: number;
VideoKey: string;
DisplayNameLong: string;
PercentProgress: number;
LoadTime: number;
Id: number;
Number: number;
Name: string;
Summary: string;
HIDIVEPremiereDate: Date;
ScreenShotSmallUrl: string;
ScreenShotCompressedUrl: string;
SeasonNumber: number;
TitleId: number;
SeasonNumberValue: number;
EpisodeNumberValue: number;
VideoKey: string;
DisplayNameLong: string;
PercentProgress: number;
LoadTime: number;
}
export interface HidiveEpisodeExtra extends HidiveEpisode {
titleId: number;
epKey: string;
nameLong: string;
seriesTitle: string;
seriesId?: number;
isSelected: boolean;
titleId: number;
epKey: string;
nameLong: string;
seriesTitle: string;
seriesId?: number;
isSelected: boolean;
}

View file

@ -1,47 +1,47 @@
export interface HidiveSearch {
Code: number;
Status: string;
Message: null;
Messages: Record<unknown, unknown>;
Data: HidiveSearchData;
Timestamp: string;
IPAddress: string;
Code: number;
Status: string;
Message: null;
Messages: Record<unknown, unknown>;
Data: HidiveSearchData;
Timestamp: string;
IPAddress: string;
}
export interface HidiveSearchData {
Query: string;
Slug: string;
TitleResults: HidiveSearchItem[];
SearchId: number;
IsSearchPinned: boolean;
IsPinnedSearchAvailable: boolean;
Query: string;
Slug: string;
TitleResults: HidiveSearchItem[];
SearchId: number;
IsSearchPinned: boolean;
IsPinnedSearchAvailable: boolean;
}
export interface HidiveSearchItem {
Id: number;
Name: string;
ShortSynopsis: string;
MediumSynopsis: string;
LongSynopsis: string;
KeyArtUrl: string;
MasterArtUrl: string;
Rating: string;
OverallRating: number;
RatingCount: number;
MALScore: null;
UserRating: number;
RunTime: number | null;
ShowInfoTitle: string;
FirstPremiereDate: Date;
EpisodeCount: number;
SeasonName: string;
RokuHDArtUrl: string;
RokuSDArtUrl: string;
IsRateable: boolean;
InQueue: boolean;
IsFavorite: boolean;
IsContinueWatching: boolean;
ContinueWatching: null;
Episodes: any[];
LoadTime: number;
Id: number;
Name: string;
ShortSynopsis: string;
MediumSynopsis: string;
LongSynopsis: string;
KeyArtUrl: string;
MasterArtUrl: string;
Rating: string;
OverallRating: number;
RatingCount: number;
MALScore: null;
UserRating: number;
RunTime: number | null;
ShowInfoTitle: string;
FirstPremiereDate: Date;
EpisodeCount: number;
SeasonName: string;
RokuHDArtUrl: string;
RokuSDArtUrl: string;
IsRateable: boolean;
InQueue: boolean;
IsFavorite: boolean;
IsContinueWatching: boolean;
ContinueWatching: null;
Episodes: any[];
LoadTime: number;
}

View file

@ -1,61 +1,61 @@
export interface HidiveVideoList {
Code: number;
Status: string;
Message: null;
Messages: Record<unknown, unknown>;
Data: HidiveVideo;
Timestamp: string;
IPAddress: string;
Code: number;
Status: string;
Message: null;
Messages: Record<unknown, unknown>;
Data: HidiveVideo;
Timestamp: string;
IPAddress: string;
}
export interface HidiveVideo {
ShowAds: boolean;
CaptionCssUrl: string;
FontSize: number;
FontScale: number;
CaptionLanguages: string[];
CaptionLanguage: string;
CaptionVttUrls: Record<string, string>;
VideoLanguages: string[];
VideoLanguage: string;
VideoUrls: Record<string, HidiveStreamList>;
FontColorName: string;
AutoPlayNextEpisode: boolean;
MaxStreams: number;
CurrentTime: number;
FontColorCode: string;
RunTime: number;
AdUrl: null;
ShowAds: boolean;
CaptionCssUrl: string;
FontSize: number;
FontScale: number;
CaptionLanguages: string[];
CaptionLanguage: string;
CaptionVttUrls: Record<string, string>;
VideoLanguages: string[];
VideoLanguage: string;
VideoUrls: Record<string, HidiveStreamList>;
FontColorName: string;
AutoPlayNextEpisode: boolean;
MaxStreams: number;
CurrentTime: number;
FontColorCode: string;
RunTime: number;
AdUrl: null;
}
export interface HidiveStreamList {
hls: string[];
drm: string[];
drmEnabled: boolean;
hls: string[];
drm: string[];
drmEnabled: boolean;
}
export interface HidiveStreamInfo extends HidiveStreamList {
language?: string;
episodeTitle?: string;
seriesTitle?: string;
season?: number;
episodeNumber?: number;
uncut?: boolean;
image?: string;
language?: string;
episodeTitle?: string;
seriesTitle?: string;
season?: number;
episodeNumber?: number;
uncut?: boolean;
image?: string;
}
export interface HidiveSubtitleInfo {
language: string;
cc: boolean;
url: string;
language: string;
cc: boolean;
url: string;
}
export type DownloadedMedia = {
type: 'Video',
lang: LanguageItem,
path: string,
uncut: boolean
type: 'Video',
lang: LanguageItem,
path: string,
uncut: boolean
} | ({
type: 'Subtitle',
cc: boolean
type: 'Subtitle',
cc: boolean
} & sxItem )

14
@types/iso639.d.ts vendored
View file

@ -1,9 +1,9 @@
declare module 'iso-639' {
export type iso639Type = {
[key: string]: {
'639-1'?: string,
'639-2'?: string
}
}
export const iso_639_2: iso639Type;
export type iso639Type = {
[key: string]: {
'639-1'?: string,
'639-2'?: string
}
}
export const iso_639_2: iso639Type;
}

262
@types/items.d.ts vendored
View file

@ -1,169 +1,169 @@
export interface Item {
// Added later
id: string,
id_split: (number|string)[]
// Added from the start
mostRecentSvodJpnUs: MostRecentSvodJpnUs;
synopsis: string;
mediaCategory: ContentType;
mostRecentSvodUsEndTimestamp: number;
quality: QualityClass;
genres: Genre[];
titleImages: TitleImages;
engAllTerritoryAvail: EngAllTerritoryAvail;
thumb: string;
mostRecentSvodJpnAllTerrStartTimestamp: number;
title: string;
starRating: number;
primaryAvail: PrimaryAvail;
access: Access[];
version: Version[];
mostRecentSvodJpnAllTerrEndTimestamp: number;
itemId: number;
versionAudio: VersionAudio;
contentType: ContentType;
mostRecentSvodUsStartTimestamp: number;
poster: string;
mostRecentSvodEngAllTerrEndTimestamp: number;
mostRecentSvodJpnUsStartTimestamp: number;
mostRecentSvodJpnUsEndTimestamp: number;
mostRecentSvodStartTimestamp: number;
mostRecentSvod: MostRecent;
altAvail: AltAvail;
ids: IDs;
mostRecentSvodUs: MostRecent;
item: Item;
mostRecentSvodEngAllTerrStartTimestamp: number;
audio: string[];
mostRecentAvod: MostRecent;
// Added later
id: string,
id_split: (number|string)[]
// Added from the start
mostRecentSvodJpnUs: MostRecentSvodJpnUs;
synopsis: string;
mediaCategory: ContentType;
mostRecentSvodUsEndTimestamp: number;
quality: QualityClass;
genres: Genre[];
titleImages: TitleImages;
engAllTerritoryAvail: EngAllTerritoryAvail;
thumb: string;
mostRecentSvodJpnAllTerrStartTimestamp: number;
title: string;
starRating: number;
primaryAvail: PrimaryAvail;
access: Access[];
version: Version[];
mostRecentSvodJpnAllTerrEndTimestamp: number;
itemId: number;
versionAudio: VersionAudio;
contentType: ContentType;
mostRecentSvodUsStartTimestamp: number;
poster: string;
mostRecentSvodEngAllTerrEndTimestamp: number;
mostRecentSvodJpnUsStartTimestamp: number;
mostRecentSvodJpnUsEndTimestamp: number;
mostRecentSvodStartTimestamp: number;
mostRecentSvod: MostRecent;
altAvail: AltAvail;
ids: IDs;
mostRecentSvodUs: MostRecent;
item: Item;
mostRecentSvodEngAllTerrStartTimestamp: number;
audio: string[];
mostRecentAvod: MostRecent;
}
export enum ContentType {
Episode = 'episode',
Ova = 'ova',
Episode = 'episode',
Ova = 'ova',
}
export interface IDs {
externalShowId: ID;
externalSeasonId: ExternalSeasonID;
externalEpisodeId: string;
externalAsianId?: string
externalShowId: ID;
externalSeasonId: ExternalSeasonID;
externalEpisodeId: string;
externalAsianId?: string
}
export interface Item {
seasonTitle: string;
seasonId: number;
episodeOrder: number;
episodeSlug: string;
created: Date;
titleSlug: string;
episodeNum: string;
episodeId: number;
titleId: number;
seasonNum: string;
ratings: Array<string[]>;
showImage: string;
titleName: string;
runtime: string;
episodeName: string;
seasonOrder: number;
titleExternalId: string;
seasonTitle: string;
seasonId: number;
episodeOrder: number;
episodeSlug: string;
created: Date;
titleSlug: string;
episodeNum: string;
episodeId: number;
titleId: number;
seasonNum: string;
ratings: Array<string[]>;
showImage: string;
titleName: string;
runtime: string;
episodeName: string;
seasonOrder: number;
titleExternalId: string;
}
export interface MostRecent {
image?: string;
siblingStartTimestamp?: string;
devices?: Device[];
availId?: number;
distributor?: Distributor;
quality?: MostRecentAvodQuality;
endTimestamp?: string;
mediaCategory?: ContentType;
isPromo?: boolean;
siblingType?: Purchase;
version?: Version;
territory?: Territory;
startDate?: Date;
endDate?: Date;
versionId?: number;
tier?: Device | null;
purchase?: Purchase;
startTimestamp?: string;
language?: Audio;
itemTitle?: string;
ids?: MostRecentAvodIDS;
experience?: number;
siblingEndTimestamp?: string;
item?: Item;
subscriptionRequired?: boolean;
purchased?: boolean;
image?: string;
siblingStartTimestamp?: string;
devices?: Device[];
availId?: number;
distributor?: Distributor;
quality?: MostRecentAvodQuality;
endTimestamp?: string;
mediaCategory?: ContentType;
isPromo?: boolean;
siblingType?: Purchase;
version?: Version;
territory?: Territory;
startDate?: Date;
endDate?: Date;
versionId?: number;
tier?: Device | null;
purchase?: Purchase;
startTimestamp?: string;
language?: Audio;
itemTitle?: string;
ids?: MostRecentAvodIDS;
experience?: number;
siblingEndTimestamp?: string;
item?: Item;
subscriptionRequired?: boolean;
purchased?: boolean;
}
export interface MostRecentAvodIDS {
externalSeasonId: ExternalSeasonID;
externalAsianId: null;
externalShowId: ID;
externalEpisodeId: string;
externalEnglishId: string;
externalAlphaId: string;
externalSeasonId: ExternalSeasonID;
externalAsianId: null;
externalShowId: ID;
externalEpisodeId: string;
externalEnglishId: string;
externalAlphaId: string;
}
export enum Purchase {
AVOD = 'A-VOD',
Dfov = 'DFOV',
Est = 'EST',
Svod = 'SVOD',
AVOD = 'A-VOD',
Dfov = 'DFOV',
Est = 'EST',
Svod = 'SVOD',
}
export enum Version {
Simulcast = 'Simulcast',
Uncut = 'Uncut',
Simulcast = 'Simulcast',
Uncut = 'Uncut',
}
export type MostRecentSvodJpnUs = Record<string, any>
export interface QualityClass {
quality: QualityQuality;
height: number;
quality: QualityQuality;
height: number;
}
export enum QualityQuality {
HD = 'HD',
SD = 'SD',
HD = 'HD',
SD = 'SD',
}
export interface TitleImages {
showThumbnail: string;
showBackgroundSite: string;
showDetailHeaderDesktop: string;
continueWatchingDesktop: string;
showDetailHeroSite: string;
appleHorizontalBannerShow: string;
backgroundImageXbox_360: string;
applePosterCover: string;
showDetailBoxArtTablet: string;
featuredShowBackgroundTablet: string;
backgroundImageAppletvfiretv: string;
newShowDetailHero: string;
showDetailHeroDesktop: string;
showKeyart: string;
continueWatchingMobile: string;
featuredSpotlightShowPhone: string;
appleHorizontalBannerMovie: string;
featuredSpotlightShowTablet: string;
showDetailBoxArtPhone: string;
featuredShowBackgroundPhone: string;
appleSquareCover: string;
backgroundVideo: string;
showMasterKeyArt: string;
newShowDetailHeroPhone: string;
showDetailBoxArtXbox_360: string;
showDetailHeaderMobile: string;
showLogo: string;
showThumbnail: string;
showBackgroundSite: string;
showDetailHeaderDesktop: string;
continueWatchingDesktop: string;
showDetailHeroSite: string;
appleHorizontalBannerShow: string;
backgroundImageXbox_360: string;
applePosterCover: string;
showDetailBoxArtTablet: string;
featuredShowBackgroundTablet: string;
backgroundImageAppletvfiretv: string;
newShowDetailHero: string;
showDetailHeroDesktop: string;
showKeyart: string;
continueWatchingMobile: string;
featuredSpotlightShowPhone: string;
appleHorizontalBannerMovie: string;
featuredSpotlightShowTablet: string;
showDetailBoxArtPhone: string;
featuredShowBackgroundPhone: string;
appleSquareCover: string;
backgroundVideo: string;
showMasterKeyArt: string;
newShowDetailHeroPhone: string;
showDetailBoxArtXbox_360: string;
showDetailHeaderMobile: string;
showLogo: string;
}
export interface VersionAudio {
Uncut?: Audio[];
Simulcast: Audio[];
Uncut?: Audio[];
Simulcast: Audio[];
}

View file

@ -1,49 +1,49 @@
declare module 'm3u8-parsed' {
export type M3U8 = {
allowCache: boolean,
discontinuityStarts: [],
segments: {
duration: number,
byterange?: {
length: number,
offset: number
},
uri: string,
key: {
method: string,
uri: string,
},
timeline: number
}[],
version: number,
mediaGroups: {
[type: string]: {
[index: string]: {
[language: string]: {
default: boolean,
autoselect: boolean,
language: string,
uri: string
}
}
}
},
playlists: {
uri: string,
timeline: number,
attributes: {
'CLOSED-CAPTIONS': string,
'AUDIO': string,
'FRAME-RATE': number,
'RESOLUTION': {
width: number,
height: number
},
'CODECS': string,
'AVERAGE-BANDWIDTH': string,
'BANDWIDTH': number
}
}[],
}
export default function (data: string): M3U8;
export type M3U8 = {
allowCache: boolean,
discontinuityStarts: [],
segments: {
duration: number,
byterange?: {
length: number,
offset: number
},
uri: string,
key: {
method: string,
uri: string,
},
timeline: number
}[],
version: number,
mediaGroups: {
[type: string]: {
[index: string]: {
[language: string]: {
default: boolean,
autoselect: boolean,
language: string,
uri: string
}
}
}
},
playlists: {
uri: string,
timeline: number,
attributes: {
'CLOSED-CAPTIONS': string,
'AUDIO': string,
'FRAME-RATE': number,
'RESOLUTION': {
width: number,
height: number
},
'CODECS': string,
'AVERAGE-BANDWIDTH': string,
'BANDWIDTH': number
}
}[],
}
export default function (data: string): M3U8;
}

View file

@ -4,97 +4,97 @@ import type { AvailableMuxer } from '../modules/module.args';
import { LanguageItem } from '../modules/module.langsData';
export interface MessageHandler {
name: string
auth: (data: AuthData) => Promise<AuthResponse>;
version: () => Promise<string>;
checkToken: () => Promise<CheckTokenResponse>;
search: (data: SearchData) => Promise<SearchResponse>,
availableDubCodes: () => Promise<string[]>,
availableSubCodes: () => Promise<string[]>,
handleDefault: (name: string) => Promise<any>,
resolveItems: (data: ResolveItemsData) => Promise<boolean>,
listEpisodes: (id: string) => Promise<EpisodeListResponse>,
downloadItem: (data: QueueItem) => void,
isDownloading: () => Promise<boolean>,
openFolder: (path: FolderTypes) => void,
openFile: (data: [FolderTypes, string]) => void,
openURL: (data: string) => void;
getQueue: () => Promise<QueueItem[]>,
removeFromQueue: (index: number) => void,
clearQueue: () => void,
setDownloadQueue: (data: boolean) => void,
getDownloadQueue: () => Promise<boolean>
name: string
auth: (data: AuthData) => Promise<AuthResponse>;
version: () => Promise<string>;
checkToken: () => Promise<CheckTokenResponse>;
search: (data: SearchData) => Promise<SearchResponse>,
availableDubCodes: () => Promise<string[]>,
availableSubCodes: () => Promise<string[]>,
handleDefault: (name: string) => Promise<any>,
resolveItems: (data: ResolveItemsData) => Promise<boolean>,
listEpisodes: (id: string) => Promise<EpisodeListResponse>,
downloadItem: (data: QueueItem) => void,
isDownloading: () => Promise<boolean>,
openFolder: (path: FolderTypes) => void,
openFile: (data: [FolderTypes, string]) => void,
openURL: (data: string) => void;
getQueue: () => Promise<QueueItem[]>,
removeFromQueue: (index: number) => void,
clearQueue: () => void,
setDownloadQueue: (data: boolean) => void,
getDownloadQueue: () => Promise<boolean>
}
export type FolderTypes = 'content' | 'config';
export type QueueItem = {
title: string,
episode: string,
fileName: string,
dlsubs: string[],
parent: {
title: string,
season: string
},
q: number,
dlVideoOnce: boolean,
dubLang: string[],
image: string,
title: string,
episode: string,
fileName: string,
dlsubs: string[],
parent: {
title: string,
season: string
},
q: number,
dlVideoOnce: boolean,
dubLang: string[],
image: string,
} & ResolveItemsData
export type ResolveItemsData = {
id: string,
dubLang: string[],
all: boolean,
but: boolean,
novids: boolean,
noaudio: boolean
dlVideoOnce: boolean,
e: string,
fileName: string,
q: number,
dlsubs: string[]
id: string,
dubLang: string[],
all: boolean,
but: boolean,
novids: boolean,
noaudio: boolean
dlVideoOnce: boolean,
e: string,
fileName: string,
q: number,
dlsubs: string[]
}
export type SearchResponseItem = {
image: string,
name: string,
desc?: string,
id: string,
lang?: string[],
rating: number
image: string,
name: string,
desc?: string,
id: string,
lang?: string[],
rating: number
};
export type Episode = {
e: string,
lang: string[],
name: string,
season: string,
seasonTitle: string,
episode: string,
id: string,
img: string,
description: string,
time: string
e: string,
lang: string[],
name: string,
season: string,
seasonTitle: string,
episode: string,
id: string,
img: string,
description: string,
time: string
}
export type SearchResponse = ResponseBase<SearchResponseItem[]>
export type EpisodeListResponse = ResponseBase<Episode[]>
export type FuniEpisodeData = {
title: string,
episode: string,
epsiodeNumber: string,
episodeID: string,
seasonTitle: string,
seasonNumber: string,
ids: {
episode: string,
show: string,
season: string
},
image: string
title: string,
episode: string,
epsiodeNumber: string,
episodeID: string,
seasonTitle: string,
seasonNumber: string,
ids: {
episode: string,
show: string,
season: string
},
image: string
};
export type AuthData = { username: string, password: string };
@ -102,12 +102,12 @@ export type SearchData = { search: string, page?: number, 'search-type'?: string
export type FuniGetShowData = { id: number, e?: string, but: boolean, all: boolean };
export type FuniGetEpisodeData = { subs: FuniSubsData, fnSlug: FuniEpisodeData, simul?: boolean; dubLang: string[], s: string }
export type FuniStreamData = { force?: 'Y'|'y'|'N'|'n'|'C'|'c', callbackMaker?: (data: DownloadInfo) => HLSCallback, q: number, x: number, fileName: string, numbers: number, novids?: boolean,
timeout: number, partsize: number, fsRetryTime: number, noaudio?: boolean, mp4: boolean, ass: boolean, fontSize: number, fontName?: string, skipmux?: boolean,
forceMuxer: AvailableMuxer | undefined, simul: boolean, skipSubMux: boolean, nocleanup: boolean, override: string[], videoTitle: string,
ffmpegOptions: string[], mkvmergeOptions: string[], defaultAudio: LanguageItem, defaultSub: LanguageItem, ccTag: string }
timeout: number, partsize: number, fsRetryTime: number, noaudio?: boolean, mp4: boolean, ass: boolean, fontSize: number, fontName?: string, skipmux?: boolean,
forceMuxer: AvailableMuxer | undefined, simul: boolean, skipSubMux: boolean, nocleanup: boolean, override: string[], videoTitle: string,
ffmpegOptions: string[], mkvmergeOptions: string[], defaultAudio: LanguageItem, defaultSub: LanguageItem, ccTag: string }
export type FuniSubsData = { nosubs?: boolean, sub: boolean, dlsubs: string[], ccTag: string }
export type DownloadData = {
hslang?: string; id: string, e: string, dubLang: string[], dlsubs: string[], fileName: string, q: number, novids: boolean, noaudio: boolean, dlVideoOnce: boolean
hslang?: string; id: string, e: string, dubLang: string[], dlsubs: string[], fileName: string, q: number, novids: boolean, noaudio: boolean, dlVideoOnce: boolean
}
export type AuthResponse = ResponseBase<undefined>;
@ -118,44 +118,44 @@ export type CheckTokenResponse = ResponseBase<undefined>;
export type ResponseBase<T> = ({
isOk: true,
value: T
isOk: true,
value: T
} | {
isOk: false,
reason: Error
isOk: false,
reason: Error
});
export type ProgressData = {
total: number,
cur: number,
percent: number|string,
time: number,
downloadSpeed: number,
bytes: number
total: number,
cur: number,
percent: number|string,
time: number,
downloadSpeed: number,
bytes: number
};
export type PossibleMessages = keyof ServiceHandler;
export type DownloadInfo = {
image: string,
parent: {
title: string
},
title: string,
language: LanguageItem,
fileName: string
image: string,
parent: {
title: string
},
title: string,
language: LanguageItem,
fileName: string
}
export type ExtendedProgress = {
progress: ProgressData,
downloadInfo: DownloadInfo
progress: ProgressData,
downloadInfo: DownloadInfo
}
export type GuiState = {
setup: boolean,
services: Record<string, GuiStateService>
setup: boolean,
services: Record<string, GuiStateService>
}
export type GuiStateService = {
queue: QueueItem[]
queue: QueueItem[]
}

192
@types/mpd-parser.d.ts vendored
View file

@ -1,101 +1,101 @@
declare module 'mpd-parser' {
export type Segment = {
uri: string,
timeline: number,
duration: number,
resolvedUri: string,
map: {
uri: string,
resolvedUri: string,
byterange?: {
length: number,
offset: number
}
},
byterange?: {
length: number,
offset: number
},
number: number,
presentationTime: number
}
export type Segment = {
uri: string,
timeline: number,
duration: number,
resolvedUri: string,
map: {
uri: string,
resolvedUri: string,
byterange?: {
length: number,
offset: number
}
},
byterange?: {
length: number,
offset: number
},
number: number,
presentationTime: number
}
export type Sidx = {
uri: string,
resolvedUri: string,
byterange: {
length: number,
offset: number
},
map: {
uri: string,
resolvedUri: string,
byterange: {
length: number,
offset: number
}
},
duration: number,
timeline: number,
presentationTime: number,
number: number
}
export type Sidx = {
uri: string,
resolvedUri: string,
byterange: {
length: number,
offset: number
},
map: {
uri: string,
resolvedUri: string,
byterange: {
length: number,
offset: number
}
},
duration: number,
timeline: number,
presentationTime: number,
number: 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[]
sidx?: Sidx
}
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[]
sidx?: Sidx
}
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
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

@ -1,43 +1,43 @@
export interface NewHidiveEpisode {
description: string;
duration: number;
title: string;
categories: string[];
contentDownload: ContentDownload;
favourite: boolean;
subEvents: any[];
thumbnailUrl: string;
longDescription: string;
posterUrl: string;
offlinePlaybackLanguages: string[];
externalAssetId: string;
maxHeight: number;
rating: Rating;
episodeInformation: EpisodeInformation;
id: number;
accessLevel: string;
playerUrlCallback: string;
thumbnailsPreview: string;
displayableTags: any[];
plugins: any[];
watchStatus: string;
computedReleases: any[];
licences: any[];
type: string;
description: string;
duration: number;
title: string;
categories: string[];
contentDownload: ContentDownload;
favourite: boolean;
subEvents: any[];
thumbnailUrl: string;
longDescription: string;
posterUrl: string;
offlinePlaybackLanguages: string[];
externalAssetId: string;
maxHeight: number;
rating: Rating;
episodeInformation: EpisodeInformation;
id: number;
accessLevel: string;
playerUrlCallback: string;
thumbnailsPreview: string;
displayableTags: any[];
plugins: any[];
watchStatus: string;
computedReleases: any[];
licences: any[];
type: string;
}
export interface ContentDownload {
permission: string;
period: string;
permission: string;
period: string;
}
export interface EpisodeInformation {
seasonNumber: number;
episodeNumber: number;
season: number;
seasonNumber: number;
episodeNumber: number;
season: number;
}
export interface Rating {
rating: string;
descriptors: any[];
rating: string;
descriptors: any[];
}

View file

@ -1,33 +1,33 @@
export interface NewHidivePlayback {
watermark: null;
skipMarkers: any[];
annotations: null;
dash: Format[];
hls: Format[];
watermark: null;
skipMarkers: any[];
annotations: null;
dash: Format[];
hls: Format[];
}
export interface Format {
subtitles: Subtitle[];
url: string;
drm: DRM;
subtitles: Subtitle[];
url: string;
drm: DRM;
}
export interface DRM {
encryptionMode: string;
containerType: string;
jwtToken: string;
url: string;
keySystems: string[];
encryptionMode: string;
containerType: string;
jwtToken: string;
url: string;
keySystems: string[];
}
export interface Subtitle {
format: Formats;
language: string;
url: string;
format: Formats;
language: string;
url: string;
}
export enum Formats {
Scc = 'scc',
Srt = 'srt',
Vtt = 'vtt',
Scc = 'scc',
Srt = 'srt',
Vtt = 'vtt',
}

View file

@ -1,88 +1,88 @@
export interface NewHidiveSearch {
results: Result[];
results: Result[];
}
export interface Result {
hits: Hit[];
nbHits: number;
page: number;
nbPages: number;
hitsPerPage: number;
exhaustiveNbHits: boolean;
exhaustiveTypo: boolean;
exhaustive: Exhaustive;
query: string;
params: string;
index: string;
renderingContent: object;
processingTimeMS: number;
processingTimingsMS: ProcessingTimingsMS;
serverTimeMS: number;
hits: Hit[];
nbHits: number;
page: number;
nbPages: number;
hitsPerPage: number;
exhaustiveNbHits: boolean;
exhaustiveTypo: boolean;
exhaustive: Exhaustive;
query: string;
params: string;
index: string;
renderingContent: object;
processingTimeMS: number;
processingTimingsMS: ProcessingTimingsMS;
serverTimeMS: number;
}
export interface Exhaustive {
nbHits: boolean;
typo: boolean;
nbHits: boolean;
typo: boolean;
}
export interface Hit {
type: string;
weight: number;
id: number;
name: string;
description: string;
meta: object;
coverUrl: string;
smallCoverUrl: string;
seasonsCount: number;
tags: string[];
localisations: HitLocalisations;
ratings: Ratings;
objectID: string;
_highlightResult: HighlightResult;
type: string;
weight: number;
id: number;
name: string;
description: string;
meta: object;
coverUrl: string;
smallCoverUrl: string;
seasonsCount: number;
tags: string[];
localisations: HitLocalisations;
ratings: Ratings;
objectID: string;
_highlightResult: HighlightResult;
}
export interface HighlightResult {
name: Description;
description: Description;
tags: Description[];
localisations: HighlightResultLocalisations;
name: Description;
description: Description;
tags: Description[];
localisations: HighlightResultLocalisations;
}
export interface Description {
value: string;
matchLevel: string;
matchedWords: string[];
fullyHighlighted?: boolean;
value: string;
matchLevel: string;
matchedWords: string[];
fullyHighlighted?: boolean;
}
export interface HighlightResultLocalisations {
en_US: PurpleEnUS;
en_US: PurpleEnUS;
}
export interface PurpleEnUS {
title: Description;
description: Description;
title: Description;
description: Description;
}
export interface HitLocalisations {
[language: string]: HitLocalization;
[language: string]: HitLocalization;
}
export interface HitLocalization {
title: string;
description: string;
title: string;
description: string;
}
export interface Ratings {
US: string[];
US: string[];
}
export interface ProcessingTimingsMS {
_request: Request;
_request: Request;
}
export interface Request {
queue: number;
roundTrip: number;
queue: number;
roundTrip: number;
}

View file

@ -1,89 +1,89 @@
export interface NewHidiveSeason {
title: string;
description: string;
longDescription: string;
smallCoverUrl: string;
coverUrl: string;
titleUrl: string;
posterUrl: string;
seasonNumber: number;
episodeCount: number;
displayableTags: any[];
rating: Rating;
contentRating: Rating;
id: number;
series: Series;
episodes: Episode[];
paging: Paging;
licences: any[];
title: string;
description: string;
longDescription: string;
smallCoverUrl: string;
coverUrl: string;
titleUrl: string;
posterUrl: string;
seasonNumber: number;
episodeCount: number;
displayableTags: any[];
rating: Rating;
contentRating: Rating;
id: number;
series: Series;
episodes: Episode[];
paging: Paging;
licences: any[];
}
export interface Rating {
rating: string;
descriptors: any[];
rating: string;
descriptors: any[];
}
export interface Episode {
accessLevel: string;
availablePurchases?: any[];
licenceIds?: any[];
type: string;
id: number;
title: string;
description: string;
thumbnailUrl: string;
posterUrl: string;
duration: number;
favourite: boolean;
contentDownload: ContentDownload;
offlinePlaybackLanguages: string[];
externalAssetId: string;
subEvents: any[];
maxHeight: number;
thumbnailsPreview: string;
longDescription: string;
episodeInformation: EpisodeInformation;
categories: string[];
displayableTags: any[];
watchStatus: string;
computedReleases: any[];
accessLevel: string;
availablePurchases?: any[];
licenceIds?: any[];
type: string;
id: number;
title: string;
description: string;
thumbnailUrl: string;
posterUrl: string;
duration: number;
favourite: boolean;
contentDownload: ContentDownload;
offlinePlaybackLanguages: string[];
externalAssetId: string;
subEvents: any[];
maxHeight: number;
thumbnailsPreview: string;
longDescription: string;
episodeInformation: EpisodeInformation;
categories: string[];
displayableTags: any[];
watchStatus: string;
computedReleases: any[];
}
export interface ContentDownload {
permission: string;
permission: string;
}
export interface EpisodeInformation {
seasonNumber: number;
episodeNumber: number;
season: number;
seasonNumber: number;
episodeNumber: number;
season: number;
}
export interface Paging {
moreDataAvailable: boolean;
lastSeen: number;
moreDataAvailable: boolean;
lastSeen: number;
}
export interface Series {
seriesId: number;
title: string;
description: string;
longDescription: string;
displayableTags: any[];
rating: Rating;
contentRating: Rating;
seriesId: number;
title: string;
description: string;
longDescription: string;
displayableTags: any[];
rating: Rating;
contentRating: Rating;
}
export interface NewHidiveSeriesExtra extends Series {
season: NewHidiveSeason;
season: NewHidiveSeason;
}
export interface NewHidiveEpisodeExtra extends Episode {
titleId: number;
nameLong: string;
seasonTitle: string;
seriesTitle: string;
seriesId?: number;
isSelected: boolean;
jwtToken?: string;
titleId: number;
nameLong: string;
seasonTitle: string;
seriesTitle: string;
seriesId?: number;
isSelected: boolean;
jwtToken?: string;
}

View file

@ -1,35 +1,35 @@
export interface NewHidiveSeries {
id: number;
title: string;
description: string;
longDescription: string;
smallCoverUrl: string;
coverUrl: string;
titleUrl: string;
posterUrl: string;
seasons: Season[];
rating: Rating;
contentRating: Rating;
displayableTags: any[];
paging: Paging;
id: number;
title: string;
description: string;
longDescription: string;
smallCoverUrl: string;
coverUrl: string;
titleUrl: string;
posterUrl: string;
seasons: Season[];
rating: Rating;
contentRating: Rating;
displayableTags: any[];
paging: Paging;
}
export interface Rating {
rating: string;
descriptors: any[];
rating: string;
descriptors: any[];
}
export interface Paging {
moreDataAvailable: boolean;
lastSeen: number;
moreDataAvailable: boolean;
lastSeen: number;
}
export interface Season {
title: string;
description: string;
longDescription: string;
seasonNumber: number;
episodeCount: number;
displayableTags: any[];
id: number;
title: string;
description: string;
longDescription: string;
seasonNumber: number;
episodeCount: number;
displayableTags: any[];
id: number;
}

332
@types/objectInfo.d.ts vendored
View file

@ -1,211 +1,211 @@
// Generated by https://quicktype.io
export interface ObjectInfo {
total: number;
data: CrunchyObject[];
meta: Record<unknown>;
total: number;
data: CrunchyObject[];
meta: Record<unknown>;
}
export interface CrunchyObject {
__links__?: Links;
channel_id: string;
slug: string;
images: Images;
linked_resource_key: string;
description: string;
promo_description: string;
external_id: string;
title: string;
series_metadata?: SeriesMetadata;
id: string;
slug_title: string;
type: string;
promo_title: string;
movie_listing_metadata?: MovieListingMetadata;
movie_metadata?: MovieMetadata;
playback?: string;
episode_metadata?: EpisodeMetadata;
streams_link?: string;
season_metadata?: SeasonMetadata;
isSelected?: boolean;
f_num: string;
s_num: string;
__links__?: Links;
channel_id: string;
slug: string;
images: Images;
linked_resource_key: string;
description: string;
promo_description: string;
external_id: string;
title: string;
series_metadata?: SeriesMetadata;
id: string;
slug_title: string;
type: string;
promo_title: string;
movie_listing_metadata?: MovieListingMetadata;
movie_metadata?: MovieMetadata;
playback?: string;
episode_metadata?: EpisodeMetadata;
streams_link?: string;
season_metadata?: SeasonMetadata;
isSelected?: boolean;
f_num: string;
s_num: string;
}
export interface Links {
'episode/season': LinkData;
'episode/series': LinkData;
resource: LinkData;
'resource/channel': LinkData;
streams: LinkData;
'episode/season': LinkData;
'episode/series': LinkData;
resource: LinkData;
'resource/channel': LinkData;
streams: LinkData;
}
export interface LinkData {
href: string;
href: string;
}
export interface EpisodeMetadata {
audio_locale: Locale;
availability_ends: Date;
availability_notes: string;
availability_starts: Date;
available_date: null;
available_offline: boolean;
closed_captions_available: boolean;
duration_ms: number;
eligible_region: string;
episode: string;
episode_air_date: Date;
episode_number: number;
extended_maturity_rating: Record<unknown>;
free_available_date: Date;
identifier: string;
is_clip: boolean;
is_dubbed: boolean;
is_mature: boolean;
is_premium_only: boolean;
is_subbed: boolean;
mature_blocked: boolean;
maturity_ratings: string[];
premium_available_date: Date;
premium_date: null;
season_id: string;
season_number: number;
season_slug_title: string;
season_title: string;
sequence_number: number;
series_id: string;
series_slug_title: string;
series_title: string;
subtitle_locales: Locale[];
tenant_categories?: string[];
upload_date: Date;
versions: EpisodeMetadataVersion[];
audio_locale: Locale;
availability_ends: Date;
availability_notes: string;
availability_starts: Date;
available_date: null;
available_offline: boolean;
closed_captions_available: boolean;
duration_ms: number;
eligible_region: string;
episode: string;
episode_air_date: Date;
episode_number: number;
extended_maturity_rating: Record<unknown>;
free_available_date: Date;
identifier: string;
is_clip: boolean;
is_dubbed: boolean;
is_mature: boolean;
is_premium_only: boolean;
is_subbed: boolean;
mature_blocked: boolean;
maturity_ratings: string[];
premium_available_date: Date;
premium_date: null;
season_id: string;
season_number: number;
season_slug_title: string;
season_title: string;
sequence_number: number;
series_id: string;
series_slug_title: string;
series_title: string;
subtitle_locales: Locale[];
tenant_categories?: string[];
upload_date: Date;
versions: EpisodeMetadataVersion[];
}
export interface EpisodeMetadataVersion {
audio_locale: Locale;
guid: string;
is_premium_only: boolean;
media_guid: string;
original: boolean;
season_guid: string;
variant: string;
audio_locale: Locale;
guid: string;
is_premium_only: boolean;
media_guid: string;
original: boolean;
season_guid: string;
variant: string;
}
export interface Images {
poster_tall?: Array<Image[]>;
poster_wide?: Array<Image[]>;
promo_image?: Array<Image[]>;
thumbnail?: Array<Image[]>;
poster_tall?: Array<Image[]>;
poster_wide?: Array<Image[]>;
promo_image?: Array<Image[]>;
thumbnail?: Array<Image[]>;
}
export interface Image {
height: number;
source: string;
type: ImageType;
width: number;
height: number;
source: string;
type: ImageType;
width: number;
}
export enum ImageType {
PosterTall = 'poster_tall',
PosterWide = 'poster_wide',
PromoImage = 'promo_image',
Thumbnail = 'thumbnail',
PosterTall = 'poster_tall',
PosterWide = 'poster_wide',
PromoImage = 'promo_image',
Thumbnail = 'thumbnail',
}
export interface MovieListingMetadata {
availability_notes: string;
available_date: null;
available_offline: boolean;
duration_ms: number;
extended_description: string;
extended_maturity_rating: Record<unknown>;
first_movie_id: string;
free_available_date: Date;
is_dubbed: boolean;
is_mature: boolean;
is_premium_only: boolean;
is_subbed: boolean;
mature_blocked: boolean;
maturity_ratings: string[];
movie_release_year: number;
premium_available_date: Date;
premium_date: null;
subtitle_locales: Locale[];
tenant_categories: string[];
availability_notes: string;
available_date: null;
available_offline: boolean;
duration_ms: number;
extended_description: string;
extended_maturity_rating: Record<unknown>;
first_movie_id: string;
free_available_date: Date;
is_dubbed: boolean;
is_mature: boolean;
is_premium_only: boolean;
is_subbed: boolean;
mature_blocked: boolean;
maturity_ratings: string[];
movie_release_year: number;
premium_available_date: Date;
premium_date: null;
subtitle_locales: Locale[];
tenant_categories: string[];
}
export interface MovieMetadata {
availability_notes: string;
available_offline: boolean;
closed_captions_available: boolean;
duration_ms: number;
extended_maturity_rating: Record<unknown>;
is_dubbed: boolean;
is_mature: boolean;
is_premium_only: boolean;
is_subbed: boolean;
mature_blocked: boolean;
maturity_ratings: string[];
movie_listing_id: string;
movie_listing_slug_title: string;
movie_listing_title: string;
availability_notes: string;
available_offline: boolean;
closed_captions_available: boolean;
duration_ms: number;
extended_maturity_rating: Record<unknown>;
is_dubbed: boolean;
is_mature: boolean;
is_premium_only: boolean;
is_subbed: boolean;
mature_blocked: boolean;
maturity_ratings: string[];
movie_listing_id: string;
movie_listing_slug_title: string;
movie_listing_title: string;
}
export interface SeasonMetadata {
audio_locale: Locale;
audio_locales: Locale[];
extended_maturity_rating: Record<unknown>;
identifier: string;
is_mature: boolean;
mature_blocked: boolean;
maturity_ratings: string[];
season_display_number: string;
season_sequence_number: number;
subtitle_locales: Locale[];
versions: SeasonMetadataVersion[];
audio_locale: Locale;
audio_locales: Locale[];
extended_maturity_rating: Record<unknown>;
identifier: string;
is_mature: boolean;
mature_blocked: boolean;
maturity_ratings: string[];
season_display_number: string;
season_sequence_number: number;
subtitle_locales: Locale[];
versions: SeasonMetadataVersion[];
}
export interface SeasonMetadataVersion {
audio_locale: Locale;
guid: string;
original: boolean;
variant: string;
audio_locale: Locale;
guid: string;
original: boolean;
variant: string;
}
export interface SeriesMetadata {
audio_locales: Locale[];
availability_notes: string;
episode_count: number;
extended_description: string;
extended_maturity_rating: Record<unknown>;
is_dubbed: boolean;
is_mature: boolean;
is_simulcast: boolean;
is_subbed: boolean;
mature_blocked: boolean;
maturity_ratings: string[];
season_count: number;
series_launch_year: number;
subtitle_locales: Locale[];
tenant_categories?: string[];
audio_locales: Locale[];
availability_notes: string;
episode_count: number;
extended_description: string;
extended_maturity_rating: Record<unknown>;
is_dubbed: boolean;
is_mature: boolean;
is_simulcast: boolean;
is_subbed: boolean;
mature_blocked: boolean;
maturity_ratings: string[];
season_count: number;
series_launch_year: number;
subtitle_locales: Locale[];
tenant_categories?: string[];
}
export enum Locale {
enUS = 'en-US',
esLA = 'es-LA',
es419 = 'es-419',
esES = 'es-ES',
ptBR = 'pt-BR',
frFR = 'fr-FR',
deDE = 'de-DE',
arME = 'ar-ME',
arSA = 'ar-SA',
itIT = 'it-IT',
ruRU = 'ru-RU',
trTR = 'tr-TR',
hiIN = 'hi-IN',
zhCN = 'zh-CN',
koKR = 'ko-KR',
jaJP = 'ja-JP',
enUS = 'en-US',
esLA = 'es-LA',
es419 = 'es-419',
esES = 'es-ES',
ptBR = 'pt-BR',
frFR = 'fr-FR',
deDE = 'de-DE',
arME = 'ar-ME',
arSA = 'ar-SA',
itIT = 'it-IT',
ruRU = 'ru-RU',
trTR = 'tr-TR',
hiIN = 'hi-IN',
zhCN = 'zh-CN',
koKR = 'ko-KR',
jaJP = 'ja-JP',
}

2
@types/pkg.d.ts vendored
View file

@ -1,3 +1,3 @@
declare module 'pkg' {
export async function exec(config: string[]);
export async function exec(config: string[]);
}

View file

@ -1,120 +1,120 @@
// Generated by https://quicktype.io
export interface PlaybackData {
total: number;
vpb: { [key: string]: { [key: string]: StreamDetails } };
apb: { [key: string]: { [key: string]: StreamDetails } };
meta: Meta;
total: number;
vpb: { [key: string]: { [key: string]: StreamDetails } };
apb: { [key: string]: { [key: string]: StreamDetails } };
meta: Meta;
}
export interface StreamList {
download_hls: CrunchyStreams;
drm_adaptive_hls: CrunchyStreams;
multitrack_adaptive_hls_v2: CrunchyStreams;
vo_adaptive_hls: CrunchyStreams;
vo_drm_adaptive_hls: CrunchyStreams;
adaptive_hls: CrunchyStreams;
drm_download_dash: CrunchyStreams;
drm_download_hls: CrunchyStreams;
drm_multitrack_adaptive_hls_v2: CrunchyStreams;
vo_drm_adaptive_dash: CrunchyStreams;
adaptive_dash: CrunchyStreams;
urls: CrunchyStreams;
vo_adaptive_dash: CrunchyStreams;
download_dash: CrunchyStreams;
drm_adaptive_dash: CrunchyStreams;
download_hls: CrunchyStreams;
drm_adaptive_hls: CrunchyStreams;
multitrack_adaptive_hls_v2: CrunchyStreams;
vo_adaptive_hls: CrunchyStreams;
vo_drm_adaptive_hls: CrunchyStreams;
adaptive_hls: CrunchyStreams;
drm_download_dash: CrunchyStreams;
drm_download_hls: CrunchyStreams;
drm_multitrack_adaptive_hls_v2: CrunchyStreams;
vo_drm_adaptive_dash: CrunchyStreams;
adaptive_dash: CrunchyStreams;
urls: CrunchyStreams;
vo_adaptive_dash: CrunchyStreams;
download_dash: CrunchyStreams;
drm_adaptive_dash: CrunchyStreams;
}
export interface CrunchyStreams {
'': StreamDetails;
'en-US'?: StreamDetails;
'es-LA'?: StreamDetails;
'es-419'?: StreamDetails;
'es-ES'?: StreamDetails;
'pt-BR'?: StreamDetails;
'fr-FR'?: StreamDetails;
'de-DE'?: StreamDetails;
'ar-ME'?: StreamDetails;
'ar-SA'?: StreamDetails;
'it-IT'?: StreamDetails;
'ru-RU'?: StreamDetails;
'tr-TR'?: StreamDetails;
'hi-IN'?: StreamDetails;
'zh-CN'?: StreamDetails;
'ko-KR'?: StreamDetails;
'ja-JP'?: StreamDetails;
[string: string]: StreamDetails;
'': StreamDetails;
'en-US'?: StreamDetails;
'es-LA'?: StreamDetails;
'es-419'?: StreamDetails;
'es-ES'?: StreamDetails;
'pt-BR'?: StreamDetails;
'fr-FR'?: StreamDetails;
'de-DE'?: StreamDetails;
'ar-ME'?: StreamDetails;
'ar-SA'?: StreamDetails;
'it-IT'?: StreamDetails;
'ru-RU'?: StreamDetails;
'tr-TR'?: StreamDetails;
'hi-IN'?: StreamDetails;
'zh-CN'?: StreamDetails;
'ko-KR'?: StreamDetails;
'ja-JP'?: StreamDetails;
[string: string]: StreamDetails;
}
export interface StreamDetails {
//hardsub_locale: Locale;
hardsub_locale: string;
url: string;
hardsub_lang?: string;
audio_lang?: string;
type?: string;
//hardsub_locale: Locale;
hardsub_locale: string;
url: string;
hardsub_lang?: string;
audio_lang?: string;
type?: string;
}
export interface Meta {
media_id: string;
subtitles: Subtitles;
bifs: string[];
versions: Version[];
audio_locale: Locale;
closed_captions: Subtitles;
captions: Subtitles;
media_id: string;
subtitles: Subtitles;
bifs: string[];
versions: Version[];
audio_locale: Locale;
closed_captions: Subtitles;
captions: Subtitles;
}
export interface Subtitles {
''?: SubtitleInfo;
'en-US'?: SubtitleInfo;
'es-LA'?: SubtitleInfo;
'es-419'?: SubtitleInfo;
'es-ES'?: SubtitleInfo;
'pt-BR'?: SubtitleInfo;
'fr-FR'?: SubtitleInfo;
'de-DE'?: SubtitleInfo;
'ar-ME'?: SubtitleInfo;
'ar-SA'?: SubtitleInfo;
'it-IT'?: SubtitleInfo;
'ru-RU'?: SubtitleInfo;
'tr-TR'?: SubtitleInfo;
'hi-IN'?: SubtitleInfo;
'zh-CN'?: SubtitleInfo;
'ko-KR'?: SubtitleInfo;
'ja-JP'?: SubtitleInfo;
''?: SubtitleInfo;
'en-US'?: SubtitleInfo;
'es-LA'?: SubtitleInfo;
'es-419'?: SubtitleInfo;
'es-ES'?: SubtitleInfo;
'pt-BR'?: SubtitleInfo;
'fr-FR'?: SubtitleInfo;
'de-DE'?: SubtitleInfo;
'ar-ME'?: SubtitleInfo;
'ar-SA'?: SubtitleInfo;
'it-IT'?: SubtitleInfo;
'ru-RU'?: SubtitleInfo;
'tr-TR'?: SubtitleInfo;
'hi-IN'?: SubtitleInfo;
'zh-CN'?: SubtitleInfo;
'ko-KR'?: SubtitleInfo;
'ja-JP'?: SubtitleInfo;
}
export interface SubtitleInfo {
format: string;
locale: Locale;
url: string;
format: string;
locale: Locale;
url: string;
}
export interface Version {
audio_locale: Locale;
guid: string;
is_premium_only: boolean;
media_guid: string;
original: boolean;
season_guid: string;
variant: string;
audio_locale: Locale;
guid: string;
is_premium_only: boolean;
media_guid: string;
original: boolean;
season_guid: string;
variant: string;
}
export enum Locale {
default = '',
enUS = 'en-US',
esLA = 'es-LA',
es419 = 'es-419',
esES = 'es-ES',
ptBR = 'pt-BR',
frFR = 'fr-FR',
deDE = 'de-DE',
arME = 'ar-ME',
arSA = 'ar-SA',
itIT = 'it-IT',
ruRU = 'ru-RU',
trTR = 'tr-TR',
hiIN = 'hi-IN',
zhCN = 'zh-CN',
koKR = 'ko-KR',
jaJP = 'ja-JP',
default = '',
enUS = 'en-US',
esLA = 'es-LA',
es419 = 'es-419',
esES = 'es-ES',
ptBR = 'pt-BR',
frFR = 'fr-FR',
deDE = 'de-DE',
arME = 'ar-ME',
arSA = 'ar-SA',
itIT = 'it-IT',
ruRU = 'ru-RU',
trTR = 'tr-TR',
hiIN = 'hi-IN',
zhCN = 'zh-CN',
koKR = 'ko-KR',
jaJP = 'ja-JP',
}

View file

@ -1,15 +1,15 @@
import { ExtendedProgress, QueueItem } from './messageHandler';
export type RandomEvents = {
progress: ExtendedProgress,
finish: undefined,
queueChange: QueueItem[],
current: QueueItem|undefined
progress: ExtendedProgress,
finish: undefined,
queueChange: QueueItem[],
current: QueueItem|undefined
}
export interface RandomEvent<T extends keyof RandomEvents> {
name: T,
data: RandomEvents[T]
name: T,
data: RandomEvents[T]
}
export type Handler<T extends keyof RandomEvents> = (data: RandomEvent<T>) => unknown;

View file

@ -1,3 +1,3 @@
declare module 'removeNPMAbsolutePaths' {
export default async function modulesCleanup(path: string);
export default async function modulesCleanup(path: string);
}

View file

@ -1,3 +1,3 @@
export interface ServiceClass {
cli: () => Promise<boolean|undefined|void>
cli: () => Promise<boolean|undefined|void>
}

View file

@ -1,28 +1,28 @@
// Generated by https://quicktype.io
export interface StreamData {
items: Item[];
watchHistorySaveInterval: number;
errors?: Error[]
items: Item[];
watchHistorySaveInterval: number;
errors?: Error[]
}
export interface Error {
detail: string,
code: number
detail: string,
code: number
}
export interface Item {
src: string;
kind: string;
isPromo: boolean;
videoType: string;
aips: Aip[];
experienceId: string;
showAds: boolean;
id: number;
src: string;
kind: string;
isPromo: boolean;
videoType: string;
aips: Aip[];
experienceId: string;
showAds: boolean;
id: number;
}
export interface Aip {
out: number;
in: number;
out: number;
in: number;
}

View file

@ -1,4 +1,4 @@
export type UpdateFile = {
lastCheck: number,
nextCheck: number
lastCheck: number,
nextCheck: number
}

62
@types/ws.d.ts vendored
View file

@ -2,44 +2,44 @@ import { GUIConfig } from '../modules/module.cfg-loader';
import { AuthResponse, CheckTokenResponse, EpisodeListResponse, FolderTypes, QueueItem, ResolveItemsData, SearchData, SearchResponse } from './messageHandler';
export type WSMessage<T extends keyof MessageTypes, P extends 0|1 = 0> = {
name: T,
data: MessageTypes[T][P]
name: T,
data: MessageTypes[T][P]
}
export type WSMessageWithID<T extends keyof MessageTypes, P extends 0|1 = 0> = WSMessage<T, P> & {
id: string
id: string
}
export type UnknownWSMessage = {
name: keyof MessageTypes,
data: MessageTypes[keyof MessageTypes][0],
id: string
name: keyof MessageTypes,
data: MessageTypes[keyof MessageTypes][0],
id: string
}
export type MessageTypes = {
'auth': [AuthData, AuthResponse],
'version': [undefined, string],
'checkToken': [undefined, CheckTokenResponse],
'search': [SearchData, SearchResponse],
'default': [string, unknown],
'availableDubCodes': [undefined, string[]],
'availableSubCodes': [undefined, string[]],
'resolveItems': [ResolveItemsData, boolean],
'listEpisodes': [string, EpisodeListResponse],
'downloadItem': [QueueItem, undefined],
'isDownloading': [undefined, boolean],
'openFolder': [FolderTypes, undefined],
'changeProvider': [undefined, boolean],
'type': [undefined, 'crunchy'|'hidive'|'ao'|'adn'|undefined],
'setup': ['crunchy'|'hidive'|'ao'|'adn'|undefined, undefined],
'openFile': [[FolderTypes, string], undefined],
'openURL': [string, undefined],
'isSetup': [undefined, boolean],
'setupServer': [GUIConfig, boolean],
'requirePassword': [undefined, boolean],
'getQueue': [undefined, QueueItem[]],
'removeFromQueue': [number, undefined],
'clearQueue': [undefined, undefined],
'setDownloadQueue': [boolean, undefined],
'getDownloadQueue': [undefined, boolean]
'auth': [AuthData, AuthResponse],
'version': [undefined, string],
'checkToken': [undefined, CheckTokenResponse],
'search': [SearchData, SearchResponse],
'default': [string, unknown],
'availableDubCodes': [undefined, string[]],
'availableSubCodes': [undefined, string[]],
'resolveItems': [ResolveItemsData, boolean],
'listEpisodes': [string, EpisodeListResponse],
'downloadItem': [QueueItem, undefined],
'isDownloading': [undefined, boolean],
'openFolder': [FolderTypes, undefined],
'changeProvider': [undefined, boolean],
'type': [undefined, 'crunchy'|'hidive'|'ao'|'adn'|undefined],
'setup': ['crunchy'|'hidive'|'ao'|'adn'|undefined, undefined],
'openFile': [[FolderTypes, string], undefined],
'openURL': [string, undefined],
'isSetup': [undefined, boolean],
'setupServer': [GUIConfig, boolean],
'requirePassword': [undefined, boolean],
'getQueue': [undefined, QueueItem[]],
'removeFromQueue': [number, undefined],
'clearQueue': [undefined, undefined],
'setDownloadQueue': [boolean, undefined],
'getDownloadQueue': [undefined, boolean]
}

1650
adn.ts

File diff suppressed because it is too large Load diff

1502
ao.ts

File diff suppressed because it is too large Load diff

5652
crunchy.ts

File diff suppressed because it is too large Load diff

View file

@ -4,60 +4,59 @@ import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
{
rules: {
'no-console': 2,
'react/prop-types': 0,
'react-hooks/exhaustive-deps': 0,
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unsafe-declaration-merging': 'warn',
'@typescript-eslint/no-unused-vars' : 'warn',
'@typescript-eslint/no-unused-expressions': 'warn',
'indent': [
'error',
2
],
'linebreak-style': [
'warn',
'windows'
],
'quotes': [
'error',
'single'
],
'semi': [
'error',
'always'
]
},
languageOptions: {
parserOptions: {
ecmaFeatures: {
jsx: true,
eslint.configs.recommended,
...tseslint.configs.recommended,
{
rules: {
'no-console': 2,
'react/prop-types': 0,
'react-hooks/exhaustive-deps': 0,
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unsafe-declaration-merging': 'warn',
'@typescript-eslint/no-unused-vars' : 'warn',
'@typescript-eslint/no-unused-expressions': 'warn',
'indent': [
'error',
4
],
'linebreak-style': [
'warn',
'windows'
],
'quotes': [
'error',
'single'
],
'semi': [
'error',
'always'
]
},
ecmaVersion: 2020,
sourceType: 'module'
},
parser: tseslint.parser
languageOptions: {
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 2020,
sourceType: 'module'
},
parser: tseslint.parser
}
},
{
ignores: [
'**/lib',
'**/videos/*.ts',
'**/build',
'dev.js',
'tsc.ts'
]
},
{
files: ['gui/react/**/*'],
rules: {
'no-console': 0,
'indent': 'off'
}
}
},
{
ignores: [
'**/lib',
'**/videos/*.ts',
'**/build',
'dev.js',
'tsc.ts'
]
},
{
files: ['gui/react/**/*'],
rules: {
'no-console': 0,
// Disabled because ESLint bugs around on .tsx files somehow?
indent: 'off'
}
}
);

View file

@ -1,3 +1,3 @@
{
"presets": ["@babel/preset-env","@babel/preset-react", "@babel/preset-typescript"]
"presets": ["@babel/preset-env","@babel/preset-react", "@babel/preset-typescript"]
}

View file

@ -1,57 +1,57 @@
{
"name": "anidl-gui",
"version": "1.0.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^7.1.2",
"@mui/lab": "7.0.0-beta.12",
"@mui/material": "^7.1.2",
"concurrently": "^9.2.0",
"notistack": "^3.0.2",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"typescript": "^5.8.3",
"uuid": "^11.1.0",
"ws": "^8.18.2"
},
"devDependencies": {
"@babel/cli": "^7.27.2",
"@babel/core": "^7.27.4",
"@babel/preset-env": "^7.27.2",
"@babel/preset-react": "^7.27.1",
"@babel/preset-typescript": "^7.27.1",
"@types/node": "^22.15.32",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/uuid": "^10.0.0",
"babel-loader": "^10.0.0",
"css-loader": "^7.1.2",
"html-webpack-plugin": "^5.6.3",
"style-loader": "^4.0.0",
"ts-node": "^10.9.2",
"webpack": "^5.99.9",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.2"
},
"proxy": "http://localhost:3000",
"scripts": {
"build": "npx tsc && npx webpack",
"start": "npx concurrently -k npm:frontend npm:backend",
"frontend": "npx webpack-dev-server",
"backend": "npx ts-node -T ../../gui.ts"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
"name": "anidl-gui",
"version": "1.0.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^7.1.2",
"@mui/lab": "7.0.0-beta.12",
"@mui/material": "^7.1.2",
"concurrently": "^9.2.0",
"notistack": "^3.0.2",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"typescript": "^5.8.3",
"uuid": "^11.1.0",
"ws": "^8.18.2"
},
"devDependencies": {
"@babel/cli": "^7.27.2",
"@babel/core": "^7.27.4",
"@babel/preset-env": "^7.27.2",
"@babel/preset-react": "^7.27.1",
"@babel/preset-typescript": "^7.27.1",
"@types/node": "^22.15.32",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/uuid": "^10.0.0",
"babel-loader": "^10.0.0",
"css-loader": "^7.1.2",
"html-webpack-plugin": "^5.6.3",
"style-loader": "^4.0.0",
"ts-node": "^10.9.2",
"webpack": "^5.99.9",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.2"
},
"proxy": "http://localhost:3000",
"scripts": {
"build": "npx tsc && npx webpack",
"start": "npx concurrently -k npm:frontend npm:backend",
"frontend": "npx webpack-dev-server",
"backend": "npx ts-node -T ../../gui.ts"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View file

@ -1,15 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>Multi Downloader</title>
<link rel="icon" type="image/webp" href="favicon.webp">
<meta charset="UTF-8">
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self' 'unsafe-eval'"
/>
</head>
<body>
<div id="root"></div>
</body>
<head>
<title>Multi Downloader</title>
<link rel="icon" type="image/webp" href="favicon.webp">
<meta charset="UTF-8">
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self' 'unsafe-eval'"
/>
</head>
<body>
<div id="root"></div>
</body>
</html>

View file

@ -1,3 +1,3 @@
type FCWithChildren<T = object> = React.FC<{
children?: React.ReactNode[]|React.ReactNode
children?: React.ReactNode[]|React.ReactNode
} & T>

View file

@ -2,9 +2,9 @@ import React from 'react';
import Layout from './Layout';
const App: React.FC = () => {
return (
<Layout />
);
return (
<Layout />
);
};
export default App;

View file

@ -11,28 +11,28 @@ import MenuBar from './components/MenuBar/MenuBar';
const Layout: React.FC = () => {
const messageHandler = React.useContext(messageChannelContext);
const messageHandler = React.useContext(messageChannelContext);
return <Box sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '100%', alignItems: 'center',}}>
<MenuBar />
<Box sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '93vw',
maxWidth: '93rem',
maxHeight: '3rem'
//backgroundColor: '#ffffff',
}}>
<LogoutButton />
<AuthButton />
<Button variant="contained" startIcon={<Folder />} onClick={() => messageHandler?.openFolder('content')} sx={{ height: '37px' }}>Open Output Directory</Button>
<Button variant="contained" startIcon={<ClearAll />} onClick={() => messageHandler?.clearQueue() } sx={{ height: '37px' }}>Clear Queue</Button>
<AddToQueue />
<StartQueueButton />
</Box>
<MainFrame />
</Box>;
return <Box sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '100%', alignItems: 'center',}}>
<MenuBar />
<Box sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '93vw',
maxWidth: '93rem',
maxHeight: '3rem'
//backgroundColor: '#ffffff',
}}>
<LogoutButton />
<AuthButton />
<Button variant="contained" startIcon={<Folder />} onClick={() => messageHandler?.openFolder('content')} sx={{ height: '37px' }}>Open Output Directory</Button>
<Button variant="contained" startIcon={<ClearAll />} onClick={() => messageHandler?.clearQueue() } sx={{ height: '37px' }}>Clear Queue</Button>
<AddToQueue />
<StartQueueButton />
</Box>
<MainFrame />
</Box>;
};
export default Layout;

View file

@ -2,18 +2,18 @@ import React from 'react';
import { Container, Box, ThemeProvider, createTheme, Theme } from '@mui/material';
const makeTheme = (mode: 'dark'|'light') : Partial<Theme> => {
return createTheme({
palette: {
mode,
},
});
return createTheme({
palette: {
mode,
},
});
};
const Style: FCWithChildren = ({children}) => {
return <ThemeProvider theme={makeTheme('dark')}>
<Box sx={{ }}/>
{children}
</ThemeProvider>;
return <ThemeProvider theme={makeTheme('dark')}>
<Box sx={{ }}/>
{children}
</ThemeProvider>;
};
export default Style;

View file

@ -6,22 +6,22 @@ import EpisodeListing from './DownloadSelector/Listing/EpisodeListing';
import SearchBox from './SearchBox/SearchBox';
const AddToQueue: React.FC = () => {
const [isOpen, setOpen] = React.useState(false);
const [isOpen, setOpen] = React.useState(false);
return <Box>
<EpisodeListing />
<Dialog open={isOpen} onClose={() => setOpen(false)} maxWidth='md' PaperProps={{ elevation:4 }}>
<Box>
<SearchBox />
<Divider variant='middle'/>
<DownloadSelector onFinish={() => setOpen(false)} />
</Box>
</Dialog>
<Button variant='contained' onClick={() => setOpen(true)} sx={{ maxHeight: '2.3rem' }}>
<Add />
return <Box>
<EpisodeListing />
<Dialog open={isOpen} onClose={() => setOpen(false)} maxWidth='md' PaperProps={{ elevation:4 }}>
<Box>
<SearchBox />
<Divider variant='middle'/>
<DownloadSelector onFinish={() => setOpen(false)} />
</Box>
</Dialog>
<Button variant='contained' onClick={() => setOpen(true)} sx={{ maxHeight: '2.3rem' }}>
<Add />
Add to Queue
</Button>
</Box>;
</Button>
</Box>;
};
export default AddToQueue;

View file

@ -12,316 +12,316 @@ type DownloadSelectorProps = {
}
const DownloadSelector: React.FC<DownloadSelectorProps> = ({ onFinish }) => {
const messageHandler = React.useContext(messageChannelContext);
const [store, dispatch] = useStore();
const [availableDubs, setAvailableDubs] = React.useState<string[]>([]);
const [availableSubs, setAvailableSubs ] = React.useState<string[]>([]);
const [ loading, setLoading ] = React.useState(false);
const { enqueueSnackbar } = useSnackbar();
const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8;
const messageHandler = React.useContext(messageChannelContext);
const [store, dispatch] = useStore();
const [availableDubs, setAvailableDubs] = React.useState<string[]>([]);
const [availableSubs, setAvailableSubs ] = React.useState<string[]>([]);
const [ loading, setLoading ] = React.useState(false);
const { enqueueSnackbar } = useSnackbar();
const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8;
React.useEffect(() => {
(async () => {
/* If we don't wait the response is undefined? */
await new Promise((resolve) => setTimeout(() => resolve(undefined), 100));
const dubLang = messageHandler?.handleDefault('dubLang');
const subLang = messageHandler?.handleDefault('dlsubs');
const q = messageHandler?.handleDefault('q');
const fileName = messageHandler?.handleDefault('fileName');
const dlVideoOnce = messageHandler?.handleDefault('dlVideoOnce');
const result = await Promise.all([dubLang, subLang, q, fileName, dlVideoOnce]);
dispatch({
type: 'downloadOptions',
payload: {
...store.downloadOptions,
dubLang: result[0],
dlsubs: result[1],
q: result[2],
fileName: result[3],
dlVideoOnce: result[4],
React.useEffect(() => {
(async () => {
/* If we don't wait the response is undefined? */
await new Promise((resolve) => setTimeout(() => resolve(undefined), 100));
const dubLang = messageHandler?.handleDefault('dubLang');
const subLang = messageHandler?.handleDefault('dlsubs');
const q = messageHandler?.handleDefault('q');
const fileName = messageHandler?.handleDefault('fileName');
const dlVideoOnce = messageHandler?.handleDefault('dlVideoOnce');
const result = await Promise.all([dubLang, subLang, q, fileName, dlVideoOnce]);
dispatch({
type: 'downloadOptions',
payload: {
...store.downloadOptions,
dubLang: result[0],
dlsubs: result[1],
q: result[2],
fileName: result[3],
dlVideoOnce: result[4],
}
});
setAvailableDubs(await messageHandler?.availableDubCodes() ?? []);
setAvailableSubs(await messageHandler?.availableSubCodes() ?? []);
})();
}, []);
const addToQueue = async () => {
setLoading(true);
const res = await messageHandler?.resolveItems(store.downloadOptions);
if (!res)
return enqueueSnackbar('The request failed. Please check if the ID is correct.', {
variant: 'error'
});
setLoading(false);
if (onFinish)
onFinish();
};
const listEpisodes = async () => {
if (!store.downloadOptions.id) {
return enqueueSnackbar('Please enter a ID', {
variant: 'error'
});
}
});
setAvailableDubs(await messageHandler?.availableDubCodes() ?? []);
setAvailableSubs(await messageHandler?.availableSubCodes() ?? []);
})();
}, []);
const addToQueue = async () => {
setLoading(true);
const res = await messageHandler?.resolveItems(store.downloadOptions);
if (!res)
return enqueueSnackbar('The request failed. Please check if the ID is correct.', {
variant: 'error'
});
setLoading(false);
if (onFinish)
onFinish();
};
const listEpisodes = async () => {
if (!store.downloadOptions.id) {
return enqueueSnackbar('Please enter a ID', {
variant: 'error'
});
}
setLoading(true);
const res = await messageHandler?.listEpisodes(store.downloadOptions.id);
if (!res || !res.isOk) {
setLoading(false);
return enqueueSnackbar('The request failed. Please check if the ID is correct.', {
variant: 'error'
});
} else {
dispatch({
type: 'episodeListing',
payload: res.value
});
}
setLoading(false);
};
return <Box sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center' }}>
<Box sx={{display: 'flex',
flexDirection: 'column',
alignItems: 'center',
margin: '5px',
}}>
<Box sx={{
width: '50rem',
height: '21rem',
margin: '10px',
display: 'flex',
justifyContent: 'space-between',
//backgroundColor: '#ffffff30',
}}>
<Box sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '0.7rem',
//backgroundColor: '#ff000030'
}}>
<Typography sx={{fontSize: '1.4rem'}}>
General Options
</Typography>
<TextField value={store.downloadOptions.id} required onChange={e => {
dispatch({
type: 'downloadOptions',
payload: { ...store.downloadOptions, id: e.target.value }
setLoading(true);
const res = await messageHandler?.listEpisodes(store.downloadOptions.id);
if (!res || !res.isOk) {
setLoading(false);
return enqueueSnackbar('The request failed. Please check if the ID is correct.', {
variant: 'error'
});
}} label='Show ID'/>
<TextField type='number' value={store.downloadOptions.q} required onChange={e => {
const parsed = parseInt(e.target.value);
if (isNaN(parsed) || parsed < 0 || parsed > 10)
return;
} else {
dispatch({
type: 'downloadOptions',
payload: { ...store.downloadOptions, q: parsed }
type: 'episodeListing',
payload: res.value
});
}} label='Quality Level (0 for max)'/>
<Box sx={{ display: 'flex', gap: '5px' }}>
<Button sx={{ textTransform: 'none'}} onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, noaudio: !store.downloadOptions.noaudio } })} variant={store.downloadOptions.noaudio ? 'contained' : 'outlined'}>Skip Audio</Button>
<Button sx={{ textTransform: 'none'}} onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, novids: !store.downloadOptions.novids } })} variant={store.downloadOptions.novids ? 'contained' : 'outlined'}>Skip Video</Button>
</Box>
<Button sx={{ textTransform: 'none'}} onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, dlVideoOnce: !store.downloadOptions.dlVideoOnce } })} variant={store.downloadOptions.dlVideoOnce ? 'contained' : 'outlined'}>Skip Unnecessary</Button>
<Tooltip title={store.service == 'hidive' ? '' :
<Typography>
Simulcast is only supported on Hidive
</Typography>}
arrow placement='top'
>
<Box>
<Button sx={{ textTransform: 'none'}} disabled={store.service != 'hidive'} onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, simul: !store.downloadOptions.simul } })} variant={store.downloadOptions.simul ? 'contained' : 'outlined'}>Download Simulcast ver.</Button>
</Box>
</Tooltip>
</Box>
<Box sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '0.7rem',
//backgroundColor: '#00000020'
}}>
<Typography sx={{fontSize: '1.4rem'}}>
Episode Options
</Typography>
<Box sx={{
display: 'flex',
}
setLoading(false);
};
return <Box sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center' }}>
<Box sx={{display: 'flex',
flexDirection: 'column',
gap: '1px'
}}>
<Box sx={{
borderColor: '#595959',
borderStyle: 'solid',
borderWidth: '1px',
borderRadius: '5px',
//backgroundColor: '#ff4567',
width: '15rem',
height: '3.5rem',
display: 'flex',
'&:hover' : {
borderColor: '#ffffff',
},
}}>
<InputBase sx={{
ml: 2,
flex: 1,
}}
disabled={store.downloadOptions.all} value={store.downloadOptions.e} required onChange={e => {
dispatch({
type: 'downloadOptions',
payload: { ...store.downloadOptions, e: e.target.value }
});
}} placeholder='Episode Select'/>
<Divider orientation='vertical'/>
<LoadingButton loading={loading} disableElevation disableFocusRipple disableRipple disableTouchRipple onClick={listEpisodes} variant='text' sx={{ textTransform: 'none'}}><Typography>List<br/>Episodes</Typography></LoadingButton>
</Box>
</Box>
<Button sx={{ textTransform: 'none'}} onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, all: !store.downloadOptions.all } })} variant={store.downloadOptions.all ? 'contained' : 'outlined'}>Download All</Button>
<Button sx={{ textTransform: 'none'}} onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, but: !store.downloadOptions.but } })} variant={store.downloadOptions.but ? 'contained' : 'outlined'}>Download All but</Button>
</Box>
<Box sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '0.7rem',
//backgroundColor: '#00ff0020'
alignItems: 'center',
margin: '5px',
}}>
<Typography sx={{fontSize: '1.4rem'}}>
Language Options
</Typography>
<MultiSelect
title='Dub Languages'
values={availableDubs}
selected={store.downloadOptions.dubLang}
onChange={(e) => {
dispatch({
type: 'downloadOptions',
payload: { ...store.downloadOptions, dubLang: e }
});
}}
allOption
/>
<MultiSelect
title='Sub Languages'
values={availableSubs}
selected={store.downloadOptions.dlsubs}
onChange={(e) => {
dispatch({
type: 'downloadOptions',
payload: { ...store.downloadOptions, dlsubs: e }
});
}}
/>
<Tooltip title={store.service == 'crunchy' ? '' :
<Typography>
Hardsubs are only supported on Crunchyroll
</Typography>
}
arrow placement='top'>
<Box sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
gap: '1rem'
}}>
<Box sx={{
borderRadius: '5px',
//backgroundColor: '#ff4567',
width: '15rem',
height: '3.5rem',
width: '50rem',
height: '21rem',
margin: '10px',
display: 'flex',
}}>
<FormControl fullWidth>
<InputLabel id='hsLabel'>Hardsub Language</InputLabel>
<Select
MenuProps={{
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 250
}
}
}}
labelId='hsLabel'
label='Hardsub Language'
disabled={store.service != 'crunchy'}
value={store.downloadOptions.hslang}
onChange={(e) => {
dispatch({
type: 'downloadOptions',
payload: { ...store.downloadOptions, hslang: (e.target.value as string) === '' ? undefined : e.target.value as string }
});
}}
>
<MenuItem value=''>No Hardsub</MenuItem>
{availableSubs.map((lang) => {
if(lang === 'all' || lang === 'none')
return undefined;
return <MenuItem value={lang}>{lang}</MenuItem>;
})}
</Select>
</FormControl>
</Box>
justifyContent: 'space-between',
//backgroundColor: '#ffffff30',
}}>
<Box sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '0.7rem',
//backgroundColor: '#ff000030'
}}>
<Typography sx={{fontSize: '1.4rem'}}>
General Options
</Typography>
<TextField value={store.downloadOptions.id} required onChange={e => {
dispatch({
type: 'downloadOptions',
payload: { ...store.downloadOptions, id: e.target.value }
});
}} label='Show ID'/>
<TextField type='number' value={store.downloadOptions.q} required onChange={e => {
const parsed = parseInt(e.target.value);
if (isNaN(parsed) || parsed < 0 || parsed > 10)
return;
dispatch({
type: 'downloadOptions',
payload: { ...store.downloadOptions, q: parsed }
});
}} label='Quality Level (0 for max)'/>
<Box sx={{ display: 'flex', gap: '5px' }}>
<Button sx={{ textTransform: 'none'}} onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, noaudio: !store.downloadOptions.noaudio } })} variant={store.downloadOptions.noaudio ? 'contained' : 'outlined'}>Skip Audio</Button>
<Button sx={{ textTransform: 'none'}} onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, novids: !store.downloadOptions.novids } })} variant={store.downloadOptions.novids ? 'contained' : 'outlined'}>Skip Video</Button>
</Box>
<Button sx={{ textTransform: 'none'}} onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, dlVideoOnce: !store.downloadOptions.dlVideoOnce } })} variant={store.downloadOptions.dlVideoOnce ? 'contained' : 'outlined'}>Skip Unnecessary</Button>
<Tooltip title={store.service == 'hidive' ? '' :
<Typography>
Simulcast is only supported on Hidive
</Typography>}
arrow placement='top'
>
<Box>
<Button sx={{ textTransform: 'none'}} disabled={store.service != 'hidive'} onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, simul: !store.downloadOptions.simul } })} variant={store.downloadOptions.simul ? 'contained' : 'outlined'}>Download Simulcast ver.</Button>
</Box>
</Tooltip>
</Box>
<Box sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '0.7rem',
//backgroundColor: '#00000020'
}}>
<Typography sx={{fontSize: '1.4rem'}}>
Episode Options
</Typography>
<Box sx={{
display: 'flex',
flexDirection: 'column',
gap: '1px'
}}>
<Box sx={{
borderColor: '#595959',
borderStyle: 'solid',
borderWidth: '1px',
borderRadius: '5px',
//backgroundColor: '#ff4567',
width: '15rem',
height: '3.5rem',
display: 'flex',
'&:hover' : {
borderColor: '#ffffff',
},
}}>
<InputBase sx={{
ml: 2,
flex: 1,
}}
disabled={store.downloadOptions.all} value={store.downloadOptions.e} required onChange={e => {
dispatch({
type: 'downloadOptions',
payload: { ...store.downloadOptions, e: e.target.value }
});
}} placeholder='Episode Select'/>
<Divider orientation='vertical'/>
<LoadingButton loading={loading} disableElevation disableFocusRipple disableRipple disableTouchRipple onClick={listEpisodes} variant='text' sx={{ textTransform: 'none'}}><Typography>List<br/>Episodes</Typography></LoadingButton>
</Box>
</Box>
<Button sx={{ textTransform: 'none'}} onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, all: !store.downloadOptions.all } })} variant={store.downloadOptions.all ? 'contained' : 'outlined'}>Download All</Button>
<Button sx={{ textTransform: 'none'}} onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, but: !store.downloadOptions.but } })} variant={store.downloadOptions.but ? 'contained' : 'outlined'}>Download All but</Button>
</Box>
<Box sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '0.7rem',
//backgroundColor: '#00ff0020'
}}>
<Typography sx={{fontSize: '1.4rem'}}>
Language Options
</Typography>
<MultiSelect
title='Dub Languages'
values={availableDubs}
selected={store.downloadOptions.dubLang}
onChange={(e) => {
dispatch({
type: 'downloadOptions',
payload: { ...store.downloadOptions, dubLang: e }
});
}}
allOption
/>
<MultiSelect
title='Sub Languages'
values={availableSubs}
selected={store.downloadOptions.dlsubs}
onChange={(e) => {
dispatch({
type: 'downloadOptions',
payload: { ...store.downloadOptions, dlsubs: e }
});
}}
/>
<Tooltip title={store.service == 'crunchy' ? '' :
<Typography>
Hardsubs are only supported on Crunchyroll
</Typography>
}
arrow placement='top'>
<Box sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
gap: '1rem'
}}>
<Box sx={{
borderRadius: '5px',
//backgroundColor: '#ff4567',
width: '15rem',
height: '3.5rem',
display: 'flex',
}}>
<FormControl fullWidth>
<InputLabel id='hsLabel'>Hardsub Language</InputLabel>
<Select
MenuProps={{
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 250
}
}
}}
labelId='hsLabel'
label='Hardsub Language'
disabled={store.service != 'crunchy'}
value={store.downloadOptions.hslang}
onChange={(e) => {
dispatch({
type: 'downloadOptions',
payload: { ...store.downloadOptions, hslang: (e.target.value as string) === '' ? undefined : e.target.value as string }
});
}}
>
<MenuItem value=''>No Hardsub</MenuItem>
{availableSubs.map((lang) => {
if(lang === 'all' || lang === 'none')
return undefined;
return <MenuItem value={lang}>{lang}</MenuItem>;
})}
</Select>
</FormControl>
</Box>
<Tooltip title={
<Typography>
<Tooltip title={
<Typography>
Downloads the hardsub version of the selected subtitle.<br/>Subtitles are displayed <b>PERMANENTLY!</b><br/>You can choose only <b>1</b> subtitle per video!
</Typography>
} arrow placement='top'>
<InfoOutlinedIcon sx={{
transition: '100ms',
ml: '0.35rem',
mr: '0.65rem',
'&:hover' : {
color: '#ffffff30',
}
}} />
</Tooltip>
</Typography>
} arrow placement='top'>
<InfoOutlinedIcon sx={{
transition: '100ms',
ml: '0.35rem',
mr: '0.65rem',
'&:hover' : {
color: '#ffffff30',
}
}} />
</Tooltip>
</Box>
</Tooltip>
</Box>
</Box>
</Tooltip>
</Box>
</Box>
<Box sx={{width: '95%', height: '0.3rem', backgroundColor: '#ffffff50', borderRadius: '10px', marginBottom: '20px'}}/>
<Box sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
gap: '15px'
}}>
<TextField value={store.downloadOptions.fileName} onChange={e => {
dispatch({
type: 'downloadOptions',
payload: { ...store.downloadOptions, fileName: e.target.value }
});
}} sx={{ width: '87%' }} label='Filename Overwrite' />
<Tooltip title={
<Typography>
<Box sx={{width: '95%', height: '0.3rem', backgroundColor: '#ffffff50', borderRadius: '10px', marginBottom: '20px'}}/>
<Box sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
gap: '15px'
}}>
<TextField value={store.downloadOptions.fileName} onChange={e => {
dispatch({
type: 'downloadOptions',
payload: { ...store.downloadOptions, fileName: e.target.value }
});
}} sx={{ width: '87%' }} label='Filename Overwrite' />
<Tooltip title={
<Typography>
Click here to see the documentation
</Typography>
} arrow placement='top'>
<Link href='https://github.com/anidl/multi-downloader-nx/blob/master/docs/DOCUMENTATION.md#filename-template' rel="noopener noreferrer" target="_blank">
<InfoOutlinedIcon sx={{
transition: '100ms',
'&:hover' : {
color: '#ffffff30',
}
}} />
</Link>
</Tooltip>
</Box>
</Box>
<Box sx={{width: '95%', height: '0.3rem', backgroundColor: '#ffffff50', borderRadius: '10px', marginTop: '10px'}}/>
</Typography>
} arrow placement='top'>
<Link href='https://github.com/anidl/multi-downloader-nx/blob/master/docs/DOCUMENTATION.md#filename-template' rel="noopener noreferrer" target="_blank">
<InfoOutlinedIcon sx={{
transition: '100ms',
'&:hover' : {
color: '#ffffff30',
}
}} />
</Link>
</Tooltip>
</Box>
</Box>
<Box sx={{width: '95%', height: '0.3rem', backgroundColor: '#ffffff50', borderRadius: '10px', marginTop: '10px'}}/>
<LoadingButton sx={{ margin: '15px', textTransform: 'none' }} loading={loading} onClick={addToQueue} variant='contained'>Add to Queue</LoadingButton>
<LoadingButton sx={{ margin: '15px', textTransform: 'none' }} loading={loading} onClick={addToQueue} variant='contained'>Add to Queue</LoadingButton>
</Box>;
</Box>;
};
export default DownloadSelector;

View file

@ -7,185 +7,185 @@ import { useSnackbar } from 'notistack';
const EpisodeListing: React.FC = () => {
const [store, dispatch] = useStore();
const [store, dispatch] = useStore();
const [season, setSeason] = React.useState<'all'|string>('all');
const { enqueueSnackbar } = useSnackbar();
const [season, setSeason] = React.useState<'all'|string>('all');
const { enqueueSnackbar } = useSnackbar();
const seasons = React.useMemo(() => {
const s: string[] = [];
for (const {season} of store.episodeListing) {
if (s.includes(season))
continue;
s.push(season);
}
return s;
}, [ store.episodeListing ]);
const seasons = React.useMemo(() => {
const s: string[] = [];
for (const {season} of store.episodeListing) {
if (s.includes(season))
continue;
s.push(season);
}
return s;
}, [ store.episodeListing ]);
const [selected, setSelected] = React.useState<string[]>([]);
const [selected, setSelected] = React.useState<string[]>([]);
React.useEffect(() => {
setSelected(parseSelect(store.downloadOptions.e));
}, [ store.episodeListing ]);
React.useEffect(() => {
setSelected(parseSelect(store.downloadOptions.e));
}, [ store.episodeListing ]);
const close = () => {
dispatch({
type: 'episodeListing',
payload: []
});
dispatch({
type: 'downloadOptions',
payload: {
...store.downloadOptions,
e: `${([...new Set([...parseSelect(store.downloadOptions.e), ...selected])]).join(',')}`
}
});
};
const close = () => {
dispatch({
type: 'episodeListing',
payload: []
});
dispatch({
type: 'downloadOptions',
payload: {
...store.downloadOptions,
e: `${([...new Set([...parseSelect(store.downloadOptions.e), ...selected])]).join(',')}`
}
});
};
const getEpisodesForSeason = (season: string|'all') => {
return store.episodeListing.filter((a) => season === 'all' ? true : a.season === season);
};
const getEpisodesForSeason = (season: string|'all') => {
return store.episodeListing.filter((a) => season === 'all' ? true : a.season === season);
};
return <Dialog open={store.episodeListing.length > 0} onClose={close} scroll='paper' maxWidth='xl' sx={{ p: 2 }}>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 200px 20px' }}>
<Typography color='text.primary' variant="h5" sx={{ textAlign: 'center', alignItems: 'center', justifyContent: 'center', display: 'flex' }}>
return <Dialog open={store.episodeListing.length > 0} onClose={close} scroll='paper' maxWidth='xl' sx={{ p: 2 }}>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 200px 20px' }}>
<Typography color='text.primary' variant="h5" sx={{ textAlign: 'center', alignItems: 'center', justifyContent: 'center', display: 'flex' }}>
Episodes
</Typography>
<FormControl sx={{ mr: 2, mt: 2 }}>
<InputLabel id='seasonSelectLabel'>Season</InputLabel>
<Select labelId="seasonSelectLabel" label='Season' value={season} onChange={(e) => setSeason(e.target.value)}>
<MenuItem value='all'>Show all Epsiodes</MenuItem>
{seasons.map((a, index) => {
return <MenuItem value={a} key={`MenuItem_SeasonSelect_${index}`}>
{a}
</MenuItem>;
})}
</Select>
</FormControl>
</Box>
<List>
<ListItem sx={{ display: 'grid', gridTemplateColumns: '25px 1fr 5fr' }}>
<Checkbox
indeterminate={store.episodeListing.some(a => selected.includes(a.e)) && !store.episodeListing.every(a => selected.includes(a.e))}
checked={store.episodeListing.every(a => selected.includes(a.e))}
onChange={() => {
if (selected.length > 0) {
setSelected([]);
} else {
setSelected(getEpisodesForSeason(season).map(a => a.e));
}
}}
/>
</ListItem>
{getEpisodesForSeason(season).map((item, index, { length }) => {
const e = isNaN(parseInt(item.e)) ? item.e : parseInt(item.e);
const idStr = `S${item.season}E${e}`;
const isSelected = selected.includes(e.toString());
const imageRef = React.createRef<HTMLImageElement>();
const summaryRef = React.createRef<HTMLParagraphElement>();
return <Box {...{ mouseData: isSelected }} key={`Episode_List_Item_${index}`}>
<ListItem sx={{backdropFilter: isSelected ? 'brightness(1.5)' : '', '&:hover': {backdropFilter: 'brightness(1.5)'}, display: 'grid', gridTemplateColumns: '25px 50px 1fr 5fr' }}
onClick={() => {
let arr: string[] = [];
if (isSelected) {
arr = [...selected.filter(a => a !== e.toString())];
} else {
arr = [...selected, e.toString()];
}
setSelected(arr.filter(a => a.length > 0));
}}>
{ isSelected ? <CheckBox /> : <CheckBoxOutlineBlank /> }
<Typography color='text.primary' sx={{ textAlign: 'center' }}>
{idStr}
</Typography>
<img ref={imageRef} style={{ width: 'inherit', maxHeight: '200px', minWidth: '150px' }} src={item.img} alt="thumbnail" />
<Box sx={{ display: 'flex', flexDirection: 'column', pl: 1 }}>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr min-content' }}>
<Typography color='text.primary' variant="h5">
{item.name}
</Typography>
<Typography color='text.primary'>
{item.time.startsWith('00:') ? item.time.slice(3) : item.time}
</Typography>
</Box>
<Typography color='text.primary' ref={summaryRef}>
{item.description}
</Typography>
<Box sx={{ display: 'grid', gridTemplateColumns: 'fit-content 1fr' }}>
<Typography>
<br />
<FormControl sx={{ mr: 2, mt: 2 }}>
<InputLabel id='seasonSelectLabel'>Season</InputLabel>
<Select labelId="seasonSelectLabel" label='Season' value={season} onChange={(e) => setSeason(e.target.value)}>
<MenuItem value='all'>Show all Epsiodes</MenuItem>
{seasons.map((a, index) => {
return <MenuItem value={a} key={`MenuItem_SeasonSelect_${index}`}>
{a}
</MenuItem>;
})}
</Select>
</FormControl>
</Box>
<List>
<ListItem sx={{ display: 'grid', gridTemplateColumns: '25px 1fr 5fr' }}>
<Checkbox
indeterminate={store.episodeListing.some(a => selected.includes(a.e)) && !store.episodeListing.every(a => selected.includes(a.e))}
checked={store.episodeListing.every(a => selected.includes(a.e))}
onChange={() => {
if (selected.length > 0) {
setSelected([]);
} else {
setSelected(getEpisodesForSeason(season).map(a => a.e));
}
}}
/>
</ListItem>
{getEpisodesForSeason(season).map((item, index, { length }) => {
const e = isNaN(parseInt(item.e)) ? item.e : parseInt(item.e);
const idStr = `S${item.season}E${e}`;
const isSelected = selected.includes(e.toString());
const imageRef = React.createRef<HTMLImageElement>();
const summaryRef = React.createRef<HTMLParagraphElement>();
return <Box {...{ mouseData: isSelected }} key={`Episode_List_Item_${index}`}>
<ListItem sx={{backdropFilter: isSelected ? 'brightness(1.5)' : '', '&:hover': {backdropFilter: 'brightness(1.5)'}, display: 'grid', gridTemplateColumns: '25px 50px 1fr 5fr' }}
onClick={() => {
let arr: string[] = [];
if (isSelected) {
arr = [...selected.filter(a => a !== e.toString())];
} else {
arr = [...selected, e.toString()];
}
setSelected(arr.filter(a => a.length > 0));
}}>
{ isSelected ? <CheckBox /> : <CheckBoxOutlineBlank /> }
<Typography color='text.primary' sx={{ textAlign: 'center' }}>
{idStr}
</Typography>
<img ref={imageRef} style={{ width: 'inherit', maxHeight: '200px', minWidth: '150px' }} src={item.img} alt="thumbnail" />
<Box sx={{ display: 'flex', flexDirection: 'column', pl: 1 }}>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr min-content' }}>
<Typography color='text.primary' variant="h5">
{item.name}
</Typography>
<Typography color='text.primary'>
{item.time.startsWith('00:') ? item.time.slice(3) : item.time}
</Typography>
</Box>
<Typography color='text.primary' ref={summaryRef}>
{item.description}
</Typography>
<Box sx={{ display: 'grid', gridTemplateColumns: 'fit-content 1fr' }}>
<Typography>
<br />
Available audio languages: {item.lang.join(', ')}
</Typography>
</Box>
</Box>
</ListItem>
<ContextMenu options={[ { text: 'Copy image URL', onClick: async () => {
await navigator.clipboard.writeText(item.img);
enqueueSnackbar('Copied URL to clipboard', {
variant: 'info'
});
}},
{
text: 'Open image in new tab',
onClick: () => {
window.open(item.img);
}
} ]} popupItem={imageRef as RefObject<HTMLElement>} />
<ContextMenu options={[
{
onClick: async () => {
await navigator.clipboard.writeText(item.description!);
enqueueSnackbar('Copied summary to clipboard', {
variant: 'info'
});
},
text: 'Copy summary to clipboard'
}
]} popupItem={summaryRef as RefObject<HTMLElement>} />
{index < length - 1 && <Divider />}
</Box>;
})}
</List>
</Dialog>;
</Typography>
</Box>
</Box>
</ListItem>
<ContextMenu options={[ { text: 'Copy image URL', onClick: async () => {
await navigator.clipboard.writeText(item.img);
enqueueSnackbar('Copied URL to clipboard', {
variant: 'info'
});
}},
{
text: 'Open image in new tab',
onClick: () => {
window.open(item.img);
}
} ]} popupItem={imageRef as RefObject<HTMLElement>} />
<ContextMenu options={[
{
onClick: async () => {
await navigator.clipboard.writeText(item.description!);
enqueueSnackbar('Copied summary to clipboard', {
variant: 'info'
});
},
text: 'Copy summary to clipboard'
}
]} popupItem={summaryRef as RefObject<HTMLElement>} />
{index < length - 1 && <Divider />}
</Box>;
})}
</List>
</Dialog>;
};
const parseSelect = (s: string): string[] => {
const ret: string[] = [];
s.split(',').forEach(item => {
if (item.includes('-')) {
const split = item.split('-');
if (split.length !== 2)
return;
const match = split[0].match(/[A-Za-z]+/);
if (match && match.length > 0) {
if (match.index && match.index !== 0) {
return;
}
const letters = split[0].substring(0, match[0].length);
const number = parseInt(split[0].substring(match[0].length));
const b = parseInt(split[1]);
if (isNaN(number) || isNaN(b)) {
return;
}
for (let i = number; i <= b; i++) {
ret.push(`${letters}${i}`);
}
const ret: string[] = [];
s.split(',').forEach(item => {
if (item.includes('-')) {
const split = item.split('-');
if (split.length !== 2)
return;
const match = split[0].match(/[A-Za-z]+/);
if (match && match.length > 0) {
if (match.index && match.index !== 0) {
return;
}
const letters = split[0].substring(0, match[0].length);
const number = parseInt(split[0].substring(match[0].length));
const b = parseInt(split[1]);
if (isNaN(number) || isNaN(b)) {
return;
}
for (let i = number; i <= b; i++) {
ret.push(`${letters}${i}`);
}
} else {
const a = parseInt(split[0]);
const b = parseInt(split[1]);
if (isNaN(a) || isNaN(b)) {
return;
} else {
const a = parseInt(split[0]);
const b = parseInt(split[1]);
if (isNaN(a) || isNaN(b)) {
return;
}
for (let i = a; i <= b; i++) {
ret.push(`${i}`);
}
}
} else {
ret.push(item);
}
for (let i = a; i <= b; i++) {
ret.push(`${i}`);
}
}
} else {
ret.push(item);
}
});
return [...new Set(ret)];
});
return [...new Set(ret)];
};
export default EpisodeListing;

View file

@ -8,112 +8,112 @@ import ContextMenu from '../../reusable/ContextMenu';
import { useSnackbar } from 'notistack';
const SearchBox: React.FC = () => {
const messageHandler = React.useContext(messageChannelContext);
const [store, dispatch] = useStore();
const [search, setSearch] = React.useState('');
const messageHandler = React.useContext(messageChannelContext);
const [store, dispatch] = useStore();
const [search, setSearch] = React.useState('');
const [focus, setFocus] = React.useState(false);
const [focus, setFocus] = React.useState(false);
const [searchResult, setSearchResult] = React.useState<undefined|SearchResponse>();
const anchor = React.useRef<HTMLDivElement>(null);
const [searchResult, setSearchResult] = React.useState<undefined|SearchResponse>();
const anchor = React.useRef<HTMLDivElement>(null);
const { enqueueSnackbar } = useSnackbar();
const { enqueueSnackbar } = useSnackbar();
const selectItem = (id: string) => {
dispatch({
type: 'downloadOptions',
payload: {
...store.downloadOptions,
id
}
});
};
const selectItem = (id: string) => {
dispatch({
type: 'downloadOptions',
payload: {
...store.downloadOptions,
id
}
});
};
React.useEffect(() => {
if (search.trim().length === 0)
return setSearchResult({ isOk: true, value: [] });
React.useEffect(() => {
if (search.trim().length === 0)
return setSearchResult({ isOk: true, value: [] });
const timeOutId = setTimeout(async () => {
if (search.trim().length > 3) {
const s = await messageHandler?.search({search});
if (s && s.isOk)
s.value = s.value.slice(0, 10);
setSearchResult(s);
}
}, 500);
return () => clearTimeout(timeOutId);
}, [search]);
const timeOutId = setTimeout(async () => {
if (search.trim().length > 3) {
const s = await messageHandler?.search({search});
if (s && s.isOk)
s.value = s.value.slice(0, 10);
setSearchResult(s);
}
}, 500);
return () => clearTimeout(timeOutId);
}, [search]);
const anchorBounding = anchor.current?.getBoundingClientRect();
return <ClickAwayListener onClickAway={() => setFocus(false)}>
<Box sx={{ m: 2 }}>
<TextField ref={anchor} value={search} onClick={() => setFocus(true)} onChange={e => setSearch(e.target.value)} variant='outlined' label='Search' fullWidth />
{searchResult !== undefined && searchResult.isOk && searchResult.value.length > 0 && focus &&
const anchorBounding = anchor.current?.getBoundingClientRect();
return <ClickAwayListener onClickAway={() => setFocus(false)}>
<Box sx={{ m: 2 }}>
<TextField ref={anchor} value={search} onClick={() => setFocus(true)} onChange={e => setSearch(e.target.value)} variant='outlined' label='Search' fullWidth />
{searchResult !== undefined && searchResult.isOk && searchResult.value.length > 0 && focus &&
<Paper sx={{ position: 'fixed', maxHeight: '50%', width: `${anchorBounding?.width}px`,
left: anchorBounding?.x, top: (anchorBounding?.y ?? 0) + (anchorBounding?.height ?? 0), zIndex: 99, overflowY: 'scroll'}}>
<List>
{searchResult && searchResult.isOk ?
searchResult.value.map((a, ind, arr) => {
const imageRef = React.createRef<HTMLImageElement>();
const summaryRef = React.createRef<HTMLParagraphElement>();
return <Box key={a.id}>
<ListItem className='listitem-hover' onClick={() => {
selectItem(a.id);
setFocus(false);
}}>
<Box sx={{ display: 'flex' }}>
<Box sx={{ width: '20%', height: '100%', pr: 2 }}>
<img ref={imageRef} src={a.image} style={{ width: '100%', height: 'auto' }} alt="thumbnail"/>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', maxWidth: '70%' }}>
<Typography variant='h6' component='h6' color='text.primary' sx={{ }}>
{a.name}
</Typography>
{a.desc && <Typography variant='caption' component='p' color='text.primary' sx={{ pt: 1, pb: 1 }} ref={summaryRef}>
{a.desc}
</Typography>}
{a.lang && <Typography variant='caption' component='p' color='text.primary' sx={{ }}>
left: anchorBounding?.x, top: (anchorBounding?.y ?? 0) + (anchorBounding?.height ?? 0), zIndex: 99, overflowY: 'scroll'}}>
<List>
{searchResult && searchResult.isOk ?
searchResult.value.map((a, ind, arr) => {
const imageRef = React.createRef<HTMLImageElement>();
const summaryRef = React.createRef<HTMLParagraphElement>();
return <Box key={a.id}>
<ListItem className='listitem-hover' onClick={() => {
selectItem(a.id);
setFocus(false);
}}>
<Box sx={{ display: 'flex' }}>
<Box sx={{ width: '20%', height: '100%', pr: 2 }}>
<img ref={imageRef} src={a.image} style={{ width: '100%', height: 'auto' }} alt="thumbnail"/>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', maxWidth: '70%' }}>
<Typography variant='h6' component='h6' color='text.primary' sx={{ }}>
{a.name}
</Typography>
{a.desc && <Typography variant='caption' component='p' color='text.primary' sx={{ pt: 1, pb: 1 }} ref={summaryRef}>
{a.desc}
</Typography>}
{a.lang && <Typography variant='caption' component='p' color='text.primary' sx={{ }}>
Languages: {a.lang.join(', ')}
</Typography>}
<Typography variant='caption' component='p' color='text.primary' sx={{ }}>
</Typography>}
<Typography variant='caption' component='p' color='text.primary' sx={{ }}>
ID: {a.id}
</Typography>
</Box>
</Box>
</ListItem>
<ContextMenu options={[ { text: 'Copy image URL', onClick: async () => {
await navigator.clipboard.writeText(a.image);
enqueueSnackbar('Copied URL to clipboard', {
variant: 'info'
});
}},
{
text: 'Open image in new tab',
onClick: () => {
window.open(a.image);
}
} ]} popupItem={imageRef as RefObject<HTMLElement>} />
{a.desc &&
</Typography>
</Box>
</Box>
</ListItem>
<ContextMenu options={[ { text: 'Copy image URL', onClick: async () => {
await navigator.clipboard.writeText(a.image);
enqueueSnackbar('Copied URL to clipboard', {
variant: 'info'
});
}},
{
text: 'Open image in new tab',
onClick: () => {
window.open(a.image);
}
} ]} popupItem={imageRef as RefObject<HTMLElement>} />
{a.desc &&
<ContextMenu options={[
{
onClick: async () => {
await navigator.clipboard.writeText(a.desc!);
enqueueSnackbar('Copied summary to clipboard', {
variant: 'info'
});
},
text: 'Copy summary to clipboard'
}
{
onClick: async () => {
await navigator.clipboard.writeText(a.desc!);
enqueueSnackbar('Copied summary to clipboard', {
variant: 'info'
});
},
text: 'Copy summary to clipboard'
}
]} popupItem={summaryRef as RefObject<HTMLElement>} />
}
{(ind < arr.length - 1) && <Divider />}
</Box>;
})
: <></>}
</List>
}
{(ind < arr.length - 1) && <Divider />}
</Box>;
})
: <></>}
</List>
</Paper>}
</Box>
</ClickAwayListener>;
</Box>
</ClickAwayListener>;
};
export default SearchBox;

View file

@ -6,107 +6,107 @@ import Require from './Require';
import { useSnackbar } from 'notistack';
const AuthButton: React.FC = () => {
const snackbar = useSnackbar();
const snackbar = useSnackbar();
const [open, setOpen] = React.useState(false);
const [open, setOpen] = React.useState(false);
const [username, setUsername] = React.useState('');
const [password, setPassword] = React.useState('');
const [username, setUsername] = React.useState('');
const [password, setPassword] = React.useState('');
const [usernameError, setUsernameError] = React.useState(false);
const [passwordError, setPasswordError] = React.useState(false);
const [usernameError, setUsernameError] = React.useState(false);
const [passwordError, setPasswordError] = React.useState(false);
const messageChannel = React.useContext(messageChannelContext);
const messageChannel = React.useContext(messageChannelContext);
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<Error|undefined>(undefined);
const [authed, setAuthed] = React.useState(false);
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<Error|undefined>(undefined);
const [authed, setAuthed] = React.useState(false);
const checkAuth = async () => {
setAuthed((await messageChannel?.checkToken())?.isOk ?? false);
};
const checkAuth = async () => {
setAuthed((await messageChannel?.checkToken())?.isOk ?? false);
};
React.useEffect(() => { checkAuth(); }, []);
React.useEffect(() => { checkAuth(); }, []);
const handleSubmit = async () => {
if (!messageChannel)
throw new Error('Invalid state'); //The components to confirm only render if the messageChannel is not undefinded
if (username.trim().length === 0)
return setUsernameError(true);
if (password.trim().length === 0)
return setPasswordError(true);
setUsernameError(false);
setPasswordError(false);
setLoading(true);
const handleSubmit = async () => {
if (!messageChannel)
throw new Error('Invalid state'); //The components to confirm only render if the messageChannel is not undefinded
if (username.trim().length === 0)
return setUsernameError(true);
if (password.trim().length === 0)
return setPasswordError(true);
setUsernameError(false);
setPasswordError(false);
setLoading(true);
const res = await messageChannel.auth({ username, password });
if (res.isOk) {
setOpen(false);
snackbar.enqueueSnackbar('Logged in', {
variant: 'success'
});
setUsername('');
setPassword('');
} else {
setError(res.reason);
}
await checkAuth();
setLoading(false);
};
const res = await messageChannel.auth({ username, password });
if (res.isOk) {
setOpen(false);
snackbar.enqueueSnackbar('Logged in', {
variant: 'success'
});
setUsername('');
setPassword('');
} else {
setError(res.reason);
}
await checkAuth();
setLoading(false);
};
return <Require value={messageChannel}>
<Dialog open={open}>
<Dialog open={!!error}>
<DialogTitle>Error during Authentication</DialogTitle>
<DialogContentText>
{error?.name}
{error?.message}
</DialogContentText>
<DialogActions>
<Button onClick={() => setError(undefined)}>Close</Button>
</DialogActions>
</Dialog>
<DialogTitle sx={{ flexGrow: 1 }}>Authentication</DialogTitle>
<DialogContent>
<DialogContentText>
return <Require value={messageChannel}>
<Dialog open={open}>
<Dialog open={!!error}>
<DialogTitle>Error during Authentication</DialogTitle>
<DialogContentText>
{error?.name}
{error?.message}
</DialogContentText>
<DialogActions>
<Button onClick={() => setError(undefined)}>Close</Button>
</DialogActions>
</Dialog>
<DialogTitle sx={{ flexGrow: 1 }}>Authentication</DialogTitle>
<DialogContent>
<DialogContentText>
Here, you need to enter your username (most likely your Email) and your password.<br />
These information are not stored anywhere and are only used to authenticate with the service once.
</DialogContentText>
<TextField
error={usernameError}
helperText={usernameError ? 'Please enter something before submiting' : undefined}
margin="dense"
id="username"
label="Username"
type="text"
fullWidth
variant="standard"
value={username}
onChange={(e) => setUsername(e.target.value)}
disabled={loading}
/>
<TextField
error={passwordError}
helperText={passwordError ? 'Please enter something before submiting' : undefined}
margin="dense"
id="password"
label="Password"
type="password"
fullWidth
variant="standard"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={loading}
/>
</DialogContent>
<DialogActions>
{loading && <CircularProgress size={30}/>}
<Button disabled={loading} onClick={() => setOpen(false)}>Close</Button>
<Button disabled={loading} onClick={() => handleSubmit()}>Authenticate</Button>
</DialogActions>
</Dialog>
<Button startIcon={authed ? <Check />: <Close />} variant="contained" onClick={() => setOpen(true)}>Authenticate</Button>
</Require>;
</DialogContentText>
<TextField
error={usernameError}
helperText={usernameError ? 'Please enter something before submiting' : undefined}
margin="dense"
id="username"
label="Username"
type="text"
fullWidth
variant="standard"
value={username}
onChange={(e) => setUsername(e.target.value)}
disabled={loading}
/>
<TextField
error={passwordError}
helperText={passwordError ? 'Please enter something before submiting' : undefined}
margin="dense"
id="password"
label="Password"
type="password"
fullWidth
variant="standard"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={loading}
/>
</DialogContent>
<DialogActions>
{loading && <CircularProgress size={30}/>}
<Button disabled={loading} onClick={() => setOpen(false)}>Close</Button>
<Button disabled={loading} onClick={() => handleSubmit()}>Authenticate</Button>
</DialogActions>
</Dialog>
<Button startIcon={authed ? <Check />: <Close />} variant="contained" onClick={() => setOpen(true)}>Authenticate</Button>
</Require>;
};
export default AuthButton;

View file

@ -6,31 +6,31 @@ import { messageChannelContext } from '../provider/MessageChannel';
import Require from './Require';
const LogoutButton: React.FC = () => {
const messageChannel = React.useContext(messageChannelContext);
const [, dispatch] = useStore();
const messageChannel = React.useContext(messageChannelContext);
const [, dispatch] = useStore();
const logout = async () => {
if (await messageChannel?.isDownloading())
return alert('You are currently downloading. Please finish the download first.');
if (await messageChannel?.logout())
dispatch({
type: 'service',
payload: undefined
});
else
alert('Unable to change service');
};
const logout = async () => {
if (await messageChannel?.isDownloading())
return alert('You are currently downloading. Please finish the download first.');
if (await messageChannel?.logout())
dispatch({
type: 'service',
payload: undefined
});
else
alert('Unable to change service');
};
return <Require value={messageChannel}>
<Button
startIcon={<ExitToApp />}
variant='contained'
onClick={logout}
sx={{ maxHeight: '2.3rem' }}
>
return <Require value={messageChannel}>
<Button
startIcon={<ExitToApp />}
variant='contained'
onClick={logout}
sx={{ maxHeight: '2.3rem' }}
>
Service select
</Button>
</Require>;
</Button>
</Require>;
};

View file

@ -4,37 +4,37 @@ import { RandomEvent } from '../../../../../../@types/randomEvents';
import { messageChannelContext } from '../../../provider/MessageChannel';
const useDownloadManager = () => {
const messageHandler = React.useContext(messageChannelContext);
const messageHandler = React.useContext(messageChannelContext);
const [progressData, setProgressData] = React.useState<ExtendedProgress|undefined>();
const [current, setCurrent] = React.useState<undefined|QueueItem>();
const [progressData, setProgressData] = React.useState<ExtendedProgress|undefined>();
const [current, setCurrent] = React.useState<undefined|QueueItem>();
React.useEffect(() => {
const handler = (ev: RandomEvent<'progress'>) => {
console.log(ev.data);
setProgressData(ev.data);
};
React.useEffect(() => {
const handler = (ev: RandomEvent<'progress'>) => {
console.log(ev.data);
setProgressData(ev.data);
};
const currentHandler = (ev: RandomEvent<'current'>) => {
setCurrent(ev.data);
};
const currentHandler = (ev: RandomEvent<'current'>) => {
setCurrent(ev.data);
};
const finishHandler = () => {
setProgressData(undefined);
};
const finishHandler = () => {
setProgressData(undefined);
};
messageHandler?.randomEvents.on('progress', handler);
messageHandler?.randomEvents.on('current', currentHandler);
messageHandler?.randomEvents.on('finish', finishHandler);
messageHandler?.randomEvents.on('progress', handler);
messageHandler?.randomEvents.on('current', currentHandler);
messageHandler?.randomEvents.on('finish', finishHandler);
return () => {
messageHandler?.randomEvents.removeListener('progress', handler);
messageHandler?.randomEvents.removeListener('finish', finishHandler);
messageHandler?.randomEvents.removeListener('current', currentHandler);
};
}, [messageHandler]);
return () => {
messageHandler?.randomEvents.removeListener('progress', handler);
messageHandler?.randomEvents.removeListener('finish', finishHandler);
messageHandler?.randomEvents.removeListener('current', currentHandler);
};
}, [messageHandler]);
return { data: progressData, current};
return { data: progressData, current};
};
export default useDownloadManager;

View file

@ -3,9 +3,9 @@ import React from 'react';
import Queue from './Queue/Queue';
const MainFrame: React.FC = () => {
return <Box sx={{ }}>
<Queue />
</Box>;
return <Box sx={{ }}>
<Queue />
</Box>;
};
export default MainFrame;

View file

@ -7,414 +7,414 @@ import DeleteIcon from '@mui/icons-material/Delete';
import useDownloadManager from '../DownloadManager/DownloadManager';
const Queue: React.FC = () => {
const { data, current } = useDownloadManager();
const queue = React.useContext(queueContext);
const msg = React.useContext(messageChannelContext);
const { data, current } = useDownloadManager();
const queue = React.useContext(queueContext);
const msg = React.useContext(messageChannelContext);
if (!msg)
return <>Never</>;
if (!msg)
return <>Never</>;
return data || queue.length > 0 ? <>
{data && <>
<Box sx={{
display: 'flex',
width: '100%',
flexDirection: 'column',
alignItems: 'center',
}}>
<Box sx={{
marginTop: '2rem',
marginBottom: '1rem',
height: '12rem',
width: '93vw',
maxWidth: '93rem',
backgroundColor: '#282828',
boxShadow: '0px 0px 50px #00000090',
borderRadius: '10px',
display: 'flex',
transition: '250ms'
}}>
<img style={{
borderRadius: '5px',
margin: '5px',
boxShadow: '0px 0px 10px #00000090',
userSelect: 'none',
}}
src={data.downloadInfo.image} height='auto' width='auto' alt="Thumbnail" />
<Box
sx={{
display: 'flex',
flexDirection: 'column',
width: '100%',
justifyContent: 'center'
}}>
return data || queue.length > 0 ? <>
{data && <>
<Box sx={{
display: 'flex',
}}>
<Box sx={{
//backgroundColor: '#ff0000',
width: '70%',
marginLeft: '10px'
}}>
<Box sx={{
flexDirection: 'column',
display: 'flex',
justifyContent: 'space-between',
}}>
<Typography color='text.primary' sx={{
fontSize: '1.8rem',
}}>
{data.downloadInfo.parent.title}
</Typography>
<Typography color='text.primary' sx={{
fontSize: '1.2rem',
}}>
{data.downloadInfo.title}
</Typography>
</Box>
</Box>
<Box sx={{
//backgroundColor: '#00ff00',
width: '30%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}>
<Typography color='text.primary' sx={{
fontSize: '1.8rem',
}}>
Downloading: {data.downloadInfo.language.name}
</Typography>
</Box>
</Box>
<Box sx={{
height: '50%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
//backgroundColor: '#0000ff',
}}>
<LinearProgress variant='determinate'
sx={{
height: '20px',
width: '97.53%',
margin: '10px',
boxShadow: '0px 0px 10px #00000090',
borderRadius: '10px',
}} value={(typeof data.progress.percent === 'string' ? parseInt(data.progress.percent) : data.progress.percent)}
/>
<Box>
<Typography color='text.primary'
sx={{
fontSize: '1.3rem',
}}>
{data.progress.cur} / {(data.progress.total)} parts ({data.progress.percent}% | {formatTime(data.progress.time)} | {(data.progress.downloadSpeed / 1024 / 1024).toFixed(2)} MB/s | {(data.progress.bytes / 1024 / 1024).toFixed(2)}MB)
</Typography>
</Box>
</Box>
</Box>
</Box>
</Box>
</>
}
{
current && !data && <>
<Box sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}>
<Box sx={{
marginTop: '2rem',
marginBottom: '1rem',
height: '12rem',
width: '93vw',
maxWidth: '93rem',
backgroundColor: '#282828',
boxShadow: '0px 0px 50px #00000090',
borderRadius: '10px',
display: 'flex',
overflow: 'hidden',
transition: '250ms'
}}>
<img style={{
borderRadius: '5px',
margin: '5px',
boxShadow: '0px 0px 10px #00000090',
userSelect: 'none',
maxWidth: '20.5rem',
}}
src={current.image} height='auto' width='auto' alt="Thumbnail" />
<Box
sx={{
display: 'flex',
flexDirection: 'column',
width: '100%',
justifyContent: 'center',
//backgroundColor: '#ffffff0f'
}}>
<Box sx={{
display: 'flex',
}}>
<Box sx={{
width: '70%',
marginLeft: '10px'
}}>
<Box sx={{
flexDirection: 'column',
display: 'flex',
justifyContent: 'space-between',
}}>
<Typography color='text.primary' sx={{
fontSize: '1.8rem',
}}>
{current.parent.title}
</Typography>
<Typography color='text.primary' sx={{
fontSize: '1.2rem',
}}>
{current.title}
</Typography>
</Box>
</Box>
<Box sx={{
//backgroundColor: '#00ff00',
width: '30%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}>
<Box sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
position: 'relative',
}}>
<Typography color='text.primary' sx={{
fontSize: '1.8rem',
}}>
Downloading:
</Typography>
<CircularProgress variant="indeterminate" sx={{
marginLeft: '2rem',
}}/>
</Box>
</Box>
</Box>
<Box sx={{
height: '50%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
//backgroundColor: '#0000ff',
}}>
<LinearProgress variant='indeterminate'
sx={{
height: '20px',
width: '97.53%',
margin: '10px',
}}>
<Box sx={{
marginTop: '2rem',
marginBottom: '1rem',
height: '12rem',
width: '93vw',
maxWidth: '93rem',
backgroundColor: '#282828',
boxShadow: '0px 0px 50px #00000090',
borderRadius: '10px',
display: 'flex',
transition: '250ms'
}}>
<img style={{
borderRadius: '5px',
margin: '5px',
boxShadow: '0px 0px 10px #00000090',
userSelect: 'none',
}}
src={data.downloadInfo.image} height='auto' width='auto' alt="Thumbnail" />
<Box
sx={{
display: 'flex',
flexDirection: 'column',
width: '100%',
justifyContent: 'center'
}}>
<Box sx={{
display: 'flex',
}}>
<Box sx={{
//backgroundColor: '#ff0000',
width: '70%',
marginLeft: '10px'
}}>
<Box sx={{
flexDirection: 'column',
display: 'flex',
justifyContent: 'space-between',
}}>
<Typography color='text.primary' sx={{
fontSize: '1.8rem',
}}>
{data.downloadInfo.parent.title}
</Typography>
<Typography color='text.primary' sx={{
fontSize: '1.2rem',
}}>
{data.downloadInfo.title}
</Typography>
</Box>
</Box>
<Box sx={{
//backgroundColor: '#00ff00',
width: '30%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}>
<Typography color='text.primary' sx={{
fontSize: '1.8rem',
}}>
Downloading: {data.downloadInfo.language.name}
</Typography>
</Box>
</Box>
<Box sx={{
height: '50%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
//backgroundColor: '#0000ff',
}}>
<LinearProgress variant='determinate'
sx={{
height: '20px',
width: '97.53%',
margin: '10px',
boxShadow: '0px 0px 10px #00000090',
borderRadius: '10px',
}} value={(typeof data.progress.percent === 'string' ? parseInt(data.progress.percent) : data.progress.percent)}
/>
<Box>
<Typography color='text.primary'
sx={{
fontSize: '1.3rem',
}}>
{data.progress.cur} / {(data.progress.total)} parts ({data.progress.percent}% | {formatTime(data.progress.time)} | {(data.progress.downloadSpeed / 1024 / 1024).toFixed(2)} MB/s | {(data.progress.bytes / 1024 / 1024).toFixed(2)}MB)
</Typography>
</Box>
</Box>
</Box>
</Box>
</Box>
</>
}
{
current && !data && <>
<Box sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}>
<Box sx={{
marginTop: '2rem',
marginBottom: '1rem',
height: '12rem',
width: '93vw',
maxWidth: '93rem',
backgroundColor: '#282828',
boxShadow: '0px 0px 50px #00000090',
borderRadius: '10px',
display: 'flex',
overflow: 'hidden',
transition: '250ms'
}}>
<img style={{
borderRadius: '5px',
margin: '5px',
boxShadow: '0px 0px 10px #00000090',
userSelect: 'none',
maxWidth: '20.5rem',
}}
src={current.image} height='auto' width='auto' alt="Thumbnail" />
<Box
sx={{
display: 'flex',
flexDirection: 'column',
width: '100%',
justifyContent: 'center',
//backgroundColor: '#ffffff0f'
}}>
<Box sx={{
display: 'flex',
}}>
<Box sx={{
width: '70%',
marginLeft: '10px'
}}>
<Box sx={{
flexDirection: 'column',
display: 'flex',
justifyContent: 'space-between',
}}>
<Typography color='text.primary' sx={{
fontSize: '1.8rem',
}}>
{current.parent.title}
</Typography>
<Typography color='text.primary' sx={{
fontSize: '1.2rem',
}}>
{current.title}
</Typography>
</Box>
</Box>
<Box sx={{
//backgroundColor: '#00ff00',
width: '30%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}>
<Box sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
position: 'relative',
}}>
<Typography color='text.primary' sx={{
fontSize: '1.8rem',
}}>
Downloading:
</Typography>
<CircularProgress variant="indeterminate" sx={{
marginLeft: '2rem',
}}/>
</Box>
</Box>
</Box>
<Box sx={{
height: '50%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
//backgroundColor: '#0000ff',
}}>
<LinearProgress variant='indeterminate'
sx={{
height: '20px',
width: '97.53%',
margin: '10px',
boxShadow: '0px 0px 10px #00000090',
borderRadius: '10px',
}}
/>
<Box>
<Typography color='text.primary'
sx={{
fontSize: '1.3rem',
}}>
0 / ? parts (0% | XX:XX | 0 MB/s | 0MB)
</Typography>
</Box>
</Box>
</Box>
</Box>
</Box>
</>
}
{queue.map((queueItem, index, { length }) => {
return <Box key={`queue_item_${index}`} sx={{
display: 'flex',
mb: '-1.5rem',
flexDirection: 'column',
alignItems: 'center',
}}>
<Box sx={{
marginTop: '1.5rem',
marginBottom: '1.5rem',
height: '11rem',
width: '90vw',
maxWidth: '90rem',
backgroundColor: '#282828',
boxShadow: '0px 0px 10px #00000090',
borderRadius: '10px',
}}
/>
<Box>
<Typography color='text.primary'
sx={{
fontSize: '1.3rem',
display: 'flex',
overflow: 'hidden',
}}>
<img style={{
borderRadius: '5px',
margin: '5px',
boxShadow: '0px 0px 5px #00000090',
userSelect: 'none',
maxWidth: '18.5rem'
}}
src={queueItem.image} height='auto' width='auto' alt="Thumbnail" />
<Box sx={{
margin: '5px',
display: 'flex',
width: '100%',
justifyContent: 'space-between',
}}>
0 / ? parts (0% | XX:XX | 0 MB/s | 0MB)
</Typography>
<Box sx={{
width: '30%',
marginRight: '5px',
marginLeft: '5px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}>
<Typography color='text.primary' sx={{
fontSize: '1.8rem',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}>
{queueItem.parent.title}
</Typography>
<Typography color='text.primary' sx={{
fontSize: '1.6rem',
marginTop: '-0.4rem',
marginBottom: '0.4rem',
}}>
S{queueItem.parent.season}E{queueItem.episode}
</Typography>
<Typography color='text.primary' sx={{
fontSize: '1.2rem',
marginTop: '-0.4rem',
marginBottom: '0.4rem',
textOverflow: 'ellipsis',
}}>
{queueItem.title}
</Typography>
</Box>
<Box sx={{
width: '40%',
marginRight: '5px',
marginLeft: '5px',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
whiteSpace: 'nowrap',
justifyContent: 'space-between',
}}>
<Typography color='text.primary' sx={{
fontSize: '1.8rem',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}>
Dub(s): {queueItem.dubLang.join(', ')}
</Typography>
<Typography color='text.primary' sx={{
fontSize: '1.8rem',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}>
Sub(s): {queueItem.dlsubs.join(', ')}
</Typography>
<Typography color='text.primary' sx={{
fontSize: '1.8rem',
}}>
Quality: {queueItem.q}
</Typography>
</Box>
<Box sx={{
marginRight: '5px',
marginLeft: '5px',
width: '30%',
justifyContent: 'center',
alignItems: 'center',
display: 'flex'
}}>
<Tooltip title="Delete from queue" arrow placement='top'>
<IconButton
onClick={() => {
msg.removeFromQueue(index);
}}
sx={{
backgroundColor: '#ff573a25',
height: '40px',
transition: '250ms',
'&:hover' : {
backgroundColor: '#ff573a',
}
}}>
<DeleteIcon />
</IconButton>
</Tooltip>
</Box>
</Box>
</Box>
</Box>
</Box>
</Box>
</Box>
</>
}
{queue.map((queueItem, index, { length }) => {
return <Box key={`queue_item_${index}`} sx={{
;
})}
</> : <Box sx={{
display: 'flex',
mb: '-1.5rem',
width: '100%',
height: '12rem',
flexDirection: 'column',
alignItems: 'center',
}}>
<Box sx={{
marginTop: '1.5rem',
marginBottom: '1.5rem',
height: '11rem',
width: '90vw',
maxWidth: '90rem',
backgroundColor: '#282828',
boxShadow: '0px 0px 10px #00000090',
borderRadius: '10px',
display: 'flex',
overflow: 'hidden',
}}>
<Typography color='text.primary' sx={{
fontSize: '2rem',
margin: '10px'
}}>
<img style={{
borderRadius: '5px',
margin: '5px',
boxShadow: '0px 0px 5px #00000090',
userSelect: 'none',
maxWidth: '18.5rem'
}}
src={queueItem.image} height='auto' width='auto' alt="Thumbnail" />
<Box sx={{
margin: '5px',
display: 'flex',
width: '100%',
justifyContent: 'space-between',
}}>
<Box sx={{
width: '30%',
marginRight: '5px',
marginLeft: '5px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}>
<Typography color='text.primary' sx={{
fontSize: '1.8rem',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}>
{queueItem.parent.title}
</Typography>
<Typography color='text.primary' sx={{
fontSize: '1.6rem',
marginTop: '-0.4rem',
marginBottom: '0.4rem',
}}>
S{queueItem.parent.season}E{queueItem.episode}
</Typography>
<Typography color='text.primary' sx={{
fontSize: '1.2rem',
marginTop: '-0.4rem',
marginBottom: '0.4rem',
textOverflow: 'ellipsis',
}}>
{queueItem.title}
</Typography>
</Box>
<Box sx={{
width: '40%',
marginRight: '5px',
marginLeft: '5px',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
whiteSpace: 'nowrap',
justifyContent: 'space-between',
}}>
<Typography color='text.primary' sx={{
fontSize: '1.8rem',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}>
Dub(s): {queueItem.dubLang.join(', ')}
</Typography>
<Typography color='text.primary' sx={{
fontSize: '1.8rem',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}>
Sub(s): {queueItem.dlsubs.join(', ')}
</Typography>
<Typography color='text.primary' sx={{
fontSize: '1.8rem',
}}>
Quality: {queueItem.q}
</Typography>
</Box>
<Box sx={{
marginRight: '5px',
marginLeft: '5px',
width: '30%',
justifyContent: 'center',
alignItems: 'center',
display: 'flex'
}}>
<Tooltip title="Delete from queue" arrow placement='top'>
<IconButton
onClick={() => {
msg.removeFromQueue(index);
}}
sx={{
backgroundColor: '#ff573a25',
height: '40px',
transition: '250ms',
'&:hover' : {
backgroundColor: '#ff573a',
}
}}>
<DeleteIcon />
</IconButton>
</Tooltip>
</Box>
</Box>
</Box>
</Box>
;
})}
</> : <Box sx={{
display: 'flex',
width: '100%',
height: '12rem',
flexDirection: 'column',
alignItems: 'center',
}}>
<Typography color='text.primary' sx={{
fontSize: '2rem',
margin: '10px'
}}>
Selected episodes will be shown here
</Typography>
<Box sx={{
display: 'flex',
margin: '10px'
}}>
<Skeleton variant='rectangular' height={'10rem'} width={'20rem'} sx={{ margin: '5px', borderRadius: '5px' }}/>
<Box sx={{
display: 'flex',
flexDirection: 'column',
}}>
<Skeleton variant='text' height={'100%'} width={'30rem'} sx={{ margin: '5px', borderRadius: '5px' }}/>
<Skeleton variant='text' height={'100%'} width={'30rem'} sx={{ margin: '5px', borderRadius: '5px' }}/>
</Box>
</Box>
<Box sx={{
display: 'flex',
margin: '10px'
}}>
<Skeleton variant='rectangular' height={'10rem'} width={'20rem'} sx={{ margin: '5px', borderRadius: '5px' }}/>
<Box sx={{
display: 'flex',
flexDirection: 'column',
}}>
<Skeleton variant='text' height={'100%'} width={'30rem'} sx={{ margin: '5px', borderRadius: '5px' }}/>
<Skeleton variant='text' height={'100%'} width={'30rem'} sx={{ margin: '5px', borderRadius: '5px' }}/>
</Box>
</Box>
</Box>;
</Typography>
<Box sx={{
display: 'flex',
margin: '10px'
}}>
<Skeleton variant='rectangular' height={'10rem'} width={'20rem'} sx={{ margin: '5px', borderRadius: '5px' }}/>
<Box sx={{
display: 'flex',
flexDirection: 'column',
}}>
<Skeleton variant='text' height={'100%'} width={'30rem'} sx={{ margin: '5px', borderRadius: '5px' }}/>
<Skeleton variant='text' height={'100%'} width={'30rem'} sx={{ margin: '5px', borderRadius: '5px' }}/>
</Box>
</Box>
<Box sx={{
display: 'flex',
margin: '10px'
}}>
<Skeleton variant='rectangular' height={'10rem'} width={'20rem'} sx={{ margin: '5px', borderRadius: '5px' }}/>
<Box sx={{
display: 'flex',
flexDirection: 'column',
}}>
<Skeleton variant='text' height={'100%'} width={'30rem'} sx={{ margin: '5px', borderRadius: '5px' }}/>
<Skeleton variant='text' height={'100%'} width={'30rem'} sx={{ margin: '5px', borderRadius: '5px' }}/>
</Box>
</Box>
</Box>;
};
const formatTime = (time: number) => {
time = Math.floor(time / 1000);
const minutes = Math.floor(time / 60);
time = time % 60;
time = Math.floor(time / 1000);
const minutes = Math.floor(time / 60);
time = time % 60;
return `${minutes.toFixed(0).length < 2 ? `0${minutes}` : minutes}m${time.toFixed(0).length < 2 ? `0${time}` : time}s`;
return `${minutes.toFixed(0).length < 2 ? `0${minutes}` : minutes}m${time.toFixed(0).length < 2 ? `0${time}` : time}s`;
};
export default Queue;

View file

@ -5,120 +5,120 @@ import useStore from '../../hooks/useStore';
import { StoreState } from '../../provider/Store';
const MenuBar: React.FC = () => {
const [ openMenu, setMenuOpen ] = React.useState<'settings'|'help'|undefined>();
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const [store, dispatch] = useStore();
const [ openMenu, setMenuOpen ] = React.useState<'settings'|'help'|undefined>();
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const [store, dispatch] = useStore();
const messageChannel = React.useContext(messageChannelContext);
const messageChannel = React.useContext(messageChannelContext);
React.useEffect(() => {
(async () => {
if (!messageChannel || store.version !== '')
return;
dispatch({
type: 'version',
payload: await messageChannel.version()
});
})();
}, [messageChannel]);
React.useEffect(() => {
(async () => {
if (!messageChannel || store.version !== '')
return;
dispatch({
type: 'version',
payload: await messageChannel.version()
});
})();
}, [messageChannel]);
const transformService = (service: StoreState['service']) => {
switch(service) {
case 'crunchy':
return 'Crunchyroll';
case 'hidive':
return 'Hidive';
case 'ao':
return 'AnimeOnegai';
case 'adn':
return 'AnimationDigitalNetwork';
}
};
const transformService = (service: StoreState['service']) => {
switch(service) {
case 'crunchy':
return 'Crunchyroll';
case 'hidive':
return 'Hidive';
case 'ao':
return 'AnimeOnegai';
case 'adn':
return 'AnimationDigitalNetwork';
}
};
const msg = React.useContext(messageChannelContext);
const msg = React.useContext(messageChannelContext);
const handleClick = (event: React.MouseEvent<HTMLElement>, n: 'settings'|'help') => {
setAnchorEl(event.currentTarget);
setMenuOpen(n);
};
const handleClose = () => {
setAnchorEl(null);
setMenuOpen(undefined);
};
const handleClick = (event: React.MouseEvent<HTMLElement>, n: 'settings'|'help') => {
setAnchorEl(event.currentTarget);
setMenuOpen(n);
};
const handleClose = () => {
setAnchorEl(null);
setMenuOpen(undefined);
};
if (!msg)
return <></>;
if (!msg)
return <></>;
return <Box sx={{ display: 'flex', marginBottom: '1rem', width: '100%', alignItems: 'center' }}>
<Box sx={{ position: 'relative', left: '0%', width: '50%'}}>
<Button onClick={(e) => handleClick(e, 'settings')}>
return <Box sx={{ display: 'flex', marginBottom: '1rem', width: '100%', alignItems: 'center' }}>
<Box sx={{ position: 'relative', left: '0%', width: '50%'}}>
<Button onClick={(e) => handleClick(e, 'settings')}>
Settings
</Button>
<Button onClick={(e) => handleClick(e, 'help')}>
</Button>
<Button onClick={(e) => handleClick(e, 'help')}>
Help
</Button>
</Box>
<Menu open={openMenu === 'settings'} anchorEl={anchorEl} onClose={handleClose}>
<MenuItem onClick={() => {
msg.openFolder('config');
handleClose();
}}>
</Button>
</Box>
<Menu open={openMenu === 'settings'} anchorEl={anchorEl} onClose={handleClose}>
<MenuItem onClick={() => {
msg.openFolder('config');
handleClose();
}}>
Open settings folder
</MenuItem>
<MenuItem onClick={() => {
msg.openFile(['config', 'bin-path.yml']);
handleClose();
}}>
</MenuItem>
<MenuItem onClick={() => {
msg.openFile(['config', 'bin-path.yml']);
handleClose();
}}>
Open FFmpeg/Mkvmerge file
</MenuItem>
<MenuItem onClick={() => {
msg.openFile(['config', 'cli-defaults.yml']);
handleClose();
}}>
</MenuItem>
<MenuItem onClick={() => {
msg.openFile(['config', 'cli-defaults.yml']);
handleClose();
}}>
Open advanced options
</MenuItem>
<MenuItem onClick={() => {
msg.openFolder('content');
handleClose();
}}>
</MenuItem>
<MenuItem onClick={() => {
msg.openFolder('content');
handleClose();
}}>
Open output path
</MenuItem>
</Menu>
<Menu open={openMenu === 'help'} anchorEl={anchorEl} onClose={handleClose}>
<MenuItem onClick={() => {
msg.openURL('https://github.com/anidl/multi-downloader-nx');
handleClose();
}}>
</MenuItem>
</Menu>
<Menu open={openMenu === 'help'} anchorEl={anchorEl} onClose={handleClose}>
<MenuItem onClick={() => {
msg.openURL('https://github.com/anidl/multi-downloader-nx');
handleClose();
}}>
GitHub
</MenuItem>
<MenuItem onClick={() => {
msg.openURL('https://github.com/anidl/multi-downloader-nx/issues/new?assignees=AnimeDL,AnidlSupport&labels=bug&template=bug.yml&title=BUG');
handleClose();
}}>
</MenuItem>
<MenuItem onClick={() => {
msg.openURL('https://github.com/anidl/multi-downloader-nx/issues/new?assignees=AnimeDL,AnidlSupport&labels=bug&template=bug.yml&title=BUG');
handleClose();
}}>
Report a bug
</MenuItem>
<MenuItem onClick={() => {
msg.openURL('https://github.com/anidl/multi-downloader-nx/graphs/contributors');
handleClose();
}}>
</MenuItem>
<MenuItem onClick={() => {
msg.openURL('https://github.com/anidl/multi-downloader-nx/graphs/contributors');
handleClose();
}}>
Contributors
</MenuItem>
<MenuItem onClick={() => {
msg.openURL('https://discord.gg/qEpbWen5vq');
handleClose();
}}>
</MenuItem>
<MenuItem onClick={() => {
msg.openURL('https://discord.gg/qEpbWen5vq');
handleClose();
}}>
Discord
</MenuItem>
<MenuItem onClick={() => {
handleClose();
}}>
</MenuItem>
<MenuItem onClick={() => {
handleClose();
}}>
Version: {store.version}
</MenuItem>
</Menu>
<Typography variant="h5" color="text.primary">
{transformService(store.service)}
</Typography>
</Box>;
</MenuItem>
</Menu>
<Typography variant="h5" color="text.primary">
{transformService(store.service)}
</Typography>
</Box>;
};
export default MenuBar;

View file

@ -6,9 +6,9 @@ export type RequireType<T> = {
}
const Require = <T, >(props: React.PropsWithChildren<RequireType<T>>) => {
return props.value === undefined ? <Backdrop open>
<CircularProgress />
</Backdrop> : <Box>{props.children}</Box>;
return props.value === undefined ? <Backdrop open>
<CircularProgress />
</Backdrop> : <Box>{props.children}</Box>;
};
export default Require;

View file

@ -5,37 +5,37 @@ import { messageChannelContext } from '../provider/MessageChannel';
import Require from './Require';
const StartQueueButton: React.FC = () => {
const messageChannel = React.useContext(messageChannelContext);
const [start, setStart] = React.useState(false);
const msg = React.useContext(messageChannelContext);
const messageChannel = React.useContext(messageChannelContext);
const [start, setStart] = React.useState(false);
const msg = React.useContext(messageChannelContext);
React.useEffect(() => {
(async () => {
if (!msg)
return alert('Invalid state: msg not found');
setStart(await msg.getDownloadQueue());
})();
}, []);
React.useEffect(() => {
(async () => {
if (!msg)
return alert('Invalid state: msg not found');
setStart(await msg.getDownloadQueue());
})();
}, []);
const change = async () => {
if (await messageChannel?.isDownloading())
alert('The current download will be finished before the queue stops');
msg?.setDownloadQueue(!start);
setStart(!start);
};
const change = async () => {
if (await messageChannel?.isDownloading())
alert('The current download will be finished before the queue stops');
msg?.setDownloadQueue(!start);
setStart(!start);
};
return <Require value={messageChannel}>
<Button
startIcon={start ? <PauseCircleFilled /> : <PlayCircleFilled /> }
variant='contained'
onClick={change}
sx={{ maxHeight: '2.3rem' }}
>
{
start ? 'Stop Queue' : 'Start Queue'
}
</Button>
</Require>;
return <Require value={messageChannel}>
<Button
startIcon={start ? <PauseCircleFilled /> : <PlayCircleFilled /> }
variant='contained'
onClick={change}
sx={{ maxHeight: '2.3rem' }}
>
{
start ? 'Stop Queue' : 'Start Queue'
}
</Button>
</Require>;
};

View file

@ -12,54 +12,54 @@ export type ContextMenuProps<T extends HTMLElement> = {
}
const buttonSx: SxProps = {
'&:hover': {
background: 'rgb(0, 30, 60)'
},
fontSize: '0.7rem',
minHeight: '30px',
justifyContent: 'center',
p: 0
'&:hover': {
background: 'rgb(0, 30, 60)'
},
fontSize: '0.7rem',
minHeight: '30px',
justifyContent: 'center',
p: 0
};
function ContextMenu<T extends HTMLElement, >(props: ContextMenuProps<T>) {
const [anchor, setAnchor] = React.useState( { x: 0, y: 0 } );
const [anchor, setAnchor] = React.useState( { x: 0, y: 0 } );
const [show, setShow] = React.useState(false);
const [show, setShow] = React.useState(false);
React.useEffect(() => {
const { popupItem: ref } = props;
if (ref.current === null)
return;
const listener = (ev: MouseEvent) => {
ev.preventDefault();
setAnchor({ x: ev.x + 10, y: ev.y + 10 });
setShow(true);
};
ref.current.addEventListener('contextmenu', listener);
React.useEffect(() => {
const { popupItem: ref } = props;
if (ref.current === null)
return;
const listener = (ev: MouseEvent) => {
ev.preventDefault();
setAnchor({ x: ev.x + 10, y: ev.y + 10 });
setShow(true);
};
ref.current.addEventListener('contextmenu', listener);
return () => {
if (ref.current)
ref.current.removeEventListener('contextmenu', listener);
};
}, [ props.popupItem ]);
return () => {
if (ref.current)
ref.current.removeEventListener('contextmenu', listener);
};
}, [ props.popupItem ]);
return show ? <Box sx={{ zIndex: 1400, p: 1, background: 'rgba(0, 0, 0, 0.75)', backdropFilter: 'blur(5px)', position: 'fixed', left: anchor.x, top: anchor.y }}>
<List sx={{ p: 0, m: 0, display: 'flex', flexDirection: 'column' }}>
{props.options.map((item, i) => {
return item === 'divider' ? <Divider key={`ContextMenu_Divider_${i}_${item}`}/> :
<Button color='inherit' key={`ContextMenu_Value_${i}_${item}`} onClick={() => {
item.onClick();
setShow(false);
}} sx={buttonSx}>
{item.text}
</Button>;
})}
<Divider />
<Button fullWidth color='inherit' onClick={() => setShow(false)} sx={buttonSx} >
return show ? <Box sx={{ zIndex: 1400, p: 1, background: 'rgba(0, 0, 0, 0.75)', backdropFilter: 'blur(5px)', position: 'fixed', left: anchor.x, top: anchor.y }}>
<List sx={{ p: 0, m: 0, display: 'flex', flexDirection: 'column' }}>
{props.options.map((item, i) => {
return item === 'divider' ? <Divider key={`ContextMenu_Divider_${i}_${item}`}/> :
<Button color='inherit' key={`ContextMenu_Value_${i}_${item}`} onClick={() => {
item.onClick();
setShow(false);
}} sx={buttonSx}>
{item.text}
</Button>;
})}
<Divider />
<Button fullWidth color='inherit' onClick={() => setShow(false)} sx={buttonSx} >
Close
</Button>
</List>
</Box> : <></>;
</Button>
</List>
</Box> : <></>;
}
export default ContextMenu;

View file

@ -7,18 +7,18 @@ import React from 'react';
export type LinearProgressWithLabelProps = LinearProgressProps & { value: number };
const LinearProgressWithLabel: React.FC<LinearProgressWithLabelProps> = (props) => {
return (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Box sx={{ width: '100%', mr: 1 }}>
<LinearProgress variant="determinate" {...props} />
</Box>
<Box sx={{ minWidth: 35 }}>
<Typography variant="body2" color="text.secondary">{`${Math.round(
props.value,
)}%`}</Typography>
</Box>
</Box>
);
return (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Box sx={{ width: '100%', mr: 1 }}>
<LinearProgress variant="determinate" {...props} />
</Box>
<Box sx={{ minWidth: 35 }}>
<Typography variant="body2" color="text.secondary">{`${Math.round(
props.value,
)}%`}</Typography>
</Box>
</Box>
);
};
export default LinearProgressWithLabel;

View file

@ -12,63 +12,63 @@ export type MultiSelectProps = {
const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8;
const MenuProps = {
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 250
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 250
}
}
}
};
function getStyles(name: string, personName: readonly string[], theme: Theme) {
return {
fontWeight:
return {
fontWeight:
(personName ?? []).indexOf(name) === -1
? theme.typography.fontWeightRegular
: theme.typography.fontWeightMedium
};
? theme.typography.fontWeightRegular
: theme.typography.fontWeightMedium
};
}
const MultiSelect: React.FC<MultiSelectProps> = (props) => {
const theme = useTheme();
const theme = useTheme();
return <div>
<FormControl sx={{ width: 300 }}>
<InputLabel id="multi-select-label">{props.title}</InputLabel>
<Select
labelId="multi-select-label"
id="multi-select"
multiple
value={(props.selected ?? [])}
onChange={e => {
const val = typeof e.target.value === 'string' ? e.target.value.split(',') : e.target.value;
if (props.allOption && val.includes('all')) {
if (props.values.length === val.length - 1)
props.onChange([]);
else
props.onChange(props.values);
} else {
props.onChange(val);
}
}}
input={<OutlinedInput id="select-multiple-chip" label={props.title} />}
renderValue={(selected) => (
selected.join(', ')
)}
MenuProps={MenuProps}
>
{props.values.concat(props.allOption ? 'all' : []).map((name) => (
<MenuItem
key={`${props.title}_${name}`}
value={name}
style={getStyles(name, props.selected, theme)}
>
{name}
</MenuItem>
))}
</Select>
</FormControl>
</div>;
return <div>
<FormControl sx={{ width: 300 }}>
<InputLabel id="multi-select-label">{props.title}</InputLabel>
<Select
labelId="multi-select-label"
id="multi-select"
multiple
value={(props.selected ?? [])}
onChange={e => {
const val = typeof e.target.value === 'string' ? e.target.value.split(',') : e.target.value;
if (props.allOption && val.includes('all')) {
if (props.values.length === val.length - 1)
props.onChange([]);
else
props.onChange(props.values);
} else {
props.onChange(val);
}
}}
input={<OutlinedInput id="select-multiple-chip" label={props.title} />}
renderValue={(selected) => (
selected.join(', ')
)}
MenuProps={MenuProps}
>
{props.values.concat(props.allOption ? 'all' : []).map((name) => (
<MenuItem
key={`${props.title}_${name}`}
value={name}
style={getStyles(name, props.selected, theme)}
>
{name}
</MenuItem>
))}
</Select>
</FormControl>
</div>;
};
export default MultiSelect;

View file

@ -2,11 +2,11 @@ import React from 'react';
import { StoreAction, StoreContext, StoreState } from '../provider/Store';
const useStore = () => {
const context = React.useContext(StoreContext as unknown as React.Context<[StoreState, React.Dispatch<StoreAction<keyof StoreState>>]>);
if (!context) {
throw new Error('useStore must be used under Store');
}
return context;
const context = React.useContext(StoreContext as unknown as React.Context<[StoreState, React.Dispatch<StoreAction<keyof StoreState>>]>);
if (!context) {
throw new Error('useStore must be used under Store');
}
return context;
};
export default useStore;

View file

@ -17,35 +17,35 @@ document.body.style.justifyContent = 'center';
const notistackRef = React.createRef<SnackbarProvider>();
const onClickDismiss = (key: SnackbarKey | undefined) => () => {
if (notistackRef.current)
notistackRef.current.closeSnackbar(key);
if (notistackRef.current)
notistackRef.current.closeSnackbar(key);
};
const container = document.getElementById('root');
const root = createRoot(container as HTMLElement);
root.render(
<ErrorHandler>
<Store>
<SnackbarProvider
ref={notistackRef}
action={(key) => (
<IconButton onClick={onClickDismiss(key)} color="inherit">
<CloseOutlined />
</IconButton>
)}
>
<Style>
<MessageChannel>
<ServiceProvider>
<QueueProvider>
<Box>
<App />
</Box>
</QueueProvider>
</ServiceProvider>
</MessageChannel>
</Style>
</SnackbarProvider>
</Store>
</ErrorHandler>
<ErrorHandler>
<Store>
<SnackbarProvider
ref={notistackRef}
action={(key) => (
<IconButton onClick={onClickDismiss(key)} color="inherit">
<CloseOutlined />
</IconButton>
)}
>
<Style>
<MessageChannel>
<ServiceProvider>
<QueueProvider>
<Box>
<App />
</Box>
</QueueProvider>
</ServiceProvider>
</MessageChannel>
</Style>
</SnackbarProvider>
</Store>
</ErrorHandler>
);

View file

@ -10,30 +10,30 @@ export default class ErrorHandler extends React.Component<{
}
}> {
constructor(props: {
constructor(props: {
children: React.ReactNode|React.ReactNode[]
}) {
super(props);
this.state = { error: undefined };
}
super(props);
this.state = { error: undefined };
}
componentDidCatch(er: Error, stack: React.ErrorInfo) {
this.setState({ error: { er, stack } });
}
componentDidCatch(er: Error, stack: React.ErrorInfo) {
this.setState({ error: { er, stack } });
}
render(): React.ReactNode {
return this.state.error ?
<Box sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', p: 2 }}>
<Typography variant='body1' color='red'>
{`${this.state.error.er.name}: ${this.state.error.er.message}`}
<br/>
{this.state.error.stack.componentStack?.split('\n').map(a => {
return <>
{a}
<br/>
</>;
})}
</Typography>
</Box> : this.props.children;
}
render(): React.ReactNode {
return this.state.error ?
<Box sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', p: 2 }}>
<Typography variant='body1' color='red'>
{`${this.state.error.er.name}: ${this.state.error.er.message}`}
<br/>
{this.state.error.stack.componentStack?.split('\n').map(a => {
return <>
{a}
<br/>
</>;
})}
</Typography>
</Box> : this.props.children;
}
}

View file

@ -12,233 +12,233 @@ import { GUIConfig } from '../../../../modules/module.cfg-loader';
export type FrontEndMessages = (MessageHandler & { randomEvents: RandomEventHandler, logout: () => Promise<boolean> });
export class RandomEventHandler {
private handler: {
private handler: {
[eventName in keyof RandomEvents]: Handler<eventName>[]
} = {
progress: [],
finish: [],
queueChange: [],
current: []
};
progress: [],
finish: [],
queueChange: [],
current: []
};
public on<T extends keyof RandomEvents>(name: T, listener: Handler<T>) {
if (Object.prototype.hasOwnProperty.call(this.handler, name)) {
this.handler[name].push(listener as any);
} else {
this.handler[name] = [ listener as any ];
public on<T extends keyof RandomEvents>(name: T, listener: Handler<T>) {
if (Object.prototype.hasOwnProperty.call(this.handler, name)) {
this.handler[name].push(listener as any);
} else {
this.handler[name] = [ listener as any ];
}
}
}
public emit<T extends keyof RandomEvents>(name: keyof RandomEvents, data: RandomEvent<T>) {
(this.handler[name] ?? []).forEach(handler => handler(data as any));
}
public emit<T extends keyof RandomEvents>(name: keyof RandomEvents, data: RandomEvent<T>) {
(this.handler[name] ?? []).forEach(handler => handler(data as any));
}
public removeListener<T extends keyof RandomEvents>(name: T, listener: Handler<T>) {
this.handler[name] = (this.handler[name] as Handler<T>[]).filter(a => a !== listener) as any;
}
public removeListener<T extends keyof RandomEvents>(name: T, listener: Handler<T>) {
this.handler[name] = (this.handler[name] as Handler<T>[]).filter(a => a !== listener) as any;
}
}
export const messageChannelContext = React.createContext<FrontEndMessages|undefined>(undefined);
async function messageAndResponse<T extends keyof MessageTypes>(socket: WebSocket, msg: WSMessage<T>): Promise<WSMessage<T, 1>> {
const id = v4();
const ret = new Promise<WSMessage<T, 1>>((resolve) => {
const handler = function({ data }: MessageEvent) {
const parsed = JSON.parse(data.toString()) as WSMessageWithID<T, 1>;
if (parsed.id === id) {
socket.removeEventListener('message', handler);
resolve(parsed);
}
};
socket.addEventListener('message', handler);
});
const toSend = msg as WSMessageWithID<T>;
toSend.id = id;
const id = v4();
const ret = new Promise<WSMessage<T, 1>>((resolve) => {
const handler = function({ data }: MessageEvent) {
const parsed = JSON.parse(data.toString()) as WSMessageWithID<T, 1>;
if (parsed.id === id) {
socket.removeEventListener('message', handler);
resolve(parsed);
}
};
socket.addEventListener('message', handler);
});
const toSend = msg as WSMessageWithID<T>;
toSend.id = id;
socket.send(JSON.stringify(toSend));
return ret;
socket.send(JSON.stringify(toSend));
return ret;
}
const MessageChannelProvider: FCWithChildren = ({ children }) => {
const [store, dispatch] = useStore();
const [socket, setSocket] = React.useState<undefined|WebSocket>();
const [publicWS, setPublicWS] = React.useState<undefined|WebSocket>();
const [usePassword, setUsePassword] = React.useState<'waiting'|'yes'|'no'>('waiting');
const [isSetup, setIsSetup] = React.useState<'waiting'|'yes'|'no'>('waiting');
const [store, dispatch] = useStore();
const [socket, setSocket] = React.useState<undefined|WebSocket>();
const [publicWS, setPublicWS] = React.useState<undefined|WebSocket>();
const [usePassword, setUsePassword] = React.useState<'waiting'|'yes'|'no'>('waiting');
const [isSetup, setIsSetup] = React.useState<'waiting'|'yes'|'no'>('waiting');
const { enqueueSnackbar } = useSnackbar();
const { enqueueSnackbar } = useSnackbar();
React.useEffect(() => {
const wss = new WebSocket(`${location.protocol == 'https:' ? 'wss' : 'ws'}://${process.env.NODE_ENV === 'development' ? 'localhost:3000' : window.location.host}/public`);
wss.addEventListener('open', () => {
setPublicWS(wss);
});
wss.addEventListener('error', () => {
enqueueSnackbar('Unable to connect to server. Please reload the page to try again.', { variant: 'error' });
});
}, []);
React.useEffect(() => {
(async () => {
if (!publicWS)
return;
setUsePassword((await messageAndResponse(publicWS, { name: 'requirePassword', data: undefined })).data ? 'yes' : 'no');
setIsSetup((await messageAndResponse(publicWS, { name: 'isSetup', data: undefined })).data ? 'yes' : 'no');
})();
}, [publicWS]);
const connect = (ev?: React.FormEvent<HTMLFormElement>) => {
let search = new URLSearchParams();
if (ev) {
ev.preventDefault();
const formData = new FormData(ev.currentTarget);
const password = formData.get('password')?.toString();
if (!password)
return enqueueSnackbar('Please provide both a username and password', {
variant: 'error'
React.useEffect(() => {
const wss = new WebSocket(`${location.protocol == 'https:' ? 'wss' : 'ws'}://${process.env.NODE_ENV === 'development' ? 'localhost:3000' : window.location.host}/public`);
wss.addEventListener('open', () => {
setPublicWS(wss);
});
search = new URLSearchParams({
password
});
}
wss.addEventListener('error', () => {
enqueueSnackbar('Unable to connect to server. Please reload the page to try again.', { variant: 'error' });
});
}, []);
const wws = new WebSocket(`${location.protocol == 'https:' ? 'wss' : 'ws'}://${process.env.NODE_ENV === 'development' ? 'localhost:3000' : window.location.host}/private?${search}`, );
wws.addEventListener('open', () => {
console.log('[INFO] [WS] Connected');
setSocket(wws);
});
wws.addEventListener('error', (er) => {
console.error('[ERROR] [WS]', er);
enqueueSnackbar('Unable to connect to server. Please check the password and try again.', {
variant: 'error'
});
});
};
React.useEffect(() => {
(async () => {
if (!publicWS)
return;
setUsePassword((await messageAndResponse(publicWS, { name: 'requirePassword', data: undefined })).data ? 'yes' : 'no');
setIsSetup((await messageAndResponse(publicWS, { name: 'isSetup', data: undefined })).data ? 'yes' : 'no');
})();
}, [publicWS]);
const setup = async (ev: React.FormEvent<HTMLFormElement>) => {
ev.preventDefault();
if (!socket)
return enqueueSnackbar('Invalid state: socket not found', { variant: 'error' });
const formData = new FormData(ev.currentTarget);
const password = formData.get('password');
const data = {
port: parseInt(formData.get('port')?.toString() ?? '') ?? 3000,
password: password ? password.toString() : undefined
} as GUIConfig;
await messageAndResponse(socket, { name: 'setupServer', data });
enqueueSnackbar(`The following settings have been set: Port=${data.port}, Password=${data.password ?? 'noPasswordRequired'}`, {
variant: 'success',
persist: true
});
enqueueSnackbar('Please restart the server now.', {
variant: 'info',
persist: true
});
};
const connect = (ev?: React.FormEvent<HTMLFormElement>) => {
let search = new URLSearchParams();
if (ev) {
ev.preventDefault();
const formData = new FormData(ev.currentTarget);
const password = formData.get('password')?.toString();
if (!password)
return enqueueSnackbar('Please provide both a username and password', {
variant: 'error'
});
search = new URLSearchParams({
password
});
}
const randomEventHandler = React.useMemo(() => new RandomEventHandler(), []);
React.useEffect(() => {
(async () => {
if (!socket)
return;
const currentService = await messageAndResponse(socket, { name: 'type', data: undefined });
if (currentService.data !== undefined)
return dispatch({ type: 'service', payload: currentService.data });
if (store.service !== currentService.data)
messageAndResponse(socket, { name: 'setup', data: store.service });
})();
}, [store.service, dispatch, socket]);
React.useEffect(() => {
if (!socket)
return;
/* finish is a placeholder */
const listener = (initalData: MessageEvent<string>) => {
const data = JSON.parse(initalData.data) as RandomEvent<'finish'>;
randomEventHandler.emit(data.name, data);
const wws = new WebSocket(`${location.protocol == 'https:' ? 'wss' : 'ws'}://${process.env.NODE_ENV === 'development' ? 'localhost:3000' : window.location.host}/private?${search}`, );
wws.addEventListener('open', () => {
console.log('[INFO] [WS] Connected');
setSocket(wws);
});
wws.addEventListener('error', (er) => {
console.error('[ERROR] [WS]', er);
enqueueSnackbar('Unable to connect to server. Please check the password and try again.', {
variant: 'error'
});
});
};
socket.addEventListener('message', listener);
return () => {
socket.removeEventListener('message', listener);
const setup = async (ev: React.FormEvent<HTMLFormElement>) => {
ev.preventDefault();
if (!socket)
return enqueueSnackbar('Invalid state: socket not found', { variant: 'error' });
const formData = new FormData(ev.currentTarget);
const password = formData.get('password');
const data = {
port: parseInt(formData.get('port')?.toString() ?? '') ?? 3000,
password: password ? password.toString() : undefined
} as GUIConfig;
await messageAndResponse(socket, { name: 'setupServer', data });
enqueueSnackbar(`The following settings have been set: Port=${data.port}, Password=${data.password ?? 'noPasswordRequired'}`, {
variant: 'success',
persist: true
});
enqueueSnackbar('Please restart the server now.', {
variant: 'info',
persist: true
});
};
}, [ socket ]);
if (usePassword === 'waiting')
return <></>;
const randomEventHandler = React.useMemo(() => new RandomEventHandler(), []);
if (socket === undefined) {
if (usePassword === 'no') {
connect(undefined);
return <></>;
}
return <Box sx={{ mt: 3, display: 'flex', flexDirection: 'column', justifyItems: 'center', alignItems: 'center' }}>
<Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
<LockOutlined />
</Avatar>
<Typography component="h1" variant="h5" color="text.primary">
React.useEffect(() => {
(async () => {
if (!socket)
return;
const currentService = await messageAndResponse(socket, { name: 'type', data: undefined });
if (currentService.data !== undefined)
return dispatch({ type: 'service', payload: currentService.data });
if (store.service !== currentService.data)
messageAndResponse(socket, { name: 'setup', data: store.service });
})();
}, [store.service, dispatch, socket]);
React.useEffect(() => {
if (!socket)
return;
/* finish is a placeholder */
const listener = (initalData: MessageEvent<string>) => {
const data = JSON.parse(initalData.data) as RandomEvent<'finish'>;
randomEventHandler.emit(data.name, data);
};
socket.addEventListener('message', listener);
return () => {
socket.removeEventListener('message', listener);
};
}, [ socket ]);
if (usePassword === 'waiting')
return <></>;
if (socket === undefined) {
if (usePassword === 'no') {
connect(undefined);
return <></>;
}
return <Box sx={{ mt: 3, display: 'flex', flexDirection: 'column', justifyItems: 'center', alignItems: 'center' }}>
<Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
<LockOutlined />
</Avatar>
<Typography component="h1" variant="h5" color="text.primary">
Login
</Typography>
<Box component="form" onSubmit={connect} sx={{ mt: 1 }}>
<TextField name="password" margin='normal' type="password" fullWidth variant="filled" required label={'Password'} />
<Button type='submit' variant='contained' sx={{ mt: 3, mb: 2 }} fullWidth>Login</Button>
<Typography color="text.secondary" align='center' component="p" variant='body2'>
</Typography>
<Box component="form" onSubmit={connect} sx={{ mt: 1 }}>
<TextField name="password" margin='normal' type="password" fullWidth variant="filled" required label={'Password'} />
<Button type='submit' variant='contained' sx={{ mt: 3, mb: 2 }} fullWidth>Login</Button>
<Typography color="text.secondary" align='center' component="p" variant='body2'>
You need to login in order to use this tool.
</Typography>
</Box>
</Box>;
}
</Typography>
</Box>
</Box>;
}
if (isSetup === 'no') {
return <Box sx={{ mt: 3, display: 'flex', flexDirection: 'column', justifyItems: 'center', alignItems: 'center' }}>
<Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
<PowerSettingsNew />
</Avatar>
<Typography component="h1" variant="h5" color="text.primary">
if (isSetup === 'no') {
return <Box sx={{ mt: 3, display: 'flex', flexDirection: 'column', justifyItems: 'center', alignItems: 'center' }}>
<Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
<PowerSettingsNew />
</Avatar>
<Typography component="h1" variant="h5" color="text.primary">
Confirm
</Typography>
<Box component="form" onSubmit={setup} sx={{ mt: 1 }}>
<TextField name="port" margin='normal' type="number" fullWidth variant="filled" required label={'Port'} defaultValue={3000} />
<TextField name="password" margin='normal' type="password" fullWidth variant="filled" label={'Password'} />
<Button type='submit' variant='contained' sx={{ mt: 3, mb: 2 }} fullWidth>Confirm</Button>
<Typography color="text.secondary" align='center' component="p" variant='body2'>
</Typography>
<Box component="form" onSubmit={setup} sx={{ mt: 1 }}>
<TextField name="port" margin='normal' type="number" fullWidth variant="filled" required label={'Port'} defaultValue={3000} />
<TextField name="password" margin='normal' type="password" fullWidth variant="filled" label={'Password'} />
<Button type='submit' variant='contained' sx={{ mt: 3, mb: 2 }} fullWidth>Confirm</Button>
<Typography color="text.secondary" align='center' component="p" variant='body2'>
Please enter data that will be set to use this tool.
<br />
<br />
Leave blank to use no password (NOT RECOMMENDED)!
</Typography>
</Box>
</Box>;
}
</Typography>
</Box>
</Box>;
}
const messageHandler: FrontEndMessages = {
name: 'default',
auth: async (data) => (await messageAndResponse(socket, { name: 'auth', data })).data,
version: async () => (await messageAndResponse(socket, { name: 'version', data: undefined })).data,
checkToken: async () => (await messageAndResponse(socket, { name: 'checkToken', data: undefined })).data,
search: async (data) => (await messageAndResponse(socket, { name: 'search', data })).data,
handleDefault: async (data) => (await messageAndResponse(socket, { name: 'default', data })).data,
availableDubCodes: async () => (await messageAndResponse(socket, { name: 'availableDubCodes', data: undefined})).data,
availableSubCodes: async () => (await messageAndResponse(socket, { name: 'availableSubCodes', data: undefined })).data,
resolveItems: async (data) => (await messageAndResponse(socket, { name: 'resolveItems', data })).data,
listEpisodes: async (data) => (await messageAndResponse(socket, { name: 'listEpisodes', data })).data,
randomEvents: randomEventHandler,
downloadItem: (data) => messageAndResponse(socket, { name: 'downloadItem', data }),
isDownloading: async () => (await messageAndResponse(socket, { name: 'isDownloading', data: undefined })).data,
openFolder: async (data) => messageAndResponse(socket, { name: 'openFolder', data }),
logout: async () => (await messageAndResponse(socket, { name: 'changeProvider', data: undefined })).data,
openFile: async (data) => await messageAndResponse(socket, { name: 'openFile', data }),
openURL: async (data) => await messageAndResponse(socket, { name: 'openURL', data }),
getQueue: async () => (await messageAndResponse(socket, { name: 'getQueue', data: undefined })).data,
removeFromQueue: async (data) => await messageAndResponse(socket, { name: 'removeFromQueue', data }),
clearQueue: async () => await messageAndResponse(socket, { name: 'clearQueue', data: undefined }),
setDownloadQueue: async (data) => await messageAndResponse(socket, { name: 'setDownloadQueue', data }),
getDownloadQueue: async () => (await messageAndResponse(socket, { name: 'getDownloadQueue', data: undefined })).data,
};
const messageHandler: FrontEndMessages = {
name: 'default',
auth: async (data) => (await messageAndResponse(socket, { name: 'auth', data })).data,
version: async () => (await messageAndResponse(socket, { name: 'version', data: undefined })).data,
checkToken: async () => (await messageAndResponse(socket, { name: 'checkToken', data: undefined })).data,
search: async (data) => (await messageAndResponse(socket, { name: 'search', data })).data,
handleDefault: async (data) => (await messageAndResponse(socket, { name: 'default', data })).data,
availableDubCodes: async () => (await messageAndResponse(socket, { name: 'availableDubCodes', data: undefined})).data,
availableSubCodes: async () => (await messageAndResponse(socket, { name: 'availableSubCodes', data: undefined })).data,
resolveItems: async (data) => (await messageAndResponse(socket, { name: 'resolveItems', data })).data,
listEpisodes: async (data) => (await messageAndResponse(socket, { name: 'listEpisodes', data })).data,
randomEvents: randomEventHandler,
downloadItem: (data) => messageAndResponse(socket, { name: 'downloadItem', data }),
isDownloading: async () => (await messageAndResponse(socket, { name: 'isDownloading', data: undefined })).data,
openFolder: async (data) => messageAndResponse(socket, { name: 'openFolder', data }),
logout: async () => (await messageAndResponse(socket, { name: 'changeProvider', data: undefined })).data,
openFile: async (data) => await messageAndResponse(socket, { name: 'openFile', data }),
openURL: async (data) => await messageAndResponse(socket, { name: 'openURL', data }),
getQueue: async () => (await messageAndResponse(socket, { name: 'getQueue', data: undefined })).data,
removeFromQueue: async (data) => await messageAndResponse(socket, { name: 'removeFromQueue', data }),
clearQueue: async () => await messageAndResponse(socket, { name: 'clearQueue', data: undefined }),
setDownloadQueue: async (data) => await messageAndResponse(socket, { name: 'setDownloadQueue', data }),
getDownloadQueue: async () => (await messageAndResponse(socket, { name: 'getDownloadQueue', data: undefined })).data,
};
return <messageChannelContext.Provider value={messageHandler}>
{children}
</messageChannelContext.Provider>;
return <messageChannelContext.Provider value={messageHandler}>
{children}
</messageChannelContext.Provider>;
};
export default MessageChannelProvider;

View file

@ -6,30 +6,30 @@ import { RandomEvent } from '../../../../@types/randomEvents';
export const queueContext = React.createContext<QueueItem[]>([]);
const QueueProvider: FCWithChildren = ({ children }) => {
const msg = React.useContext(messageChannelContext);
const msg = React.useContext(messageChannelContext);
const [ready, setReady] = React.useState(false);
const [queue, setQueue] = React.useState<QueueItem[]>([]);
const [ready, setReady] = React.useState(false);
const [queue, setQueue] = React.useState<QueueItem[]>([]);
React.useEffect(() => {
if (msg && !ready) {
msg.getQueue().then(data => {
setQueue(data);
setReady(true);
});
}
const listener = (ev: RandomEvent<'queueChange'>) => {
setQueue(ev.data);
};
msg?.randomEvents.on('queueChange', listener);
return () => {
msg?.randomEvents.removeListener('queueChange', listener);
};
}, [ msg ]);
React.useEffect(() => {
if (msg && !ready) {
msg.getQueue().then(data => {
setQueue(data);
setReady(true);
});
}
const listener = (ev: RandomEvent<'queueChange'>) => {
setQueue(ev.data);
};
msg?.randomEvents.on('queueChange', listener);
return () => {
msg?.randomEvents.removeListener('queueChange', listener);
};
}, [ msg ]);
return <queueContext.Provider value={queue}>
{children}
</queueContext.Provider>;
return <queueContext.Provider value={queue}>
{children}
</queueContext.Provider>;
};
export default QueueProvider;

View file

@ -8,28 +8,28 @@ type Services = 'crunchy'|'hidive'|'ao'|'adn';
export const serviceContext = React.createContext<Services|undefined>(undefined);
const ServiceProvider: FCWithChildren = ({ children }) => {
const [ { service }, dispatch ] = useStore();
const [ { service }, dispatch ] = useStore();
const setService = (s: StoreState['service']) => {
dispatch({
type: 'service',
payload: s
});
};
const setService = (s: StoreState['service']) => {
dispatch({
type: 'service',
payload: s
});
};
return service === undefined ?
<Box sx={{ justifyContent: 'center', alignItems: 'center', display: 'flex', flexDirection: 'column', position: 'relative', top: '40vh'}}>
<Typography color="text.primary" variant='h3' sx={{ textAlign: 'center', mb: 5 }}>Please select your service</Typography>
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'center' }}>
<Button size='large' variant="contained" onClick={() => setService('crunchy')} startIcon={<Avatar src={'https://static.crunchyroll.com/cxweb/assets/img/favicons/favicon-32x32.png'} />}>Crunchyroll</Button>
<Button size='large' variant="contained" onClick={() => setService('hidive')} startIcon={<Avatar src={'https://static.diceplatform.com/prod/original/dce.hidive/settings/HIDIVE_AppLogo_1024x1024.0G0vK.jpg'} />}>Hidive</Button>
<Button size='large' variant="contained" onClick={() => setService('ao')} startIcon={<Avatar src={'https://www.animeonegai.com/assets/img/anime/general/ao3-favicon.png'} />}>AnimeOnegai</Button>
<Button size='large' variant="contained" onClick={() => setService('adn')} startIcon={<Avatar src={'https://animationdigitalnetwork.com/favicon.ico'} />}>AnimationDigitalNetwork</Button>
</Box>
</Box>
: <serviceContext.Provider value={service}>
{children}
</serviceContext.Provider>;
return service === undefined ?
<Box sx={{ justifyContent: 'center', alignItems: 'center', display: 'flex', flexDirection: 'column', position: 'relative', top: '40vh'}}>
<Typography color="text.primary" variant='h3' sx={{ textAlign: 'center', mb: 5 }}>Please select your service</Typography>
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'center' }}>
<Button size='large' variant="contained" onClick={() => setService('crunchy')} startIcon={<Avatar src={'https://static.crunchyroll.com/cxweb/assets/img/favicons/favicon-32x32.png'} />}>Crunchyroll</Button>
<Button size='large' variant="contained" onClick={() => setService('hidive')} startIcon={<Avatar src={'https://static.diceplatform.com/prod/original/dce.hidive/settings/HIDIVE_AppLogo_1024x1024.0G0vK.jpg'} />}>Hidive</Button>
<Button size='large' variant="contained" onClick={() => setService('ao')} startIcon={<Avatar src={'https://www.animeonegai.com/assets/img/anime/general/ao3-favicon.png'} />}>AnimeOnegai</Button>
<Button size='large' variant="contained" onClick={() => setService('adn')} startIcon={<Avatar src={'https://animationdigitalnetwork.com/favicon.ico'} />}>AnimationDigitalNetwork</Button>
</Box>
</Box>
: <serviceContext.Provider value={service}>
{children}
</serviceContext.Provider>;
};
export default ServiceProvider;

View file

@ -32,35 +32,35 @@ export type StoreAction<T extends (keyof StoreState)> = {
}
const Reducer = <T extends keyof StoreState,>(state: StoreState, action: StoreAction<T>): StoreState => {
switch(action.type) {
default:
return { ...state, [action.type]: action.payload };
}
switch(action.type) {
default:
return { ...state, [action.type]: action.payload };
}
};
const initialState: StoreState = {
downloadOptions: {
id: '',
q: 0,
e: '',
dubLang: [ 'jpn' ],
dlsubs: [ 'all' ],
fileName: '',
dlVideoOnce: false,
all: false,
but: false,
noaudio: false,
novids: false,
simul: false
},
service: undefined,
episodeListing: [],
version: '',
downloadOptions: {
id: '',
q: 0,
e: '',
dubLang: [ 'jpn' ],
dlsubs: [ 'all' ],
fileName: '',
dlVideoOnce: false,
all: false,
but: false,
noaudio: false,
novids: false,
simul: false
},
service: undefined,
episodeListing: [],
version: '',
};
const Store: FCWithChildren = ({children}) => {
const [state, dispatch] = React.useReducer(Reducer, initialState);
/*React.useEffect(() => {
const [state, dispatch] = React.useReducer(Reducer, initialState);
/*React.useEffect(() => {
if (!state.unsavedChanges.has)
return;
const unsavedChanges = (ev: BeforeUnloadEvent, lang: LanguageContextType) => {
@ -79,11 +79,11 @@ const Store: FCWithChildren = ({children}) => {
return () => window.removeEventListener('beforeunload', windowListener);
}, [state.unsavedChanges.has]);*/
return (
<StoreContext.Provider value={[state, dispatch]}>
{children}
</StoreContext.Provider>
);
return (
<StoreContext.Provider value={[state, dispatch]}>
{children}
</StoreContext.Provider>
);
};
/* Importent Notice -- The 'queue' generic will be overriden */

View file

@ -1,29 +1,29 @@
{
"compilerOptions": {
"outDir": "./build",
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "CommonJS",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
//"noEmit": true,
"jsx": "react-jsx",
"downlevelIteration": true
},
"include": [
"./src",
"./webpack.config.ts"
]
"compilerOptions": {
"outDir": "./build",
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "CommonJS",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
//"noEmit": true,
"jsx": "react-jsx",
"downlevelIteration": true
},
"include": [
"./src",
"./webpack.config.ts"
]
}

View file

@ -4,55 +4,55 @@ import path from 'path';
import type { Configuration as DevServerConfig } from 'webpack-dev-server';
const config: Configuration & DevServerConfig = {
devServer: {
proxy: [
{
target: 'http://localhost:3000',
context: ['/public', '/private'],
ws: true
}
],
},
entry: './src/index.tsx',
mode: 'production',
output: {
path: path.resolve(process.cwd(), './build'),
filename: 'index.js',
},
target: 'web',
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
},
performance: false,
module: {
rules: [
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: {
'loader': 'babel-loader',
options: {
presets: [
'@babel/typescript',
'@babel/preset-react',
['@babel/preset-env', {
targets: 'defaults'
}]
]
}
},
},
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: path.join(process.cwd(), 'public', 'index.html')
})
]
devServer: {
proxy: [
{
target: 'http://localhost:3000',
context: ['/public', '/private'],
ws: true
}
],
},
entry: './src/index.tsx',
mode: 'production',
output: {
path: path.resolve(process.cwd(), './build'),
filename: 'index.js',
},
target: 'web',
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
},
performance: false,
module: {
rules: [
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: {
'loader': 'babel-loader',
options: {
presets: [
'@babel/typescript',
'@babel/preset-react',
['@babel/preset-env', {
targets: 'defaults'
}]
]
}
},
},
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: path.join(process.cwd(), 'public', 'index.html')
})
]
};
export default config;

View file

@ -25,7 +25,7 @@ app.use(express.static(path.join(workingDir, 'gui', 'server', 'build'), { maxAge
console.info(`\n=== Multi Downloader NX GUI ${packageJson.version} ===\n`);
const server = app.listen(cfg.gui.port, () => {
console.info(`GUI server started on port ${cfg.gui.port}`);
console.info(`GUI server started on port ${cfg.gui.port}`);
});
new PublicWebSocket(server);

View file

@ -12,123 +12,123 @@ import packageJson from '../../package.json';
export default class ServiceHandler {
private service: MessageHandler|undefined = undefined;
private ws: WebSocketHandler;
private state: GuiState;
private service: MessageHandler|undefined = undefined;
private ws: WebSocketHandler;
private state: GuiState;
constructor(server: Server<typeof IncomingMessage, typeof ServerResponse>) {
this.ws = new WebSocketHandler(server);
this.handleMessages();
this.state = getState();
}
constructor(server: Server<typeof IncomingMessage, typeof ServerResponse>) {
this.ws = new WebSocketHandler(server);
this.handleMessages();
this.state = getState();
}
private handleMessages() {
this.ws.events.on('setupServer', ({ data }, respond) => {
writeYamlCfgFile('gui', data);
this.state.setup = true;
setState(this.state);
respond(true);
process.exit(0);
});
private handleMessages() {
this.ws.events.on('setupServer', ({ data }, respond) => {
writeYamlCfgFile('gui', data);
this.state.setup = true;
setState(this.state);
respond(true);
process.exit(0);
});
this.ws.events.on('setup', ({ data }) => {
if (data === 'crunchy') {
this.service = new CrunchyHandler(this.ws);
} else if (data === 'hidive') {
this.service = new HidiveHandler(this.ws);
} else if (data === 'ao') {
this.service = new AnimeOnegaiHandler(this.ws);
} else if (data === 'adn') {
this.service = new ADNHandler(this.ws);
}
});
this.ws.events.on('setup', ({ data }) => {
if (data === 'crunchy') {
this.service = new CrunchyHandler(this.ws);
} else if (data === 'hidive') {
this.service = new HidiveHandler(this.ws);
} else if (data === 'ao') {
this.service = new AnimeOnegaiHandler(this.ws);
} else if (data === 'adn') {
this.service = new ADNHandler(this.ws);
}
});
this.ws.events.on('changeProvider', async (_, respond) => {
if (await this.service?.isDownloading())
return respond(false);
this.service = undefined;
respond(true);
});
this.ws.events.on('changeProvider', async (_, respond) => {
if (await this.service?.isDownloading())
return respond(false);
this.service = undefined;
respond(true);
});
this.ws.events.on('auth', async ({ data }, respond) => {
if (this.service === undefined)
return respond({ isOk: false, reason: new Error('No service selected') });
respond(await this.service.auth(data));
});
this.ws.events.on('version', async (_, respond) => {
respond(packageJson.version);
});
this.ws.events.on('type', async (_, respond) => respond(this.service === undefined ? undefined : this.service.name as 'hidive'|'crunchy'|'ao'|'adn'));
this.ws.events.on('checkToken', async (_, respond) => {
if (this.service === undefined)
return respond({ isOk: false, reason: new Error('No service selected') });
respond(await this.service.checkToken());
});
this.ws.events.on('search', async ({ data }, respond) => {
if (this.service === undefined)
return respond({ isOk: false, reason: new Error('No service selected') });
respond(await this.service.search(data));
});
this.ws.events.on('default', async ({ data }, respond) => {
if (this.service === undefined)
return respond({ isOk: false, reason: new Error('No service selected') });
respond(await this.service.handleDefault(data));
});
this.ws.events.on('availableDubCodes', async (_, respond) => {
if (this.service === undefined)
return respond([]);
respond(await this.service.availableDubCodes());
});
this.ws.events.on('availableSubCodes', async (_, respond) => {
if (this.service === undefined)
return respond([]);
respond(await this.service.availableSubCodes());
});
this.ws.events.on('resolveItems', async ({ data }, respond) => {
if (this.service === undefined)
return respond(false);
respond(await this.service.resolveItems(data));
});
this.ws.events.on('listEpisodes', async ({ data }, respond) => {
if (this.service === undefined)
return respond({ isOk: false, reason: new Error('No service selected') });
respond(await this.service.listEpisodes(data));
});
this.ws.events.on('downloadItem', async ({ data }, respond) => {
this.service?.downloadItem(data);
respond(undefined);
});
this.ws.events.on('openFolder', async ({ data }, respond) => {
this.service?.openFolder(data);
respond(undefined);
});
this.ws.events.on('openFile', async ({ data }, respond) => {
this.service?.openFile(data);
respond(undefined);
});
this.ws.events.on('openURL', async ({ data }, respond) => {
this.service?.openURL(data);
respond(undefined);
});
this.ws.events.on('getQueue', async (_, respond) => {
respond(await this.service?.getQueue() ?? []);
});
this.ws.events.on('removeFromQueue', async ({ data }, respond) => {
this.service?.removeFromQueue(data);
respond(undefined);
});
this.ws.events.on('clearQueue', async (_, respond) => {
this.service?.clearQueue();
respond(undefined);
});
this.ws.events.on('setDownloadQueue', async ({ data }, respond) => {
this.service?.setDownloadQueue(data);
respond(undefined);
});
this.ws.events.on('getDownloadQueue', async (_, respond) => {
respond(await this.service?.getDownloadQueue() ?? false);
});
this.ws.events.on('isDownloading', async (_, respond) => respond(await this.service?.isDownloading() ?? false));
}
this.ws.events.on('auth', async ({ data }, respond) => {
if (this.service === undefined)
return respond({ isOk: false, reason: new Error('No service selected') });
respond(await this.service.auth(data));
});
this.ws.events.on('version', async (_, respond) => {
respond(packageJson.version);
});
this.ws.events.on('type', async (_, respond) => respond(this.service === undefined ? undefined : this.service.name as 'hidive'|'crunchy'|'ao'|'adn'));
this.ws.events.on('checkToken', async (_, respond) => {
if (this.service === undefined)
return respond({ isOk: false, reason: new Error('No service selected') });
respond(await this.service.checkToken());
});
this.ws.events.on('search', async ({ data }, respond) => {
if (this.service === undefined)
return respond({ isOk: false, reason: new Error('No service selected') });
respond(await this.service.search(data));
});
this.ws.events.on('default', async ({ data }, respond) => {
if (this.service === undefined)
return respond({ isOk: false, reason: new Error('No service selected') });
respond(await this.service.handleDefault(data));
});
this.ws.events.on('availableDubCodes', async (_, respond) => {
if (this.service === undefined)
return respond([]);
respond(await this.service.availableDubCodes());
});
this.ws.events.on('availableSubCodes', async (_, respond) => {
if (this.service === undefined)
return respond([]);
respond(await this.service.availableSubCodes());
});
this.ws.events.on('resolveItems', async ({ data }, respond) => {
if (this.service === undefined)
return respond(false);
respond(await this.service.resolveItems(data));
});
this.ws.events.on('listEpisodes', async ({ data }, respond) => {
if (this.service === undefined)
return respond({ isOk: false, reason: new Error('No service selected') });
respond(await this.service.listEpisodes(data));
});
this.ws.events.on('downloadItem', async ({ data }, respond) => {
this.service?.downloadItem(data);
respond(undefined);
});
this.ws.events.on('openFolder', async ({ data }, respond) => {
this.service?.openFolder(data);
respond(undefined);
});
this.ws.events.on('openFile', async ({ data }, respond) => {
this.service?.openFile(data);
respond(undefined);
});
this.ws.events.on('openURL', async ({ data }, respond) => {
this.service?.openURL(data);
respond(undefined);
});
this.ws.events.on('getQueue', async (_, respond) => {
respond(await this.service?.getQueue() ?? []);
});
this.ws.events.on('removeFromQueue', async ({ data }, respond) => {
this.service?.removeFromQueue(data);
respond(undefined);
});
this.ws.events.on('clearQueue', async (_, respond) => {
this.service?.clearQueue();
respond(undefined);
});
this.ws.events.on('setDownloadQueue', async ({ data }, respond) => {
this.service?.setDownloadQueue(data);
respond(undefined);
});
this.ws.events.on('getDownloadQueue', async (_, respond) => {
respond(await this.service?.getDownloadQueue() ?? false);
});
this.ws.events.on('isDownloading', async (_, respond) => respond(await this.service?.isDownloading() ?? false));
}
}

View file

@ -8,132 +8,132 @@ import { console } from '../../../modules/log';
import * as yargs from '../../../modules/module.app-args';
class ADNHandler extends Base implements MessageHandler {
private adn: AnimationDigitalNetwork;
public name = 'adn';
constructor(ws: WebSocketHandler) {
super(ws);
this.adn = new AnimationDigitalNetwork();
this.initState();
this.getDefaults();
}
private adn: AnimationDigitalNetwork;
public name = 'adn';
constructor(ws: WebSocketHandler) {
super(ws);
this.adn = new AnimationDigitalNetwork();
this.initState();
this.getDefaults();
}
public getDefaults() {
const _default = yargs.appArgv(this.adn.cfg.cli, true);
if (['fr', 'de'].includes(_default.locale))
this.adn.locale = _default.locale;
}
public getDefaults() {
const _default = yargs.appArgv(this.adn.cfg.cli, true);
if (['fr', 'de'].includes(_default.locale))
this.adn.locale = _default.locale;
}
public async auth(data: AuthData) {
return this.adn.doAuth(data);
}
public async auth(data: AuthData) {
return this.adn.doAuth(data);
}
public async checkToken(): Promise<CheckTokenResponse> {
public async checkToken(): Promise<CheckTokenResponse> {
//TODO: implement proper method to check token
return { isOk: true, value: undefined };
}
public async search(data: SearchData): Promise<SearchResponse> {
console.debug(`Got search options: ${JSON.stringify(data)}`);
const search = await this.adn.doSearch(data);
if (!search.isOk) {
return search;
return { isOk: true, value: undefined };
}
return { isOk: true, value: search.value };
}
public async handleDefault(name: string) {
return getDefault(name, this.adn.cfg.cli);
}
public async availableDubCodes(): Promise<string[]> {
const dubLanguageCodesArray: string[] = [];
for(const language of languages){
if (language.adn_locale)
dubLanguageCodesArray.push(language.code);
}
return [...new Set(dubLanguageCodesArray)];
}
public async availableSubCodes(): Promise<string[]> {
const subLanguageCodesArray: string[] = [];
for(const language of languages){
if (language.adn_locale)
subLanguageCodesArray.push(language.locale);
}
return ['all', 'none', ...new Set(subLanguageCodesArray)];
}
public async resolveItems(data: ResolveItemsData): Promise<boolean> {
const parse = parseInt(data.id);
if (isNaN(parse) || parse <= 0)
return false;
console.debug(`Got resolve options: ${JSON.stringify(data)}`);
const res = await this.adn.selectShow(parseInt(data.id), data.e, data.but, data.all);
if (!res.isOk || !res.value)
return res.isOk;
this.addToQueue(res.value.map(a => {
return {
...data,
ids: [a.id],
title: a.title,
parent: {
title: a.show.shortTitle,
season: a.season
},
e: a.shortNumber,
image: a.image,
episode: a.shortNumber
};
}));
return true;
}
public async listEpisodes(id: string): Promise<EpisodeListResponse> {
const parse = parseInt(id);
if (isNaN(parse) || parse <= 0)
return { isOk: false, reason: new Error('The ID is invalid') };
const request = await this.adn.listShow(parse);
if (!request.isOk || !request.value)
return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')};
return { isOk: true, value: request.value.videos.map(function(item) {
return {
e: item.shortNumber,
lang: [],
name: item.title,
season: item.season,
seasonTitle: item.show.title,
episode: item.shortNumber,
id: item.id+'',
img: item.image,
description: item.summary,
time: item.duration+''
};
})};
}
public async downloadItem(data: DownloadData) {
this.setDownloading(true);
console.debug(`Got download options: ${JSON.stringify(data)}`);
const _default = yargs.appArgv(this.adn.cfg.cli, true);
const res = await this.adn.selectShow(parseInt(data.id), data.e, false, false);
if (res.isOk) {
for (const select of res.value) {
if (!(await this.adn.getEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y',
novids: data.novids, noaudio: data.noaudio, hslang: data.hslang || 'none', dubLang: data.dubLang }))) {
const er = new Error(`Unable to download episode ${data.e} from ${data.id}`);
er.name = 'Download error';
this.alertError(er);
public async search(data: SearchData): Promise<SearchResponse> {
console.debug(`Got search options: ${JSON.stringify(data)}`);
const search = await this.adn.doSearch(data);
if (!search.isOk) {
return search;
}
}
} else {
this.alertError(new Error('Failed to download episode, check for additional logs.'));
return { isOk: true, value: search.value };
}
public async handleDefault(name: string) {
return getDefault(name, this.adn.cfg.cli);
}
public async availableDubCodes(): Promise<string[]> {
const dubLanguageCodesArray: string[] = [];
for(const language of languages){
if (language.adn_locale)
dubLanguageCodesArray.push(language.code);
}
return [...new Set(dubLanguageCodesArray)];
}
public async availableSubCodes(): Promise<string[]> {
const subLanguageCodesArray: string[] = [];
for(const language of languages){
if (language.adn_locale)
subLanguageCodesArray.push(language.locale);
}
return ['all', 'none', ...new Set(subLanguageCodesArray)];
}
public async resolveItems(data: ResolveItemsData): Promise<boolean> {
const parse = parseInt(data.id);
if (isNaN(parse) || parse <= 0)
return false;
console.debug(`Got resolve options: ${JSON.stringify(data)}`);
const res = await this.adn.selectShow(parseInt(data.id), data.e, data.but, data.all);
if (!res.isOk || !res.value)
return res.isOk;
this.addToQueue(res.value.map(a => {
return {
...data,
ids: [a.id],
title: a.title,
parent: {
title: a.show.shortTitle,
season: a.season
},
e: a.shortNumber,
image: a.image,
episode: a.shortNumber
};
}));
return true;
}
public async listEpisodes(id: string): Promise<EpisodeListResponse> {
const parse = parseInt(id);
if (isNaN(parse) || parse <= 0)
return { isOk: false, reason: new Error('The ID is invalid') };
const request = await this.adn.listShow(parse);
if (!request.isOk || !request.value)
return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')};
return { isOk: true, value: request.value.videos.map(function(item) {
return {
e: item.shortNumber,
lang: [],
name: item.title,
season: item.season,
seasonTitle: item.show.title,
episode: item.shortNumber,
id: item.id+'',
img: item.image,
description: item.summary,
time: item.duration+''
};
})};
}
public async downloadItem(data: DownloadData) {
this.setDownloading(true);
console.debug(`Got download options: ${JSON.stringify(data)}`);
const _default = yargs.appArgv(this.adn.cfg.cli, true);
const res = await this.adn.selectShow(parseInt(data.id), data.e, false, false);
if (res.isOk) {
for (const select of res.value) {
if (!(await this.adn.getEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y',
novids: data.novids, noaudio: data.noaudio, hslang: data.hslang || 'none', dubLang: data.dubLang }))) {
const er = new Error(`Unable to download episode ${data.e} from ${data.id}`);
er.name = 'Download error';
this.alertError(er);
}
}
} else {
this.alertError(new Error('Failed to download episode, check for additional logs.'));
}
this.sendMessage({ name: 'finish', data: undefined });
this.setDownloading(false);
this.onFinish();
}
this.sendMessage({ name: 'finish', data: undefined });
this.setDownloading(false);
this.onFinish();
}
}
export default ADNHandler;

View file

@ -8,144 +8,144 @@ import { console } from '../../../modules/log';
import * as yargs from '../../../modules/module.app-args';
class AnimeOnegaiHandler extends Base implements MessageHandler {
private ao: AnimeOnegai;
public name = 'ao';
constructor(ws: WebSocketHandler) {
super(ws);
this.ao = new AnimeOnegai();
this.initState();
this.getDefaults();
}
private ao: AnimeOnegai;
public name = 'ao';
constructor(ws: WebSocketHandler) {
super(ws);
this.ao = new AnimeOnegai();
this.initState();
this.getDefaults();
}
public getDefaults() {
const _default = yargs.appArgv(this.ao.cfg.cli, true);
if (['es', 'pt'].includes(_default.locale))
this.ao.locale = _default.locale;
}
public getDefaults() {
const _default = yargs.appArgv(this.ao.cfg.cli, true);
if (['es', 'pt'].includes(_default.locale))
this.ao.locale = _default.locale;
}
public async auth(data: AuthData) {
return this.ao.doAuth(data);
}
public async auth(data: AuthData) {
return this.ao.doAuth(data);
}
public async checkToken(): Promise<CheckTokenResponse> {
public async checkToken(): Promise<CheckTokenResponse> {
//TODO: implement proper method to check token
return { isOk: true, value: undefined };
}
public async search(data: SearchData): Promise<SearchResponse> {
console.debug(`Got search options: ${JSON.stringify(data)}`);
const search = await this.ao.doSearch(data);
if (!search.isOk) {
return search;
return { isOk: true, value: undefined };
}
return { isOk: true, value: search.value };
}
public async handleDefault(name: string) {
return getDefault(name, this.ao.cfg.cli);
}
public async availableDubCodes(): Promise<string[]> {
const dubLanguageCodesArray: string[] = [];
for(const language of languages){
if (language.ao_locale)
dubLanguageCodesArray.push(language.code);
}
return [...new Set(dubLanguageCodesArray)];
}
public async availableSubCodes(): Promise<string[]> {
const subLanguageCodesArray: string[] = [];
for(const language of languages){
if (language.ao_locale)
subLanguageCodesArray.push(language.locale);
}
return ['all', 'none', ...new Set(subLanguageCodesArray)];
}
public async resolveItems(data: ResolveItemsData): Promise<boolean> {
const parse = parseInt(data.id);
if (isNaN(parse) || parse <= 0)
return false;
console.debug(`Got resolve options: ${JSON.stringify(data)}`);
const _default = yargs.appArgv(this.ao.cfg.cli, true);
const res = await this.ao.selectShow(parseInt(data.id), data.e, data.but, data.all, _default);
if (!res.isOk || !res.value)
return res.isOk;
this.addToQueue(res.value.map(a => {
return {
...data,
ids: a.data.map(a => a.videoId),
title: a.episodeTitle,
parent: {
title: a.seasonTitle,
season: a.seasonTitle
},
e: a.episodeNumber+'',
image: a.image,
episode: a.episodeNumber+''
};
}));
return true;
}
public async listEpisodes(id: string): Promise<EpisodeListResponse> {
const parse = parseInt(id);
if (isNaN(parse) || parse <= 0)
return { isOk: false, reason: new Error('The ID is invalid') };
const request = await this.ao.listShow(parse);
if (!request.isOk || !request.value)
return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')};
const episodes: Episode[] = [];
const seasonNumberTitleParse = request.series.data.title.match(/\d+$/);
const seasonNumber = seasonNumberTitleParse ? parseInt(seasonNumberTitleParse[0]) : 1;
//request.value
for (const episodeKey in request.value) {
const episode = request.value[episodeKey][0];
const langs = Array.from(new Set(request.value[episodeKey].map(a=>a.lang)));
episodes.push({
e: episode.number+'',
lang: langs as string[],
name: episode.name,
season: seasonNumber+'',
seasonTitle: '',
episode: episode.number+'',
id: episode.video_entry+'',
img: episode.thumbnail,
description: episode.description,
time: ''
});
}
return { isOk: true, value: episodes };
}
public async downloadItem(data: DownloadData) {
this.setDownloading(true);
console.debug(`Got download options: ${JSON.stringify(data)}`);
const _default = yargs.appArgv(this.ao.cfg.cli, true);
const res = await this.ao.selectShow(parseInt(data.id), data.e, false, false, {
..._default,
dubLang: data.dubLang,
e: data.e
});
if (res.isOk) {
for (const select of res.value) {
if (!(await this.ao.downloadEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y',
novids: data.novids, noaudio: data.noaudio, hslang: data.hslang || 'none', dubLang: data.dubLang }))) {
const er = new Error(`Unable to download episode ${data.e} from ${data.id}`);
er.name = 'Download error';
this.alertError(er);
public async search(data: SearchData): Promise<SearchResponse> {
console.debug(`Got search options: ${JSON.stringify(data)}`);
const search = await this.ao.doSearch(data);
if (!search.isOk) {
return search;
}
}
} else {
this.alertError(new Error('Failed to download episode, check for additional logs.'));
return { isOk: true, value: search.value };
}
public async handleDefault(name: string) {
return getDefault(name, this.ao.cfg.cli);
}
public async availableDubCodes(): Promise<string[]> {
const dubLanguageCodesArray: string[] = [];
for(const language of languages){
if (language.ao_locale)
dubLanguageCodesArray.push(language.code);
}
return [...new Set(dubLanguageCodesArray)];
}
public async availableSubCodes(): Promise<string[]> {
const subLanguageCodesArray: string[] = [];
for(const language of languages){
if (language.ao_locale)
subLanguageCodesArray.push(language.locale);
}
return ['all', 'none', ...new Set(subLanguageCodesArray)];
}
public async resolveItems(data: ResolveItemsData): Promise<boolean> {
const parse = parseInt(data.id);
if (isNaN(parse) || parse <= 0)
return false;
console.debug(`Got resolve options: ${JSON.stringify(data)}`);
const _default = yargs.appArgv(this.ao.cfg.cli, true);
const res = await this.ao.selectShow(parseInt(data.id), data.e, data.but, data.all, _default);
if (!res.isOk || !res.value)
return res.isOk;
this.addToQueue(res.value.map(a => {
return {
...data,
ids: a.data.map(a => a.videoId),
title: a.episodeTitle,
parent: {
title: a.seasonTitle,
season: a.seasonTitle
},
e: a.episodeNumber+'',
image: a.image,
episode: a.episodeNumber+''
};
}));
return true;
}
public async listEpisodes(id: string): Promise<EpisodeListResponse> {
const parse = parseInt(id);
if (isNaN(parse) || parse <= 0)
return { isOk: false, reason: new Error('The ID is invalid') };
const request = await this.ao.listShow(parse);
if (!request.isOk || !request.value)
return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')};
const episodes: Episode[] = [];
const seasonNumberTitleParse = request.series.data.title.match(/\d+$/);
const seasonNumber = seasonNumberTitleParse ? parseInt(seasonNumberTitleParse[0]) : 1;
//request.value
for (const episodeKey in request.value) {
const episode = request.value[episodeKey][0];
const langs = Array.from(new Set(request.value[episodeKey].map(a=>a.lang)));
episodes.push({
e: episode.number+'',
lang: langs as string[],
name: episode.name,
season: seasonNumber+'',
seasonTitle: '',
episode: episode.number+'',
id: episode.video_entry+'',
img: episode.thumbnail,
description: episode.description,
time: ''
});
}
return { isOk: true, value: episodes };
}
public async downloadItem(data: DownloadData) {
this.setDownloading(true);
console.debug(`Got download options: ${JSON.stringify(data)}`);
const _default = yargs.appArgv(this.ao.cfg.cli, true);
const res = await this.ao.selectShow(parseInt(data.id), data.e, false, false, {
..._default,
dubLang: data.dubLang,
e: data.e
});
if (res.isOk) {
for (const select of res.value) {
if (!(await this.ao.downloadEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y',
novids: data.novids, noaudio: data.noaudio, hslang: data.hslang || 'none', dubLang: data.dubLang }))) {
const er = new Error(`Unable to download episode ${data.e} from ${data.id}`);
er.name = 'Download error';
this.alertError(er);
}
}
} else {
this.alertError(new Error('Failed to download episode, check for additional logs.'));
}
this.sendMessage({ name: 'finish', data: undefined });
this.setDownloading(false);
this.onFinish();
}
this.sendMessage({ name: 'finish', data: undefined });
this.setDownloading(false);
this.onFinish();
}
}
export default AnimeOnegaiHandler;

View file

@ -9,140 +9,140 @@ import { getState, setState } from '../../../modules/module.cfg-loader';
import packageJson from '../../../package.json';
export default class Base {
private state: GuiState;
public name = 'default';
constructor(private ws: WebSocketHandler) {
this.state = getState();
}
private downloading = false;
private queue: QueueItem[] = [];
private workOnQueue = false;
version(): Promise<string> {
return new Promise(() => {
return packageJson.version;
});
}
initState() {
if (this.state.services[this.name]) {
this.queue = this.state.services[this.name].queue;
this.queueChange();
} else {
this.state.services[this.name] = {
'queue': []
};
private state: GuiState;
public name = 'default';
constructor(private ws: WebSocketHandler) {
this.state = getState();
}
}
setDownloading(downloading: boolean) {
this.downloading = downloading;
}
private downloading = false;
getDownloading() {
return this.downloading;
}
private queue: QueueItem[] = [];
private workOnQueue = false;
alertError(error: Error) {
console.error(`${error}`);
}
version(): Promise<string> {
return new Promise(() => {
return packageJson.version;
});
}
makeProgressHandler(videoInfo: DownloadInfo) {
return ((data: ProgressData) => {
this.sendMessage({
name: 'progress',
data: {
downloadInfo: videoInfo,
progress: data
initState() {
if (this.state.services[this.name]) {
this.queue = this.state.services[this.name].queue;
this.queueChange();
} else {
this.state.services[this.name] = {
'queue': []
};
}
});
});
}
sendMessage<T extends keyof RandomEvents>(data: RandomEvent<T>) {
this.ws.sendMessage(data);
}
async isDownloading() {
return this.downloading;
}
async openFolder(folderType: FolderTypes) {
switch (folderType) {
case 'content':
open(cfg.dir.content);
break;
case 'config':
open(cfg.dir.config);
break;
}
}
async openFile(data: [FolderTypes, string]) {
switch (data[0]) {
case 'config':
open(path.join(cfg.dir.config, data[1]));
break;
case 'content':
throw new Error('No subfolders');
setDownloading(downloading: boolean) {
this.downloading = downloading;
}
}
async openURL(data: string) {
open(data);
}
public async getQueue(): Promise<QueueItem[]> {
return this.queue;
}
public async removeFromQueue(index: number) {
this.queue.splice(index, 1);
this.queueChange();
}
public async clearQueue() {
this.queue = [];
this.queueChange();
}
public addToQueue(data: QueueItem[]) {
this.queue = this.queue.concat(...data);
this.queueChange();
}
public setDownloadQueue(data: boolean) {
this.workOnQueue = data;
this.queueChange();
}
public async getDownloadQueue(): Promise<boolean> {
return this.workOnQueue;
}
private async queueChange() {
this.sendMessage({ name: 'queueChange', data: this.queue });
if (this.workOnQueue && this.queue.length > 0 && !await this.isDownloading()) {
this.setDownloading(true);
this.sendMessage({ name: 'current', data: this.queue[0] });
this.downloadItem(this.queue[0]);
this.queue = this.queue.slice(1);
this.queueChange();
getDownloading() {
return this.downloading;
}
this.state.services[this.name].queue = this.queue;
setState(this.state);
}
public async onFinish() {
this.sendMessage({ name: 'current', data: undefined });
this.queueChange();
}
alertError(error: Error) {
console.error(`${error}`);
}
//Overriten
// eslint-disable-next-line
makeProgressHandler(videoInfo: DownloadInfo) {
return ((data: ProgressData) => {
this.sendMessage({
name: 'progress',
data: {
downloadInfo: videoInfo,
progress: data
}
});
});
}
sendMessage<T extends keyof RandomEvents>(data: RandomEvent<T>) {
this.ws.sendMessage(data);
}
async isDownloading() {
return this.downloading;
}
async openFolder(folderType: FolderTypes) {
switch (folderType) {
case 'content':
open(cfg.dir.content);
break;
case 'config':
open(cfg.dir.config);
break;
}
}
async openFile(data: [FolderTypes, string]) {
switch (data[0]) {
case 'config':
open(path.join(cfg.dir.config, data[1]));
break;
case 'content':
throw new Error('No subfolders');
}
}
async openURL(data: string) {
open(data);
}
public async getQueue(): Promise<QueueItem[]> {
return this.queue;
}
public async removeFromQueue(index: number) {
this.queue.splice(index, 1);
this.queueChange();
}
public async clearQueue() {
this.queue = [];
this.queueChange();
}
public addToQueue(data: QueueItem[]) {
this.queue = this.queue.concat(...data);
this.queueChange();
}
public setDownloadQueue(data: boolean) {
this.workOnQueue = data;
this.queueChange();
}
public async getDownloadQueue(): Promise<boolean> {
return this.workOnQueue;
}
private async queueChange() {
this.sendMessage({ name: 'queueChange', data: this.queue });
if (this.workOnQueue && this.queue.length > 0 && !await this.isDownloading()) {
this.setDownloading(true);
this.sendMessage({ name: 'current', data: this.queue[0] });
this.downloadItem(this.queue[0]);
this.queue = this.queue.slice(1);
this.queueChange();
}
this.state.services[this.name].queue = this.queue;
setState(this.state);
}
public async onFinish() {
this.sendMessage({ name: 'current', data: undefined });
this.queueChange();
}
//Overriten
// eslint-disable-next-line
public async downloadItem(_: QueueItem) {
throw new Error('downloadItem not overriden');
}
throw new Error('downloadItem not overriden');
}
}

View file

@ -8,120 +8,120 @@ import { console } from '../../../modules/log';
import * as yargs from '../../../modules/module.app-args';
class CrunchyHandler extends Base implements MessageHandler {
private crunchy: Crunchy;
public name = 'crunchy';
constructor(ws: WebSocketHandler) {
super(ws);
this.crunchy = new Crunchy();
this.crunchy.refreshToken();
this.initState();
this.getDefaults();
}
private crunchy: Crunchy;
public name = 'crunchy';
constructor(ws: WebSocketHandler) {
super(ws);
this.crunchy = new Crunchy();
this.crunchy.refreshToken();
this.initState();
this.getDefaults();
}
public getDefaults() {
const _default = yargs.appArgv(this.crunchy.cfg.cli, true);
this.crunchy.locale = _default.locale;
}
public getDefaults() {
const _default = yargs.appArgv(this.crunchy.cfg.cli, true);
this.crunchy.locale = _default.locale;
}
public async listEpisodes (id: string): Promise<EpisodeListResponse> {
this.getDefaults();
await this.crunchy.refreshToken(true);
return { isOk: true, value: (await this.crunchy.listSeriesID(id)).list };
}
public async listEpisodes (id: string): Promise<EpisodeListResponse> {
this.getDefaults();
await this.crunchy.refreshToken(true);
return { isOk: true, value: (await this.crunchy.listSeriesID(id)).list };
}
public async handleDefault(name: string) {
return getDefault(name, this.crunchy.cfg.cli);
}
public async availableDubCodes(): Promise<string[]> {
const dubLanguageCodesArray: string[] = [];
for(const language of languages){
if (language.cr_locale)
dubLanguageCodesArray.push(language.code);
public async handleDefault(name: string) {
return getDefault(name, this.crunchy.cfg.cli);
}
return [...new Set(dubLanguageCodesArray)];
}
public async availableSubCodes(): Promise<string[]> {
return subtitleLanguagesFilter;
}
public async resolveItems(data: ResolveItemsData): Promise<boolean> {
this.getDefaults();
await this.crunchy.refreshToken(true);
console.debug(`Got resolve options: ${JSON.stringify(data)}`);
const res = await this.crunchy.downloadFromSeriesID(data.id, data);
if (!res.isOk)
return res.isOk;
this.addToQueue(res.value.map(a => {
return {
...data,
ids: a.data.map(a => a.mediaId),
title: a.episodeTitle,
parent: {
title: a.seasonTitle,
season: a.season.toString()
},
e: a.e,
image: a.image,
episode: a.episodeNumber
};
}));
return true;
}
public async search(data: SearchData): Promise<SearchResponse> {
this.getDefaults();
await this.crunchy.refreshToken(true);
if (!data['search-type']) data['search-type'] = 'series';
console.debug(`Got search options: ${JSON.stringify(data)}`);
const crunchySearch = await this.crunchy.doSearch(data);
if (!crunchySearch.isOk) {
this.crunchy.refreshToken();
return crunchySearch;
}
return { isOk: true, value: crunchySearch.value };
}
public async checkToken(): Promise<CheckTokenResponse> {
if (await this.crunchy.getProfile()) {
return { isOk: true, value: undefined };
} else {
return { isOk: false, reason: new Error('') };
}
}
public auth(data: AuthData) {
return this.crunchy.doAuth(data);
}
public async downloadItem(data: DownloadData) {
this.getDefaults();
await this.crunchy.refreshToken(true);
console.debug(`Got download options: ${JSON.stringify(data)}`);
this.setDownloading(true);
const _default = yargs.appArgv(this.crunchy.cfg.cli, true);
const res = await this.crunchy.downloadFromSeriesID(data.id, {
dubLang: data.dubLang,
e: data.e
});
if (res.isOk) {
for (const select of res.value) {
if (!(await this.crunchy.downloadEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y',
novids: data.novids, noaudio: data.noaudio, hslang: data.hslang || 'none' }))) {
const er = new Error(`Unable to download episode ${data.e} from ${data.id}`);
er.name = 'Download error';
this.alertError(er);
public async availableDubCodes(): Promise<string[]> {
const dubLanguageCodesArray: string[] = [];
for(const language of languages){
if (language.cr_locale)
dubLanguageCodesArray.push(language.code);
}
}
} else {
this.alertError(res.reason);
return [...new Set(dubLanguageCodesArray)];
}
public async availableSubCodes(): Promise<string[]> {
return subtitleLanguagesFilter;
}
public async resolveItems(data: ResolveItemsData): Promise<boolean> {
this.getDefaults();
await this.crunchy.refreshToken(true);
console.debug(`Got resolve options: ${JSON.stringify(data)}`);
const res = await this.crunchy.downloadFromSeriesID(data.id, data);
if (!res.isOk)
return res.isOk;
this.addToQueue(res.value.map(a => {
return {
...data,
ids: a.data.map(a => a.mediaId),
title: a.episodeTitle,
parent: {
title: a.seasonTitle,
season: a.season.toString()
},
e: a.e,
image: a.image,
episode: a.episodeNumber
};
}));
return true;
}
public async search(data: SearchData): Promise<SearchResponse> {
this.getDefaults();
await this.crunchy.refreshToken(true);
if (!data['search-type']) data['search-type'] = 'series';
console.debug(`Got search options: ${JSON.stringify(data)}`);
const crunchySearch = await this.crunchy.doSearch(data);
if (!crunchySearch.isOk) {
this.crunchy.refreshToken();
return crunchySearch;
}
return { isOk: true, value: crunchySearch.value };
}
public async checkToken(): Promise<CheckTokenResponse> {
if (await this.crunchy.getProfile()) {
return { isOk: true, value: undefined };
} else {
return { isOk: false, reason: new Error('') };
}
}
public auth(data: AuthData) {
return this.crunchy.doAuth(data);
}
public async downloadItem(data: DownloadData) {
this.getDefaults();
await this.crunchy.refreshToken(true);
console.debug(`Got download options: ${JSON.stringify(data)}`);
this.setDownloading(true);
const _default = yargs.appArgv(this.crunchy.cfg.cli, true);
const res = await this.crunchy.downloadFromSeriesID(data.id, {
dubLang: data.dubLang,
e: data.e
});
if (res.isOk) {
for (const select of res.value) {
if (!(await this.crunchy.downloadEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y',
novids: data.novids, noaudio: data.noaudio, hslang: data.hslang || 'none' }))) {
const er = new Error(`Unable to download episode ${data.e} from ${data.id}`);
er.name = 'Download error';
this.alertError(er);
}
}
} else {
this.alertError(res.reason);
}
this.sendMessage({ name: 'finish', data: undefined });
this.setDownloading(false);
this.onFinish();
}
this.sendMessage({ name: 'finish', data: undefined });
this.setDownloading(false);
this.onFinish();
}
}
export default CrunchyHandler;

View file

@ -8,120 +8,120 @@ import { console } from '../../../modules/log';
import * as yargs from '../../../modules/module.app-args';
class HidiveHandler extends Base implements MessageHandler {
private hidive: Hidive;
public name = 'hidive';
constructor(ws: WebSocketHandler) {
super(ws);
this.hidive = new Hidive();
this.initState();
}
private hidive: Hidive;
public name = 'hidive';
constructor(ws: WebSocketHandler) {
super(ws);
this.hidive = new Hidive();
this.initState();
}
public async auth(data: AuthData) {
return this.hidive.doAuth(data);
}
public async auth(data: AuthData) {
return this.hidive.doAuth(data);
}
public async checkToken(): Promise<CheckTokenResponse> {
public async checkToken(): Promise<CheckTokenResponse> {
//TODO: implement proper method to check token
return { isOk: true, value: undefined };
}
public async search(data: SearchData): Promise<SearchResponse> {
console.debug(`Got search options: ${JSON.stringify(data)}`);
const hidiveSearch = await this.hidive.doSearch(data);
if (!hidiveSearch.isOk) {
return hidiveSearch;
return { isOk: true, value: undefined };
}
return { isOk: true, value: hidiveSearch.value };
}
public async handleDefault(name: string) {
return getDefault(name, this.hidive.cfg.cli);
}
public async availableDubCodes(): Promise<string[]> {
const dubLanguageCodesArray: string[] = [];
for(const language of languages){
if (language.new_hd_locale)
dubLanguageCodesArray.push(language.code);
public async search(data: SearchData): Promise<SearchResponse> {
console.debug(`Got search options: ${JSON.stringify(data)}`);
const hidiveSearch = await this.hidive.doSearch(data);
if (!hidiveSearch.isOk) {
return hidiveSearch;
}
return { isOk: true, value: hidiveSearch.value };
}
return [...new Set(dubLanguageCodesArray)];
}
public async availableSubCodes(): Promise<string[]> {
const subLanguageCodesArray: string[] = [];
for(const language of languages){
if (language.new_hd_locale)
subLanguageCodesArray.push(language.locale);
public async handleDefault(name: string) {
return getDefault(name, this.hidive.cfg.cli);
}
return ['all', 'none', ...new Set(subLanguageCodesArray)];
}
public async resolveItems(data: ResolveItemsData): Promise<boolean> {
const parse = parseInt(data.id);
if (isNaN(parse) || parse <= 0)
return false;
console.debug(`Got resolve options: ${JSON.stringify(data)}`);
const res = await this.hidive.selectSeries(parseInt(data.id), data.e, data.but, data.all);
if (!res.isOk || !res.value)
return res.isOk;
this.addToQueue(res.value.map(item => {
return {
...data,
ids: [item.id],
title: item.title,
parent: {
title: item.seriesTitle,
season: item.episodeInformation.seasonNumber+''
},
image: item.thumbnailUrl,
e: item.episodeInformation.episodeNumber+'',
episode: item.episodeInformation.episodeNumber+'',
};
}));
return true;
}
public async listEpisodes(id: string): Promise<EpisodeListResponse> {
const parse = parseInt(id);
if (isNaN(parse) || parse <= 0)
return { isOk: false, reason: new Error('The ID is invalid') };
const request = await this.hidive.listSeries(parse);
if (!request.isOk || !request.value)
return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')};
return { isOk: true, value: request.value.map(function(item) {
const description = item.description.split('\r\n');
return {
e: item.episodeInformation.episodeNumber+'',
lang: [],
name: item.title,
season: item.episodeInformation.seasonNumber+'',
seasonTitle: request.series.seasons[item.episodeInformation.seasonNumber-1]?.title ?? request.series.title,
episode: item.episodeInformation.episodeNumber+'',
id: item.id+'',
img: item.thumbnailUrl,
description: description ? description[0] : '',
time: ''
};
})};
}
public async downloadItem(data: DownloadData) {
this.setDownloading(true);
console.debug(`Got download options: ${JSON.stringify(data)}`);
const _default = yargs.appArgv(this.hidive.cfg.cli, true);
const res = await this.hidive.selectSeries(parseInt(data.id), data.e, false, false);
if (!res.isOk || !res.showData)
return this.alertError(new Error('Download failed upstream, check for additional logs'));
for (const ep of res.value) {
await this.hidive.downloadEpisode(ep, {..._default, callbackMaker: this.makeProgressHandler.bind(this), dubLang: data.dubLang, dlsubs: data.dlsubs, fileName: data.fileName, q: data.q, force: 'y', noaudio: data.noaudio, novids: data.novids });
public async availableDubCodes(): Promise<string[]> {
const dubLanguageCodesArray: string[] = [];
for(const language of languages){
if (language.new_hd_locale)
dubLanguageCodesArray.push(language.code);
}
return [...new Set(dubLanguageCodesArray)];
}
public async availableSubCodes(): Promise<string[]> {
const subLanguageCodesArray: string[] = [];
for(const language of languages){
if (language.new_hd_locale)
subLanguageCodesArray.push(language.locale);
}
return ['all', 'none', ...new Set(subLanguageCodesArray)];
}
public async resolveItems(data: ResolveItemsData): Promise<boolean> {
const parse = parseInt(data.id);
if (isNaN(parse) || parse <= 0)
return false;
console.debug(`Got resolve options: ${JSON.stringify(data)}`);
const res = await this.hidive.selectSeries(parseInt(data.id), data.e, data.but, data.all);
if (!res.isOk || !res.value)
return res.isOk;
this.addToQueue(res.value.map(item => {
return {
...data,
ids: [item.id],
title: item.title,
parent: {
title: item.seriesTitle,
season: item.episodeInformation.seasonNumber+''
},
image: item.thumbnailUrl,
e: item.episodeInformation.episodeNumber+'',
episode: item.episodeInformation.episodeNumber+'',
};
}));
return true;
}
public async listEpisodes(id: string): Promise<EpisodeListResponse> {
const parse = parseInt(id);
if (isNaN(parse) || parse <= 0)
return { isOk: false, reason: new Error('The ID is invalid') };
const request = await this.hidive.listSeries(parse);
if (!request.isOk || !request.value)
return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')};
return { isOk: true, value: request.value.map(function(item) {
const description = item.description.split('\r\n');
return {
e: item.episodeInformation.episodeNumber+'',
lang: [],
name: item.title,
season: item.episodeInformation.seasonNumber+'',
seasonTitle: request.series.seasons[item.episodeInformation.seasonNumber-1]?.title ?? request.series.title,
episode: item.episodeInformation.episodeNumber+'',
id: item.id+'',
img: item.thumbnailUrl,
description: description ? description[0] : '',
time: ''
};
})};
}
public async downloadItem(data: DownloadData) {
this.setDownloading(true);
console.debug(`Got download options: ${JSON.stringify(data)}`);
const _default = yargs.appArgv(this.hidive.cfg.cli, true);
const res = await this.hidive.selectSeries(parseInt(data.id), data.e, false, false);
if (!res.isOk || !res.showData)
return this.alertError(new Error('Download failed upstream, check for additional logs'));
for (const ep of res.value) {
await this.hidive.downloadEpisode(ep, {..._default, callbackMaker: this.makeProgressHandler.bind(this), dubLang: data.dubLang, dlsubs: data.dlsubs, fileName: data.fileName, q: data.q, force: 'y', noaudio: data.noaudio, novids: data.novids });
}
this.sendMessage({ name: 'finish', data: undefined });
this.setDownloading(false);
this.onFinish();
}
this.sendMessage({ name: 'finish', data: undefined });
this.setDownloading(false);
this.onFinish();
}
}
export default HidiveHandler;

View file

@ -16,108 +16,108 @@ class ExternalEvent extends EventEmitter {}
export default class WebSocketHandler {
private wsServer: ws.Server;
private wsServer: ws.Server;
public events: ExternalEvent = new ExternalEvent();
public events: ExternalEvent = new ExternalEvent();
constructor(server: Server) {
this.wsServer = new ws.WebSocketServer({ noServer: true, path: '/private' });
constructor(server: Server) {
this.wsServer = new ws.WebSocketServer({ noServer: true, path: '/private' });
this.wsServer.on('connection', (socket, req) => {
console.info(`[WS] Connection from '${req.socket.remoteAddress}'`);
socket.on('error', (er) => console.error(`[WS] ${er}`));
socket.on('message', (data) => {
const json = JSON.parse(data.toString()) as UnknownWSMessage;
this.events.emit(json.name, json as any, (data) => {
this.wsServer.clients.forEach(client => {
if (client.readyState !== WebSocket.OPEN)
return;
client.send(JSON.stringify({
data,
id: json.id,
name: json.name
}), (er) => {
if (er)
console.error(`[WS] ${er}`);
this.wsServer.on('connection', (socket, req) => {
console.info(`[WS] Connection from '${req.socket.remoteAddress}'`);
socket.on('error', (er) => console.error(`[WS] ${er}`));
socket.on('message', (data) => {
const json = JSON.parse(data.toString()) as UnknownWSMessage;
this.events.emit(json.name, json as any, (data) => {
this.wsServer.clients.forEach(client => {
if (client.readyState !== WebSocket.OPEN)
return;
client.send(JSON.stringify({
data,
id: json.id,
name: json.name
}), (er) => {
if (er)
console.error(`[WS] ${er}`);
});
});
});
});
});
});
});
});
server.on('upgrade', (request, socket, head) => {
if (!this.wsServer.shouldHandle(request))
return;
if (!this.authenticate(request)) {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.destroy();
console.info(`[WS] ${request.socket.remoteAddress} tried to connect but used a wrong password.`);
return;
}
this.wsServer.handleUpgrade(request, socket, head, socket => {
this.wsServer.emit('connection', socket, request);
});
});
}
server.on('upgrade', (request, socket, head) => {
if (!this.wsServer.shouldHandle(request))
return;
if (!this.authenticate(request)) {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.destroy();
console.info(`[WS] ${request.socket.remoteAddress} tried to connect but used a wrong password.`);
return;
}
this.wsServer.handleUpgrade(request, socket, head, socket => {
this.wsServer.emit('connection', socket, request);
});
});
}
public sendMessage<T extends keyof RandomEvents>(data: RandomEvent<T>) {
this.wsServer.clients.forEach(client => {
if (client.readyState !== WebSocket.OPEN)
return;
client.send(JSON.stringify(data), (er) => {
if (er)
console.error(`[WS] ${er}`);
});
});
}
public sendMessage<T extends keyof RandomEvents>(data: RandomEvent<T>) {
this.wsServer.clients.forEach(client => {
if (client.readyState !== WebSocket.OPEN)
return;
client.send(JSON.stringify(data), (er) => {
if (er)
console.error(`[WS] ${er}`);
});
});
}
private authenticate(request: IncomingMessage): boolean {
const search = new URL(`http://${request.headers.host}${request.url}`).searchParams;
return cfg.gui.password === (search.get('password') ?? undefined);
}
private authenticate(request: IncomingMessage): boolean {
const search = new URL(`http://${request.headers.host}${request.url}`).searchParams;
return cfg.gui.password === (search.get('password') ?? undefined);
}
}
export class PublicWebSocket {
private wsServer: ws.Server;
private wsServer: ws.Server;
private state = getState();
constructor(server: Server) {
this.wsServer = new ws.WebSocketServer({ noServer: true, path: '/public' });
private state = getState();
constructor(server: Server) {
this.wsServer = new ws.WebSocketServer({ noServer: true, path: '/public' });
this.wsServer.on('connection', (socket, req) => {
console.info(`[WS] Connection to public ws from '${req.socket.remoteAddress}'`);
socket.on('error', (er) => console.error(`[WS] ${er}`));
socket.on('message', (msg) => {
const data = JSON.parse(msg.toString()) as UnknownWSMessage;
switch (data.name) {
case 'isSetup':
this.send(socket, data.id, data.name, this.state.setup);
break;
case 'requirePassword':
this.send(socket, data.id, data.name, cfg.gui.password !== undefined);
break;
}
});
});
this.wsServer.on('connection', (socket, req) => {
console.info(`[WS] Connection to public ws from '${req.socket.remoteAddress}'`);
socket.on('error', (er) => console.error(`[WS] ${er}`));
socket.on('message', (msg) => {
const data = JSON.parse(msg.toString()) as UnknownWSMessage;
switch (data.name) {
case 'isSetup':
this.send(socket, data.id, data.name, this.state.setup);
break;
case 'requirePassword':
this.send(socket, data.id, data.name, cfg.gui.password !== undefined);
break;
}
});
});
server.on('upgrade', (request, socket, head) => {
if (!this.wsServer.shouldHandle(request))
return;
this.wsServer.handleUpgrade(request, socket, head, socket => {
this.wsServer.emit('connection', socket, request);
});
});
}
server.on('upgrade', (request, socket, head) => {
if (!this.wsServer.shouldHandle(request))
return;
this.wsServer.handleUpgrade(request, socket, head, socket => {
this.wsServer.emit('connection', socket, request);
});
});
}
private send(client: ws.WebSocket, id: string, name: string, data: any) {
client.send(JSON.stringify({
data,
id,
name
}), (er) => {
if (er)
console.error(`[WS] ${er}`);
});
}
private send(client: ws.WebSocket, id: string, name: string, data: any) {
client.send(JSON.stringify({
data,
id,
name
}), (er) => {
if (er)
console.error(`[WS] ${er}`);
});
}
}

1960
hidive.ts

File diff suppressed because it is too large Load diff

174
index.ts
View file

@ -7,94 +7,94 @@ import { makeCommand, addToArchive } from './modules/module.downloadArchive';
import update from './modules/module.updater';
(async () => {
const cfg = yamlCfg.loadCfg();
const argv = appArgv(cfg.cli);
if (!argv.skipUpdate)
await update(argv.update);
const cfg = yamlCfg.loadCfg();
const argv = appArgv(cfg.cli);
if (!argv.skipUpdate)
await update(argv.update);
if (argv.all && argv.but) {
console.error('--all and --but exclude each other!');
return;
}
if (argv.all && argv.but) {
console.error('--all and --but exclude each other!');
return;
}
if (argv.addArchive) {
if (argv.service === 'crunchy') {
if (argv.s === undefined && argv.series === undefined)
return console.error('`-s` or `--srz` not found');
if (argv.s && argv.series)
return console.error('Both `-s` and `--srz` found');
addToArchive({
service: 'crunchy',
type: argv.s === undefined ? 'srz' : 's'
}, (argv.s === undefined ? argv.series : argv.s) as string);
console.info('Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s));
} else if (argv.service === 'hidive') {
if (argv.s === undefined)
return console.error('`-s` not found');
addToArchive({
service: 'hidive',
//type: argv.s === undefined ? 'srz' : 's'
type: 's'
}, (argv.s === undefined ? argv.series : argv.s) as string);
console.info('Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s));
} else if (argv.service === 'ao') {
if (argv.s === undefined)
return console.error('`-s` not found');
addToArchive({
service: 'hidive',
//type: argv.s === undefined ? 'srz' : 's'
type: 's'
}, (argv.s === undefined ? argv.series : argv.s) as string);
console.info('Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s));
if (argv.addArchive) {
if (argv.service === 'crunchy') {
if (argv.s === undefined && argv.series === undefined)
return console.error('`-s` or `--srz` not found');
if (argv.s && argv.series)
return console.error('Both `-s` and `--srz` found');
addToArchive({
service: 'crunchy',
type: argv.s === undefined ? 'srz' : 's'
}, (argv.s === undefined ? argv.series : argv.s) as string);
console.info('Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s));
} else if (argv.service === 'hidive') {
if (argv.s === undefined)
return console.error('`-s` not found');
addToArchive({
service: 'hidive',
//type: argv.s === undefined ? 'srz' : 's'
type: 's'
}, (argv.s === undefined ? argv.series : argv.s) as string);
console.info('Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s));
} else if (argv.service === 'ao') {
if (argv.s === undefined)
return console.error('`-s` not found');
addToArchive({
service: 'hidive',
//type: argv.s === undefined ? 'srz' : 's'
type: 's'
}, (argv.s === undefined ? argv.series : argv.s) as string);
console.info('Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s));
}
} else if (argv.downloadArchive) {
const ids = makeCommand(argv.service);
for (const id of ids) {
overrideArguments(cfg.cli, id);
/* Reimport module to override appArgv */
Object.keys(require.cache).forEach(key => {
if (key.endsWith('crunchy.js') || key.endsWith('hidive.js') || key.endsWith('ao.js'))
delete require.cache[key];
});
let service: ServiceClass;
switch(argv.service) {
case 'crunchy':
service = new (await import('./crunchy')).default;
break;
case 'hidive':
service = new (await import('./hidive')).default;
break;
case 'ao':
service = new (await import('./ao')).default;
break;
case 'adn':
service = new (await import('./adn')).default;
break;
default:
service = new (await import(`./${argv.service}`)).default;
break;
}
await service.cli();
}
} else {
let service: ServiceClass;
switch(argv.service) {
case 'crunchy':
service = new (await import('./crunchy')).default;
break;
case 'hidive':
service = new (await import('./hidive')).default;
break;
case 'ao':
service = new (await import('./ao')).default;
break;
case 'adn':
service = new (await import('./adn')).default;
break;
default:
service = new (await import(`./${argv.service}`)).default;
break;
}
await service.cli();
}
} else if (argv.downloadArchive) {
const ids = makeCommand(argv.service);
for (const id of ids) {
overrideArguments(cfg.cli, id);
/* Reimport module to override appArgv */
Object.keys(require.cache).forEach(key => {
if (key.endsWith('crunchy.js') || key.endsWith('hidive.js') || key.endsWith('ao.js'))
delete require.cache[key];
});
let service: ServiceClass;
switch(argv.service) {
case 'crunchy':
service = new (await import('./crunchy')).default;
break;
case 'hidive':
service = new (await import('./hidive')).default;
break;
case 'ao':
service = new (await import('./ao')).default;
break;
case 'adn':
service = new (await import('./adn')).default;
break;
default:
service = new (await import(`./${argv.service}`)).default;
break;
}
await service.cli();
}
} else {
let service: ServiceClass;
switch(argv.service) {
case 'crunchy':
service = new (await import('./crunchy')).default;
break;
case 'hidive':
service = new (await import('./hidive')).default;
break;
case 'ao':
service = new (await import('./ao')).default;
break;
case 'adn':
service = new (await import('./adn')).default;
break;
default:
service = new (await import(`./${argv.service}`)).default;
break;
}
await service.cli();
}
})();

View file

@ -4,27 +4,27 @@ import path from 'path';
import { args, groups } from './module.args';
const transformService = (str: Array<'crunchy'|'hidive'|'ao'|'adn'|'all'>) => {
const services: string[] = [];
str.forEach(function(part) {
switch(part) {
case 'crunchy':
services.push('Crunchyroll');
break;
case 'hidive':
services.push('Hidive');
break;
case 'ao':
services.push('AnimeOnegai');
break;
case 'adn':
services.push('AnimationDigitalNetwork');
break;
case 'all':
services.push('All');
break;
}
});
return services.join(', ');
const services: string[] = [];
str.forEach(function(part) {
switch(part) {
case 'crunchy':
services.push('Crunchyroll');
break;
case 'hidive':
services.push('Hidive');
break;
case 'ao':
services.push('AnimeOnegai');
break;
case 'adn':
services.push('AnimationDigitalNetwork');
break;
case 'all':
services.push('All');
break;
}
});
return services.join(', ');
};
let docs = `# ${packageJSON.name} (v${packageJSON.version})
@ -45,30 +45,30 @@ This tool is not responsible for your actions; please make an informed decision
`;
Object.entries(groups).forEach(([key, value]) => {
docs += `\n### ${value.slice(0, -1)}\n`;
docs += `\n### ${value.slice(0, -1)}\n`;
docs += args.filter(a => a.group === key).map(argument => {
return [`#### \`${argument.name.length > 1 ? '--' : '-'}${argument.name}\``,
`| **Service** | **Usage** | **Type** | **Required** | **Alias** | ${argument.choices ? '**Choices** |' : ''} ${argument.default ? '**Default** |' : ''}**cli-default Entry**`,
`| --- | --- | --- | --- | --- | ${argument.choices ? '--- | ' : ''}${argument.default ? '--- | ' : ''}---| `,
`| ${transformService(argument.service)} | \`${argument.name.length > 1 ? '--' : '-'}${argument.name} ${argument.usage}\` | \`${argument.type}\` | \`${argument.demandOption ? 'Yes' : 'No'}\`|`
docs += args.filter(a => a.group === key).map(argument => {
return [`#### \`${argument.name.length > 1 ? '--' : '-'}${argument.name}\``,
`| **Service** | **Usage** | **Type** | **Required** | **Alias** | ${argument.choices ? '**Choices** |' : ''} ${argument.default ? '**Default** |' : ''}**cli-default Entry**`,
`| --- | --- | --- | --- | --- | ${argument.choices ? '--- | ' : ''}${argument.default ? '--- | ' : ''}---| `,
`| ${transformService(argument.service)} | \`${argument.name.length > 1 ? '--' : '-'}${argument.name} ${argument.usage}\` | \`${argument.type}\` | \`${argument.demandOption ? 'Yes' : 'No'}\`|`
+ ` \`${(argument.alias ? `${argument.alias.length > 1 ? '--' : '-'}${argument.alias}` : undefined) ?? 'NaN'}\` |`
+ `${argument.choices ? ` [${argument.choices.map(a => `\`${a || '\'\''}\``).join(', ')}] |` : ''}`
+ `${argument.default ? ` \`${
typeof argument.default === 'object'
? Array.isArray(argument.default)
? JSON.stringify(argument.default)
: (argument.default as any).default
: argument.default
typeof argument.default === 'object'
? Array.isArray(argument.default)
? JSON.stringify(argument.default)
: (argument.default as any).default
: argument.default
}\`|` : ''}`
+ ` ${typeof argument.default === 'object' && !Array.isArray(argument.default)
? `\`${argument.default.name || argument.name}: \``
: '`NaN`'
? `\`${argument.default.name || argument.name}: \``
: '`NaN`'
} |`,
'',
argument.docDescribe === true ? argument.describe : argument.docDescribe
].join('\n');
}).join('\n');
'',
argument.docDescribe === true ? argument.describe : argument.docDescribe
].join('\n');
}).join('\n');
});

View file

@ -15,104 +15,104 @@ const nodeVer = 'node20-';
type BuildTypes = `${'windows'|'macos'|'linux'|'linuxstatic'|'alpine'}-${'x64'|'arm64'}`|'linuxstatic-armv7'
(async () => {
const buildType = process.argv[2] as BuildTypes;
const isGUI = process.argv[3] === 'true';
const buildType = process.argv[2] as BuildTypes;
const isGUI = process.argv[3] === 'true';
buildBinary(buildType, isGUI);
buildBinary(buildType, isGUI);
})();
// main
async function buildBinary(buildType: BuildTypes, gui: boolean) {
const buildStr = 'multi-downloader-nx';
const acceptablePlatforms = ['windows','linux','linuxstatic','macos','alpine'];
const acceptableArchs = ['x64','arm64'];
const acceptableBuilds: string[] = ['linuxstatic-armv7'];
for (const platform of acceptablePlatforms) {
for (const arch of acceptableArchs) {
acceptableBuilds.push(platform+'-'+arch);
const buildStr = 'multi-downloader-nx';
const acceptablePlatforms = ['windows','linux','linuxstatic','macos','alpine'];
const acceptableArchs = ['x64','arm64'];
const acceptableBuilds: string[] = ['linuxstatic-armv7'];
for (const platform of acceptablePlatforms) {
for (const arch of acceptableArchs) {
acceptableBuilds.push(platform+'-'+arch);
}
}
}
if(!acceptableBuilds.includes(buildType)){
console.error('Unknown build type!');
process.exit(1);
}
await modulesCleanup('.');
if(!fs.existsSync(buildsDir)){
fs.mkdirSync(buildsDir);
}
const buildFull = `${buildStr}-${getFriendlyName(buildType)}-${gui ? 'gui' : 'cli'}`;
const buildDir = `${buildsDir}/${buildFull}`;
if(fs.existsSync(buildDir)){
fs.removeSync(buildDir);
}
fs.mkdirSync(buildDir);
console.info('Running esbuild');
if(!acceptableBuilds.includes(buildType)){
console.error('Unknown build type!');
process.exit(1);
}
await modulesCleanup('.');
if(!fs.existsSync(buildsDir)){
fs.mkdirSync(buildsDir);
}
const buildFull = `${buildStr}-${getFriendlyName(buildType)}-${gui ? 'gui' : 'cli'}`;
const buildDir = `${buildsDir}/${buildFull}`;
if(fs.existsSync(buildDir)){
fs.removeSync(buildDir);
}
fs.mkdirSync(buildDir);
console.info('Running esbuild');
const build = await esbuild.build({
entryPoints: [
gui ? 'gui.js' : 'index.js',
],
sourceRoot: './',
bundle: true,
platform: 'node',
format: 'cjs',
treeShaking: true,
// External source map for debugging
sourcemap: true,
// Minify and keep the original names
minify: true,
keepNames: true,
outfile: path.join(buildsDir, 'index.cjs'),
metafile: true,
external: ['cheerio', 'sleep', ...builtinModules]
});
const build = await esbuild.build({
entryPoints: [
gui ? 'gui.js' : 'index.js',
],
sourceRoot: './',
bundle: true,
platform: 'node',
format: 'cjs',
treeShaking: true,
// External source map for debugging
sourcemap: true,
// Minify and keep the original names
minify: true,
keepNames: true,
outfile: path.join(buildsDir, 'index.cjs'),
metafile: true,
external: ['cheerio', 'sleep', ...builtinModules]
});
if (build.errors?.length > 0) console.error(build.errors);
if (build.warnings?.length > 0) console.warn(build.warnings);
if (build.errors?.length > 0) console.error(build.errors);
if (build.warnings?.length > 0) console.warn(build.warnings);
const buildConfig = [
`${buildsDir}/index.cjs`,
'--target', nodeVer + buildType,
'--output', `${buildDir}/${pkg.short_name}`,
'--compress', 'GZip'
];
console.info(`[Build] Build configuration: ${buildFull}`);
try {
await exec(buildConfig);
}
catch(e){
console.info(e);
process.exit(1);
}
fs.mkdirSync(`${buildDir}/config`);
fs.mkdirSync(`${buildDir}/videos`);
fs.mkdirSync(`${buildDir}/widevine`);
fs.mkdirSync(`${buildDir}/playready`);
fs.copySync('./config/bin-path.yml', `${buildDir}/config/bin-path.yml`);
fs.copySync('./config/cli-defaults.yml', `${buildDir}/config/cli-defaults.yml`);
fs.copySync('./config/dir-path.yml', `${buildDir}/config/dir-path.yml`);
fs.copySync('./config/gui.yml', `${buildDir}/config/gui.yml`);
fs.copySync('./modules/cmd-here.bat', `${buildDir}/cmd-here.bat`);
fs.copySync('./modules/NotoSans-Regular.ttf', `${buildDir}/NotoSans-Regular.ttf`);
fs.copySync('./package.json', `${buildDir}/package.json`);
fs.copySync('./docs/', `${buildDir}/docs/`);
fs.copySync('./LICENSE.md', `${buildDir}/docs/LICENSE.md`);
if (gui) {
fs.copySync('./gui', `${buildDir}/gui`);
fs.copySync('./node_modules/open/xdg-open', `${buildDir}/xdg-open`);
}
if(fs.existsSync(`${buildsDir}/${buildFull}.7z`)){
fs.removeSync(`${buildsDir}/${buildFull}.7z`);
}
execSync(`7z a -t7z "${buildsDir}/${buildFull}.7z" "${buildDir}"`,{stdio:[0,1,2]});
const buildConfig = [
`${buildsDir}/index.cjs`,
'--target', nodeVer + buildType,
'--output', `${buildDir}/${pkg.short_name}`,
'--compress', 'GZip'
];
console.info(`[Build] Build configuration: ${buildFull}`);
try {
await exec(buildConfig);
}
catch(e){
console.info(e);
process.exit(1);
}
fs.mkdirSync(`${buildDir}/config`);
fs.mkdirSync(`${buildDir}/videos`);
fs.mkdirSync(`${buildDir}/widevine`);
fs.mkdirSync(`${buildDir}/playready`);
fs.copySync('./config/bin-path.yml', `${buildDir}/config/bin-path.yml`);
fs.copySync('./config/cli-defaults.yml', `${buildDir}/config/cli-defaults.yml`);
fs.copySync('./config/dir-path.yml', `${buildDir}/config/dir-path.yml`);
fs.copySync('./config/gui.yml', `${buildDir}/config/gui.yml`);
fs.copySync('./modules/cmd-here.bat', `${buildDir}/cmd-here.bat`);
fs.copySync('./modules/NotoSans-Regular.ttf', `${buildDir}/NotoSans-Regular.ttf`);
fs.copySync('./package.json', `${buildDir}/package.json`);
fs.copySync('./docs/', `${buildDir}/docs/`);
fs.copySync('./LICENSE.md', `${buildDir}/docs/LICENSE.md`);
if (gui) {
fs.copySync('./gui', `${buildDir}/gui`);
fs.copySync('./node_modules/open/xdg-open', `${buildDir}/xdg-open`);
}
if(fs.existsSync(`${buildsDir}/${buildFull}.7z`)){
fs.removeSync(`${buildsDir}/${buildFull}.7z`);
}
execSync(`7z a -t7z "${buildsDir}/${buildFull}.7z" "${buildDir}"`,{stdio:[0,1,2]});
}
function getFriendlyName(buildString: string): string {
if (buildString.includes('armv7')) {
return 'android';
}
if (buildString.includes('linuxstatic')) {
buildString = buildString.replace('linuxstatic', 'linux');
}
return buildString;
if (buildString.includes('armv7')) {
return 'android';
}
if (buildString.includes('linuxstatic')) {
buildString = buildString.replace('linuxstatic', 'linux');
}
return buildString;
}

View file

@ -10,176 +10,176 @@ import { ofetch } from 'ofetch';
//read cdm files located in the same directory
let privateKey: Buffer = Buffer.from([]),
identifierBlob: Buffer = Buffer.from([]),
prd: Buffer = Buffer.from([]),
prd_cdm: Cdm | undefined;
identifierBlob: Buffer = Buffer.from([]),
prd: Buffer = Buffer.from([]),
prd_cdm: Cdm | undefined;
export let cdm: 'widevine' | 'playready';
export let canDecrypt: boolean;
try {
const files_prd = fs.readdirSync(path.join(workingDir, 'playready'));
const prd_file_found = files_prd.find((f) => f.includes('.prd'));
try {
if (prd_file_found) {
const file_prd = path.join(workingDir, 'playready', prd_file_found);
const stats = fs.statSync(file_prd);
if (stats.size < 1024 * 8 && stats.isFile()) {
const fileContents = fs.readFileSync(file_prd, {
encoding: 'utf8'
});
if (fileContents.includes('CERT')) {
prd = fs.readFileSync(file_prd);
const device = Device.loads(prd);
prd_cdm = Cdm.fromDevice(device);
const files_prd = fs.readdirSync(path.join(workingDir, 'playready'));
const prd_file_found = files_prd.find((f) => f.includes('.prd'));
try {
if (prd_file_found) {
const file_prd = path.join(workingDir, 'playready', prd_file_found);
const stats = fs.statSync(file_prd);
if (stats.size < 1024 * 8 && stats.isFile()) {
const fileContents = fs.readFileSync(file_prd, {
encoding: 'utf8'
});
if (fileContents.includes('CERT')) {
prd = fs.readFileSync(file_prd);
const device = Device.loads(prd);
prd_cdm = Cdm.fromDevice(device);
}
}
}
}
} catch (e) {
console.error('Error loading Playready CDM, ensure the CDM is provisioned as a V3 Device and not malformed. For more informations read the readme.');
prd = Buffer.from([]);
}
} catch (e) {
console.error('Error loading Playready CDM, ensure the CDM is provisioned as a V3 Device and not malformed. For more informations read the readme.');
prd = Buffer.from([]);
}
const files_wvd = fs.readdirSync(path.join(workingDir, 'widevine'));
try {
files_wvd.forEach(function (file) {
file = path.join(workingDir, 'widevine', file);
const stats = fs.statSync(file);
if (stats.size < 1024 * 8 && stats.isFile()) {
const fileContents = fs.readFileSync(file, { encoding: 'utf8' });
if ((fileContents.startsWith('-----BEGIN RSA PRIVATE KEY-----') && fileContents.endsWith('-----END RSA PRIVATE KEY-----')) || (fileContents.startsWith('-----BEGIN PRIVATE KEY-----') && fileContents.endsWith('-----END PRIVATE KEY-----'))) {
privateKey = fs.readFileSync(file);
}
if (fileContents.includes('widevine_cdm_version') && fileContents.includes('oem_crypto_security_patch_level') && !fileContents.startsWith('WVD')) {
identifierBlob = fs.readFileSync(file);
}
if (fileContents.startsWith('WVD')) {
console.warn('Found WVD file in folder, AniDL currently only supports device_client_id_blob and device_private_key, make sure to have them in the widevine folder.');
}
}
});
} catch (e) {
console.error('Error loading Widevine CDM, malformed client blob or private key.');
privateKey = Buffer.from([]);
identifierBlob = Buffer.from([]);
}
const files_wvd = fs.readdirSync(path.join(workingDir, 'widevine'));
try {
files_wvd.forEach(function (file) {
file = path.join(workingDir, 'widevine', file);
const stats = fs.statSync(file);
if (stats.size < 1024 * 8 && stats.isFile()) {
const fileContents = fs.readFileSync(file, { encoding: 'utf8' });
if ((fileContents.startsWith('-----BEGIN RSA PRIVATE KEY-----') && fileContents.endsWith('-----END RSA PRIVATE KEY-----')) || (fileContents.startsWith('-----BEGIN PRIVATE KEY-----') && fileContents.endsWith('-----END PRIVATE KEY-----'))) {
privateKey = fs.readFileSync(file);
}
if (fileContents.includes('widevine_cdm_version') && fileContents.includes('oem_crypto_security_patch_level') && !fileContents.startsWith('WVD')) {
identifierBlob = fs.readFileSync(file);
}
if (fileContents.startsWith('WVD')) {
console.warn('Found WVD file in folder, AniDL currently only supports device_client_id_blob and device_private_key, make sure to have them in the widevine folder.');
}
}
});
} catch (e) {
console.error('Error loading Widevine CDM, malformed client blob or private key.');
privateKey = Buffer.from([]);
identifierBlob = Buffer.from([]);
}
if (privateKey.length !== 0 && identifierBlob.length !== 0) {
cdm = 'widevine';
canDecrypt = true;
} else if (prd.length !== 0) {
cdm = 'playready';
canDecrypt = true;
} else if (privateKey.length === 0 && identifierBlob.length !== 0) {
console.warn('Private key missing');
canDecrypt = false;
} else if (identifierBlob.length === 0 && privateKey.length !== 0) {
console.warn('Identifier blob missing');
canDecrypt = false;
} else if (prd.length == 0) {
canDecrypt = false;
} else {
canDecrypt = false;
}
if (privateKey.length !== 0 && identifierBlob.length !== 0) {
cdm = 'widevine';
canDecrypt = true;
} else if (prd.length !== 0) {
cdm = 'playready';
canDecrypt = true;
} else if (privateKey.length === 0 && identifierBlob.length !== 0) {
console.warn('Private key missing');
canDecrypt = false;
} else if (identifierBlob.length === 0 && privateKey.length !== 0) {
console.warn('Identifier blob missing');
canDecrypt = false;
} else if (prd.length == 0) {
canDecrypt = false;
} else {
canDecrypt = false;
}
} catch (e) {
console.error(e);
canDecrypt = false;
console.error(e);
canDecrypt = false;
}
export async function getKeysWVD(pssh: string | undefined, licenseServer: string, authData: Record<string, string>): Promise<KeyContainer[]> {
if (!pssh || !canDecrypt) return [];
//pssh found in the mpd manifest
const psshBuffer = Buffer.from(pssh, 'base64');
if (!pssh || !canDecrypt) return [];
//pssh found in the mpd manifest
const psshBuffer = Buffer.from(pssh, 'base64');
//Create a new widevine session
const session = new Session({ privateKey, identifierBlob }, psshBuffer);
//Create a new widevine session
const session = new Session({ privateKey, identifierBlob }, psshBuffer);
//Generate license
const data = await ofetch(licenseServer, {
method: 'POST',
body: session.createLicenseRequest(),
headers: authData,
responseType: 'arrayBuffer'
}).catch((error) => {
if (error.status && error.statusText) {
console.error(`${error.name} ${error.status}: ${error.statusText}`);
} else {
console.error(`${error.name}: ${error.message}`);
}
//Generate license
const data = await ofetch(licenseServer, {
method: 'POST',
body: session.createLicenseRequest(),
headers: authData,
responseType: 'arrayBuffer'
}).catch((error) => {
if (error.status && error.statusText) {
console.error(`${error.name} ${error.status}: ${error.statusText}`);
} else {
console.error(`${error.name}: ${error.message}`);
}
if (!error.data) return;
const data = error.data instanceof ArrayBuffer ? new TextDecoder().decode(error.data) : error.data;
if (data) {
const docTitle = data.match(/<title>(.*)<\/title>/);
if (docTitle) {
console.error(docTitle[1]);
}
if (error.status && error.status != 404 && error.status != 403) {
console.error('Body:', data);
}
}
});
if (!error.data) return;
const data = error.data instanceof ArrayBuffer ? new TextDecoder().decode(error.data) : error.data;
if (data) {
const docTitle = data.match(/<title>(.*)<\/title>/);
if (docTitle) {
console.error(docTitle[1]);
}
if (error.status && error.status != 404 && error.status != 403) {
console.error('Body:', data);
}
}
});
if (data) {
//Parse License and return keys
const text = new TextDecoder().decode(data);
try {
const json = JSON.parse(text);
return session.parseLicense(Buffer.from(json['license'], 'base64')) as KeyContainer[];
} catch {
return session.parseLicense(Buffer.from(new Uint8Array(data))) as KeyContainer[];
const text = new TextDecoder().decode(data);
try {
const json = JSON.parse(text);
return session.parseLicense(Buffer.from(json['license'], 'base64')) as KeyContainer[];
} catch {
return session.parseLicense(Buffer.from(new Uint8Array(data))) as KeyContainer[];
}
} else {
console.error('License request failed');
return [];
}
} else {
console.error('License request failed');
return [];
}
}
export async function getKeysPRD(pssh: string | undefined, licenseServer: string, authData: Record<string, string>): Promise<KeyContainer[]> {
if (!pssh || !canDecrypt || !prd_cdm) return [];
const pssh_parsed = new PSSH(pssh);
if (!pssh || !canDecrypt || !prd_cdm) return [];
const pssh_parsed = new PSSH(pssh);
//Create a new playready session
const session = prd_cdm.getLicenseChallenge(pssh_parsed.get_wrm_headers(true)[0]);
//Create a new playready session
const session = prd_cdm.getLicenseChallenge(pssh_parsed.get_wrm_headers(true)[0]);
//Generate license
const data = await ofetch(licenseServer, {
method: 'POST',
body: session,
headers: authData,
responseType: 'text'
}).catch((error) => {
if (error && error.status && error.statusText) {
console.error(`${error.name} ${error.status}: ${error.statusText}`);
} else {
console.error(`${error.name}: ${error.message}`);
}
//Generate license
const data = await ofetch(licenseServer, {
method: 'POST',
body: session,
headers: authData,
responseType: 'text'
}).catch((error) => {
if (error && error.status && error.statusText) {
console.error(`${error.name} ${error.status}: ${error.statusText}`);
} else {
console.error(`${error.name}: ${error.message}`);
}
if (!error.data) return;
const docTitle = error.data.match(/<title>(.*)<\/title>/);
if (docTitle) {
console.error(docTitle[1]);
}
if (error.status && error.status != 404 && error.status != 403) {
console.error('Body:', error.data);
}
});
if (!error.data) return;
const docTitle = error.data.match(/<title>(.*)<\/title>/);
if (docTitle) {
console.error(docTitle[1]);
}
if (error.status && error.status != 404 && error.status != 403) {
console.error('Body:', error.data);
}
});
if (data) {
if (data) {
//Parse License and return keys
try {
const keys = prd_cdm.parseLicense(data);
try {
const keys = prd_cdm.parseLicense(data);
return keys.map((k) => {
return {
kid: k.key_id,
key: k.key
};
});
} catch {
console.error('License parsing failed');
return [];
return keys.map((k) => {
return {
kid: k.key_id,
key: k.key
};
});
} catch {
console.error('License parsing failed');
return [];
}
} else {
console.error('License request failed');
return [];
}
} else {
console.error('License request failed');
return [];
}
}

View file

@ -72,352 +72,352 @@ type Data = {
// hls class
class hlsDownload {
private data: Data;
constructor(options: HLSOptions) {
private data: Data;
constructor(options: HLSOptions) {
// check playlist
if (!options || !options.m3u8json || !options.m3u8json.segments || options.m3u8json.segments.length === 0) {
throw new Error('Playlist is empty!');
if (!options || !options.m3u8json || !options.m3u8json.segments || options.m3u8json.segments.length === 0) {
throw new Error('Playlist is empty!');
}
// init options
this.data = {
parts: {
first: options.m3u8json.mediaSequence || 0,
total: options.m3u8json.segments.length,
completed: 0
},
m3u8json: options.m3u8json,
outputFile: options.output || 'stream.ts',
threads: options.threads || 5,
retries: options.retries || 4,
offset: options.offset || 0,
baseurl: options.baseurl,
skipInit: options.skipInit,
keys: {},
timeout: options.timeout ? options.timeout : 60 * 1000,
checkPartLength: false,
isResume: options.offset ? options.offset > 0 : false,
bytesDownloaded: 0,
waitTime: options.fsRetryTime ?? 1000 * 5,
callback: options.callback,
override: options.override,
dateStart: 0
};
}
// init options
this.data = {
parts: {
first: options.m3u8json.mediaSequence || 0,
total: options.m3u8json.segments.length,
completed: 0
},
m3u8json: options.m3u8json,
outputFile: options.output || 'stream.ts',
threads: options.threads || 5,
retries: options.retries || 4,
offset: options.offset || 0,
baseurl: options.baseurl,
skipInit: options.skipInit,
keys: {},
timeout: options.timeout ? options.timeout : 60 * 1000,
checkPartLength: false,
isResume: options.offset ? options.offset > 0 : false,
bytesDownloaded: 0,
waitTime: options.fsRetryTime ?? 1000 * 5,
callback: options.callback,
override: options.override,
dateStart: 0
};
}
async download() {
async download() {
// set output
const fn = this.data.outputFile;
// try load resume file
if (fsp.existsSync(fn) && fsp.existsSync(`${fn}.resume`) && this.data.offset < 1) {
try {
console.info('Resume data found! Trying to resume...');
const resumeData = JSON.parse(await fs.readFile(`${fn}.resume`, 'utf-8'));
if (resumeData.total == this.data.m3u8json.segments.length && resumeData.completed != resumeData.total && !isNaN(resumeData.completed)) {
console.info('Resume data is ok!');
this.data.offset = resumeData.completed;
this.data.isResume = true;
} else {
console.warn(' Resume data is wrong!');
console.warn({
resume: { total: resumeData.total, dled: resumeData.completed },
current: { total: this.data.m3u8json.segments.length }
});
}
} catch (e) {
console.error('Resume failed, downloading will be not resumed!');
console.error(e);
}
}
// ask before rewrite file
if (fsp.existsSync(`${fn}`) && !this.data.isResume) {
let rwts = this.data.override ?? (await Helper.question(`[Q] File «${fn}» already exists! Rewrite? ([y]es/[N]o/[c]ontinue)`));
rwts = rwts || 'N';
if (['Y', 'y'].includes(rwts[0])) {
console.info(`Deleting «${fn}»...`);
await fs.unlink(fn);
} else if (['C', 'c'].includes(rwts[0])) {
return { ok: true, parts: this.data.parts };
} else {
return { ok: false, parts: this.data.parts };
}
}
// show output filename
if (fsp.existsSync(fn) && this.data.isResume) {
console.info(`Adding content to «${fn}»...`);
} else {
console.info(`Saving stream to «${fn}»...`);
}
// start time
this.data.dateStart = Date.now();
let segments = this.data.m3u8json.segments;
// download init part
if (segments[0].map && this.data.offset === 0 && !this.data.skipInit) {
console.info('Download and save init part...');
const initSeg = segments[0].map as Segment;
if (segments[0].key) {
initSeg.key = segments[0].key as Key;
}
try {
const initDl = await this.downloadPart(initSeg, 0, 0);
await fs.writeFile(fn, initDl.dec, { flag: 'a' });
await fs.writeFile(
`${fn}.resume`,
JSON.stringify({
completed: 0,
total: this.data.m3u8json.segments.length
})
);
console.info('Init part downloaded.');
} catch (e: any) {
console.error(`Part init download error:\n\t${e.message}`);
return { ok: false, parts: this.data.parts };
}
} else if (segments[0].map && this.data.offset === 0 && this.data.skipInit) {
console.warn('Skipping init part can lead to broken video!');
}
// resuming ...
if (this.data.offset > 0) {
segments = segments.slice(this.data.offset);
console.info(`Resuming download from part ${this.data.offset + 1}...`);
this.data.parts.completed = this.data.offset;
}
// dl process
for (let p = 0; p < segments.length / this.data.threads; p++) {
// set offsets
const offset = p * this.data.threads;
const dlOffset = offset + this.data.threads;
// map download threads
const krq = new Map(),
prq = new Map();
const res: any[] = [];
let errcnt = 0;
for (let px = offset; px < dlOffset && px < segments.length; px++) {
const curp = segments[px];
const key = curp.key as Key;
if (key && !krq.has(key.uri) && !this.data.keys[key.uri as string]) {
krq.set(key.uri, this.downloadKey(key, px, this.data.offset));
}
}
try {
await Promise.all(krq.values());
} catch (er: any) {
console.error(`Key ${er.p + 1} download error:\n\t${er.message}`);
return { ok: false, parts: this.data.parts };
}
for (let px = offset; px < dlOffset && px < segments.length; px++) {
const curp = segments[px] as Segment;
prq.set(px, () => this.downloadPart(curp, px, this.data.offset));
}
// Parallelized part download with retry logic and optional concurrency limit
const maxConcurrency = this.data.threads;
const partEntries = [...prq.entries()];
let index = 0;
async function worker(this: hlsDownload) {
while (index < partEntries.length) {
const i = index++;
const [px, downloadFn] = partEntries[i];
let retriesLeft = this.data.retries;
let success = false;
while (retriesLeft > 0 && !success) {
const fn = this.data.outputFile;
// try load resume file
if (fsp.existsSync(fn) && fsp.existsSync(`${fn}.resume`) && this.data.offset < 1) {
try {
const r = await downloadFn();
res[px - offset] = r.dec;
success = true;
console.info('Resume data found! Trying to resume...');
const resumeData = JSON.parse(await fs.readFile(`${fn}.resume`, 'utf-8'));
if (resumeData.total == this.data.m3u8json.segments.length && resumeData.completed != resumeData.total && !isNaN(resumeData.completed)) {
console.info('Resume data is ok!');
this.data.offset = resumeData.completed;
this.data.isResume = true;
} else {
console.warn(' Resume data is wrong!');
console.warn({
resume: { total: resumeData.total, dled: resumeData.completed },
current: { total: this.data.m3u8json.segments.length }
});
}
} catch (e) {
console.error('Resume failed, downloading will be not resumed!');
console.error(e);
}
}
// ask before rewrite file
if (fsp.existsSync(`${fn}`) && !this.data.isResume) {
let rwts = this.data.override ?? (await Helper.question(`[Q] File «${fn}» already exists! Rewrite? ([y]es/[N]o/[c]ontinue)`));
rwts = rwts || 'N';
if (['Y', 'y'].includes(rwts[0])) {
console.info(`Deleting «${fn}»...`);
await fs.unlink(fn);
} else if (['C', 'c'].includes(rwts[0])) {
return { ok: true, parts: this.data.parts };
} else {
return { ok: false, parts: this.data.parts };
}
}
// show output filename
if (fsp.existsSync(fn) && this.data.isResume) {
console.info(`Adding content to «${fn}»...`);
} else {
console.info(`Saving stream to «${fn}»...`);
}
// start time
this.data.dateStart = Date.now();
let segments = this.data.m3u8json.segments;
// download init part
if (segments[0].map && this.data.offset === 0 && !this.data.skipInit) {
console.info('Download and save init part...');
const initSeg = segments[0].map as Segment;
if (segments[0].key) {
initSeg.key = segments[0].key as Key;
}
try {
const initDl = await this.downloadPart(initSeg, 0, 0);
await fs.writeFile(fn, initDl.dec, { flag: 'a' });
await fs.writeFile(
`${fn}.resume`,
JSON.stringify({
completed: 0,
total: this.data.m3u8json.segments.length
})
);
console.info('Init part downloaded.');
} catch (e: any) {
console.error(`Part init download error:\n\t${e.message}`);
return { ok: false, parts: this.data.parts };
}
} else if (segments[0].map && this.data.offset === 0 && this.data.skipInit) {
console.warn('Skipping init part can lead to broken video!');
}
// resuming ...
if (this.data.offset > 0) {
segments = segments.slice(this.data.offset);
console.info(`Resuming download from part ${this.data.offset + 1}...`);
this.data.parts.completed = this.data.offset;
}
// dl process
for (let p = 0; p < segments.length / this.data.threads; p++) {
// set offsets
const offset = p * this.data.threads;
const dlOffset = offset + this.data.threads;
// map download threads
const krq = new Map(),
prq = new Map();
const res: any[] = [];
let errcnt = 0;
for (let px = offset; px < dlOffset && px < segments.length; px++) {
const curp = segments[px];
const key = curp.key as Key;
if (key && !krq.has(key.uri) && !this.data.keys[key.uri as string]) {
krq.set(key.uri, this.downloadKey(key, px, this.data.offset));
}
}
try {
await Promise.all(krq.values());
} catch (er: any) {
console.error(`Key ${er.p + 1} download error:\n\t${er.message}`);
return { ok: false, parts: this.data.parts };
}
for (let px = offset; px < dlOffset && px < segments.length; px++) {
const curp = segments[px] as Segment;
prq.set(px, () => this.downloadPart(curp, px, this.data.offset));
}
// Parallelized part download with retry logic and optional concurrency limit
const maxConcurrency = this.data.threads;
const partEntries = [...prq.entries()];
let index = 0;
async function worker(this: hlsDownload) {
while (index < partEntries.length) {
const i = index++;
const [px, downloadFn] = partEntries[i];
let retriesLeft = this.data.retries;
let success = false;
while (retriesLeft > 0 && !success) {
try {
const r = await downloadFn();
res[px - offset] = r.dec;
success = true;
} catch (error: any) {
retriesLeft--;
console.warn(`Retrying part ${error.p + 1 + this.data.offset} (${this.data.retries - retriesLeft}/${this.data.retries})`);
if (retriesLeft > 0) {
await new Promise((resolve) => setTimeout(resolve, 1000));
} else {
console.error(`Part ${error.p + 1 + this.data.offset} download failed after ${this.data.retries} retries:\n\t${error.message}`);
errcnt++;
}
}
}
}
}
const workers = [];
for (let i = 0; i < maxConcurrency; i++) {
workers.push(worker.call(this));
}
await Promise.all(workers);
// catch error
if (errcnt > 0) {
console.error(`${errcnt} parts not downloaded`);
return { ok: false, parts: this.data.parts };
}
// write downloaded
for (const r of res) {
let error = 0;
while (error < 3) {
try {
await fs.writeFile(fn, r, { flag: 'a' });
break;
} catch (err) {
console.error(err);
console.error(`Unable to write to file '${fn}' (Attempt ${error + 1}/3)`);
console.info(`Waiting ${Math.round(this.data.waitTime / 1000)}s before retrying`);
await new Promise<void>((resolve) => setTimeout(() => resolve(), this.data.waitTime));
}
error++;
}
if (error === 3) {
console.error(`Unable to write content to '${fn}'.`);
return { ok: false, parts: this.data.parts };
}
}
// log downloaded
const totalSeg = segments.length + this.data.offset; // Add the sliced lenght back so the resume data will be correct even if an resumed download fails
const downloadedSeg = dlOffset < totalSeg ? dlOffset : totalSeg;
this.data.parts.completed = downloadedSeg + this.data.offset;
const data = extFn.getDownloadInfo(this.data.dateStart, downloadedSeg, totalSeg, this.data.bytesDownloaded);
await fs.writeFile(
`${fn}.resume`,
JSON.stringify({
completed: this.data.parts.completed,
total: totalSeg
})
);
function formatDLSpeedB(s: number) {
if (s < 1000000) return `${(s / 1000).toFixed(2)} KB/s`;
if (s < 1000000000) return `${(s / 1000000).toFixed(2)} MB/s`;
return `${(s / 1000000000).toFixed(2)} GB/s`;
}
function formatDLSpeedBit(s: number) {
if (s * 8 < 1000000) return `${(s * 8 / 1000).toFixed(2)} KBit/s`;
if (s * 8 < 1000000000) return `${(s * 8 / 1000000).toFixed(2)} MBit/s`;
return `${(s * 8 / 1000000000).toFixed(2)} GBit/s`;
}
console.info(
`${downloadedSeg} of ${totalSeg} parts downloaded [${data.percent}%] (${Helper.formatTime(parseInt((data.time / 1000).toFixed(0)))} | ${formatDLSpeedB(data.downloadSpeed)} / ${formatDLSpeedBit(data.downloadSpeed)})`
);
if (this.data.callback)
this.data.callback({
total: this.data.parts.total,
cur: this.data.parts.completed,
bytes: this.data.bytesDownloaded,
percent: data.percent,
time: data.time,
downloadSpeed: data.downloadSpeed
});
}
// return result
await fs.unlink(`${fn}.resume`);
return { ok: true, parts: this.data.parts };
}
async downloadPart(seg: Segment, segIndex: number, segOffset: number) {
const sURI = extFn.getURI(seg.uri, this.data.baseurl);
let decipher, part, dec;
const p = segIndex;
try {
if (seg.key != undefined) {
decipher = await this.getKey(seg.key, p, segOffset);
}
part = await extFn.getData(
p,
sURI,
{
...(seg.byterange
? {
Range: `bytes=${seg.byterange.offset}-${seg.byterange.offset + seg.byterange.length - 1}`
}
: {})
},
segOffset,
false
);
// if (this.data.checkPartLength) {
// this.data.checkPartLength = false;
// console.warn(`Part ${segIndex + segOffset + 1}: can't check parts size!`);
// }
if (decipher == undefined) {
this.data.bytesDownloaded += Buffer.from(part).byteLength;
return { dec: Buffer.from(part), p };
}
dec = decipher.update(Buffer.from(part));
dec = Buffer.concat([dec, decipher.final()]);
this.data.bytesDownloaded += dec.byteLength;
} catch (error: any) {
error.p = p;
throw error;
}
return { dec, p };
}
async downloadKey(key: Key, segIndex: number, segOffset: number) {
const kURI = extFn.getURI(key.uri, this.data.baseurl);
if (!this.data.keys[kURI]) {
try {
const rkey = await extFn.getData(segIndex, kURI, {}, segOffset, true);
return rkey;
} catch (error: any) {
retriesLeft--;
console.warn(`Retrying part ${error.p + 1 + this.data.offset} (${this.data.retries - retriesLeft}/${this.data.retries})`);
if (retriesLeft > 0) {
await new Promise((resolve) => setTimeout(resolve, 1000));
} else {
console.error(`Part ${error.p + 1 + this.data.offset} download failed after ${this.data.retries} retries:\n\t${error.message}`);
errcnt++;
}
error.p = segIndex;
throw error;
}
}
}
}
const workers = [];
for (let i = 0; i < maxConcurrency; i++) {
workers.push(worker.call(this));
}
await Promise.all(workers);
// catch error
if (errcnt > 0) {
console.error(`${errcnt} parts not downloaded`);
return { ok: false, parts: this.data.parts };
}
// write downloaded
for (const r of res) {
let error = 0;
while (error < 3) {
try {
await fs.writeFile(fn, r, { flag: 'a' });
break;
} catch (err) {
console.error(err);
console.error(`Unable to write to file '${fn}' (Attempt ${error + 1}/3)`);
console.info(`Waiting ${Math.round(this.data.waitTime / 1000)}s before retrying`);
await new Promise<void>((resolve) => setTimeout(() => resolve(), this.data.waitTime));
}
error++;
}
if (error === 3) {
console.error(`Unable to write content to '${fn}'.`);
return { ok: false, parts: this.data.parts };
}
}
// log downloaded
const totalSeg = segments.length + this.data.offset; // Add the sliced lenght back so the resume data will be correct even if an resumed download fails
const downloadedSeg = dlOffset < totalSeg ? dlOffset : totalSeg;
this.data.parts.completed = downloadedSeg + this.data.offset;
const data = extFn.getDownloadInfo(this.data.dateStart, downloadedSeg, totalSeg, this.data.bytesDownloaded);
await fs.writeFile(
`${fn}.resume`,
JSON.stringify({
completed: this.data.parts.completed,
total: totalSeg
})
);
function formatDLSpeedB(s: number) {
if (s < 1000000) return `${(s / 1000).toFixed(2)} KB/s`;
if (s < 1000000000) return `${(s / 1000000).toFixed(2)} MB/s`;
return `${(s / 1000000000).toFixed(2)} GB/s`;
}
function formatDLSpeedBit(s: number) {
if (s * 8 < 1000000) return `${(s * 8 / 1000).toFixed(2)} KBit/s`;
if (s * 8 < 1000000000) return `${(s * 8 / 1000000).toFixed(2)} MBit/s`;
return `${(s * 8 / 1000000000).toFixed(2)} GBit/s`;
}
console.info(
`${downloadedSeg} of ${totalSeg} parts downloaded [${data.percent}%] (${Helper.formatTime(parseInt((data.time / 1000).toFixed(0)))} | ${formatDLSpeedB(data.downloadSpeed)} / ${formatDLSpeedBit(data.downloadSpeed)})`
);
if (this.data.callback)
this.data.callback({
total: this.data.parts.total,
cur: this.data.parts.completed,
bytes: this.data.bytesDownloaded,
percent: data.percent,
time: data.time,
downloadSpeed: data.downloadSpeed
});
}
// return result
await fs.unlink(`${fn}.resume`);
return { ok: true, parts: this.data.parts };
}
async downloadPart(seg: Segment, segIndex: number, segOffset: number) {
const sURI = extFn.getURI(seg.uri, this.data.baseurl);
let decipher, part, dec;
const p = segIndex;
try {
if (seg.key != undefined) {
decipher = await this.getKey(seg.key, p, segOffset);
}
part = await extFn.getData(
p,
sURI,
{
...(seg.byterange
? {
Range: `bytes=${seg.byterange.offset}-${seg.byterange.offset + seg.byterange.length - 1}`
async getKey(key: Key, segIndex: number, segOffset: number) {
const kURI = extFn.getURI(key.uri, this.data.baseurl);
const p = segIndex;
if (!this.data.keys[kURI]) {
try {
const rkey = await this.downloadKey(key, segIndex, segOffset);
if (!rkey) throw new Error();
this.data.keys[kURI] = Buffer.from(rkey);
} catch (error: any) {
error.p = p;
throw error;
}
: {})
},
segOffset,
false
);
// if (this.data.checkPartLength) {
// this.data.checkPartLength = false;
// console.warn(`Part ${segIndex + segOffset + 1}: can't check parts size!`);
// }
if (decipher == undefined) {
this.data.bytesDownloaded += Buffer.from(part).byteLength;
return { dec: Buffer.from(part), p };
}
dec = decipher.update(Buffer.from(part));
dec = Buffer.concat([dec, decipher.final()]);
this.data.bytesDownloaded += dec.byteLength;
} catch (error: any) {
error.p = p;
throw error;
}
// get ivs
const iv = Buffer.alloc(16);
const ivs = key.iv ? key.iv : [0, 0, 0, p + 1];
for (let i = 0; i < ivs.length; i++) {
iv.writeUInt32BE(ivs[i], i * 4);
}
return crypto.createDecipheriv('aes-128-cbc', this.data.keys[kURI], iv);
}
return { dec, p };
}
async downloadKey(key: Key, segIndex: number, segOffset: number) {
const kURI = extFn.getURI(key.uri, this.data.baseurl);
if (!this.data.keys[kURI]) {
try {
const rkey = await extFn.getData(segIndex, kURI, {}, segOffset, true);
return rkey;
} catch (error: any) {
error.p = segIndex;
throw error;
}
}
}
async getKey(key: Key, segIndex: number, segOffset: number) {
const kURI = extFn.getURI(key.uri, this.data.baseurl);
const p = segIndex;
if (!this.data.keys[kURI]) {
try {
const rkey = await this.downloadKey(key, segIndex, segOffset);
if (!rkey) throw new Error();
this.data.keys[kURI] = Buffer.from(rkey);
} catch (error: any) {
error.p = p;
throw error;
}
}
// get ivs
const iv = Buffer.alloc(16);
const ivs = key.iv ? key.iv : [0, 0, 0, p + 1];
for (let i = 0; i < ivs.length; i++) {
iv.writeUInt32BE(ivs[i], i * 4);
}
return crypto.createDecipheriv('aes-128-cbc', this.data.keys[kURI], iv);
}
}
const extFn = {
getURI: (uri: string, baseurl?: string) => {
const httpURI = /^https{0,1}:/.test(uri);
if (!baseurl && !httpURI) {
throw new Error('No base and not http(s) uri');
} else if (httpURI) {
return uri;
}
return baseurl + uri;
},
getDownloadInfo: (dateStart: number, partsDL: number, partsTotal: number, downloadedBytes: number) => {
const dateElapsed = Date.now() - dateStart;
const percentFxd = parseInt(((partsDL / partsTotal) * 100).toFixed());
const percent = percentFxd < 100 ? percentFxd : partsTotal == partsDL ? 100 : 99;
const revParts = dateElapsed * (partsTotal / partsDL - 1);
const downloadSpeed = downloadedBytes / (dateElapsed / 1000); //Bytes per second
return { percent, time: revParts, downloadSpeed };
},
getData: async (partIndex: number, uri: string, headers: Record<string, string>, segOffset: number, isKey: boolean) => {
getURI: (uri: string, baseurl?: string) => {
const httpURI = /^https{0,1}:/.test(uri);
if (!baseurl && !httpURI) {
throw new Error('No base and not http(s) uri');
} else if (httpURI) {
return uri;
}
return baseurl + uri;
},
getDownloadInfo: (dateStart: number, partsDL: number, partsTotal: number, downloadedBytes: number) => {
const dateElapsed = Date.now() - dateStart;
const percentFxd = parseInt(((partsDL / partsTotal) * 100).toFixed());
const percent = percentFxd < 100 ? percentFxd : partsTotal == partsDL ? 100 : 99;
const revParts = dateElapsed * (partsTotal / partsDL - 1);
const downloadSpeed = downloadedBytes / (dateElapsed / 1000); //Bytes per second
return { percent, time: revParts, downloadSpeed };
},
getData: async (partIndex: number, uri: string, headers: Record<string, string>, segOffset: number, isKey: boolean) => {
// get file if uri is local
if (uri.startsWith('file://')) {
const buffer = await fs.readFile(url.fileURLToPath(uri));
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
if (uri.startsWith('file://')) {
const buffer = await fs.readFile(url.fileURLToPath(uri));
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
}
// do request
return await ofetch(uri, {
method: 'GET',
headers: headers,
responseType: 'arrayBuffer',
retry: 0,
async onRequestError({ error }) {
const partType = isKey ? 'Key' : 'Part';
const partIndx = partIndex + 1 + segOffset;
console.warn(`%s %s: ${error.message}`, partType, partIndx);
}
});
}
// do request
return await ofetch(uri, {
method: 'GET',
headers: headers,
responseType: 'arrayBuffer',
retry: 0,
async onRequestError({ error }) {
const partType = isKey ? 'Key' : 'Part';
const partIndx = partIndex + 1 + segOffset;
console.warn(`%s %s: ${error.message}`, partType, partIndx);
}
});
}
};
export default hlsDownload;

View file

@ -7,63 +7,63 @@ const logFolder = path.join(workingDir, 'logs');
const latest = path.join(logFolder, 'latest.log');
const makeLogFolder = () => {
if (!fs.existsSync(logFolder))
fs.mkdirSync(logFolder);
if (fs.existsSync(latest)) {
const stats = fs.statSync(latest);
fs.renameSync(latest, path.join(logFolder, `${stats.mtimeMs}.log`));
}
if (!fs.existsSync(logFolder))
fs.mkdirSync(logFolder);
if (fs.existsSync(latest)) {
const stats = fs.statSync(latest);
fs.renameSync(latest, path.join(logFolder, `${stats.mtimeMs}.log`));
}
};
const makeLogger = () => {
global.console.log =
global.console.log =
global.console.info =
global.console.warn =
global.console.error =
global.console.debug = (...data: any[]) => {
console.info((data.length >= 1 ? data.shift() : ''), ...data);
console.info((data.length >= 1 ? data.shift() : ''), ...data);
};
makeLogFolder();
log4js.configure({
appenders: {
console: {
type: 'console', layout: {
type: 'pattern',
pattern: process.env.isGUI === 'true' ? '%[%x{info}%m%]' : '%x{info}%m',
tokens: {
info: (ev) => {
return ev.level.levelStr === 'INFO' ? '' : `[${ev.level.levelStr}] `;
makeLogFolder();
log4js.configure({
appenders: {
console: {
type: 'console', layout: {
type: 'pattern',
pattern: process.env.isGUI === 'true' ? '%[%x{info}%m%]' : '%x{info}%m',
tokens: {
info: (ev) => {
return ev.level.levelStr === 'INFO' ? '' : `[${ev.level.levelStr}] `;
}
}
}
},
file: {
type: 'file',
filename: latest,
layout: {
type: 'pattern',
pattern: '%x{info}%m',
tokens: {
info: (ev) => {
return ev.level.levelStr === 'INFO' ? '' : `[${ev.level.levelStr}] `;
}
}
}
}
}
}
},
file: {
type: 'file',
filename: latest,
layout: {
type: 'pattern',
pattern: '%x{info}%m',
tokens: {
info: (ev) => {
return ev.level.levelStr === 'INFO' ? '' : `[${ev.level.levelStr}] `;
},
categories: {
default: {
appenders: ['console', 'file'],
level: 'all',
}
}
}
}
},
categories: {
default: {
appenders: ['console', 'file'],
level: 'all',
}
}
});
});
};
const getLogger = () => {
if (!log4js.isConfigured())
makeLogger();
return log4js.getLogger();
if (!log4js.isConfigured())
makeLogger();
return log4js.getLogger();
};
export const console = getLogger();

View file

@ -1,10 +1,10 @@
// api domains
const domain = {
cr_www: 'https://www.crunchyroll.com',
cr_api: 'https://api.crunchyroll.com',
hd_www: 'https://www.hidive.com',
hd_api: 'https://api.hidive.com',
hd_new: 'https://dce-frontoffice.imggaming.com'
cr_www: 'https://www.crunchyroll.com',
cr_api: 'https://api.crunchyroll.com',
hd_www: 'https://www.hidive.com',
hd_api: 'https://api.hidive.com',
hd_new: 'https://dce-frontoffice.imggaming.com'
};
export type APIType = {
@ -42,63 +42,63 @@ export type APIType = {
};
const api: APIType = {
//
//
// Crunchyroll
// Vilos bundle.js (where we can extract the basic token thats needed for the initial auth)
bundlejs: 'https://static.crunchyroll.com/vilos-v2/web/vilos/js/bundle.js',
//
// Crunchyroll API
basic_auth_token: 'Y2I5bnpybWh0MzJ2Z3RleHlna286S1V3bU1qSlh4eHVyc0hJVGQxenZsMkMyeVFhUW84TjQ=',
auth: `${domain.cr_www}/auth/v1/token`,
me: `${domain.cr_www}/accounts/v1/me`,
profile: `${domain.cr_www}/accounts/v1/me/profile`,
search: `${domain.cr_www}/content/v2/discover/search`,
content_cms: `${domain.cr_www}/content/v2/cms`,
browse: `${domain.cr_www}/content/v1/browse`,
browse_all_series: `${domain.cr_www}/content/v2/discover/browse`,
streaming_sessions: `${domain.cr_www}/playback/v1/sessions/streaming`,
drm_widevine: `${domain.cr_www}/license/v1/license/widevine`,
drm_playready: `${domain.cr_www}/license/v1/license/playReady`,
//
// Crunchyroll Bucket
cms_bucket: `${domain.cr_www}/cms/v2`,
cms_auth: `${domain.cr_www}/index/v2`,
//
// Crunchyroll Headers
crunchyDefUserAgent: 'Crunchyroll/ANDROIDTV/3.45.2_22274 (Android 12; en-US; SHIELD Android TV Build/SR1A.211012.001)',
crunchyDefHeader: {},
crunchyAuthHeader: {},
//
//
// Hidive
// Hidive API
hd_apikey: '508efd7b42d546e19cc24f4d0b414e57e351ca73',
hd_devName: 'Android',
hd_appId: '24i-Android',
hd_clientWeb: 'okhttp/3.4.1',
hd_clientExo: 'smartexoplayer/1.6.0.R (Linux;Android 6.0) ExoPlayerLib/2.6.0',
hd_api: `${domain.hd_api}/api/v1`,
// Hidive New API
hd_new_api: `${domain.hd_new}/api`,
hd_new_apiKey: '857a1e5d-e35e-4fdf-805b-a87b6f8364bf',
hd_new_version: '6.0.1.bbf09a2'
//
//
// Crunchyroll
// Vilos bundle.js (where we can extract the basic token thats needed for the initial auth)
bundlejs: 'https://static.crunchyroll.com/vilos-v2/web/vilos/js/bundle.js',
//
// Crunchyroll API
basic_auth_token: 'Y2I5bnpybWh0MzJ2Z3RleHlna286S1V3bU1qSlh4eHVyc0hJVGQxenZsMkMyeVFhUW84TjQ=',
auth: `${domain.cr_www}/auth/v1/token`,
me: `${domain.cr_www}/accounts/v1/me`,
profile: `${domain.cr_www}/accounts/v1/me/profile`,
search: `${domain.cr_www}/content/v2/discover/search`,
content_cms: `${domain.cr_www}/content/v2/cms`,
browse: `${domain.cr_www}/content/v1/browse`,
browse_all_series: `${domain.cr_www}/content/v2/discover/browse`,
streaming_sessions: `${domain.cr_www}/playback/v1/sessions/streaming`,
drm_widevine: `${domain.cr_www}/license/v1/license/widevine`,
drm_playready: `${domain.cr_www}/license/v1/license/playReady`,
//
// Crunchyroll Bucket
cms_bucket: `${domain.cr_www}/cms/v2`,
cms_auth: `${domain.cr_www}/index/v2`,
//
// Crunchyroll Headers
crunchyDefUserAgent: 'Crunchyroll/ANDROIDTV/3.45.2_22274 (Android 12; en-US; SHIELD Android TV Build/SR1A.211012.001)',
crunchyDefHeader: {},
crunchyAuthHeader: {},
//
//
// Hidive
// Hidive API
hd_apikey: '508efd7b42d546e19cc24f4d0b414e57e351ca73',
hd_devName: 'Android',
hd_appId: '24i-Android',
hd_clientWeb: 'okhttp/3.4.1',
hd_clientExo: 'smartexoplayer/1.6.0.R (Linux;Android 6.0) ExoPlayerLib/2.6.0',
hd_api: `${domain.hd_api}/api/v1`,
// Hidive New API
hd_new_api: `${domain.hd_new}/api`,
hd_new_apiKey: '857a1e5d-e35e-4fdf-805b-a87b6f8364bf',
hd_new_version: '6.0.1.bbf09a2'
};
api.crunchyDefHeader = {
'User-Agent': api.crunchyDefUserAgent,
Accept: '*/*',
'Accept-Encoding': 'gzip',
Connection: 'Keep-Alive',
Host: 'www.crunchyroll.com'
'User-Agent': api.crunchyDefUserAgent,
Accept: '*/*',
'Accept-Encoding': 'gzip',
Connection: 'Keep-Alive',
Host: 'www.crunchyroll.com'
};
// set header
api.crunchyAuthHeader = {
Authorization: `Basic ${api.basic_auth_token}`,
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
'Request-Type': 'SignIn',
...api.crunchyDefHeader
Authorization: `Basic ${api.basic_auth_token}`,
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
'Request-Type': 'SignIn',
...api.crunchyDefHeader
};
export { domain, api };

View file

@ -94,91 +94,91 @@ export type ArgvType = typeof argvC;
const appArgv = (cfg: {
[key: string]: unknown
}, isGUI = false) => {
if (argvC)
return argvC;
yargs(process.argv.slice(2));
const argv = getArgv(cfg, isGUI)
.parseSync();
argvC = argv;
return argv;
if (argvC)
return argvC;
yargs(process.argv.slice(2));
const argv = getArgv(cfg, isGUI)
.parseSync();
argvC = argv;
return argv;
};
const overrideArguments = (cfg: { [key:string]: unknown }, override: Partial<typeof argvC>, isGUI = false) => {
const argv = getArgv(cfg, isGUI).middleware((ar) => {
for (const key of Object.keys(override)) {
ar[key] = override[key];
}
}).parseSync();
argvC = argv;
const argv = getArgv(cfg, isGUI).middleware((ar) => {
for (const key of Object.keys(override)) {
ar[key] = override[key];
}
}).parseSync();
argvC = argv;
};
export {
appArgv,
overrideArguments
appArgv,
overrideArguments
};
const getArgv = (cfg: { [key:string]: unknown }, isGUI: boolean) => {
const parseDefault = <T = unknown>(key: string, _default: T) : T=> {
if (Object.prototype.hasOwnProperty.call(cfg, key)) {
return cfg[key] as T;
} else
return _default;
};
const argv = yargs.parserConfiguration({
'duplicate-arguments-array': false,
'camel-case-expansion': false,
})
.wrap(yargs.terminalWidth())
.usage('Usage: $0 [options]')
.help(true);
const parseDefault = <T = unknown>(key: string, _default: T) : T=> {
if (Object.prototype.hasOwnProperty.call(cfg, key)) {
return cfg[key] as T;
} else
return _default;
};
const argv = yargs.parserConfiguration({
'duplicate-arguments-array': false,
'camel-case-expansion': false,
})
.wrap(yargs.terminalWidth())
.usage('Usage: $0 [options]')
.help(true);
//.strictOptions()
const data = args.map(a => {
return {
...a,
demandOption: !isGUI && a.demandOption,
group: groups[a.group],
default: typeof a.default === 'object' && !Array.isArray(a.default) ?
parseDefault((a.default as any).name || a.name, (a.default as any).default) : a.default
};
});
for (const item of data)
argv.option(item.name, {
...item,
coerce: (value) => {
if (item.transformer) {
return item.transformer(value);
} else {
return value;
}
},
choices: item.name === 'service' && isGUI ? undefined : item.choices as unknown as Choices
const data = args.map(a => {
return {
...a,
demandOption: !isGUI && a.demandOption,
group: groups[a.group],
default: typeof a.default === 'object' && !Array.isArray(a.default) ?
parseDefault((a.default as any).name || a.name, (a.default as any).default) : a.default
};
});
for (const item of data)
argv.option(item.name, {
...item,
coerce: (value) => {
if (item.transformer) {
return item.transformer(value);
} else {
return value;
}
},
choices: item.name === 'service' && isGUI ? undefined : item.choices as unknown as Choices
});
// Custom logic for suggesting corrections for misspelled options
argv.middleware((argv: Record<string, any>) => {
// Custom logic for suggesting corrections for misspelled options
argv.middleware((argv: Record<string, any>) => {
// List of valid options
const validOptions = [
...args.map(a => a.name),
...args.map(a => a.alias).filter(alias => alias !== undefined) as string[]
];
const unknownOptions = Object.keys(argv).filter(key => !validOptions.includes(key) && key !== '_' && key !== '$0'); // Filter out known options
const validOptions = [
...args.map(a => a.name),
...args.map(a => a.alias).filter(alias => alias !== undefined) as string[]
];
const unknownOptions = Object.keys(argv).filter(key => !validOptions.includes(key) && key !== '_' && key !== '$0'); // Filter out known options
const suggestedOptions: Record<string, boolean> = {};
unknownOptions.forEach(actualOption => {
const closestOption = validOptions.find(option => {
const levenVal = leven(option, actualOption);
return levenVal <= 2 && levenVal > 0;
});
const suggestedOptions: Record<string, boolean> = {};
unknownOptions.forEach(actualOption => {
const closestOption = validOptions.find(option => {
const levenVal = leven(option, actualOption);
return levenVal <= 2 && levenVal > 0;
});
if (closestOption && !suggestedOptions[closestOption]) {
suggestedOptions[closestOption] = true;
console.info(`Unknown option ${actualOption}, did you mean ${closestOption}?`);
} else if (!suggestedOptions[actualOption]) {
suggestedOptions[actualOption] = true;
console.info(`Unknown option ${actualOption}`);
}
if (closestOption && !suggestedOptions[closestOption]) {
suggestedOptions[closestOption] = true;
console.info(`Unknown option ${actualOption}, did you mean ${closestOption}?`);
} else if (!suggestedOptions[actualOption]) {
suggestedOptions[actualOption] = true;
console.info(`Unknown option ${actualOption}`);
}
});
});
});
return argv as unknown as yargs.Argv<typeof argvC>;
return argv as unknown as yargs.Argv<typeof argvC>;
};

File diff suppressed because it is too large Load diff

View file

@ -18,45 +18,45 @@ const guiCfgFile = path.join(workingDir, 'config', 'gui');
const cliCfgFile = path.join(workingDir, 'config', 'cli-defaults');
const hdPflCfgFile = path.join(workingDir, 'config', 'hd_profile');
const sessCfgFile = {
cr: path.join(workingDir, 'config', 'cr_sess'),
hd: path.join(workingDir, 'config', 'hd_sess'),
ao: path.join(workingDir, 'config', 'ao_sess'),
adn: path.join(workingDir, 'config', 'adn_sess')
cr: path.join(workingDir, 'config', 'cr_sess'),
hd: path.join(workingDir, 'config', 'hd_sess'),
ao: path.join(workingDir, 'config', 'ao_sess'),
adn: path.join(workingDir, 'config', 'adn_sess')
};
const stateFile = path.join(workingDir, 'config', 'guistate');
const tokenFile = {
cr: path.join(workingDir, 'config', 'cr_token'),
hd: path.join(workingDir, 'config', 'hd_token'),
hdNew:path.join(workingDir, 'config', 'hd_new_token'),
ao: path.join(workingDir, 'config', 'ao_token'),
adn: path.join(workingDir, 'config', 'adn_token')
cr: path.join(workingDir, 'config', 'cr_token'),
hd: path.join(workingDir, 'config', 'hd_token'),
hdNew:path.join(workingDir, 'config', 'hd_new_token'),
ao: path.join(workingDir, 'config', 'ao_token'),
adn: path.join(workingDir, 'config', 'adn_token')
};
export const ensureConfig = () => {
if (!fs.existsSync(path.join(workingDir, 'config')))
fs.mkdirSync(path.join(workingDir, 'config'));
if (process.env.contentDirectory)
[binCfgFile, dirCfgFile, cliCfgFile, guiCfgFile].forEach(a => {
if (!fs.existsSync(`${a}.yml`))
fs.copyFileSync(path.join(__dirname, '..', 'config', `${path.basename(a)}.yml`), `${a}.yml`);
});
if (!fs.existsSync(path.join(workingDir, 'config')))
fs.mkdirSync(path.join(workingDir, 'config'));
if (process.env.contentDirectory)
[binCfgFile, dirCfgFile, cliCfgFile, guiCfgFile].forEach(a => {
if (!fs.existsSync(`${a}.yml`))
fs.copyFileSync(path.join(__dirname, '..', 'config', `${path.basename(a)}.yml`), `${a}.yml`);
});
};
const loadYamlCfgFile = <T extends Record<string, any>>(file: string, isSess?: boolean): T => {
if(fs.existsSync(`${file}.user.yml`) && !isSess){
file += '.user';
}
file += '.yml';
if(fs.existsSync(file)){
try{
return yaml.parse(fs.readFileSync(file, 'utf8'));
if(fs.existsSync(`${file}.user.yml`) && !isSess){
file += '.user';
}
catch(e){
console.error('[ERROR]', e);
return {} as T;
file += '.yml';
if(fs.existsSync(file)){
try{
return yaml.parse(fs.readFileSync(file, 'utf8'));
}
catch(e){
console.error('[ERROR]', e);
return {} as T;
}
}
}
return {} as T;
return {} as T;
};
export type WriteObjects = {
@ -64,10 +64,10 @@ export type WriteObjects = {
}
const writeYamlCfgFile = <T extends keyof WriteObjects>(file: T, data: WriteObjects[T]) => {
const fn = path.join(workingDir, 'config', `${file}.yml`);
if (fs.existsSync(fn))
fs.removeSync(fn);
fs.writeFileSync(fn, yaml.stringify(data));
const fn = path.join(workingDir, 'config', `${file}.yml`);
if (fs.existsSync(fn))
fs.removeSync(fn);
fs.writeFileSync(fn, yaml.stringify(data));
};
export type GUIConfig = {
@ -96,317 +96,317 @@ export type ConfigObject = {
}
const loadCfg = () : ConfigObject => {
// load cfgs
const defaultCfg: ConfigObject = {
bin: {},
dir: loadYamlCfgFile<{
// load cfgs
const defaultCfg: ConfigObject = {
bin: {},
dir: loadYamlCfgFile<{
content: string,
trash: string,
fonts: string
config: string
}>(dirCfgFile),
cli: loadYamlCfgFile<{
cli: loadYamlCfgFile<{
[key: string]: any
}>(cliCfgFile),
gui: loadYamlCfgFile<GUIConfig>(guiCfgFile)
};
const defaultDirs = {
fonts: '${wdir}/fonts/',
content: '${wdir}/videos/',
trash: '${wdir}/videos/_trash/',
config: '${wdir}/config'
};
if (typeof defaultCfg.dir !== 'object' || defaultCfg.dir === null || Array.isArray(defaultCfg.dir)) {
defaultCfg.dir = defaultDirs;
}
gui: loadYamlCfgFile<GUIConfig>(guiCfgFile)
};
const defaultDirs = {
fonts: '${wdir}/fonts/',
content: '${wdir}/videos/',
trash: '${wdir}/videos/_trash/',
config: '${wdir}/config'
};
if (typeof defaultCfg.dir !== 'object' || defaultCfg.dir === null || Array.isArray(defaultCfg.dir)) {
defaultCfg.dir = defaultDirs;
}
const keys = Object.keys(defaultDirs) as (keyof typeof defaultDirs)[];
for (const key of keys) {
if (!Object.prototype.hasOwnProperty.call(defaultCfg.dir, key) || typeof defaultCfg.dir[key] !== 'string') {
defaultCfg.dir[key] = defaultDirs[key];
const keys = Object.keys(defaultDirs) as (keyof typeof defaultDirs)[];
for (const key of keys) {
if (!Object.prototype.hasOwnProperty.call(defaultCfg.dir, key) || typeof defaultCfg.dir[key] !== 'string') {
defaultCfg.dir[key] = defaultDirs[key];
}
if (!path.isAbsolute(defaultCfg.dir[key])) {
defaultCfg.dir[key] = path.join(workingDir, defaultCfg.dir[key].replace(/^\${wdir}/, ''));
}
}
if (!path.isAbsolute(defaultCfg.dir[key])) {
defaultCfg.dir[key] = path.join(workingDir, defaultCfg.dir[key].replace(/^\${wdir}/, ''));
if(!fs.existsSync(defaultCfg.dir.content)){
try{
fs.ensureDirSync(defaultCfg.dir.content);
}
catch(e){
console.error('Content directory not accessible!');
return defaultCfg;
}
}
}
if(!fs.existsSync(defaultCfg.dir.content)){
try{
fs.ensureDirSync(defaultCfg.dir.content);
if(!fs.existsSync(defaultCfg.dir.trash)){
defaultCfg.dir.trash = defaultCfg.dir.content;
}
catch(e){
console.error('Content directory not accessible!');
return defaultCfg;
}
}
if(!fs.existsSync(defaultCfg.dir.trash)){
defaultCfg.dir.trash = defaultCfg.dir.content;
}
// output
return defaultCfg;
// output
return defaultCfg;
};
const loadBinCfg = async () => {
const binCfg = loadYamlCfgFile<ConfigObject['bin']>(binCfgFile);
// binaries
const defaultBin = {
ffmpeg: 'ffmpeg',
mkvmerge: 'mkvmerge',
ffprobe: 'ffprobe',
mp4decrypt: 'mp4decrypt',
shaka: 'shaka-packager'
};
const keys = Object.keys(defaultBin) as (keyof typeof defaultBin)[];
for(const dir of keys){
if(!Object.prototype.hasOwnProperty.call(binCfg, dir) || typeof binCfg[dir] != 'string'){
binCfg[dir] = defaultBin[dir];
const binCfg = loadYamlCfgFile<ConfigObject['bin']>(binCfgFile);
// binaries
const defaultBin = {
ffmpeg: 'ffmpeg',
mkvmerge: 'mkvmerge',
ffprobe: 'ffprobe',
mp4decrypt: 'mp4decrypt',
shaka: 'shaka-packager'
};
const keys = Object.keys(defaultBin) as (keyof typeof defaultBin)[];
for(const dir of keys){
if(!Object.prototype.hasOwnProperty.call(binCfg, dir) || typeof binCfg[dir] != 'string'){
binCfg[dir] = defaultBin[dir];
}
if ((binCfg[dir] as string).match(/^\${wdir}/)) {
binCfg[dir] = (binCfg[dir] as string).replace(/^\${wdir}/, '');
binCfg[dir] = path.join(workingDir, binCfg[dir] as string);
}
if (!path.isAbsolute(binCfg[dir] as string)){
binCfg[dir] = path.join(workingDir, binCfg[dir] as string);
}
binCfg[dir] = await lookpath(binCfg[dir] as string);
binCfg[dir] = binCfg[dir] ? binCfg[dir] : undefined;
if(!binCfg[dir]){
const binFile = await lookpath(path.basename(defaultBin[dir]));
binCfg[dir] = binFile ? binFile : binCfg[dir];
}
}
if ((binCfg[dir] as string).match(/^\${wdir}/)) {
binCfg[dir] = (binCfg[dir] as string).replace(/^\${wdir}/, '');
binCfg[dir] = path.join(workingDir, binCfg[dir] as string);
}
if (!path.isAbsolute(binCfg[dir] as string)){
binCfg[dir] = path.join(workingDir, binCfg[dir] as string);
}
binCfg[dir] = await lookpath(binCfg[dir] as string);
binCfg[dir] = binCfg[dir] ? binCfg[dir] : undefined;
if(!binCfg[dir]){
const binFile = await lookpath(path.basename(defaultBin[dir]));
binCfg[dir] = binFile ? binFile : binCfg[dir];
}
}
return binCfg;
return binCfg;
};
const loadCRSession = () => {
let session = loadYamlCfgFile(sessCfgFile.cr, true);
if(typeof session !== 'object' || session === null || Array.isArray(session)){
session = {};
}
for(const cv of Object.keys(session)){
if(typeof session[cv] !== 'object' || session[cv] === null || Array.isArray(session[cv])){
session[cv] = {};
let session = loadYamlCfgFile(sessCfgFile.cr, true);
if(typeof session !== 'object' || session === null || Array.isArray(session)){
session = {};
}
}
return session;
for(const cv of Object.keys(session)){
if(typeof session[cv] !== 'object' || session[cv] === null || Array.isArray(session[cv])){
session[cv] = {};
}
}
return session;
};
const saveCRSession = (data: Record<string, unknown>) => {
const cfgFolder = path.dirname(sessCfgFile.cr);
try{
fs.ensureDirSync(cfgFolder);
fs.writeFileSync(`${sessCfgFile.cr}.yml`, yaml.stringify(data));
}
catch(e){
console.error('Can\'t save session file to disk!');
}
const cfgFolder = path.dirname(sessCfgFile.cr);
try{
fs.ensureDirSync(cfgFolder);
fs.writeFileSync(`${sessCfgFile.cr}.yml`, yaml.stringify(data));
}
catch(e){
console.error('Can\'t save session file to disk!');
}
};
const loadCRToken = () => {
let token = loadYamlCfgFile(tokenFile.cr, true);
if(typeof token !== 'object' || token === null || Array.isArray(token)){
token = {};
}
return token;
let token = loadYamlCfgFile(tokenFile.cr, true);
if(typeof token !== 'object' || token === null || Array.isArray(token)){
token = {};
}
return token;
};
const saveCRToken = (data: Record<string, unknown>) => {
const cfgFolder = path.dirname(tokenFile.cr);
try{
fs.ensureDirSync(cfgFolder);
fs.writeFileSync(`${tokenFile.cr}.yml`, yaml.stringify(data));
}
catch(e){
console.error('Can\'t save token file to disk!');
}
const cfgFolder = path.dirname(tokenFile.cr);
try{
fs.ensureDirSync(cfgFolder);
fs.writeFileSync(`${tokenFile.cr}.yml`, yaml.stringify(data));
}
catch(e){
console.error('Can\'t save token file to disk!');
}
};
const loadADNToken = () => {
let token = loadYamlCfgFile(tokenFile.adn, true);
if(typeof token !== 'object' || token === null || Array.isArray(token)){
token = {};
}
return token;
let token = loadYamlCfgFile(tokenFile.adn, true);
if(typeof token !== 'object' || token === null || Array.isArray(token)){
token = {};
}
return token;
};
const saveADNToken = (data: Record<string, unknown>) => {
const cfgFolder = path.dirname(tokenFile.adn);
try{
fs.ensureDirSync(cfgFolder);
fs.writeFileSync(`${tokenFile.adn}.yml`, yaml.stringify(data));
}
catch(e){
console.error('Can\'t save token file to disk!');
}
const cfgFolder = path.dirname(tokenFile.adn);
try{
fs.ensureDirSync(cfgFolder);
fs.writeFileSync(`${tokenFile.adn}.yml`, yaml.stringify(data));
}
catch(e){
console.error('Can\'t save token file to disk!');
}
};
const loadAOToken = () => {
let token = loadYamlCfgFile(tokenFile.ao, true);
if(typeof token !== 'object' || token === null || Array.isArray(token)){
token = {};
}
return token;
let token = loadYamlCfgFile(tokenFile.ao, true);
if(typeof token !== 'object' || token === null || Array.isArray(token)){
token = {};
}
return token;
};
const saveAOToken = (data: Record<string, unknown>) => {
const cfgFolder = path.dirname(tokenFile.ao);
try{
fs.ensureDirSync(cfgFolder);
fs.writeFileSync(`${tokenFile.ao}.yml`, yaml.stringify(data));
}
catch(e){
console.error('Can\'t save token file to disk!');
}
const cfgFolder = path.dirname(tokenFile.ao);
try{
fs.ensureDirSync(cfgFolder);
fs.writeFileSync(`${tokenFile.ao}.yml`, yaml.stringify(data));
}
catch(e){
console.error('Can\'t save token file to disk!');
}
};
const loadHDSession = () => {
let session = loadYamlCfgFile(sessCfgFile.hd, true);
if(typeof session !== 'object' || session === null || Array.isArray(session)){
session = {};
}
for(const cv of Object.keys(session)){
if(typeof session[cv] !== 'object' || session[cv] === null || Array.isArray(session[cv])){
session[cv] = {};
let session = loadYamlCfgFile(sessCfgFile.hd, true);
if(typeof session !== 'object' || session === null || Array.isArray(session)){
session = {};
}
}
return session;
for(const cv of Object.keys(session)){
if(typeof session[cv] !== 'object' || session[cv] === null || Array.isArray(session[cv])){
session[cv] = {};
}
}
return session;
};
const saveHDSession = (data: Record<string, unknown>) => {
const cfgFolder = path.dirname(sessCfgFile.hd);
try{
fs.ensureDirSync(cfgFolder);
fs.writeFileSync(`${sessCfgFile.hd}.yml`, yaml.stringify(data));
}
catch(e){
console.error('Can\'t save session file to disk!');
}
const cfgFolder = path.dirname(sessCfgFile.hd);
try{
fs.ensureDirSync(cfgFolder);
fs.writeFileSync(`${sessCfgFile.hd}.yml`, yaml.stringify(data));
}
catch(e){
console.error('Can\'t save session file to disk!');
}
};
const loadHDToken = () => {
let token = loadYamlCfgFile(tokenFile.hd, true);
if(typeof token !== 'object' || token === null || Array.isArray(token)){
token = {};
}
return token;
let token = loadYamlCfgFile(tokenFile.hd, true);
if(typeof token !== 'object' || token === null || Array.isArray(token)){
token = {};
}
return token;
};
const saveHDToken = (data: Record<string, unknown>) => {
const cfgFolder = path.dirname(tokenFile.hd);
try{
fs.ensureDirSync(cfgFolder);
fs.writeFileSync(`${tokenFile.hd}.yml`, yaml.stringify(data));
}
catch(e){
console.error('Can\'t save token file to disk!');
}
const cfgFolder = path.dirname(tokenFile.hd);
try{
fs.ensureDirSync(cfgFolder);
fs.writeFileSync(`${tokenFile.hd}.yml`, yaml.stringify(data));
}
catch(e){
console.error('Can\'t save token file to disk!');
}
};
const saveHDProfile = (data: Record<string, unknown>) => {
const cfgFolder = path.dirname(hdPflCfgFile);
try{
fs.ensureDirSync(cfgFolder);
fs.writeFileSync(`${hdPflCfgFile}.yml`, yaml.stringify(data));
}
catch(e){
console.error('Can\'t save profile file to disk!');
}
const cfgFolder = path.dirname(hdPflCfgFile);
try{
fs.ensureDirSync(cfgFolder);
fs.writeFileSync(`${hdPflCfgFile}.yml`, yaml.stringify(data));
}
catch(e){
console.error('Can\'t save profile file to disk!');
}
};
const loadHDProfile = () => {
let profile = loadYamlCfgFile(hdPflCfgFile, true);
if(typeof profile !== 'object' || profile === null || Array.isArray(profile) || Object.keys(profile).length === 0){
profile = {
// base
ipAddress : '',
xNonce : '',
xSignature: '',
// personal
visitId : '',
// profile data
profile: {
userId : 0,
profileId: 0,
deviceId : '',
},
};
}
return profile;
let profile = loadYamlCfgFile(hdPflCfgFile, true);
if(typeof profile !== 'object' || profile === null || Array.isArray(profile) || Object.keys(profile).length === 0){
profile = {
// base
ipAddress : '',
xNonce : '',
xSignature: '',
// personal
visitId : '',
// profile data
profile: {
userId : 0,
profileId: 0,
deviceId : '',
},
};
}
return profile;
};
const loadNewHDToken = () => {
let token = loadYamlCfgFile(tokenFile.hdNew, true);
if(typeof token !== 'object' || token === null || Array.isArray(token)){
token = {};
}
return token;
let token = loadYamlCfgFile(tokenFile.hdNew, true);
if(typeof token !== 'object' || token === null || Array.isArray(token)){
token = {};
}
return token;
};
const saveNewHDToken = (data: Record<string, unknown>) => {
const cfgFolder = path.dirname(tokenFile.hdNew);
try{
fs.ensureDirSync(cfgFolder);
fs.writeFileSync(`${tokenFile.hdNew}.yml`, yaml.stringify(data));
}
catch(e){
console.error('Can\'t save token file to disk!');
}
const cfgFolder = path.dirname(tokenFile.hdNew);
try{
fs.ensureDirSync(cfgFolder);
fs.writeFileSync(`${tokenFile.hdNew}.yml`, yaml.stringify(data));
}
catch(e){
console.error('Can\'t save token file to disk!');
}
};
const cfgDir = path.join(workingDir, 'config');
const getState = (): GuiState => {
const fn = `${stateFile}.json`;
if (!fs.existsSync(fn)) {
return {
'setup': false,
'services': {}
};
}
try {
return JSON.parse(fs.readFileSync(fn).toString());
} catch(e) {
console.error('Invalid state file, regenerating');
return {
'setup': false,
'services': {}
};
}
const fn = `${stateFile}.json`;
if (!fs.existsSync(fn)) {
return {
'setup': false,
'services': {}
};
}
try {
return JSON.parse(fs.readFileSync(fn).toString());
} catch(e) {
console.error('Invalid state file, regenerating');
return {
'setup': false,
'services': {}
};
}
};
const setState = (state: GuiState) => {
const fn = `${stateFile}.json`;
try {
fs.writeFileSync(fn, JSON.stringify(state, null, 2));
} catch(e) {
console.error('Failed to write state file.');
}
const fn = `${stateFile}.json`;
try {
fs.writeFileSync(fn, JSON.stringify(state, null, 2));
} catch(e) {
console.error('Failed to write state file.');
}
};
export {
loadBinCfg,
loadCfg,
saveCRSession,
loadCRSession,
saveCRToken,
loadCRToken,
saveADNToken,
loadADNToken,
saveHDSession,
loadHDSession,
saveHDToken,
loadHDToken,
saveNewHDToken,
loadNewHDToken,
saveHDProfile,
loadHDProfile,
saveAOToken,
loadAOToken,
getState,
setState,
writeYamlCfgFile,
sessCfgFile,
hdPflCfgFile,
cfgDir
loadBinCfg,
loadCfg,
saveCRSession,
loadCRSession,
saveCRToken,
loadCRToken,
saveADNToken,
loadADNToken,
saveHDSession,
loadHDSession,
saveHDToken,
loadHDToken,
saveNewHDToken,
loadNewHDToken,
saveHDProfile,
loadHDProfile,
saveAOToken,
loadAOToken,
getState,
setState,
writeYamlCfgFile,
sessCfgFile,
hdPflCfgFile,
cfgDir
};

View file

@ -1,26 +1,26 @@
const parse = (data: string) => {
const res: Record<string, {
const res: Record<string, {
value: string,
expires: Date,
path: string,
domain: string,
secure: boolean
}> = {};
const split = data.replace(/\r/g,'').split('\n');
for (const line of split) {
const c = line.split('\t');
if(c.length < 7){
continue;
const split = data.replace(/\r/g,'').split('\n');
for (const line of split) {
const c = line.split('\t');
if(c.length < 7){
continue;
}
res[c[5]] = {
value: c[6],
expires: new Date(parseInt(c[4])*1000),
path: c[2],
domain: c[0].replace(/^\./,''),
secure: c[3] == 'TRUE' ? true : false
};
}
res[c[5]] = {
value: c[6],
expires: new Date(parseInt(c[4])*1000),
path: c[2],
domain: c[0].replace(/^\./,''),
secure: c[3] == 'TRUE' ? true : false
};
}
return res;
return res;
};
export default parse;

View file

@ -39,59 +39,59 @@ const addToArchive = (kind: {
service: 'adn',
type: 's'
}, ID: string) => {
const data = loadData();
const data = loadData();
if (Object.prototype.hasOwnProperty.call(data, kind.service)) {
const items = kind.service === 'crunchy' ? data[kind.service][kind.type] : data[kind.service][kind.type];
if (items.findIndex(a => a.id === ID) >= 0) // Prevent duplicate
return;
items.push({
id: ID,
already: []
});
(data as any)[kind.service][kind.type] = items;
} else {
if (kind.service === 'ao') {
data['ao'] = {
s: [
{
if (Object.prototype.hasOwnProperty.call(data, kind.service)) {
const items = kind.service === 'crunchy' ? data[kind.service][kind.type] : data[kind.service][kind.type];
if (items.findIndex(a => a.id === ID) >= 0) // Prevent duplicate
return;
items.push({
id: ID,
already: []
}
]
};
} else if (kind.service === 'crunchy') {
data['crunchy'] = {
s: ([] as ItemType).concat(kind.type === 's' ? {
id: ID,
already: [] as string[]
} : []),
srz: ([] as ItemType).concat(kind.type === 'srz' ? {
id: ID,
already: [] as string[]
} : []),
};
} else if (kind.service === 'adn') {
data['adn'] = {
s: [
{
id: ID,
already: []
}
]
};
});
(data as any)[kind.service][kind.type] = items;
} else {
data['hidive'] = {
s: [
{
id: ID,
already: []
}
]
};
if (kind.service === 'ao') {
data['ao'] = {
s: [
{
id: ID,
already: []
}
]
};
} else if (kind.service === 'crunchy') {
data['crunchy'] = {
s: ([] as ItemType).concat(kind.type === 's' ? {
id: ID,
already: [] as string[]
} : []),
srz: ([] as ItemType).concat(kind.type === 'srz' ? {
id: ID,
already: [] as string[]
} : []),
};
} else if (kind.service === 'adn') {
data['adn'] = {
s: [
{
id: ID,
already: []
}
]
};
} else {
data['hidive'] = {
s: [
{
id: ID,
already: []
}
]
};
}
}
}
fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4));
fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4));
};
const downloaded = (kind: {
@ -107,49 +107,49 @@ const downloaded = (kind: {
service: 'adn',
type: 's'
}, ID: string, episode: string[]) => {
let data = loadData();
if (!Object.prototype.hasOwnProperty.call(data, kind.service) || !Object.prototype.hasOwnProperty.call(data[kind.service], kind.type)
let data = loadData();
if (!Object.prototype.hasOwnProperty.call(data, kind.service) || !Object.prototype.hasOwnProperty.call(data[kind.service], kind.type)
|| !Object.prototype.hasOwnProperty.call((data as any)[kind.service][kind.type], ID)) {
addToArchive(kind, ID);
data = loadData(); // Load updated version
}
addToArchive(kind, ID);
data = loadData(); // Load updated version
}
const archivedata = (kind.service == 'crunchy' ? data[kind.service][kind.type] : data[kind.service][kind.type]);
const alreadyData = archivedata.find(a => a.id === ID)?.already;
for (const ep of episode) {
if (alreadyData?.includes(ep)) continue;
alreadyData?.push(ep);
}
fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4));
const archivedata = (kind.service == 'crunchy' ? data[kind.service][kind.type] : data[kind.service][kind.type]);
const alreadyData = archivedata.find(a => a.id === ID)?.already;
for (const ep of episode) {
if (alreadyData?.includes(ep)) continue;
alreadyData?.push(ep);
}
fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4));
};
const makeCommand = (service: 'crunchy'|'hidive'|'ao'|'adn') : Partial<ArgvType>[] => {
const data = loadData();
const ret: Partial<ArgvType>[] = [];
const kind = data[service];
for (const type of Object.keys(kind)) {
const item = kind[type as 's']; // 'srz' is also possible but will be ignored for the compiler
item.forEach(i => ret.push({
but: true,
all: false,
service,
e: i.already.join(','),
...(type === 's' ? {
s: i.id,
series: undefined
} : {
series: i.id,
s: undefined
})
}));
}
return ret;
const data = loadData();
const ret: Partial<ArgvType>[] = [];
const kind = data[service];
for (const type of Object.keys(kind)) {
const item = kind[type as 's']; // 'srz' is also possible but will be ignored for the compiler
item.forEach(i => ret.push({
but: true,
all: false,
service,
e: i.already.join(','),
...(type === 's' ? {
s: i.id,
series: undefined
} : {
series: i.id,
s: undefined
})
}));
}
return ret;
};
const loadData = () : DataType => {
if (fs.existsSync(archiveFile))
return JSON.parse(fs.readFileSync(archiveFile).toString()) as DataType;
return {} as DataType;
if (fs.existsSync(archiveFile))
return JSON.parse(fs.readFileSync(archiveFile).toString()) as DataType;
return {} as DataType;
};
export { addToArchive, downloaded, makeCommand };

View file

@ -22,18 +22,18 @@ type GetDataResponse = {
};
function hasDisplay(): boolean {
if (process.platform === 'linux') {
return !!process.env.DISPLAY || !!process.env.WAYLAND_DISPLAY;
}
// Win and Mac true by default
return true;
if (process.platform === 'linux') {
return !!process.env.DISPLAY || !!process.env.WAYLAND_DISPLAY;
}
// Win and Mac true by default
return true;
}
// req
export class Req {
private sessCfg: string;
private service: 'cr' | 'hd' | 'ao' | 'adn';
private session: Record<
private sessCfg: string;
private service: 'cr' | 'hd' | 'ao' | 'adn';
private session: Record<
string,
{
value: string;
@ -44,133 +44,133 @@ export class Req {
'Max-Age'?: string;
}
> = {};
private cfgDir = yamlCfg.cfgDir;
private curl: boolean | string = false;
private cfgDir = yamlCfg.cfgDir;
private curl: boolean | string = false;
constructor(private domain: Record<string, unknown>, private debug: boolean, private nosess = false, private type: 'cr' | 'hd' | 'ao' | 'adn') {
this.sessCfg = yamlCfg.sessCfgFile[type];
this.service = type;
}
async getData(durl: string, params?: RequestInit): Promise<GetDataResponse> {
params = params || {};
// options
const options: RequestInit = {
method: params.method ? params.method : 'GET'
};
// additional params
if (params.headers) {
options.headers = params.headers;
constructor(private domain: Record<string, unknown>, private debug: boolean, private nosess = false, private type: 'cr' | 'hd' | 'ao' | 'adn') {
this.sessCfg = yamlCfg.sessCfgFile[type];
this.service = type;
}
if (params.body) {
options.body = params.body;
}
if (typeof params.redirect == 'string') {
options.redirect = params.redirect;
}
// debug
if (this.debug) {
console.debug('[DEBUG] FETCH OPTIONS:');
console.debug(options);
}
// try do request
try {
const res = await fetch(durl, options);
if (!res.ok) {
console.error(`${res.status}: ${res.statusText}`);
const body = await res.text();
const docTitle = body.match(/<title>(.*)<\/title>/);
if (body && docTitle) {
if (docTitle[1] === 'Just a moment...' && durl.includes('crunchyroll') && hasDisplay()) {
console.warn('Cloudflare triggered, trying to get cookies...');
const { page } = await connect({
headless: false,
turnstile: true
});
await page.goto('https://www.crunchyroll.com/', {
waitUntil: 'networkidle2'
});
await page.waitForRequest('https://www.crunchyroll.com/auth/v1/token');
const cookies = await page.cookies();
await page.close();
params.headers = {
...params.headers,
Cookie: cookies.map((c) => `${c.name}=${c.value}`).join('; '),
'Set-Cookie': cookies.map((c) => `${c.name}=${c.value}`).join('; ')
};
(params as any).headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36';
return await this.getData(durl, params);
} else {
console.error(docTitle[1]);
}
} else {
console.error(body);
async getData(durl: string, params?: RequestInit): Promise<GetDataResponse> {
params = params || {};
// options
const options: RequestInit = {
method: params.method ? params.method : 'GET'
};
// additional params
if (params.headers) {
options.headers = params.headers;
}
}
return {
ok: res.ok,
res,
headers: params.headers as Record<string, string>
};
} catch (_error) {
const error = _error as {
if (params.body) {
options.body = params.body;
}
if (typeof params.redirect == 'string') {
options.redirect = params.redirect;
}
// debug
if (this.debug) {
console.debug('[DEBUG] FETCH OPTIONS:');
console.debug(options);
}
// try do request
try {
const res = await fetch(durl, options);
if (!res.ok) {
console.error(`${res.status}: ${res.statusText}`);
const body = await res.text();
const docTitle = body.match(/<title>(.*)<\/title>/);
if (body && docTitle) {
if (docTitle[1] === 'Just a moment...' && durl.includes('crunchyroll') && hasDisplay()) {
console.warn('Cloudflare triggered, trying to get cookies...');
const { page } = await connect({
headless: false,
turnstile: true
});
await page.goto('https://www.crunchyroll.com/', {
waitUntil: 'networkidle2'
});
await page.waitForRequest('https://www.crunchyroll.com/auth/v1/token');
const cookies = await page.cookies();
await page.close();
params.headers = {
...params.headers,
Cookie: cookies.map((c) => `${c.name}=${c.value}`).join('; '),
'Set-Cookie': cookies.map((c) => `${c.name}=${c.value}`).join('; ')
};
(params as any).headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36';
return await this.getData(durl, params);
} else {
console.error(docTitle[1]);
}
} else {
console.error(body);
}
}
return {
ok: res.ok,
res,
headers: params.headers as Record<string, string>
};
} catch (_error) {
const error = _error as {
name: string;
} & TypeError & {
res: Response;
};
if (error.res && error.res.status && error.res.statusText) {
console.error(`${error.name} ${error.res.status}: ${error.res.statusText}`);
} else {
console.error(`${error.name}: ${error.res?.statusText || error.message}`);
}
if (error.res) {
const body = await error.res.text();
const docTitle = body.match(/<title>(.*)<\/title>/);
if (body && docTitle) {
console.error(docTitle[1]);
if (error.res && error.res.status && error.res.statusText) {
console.error(`${error.name} ${error.res.status}: ${error.res.statusText}`);
} else {
console.error(`${error.name}: ${error.res?.statusText || error.message}`);
}
if (error.res) {
const body = await error.res.text();
const docTitle = body.match(/<title>(.*)<\/title>/);
if (body && docTitle) {
console.error(docTitle[1]);
}
}
return {
ok: false,
error
};
}
}
return {
ok: false,
error
};
}
}
}
export function buildProxy(proxyBaseUrl: string, proxyAuth: string) {
if (!proxyBaseUrl.match(/^(https?|socks4|socks5):/)) {
proxyBaseUrl = 'http://' + proxyBaseUrl;
}
if (!proxyBaseUrl.match(/^(https?|socks4|socks5):/)) {
proxyBaseUrl = 'http://' + proxyBaseUrl;
}
const proxyCfg = new URL(proxyBaseUrl);
let proxyStr = `${proxyCfg.protocol}//`;
const proxyCfg = new URL(proxyBaseUrl);
let proxyStr = `${proxyCfg.protocol}//`;
if (typeof proxyCfg.hostname != 'string' || proxyCfg.hostname == '') {
throw new Error('[ERROR] Hostname and port required for proxy!');
}
if (typeof proxyCfg.hostname != 'string' || proxyCfg.hostname == '') {
throw new Error('[ERROR] Hostname and port required for proxy!');
}
if (proxyAuth && typeof proxyAuth == 'string' && proxyAuth.match(':')) {
proxyCfg.username = proxyAuth.split(':')[0];
proxyCfg.password = proxyAuth.split(':')[1];
proxyStr += `${proxyCfg.username}:${proxyCfg.password}@`;
}
if (proxyAuth && typeof proxyAuth == 'string' && proxyAuth.match(':')) {
proxyCfg.username = proxyAuth.split(':')[0];
proxyCfg.password = proxyAuth.split(':')[1];
proxyStr += `${proxyCfg.username}:${proxyCfg.password}@`;
}
proxyStr += proxyCfg.hostname;
proxyStr += proxyCfg.hostname;
if (!proxyCfg.port && proxyCfg.protocol == 'http:') {
proxyStr += ':80';
} else if (!proxyCfg.port && proxyCfg.protocol == 'https:') {
proxyStr += ':443';
}
if (!proxyCfg.port && proxyCfg.protocol == 'http:') {
proxyStr += ':80';
} else if (!proxyCfg.port && proxyCfg.protocol == 'https:') {
proxyStr += ':443';
}
return proxyStr;
return proxyStr;
}

View file

@ -1,51 +1,51 @@
import fs from 'fs';
export function convertChaptersToFFmpegFormat(inputFilePath: string): string {
const content = fs.readFileSync(inputFilePath, 'utf-8');
const content = fs.readFileSync(inputFilePath, 'utf-8');
const chapterMatches = Array.from(content.matchAll(/CHAPTER(\d+)=([\d:.]+)/g));
const nameMatches = Array.from(content.matchAll(/CHAPTER(\d+)NAME=([^\n]+)/g));
const chapterMatches = Array.from(content.matchAll(/CHAPTER(\d+)=([\d:.]+)/g));
const nameMatches = Array.from(content.matchAll(/CHAPTER(\d+)NAME=([^\n]+)/g));
const chapters = chapterMatches.map((m) => ({
index: parseInt(m[1], 10),
time: m[2],
})).sort((a, b) => a.index - b.index);
const chapters = chapterMatches.map((m) => ({
index: parseInt(m[1], 10),
time: m[2],
})).sort((a, b) => a.index - b.index);
const nameDict: Record<number, string> = {};
nameMatches.forEach((m) => {
nameDict[parseInt(m[1], 10)] = m[2];
});
const nameDict: Record<number, string> = {};
nameMatches.forEach((m) => {
nameDict[parseInt(m[1], 10)] = m[2];
});
let ffmpegContent = ';FFMETADATA1\n';
let startTimeInNs = 0;
let ffmpegContent = ';FFMETADATA1\n';
let startTimeInNs = 0;
for (let i = 0; i < chapters.length; i++) {
const chapterStartTime = timeToNanoSeconds(chapters[i].time);
const chapterEndTime = (i + 1 < chapters.length)
? timeToNanoSeconds(chapters[i + 1].time)
: chapterStartTime + 1000000000;
for (let i = 0; i < chapters.length; i++) {
const chapterStartTime = timeToNanoSeconds(chapters[i].time);
const chapterEndTime = (i + 1 < chapters.length)
? timeToNanoSeconds(chapters[i + 1].time)
: chapterStartTime + 1000000000;
const chapterName = nameDict[chapters[i].index] || `Chapter ${chapters[i].index}`;
const chapterName = nameDict[chapters[i].index] || `Chapter ${chapters[i].index}`;
ffmpegContent += '[CHAPTER]\n';
ffmpegContent += 'TIMEBASE=1/1000000000\n';
ffmpegContent += `START=${startTimeInNs}\n`;
ffmpegContent += `END=${chapterEndTime}\n`;
ffmpegContent += `title=${chapterName}\n`;
ffmpegContent += '[CHAPTER]\n';
ffmpegContent += 'TIMEBASE=1/1000000000\n';
ffmpegContent += `START=${startTimeInNs}\n`;
ffmpegContent += `END=${chapterEndTime}\n`;
ffmpegContent += `title=${chapterName}\n`;
startTimeInNs = chapterEndTime;
}
startTimeInNs = chapterEndTime;
}
return ffmpegContent;
return ffmpegContent;
}
export function timeToNanoSeconds(time: string): number {
const parts = time.split(':');
const hours = parseInt(parts[0], 10);
const minutes = parseInt(parts[1], 10);
const secondsAndMs = parts[2].split('.');
const seconds = parseInt(secondsAndMs[0], 10);
const milliseconds = parseInt(secondsAndMs[1], 10);
const parts = time.split(':');
const hours = parseInt(parts[0], 10);
const minutes = parseInt(parts[1], 10);
const secondsAndMs = parts[2].split('.');
const seconds = parseInt(secondsAndMs[0], 10);
const milliseconds = parseInt(secondsAndMs[1], 10);
return (hours * 3600 + minutes * 60 + seconds) * 1000000000 + milliseconds * 1000000;
return (hours * 3600 + minutes * 60 + seconds) * 1000000000 + milliseconds * 1000000;
}

Some files were not shown because too many files have changed in this diff Show more