mirror of
https://github.com/anidl/multi-downloader-nx.git
synced 2026-03-11 09:35:30 +00:00
Merge pull request #1078 from anidl/tabs
Convert to Tabs (4 space) instead of 2 space
This commit is contained in:
commit
8365e9d9e4
125 changed files with 15868 additions and 15866 deletions
3
.git-blame-ignore-revs
Normal file
3
.git-blame-ignore-revs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Ignore the whitespace changes in the following commits
|
||||
460b4c1d0e12c88459aaff786fbe348c4c3d517a
|
||||
a14466ec5d29accbe81b5ffac6e0a1373d04e356
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
54
@types/adnPlayerConfig.d.ts
vendored
54
@types/adnPlayerConfig.d.ts
vendored
|
|
@ -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
76
@types/adnSearch.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
62
@types/adnStreams.d.ts
vendored
62
@types/adnStreams.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
12
@types/adnSubtitles.d.ts
vendored
12
@types/adnSubtitles.d.ts
vendored
|
|
@ -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
132
@types/adnVideos.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
166
@types/animeOnegaiSearch.d.ts
vendored
166
@types/animeOnegaiSearch.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
62
@types/animeOnegaiSeasons.d.ts
vendored
62
@types/animeOnegaiSeasons.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
200
@types/animeOnegaiSeries.d.ts
vendored
200
@types/animeOnegaiSeries.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
72
@types/animeOnegaiStream.d.ts
vendored
72
@types/animeOnegaiStream.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
208
@types/crunchyAndroidEpisodes.d.ts
vendored
208
@types/crunchyAndroidEpisodes.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
306
@types/crunchyAndroidObject.d.ts
vendored
306
@types/crunchyAndroidObject.d.ts
vendored
|
|
@ -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',
|
||||
}
|
||||
128
@types/crunchyAndroidStreams.d.ts
vendored
128
@types/crunchyAndroidStreams.d.ts
vendored
|
|
@ -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',
|
||||
}
|
||||
200
@types/crunchyEpisodeList.d.ts
vendored
200
@types/crunchyEpisodeList.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
274
@types/crunchySearch.d.ts
vendored
274
@types/crunchySearch.d.ts
vendored
|
|
@ -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',
|
||||
}
|
||||
344
@types/crunchyTypes.d.ts
vendored
344
@types/crunchyTypes.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
4
@types/downloadedFile.d.ts
vendored
4
@types/downloadedFile.d.ts
vendored
|
|
@ -1,6 +1,6 @@
|
|||
import { LanguageItem } from '../modules/module.langsData';
|
||||
|
||||
export type DownloadedFile = {
|
||||
path: string,
|
||||
lang: LanguageItem
|
||||
path: string,
|
||||
lang: LanguageItem
|
||||
}
|
||||
|
|
@ -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'
|
||||
}
|
||||
106
@types/hidiveDashboard.d.ts
vendored
106
@types/hidiveDashboard.d.ts
vendored
|
|
@ -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',
|
||||
}
|
||||
134
@types/hidiveEpisodeList.d.ts
vendored
134
@types/hidiveEpisodeList.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
78
@types/hidiveSearch.d.ts
vendored
78
@types/hidiveSearch.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
86
@types/hidiveTypes.d.ts
vendored
86
@types/hidiveTypes.d.ts
vendored
|
|
@ -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
14
@types/iso639.d.ts
vendored
|
|
@ -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
262
@types/items.d.ts
vendored
|
|
@ -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[];
|
||||
}
|
||||
94
@types/m3u8-parsed.d.ts
vendored
94
@types/m3u8-parsed.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
194
@types/messageHandler.d.ts
vendored
194
@types/messageHandler.d.ts
vendored
|
|
@ -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
192
@types/mpd-parser.d.ts
vendored
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
64
@types/newHidiveEpisode.d.ts
vendored
64
@types/newHidiveEpisode.d.ts
vendored
|
|
@ -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[];
|
||||
}
|
||||
38
@types/newHidivePlayback.d.ts
vendored
38
@types/newHidivePlayback.d.ts
vendored
|
|
@ -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',
|
||||
}
|
||||
|
|
|
|||
100
@types/newHidiveSearch.d.ts
vendored
100
@types/newHidiveSearch.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
126
@types/newHidiveSeason.d.ts
vendored
126
@types/newHidiveSeason.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
48
@types/newHidiveSeries.d.ts
vendored
48
@types/newHidiveSeries.d.ts
vendored
|
|
@ -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
332
@types/objectInfo.d.ts
vendored
|
|
@ -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
2
@types/pkg.d.ts
vendored
|
|
@ -1,3 +1,3 @@
|
|||
declare module 'pkg' {
|
||||
export async function exec(config: string[]);
|
||||
export async function exec(config: string[]);
|
||||
}
|
||||
188
@types/playbackData.d.ts
vendored
188
@types/playbackData.d.ts
vendored
|
|
@ -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',
|
||||
}
|
||||
12
@types/randomEvents.d.ts
vendored
12
@types/randomEvents.d.ts
vendored
|
|
@ -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;
|
||||
2
@types/removeNPMAbsolutePaths.d.ts
vendored
2
@types/removeNPMAbsolutePaths.d.ts
vendored
|
|
@ -1,3 +1,3 @@
|
|||
declare module 'removeNPMAbsolutePaths' {
|
||||
export default async function modulesCleanup(path: string);
|
||||
export default async function modulesCleanup(path: string);
|
||||
}
|
||||
2
@types/serviceClassInterface.d.ts
vendored
2
@types/serviceClassInterface.d.ts
vendored
|
|
@ -1,3 +1,3 @@
|
|||
export interface ServiceClass {
|
||||
cli: () => Promise<boolean|undefined|void>
|
||||
cli: () => Promise<boolean|undefined|void>
|
||||
}
|
||||
30
@types/streamData.d.ts
vendored
30
@types/streamData.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
4
@types/updateFile.d.ts
vendored
4
@types/updateFile.d.ts
vendored
|
|
@ -1,4 +1,4 @@
|
|||
export type UpdateFile = {
|
||||
lastCheck: number,
|
||||
nextCheck: number
|
||||
lastCheck: number,
|
||||
nextCheck: number
|
||||
}
|
||||
62
@types/ws.d.ts
vendored
62
@types/ws.d.ts
vendored
|
|
@ -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]
|
||||
}
|
||||
5652
crunchy.ts
5652
crunchy.ts
File diff suppressed because it is too large
Load diff
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"presets": ["@babel/preset-env","@babel/preset-react", "@babel/preset-typescript"]
|
||||
"presets": ["@babel/preset-env","@babel/preset-react", "@babel/preset-typescript"]
|
||||
}
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
2
gui/react/src/@types/FC.d.ts
vendored
2
gui/react/src/@types/FC.d.ts
vendored
|
|
@ -1,3 +1,3 @@
|
|||
type FCWithChildren<T = object> = React.FC<{
|
||||
children?: React.ReactNode[]|React.ReactNode
|
||||
children?: React.ReactNode[]|React.ReactNode
|
||||
} & T>
|
||||
|
|
@ -2,9 +2,9 @@ import React from 'react';
|
|||
import Layout from './Layout';
|
||||
|
||||
const App: React.FC = () => {
|
||||
return (
|
||||
<Layout />
|
||||
);
|
||||
return (
|
||||
<Layout />
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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>;
|
||||
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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>;
|
||||
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
174
index.ts
174
index.ts
|
|
@ -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();
|
||||
}
|
||||
})();
|
||||
|
|
@ -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');
|
||||
});
|
||||
|
||||
|
||||
|
|
|
|||
176
modules/build.ts
176
modules/build.ts
|
|
@ -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;
|
||||
}
|
||||
290
modules/cdm.ts
290
modules/cdm.ts
|
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue