Compare commits
292 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64c927c761 | ||
|
|
16e7fcb71e | ||
|
|
d61d7e471f | ||
|
|
64272b7689 | ||
|
|
35777ecb58 | ||
|
|
423ad7b2c9 | ||
|
|
5cbead06bc | ||
|
|
1704bfb4ec | ||
|
|
b488834c0f | ||
|
|
8d59666a6c | ||
|
|
c14a963024 | ||
|
|
0026de73bf | ||
|
|
d3238d22ba | ||
|
|
ab090a6858 | ||
|
|
64783a0529 | ||
|
|
05d679e6ca | ||
|
|
cd9586ab13 | ||
|
|
0a7cfcd917 | ||
|
|
ff978e2c88 | ||
|
|
d81ca76594 | ||
|
|
9457ee6d26 | ||
|
|
9a94c33c8b | ||
|
|
e88352af3f | ||
|
|
870b775175 | ||
|
|
78f5016dd3 | ||
|
|
eaec9e62a7 | ||
|
|
f92e8dfacb | ||
|
|
a6d740e9e9 | ||
|
|
436a4ca4d1 | ||
|
|
ea7df30aa7 | ||
|
|
240ff3870b | ||
|
|
1340624742 | ||
|
|
b0b7cffddf | ||
|
|
fe3f978082 | ||
|
|
0045abe08c | ||
|
|
ef975868a3 | ||
|
|
345aa0f267 | ||
|
|
1f8ddb27a1 | ||
|
|
575ea260b6 | ||
|
|
9fdc1ac4db | ||
|
|
cf921295f8 | ||
|
|
ed22970346 | ||
|
|
5f034dc348 | ||
|
|
c294cdc280 | ||
|
|
16dbc4f1eb | ||
|
|
fba1b1cf22 | ||
|
|
141fdcf552 | ||
|
|
3aa844f90b | ||
|
|
be95c1f3bc | ||
|
|
6275d5abe3 | ||
|
|
85c5d45829 | ||
|
|
9feb3d2f13 | ||
|
|
8b5cafff3d | ||
|
|
9ea6258fec | ||
|
|
fc0736c686 | ||
|
|
dbc2c7d52b | ||
|
|
38f849f1a8 | ||
|
|
33afc263e7 | ||
|
|
ab73931fb9 | ||
|
|
87c7de7417 | ||
|
|
f1042ded9f | ||
|
|
8dd0725f9a | ||
|
|
5730450e11 | ||
|
|
8da4074b1b | ||
|
|
130fa5ee11 | ||
|
|
b2488edc02 | ||
|
|
773bbf034c | ||
|
|
d507135eaa | ||
|
|
24191c91d0 | ||
|
|
2fe20c35f8 | ||
|
|
afefbbf9a5 | ||
|
|
b2a602e96e | ||
|
|
e47deb7c57 | ||
|
|
5b8c497800 | ||
|
|
fb58d306bc | ||
|
|
282fc1ac1a | ||
|
|
c74e6fcb18 | ||
|
|
39fec7c35c | ||
|
|
591fac8b08 | ||
|
|
f1982d1cd5 | ||
|
|
ba08c7b4e7 | ||
|
|
78630bb5f7 | ||
|
|
e3a64abfd8 | ||
|
|
0403ee0690 | ||
|
|
bc1f69b3b1 | ||
|
|
78a16410cc | ||
|
|
8103bc09d2 | ||
|
|
4d134a613d | ||
|
|
0927e8287c | ||
|
|
52aaef0515 | ||
|
|
e5c62a9ed4 | ||
|
|
e7dfc28513 | ||
|
|
4b8e683bba | ||
|
|
3957977a2c | ||
|
|
56be50c43a | ||
|
|
de0b33c7c6 | ||
|
|
6093c202d2 | ||
|
|
da2a8ffbff | ||
|
|
1fa4d5bbbb | ||
|
|
3486ec67a7 | ||
|
|
0883776580 | ||
|
|
6629a2ac87 | ||
|
|
3d3a94c991 | ||
|
|
6c71f0b808 | ||
|
|
2efc3683b2 | ||
|
|
b453d1927a | ||
|
|
6aa37c3426 | ||
|
|
b0790145cc | ||
|
|
a472ab5dd3 | ||
|
|
533818b812 | ||
|
|
257c3b6266 | ||
|
|
e61c1717ae | ||
|
|
82146f2c51 | ||
|
|
ac06400d2a | ||
|
|
a1feba6e6f | ||
|
|
bc79628f4f | ||
|
|
f39645166e | ||
|
|
d260d8f3e6 | ||
|
|
cd0476b700 | ||
|
|
01abffa85f | ||
|
|
71ae48000b | ||
|
|
b42a543ed5 | ||
|
|
3952ee4376 | ||
|
|
7107cef7a4 | ||
|
|
c06b990bf5 | ||
|
|
a29807895c | ||
|
|
f5f32fa701 | ||
|
|
e9c040ceb7 | ||
|
|
d2117a1390 | ||
|
|
67cdc42d64 | ||
|
|
4b5b3919f5 | ||
|
|
79fc6584d7 | ||
|
|
44381a04be | ||
|
|
9972a48366 | ||
|
|
4c4436814b | ||
|
|
b1bae92308 | ||
|
|
84ebabffc8 | ||
|
|
6b913d6e85 | ||
|
|
5226b963ee | ||
|
|
3567edae4b | ||
|
|
d6db234ad1 | ||
|
|
cee5207ac1 | ||
|
|
00a4f7b9ee | ||
|
|
93ace68398 | ||
|
|
66b219ea0a | ||
|
|
0d065fdd6a | ||
|
|
68e4a344d8 | ||
|
|
a022855400 | ||
|
|
469fd1b4a4 | ||
|
|
413dea6564 | ||
|
|
551b27280e | ||
|
|
6e4e10930b | ||
|
|
16d2277d3e | ||
|
|
0a3b638c55 | ||
|
|
e9e14aef2f | ||
|
|
23f185c877 | ||
|
|
036440b0e5 | ||
|
|
ea51c8a7ea | ||
|
|
645cdc2068 | ||
|
|
dce10bc7fc | ||
|
|
5846a13c10 | ||
|
|
2b65f3067e | ||
|
|
7d5e8fa461 | ||
|
|
42da4ebd30 | ||
|
|
d83b2b55c8 | ||
|
|
1e6406774d | ||
|
|
adc1147ca4 | ||
|
|
866bd26067 | ||
|
|
2885913205 | ||
|
|
01888286da | ||
|
|
3ca4000ed1 | ||
|
|
4c5bdb4226 | ||
|
|
d5272e7108 | ||
|
|
90100a077d | ||
|
|
6bf7e9721f | ||
|
|
80c7f5ba77 | ||
|
|
9aefa9c598 | ||
|
|
652e0feeca | ||
|
|
a852cb37c5 | ||
|
|
0f3cf5fbb8 | ||
|
|
c990e744d3 | ||
|
|
dc87b23c0d | ||
|
|
b54d54d374 | ||
|
|
25cf19f65d | ||
|
|
c82197a585 | ||
|
|
8ccb1301d0 | ||
|
|
a777b26517 | ||
|
|
cd7db09804 | ||
|
|
d298521202 | ||
|
|
4726bd0416 | ||
|
|
f1bb6c8a64 | ||
|
|
3c8f9f2d46 | ||
|
|
175ffbc71f | ||
|
|
b96fca8ed8 | ||
|
|
eb2e9c2425 | ||
|
|
ed02017bca | ||
|
|
69336c5c6e | ||
|
|
74da730dc4 | ||
|
|
250c8925a4 | ||
|
|
978159c80e | ||
|
|
d2a69fdc4d | ||
|
|
aae69fa2a1 | ||
|
|
421cd5fcee | ||
|
|
e98c770dab | ||
|
|
6009bdaaf9 | ||
|
|
0e674fcd67 | ||
|
|
34c8215bd0 | ||
|
|
f7f8806b14 | ||
|
|
3318fbba23 | ||
|
|
48224c7b53 | ||
|
|
edd80a1569 | ||
|
|
8cd863c885 | ||
|
|
cd9a83d3fc | ||
|
|
4eea557c44 | ||
|
|
30771374e5 | ||
|
|
201a772ecf | ||
|
|
818d60e196 | ||
|
|
6a4566be17 | ||
|
|
73ddab8b2e | ||
|
|
886a0bd85b | ||
|
|
15f2651b20 | ||
|
|
19f0a3cc7d | ||
|
|
fbc1e43260 | ||
|
|
8dea7cc400 | ||
|
|
59d086006b | ||
|
|
97b9778801 | ||
|
|
0b22b4ec36 | ||
|
|
1852ce1282 | ||
|
|
97d64f6021 | ||
|
|
a5df6bee2f | ||
|
|
83d410378a | ||
|
|
4693b60af4 | ||
|
|
b830a73e04 | ||
|
|
0c7c047a7b | ||
|
|
510847d3d5 | ||
|
|
789ed2c5b0 | ||
|
|
b497fb40df | ||
|
|
6280d13d36 | ||
|
|
63f0b496f1 | ||
|
|
3e071b1386 | ||
|
|
ef567903ad | ||
|
|
9e9fb6eb76 | ||
|
|
1e09625b85 | ||
|
|
50c6ca66a3 | ||
|
|
caab478956 | ||
|
|
e5bbc09f25 | ||
|
|
9590aa56d1 | ||
|
|
1500daf207 | ||
|
|
f8307620ce | ||
|
|
48c9528116 | ||
|
|
546a5327e8 | ||
|
|
becaed79d1 | ||
|
|
49519f1e17 | ||
|
|
7d62224d11 | ||
|
|
9645f6255b | ||
|
|
5b134978c7 | ||
|
|
a61d7d315d | ||
|
|
12780eec26 | ||
|
|
c8b39301cb | ||
|
|
48d8a276d7 | ||
|
|
34fa052bc2 | ||
|
|
c7addc1c1a | ||
|
|
824154594d | ||
|
|
211a593796 | ||
|
|
f3fd33d241 | ||
|
|
1c39e349ad | ||
|
|
5e95f600d9 | ||
|
|
19521be3ff | ||
|
|
3c41414f4a | ||
|
|
f06dc21d56 | ||
|
|
172e6ac6ac | ||
|
|
d5c72c5c86 | ||
|
|
0a8c29189e | ||
|
|
31096899e1 | ||
|
|
5143f2db4a | ||
|
|
9bc62ba91e | ||
|
|
1e2b396ed5 | ||
|
|
2e77593e14 | ||
|
|
4d9b1c7480 | ||
|
|
1e0e414ae6 | ||
|
|
b491ba1917 | ||
|
|
d07603de8b | ||
|
|
1996e44ef5 | ||
|
|
1c6e4640c5 | ||
|
|
a1a9729134 | ||
|
|
70f8e36ffc | ||
|
|
7998395c56 | ||
|
|
60da30c92b | ||
|
|
fb0559abd3 | ||
|
|
3174a4db5e | ||
|
|
63e1d1dda8 | ||
|
|
788afbc6f5 |
91 changed files with 10234 additions and 11693 deletions
|
|
@ -1,5 +0,0 @@
|
|||
lib
|
||||
/videos/*.ts
|
||||
build
|
||||
dev.js
|
||||
tsc.ts
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"react",
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["gui/react/**/*"],
|
||||
"rules": {
|
||||
"no-console": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"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",
|
||||
"indent": [
|
||||
"error",
|
||||
2
|
||||
],
|
||||
"linebreak-style": [
|
||||
"warn",
|
||||
"windows"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
]
|
||||
}
|
||||
}
|
||||
3
.github/ISSUE_TEMPLATE/bug.yml
vendored
3
.github/ISSUE_TEMPLATE/bug.yml
vendored
|
|
@ -47,9 +47,10 @@ body:
|
|||
label: Service
|
||||
description: "Please tell us what service the bug occured in."
|
||||
options:
|
||||
- Funimation
|
||||
- Crunchyroll
|
||||
- Hidive
|
||||
- AnimationDigitalNetwork
|
||||
- AnimeOnegai
|
||||
- All
|
||||
- Irrelevant
|
||||
validations:
|
||||
|
|
|
|||
4
.github/workflows/auto-documentation.yml
vendored
4
.github/workflows/auto-documentation.yml
vendored
|
|
@ -3,8 +3,6 @@ name: auto-documentation
|
|||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
documentation:
|
||||
|
|
@ -20,7 +18,7 @@ jobs:
|
|||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
- run: pnpm i
|
||||
- run: pnpm run docs
|
||||
- uses: stefanzweifel/git-auto-commit-action@v4
|
||||
|
|
|
|||
6
.github/workflows/docker.yml
vendored
6
.github/workflows/docker.yml
vendored
|
|
@ -5,8 +5,6 @@ name: build and push docker image
|
|||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build-node:
|
||||
|
|
@ -29,6 +27,6 @@ jobs:
|
|||
github-token: ${{ github.token }}
|
||||
push: ${{ github.ref == 'refs/heads/master' }}
|
||||
tags: |
|
||||
"izuco/multi-downloader-nx:latest"
|
||||
"multidl/multi-downloader-nx:latest"
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
|
|
|
|||
6
.github/workflows/release-matrix.yml
vendored
6
.github/workflows/release-matrix.yml
vendored
|
|
@ -2,7 +2,7 @@ name: Release Builds
|
|||
|
||||
on:
|
||||
release:
|
||||
types: [ created ]
|
||||
types: [ published ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
@ -21,7 +21,7 @@ jobs:
|
|||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
- name: Install Node modules
|
||||
run: |
|
||||
|
|
@ -61,6 +61,6 @@ jobs:
|
|||
github-token: ${{ github.token }}
|
||||
push: true
|
||||
tags: |
|
||||
"izuco/multi-downloader-nx:${{ github.event.release.tag_name }}"
|
||||
"multidl/multi-downloader-nx:${{ github.event.release.tag_name }}"
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
|
|
|
|||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
|
|
@ -17,10 +17,10 @@ jobs:
|
|||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
- run: pnpm i
|
||||
- run: pnpx eslint .
|
||||
- run: npx eslint .
|
||||
test:
|
||||
needs: eslint
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -32,7 +32,7 @@ jobs:
|
|||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
- run: pnpm i
|
||||
- run: pnpm run test
|
||||
|
|
|
|||
16
.gitignore
vendored
16
.gitignore
vendored
|
|
@ -4,6 +4,8 @@
|
|||
**/node_modules/
|
||||
/videos/*.json
|
||||
/videos/*.ts
|
||||
/videos/*.m4s
|
||||
/videos/*.txt
|
||||
.DS_Store
|
||||
ffmpeg
|
||||
mkvmerge
|
||||
|
|
@ -19,11 +21,9 @@ token.yml
|
|||
lib
|
||||
test.*
|
||||
updates.json
|
||||
funi_token.yml
|
||||
cr_token.yml
|
||||
hd_profile.yml
|
||||
hd_sess.yml
|
||||
hd_token.yml
|
||||
*_token.yml
|
||||
*_profile.yml
|
||||
*_sess.yml
|
||||
archive.json
|
||||
guistate.json
|
||||
fonts
|
||||
|
|
@ -34,10 +34,12 @@ gui/react/build/
|
|||
docker-compose.yml
|
||||
crunchyendpoints
|
||||
.vscode
|
||||
.idea
|
||||
/logs
|
||||
/tmp/*/
|
||||
/videos/*/
|
||||
!videos/.gitkeep
|
||||
/videos/*
|
||||
/tmp/*.*
|
||||
bin
|
||||
widevine/*
|
||||
!widevine/.gitkeep
|
||||
!widevine/.gitkeep
|
||||
50
@types/adnPlayerConfig.d.ts
vendored
Normal file
50
@types/adnPlayerConfig.d.ts
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
export interface ADNPlayerConfig {
|
||||
player: Player;
|
||||
}
|
||||
|
||||
export interface Player {
|
||||
image: string;
|
||||
options: Options;
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
user: User;
|
||||
chromecast: Chromecast;
|
||||
ios: Ios;
|
||||
video: Video;
|
||||
dock: any[];
|
||||
preference: Preference;
|
||||
}
|
||||
|
||||
export interface Chromecast {
|
||||
appId: string;
|
||||
refreshTokenUrl: string;
|
||||
}
|
||||
|
||||
export interface Ios {
|
||||
videoUrl: string;
|
||||
appUrl: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface Preference {
|
||||
quality: string;
|
||||
autoplay: boolean;
|
||||
language: string;
|
||||
green: boolean;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
hasAccess: boolean;
|
||||
profileId: number;
|
||||
refreshToken: string;
|
||||
refreshTokenUrl: string;
|
||||
}
|
||||
|
||||
export interface Video {
|
||||
startDate: null;
|
||||
currentDate: Date;
|
||||
available: boolean;
|
||||
free: boolean;
|
||||
url: string;
|
||||
}
|
||||
46
@types/adnSearch.d.ts
vendored
Normal file
46
@types/adnSearch.d.ts
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
export interface ADNSearch {
|
||||
shows: ADNSearchShow[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ADNSearchShow {
|
||||
id: number;
|
||||
title: string;
|
||||
type: string;
|
||||
originalTitle: string;
|
||||
shortTitle: string;
|
||||
reference: string;
|
||||
age: string;
|
||||
languages: string[];
|
||||
summary: string;
|
||||
image: string;
|
||||
image2x: string;
|
||||
imageHorizontal: string;
|
||||
imageHorizontal2x: string;
|
||||
url: string;
|
||||
urlPath: string;
|
||||
episodeCount: number;
|
||||
genres: string[];
|
||||
copyright: string;
|
||||
rating: number;
|
||||
ratingsCount: number;
|
||||
commentsCount: number;
|
||||
qualities: string[];
|
||||
simulcast: boolean;
|
||||
free: boolean;
|
||||
available: boolean;
|
||||
download: boolean;
|
||||
basedOn: string;
|
||||
tagline: null;
|
||||
firstReleaseYear: string;
|
||||
productionStudio: string;
|
||||
countryOfOrigin: string;
|
||||
productionTeam: ProductionTeam[];
|
||||
nextVideoReleaseDate: null;
|
||||
indexable: boolean;
|
||||
}
|
||||
|
||||
export interface ProductionTeam {
|
||||
role: string;
|
||||
name: string;
|
||||
}
|
||||
51
@types/adnStreams.d.ts
vendored
Normal file
51
@types/adnStreams.d.ts
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
export interface ADNStreams {
|
||||
links: Links;
|
||||
video: Video;
|
||||
metadata: Metadata;
|
||||
}
|
||||
|
||||
export interface Links {
|
||||
streaming: Streaming;
|
||||
subtitles: Subtitles;
|
||||
history: string;
|
||||
nextVideoUrl: string;
|
||||
previousVideoUrl: string;
|
||||
}
|
||||
|
||||
export interface Streaming {
|
||||
[streams: string]: Streams;
|
||||
}
|
||||
|
||||
export interface Streams {
|
||||
mobile: string;
|
||||
sd: string;
|
||||
hd: string;
|
||||
fhd: string;
|
||||
auto: string;
|
||||
}
|
||||
|
||||
export interface Subtitles {
|
||||
all: string;
|
||||
}
|
||||
|
||||
export interface Metadata {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
summary: null;
|
||||
rating: number;
|
||||
}
|
||||
|
||||
export interface Video {
|
||||
guid: string;
|
||||
id: number;
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
url: string;
|
||||
image: string;
|
||||
tcEpisodeStart?:string;
|
||||
tcEpisodeEnd?: string;
|
||||
tcIntroStart?: string;
|
||||
tcIntroEnd?: string;
|
||||
tcEndingStart?: string;
|
||||
tcEndingEnd?: string;
|
||||
}
|
||||
11
@types/adnSubtitles.d.ts
vendored
Normal file
11
@types/adnSubtitles.d.ts
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export interface ADNSubtitles {
|
||||
[subtitleLang: string]: Subtitle[];
|
||||
}
|
||||
|
||||
export interface Subtitle {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
positionAlign: string;
|
||||
lineAlign: string;
|
||||
text: string;
|
||||
}
|
||||
77
@types/adnVideos.d.ts
vendored
Normal file
77
@types/adnVideos.d.ts
vendored
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
export interface ADNVideos {
|
||||
videos: ADNVideo[];
|
||||
}
|
||||
|
||||
export interface ADNVideo {
|
||||
id: number;
|
||||
title: string;
|
||||
name: string;
|
||||
number: string;
|
||||
shortNumber: string;
|
||||
season: string;
|
||||
reference: string;
|
||||
type: string;
|
||||
order: number;
|
||||
image: string;
|
||||
image2x: string;
|
||||
summary: string;
|
||||
releaseDate: Date;
|
||||
duration: number;
|
||||
url: string;
|
||||
urlPath: string;
|
||||
embeddedUrl: string;
|
||||
languages: string[];
|
||||
qualities: string[];
|
||||
rating: number;
|
||||
ratingsCount: number;
|
||||
commentsCount: number;
|
||||
available: boolean;
|
||||
download: boolean;
|
||||
free: boolean;
|
||||
freeWithAds: boolean;
|
||||
show: Show;
|
||||
indexable: boolean;
|
||||
isSelected?: boolean;
|
||||
}
|
||||
|
||||
export interface Show {
|
||||
id: number;
|
||||
title: string;
|
||||
type: string;
|
||||
originalTitle: string;
|
||||
shortTitle: string;
|
||||
reference: string;
|
||||
age: string;
|
||||
languages: string[];
|
||||
summary: string;
|
||||
image: string;
|
||||
image2x: string;
|
||||
imageHorizontal: string;
|
||||
imageHorizontal2x: string;
|
||||
url: string;
|
||||
urlPath: string;
|
||||
episodeCount: number;
|
||||
genres: string[];
|
||||
copyright: string;
|
||||
rating: number;
|
||||
ratingsCount: number;
|
||||
commentsCount: number;
|
||||
qualities: string[];
|
||||
simulcast: boolean;
|
||||
free: boolean;
|
||||
available: boolean;
|
||||
download: boolean;
|
||||
basedOn: string;
|
||||
tagline: string;
|
||||
firstReleaseYear: string;
|
||||
productionStudio: string;
|
||||
countryOfOrigin: string;
|
||||
productionTeam: ProductionTeam[];
|
||||
nextVideoReleaseDate: Date;
|
||||
indexable: boolean;
|
||||
}
|
||||
|
||||
export interface ProductionTeam {
|
||||
role: string;
|
||||
name: string;
|
||||
}
|
||||
88
@types/animeOnegaiSearch.d.ts
vendored
Normal file
88
@types/animeOnegaiSearch.d.ts
vendored
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
export interface AnimeOnegaiSearch {
|
||||
text: string;
|
||||
list: AOSearchResult[];
|
||||
}
|
||||
|
||||
export interface AOSearchResult {
|
||||
/**
|
||||
* Asset ID
|
||||
*/
|
||||
ID: number;
|
||||
CreatedAt: Date;
|
||||
UpdatedAt: Date;
|
||||
DeletedAt: null;
|
||||
title: string;
|
||||
active: boolean;
|
||||
excerpt: string;
|
||||
description: string;
|
||||
bg: string;
|
||||
poster: string;
|
||||
entry: string;
|
||||
code_name: string;
|
||||
/**
|
||||
* The Video ID required to get the streams
|
||||
*/
|
||||
video_entry: string;
|
||||
trailer: string;
|
||||
year: number;
|
||||
/**
|
||||
* Asset Type, Known Possibilities
|
||||
* * 1 - Video
|
||||
* * 2 - Series
|
||||
*/
|
||||
asset_type: 1 | 2;
|
||||
status: number;
|
||||
permalink: string;
|
||||
duration: string;
|
||||
subtitles: boolean;
|
||||
price: number;
|
||||
rent_price: number;
|
||||
rating: number;
|
||||
color: number | null;
|
||||
classification: number;
|
||||
brazil_classification: null | string;
|
||||
likes: number;
|
||||
views: number;
|
||||
button: string;
|
||||
stream_url: string;
|
||||
stream_url_backup: string;
|
||||
copyright: null | string;
|
||||
skip_intro: null | string;
|
||||
ending: null | string;
|
||||
bumper_intro: string;
|
||||
ads: string;
|
||||
age_restriction: boolean | null;
|
||||
epg: null;
|
||||
allow_languages: string[] | null;
|
||||
allow_countries: string[] | null;
|
||||
classification_text: string;
|
||||
locked: boolean;
|
||||
resign: boolean;
|
||||
favorite: boolean;
|
||||
actors_list: null;
|
||||
voiceactors_list: null;
|
||||
artdirectors_list: null;
|
||||
audios_list: null;
|
||||
awards_list: null;
|
||||
companies_list: null;
|
||||
countries_list: null;
|
||||
directors_list: null;
|
||||
edition_list: null;
|
||||
genres_list: null;
|
||||
music_list: null;
|
||||
photograpy_list: null;
|
||||
producer_list: null;
|
||||
screenwriter_list: null;
|
||||
season_list: null;
|
||||
tags_list: null;
|
||||
chapter_id: number;
|
||||
chapter_entry: string;
|
||||
chapter_poster: string;
|
||||
progress_time: number;
|
||||
progress_percent: number;
|
||||
included_subscription: number;
|
||||
paid_content: number;
|
||||
rent_content: number;
|
||||
objectID: string;
|
||||
lang: string;
|
||||
}
|
||||
36
@types/animeOnegaiSeasons.d.ts
vendored
Normal file
36
@types/animeOnegaiSeasons.d.ts
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
export interface AnimeOnegaiSeasons {
|
||||
ID: number;
|
||||
CreatedAt: Date;
|
||||
UpdatedAt: Date;
|
||||
DeletedAt: null;
|
||||
name: string;
|
||||
number: number;
|
||||
asset_id: number;
|
||||
entry: string;
|
||||
description: string;
|
||||
active: boolean;
|
||||
allow_languages: string[];
|
||||
allow_countries: string[];
|
||||
list: Episode[];
|
||||
}
|
||||
|
||||
export interface Episode {
|
||||
ID: number;
|
||||
CreatedAt: Date;
|
||||
UpdatedAt: Date;
|
||||
DeletedAt: null;
|
||||
name: string;
|
||||
number: number;
|
||||
description: string;
|
||||
thumbnail: string;
|
||||
entry: string;
|
||||
video_entry: string;
|
||||
active: boolean;
|
||||
season_id: number;
|
||||
stream_url: string;
|
||||
skip_intro: null;
|
||||
ending: null;
|
||||
open_free: boolean;
|
||||
asset_id: number;
|
||||
age_restriction: boolean;
|
||||
}
|
||||
111
@types/animeOnegaiSeries.d.ts
vendored
Normal file
111
@types/animeOnegaiSeries.d.ts
vendored
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
export interface AnimeOnegaiSeries {
|
||||
ID: number;
|
||||
CreatedAt: Date;
|
||||
UpdatedAt: Date;
|
||||
DeletedAt: null;
|
||||
title: string;
|
||||
active: boolean;
|
||||
excerpt: string;
|
||||
description: string;
|
||||
bg: string;
|
||||
poster: string;
|
||||
entry: string;
|
||||
code_name: string;
|
||||
/**
|
||||
* The Video ID required to get the streams
|
||||
*/
|
||||
video_entry: string;
|
||||
trailer: string;
|
||||
year: number;
|
||||
asset_type: number;
|
||||
status: number;
|
||||
permalink: string;
|
||||
duration: string;
|
||||
subtitles: boolean;
|
||||
price: number;
|
||||
rent_price: number;
|
||||
rating: number;
|
||||
color: number;
|
||||
classification: number;
|
||||
brazil_classification: string;
|
||||
likes: number;
|
||||
views: number;
|
||||
button: string;
|
||||
stream_url: string;
|
||||
stream_url_backup: string;
|
||||
copyright: string;
|
||||
skip_intro: null;
|
||||
ending: null;
|
||||
bumper_intro: string;
|
||||
ads: string;
|
||||
age_restriction: boolean;
|
||||
epg: null;
|
||||
allow_languages: string[];
|
||||
allow_countries: string[];
|
||||
classification_text: string;
|
||||
locked: boolean;
|
||||
resign: boolean;
|
||||
favorite: boolean;
|
||||
actors_list: CtorsList[];
|
||||
voiceactors_list: CtorsList[];
|
||||
artdirectors_list: any[];
|
||||
audios_list: SList[];
|
||||
awards_list: any[];
|
||||
companies_list: any[];
|
||||
countries_list: any[];
|
||||
directors_list: CtorsList[];
|
||||
edition_list: any[];
|
||||
genres_list: SList[];
|
||||
music_list: any[];
|
||||
photograpy_list: any[];
|
||||
producer_list: any[];
|
||||
screenwriter_list: any[];
|
||||
season_list: any[];
|
||||
tags_list: TagsList[];
|
||||
chapter_id: number;
|
||||
chapter_entry: string;
|
||||
chapter_poster: string;
|
||||
progress_time: number;
|
||||
progress_percent: number;
|
||||
included_subscription: number;
|
||||
paid_content: number;
|
||||
rent_content: number;
|
||||
objectID: string;
|
||||
lang: string;
|
||||
}
|
||||
|
||||
export interface CtorsList {
|
||||
ID: number;
|
||||
CreatedAt: Date;
|
||||
UpdatedAt: Date;
|
||||
DeletedAt: null;
|
||||
name: string;
|
||||
Permalink?: string;
|
||||
country: number | null;
|
||||
year: number | null;
|
||||
death: number | null;
|
||||
image: string;
|
||||
genre: null;
|
||||
description: string;
|
||||
permalink?: string;
|
||||
background?: string;
|
||||
}
|
||||
|
||||
export interface SList {
|
||||
ID: number;
|
||||
CreatedAt: Date;
|
||||
UpdatedAt: Date;
|
||||
DeletedAt: null;
|
||||
name: string;
|
||||
age_restriction?: number;
|
||||
}
|
||||
|
||||
export interface TagsList {
|
||||
ID: number;
|
||||
CreatedAt: Date;
|
||||
UpdatedAt: Date;
|
||||
DeletedAt: null;
|
||||
name: string;
|
||||
position: number;
|
||||
status: boolean;
|
||||
}
|
||||
41
@types/animeOnegaiStream.d.ts
vendored
Normal file
41
@types/animeOnegaiStream.d.ts
vendored
Normal file
|
|
@ -0,0 +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;
|
||||
CreatedAt: Date;
|
||||
UpdatedAt: Date;
|
||||
DeletedAt: null;
|
||||
name: string;
|
||||
lang: string;
|
||||
entry_id: string;
|
||||
url: string;
|
||||
}
|
||||
44
@types/crunchyPlayStreams.d.ts
vendored
Normal file
44
@types/crunchyPlayStreams.d.ts
vendored
Normal file
|
|
@ -0,0 +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[];
|
||||
}
|
||||
|
||||
export interface Caption {
|
||||
format: string;
|
||||
language: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface HardSub {
|
||||
hlang: string;
|
||||
url: string;
|
||||
quality: string;
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
renewSeconds: number;
|
||||
noNetworkRetryIntervalSeconds: number;
|
||||
noNetworkTimeoutSeconds: number;
|
||||
maximumPauseSeconds: number;
|
||||
endOfVideoUnloadSeconds: number;
|
||||
sessionExpirationSeconds: number;
|
||||
usesStreamLimits: boolean;
|
||||
}
|
||||
|
||||
export interface Subtitle {
|
||||
format: string;
|
||||
language: string;
|
||||
url: string;
|
||||
}
|
||||
7
@types/crunchyTypes.d.ts
vendored
7
@types/crunchyTypes.d.ts
vendored
|
|
@ -2,11 +2,14 @@ import { HLSCallback } from 'hls-download';
|
|||
import { sxItem } from '../crunchy';
|
||||
import { LanguageItem } from '../modules/module.langsData';
|
||||
import { DownloadInfo } from './messageHandler';
|
||||
import { CrunchyPlayStreams } from './enums';
|
||||
|
||||
export type CrunchyDownloadOptions = {
|
||||
hslang: string,
|
||||
kstream: number,
|
||||
cstream: keyof typeof CrunchyPlayStreams | 'none',
|
||||
novids?: boolean,
|
||||
noaudio?: boolean,
|
||||
x: number,
|
||||
q: number,
|
||||
fileName: string,
|
||||
|
|
@ -34,6 +37,7 @@ export type CrunchyDownloadOptions = {
|
|||
nocleanup: boolean,
|
||||
chapters: boolean,
|
||||
fontName: string | undefined,
|
||||
originalFontSize: boolean,
|
||||
fontSize: number,
|
||||
dubLang: string[],
|
||||
}
|
||||
|
|
@ -42,7 +46,8 @@ export type CrunchyMultiDownload = {
|
|||
dubLang: string[],
|
||||
all?: boolean,
|
||||
but?: boolean,
|
||||
e?: string
|
||||
e?: string,
|
||||
s?: string
|
||||
}
|
||||
|
||||
export type CrunchyMuxOptions = {
|
||||
|
|
|
|||
16
@types/enums.ts
Normal file
16
@types/enums.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
export enum CrunchyPlayStreams {
|
||||
'chrome' = 'web/chrome',
|
||||
'firefox' = 'web/firefox',
|
||||
'safari' = 'web/safari',
|
||||
'edge' = 'web/edge',
|
||||
'fallback' = 'web/fallback',
|
||||
'ps4' = 'console/ps4',
|
||||
'ps5' = 'console/ps5',
|
||||
'switch' = 'console/switch',
|
||||
'samsungtv' = 'tv/samsung',
|
||||
'lgtv' = 'tv/lg',
|
||||
'rokutv' = 'tv/roku',
|
||||
'android' = 'android/phone',
|
||||
'iphone' = 'ios/iphone',
|
||||
'ipad' = 'ios/ipad',
|
||||
}
|
||||
34
@types/funiSearch.d.ts
vendored
34
@types/funiSearch.d.ts
vendored
|
|
@ -1,34 +0,0 @@
|
|||
// Generated by https://quicktype.io
|
||||
|
||||
export interface FunimationSearch {
|
||||
count: number;
|
||||
items: Items;
|
||||
limit: string;
|
||||
offset: string;
|
||||
}
|
||||
|
||||
export interface Items {
|
||||
hits: Hit[];
|
||||
}
|
||||
|
||||
export interface Hit {
|
||||
ratings: string;
|
||||
description: string;
|
||||
title: string;
|
||||
image: {
|
||||
showThumbnail: string,
|
||||
[key: string]: string
|
||||
};
|
||||
starRating: number;
|
||||
slug: string;
|
||||
languages: string[];
|
||||
synopsis: string;
|
||||
quality: Quality;
|
||||
id: string;
|
||||
txDate: number;
|
||||
}
|
||||
|
||||
export interface Quality {
|
||||
quality: string;
|
||||
height: number;
|
||||
}
|
||||
75
@types/funiSubtitleRequest.d.ts
vendored
75
@types/funiSubtitleRequest.d.ts
vendored
|
|
@ -1,75 +0,0 @@
|
|||
// Generated by https://quicktype.io
|
||||
|
||||
export interface SubtitleRequest {
|
||||
primary: Primary;
|
||||
fallback: Primary[];
|
||||
}
|
||||
|
||||
export interface Primary {
|
||||
venueVideoId: string;
|
||||
alphaPackageId: string;
|
||||
versionContentId: VersionContentID;
|
||||
manifestPath: string;
|
||||
fileExt: PrimaryFileEXT;
|
||||
subtitles: Subtitle[];
|
||||
accessType: AccessType;
|
||||
sessionId: string;
|
||||
audioLanguage: AudioLanguage;
|
||||
version: Version;
|
||||
aips: Aip[];
|
||||
drmToken: string;
|
||||
drmType: string;
|
||||
}
|
||||
|
||||
export enum AccessType {
|
||||
Subscription = 'subscription',
|
||||
}
|
||||
|
||||
export interface Aip {
|
||||
in: number;
|
||||
out: number;
|
||||
}
|
||||
|
||||
export enum AudioLanguage {
|
||||
En = 'en',
|
||||
Ja = 'ja',
|
||||
}
|
||||
|
||||
export enum PrimaryFileEXT {
|
||||
M3U8 = 'm3u8',
|
||||
Mp4 = 'mp4',
|
||||
}
|
||||
|
||||
export interface Subtitle {
|
||||
filePath: string;
|
||||
fileExt: SubtitleFileEXT;
|
||||
contentType: ContentType;
|
||||
languageCode: LanguageCode;
|
||||
}
|
||||
|
||||
export enum ContentType {
|
||||
Cc = 'cc',
|
||||
Full = 'full',
|
||||
}
|
||||
|
||||
export enum SubtitleFileEXT {
|
||||
Dfxp = 'dfxp',
|
||||
Srt = 'srt',
|
||||
Vtt = 'vtt',
|
||||
}
|
||||
|
||||
export enum LanguageCode {
|
||||
En = 'en',
|
||||
Es = 'es',
|
||||
Pt = 'pt',
|
||||
}
|
||||
|
||||
export enum Version {
|
||||
Simulcast = 'simulcast',
|
||||
Uncut = 'uncut',
|
||||
}
|
||||
|
||||
export enum VersionContentID {
|
||||
Akusim0012 = 'AKUSIM0012',
|
||||
Akuunc0012 = 'AKUUNC0012',
|
||||
}
|
||||
16
@types/funiTypes.d.ts
vendored
16
@types/funiTypes.d.ts
vendored
|
|
@ -1,16 +0,0 @@
|
|||
import { LanguageItem } from '../modules/module.langsData';
|
||||
|
||||
export type FunimationMediaDownload = {
|
||||
id: string,
|
||||
title: string,
|
||||
showTitle: string,
|
||||
image: string
|
||||
}
|
||||
|
||||
export type Subtitle = {
|
||||
url: string,
|
||||
lang: LanguageItem,
|
||||
ext: string,
|
||||
out?: string,
|
||||
closedCaption?: boolean
|
||||
}
|
||||
30
@types/mpd-parser.d.ts
vendored
30
@types/mpd-parser.d.ts
vendored
|
|
@ -7,11 +7,40 @@ declare module 'mpd-parser' {
|
|||
map: {
|
||||
uri: string,
|
||||
resolvedUri: string,
|
||||
byterange?: {
|
||||
length: number,
|
||||
offset: number
|
||||
}
|
||||
},
|
||||
byterange?: {
|
||||
length: number,
|
||||
offset: number
|
||||
},
|
||||
number: number,
|
||||
presentationTime: number
|
||||
}
|
||||
|
||||
export type Sidx = {
|
||||
uri: string,
|
||||
resolvedUri: string,
|
||||
byterange: {
|
||||
length: number,
|
||||
offset: number
|
||||
},
|
||||
map: {
|
||||
uri: string,
|
||||
resolvedUri: string,
|
||||
byterange: {
|
||||
length: number,
|
||||
offset: number
|
||||
}
|
||||
},
|
||||
duration: number,
|
||||
timeline: number,
|
||||
presentationTime: number,
|
||||
number: number
|
||||
}
|
||||
|
||||
export type Playlist = {
|
||||
attributes: {
|
||||
NAME: string,
|
||||
|
|
@ -45,6 +74,7 @@ declare module 'mpd-parser' {
|
|||
}
|
||||
}
|
||||
segments: Segment[]
|
||||
sidx?: Sidx
|
||||
}
|
||||
|
||||
export type Manifest = {
|
||||
|
|
|
|||
43
@types/newHidiveEpisode.d.ts
vendored
Normal file
43
@types/newHidiveEpisode.d.ts
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
export interface NewHidiveEpisode {
|
||||
description: string;
|
||||
duration: number;
|
||||
title: string;
|
||||
categories: string[];
|
||||
contentDownload: ContentDownload;
|
||||
favourite: boolean;
|
||||
subEvents: any[];
|
||||
thumbnailUrl: string;
|
||||
longDescription: string;
|
||||
posterUrl: string;
|
||||
offlinePlaybackLanguages: string[];
|
||||
externalAssetId: string;
|
||||
maxHeight: number;
|
||||
rating: Rating;
|
||||
episodeInformation: EpisodeInformation;
|
||||
id: number;
|
||||
accessLevel: string;
|
||||
playerUrlCallback: string;
|
||||
thumbnailsPreview: string;
|
||||
displayableTags: any[];
|
||||
plugins: any[];
|
||||
watchStatus: string;
|
||||
computedReleases: any[];
|
||||
licences: any[];
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface ContentDownload {
|
||||
permission: string;
|
||||
period: string;
|
||||
}
|
||||
|
||||
export interface EpisodeInformation {
|
||||
seasonNumber: number;
|
||||
episodeNumber: number;
|
||||
season: number;
|
||||
}
|
||||
|
||||
export interface Rating {
|
||||
rating: string;
|
||||
descriptors: any[];
|
||||
}
|
||||
33
@types/newHidivePlayback.d.ts
vendored
Normal file
33
@types/newHidivePlayback.d.ts
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
export interface NewHidivePlayback {
|
||||
watermark: null;
|
||||
skipMarkers: any[];
|
||||
annotations: null;
|
||||
dash: Format[];
|
||||
hls: Format[];
|
||||
}
|
||||
|
||||
export interface Format {
|
||||
subtitles: Subtitle[];
|
||||
url: string;
|
||||
drm: DRM;
|
||||
}
|
||||
|
||||
export interface DRM {
|
||||
encryptionMode: string;
|
||||
containerType: string;
|
||||
jwtToken: string;
|
||||
url: string;
|
||||
keySystems: string[];
|
||||
}
|
||||
|
||||
export interface Subtitle {
|
||||
format: Formats;
|
||||
language: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export enum Formats {
|
||||
Scc = 'scc',
|
||||
Srt = 'srt',
|
||||
Vtt = 'vtt',
|
||||
}
|
||||
91
@types/newHidiveSearch.d.ts
vendored
Normal file
91
@types/newHidiveSearch.d.ts
vendored
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
export interface NewHidiveSearch {
|
||||
results: Result[];
|
||||
}
|
||||
|
||||
export interface Result {
|
||||
hits: Hit[];
|
||||
nbHits: number;
|
||||
page: number;
|
||||
nbPages: number;
|
||||
hitsPerPage: number;
|
||||
exhaustiveNbHits: boolean;
|
||||
exhaustiveTypo: boolean;
|
||||
exhaustive: Exhaustive;
|
||||
query: string;
|
||||
params: string;
|
||||
index: string;
|
||||
renderingContent: RenderingContent;
|
||||
processingTimeMS: number;
|
||||
processingTimingsMS: ProcessingTimingsMS;
|
||||
serverTimeMS: number;
|
||||
}
|
||||
|
||||
export interface Exhaustive {
|
||||
nbHits: boolean;
|
||||
typo: boolean;
|
||||
}
|
||||
|
||||
export interface Hit {
|
||||
type: string;
|
||||
weight: number;
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
meta: RenderingContent;
|
||||
coverUrl: string;
|
||||
smallCoverUrl: string;
|
||||
seasonsCount: number;
|
||||
tags: string[];
|
||||
localisations: HitLocalisations;
|
||||
ratings: Ratings;
|
||||
objectID: string;
|
||||
_highlightResult: HighlightResult;
|
||||
}
|
||||
|
||||
export interface HighlightResult {
|
||||
name: Description;
|
||||
description: Description;
|
||||
tags: Description[];
|
||||
localisations: HighlightResultLocalisations;
|
||||
}
|
||||
|
||||
export interface Description {
|
||||
value: string;
|
||||
matchLevel: string;
|
||||
matchedWords: string[];
|
||||
fullyHighlighted?: boolean;
|
||||
}
|
||||
|
||||
export interface HighlightResultLocalisations {
|
||||
en_US: PurpleEnUS;
|
||||
}
|
||||
|
||||
export interface PurpleEnUS {
|
||||
title: Description;
|
||||
description: Description;
|
||||
}
|
||||
|
||||
export interface HitLocalisations {
|
||||
[language: string]: HitLocalization;
|
||||
}
|
||||
|
||||
export interface HitLocalization {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface RenderingContent {
|
||||
}
|
||||
|
||||
export interface Ratings {
|
||||
US: string[];
|
||||
}
|
||||
|
||||
export interface ProcessingTimingsMS {
|
||||
_request: Request;
|
||||
}
|
||||
|
||||
export interface Request {
|
||||
queue: number;
|
||||
roundTrip: number;
|
||||
}
|
||||
89
@types/newHidiveSeason.d.ts
vendored
Normal file
89
@types/newHidiveSeason.d.ts
vendored
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
export interface NewHidiveSeason {
|
||||
title: string;
|
||||
description: string;
|
||||
longDescription: string;
|
||||
smallCoverUrl: string;
|
||||
coverUrl: string;
|
||||
titleUrl: string;
|
||||
posterUrl: string;
|
||||
seasonNumber: number;
|
||||
episodeCount: number;
|
||||
displayableTags: any[];
|
||||
rating: Rating;
|
||||
contentRating: Rating;
|
||||
id: number;
|
||||
series: Series;
|
||||
episodes: Episode[];
|
||||
paging: Paging;
|
||||
licences: any[];
|
||||
}
|
||||
|
||||
export interface Rating {
|
||||
rating: string;
|
||||
descriptors: any[];
|
||||
}
|
||||
|
||||
export interface Episode {
|
||||
accessLevel: string;
|
||||
availablePurchases?: any[];
|
||||
licenceIds?: any[];
|
||||
type: string;
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
thumbnailUrl: string;
|
||||
posterUrl: string;
|
||||
duration: number;
|
||||
favourite: boolean;
|
||||
contentDownload: ContentDownload;
|
||||
offlinePlaybackLanguages: string[];
|
||||
externalAssetId: string;
|
||||
subEvents: any[];
|
||||
maxHeight: number;
|
||||
thumbnailsPreview: string;
|
||||
longDescription: string;
|
||||
episodeInformation: EpisodeInformation;
|
||||
categories: string[];
|
||||
displayableTags: any[];
|
||||
watchStatus: string;
|
||||
computedReleases: any[];
|
||||
}
|
||||
|
||||
export interface ContentDownload {
|
||||
permission: string;
|
||||
}
|
||||
|
||||
export interface EpisodeInformation {
|
||||
seasonNumber: number;
|
||||
episodeNumber: number;
|
||||
season: number;
|
||||
}
|
||||
|
||||
export interface Paging {
|
||||
moreDataAvailable: boolean;
|
||||
lastSeen: number;
|
||||
}
|
||||
|
||||
export interface Series {
|
||||
seriesId: number;
|
||||
title: string;
|
||||
description: string;
|
||||
longDescription: string;
|
||||
displayableTags: any[];
|
||||
rating: Rating;
|
||||
contentRating: Rating;
|
||||
}
|
||||
|
||||
export interface NewHidiveSeriesExtra extends Series {
|
||||
season: NewHidiveSeason;
|
||||
}
|
||||
|
||||
export interface NewHidiveEpisodeExtra extends Episode {
|
||||
titleId: number;
|
||||
nameLong: string;
|
||||
seasonTitle: string;
|
||||
seriesTitle: string;
|
||||
seriesId?: number;
|
||||
isSelected: boolean;
|
||||
jwtToken?: string;
|
||||
}
|
||||
35
@types/newHidiveSeries.d.ts
vendored
Normal file
35
@types/newHidiveSeries.d.ts
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
export interface NewHidiveSeries {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
longDescription: string;
|
||||
smallCoverUrl: string;
|
||||
coverUrl: string;
|
||||
titleUrl: string;
|
||||
posterUrl: string;
|
||||
seasons: Season[];
|
||||
rating: Rating;
|
||||
contentRating: Rating;
|
||||
displayableTags: any[];
|
||||
paging: Paging;
|
||||
}
|
||||
|
||||
export interface Rating {
|
||||
rating: string;
|
||||
descriptors: any[];
|
||||
}
|
||||
|
||||
export interface Paging {
|
||||
moreDataAvailable: boolean;
|
||||
lastSeen: number;
|
||||
}
|
||||
|
||||
export interface Season {
|
||||
title: string;
|
||||
description: string;
|
||||
longDescription: string;
|
||||
seasonNumber: number;
|
||||
episodeCount: number;
|
||||
displayableTags: any[];
|
||||
id: number;
|
||||
}
|
||||
44
@types/playbackData.d.ts
vendored
44
@types/playbackData.d.ts
vendored
|
|
@ -1,29 +1,29 @@
|
|||
// Generated by https://quicktype.io
|
||||
export interface PlaybackData {
|
||||
total: number;
|
||||
data: [{ [key: string]: { [key: string]: StreamDetails } }];
|
||||
meta: Meta;
|
||||
data: { [key: string]: { [key: string]: StreamDetails } }[];
|
||||
meta: Meta;
|
||||
}
|
||||
|
||||
export interface StreamList {
|
||||
download_hls: Streams;
|
||||
drm_adaptive_hls: Streams;
|
||||
multitrack_adaptive_hls_v2: Streams;
|
||||
vo_adaptive_hls: Streams;
|
||||
vo_drm_adaptive_hls: Streams;
|
||||
adaptive_hls: Streams;
|
||||
drm_download_dash: Streams;
|
||||
drm_download_hls: Streams;
|
||||
drm_multitrack_adaptive_hls_v2: Streams;
|
||||
vo_drm_adaptive_dash: Streams;
|
||||
adaptive_dash: Streams;
|
||||
urls: Streams;
|
||||
vo_adaptive_dash: Streams;
|
||||
download_dash: Streams;
|
||||
drm_adaptive_dash: Streams;
|
||||
download_hls: CrunchyStreams;
|
||||
drm_adaptive_hls: CrunchyStreams;
|
||||
multitrack_adaptive_hls_v2: CrunchyStreams;
|
||||
vo_adaptive_hls: CrunchyStreams;
|
||||
vo_drm_adaptive_hls: CrunchyStreams;
|
||||
adaptive_hls: CrunchyStreams;
|
||||
drm_download_dash: CrunchyStreams;
|
||||
drm_download_hls: CrunchyStreams;
|
||||
drm_multitrack_adaptive_hls_v2: CrunchyStreams;
|
||||
vo_drm_adaptive_dash: CrunchyStreams;
|
||||
adaptive_dash: CrunchyStreams;
|
||||
urls: CrunchyStreams;
|
||||
vo_adaptive_dash: CrunchyStreams;
|
||||
download_dash: CrunchyStreams;
|
||||
drm_adaptive_dash: CrunchyStreams;
|
||||
}
|
||||
|
||||
export interface Streams {
|
||||
export interface CrunchyStreams {
|
||||
'': StreamDetails;
|
||||
'en-US'?: StreamDetails;
|
||||
'es-LA'?: StreamDetails;
|
||||
|
|
@ -41,10 +41,12 @@ export interface Streams {
|
|||
'zh-CN'?: StreamDetails;
|
||||
'ko-KR'?: StreamDetails;
|
||||
'ja-JP'?: StreamDetails;
|
||||
[string: string]: StreamDetails;
|
||||
}
|
||||
|
||||
export interface StreamDetails {
|
||||
hardsub_locale: Locale;
|
||||
//hardsub_locale: Locale;
|
||||
hardsub_locale: string;
|
||||
url: string;
|
||||
hardsub_lang?: string;
|
||||
audio_lang?: string;
|
||||
|
|
@ -57,11 +59,11 @@ export interface Meta {
|
|||
versions: Version[];
|
||||
audio_locale: Locale;
|
||||
closed_captions: Subtitles;
|
||||
captions: Record<unknown>;
|
||||
captions: Subtitles;
|
||||
}
|
||||
|
||||
export interface Subtitles {
|
||||
'': SubtitleInfo;
|
||||
''?: SubtitleInfo;
|
||||
'en-US'?: SubtitleInfo;
|
||||
'es-LA'?: SubtitleInfo;
|
||||
'es-419'?: SubtitleInfo;
|
||||
|
|
|
|||
4
@types/ws.d.ts
vendored
4
@types/ws.d.ts
vendored
|
|
@ -30,8 +30,8 @@ export type MessageTypes = {
|
|||
'isDownloading': [undefined, boolean],
|
||||
'openFolder': [FolderTypes, undefined],
|
||||
'changeProvider': [undefined, boolean],
|
||||
'type': [undefined, 'funi'|'crunchy'|'hidive'|undefined],
|
||||
'setup': ['funi'|'crunchy'|'hidive'|undefined, undefined],
|
||||
'type': [undefined, 'crunchy'|'hidive'|'ao'|'adn'|undefined],
|
||||
'setup': ['crunchy'|'hidive'|'ao'|'adn'|undefined, undefined],
|
||||
'openFile': [[FolderTypes, string], undefined],
|
||||
'openURL': [string, undefined],
|
||||
'isSetup': [undefined, boolean],
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ RUN pnpm run build-linux-gui
|
|||
|
||||
FROM node
|
||||
WORKDIR "/app"
|
||||
COPY --from=builder /app/lib/_builds/multi-downloader-nx-linux64-gui ./
|
||||
COPY --from=builder /app/lib/_builds/multi-downloader-nx-linux-x64-gui ./
|
||||
|
||||
# Install mkvmerge and ffmpeg
|
||||
|
||||
|
|
|
|||
33
TODO.md
33
TODO.md
|
|
@ -1,20 +1,15 @@
|
|||
# GUI
|
||||
# Todo/Future Ideas list
|
||||
|
||||
- [ ] Hls-Download force yes or no on rewrite promt as well as for mkvmerge/ffmpeg
|
||||
- [x] Pick up if a download is currently in progress
|
||||
- [x] Send more information with the progress event like the title and image to display more information
|
||||
- [x] Use Click away listener for the search popup
|
||||
- [x] Quality select button is uncrontrolled/controlled
|
||||
- [ ] Set Options font in divider
|
||||
- [x] Window title
|
||||
- [x] Only open dev tools in test version
|
||||
- [x] Add help information (version, contributor, documentation...)
|
||||
- [x] ContextMenu
|
||||
- [x] Better episode listing with selectio via left mouse button
|
||||
- [x] Use Child for Context Menu
|
||||
|
||||
# CLI
|
||||
## New API ?
|
||||
- [ ] https://playback.prd.funimationsvc.com/v1/play/FMB0001?deviceType=web&playbackStreamId=137917d5-dc9b-4a72-83da-14231fd1d05e
|
||||
- [ ] https://playlist-service.prd.funimationsvc.com/v1/playlist/show/FMB
|
||||
- [ ] https://d33et77evd9bgg.cloudfront.net/data/v1/episodes/fullmetal-alchemist.json
|
||||
- [ ] Look into implementing wvd file support
|
||||
- [ ] Merge sync branch with latest master
|
||||
- [ ] Finish implementing old algorithm
|
||||
- [ ] Look into adding suggested algorithm [#599](https://github.com/anidl/multi-downloader-nx/issues/599)
|
||||
- [ ] Remove Funimation
|
||||
- [ ] Remove old hidive API or find a way to make it work
|
||||
- [ ] Look into adding other services
|
||||
- [ ] Refactor downloading code
|
||||
- [ ] Allow audio and video download at the same time
|
||||
- [ ] Reduce/Refactor the amount of duplicate/boilerplate code required
|
||||
- [ ] Create a generic service class for the CLI with set inputs/outputs
|
||||
- [ ] Modularize site modules to ease addition of new sites
|
||||
- [ ] Create generic MPD/M3U8 playlist downloader
|
||||
|
|
|
|||
924
adn.ts
Normal file
924
adn.ts
Normal file
|
|
@ -0,0 +1,924 @@
|
|||
// Package Info
|
||||
import packageJson from './package.json';
|
||||
|
||||
// Node
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import crypto from 'crypto';
|
||||
|
||||
// Plugins
|
||||
import shlp from 'sei-helper';
|
||||
import m3u8 from 'm3u8-parsed';
|
||||
|
||||
// Modules
|
||||
import * as fontsData from './modules/module.fontsData';
|
||||
import * as langsData from './modules/module.langsData';
|
||||
import * as yamlCfg from './modules/module.cfg-loader';
|
||||
import * as yargs from './modules/module.app-args';
|
||||
import * as reqModule from './modules/module.fetch';
|
||||
import Merger, { Font, MergerInput, SubtitleInput } from './modules/module.merger';
|
||||
import streamdl from './modules/hls-download';
|
||||
import { console } from './modules/log';
|
||||
import { domain } from './modules/module.api-urls';
|
||||
import { downloaded } from './modules/module.downloadArchive';
|
||||
import parseSelect from './modules/module.parseSelect';
|
||||
import parseFileName, { Variable } from './modules/module.filename';
|
||||
import { AvailableFilenameVars } from './modules/module.args';
|
||||
|
||||
// Types
|
||||
import { ServiceClass } from './@types/serviceClassInterface';
|
||||
import { AuthData, AuthResponse, SearchData, SearchResponse, SearchResponseItem } from './@types/messageHandler';
|
||||
import { sxItem } from './crunchy';
|
||||
import { DownloadedMedia } from './@types/hidiveTypes';
|
||||
import { ADNSearch, ADNSearchShow } from './@types/adnSearch';
|
||||
import { ADNVideo, ADNVideos } from './@types/adnVideos';
|
||||
import { ADNPlayerConfig } from './@types/adnPlayerConfig';
|
||||
import { ADNStreams } from './@types/adnStreams';
|
||||
import { ADNSubtitles } from './@types/adnSubtitles';
|
||||
|
||||
export default class AnimationDigitalNetwork implements ServiceClass {
|
||||
public cfg: yamlCfg.ConfigObject;
|
||||
public locale: string;
|
||||
private token: Record<string, any>;
|
||||
private req: reqModule.Req;
|
||||
private posAlignMap: { [key: string]: number } = {
|
||||
'start': 1,
|
||||
'end': 3
|
||||
};
|
||||
private lineAlignMap: { [key: string]: number } = {
|
||||
'middle': 8,
|
||||
'end': 4
|
||||
};
|
||||
private jpnStrings: string[] = [
|
||||
'vostf',
|
||||
'vostde'
|
||||
];
|
||||
private deuStrings: string[] = [
|
||||
'vde'
|
||||
];
|
||||
private fraStrings: string[] = [
|
||||
'vf'
|
||||
];
|
||||
private deuSubStrings: string[] = [
|
||||
'vde',
|
||||
'vostde'
|
||||
];
|
||||
private fraSubStrings: string[] = [
|
||||
'vf',
|
||||
'vostf'
|
||||
];
|
||||
|
||||
constructor(private debug = false) {
|
||||
this.cfg = yamlCfg.loadCfg();
|
||||
this.token = yamlCfg.loadADNToken();
|
||||
this.req = new reqModule.Req(domain, debug, false, 'adn');
|
||||
this.locale = 'fr';
|
||||
}
|
||||
|
||||
public async cli() {
|
||||
console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`);
|
||||
const argv = yargs.appArgv(this.cfg.cli);
|
||||
if (['fr', 'de'].includes(argv.locale))
|
||||
this.locale = argv.locale;
|
||||
if (argv.debug)
|
||||
this.debug = true;
|
||||
|
||||
// load binaries
|
||||
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||
if (argv.allDubs) {
|
||||
argv.dubLang = langsData.dubLanguageCodes;
|
||||
}
|
||||
if (argv.auth) {
|
||||
//Authenticate
|
||||
await this.doAuth({
|
||||
username: argv.username ?? await shlp.question('[Q] LOGIN/EMAIL'),
|
||||
password: argv.password ?? await shlp.question('[Q] PASSWORD ')
|
||||
});
|
||||
} else if (argv.search && argv.search.length > 2) {
|
||||
//Search
|
||||
await this.doSearch({ ...argv, search: argv.search as string });
|
||||
} else if (argv.s && !isNaN(parseInt(argv.s,10)) && parseInt(argv.s,10) > 0) {
|
||||
const selected = await this.selectShow(parseInt(argv.s), argv.e, argv.but, argv.all);
|
||||
if (selected.isOk) {
|
||||
for (const select of selected.value) {
|
||||
if (!(await this.getEpisode(select, {...argv, skipsubs: false}))) {
|
||||
console.error(`Unable to download selected episode ${select.shortNumber}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
console.info('No option selected or invalid value entered. Try --help.');
|
||||
}
|
||||
}
|
||||
|
||||
private generateRandomString(length: number) {
|
||||
const characters = '0123456789abcdef';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private parseCookies(cookiesString: string | null): Record<string, string> {
|
||||
const cookies: Record<string, string> = {};
|
||||
if (cookiesString) {
|
||||
cookiesString.split(';').forEach(cookie => {
|
||||
const parts = cookie.split('=');
|
||||
const name = parts.shift()?.trim();
|
||||
const value = decodeURIComponent(parts.join('='));
|
||||
if (name) {
|
||||
cookies[name] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
return cookies;
|
||||
}
|
||||
|
||||
private convertToSSATimestamp(timestamp: number): string {
|
||||
const seconds = Math.floor(timestamp);
|
||||
const centiseconds = Math.round((timestamp - seconds) * 100);
|
||||
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
public async doSearch(data: SearchData): Promise<SearchResponse> {
|
||||
const limit = 12;
|
||||
const offset = data.page ? data.page * limit : 0;
|
||||
const searchReq = await this.req.getData(`https://gw.api.animationdigitalnetwork.fr/show/catalog?maxAgeCategory=18&offset=${offset}&limit=${limit}&search=${encodeURIComponent(data.search)}`, {
|
||||
'headers': {
|
||||
'X-Target-Distribution': this.locale
|
||||
}
|
||||
});
|
||||
if (!searchReq.ok || !searchReq.res) {
|
||||
console.error('Search FAILED!');
|
||||
return { isOk: false, reason: new Error('Search failed. No more information provided') };
|
||||
}
|
||||
const searchData = await searchReq.res.json() as ADNSearch;
|
||||
const searchItems: ADNSearchShow[] = [];
|
||||
console.info('Search Results:');
|
||||
for (const show of searchData.shows) {
|
||||
searchItems.push(show);
|
||||
let fullType: string;
|
||||
if (show.type == 'EPS') {
|
||||
fullType = `S.${show.id}`;
|
||||
} else if (show.type == 'MOV' || show.type == 'OAV') {
|
||||
fullType = `E.${show.id}`;
|
||||
} else {
|
||||
fullType = 'Unknown';
|
||||
console.warn(`Unknown type ${show.type}, please report this.`);
|
||||
}
|
||||
console.log(`[${fullType}] ${show.title}`);
|
||||
}
|
||||
return { isOk: true, value: searchItems.flatMap((a): SearchResponseItem => {
|
||||
return {
|
||||
id: a.id+'',
|
||||
image: a.image ?? '/notFound.png',
|
||||
name: a.title,
|
||||
rating: a.rating,
|
||||
desc: a.summary
|
||||
};
|
||||
})};
|
||||
}
|
||||
|
||||
public async doAuth(data: AuthData): Promise<AuthResponse> {
|
||||
const authData = new URLSearchParams({
|
||||
'username': data.username,
|
||||
'password': data.password,
|
||||
'source': 'Web',
|
||||
'rememberMe': 'true'
|
||||
}).toString();
|
||||
const authReqOpts: reqModule.Params = {
|
||||
method: 'POST',
|
||||
body: authData
|
||||
};
|
||||
const authReq = await this.req.getData('https://gw.api.animationdigitalnetwork.fr/authentication/login', authReqOpts);
|
||||
if(!authReq.ok || !authReq.res){
|
||||
console.error('Authentication failed!');
|
||||
return { isOk: false, reason: new Error('Authentication failed') };
|
||||
}
|
||||
this.token = await authReq.res.json();
|
||||
yamlCfg.saveADNToken(this.token);
|
||||
console.info('Authentication Success');
|
||||
return { isOk: true, value: undefined };
|
||||
}
|
||||
|
||||
public async refreshToken() {
|
||||
const authReq = await this.req.getData('https://gw.api.animationdigitalnetwork.fr/authentication/refresh', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token.accessToken}`,
|
||||
'X-Access-Token': this.token.accessToken,
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({refreshToken: this.token.refreshToken})
|
||||
});
|
||||
if(!authReq.ok || !authReq.res){
|
||||
console.error('Token refresh failed!');
|
||||
return { isOk: false, reason: new Error('Token refresh failed') };
|
||||
}
|
||||
this.token = await authReq.res.json();
|
||||
yamlCfg.saveADNToken(this.token);
|
||||
return { isOk: true, value: undefined };
|
||||
}
|
||||
|
||||
public async getShow(id: number) {
|
||||
const getShowData = await this.req.getData(`https://gw.api.animationdigitalnetwork.fr/video/show/${id}?maxAgeCategory=18&limit=-1&order=asc`, {
|
||||
'headers': {
|
||||
'X-Target-Distribution': this.locale
|
||||
}
|
||||
});
|
||||
if (!getShowData.ok || !getShowData.res) {
|
||||
console.error('Failed to get Series Data');
|
||||
return { isOk: false };
|
||||
}
|
||||
const showData = await getShowData.res.json() as ADNVideos;
|
||||
return { isOk: true, value: showData };
|
||||
}
|
||||
|
||||
public async listShow(id: number) {
|
||||
const show = await this.getShow(id);
|
||||
if (!show.isOk || !show.value) {
|
||||
console.error('Failed to list show data: Failed to get show');
|
||||
return { isOk: false };
|
||||
}
|
||||
if (show.value.videos.length == 0) {
|
||||
console.error('No episodes found!');
|
||||
return { isOk: false };
|
||||
}
|
||||
const showData = show.value.videos[0].show;
|
||||
console.info(`[S.${showData.id}] ${showData.title}`);
|
||||
const specials: ADNVideo[] = [];
|
||||
let episodeIndex = 0, specialIndex = 0;
|
||||
for (const episode of show.value.videos) {
|
||||
episode.season = episode.season+'';
|
||||
const seasonNumberTitleParse = episode.season.match(/\d+/);
|
||||
const seriesNumberTitleParse = episode.show.title.match(/\d+/);
|
||||
const episodeNumber = parseInt(episode.shortNumber);
|
||||
if (seasonNumberTitleParse && !isNaN(parseInt(seasonNumberTitleParse[0]))) {
|
||||
episode.season = seasonNumberTitleParse[0];
|
||||
} else if (seriesNumberTitleParse && !isNaN(parseInt(seriesNumberTitleParse[0]))) {
|
||||
episode.season = seriesNumberTitleParse[0];
|
||||
} else {
|
||||
episode.season = '1';
|
||||
}
|
||||
show.value.videos[episodeIndex].season = episode.season;
|
||||
if (!episodeNumber) {
|
||||
specialIndex++;
|
||||
const special = show.value.videos.splice(episodeIndex, 1);
|
||||
special[0].shortNumber = 'S'+specialIndex;
|
||||
specials.push(...special);
|
||||
episodeIndex--;
|
||||
} else {
|
||||
console.info(` (${episode.id}) [E${episode.shortNumber}] ${episode.number} - ${episode.name}`);
|
||||
}
|
||||
episodeIndex++;
|
||||
}
|
||||
for (const special of specials) {
|
||||
console.info(` (${special.id}) [${special.shortNumber}] ${special.number} - ${special.name}`);
|
||||
}
|
||||
show.value.videos.push(...specials);
|
||||
return { isOk: true, value: show.value };
|
||||
}
|
||||
|
||||
public async selectShow(id: number, e: string | undefined, but: boolean, all: boolean) {
|
||||
const getShowData = await this.listShow(id);
|
||||
if (!getShowData.isOk || !getShowData.value) {
|
||||
return { isOk: false, value: [] };
|
||||
}
|
||||
console.info('');
|
||||
console.info('-'.repeat(30));
|
||||
console.info('');
|
||||
const showData = getShowData.value;
|
||||
const doEpsFilter = parseSelect(e as string);
|
||||
const selEpsArr: ADNVideo[] = [];
|
||||
for (const episode of showData.videos) {
|
||||
if (
|
||||
all ||
|
||||
but && !doEpsFilter.isSelected([episode.shortNumber, episode.id+'']) ||
|
||||
!but && doEpsFilter.isSelected([episode.shortNumber, episode.id+''])
|
||||
) {
|
||||
selEpsArr.push({ isSelected: true, ...episode });
|
||||
console.info('%s[S%sE%s] %s',
|
||||
'✓ ',
|
||||
episode.season,
|
||||
episode.shortNumber,
|
||||
episode.name,
|
||||
);
|
||||
}
|
||||
}
|
||||
return { isOk: true, value: selEpsArr };
|
||||
}
|
||||
|
||||
public async muxStreams(data: DownloadedMedia[], options: yargs.ArgvType) {
|
||||
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||
let hasAudioStreams = false;
|
||||
if (options.novids || data.filter(a => a.type === 'Video').length === 0)
|
||||
return console.info('Skip muxing since no vids are downloaded');
|
||||
if (data.some(a => a.type === 'Audio')) {
|
||||
hasAudioStreams = true;
|
||||
}
|
||||
const merger = new Merger({
|
||||
onlyVid: hasAudioStreams ? data.filter(a => a.type === 'Video').map((a) : MergerInput => {
|
||||
if (a.type === 'Subtitle')
|
||||
throw new Error('Never');
|
||||
return {
|
||||
lang: a.lang,
|
||||
path: a.path,
|
||||
};
|
||||
}) : [],
|
||||
skipSubMux: options.skipSubMux,
|
||||
inverseTrackOrder: false,
|
||||
keepAllVideos: options.keepAllVideos,
|
||||
onlyAudio: hasAudioStreams ? data.filter(a => a.type === 'Audio').map((a) : MergerInput => {
|
||||
if (a.type === 'Subtitle')
|
||||
throw new Error('Never');
|
||||
return {
|
||||
lang: a.lang,
|
||||
path: a.path,
|
||||
};
|
||||
}) : [],
|
||||
output: `${options.output}.${options.mp4 ? 'mp4' : 'mkv'}`,
|
||||
subtitles: data.filter(a => a.type === 'Subtitle').map((a) : SubtitleInput => {
|
||||
if (a.type === 'Video')
|
||||
throw new Error('Never');
|
||||
if (a.type === 'Audio')
|
||||
throw new Error('Never');
|
||||
return {
|
||||
file: a.path,
|
||||
language: a.language,
|
||||
closedCaption: a.cc
|
||||
};
|
||||
}),
|
||||
simul: data.filter(a => a.type === 'Video').map((a) : boolean => {
|
||||
if (a.type === 'Subtitle')
|
||||
throw new Error('Never');
|
||||
return !a.uncut as boolean;
|
||||
})[0],
|
||||
fonts: Merger.makeFontsList(this.cfg.dir.fonts, data.filter(a => a.type === 'Subtitle') as sxItem[]),
|
||||
videoAndAudio: hasAudioStreams ? [] : data.filter(a => a.type === 'Video').map((a) : MergerInput => {
|
||||
if (a.type === 'Subtitle')
|
||||
throw new Error('Never');
|
||||
return {
|
||||
lang: a.lang,
|
||||
path: a.path,
|
||||
};
|
||||
}),
|
||||
chapters: data.filter(a => a.type === 'Chapters').map((a) : MergerInput => {
|
||||
return {
|
||||
path: a.path,
|
||||
lang: a.lang
|
||||
};
|
||||
}),
|
||||
videoTitle: options.videoTitle,
|
||||
options: {
|
||||
ffmpeg: options.ffmpegOptions,
|
||||
mkvmerge: options.mkvmergeOptions
|
||||
},
|
||||
defaults: {
|
||||
audio: options.defaultAudio,
|
||||
sub: options.defaultSub
|
||||
},
|
||||
ccTag: options.ccTag
|
||||
});
|
||||
const bin = Merger.checkMerger(this.cfg.bin, options.mp4, options.forceMuxer);
|
||||
// collect fonts info
|
||||
// mergers
|
||||
let isMuxed = false;
|
||||
if (options.syncTiming) {
|
||||
await merger.createDelays();
|
||||
}
|
||||
if (bin.MKVmerge) {
|
||||
await merger.merge('mkvmerge', bin.MKVmerge);
|
||||
isMuxed = true;
|
||||
} else if (bin.FFmpeg) {
|
||||
await merger.merge('ffmpeg', bin.FFmpeg);
|
||||
isMuxed = true;
|
||||
} else{
|
||||
console.info('\nDone!\n');
|
||||
return;
|
||||
}
|
||||
if (isMuxed && !options.nocleanup)
|
||||
merger.cleanUp();
|
||||
}
|
||||
|
||||
|
||||
public async getEpisode(data: ADNVideo, options: yargs.ArgvType) {
|
||||
//TODO: Move all the requests for getting the m3u8 here
|
||||
const res = await this.downloadEpisode(data, options);
|
||||
if (res === undefined || res.error) {
|
||||
console.error('Failed to download media list');
|
||||
return { isOk: false, reason: new Error('Failed to download media list') };
|
||||
} else {
|
||||
if (!options.skipmux) {
|
||||
await this.muxStreams(res.data, { ...options, output: res.fileName });
|
||||
} else {
|
||||
console.info('Skipping mux');
|
||||
}
|
||||
downloaded({
|
||||
service: 'adn',
|
||||
type: 's'
|
||||
}, data.id+'', [data.shortNumber]);
|
||||
return { isOk: res, value: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
public async downloadEpisode(data: ADNVideo, options: yargs.ArgvType) {
|
||||
if(!this.token.accessToken){
|
||||
console.error('Authentication required!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.cfg.bin.ffmpeg)
|
||||
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||
|
||||
let mediaName = '...';
|
||||
let fileName;
|
||||
const variables: Variable[] = [];
|
||||
if(data.show.title && data.shortNumber && data.title){
|
||||
mediaName = `${data.show.shortTitle ?? data.show.title} - ${data.shortNumber} - ${data.title}`;
|
||||
}
|
||||
|
||||
const files: DownloadedMedia[] = [];
|
||||
|
||||
let dlFailed = false;
|
||||
let dlVideoOnce = false; // Variable to save if best selected video quality was downloaded
|
||||
|
||||
const refreshToken = await this.refreshToken();
|
||||
if (!refreshToken.isOk) {
|
||||
console.error('Failed to refresh token');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const configReq = await this.req.getData(`https://gw.api.animationdigitalnetwork.fr/player/video/${data.id}/configuration`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token.accessToken}`,
|
||||
'X-Target-Distribution': this.locale
|
||||
}
|
||||
});
|
||||
if(!configReq.ok || !configReq.res){
|
||||
console.error('Player Config Request failed!');
|
||||
return undefined;
|
||||
}
|
||||
const configuration = await configReq.res.json() as ADNPlayerConfig;
|
||||
if (!configuration.player.options.user.hasAccess) {
|
||||
console.error('You don\'t have access to this video!');
|
||||
return undefined;
|
||||
}
|
||||
const tokenReq = await this.req.getData(configuration.player.options.user.refreshTokenUrl || 'https://gw.api.animationdigitalnetwork.fr/player/refresh/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Player-Refresh-Token': `${configuration.player.options.user.refreshToken}`
|
||||
}
|
||||
});
|
||||
if(!tokenReq.ok || !tokenReq.res){
|
||||
console.error('Player Token Request failed!');
|
||||
return undefined;
|
||||
}
|
||||
const token = await tokenReq.res.json() as {
|
||||
refreshToken: string,
|
||||
accessToken: string,
|
||||
token: string
|
||||
};
|
||||
|
||||
const linksUrl = configuration.player.options.video.url || `https://gw.api.animationdigitalnetwork.fr/player/video/${data.id}/link`;
|
||||
const key = this.generateRandomString(16);
|
||||
const decryptionKey = key + '7fac1178830cfe0c';
|
||||
|
||||
const authorization = crypto.publicEncrypt({
|
||||
'key': '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCbQrCJBRmaXM4gJidDmcpWDssg\nnumHinCLHAgS4buMtdH7dEGGEUfBofLzoEdt1jqcrCDT6YNhM0aFCqbLOPFtx9cg\n/X2G/G5bPVu8cuFM0L+ehp8s6izK1kjx3OOPH/kWzvstM5tkqgJkNyNEvHdeJl6\nKhS+IFEqwvZqgbBpKuwIDAQAB\n-----END PUBLIC KEY-----',
|
||||
padding: crypto.constants.RSA_PKCS1_PADDING
|
||||
}, Buffer.from(JSON.stringify({
|
||||
k: key,
|
||||
t: token.token
|
||||
}), 'utf-8')).toString('base64');
|
||||
|
||||
//TODO: Add chapter support
|
||||
const streamsRequest = await this.req.getData(linksUrl+'?freeWithAds=true&adaptive=true&withMetadata=true&source=Web', {
|
||||
'headers': {
|
||||
'X-Player-Token': authorization,
|
||||
'X-Target-Distribution': this.locale
|
||||
}
|
||||
});
|
||||
if(!streamsRequest.ok || !streamsRequest.res){
|
||||
if (streamsRequest.error?.res.status == 403 || streamsRequest.res?.status == 403) {
|
||||
console.error('Georestricted!');
|
||||
} else {
|
||||
console.error('Streams request failed!');
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
const streams = await streamsRequest.res.json() as ADNStreams;
|
||||
for (const streamName in streams.links.streaming) {
|
||||
let audDub: langsData.LanguageItem;
|
||||
if (this.jpnStrings.includes(streamName)) {
|
||||
audDub = langsData.languages.find(a=>a.code == 'jpn') as langsData.LanguageItem;
|
||||
} else if (this.deuStrings.includes(streamName)) {
|
||||
audDub = langsData.languages.find(a=>a.code == 'deu') as langsData.LanguageItem;
|
||||
} else if (this.fraStrings.includes(streamName)) {
|
||||
audDub = langsData.languages.find(a=>a.code == 'fra') as langsData.LanguageItem;
|
||||
} else {
|
||||
console.error(`Language ${streamName} not recognized, please report this.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!options.dubLang.includes(audDub.code)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
console.info(`Requesting: [${data.id}] ${mediaName} (${audDub.name})`);
|
||||
|
||||
variables.push(...([
|
||||
['title', data.title, true],
|
||||
['episode', isNaN(parseFloat(data.shortNumber)) ? data.shortNumber : parseFloat(data.shortNumber), false],
|
||||
['service', 'ADN', false],
|
||||
['seriesTitle', data.show.shortTitle ?? data.show.title, true],
|
||||
['showTitle', data.show.title, true],
|
||||
['season', isNaN(parseFloat(data.season)) ? data.season : parseFloat(data.season), false]
|
||||
] as [AvailableFilenameVars, string|number, boolean][]).map((a): Variable => {
|
||||
return {
|
||||
name: a[0],
|
||||
replaceWith: a[1],
|
||||
type: typeof a[1],
|
||||
sanitize: a[2]
|
||||
} as Variable;
|
||||
}));
|
||||
|
||||
console.info('Playlists URL: %s', streams.links.streaming[streamName].auto);
|
||||
|
||||
let tsFile = undefined;
|
||||
|
||||
if (!dlFailed && !options.novids) {
|
||||
const streamPlaylistsLocationReq = await this.req.getData(streams.links.streaming[streamName].auto);
|
||||
if (!streamPlaylistsLocationReq.ok || !streamPlaylistsLocationReq.res) {
|
||||
console.error('CAN\'T FETCH VIDEO PLAYLIST LOCATION!');
|
||||
return undefined;
|
||||
}
|
||||
const streamPlaylistLocation = await streamPlaylistsLocationReq.res.json() as {'location': string};
|
||||
const streamPlaylistsReq = await this.req.getData(streamPlaylistLocation.location);
|
||||
if (!streamPlaylistsReq.ok || !streamPlaylistsReq.res) {
|
||||
console.error('CAN\'T FETCH VIDEO PLAYLISTS!');
|
||||
dlFailed = true;
|
||||
} else {
|
||||
const streamPlaylistBody = await streamPlaylistsReq.res.text();
|
||||
const streamPlaylists = m3u8(streamPlaylistBody);
|
||||
const plServerList: string[] = [],
|
||||
plStreams: Record<string, Record<string, string>> = {},
|
||||
plQuality: {
|
||||
str: string,
|
||||
dim: string,
|
||||
CODECS: string,
|
||||
RESOLUTION: {
|
||||
width: number,
|
||||
height: number
|
||||
}
|
||||
}[] = [];
|
||||
for(const pl of streamPlaylists.playlists){
|
||||
// set quality
|
||||
const plResolution = pl.attributes.RESOLUTION;
|
||||
const plResolutionText = `${plResolution.width}x${plResolution.height}`;
|
||||
// set codecs
|
||||
const plCodecs = pl.attributes.CODECS;
|
||||
// parse uri
|
||||
const plUri = new URL(pl.uri);
|
||||
let plServer = plUri.hostname;
|
||||
// set server list
|
||||
if (plUri.searchParams.get('cdn')){
|
||||
plServer += ` (${plUri.searchParams.get('cdn')})`;
|
||||
}
|
||||
if (!plServerList.includes(plServer)){
|
||||
plServerList.push(plServer);
|
||||
}
|
||||
// add to server
|
||||
if (!Object.keys(plStreams).includes(plServer)){
|
||||
plStreams[plServer] = {};
|
||||
}
|
||||
if(
|
||||
plStreams[plServer][plResolutionText]
|
||||
&& plStreams[plServer][plResolutionText] != pl.uri
|
||||
&& typeof plStreams[plServer][plResolutionText] != 'undefined'
|
||||
) {
|
||||
console.error(`Non duplicate url for ${plServer} detected, please report to developer!`);
|
||||
} else{
|
||||
plStreams[plServer][plResolutionText] = pl.uri;
|
||||
}
|
||||
// set plQualityStr
|
||||
const plBandwidth = Math.round(pl.attributes.BANDWIDTH/1024);
|
||||
const qualityStrAdd = `${plResolutionText} (${plBandwidth}KiB/s)`;
|
||||
const qualityStrRegx = new RegExp(qualityStrAdd.replace(/([:()/])/g, '\\$1'), 'm');
|
||||
const qualityStrMatch = !plQuality.map(a => a.str).join('\r\n').match(qualityStrRegx);
|
||||
if(qualityStrMatch){
|
||||
plQuality.push({
|
||||
str: qualityStrAdd,
|
||||
dim: plResolutionText,
|
||||
CODECS: plCodecs,
|
||||
RESOLUTION: plResolution
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
options.x = options.x > plServerList.length ? 1 : options.x;
|
||||
|
||||
const plSelectedServer = plServerList[options.x - 1];
|
||||
const plSelectedList = plStreams[plSelectedServer];
|
||||
plQuality.sort((a, b) => {
|
||||
const aMatch: RegExpMatchArray | never[] = a.dim.match(/[0-9]+/) || [];
|
||||
const bMatch: RegExpMatchArray | never[] = b.dim.match(/[0-9]+/) || [];
|
||||
return parseInt(aMatch[0]) - parseInt(bMatch[0]);
|
||||
});
|
||||
let quality = options.q === 0 ? plQuality.length : options.q;
|
||||
if(quality > plQuality.length) {
|
||||
console.warn(`The requested quality of ${options.q} is greater than the maximum ${plQuality.length}.\n[WARN] Therefor the maximum will be capped at ${plQuality.length}.`);
|
||||
quality = plQuality.length;
|
||||
}
|
||||
// When best selected video quality is already downloaded
|
||||
if(dlVideoOnce && options.dlVideoOnce) {
|
||||
// Select the lowest resolution with the same codecs
|
||||
while(quality !=1 && plQuality[quality - 1].CODECS == plQuality[quality - 2].CODECS) {
|
||||
quality--;
|
||||
}
|
||||
}
|
||||
const selPlUrl = plSelectedList[plQuality.map(a => a.dim)[quality - 1]] ? plSelectedList[plQuality.map(a => a.dim)[quality - 1]] : '';
|
||||
console.info(`Servers available:\n\t${plServerList.join('\n\t')}`);
|
||||
console.info(`Available qualities:\n\t${plQuality.map((a, ind) => `[${ind+1}] ${a.str}`).join('\n\t')}`);
|
||||
|
||||
if(selPlUrl != ''){
|
||||
variables.push({
|
||||
name: 'height',
|
||||
type: 'number',
|
||||
replaceWith: quality === 0 ? plQuality[plQuality.length - 1].RESOLUTION.height as number : plQuality[quality - 1].RESOLUTION.height
|
||||
}, {
|
||||
name: 'width',
|
||||
type: 'number',
|
||||
replaceWith: quality === 0 ? plQuality[plQuality.length - 1].RESOLUTION.width as number : plQuality[quality - 1].RESOLUTION.width
|
||||
});
|
||||
|
||||
console.info(`Selected quality: ${Object.keys(plSelectedList).find(a => plSelectedList[a] === selPlUrl)} @ ${plSelectedServer}`);
|
||||
console.info('Stream URL:', selPlUrl);
|
||||
// TODO check filename
|
||||
fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep);
|
||||
const outFile = parseFileName(options.fileName + '.' + audDub.name, variables, options.numbers, options.override).join(path.sep);
|
||||
console.info(`Output filename: ${outFile}`);
|
||||
const chunkPage = await this.req.getData(selPlUrl);
|
||||
if(!chunkPage.ok || !chunkPage.res){
|
||||
console.error('CAN\'T FETCH VIDEO PLAYLIST!');
|
||||
dlFailed = true;
|
||||
} else {
|
||||
const chunkPageBody = await chunkPage.res.text();
|
||||
const chunkPlaylist = m3u8(chunkPageBody);
|
||||
const totalParts = chunkPlaylist.segments.length;
|
||||
const mathParts = Math.ceil(totalParts / options.partsize);
|
||||
const mathMsg = `(${mathParts}*${options.partsize})`;
|
||||
console.info('Total parts in stream:', totalParts, mathMsg);
|
||||
tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile);
|
||||
const split = outFile.split(path.sep).slice(0, -1);
|
||||
split.forEach((val, ind, arr) => {
|
||||
const isAbsolut = path.isAbsolute(outFile as string);
|
||||
if (!fs.existsSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val)))
|
||||
fs.mkdirSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val));
|
||||
});
|
||||
const dlStreamByPl = await new streamdl({
|
||||
output: `${tsFile}.ts`,
|
||||
timeout: options.timeout,
|
||||
m3u8json: chunkPlaylist,
|
||||
baseurl: selPlUrl.replace('playlist.m3u8',''),
|
||||
threads: options.partsize,
|
||||
fsRetryTime: options.fsRetryTime * 1000,
|
||||
override: options.force,
|
||||
callback: options.callbackMaker ? options.callbackMaker({
|
||||
fileName: `${path.isAbsolute(outFile) ? outFile.slice(this.cfg.dir.content.length) : outFile}`,
|
||||
image: data.image,
|
||||
parent: {
|
||||
title: data.show.title
|
||||
},
|
||||
title: data.title,
|
||||
language: audDub
|
||||
}) : undefined
|
||||
}).download();
|
||||
if (!dlStreamByPl.ok) {
|
||||
console.error(`DL Stats: ${JSON.stringify(dlStreamByPl.parts)}\n`);
|
||||
dlFailed = true;
|
||||
}
|
||||
files.push({
|
||||
type: 'Video',
|
||||
path: `${tsFile}.ts`,
|
||||
lang: audDub
|
||||
});
|
||||
dlVideoOnce = true;
|
||||
}
|
||||
} else{
|
||||
console.error('Quality not selected!\n');
|
||||
dlFailed = true;
|
||||
}
|
||||
}
|
||||
} else if (options.novids) {
|
||||
console.info('Downloading skipped!');
|
||||
fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep);
|
||||
}
|
||||
await this.sleep(options.waittime);
|
||||
}
|
||||
|
||||
const compiledChapters: string[] = [];
|
||||
if (options.chapters) {
|
||||
if (streams.video.tcIntroStart) {
|
||||
if (streams.video.tcIntroStart != '00:00:00') {
|
||||
compiledChapters.push(
|
||||
`CHAPTER${(compiledChapters.length/2)+1}=00:00:00.00`,
|
||||
`CHAPTER${(compiledChapters.length/2)+1}NAME=Prologue`
|
||||
);
|
||||
}
|
||||
compiledChapters.push(
|
||||
`CHAPTER${(compiledChapters.length/2)+1}=${streams.video.tcIntroStart+'.00'}`,
|
||||
`CHAPTER${(compiledChapters.length/2)+1}NAME=Opening`
|
||||
);
|
||||
compiledChapters.push(
|
||||
`CHAPTER${(compiledChapters.length/2)+1}=${streams.video.tcIntroEnd+'.00'}`,
|
||||
`CHAPTER${(compiledChapters.length/2)+1}NAME=Episode`
|
||||
);
|
||||
} else {
|
||||
compiledChapters.push(
|
||||
`CHAPTER${(compiledChapters.length/2)+1}=00:00:00.00`,
|
||||
`CHAPTER${(compiledChapters.length/2)+1}NAME=Episode`
|
||||
);
|
||||
}
|
||||
|
||||
if (streams.video.tcEndingStart) {
|
||||
compiledChapters.push(
|
||||
`CHAPTER${(compiledChapters.length/2)+1}=${streams.video.tcEndingStart+'.00'}`,
|
||||
`CHAPTER${(compiledChapters.length/2)+1}NAME=Ending Start`
|
||||
);
|
||||
compiledChapters.push(
|
||||
`CHAPTER${(compiledChapters.length/2)+1}=${streams.video.tcEndingEnd+'.00'}`,
|
||||
`CHAPTER${(compiledChapters.length/2)+1}NAME=Ending End`
|
||||
);
|
||||
}
|
||||
|
||||
if (compiledChapters.length > 0) {
|
||||
try {
|
||||
fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep);
|
||||
const outFile = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep);
|
||||
const tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile);
|
||||
const split = outFile.split(path.sep).slice(0, -1);
|
||||
split.forEach((val, ind, arr) => {
|
||||
const isAbsolut = path.isAbsolute(outFile as string);
|
||||
if (!fs.existsSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val)))
|
||||
fs.mkdirSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val));
|
||||
});
|
||||
fs.writeFileSync(`${tsFile}.txt`, compiledChapters.join('\r\n'));
|
||||
files.push({
|
||||
path: `${tsFile}.txt`,
|
||||
lang: langsData.languages.find(a=>a.code=='jpn'),
|
||||
type: 'Chapters'
|
||||
});
|
||||
} catch {
|
||||
console.error('Failed to write chapter file');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(options.dlsubs.indexOf('all') > -1){
|
||||
options.dlsubs = ['all'];
|
||||
}
|
||||
|
||||
if (options.nosubs) {
|
||||
console.info('Subtitles downloading disabled from nosubs flag.');
|
||||
options.skipsubs = true;
|
||||
}
|
||||
|
||||
if(!options.skipsubs && options.dlsubs.indexOf('none') == -1) {
|
||||
if (Object.keys(streams.links.subtitles).length !== 0) {
|
||||
const subtitlesUrlReq = await this.req.getData(streams.links.subtitles.all);
|
||||
if(!subtitlesUrlReq.ok || !subtitlesUrlReq.res){
|
||||
console.error('Subtitle location request failed!');
|
||||
return undefined;
|
||||
}
|
||||
const subtitleUrl = await subtitlesUrlReq.res.json() as {'location': string};
|
||||
const encryptedSubtitlesReq = await this.req.getData(subtitleUrl.location);
|
||||
if(!encryptedSubtitlesReq.ok || !encryptedSubtitlesReq.res){
|
||||
console.error('Subtitle request failed!');
|
||||
return undefined;
|
||||
}
|
||||
const encryptedSubtitles = await encryptedSubtitlesReq.res.text();
|
||||
const iv = Buffer.from(encryptedSubtitles.slice(0, 24), 'base64');
|
||||
const derivedKey = Buffer.from(decryptionKey, 'hex');
|
||||
const encryptedData = Buffer.from(encryptedSubtitles.slice(24), 'base64');
|
||||
const decipher = crypto.createDecipheriv('aes-128-cbc', derivedKey, iv);
|
||||
const decryptedData = Buffer.concat([decipher.update(encryptedData), decipher.final()]).toString('utf8');
|
||||
|
||||
let subIndex = 0;
|
||||
const subtitles = JSON.parse(decryptedData) as ADNSubtitles;
|
||||
if (Object.keys(subtitles).length === 0) {
|
||||
console.warn('No subtitles found.');
|
||||
}
|
||||
for (const subName in subtitles) {
|
||||
let subLang: langsData.LanguageItem;
|
||||
if (this.deuSubStrings.includes(subName)) {
|
||||
subLang = langsData.languages.find(a=>a.code == 'deu') as langsData.LanguageItem;
|
||||
} else if (this.fraSubStrings.includes(subName)) {
|
||||
subLang = langsData.languages.find(a=>a.code == 'fra') as langsData.LanguageItem;
|
||||
} else {
|
||||
console.error(`Language ${subName} not recognized, please report this.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!options.dlsubs.includes(subLang.locale) && !options.dlsubs.includes('all')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sxData: Partial<sxItem> = {};
|
||||
sxData.file = langsData.subsFile(fileName as string, subIndex+'', subLang, false, options.ccTag);
|
||||
sxData.path = path.join(this.cfg.dir.content, sxData.file);
|
||||
const split = sxData.path.split(path.sep).slice(0, -1);
|
||||
split.forEach((val, ind, arr) => {
|
||||
const isAbsolut = path.isAbsolute(sxData.path as string);
|
||||
if (!fs.existsSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val)))
|
||||
fs.mkdirSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val));
|
||||
});
|
||||
sxData.language = subLang;
|
||||
if(options.dlsubs.includes('all') || options.dlsubs.includes(subLang.locale)) {
|
||||
let subBody = '[Script Info]'
|
||||
+ '\nScriptType:V4.00+'
|
||||
+ '\nWrapStyle: 0'
|
||||
+ '\nPlayResX: 1280'
|
||||
+ '\nPlayResY: 720'
|
||||
+ '\nScaledBorderAndShadow: yes'
|
||||
+ ''
|
||||
+ '\n[V4+ Styles]'
|
||||
+ '\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding'
|
||||
+ `\nStyle: Default,${options.fontName ?? 'Arial'},${options.fontSize ?? 50},&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,-1,0,0,0,100,100,0,0,1,1.95,0,2,0,0,70,0`
|
||||
+ '\n[Events]'
|
||||
+ '\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text';
|
||||
|
||||
for (const sub of subtitles[subName]) {
|
||||
const [start, end, text, lineAlign, positionAlign] =
|
||||
[sub.startTime, sub.endTime, sub.text, sub.lineAlign, sub.positionAlign];
|
||||
for (const subProp in sub) {
|
||||
switch (subProp) {
|
||||
case 'startTime':
|
||||
case 'endTime':
|
||||
case 'text':
|
||||
case 'lineAlign':
|
||||
case 'positionAlign':
|
||||
break;
|
||||
default:
|
||||
console.warn(`json2ass: Unknown style: ${subProp}`);
|
||||
}
|
||||
}
|
||||
const alignment = (this.posAlignMap[positionAlign] || 2) + (this.lineAlignMap[lineAlign] || 0);
|
||||
const xtext = text
|
||||
.replace(/ \\N$/g, '\\N')
|
||||
.replace(/\\N$/, '')
|
||||
.replace(/\r/g, '')
|
||||
.replace(/\n/g, '\\N')
|
||||
.replace(/\\N +/g, '\\N')
|
||||
.replace(/ +\\N/g, '\\N')
|
||||
.replace(/(\\N)+/g, '\\N')
|
||||
.replace(/<b[^>]*>([^<]*)<\/b>/g, '{\\b1}$1{\\b0}')
|
||||
.replace(/<i[^>]*>([^<]*)<\/i>/g, '{\\i1}$1{\\i0}')
|
||||
.replace(/<u[^>]*>([^<]*)<\/u>/g, '{\\u1}$1{\\u0}')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/<[^>]>/g, '')
|
||||
.replace(/\\N$/, '')
|
||||
.replace(/ +$/, '');
|
||||
subBody += `\nDialogue: 0,${this.convertToSSATimestamp(start)},${this.convertToSSATimestamp(end)},Default,,0,0,0,,${(alignment !== 2 ? `{\\a${alignment}}` : '')}${xtext}`;
|
||||
}
|
||||
sxData.title = `${subLang.language}`;
|
||||
sxData.fonts = fontsData.assFonts(subBody) as Font[];
|
||||
fs.writeFileSync(sxData.path, subBody);
|
||||
console.info(`Subtitle converted: ${sxData.file}`);
|
||||
files.push({
|
||||
type: 'Subtitle',
|
||||
...sxData as sxItem,
|
||||
cc: false
|
||||
});
|
||||
}
|
||||
subIndex++;
|
||||
}
|
||||
} else {
|
||||
console.warn('Couldn\'t find subtitles.');
|
||||
}
|
||||
} else{
|
||||
console.info('Subtitles downloading skipped!');
|
||||
}
|
||||
|
||||
return {
|
||||
error: dlFailed,
|
||||
data: files,
|
||||
fileName: fileName ? (path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName)) || './unknown' : './unknown'
|
||||
};
|
||||
}
|
||||
|
||||
public sleep(ms: number) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
}
|
||||
825
ao.ts
Normal file
825
ao.ts
Normal file
|
|
@ -0,0 +1,825 @@
|
|||
// Package Info
|
||||
import packageJson from './package.json';
|
||||
|
||||
// Node
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
// Plugins
|
||||
import shlp from 'sei-helper';
|
||||
|
||||
// Modules
|
||||
import * as fontsData from './modules/module.fontsData';
|
||||
import * as langsData from './modules/module.langsData';
|
||||
import * as yamlCfg from './modules/module.cfg-loader';
|
||||
import * as yargs from './modules/module.app-args';
|
||||
import * as reqModule from './modules/module.fetch';
|
||||
import Merger, { Font, MergerInput, SubtitleInput } from './modules/module.merger';
|
||||
import getKeys, { canDecrypt } from './modules/widevine';
|
||||
import streamdl, { M3U8Json } from './modules/hls-download';
|
||||
import { exec } from './modules/sei-helper-fixes';
|
||||
import { console } from './modules/log';
|
||||
import { domain } from './modules/module.api-urls';
|
||||
import { downloaded } from './modules/module.downloadArchive';
|
||||
import parseSelect from './modules/module.parseSelect';
|
||||
import parseFileName, { Variable } from './modules/module.filename';
|
||||
import { AvailableFilenameVars } from './modules/module.args';
|
||||
import { parse } from './modules/module.transform-mpd';
|
||||
|
||||
// Types
|
||||
import { ServiceClass } from './@types/serviceClassInterface';
|
||||
import { AuthData, AuthResponse, SearchData, SearchResponse, SearchResponseItem } from './@types/messageHandler';
|
||||
import { AOSearchResult, AnimeOnegaiSearch } from './@types/animeOnegaiSearch';
|
||||
import { AnimeOnegaiSeries } from './@types/animeOnegaiSeries';
|
||||
import { AnimeOnegaiSeasons, Episode } from './@types/animeOnegaiSeasons';
|
||||
import { DownloadedMedia } from './@types/hidiveTypes';
|
||||
import { AnimeOnegaiStream } from './@types/animeOnegaiStream';
|
||||
import { sxItem } from './crunchy';
|
||||
|
||||
type parsedMultiDubDownload = {
|
||||
data: {
|
||||
lang: string,
|
||||
videoId: string
|
||||
episode: Episode
|
||||
}[],
|
||||
seriesTitle: string,
|
||||
seasonTitle: string,
|
||||
episodeTitle: string,
|
||||
episodeNumber: number,
|
||||
seasonNumber: number,
|
||||
seriesID: number,
|
||||
seasonID: number,
|
||||
image: string,
|
||||
}
|
||||
|
||||
export default class AnimeOnegai implements ServiceClass {
|
||||
public cfg: yamlCfg.ConfigObject;
|
||||
private token: Record<string, any>;
|
||||
private req: reqModule.Req;
|
||||
public locale: string;
|
||||
public jpnStrings: string[] = [
|
||||
'Japonés con Subtítulos en Español',
|
||||
'Japonés con Subtítulos en Portugués',
|
||||
'Japonês com legendas em espanhol',
|
||||
'Japonês com legendas em português',
|
||||
'Japonés'
|
||||
];
|
||||
public spaStrings: string[] = [
|
||||
'Doblaje en Español',
|
||||
'Dublagem em espanhol',
|
||||
'Español',
|
||||
];
|
||||
public porStrings: string[] = [
|
||||
'Doblaje en Portugués',
|
||||
'Dublagem em português'
|
||||
];
|
||||
private defaultOptions: RequestInit = {
|
||||
'headers': {
|
||||
'origin': 'https://www.animeonegai.com',
|
||||
'referer': 'https://www.animeonegai.com/',
|
||||
}
|
||||
};
|
||||
|
||||
constructor(private debug = false) {
|
||||
this.cfg = yamlCfg.loadCfg();
|
||||
this.token = yamlCfg.loadAOToken();
|
||||
this.req = new reqModule.Req(domain, debug, false, 'ao');
|
||||
this.locale = 'es';
|
||||
}
|
||||
|
||||
public async cli() {
|
||||
console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`);
|
||||
const argv = yargs.appArgv(this.cfg.cli);
|
||||
if (['pt', 'es'].includes(argv.locale))
|
||||
this.locale = argv.locale;
|
||||
if (argv.debug)
|
||||
this.debug = true;
|
||||
|
||||
// load binaries
|
||||
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||
if (argv.allDubs) {
|
||||
argv.dubLang = langsData.dubLanguageCodes;
|
||||
}
|
||||
if (argv.auth) {
|
||||
//Authenticate
|
||||
await this.doAuth({
|
||||
username: argv.username ?? await shlp.question('[Q] LOGIN/EMAIL'),
|
||||
password: argv.password ?? await shlp.question('[Q] PASSWORD ')
|
||||
});
|
||||
} else if (argv.search && argv.search.length > 2) {
|
||||
//Search
|
||||
await this.doSearch({ ...argv, search: argv.search as string });
|
||||
} else if (argv.s && !isNaN(parseInt(argv.s,10)) && parseInt(argv.s,10) > 0) {
|
||||
const selected = await this.selectShow(parseInt(argv.s), argv.e, argv.but, argv.all, argv);
|
||||
if (selected.isOk) {
|
||||
for (const select of selected.value) {
|
||||
if (!(await this.downloadEpisode(select, {...argv, skipsubs: false}))) {
|
||||
console.error(`Unable to download selected episode ${select.episodeNumber}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else if (argv.token) {
|
||||
this.token = {token: argv.token};
|
||||
yamlCfg.saveAOToken(this.token);
|
||||
console.info('Saved token');
|
||||
} else {
|
||||
console.info('No option selected or invalid value entered. Try --help.');
|
||||
}
|
||||
}
|
||||
|
||||
public async doSearch(data: SearchData): Promise<SearchResponse> {
|
||||
const searchReq = await this.req.getData(`https://api.animeonegai.com/v1/search/algolia/${encodeURIComponent(data.search)}?lang=${this.locale}`, this.defaultOptions);
|
||||
if (!searchReq.ok || !searchReq.res) {
|
||||
console.error('Search FAILED!');
|
||||
return { isOk: false, reason: new Error('Search failed. No more information provided') };
|
||||
}
|
||||
const searchData = await searchReq.res.json() as AnimeOnegaiSearch;
|
||||
const searchItems: AOSearchResult[] = [];
|
||||
console.info('Search Results:');
|
||||
for (const hit of searchData.list) {
|
||||
searchItems.push(hit);
|
||||
let fullType: string;
|
||||
if (hit.asset_type == 2) {
|
||||
fullType = `S.${hit.ID}`;
|
||||
} else if (hit.asset_type == 1) {
|
||||
fullType = `E.${hit.ID}`;
|
||||
} else {
|
||||
fullType = 'Unknown';
|
||||
console.warn(`Unknown asset type ${hit.asset_type}, please report this.`);
|
||||
}
|
||||
console.log(`[${fullType}] ${hit.title}`);
|
||||
}
|
||||
return { isOk: true, value: searchItems.filter(a => a.asset_type == 2).flatMap((a): SearchResponseItem => {
|
||||
return {
|
||||
id: a.ID+'',
|
||||
image: a.poster ?? '/notFound.png',
|
||||
name: a.title,
|
||||
rating: a.likes,
|
||||
desc: a.description
|
||||
};
|
||||
})};
|
||||
}
|
||||
|
||||
public async doAuth(data: AuthData): Promise<AuthResponse> {
|
||||
data;
|
||||
console.error('Authentication not possible, manual authentication required due to recaptcha. In order to login use the --token flag. You can get the token by logging into the website, and opening the dev console and running the command "localStorage.ott_token"');
|
||||
return { isOk: false, reason: new Error('Authentication not possible, manual authentication required do to recaptcha.') };
|
||||
}
|
||||
|
||||
public async getShow(id: number) {
|
||||
const getSeriesData = await this.req.getData(`https://api.animeonegai.com/v1/asset/${id}?lang=${this.locale}`, this.defaultOptions);
|
||||
if (!getSeriesData.ok || !getSeriesData.res) {
|
||||
console.error('Failed to get Show Data');
|
||||
return { isOk: false };
|
||||
}
|
||||
const seriesData = await getSeriesData.res.json() as AnimeOnegaiSeries;
|
||||
|
||||
const getSeasonData = await this.req.getData(`https://api.animeonegai.com/v1/asset/content/${id}?lang=${this.locale}`, this.defaultOptions);
|
||||
if (!getSeasonData.ok || !getSeasonData.res) {
|
||||
console.error('Failed to get Show Data');
|
||||
return { isOk: false };
|
||||
}
|
||||
const seasonData = await getSeasonData.res.json() as AnimeOnegaiSeasons[];
|
||||
|
||||
return { isOk: true, data: seriesData, seasons: seasonData };
|
||||
}
|
||||
|
||||
public async listShow(id: number, outputEpisode: boolean = true) {
|
||||
const series = await this.getShow(id);
|
||||
if (!series.isOk || !series.data) {
|
||||
console.error('Failed to list series data: Failed to get series');
|
||||
return { isOk: false };
|
||||
}
|
||||
console.info(`[S.${series.data.ID}] ${series.data.title} (${series.seasons.length} Seasons)`);
|
||||
if (series.seasons.length === 0 && series.data.asset_type !== 1) {
|
||||
console.info(' No Seasons found!');
|
||||
return { isOk: false };
|
||||
}
|
||||
const episodes: { [key: string]: (Episode & { lang?: string })[] } = {};
|
||||
for (const season of series.seasons) {
|
||||
let lang: string | undefined = undefined;
|
||||
if (this.jpnStrings.includes(season.name.trim())) lang = 'ja';
|
||||
else if (this.porStrings.includes(season.name.trim())) lang = 'pt';
|
||||
else if (this.spaStrings.includes(season.name.trim())) lang = 'es';
|
||||
else {lang = 'unknown';console.error(`Language (${season.name.trim()}) not known, please report this!`);}
|
||||
for (const episode of season.list) {
|
||||
if (!episodes[episode.number]) {
|
||||
episodes[episode.number] = [];
|
||||
}
|
||||
/*if (!episodes[episode.number].find(a=>a.lang == lang))*/ episodes[episode.number].push({...episode, lang});
|
||||
}
|
||||
}
|
||||
//Item is movie, lets define it manually
|
||||
if (series.data.asset_type === 1 && series.seasons.length === 0) {
|
||||
let lang: string | undefined;
|
||||
if (this.jpnStrings.some(str => series.data.title.includes(str))) lang = 'ja';
|
||||
else if (this.porStrings.some(str => series.data.title.includes(str))) lang = 'pt';
|
||||
else if (this.spaStrings.some(str => series.data.title.includes(str))) lang = 'es';
|
||||
else {lang = 'unknown';console.error('Language could not be parsed from movie title, please report this!');}
|
||||
episodes[1] = [{
|
||||
'video_entry': series.data.video_entry,
|
||||
'number': 1,
|
||||
'season_id': 1,
|
||||
'name': series.data.title,
|
||||
'ID': series.data.ID,
|
||||
'CreatedAt': series.data.CreatedAt,
|
||||
'DeletedAt': series.data.DeletedAt,
|
||||
'UpdatedAt': series.data.UpdatedAt,
|
||||
'active': series.data.active,
|
||||
'description': series.data.description,
|
||||
'age_restriction': series.data.age_restriction,
|
||||
'asset_id': series.data.ID,
|
||||
'ending': null,
|
||||
'entry': series.data.entry,
|
||||
'stream_url': series.data.stream_url,
|
||||
'skip_intro': null,
|
||||
'thumbnail': series.data.bg,
|
||||
'open_free': false,
|
||||
lang
|
||||
}]; // as unknown as (Episode & { lang?: string })[];
|
||||
// The above needs to be uncommented if the episode number should be M1 instead of 1
|
||||
}
|
||||
//Enable to output episodes seperate from selection
|
||||
if (outputEpisode) {
|
||||
for (const episodeKey in episodes) {
|
||||
const episode = episodes[episodeKey][0];
|
||||
const langs = Array.from(new Set(episodes[episodeKey].map(a=>a.lang)));
|
||||
console.info(` [E.${episode.ID}] E${episode.number} - ${episode.name} (${langs.map(a=>{
|
||||
if (a) return langsData.languages.find(b=>b.ao_locale === a)?.name;
|
||||
return 'Unknown';
|
||||
}).join(', ')})`);
|
||||
}
|
||||
}
|
||||
return { isOk: true, value: episodes, series: series };
|
||||
}
|
||||
|
||||
public async selectShow(id: number, e: string | undefined, but: boolean, all: boolean, options: yargs.ArgvType) {
|
||||
const getShowData = await this.listShow(id, false);
|
||||
if (!getShowData.isOk || !getShowData.value) {
|
||||
return { isOk: false, value: [] };
|
||||
}
|
||||
//const showData = getShowData.value;
|
||||
const doEpsFilter = parseSelect(e as string);
|
||||
// build selected episodes
|
||||
const selEpsArr: parsedMultiDubDownload[] = [];
|
||||
const episodes = getShowData.value;
|
||||
const seasonNumberTitleParse = getShowData.series.data.title.match(/\d+/);
|
||||
const seasonNumber = seasonNumberTitleParse ? parseInt(seasonNumberTitleParse[0]) : 1;
|
||||
for (const episodeKey in getShowData.value) {
|
||||
const episode = episodes[episodeKey][0];
|
||||
const selectedLangs: string[] = [];
|
||||
const selected: {
|
||||
lang: string,
|
||||
videoId: string
|
||||
episode: Episode
|
||||
}[] = [];
|
||||
for (const episode of episodes[episodeKey]) {
|
||||
const lang = langsData.languages.find(a=>a.ao_locale === episode.lang);
|
||||
let isSelected = false;
|
||||
if (typeof selected.find(a=>a.lang == episode.lang) == 'undefined') {
|
||||
if (options.dubLang.includes(lang?.code ?? 'Unknown')) {
|
||||
if ((but && !doEpsFilter.isSelected([episode.number+'', episode.ID+''])) || all || (!but && doEpsFilter.isSelected([episode.number+'', episode.ID+'']))) {
|
||||
isSelected = true;
|
||||
selected.push({lang: episode.lang as string, videoId: episode.video_entry, episode: episode });
|
||||
}
|
||||
}
|
||||
const selectedLang = isSelected ? `✓ ${lang?.name ?? 'Unknown'}` : `${lang?.name ?? 'Unknown'}`;
|
||||
if (!selectedLangs.includes(selectedLang)) {
|
||||
selectedLangs.push(selectedLang);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (selected.length > 0) {
|
||||
selEpsArr.push({
|
||||
'data': selected,
|
||||
'seasonNumber': seasonNumber,
|
||||
'episodeNumber': episode.number,
|
||||
'episodeTitle': episode.name,
|
||||
'image': episode.thumbnail,
|
||||
'seasonID': episode.season_id,
|
||||
'seasonTitle': getShowData.series.data.title,
|
||||
'seriesTitle': getShowData.series.data.title,
|
||||
'seriesID': getShowData.series.data.ID
|
||||
});
|
||||
}
|
||||
console.info(` [S${seasonNumber}E${episode.number}] - ${episode.name} (${selectedLangs.join(', ')})`);
|
||||
}
|
||||
return { isOk: true, value: selEpsArr, showData: getShowData.series };
|
||||
}
|
||||
|
||||
public async downloadEpisode(data: parsedMultiDubDownload, options: yargs.ArgvType): Promise<boolean> {
|
||||
const res = await this.downloadMediaList(data, options);
|
||||
if (res === undefined || res.error) {
|
||||
return false;
|
||||
} else {
|
||||
if (!options.skipmux) {
|
||||
await this.muxStreams(res.data, { ...options, output: res.fileName });
|
||||
} else {
|
||||
console.info('Skipping mux');
|
||||
}
|
||||
downloaded({
|
||||
service: 'ao',
|
||||
type: 's'
|
||||
}, data.seasonID+'', [data.episodeNumber+'']);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public async muxStreams(data: DownloadedMedia[], options: yargs.ArgvType) {
|
||||
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||
let hasAudioStreams = false;
|
||||
if (options.novids || data.filter(a => a.type === 'Video').length === 0)
|
||||
return console.info('Skip muxing since no vids are downloaded');
|
||||
if (data.some(a => a.type === 'Audio')) {
|
||||
hasAudioStreams = true;
|
||||
}
|
||||
const merger = new Merger({
|
||||
onlyVid: hasAudioStreams ? data.filter(a => a.type === 'Video').map((a) : MergerInput => {
|
||||
if (a.type === 'Subtitle')
|
||||
throw new Error('Never');
|
||||
return {
|
||||
lang: a.lang,
|
||||
path: a.path,
|
||||
};
|
||||
}) : [],
|
||||
skipSubMux: options.skipSubMux,
|
||||
inverseTrackOrder: false,
|
||||
keepAllVideos: options.keepAllVideos,
|
||||
onlyAudio: hasAudioStreams ? data.filter(a => a.type === 'Audio').map((a) : MergerInput => {
|
||||
if (a.type === 'Subtitle')
|
||||
throw new Error('Never');
|
||||
return {
|
||||
lang: a.lang,
|
||||
path: a.path,
|
||||
};
|
||||
}) : [],
|
||||
output: `${options.output}.${options.mp4 ? 'mp4' : 'mkv'}`,
|
||||
subtitles: data.filter(a => a.type === 'Subtitle').map((a) : SubtitleInput => {
|
||||
if (a.type === 'Video')
|
||||
throw new Error('Never');
|
||||
if (a.type === 'Audio')
|
||||
throw new Error('Never');
|
||||
return {
|
||||
file: a.path,
|
||||
language: a.language,
|
||||
closedCaption: a.cc
|
||||
};
|
||||
}),
|
||||
simul: data.filter(a => a.type === 'Video').map((a) : boolean => {
|
||||
if (a.type === 'Subtitle')
|
||||
throw new Error('Never');
|
||||
return !a.uncut as boolean;
|
||||
})[0],
|
||||
fonts: Merger.makeFontsList(this.cfg.dir.fonts, data.filter(a => a.type === 'Subtitle') as sxItem[]),
|
||||
videoAndAudio: hasAudioStreams ? [] : data.filter(a => a.type === 'Video').map((a) : MergerInput => {
|
||||
if (a.type === 'Subtitle')
|
||||
throw new Error('Never');
|
||||
return {
|
||||
lang: a.lang,
|
||||
path: a.path,
|
||||
};
|
||||
}),
|
||||
videoTitle: options.videoTitle,
|
||||
options: {
|
||||
ffmpeg: options.ffmpegOptions,
|
||||
mkvmerge: options.mkvmergeOptions
|
||||
},
|
||||
defaults: {
|
||||
audio: options.defaultAudio,
|
||||
sub: options.defaultSub
|
||||
},
|
||||
ccTag: options.ccTag
|
||||
});
|
||||
const bin = Merger.checkMerger(this.cfg.bin, options.mp4, options.forceMuxer);
|
||||
// collect fonts info
|
||||
// mergers
|
||||
let isMuxed = false;
|
||||
if (options.syncTiming) {
|
||||
await merger.createDelays();
|
||||
}
|
||||
if (bin.MKVmerge) {
|
||||
await merger.merge('mkvmerge', bin.MKVmerge);
|
||||
isMuxed = true;
|
||||
} else if (bin.FFmpeg) {
|
||||
await merger.merge('ffmpeg', bin.FFmpeg);
|
||||
isMuxed = true;
|
||||
} else{
|
||||
console.info('\nDone!\n');
|
||||
return;
|
||||
}
|
||||
if (isMuxed && !options.nocleanup)
|
||||
merger.cleanUp();
|
||||
}
|
||||
|
||||
public async downloadMediaList(medias: parsedMultiDubDownload, options: yargs.ArgvType) : Promise<{
|
||||
data: DownloadedMedia[],
|
||||
fileName: string,
|
||||
error: boolean
|
||||
} | undefined> {
|
||||
if(!this.token.token){
|
||||
console.error('Authentication required!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.cfg.bin.ffmpeg)
|
||||
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||
|
||||
let mediaName = '...';
|
||||
let fileName;
|
||||
const variables: Variable[] = [];
|
||||
if(medias.seasonTitle && medias.episodeNumber && medias.episodeTitle){
|
||||
mediaName = `${medias.seasonTitle} - ${medias.episodeNumber} - ${medias.episodeTitle}`;
|
||||
}
|
||||
|
||||
const files: DownloadedMedia[] = [];
|
||||
|
||||
let subIndex = 0;
|
||||
let dlFailed = false;
|
||||
let dlVideoOnce = false; // Variable to save if best selected video quality was downloaded
|
||||
|
||||
for (const media of medias.data) {
|
||||
console.info(`Requesting: [E.${media.episode.ID}] ${mediaName}`);
|
||||
|
||||
const AuthHeaders = {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token.token}`,
|
||||
'Referer': 'https://www.animeonegai.com/',
|
||||
'Origin': 'https://www.animeonegai.com',
|
||||
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
const playbackReq = await this.req.getData(`https://api.animeonegai.com/v1/media/${media.videoId}?lang=${this.locale}`, AuthHeaders);
|
||||
if(!playbackReq.ok || !playbackReq.res){
|
||||
console.error('Request Stream URLs FAILED!');
|
||||
return undefined;
|
||||
}
|
||||
const streamData = await playbackReq.res.json() as AnimeOnegaiStream;
|
||||
|
||||
variables.push(...([
|
||||
['title', medias.episodeTitle, true],
|
||||
['episode', medias.episodeNumber, false],
|
||||
['service', 'AO', false],
|
||||
['seriesTitle', medias.seriesTitle, true],
|
||||
['showTitle', medias.seasonTitle, true],
|
||||
['season', medias.seasonNumber, false]
|
||||
] as [AvailableFilenameVars, string|number, boolean][]).map((a): Variable => {
|
||||
return {
|
||||
name: a[0],
|
||||
replaceWith: a[1],
|
||||
type: typeof a[1],
|
||||
sanitize: a[2]
|
||||
} as Variable;
|
||||
}));
|
||||
|
||||
if (!canDecrypt) {
|
||||
console.warn('Decryption not enabled!');
|
||||
}
|
||||
|
||||
const lang = langsData.languages.find(a=>a.ao_locale == media.lang) as langsData.LanguageItem;
|
||||
if (!lang) {
|
||||
console.error(`Unable to find language for code ${media.lang}`);
|
||||
return;
|
||||
}
|
||||
let tsFile = undefined;
|
||||
|
||||
if (!streamData.dash) {
|
||||
console.error('You don\'t have access to download this content');
|
||||
continue;
|
||||
}
|
||||
|
||||
console.info('Playlists URL: %s', streamData.dash);
|
||||
|
||||
if(!dlFailed && !(options.novids && options.noaudio)){
|
||||
const streamPlaylistsReq = await this.req.getData(streamData.dash, AuthHeaders);
|
||||
if(!streamPlaylistsReq.ok || !streamPlaylistsReq.res){
|
||||
console.error('CAN\'T FETCH VIDEO PLAYLISTS!');
|
||||
dlFailed = true;
|
||||
} else {
|
||||
const streamPlaylistBody = (await streamPlaylistsReq.res.text()).replace(/<BaseURL>(.*?)<\/BaseURL>/g, `<BaseURL>${streamData.dash.split('/dash/')[0]}/dash/$1</BaseURL>`);
|
||||
//Parse MPD Playlists
|
||||
const streamPlaylists = await parse(streamPlaylistBody, lang as langsData.LanguageItem, streamData.dash.split('/dash/')[0]+'/dash/');
|
||||
|
||||
//Get name of CDNs/Servers
|
||||
const streamServers = Object.keys(streamPlaylists);
|
||||
|
||||
options.x = options.x > streamServers.length ? 1 : options.x;
|
||||
|
||||
const selectedServer = streamServers[options.x - 1];
|
||||
const selectedList = streamPlaylists[selectedServer];
|
||||
|
||||
//set Video Qualities
|
||||
const videos = selectedList.video.map(item => {
|
||||
return {
|
||||
...item,
|
||||
resolutionText: `${item.quality.width}x${item.quality.height} (${Math.round(item.bandwidth/1024)}KiB/s)`
|
||||
};
|
||||
});
|
||||
|
||||
const audios = selectedList.audio.map(item => {
|
||||
return {
|
||||
...item,
|
||||
resolutionText: `${Math.round(item.bandwidth/1024)}kB/s`
|
||||
};
|
||||
});
|
||||
|
||||
videos.sort((a, b) => {
|
||||
return a.quality.width - b.quality.width;
|
||||
});
|
||||
|
||||
audios.sort((a, b) => {
|
||||
return a.bandwidth - b.bandwidth;
|
||||
});
|
||||
|
||||
let chosenVideoQuality = options.q === 0 ? videos.length : options.q;
|
||||
if(chosenVideoQuality > videos.length) {
|
||||
console.warn(`The requested quality of ${options.q} is greater than the maximum ${videos.length}.\n[WARN] Therefor the maximum will be capped at ${videos.length}.`);
|
||||
chosenVideoQuality = videos.length;
|
||||
}
|
||||
chosenVideoQuality--;
|
||||
|
||||
let chosenAudioQuality = options.q === 0 ? audios.length : options.q;
|
||||
if(chosenAudioQuality > audios.length) {
|
||||
chosenAudioQuality = audios.length;
|
||||
}
|
||||
chosenAudioQuality--;
|
||||
|
||||
const chosenVideoSegments = videos[chosenVideoQuality];
|
||||
const chosenAudioSegments = audios[chosenAudioQuality];
|
||||
|
||||
console.info(`Servers available:\n\t${streamServers.join('\n\t')}`);
|
||||
console.info(`Available Video Qualities:\n\t${videos.map((a, ind) => `[${ind+1}] ${a.resolutionText}`).join('\n\t')}`);
|
||||
console.info(`Available Audio Qualities:\n\t${audios.map((a, ind) => `[${ind+1}] ${a.resolutionText}`).join('\n\t')}`);
|
||||
|
||||
variables.push({
|
||||
name: 'height',
|
||||
type: 'number',
|
||||
replaceWith: chosenVideoSegments.quality.height
|
||||
}, {
|
||||
name: 'width',
|
||||
type: 'number',
|
||||
replaceWith: chosenVideoSegments.quality.width
|
||||
});
|
||||
|
||||
console.info(`Selected quality: \n\tVideo: ${chosenVideoSegments.resolutionText}\n\tAudio: ${chosenAudioSegments.resolutionText}\n\tServer: ${selectedServer}`);
|
||||
//console.info('Stream URL:', chosenVideoSegments.segments[0].uri);
|
||||
// TODO check filename
|
||||
fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep);
|
||||
const outFile = parseFileName(options.fileName + '.' + lang.name, variables, options.numbers, options.override).join(path.sep);
|
||||
const tempFile = parseFileName(`temp-${media.videoId}`, variables, options.numbers, options.override).join(path.sep);
|
||||
const tempTsFile = path.isAbsolute(tempFile as string) ? tempFile : path.join(this.cfg.dir.content, tempFile);
|
||||
|
||||
let [audioDownloaded, videoDownloaded] = [false, false];
|
||||
|
||||
// When best selected video quality is already downloaded
|
||||
if(dlVideoOnce && options.dlVideoOnce) {
|
||||
console.info('Already downloaded video, skipping video download...');
|
||||
} else if (options.novids) {
|
||||
console.info('Skipping video download...');
|
||||
} else {
|
||||
//Download Video
|
||||
const totalParts = chosenVideoSegments.segments.length;
|
||||
const mathParts = Math.ceil(totalParts / options.partsize);
|
||||
const mathMsg = `(${mathParts}*${options.partsize})`;
|
||||
console.info('Total parts in video stream:', totalParts, mathMsg);
|
||||
tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile);
|
||||
const split = outFile.split(path.sep).slice(0, -1);
|
||||
split.forEach((val, ind, arr) => {
|
||||
const isAbsolut = path.isAbsolute(outFile as string);
|
||||
if (!fs.existsSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val)))
|
||||
fs.mkdirSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val));
|
||||
});
|
||||
const videoJson: M3U8Json = {
|
||||
segments: chosenVideoSegments.segments
|
||||
};
|
||||
try {
|
||||
const videoDownload = await new streamdl({
|
||||
output: chosenVideoSegments.pssh ? `${tempTsFile}.video.enc.mp4` : `${tsFile}.video.mp4`,
|
||||
timeout: options.timeout,
|
||||
m3u8json: videoJson,
|
||||
// baseurl: chunkPlaylist.baseUrl,
|
||||
threads: options.partsize,
|
||||
fsRetryTime: options.fsRetryTime * 1000,
|
||||
override: options.force,
|
||||
callback: options.callbackMaker ? options.callbackMaker({
|
||||
fileName: `${path.isAbsolute(outFile) ? outFile.slice(this.cfg.dir.content.length) : outFile}`,
|
||||
image: medias.image,
|
||||
parent: {
|
||||
title: medias.seasonTitle
|
||||
},
|
||||
title: medias.episodeTitle,
|
||||
language: lang
|
||||
}) : undefined
|
||||
}).download();
|
||||
if(!videoDownload.ok){
|
||||
console.error(`DL Stats: ${JSON.stringify(videoDownload.parts)}\n`);
|
||||
dlFailed = true;
|
||||
} else {
|
||||
dlVideoOnce = true;
|
||||
videoDownloaded = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
dlFailed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (chosenAudioSegments && !options.noaudio) {
|
||||
//Download Audio (if available)
|
||||
const totalParts = chosenAudioSegments.segments.length;
|
||||
const mathParts = Math.ceil(totalParts / options.partsize);
|
||||
const mathMsg = `(${mathParts}*${options.partsize})`;
|
||||
console.info('Total parts in audio stream:', totalParts, mathMsg);
|
||||
tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile);
|
||||
const split = outFile.split(path.sep).slice(0, -1);
|
||||
split.forEach((val, ind, arr) => {
|
||||
const isAbsolut = path.isAbsolute(outFile as string);
|
||||
if (!fs.existsSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val)))
|
||||
fs.mkdirSync(path.join(isAbsolut ? '' : this.cfg.dir.content, ...arr.slice(0, ind), val));
|
||||
});
|
||||
const audioJson: M3U8Json = {
|
||||
segments: chosenAudioSegments.segments
|
||||
};
|
||||
try {
|
||||
const audioDownload = await new streamdl({
|
||||
output: chosenAudioSegments.pssh ? `${tempTsFile}.audio.enc.mp4` : `${tsFile}.audio.mp4`,
|
||||
timeout: options.timeout,
|
||||
m3u8json: audioJson,
|
||||
// baseurl: chunkPlaylist.baseUrl,
|
||||
threads: options.partsize,
|
||||
fsRetryTime: options.fsRetryTime * 1000,
|
||||
override: options.force,
|
||||
callback: options.callbackMaker ? options.callbackMaker({
|
||||
fileName: `${path.isAbsolute(outFile) ? outFile.slice(this.cfg.dir.content.length) : outFile}`,
|
||||
image: medias.image,
|
||||
parent: {
|
||||
title: medias.seasonTitle
|
||||
},
|
||||
title: medias.episodeTitle,
|
||||
language: lang
|
||||
}) : undefined
|
||||
}).download();
|
||||
if(!audioDownload.ok){
|
||||
console.error(`DL Stats: ${JSON.stringify(audioDownload.parts)}\n`);
|
||||
dlFailed = true;
|
||||
} else {
|
||||
audioDownloaded = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
dlFailed = true;
|
||||
}
|
||||
} else if (options.noaudio) {
|
||||
console.info('Skipping audio download...');
|
||||
}
|
||||
|
||||
//Handle Decryption if needed
|
||||
if ((chosenVideoSegments.pssh || chosenAudioSegments.pssh) && (videoDownloaded || audioDownloaded)) {
|
||||
console.info('Decryption Needed, attempting to decrypt');
|
||||
const encryptionKeys = await getKeys(chosenVideoSegments.pssh, streamData.widevine_proxy, {});
|
||||
if (encryptionKeys.length == 0) {
|
||||
console.error('Failed to get encryption keys');
|
||||
return undefined;
|
||||
}
|
||||
/*const keys = {} as Record<string, string>;
|
||||
encryptionKeys.forEach(function(key) {
|
||||
keys[key.kid] = key.key;
|
||||
});*/
|
||||
|
||||
if (this.cfg.bin.mp4decrypt) {
|
||||
const commandBase = `--show-progress --key ${encryptionKeys[1].kid}:${encryptionKeys[1].key} `;
|
||||
const commandVideo = commandBase+`"${tempTsFile}.video.enc.mp4" "${tempTsFile}.video.mp4"`;
|
||||
const commandAudio = commandBase+`"${tempTsFile}.audio.enc.mp4" "${tempTsFile}.audio.mp4"`;
|
||||
|
||||
if (videoDownloaded) {
|
||||
console.info('Started decrypting video');
|
||||
const decryptVideo = exec('mp4decrypt', `"${this.cfg.bin.mp4decrypt}"`, commandVideo);
|
||||
if (!decryptVideo.isOk) {
|
||||
console.error(decryptVideo.err);
|
||||
console.error(`Decryption failed with exit code ${decryptVideo.err.code}`);
|
||||
fs.renameSync(`${tempTsFile}.video.enc.mp4`, `${tsFile}.video.enc.mp4`);
|
||||
return undefined;
|
||||
} else {
|
||||
console.info('Decryption done for video');
|
||||
if (!options.nocleanup) {
|
||||
fs.removeSync(`${tempTsFile}.video.enc.mp4`);
|
||||
}
|
||||
fs.renameSync(`${tempTsFile}.video.mp4`, `${tsFile}.video.mp4`);
|
||||
files.push({
|
||||
type: 'Video',
|
||||
path: `${tsFile}.video.mp4`,
|
||||
lang: lang
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (audioDownloaded) {
|
||||
console.info('Started decrypting audio');
|
||||
const decryptAudio = exec('mp4decrypt', `"${this.cfg.bin.mp4decrypt}"`, commandAudio);
|
||||
if (!decryptAudio.isOk) {
|
||||
console.error(decryptAudio.err);
|
||||
console.error(`Decryption failed with exit code ${decryptAudio.err.code}`);
|
||||
fs.renameSync(`${tempTsFile}.audio.enc.mp4`, `${tsFile}.audio.enc.mp4`);
|
||||
return undefined;
|
||||
} else {
|
||||
if (!options.nocleanup) {
|
||||
fs.removeSync(`${tempTsFile}.audio.enc.mp4`);
|
||||
}
|
||||
fs.renameSync(`${tempTsFile}.audio.mp4`, `${tsFile}.audio.mp4`);
|
||||
files.push({
|
||||
type: 'Audio',
|
||||
path: `${tsFile}.audio.mp4`,
|
||||
lang: lang
|
||||
});
|
||||
console.info('Decryption done for audio');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn('mp4decrypt not found, files need decryption. Decryption Keys:', encryptionKeys);
|
||||
}
|
||||
} else {
|
||||
if (videoDownloaded) {
|
||||
files.push({
|
||||
type: 'Video',
|
||||
path: `${tsFile}.video.mp4`,
|
||||
lang: lang
|
||||
});
|
||||
}
|
||||
if (audioDownloaded) {
|
||||
files.push({
|
||||
type: 'Audio',
|
||||
path: `${tsFile}.audio.mp4`,
|
||||
lang: lang
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (options.novids && options.noaudio) {
|
||||
fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep);
|
||||
}
|
||||
|
||||
if(options.dlsubs.indexOf('all') > -1){
|
||||
options.dlsubs = ['all'];
|
||||
}
|
||||
|
||||
if (options.nosubs) {
|
||||
console.info('Subtitles downloading disabled from nosubs flag.');
|
||||
options.skipsubs = true;
|
||||
}
|
||||
|
||||
if (!options.skipsubs && options.dlsubs.indexOf('none') == -1) {
|
||||
if(streamData.subtitles.length > 0) {
|
||||
for(const sub of streamData.subtitles) {
|
||||
const subLang = langsData.languages.find(a => a.ao_locale === sub.lang);
|
||||
if (!subLang) {
|
||||
console.warn(`Language not found for subtitle language: ${sub.lang}, Skipping`);
|
||||
continue;
|
||||
}
|
||||
const sxData: Partial<sxItem> = {};
|
||||
sxData.file = langsData.subsFile(fileName as string, subIndex+'', subLang, false, options.ccTag);
|
||||
sxData.path = path.join(this.cfg.dir.content, sxData.file);
|
||||
sxData.language = subLang;
|
||||
if((options.dlsubs.includes('all') || options.dlsubs.includes(subLang.locale)) && sub.url.includes('.ass')) {
|
||||
const getSubtitle = await this.req.getData(sub.url, AuthHeaders);
|
||||
if (getSubtitle.ok && getSubtitle.res) {
|
||||
console.info(`Subtitle Downloaded: ${sub.url}`);
|
||||
const sBody = await getSubtitle.res.text();
|
||||
sxData.title = `${subLang.language}`;
|
||||
sxData.fonts = fontsData.assFonts(sBody) as Font[];
|
||||
fs.writeFileSync(sxData.path, sBody);
|
||||
files.push({
|
||||
type: 'Subtitle',
|
||||
...sxData as sxItem,
|
||||
cc: false
|
||||
});
|
||||
} else{
|
||||
console.warn(`Failed to download subtitle: ${sxData.file}`);
|
||||
}
|
||||
}
|
||||
subIndex++;
|
||||
}
|
||||
} else{
|
||||
console.warn('Can\'t find urls for subtitles!');
|
||||
}
|
||||
}
|
||||
else{
|
||||
console.info('Subtitles downloading skipped!');
|
||||
}
|
||||
await this.sleep(options.waittime);
|
||||
}
|
||||
return {
|
||||
error: dlFailed,
|
||||
data: files,
|
||||
fileName: fileName ? (path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName)) || './unknown' : './unknown'
|
||||
};
|
||||
}
|
||||
|
||||
public sleep(ms: number) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,24 @@
|
|||
# Set the quality of the stream, 0 is highest available.
|
||||
q: 0
|
||||
nServer: 1
|
||||
mp4mux: false
|
||||
noCleanUp: false
|
||||
dlVideoOnce: false
|
||||
# Set which stream to use
|
||||
kstream: 1
|
||||
# Set which server to use
|
||||
server: 1
|
||||
# How many parts to download at once. Increasing may improve download speed.
|
||||
partsize: 10
|
||||
# Set whether to mux into an mp4 or not. Not recommended.
|
||||
mp4: false
|
||||
# Whether to delete any created files or not
|
||||
nocleanup: false
|
||||
# Whether to only download the relevant video once
|
||||
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]"
|
||||
# What Audio languages to download
|
||||
dubLang: ["jpn"]
|
||||
# What Subtitle languages to download
|
||||
dlsubs: ["all"]
|
||||
# What language Audio to set as default
|
||||
defaultAudio: "jpn"
|
||||
514
crunchy.ts
514
crunchy.ts
File diff suppressed because it is too large
Load diff
|
|
@ -1,10 +1,10 @@
|
|||
# multi-downloader-nx (4.4.2v)
|
||||
# multi-downloader-nx (5.1.5v)
|
||||
|
||||
If you find any bugs in this documentation or in the programm itself please report it [over on GitHub](https://github.com/anidl/multi-downloader-nx/issues).
|
||||
If you find any bugs in this documentation or in the program itself please report it [over on GitHub](https://github.com/anidl/multi-downloader-nx/issues).
|
||||
|
||||
## Legal Warning
|
||||
|
||||
This application is not endorsed by or affiliated with *Funimation* or *Crunchyroll*.
|
||||
This application is not endorsed by or affiliated with *Crunchyroll*, *Hidive*, *AnimeOnegai*, or *AnimationDigitalNetwork*.
|
||||
This application enables you to download videos for offline viewing which may be forbidden by law in your country.
|
||||
The usage of this application may also cause a violation of the *Terms of Service* between you and the stream provider.
|
||||
This tool is not responsible for your actions; please make an informed decision before using this application.
|
||||
|
|
@ -38,9 +38,15 @@ Set the password to use for the authentication. If not provided, you will be pro
|
|||
#### `--silentAuth`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | --- | ---|
|
||||
| Funimation, Crunchyroll | `--silentAuth ` | `boolean` | `No`| `NaN` | `false`| `silentAuth: ` |
|
||||
| Crunchyroll | `--silentAuth ` | `boolean` | `No`| `NaN` | `false`| `silentAuth: ` |
|
||||
|
||||
Authenticate every time the script runs. Use at your own risk.
|
||||
#### `--token`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | --- | ---|
|
||||
| Crunchyroll, AnimeOnegai | `--token ${token}` | `string` | `No`| `NaN` | `undefined`| `token: ` |
|
||||
|
||||
Allows you to login with your token (Example on crunchy is Refresh Token/etp-rt cookie)
|
||||
### Fonts
|
||||
#### `--dlFonts`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||
|
|
@ -52,7 +58,7 @@ Use this command to download all the fonts and add them to the muxed **mkv** fil
|
|||
#### `--fontName`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | ---|
|
||||
| Funimation, Hidive | `--fontName ${fontName}` | `string` | `No`| `NaN` | `NaN` |
|
||||
| Hidive, AnimationDigitalNetwork | `--fontName ${fontName}` | `string` | `No`| `NaN` | `NaN` |
|
||||
|
||||
Set the font to use in subtiles
|
||||
### Search
|
||||
|
|
@ -74,12 +80,12 @@ Search only for type of anime listings (e.g. episodes, series)
|
|||
| Crunchyroll, Hidive | `--page ${page}` | `number` | `No`| `-p` | `NaN` |
|
||||
|
||||
The output is organized in pages. Use this command to output the items for the given page
|
||||
#### `--search-locale`
|
||||
#### `--locale`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | --- | --- | ---|
|
||||
| Crunchyroll | `--search-locale ${locale}` | `string` | `No`| `NaN` | [`''`, `en-US`, `en-IN`, `es-LA`, `es-419`, `es-ES`, `pt-BR`, `pt-PT`, `fr-FR`, `de-DE`, `ar-ME`, `ar-SA`, `it-IT`, `ru-RU`, `tr-TR`, `hi-IN`, `zh-CN`, `zh-TW`, `ko-KR`, `ca-ES`, `pl-PL`, `th-TH`, `ta-IN`, `ms-MY`] | ``| `search-locale: ` |
|
||||
| Crunchyroll, AnimeOnegai, AnimationDigitalNetwork | `--locale ${locale}` | `string` | `No`| `NaN` | [`''`, `en-US`, `en-IN`, `es-LA`, `es-419`, `es-ES`, `pt-BR`, `pt-PT`, `fr-FR`, `de-DE`, `ar-ME`, `ar-SA`, `it-IT`, `ru-RU`, `tr-TR`, `hi-IN`, `zh-CN`, `zh-TW`, `zh-HK`, `ko-KR`, `ca-ES`, `pl-PL`, `th-TH`, `ta-IN`, `ms-MY`, `vi-VN`, `id-ID`, `te-IN`, `fr`, `de`, `''`, `es`, `pt`] | `en-US`| `locale: ` |
|
||||
|
||||
Set the search local that will be used for searching for items.
|
||||
Set the local that will be used for the API.
|
||||
#### `--new`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | ---|
|
||||
|
|
@ -98,8 +104,7 @@ Get video list by Movie Listing ID
|
|||
| --- | --- | --- | --- | --- | ---|
|
||||
| Crunchyroll | `--series ${ID}` | `string` | `No`| `--srz` | `NaN` |
|
||||
|
||||
This command is used only for crunchyroll.
|
||||
Requested is the ID of a show not a season.
|
||||
Requested is the ID of a show not a season.
|
||||
#### `-s`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | ---|
|
||||
|
|
@ -130,12 +135,26 @@ Set the quality level. Use 0 to use the maximum quality.
|
|||
#### `--dlVideoOnce`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | --- | ---|
|
||||
| Crunchyroll | `--dlVideoOnce ` | `boolean` | `No`| `NaN` | `false`| `dlVideoOnce: ` |
|
||||
| Crunchyroll, AnimeOnegai | `--dlVideoOnce ` | `boolean` | `No`| `NaN` | `false`| `dlVideoOnce: ` |
|
||||
|
||||
If selected, the best selected quality will be downloaded only for the first language,
|
||||
then the worst video quality with the same audio quality will be downloaded for every other language.
|
||||
By the later merge of the videos, no quality difference will be present.
|
||||
This will speed up the download speed, if multiple languages are selected.
|
||||
#### `--chapters`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | --- | ---|
|
||||
| Crunchyroll, AnimationDigitalNetwork | `--chapters ` | `boolean` | `No`| `NaN` | `true`| `chapters: ` |
|
||||
|
||||
Will fetch the chapters and add them into the final video.
|
||||
Currently only works with mkvmerge.
|
||||
#### `--crapi`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | --- | --- | ---|
|
||||
| Crunchyroll | `--crapi ` | `string` | `No`| `NaN` | [`android`, `web`] | `web`| `crapi: ` |
|
||||
|
||||
If set to Android, it has lower quality, but Non-DRM streams,
|
||||
If set to Web, it has a higher quality adaptive stream, but everything is DRM.
|
||||
#### `--removeBumpers`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | --- | ---|
|
||||
|
|
@ -152,29 +171,34 @@ If selected, it will prefer to keep the original Font Size defined by the servic
|
|||
#### `-x`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | --- | --- | ---|
|
||||
| Crunchyroll, Funimation | `-x ${server}` | `number` | `No`| `--server` | [`1`, `2`, `3`, `4`] | `1`| `x: ` |
|
||||
| Crunchyroll | `-x ${server}` | `number` | `No`| `--server` | [`1`, `2`, `3`, `4`] | `1`| `x: ` |
|
||||
|
||||
Select the server to use
|
||||
#### `--kstream`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | --- | --- | ---|
|
||||
| Crunchyroll | `--kstream ${stream}` | `number` | `No`| `-k` | [`1`, `2`, `3`, `4`, `5`, `6`, `7`, `8`, `9`, `10`] | `2`| `kstream: ` |
|
||||
| Crunchyroll | `--kstream ${stream}` | `number` | `No`| `-k` | [`1`, `2`, `3`, `4`, `5`, `6`, `7`, `8`, `9`, `10`] | `1`| `kstream: ` |
|
||||
|
||||
Select specific stream
|
||||
#### `--cstream`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | --- | --- | ---|
|
||||
| Crunchyroll | `--cstream ${device}` | `string` | `No`| `--cs` | [`chrome`, `firefox`, `safari`, `edge`, `fallback`, `ps4`, `ps5`, `switch`, `samsungtv`, `lgtv`, `rokutv`, `android`, `iphone`, `ipad`, `none`] | `chrome`| `cstream: ` |
|
||||
|
||||
Select specific crunchy play stream by device, or disable stream with "none"
|
||||
#### `--hslang`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | --- | --- | ---|
|
||||
| Crunchyroll | `--hslang ${hslang}` | `string` | `No`| `NaN` | [`none`, `en`, `en-IN`, `es-419`, `es-ES`, `pt-BR`, `pt-PT`, `fr`, `de`, `ar`, `it`, `ru`, `tr`, `hi`, `zh`, `zh-CN`, `zh-TW`, `ko`, `ca-ES`, `pl-PL`, `th-TH`, `ta-IN`, `ms-MY`, `ja`] | `none`| `hslang: ` |
|
||||
| Crunchyroll | `--hslang ${hslang}` | `string` | `No`| `NaN` | [`none`, `en`, `en-IN`, `es-419`, `es-ES`, `pt-BR`, `pt-PT`, `fr`, `de`, `ar`, `it`, `ru`, `tr`, `hi`, `zh`, `zh-CN`, `zh-TW`, `zh-HK`, `ko`, `ca-ES`, `pl-PL`, `th-TH`, `ta-IN`, `ms-MY`, `vi-VN`, `id-ID`, `te-IN`, `ja`] | `none`| `hslang: ` |
|
||||
|
||||
Download video with specific hardsubs
|
||||
#### `--dlsubs`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | --- | --- | ---|
|
||||
| All | `--dlsubs ${sub1} ${sub2}` | `array` | `No`| `NaN` | [`all`, `none`, `en`, `en-IN`, `es-419`, `es-ES`, `pt-BR`, `pt-PT`, `fr`, `de`, `ar`, `it`, `ru`, `tr`, `hi`, `zh`, `zh-CN`, `zh-TW`, `ko`, `ca-ES`, `pl-PL`, `th-TH`, `ta-IN`, `ms-MY`, `ja`] | `all`| `dlsubs: ` |
|
||||
| All | `--dlsubs ${sub1} ${sub2}` | `array` | `No`| `NaN` | [`all`, `none`, `en`, `en-IN`, `es-419`, `es-ES`, `pt-BR`, `pt-PT`, `fr`, `de`, `ar`, `it`, `ru`, `tr`, `hi`, `zh`, `zh-CN`, `zh-TW`, `zh-HK`, `ko`, `ca-ES`, `pl-PL`, `th-TH`, `ta-IN`, `ms-MY`, `vi-VN`, `id-ID`, `te-IN`, `ja`] | `all`| `dlsubs: ` |
|
||||
|
||||
Download subtitles by language tag (space-separated)
|
||||
Funi Only: zh
|
||||
Crunchy Only: en-IN, es-419, es-ES, pt-PT, fr, de, ar, ar, it, ru, tr, hi, zh-CN, zh-TW, ko, ca-ES, pl-PL, th-TH, ta-IN, ms-MY
|
||||
Crunchy Only: en, en-IN, es-419, es-419, es-ES, pt-BR, pt-PT, fr, de, ar, ar, it, ru, tr, hi, zh-CN, zh-TW, zh-HK, ko, ca-ES, pl-PL, th-TH, ta-IN, ms-MY, vi-VN, id-ID, te-IN, ja
|
||||
#### `--novids`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | ---|
|
||||
|
|
@ -184,7 +208,7 @@ Skip downloading videos
|
|||
#### `--noaudio`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | ---|
|
||||
| Funimation | `--noaudio ` | `boolean` | `No`| `NaN` | `NaN` |
|
||||
| Crunchyroll, Hidive | `--noaudio ` | `boolean` | `No`| `NaN` | `NaN` |
|
||||
|
||||
Skip downloading audio
|
||||
#### `--nosubs`
|
||||
|
|
@ -196,11 +220,10 @@ Skip downloading subtitles
|
|||
#### `--dubLang`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | --- | --- | ---|
|
||||
| All | `--dubLang ${dub1} ${dub2}` | `array` | `No`| `NaN` | [`eng`, `spa`, `spa-419`, `spa-ES`, `por`, `fra`, `deu`, `ara-ME`, `ara`, `ita`, `rus`, `tur`, `hin`, `cmn`, `zho`, `chi`, `kor`, `cat`, `pol`, `tha`, `tam`, `may`, `jpn`] | `jpn`| `dubLang: ` |
|
||||
| All | `--dubLang ${dub1} ${dub2}` | `array` | `No`| `NaN` | [`eng`, `spa`, `spa-419`, `spa-ES`, `por`, `fra`, `deu`, `ara-ME`, `ara`, `ita`, `rus`, `tur`, `hin`, `cmn`, `zho`, `chi`, `zh-HK`, `kor`, `cat`, `pol`, `tha`, `tam`, `may`, `vie`, `ind`, `tel`, `jpn`] | `jpn`| `dubLang: ` |
|
||||
|
||||
Set the language to download:
|
||||
Funi Only: cmn
|
||||
Crunchy Only: eng, spa-419, spa-ES, por, fra, deu, ara-ME, ara, ita, rus, tur, hin, zho, chi, kor, cat, pol, tha, tam, may
|
||||
Crunchy Only: eng, eng, spa, spa-419, spa-ES, por, por, fra, deu, ara-ME, ara, ita, rus, tur, hin, zho, chi, zh-HK, kor, cat, pol, tha, tam, may, vie, ind, tel, jpn
|
||||
#### `--all`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | --- | ---|
|
||||
|
|
@ -212,7 +235,14 @@ Used to download all episodes from the show
|
|||
| --- | --- | --- | --- | --- | --- | ---|
|
||||
| All | `--fontSize ${fontSize}` | `number` | `No`| `NaN` | `55`| `fontSize: ` |
|
||||
|
||||
Used to set the fontsize of the subtitles
|
||||
When converting the subtitles to ass, this will change the font size
|
||||
In most cases, requires "--originaFontSize false" to take effect
|
||||
#### `--combineLines`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | ---|
|
||||
| Hidive | `--combineLines ` | `boolean` | `No`| `NaN` | `NaN` |
|
||||
|
||||
If selected, will prevent a line from shifting downwards
|
||||
#### `--allDubs`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | ---|
|
||||
|
|
@ -234,7 +264,7 @@ Set the time the program waits between downloads. Set in millisecods
|
|||
#### `--simul`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | --- | ---|
|
||||
| Funimation, Hidive | `--simul ` | `boolean` | `No`| `NaN` | `false`| `simul: ` |
|
||||
| Hidive | `--simul ` | `boolean` | `No`| `NaN` | `false`| `simul: ` |
|
||||
|
||||
Force downloading simulcast version instead of uncut version (if available).
|
||||
#### `--but`
|
||||
|
|
@ -343,14 +373,14 @@ Set the options given to ffmpeg
|
|||
| All | `--defaultAudio ${args}` | `string` | `No`| `NaN` | `eng`| `defaultAudio: ` |
|
||||
|
||||
Set the default audio track by language code
|
||||
Possible Values: eng, eng, spa, spa-419, spa-ES, por, por, fra, deu, ara-ME, ara, ita, rus, tur, hin, cmn, zho, chi, kor, cat, pol, tha, tam, may, jpn
|
||||
Possible Values: eng, eng, spa, spa-419, spa-ES, por, por, fra, deu, ara-ME, ara, ita, rus, tur, hin, cmn, zho, chi, zh-HK, kor, cat, pol, tha, tam, may, vie, ind, tel, jpn
|
||||
#### `--defaultSub`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | --- | ---|
|
||||
| All | `--defaultSub ${args}` | `string` | `No`| `NaN` | `eng`| `defaultSub: ` |
|
||||
|
||||
Set the default subtitle track by language code
|
||||
Possible Values: eng, eng, spa, spa-419, spa-ES, por, por, fra, deu, ara-ME, ara, ita, rus, tur, hin, cmn, zho, chi, kor, cat, pol, tha, tam, may, jpn
|
||||
Possible Values: eng, eng, spa, spa-419, spa-ES, por, por, fra, deu, ara-ME, ara, ita, rus, tur, hin, cmn, zho, chi, zh-HK, kor, cat, pol, tha, tam, may, vie, ind, tel, jpn
|
||||
### Filename Template
|
||||
#### `--fileName`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||
|
|
@ -359,7 +389,7 @@ Possible Values: eng, eng, spa, spa-419, spa-ES, por, por, fra, deu, ara-ME, ara
|
|||
|
||||
Set the filename template. Use ${variable_name} to insert variables.
|
||||
You can also create folders by inserting a path seperator in the filename
|
||||
You may use 'title', 'episode', 'showTitle', 'season', 'width', 'height', 'service' as variables.
|
||||
You may use 'title', 'episode', 'showTitle', 'seriesTitle', 'season', 'width', 'height', 'service' as variables.
|
||||
#### `--numbers`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | --- | ---|
|
||||
|
|
@ -398,7 +428,7 @@ Debug mode (tokens may be revealed in the console output)
|
|||
#### `--service`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | --- | --- | ---|
|
||||
| All | `--service ${service}` | `string` | `Yes`| `NaN` | [`funi`, `crunchy`, `hidive`] | ``| `service: ` |
|
||||
| All | `--service ${service}` | `string` | `Yes`| `NaN` | [`crunchy`, `hidive`, `ao`, `adn`] | ``| `service: ` |
|
||||
|
||||
Set the service you want to use
|
||||
#### `--update`
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
[](https://discord.gg/qEpbWen5vq)
|
||||
|
||||
This downloader can download anime from different sites. Currently supported are *Funimation*, *Crunchyroll*, and *Hidive*.
|
||||
This downloader can download anime from different sites. Currently supported are *Crunchyroll*, *Hidive*, *AnimeOnegai*, and *AnimationDigitalNetwork*.
|
||||
|
||||
## Legal Warning
|
||||
|
||||
This application is not endorsed by or affiliated with *Funimation*, *Crunchyroll*, or *Hidive*. This application enables you to download videos for offline viewing which may be forbidden by law in your country. The usage of this application may also cause a violation of the *Terms of Service* between you and the stream provider. This tool is not responsible for your actions; please make an informed decision before using this application.
|
||||
This application is not endorsed by or affiliated with *Crunchyroll*, *Hidive*, *AnimeOnegai*, or *AnimationDigitalNetwork*. This application enables you to download videos for offline viewing which may be forbidden by law in your country. The usage of this application may also cause a violation of the *Terms of Service* between you and the stream provider. This tool is not responsible for your actions; please make an informed decision before using this application.
|
||||
|
||||
## Dependencies
|
||||
|
||||
|
|
@ -60,20 +60,39 @@ AniDL --service {ServiceName} -s {SeasonID} -e {EpisodeNumber}
|
|||
|
||||
Dependencies that are only required for running from code. These are not required if you are using the prebuilt binaries.
|
||||
|
||||
* NodeJS >= 14.6.0 (https://nodejs.org/)
|
||||
* NodeJS >= 18.0.0 (https://nodejs.org/)
|
||||
* NPM >= 6.9.0 (https://www.npmjs.org/)
|
||||
* PNPM >= 7.0.0 (https://pnpm.io/)
|
||||
|
||||
### Build Instructions
|
||||
### Build Setup
|
||||
|
||||
Please note that NodeJS, NPM, and PNPM must be installed on your system. For instructions on how to install pnpm, check (https://pnpm.io/installation)
|
||||
|
||||
First clone this repo `git clone https://github.com/anidl/multi-downloader-nx.git`.
|
||||
|
||||
`cd` into the cloned directory and run `pnpm i`.
|
||||
Afterwards run `pnpm run tsc false [true if you want gui, false otherwise]`.
|
||||
`cd` into the cloned directory and run `pnpm i`. Next, decide if you want to package the application, build the code, or run from typescript.
|
||||
|
||||
If you want the `js` files you are done. Just `cd` into the `lib` folder, and run `node index.js --help` to get started with the CLI, or run `node gui.js` to run the GUI
|
||||
### Run from TypeScript
|
||||
|
||||
You can run the code from native TypeScript, this requires ts-node which you can install with pnpm with the following command: `pnpm -g i ts-node`
|
||||
|
||||
Afterwords, you can run the application like this:
|
||||
|
||||
* CLI: `ts-node -T ./index.ts --help`
|
||||
|
||||
### Run as JavaScript
|
||||
|
||||
If you want to build the application into JavaScript code to run, you can do that as well like this:
|
||||
|
||||
* CLI: `pnpm run prebuild-cli`
|
||||
* GUI: `pnpm run prebuild-gui`
|
||||
|
||||
Then you can cd into the `lib` folder and you will be able to run the CLI or GUI as follows:
|
||||
|
||||
* CLI: `node ./index.js --help`
|
||||
* GUI: `node ./gui.js`
|
||||
|
||||
### Build the application into an executable
|
||||
|
||||
If you want to package the application, run pnpm run build-`{platform}`-`{type}` where `{platform}` is the operating system (currently the choices are windows, linux, macos, alpine, android, and arm) and `{type}` is cli or gui.
|
||||
|
||||
|
|
|
|||
64
eslint.config.mjs
Normal file
64
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
// @ts-check
|
||||
|
||||
import eslint from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import react from 'eslint-plugin-react';
|
||||
|
||||
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',
|
||||
'indent': [
|
||||
'error',
|
||||
2
|
||||
],
|
||||
'linebreak-style': [
|
||||
'warn',
|
||||
'windows'
|
||||
],
|
||||
'quotes': [
|
||||
'error',
|
||||
'single'
|
||||
],
|
||||
'semi': [
|
||||
'error',
|
||||
'always'
|
||||
]
|
||||
},
|
||||
plugins: {
|
||||
react
|
||||
},
|
||||
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
|
||||
}
|
||||
}
|
||||
);
|
||||
923
funi.ts
923
funi.ts
|
|
@ -1,923 +0,0 @@
|
|||
// modules build-in
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// package json
|
||||
import packageJson from './package.json';
|
||||
|
||||
// modules extra
|
||||
import { console } from './modules/log';
|
||||
import * as shlp from 'sei-helper';
|
||||
import m3u8 from 'm3u8-parsed';
|
||||
import hlsDownload, { HLSCallback } from './modules/hls-download';
|
||||
|
||||
// extra
|
||||
import * as appYargs from './modules/module.app-args';
|
||||
import * as yamlCfg from './modules/module.cfg-loader';
|
||||
import vttConvert from './modules/module.vttconvert';
|
||||
|
||||
// types
|
||||
import type { Item } from './@types/items.js';
|
||||
|
||||
// params
|
||||
|
||||
// Import modules after argv has been exported
|
||||
import getData from './modules/module.getdata';
|
||||
import merger from './modules/module.merger';
|
||||
import parseSelect from './modules/module.parseSelect';
|
||||
import { EpisodeData, MediaChild } from './@types/episode';
|
||||
import { Subtitle } from './@types/funiTypes';
|
||||
import { StreamData } from './@types/streamData';
|
||||
import { DownloadedFile } from './@types/downloadedFile';
|
||||
import parseFileName, { Variable } from './modules/module.filename';
|
||||
import { downloaded } from './modules/module.downloadArchive';
|
||||
import { FunimationMediaDownload } from './@types/funiTypes';
|
||||
import * as langsData from './modules/module.langsData';
|
||||
import { TitleElement } from './@types/episode';
|
||||
import { AvailableFilenameVars } from './modules/module.args';
|
||||
import { AuthData, AuthResponse, CheckTokenResponse, FuniGetEpisodeData, FuniGetEpisodeResponse, FuniGetShowData, SearchData, FuniSearchReponse, FuniShowResponse, FuniStreamData, FuniSubsData, FuniEpisodeData, ResponseBase } from './@types/messageHandler';
|
||||
import { ServiceClass } from './@types/serviceClassInterface';
|
||||
import { SubtitleRequest } from './@types/funiSubtitleRequest';
|
||||
|
||||
// program name
|
||||
const api_host = 'https://prod-api-funimationnow.dadcdigital.com/api';
|
||||
// check page
|
||||
|
||||
// fn variables
|
||||
let fnEpNum: string|number = 0,
|
||||
fnOutput: string[] = [],
|
||||
season = 0,
|
||||
tsDlPath: {
|
||||
path: string,
|
||||
lang: langsData.LanguageItem
|
||||
}[] = [],
|
||||
stDlPath: Subtitle[] = [];
|
||||
|
||||
export default class Funi implements ServiceClass {
|
||||
public static epIdLen = 4;
|
||||
public static typeIdLen = 0;
|
||||
|
||||
public cfg: yamlCfg.ConfigObject;
|
||||
private token: string | boolean;
|
||||
|
||||
constructor(private debug = false) {
|
||||
this.cfg = yamlCfg.loadCfg();
|
||||
this.token = yamlCfg.loadFuniToken();
|
||||
}
|
||||
|
||||
public checkToken(): CheckTokenResponse {
|
||||
const isOk = typeof this.token === 'string';
|
||||
return isOk ? { isOk, value: undefined } : { isOk, reason: new Error('Not authenticated') };
|
||||
}
|
||||
|
||||
public async cli() : Promise<boolean|undefined> {
|
||||
const argv = appYargs.appArgv(this.cfg.cli);
|
||||
if (argv.debug)
|
||||
this.debug = true;
|
||||
console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`);
|
||||
if (argv.allDubs) {
|
||||
argv.dubLang = langsData.dubLanguageCodes;
|
||||
}
|
||||
// select mode
|
||||
if (argv.silentAuth && !argv.auth) {
|
||||
const data: AuthData = {
|
||||
username: argv.username ?? await shlp.question('[Q] LOGIN/EMAIL'),
|
||||
password: argv.password ?? await shlp.question('[Q] PASSWORD ')
|
||||
};
|
||||
await this.auth(data);
|
||||
}
|
||||
if(argv.auth){
|
||||
const data: AuthData = {
|
||||
username: argv.username ?? await shlp.question('[Q] LOGIN/EMAIL'),
|
||||
password: argv.password ?? await shlp.question('[Q] PASSWORD ')
|
||||
};
|
||||
await this.auth(data);
|
||||
}
|
||||
else if(argv.search){
|
||||
this.searchShow(true, { search: argv.search });
|
||||
}
|
||||
else if(argv.s && !isNaN(parseInt(argv.s)) && parseInt(argv.s) > 0){
|
||||
const data = await this.getShow(true, { id: parseInt(argv.s), but: argv.but, all: argv.all, e: argv.e });
|
||||
if (!data.isOk) {
|
||||
console.error(`${data.reason.message}`);
|
||||
return false;
|
||||
}
|
||||
let ok = true;
|
||||
for (const episodeData of data.value) {
|
||||
if ((await this.getEpisode(true, { subs: { dlsubs: argv.dlsubs, nosubs: argv.nosubs, sub: false, ccTag: argv.ccTag }, dubLang: argv.dubLang, fnSlug: episodeData, s: argv.s, simul: argv.simul }, {
|
||||
ass: false,
|
||||
...argv
|
||||
})).isOk !== true)
|
||||
ok = false;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
else{
|
||||
console.info('No option selected or invalid value entered. Try --help.');
|
||||
}
|
||||
}
|
||||
public async auth(data: AuthData): Promise<AuthResponse> {
|
||||
const authOpts = {
|
||||
user: data.username,
|
||||
pass: data.password
|
||||
};
|
||||
const authData = await getData({
|
||||
baseUrl: api_host,
|
||||
url: '/auth/login/',
|
||||
auth: authOpts,
|
||||
debug: this.debug,
|
||||
});
|
||||
if(authData.ok && authData.res){
|
||||
const resJSON = JSON.parse(authData.res.body);
|
||||
if(resJSON.token){
|
||||
console.info('Authentication success, your token: %s%s\n', resJSON.token.slice(0,8),'*'.repeat(32));
|
||||
yamlCfg.saveFuniToken({'token': resJSON.token});
|
||||
this.token = resJSON.token;
|
||||
return { isOk: true, value: undefined };
|
||||
} else {
|
||||
console.info('[ERROR]%s\n', ' No token found');
|
||||
if (this.debug) {
|
||||
console.info(resJSON);
|
||||
}
|
||||
return { isOk: false, reason: new Error(resJSON) };
|
||||
}
|
||||
}
|
||||
return { isOk: false, reason: new Error('Login request failed') };
|
||||
}
|
||||
|
||||
public async searchShow(log: boolean, data: SearchData): Promise<FuniSearchReponse> {
|
||||
const qs = {unique: true, limit: 100, q: data.search, offset: 0 };
|
||||
const searchData = await getData({
|
||||
baseUrl: api_host,
|
||||
url: '/source/funimation/search/auto/',
|
||||
querystring: qs,
|
||||
token: this.token,
|
||||
useToken: true,
|
||||
debug: this.debug,
|
||||
});
|
||||
if(!searchData.ok || !searchData.res){
|
||||
return { isOk: false, reason: new Error('Request is not ok') };
|
||||
}
|
||||
const searchDataJSON = JSON.parse(searchData.res.body);
|
||||
if(searchDataJSON.detail){
|
||||
console.error(`${searchDataJSON.detail}`);
|
||||
return { isOk: false, reason: new Error(searchDataJSON.defail) };
|
||||
}
|
||||
if(searchDataJSON.items && searchDataJSON.items.hits && log){
|
||||
const shows = searchDataJSON.items.hits;
|
||||
console.info('Search Results:');
|
||||
for(const ssn in shows){
|
||||
console.info(`[#${shows[ssn].id}] ${shows[ssn].title}` + (shows[ssn].tx_date?` (${shows[ssn].tx_date})`:''));
|
||||
}
|
||||
}
|
||||
if (log)
|
||||
console.info('Total shows found: %s\n',searchDataJSON.count);
|
||||
return { isOk: true, value: searchDataJSON };
|
||||
}
|
||||
|
||||
public async listShowItems(id: number) : Promise<ResponseBase<Item[]>> {
|
||||
const showData = await getData({
|
||||
baseUrl: api_host,
|
||||
url: `/source/catalog/title/${id}`,
|
||||
token: this.token,
|
||||
useToken: true,
|
||||
debug: this.debug,
|
||||
});
|
||||
// check errors
|
||||
if(!showData.ok || !showData.res){ return { isOk: false, reason: new Error('ShowData is not ok') }; }
|
||||
const showDataJSON = JSON.parse(showData.res.body);
|
||||
if(showDataJSON.status){
|
||||
console.error('Error #%d: %s\n', showDataJSON.status, showDataJSON.data.errors[0].detail);
|
||||
return { isOk: false, reason: new Error(showDataJSON.data.errors[0].detail) };
|
||||
}
|
||||
else if(!showDataJSON.items || showDataJSON.items.length<1){
|
||||
console.error('Show not found\n');
|
||||
return { isOk: false, reason: new Error('Show not found') };
|
||||
}
|
||||
const showDataItem = showDataJSON.items[0];
|
||||
console.info('[#%s] %s (%s)',showDataItem.id,showDataItem.title,showDataItem.releaseYear);
|
||||
// show episodes
|
||||
const qs: {
|
||||
limit: number,
|
||||
sort: string,
|
||||
sort_direction: string,
|
||||
title_id: number,
|
||||
language?: string
|
||||
} = { limit: -1, sort: 'order', sort_direction: 'ASC', title_id: id };
|
||||
const episodesData = await getData({
|
||||
baseUrl: api_host,
|
||||
url: '/funimation/episodes/',
|
||||
querystring: qs,
|
||||
token: this.token,
|
||||
useToken: true,
|
||||
debug: this.debug,
|
||||
});
|
||||
if(!episodesData.ok || !episodesData.res){ return { isOk: false, reason: new Error('episodesData is not ok') }; }
|
||||
|
||||
let epsDataArr: Item[] = JSON.parse(episodesData.res.body).items;
|
||||
const epNumRegex = /^([A-Z0-9]*[A-Z])?(\d+)$/i;
|
||||
|
||||
const parseEpStr = (epStr: string) => {
|
||||
const match = epStr.match(epNumRegex);
|
||||
if (!match) {
|
||||
console.error('No match found');
|
||||
return ['', ''];
|
||||
}
|
||||
if(match.length > 2){
|
||||
const spliced = [...match].splice(1);
|
||||
spliced[0] = spliced[0] ? spliced[0] : '';
|
||||
return spliced;
|
||||
}
|
||||
else return [ '', match[0] ];
|
||||
};
|
||||
|
||||
epsDataArr = epsDataArr.map(e => {
|
||||
const baseId = e.ids.externalAsianId ? e.ids.externalAsianId : e.ids.externalEpisodeId;
|
||||
e.id = baseId.replace(new RegExp('^' + e.ids.externalShowId), '');
|
||||
if(e.id.match(epNumRegex)){
|
||||
const epMatch = parseEpStr(e.id);
|
||||
Funi.epIdLen = epMatch[1].length > Funi.epIdLen ? epMatch[1].length : Funi.epIdLen;
|
||||
Funi.typeIdLen = epMatch[0].length > Funi.typeIdLen ? epMatch[0].length : Funi.typeIdLen;
|
||||
e.id_split = epMatch;
|
||||
}
|
||||
else{
|
||||
Funi.typeIdLen = 3 > Funi.typeIdLen? 3 : Funi.typeIdLen;
|
||||
console.error('FAILED TO PARSE: ', e.id);
|
||||
e.id_split = [ 'ZZZ', 9999 ];
|
||||
}
|
||||
return e;
|
||||
});
|
||||
|
||||
epsDataArr.sort((a, b) => {
|
||||
if (a.item.seasonOrder < b.item.seasonOrder && a.id.localeCompare(b.id) < 0) {
|
||||
return -1;
|
||||
}
|
||||
if (a.item.seasonOrder > b.item.seasonOrder && a.id.localeCompare(b.id) > 0) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return { isOk: true, value: epsDataArr };
|
||||
}
|
||||
|
||||
public async getShow(log: boolean, data: FuniGetShowData) : Promise<FuniShowResponse> {
|
||||
const showList = await this.listShowItems(data.id);
|
||||
if (!showList.isOk)
|
||||
return showList;
|
||||
const eps = showList.value;
|
||||
const epSelList = parseSelect(data.e as string, data.but);
|
||||
const fnSlug: FuniEpisodeData[] = [], epSelEpsTxt: string[] = []; let is_selected = false;
|
||||
|
||||
|
||||
for(const e in eps){
|
||||
eps[e].id_split[1] = parseInt(eps[e].id_split[1].toString()).toString().padStart(Funi.epIdLen, '0');
|
||||
let epStrId = eps[e].id_split.join('');
|
||||
// select
|
||||
is_selected = false;
|
||||
if (data.all || epSelList.isSelected(epStrId)) {
|
||||
fnSlug.push({
|
||||
title:eps[e].item.titleSlug,
|
||||
episode:eps[e].item.episodeSlug,
|
||||
episodeID:epStrId,
|
||||
epsiodeNumber: eps[e].item.episodeNum,
|
||||
seasonTitle: eps[e].item.seasonTitle,
|
||||
seasonNumber: eps[e].item.seasonNum,
|
||||
ids: {
|
||||
episode: eps[e].ids.externalEpisodeId,
|
||||
season: eps[e].ids.externalSeasonId,
|
||||
show: eps[e].ids.externalShowId
|
||||
},
|
||||
image: eps[e].item.poster
|
||||
});
|
||||
epSelEpsTxt.push(epStrId);
|
||||
is_selected = true;
|
||||
}
|
||||
// console vars
|
||||
const tx_snum = eps[e].item.seasonNum=='1'?'':` S${eps[e].item.seasonNum}`;
|
||||
const tx_type = eps[e].mediaCategory != 'episode' ? eps[e].mediaCategory : '';
|
||||
const tx_enum = eps[e].item.episodeNum && eps[e].item.episodeNum !== '' ?
|
||||
`#${(parseInt(eps[e].item.episodeNum) < 10 ? '0' : '')+eps[e].item.episodeNum}` : '#'+eps[e].item.episodeId;
|
||||
const qua_str = eps[e].quality.height ? eps[e].quality.quality + eps[e].quality.height : 'UNK';
|
||||
const aud_str = eps[e].audio.length > 0 ? `, ${eps[e].audio.join(', ')}` : '';
|
||||
const rtm_str = eps[e].item.runtime !== '' ? eps[e].item.runtime : '??:??';
|
||||
// console string
|
||||
eps[e].id_split[0] = eps[e].id_split[0].toString().padStart(Funi.typeIdLen, ' ');
|
||||
epStrId = eps[e].id_split.join('');
|
||||
let conOut = `[${epStrId}] `;
|
||||
conOut += `${eps[e].item.titleName+tx_snum} - ${tx_type+tx_enum} ${eps[e].item.episodeName} `;
|
||||
conOut += `(${rtm_str}) [${qua_str+aud_str}]`;
|
||||
conOut += is_selected ? ' (selected)' : '';
|
||||
conOut += eps.length-1 == parseInt(e) ? '\n' : '';
|
||||
console.info(conOut);
|
||||
}
|
||||
if(fnSlug.length < 1){
|
||||
if (log)
|
||||
console.info('Episodes not selected!\n');
|
||||
return { isOk: true, value: [] } ;
|
||||
}
|
||||
else{
|
||||
if (log)
|
||||
console.info('Selected Episodes: %s\n',epSelEpsTxt.join(', '));
|
||||
return { isOk: true, value: fnSlug };
|
||||
}
|
||||
}
|
||||
|
||||
public async getEpisode(log: boolean, data: FuniGetEpisodeData, downloadData: FuniStreamData) : Promise<FuniGetEpisodeResponse> {
|
||||
const episodeData = await getData({
|
||||
baseUrl: api_host,
|
||||
url: `/source/catalog/episode/${data.fnSlug.title}/${data.fnSlug.episode}/`,
|
||||
token: this.token,
|
||||
useToken: true,
|
||||
debug: this.debug,
|
||||
});
|
||||
if(!episodeData.ok || !episodeData.res){return { isOk: false, reason: new Error('Unable to get episodeData') }; }
|
||||
const ep = JSON.parse(episodeData.res.body).items[0] as EpisodeData, streamIds: { id: number, lang: langsData.LanguageItem }[] = [];
|
||||
// build fn
|
||||
season = parseInt(ep.parent.seasonNumber);
|
||||
if(ep.mediaCategory != 'Episode'){
|
||||
ep.number = ep.number !== '' ? ep.mediaCategory+ep.number : ep.mediaCategory+'#'+ep.id;
|
||||
}
|
||||
fnEpNum = isNaN(parseInt(ep.number)) ? ep.number : parseInt(ep.number);
|
||||
|
||||
// is uncut
|
||||
const uncut = {
|
||||
Japanese: false,
|
||||
English: false
|
||||
};
|
||||
|
||||
// end
|
||||
if (log) {
|
||||
console.info(
|
||||
'%s - S%sE%s - %s',
|
||||
ep.parent.title,
|
||||
(ep.parent.seasonNumber ? ep.parent.seasonNumber : '?'),
|
||||
(ep.number ? ep.number : '?'),
|
||||
ep.title
|
||||
);
|
||||
|
||||
console.info('Available streams (Non-Encrypted):');
|
||||
}
|
||||
// map medias
|
||||
const media = await Promise.all(ep.media.map(async (m) =>{
|
||||
if(m.mediaType == 'experience'){
|
||||
if(m.version.match(/uncut/i) && m.language){
|
||||
uncut[m.language] = true;
|
||||
}
|
||||
return {
|
||||
id: m.id,
|
||||
language: m.language,
|
||||
version: m.version,
|
||||
type: m.experienceType,
|
||||
subtitles: await this.getSubsUrl(m.mediaChildren, m.language, data.subs, ep.ids.externalEpisodeId, data.subs.ccTag)
|
||||
};
|
||||
}
|
||||
else{
|
||||
return { id: 0, type: '' };
|
||||
}
|
||||
}));
|
||||
|
||||
// select
|
||||
stDlPath = [];
|
||||
for(const m of media){
|
||||
let selected = false;
|
||||
if(m.id > 0 && m.type == 'Non-Encrypted'){
|
||||
const dub_type = m.language;
|
||||
if (!dub_type)
|
||||
continue;
|
||||
let localSubs: Subtitle[] = [];
|
||||
const selUncut = !data.simul && uncut[dub_type] && m.version?.match(/uncut/i)
|
||||
? true
|
||||
: (!uncut[dub_type] || data.simul && m.version?.match(/simulcast/i) ? true : false);
|
||||
for (const curDub of data.dubLang) {
|
||||
const item = langsData.languages.find(a => a.code === curDub);
|
||||
if(item && (dub_type === item.funi_name_lagacy || dub_type === (item.funi_name ?? item.name)) && selUncut){
|
||||
streamIds.push({
|
||||
id: m.id,
|
||||
lang: item
|
||||
});
|
||||
stDlPath.push(...m.subtitles);
|
||||
localSubs = m.subtitles;
|
||||
selected = true;
|
||||
}
|
||||
}
|
||||
if (log) {
|
||||
const subsToDisplay: langsData.LanguageItem[] = [];
|
||||
localSubs.forEach(a => {
|
||||
if (!subsToDisplay.includes(a.lang))
|
||||
subsToDisplay.push(a.lang);
|
||||
});
|
||||
console.info(`[#${m.id}] ${dub_type} [${m.version}]${(selected?' (selected)':'')}${
|
||||
localSubs && localSubs.length > 0 && selected ? ` (using ${subsToDisplay.map(a => `'${a.name}'`).join(', ')} for subtitles)` : ''
|
||||
}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const already: string[] = [];
|
||||
stDlPath = stDlPath.filter(a => {
|
||||
if (already.includes(`${a.closedCaption ? 'cc' : ''}-${a.lang.code}`)) {
|
||||
return false;
|
||||
} else {
|
||||
already.push(`${a.closedCaption ? 'cc' : ''}-${a.lang.code}`);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if(streamIds.length < 1){
|
||||
if (log)
|
||||
console.error('Track not selected\n');
|
||||
return { isOk: false, reason: new Error('Track not selected') };
|
||||
}
|
||||
else{
|
||||
tsDlPath = [];
|
||||
for (const streamId of streamIds) {
|
||||
const streamData = await getData({
|
||||
baseUrl: api_host,
|
||||
url: `/source/catalog/video/${streamId.id}/signed`,
|
||||
token: this.token,
|
||||
dinstid: 'uuid',
|
||||
useToken: true,
|
||||
debug: this.debug,
|
||||
});
|
||||
if(!streamData.ok || !streamData.res){return { isOk: false, reason: new Error('Unable to get streamdata') };}
|
||||
const streamDataRes = JSON.parse(streamData.res.body) as StreamData;
|
||||
if(streamDataRes.errors){
|
||||
if (log)
|
||||
console.info('Error #%s: %s\n',streamDataRes.errors[0].code,streamDataRes.errors[0].detail);
|
||||
return { isOk: false, reason: new Error(streamDataRes.errors[0].detail) };
|
||||
}
|
||||
else{
|
||||
for(const u in streamDataRes.items){
|
||||
if(streamDataRes.items[u].videoType == 'm3u8'){
|
||||
tsDlPath.push({
|
||||
path: streamDataRes.items[u].src,
|
||||
lang: streamId.lang
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(tsDlPath.length < 1){
|
||||
if (log)
|
||||
console.error('Unknown error\n');
|
||||
return { isOk: false, reason: new Error('Unknown error') };
|
||||
}
|
||||
else{
|
||||
const res = await this.downloadStreams(true, {
|
||||
id: data.fnSlug.episodeID,
|
||||
title: ep.title,
|
||||
showTitle: ep.parent.title,
|
||||
image: ep.thumb
|
||||
}, downloadData);
|
||||
if (res === true) {
|
||||
downloaded({
|
||||
service: 'funi',
|
||||
type: 's'
|
||||
}, data.s, [data.fnSlug.episodeID]);
|
||||
return { isOk: res, value: undefined };
|
||||
}
|
||||
return { isOk: false, reason: new Error('Unknown download error') };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async downloadStreams(log: boolean, episode: FunimationMediaDownload, data: FuniStreamData): Promise<boolean|void> {
|
||||
|
||||
// req playlist
|
||||
|
||||
const purvideo: DownloadedFile[] = [];
|
||||
const puraudio: DownloadedFile[] = [];
|
||||
const audioAndVideo: DownloadedFile[] = [];
|
||||
for (const streamPath of tsDlPath) {
|
||||
const plQualityReq = await getData({
|
||||
url: streamPath.path,
|
||||
debug: this.debug,
|
||||
});
|
||||
if(!plQualityReq.ok || !plQualityReq.res){return;}
|
||||
|
||||
const plQualityLinkList = m3u8(plQualityReq.res.body);
|
||||
|
||||
const mainServersList = [
|
||||
'vmfst-api.prd.funimationsvc.com',
|
||||
'd33et77evd9bgg.cloudfront.net',
|
||||
'd132fumi6di1wa.cloudfront.net',
|
||||
'funiprod.akamaized.net',
|
||||
];
|
||||
|
||||
const plServerList: string[] = [],
|
||||
plStreams: Record<string|number, {
|
||||
[key: string]: string
|
||||
}> = {},
|
||||
plLayersStr: string[] = [],
|
||||
plLayersRes: Record<string|number, {
|
||||
width: number,
|
||||
height: number
|
||||
}> = {};
|
||||
let plMaxLayer = 1,
|
||||
plNewIds = 1,
|
||||
plAud: undefined|{
|
||||
uri: string
|
||||
language: langsData.LanguageItem
|
||||
};
|
||||
|
||||
// new uris
|
||||
const vplReg = /streaming_video_(\d+)_(\d+)_(\d+)_index\.m3u8/;
|
||||
if(plQualityLinkList.playlists[0].uri.match(vplReg)){
|
||||
const audioKey = Object.keys(plQualityLinkList.mediaGroups.AUDIO).pop();
|
||||
if (!audioKey)
|
||||
return console.error('No audio key found');
|
||||
if(plQualityLinkList.mediaGroups.AUDIO[audioKey]){
|
||||
const audioDataParts = plQualityLinkList.mediaGroups.AUDIO[audioKey],
|
||||
audioEl = Object.keys(audioDataParts);
|
||||
const audioData = audioDataParts[audioEl[0]];
|
||||
let language = langsData.languages.find(a => a.code === audioData.language || a.locale === audioData.language);
|
||||
if (!language) {
|
||||
language = langsData.languages.find(a => a.funi_name_lagacy === audioEl[0] || ((a.funi_name ?? a.name) === audioEl[0]));
|
||||
if (!language) {
|
||||
if (log)
|
||||
console.error(`Unable to find language for locale ${audioData.language} or name ${audioEl[0]}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
plAud = {
|
||||
uri: audioData.uri,
|
||||
language: language
|
||||
};
|
||||
}
|
||||
plQualityLinkList.playlists.sort((a, b) => {
|
||||
const aMatch = a.uri.match(vplReg), bMatch = b.uri.match(vplReg);
|
||||
if (!aMatch || !bMatch) {
|
||||
console.info('Unable to match');
|
||||
return 0;
|
||||
}
|
||||
const av = parseInt(aMatch[3]);
|
||||
const bv = parseInt(bMatch[3]);
|
||||
if(av > bv){
|
||||
return 1;
|
||||
}
|
||||
if (av < bv) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
for(const s of plQualityLinkList.playlists){
|
||||
if(s.uri.match(/_Layer(\d+)\.m3u8/) || s.uri.match(vplReg)){
|
||||
// set layer and max layer
|
||||
let plLayerId: number|string = 0;
|
||||
const match = s.uri.match(/_Layer(\d+)\.m3u8/);
|
||||
if(match){
|
||||
plLayerId = parseInt(match[1]);
|
||||
}
|
||||
else{
|
||||
plLayerId = plNewIds, plNewIds++;
|
||||
}
|
||||
plMaxLayer = plMaxLayer < plLayerId ? plLayerId : plMaxLayer;
|
||||
// set urls and servers
|
||||
const plUrlDl = s.uri;
|
||||
const plServer = new URL(plUrlDl).host;
|
||||
if(!plServerList.includes(plServer)){
|
||||
plServerList.push(plServer);
|
||||
}
|
||||
if(!Object.keys(plStreams).includes(plServer)){
|
||||
plStreams[plServer] = {};
|
||||
}
|
||||
if(plStreams[plServer][plLayerId] && plStreams[plServer][plLayerId] != plUrlDl){
|
||||
console.warn(`Non duplicate url for ${plServer} detected, please report to developer!`);
|
||||
}
|
||||
else{
|
||||
plStreams[plServer][plLayerId] = plUrlDl;
|
||||
}
|
||||
// set plLayersStr
|
||||
const plResolution = s.attributes.RESOLUTION;
|
||||
plLayersRes[plLayerId] = plResolution;
|
||||
const plBandwidth = Math.round(s.attributes.BANDWIDTH/1024);
|
||||
if(plLayerId<10){
|
||||
plLayerId = plLayerId.toString().padStart(2,' ');
|
||||
}
|
||||
const qualityStrAdd = `${plLayerId}: ${plResolution.width}x${plResolution.height} (${plBandwidth}KiB/s)`;
|
||||
const qualityStrRegx = new RegExp(qualityStrAdd.replace(/(:|\(|\)|\/)/g,'\\$1'),'m');
|
||||
const qualityStrMatch = !plLayersStr.join('\r\n').match(qualityStrRegx);
|
||||
if(qualityStrMatch){
|
||||
plLayersStr.push(qualityStrAdd);
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.info(s.uri);
|
||||
}
|
||||
}
|
||||
|
||||
for(const s of mainServersList){
|
||||
if(plServerList.includes(s)){
|
||||
plServerList.splice(plServerList.indexOf(s), 1);
|
||||
plServerList.unshift(s);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const plSelectedServer = plServerList[data.x-1];
|
||||
const plSelectedList = plStreams[plSelectedServer];
|
||||
|
||||
plLayersStr.sort();
|
||||
if (log) {
|
||||
console.info(`Servers available:\n\t${plServerList.join('\n\t')}`);
|
||||
console.info(`Available qualities:\n\t${plLayersStr.join('\n\t')}`);
|
||||
}
|
||||
|
||||
const selectedQuality = data.q === 0 || data.q > Object.keys(plLayersRes).length
|
||||
? Object.keys(plLayersRes).pop() as string
|
||||
: data.q;
|
||||
const videoUrl = data.x < plServerList.length+1 && plSelectedList[selectedQuality] ? plSelectedList[selectedQuality] : '';
|
||||
|
||||
if(videoUrl != ''){
|
||||
if (log) {
|
||||
console.info(`Selected layer: ${selectedQuality} (${plLayersRes[selectedQuality].width}x${plLayersRes[selectedQuality].height}) @ ${plSelectedServer}`);
|
||||
console.info('Stream URL:',videoUrl);
|
||||
}
|
||||
|
||||
fnOutput = parseFileName(data.fileName, ([
|
||||
['episode', isNaN(parseInt(fnEpNum as string)) ? fnEpNum : parseInt(fnEpNum as string), true],
|
||||
['title', episode.title, true],
|
||||
['showTitle', episode.showTitle, true],
|
||||
['season', season, false],
|
||||
['width', plLayersRes[selectedQuality].width, false],
|
||||
['height', plLayersRes[selectedQuality].height, false],
|
||||
['service', 'Funimation', false]
|
||||
] as [AvailableFilenameVars, string|number, boolean][]).map((a): Variable => {
|
||||
return {
|
||||
name: a[0],
|
||||
replaceWith: a[1],
|
||||
type: typeof a[1],
|
||||
sanitize: a[2]
|
||||
} as Variable;
|
||||
}), data.numbers, data.override);
|
||||
if (fnOutput.length < 1)
|
||||
throw new Error(`Invalid path generated for input ${data.fileName}`);
|
||||
if (log)
|
||||
console.info(`Output filename: ${fnOutput.join(path.sep)}.ts`);
|
||||
}
|
||||
else if(data.x > plServerList.length){
|
||||
if (log)
|
||||
console.error('Server not selected!\n');
|
||||
return;
|
||||
}
|
||||
else{
|
||||
if (log)
|
||||
console.error('Layer not selected!\n');
|
||||
return;
|
||||
}
|
||||
|
||||
let dlFailed = false;
|
||||
let dlFailedA = false;
|
||||
|
||||
await fs.promises.mkdir(path.join(this.cfg.dir.content, ...fnOutput.slice(0, -1)), { recursive: true });
|
||||
|
||||
video: if (!data.novids) {
|
||||
if (plAud && (purvideo.length > 0 || audioAndVideo.length > 0)) {
|
||||
break video;
|
||||
} else if (!plAud && (audioAndVideo.some(a => a.lang === streamPath.lang) || puraudio.some(a => a.lang === streamPath.lang))) {
|
||||
break video;
|
||||
}
|
||||
// download video
|
||||
const reqVideo = await getData({
|
||||
url: videoUrl,
|
||||
debug: this.debug,
|
||||
});
|
||||
if (!reqVideo.ok || !reqVideo.res) { break video; }
|
||||
|
||||
const chunkList = m3u8(reqVideo.res.body);
|
||||
|
||||
const tsFile = path.join(this.cfg.dir.content, ...fnOutput.slice(0, -1), `${fnOutput.slice(-1)}.video${(plAud?.uri ? '' : '.' + streamPath.lang.code )}`);
|
||||
dlFailed = !await this.downloadFile(tsFile, chunkList, data.timeout, data.partsize, data.fsRetryTime, data.force, data.callbackMaker ? data.callbackMaker({
|
||||
fileName: `${fnOutput.slice(-1)}.video${(plAud?.uri ? '' : '.' + streamPath.lang.code )}.ts`,
|
||||
parent: {
|
||||
title: episode.showTitle
|
||||
},
|
||||
title: episode.title,
|
||||
image: episode.image,
|
||||
language: streamPath.lang,
|
||||
}) : undefined);
|
||||
if (!dlFailed) {
|
||||
if (plAud) {
|
||||
purvideo.push({
|
||||
path: `${tsFile}.ts`,
|
||||
lang: plAud.language
|
||||
});
|
||||
} else {
|
||||
audioAndVideo.push({
|
||||
path: `${tsFile}.ts`,
|
||||
lang: streamPath.lang
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
else{
|
||||
if (log)
|
||||
console.info('Skip video downloading...\n');
|
||||
}
|
||||
audio: if (plAud && !data.noaudio) {
|
||||
// download audio
|
||||
if (audioAndVideo.some(a => a.lang === plAud?.language) || puraudio.some(a => a.lang === plAud?.language))
|
||||
break audio;
|
||||
const reqAudio = await getData({
|
||||
url: plAud.uri,
|
||||
debug: this.debug,
|
||||
});
|
||||
if (!reqAudio.ok || !reqAudio.res) { return; }
|
||||
|
||||
const chunkListA = m3u8(reqAudio.res.body);
|
||||
|
||||
const tsFileA = path.join(this.cfg.dir.content, ...fnOutput.slice(0, -1), `${fnOutput.slice(-1)}.audio.${plAud.language.code}`);
|
||||
|
||||
dlFailedA = !await this.downloadFile(tsFileA, chunkListA, data.timeout, data.partsize, data.fsRetryTime, data.force, data.callbackMaker ? data.callbackMaker({
|
||||
fileName: `${fnOutput.slice(-1)}.audio.${plAud.language.code}.ts`,
|
||||
parent: {
|
||||
title: episode.showTitle
|
||||
},
|
||||
title: episode.title,
|
||||
image: episode.image,
|
||||
language: plAud.language
|
||||
}) : undefined);
|
||||
if (!dlFailedA)
|
||||
puraudio.push({
|
||||
path: `${tsFileA}.ts`,
|
||||
lang: plAud.language
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// add subs
|
||||
const subsExt = !data.mp4 || data.mp4 && data.ass ? '.ass' : '.srt';
|
||||
let addSubs = true;
|
||||
|
||||
// download subtitles
|
||||
if(stDlPath.length > 0){
|
||||
if (log)
|
||||
console.info('Downloading subtitles...');
|
||||
for (const subObject of stDlPath) {
|
||||
const subsSrc = await getData({
|
||||
url: subObject.url,
|
||||
debug: this.debug,
|
||||
});
|
||||
if(subsSrc.ok && subsSrc.res){
|
||||
const assData = vttConvert(subsSrc.res.body, (subsExt == '.srt' ? true : false), subObject.lang.name, data.fontSize, data.fontName);
|
||||
subObject.out = path.join(this.cfg.dir.content, ...fnOutput.slice(0, -1), `${fnOutput.slice(-1)}.subtitle${subObject.ext}${subsExt}`);
|
||||
fs.writeFileSync(subObject.out, assData);
|
||||
}
|
||||
else{
|
||||
if (log)
|
||||
console.error('Failed to download subtitles!');
|
||||
addSubs = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (addSubs && log)
|
||||
console.info('Subtitles downloaded!');
|
||||
}
|
||||
|
||||
if((puraudio.length < 1 && audioAndVideo.length < 1) || (purvideo.length < 1 && audioAndVideo.length < 1)){
|
||||
if (log)
|
||||
console.info('\nUnable to locate a video AND audio file\n');
|
||||
return;
|
||||
}
|
||||
|
||||
if(data.skipmux){
|
||||
if (log)
|
||||
console.info('Skipping muxing...');
|
||||
return;
|
||||
}
|
||||
|
||||
// check exec
|
||||
this.cfg.bin = await yamlCfg.loadBinCfg();
|
||||
const mergerBin = merger.checkMerger(this.cfg.bin, data.mp4, data.forceMuxer);
|
||||
|
||||
if ( data.novids ){
|
||||
if (log)
|
||||
console.info('Video not downloaded. Skip muxing video.');
|
||||
}
|
||||
|
||||
const ffext = !data.mp4 ? 'mkv' : 'mp4';
|
||||
const mergeInstance = new merger({
|
||||
onlyAudio: puraudio,
|
||||
onlyVid: purvideo,
|
||||
output: `${path.join(this.cfg.dir.content, ...fnOutput)}.${ffext}`,
|
||||
subtitles: stDlPath.map(a => {
|
||||
return {
|
||||
file: a.out as string,
|
||||
language: a.lang,
|
||||
title: a.lang.name,
|
||||
closedCaption: a.closedCaption
|
||||
};
|
||||
}),
|
||||
videoAndAudio: audioAndVideo,
|
||||
simul: data.simul,
|
||||
skipSubMux: data.skipSubMux,
|
||||
videoTitle: data.videoTitle,
|
||||
options: {
|
||||
ffmpeg: data.ffmpegOptions,
|
||||
mkvmerge: data.mkvmergeOptions
|
||||
},
|
||||
defaults: {
|
||||
audio: data.defaultAudio,
|
||||
sub: data.defaultSub
|
||||
},
|
||||
ccTag: data.ccTag
|
||||
});
|
||||
|
||||
if(mergerBin.MKVmerge){
|
||||
await mergeInstance.merge('mkvmerge', mergerBin.MKVmerge);
|
||||
}
|
||||
else if(mergerBin.FFmpeg){
|
||||
await mergeInstance.merge('ffmpeg', mergerBin.FFmpeg);
|
||||
}
|
||||
else{
|
||||
if (log)
|
||||
console.info('\nDone!\n');
|
||||
return true;
|
||||
}
|
||||
if (data.nocleanup) {
|
||||
return true;
|
||||
}
|
||||
|
||||
mergeInstance.cleanUp();
|
||||
if (log)
|
||||
console.info('\nDone!\n');
|
||||
return true;
|
||||
}
|
||||
|
||||
public async downloadFile(filename: string, chunkList: {
|
||||
segments: Record<string, unknown>[],
|
||||
}, timeout: number, partsize: number, fsRetryTime: number, override?: 'Y' | 'y' | 'N' | 'n' | 'C' | 'c', callback?: HLSCallback) {
|
||||
const downloadStatus = await new hlsDownload({
|
||||
m3u8json: chunkList,
|
||||
output: `${filename + '.ts'}`,
|
||||
timeout: timeout,
|
||||
threads: partsize,
|
||||
fsRetryTime: fsRetryTime * 1000,
|
||||
override,
|
||||
callback
|
||||
}).download();
|
||||
|
||||
return downloadStatus.ok;
|
||||
}
|
||||
|
||||
public async getSubsUrl(m: MediaChild[], parentLanguage: TitleElement|undefined, data: FuniSubsData, episodeID: string, ccTag: string) : Promise<Subtitle[]> {
|
||||
if((data.nosubs && !data.sub) || data.dlsubs.includes('none')){
|
||||
return [];
|
||||
}
|
||||
|
||||
const subs = await getData({
|
||||
baseUrl: 'https://playback.prd.funimationsvc.com/v1/play',
|
||||
url: `/${episodeID}`,
|
||||
token: this.token,
|
||||
useToken: true,
|
||||
debug: this.debug,
|
||||
querystring: { deviceType: 'web' }
|
||||
});
|
||||
if (!subs.ok || !subs.res || !subs.res.body) {
|
||||
console.error('Subtitle Request failed.');
|
||||
return [];
|
||||
}
|
||||
const parsed: SubtitleRequest = JSON.parse(subs.res.body);
|
||||
|
||||
const found: {
|
||||
isCC: boolean;
|
||||
url: string;
|
||||
lang: langsData.LanguageItem;
|
||||
}[] = parsed.primary.subtitles.filter(a => a.fileExt === 'vtt').map(subtitle => {
|
||||
return {
|
||||
isCC: subtitle.contentType === 'cc',
|
||||
url: subtitle.filePath,
|
||||
lang: langsData.languages.find(a => a.funi_locale === subtitle.languageCode || a.locale === subtitle.languageCode)
|
||||
};
|
||||
}).concat(m.filter(a => a.filePath.split('.').pop() === 'vtt').map(media => {
|
||||
const lang = langsData.languages.find(a => media.language === a.funi_name_lagacy || media.language === (a.funi_name || a.name));
|
||||
const pLang = langsData.languages.find(a => parentLanguage === a.funi_name_lagacy || (a.funi_name || a.name) === parentLanguage);
|
||||
return {
|
||||
isCC: pLang?.code === lang?.code,
|
||||
url: media.filePath,
|
||||
lang
|
||||
};
|
||||
})).filter((a) => a.lang !== undefined) as {
|
||||
isCC: boolean;
|
||||
url: string;
|
||||
lang: langsData.LanguageItem;
|
||||
}[];
|
||||
|
||||
const ret = found.filter(item => {
|
||||
return data.dlsubs.includes('all') || data.dlsubs.some(a => a === item.lang.locale);
|
||||
});
|
||||
|
||||
return ret.map(a => ({
|
||||
ext: `.${a.lang.code}${a.isCC ? `.${ccTag}` : ''}`,
|
||||
lang: a.lang,
|
||||
url: a.url,
|
||||
closedCaption: a.isCC
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
3
gui/react/.babelrc
Normal file
3
gui/react/.babelrc
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"presets": ["@babel/preset-env","@babel/preset-react", "@babel/preset-typescript"]
|
||||
}
|
||||
|
|
@ -1,39 +1,46 @@
|
|||
{
|
||||
"name": "react",
|
||||
"version": "0.1.0",
|
||||
"name": "anidl-gui",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@babel/core": ">=7.0.0-0 <8.0.0",
|
||||
"@babel/plugin-syntax-flow": "^7.14.5",
|
||||
"@babel/plugin-transform-react-jsx": "^7.14.9",
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@mui/icons-material": "^5.11.9",
|
||||
"@mui/lab": "^5.0.0-alpha.120",
|
||||
"@mui/material": "^5.11.9",
|
||||
"@types/node": "^18.14.0",
|
||||
"@types/react": "^18.0.25",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/styled": "^11.11.5",
|
||||
"@mui/icons-material": "^5.15.20",
|
||||
"@mui/lab": "^5.0.0-alpha.170",
|
||||
"@mui/material": "^5.15.20",
|
||||
"concurrently": "^8.2.2",
|
||||
"notistack": "^2.0.8",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"typescript": "^4.9.5",
|
||||
"uuid": "^9.0.0",
|
||||
"ws": "^8.12.1"
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"typescript": "^5.5.2",
|
||||
"uuid": "^9.0.1",
|
||||
"ws": "^8.17.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.24.7",
|
||||
"@babel/core": "^7.24.7",
|
||||
"@babel/preset-env": "^7.24.7",
|
||||
"@babel/preset-react": "^7.24.7",
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"@types/node": "^20.14.6",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"babel-loader": "^9.1.3",
|
||||
"css-loader": "^7.1.2",
|
||||
"html-webpack-plugin": "^5.6.0",
|
||||
"style-loader": "^3.3.4",
|
||||
"ts-node": "^10.9.2",
|
||||
"webpack": "^5.92.1",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^5.0.4"
|
||||
},
|
||||
"proxy": "http://localhost:3000",
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
"build": "npx tsc && npx webpack",
|
||||
"start": "npx concurrently -k npm:frontend npm:backend",
|
||||
"frontend": "npx webpack-dev-server",
|
||||
"backend": "npx ts-node -T ../../gui.ts"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
|
@ -46,8 +53,5 @@
|
|||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/uuid": "^9.0.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10631
gui/react/pnpm-lock.yaml
10631
gui/react/pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -23,11 +23,11 @@ const Layout: React.FC = () => {
|
|||
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>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ const makeTheme = (mode: 'dark'|'light') : Partial<Theme> => {
|
|||
|
||||
const Style: FCWithChildren = ({children}) => {
|
||||
return <ThemeProvider theme={makeTheme('dark')}>
|
||||
<Box sx={{ }}/>
|
||||
{children}
|
||||
<Box sx={{ }}/>
|
||||
{children}
|
||||
</ThemeProvider>;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Box, Button, Divider, InputBase, Link, MenuItem, Select, TextField, Tooltip, Typography } from '@mui/material';
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import { Box, Button, Divider, FormControl, InputBase, InputLabel, Link, MenuItem, Select, TextField, Tooltip, Typography } from '@mui/material';
|
||||
import useStore from '../../../hooks/useStore';
|
||||
import MultiSelect from '../../reusable/MultiSelect';
|
||||
import { messageChannelContext } from '../../../provider/MessageChannel';
|
||||
|
|
@ -18,6 +18,8 @@ const DownloadSelector: React.FC<DownloadSelectorProps> = ({ onFinish }) => {
|
|||
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 () => {
|
||||
|
|
@ -84,232 +86,240 @@ const DownloadSelector: React.FC<DownloadSelectorProps> = ({ onFinish }) => {
|
|||
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={
|
||||
<Typography>
|
||||
Currently only supported on Hidive
|
||||
</Typography>
|
||||
}
|
||||
arrow
|
||||
placement='top'>
|
||||
<Button sx={{ textTransform: 'none'}} onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, simul: !store.downloadOptions.simul } })} variant={store.downloadOptions.simul ? 'contained' : 'outlined'}>Download Simulcast ver.</Button>
|
||||
</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={{
|
||||
width: '50rem',
|
||||
height: '21rem',
|
||||
margin: '10px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1px'
|
||||
justifyContent: 'space-between',
|
||||
//backgroundColor: '#ffffff30',
|
||||
}}>
|
||||
<Box sx={{
|
||||
borderColor: '#595959',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: '1px',
|
||||
borderRadius: '5px',
|
||||
//backgroundColor: '#ff4567',
|
||||
width: '15rem',
|
||||
height: '3.5rem',
|
||||
display: 'flex',
|
||||
'&:hover' : {
|
||||
borderColor: '#ffffff',
|
||||
},
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '0.7rem',
|
||||
//backgroundColor: '#ff000030'
|
||||
}}>
|
||||
<InputBase sx={{
|
||||
ml: 2,
|
||||
flex: 1,
|
||||
}}
|
||||
disabled={store.downloadOptions.all} value={store.downloadOptions.e} required onChange={e => {
|
||||
<Typography sx={{fontSize: '1.4rem'}}>
|
||||
General Options
|
||||
</Typography>
|
||||
<TextField value={store.downloadOptions.id} required onChange={e => {
|
||||
dispatch({
|
||||
type: 'downloadOptions',
|
||||
payload: { ...store.downloadOptions, e: e.target.value }
|
||||
payload: { ...store.downloadOptions, id: 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={
|
||||
}} 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>
|
||||
Comming Soon™
|
||||
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={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
gap: '1rem'
|
||||
}}>
|
||||
|
||||
<Box sx={{
|
||||
borderColor: '#595959',
|
||||
borderStyle: 'solid',
|
||||
borderWidth: '1px',
|
||||
borderRadius: '5px',
|
||||
//backgroundColor: '#ff4567',
|
||||
width: '15rem',
|
||||
height: '3.5rem',
|
||||
display: 'flex',
|
||||
'&:hover' : {
|
||||
borderColor: '#ffffff',
|
||||
},
|
||||
}}>
|
||||
|
||||
<Button sx={{ textTransform: 'none' }} variant='outlined' disabled={true}>Hardsub</Button>
|
||||
<Divider orientation='vertical'/>
|
||||
<Select sx={{
|
||||
flex: 1
|
||||
}}
|
||||
title='Hardsub lang.'
|
||||
placeholder='Hardsub lang.'
|
||||
disabled={true}
|
||||
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>Deutsch</MenuItem>
|
||||
</Select>
|
||||
<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>
|
||||
<Tooltip title={
|
||||
<Typography>
|
||||
Burns the selected subtitle <b>PERMANENTLY</b> onto the video<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>
|
||||
<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>
|
||||
</Typography>
|
||||
} arrow placement='top'>
|
||||
<Link href='https://github.com/anidl/multi-downloader-nx/blob/master/docs/DOCUMENTATION.md#filename-template' rel="noopener noreferrer" target="_blank">
|
||||
<InfoOutlinedIcon sx={{
|
||||
transition: '100ms',
|
||||
'&:hover' : {
|
||||
color: '#ffffff30',
|
||||
}
|
||||
}} />
|
||||
</Link>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{width: '95%', height: '0.3rem', backgroundColor: '#ffffff50', borderRadius: '10px', marginTop: '10px'}}/>
|
||||
|
||||
<LoadingButton sx={{ margin: '15px', textTransform: 'none' }} loading={loading} onClick={addToQueue} variant='contained'>Add to Queue</LoadingButton>
|
||||
<LoadingButton sx={{ margin: '15px', textTransform: 'none' }} loading={loading} onClick={addToQueue} variant='contained'>Add to Queue</LoadingButton>
|
||||
|
||||
</Box>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@ const EpisodeListing: React.FC = () => {
|
|||
});
|
||||
};
|
||||
|
||||
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' }}>
|
||||
|
|
@ -68,28 +72,28 @@ const EpisodeListing: React.FC = () => {
|
|||
if (selected.length > 0) {
|
||||
setSelected([]);
|
||||
} else {
|
||||
setSelected(store.episodeListing.map(a => a.e));
|
||||
setSelected(getEpisodesForSeason(season).map(a => a.e));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
{store.episodeListing.filter((a) => season === 'all' ? true : a.season === season).map((item, index, { length }) => {
|
||||
{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 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));
|
||||
}}>
|
||||
<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}
|
||||
|
|
@ -133,9 +137,9 @@ const EpisodeListing: React.FC = () => {
|
|||
await navigator.clipboard.writeText(item.description!);
|
||||
enqueueSnackbar('Copied summary to clipboard', {
|
||||
variant: 'info'
|
||||
})
|
||||
});
|
||||
},
|
||||
text: "Copy summary to clipboard"
|
||||
text: 'Copy summary to clipboard'
|
||||
}
|
||||
]} popupItem={summaryRef} />
|
||||
{index < length - 1 && <Divider />}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ const SearchBox: React.FC = () => {
|
|||
s.value = s.value.slice(0, 10);
|
||||
setSearchResult(s);
|
||||
}
|
||||
}, 1000);
|
||||
}, 500);
|
||||
return () => clearTimeout(timeOutId);
|
||||
}, [search]);
|
||||
|
||||
|
|
@ -100,9 +100,9 @@ const SearchBox: React.FC = () => {
|
|||
await navigator.clipboard.writeText(a.desc!);
|
||||
enqueueSnackbar('Copied summary to clipboard', {
|
||||
variant: 'info'
|
||||
})
|
||||
});
|
||||
},
|
||||
text: "Copy summary to clipboard"
|
||||
text: 'Copy summary to clipboard'
|
||||
}
|
||||
]} popupItem={summaryRef} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,113 +17,9 @@ const Queue: React.FC = () => {
|
|||
|
||||
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',
|
||||
width: '100%',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
|
|
@ -137,104 +33,208 @@ const Queue: React.FC = () => {
|
|||
boxShadow: '0px 0px 50px #00000090',
|
||||
borderRadius: '10px',
|
||||
display: 'flex',
|
||||
overflow: 'hidden',
|
||||
transition: '250ms'
|
||||
}}>
|
||||
<img style={{
|
||||
}}>
|
||||
<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'
|
||||
src={data.downloadInfo.image} height='auto' width='auto' alt="Thumbnail" />
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: '1.3rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
0 / ? parts (0% | XX:XX | 0 MB/s | 0MB)
|
||||
</Typography>
|
||||
<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>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
{queue.map((queueItem, index, { length }) => {
|
||||
|
|
@ -255,7 +255,7 @@ const Queue: React.FC = () => {
|
|||
borderRadius: '10px',
|
||||
display: 'flex',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
}}>
|
||||
<img style={{
|
||||
borderRadius: '5px',
|
||||
margin: '5px',
|
||||
|
|
@ -269,102 +269,102 @@ const Queue: React.FC = () => {
|
|||
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',
|
||||
}}>
|
||||
<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',
|
||||
<Typography color='text.primary' sx={{
|
||||
fontSize: '1.8rem',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
justifyContent: 'space-between',
|
||||
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',
|
||||
}}>
|
||||
<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'
|
||||
</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',
|
||||
}
|
||||
}}>
|
||||
onClick={() => {
|
||||
msg.removeFromQueue(index);
|
||||
}}
|
||||
sx={{
|
||||
backgroundColor: '#ff573a25',
|
||||
height: '40px',
|
||||
transition: '250ms',
|
||||
'&:hover' : {
|
||||
backgroundColor: '#ff573a',
|
||||
}
|
||||
}}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
;
|
||||
})}
|
||||
</> : <Box sx={{
|
||||
|
|
@ -383,12 +383,12 @@ const Queue: React.FC = () => {
|
|||
<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>
|
||||
|
|
@ -396,12 +396,12 @@ const Queue: React.FC = () => {
|
|||
<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>
|
||||
|
|
|
|||
|
|
@ -2,33 +2,38 @@ import { Box, Button, Menu, MenuItem, Typography } from '@mui/material';
|
|||
import React from 'react';
|
||||
import { messageChannelContext } from '../../provider/MessageChannel';
|
||||
import useStore from '../../hooks/useStore';
|
||||
import { StoreState } from '../../provider/Store'
|
||||
import { StoreState } from '../../provider/Store';
|
||||
|
||||
const MenuBar: React.FC = () => {
|
||||
const [ openMenu, setMenuOpen ] = React.useState<'settings'|'help'|undefined>();
|
||||
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 getVersion = async() => {
|
||||
dispatch({
|
||||
type: 'version',
|
||||
payload: await messageChannel?.version()
|
||||
});
|
||||
}
|
||||
|
||||
getVersion();
|
||||
|
||||
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 'funi':
|
||||
return "Funimation"
|
||||
case "hidive":
|
||||
return "Hidive"
|
||||
case 'crunchy':
|
||||
return 'Crunchyroll';
|
||||
case 'hidive':
|
||||
return 'Hidive';
|
||||
case 'ao':
|
||||
return 'AnimeOnegai';
|
||||
case 'adn':
|
||||
return 'AnimationDigitalNetwork';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const msg = React.useContext(messageChannelContext);
|
||||
|
||||
|
|
@ -46,12 +51,12 @@ const MenuBar: React.FC = () => {
|
|||
|
||||
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')}>
|
||||
<Button onClick={(e) => handleClick(e, 'settings')}>
|
||||
Settings
|
||||
</Button>
|
||||
<Button onClick={(e) => handleClick(e, 'help')}>
|
||||
</Button>
|
||||
<Button onClick={(e) => handleClick(e, 'help')}>
|
||||
Help
|
||||
</Button>
|
||||
</Button>
|
||||
</Box>
|
||||
<Menu open={openMenu === 'settings'} anchorEl={anchorEl} onClose={handleClose}>
|
||||
<MenuItem onClick={() => {
|
||||
|
|
@ -84,7 +89,7 @@ const MenuBar: React.FC = () => {
|
|||
msg.openURL('https://github.com/anidl/multi-downloader-nx');
|
||||
handleClose();
|
||||
}}>
|
||||
GitHub
|
||||
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');
|
||||
|
|
@ -116,4 +121,4 @@ const MenuBar: React.FC = () => {
|
|||
</Box>;
|
||||
};
|
||||
|
||||
export default MenuBar;
|
||||
export default MenuBar;
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ const MultiSelect: React.FC<MultiSelectProps> = (props) => {
|
|||
const theme = useTheme();
|
||||
|
||||
return <div>
|
||||
<FormControl sx={{ m: 1, width: 300 }}>
|
||||
<FormControl sx={{ width: 300 }}>
|
||||
<InputLabel id="multi-select-label">{props.title}</InputLabel>
|
||||
<Select
|
||||
labelId="multi-select-label"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import Store from './provider/Store';
|
|||
import ErrorHandler from './provider/ErrorHandler';
|
||||
import QueueProvider from './provider/QueueProvider';
|
||||
|
||||
document.body.style.backgroundColor = "rgb(0, 30, 60)";
|
||||
document.body.style.backgroundColor = 'rgb(0, 30, 60)';
|
||||
document.body.style.display = 'flex';
|
||||
document.body.style.justifyContent = 'center';
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export default class ErrorHandler extends React.Component<{
|
|||
<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 => {
|
||||
{this.state.error.stack.componentStack?.split('\n').map(a => {
|
||||
return <>
|
||||
{a}
|
||||
<br/>
|
||||
|
|
|
|||
|
|
@ -15,11 +15,11 @@ export class RandomEventHandler {
|
|||
private handler: {
|
||||
[eventName in keyof RandomEvents]: Handler<eventName>[]
|
||||
} = {
|
||||
progress: [],
|
||||
finish: [],
|
||||
queueChange: [],
|
||||
current: []
|
||||
};
|
||||
progress: [],
|
||||
finish: [],
|
||||
queueChange: [],
|
||||
current: []
|
||||
};
|
||||
|
||||
public on<T extends keyof RandomEvents>(name: T, listener: Handler<T>) {
|
||||
if (Object.prototype.hasOwnProperty.call(this.handler, name)) {
|
||||
|
|
@ -50,14 +50,14 @@ async function messageAndResponse<T extends keyof MessageTypes>(socket: WebSocke
|
|||
resolve(parsed);
|
||||
}
|
||||
};
|
||||
socket.addEventListener('message', handler);
|
||||
socket.addEventListener('message', handler);
|
||||
});
|
||||
const toSend = msg as WSMessageWithID<T>;
|
||||
toSend.id = id;
|
||||
|
||||
socket.send(JSON.stringify(toSend));
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
const MessageChannelProvider: FCWithChildren = ({ children }) => {
|
||||
|
||||
|
|
@ -70,7 +70,7 @@ const MessageChannelProvider: FCWithChildren = ({ children }) => {
|
|||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
React.useEffect(() => {
|
||||
const wss = new WebSocket(`ws://${process.env.NODE_ENV === 'development' ? 'localhost:3000' : window.location.host}/public`);
|
||||
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);
|
||||
});
|
||||
|
|
@ -103,7 +103,7 @@ const MessageChannelProvider: FCWithChildren = ({ children }) => {
|
|||
});
|
||||
}
|
||||
|
||||
const wws = new WebSocket(`ws://${process.env.NODE_ENV === 'development' ? 'localhost:3000' : window.location.host}/ws?${search}`, );
|
||||
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);
|
||||
|
|
@ -146,7 +146,7 @@ const MessageChannelProvider: FCWithChildren = ({ children }) => {
|
|||
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)
|
||||
if (store.service !== currentService.data)
|
||||
messageAndResponse(socket, { name: 'setup', data: store.service });
|
||||
})();
|
||||
}, [store.service, dispatch, socket]);
|
||||
|
|
@ -212,7 +212,7 @@ const MessageChannelProvider: FCWithChildren = ({ children }) => {
|
|||
}
|
||||
|
||||
const messageHandler: FrontEndMessages = {
|
||||
name: "default",
|
||||
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,
|
||||
|
|
@ -241,4 +241,4 @@ const MessageChannelProvider: FCWithChildren = ({ children }) => {
|
|||
</messageChannelContext.Provider>;
|
||||
};
|
||||
|
||||
export default MessageChannelProvider;
|
||||
export default MessageChannelProvider;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import {Divider, Box, Button, Typography, Avatar} from '@mui/material';
|
|||
import useStore from '../hooks/useStore';
|
||||
import { StoreState } from './Store';
|
||||
|
||||
type Services = 'funi'|'crunchy'|'hidive';
|
||||
type Services = 'crunchy'|'hidive'|'ao'|'adn';
|
||||
|
||||
export const serviceContext = React.createContext<Services|undefined>(undefined);
|
||||
|
||||
|
|
@ -21,9 +21,10 @@ const ServiceProvider: FCWithChildren = ({ children }) => {
|
|||
<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('funi')} startIcon={<Avatar src={'https://static.funimation.com/static/img/favicon.ico'} />}>Funimation</Button>
|
||||
<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://www.hidive.com/favicon.ico'} />}>Hidive</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.de/favicon.ico'} />}>AnimationDigitalNetwork</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
: <serviceContext.Provider value={service}>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export type DownloadOptions = {
|
|||
export type StoreState = {
|
||||
episodeListing: Episode[];
|
||||
downloadOptions: DownloadOptions,
|
||||
service: 'crunchy'|'funi'|'hidive'|undefined,
|
||||
service: 'crunchy'|'hidive'|'ao'|'adn'|undefined,
|
||||
version: string,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./build",
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
|
|
@ -13,15 +14,16 @@
|
|||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
//"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"downlevelIteration": true
|
||||
},
|
||||
"include": [
|
||||
"./src"
|
||||
"./src",
|
||||
"./webpack.config.ts"
|
||||
]
|
||||
}
|
||||
58
gui/react/webpack.config.ts
Normal file
58
gui/react/webpack.config.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import type { Configuration } from 'webpack';
|
||||
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
||||
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(__dirname, './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(__dirname, 'public', 'index.html')
|
||||
})
|
||||
]
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
@ -4,8 +4,9 @@ import { IncomingMessage } from 'http';
|
|||
import { MessageHandler, GuiState } from '../../@types/messageHandler';
|
||||
import { setState, getState, writeYamlCfgFile } from '../../modules/module.cfg-loader';
|
||||
import CrunchyHandler from './services/crunchyroll';
|
||||
import FunimationHandler from './services/funimation';
|
||||
import HidiveHandler from './services/hidive';
|
||||
import AnimeOnegaiHandler from './services/animeonegai';
|
||||
import ADNHandler from './services/adn';
|
||||
import WebSocketHandler from './websocket';
|
||||
import packageJson from '../../package.json';
|
||||
|
||||
|
|
@ -31,12 +32,14 @@ export default class ServiceHandler {
|
|||
});
|
||||
|
||||
this.ws.events.on('setup', ({ data }) => {
|
||||
if (data === 'funi') {
|
||||
this.service = new FunimationHandler(this.ws);
|
||||
} else if (data === 'crunchy') {
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -55,7 +58,7 @@ export default class ServiceHandler {
|
|||
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'|'funi'));
|
||||
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') });
|
||||
|
|
|
|||
139
gui/server/services/adn.ts
Normal file
139
gui/server/services/adn.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import { AuthData, CheckTokenResponse, DownloadData, EpisodeListResponse, MessageHandler, ResolveItemsData, SearchData, SearchResponse } from '../../../@types/messageHandler';
|
||||
import AnimationDigitalNetwork from '../../../adn';
|
||||
import { getDefault } from '../../../modules/module.args';
|
||||
import { languages } from '../../../modules/module.langsData';
|
||||
import WebSocketHandler from '../websocket';
|
||||
import Base from './base';
|
||||
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();
|
||||
}
|
||||
|
||||
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 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 handleDefault(name: string) {
|
||||
return getDefault(name, this.adn.cfg.cli);
|
||||
}
|
||||
|
||||
public async availableDubCodes(): Promise<string[]> {
|
||||
const dubLanguageCodesArray: string[] = [];
|
||||
for(const language of languages){
|
||||
if (language.adn_locale)
|
||||
dubLanguageCodesArray.push(language.code);
|
||||
}
|
||||
return [...new Set(dubLanguageCodesArray)];
|
||||
}
|
||||
|
||||
public async availableSubCodes(): Promise<string[]> {
|
||||
const subLanguageCodesArray: string[] = [];
|
||||
for(const language of languages){
|
||||
if (language.adn_locale)
|
||||
subLanguageCodesArray.push(language.locale);
|
||||
}
|
||||
return ['all', 'none', ...new Set(subLanguageCodesArray)];
|
||||
}
|
||||
|
||||
public async resolveItems(data: ResolveItemsData): Promise<boolean> {
|
||||
const parse = parseInt(data.id);
|
||||
if (isNaN(parse) || parse <= 0)
|
||||
return false;
|
||||
console.debug(`Got resolve options: ${JSON.stringify(data)}`);
|
||||
const res = await this.adn.selectShow(parseInt(data.id), data.e, data.but, data.all);
|
||||
if (!res.isOk || !res.value)
|
||||
return res.isOk;
|
||||
this.addToQueue(res.value.map(a => {
|
||||
return {
|
||||
...data,
|
||||
ids: [a.id],
|
||||
title: a.title,
|
||||
parent: {
|
||||
title: a.show.shortTitle,
|
||||
season: a.season
|
||||
},
|
||||
e: a.shortNumber,
|
||||
image: a.image,
|
||||
episode: a.shortNumber
|
||||
};
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
public async listEpisodes(id: string): Promise<EpisodeListResponse> {
|
||||
const parse = parseInt(id);
|
||||
if (isNaN(parse) || parse <= 0)
|
||||
return { isOk: false, reason: new Error('The ID is invalid') };
|
||||
|
||||
const request = await this.adn.listShow(parse);
|
||||
if (!request.isOk || !request.value)
|
||||
return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')};
|
||||
|
||||
return { isOk: true, value: request.value.videos.map(function(item) {
|
||||
return {
|
||||
e: item.shortNumber,
|
||||
lang: [],
|
||||
name: item.title,
|
||||
season: item.season,
|
||||
seasonTitle: item.show.title,
|
||||
episode: item.shortNumber,
|
||||
id: item.id+'',
|
||||
img: item.image,
|
||||
description: item.summary,
|
||||
time: item.duration+''
|
||||
};
|
||||
})};
|
||||
}
|
||||
|
||||
public async downloadItem(data: DownloadData) {
|
||||
this.setDownloading(true);
|
||||
console.debug(`Got download options: ${JSON.stringify(data)}`);
|
||||
const _default = yargs.appArgv(this.adn.cfg.cli, true);
|
||||
const res = await this.adn.selectShow(parseInt(data.id), data.e, false, false);
|
||||
if (res.isOk) {
|
||||
for (const select of res.value) {
|
||||
if (!(await this.adn.getEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y',
|
||||
novids: data.novids, noaudio: data.noaudio, hslang: data.hslang || 'none', dubLang: data.dubLang }))) {
|
||||
const er = new Error(`Unable to download episode ${data.e} from ${data.id}`);
|
||||
er.name = 'Download error';
|
||||
this.alertError(er);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.alertError(new Error('Failed to download episode, check for additional logs.'));
|
||||
}
|
||||
this.sendMessage({ name: 'finish', data: undefined });
|
||||
this.setDownloading(false);
|
||||
this.onFinish();
|
||||
}
|
||||
}
|
||||
|
||||
export default ADNHandler;
|
||||
151
gui/server/services/animeonegai.ts
Normal file
151
gui/server/services/animeonegai.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
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';
|
||||
import WebSocketHandler from '../websocket';
|
||||
import Base from './base';
|
||||
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();
|
||||
}
|
||||
|
||||
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 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 handleDefault(name: string) {
|
||||
return getDefault(name, this.ao.cfg.cli);
|
||||
}
|
||||
|
||||
public async availableDubCodes(): Promise<string[]> {
|
||||
const dubLanguageCodesArray: string[] = [];
|
||||
for(const language of languages){
|
||||
if (language.ao_locale)
|
||||
dubLanguageCodesArray.push(language.code);
|
||||
}
|
||||
return [...new Set(dubLanguageCodesArray)];
|
||||
}
|
||||
|
||||
public async availableSubCodes(): Promise<string[]> {
|
||||
const subLanguageCodesArray: string[] = [];
|
||||
for(const language of languages){
|
||||
if (language.ao_locale)
|
||||
subLanguageCodesArray.push(language.locale);
|
||||
}
|
||||
return ['all', 'none', ...new Set(subLanguageCodesArray)];
|
||||
}
|
||||
|
||||
public async resolveItems(data: ResolveItemsData): Promise<boolean> {
|
||||
const parse = parseInt(data.id);
|
||||
if (isNaN(parse) || parse <= 0)
|
||||
return false;
|
||||
console.debug(`Got resolve options: ${JSON.stringify(data)}`);
|
||||
const _default = yargs.appArgv(this.ao.cfg.cli, true);
|
||||
const res = await this.ao.selectShow(parseInt(data.id), data.e, data.but, data.all, _default);
|
||||
if (!res.isOk || !res.value)
|
||||
return res.isOk;
|
||||
this.addToQueue(res.value.map(a => {
|
||||
return {
|
||||
...data,
|
||||
ids: a.data.map(a => a.videoId),
|
||||
title: a.episodeTitle,
|
||||
parent: {
|
||||
title: a.seasonTitle,
|
||||
season: a.seasonTitle
|
||||
},
|
||||
e: a.episodeNumber+'',
|
||||
image: a.image,
|
||||
episode: a.episodeNumber+''
|
||||
};
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
public async listEpisodes(id: string): Promise<EpisodeListResponse> {
|
||||
const parse = parseInt(id);
|
||||
if (isNaN(parse) || parse <= 0)
|
||||
return { isOk: false, reason: new Error('The ID is invalid') };
|
||||
|
||||
const request = await this.ao.listShow(parse);
|
||||
if (!request.isOk || !request.value)
|
||||
return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')};
|
||||
|
||||
const episodes: Episode[] = [];
|
||||
const seasonNumberTitleParse = request.series.data.title.match(/\d+$/);
|
||||
const seasonNumber = seasonNumberTitleParse ? parseInt(seasonNumberTitleParse[0]) : 1;
|
||||
//request.value
|
||||
for (const episodeKey in request.value) {
|
||||
const episode = request.value[episodeKey][0];
|
||||
const langs = Array.from(new Set(request.value[episodeKey].map(a=>a.lang)));
|
||||
episodes.push({
|
||||
e: episode.number+'',
|
||||
lang: langs as string[],
|
||||
name: episode.name,
|
||||
season: seasonNumber+'',
|
||||
seasonTitle: '',
|
||||
episode: episode.number+'',
|
||||
id: episode.video_entry+'',
|
||||
img: episode.thumbnail,
|
||||
description: episode.description,
|
||||
time: ''
|
||||
});
|
||||
}
|
||||
return { isOk: true, value: episodes };
|
||||
}
|
||||
|
||||
public async downloadItem(data: DownloadData) {
|
||||
this.setDownloading(true);
|
||||
console.debug(`Got download options: ${JSON.stringify(data)}`);
|
||||
const _default = yargs.appArgv(this.ao.cfg.cli, true);
|
||||
const res = await this.ao.selectShow(parseInt(data.id), data.e, false, false, {
|
||||
..._default,
|
||||
dubLang: data.dubLang,
|
||||
e: data.e
|
||||
});
|
||||
if (res.isOk) {
|
||||
for (const select of res.value) {
|
||||
if (!(await this.ao.downloadEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y',
|
||||
novids: data.novids, noaudio: data.noaudio, hslang: data.hslang || 'none', dubLang: data.dubLang }))) {
|
||||
const er = new Error(`Unable to download episode ${data.e} from ${data.id}`);
|
||||
er.name = 'Download error';
|
||||
this.alertError(er);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.alertError(new Error('Failed to download episode, check for additional logs.'));
|
||||
}
|
||||
this.sendMessage({ name: 'finish', data: undefined });
|
||||
this.setDownloading(false);
|
||||
this.onFinish();
|
||||
}
|
||||
}
|
||||
|
||||
export default AnimeOnegaiHandler;
|
||||
|
|
@ -15,9 +15,17 @@ class CrunchyHandler extends Base implements MessageHandler {
|
|||
this.crunchy = new Crunchy();
|
||||
this.crunchy.refreshToken();
|
||||
this.initState();
|
||||
this.getDefaults();
|
||||
}
|
||||
|
||||
public getDefaults() {
|
||||
const _default = yargs.appArgv(this.crunchy.cfg.cli, true);
|
||||
this.crunchy.api = _default.crapi;
|
||||
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 };
|
||||
}
|
||||
|
|
@ -40,6 +48,7 @@ class CrunchyHandler extends Base implements MessageHandler {
|
|||
}
|
||||
|
||||
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);
|
||||
|
|
@ -64,7 +73,9 @@ class CrunchyHandler extends Base implements MessageHandler {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
|
@ -87,6 +98,7 @@ class CrunchyHandler extends Base implements MessageHandler {
|
|||
}
|
||||
|
||||
public async downloadItem(data: DownloadData) {
|
||||
this.getDefaults();
|
||||
await this.crunchy.refreshToken(true);
|
||||
console.debug(`Got download options: ${JSON.stringify(data)}`);
|
||||
this.setDownloading(true);
|
||||
|
|
@ -99,7 +111,7 @@ class CrunchyHandler extends Base implements MessageHandler {
|
|||
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, hslang: data.hslang || 'none' }))) {
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -1,120 +0,0 @@
|
|||
import { AuthData, CheckTokenResponse, EpisodeListResponse, MessageHandler, QueueItem, ResolveItemsData, SearchData, SearchResponse } from '../../../@types/messageHandler';
|
||||
import Funimation from '../../../funi';
|
||||
import { getDefault } from '../../../modules/module.args';
|
||||
import { languages, subtitleLanguagesFilter } from '../../../modules/module.langsData';
|
||||
import WebSocketHandler from '../websocket';
|
||||
import Base from './base';
|
||||
import { console } from '../../../modules/log';
|
||||
import * as yargs from '../../../modules/module.app-args';
|
||||
|
||||
class FunimationHandler extends Base implements MessageHandler {
|
||||
private funi: Funimation;
|
||||
public name = 'funi';
|
||||
constructor(ws: WebSocketHandler) {
|
||||
super(ws);
|
||||
this.funi = new Funimation();
|
||||
this.initState();
|
||||
}
|
||||
|
||||
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.funi.listShowItems(parse);
|
||||
if (!request.isOk)
|
||||
return request;
|
||||
return { isOk: true, value: request.value.map(item => ({
|
||||
e: item.id_split.join(''),
|
||||
lang: item.audio ?? [],
|
||||
name: item.title,
|
||||
season: item.seasonNum ?? item.seasonTitle ?? item.item.seasonNum ?? item.item.seasonTitle,
|
||||
seasonTitle: item.seasonTitle,
|
||||
episode: item.episodeNum,
|
||||
id: item.id,
|
||||
img: item.thumb,
|
||||
description: item.synopsis,
|
||||
time: item.runtime ?? item.item.runtime
|
||||
})) };
|
||||
}
|
||||
|
||||
public async handleDefault(name: string) {
|
||||
return getDefault(name, this.funi.cfg.cli);
|
||||
}
|
||||
|
||||
public async availableDubCodes(): Promise<string[]> {
|
||||
const dubLanguageCodesArray: string[] = [];
|
||||
for(const language of languages){
|
||||
if (language.funi_locale)
|
||||
dubLanguageCodesArray.push(language.code);
|
||||
}
|
||||
return [...new Set(dubLanguageCodesArray)];
|
||||
}
|
||||
|
||||
public async availableSubCodes(): Promise<string[]> {
|
||||
return subtitleLanguagesFilter;
|
||||
}
|
||||
|
||||
public async resolveItems(data: ResolveItemsData): Promise<boolean> {
|
||||
console.debug(`Got resolve options: ${JSON.stringify(data)}`);
|
||||
const res = await this.funi.getShow(false, { ...data, id: parseInt(data.id) });
|
||||
if (!res.isOk)
|
||||
return res.isOk;
|
||||
this.addToQueue(res.value.map(a => {
|
||||
return {
|
||||
...data,
|
||||
ids: [a.episodeID],
|
||||
title: a.title,
|
||||
parent: {
|
||||
title: a.seasonTitle,
|
||||
season: a.seasonNumber
|
||||
},
|
||||
image: a.image,
|
||||
e: a.episodeID,
|
||||
episode: a.epsiodeNumber
|
||||
};
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
public async search(data: SearchData): Promise<SearchResponse> {
|
||||
console.debug(`Got search options: ${JSON.stringify(data)}`);
|
||||
const funiSearch = await this.funi.searchShow(false, data);
|
||||
if (!funiSearch.isOk)
|
||||
return funiSearch;
|
||||
return { isOk: true, value: funiSearch.value.items.hits.map(a => ({
|
||||
image: a.image.showThumbnail,
|
||||
name: a.title,
|
||||
desc: a.description,
|
||||
id: a.id,
|
||||
lang: a.languages,
|
||||
rating: a.starRating
|
||||
})) };
|
||||
}
|
||||
|
||||
public async checkToken(): Promise<CheckTokenResponse> {
|
||||
return this.funi.checkToken();
|
||||
}
|
||||
|
||||
public auth(data: AuthData) {
|
||||
return this.funi.auth(data);
|
||||
}
|
||||
|
||||
public async downloadItem(data: QueueItem) {
|
||||
this.setDownloading(true);
|
||||
console.debug(`Got download options: ${JSON.stringify(data)}`);
|
||||
const res = await this.funi.getShow(false, { all: false, but: false, id: parseInt(data.id), e: data.e });
|
||||
const _default = yargs.appArgv(this.funi.cfg.cli, true);
|
||||
if (!res.isOk)
|
||||
return this.alertError(res.reason);
|
||||
|
||||
for (const ep of res.value) {
|
||||
await this.funi.getEpisode(false, { dubLang: data.dubLang, fnSlug: ep, s: data.id, subs: { dlsubs: data.dlsubs, sub: false, ccTag: _default.ccTag } }, { ..._default, callbackMaker: this.makeProgressHandler.bind(this), ass: true, 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 FunimationHandler;
|
||||
|
|
@ -13,11 +13,10 @@ class HidiveHandler extends Base implements MessageHandler {
|
|||
constructor(ws: WebSocketHandler) {
|
||||
super(ws);
|
||||
this.hidive = new Hidive();
|
||||
this.hidive.doInit();
|
||||
this.initState();
|
||||
}
|
||||
|
||||
public auth(data: AuthData) {
|
||||
public async auth(data: AuthData) {
|
||||
return this.hidive.doAuth(data);
|
||||
}
|
||||
|
||||
|
|
@ -42,7 +41,7 @@ class HidiveHandler extends Base implements MessageHandler {
|
|||
public async availableDubCodes(): Promise<string[]> {
|
||||
const dubLanguageCodesArray: string[] = [];
|
||||
for(const language of languages){
|
||||
if (language.hd_locale)
|
||||
if (language.new_hd_locale)
|
||||
dubLanguageCodesArray.push(language.code);
|
||||
}
|
||||
return [...new Set(dubLanguageCodesArray)];
|
||||
|
|
@ -51,7 +50,7 @@ class HidiveHandler extends Base implements MessageHandler {
|
|||
public async availableSubCodes(): Promise<string[]> {
|
||||
const subLanguageCodesArray: string[] = [];
|
||||
for(const language of languages){
|
||||
if (language.hd_locale)
|
||||
if (language.new_hd_locale)
|
||||
subLanguageCodesArray.push(language.locale);
|
||||
}
|
||||
return ['all', 'none', ...new Set(subLanguageCodesArray)];
|
||||
|
|
@ -62,21 +61,21 @@ class HidiveHandler extends Base implements MessageHandler {
|
|||
if (isNaN(parse) || parse <= 0)
|
||||
return false;
|
||||
console.debug(`Got resolve options: ${JSON.stringify(data)}`);
|
||||
const res = await this.hidive.getShow(parseInt(data.id), data.e, data.but, data.all);
|
||||
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.Name,
|
||||
ids: [item.id],
|
||||
title: item.title,
|
||||
parent: {
|
||||
title: item.seriesTitle,
|
||||
season: parseFloat(item.SeasonNumberValue+'')+''
|
||||
season: item.episodeInformation.seasonNumber+''
|
||||
},
|
||||
image: item.ScreenShotSmallUrl,
|
||||
e: parseFloat(item.EpisodeNumberValue+'')+'',
|
||||
episode: parseFloat(item.EpisodeNumberValue+'')+'',
|
||||
image: item.thumbnailUrl,
|
||||
e: item.episodeInformation.episodeNumber+'',
|
||||
episode: item.episodeInformation.episodeNumber+'',
|
||||
};
|
||||
}));
|
||||
return true;
|
||||
|
|
@ -86,23 +85,22 @@ class HidiveHandler extends Base implements MessageHandler {
|
|||
const parse = parseInt(id);
|
||||
if (isNaN(parse) || parse <= 0)
|
||||
return { isOk: false, reason: new Error('The ID is invalid') };
|
||||
const request = await this.hidive.listShow(parse);
|
||||
|
||||
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.Episodes.map(function(item) {
|
||||
const language = item.Summary.match(/^Audio: (.*)/m);
|
||||
language?.shift();
|
||||
const description = item.Summary.split('\r\n');
|
||||
return { isOk: true, value: request.value.map(function(item) {
|
||||
const description = item.description.split('\r\n');
|
||||
return {
|
||||
e: parseFloat(item.EpisodeNumberValue+'')+'',
|
||||
lang: language ? language[0].split(', ') : [],
|
||||
name: item.Name,
|
||||
season: parseFloat(item.SeasonNumberValue+'')+'',
|
||||
seasonTitle: request.value.Name,
|
||||
episode: parseFloat(item.EpisodeNumberValue+'')+'',
|
||||
id: item.Id+'',
|
||||
img: item.ScreenShotSmallUrl,
|
||||
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: ''
|
||||
};
|
||||
|
|
@ -113,12 +111,12 @@ class HidiveHandler extends Base implements MessageHandler {
|
|||
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.getShow(parseInt(data.id), data.e, false, false);
|
||||
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.getEpisode(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 });
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -21,12 +21,12 @@ export default class WebSocketHandler {
|
|||
public events: ExternalEvent = new ExternalEvent();
|
||||
|
||||
constructor(server: Server) {
|
||||
this.wsServer = new ws.WebSocketServer({ noServer: true, path: '/ws' });
|
||||
this.wsServer = new ws.WebSocketServer({ noServer: true, path: '/private' });
|
||||
|
||||
this.wsServer.on('connection', (socket, req) => {
|
||||
console.info(`[WS] Connection from '${req.socket.remoteAddress}'`);
|
||||
socket.on('error', (er) => console.error(`[WS] ${er}`));
|
||||
socket.on('message', (data) => {
|
||||
socket.on('message', (data) => {
|
||||
const json = JSON.parse(data.toString()) as UnknownWSMessage;
|
||||
this.events.emit(json.name, json as any, (data) => {
|
||||
this.wsServer.clients.forEach(client => {
|
||||
|
|
@ -88,7 +88,7 @@ export class PublicWebSocket {
|
|||
this.wsServer.on('connection', (socket, req) => {
|
||||
console.info(`[WS] Connection to public ws from '${req.socket.remoteAddress}'`);
|
||||
socket.on('error', (er) => console.error(`[WS] ${er}`));
|
||||
socket.on('message', (msg) => {
|
||||
socket.on('message', (msg) => {
|
||||
const data = JSON.parse(msg.toString()) as UnknownWSMessage;
|
||||
switch (data.name) {
|
||||
case 'isSetup':
|
||||
|
|
@ -120,4 +120,4 @@ export class PublicWebSocket {
|
|||
console.error(`[WS] ${er}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
59
index.ts
59
index.ts
|
|
@ -18,15 +18,7 @@ import update from './modules/module.updater';
|
|||
}
|
||||
|
||||
if (argv.addArchive) {
|
||||
if (argv.service === 'funi') {
|
||||
if (argv.s === undefined)
|
||||
return console.error('`-s` not found');
|
||||
addToArchive({
|
||||
service: 'funi',
|
||||
type: 's'
|
||||
}, argv.s);
|
||||
console.info('Added %s to the downloadArchive list', argv.s);
|
||||
} else if (argv.service === 'crunchy') {
|
||||
if (argv.service === 'crunchy') {
|
||||
if (argv.s === undefined && argv.series === undefined)
|
||||
return console.error('`-s` or `--srz` not found');
|
||||
if (argv.s && argv.series)
|
||||
|
|
@ -45,6 +37,15 @@ import update from './modules/module.updater';
|
|||
type: 's'
|
||||
}, (argv.s === undefined ? argv.series : argv.s) as string);
|
||||
console.info('Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s));
|
||||
} else if (argv.service === 'ao') {
|
||||
if (argv.s === undefined)
|
||||
return console.error('`-s` not found');
|
||||
addToArchive({
|
||||
service: 'hidive',
|
||||
//type: argv.s === undefined ? 'srz' : 's'
|
||||
type: 's'
|
||||
}, (argv.s === undefined ? argv.series : argv.s) as string);
|
||||
console.info('Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s));
|
||||
}
|
||||
} else if (argv.downloadArchive) {
|
||||
const ids = makeCommand(argv.service);
|
||||
|
|
@ -52,14 +53,48 @@ import update from './modules/module.updater';
|
|||
overrideArguments(cfg.cli, id);
|
||||
/* Reimport module to override appArgv */
|
||||
Object.keys(require.cache).forEach(key => {
|
||||
if (key.endsWith('crunchy.js') || key.endsWith('funi.js') || key.endsWith('hidive.js'))
|
||||
if (key.endsWith('crunchy.js') || key.endsWith('hidive.js') || key.endsWith('ao.js'))
|
||||
delete require.cache[key];
|
||||
});
|
||||
const service = new (argv.service === 'funi' ? (await import('./funi')).default : argv.service === 'hidive' ? (await import('./hidive')).default : (await import('./crunchy')).default)(argv.debug) as ServiceClass;
|
||||
let service: ServiceClass;
|
||||
switch(argv.service) {
|
||||
case 'crunchy':
|
||||
service = new (await import('./crunchy')).default;
|
||||
break;
|
||||
case 'hidive':
|
||||
service = new (await import('./hidive')).default;
|
||||
break;
|
||||
case 'ao':
|
||||
service = new (await import('./ao')).default;
|
||||
break;
|
||||
case 'adn':
|
||||
service = new (await import('./adn')).default;
|
||||
break;
|
||||
default:
|
||||
service = new (await import(`./${argv.service}`)).default;
|
||||
break;
|
||||
}
|
||||
await service.cli();
|
||||
}
|
||||
} else {
|
||||
const service = new (argv.service === 'funi' ? (await import('./funi')).default : argv.service === 'hidive' ? (await import('./hidive')).default : (await import('./crunchy')).default)(argv.debug) as ServiceClass;
|
||||
let service: ServiceClass;
|
||||
switch(argv.service) {
|
||||
case 'crunchy':
|
||||
service = new (await import('./crunchy')).default;
|
||||
break;
|
||||
case 'hidive':
|
||||
service = new (await import('./hidive')).default;
|
||||
break;
|
||||
case 'ao':
|
||||
service = new (await import('./ao')).default;
|
||||
break;
|
||||
case 'adn':
|
||||
service = new (await import('./adn')).default;
|
||||
break;
|
||||
default:
|
||||
service = new (await import(`./${argv.service}`)).default;
|
||||
break;
|
||||
}
|
||||
await service.cli();
|
||||
}
|
||||
})();
|
||||
|
|
@ -3,19 +3,22 @@ import fs from 'fs';
|
|||
import path from 'path';
|
||||
import { args, groups } from './module.args';
|
||||
|
||||
const transformService = (str: Array<'funi'|'crunchy'|'hidive'|'all'>) => {
|
||||
const transformService = (str: Array<'crunchy'|'hidive'|'ao'|'adn'|'all'>) => {
|
||||
const services: string[] = [];
|
||||
str.forEach(function(part) {
|
||||
switch(part) {
|
||||
case 'funi':
|
||||
services.push('Funimation');
|
||||
break;
|
||||
case 'crunchy':
|
||||
services.push('Crunchyroll');
|
||||
break;
|
||||
case 'hidive':
|
||||
services.push('Hidive');
|
||||
break;
|
||||
case 'ao':
|
||||
services.push('AnimeOnegai');
|
||||
break;
|
||||
case 'adn':
|
||||
services.push('AnimationDigitalNetwork');
|
||||
break;
|
||||
case 'all':
|
||||
services.push('All');
|
||||
break;
|
||||
|
|
@ -26,11 +29,11 @@ const transformService = (str: Array<'funi'|'crunchy'|'hidive'|'all'>) => {
|
|||
|
||||
let docs = `# ${packageJSON.name} (${packageJSON.version}v)
|
||||
|
||||
If you find any bugs in this documentation or in the programm itself please report it [over on GitHub](${packageJSON.bugs.url}).
|
||||
If you find any bugs in this documentation or in the program itself please report it [over on GitHub](${packageJSON.bugs.url}).
|
||||
|
||||
## Legal Warning
|
||||
|
||||
This application is not endorsed by or affiliated with *Funimation* or *Crunchyroll*.
|
||||
This application is not endorsed by or affiliated with *Crunchyroll*, *Hidive*, *AnimeOnegai*, or *AnimationDigitalNetwork*.
|
||||
This application enables you to download videos for offline viewing which may be forbidden by law in your country.
|
||||
The usage of this application may also cause a violation of the *Terms of Service* between you and the stream provider.
|
||||
This tool is not responsible for your actions; please make an informed decision before using this application.
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ import modulesCleanup from 'removeNPMAbsolutePaths';
|
|||
import { exec } from '@yao-pkg/pkg';
|
||||
import { execSync } from 'child_process';
|
||||
import { console } from './log';
|
||||
import esbuild from 'esbuild';
|
||||
import path from 'path';
|
||||
|
||||
const buildsDir = './_builds';
|
||||
const nodeVer = 'node18-';
|
||||
const nodeVer = 'node20-';
|
||||
|
||||
type BuildTypes = `${'windows'|'macos'|'linux'|'linuxstatic'|'alpine'}-${'x64'|'arm64'}`|'linuxstatic-armv7'
|
||||
|
||||
|
|
@ -43,10 +45,35 @@ async function buildBinary(buildType: BuildTypes, gui: boolean) {
|
|||
fs.removeSync(buildDir);
|
||||
}
|
||||
fs.mkdirSync(buildDir);
|
||||
console.info('Running esbuild');
|
||||
|
||||
const build = await esbuild.build({
|
||||
entryPoints: [
|
||||
gui ? 'gui.js' : 'index.js',
|
||||
],
|
||||
sourceRoot: './',
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
treeShaking: true,
|
||||
// External source map for debugging
|
||||
sourcemap: true,
|
||||
// Minify and keep the original names
|
||||
minify: true,
|
||||
keepNames: true,
|
||||
outfile: path.join(buildsDir, 'index.cjs'),
|
||||
metafile: true,
|
||||
external: ['cheerio']
|
||||
});
|
||||
|
||||
if (build.errors?.length > 0) console.error(build.errors);
|
||||
if (build.warnings?.length > 0) console.warn(build.warnings);
|
||||
|
||||
const buildConfig = [
|
||||
gui ? 'gui.js' : 'index.js',
|
||||
`${buildsDir}/index.cjs`,
|
||||
'--target', nodeVer + buildType,
|
||||
'--output', `${buildDir}/${pkg.short_name}`,
|
||||
'--compress', 'GZip'
|
||||
];
|
||||
console.info(`[Build] Build configuration: ${buildFull}`);
|
||||
try {
|
||||
|
|
@ -58,6 +85,7 @@ async function buildBinary(buildType: BuildTypes, gui: boolean) {
|
|||
}
|
||||
fs.mkdirSync(`${buildDir}/config`);
|
||||
fs.mkdirSync(`${buildDir}/videos`);
|
||||
fs.mkdirSync(`${buildDir}/widevine`);
|
||||
fs.copySync('./config/bin-path.yml', `${buildDir}/config/bin-path.yml`);
|
||||
fs.copySync('./config/cli-defaults.yml', `${buildDir}/config/cli-defaults.yml`);
|
||||
fs.copySync('./config/dir-path.yml', `${buildDir}/config/dir-path.yml`);
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
import { KeyContainer, Session } from './license';
|
||||
import fs from 'fs';
|
||||
import { console } from './log';
|
||||
import got from 'got';
|
||||
import { workingDir } from './module.cfg-loader';
|
||||
import path from 'path';
|
||||
|
||||
//read cdm files located in the same directory
|
||||
let privateKey: Buffer, identifierBlob: Buffer;
|
||||
export let canDecrypt: boolean;
|
||||
try {
|
||||
privateKey = fs.readFileSync(path.join(workingDir, 'widevine', 'device_private_key'));
|
||||
identifierBlob = fs.readFileSync(path.join(workingDir, 'widevine', 'device_client_id_blob'));
|
||||
canDecrypt = true;
|
||||
} catch (e) {
|
||||
canDecrypt = false;
|
||||
}
|
||||
|
||||
export default async function getKeys(pssh: string | undefined, licenseServer: string, authData: Record<string, string>): Promise<KeyContainer[]> {
|
||||
if (!pssh || !canDecrypt) return [];
|
||||
//pssh found in the mpd manifest
|
||||
const psshBuffer = Buffer.from(
|
||||
pssh,
|
||||
'base64'
|
||||
);
|
||||
|
||||
//Create a new widevine session
|
||||
const session = new Session({ privateKey, identifierBlob }, psshBuffer);
|
||||
|
||||
//Generate license
|
||||
const response = await got(licenseServer, {
|
||||
method: 'POST',
|
||||
body: session.createLicenseRequest(),
|
||||
headers: authData,
|
||||
responseType: 'text'
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
//Parse License and return keys
|
||||
const json = JSON.parse(response.body);
|
||||
const keys = session.parseLicense(Buffer.from(json['license'], 'base64'));
|
||||
return keys;
|
||||
} else {
|
||||
console.info('License request failed:', response.statusMessage);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -421,6 +421,12 @@ const extFn = {
|
|||
if ((options.url.hostname as string).match('hidive')) {
|
||||
options.headers['referrer'] = 'https://www.hidive.com/';
|
||||
options.headers['origin'] = 'https://www.hidive.com';
|
||||
} else if ((options.url.hostname as string).includes('animecdn')) {
|
||||
options.headers = {
|
||||
origin: 'https://www.animeonegai.com',
|
||||
referer: 'https://www.animeonegai.com/',
|
||||
range: options.headers['range']
|
||||
};
|
||||
}
|
||||
// console.log(' - Req:', options.url.pathname);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,12 @@ const makeLogFolder = () => {
|
|||
};
|
||||
|
||||
const makeLogger = () => {
|
||||
const oldLog = global.console.log;
|
||||
global.console.log = (data) => {
|
||||
oldLog(`Unexpected use of console.log. Use the log4js logger instead. ${data}`);
|
||||
global.console.log =
|
||||
global.console.info =
|
||||
global.console.warn =
|
||||
global.console.error =
|
||||
global.console.debug = (...data: any[]) => {
|
||||
console.info((data.length >= 1 ? data.shift() : ''), ...data);
|
||||
};
|
||||
makeLogFolder();
|
||||
log4js.configure({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { Headers } from 'got/dist/source';
|
||||
|
||||
// api domains
|
||||
const domain = {
|
||||
www: 'https://www.crunchyroll.com',
|
||||
|
|
@ -7,7 +5,8 @@ const domain = {
|
|||
www_beta: 'https://beta.crunchyroll.com',
|
||||
api_beta: 'https://beta-api.crunchyroll.com',
|
||||
hd_www: 'https://www.hidive.com',
|
||||
hd_api: 'https://api.hidive.com'
|
||||
hd_api: 'https://api.hidive.com',
|
||||
hd_new: 'https://dce-frontoffice.imggaming.com'
|
||||
};
|
||||
|
||||
export type APIType = {
|
||||
|
|
@ -25,22 +24,37 @@ export type APIType = {
|
|||
collections: string
|
||||
// beta api
|
||||
beta_auth: string
|
||||
beta_authBasic: string
|
||||
beta_authBasicMob: string
|
||||
authBasic: string
|
||||
authBasicMob: string
|
||||
authBasicSwitch: string,
|
||||
beta_profile: string
|
||||
beta_cmsToken: string
|
||||
search: string
|
||||
cms: string
|
||||
beta_browse: string
|
||||
beta_cms: string,
|
||||
beta_authHeader: Headers,
|
||||
beta_authHeaderMob: Headers,
|
||||
drm: string;
|
||||
/**
|
||||
* Web Header
|
||||
*/
|
||||
crunchyAuthHeader: Record<string, string>,
|
||||
/**
|
||||
* Mobile Header
|
||||
*/
|
||||
crunchyAuthHeaderMob: Record<string, string>,
|
||||
/**
|
||||
* Switch Header
|
||||
*/
|
||||
crunchyAuthHeaderSwitch: Record<string, string>,
|
||||
hd_apikey: string,
|
||||
hd_devName: string,
|
||||
hd_appId: string,
|
||||
hd_clientWeb: string,
|
||||
hd_clientExo: string,
|
||||
hd_api: string,
|
||||
hd_new_api: string,
|
||||
hd_new_apiKey: string,
|
||||
hd_new_version: string,
|
||||
}
|
||||
|
||||
// api urls
|
||||
|
|
@ -60,16 +74,19 @@ const api: APIType = {
|
|||
collections: `${domain.api}/list_collections.0.json`,
|
||||
// beta api
|
||||
beta_auth: `${domain.api_beta}/auth/v1/token`,
|
||||
beta_authBasic: 'Basic bm9haWhkZXZtXzZpeWcwYThsMHE6',
|
||||
beta_authBasicMob: 'Basic b2VkYXJteHN0bGgxanZhd2ltbnE6OWxFaHZIWkpEMzJqdVY1ZFc5Vk9TNTdkb3BkSnBnbzE=',
|
||||
authBasic: 'Basic bm9haWhkZXZtXzZpeWcwYThsMHE6',
|
||||
authBasicMob: 'Basic dXU4aG0wb2g4dHFpOWV0eXl2aGo6SDA2VnVjRnZUaDJ1dEYxM0FBS3lLNE85UTRhX3BlX1o=',
|
||||
authBasicSwitch: 'Basic dC1rZGdwMmg4YzNqdWI4Zm4wZnE6eWZMRGZNZnJZdktYaDRKWFMxTEVJMmNDcXUxdjVXYW4=',
|
||||
beta_profile: `${domain.api_beta}/accounts/v1/me/profile`,
|
||||
beta_cmsToken: `${domain.api_beta}/index/v2`,
|
||||
search: `${domain.api_beta}/content/v2/discover/search`,
|
||||
cms: `${domain.api_beta}/content/v2/cms`,
|
||||
beta_browse: `${domain.api_beta}/content/v1/browse`,
|
||||
beta_cms: `${domain.api_beta}/cms/v2`,
|
||||
beta_authHeader: {},
|
||||
beta_authHeaderMob: {},
|
||||
drm: `${domain.api_beta}/drm/v1/auth`,
|
||||
crunchyAuthHeader: {},
|
||||
crunchyAuthHeaderMob: {},
|
||||
crunchyAuthHeaderSwitch: {},
|
||||
//hidive API
|
||||
hd_apikey: '508efd7b42d546e19cc24f4d0b414e57e351ca73',
|
||||
hd_devName: 'Android',
|
||||
|
|
@ -77,14 +94,24 @@ const api: APIType = {
|
|||
hd_clientWeb: 'okhttp/3.4.1',
|
||||
hd_clientExo: 'smartexoplayer/1.6.0.R (Linux;Android 6.0) ExoPlayerLib/2.6.0',
|
||||
hd_api: `${domain.hd_api}/api/v1`,
|
||||
//Hidive New API
|
||||
hd_new_api: `${domain.hd_new}/api`,
|
||||
hd_new_apiKey: '857a1e5d-e35e-4fdf-805b-a87b6f8364bf',
|
||||
hd_new_version: '6.0.1.bbf09a2'
|
||||
};
|
||||
|
||||
// set header
|
||||
api.beta_authHeader = {
|
||||
Authorization: api.beta_authBasic,
|
||||
api.crunchyAuthHeader = {
|
||||
Authorization: api.authBasic,
|
||||
};
|
||||
api.beta_authHeaderMob = {
|
||||
Authorization: api.beta_authBasicMob,
|
||||
|
||||
api.crunchyAuthHeaderMob = {
|
||||
Authorization: api.authBasicMob,
|
||||
'user-agent': 'Crunchyroll/3.60.0 Android/9 okhttp/4.12.0'
|
||||
};
|
||||
|
||||
api.crunchyAuthHeaderSwitch = {
|
||||
Authorization: api.authBasicSwitch,
|
||||
};
|
||||
|
||||
export {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import yargs, { Choices } from 'yargs';
|
||||
import { args, AvailableMuxer, groups } from './module.args';
|
||||
import { LanguageItem } from './module.langsData';
|
||||
import { DownloadInfo } from '../@types/messageHandler';
|
||||
import { HLSCallback } from './hls-download';
|
||||
import leven from 'leven';
|
||||
import { console } from './log';
|
||||
import { CrunchyPlayStreams } from '../@types/enums';
|
||||
|
||||
let argvC: {
|
||||
[x: string]: unknown;
|
||||
|
|
@ -17,6 +22,7 @@ let argvC: {
|
|||
forceMuxer: AvailableMuxer|undefined;
|
||||
username: string|undefined,
|
||||
password: string|undefined,
|
||||
token: string|undefined,
|
||||
silentAuth: boolean,
|
||||
skipSubMux: boolean,
|
||||
downloadArchive: boolean,
|
||||
|
|
@ -27,16 +33,18 @@ let argvC: {
|
|||
search: string | undefined;
|
||||
'search-type': string;
|
||||
page: number | undefined;
|
||||
'search-locale': string;
|
||||
locale: string;
|
||||
new: boolean | undefined;
|
||||
'movie-listing': string | undefined;
|
||||
series: string | undefined;
|
||||
s: string | undefined;
|
||||
s: string | undefined;
|
||||
srz: string | undefined;
|
||||
e: string | undefined;
|
||||
extid: string | undefined;
|
||||
q: number;
|
||||
x: number;
|
||||
kstream: number;
|
||||
kstream: number;
|
||||
cstream: keyof typeof CrunchyPlayStreams | 'none';
|
||||
partsize: number;
|
||||
hslang: string;
|
||||
dlsubs: string[];
|
||||
|
|
@ -46,6 +54,7 @@ let argvC: {
|
|||
dubLang: string[];
|
||||
all: boolean;
|
||||
fontSize: number;
|
||||
combineLines: boolean;
|
||||
allDubs: boolean;
|
||||
timeout: number;
|
||||
waittime: number;
|
||||
|
|
@ -58,7 +67,7 @@ let argvC: {
|
|||
debug: boolean | undefined;
|
||||
nocleanup: boolean;
|
||||
help: boolean | undefined;
|
||||
service: 'funi' | 'crunchy' | 'hidive';
|
||||
service: 'crunchy' | 'hidive' | 'ao' | 'adn';
|
||||
update: boolean;
|
||||
fontName: string | undefined;
|
||||
_: (string | number)[];
|
||||
|
|
@ -70,6 +79,7 @@ let argvC: {
|
|||
originalFontSize: boolean;
|
||||
keepAllVideos: boolean;
|
||||
syncTiming: boolean;
|
||||
callbackMaker?: (data: DownloadInfo) => HLSCallback;
|
||||
};
|
||||
|
||||
export type ArgvType = typeof argvC;
|
||||
|
|
@ -107,15 +117,15 @@ const getArgv = (cfg: { [key:string]: unknown }, isGUI: boolean) => {
|
|||
return cfg[key] as T;
|
||||
} else
|
||||
return _default;
|
||||
};
|
||||
|
||||
};
|
||||
const argv = yargs.parserConfiguration({
|
||||
'duplicate-arguments-array': false,
|
||||
'camel-case-expansion': false,
|
||||
})
|
||||
.wrap(yargs.terminalWidth())
|
||||
.usage('Usage: $0 [options]')
|
||||
.help(true).version(false);
|
||||
.help(true);
|
||||
//.strictOptions()
|
||||
const data = args.map(a => {
|
||||
return {
|
||||
...a,
|
||||
|
|
@ -137,7 +147,31 @@ const getArgv = (cfg: { [key:string]: unknown }, isGUI: boolean) => {
|
|||
},
|
||||
choices: item.name === 'service' && isGUI ? undefined : item.choices as unknown as Choices
|
||||
});
|
||||
return argv as unknown as yargs.Argv<typeof argvC>;
|
||||
};
|
||||
|
||||
// Custom logic for suggesting corrections for misspelled options
|
||||
argv.middleware((argv: Record<string, any>) => {
|
||||
// List of valid options
|
||||
const validOptions = [
|
||||
...args.map(a => a.name),
|
||||
...args.map(a => a.alias).filter(alias => alias !== undefined) as string[]
|
||||
];
|
||||
const unknownOptions = Object.keys(argv).filter(key => !validOptions.includes(key) && key !== '_' && key !== '$0'); // Filter out known options
|
||||
|
||||
const suggestedOptions: Record<string, boolean> = {};
|
||||
unknownOptions.forEach(actualOption => {
|
||||
const closestOption = validOptions.find(option => {
|
||||
const levenVal = leven(option, actualOption);
|
||||
return levenVal <= 2 && levenVal > 0;
|
||||
});
|
||||
|
||||
|
||||
if (closestOption && !suggestedOptions[closestOption]) {
|
||||
suggestedOptions[closestOption] = true;
|
||||
console.info(`Unknown option ${actualOption}, did you mean ${closestOption}?`);
|
||||
} else if (!suggestedOptions[actualOption]) {
|
||||
suggestedOptions[actualOption] = true;
|
||||
console.info(`Unknown option ${actualOption}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
return argv as unknown as yargs.Argv<typeof argvC>;
|
||||
};
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { dubLanguageCodes, languages, searchLocales, subtitleLanguagesFilter } from './module.langsData';
|
||||
import { aoSearchLocales, dubLanguageCodes, languages, searchLocales, subtitleLanguagesFilter } from './module.langsData';
|
||||
import { CrunchyPlayStreams } from '../@types/enums';
|
||||
|
||||
const groups = {
|
||||
'auth': 'Authentication:',
|
||||
|
|
@ -13,12 +14,13 @@ const groups = {
|
|||
'gui': 'GUI:'
|
||||
};
|
||||
|
||||
export type AvailableFilenameVars = 'title' | 'episode' | 'showTitle' | 'season' | 'width' | 'height' | 'service'
|
||||
export type AvailableFilenameVars = 'title' | 'episode' | 'showTitle' | 'seriesTitle' | 'season' | 'width' | 'height' | 'service'
|
||||
|
||||
const availableFilenameVars: AvailableFilenameVars[] = [
|
||||
'title',
|
||||
'episode',
|
||||
'showTitle',
|
||||
'seriesTitle',
|
||||
'season',
|
||||
'width',
|
||||
'height',
|
||||
|
|
@ -28,7 +30,7 @@ const availableFilenameVars: AvailableFilenameVars[] = [
|
|||
export type AvailableMuxer = 'ffmpeg' | 'mkvmerge'
|
||||
export const muxer: AvailableMuxer[] = [ 'ffmpeg', 'mkvmerge' ];
|
||||
|
||||
type TAppArg<T extends boolean|string|number|unknown[], K = any> = {
|
||||
export type TAppArg<T extends boolean|string|number|unknown[], K = any> = {
|
||||
name: string,
|
||||
group: keyof typeof groups,
|
||||
type: 'boolean'|'string'|'number'|'array',
|
||||
|
|
@ -40,7 +42,7 @@ type TAppArg<T extends boolean|string|number|unknown[], K = any> = {
|
|||
default: T|undefined,
|
||||
name?: string
|
||||
},
|
||||
service: Array<'funi'|'crunchy'|'hidive'|'all'>,
|
||||
service: Array<'crunchy'|'hidive'|'ao'|'adn'|'all'>,
|
||||
usage: string // -(-)${name} will be added for each command,
|
||||
demandOption?: true,
|
||||
transformer?: (value: T) => K
|
||||
|
|
@ -102,16 +104,16 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
|||
usage: '${page}'
|
||||
},
|
||||
{
|
||||
name: 'search-locale',
|
||||
describe: 'Set the search locale',
|
||||
docDescribe: 'Set the search local that will be used for searching for items.',
|
||||
name: 'locale',
|
||||
describe: 'Set the service locale',
|
||||
docDescribe: 'Set the local that will be used for the API.',
|
||||
group: 'search',
|
||||
choices: (searchLocales.filter(a => a !== undefined) as string[]),
|
||||
choices: ([...searchLocales.filter(a => a !== undefined), ...aoSearchLocales.filter(a => a !== undefined)] as string[]),
|
||||
default: {
|
||||
default: ''
|
||||
default: 'en-US'
|
||||
},
|
||||
type: 'string',
|
||||
service: ['crunchy'],
|
||||
service: ['crunchy', 'ao', 'adn'],
|
||||
usage: '${locale}'
|
||||
},
|
||||
{
|
||||
|
|
@ -138,8 +140,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
|||
group: 'dl',
|
||||
alias: 'srz',
|
||||
describe: 'Get season list by series ID',
|
||||
docDescribe: 'This command is used only for crunchyroll.'
|
||||
+ '\n Requested is the ID of a show not a season.',
|
||||
docDescribe: 'Requested is the ID of a show not a season.',
|
||||
service: ['crunchy'],
|
||||
type: 'string',
|
||||
usage: '${ID}'
|
||||
|
|
@ -193,7 +194,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
|||
describe: 'Download only once the video with the best selected quality',
|
||||
type: 'boolean',
|
||||
group: 'dl',
|
||||
service: ['crunchy'],
|
||||
service: ['crunchy', 'ao'],
|
||||
docDescribe: 'If selected, the best selected quality will be downloaded only for the first language,'
|
||||
+ '\nthen the worst video quality with the same audio quality will be downloaded for every other language.'
|
||||
+ '\nBy the later merge of the videos, no quality difference will be present.'
|
||||
|
|
@ -208,12 +209,12 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
|||
describe: 'Will fetch the chapters and add them into the final video',
|
||||
type: 'boolean',
|
||||
group: 'dl',
|
||||
service: ['crunchy'],
|
||||
service: ['crunchy', 'adn'],
|
||||
docDescribe: 'Will fetch the chapters and add them into the final video.'
|
||||
+ '\nCurrently only works with mkvmerge.',
|
||||
usage: '',
|
||||
default: {
|
||||
default: false
|
||||
default: true
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -227,7 +228,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
|||
usage: '',
|
||||
choices: ['android', 'web'],
|
||||
default: {
|
||||
default: 'android'
|
||||
default: 'web'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -266,7 +267,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
|||
type: 'number',
|
||||
alias: 'server',
|
||||
docDescribe: true,
|
||||
service: ['crunchy','funi'],
|
||||
service: ['crunchy'],
|
||||
usage: '${server}'
|
||||
},
|
||||
{
|
||||
|
|
@ -276,13 +277,27 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
|||
describe: 'Select specific stream',
|
||||
choices: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
default: {
|
||||
default: 4
|
||||
default: 1
|
||||
},
|
||||
docDescribe: true,
|
||||
service: ['crunchy'],
|
||||
type: 'number',
|
||||
usage: '${stream}'
|
||||
},
|
||||
{
|
||||
name: 'cstream',
|
||||
group: 'dl',
|
||||
alias: 'cs',
|
||||
service: ['crunchy'],
|
||||
type: 'string',
|
||||
describe: 'Select specific crunchy play stream by device, or disable stream with "none"',
|
||||
choices: [...Object.keys(CrunchyPlayStreams), 'none'],
|
||||
default: {
|
||||
default: 'chrome'
|
||||
},
|
||||
docDescribe: true,
|
||||
usage: '${device}'
|
||||
},
|
||||
{
|
||||
name: 'hslang',
|
||||
group: 'dl',
|
||||
|
|
@ -300,8 +315,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
|||
name: 'dlsubs',
|
||||
group: 'dl',
|
||||
describe: 'Download subtitles by language tag (space-separated)'
|
||||
+ `\nFuni Only: ${languages.filter(a => a.funi_locale && !a.cr_locale).map(a => a.locale).join(', ')}`
|
||||
+ `\nCrunchy Only: ${languages.filter(a => a.cr_locale && !a.funi_locale).map(a => a.locale).join(', ')}`,
|
||||
+ `\nCrunchy Only: ${languages.filter(a => a.cr_locale).map(a => a.locale).join(', ')}`,
|
||||
docDescribe: true,
|
||||
service: ['all'],
|
||||
type: 'array',
|
||||
|
|
@ -325,7 +339,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
|||
group: 'dl',
|
||||
describe: 'Skip downloading audio',
|
||||
docDescribe: true,
|
||||
service: ['funi'],
|
||||
service: ['crunchy', 'hidive'],
|
||||
type: 'boolean',
|
||||
usage: ''
|
||||
},
|
||||
|
|
@ -341,8 +355,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
|||
{
|
||||
name: 'dubLang',
|
||||
describe: 'Set the language to download: '
|
||||
+ `\nFuni Only: ${languages.filter(a => a.funi_locale && !a.cr_locale).map(a => a.code).join(', ')}`
|
||||
+ `\nCrunchy Only: ${languages.filter(a => a.cr_locale && !a.funi_locale).map(a => a.code).join(', ')}`,
|
||||
+ `\nCrunchy Only: ${languages.filter(a => a.cr_locale).map(a => a.code).join(', ')}`,
|
||||
docDescribe: true,
|
||||
group: 'dl',
|
||||
choices: dubLanguageCodes,
|
||||
|
|
@ -371,12 +384,22 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
|||
default: {
|
||||
default: 55
|
||||
},
|
||||
docDescribe: true,
|
||||
docDescribe: 'When converting the subtitles to ass, this will change the font size'
|
||||
+ '\nIn most cases, requires "--originaFontSize false" to take effect',
|
||||
group: 'dl',
|
||||
service: ['all'],
|
||||
type: 'number',
|
||||
usage: '${fontSize}'
|
||||
},
|
||||
{
|
||||
name: 'combineLines',
|
||||
describe: 'Merge adjacent lines with same style and text',
|
||||
docDescribe: 'If selected, will prevent a line from shifting downwards',
|
||||
group: 'dl',
|
||||
service: ['hidive'],
|
||||
type: 'boolean',
|
||||
usage: ''
|
||||
},
|
||||
{
|
||||
name: 'allDubs',
|
||||
describe: 'If selected, all available dubs will get downloaded',
|
||||
|
|
@ -415,7 +438,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
|||
group: 'dl',
|
||||
describe: 'Force downloading simulcast version instead of uncut version (if available).',
|
||||
docDescribe: true,
|
||||
service: ['funi', 'hidive'],
|
||||
service: ['hidive'],
|
||||
type: 'boolean',
|
||||
usage: '',
|
||||
default: {
|
||||
|
|
@ -548,7 +571,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
|||
group: 'util',
|
||||
service: ['all'],
|
||||
type: 'string',
|
||||
choices: ['funi', 'crunchy', 'hidive'],
|
||||
choices: ['crunchy', 'hidive', 'ao', 'adn'],
|
||||
usage: '${service}',
|
||||
default: {
|
||||
default: ''
|
||||
|
|
@ -569,7 +592,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
|||
group: 'fonts',
|
||||
describe: 'Set the font to use in subtiles',
|
||||
docDescribe: true,
|
||||
service: ['funi', 'hidive'],
|
||||
service: ['hidive', 'adn'],
|
||||
type: 'string',
|
||||
usage: '${fontName}',
|
||||
},
|
||||
|
|
@ -653,13 +676,25 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
|||
describe: 'Authenticate every time the script runs. Use at your own risk.',
|
||||
docDescribe: true,
|
||||
group: 'auth',
|
||||
service: ['funi','crunchy'],
|
||||
service: ['crunchy'],
|
||||
type: 'boolean',
|
||||
usage: '',
|
||||
default: {
|
||||
default: false
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'token',
|
||||
describe: 'Allows you to login with your token (Example on crunchy is Refresh Token/etp-rt cookie)',
|
||||
docDescribe: true,
|
||||
group: 'auth',
|
||||
service: ['crunchy', 'ao'],
|
||||
type: 'string',
|
||||
usage: '${token}',
|
||||
default: {
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'forceMuxer',
|
||||
describe: 'Force the program to use said muxer or don\'t mux if the given muxer is not present',
|
||||
|
|
@ -845,7 +880,6 @@ const buildDefault = () => {
|
|||
};
|
||||
|
||||
export {
|
||||
TAppArg,
|
||||
getDefault,
|
||||
buildDefault,
|
||||
args,
|
||||
|
|
|
|||
|
|
@ -18,15 +18,18 @@ const guiCfgFile = path.join(workingDir, 'config', 'gui');
|
|||
const cliCfgFile = path.join(workingDir, 'config', 'cli-defaults');
|
||||
const hdPflCfgFile = path.join(workingDir, 'config', 'hd_profile');
|
||||
const sessCfgFile = {
|
||||
funi: path.join(workingDir, 'config', 'funi_sess'),
|
||||
cr: path.join(workingDir, 'config', 'cr_sess'),
|
||||
hd: path.join(workingDir, 'config', 'hd_sess')
|
||||
hd: path.join(workingDir, 'config', 'hd_sess'),
|
||||
ao: path.join(workingDir, 'config', 'ao_sess'),
|
||||
adn: path.join(workingDir, 'config', 'adn_sess')
|
||||
};
|
||||
const stateFile = path.join(workingDir, 'config', 'guistate');
|
||||
const tokenFile = {
|
||||
funi: path.join(workingDir, 'config', 'funi_token'),
|
||||
cr: path.join(workingDir, 'config', 'cr_token'),
|
||||
hd: path.join(workingDir, 'config', 'hd_token')
|
||||
hd: path.join(workingDir, 'config', 'hd_token'),
|
||||
hdNew:path.join(workingDir, 'config', 'hd_new_token'),
|
||||
ao: path.join(workingDir, 'config', 'ao_token'),
|
||||
adn: path.join(workingDir, 'config', 'adn_token')
|
||||
};
|
||||
|
||||
export const ensureConfig = () => {
|
||||
|
|
@ -214,7 +217,44 @@ const saveCRToken = (data: Record<string, unknown>) => {
|
|||
console.error('Can\'t save token file to disk!');
|
||||
}
|
||||
};
|
||||
|
||||
const loadADNToken = () => {
|
||||
let token = loadYamlCfgFile(tokenFile.adn, true);
|
||||
if(typeof token !== 'object' || token === null || Array.isArray(token)){
|
||||
token = {};
|
||||
}
|
||||
return token;
|
||||
};
|
||||
|
||||
const saveADNToken = (data: Record<string, unknown>) => {
|
||||
const cfgFolder = path.dirname(tokenFile.adn);
|
||||
try{
|
||||
fs.ensureDirSync(cfgFolder);
|
||||
fs.writeFileSync(`${tokenFile.adn}.yml`, yaml.stringify(data));
|
||||
}
|
||||
catch(e){
|
||||
console.error('Can\'t save token file to disk!');
|
||||
}
|
||||
};
|
||||
|
||||
const loadAOToken = () => {
|
||||
let token = loadYamlCfgFile(tokenFile.ao, true);
|
||||
if(typeof token !== 'object' || token === null || Array.isArray(token)){
|
||||
token = {};
|
||||
}
|
||||
return token;
|
||||
};
|
||||
|
||||
const saveAOToken = (data: Record<string, unknown>) => {
|
||||
const cfgFolder = path.dirname(tokenFile.ao);
|
||||
try{
|
||||
fs.ensureDirSync(cfgFolder);
|
||||
fs.writeFileSync(`${tokenFile.ao}.yml`, yaml.stringify(data));
|
||||
}
|
||||
catch(e){
|
||||
console.error('Can\'t save token file to disk!');
|
||||
}
|
||||
};
|
||||
|
||||
const loadHDSession = () => {
|
||||
let session = loadYamlCfgFile(sessCfgFile.hd, true);
|
||||
|
|
@ -242,7 +282,7 @@ const saveHDSession = (data: Record<string, unknown>) => {
|
|||
|
||||
|
||||
const loadHDToken = () => {
|
||||
let token = loadYamlCfgFile(tokenFile.cr, true);
|
||||
let token = loadYamlCfgFile(tokenFile.hd, true);
|
||||
if(typeof token !== 'object' || token === null || Array.isArray(token)){
|
||||
token = {};
|
||||
}
|
||||
|
|
@ -292,27 +332,19 @@ const loadHDProfile = () => {
|
|||
return profile;
|
||||
};
|
||||
|
||||
const loadFuniToken = () => {
|
||||
const loadedToken = loadYamlCfgFile<{
|
||||
token?: string
|
||||
}>(tokenFile.funi, true);
|
||||
let token: false|string = false;
|
||||
if (loadedToken && loadedToken.token)
|
||||
token = loadedToken.token;
|
||||
// info if token not set
|
||||
if(!token){
|
||||
console.info('[INFO] Token not set!\n');
|
||||
const loadNewHDToken = () => {
|
||||
let token = loadYamlCfgFile(tokenFile.hdNew, true);
|
||||
if(typeof token !== 'object' || token === null || Array.isArray(token)){
|
||||
token = {};
|
||||
}
|
||||
return token;
|
||||
};
|
||||
|
||||
const saveFuniToken = (data: {
|
||||
token?: string
|
||||
}) => {
|
||||
const cfgFolder = path.dirname(tokenFile.funi);
|
||||
const saveNewHDToken = (data: Record<string, unknown>) => {
|
||||
const cfgFolder = path.dirname(tokenFile.hdNew);
|
||||
try{
|
||||
fs.ensureDirSync(cfgFolder);
|
||||
fs.writeFileSync(`${tokenFile.funi}.yml`, yaml.stringify(data));
|
||||
fs.writeFileSync(`${tokenFile.hdNew}.yml`, yaml.stringify(data));
|
||||
}
|
||||
catch(e){
|
||||
console.error('Can\'t save token file to disk!');
|
||||
|
|
@ -353,18 +385,22 @@ const setState = (state: GuiState) => {
|
|||
export {
|
||||
loadBinCfg,
|
||||
loadCfg,
|
||||
loadFuniToken,
|
||||
saveFuniToken,
|
||||
saveCRSession,
|
||||
loadCRSession,
|
||||
saveCRToken,
|
||||
loadCRToken,
|
||||
saveADNToken,
|
||||
loadADNToken,
|
||||
saveHDSession,
|
||||
loadHDSession,
|
||||
saveHDToken,
|
||||
loadHDToken,
|
||||
saveNewHDToken,
|
||||
loadNewHDToken,
|
||||
saveHDProfile,
|
||||
loadHDProfile,
|
||||
saveAOToken,
|
||||
loadAOToken,
|
||||
getState,
|
||||
setState,
|
||||
writeYamlCfgFile,
|
||||
|
|
|
|||
|
|
@ -11,10 +11,13 @@ export type ItemType = {
|
|||
}[]
|
||||
|
||||
export type DataType = {
|
||||
funi: {
|
||||
hidive: {
|
||||
s: ItemType
|
||||
},
|
||||
hidive: {
|
||||
ao: {
|
||||
s: ItemType
|
||||
},
|
||||
adn: {
|
||||
s: ItemType
|
||||
},
|
||||
crunchy: {
|
||||
|
|
@ -24,14 +27,17 @@ export type DataType = {
|
|||
}
|
||||
|
||||
const addToArchive = (kind: {
|
||||
service: 'funi',
|
||||
type: 's'
|
||||
} | {
|
||||
service: 'crunchy',
|
||||
type: 's'|'srz'
|
||||
} | {
|
||||
service: 'hidive',
|
||||
type: 's'
|
||||
} | {
|
||||
service: 'ao',
|
||||
type: 's'
|
||||
} | {
|
||||
service: 'adn',
|
||||
type: 's'
|
||||
}, ID: string) => {
|
||||
const data = loadData();
|
||||
|
||||
|
|
@ -45,8 +51,8 @@ const addToArchive = (kind: {
|
|||
});
|
||||
(data as any)[kind.service][kind.type] = items;
|
||||
} else {
|
||||
if (kind.service === 'funi') {
|
||||
data['funi'] = {
|
||||
if (kind.service === 'ao') {
|
||||
data['ao'] = {
|
||||
s: [
|
||||
{
|
||||
id: ID,
|
||||
|
|
@ -65,6 +71,15 @@ const addToArchive = (kind: {
|
|||
already: [] as string[]
|
||||
} : []),
|
||||
};
|
||||
} else if (kind.service === 'adn') {
|
||||
data['adn'] = {
|
||||
s: [
|
||||
{
|
||||
id: ID,
|
||||
already: []
|
||||
}
|
||||
]
|
||||
};
|
||||
} else {
|
||||
data['hidive'] = {
|
||||
s: [
|
||||
|
|
@ -80,14 +95,17 @@ const addToArchive = (kind: {
|
|||
};
|
||||
|
||||
const downloaded = (kind: {
|
||||
service: 'funi',
|
||||
type: 's'
|
||||
} | {
|
||||
service: 'crunchy',
|
||||
type: 's'|'srz'
|
||||
} | {
|
||||
service: 'hidive',
|
||||
type: 's'
|
||||
} | {
|
||||
service: 'ao',
|
||||
type: 's'
|
||||
} | {
|
||||
service: 'adn',
|
||||
type: 's'
|
||||
}, ID: string, episode: string[]) => {
|
||||
let data = loadData();
|
||||
if (!Object.prototype.hasOwnProperty.call(data, kind.service) || !Object.prototype.hasOwnProperty.call(data[kind.service], kind.type)
|
||||
|
|
@ -105,7 +123,7 @@ const downloaded = (kind: {
|
|||
fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4));
|
||||
};
|
||||
|
||||
const makeCommand = (service: 'funi'|'crunchy'|'hidive') : Partial<ArgvType>[] => {
|
||||
const makeCommand = (service: 'crunchy'|'hidive'|'ao'|'adn') : Partial<ArgvType>[] => {
|
||||
const data = loadData();
|
||||
const ret: Partial<ArgvType>[] = [];
|
||||
const kind = data[service];
|
||||
|
|
|
|||
146
modules/module.fetch.ts
Normal file
146
modules/module.fetch.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import * as yamlCfg from './module.cfg-loader';
|
||||
import { console } from './log';
|
||||
import { Method } from 'got';
|
||||
|
||||
export type Params = {
|
||||
method?: Method,
|
||||
headers?: Record<string, string>,
|
||||
body?: string | Buffer,
|
||||
binary?: boolean,
|
||||
followRedirect?: 'follow' | 'error' | 'manual'
|
||||
}
|
||||
|
||||
// req
|
||||
export class Req {
|
||||
private sessCfg: string;
|
||||
private service: 'cr'|'hd'|'ao'|'adn';
|
||||
private session: Record<string, {
|
||||
value: string;
|
||||
expires: Date;
|
||||
path: string;
|
||||
domain: string;
|
||||
secure: boolean;
|
||||
'Max-Age'?: string
|
||||
}> = {};
|
||||
private cfgDir = yamlCfg.cfgDir;
|
||||
private curl: boolean|string = false;
|
||||
|
||||
constructor(private domain: Record<string, unknown>, private debug: boolean, private nosess = false, private type: 'cr'|'hd'|'ao'|'adn') {
|
||||
this.sessCfg = yamlCfg.sessCfgFile[type];
|
||||
this.service = type;
|
||||
}
|
||||
|
||||
async getData(durl: string, params?: RequestInit) {
|
||||
params = params || {};
|
||||
// options
|
||||
const options: RequestInit = {
|
||||
method: params.method ? params.method : 'GET',
|
||||
headers: {
|
||||
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
||||
'accept-language': 'en-US,en;q=0.9',
|
||||
'cache-control': 'no-cache',
|
||||
'pragma': 'no-cache',
|
||||
'sec-ch-ua': '"Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
'sec-fetch-dest': 'document',
|
||||
'sec-fetch-mode': 'navigate',
|
||||
'sec-fetch-site': 'none',
|
||||
'sec-fetch-user': '?1',
|
||||
'upgrade-insecure-requests': '1',
|
||||
},
|
||||
};
|
||||
// additional params
|
||||
if(params.headers){
|
||||
options.headers = {...options.headers, ...params.headers};
|
||||
}
|
||||
if(options.method == 'POST'){
|
||||
if (!(options.headers as Record<string, string>)['Content-Type']) {
|
||||
(options.headers as Record<string, string>)['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||
}
|
||||
}
|
||||
if(params.body){
|
||||
options.body = params.body;
|
||||
}
|
||||
if(typeof params.redirect == 'string'){
|
||||
options.redirect = params.redirect;
|
||||
}
|
||||
// debug
|
||||
if(this.debug){
|
||||
console.debug('[DEBUG] FETCH OPTIONS:');
|
||||
console.debug(options);
|
||||
}
|
||||
// try do request
|
||||
try {
|
||||
const res = await fetch(durl, options);
|
||||
if (!res.ok) {
|
||||
console.error(`${res.status}: ${res.statusText}`);
|
||||
const body = await res.text();
|
||||
const docTitle = body.match(/<title>(.*)<\/title>/);
|
||||
if(body && docTitle){
|
||||
console.error(docTitle[1]);
|
||||
} else {
|
||||
console.error(body);
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: res.ok,
|
||||
res
|
||||
};
|
||||
}
|
||||
catch(_error){
|
||||
const error = _error as {
|
||||
name: string
|
||||
} & TypeError & {
|
||||
res: Response
|
||||
};
|
||||
if (error.res && error.res.status && error.res.statusText) {
|
||||
console.error(`${error.name} ${error.res.status}: ${error.res.statusText}`);
|
||||
} else {
|
||||
console.error(`${error.name}: ${error.res?.statusText || error.message}`);
|
||||
}
|
||||
if(error.res) {
|
||||
const body = await error.res.text();
|
||||
const docTitle = body.match(/<title>(.*)<\/title>/);
|
||||
if(body && docTitle){
|
||||
console.error(docTitle[1]);
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function buildProxy(proxyBaseUrl: string, proxyAuth: string){
|
||||
if(!proxyBaseUrl.match(/^(https?|socks4|socks5):/)){
|
||||
proxyBaseUrl = 'http://' + proxyBaseUrl;
|
||||
}
|
||||
|
||||
const proxyCfg = new URL(proxyBaseUrl);
|
||||
let proxyStr = `${proxyCfg.protocol}//`;
|
||||
|
||||
if(typeof proxyCfg.hostname != 'string' || proxyCfg.hostname == ''){
|
||||
throw new Error('[ERROR] Hostname and port required for proxy!');
|
||||
}
|
||||
|
||||
if(proxyAuth && typeof proxyAuth == 'string' && proxyAuth.match(':')){
|
||||
proxyCfg.username = proxyAuth.split(':')[0];
|
||||
proxyCfg.password = proxyAuth.split(':')[1];
|
||||
proxyStr += `${proxyCfg.username}:${proxyCfg.password}@`;
|
||||
}
|
||||
|
||||
proxyStr += proxyCfg.hostname;
|
||||
|
||||
if(!proxyCfg.port && proxyCfg.protocol == 'http:'){
|
||||
proxyStr += ':80';
|
||||
}
|
||||
else if(!proxyCfg.port && proxyCfg.protocol == 'https:'){
|
||||
proxyStr += ':443';
|
||||
}
|
||||
|
||||
return proxyStr;
|
||||
}
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ const parseFileName = (input: string, variables: Variable[], numbers: number, ov
|
|||
|
||||
if (use.type === 'number') {
|
||||
const len = use.replaceWith.toFixed(0).length;
|
||||
const replaceStr = len < numbers ? '0'.repeat(numbers - len) + use.replaceWith : use.replaceWith.toFixed(0);
|
||||
const replaceStr = len < numbers ? '0'.repeat(numbers - len) + use.replaceWith : use.replaceWith+'';
|
||||
input = input.replace(type, replaceStr);
|
||||
} else {
|
||||
if (use.sanitize)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@ function assFonts(ass: string){
|
|||
styles.push(addStyle[1]);
|
||||
}
|
||||
}
|
||||
const fontMatches = ass.matchAll(/\\fn([^\\}]+)/g);
|
||||
for (const match of fontMatches) {
|
||||
styles.push(match[1]);
|
||||
}
|
||||
return [...new Set(styles)];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,34 +3,35 @@
|
|||
export type LanguageItem = {
|
||||
cr_locale?: string,
|
||||
hd_locale?: string,
|
||||
adn_locale?: string,
|
||||
new_hd_locale?: string,
|
||||
ao_locale?: string,
|
||||
locale: string,
|
||||
code: string,
|
||||
name: string,
|
||||
language?: string,
|
||||
funi_locale?: string,
|
||||
funi_name?: string,
|
||||
funi_name_lagacy?: string
|
||||
language?: string
|
||||
}
|
||||
|
||||
const languages: LanguageItem[] = [
|
||||
{ cr_locale: 'en-US', hd_locale: 'English', funi_locale: 'enUS', locale: 'en', code: 'eng', name: 'English' },
|
||||
{ cr_locale: 'en-US', new_hd_locale: 'en-US', hd_locale: 'English', locale: 'en', code: 'eng', name: 'English' },
|
||||
{ cr_locale: 'en-IN', locale: 'en-IN', code: 'eng', name: 'English (India)', },
|
||||
{ cr_locale: 'es-LA', hd_locale: 'Spanish LatAm', funi_name: 'Spanish (LAS)', funi_name_lagacy: 'Spanish (Latin Am)', funi_locale: 'esLA', locale: 'es-419', code: 'spa', name: 'Spanish', language: 'Latin American Spanish' },
|
||||
{ cr_locale: 'es-419',hd_locale: 'Spanish', locale: 'es-419', code: 'spa-419', name: 'Spanish', language: 'Latin American Spanish' },
|
||||
{ cr_locale: 'es-ES', hd_locale: 'Spanish Europe', locale: 'es-ES', code: 'spa-ES', name: 'Castilian', language: 'European Spanish' },
|
||||
{ cr_locale: 'pt-BR', hd_locale: 'Portuguese', funi_name: 'Portuguese (Brazil)', funi_locale: 'ptBR', locale: 'pt-BR', code: 'por', name: 'Portuguese', language: 'Brazilian Portuguese' },
|
||||
{ cr_locale: 'es-LA', new_hd_locale: 'es-MX', hd_locale: 'Spanish LatAm', locale: 'es-419', code: 'spa', name: 'Spanish', language: 'Latin American Spanish' },
|
||||
{ cr_locale: 'es-419',ao_locale: 'es',hd_locale: 'Spanish', locale: 'es-419', code: 'spa-419', name: 'Spanish', language: 'Latin American Spanish' },
|
||||
{ cr_locale: 'es-ES', new_hd_locale: 'es-ES', hd_locale: 'Spanish Europe', locale: 'es-ES', code: 'spa-ES', name: 'Castilian', language: 'European Spanish' },
|
||||
{ cr_locale: 'pt-BR', ao_locale: 'pt',new_hd_locale: 'pt-BR', hd_locale: 'Portuguese', locale: 'pt-BR', code: 'por', name: 'Portuguese', language: 'Brazilian Portuguese' },
|
||||
{ cr_locale: 'pt-PT', locale: 'pt-PT', code: 'por', name: 'Portuguese (Portugal)', language: 'Portugues (Portugal)' },
|
||||
{ cr_locale: 'fr-FR', hd_locale: 'French', locale: 'fr', code: 'fra', name: 'French' },
|
||||
{ cr_locale: 'de-DE', hd_locale: 'German', locale: 'de', code: 'deu', name: 'German' },
|
||||
{ cr_locale: 'fr-FR', adn_locale: 'fr', hd_locale: 'French', locale: 'fr', code: 'fra', name: 'French' },
|
||||
{ cr_locale: 'de-DE', adn_locale: 'de', hd_locale: 'German', locale: 'de', code: 'deu', name: 'German' },
|
||||
{ cr_locale: 'ar-ME', locale: 'ar', code: 'ara-ME', name: 'Arabic' },
|
||||
{ cr_locale: 'ar-SA', hd_locale: 'Arabic', locale: 'ar', code: 'ara', name: 'Arabic (Saudi Arabia)' },
|
||||
{ cr_locale: 'it-IT', hd_locale: 'Italian', locale: 'it', code: 'ita', name: 'Italian' },
|
||||
{ cr_locale: 'ru-RU', hd_locale: 'Russian', locale: 'ru', code: 'rus', name: 'Russian' },
|
||||
{ cr_locale: 'tr-TR', hd_locale: 'Turkish', locale: 'tr', code: 'tur', name: 'Turkish' },
|
||||
{ cr_locale: 'hi-IN', locale: 'hi', code: 'hin', name: 'Hindi' },
|
||||
{ funi_locale: 'zhMN', locale: 'zh', code: 'cmn', name: 'Chinese (Mandarin, PRC)' },
|
||||
{ locale: 'zh', code: 'cmn', name: 'Chinese (Mandarin, PRC)' },
|
||||
{ cr_locale: 'zh-CN', locale: 'zh-CN', code: 'zho', name: 'Chinese (Mainland China)' },
|
||||
{ cr_locale: 'zh-TW', locale: 'zh-TW', code: 'chi', name: 'Chinese (Taiwan)' },
|
||||
{ cr_locale: 'zh-HK', locale: 'zh-HK', code: 'zh-HK', name: 'Chinese (Hong-Kong)', language: '中文 (粵語)' },
|
||||
{ cr_locale: 'ko-KR', hd_locale: 'Korean', locale: 'ko', code: 'kor', name: 'Korean' },
|
||||
{ cr_locale: 'ca-ES', locale: 'ca-ES', code: 'cat', name: 'Catalan' },
|
||||
{ cr_locale: 'pl-PL', locale: 'pl-PL', code: 'pol', name: 'Polish' },
|
||||
|
|
@ -40,7 +41,7 @@ const languages: LanguageItem[] = [
|
|||
{ cr_locale: 'vi-VN', locale: 'vi-VN', code: 'vie', name: 'Vietnamese', language: 'Tiếng Việt' },
|
||||
{ cr_locale: 'id-ID', locale: 'id-ID', code: 'ind', name: 'Indonesian', language: 'Bahasa Indonesia' },
|
||||
{ cr_locale: 'te-IN', locale: 'te-IN', code: 'tel', name: 'Telugu (India)', language: 'తెలుగు' },
|
||||
{ cr_locale: 'ja-JP', hd_locale: 'Japanese', funi_locale: 'jaJP', locale: 'ja', code: 'jpn', name: 'Japanese' },
|
||||
{ cr_locale: 'ja-JP', adn_locale: 'ja', ao_locale: 'ja', hd_locale: 'Japanese', locale: 'ja', code: 'jpn', name: 'Japanese' },
|
||||
];
|
||||
|
||||
// add en language names
|
||||
|
|
@ -68,7 +69,11 @@ const subtitleLanguagesFilter = (() => {
|
|||
})();
|
||||
|
||||
const searchLocales = (() => {
|
||||
return ['', ...new Set(languages.map(l => { return l.cr_locale; }).slice(0, -1))];
|
||||
return ['', ...new Set(languages.map(l => { return l.cr_locale; }).slice(0, -1)), ...new Set(languages.map(l => { return l.adn_locale; }).slice(0, -1))];
|
||||
})();
|
||||
|
||||
export const aoSearchLocales = (() => {
|
||||
return ['', ...new Set(languages.map(l => { return l.ao_locale; }).slice(0, -1))];
|
||||
})();
|
||||
|
||||
// convert
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ class Merger {
|
|||
for (const [vnaIndex, vna] of vnas.entries()) {
|
||||
const streamInfo = await ffprobe(vna.path, { path: bin.ffprobe as string });
|
||||
const videoInfo = streamInfo.streams.filter(stream => stream.codec_type == 'video');
|
||||
vnas[vnaIndex].duration = videoInfo[0].duration;
|
||||
vnas[vnaIndex].duration = parseInt(videoInfo[0].duration as string);
|
||||
}
|
||||
//Sort videoAndAudio streams by duration (shortest first)
|
||||
vnas.sort((a,b) => {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ const usefulCookies = {
|
|||
// req
|
||||
class Req {
|
||||
private sessCfg: string;
|
||||
private service: 'cr'|'funi'|'hd';
|
||||
private service: 'cr'|'hd'|'ao';
|
||||
private session: Record<string, {
|
||||
value: string;
|
||||
expires: Date;
|
||||
|
|
@ -39,7 +39,7 @@ class Req {
|
|||
private cfgDir = yamlCfg.cfgDir;
|
||||
private curl: boolean|string = false;
|
||||
|
||||
constructor(private domain: Record<string, unknown>, private debug: boolean, private nosess = false, private type: 'cr'|'funi'|'hd') {
|
||||
constructor(private domain: Record<string, unknown>, private debug: boolean, private nosess = false, private type: 'cr'|'hd'|'ao') {
|
||||
this.sessCfg = yamlCfg.sessCfgFile[type];
|
||||
this.service = type;
|
||||
}
|
||||
|
|
@ -61,7 +61,9 @@ class Req {
|
|||
options.headers = {...options.headers, ...params.headers};
|
||||
}
|
||||
if(options.method == 'POST'){
|
||||
(options.headers as Headers)['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||
if (!(options.headers as Headers)['Content-Type']) {
|
||||
(options.headers as Headers)['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||
}
|
||||
}
|
||||
if(params.body){
|
||||
options.body = params.body;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Playlist, parse as mpdParse } from 'mpd-parser';
|
||||
import { LanguageItem } from './module.langsData';
|
||||
import { parse as mpdParse } from 'mpd-parser';
|
||||
import { LanguageItem, findLang, languages } from './module.langsData';
|
||||
import { console } from './log';
|
||||
|
||||
type Segment = {
|
||||
uri: string;
|
||||
|
|
@ -7,9 +8,17 @@ type Segment = {
|
|||
duration: number;
|
||||
map: {
|
||||
uri: string;
|
||||
byterange?: {
|
||||
length: number,
|
||||
offset: number
|
||||
};
|
||||
};
|
||||
number: number;
|
||||
presentationTime: number;
|
||||
byterange?: {
|
||||
length: number,
|
||||
offset: number
|
||||
};
|
||||
number?: number;
|
||||
presentationTime?: number;
|
||||
}
|
||||
|
||||
export type PlaylistItem = {
|
||||
|
|
@ -20,7 +29,8 @@ export type PlaylistItem = {
|
|||
|
||||
|
||||
type AudioPlayList = {
|
||||
language: LanguageItem
|
||||
language: LanguageItem,
|
||||
default: boolean
|
||||
} & PlaylistItem
|
||||
|
||||
type VideoPlayList = {
|
||||
|
|
@ -37,31 +47,77 @@ export type MPDParsed = {
|
|||
}
|
||||
}
|
||||
|
||||
export function parse(manifest: string, language: LanguageItem, url?: string) {
|
||||
export async function parse(manifest: string, language?: LanguageItem, url?: string) {
|
||||
if (!manifest.includes('BaseURL') && url) {
|
||||
manifest = manifest.replace(/(<MPD[^]^[^]*?>)/gm, `$1<BaseURL>${url}</BaseURL>`);
|
||||
manifest = manifest.replace(/(<MPD*\b[^>]*>)/gm, `$1<BaseURL>${url}</BaseURL>`);
|
||||
}
|
||||
const parsed = mpdParse(manifest);
|
||||
const ret: MPDParsed = {};
|
||||
|
||||
// Audio Loop
|
||||
for (const item of Object.values(parsed.mediaGroups.AUDIO.audio)){
|
||||
for (const playlist of item.playlists) {
|
||||
const host = new URL(playlist.resolvedUri).hostname;
|
||||
if (!Object.prototype.hasOwnProperty.call(ret, host))
|
||||
ret[host] = { audio: [], video: [] };
|
||||
|
||||
|
||||
if (playlist.sidx && playlist.segments.length == 0) {
|
||||
const options: RequestInit = {
|
||||
method: 'head'
|
||||
};
|
||||
if (playlist.sidx.uri.includes('animecdn')) options.headers = {
|
||||
'origin': 'https://www.animeonegai.com',
|
||||
'referer': 'https://www.animeonegai.com/',
|
||||
};
|
||||
const item = await fetch(playlist.sidx.uri, options);
|
||||
if (!item.ok) console.warn(`${item.status}: ${item.statusText}, Unable to fetch byteLength for audio stream ${Math.round(playlist.attributes.BANDWIDTH/1024)}KiB/s`);
|
||||
const byteLength = parseInt(item.headers.get('content-length') as string);
|
||||
let currentByte = playlist.sidx.map.byterange.length;
|
||||
while (currentByte <= byteLength) {
|
||||
playlist.segments.push({
|
||||
'duration': 0,
|
||||
'map': {
|
||||
'uri': playlist.resolvedUri,
|
||||
'resolvedUri': playlist.resolvedUri,
|
||||
'byterange': playlist.sidx.map.byterange
|
||||
},
|
||||
'uri': playlist.resolvedUri,
|
||||
'resolvedUri': playlist.resolvedUri,
|
||||
'byterange': {
|
||||
'length': 500000,
|
||||
'offset': currentByte
|
||||
},
|
||||
timeline: 0,
|
||||
number: 0,
|
||||
presentationTime: 0
|
||||
});
|
||||
currentByte = currentByte + 500000;
|
||||
}
|
||||
}
|
||||
|
||||
//Find and add audio language if it is found in the MPD
|
||||
let audiolang: LanguageItem;
|
||||
const foundlanguage = findLang(languages.find(a => a.code === item.language)?.cr_locale ?? 'unknown');
|
||||
if (item.language) {
|
||||
audiolang = foundlanguage;
|
||||
} else {
|
||||
audiolang = language ? language : foundlanguage;
|
||||
}
|
||||
const pItem: AudioPlayList = {
|
||||
bandwidth: playlist.attributes.BANDWIDTH,
|
||||
language: language,
|
||||
language: audiolang,
|
||||
default: item.default,
|
||||
segments: playlist.segments.map((segment): Segment => {
|
||||
const uri = segment.resolvedUri;
|
||||
const map_uri = segment.map.resolvedUri;
|
||||
return {
|
||||
duration: segment.duration,
|
||||
map: { uri: map_uri },
|
||||
map: { uri: map_uri, byterange: segment.map.byterange },
|
||||
number: segment.number,
|
||||
presentationTime: segment.presentationTime,
|
||||
timeline: segment.timeline,
|
||||
byterange: segment.byterange,
|
||||
uri
|
||||
};
|
||||
})
|
||||
|
|
@ -75,11 +131,46 @@ export function parse(manifest: string, language: LanguageItem, url?: string) {
|
|||
}
|
||||
}
|
||||
|
||||
// Video Loop
|
||||
for (const playlist of parsed.playlists) {
|
||||
const host = new URL(playlist.resolvedUri).hostname;
|
||||
if (!Object.prototype.hasOwnProperty.call(ret, host))
|
||||
ret[host] = { audio: [], video: [] };
|
||||
|
||||
if (playlist.sidx && playlist.segments.length == 0) {
|
||||
const options: RequestInit = {
|
||||
method: 'head'
|
||||
};
|
||||
if (playlist.sidx.uri.includes('animecdn')) options.headers = {
|
||||
'origin': 'https://www.animeonegai.com',
|
||||
'referer': 'https://www.animeonegai.com/',
|
||||
};
|
||||
const item = await fetch(playlist.sidx.uri, options);
|
||||
if (!item.ok) console.warn(`${item.status}: ${item.statusText}, Unable to fetch byteLength for video stream ${playlist.attributes.RESOLUTION?.height}x${playlist.attributes.RESOLUTION?.width}@${Math.round(playlist.attributes.BANDWIDTH/1024)}KiB/s`);
|
||||
const byteLength = parseInt(item.headers.get('content-length') as string);
|
||||
let currentByte = playlist.sidx.map.byterange.length;
|
||||
while (currentByte <= byteLength) {
|
||||
playlist.segments.push({
|
||||
'duration': 0,
|
||||
'map': {
|
||||
'uri': playlist.resolvedUri,
|
||||
'resolvedUri': playlist.resolvedUri,
|
||||
'byterange': playlist.sidx.map.byterange
|
||||
},
|
||||
'uri': playlist.resolvedUri,
|
||||
'resolvedUri': playlist.resolvedUri,
|
||||
'byterange': {
|
||||
'length': 2000000,
|
||||
'offset': currentByte
|
||||
},
|
||||
timeline: 0,
|
||||
number: 0,
|
||||
presentationTime: 0
|
||||
});
|
||||
currentByte = currentByte + 2000000;
|
||||
}
|
||||
}
|
||||
|
||||
const pItem: VideoPlayList = {
|
||||
bandwidth: playlist.attributes.BANDWIDTH,
|
||||
quality: playlist.attributes.RESOLUTION!,
|
||||
|
|
@ -88,10 +179,11 @@ export function parse(manifest: string, language: LanguageItem, url?: string) {
|
|||
const map_uri = segment.map.resolvedUri;
|
||||
return {
|
||||
duration: segment.duration,
|
||||
map: { uri: map_uri },
|
||||
map: { uri: map_uri, byterange: segment.map.byterange },
|
||||
number: segment.number,
|
||||
presentationTime: segment.presentationTime,
|
||||
timeline: segment.timeline,
|
||||
byterange: segment.byterange,
|
||||
uri
|
||||
};
|
||||
})
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ let relGroup = '';
|
|||
let fontSize = 0;
|
||||
let tmMrg = 0;
|
||||
let rFont = '';
|
||||
let doCombineLines = false;
|
||||
|
||||
type Css = Record<string, {
|
||||
params: string;
|
||||
|
|
@ -44,7 +45,7 @@ function loadCSS(cssStr: string): Css {
|
|||
if (l === '') continue;
|
||||
const m = l.match(/^(.*)\{(.*)\}$/);
|
||||
if (!m) {
|
||||
console.error(`[WARN] VTT2ASS: Invalid css in line ${i}: ${l}`);
|
||||
console.error(`VTT2ASS: Invalid css in line ${i}: ${l}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -69,7 +70,7 @@ function loadCSS(cssStr: string): Css {
|
|||
function parseStyle(stylegroup: string, line: string, style: any) {
|
||||
const defaultSFont = rFont == '' ? defaultStyleFont : rFont; //redeclare cause of let
|
||||
|
||||
if (stylegroup.startsWith('Subtitle') || stylegroup.startsWith('Song')) { //base for dialog, everything else use defaultStyle
|
||||
if (stylegroup.startsWith('Subtitle') || stylegroup.startsWith('Song') || stylegroup.startsWith('Q') || stylegroup.startsWith('Default')) { //base for dialog, everything else use defaultStyle
|
||||
style = `${defaultSFont},${fontSize},&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2.6,0,2,20,20,46,1`;
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +83,8 @@ function parseStyle(stylegroup: string, line: string, style: any) {
|
|||
for (const s of line.split(';')) {
|
||||
if (s == '') continue;
|
||||
const st = s.trim().split(':');
|
||||
if (st[0]) st[0] = st[0].trim();
|
||||
if (st[1]) st[1] = st[1].trim();
|
||||
let cl, arr, transformed_str;
|
||||
switch (st[0]) {
|
||||
case 'font-family':
|
||||
|
|
@ -123,12 +126,26 @@ function parseStyle(stylegroup: string, line: string, style: any) {
|
|||
break;
|
||||
}
|
||||
break;
|
||||
case 'text-decoration':
|
||||
if (st[1] === 'underline') {
|
||||
style[8] = -1;
|
||||
} else {
|
||||
console.warn(`vtt2ass: Unknown text-decoration value: ${st[1]}`);
|
||||
}
|
||||
break;
|
||||
case 'right':
|
||||
style[17] = 3;
|
||||
break;
|
||||
case 'left':
|
||||
style[17] = 1;
|
||||
break;
|
||||
case 'font-style':
|
||||
if (st[1] === 'italic') {
|
||||
style[7] = -1;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'background-color':
|
||||
case 'background':
|
||||
if (st[1] === 'none') {
|
||||
break;
|
||||
|
|
@ -143,18 +160,18 @@ function parseStyle(stylegroup: string, line: string, style: any) {
|
|||
transformed_str[1] = arr.map(r => r.replace(/-/g, '').replace(/px/g, '').replace(/(^| )0( |$)/g, ' ').trim()).join(' ');
|
||||
arr = transformed_str[1].split(' ');
|
||||
if (arr.length != 10) {
|
||||
console.info(`[WARN] VTT2ASS: Can't properly parse text-shadow: ${s.trim()}`);
|
||||
console.warn(`VTT2ASS: Can't properly parse text-shadow: ${s.trim()}`);
|
||||
break;
|
||||
}
|
||||
arr = [...new Set(arr)];
|
||||
if (arr.length > 1) {
|
||||
console.info(`[WARN] VTT2ASS: Can't properly parse text-shadow: ${s.trim()}`);
|
||||
console.warn(`VTT2ASS: Can't properly parse text-shadow: ${s.trim()}`);
|
||||
break;
|
||||
}
|
||||
style[16] = arr[0];
|
||||
break;
|
||||
default:
|
||||
console.error(`[WARN] VTT2ASS: Unknown style: ${s.trim()}`);
|
||||
console.error(`VTT2ASS: Unknown style: ${s.trim()}`);
|
||||
}
|
||||
}
|
||||
return style.join(',');
|
||||
|
|
@ -163,7 +180,7 @@ function parseStyle(stylegroup: string, line: string, style: any) {
|
|||
function getPxSize(size_line: string, font_size: number) {
|
||||
const m = size_line.trim().match(/([\d.]+)(.*)/);
|
||||
if (!m) {
|
||||
console.error(`[WARN] VTT2ASS: Unknown size: ${size_line}`);
|
||||
console.error(`VTT2ASS: Unknown size: ${size_line}`);
|
||||
return;
|
||||
}
|
||||
let size = parseFloat(m[1]);
|
||||
|
|
@ -174,8 +191,7 @@ function getPxSize(size_line: string, font_size: number) {
|
|||
function getColor(c: string) {
|
||||
if (c[0] !== '#') {
|
||||
c = colors[c as keyof typeof colors];
|
||||
}
|
||||
else if (c.length < 7) {
|
||||
} else if (c.length < 7 || c.length > 7) {
|
||||
c = `#${c[1]}${c[1]}${c[2]}${c[2]}${c[3]}${c[3]}`;
|
||||
}
|
||||
const m = c.match(/#(..)(..)(..)/);
|
||||
|
|
@ -226,6 +242,56 @@ function loadVTT(vttStr: string): Vtt[] {
|
|||
return data;
|
||||
}
|
||||
|
||||
function timestampToCentiseconds(timestamp: string) {
|
||||
const timestamp_split = timestamp.split(':');
|
||||
const timestamp_sec_split = timestamp_split[2].split('.');
|
||||
const hour = parseInt(timestamp_split[0]);
|
||||
const minute = parseInt(timestamp_split[1]);
|
||||
const second = parseInt(timestamp_sec_split[0]);
|
||||
const centisecond = parseInt(timestamp_sec_split[1]);
|
||||
|
||||
return 360000 * hour + 6000 * minute + 100 * second + centisecond;
|
||||
}
|
||||
|
||||
function combineLines(events: string[]): string[] {
|
||||
if (!doCombineLines) {
|
||||
return events;
|
||||
}
|
||||
// This function is for combining adjacent lines with same information
|
||||
const newLines: string[] = [];
|
||||
for (const currentLine of events) {
|
||||
let hasCombined: boolean = false;
|
||||
// Check previous 7 elements, arbritary lookback amount
|
||||
for (let j = 1; j < 8 && j < newLines.length; j++) {
|
||||
const checkLine = newLines[newLines.length - j];
|
||||
const checkLineSplit = checkLine.split(',');
|
||||
const currentLineSplit = currentLine.split(',');
|
||||
// 1 = start, 2 = end, 3 = style, 9+ = text
|
||||
if (checkLineSplit.slice(9).join(',') == currentLineSplit.slice(9).join(',') &&
|
||||
checkLineSplit[3] == currentLineSplit[3] &&
|
||||
checkLineSplit[2] == currentLineSplit[1]
|
||||
) {
|
||||
checkLineSplit[2] = currentLineSplit[2];
|
||||
newLines[newLines.length - j] = checkLineSplit.join(',');
|
||||
hasCombined = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasCombined) {
|
||||
newLines.push(currentLine);
|
||||
}
|
||||
}
|
||||
return newLines;
|
||||
}
|
||||
|
||||
function pushBuffer(buffer: ReturnType<typeof convertLine>[], events: string[]) {
|
||||
buffer.reverse();
|
||||
const bufferStrings: string[] = buffer.map(line =>
|
||||
`Dialogue: 1,${line.start},${line.end},${line.style},,0,0,0,,${line.text}`);
|
||||
events.push(...bufferStrings);
|
||||
buffer.splice(0,buffer.length);
|
||||
}
|
||||
|
||||
function convert(css: Css, vtt: Vtt[]) {
|
||||
const stylesMap: Record<string, string> = {};
|
||||
let ass = [
|
||||
|
|
@ -261,6 +327,8 @@ function convert(css: Css, vtt: Vtt[]) {
|
|||
song_cap: [],
|
||||
};
|
||||
const linesMap: Record<string, number> = {};
|
||||
const buffer: ReturnType<typeof convertLine>[] = [];
|
||||
const captionsBuffer: string[] = [];
|
||||
for (const l in vtt) {
|
||||
const x = convertLine(stylesMap, vtt[l]);
|
||||
if (x.ind !== '' && linesMap[x.ind] !== undefined) {
|
||||
|
|
@ -278,8 +346,42 @@ function convert(css: Css, vtt: Vtt[]) {
|
|||
linesMap[x.ind] = events[x.type as keyof typeof events].length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* What cursed code have I brought upon this land?
|
||||
* This handles making lines multi-line when neccesary and reverses
|
||||
* order of subtitles so that they display correctly
|
||||
*/
|
||||
if (x.type != 'subtitle') {
|
||||
// Do nothing
|
||||
} else if (x.text.includes('\\pos')) {
|
||||
events['subtitle'].pop();
|
||||
captionsBuffer.push(x.res);
|
||||
} else if (buffer.length > 0) {
|
||||
const previousBufferLine = buffer[buffer.length - 1];
|
||||
const previousStart = timestampToCentiseconds(previousBufferLine.start);
|
||||
const currentStart = timestampToCentiseconds(x.start);
|
||||
events['subtitle'].pop();
|
||||
if ((currentStart - previousStart) <= 2) {
|
||||
x.start = previousBufferLine.start;
|
||||
if (previousBufferLine.style == x.style) {
|
||||
buffer.pop();
|
||||
x.text = previousBufferLine.text + '\\N' + x.text;
|
||||
}
|
||||
} else {
|
||||
pushBuffer(buffer, events['subtitle']);
|
||||
}
|
||||
buffer.push(x);
|
||||
}
|
||||
else {
|
||||
events['subtitle'].pop();
|
||||
buffer.push(x);
|
||||
}
|
||||
}
|
||||
|
||||
pushBuffer(buffer, events['subtitle']);
|
||||
events['subtitle'].push(...captionsBuffer);
|
||||
events['subtitle'] = combineLines(events['subtitle']);
|
||||
|
||||
if (events.subtitle.length > 0) {
|
||||
ass = ass.concat(
|
||||
//`Comment: 0,0:00:00.00,0:00:00.00,${defaultStyleName},,0,0,0,,** Subtitles **`,
|
||||
|
|
@ -343,7 +445,8 @@ function convertLine(css: Record<string, string>, l: Record<any, any>) {
|
|||
}
|
||||
|
||||
function convertText(text: string) {
|
||||
const m = text.match(/<c\.([^>]*)>([\S\s]*)<\/c>/);
|
||||
//const m = text.match(/<c\.([^>]*)>([\S\s]*)<\/c>/);
|
||||
const m = text.match(/<(?:c\.|)([^>]*)>([\S\s]*)<\/(?:c|Default)>/);
|
||||
let style = '';
|
||||
if (m) {
|
||||
style = m[1];
|
||||
|
|
@ -353,7 +456,7 @@ function convertText(text: string) {
|
|||
// .replace(/<c[^>]*>[^<]*<\/c>/g, '')
|
||||
// .replace(/<ruby[^>]*>[^<]*<\/ruby>/g, '')
|
||||
.replace(/ \\N$/g, '\\N')
|
||||
.replace(/<[^>]>/g, '')
|
||||
//.replace(/<[^>]>/g, '')
|
||||
.replace(/\\N$/, '')
|
||||
.replace(/\r/g, '')
|
||||
.replace(/\n/g, '\\N')
|
||||
|
|
@ -394,15 +497,32 @@ function toSubTime(str: string) {
|
|||
return n.slice(0, 3).join(':') + '.' + n[3];
|
||||
}
|
||||
|
||||
function vtt(group: string | undefined, xFontSize: number | undefined, vttStr: string, cssStr: string, timeMargin?: number, replaceFont?: string) {
|
||||
export default function vtt2ass(group: string | undefined, xFontSize: number | undefined, vttStr: string, cssStr: string, timeMargin?: number, replaceFont?: string, combineLines?: boolean) {
|
||||
relGroup = group ?? '';
|
||||
fontSize = xFontSize && xFontSize > 0 ? xFontSize : 34; // 1em to pix
|
||||
tmMrg = timeMargin ? timeMargin : 0; //
|
||||
rFont = replaceFont ? replaceFont : rFont;
|
||||
doCombineLines = combineLines ? combineLines : doCombineLines;
|
||||
if (vttStr.match(/::cue(?:.(.+)\) *)?{([^}]+)}/g)) {
|
||||
const cssLines = [];
|
||||
let defaultCss = '';
|
||||
const cssGroups = vttStr.matchAll(/::cue(?:.(.+)\) *)?{([^}]+)}/g);
|
||||
for (const cssGroup of cssGroups) {
|
||||
//Below code will bulldoze defined sizes for custom ones
|
||||
/*if (!options.originalFontSize) {
|
||||
cssGroup[2] = cssGroup[2].replace(/( font-size:.+?;)/g, '').replace(/(font-size:.+?;)/g, '');
|
||||
}*/
|
||||
if (cssGroup[1]) {
|
||||
cssLines.push(`${cssGroup[1]}{${defaultCss}${cssGroup[2].replace(/(\r\n|\n|\r)/gm, '')}}`);
|
||||
} else {
|
||||
defaultCss = cssGroup[2].replace(/(\r\n|\n|\r)/gm, '');
|
||||
//cssLines.push(`{${defaultCss}}`);
|
||||
}
|
||||
}
|
||||
cssStr += cssLines.join('\r\n');
|
||||
}
|
||||
return convert(
|
||||
loadCSS(cssStr),
|
||||
loadVTT(vttStr)
|
||||
);
|
||||
}
|
||||
|
||||
export { vtt };
|
||||
|
|
|
|||
99
modules/widevine.ts
Normal file
99
modules/widevine.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import { KeyContainer, Session } from './license';
|
||||
import fs from 'fs';
|
||||
import { console } from './log';
|
||||
import got from 'got';
|
||||
import { workingDir } from './module.cfg-loader';
|
||||
import path from 'path';
|
||||
import { ReadError, Response } from 'got';
|
||||
|
||||
//read cdm files located in the same directory
|
||||
let privateKey: Buffer = Buffer.from([]), identifierBlob: Buffer = Buffer.from([]);
|
||||
export let canDecrypt: boolean;
|
||||
try {
|
||||
const files = fs.readdirSync(path.join(workingDir, 'widevine'));
|
||||
files.forEach(function(file) {
|
||||
file = path.join(workingDir, 'widevine', file);
|
||||
const stats = fs.statSync(file);
|
||||
if (stats.size < 1024*8 && stats.isFile()) {
|
||||
const fileContents = fs.readFileSync(file, {'encoding': 'utf8'});
|
||||
if (fileContents.includes('-BEGIN PRIVATE KEY-') || fileContents.includes('-BEGIN RSA PRIVATE KEY-')) {
|
||||
privateKey = fs.readFileSync(file);
|
||||
}
|
||||
if (fileContents.includes('widevine_cdm_version')) {
|
||||
identifierBlob = fs.readFileSync(file);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (privateKey.length !== 0 && identifierBlob.length !== 0) {
|
||||
canDecrypt = true;
|
||||
} else if (privateKey.length == 0) {
|
||||
console.warn('Private key missing');
|
||||
canDecrypt = false;
|
||||
} else if (identifierBlob.length == 0) {
|
||||
console.warn('Identifier blob missing');
|
||||
canDecrypt = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
canDecrypt = false;
|
||||
}
|
||||
|
||||
export default async function getKeys(pssh: string | undefined, licenseServer: string, authData: Record<string, string>): Promise<KeyContainer[]> {
|
||||
if (!pssh || !canDecrypt) return [];
|
||||
//pssh found in the mpd manifest
|
||||
const psshBuffer = Buffer.from(
|
||||
pssh,
|
||||
'base64'
|
||||
);
|
||||
|
||||
//Create a new widevine session
|
||||
const session = new Session({ privateKey, identifierBlob }, psshBuffer);
|
||||
|
||||
//Generate license
|
||||
let response;
|
||||
try {
|
||||
response = await got(licenseServer, {
|
||||
method: 'POST',
|
||||
body: session.createLicenseRequest(),
|
||||
headers: authData,
|
||||
responseType: 'text'
|
||||
});
|
||||
} catch(_error){
|
||||
const error = _error as {
|
||||
name: string
|
||||
} & ReadError & {
|
||||
res: Response<unknown>
|
||||
};
|
||||
if(error.response && error.response.statusCode && error.response.statusMessage){
|
||||
console.error(`${error.name} ${error.response.statusCode}: ${error.response.statusMessage}`);
|
||||
} else{
|
||||
console.error(`${error.name}: ${error.code || error.message}`);
|
||||
}
|
||||
if(error.response && !error.res){
|
||||
error.res = error.response;
|
||||
const docTitle = (error.res.body as string).match(/<title>(.*)<\/title>/);
|
||||
if(error.res.body && docTitle){
|
||||
console.error(docTitle[1]);
|
||||
}
|
||||
}
|
||||
if(error.res && error.res.body && error.response.statusCode
|
||||
&& error.response.statusCode != 404 && error.response.statusCode != 403){
|
||||
console.error('Body:', error.res.body);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
//Parse License and return keys
|
||||
try {
|
||||
const json = JSON.parse(response.body);
|
||||
return session.parseLicense(Buffer.from(json['license'], 'base64'));
|
||||
} catch {
|
||||
return session.parseLicense(response.rawBody);
|
||||
}
|
||||
} else {
|
||||
console.info('License request failed:', response.statusMessage, response.body);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
65
package.json
65
package.json
|
|
@ -1,13 +1,11 @@
|
|||
{
|
||||
"name": "multi-downloader-nx",
|
||||
"short_name": "aniDL",
|
||||
"version": "4.5.0",
|
||||
"description": "Downloader for Crunchyroll, Funimation, or Hidive via CLI or GUI",
|
||||
"version": "5.1.5",
|
||||
"description": "Downloader for Crunchyroll, Hidive, AnimeOnegai, and AnimationDigitalNetwork with CLI and GUI",
|
||||
"keywords": [
|
||||
"download",
|
||||
"downloader",
|
||||
"funimation",
|
||||
"funimationnow",
|
||||
"hidive",
|
||||
"crunchy",
|
||||
"crunchyroll",
|
||||
|
|
@ -42,60 +40,55 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.9",
|
||||
"@babel/plugin-syntax-flow": "^7.22.5",
|
||||
"@babel/plugin-transform-react-jsx": "^7.22.5",
|
||||
"@types/xmldom": "^0.1.34",
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"@yao-pkg/pkg": "^5.12.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"express": "^4.18.2",
|
||||
"esbuild": "^0.21.5",
|
||||
"express": "^4.19.2",
|
||||
"ffprobe": "^1.1.2",
|
||||
"form-data": "^4.0.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"fs-extra": "^11.2.0",
|
||||
"got": "^11.8.6",
|
||||
"iso-639": "^0.2.2",
|
||||
"leven": "^3.1.0",
|
||||
"log4js": "^6.9.1",
|
||||
"long": "^5.2.3",
|
||||
"lookpath": "^1.2.2",
|
||||
"m3u8-parsed": "^1.3.0",
|
||||
"mpd-parser": "^1.3.0",
|
||||
"open": "^8.4.2",
|
||||
"protobufjs": "^7.2.5",
|
||||
"protobufjs": "^7.3.2",
|
||||
"sei-helper": "^3.3.0",
|
||||
"typescript-eslint": "0.0.1-alpha.0",
|
||||
"ws": "^8.13.0",
|
||||
"xmldom": "^0.6.0",
|
||||
"yaml": "^2.3.1",
|
||||
"ws": "^8.17.1",
|
||||
"yaml": "^2.4.5",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.13",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/ffprobe": "^1.1.4",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/ws": "^8.5.5",
|
||||
"@types/yargs": "^17.0.24",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@vercel/webpack-asset-relocator-loader": "^1.7.3",
|
||||
"@yao-pkg/pkg": "^5.11.1",
|
||||
"eslint": "^8.45.0",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/ffprobe": "^1.1.8",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/node": "^20.14.6",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@types/yargs": "^17.0.32",
|
||||
"@typescript-eslint/eslint-plugin": "^7.13.1",
|
||||
"@typescript-eslint/parser": "^7.13.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-react": "7.32.2",
|
||||
"eslint-plugin-react": "7.34.3",
|
||||
"protoc": "^1.1.3",
|
||||
"removeNPMAbsolutePaths": "^3.0.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "5.1.6"
|
||||
"ts-node": "^10.9.2",
|
||||
"ts-proto": "^1.180.0",
|
||||
"typescript": "5.5.2",
|
||||
"typescript-eslint": "7.13.1"
|
||||
},
|
||||
"scripts": {
|
||||
"prestart": "pnpm run tsc test",
|
||||
"start": "pnpm prestart && cd lib && node gui.js",
|
||||
"gui": "cd ./gui/react/ && pnpm start",
|
||||
"docs": "ts-node modules/build-docs.ts",
|
||||
"tsc": "ts-node tsc.ts",
|
||||
"proto:compile": "protoc --plugin=protoc-gen-ts_proto=.\\node_modules\\.bin\\protoc-gen-ts_proto.cmd --ts_proto_opt=\"esModuleInterop=true\" --ts_proto_opt=\"forceLong=long\" --ts_proto_opt=\"env=node\" --ts_proto_out=. modules/*.proto",
|
||||
"prebuild-cli": "pnpm run tsc false false",
|
||||
"build-windows-cli": "pnpm run prebuild-cli && cd lib && node modules/build windows-x64",
|
||||
"build-linux-cli": "pnpm run prebuild-cli && cd lib && node modules/build linuxstatic-x64",
|
||||
|
|
@ -110,8 +103,8 @@
|
|||
"build-macos-gui": "pnpm run prebuild-gui && cd lib && node modules/build macos-x64 true",
|
||||
"build-alpine-gui": "pnpm run prebuild-gui && cd lib && node modules/build alpine-x64 true",
|
||||
"build-android-gui": "pnpm run prebuild-gui && cd lib && node modules/build linuxstatic-armv7 true",
|
||||
"eslint": "eslint *.js modules",
|
||||
"eslint-fix": "eslint *.js modules --fix",
|
||||
"eslint": "npx eslint .",
|
||||
"eslint-fix": "npx eslint . --fix",
|
||||
"pretest": "pnpm run tsc",
|
||||
"test": "pnpm run pretest && cd lib && node modules/build windows-x64 && node modules/build linuxstatic-x64 && node modules/build macos-x64"
|
||||
}
|
||||
|
|
|
|||
2407
pnpm-lock.yaml
2407
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -1,12 +0,0 @@
|
|||
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;
|
||||
};
|
||||
|
||||
export default useStore;
|
||||
13
tsc.ts
13
tsc.ts
|
|
@ -32,17 +32,18 @@ const ignore = [
|
|||
'./bin/mkvtoolnix*',
|
||||
'./config/token.yml$',
|
||||
'./config/updates.json$',
|
||||
'./config/cr_token.yml$',
|
||||
'./config/funi_token.yml$',
|
||||
'./config/hd_token.yml$',
|
||||
'./config/hd_sess.yml$',
|
||||
'./config/hd_profile.yml$',
|
||||
'./config/*_token.yml$',
|
||||
'./config/*_sess.yml$',
|
||||
'./config/*_profile.yml$',
|
||||
'*/\\.eslint*',
|
||||
'*/*\\.tsx?$',
|
||||
'./fonts*',
|
||||
'./gui/react*',
|
||||
'./dev.js$',
|
||||
'*/node_modules/*'
|
||||
'*/node_modules/*',
|
||||
'./widevine/*',
|
||||
'./videos/*',
|
||||
'./logs/*',
|
||||
].map(a => a.replace(/\*/g, '[^]*').replace(/\.\//g, escapeRegExp(__dirname) + '/').replace(/\//g, path.sep === '\\' ? '\\\\' : '/')).map(a => new RegExp(a, 'i'));
|
||||
|
||||
export { ignore };
|
||||
|
|
|
|||
Loading…
Reference in a new issue