prettier fix + updated to node 22 + added new workflow + crunchyroll updates + --version fix

This commit is contained in:
stratumadev 2025-09-30 14:43:25 +02:00
parent 8365e9d9e4
commit 130d964b64
144 changed files with 19708 additions and 18998 deletions

View file

@ -1,93 +1,93 @@
name: Bug
description: File a bug report
assignees:
- AnimeDL
- AnidlSupport
assignees:
- AnimeDL
- AnidlSupport
labels:
- bug
title: "[BUG]: "
- bug
title: '[BUG]: '
body:
- type: markdown
attributes:
value: |
Thank you for reporting the issues you found and have with this program.
This template will guide you through all the information we need.
- type: input
id: version
attributes:
label: Program version
description: "Which version of the program do you use?"
placeholder: "1.0.0"
validations:
required: true
- type: dropdown
id: opsystem
attributes:
label: Operating System
description: "Please tell us what OS you are using."
options:
- Windows
- Linux
- MacOS
validations:
required: true
- type: dropdown
id: gui
attributes:
label: Type
description: "Please tell us if you are using the gui or the cli version."
options:
- CLI
- GUI
validations:
required: true
- type: dropdown
id: service
attributes:
label: Service
description: "Please tell us what service the bug occured in."
options:
- Crunchyroll
- Hidive
- AnimationDigitalNetwork
- AnimeOnegai
- All
- Irrelevant
validations:
required: true
- type: input
id: command
attributes:
label: Command used
description: "Please tell us what command you used."
validations:
required: true
- type: input
id: ShowID
attributes:
label: Show ID
description: "Please provide the ID of an example show."
placeholder: "1234"
validations:
required: true
- type: input
id: Episode
attributes:
label: Episode
description: "Please provide the episode ID you used as an example."
placeholder: "1"
validations:
required: true
- type: textarea
id: output
attributes:
label: Console Output
description: "Please paste the console output from the beginning till termination here. If you are using the gui open the log folder under 'Debug > Open Log Folder' in the Menu. Please copy the content of latest.log here."
render: Shell
validations:
required: true
- type: textarea
id: additionalInfos
attributes:
label: Additional Information
description: "Do you have any additional information you can provide?"
- type: markdown
attributes:
value: |
Thank you for reporting the issues you found and have with this program.
This template will guide you through all the information we need.
- type: input
id: version
attributes:
label: Program version
description: 'Which version of the program do you use?'
placeholder: '1.0.0'
validations:
required: true
- type: dropdown
id: opsystem
attributes:
label: Operating System
description: 'Please tell us what OS you are using.'
options:
- Windows
- Linux
- MacOS
validations:
required: true
- type: dropdown
id: gui
attributes:
label: Type
description: 'Please tell us if you are using the gui or the cli version.'
options:
- CLI
- GUI
validations:
required: true
- type: dropdown
id: service
attributes:
label: Service
description: 'Please tell us what service the bug occured in.'
options:
- Crunchyroll
- Hidive
- AnimationDigitalNetwork
- AnimeOnegai
- All
- Irrelevant
validations:
required: true
- type: input
id: command
attributes:
label: Command used
description: 'Please tell us what command you used.'
validations:
required: true
- type: input
id: ShowID
attributes:
label: Show ID
description: 'Please provide the ID of an example show.'
placeholder: '1234'
validations:
required: true
- type: input
id: Episode
attributes:
label: Episode
description: 'Please provide the episode ID you used as an example.'
placeholder: '1'
validations:
required: true
- type: textarea
id: output
attributes:
label: Console Output
description: "Please paste the console output from the beginning till termination here. If you are using the gui open the log folder under 'Debug > Open Log Folder' in the Menu. Please copy the content of latest.log here."
render: Shell
validations:
required: true
- type: textarea
id: additionalInfos
attributes:
label: Additional Information
description: 'Do you have any additional information you can provide?'

View file

@ -1,29 +1,29 @@
name: Enhancement
description: Suggest a enhancement or feature
labels:
- enhancement
title: "[Feedback]: "
- enhancement
title: '[Feedback]: '
body:
- type: markdown
attributes:
value: |
Thank you for giving feedback with this program.
This template will guide you through all the information we need.
- type: dropdown
id: programversion
attributes:
label: Type
description: "Is this suggestion for the CLI, GUI, or Both?"
options:
- CLI
- GUI
- Both
validations:
required: true
- type: textarea
id: suggestion
attributes:
label: Suggestion
description: "What is your suggestion?"
validations:
required: true
- type: markdown
attributes:
value: |
Thank you for giving feedback with this program.
This template will guide you through all the information we need.
- type: dropdown
id: programversion
attributes:
label: Type
description: 'Is this suggestion for the CLI, GUI, or Both?'
options:
- CLI
- GUI
- Both
validations:
required: true
- type: textarea
id: suggestion
attributes:
label: Suggestion
description: 'What is your suggestion?'
validations:
required: true

View file

@ -1,6 +1,11 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: 'daily'
open-pull-requests-limit: 0
allow:
- dependency-type: 'direct'
ignore:
- dependency-type: 'all'

View file

@ -1,26 +1,26 @@
name: auto-documentation
on:
push:
branches: [ master ]
push:
branches: [master]
jobs:
documentation:
runs-on: ubuntu-latest
documentation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
ref: ${{ github.head_ref }}
- uses: pnpm/action-setup@v2
with:
version: latest
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: 20
- run: pnpm i
- run: pnpm run docs
- uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: ${{ github.event.head_commit.message }} + Documentation
steps:
- uses: actions/checkout@v2
with:
ref: ${{ github.head_ref }}
- uses: pnpm/action-setup@v2
with:
version: latest
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: 22
- run: pnpm i
- run: pnpm run docs
- uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: ${{ github.event.head_commit.message }} + Documentation

View file

@ -3,31 +3,30 @@
name: build and push docker image
on:
push:
branches: [ master ]
workflow_dispatch:
push:
branches: [master]
workflow_dispatch:
jobs:
build-node:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
if: ${{ github.ref == 'refs/heads/master' }}
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker images
uses: docker/build-push-action@v2.9.0
with:
github-token: ${{ github.token }}
push: ${{ github.ref == 'refs/heads/master' }}
tags: |
"multidl/multi-downloader-nx:latest"
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
build-node:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
if: ${{ github.ref == 'refs/heads/master' }}
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker images
uses: docker/build-push-action@v2.9.0
with:
github-token: ${{ github.token }}
push: ${{ github.ref == 'refs/heads/master' }}
tags: |
"multidl/multi-downloader-nx:latest"
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}

View file

@ -1,66 +1,66 @@
name: Release Builds
on:
release:
types: [ published ]
release:
types: [published]
jobs:
build:
strategy:
matrix:
build_type: [ linux, macos, windows ]
build_arch: [ x64 ]
gui: [ gui, cli ]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- uses: pnpm/action-setup@v2
with:
version: latest
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: 20
check-latest: true
- name: Install Node modules
run: |
pnpm install
- name: Get name and version from package.json
run: |
test -n $(node -p -e "require('./package.json').name") &&
test -n $(node -p -e "require('./package.json').version") &&
echo PACKAGE_NAME=$(node -p -e "require('./package.json').name") >> $GITHUB_ENV &&
echo PACKAGE_VERSION=$(node -p -e "require('./package.json').version") >> $GITHUB_ENV || exit 1
- name: Make build
run: pnpm run build-${{ matrix.build_type }}-${{ matrix.gui }}
- name: Upload release
uses: actions/upload-release-asset@v1
with:
upload_url: ${{ github.event.release.upload_url }}
asset_name: multi-downloader-nx-${{ matrix.build_type }}-${{ matrix.gui }}.7z
asset_path: ./lib/_builds/multi-downloader-nx-${{ matrix.build_type }}-${{ matrix.build_arch }}-${{ matrix.gui }}.7z
asset_content_type: application/x-7z-compressed
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker images
uses: docker/build-push-action@v2.9.0
with:
github-token: ${{ github.token }}
push: true
tags: |
"multidl/multi-downloader-nx:${{ github.event.release.tag_name }}"
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
build:
strategy:
matrix:
build_type: [linux, macos, windows]
build_arch: [x64]
gui: [gui, cli]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- uses: pnpm/action-setup@v2
with:
version: latest
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: 22
check-latest: true
- name: Install Node modules
run: |
pnpm install
- name: Get name and version from package.json
run: |
test -n $(node -p -e "require('./package.json').name") &&
test -n $(node -p -e "require('./package.json').version") &&
echo PACKAGE_NAME=$(node -p -e "require('./package.json').name") >> $GITHUB_ENV &&
echo PACKAGE_VERSION=$(node -p -e "require('./package.json').version") >> $GITHUB_ENV || exit 1
- name: Make build
run: pnpm run build-${{ matrix.build_type }}-${{ matrix.gui }}
- name: Upload release
uses: actions/upload-release-asset@v1
with:
upload_url: ${{ github.event.release.upload_url }}
asset_name: multi-downloader-nx-${{ matrix.build_type }}-${{ matrix.gui }}.7z
asset_path: ./lib/_builds/multi-downloader-nx-${{ matrix.build_type }}-${{ matrix.build_arch }}-${{ matrix.gui }}.7z
asset_content_type: application/x-7z-compressed
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker images
uses: docker/build-push-action@v2.9.0
with:
github-token: ${{ github.token }}
push: true
tags: |
"multidl/multi-downloader-nx:${{ github.event.release.tag_name }}"
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}

View file

@ -7,6 +7,20 @@ on:
branches: [ master ]
jobs:
tsc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: pnpm/action-setup@v2
with:
version: latest
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: 22
check-latest: true
- run: pnpm i
- run: npx tsc
eslint:
runs-on: ubuntu-latest
steps:
@ -17,10 +31,24 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: 20
node-version: 22
check-latest: true
- run: pnpm i
- run: npx eslint . --quiet
- run: pnpm run eslint
prettier:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: pnpm/action-setup@v2
with:
version: latest
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: 22
check-latest: true
- run: pnpm i
- run: pnpm run prettier
test:
needs: eslint
runs-on: ubuntu-latest
@ -32,7 +60,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: 20
node-version: 22
check-latest: true
- run: pnpm i
- run: pnpm run test

23
.prettierignore Normal file
View file

@ -0,0 +1,23 @@
# Build
build
lib
# Local
.vscode
.idea
.DS_Store
node_modules
logs
ffmpeg
mkvmerge
fonts
*.json
*.md
*.yaml
!package.json
!tsconfig.json
docker-compose.yml
# Auto generated
docs

View file

@ -1,20 +1,20 @@
{
"arrowParens": "always",
"bracketSameLine": false,
"bracketSpacing": true,
"embeddedLanguageFormatting": "auto",
"htmlWhitespaceSensitivity": "strict",
"insertPragma": false,
"jsxSingleQuote": false,
"proseWrap": "never",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": true,
"singleQuote": true,
"tabWidth": 4,
"trailingComma": "none",
"useTabs": true,
"vueIndentScriptAndStyle": false,
"printWidth": 180,
"endOfLine": "auto"
"arrowParens": "always",
"bracketSameLine": false,
"bracketSpacing": true,
"embeddedLanguageFormatting": "auto",
"htmlWhitespaceSensitivity": "strict",
"insertPragma": false,
"jsxSingleQuote": false,
"proseWrap": "never",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": true,
"singleQuote": true,
"tabWidth": 4,
"trailingComma": "none",
"useTabs": true,
"vueIndentScriptAndStyle": false,
"printWidth": 180,
"endOfLine": "auto"
}

View file

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

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

@ -4,40 +4,40 @@ export interface ADNSearch {
}
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[];
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;
indexable: boolean;
}
export interface ProductionTeam {

View file

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

View file

@ -3,9 +3,9 @@ export interface ADNSubtitles {
}
export interface Subtitle {
startTime: number;
endTime: number;
startTime: number;
endTime: number;
positionAlign: string;
lineAlign: string;
text: string;
lineAlign: string;
text: string;
}

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

@ -3,72 +3,72 @@ export interface ADNVideos {
}
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;
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;
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[];
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;
indexable: boolean;
}
export interface ProductionTeam {

View file

@ -7,82 +7,82 @@ 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;
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;
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;
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;
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;
}
paid_content: number;
rent_content: number;
objectID: string;
lang: string;
}

View file

@ -1,36 +1,36 @@
export interface AnimeOnegaiSeasons {
ID: number;
CreatedAt: Date;
UpdatedAt: Date;
DeletedAt: null;
name: string;
number: number;
asset_id: number;
entry: string;
description: string;
active: boolean;
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[];
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;
ID: number;
CreatedAt: Date;
UpdatedAt: Date;
DeletedAt: null;
name: string;
number: number;
description: string;
thumbnail: string;
entry: string;
video_entry: string;
active: boolean;
season_id: number;
stream_url: string;
skip_intro: null;
ending: null;
open_free: boolean;
asset_id: number;
age_restriction: boolean;
}

View file

@ -1,111 +1,111 @@
export interface AnimeOnegaiSeries {
ID: number;
CreatedAt: Date;
UpdatedAt: Date;
DeletedAt: null;
title: string;
active: boolean;
excerpt: string;
description: string;
bg: string;
poster: string;
entry: string;
code_name: string;
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;
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;
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;
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;
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;
permalink?: string;
background?: string;
}
export interface SList {
ID: number;
CreatedAt: Date;
UpdatedAt: Date;
DeletedAt: null;
name: string;
ID: number;
CreatedAt: Date;
UpdatedAt: Date;
DeletedAt: null;
name: string;
age_restriction?: number;
}
export interface TagsList {
ID: number;
ID: number;
CreatedAt: Date;
UpdatedAt: Date;
DeletedAt: null;
name: string;
position: number;
status: boolean;
name: string;
position: number;
status: boolean;
}

View file

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

View file

@ -1,85 +1,85 @@
import { Images } from './crunchyEpisodeList';
export interface CrunchyAndroidEpisodes {
__class__: string;
__href__: string;
__class__: string;
__href__: string;
__resource_key__: string;
__links__: object;
__actions__: object;
total: number;
items: CrunchyAndroidEpisode[];
__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[];
__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;
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/channel': Link;
'episode/next_episode': Link;
'episode/season': Link;
'episode/series': Link;
streams: Link;
'episode/season': Link;
'episode/series': Link;
streams: Link;
}
export interface Link {
@ -87,9 +87,9 @@ export interface Link {
}
export interface Thumbnail {
width: number;
width: number;
height: number;
type: string;
type: string;
source: string;
}
@ -109,28 +109,27 @@ export enum Locale {
hiIN = 'hi-IN',
zhCN = 'zh-CN',
koKR = 'ko-KR',
jaJP = 'ja-JP',
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;
audio_locale: Locale;
guid: string;
original: boolean;
variant: string;
season_guid: string;
media_guid: string;
is_premium_only: boolean;
}

View file

@ -1,49 +1,49 @@
import { ImageType, Images, Image } from './objectInfo';
export interface CrunchyAndroidObject {
__class__: string;
__href__: string;
__class__: string;
__href__: string;
__resource_key__: string;
__links__: object;
__actions__: object;
total: number;
items: AndroidObject[];
__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;
__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;
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;
'episode/season': LinkData;
'episode/series': LinkData;
resource: LinkData;
'resource/channel': LinkData;
streams: LinkData;
streams: LinkData;
}
export interface LinkData {
@ -51,119 +51,119 @@ export interface LinkData {
}
export interface EpisodeMetadata {
audio_locale: Locale;
availability_ends: Date;
availability_notes: string;
availability_starts: Date;
available_date: null;
available_offline: boolean;
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[];
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;
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[];
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;
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;
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[];
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[];
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;
guid: string;
original: boolean;
variant: string;
}
export interface SeriesMetadata {
audio_locales: Locale[];
availability_notes: string;
episode_count: number;
extended_description: 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[];
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 {
@ -182,5 +182,5 @@ export enum Locale {
hiIN = 'hi-IN',
zhCN = 'zh-CN',
koKR = 'ko-KR',
jaJP = 'ja-JP',
}
jaJP = 'ja-JP'
}

View file

@ -1,37 +1,21 @@
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>;
}
export interface Subtitles {
'': Subtitle;
'en-US'?: Subtitle;
'es-LA'?: 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;
'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 {
@ -48,8 +32,8 @@ export interface Streams {
export interface Download {
hardsub_locale: Locale;
hardsub_lang?: string;
url: string;
hardsub_lang?: string;
url: string;
}
export interface Urls {
@ -58,17 +42,17 @@ export interface Urls {
export interface Subtitle {
locale: Locale;
url: string;
url: string;
format: string;
}
export interface Version {
audio_locale: Locale;
guid: string;
original: boolean;
variant: string;
season_guid: string;
media_guid: string;
audio_locale: Locale;
guid: string;
original: boolean;
variant: string;
season_guid: string;
media_guid: string;
is_premium_only: boolean;
}
@ -89,5 +73,5 @@ export enum Locale {
hiIN = 'hi-IN',
zhCN = 'zh-CN',
koKR = 'ko-KR',
jaJP = 'ja-JP',
}
jaJP = 'ja-JP'
}

View file

@ -1,26 +1,26 @@
export interface CrunchyChapters {
[key: string]: CrunchyChapter;
lastUpdate: Date;
mediaId: string;
[key: string]: CrunchyChapter;
lastUpdate: Date;
mediaId: string;
}
export interface CrunchyChapter {
approverId: string;
distributionNumber: string;
end: number;
start: number;
title: string;
seriesId: string;
new: boolean;
type: string;
approverId: string;
distributionNumber: string;
end: number;
start: number;
title: string;
seriesId: string;
new: boolean;
type: string;
}
export interface CrunchyOldChapter {
media_id: string;
startTime: number;
endTime: number;
duration: number;
comparedWith: string;
ordering: string;
last_updated: Date;
}
media_id: string;
startTime: number;
endTime: number;
duration: number;
comparedWith: string;
ordering: string;
last_updated: Date;
}

View file

@ -2,69 +2,69 @@ import { Links } from './crunchyAndroidEpisodes';
export interface CrunchyEpisodeList {
total: number;
data: CrunchyEpisode[];
meta: Meta;
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;
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;
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 {
@ -83,52 +83,52 @@ export enum Locale {
hiIN = 'hi-IN',
zhCN = 'zh-CN',
koKR = 'ko-KR',
jaJP = 'ja-JP',
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[]>;
thumbnail?: Array<Image[]>;
}
export interface Image {
height: number;
source: string;
type: ImageType;
width: number;
type: ImageType;
width: number;
}
export enum ImageType {
PosterTall = 'poster_tall',
PosterWide = 'poster_wide',
PromoImage = 'promo_image',
Thumbnail = 'thumbnail',
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;
audio_locale: Locale;
guid: string;
is_premium_only: boolean;
media_guid: string;
original: boolean;
season_guid: string;
variant: string;
media_guid: string;
original: boolean;
season_guid: string;
variant: string;
}
export interface Meta {
versions_considered?: boolean;
}
}

View file

@ -1,44 +1,44 @@
import { Locale } from './playbackData';
export interface CrunchyPlayStream {
assetId: string;
audioLocale: Locale;
bifs: string;
burnedInLocale: string;
captions: { [key: string]: Caption };
hardSubs: { [key: string]: HardSub };
playbackType: string;
session: Session;
subtitles: { [key: string]: Subtitle };
token: string;
url: string;
versions: any[];
assetId: string;
audioLocale: Locale;
bifs: string;
burnedInLocale: string;
captions: { [key: string]: Caption };
hardSubs: { [key: string]: HardSub };
playbackType: string;
session: Session;
subtitles: { [key: string]: Subtitle };
token: string;
url: string;
versions: any[];
}
export interface Caption {
format: string;
language: string;
url: string;
format: string;
language: string;
url: string;
}
export interface HardSub {
hlang: string;
url: string;
quality: string;
hlang: string;
url: string;
quality: string;
}
export interface Session {
renewSeconds: number;
noNetworkRetryIntervalSeconds: number;
noNetworkTimeoutSeconds: number;
maximumPauseSeconds: number;
endOfVideoUnloadSeconds: number;
sessionExpirationSeconds: number;
usesStreamLimits: boolean;
renewSeconds: number;
noNetworkRetryIntervalSeconds: number;
noNetworkTimeoutSeconds: number;
maximumPauseSeconds: number;
endOfVideoUnloadSeconds: number;
sessionExpirationSeconds: number;
usesStreamLimits: boolean;
}
export interface Subtitle {
format: string;
language: string;
url: string;
format: string;
language: string;
url: string;
}

View file

@ -2,79 +2,79 @@
export interface CrunchySearch {
total: number;
data: CrunchySearchData[];
meta: Record<string, unknown>;
data: CrunchySearchData[];
meta: Record<string, unknown>;
}
export interface CrunchySearchData {
type: string;
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;
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;
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;
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[];
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 {
@ -93,65 +93,65 @@ export enum Locale {
hiIN = 'hi-IN',
zhCN = 'zh-CN',
koKR = 'ko-KR',
jaJP = 'ja-JP',
jaJP = 'ja-JP'
}
export enum MaturityRating {
Tv14 = 'TV-14',
TvMa = 'TV-MA',
TvMa = 'TV-MA'
}
export interface Version {
audio_locale: Locale;
guid: string;
audio_locale: Locale;
guid: string;
is_premium_only: boolean;
media_guid: string;
original: boolean;
season_guid: string;
variant: string;
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[]>;
thumbnail?: Array<Image[]>;
}
export interface Image {
height: number;
source: string;
type: ImageType;
width: number;
type: ImageType;
width: number;
}
export enum ImageType {
PosterTall = 'poster_tall',
PosterWide = 'poster_wide',
PromoImage = 'promo_image',
Thumbnail = 'thumbnail',
Thumbnail = 'thumbnail'
}
export interface MovieListingMetadata {
availability_notes: string;
available_date: null;
available_offline: boolean;
duration_ms: number;
extended_description: 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[];
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 {
@ -159,25 +159,25 @@ export interface SearchMetadata {
}
export interface SeriesMetadata {
audio_locales: Locale[];
availability_notes: string;
episode_count: number;
extended_description: 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[];
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',
}
Series = 'series'
}

View file

@ -5,180 +5,191 @@ import { DownloadInfo } from './messageHandler';
import { CrunchyVideoPlayStreams, CrunchyAudioPlayStreams } from './enums';
export type CrunchyDownloadOptions = {
hslang: 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[],
}
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[];
// Subtitle Fix Options
srtAssFix: boolean;
layoutResFix: boolean;
scaledBorderAndShadowFix: boolean;
scaledBorderAndShadow: 'yes' | 'no';
originalScriptFix: boolean;
};
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,
}
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: 'Audio',
lang: LanguageItem,
path: string,
isPrimary?: boolean
} | {
type: 'Chapters',
lang: LanguageItem,
path: string
} | ({
type: 'Subtitle',
signs: boolean,
cc: boolean
} & sxItem )
export type DownloadedMedia =
| {
type: 'Video';
lang: LanguageItem;
path: string;
isPrimary?: boolean;
}
| {
type: 'Audio';
lang: LanguageItem;
path: string;
isPrimary?: boolean;
}
| {
type: 'Chapters';
lang: LanguageItem;
path: string;
}
| ({
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,
}
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;
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[];
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;
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;
guid: string;
original: boolean;
variant: string;
}
export interface EpisodeVersion {
@ -207,7 +218,7 @@ export enum Locale {
hiIN = 'hi-IN',
zhCN = 'zh-CN',
koKR = 'ko-KR',
jaJP = 'ja-JP',
jaJP = 'ja-JP'
}
export interface Meta {

View file

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

View file

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

598
@types/episode.d.ts vendored
View file

@ -1,391 +1,391 @@
// Generated by https://quicktype.io
export interface EpisodeData {
id: number;
title: string;
mediaDict: { [key: string]: string };
episodeSlug: string;
starRating: number;
parent: EpisodeDataParent;
number: string;
description: string;
filename: string;
seriesBanner: string;
media: Media[];
externalItemId: string;
contentId: string;
metaItems: MetaItems;
thumb: string;
type: Type;
default: { [key: string]: Default };
published: boolean;
versions: VersionClass[];
mediaCategory: string;
order: number;
seriesVersions: any[];
source: Source;
ids: EpisodeDataIDS;
runtime: string;
siblings: PreviousSeasonEpisode[];
seriesTitle: string;
seriesSlug: string;
next: Next;
previousSeasonEpisode: PreviousSeasonEpisode;
seasonTitle: string;
quality: Quality;
ratings: Array<string[]>;
languages: TitleElement[];
releaseDate: string;
historicalSelections: HistoricalSelections;
userRating: UserRating;
id: number;
title: string;
mediaDict: { [key: string]: string };
episodeSlug: string;
starRating: number;
parent: EpisodeDataParent;
number: string;
description: string;
filename: string;
seriesBanner: string;
media: Media[];
externalItemId: string;
contentId: string;
metaItems: MetaItems;
thumb: string;
type: Type;
default: { [key: string]: Default };
published: boolean;
versions: VersionClass[];
mediaCategory: string;
order: number;
seriesVersions: any[];
source: Source;
ids: EpisodeDataIDS;
runtime: string;
siblings: PreviousSeasonEpisode[];
seriesTitle: string;
seriesSlug: string;
next: Next;
previousSeasonEpisode: PreviousSeasonEpisode;
seasonTitle: string;
quality: Quality;
ratings: Array<string[]>;
languages: TitleElement[];
releaseDate: string;
historicalSelections: HistoricalSelections;
userRating: UserRating;
}
export interface Default {
items: DefaultItem[];
items: DefaultItem[];
}
export interface DefaultItem {
languages: string[];
territories: string[];
version: null;
value: Value[];
devices: any[];
languages: string[];
territories: string[];
version: null;
value: Value[];
devices: any[];
}
export interface Value {
name: MetaType;
value: string;
label: Label;
name: MetaType;
value: string;
label: Label;
}
export enum Label {
Rating = 'Rating',
RatingSystem = 'Rating System',
ReleaseDate = 'Release Date',
Synopsis = 'Synopsis',
SynopsisType = 'Synopsis Type',
Rating = 'Rating',
RatingSystem = 'Rating System',
ReleaseDate = 'Release Date',
Synopsis = 'Synopsis',
SynopsisType = 'Synopsis Type'
}
export enum MetaType {
Rating = 'rating',
RatingSystemType = 'RatingSystemType',
ReleaseDate = 'release-date',
Synopsis = 'synopsis',
Synopsistype = 'synopsistype',
VideoRatingType = 'VideoRatingType',
Rating = 'rating',
RatingSystemType = 'RatingSystemType',
ReleaseDate = 'release-date',
Synopsis = 'synopsis',
Synopsistype = 'synopsistype',
VideoRatingType = 'VideoRatingType'
}
export interface HistoricalSelections {
version: string;
language: string;
version: string;
language: string;
}
export interface EpisodeDataIDS {
externalShowId: string;
externalSeasonId: string;
externalEpisodeId: string;
externalShowId: string;
externalSeasonId: string;
externalEpisodeId: string;
}
export enum TitleElement {
Empty = '',
English = 'English',
Empty = '',
English = 'English'
}
export interface Media {
id: number;
title: string;
experienceType: string;
created: string;
createdBy: string;
itemFieldData: Next;
keyPath: string;
filename: string;
complianceStatus: null;
events: any[];
clients: string[];
qcStatus: null;
qcStatusDate: null;
image: string;
thumb: string;
ext: string;
avails: Avail[];
version: string;
startTimecode: null;
endTimecode: null;
versionId: string;
mediaType: string;
status: string;
languages: LanguageClass[];
territories: any[];
devices: any[];
keyType: string;
purpose: null;
externalItemId: null | string;
proxyId: null;
externalDbId: null;
mediaChildren: MediaChild[];
isDefault: boolean;
parent: MediaChildParent;
filePath: null | string;
mediaInfo: Next;
type: string;
approved: boolean;
mediaKey: string;
itemFields: any[];
source: Source;
fieldData: Next;
sourceId: null | string;
timecodeOverride: null;
seriesTitle: string;
episodeTitle: string;
genre: any[];
txDate: string;
description: string;
synopsis: string;
resolution: null;
restrictedAccess: boolean;
createdById: string;
userIdsWithAccess: any[];
runtime?: number;
language?: TitleElement;
purchased: boolean;
id: number;
title: string;
experienceType: string;
created: string;
createdBy: string;
itemFieldData: Next;
keyPath: string;
filename: string;
complianceStatus: null;
events: any[];
clients: string[];
qcStatus: null;
qcStatusDate: null;
image: string;
thumb: string;
ext: string;
avails: Avail[];
version: string;
startTimecode: null;
endTimecode: null;
versionId: string;
mediaType: string;
status: string;
languages: LanguageClass[];
territories: any[];
devices: any[];
keyType: string;
purpose: null;
externalItemId: null | string;
proxyId: null;
externalDbId: null;
mediaChildren: MediaChild[];
isDefault: boolean;
parent: MediaChildParent;
filePath: null | string;
mediaInfo: Next;
type: string;
approved: boolean;
mediaKey: string;
itemFields: any[];
source: Source;
fieldData: Next;
sourceId: null | string;
timecodeOverride: null;
seriesTitle: string;
episodeTitle: string;
genre: any[];
txDate: string;
description: string;
synopsis: string;
resolution: null;
restrictedAccess: boolean;
createdById: string;
userIdsWithAccess: any[];
runtime?: number;
language?: TitleElement;
purchased: boolean;
}
export interface Avail {
id: number;
description: string;
endDate: string;
startDate: string;
ids: AvailIDS;
originalAirDate: null;
physicalReleaseDate: null;
preorderDate: null;
language: TitleElement;
territory: string;
territoryCode: string;
license: string;
parentAvail: null;
item: number;
version: string;
applyToLevel: null;
availLevel: string;
availDisplayCode: string;
availStatus: string;
bundleOnly: boolean;
contentOwnerOrganization: string;
currency: null;
price: null;
purchase: string;
priceValue: string;
resolutionFormat: null;
runtimeMilliseconds: null;
seasonOrEpisodeNumber: null;
tmsid: null;
deviceList: string;
tvodSku: null;
id: number;
description: string;
endDate: string;
startDate: string;
ids: AvailIDS;
originalAirDate: null;
physicalReleaseDate: null;
preorderDate: null;
language: TitleElement;
territory: string;
territoryCode: string;
license: string;
parentAvail: null;
item: number;
version: string;
applyToLevel: null;
availLevel: string;
availDisplayCode: string;
availStatus: string;
bundleOnly: boolean;
contentOwnerOrganization: string;
currency: null;
price: null;
purchase: string;
priceValue: string;
resolutionFormat: null;
runtimeMilliseconds: null;
seasonOrEpisodeNumber: null;
tmsid: null;
deviceList: string;
tvodSku: null;
}
export interface AvailIDS {
externalSeasonId: string;
externalAsianId: null;
externalShowId: string;
externalEpisodeId: string;
externalEnglishId: string;
externalAlphaId: string;
externalSeasonId: string;
externalAsianId: null;
externalShowId: string;
externalEpisodeId: string;
externalEnglishId: string;
externalAlphaId: string;
}
export type Next = Record<string, unknown>
export type Next = Record<string, unknown>;
export interface LanguageClass {
code: string;
id: number;
title: TitleElement;
code: string;
id: number;
title: TitleElement;
}
export interface MediaChild {
id: number;
title: string;
experienceType: string;
created: string;
createdBy: string;
itemFieldData: Next;
keyPath: null;
filename: string;
complianceStatus: null;
events: any[];
clients: string[];
qcStatus: null;
qcStatusDate: null;
image: string;
ext: string;
avails: any[];
version: string;
startTimecode: null;
endTimecode: null;
versionId: string;
mediaType: string;
status: string;
languages: LanguageClass[];
territories: any[];
devices: any[];
keyType: string;
purpose: null;
externalItemId: string;
proxyId: null;
externalDbId: null;
mediaChildren: any[];
isDefault: boolean;
parent: MediaChildParent;
filePath: string;
mediaInfo: MediaInfo;
type: string;
approved: boolean;
mediaKey: null;
itemFields: any[];
source: Source;
fieldData: Next;
sourceId: null;
timecodeOverride: null;
seriesTitle: string;
episodeTitle: string;
genre: any[];
txDate: string;
description: string;
synopsis: string;
resolution: null | string;
restrictedAccess: boolean;
createdById: string;
userIdsWithAccess: any[];
language: TitleElement;
id: number;
title: string;
experienceType: string;
created: string;
createdBy: string;
itemFieldData: Next;
keyPath: null;
filename: string;
complianceStatus: null;
events: any[];
clients: string[];
qcStatus: null;
qcStatusDate: null;
image: string;
ext: string;
avails: any[];
version: string;
startTimecode: null;
endTimecode: null;
versionId: string;
mediaType: string;
status: string;
languages: LanguageClass[];
territories: any[];
devices: any[];
keyType: string;
purpose: null;
externalItemId: string;
proxyId: null;
externalDbId: null;
mediaChildren: any[];
isDefault: boolean;
parent: MediaChildParent;
filePath: string;
mediaInfo: MediaInfo;
type: string;
approved: boolean;
mediaKey: null;
itemFields: any[];
source: Source;
fieldData: Next;
sourceId: null;
timecodeOverride: null;
seriesTitle: string;
episodeTitle: string;
genre: any[];
txDate: string;
description: string;
synopsis: string;
resolution: null | string;
restrictedAccess: boolean;
createdById: string;
userIdsWithAccess: any[];
language: TitleElement;
}
export interface MediaInfo {
imageAspectRatio: null | string;
format: string;
scanMode: null | string;
burnedInSubtitleLanguage: string;
screenAspectRatio: null | string;
subtitleFormat: null | string;
subtitleContent: null | string;
frameHeight: number | null;
frameWidth: number | null;
video: Video;
imageAspectRatio: null | string;
format: string;
scanMode: null | string;
burnedInSubtitleLanguage: string;
screenAspectRatio: null | string;
subtitleFormat: null | string;
subtitleContent: null | string;
frameHeight: number | null;
frameWidth: number | null;
video: Video;
}
export interface Video {
codecId: null | string;
container: null | string;
encodingRate: number | null;
frameRate: null | string;
height: number | null;
width: number | null;
duration: number | null;
bitRate: number | null;
codecId: null | string;
container: null | string;
encodingRate: number | null;
frameRate: null | string;
height: number | null;
width: number | null;
duration: number | null;
bitRate: number | null;
}
export interface MediaChildParent {
title: string;
type: string;
catalogParent: CatalogParent;
slug: string;
grandparentId: number;
id: number;
title: string;
type: string;
catalogParent: CatalogParent;
slug: string;
grandparentId: number;
id: number;
}
export interface CatalogParent {
id: number;
title: string;
id: number;
title: string;
}
export enum Source {
Dbb = 'dbb',
Dbb = 'dbb'
}
export interface MetaItems {
items: Items;
filters: Filters;
items: Items;
filters: Filters;
}
export interface Filters {
territory: any[];
language: any[];
territory: any[];
language: any[];
}
export interface Items {
'release-date': AnimationProductionStudio;
rating: AnimationProductionStudio;
synopsis: AnimationProductionStudio;
'animation-production-studio': AnimationProductionStudio;
'release-date': AnimationProductionStudio;
rating: AnimationProductionStudio;
synopsis: AnimationProductionStudio;
'animation-production-studio': AnimationProductionStudio;
}
export interface AnimationProductionStudio {
items: AnimationProductionStudioItem[];
label: string;
id: number;
slug: string;
items: AnimationProductionStudioItem[];
label: string;
id: number;
slug: string;
}
export interface AnimationProductionStudioItem {
id: number;
metaType: MetaType;
metaTypeId: string;
client: null;
languages: TitleElement;
territories: string;
devices: string;
isDefault: boolean;
value: Value[];
approved: boolean;
version: null;
source: Source;
id: number;
metaType: MetaType;
metaTypeId: string;
client: null;
languages: TitleElement;
territories: string;
devices: string;
isDefault: boolean;
value: Value[];
approved: boolean;
version: null;
source: Source;
}
export interface EpisodeDataParent {
seasonId: number;
seasonNumber: string;
title: string;
titleSlug: string;
titleType: string;
titleId: number;
seasonId: number;
seasonNumber: string;
title: string;
titleSlug: string;
titleType: string;
titleId: number;
}
export interface PreviousSeasonEpisode {
seasonTitle?: string;
mediaCategory: Type;
thumb: string;
title: string;
image: string;
number: string;
id: number;
version: string[];
order: number;
slug: string;
season?: number;
languages?: TitleElement[];
seasonTitle?: string;
mediaCategory: Type;
thumb: string;
title: string;
image: string;
number: string;
id: number;
version: string[];
order: number;
slug: string;
season?: number;
languages?: TitleElement[];
}
export enum Type {
Episode = 'episode',
Ova = 'ova',
Episode = 'episode',
Ova = 'ova'
}
export interface Quality {
quality: string;
height: number;
quality: string;
height: number;
}
export interface UserRating {
overall: number;
ja: number;
eng: number;
overall: number;
ja: number;
eng: number;
}
export interface VersionClass {
compliance_approved: boolean;
title: string;
version_id: string;
is_default: boolean;
runtime: string;
external_id: string;
id: number;
compliance_approved: boolean;
title: string;
version_id: string;
is_default: boolean;
runtime: string;
external_id: string;
id: number;
}

156
@types/github.d.ts vendored
View file

@ -1,106 +1,106 @@
export type GithubTag = {
name: string,
zipball_url: string,
tarball_url: string,
commit: {
sha: string,
url: string
},
node_id: string
}
name: string;
zipball_url: string;
tarball_url: string;
commit: {
sha: string;
url: string;
};
node_id: string;
};
export interface TagCompare {
url: string;
html_url: string;
permalink_url: string;
diff_url: string;
patch_url: string;
base_commit: BaseCommitClass;
merge_base_commit: BaseCommitClass;
status: string;
ahead_by: number;
behind_by: number;
total_commits: number;
commits: BaseCommitClass[];
files: File[];
url: string;
html_url: string;
permalink_url: string;
diff_url: string;
patch_url: string;
base_commit: BaseCommitClass;
merge_base_commit: BaseCommitClass;
status: string;
ahead_by: number;
behind_by: number;
total_commits: number;
commits: BaseCommitClass[];
files: File[];
}
export interface BaseCommitClass {
sha: string;
node_id: string;
commit: BaseCommitCommit;
url: string;
html_url: string;
comments_url: string;
author: BaseCommitAuthor;
committer: BaseCommitAuthor;
parents: Parent[];
sha: string;
node_id: string;
commit: BaseCommitCommit;
url: string;
html_url: string;
comments_url: string;
author: BaseCommitAuthor;
committer: BaseCommitAuthor;
parents: Parent[];
}
export interface BaseCommitAuthor {
login: string;
id: number;
node_id: string;
avatar_url: string;
gravatar_id: string;
url: string;
html_url: string;
followers_url: string;
following_url: string;
gists_url: string;
starred_url: string;
subscriptions_url: string;
organizations_url: string;
repos_url: string;
events_url: string;
received_events_url: string;
type: string;
site_admin: boolean;
login: string;
id: number;
node_id: string;
avatar_url: string;
gravatar_id: string;
url: string;
html_url: string;
followers_url: string;
following_url: string;
gists_url: string;
starred_url: string;
subscriptions_url: string;
organizations_url: string;
repos_url: string;
events_url: string;
received_events_url: string;
type: string;
site_admin: boolean;
}
export interface BaseCommitCommit {
author: PurpleAuthor;
committer: PurpleAuthor;
message: string;
tree: Tree;
url: string;
comment_count: number;
verification: Verification;
author: PurpleAuthor;
committer: PurpleAuthor;
message: string;
tree: Tree;
url: string;
comment_count: number;
verification: Verification;
}
export interface PurpleAuthor {
name: string;
email: string;
date: string;
name: string;
email: string;
date: string;
}
export interface Tree {
sha: string;
url: string;
sha: string;
url: string;
}
export interface Verification {
verified: boolean;
reason: string;
signature: string;
payload: string;
verified: boolean;
reason: string;
signature: string;
payload: string;
}
export interface Parent {
sha: string;
url: string;
html_url: string;
sha: string;
url: string;
html_url: string;
}
export interface File {
sha: string;
filename: string;
status: string;
additions: number;
deletions: number;
changes: number;
blob_url: string;
raw_url: string;
contents_url: string;
patch: string;
sha: string;
filename: string;
status: string;
additions: number;
deletions: number;
changes: number;
blob_url: string;
raw_url: string;
contents_url: string;
patch: string;
}

View file

@ -1,70 +1,70 @@
export interface HidiveDashboard {
Code: number;
Status: string;
Message: null;
Messages: object;
Data: Data;
Code: number;
Status: string;
Message: null;
Messages: object;
Data: Data;
Timestamp: string;
IPAddress: string;
}
export interface Data {
TitleRows: TitleRow[];
LoadTime: number;
LoadTime: number;
}
export interface TitleRow {
Name: string;
Titles: Title[];
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;
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;
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;
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;
CreatedDT: Date;
ModifiedDT: Date | null;
}
export enum Status {
Paused = 'Paused',
Playing = 'Playing',
Watching = 'Watching',
}
Watching = 'Watching'
}

View file

@ -1,9 +1,9 @@
export interface HidiveEpisodeList {
Code: number;
Status: string;
Message: null;
Messages: Record<unknown, unknown>;
Data: Data;
Code: number;
Status: string;
Message: null;
Messages: Record<unknown, unknown>;
Data: Data;
Timestamp: string;
IPAddress: string;
}
@ -13,72 +13,72 @@ export interface Data {
}
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;
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;
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;
Id: string;
ProfileId: number;
EpisodeId: number;
Status: string;
CurrentTime: number;
UserId: number;
TitleId: number;
SeasonId: number;
VideoId: number;
TotalSeconds: number;
CreatedDT: Date;
ModifiedDT: Date;
CreatedDT: Date;
ModifiedDT: Date;
}
export interface HidiveEpisode {
Id: number;
Number: number;
Name: string;
Summary: string;
HIDIVEPremiereDate: Date;
ScreenShotSmallUrl: string;
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;
SeasonNumber: number;
TitleId: number;
SeasonNumberValue: number;
EpisodeNumberValue: number;
VideoKey: string;
DisplayNameLong: string;
PercentProgress: number;
LoadTime: number;
}
export interface HidiveEpisodeExtra extends HidiveEpisode {
titleId: number;
epKey: string;
nameLong: string;
seriesTitle: string;
seriesId?: number;
isSelected: boolean;
}
titleId: number;
epKey: string;
nameLong: string;
seriesTitle: string;
seriesId?: number;
isSelected: boolean;
}

View file

@ -1,47 +1,47 @@
export interface HidiveSearch {
Code: number;
Status: string;
Message: null;
Messages: Record<unknown, unknown>;
Data: HidiveSearchData;
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;
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;
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;
}
ContinueWatching: null;
Episodes: any[];
LoadTime: number;
}

View file

@ -1,61 +1,63 @@
export interface HidiveVideoList {
Code: number;
Status: string;
Message: null;
Messages: Record<unknown, unknown>;
Data: HidiveVideo;
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;
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;
MaxStreams: number;
CurrentTime: number;
FontColorCode: string;
RunTime: number;
AdUrl: null;
}
export interface HidiveStreamList {
hls: string[];
drm: string[];
hls: string[];
drm: string[];
drmEnabled: boolean;
}
export interface HidiveStreamInfo extends HidiveStreamList {
language?: string;
episodeTitle?: string;
seriesTitle?: string;
season?: number;
language?: string;
episodeTitle?: string;
seriesTitle?: string;
season?: number;
episodeNumber?: number;
uncut?: boolean;
image?: string;
uncut?: boolean;
image?: string;
}
export interface HidiveSubtitleInfo {
language: string;
cc: boolean;
url: string;
cc: boolean;
url: string;
}
export type DownloadedMedia = {
type: 'Video',
lang: LanguageItem,
path: string,
uncut: boolean
} | ({
type: 'Subtitle',
cc: boolean
} & sxItem )
export type DownloadedMedia =
| {
type: 'Video';
lang: LanguageItem;
path: string;
uncut: boolean;
}
| ({
type: 'Subtitle';
cc: boolean;
} & sxItem);

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

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

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

@ -1,169 +1,168 @@
export interface Item {
// Added later
id: string,
id_split: (number|string)[]
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;
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;
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;
audio: string[];
mostRecentAvod: MostRecent;
}
export enum ContentType {
Episode = 'episode',
Ova = 'ova',
Ova = 'ova'
}
export interface IDs {
externalShowId: ID;
externalSeasonId: ExternalSeasonID;
externalShowId: ID;
externalSeasonId: ExternalSeasonID;
externalEpisodeId: string;
externalAsianId?: 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;
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;
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;
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;
externalSeasonId: ExternalSeasonID;
externalAsianId: null;
externalShowId: ID;
externalEpisodeId: string;
externalEnglishId: string;
externalAlphaId: string;
externalAlphaId: string;
}
export enum Purchase {
AVOD = 'A-VOD',
Dfov = 'DFOV',
Est = 'EST',
Svod = 'SVOD',
Svod = 'SVOD'
}
export enum Version {
Simulcast = 'Simulcast',
Uncut = 'Uncut',
Uncut = 'Uncut'
}
export type MostRecentSvodJpnUs = Record<string, any>
export type MostRecentSvodJpnUs = Record<string, any>;
export interface QualityClass {
quality: QualityQuality;
height: number;
height: number;
}
export enum QualityQuality {
HD = 'HD',
SD = 'SD',
SD = 'SD'
}
export interface TitleImages {
showThumbnail: string;
showBackgroundSite: string;
showDetailHeaderDesktop: string;
continueWatchingDesktop: string;
showDetailHeroSite: string;
appleHorizontalBannerShow: string;
backgroundImageXbox_360: string;
applePosterCover: string;
showDetailBoxArtTablet: 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;
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[];
Uncut?: Audio[];
Simulcast: Audio[];
}
}

View file

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

View file

@ -4,111 +4,145 @@ import type { AvailableMuxer } from '../modules/module.args';
import { LanguageItem } from '../modules/module.langsData';
export interface MessageHandler {
name: string
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,
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>
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[],
title: string;
episode: string;
fileName: string;
dlsubs: string[];
parent: {
title: string,
season: string
},
q: number,
dlVideoOnce: boolean,
dubLang: string[],
image: string,
} & ResolveItemsData
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
}
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
e: string;
lang: string[];
name: string;
season: string;
seasonTitle: string;
episode: string;
id: string;
img: string;
description: string;
time: string;
};
export type AuthData = { username: string, password: string };
export type SearchData = { search: string, page?: number, 'search-type'?: string, 'search-locale'?: 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 }
export type FuniSubsData = { nosubs?: boolean, sub: boolean, dlsubs: string[], ccTag: 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;
};
export type AuthData = { username: string; password: string };
export type SearchData = { search: string; page?: number; 'search-type'?: string; 'search-locale'?: 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;
};
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>;
export type FuniSearchReponse = ResponseBase<FunimationSearch>;
@ -116,46 +150,47 @@ export type FuniShowResponse = ResponseBase<FuniEpisodeData[]>;
export type FuniGetEpisodeResponse = ResponseBase<undefined>;
export type CheckTokenResponse = ResponseBase<undefined>;
export type ResponseBase<T> = ({
isOk: true,
value: T
} | {
isOk: false,
reason: Error
});
export type ResponseBase<T> =
| {
isOk: true;
value: T;
}
| {
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,
export type DownloadInfo = {
image: string;
parent: {
title: string
},
title: string,
language: LanguageItem,
fileName: string
}
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[];
};

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

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

View file

@ -1,43 +1,43 @@
export interface NewHidiveEpisode {
description: string;
duration: number;
title: string;
categories: string[];
contentDownload: ContentDownload;
favourite: boolean;
subEvents: any[];
thumbnailUrl: string;
longDescription: string;
posterUrl: string;
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;
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;
period: string;
}
export interface EpisodeInformation {
seasonNumber: number;
seasonNumber: number;
episodeNumber: number;
season: number;
season: number;
}
export interface Rating {
rating: string;
rating: string;
descriptors: any[];
}
}

View file

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

View file

@ -3,56 +3,56 @@ export interface NewHidiveSearch {
}
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;
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;
serverTimeMS: number;
}
export interface Exhaustive {
nbHits: boolean;
typo: 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;
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[];
name: Description;
description: Description;
tags: Description[];
localisations: HighlightResultLocalisations;
}
export interface Description {
value: string;
matchLevel: string;
matchedWords: string[];
value: string;
matchLevel: string;
matchedWords: string[];
fullyHighlighted?: boolean;
}
@ -61,7 +61,7 @@ export interface HighlightResultLocalisations {
}
export interface PurpleEnUS {
title: Description;
title: Description;
description: Description;
}
@ -70,7 +70,7 @@ export interface HitLocalisations {
}
export interface HitLocalization {
title: string;
title: string;
description: string;
}
@ -83,6 +83,6 @@ export interface ProcessingTimingsMS {
}
export interface Request {
queue: number;
queue: number;
roundTrip: number;
}

View file

@ -1,52 +1,52 @@
export interface NewHidiveSeason {
title: string;
description: string;
title: string;
description: string;
longDescription: string;
smallCoverUrl: string;
coverUrl: string;
titleUrl: string;
posterUrl: string;
seasonNumber: number;
episodeCount: number;
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[];
rating: Rating;
contentRating: Rating;
id: number;
series: Series;
episodes: Episode[];
paging: Paging;
licences: any[];
}
export interface Rating {
rating: string;
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;
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[];
externalAssetId: string;
subEvents: any[];
maxHeight: number;
thumbnailsPreview: string;
longDescription: string;
episodeInformation: EpisodeInformation;
categories: string[];
displayableTags: any[];
watchStatus: string;
computedReleases: any[];
}
export interface ContentDownload {
@ -54,24 +54,24 @@ export interface ContentDownload {
}
export interface EpisodeInformation {
seasonNumber: number;
seasonNumber: number;
episodeNumber: number;
season: number;
season: number;
}
export interface Paging {
moreDataAvailable: boolean;
lastSeen: number;
lastSeen: number;
}
export interface Series {
seriesId: number;
title: string;
description: string;
seriesId: number;
title: string;
description: string;
longDescription: string;
displayableTags: any[];
rating: Rating;
contentRating: Rating;
rating: Rating;
contentRating: Rating;
}
export interface NewHidiveSeriesExtra extends Series {
@ -79,11 +79,11 @@ export interface NewHidiveSeriesExtra extends Series {
}
export interface NewHidiveEpisodeExtra extends Episode {
titleId: number;
nameLong: string;
seasonTitle: string;
seriesTitle: string;
seriesId?: number;
isSelected: boolean;
jwtToken?: string;
}
titleId: number;
nameLong: string;
seasonTitle: string;
seriesTitle: string;
seriesId?: number;
isSelected: boolean;
jwtToken?: string;
}

View file

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

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

@ -2,42 +2,42 @@
export interface ObjectInfo {
total: number;
data: CrunchyObject[];
meta: Record<unknown>;
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;
__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;
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;
'episode/season': LinkData;
'episode/series': LinkData;
resource: LinkData;
'resource/channel': LinkData;
streams: LinkData;
streams: LinkData;
}
export interface LinkData {
@ -45,150 +45,150 @@ export interface LinkData {
}
export interface EpisodeMetadata {
audio_locale: Locale;
availability_ends: Date;
availability_notes: string;
availability_starts: Date;
available_date: null;
available_offline: boolean;
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[];
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;
audio_locale: Locale;
guid: string;
is_premium_only: boolean;
media_guid: string;
original: boolean;
season_guid: string;
variant: string;
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[]>;
thumbnail?: Array<Image[]>;
}
export interface Image {
height: number;
source: string;
type: ImageType;
width: number;
type: ImageType;
width: number;
}
export enum ImageType {
PosterTall = 'poster_tall',
PosterWide = 'poster_wide',
PromoImage = 'promo_image',
Thumbnail = 'thumbnail',
Thumbnail = 'thumbnail'
}
export interface MovieListingMetadata {
availability_notes: string;
available_date: null;
available_offline: boolean;
duration_ms: number;
extended_description: 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[];
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;
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;
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[];
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[];
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;
guid: string;
original: boolean;
variant: string;
}
export interface SeriesMetadata {
audio_locales: Locale[];
availability_notes: string;
episode_count: number;
extended_description: 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[];
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 {
@ -207,5 +207,5 @@ export enum Locale {
hiIN = 'hi-IN',
zhCN = 'zh-CN',
koKR = 'ko-KR',
jaJP = 'ja-JP',
}
jaJP = 'ja-JP'
}

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

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

View file

@ -7,96 +7,95 @@ export interface PlaybackData {
}
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;
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;
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;
'': 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;
'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;
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;
media_id: string;
subtitles: Subtitles;
bifs: string[];
versions: Version[];
audio_locale: Locale;
closed_captions: Subtitles;
captions: Subtitles;
captions: Subtitles;
}
export interface Subtitles {
''?: SubtitleInfo;
'en-US'?: SubtitleInfo;
'es-LA'?: 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;
'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;
url: string;
}
export interface Version {
audio_locale: Locale;
guid: string;
audio_locale: Locale;
guid: string;
is_premium_only: boolean;
media_guid: string;
original: boolean;
season_guid: string;
variant: string;
media_guid: string;
original: boolean;
season_guid: string;
variant: string;
}
export enum Locale {
@ -116,5 +115,5 @@ export enum Locale {
hiIN = 'hi-IN',
zhCN = 'zh-CN',
koKR = 'ko-KR',
jaJP = 'ja-JP',
}
jaJP = 'ja-JP'
}

View file

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

View file

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

View file

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

View file

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

View file

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

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

@ -1,45 +1,45 @@
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]
}
export type WSMessage<T extends keyof MessageTypes, P extends 0 | 1 = 0> = {
name: T;
data: MessageTypes[T][P];
};
export type WSMessageWithID<T extends keyof MessageTypes, P extends 0|1 = 0> = WSMessage<T, P> & {
id: string
}
export type WSMessageWithID<T extends keyof MessageTypes, P extends 0 | 1 = 0> = WSMessage<T, P> & {
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];
};

1717
adn.ts

File diff suppressed because it is too large Load diff

1568
ao.ts

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
ffmpeg: "ffmpeg.exe"
mkvmerge: "mkvmerge.exe"
ffprobe: "ffprobe.exe"
mp4decrypt: "mp4decrypt.exe"
shaka: "shaka-packager.exe"
ffmpeg: 'ffmpeg.exe'
mkvmerge: 'mkvmerge.exe'
ffprobe: 'ffprobe.exe'
mp4decrypt: 'mp4decrypt.exe'
shaka: 'shaka-packager.exe'

View file

@ -13,16 +13,16 @@ dlVideoOnce: false
# Whether to keep all downloaded videos or only a single copy
keepAllVideos: false
# What to use as the file name template
fileName: "[${service}] ${showTitle} - S${season}E${episode} [${height}p]"
fileName: '[${service}] ${showTitle} - S${season}E${episode} [${height}p]'
# What Audio languages to download
dubLang: ["jpn"]
dubLang: ['jpn']
# What Subtitle languages to download
dlsubs: ["all"]
dlsubs: ['all']
# What language Audio to set as default
defaultAudio: "jpn"
defaultAudio: 'jpn'
# Video Playback Endpoint (Crunchyroll)
vstream: "androidtv"
vstream: 'androidtv'
# Audio Playback Endpoint (Crunchyroll)
astream: "android"
astream: 'android'
# Total Session Death (Could kill active streaming sessions from watching users if account shared, use with caution) (Crunchyroll)
tsd: false

6130
crunchy.ts

File diff suppressed because it is too large Load diff

28
dev.js
View file

@ -3,19 +3,21 @@ const path = require('path');
const toRun = process.argv.slice(2).join(' ').split('---');
const waitForProcess = async (proc) => {
return new Promise((resolve, reject) => {
proc.stdout?.on('data', (data) => process.stdout.write(data));
proc.stderr?.on('data', (data) => process.stderr.write(data));
proc.on('close', resolve);
proc.on('error', reject);
});
return new Promise((resolve, reject) => {
proc.stdout?.on('data', (data) => process.stdout.write(data));
proc.stderr?.on('data', (data) => process.stderr.write(data));
proc.on('close', resolve);
proc.on('error', reject);
});
};
(async () => {
await waitForProcess(exec('pnpm run tsc test false'));
for (let command of toRun) {
await waitForProcess(exec(`node index.js --service hidive ${command}`, {
cwd: path.join(__dirname, 'lib')
}));
}
})();
await waitForProcess(exec('pnpm run tsc test false'));
for (let command of toRun) {
await waitForProcess(
exec(`node index.js --service hidive ${command}`, {
cwd: path.join(__dirname, 'lib')
})
);
}
})();

View file

@ -2,61 +2,46 @@
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import prettier from 'eslint-config-prettier';
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',
4
],
'linebreak-style': [
'warn',
'windows'
],
'quotes': [
'error',
'single'
],
'semi': [
'error',
'always'
]
},
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'
}
}
);
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', { avoidEscape: true }],
semi: ['error', 'always']
},
languageOptions: {
parserOptions: {
ecmaFeatures: {
jsx: true
},
ecmaVersion: 2020,
sourceType: 'module'
},
parser: tseslint.parser
}
},
{
ignores: ['**/lib', '**/videos', '**/build', 'dev.js', 'tsc.ts']
},
{
files: ['gui/react/**/*'],
rules: {
'no-console': 0,
indent: 'off'
}
},
// Disables all rules that conflict with prettier
prettier
);

2
gui.ts
View file

@ -1,3 +1,3 @@
process.env.isGUI = 'true';
import './modules/log';
import './gui/server/index';
import './gui/server/index';

View file

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

View file

@ -54,4 +54,4 @@
"last 1 safari version"
]
}
}
}

View file

@ -1,13 +1,10 @@
<!DOCTYPE html>
<!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'"
/>
<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>

View file

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

View file

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

View file

@ -10,29 +10,36 @@ import StartQueueButton from './components/StartQueue';
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;
export default Layout;

View file

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

View file

@ -6,22 +6,24 @@ 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 />
Add to Queue
</Button>
</Box>;
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>
);
};
export default AddToQueue;
export default AddToQueue;

View file

@ -8,320 +8,398 @@ import { useSnackbar } from 'notistack';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
type DownloadSelectorProps = {
onFinish?: () => unknown
}
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;
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'
});
}
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 }
});
}} 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>
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>
</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>
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'}}/>
<LoadingButton sx={{ margin: '15px', textTransform: 'none' }} loading={loading} onClick={addToQueue} variant='contained'>Add to Queue</LoadingButton>
</Box>;
onFinish?: () => unknown;
};
export default DownloadSelector;
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;
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'
});
}
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 }
});
}}
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>
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>
</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>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' }} />
<LoadingButton sx={{ margin: '15px', textTransform: 'none' }} loading={loading} onClick={addToQueue} variant="contained">
Add to Queue
</LoadingButton>
</Box>
);
};
export default DownloadSelector;

View file

@ -5,187 +5,205 @@ import useStore from '../../../../hooks/useStore';
import ContextMenu from '../../../reusable/ContextMenu';
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' }}>
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 />
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>;
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 />
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>
);
};
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}`);
}
} 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);
}
});
return [...new Set(ret)];
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;
}
for (let i = a; i <= b; i++) {
ret.push(`${i}`);
}
}
} else {
ret.push(item);
}
});
return [...new Set(ret)];
};
export default EpisodeListing;

View file

@ -1,8 +1,8 @@
.listitem-hover:hover {
-webkit-filter: brightness(70%);
filter: brightness(70%);
-webkit-filter: brightness(70%);
filter: brightness(70%);
}
.listitem-hover {
transition: filter 0.1s ease-in;
}
transition: filter 0.1s ease-in;
}

View file

@ -8,112 +8,144 @@ 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 &&
<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={{ }}>
Languages: {a.lang.join(', ')}
</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 &&
<ContextMenu options={[
{
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>
</Paper>}
</Box>
</ClickAwayListener>;
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={{}}>
Languages: {a.lang.join(', ')}
</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 && (
<ContextMenu
options={[
{
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>
</Paper>
)}
</Box>
</ClickAwayListener>
);
};
export default SearchBox;

View file

@ -6,107 +6,115 @@ 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>
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>;
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>
);
};
export default AuthButton;
export default AuthButton;

View file

@ -6,32 +6,26 @@ 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');
};
return <Require value={messageChannel}>
<Button
startIcon={<ExitToApp />}
variant='contained'
onClick={logout}
sx={{ maxHeight: '2.3rem' }}
>
Service select
</Button>
</Require>;
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' }}>
Service select
</Button>
</Require>
);
};
export default LogoutButton;
export default LogoutButton;

View file

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

View file

@ -3,9 +3,11 @@ 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;
export default MainFrame;

View file

@ -7,414 +7,537 @@ 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'
}}>
<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',
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',
}}>
<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>;
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'
}}
>
<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',
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'
}}
>
<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>
);
};
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;
export default Queue;

View file

@ -5,120 +5,134 @@ 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')}>
Settings
</Button>
<Button onClick={(e) => handleClick(e, 'help')}>
Help
</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();
}}>
Open FFmpeg/Mkvmerge file
</MenuItem>
<MenuItem onClick={() => {
msg.openFile(['config', 'cli-defaults.yml']);
handleClose();
}}>
Open advanced options
</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();
}}>
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();
}}>
Report a bug
</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();
}}>
Discord
</MenuItem>
<MenuItem onClick={() => {
handleClose();
}}>
Version: {store.version}
</MenuItem>
</Menu>
<Typography variant="h5" color="text.primary">
{transformService(store.service)}
</Typography>
</Box>;
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')}>Help</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();
}}
>
Open FFmpeg/Mkvmerge file
</MenuItem>
<MenuItem
onClick={() => {
msg.openFile(['config', 'cli-defaults.yml']);
handleClose();
}}
>
Open advanced options
</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();
}}
>
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();
}}
>
Report a bug
</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();
}}
>
Discord
</MenuItem>
<MenuItem
onClick={() => {
handleClose();
}}
>
Version: {store.version}
</MenuItem>
</Menu>
<Typography variant="h5" color="text.primary">
{transformService(store.service)}
</Typography>
</Box>
);
};
export default MenuBar;

View file

@ -2,13 +2,17 @@ import React from 'react';
import { Box, Backdrop, CircularProgress } from '@mui/material';
export type RequireType<T> = {
value?: T
}
const Require = <T, >(props: React.PropsWithChildren<RequireType<T>>) => {
return props.value === undefined ? <Backdrop open>
<CircularProgress />
</Backdrop> : <Box>{props.children}</Box>;
value?: T;
};
export default Require;
const Require = <T,>(props: React.PropsWithChildren<RequireType<T>>) => {
return props.value === undefined ? (
<Backdrop open>
<CircularProgress />
</Backdrop>
) : (
<Box>{props.children}</Box>
);
};
export default Require;

View file

@ -5,38 +5,30 @@ 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);
};
return <Require value={messageChannel}>
<Button
startIcon={start ? <PauseCircleFilled /> : <PlayCircleFilled /> }
variant='contained'
onClick={change}
sx={{ maxHeight: '2.3rem' }}
>
{
start ? 'Stop Queue' : 'Start Queue'
}
</Button>
</Require>;
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>
);
};
export default StartQueueButton;
export default StartQueueButton;

View file

@ -2,64 +2,74 @@ import { Box, Button, Divider, List, SxProps } from '@mui/material';
import React from 'react';
export type Option = {
text: string,
onClick: () => unknown
}
export type ContextMenuProps<T extends HTMLElement> = {
options: ('divider'|Option)[],
popupItem: React.RefObject<T>
}
const buttonSx: SxProps = {
'&:hover': {
background: 'rgb(0, 30, 60)'
},
fontSize: '0.7rem',
minHeight: '30px',
justifyContent: 'center',
p: 0
text: string;
onClick: () => unknown;
};
function ContextMenu<T extends HTMLElement, >(props: ContextMenuProps<T>) {
const [anchor, setAnchor] = React.useState( { x: 0, y: 0 } );
export type ContextMenuProps<T extends HTMLElement> = {
options: ('divider' | Option)[];
popupItem: React.RefObject<T>;
};
const [show, setShow] = React.useState(false);
const buttonSx: SxProps = {
'&:hover': {
background: 'rgb(0, 30, 60)'
},
fontSize: '0.7rem',
minHeight: '30px',
justifyContent: 'center',
p: 0
};
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);
function ContextMenu<T extends HTMLElement>(props: ContextMenuProps<T>) {
const [anchor, setAnchor] = React.useState({ x: 0, y: 0 });
return () => {
if (ref.current)
ref.current.removeEventListener('contextmenu', listener);
};
}, [ props.popupItem ]);
const [show, setShow] = React.useState(false);
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> : <></>;
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 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>
) : (
<></>
);
}
export default ContextMenu;

View file

@ -7,18 +7,16 @@ 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;
export default LinearProgressWithLabel;

View file

@ -2,73 +2,64 @@ import React from 'react';
import { FormControl, InputLabel, MenuItem, OutlinedInput, Select, Theme, useTheme } from '@mui/material';
export type MultiSelectProps = {
values: string[],
selected: string[],
onChange: (values: string[]) => unknown,
title: string,
allOption?: boolean
}
values: string[];
selected: string[];
onChange: (values: string[]) => unknown;
title: string;
allOption?: boolean;
};
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:
(personName ?? []).indexOf(name) === -1
? theme.typography.fontWeightRegular
: theme.typography.fontWeightMedium
};
return {
fontWeight: (personName ?? []).indexOf(name) === -1 ? 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;
export default MultiSelect;

View file

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

View file

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

View file

@ -1,39 +1,44 @@
import { Box, Typography } from '@mui/material';
import React from 'react';
export default class ErrorHandler extends React.Component<{
children: React.ReactNode|React.ReactNode[]
}, {
error?: {
er: Error,
stack: React.ErrorInfo
}
}> {
constructor(props: {
children: React.ReactNode|React.ReactNode[]
}) {
super(props);
this.state = { error: undefined };
}
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;
}
}
export default class ErrorHandler extends React.Component<
{
children: React.ReactNode | React.ReactNode[];
},
{
error?: {
er: Error;
stack: React.ErrorInfo;
};
}
> {
constructor(props: { children: React.ReactNode | React.ReactNode[] }) {
super(props);
this.state = { error: undefined };
}
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
);
}
}

View file

@ -9,236 +9,236 @@ import { useSnackbar } from 'notistack';
import { LockOutlined, PowerSettingsNew } from '@mui/icons-material';
import { GUIConfig } from '../../../../modules/module.cfg-loader';
export type FrontEndMessages = (MessageHandler & { randomEvents: RandomEventHandler, logout: () => Promise<boolean> });
export type FrontEndMessages = MessageHandler & { randomEvents: RandomEventHandler; logout: () => Promise<boolean> };
export class RandomEventHandler {
private handler: {
[eventName in keyof RandomEvents]: Handler<eventName>[]
} = {
progress: [],
finish: [],
queueChange: [],
current: []
};
private handler: {
[eventName in keyof RandomEvents]: Handler<eventName>[];
} = {
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);
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(() => {
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]);
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'
});
search = new URLSearchParams({
password
});
}
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 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'
});
});
};
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'
});
});
};
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 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 randomEventHandler = React.useMemo(() => new RandomEventHandler(), []);
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(() => {
(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]);
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 (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">
You need to login in order to use this tool.
</Typography>
</Box>
</Box>
);
}
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'>
You need to login in order to use this tool.
</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">
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">
Please enter data that will be set to use this tool.
<br />
Leave blank to use no password (NOT RECOMMENDED)!
</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">
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'>
Please enter data that will be set to use this tool.
<br />
Leave blank to use no password (NOT RECOMMENDED)!
</Typography>
</Box>
</Box>;
}
const messageHandler: FrontEndMessages = {
name: 'default',
auth: async (data) => (await messageAndResponse(socket, { name: 'auth', data })).data,
version: async () => (await messageAndResponse(socket, { name: 'version', data: undefined })).data,
checkToken: async () => (await messageAndResponse(socket, { name: 'checkToken', data: undefined })).data,
search: async (data) => (await messageAndResponse(socket, { name: 'search', data })).data,
handleDefault: async (data) => (await messageAndResponse(socket, { name: 'default', data })).data,
availableDubCodes: async () => (await messageAndResponse(socket, { name: 'availableDubCodes', data: undefined })).data,
availableSubCodes: async () => (await messageAndResponse(socket, { name: 'availableSubCodes', data: undefined })).data,
resolveItems: async (data) => (await messageAndResponse(socket, { name: 'resolveItems', data })).data,
listEpisodes: async (data) => (await messageAndResponse(socket, { name: 'listEpisodes', data })).data,
randomEvents: randomEventHandler,
downloadItem: (data) => messageAndResponse(socket, { name: 'downloadItem', data }),
isDownloading: async () => (await messageAndResponse(socket, { name: 'isDownloading', data: undefined })).data,
openFolder: async (data) => messageAndResponse(socket, { name: 'openFolder', data }),
logout: async () => (await messageAndResponse(socket, { name: 'changeProvider', data: undefined })).data,
openFile: async (data) => await messageAndResponse(socket, { name: 'openFile', data }),
openURL: async (data) => await messageAndResponse(socket, { name: 'openURL', data }),
getQueue: async () => (await messageAndResponse(socket, { name: 'getQueue', data: undefined })).data,
removeFromQueue: async (data) => await messageAndResponse(socket, { name: 'removeFromQueue', data }),
clearQueue: async () => await messageAndResponse(socket, { name: 'clearQueue', data: undefined }),
setDownloadQueue: async (data) => await messageAndResponse(socket, { name: 'setDownloadQueue', data }),
getDownloadQueue: async () => (await messageAndResponse(socket, { name: 'getDownloadQueue', data: undefined })).data
};
const messageHandler: FrontEndMessages = {
name: 'default',
auth: async (data) => (await messageAndResponse(socket, { name: 'auth', data })).data,
version: async () => (await messageAndResponse(socket, { name: 'version', data: undefined })).data,
checkToken: async () => (await messageAndResponse(socket, { name: 'checkToken', data: undefined })).data,
search: async (data) => (await messageAndResponse(socket, { name: 'search', data })).data,
handleDefault: async (data) => (await messageAndResponse(socket, { name: 'default', data })).data,
availableDubCodes: async () => (await messageAndResponse(socket, { name: 'availableDubCodes', data: undefined})).data,
availableSubCodes: async () => (await messageAndResponse(socket, { name: 'availableSubCodes', data: undefined })).data,
resolveItems: async (data) => (await messageAndResponse(socket, { name: 'resolveItems', data })).data,
listEpisodes: async (data) => (await messageAndResponse(socket, { name: 'listEpisodes', data })).data,
randomEvents: randomEventHandler,
downloadItem: (data) => messageAndResponse(socket, { name: 'downloadItem', data }),
isDownloading: async () => (await messageAndResponse(socket, { name: 'isDownloading', data: undefined })).data,
openFolder: async (data) => messageAndResponse(socket, { name: 'openFolder', data }),
logout: async () => (await messageAndResponse(socket, { name: 'changeProvider', data: undefined })).data,
openFile: async (data) => await messageAndResponse(socket, { name: 'openFile', data }),
openURL: async (data) => await messageAndResponse(socket, { name: 'openURL', data }),
getQueue: async () => (await messageAndResponse(socket, { name: 'getQueue', data: undefined })).data,
removeFromQueue: async (data) => await messageAndResponse(socket, { name: 'removeFromQueue', data }),
clearQueue: async () => await messageAndResponse(socket, { name: 'clearQueue', data: undefined }),
setDownloadQueue: async (data) => await messageAndResponse(socket, { name: 'setDownloadQueue', data }),
getDownloadQueue: async () => (await messageAndResponse(socket, { name: 'getDownloadQueue', data: undefined })).data,
};
return <messageChannelContext.Provider value={messageHandler}>
{children}
</messageChannelContext.Provider>;
return <messageChannelContext.Provider value={messageHandler}>{children}</messageChannelContext.Provider>;
};
export default MessageChannelProvider;

View file

@ -6,30 +6,28 @@ 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 ]);
return <queueContext.Provider value={queue}>
{children}
</queueContext.Provider>;
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>;
};
export default QueueProvider;
export default QueueProvider;

View file

@ -1,35 +1,60 @@
import React from 'react';
import {Divider, Box, Button, Typography, Avatar} from '@mui/material';
import { Divider, Box, Button, Typography, Avatar } from '@mui/material';
import useStore from '../hooks/useStore';
import { StoreState } from './Store';
type Services = 'crunchy'|'hidive'|'ao'|'adn';
type Services = 'crunchy' | 'hidive' | 'ao' | 'adn';
export const serviceContext = React.createContext<Services|undefined>(undefined);
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;
export default ServiceProvider;

View file

@ -3,64 +3,64 @@ import { Episode } from '../../../../@types/messageHandler';
import { dubLanguageCodes } from '../../../../modules/module.langsData';
export type DownloadOptions = {
q: number,
id: string,
e: string,
dubLang: typeof dubLanguageCodes,
dlsubs: string[],
fileName: string,
dlVideoOnce: boolean,
all: boolean,
but: boolean,
novids: boolean,
hslang?: string,
simul: boolean,
noaudio: boolean
}
q: number;
id: string;
e: string;
dubLang: typeof dubLanguageCodes;
dlsubs: string[];
fileName: string;
dlVideoOnce: boolean;
all: boolean;
but: boolean;
novids: boolean;
hslang?: string;
simul: boolean;
noaudio: boolean;
};
export type StoreState = {
episodeListing: Episode[];
downloadOptions: DownloadOptions,
service: 'crunchy'|'hidive'|'ao'|'adn'|undefined,
version: string,
}
episodeListing: Episode[];
downloadOptions: DownloadOptions;
service: 'crunchy' | 'hidive' | 'ao' | 'adn' | undefined;
version: string;
};
export type StoreAction<T extends (keyof StoreState)> = {
type: T,
payload: StoreState[T],
extraInfo?: Record<string, unknown>
}
export type StoreAction<T extends keyof StoreState> = {
type: T;
payload: StoreState[T];
extraInfo?: Record<string, unknown>;
};
const Reducer = <T extends keyof StoreState,>(state: StoreState, action: StoreAction<T>): StoreState => {
switch(action.type) {
default:
return { ...state, [action.type]: action.payload };
}
const Reducer = <T extends keyof StoreState>(state: StoreState, action: StoreAction<T>): StoreState => {
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 Store: FCWithChildren = ({ children }) => {
const [state, dispatch] = React.useReducer(Reducer, initialState);
/*React.useEffect(() => {
if (!state.unsavedChanges.has)
return;
const unsavedChanges = (ev: BeforeUnloadEvent, lang: LanguageContextType) => {
@ -79,13 +79,9 @@ 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 */
export const StoreContext = React.createContext<[StoreState, React.Dispatch<StoreAction<'downloadOptions'>>]>([initialState, undefined as any]);
export default Store;
export default Store;

View file

@ -2,11 +2,7 @@
"compilerOptions": {
"outDir": "./build",
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
@ -22,8 +18,5 @@
"jsx": "react-jsx",
"downlevelIteration": true
},
"include": [
"./src",
"./webpack.config.ts"
]
}
"include": ["./src", "./webpack.config.ts"]
}

View file

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

View file

@ -25,10 +25,10 @@ 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);
new ServiceHandler(server);
open(`http://localhost:${cfg.gui.port}`);
open(`http://localhost:${cfg.gui.port}`);

View file

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

View file

@ -8,132 +8,144 @@ 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> {
//TODO: implement proper method to check token
return { isOk: true, value: undefined };
}
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: search.value };
}
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: search.value };
}
public async handleDefault(name: string) {
return getDefault(name, this.adn.cfg.cli);
}
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 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 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 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') };
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')};
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+''
};
})};
}
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();
}
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();
}
}
export default ADNHandler;
export default ADNHandler;

View file

@ -1,4 +1,14 @@
import { AuthData, CheckTokenResponse, DownloadData, Episode, EpisodeListResponse, MessageHandler, ResolveItemsData, SearchData, SearchResponse } from '../../../@types/messageHandler';
import {
AuthData,
CheckTokenResponse,
DownloadData,
Episode,
EpisodeListResponse,
MessageHandler,
ResolveItemsData,
SearchData,
SearchResponse
} from '../../../@types/messageHandler';
import AnimeOnegai from '../../../ao';
import { getDefault } from '../../../modules/module.args';
import { languages } from '../../../modules/module.langsData';
@ -8,144 +18,153 @@ 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> {
//TODO: implement proper method to check token
return { isOk: true, value: undefined };
}
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: search.value };
}
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: search.value };
}
public async handleDefault(name: string) {
return getDefault(name, this.ao.cfg.cli);
}
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 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 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 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') };
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 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 };
}
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();
}
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();
}
}
export default AnimeOnegaiHandler;
export default AnimeOnegaiHandler;

View file

@ -9,140 +9,140 @@ import { getState, setState } from '../../../modules/module.cfg-loader';
import packageJson from '../../../package.json';
export default class Base {
private state: GuiState;
public name = 'default';
constructor(private ws: WebSocketHandler) {
this.state = getState();
}
private state: GuiState;
public name = 'default';
constructor(private ws: WebSocketHandler) {
this.state = getState();
}
private downloading = false;
private downloading = false;
private queue: QueueItem[] = [];
private workOnQueue = false;
private queue: QueueItem[] = [];
private workOnQueue = false;
version(): Promise<string> {
return new Promise(() => {
return packageJson.version;
});
}
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': []
};
}
}
initState() {
if (this.state.services[this.name]) {
this.queue = this.state.services[this.name].queue;
this.queueChange();
} else {
this.state.services[this.name] = {
queue: []
};
}
}
setDownloading(downloading: boolean) {
this.downloading = downloading;
}
setDownloading(downloading: boolean) {
this.downloading = downloading;
}
getDownloading() {
return this.downloading;
}
getDownloading() {
return this.downloading;
}
alertError(error: Error) {
console.error(`${error}`);
}
alertError(error: Error) {
console.error(`${error}`);
}
makeProgressHandler(videoInfo: DownloadInfo) {
return ((data: ProgressData) => {
this.sendMessage({
name: 'progress',
data: {
downloadInfo: videoInfo,
progress: data
}
});
});
}
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);
}
sendMessage<T extends keyof RandomEvents>(data: RandomEvent<T>) {
this.ws.sendMessage(data);
}
async isDownloading() {
return this.downloading;
}
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 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 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);
}
async openURL(data: string) {
open(data);
}
public async getQueue(): Promise<QueueItem[]> {
return this.queue;
}
public async getQueue(): Promise<QueueItem[]> {
return this.queue;
}
public async removeFromQueue(index: number) {
this.queue.splice(index, 1);
this.queueChange();
}
public async removeFromQueue(index: number) {
this.queue.splice(index, 1);
this.queueChange();
}
public async clearQueue() {
this.queue = [];
this.queueChange();
}
public async clearQueue() {
this.queue = [];
this.queueChange();
}
public addToQueue(data: QueueItem[]) {
this.queue = this.queue.concat(...data);
this.queueChange();
}
public addToQueue(data: QueueItem[]) {
this.queue = this.queue.concat(...data);
this.queueChange();
}
public setDownloadQueue(data: boolean) {
this.workOnQueue = data;
this.queueChange();
}
public setDownloadQueue(data: boolean) {
this.workOnQueue = data;
this.queueChange();
}
public async getDownloadQueue(): Promise<boolean> {
return this.workOnQueue;
}
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);
}
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();
}
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');
}
}
//Overriten
// eslint-disable-next-line
public async downloadItem(_: QueueItem) {
throw new Error('downloadItem not overriden');
}
}

View file

@ -8,120 +8,133 @@ 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 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 getDefaults() {
const _default = yargs.appArgv(this.crunchy.cfg.cli, true);
this.crunchy.locale = _default.locale;
}
public async availableDubCodes(): Promise<string[]> {
const dubLanguageCodesArray: string[] = [];
for(const language of languages){
if (language.cr_locale)
dubLanguageCodesArray.push(language.code);
}
return [...new Set(dubLanguageCodesArray)];
}
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 availableSubCodes(): Promise<string[]> {
return subtitleLanguagesFilter;
}
public async handleDefault(name: string) {
return getDefault(name, this.crunchy.cfg.cli);
}
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 availableDubCodes(): Promise<string[]> {
const dubLanguageCodesArray: string[] = [];
for (const language of languages) {
if (language.cr_locale) dubLanguageCodesArray.push(language.code);
}
return [...new Set(dubLanguageCodesArray)];
}
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 availableSubCodes(): Promise<string[]> {
return subtitleLanguagesFilter;
}
public async checkToken(): Promise<CheckTokenResponse> {
if (await this.crunchy.getProfile()) {
return { isOk: true, value: undefined };
} else {
return { isOk: false, reason: new Error('') };
}
}
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,
public auth(data: AuthData) {
return this.crunchy.doAuth(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 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();
}
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();
}
}
export default CrunchyHandler;
export default CrunchyHandler;

View file

@ -8,120 +8,128 @@ 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> {
//TODO: implement proper method to check token
return { isOk: true, value: undefined };
}
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: hidiveSearch.value };
}
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 };
}
public async handleDefault(name: string) {
return getDefault(name, this.hidive.cfg.cli);
}
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);
}
return [...new Set(dubLanguageCodesArray)];
}
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 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 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') };
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')};
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: ''
};
})};
}
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'));
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();
}
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();
}
}
export default HidiveHandler;
export default HidiveHandler;

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