Compare commits

...

812 commits

Author SHA1 Message Date
AnimeDL
64c927c761 Fix Docker Release + Documentation 2024-08-15 19:28:29 +00:00
AnimeDL
16e7fcb71e Fix Docker Release 2024-08-15 12:27:56 -07:00
AnimeDL
d61d7e471f
Update docker.yml
Change docker account
2024-08-15 11:40:39 -07:00
AnimeDL
64272b7689 [ADN] Fix DE only streams 2024-08-15 10:12:21 -07:00
AnimeDL
35777ecb58 [ADN] Forgot to fix login 2024-07-28 09:15:29 -07:00
AnimeDL
423ad7b2c9 [ADN] Fix refresh token request 2024-07-28 00:03:52 -07:00
AnimeDL
5cbead06bc Minor grammar fix 2024-07-18 09:44:12 -07:00
AnimeDL
1704bfb4ec [CR} Hotfix downloading
Pretty hacked together, but should work just fine. I'll work on a proper removal of the old API here soon + Documentation
2024-07-08 23:56:45 +00:00
AnimeDL
b488834c0f [CR} Hotfix downloading
Pretty hacked together, but should work just fine. I'll work on a proper removal of the old API here soon
2024-07-08 16:56:13 -07:00
AnimeDL
8d59666a6c [CR] Fix issue with too many streams when novids and noaudio
Fixes issue where you would have too many active streams if using no videos and no audio downloading
2024-06-29 07:49:13 -07:00
AnimeDL
c14a963024 [CR] Add device_id caching
Fixes issue with creating a lot of random device IDs by saving the created device id in the config + Documentation
2024-06-28 23:52:33 +00:00
AnimeDL
0026de73bf [CR] Add device_id caching
Fixes issue with creating a lot of random device IDs by saving the created device id in the config
2024-06-28 16:52:05 -07:00
AnimeDL
d3238d22ba [CR] Whoops
Forgot the other 3 login places + Documentation
2024-06-28 16:32:14 +00:00
AnimeDL
ab090a6858 [CR] Whoops
Forgot the other 3 login places
2024-06-28 09:31:42 -07:00
AnimeDL
64783a0529 [CR] Hotfix login
Also increment version + Documentation
2024-06-28 16:21:11 +00:00
AnimeDL
05d679e6ca [CR] Hotfix login
Also increment version
2024-06-28 09:20:35 -07:00
AnimeDL
cd9586ab13 [CR] Migrate to android token
Will require a fresh login.
Web wasn't working for password auth, for some reason.

Increment version + Documentation
2024-06-24 17:33:48 +00:00
AnimeDL
0a7cfcd917 [CR] Migrate to android token
Will require a fresh login.
Web wasn't working for password auth, for some reason.

Increment version
2024-06-24 10:33:08 -07:00
AnimeDL
ff978e2c88 Increment version + Documentation 2024-06-23 15:28:00 -07:00
AnimeDL
d81ca76594 Code cleanup
Thanks to TypeScript 5.5, this workaround is no longer needed

Increment version
2024-06-23 15:28:00 -07:00
AnimeDL
9457ee6d26 Minor types change 2024-06-22 16:00:15 -07:00
AnimeDL
9a94c33c8b [CR] Disable Non-Play streams
Disables the non-play streams since they don't work ATM. If the situation changes, I'll either re-enable them, or remove them in a future update
2024-06-22 15:58:44 -07:00
AnimeDL
e88352af3f [CR] Header changes 2024-06-22 15:57:39 -07:00
AnimeDL
870b775175 [CR] Clear stream ASAP
Clears the stream as soon as it's done being requested, rather than when the download is finished
2024-06-22 15:57:27 -07:00
AnimeDL
78f5016dd3 [CR] Migrate to web token
This will require a fresh login
2024-06-21 10:06:34 -07:00
AnimeDL
eaec9e62a7 [CR] Whoops 2024-06-21 10:03:18 -07:00
AnimeDL
f92e8dfacb spelling fix 2024-06-21 10:03:03 -07:00
AnimeDL
a6d740e9e9 Some code cleanup 2024-06-20 13:32:27 -07:00
AnimeDL
436a4ca4d1 Improve private key detection 2024-06-20 13:20:45 -07:00
AnimeDL
ea7df30aa7 Bump version + Documentation 2024-06-20 19:58:17 +00:00
AnimeDL
240ff3870b Bump version 2024-06-20 12:57:47 -07:00
AnimeDL
1340624742 Add zh-HK language + Documentation 2024-06-20 19:56:17 +00:00
AnimeDL
b0b7cffddf Add zh-HK language 2024-06-20 12:55:43 -07:00
AnimeDL
fe3f978082 Bump GUI dependencies 2024-06-20 12:53:55 -07:00
AnimeDL
0045abe08c update typescript 2024-06-20 10:57:03 -07:00
AnimeDL
ef975868a3 [CR] Rename crunchy play stream flag + Documentation 2024-06-20 17:39:15 +00:00
AnimeDL
345aa0f267 [CR] Rename crunchy play stream flag 2024-06-20 10:38:42 -07:00
AnimeDL
1f8ddb27a1 Delete weirdly unused code
inb4 somehow this breaks the whole project
2024-06-20 10:34:22 -07:00
AnimeDL
575ea260b6 [CR] Update mobile basic auth header 2024-06-20 10:01:03 -07:00
AnimeDL
9fdc1ac4db Bump node ver to 20 + Documentation 2024-06-20 04:58:14 +00:00
AnimeDL
cf921295f8 Bump node ver to 20 2024-06-19 21:54:47 -07:00
AnimeDL
ed22970346 Dependency bump 2024-06-19 21:53:12 -07:00
AnimeDL
5f034dc348 Add esbuild step to build process
Seems to run a bit faster, also allows for newer node versions with pkg, and seems to also decrease false-positives. This likely can be used to further improve our build process
2024-06-19 21:52:02 -07:00
AnimeDL
c294cdc280 [CR] Add crunchy play streams selector 2024-06-19 20:28:43 -07:00
AnimeDL
16dbc4f1eb Increment version + Documentation 2024-06-18 22:18:53 +00:00
AnimeDL
fba1b1cf22 Increment version 2024-06-18 15:18:17 -07:00
AnimeDL
141fdcf552 [HD] Added fallback to seriesTitle
Added fallback to seriesTitle when seasonTitle cannot be parsed (usually due to the seasonNumber being wrong)
2024-06-18 15:12:22 -07:00
AnimeDL
3aa844f90b Add .idea to gitignore 2024-06-17 08:58:17 -07:00
AnimeDL
be95c1f3bc [HD] Fix crashing issue in certain circumstances
Fixes a crashing issue when an API request fails with specific types of errors (such as ECONNRESET)
2024-05-30 09:53:19 -07:00
AnimeDL
6275d5abe3 Increment version + Documentation 2024-05-29 02:35:29 +00:00
AnimeDL
85c5d45829 Increment version 2024-05-28 19:34:58 -07:00
AnimeDL
9feb3d2f13 [CR] Hotfix DRM request
Addresses #701
2024-05-28 19:34:21 -07:00
AnimeDL
8b5cafff3d [CR] Hotfix stream deletion if slow download
Fixes issue where if someone's internet, or the server is so slow that the session expires, it would fail to delete the watch session
2024-05-23 17:54:54 -07:00
AnimeDL
9ea6258fec [CR] Implement deleting streams
Allows endless non-drm downloads + Documentation
2024-05-23 16:46:30 +00:00
AnimeDL
fc0736c686 [CR] Implement deleting streams
Allows endless non-drm downloads
2024-05-23 09:45:52 -07:00
AnimeDL
dbc2c7d52b [CR] Hotfix non-DRM 2024-05-22 06:49:21 -07:00
AnimeDL
38f849f1a8
Merge pull request #685 from Denoder/patch-1
[HiDive] Remove unreleased episodes from list
2024-05-21 07:28:06 -07:00
Tera
33afc263e7
[HiDive] Remove unreleased episodes from list 2024-05-21 10:56:07 +03:00
Tera
ab73931fb9
[HiDive] Remove unreleased episodes from list
Allow listing to be visible but now allow it in the episodes list if it's not available
2024-05-19 00:11:51 +03:00
Tera
87c7de7417
Update hidive.ts
Account for when the episode title is added, but still not released, when it's not available there's a 10 second clip.
2024-05-19 00:06:09 +03:00
AnimeDL
f1042ded9f [ADN] Fix some output problems
Fix season detection, and fix null instead of show title in some circumstances + Documentation
2024-05-17 23:53:56 +00:00
AnimeDL
8dd0725f9a [ADN] Fix some output problems
Fix season detection, and fix null instead of show title in some circumstances
2024-05-17 16:53:10 -07:00
AnimeDL
5730450e11
Merge pull request #692 from anidl/dependabot/npm_and_yarn/ts-proto-1.176.0
Bump ts-proto from 1.171.0 to 1.176.0
2024-05-16 21:44:06 -07:00
dependabot[bot]
8da4074b1b
Bump ts-proto from 1.171.0 to 1.176.0
Bumps [ts-proto](https://github.com/stephenh/ts-proto) from 1.171.0 to 1.176.0.
- [Release notes](https://github.com/stephenh/ts-proto/releases)
- [Changelog](https://github.com/stephenh/ts-proto/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stephenh/ts-proto/compare/v1.171.0...v1.176.0)

---
updated-dependencies:
- dependency-name: ts-proto
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-16 07:01:09 +00:00
Tera
130fa5ee11
[HiDive] Remove unreleased episodes from list
When doing a range download/single selection of an anime that's currently airing episodes that haven't released will still get downloaded at 0 bytes. This will skip them if they have a date range in them, rather than an appropriate title.
2024-05-10 00:27:35 +03:00
AnimeDL
b2488edc02 [HD] Fix movie downloading
Fixes #672
2024-04-23 14:35:56 -07:00
AnimeDL
773bbf034c Add SSL support to websocket 2024-04-23 11:10:27 -07:00
AnimeDL
d507135eaa [ADN] Add episode ID to listing 2024-04-21 08:52:12 -07:00
AnimeDL
24191c91d0 Merge pull request #666 from anidl/v5
Release of v5 with support for AnimeOnegai and AnimationDigitalNetwork + Documentation
2024-04-19 23:04:22 +00:00
AnimeDL
2fe20c35f8
Merge pull request #666 from anidl/v5
Release of v5 with support for AnimeOnegai and AnimationDigitalNetwork
2024-04-19 16:03:57 -07:00
AnimeDL
afefbbf9a5
Merge branch 'master' into v5 2024-04-19 15:50:20 -07:00
AnimeDL
b2a602e96e Increment version + Documentation 2024-04-19 22:45:30 +00:00
AnimeDL
e47deb7c57 Increment version 2024-04-19 15:45:21 -07:00
AnimeDL
5b8c497800 [CR] Fix Non-DRM downloading
Thanks bytedream for your reverse engineering
2024-04-19 15:44:40 -07:00
AnimeDL
fb58d306bc [CR] Fix Non-DRM downloading
Thanks bytedream for your reverse engineering
2024-04-19 15:42:59 -07:00
AnimeDL
282fc1ac1a fetch module, give more detailed errors 2024-04-19 15:40:36 -07:00
AnimeDL
c74e6fcb18 [AO] Fix downloading 2024-04-18 13:36:19 -07:00
AnimeDL
39fec7c35c Request Module Fixes 2024-04-18 13:33:34 -07:00
AnimeDL
591fac8b08 [AO] Fix language detection with spaces 2024-04-18 06:25:32 -07:00
AnimeDL
f1982d1cd5 [AO] Fix audio res output 2024-04-17 12:46:35 -07:00
AnimeDL
ba08c7b4e7 [AO] [ADN] Improve API locale handling 2024-04-17 11:59:47 -07:00
AnimeDL
78630bb5f7 [AO] Handle failed download more gracefully 2024-04-17 11:43:53 -07:00
AnimeDL
e3a64abfd8 byterange parsing: Add warning if playlist fails to parse 2024-04-17 10:51:57 -07:00
AnimeDL
0403ee0690 [AO] Remove debug file write 2024-04-17 10:51:34 -07:00
AnimeDL
bc1f69b3b1 Increment version number 2024-04-16 19:54:16 -07:00
AnimeDL
78a16410cc [AO] Fix downloading of movies 2024-04-16 19:42:40 -07:00
AnimeDL
8103bc09d2 [AO] Add additional languages
Also adds warning about unknown languages, and fixes unknown language identifier
2024-04-16 18:34:08 -07:00
AnimeDL
4d134a613d Add unknown option suggestion and warning 2024-04-16 08:35:20 -07:00
AnimeDL
0927e8287c change all console logs to log4js 2024-04-16 08:07:54 -07:00
AnimeDL
52aaef0515 [GUI] Add script to start dev server 2024-04-16 08:07:47 -07:00
AnimeDL
e5c62a9ed4 Add new services to github template 2024-04-15 17:58:17 -07:00
AnimeDL
e7dfc28513 [ADN] Add Chapter Support 2024-04-15 11:02:16 -07:00
AnimeDL
4b8e683bba Decrease search unbounce to 500ms 2024-04-14 16:07:25 -07:00
AnimeDL
3957977a2c eslint fixes 2024-04-14 15:41:55 -07:00
AnidlSupport
56be50c43a Fix select all button for seasons 2024-04-14 14:19:29 -07:00
AnidlSupport
de0b33c7c6 Fix WS version spam 2024-04-14 14:19:07 -07:00
AnidlSupport
6093c202d2 Add start script to gui 2024-04-14 14:18:19 -07:00
AnimeDL
da2a8ffbff [GUI] Fix issue with dub selection and noaudio flag 2024-04-14 11:13:45 -07:00
AnimeDL
1fa4d5bbbb options fixes 2024-04-14 10:00:56 -07:00
AnimeDL
3486ec67a7 [AO] Fix bug with --novids preventing audio download 2024-04-14 09:37:44 -07:00
AnimeDL
0883776580 json2ass: Add unknown style warning 2024-04-13 18:08:18 -07:00
AnimeDL
6629a2ac87 vtt2ass: Add support for right/left styles 2024-04-13 17:55:02 -07:00
AnimeDL
3d3a94c991 vtt2ass: Add support for text-decoration
Adds support for underline text-decoration subtitles, fixes #658
2024-04-13 17:21:57 -07:00
AnimeDL
6c71f0b808 Update .gitignore 2024-04-13 17:20:46 -07:00
AnimeDL
2efc3683b2 [ADN] Improve season number detection logic 2024-04-13 12:55:15 -07:00
AnimeDL
b453d1927a [ADN] Improve season detection 2024-04-13 12:25:00 -07:00
AnimeDL
6aa37c3426 [AO] Improve season detection 2024-04-13 12:21:51 -07:00
AnimeDL
b0790145cc [ADN] More subtitle fixes 2024-04-13 12:10:41 -07:00
AnimeDL
a472ab5dd3 [ADN] Fix subtitles for some players 2024-04-13 11:29:53 -07:00
AnimeDL
533818b812 [ADN] Subtitle style improvements 2024-04-13 10:58:23 -07:00
AnimeDL
257c3b6266 [ADN] Fix issue with --novids subtitle names 2024-04-13 10:49:26 -07:00
AnimeDL
e61c1717ae [ADN] Fix crash issue if no episodes were found 2024-04-13 10:35:53 -07:00
AnimeDL
82146f2c51 [AO] Minor refactor 2024-04-13 10:33:25 -07:00
AnimeDL
ac06400d2a [ADN] Fix issue with --numbers 2024-04-13 10:33:10 -07:00
AnimeDL
a1feba6e6f eslint fixes 2024-04-13 08:49:43 -07:00
AnimeDL
bc79628f4f --locale fixes 2024-04-13 08:20:22 -07:00
AnimeDL
f39645166e
Merge pull request #660 from anidl/adn-support
Add ADN Support
2024-04-13 08:09:12 -07:00
AnimeDL
d260d8f3e6
Merge branch 'v5' into adn-support 2024-04-13 08:06:51 -07:00
AnimeDL
cd0476b700
Merge pull request #661 from anidl/animeonegai
Add AnimeOnegai Support
2024-04-13 07:51:25 -07:00
AnimeDL
01abffa85f
Merge branch 'v5' into animeonegai 2024-04-13 07:50:20 -07:00
AnimeDL
71ae48000b
Merge pull request #662 from anidl/remove-funi
Remove Funimation Support
2024-04-13 07:40:01 -07:00
AnimeDL
b42a543ed5
Merge branch 'v5' into remove-funi 2024-04-13 07:39:37 -07:00
AnimeDL
3952ee4376
Merge pull request #663 from anidl/remove-old-hd-api
Remove old HD API
2024-04-13 07:38:20 -07:00
AnimeDL
7107cef7a4
Merge branch 'v5' into remove-old-hd-api 2024-04-13 07:36:32 -07:00
AnimeDL
c06b990bf5 [ADN] Fix locale downloading issue 2024-04-12 17:48:52 -07:00
AnimeDL
a29807895c [AO] Fix bug with subtitles being overwritten 2024-04-12 16:23:34 -07:00
AnimeDL
f5f32fa701 Add support for AnimationDigitalNetwork
This service brought me great pain
2024-04-12 16:19:23 -07:00
AnimeDL
e9c040ceb7 Update ignored token files 2024-04-11 13:17:29 -07:00
AnimeDL
d2117a1390 [AO] Add --locale support
Currently known supported API locales are "pt" and "es"
2024-04-11 13:11:33 -07:00
AnimeDL
67cdc42d64 [AO] Add access check 2024-04-11 10:35:15 -07:00
AnimeDL
4b5b3919f5 Make sure segments are only generated if none exist
Makes sure that it only generates byterange segments for the mpd if there are non already present.
2024-04-11 08:30:51 -07:00
AnimeDL
79fc6584d7 Make sure segments are only generated if none exist
Makes sure that it only generates byterange segments for the mpd if there are non already present.
2024-04-11 08:29:48 -07:00
AnimeDL
44381a04be [CR] Crunchy Hotfix + Documentation 2024-04-11 15:10:19 +00:00
AnimeDL
9972a48366 [CR] Crunchy Hotfix 2024-04-11 08:09:48 -07:00
AnimeDL
4c4436814b Initial commit to add AnimeOnegai 2024-04-10 22:12:57 -07:00
AnimeDL
b1bae92308 Allow for byterange mpd/dash downloads
Prep for new service
2024-04-10 22:03:17 -07:00
AnimeDL
84ebabffc8 [CR] Fix temp file naming 2024-04-09 15:59:30 -07:00
AnimeDL
6b913d6e85 Set version for release + Documentation 2024-04-09 22:19:14 +00:00
AnimeDL
5226b963ee Set version for release 2024-04-09 15:18:27 -07:00
AnimeDL
3567edae4b Use temp file for decryption
Fixes issue with unicode and path length limits
2024-04-09 14:37:21 -07:00
AnimeDL
d6db234ad1 Fix Readme 2024-04-09 13:56:53 -07:00
AnimeDL
cee5207ac1 Increment version + Documentation 2024-04-09 20:23:39 +00:00
AnimeDL
00a4f7b9ee Increment version 2024-04-09 13:23:17 -07:00
AnimeDL
93ace68398 Improve readme 2024-04-09 13:22:49 -07:00
AnimeDL
66b219ea0a Update .gitignore 2024-04-09 13:09:02 -07:00
AnimeDL
0d065fdd6a [CR] Rewrite how requests are made
Should stop cloudflare errors
2024-04-09 13:08:15 -07:00
AnimeDL
68e4a344d8 Update default cli-defaults.yml 2024-04-08 14:14:59 -07:00
AnimeDL
a022855400 Add missing dev dep 2024-04-08 14:14:47 -07:00
AnimeDL
469fd1b4a4 [CR] Add Episode chapter if no start chapters 2024-04-08 12:32:04 -07:00
AnimeDL
413dea6564 Compress built code 2024-04-08 09:42:25 -07:00
AnimeDL
551b27280e Completely Redo GUI build code 2024-04-08 09:42:15 -07:00
AnimeDL
6e4e10930b Bump Deps & Remove UnusedDeps
Bumps dependencies, and removes unused dependencies
2024-04-07 22:25:56 -07:00
AnimeDL
16d2277d3e eslint and building fixes 2024-04-07 19:48:29 -07:00
AnimeDL
0a3b638c55 Start of Removing Funimation 2024-04-06 21:20:34 -07:00
AnimeDL
e9e14aef2f Remove old HD API 2024-04-06 21:05:04 -07:00
AnimeDL
23f185c877 eslint fixes 2024-04-06 09:10:48 -07:00
AnimeDL
036440b0e5 Fix MPD part names 2024-04-06 08:41:42 -07:00
AnimeDL
ea51c8a7ea Fix linting 2024-04-05 21:57:21 -07:00
AnimeDL
645cdc2068 [CR] Fix Non-DRM stream not always working 2024-04-05 19:39:56 -07:00
AnimeDL
dce10bc7fc Increment version + Documentation 2024-04-04 17:25:19 +00:00
AnimeDL
5846a13c10 Increment version 2024-04-04 10:24:46 -07:00
AnimeDL
2b65f3067e [CR] Fix issue with multi-dub downloads with non-DRM
Fix issue with non-DRM multi-dub downloads, where it would crash trying to delete a file that wasn't downloaded
2024-04-04 10:23:35 -07:00
AnimeDL
7d5e8fa461 ass inline font regex optimization 2024-04-04 09:27:10 -07:00
AnimeDL
42da4ebd30 Increment version + Documentation 2024-04-04 01:08:13 +00:00
AnimeDL
d83b2b55c8 Increment version 2024-04-03 18:08:28 -07:00
AnimeDL
1e6406774d Improve description for fontSize flag 2024-04-03 18:08:12 -07:00
AnimeDL
adc1147ca4 Merge pull request #626 from IONI0/vtt2ass-changes-branch
Fixes/enchancements for vtt2ass with new Hidive Q styles + Documentation
2024-04-04 00:59:48 +00:00
AnimeDL
866bd26067
Merge pull request #626 from IONI0/vtt2ass-changes-branch
Fixes/enchancements for vtt2ass with new Hidive Q styles
2024-04-03 17:59:19 -07:00
IONI0
2885913205 Fix Eslint errors, logic error, and add combineLines argument 2024-04-04 09:42:41 +11:00
AnimeDL
01888286da Match ass inline fonts to include in merger
This fixes an issue where inline fonts in the ass file wouldn't be included in the merged file
2024-04-03 13:41:23 -07:00
AnimeDL
3ca4000ed1 [CR] Fix new stream dub selection logic 2024-04-03 13:38:18 -07:00
AnimeDL
4c5bdb4226 Increment version + Documentation 2024-04-03 15:41:49 +00:00
AnimeDL
d5272e7108 Increment version 2024-04-03 08:42:14 -07:00
AnimeDL
90100a077d [CR] Add Switch Stream 2024-04-03 08:41:49 -07:00
IONI0
6bf7e9721f Fixes/enchancements for vtt2ass with new Hidive Q styles 2024-04-02 16:45:29 +11:00
AnimeDL
80c7f5ba77 Bump version + Documentation 2024-04-01 21:02:13 +00:00
AnimeDL
9aefa9c598 Bump version 2024-04-01 13:40:23 -07:00
AnimeDL
652e0feeca
Merge pull request #623 from DAREK0N/master
GUI Hardsubs
2024-04-01 13:28:03 -07:00
DAREKON
a852cb37c5 GUI Fixed spelling 2024-04-01 22:22:34 +02:00
DAREKON
0f3cf5fbb8 GUI Hardsubs 2024-04-01 20:59:53 +02:00
DAREKON
c990e744d3 GUI Hardsubs 2024-04-01 20:30:27 +02:00
AnimeDL
dc87b23c0d [CR] Replace --search-locale flag with --locale
This allows for episode names and search results in the selected locale. Addresses #612 + Documentation
2024-03-30 00:56:47 +00:00
AnimeDL
b54d54d374 [CR] Replace --search-locale flag with --locale
This allows for episode names and search results in the selected locale. Addresses #612
2024-03-29 17:56:53 -07:00
AnimeDL
25cf19f65d vtt2ass: Fix reverse ordering of subtitles 2024-03-29 17:31:12 -07:00
AnimeDL
c82197a585 [HD} Add missing language 2024-03-28 20:23:46 -07:00
AnimeDL
8ccb1301d0 vtt2ass: Fix parsing error 2024-03-28 20:23:38 -07:00
AnimeDL
a777b26517 vtt2ass: Reverse all subtitles
This should (hopefully) make it line up with how subtitles are displayed on Hidive.
2024-03-25 22:59:49 -07:00
AnimeDL
cd7db09804 [HD] Get keys earlier in download process 2024-03-25 16:32:51 -07:00
AnimeDL
d298521202 [HD] Fix downloading of movies 2024-03-25 14:19:21 -07:00
AnimeDL
4726bd0416 vtt2ass: Reverse line order for certain subtitles
Reverse line order for subtitles that happen within a deviation of 3 of different types than those we merge. Should address point 2 of #608
2024-03-24 20:09:19 -07:00
AnimeDL
f1bb6c8a64 [CR] Allow for decimal places in episode number
Addresses #615
2024-03-24 19:39:43 -07:00
AnimeDL
3c8f9f2d46 [HD] Fail gracefully if audio doesn't exist
Addresses #617
2024-03-24 17:38:53 -07:00
AnimeDL
175ffbc71f vtt2ass: Improve Merging Logic for subtitles
Improves merging logic for subtitles by adding a check to see if the time is within a deviation of 2. This code is cursed. Addresses #608
2024-03-24 15:15:45 -07:00
AnimeDL
b96fca8ed8 [HD] Make new API default + Documentation 2024-03-24 21:24:06 +00:00
AnimeDL
eb2e9c2425 [HD] Make new API default 2024-03-24 14:24:08 -07:00
AnimeDL
ed02017bca vtt2ass: Make sure cc's with pos data aren't merged 2024-03-23 21:41:03 -07:00
AnimeDL
69336c5c6e [CR] Fix issue with API version detection in GUI 2024-03-23 21:03:49 -07:00
AnimeDL
74da730dc4 [CR] Fix falsey comparison for chapters
Fix chapter being ignored if chapter start time was 0 (thanks javascript falsey comparison)
2024-03-23 17:52:40 -07:00
AnimeDL
250c8925a4 [CR] Change the default API to web + Documentation 2024-03-23 20:12:25 +00:00
AnimeDL
978159c80e [CR] Change the default API to web 2024-03-23 13:12:27 -07:00
AnimeDL
d2a69fdc4d [CR] Chapters flag enabled by default 2024-03-23 13:10:46 -07:00
AnimeDL
aae69fa2a1 [CR] Fix subtitles being named undefined
Fixes subtitles being named undefined when using novids and noaudio together
2024-03-23 09:47:26 -07:00
AnimeDL
421cd5fcee Update TODO.md 2024-03-22 17:31:56 -07:00
AnimeDL
e98c770dab
Merge pull request #604 from Denoder/Episode/Special-Sort
Sort episodes to have specials at the end
2024-03-21 15:05:11 -07:00
Tera
6009bdaaf9
Sort episodes to have specials at the end 2024-03-21 23:38:46 +02:00
AnimeDL
0e674fcd67 Improve readability of index 2024-03-21 13:53:31 -07:00
AnimeDL
34c8215bd0 Remove PRs from auto-documentation workflow 2024-03-21 11:34:16 -07:00
Tera
f7f8806b14
sort episodes to have specials at the end 2024-03-21 20:14:14 +02:00
AnimeDL
3318fbba23 Change release trigger from created to published
This should allow draft releases to also have releases created for it.
2024-03-21 11:02:34 -07:00
AnimeDL
48224c7b53 Remove pull requests from docker build 2024-03-21 11:02:03 -07:00
AnimeDL
edd80a1569
Merge pull request #603 from Denoder/Feature]-Multi-Dub-w/-season-constraints
[Feature] Multi dub w/ season constraints
2024-03-21 10:53:55 -07:00
Tera
8cd863c885
Update crunchy.ts 2024-03-21 19:37:22 +02:00
Tera
cd9a83d3fc
make data param optional 2024-03-21 18:29:08 +02:00
Tera
4eea557c44
Update crunchy.ts 2024-03-21 08:06:08 +02:00
Tera
30771374e5
Update crunchy.ts 2024-03-21 07:47:54 +02:00
Tera
201a772ecf
remove bloat 2024-03-21 07:46:03 +02:00
AnimeDL
818d60e196 Verify file is file in CDM folder
Make sure when reading cdm folder that a file is a file, not a folder.
2024-03-20 21:25:33 -07:00
Tera
6a4566be17
Update crunchyTypes.d.ts 2024-03-21 06:24:53 +02:00
Tera
73ddab8b2e
Update crunchy.ts 2024-03-21 06:23:01 +02:00
AnimeDL
886a0bd85b Add seriesTitle to documentation + Documentation 2024-03-20 23:26:14 +00:00
AnimeDL
15f2651b20 Add seriesTitle to documentation 2024-03-20 16:26:14 -07:00
AnimeDL
19f0a3cc7d Documentation fixes + Documentation 2024-03-20 03:20:23 +00:00
AnimeDL
fbc1e43260 Documentation fixes 2024-03-19 20:19:51 -07:00
AnimeDL
8dea7cc400 Hidive subtitle scaling fixes 2024-03-19 17:45:54 -07:00
AnimeDL
59d086006b Give CC subtitles correct extension 2024-03-19 17:38:23 -07:00
AnimeDL
97b9778801 [CR] Make GUI only search series 2024-03-19 17:21:24 -07:00
AnimeDL
0b22b4ec36 Improve CDM detection
Automatically checks all files in the directory for a valid CDM
2024-03-19 16:27:00 -07:00
AnimeDL
1852ce1282 [CR] Fix vtt CC fontSize not working 2024-03-19 11:22:15 -07:00
AnimeDL
97d64f6021 [CR] Respect originalFontSize in CCs 2024-03-19 10:21:25 -07:00
AnimeDL
a5df6bee2f [CR] Convert CC's from vtt to ass 2024-03-19 10:07:58 -07:00
AnimeDL
83d410378a Improve vtt2ass compatibility 2024-03-19 10:07:18 -07:00
AnimeDL
4693b60af4 rename vtt function to vtt2ass 2024-03-19 08:05:07 -07:00
AnimeDL
b830a73e04 [CR] Fix logic for separate video/audio downloading 2024-03-19 07:35:12 -07:00
AnimeDL
0c7c047a7b Increment Version + Documentation 2024-03-19 00:49:52 +00:00
AnimeDL
510847d3d5 Increment Version 2024-03-18 17:49:44 -07:00
AnimeDL
789ed2c5b0 Add token flag
Allows for login using refresh token in crunchy. Adds #597 + Documentation
2024-03-19 00:44:51 +00:00
AnimeDL
b497fb40df Add token flag
Allows for login using refresh token in crunchy. Adds #597
2024-03-18 17:44:49 -07:00
AnimeDL
6280d13d36 [CR] Allow skipping video/audio download independently + Documentation 2024-03-18 23:16:40 +00:00
AnimeDL
63f0b496f1 [CR] Allow skipping video/audio download independently 2024-03-18 16:16:33 -07:00
AnimeDL
3e071b1386 Set default kstream to 5 2024-03-18 16:04:41 -07:00
AnimeDL
ef567903ad [HD] Fix issue with srz and multiple seasons 2024-03-18 08:56:26 -07:00
AnimeDL
9e9fb6eb76 [HD] Fix bin cfg sometimes not being loaded 2024-03-17 23:19:26 -07:00
AnimeDL
1e09625b85 [HD] [GUI] Fix issue where auth wouldn't work at first
Fixes issue where auth wouldn't work at first with GUI because the API version wasn't checked.
2024-03-17 22:47:56 -07:00
AnimeDL
50c6ca66a3 Update pnpm-lock.yaml 2024-03-17 20:23:04 -07:00
AnimeDL
caab478956 Revert "Update pnpm-lock.yaml"
This reverts commit 1500daf207.
2024-03-17 20:21:34 -07:00
AnimeDL
e5bbc09f25
Merge pull request #595 from anidl/mpd-support
4.5.1
2024-03-17 19:08:39 -07:00
AnimeDL
9590aa56d1 + Documentation 2024-03-18 02:04:51 +00:00
AnimeDL
1500daf207 Update pnpm-lock.yaml 2024-03-17 19:04:39 -07:00
AnimeDL
f8307620ce
Merge branch 'master' into mpd-support 2024-03-17 18:07:28 -07:00
AnimeDL
48c9528116
Merge pull request #594 from anidl/new-hidive-api
New Hidive API
2024-03-17 16:39:29 -07:00
AnimeDL
546a5327e8 [HD] Implement -e downloading in new API
Allows for single episode downloads by episode ID
2024-03-17 15:22:04 -07:00
AnimeDL
becaed79d1 Migrate ::cue code to vtt2ass
This should allow for generic styles in vtt files to be parsed and converted to an ASS file
2024-03-17 13:49:19 -07:00
AnimeDL
49519f1e17 [HD] fontSize for q1 as well 2024-03-17 11:59:58 -07:00
AnimeDL
7d62224d11 [HD] Update subtitle messages for old api 2024-03-17 11:38:23 -07:00
AnimeDL
9645f6255b Remove unneeded console log 2024-03-17 11:24:52 -07:00
AnimeDL
5b134978c7 Add to the ignored items for compilation 2024-03-17 11:10:05 -07:00
AnimeDL
a61d7d315d Make widevine folder during compilation 2024-03-17 11:00:50 -07:00
AnimeDL
12780eec26 Only apply fontSize to normal subtitles 2024-03-17 10:54:54 -07:00
AnimeDL
c8b39301cb [CR] Fix issue with finding all episodes
Fixes issue where under certain circumstances some specials wouldn't be found.
2024-03-17 10:48:32 -07:00
AnimeDL
48d8a276d7 [HD] Fix fontsize for vtt2ass for new API 2024-03-16 22:26:56 -07:00
AnimeDL
34fa052bc2 Modify vtt2ass to work with new hidive API
Modifies vtt2ass to work with the new hidive API
2024-03-16 21:20:04 -07:00
AnimeDL
c7addc1c1a Formatting fixes 2024-03-16 19:58:47 -07:00
AnimeDL
824154594d [HD] Add -s support to new hidive API 2024-03-16 19:50:33 -07:00
AnimeDL
211a593796 [HD] Move -s to --srz, and add warnings
Add warnings for commands that haven't yet been added to hidive, and move -s to --srz since it selects a series, and not a season.
2024-03-16 17:58:48 -07:00
AnimeDL
f3fd33d241 Add warning for decryption 2024-03-16 17:45:54 -07:00
AnimeDL
1c39e349ad [HD] Initial support for new Hidive API
The new API can be accessed with `--hdapi new`
2024-03-16 17:44:11 -07:00
AnimeDL
5e95f600d9 Fix required dependency mistakenly in dev
Should allow the package to be built without installing devDependencies
2024-03-16 15:52:36 -07:00
AnimeDL
19521be3ff Add missing proto compilation script 2024-03-16 12:21:33 -07:00
AnimeDL
3c41414f4a Rename WV module 2024-03-16 11:14:21 -07:00
AnimeDL
f06dc21d56 Fix docker builds 2024-03-15 10:08:25 -07:00
AnimeDL
172e6ac6ac [CR] Get all crunchy streams for android (fallback) 2024-03-14 20:30:37 -07:00
AnimeDL
d5c72c5c86 [CR] Hotfix: Fixes non DRM downloads 2024-03-14 19:21:04 -07:00
AnimeDL
0a8c29189e [CR] Hotfix: Fixes non DRM downloads + Documentation 2024-03-15 02:18:33 +00:00
AnimeDL
31096899e1 [CR] Hotfix: Fixes non DRM downloads 2024-03-14 19:18:18 -07:00
AnimeDL
4742ea8d55 [CR] Hotfix: Fixes non DRM downloads 2024-03-14 19:14:50 -07:00
AnimeDL
5143f2db4a Fix subtitle downloading for hidive 2024-03-08 08:58:30 -08:00
AnimeDL
6575e2343d Fix subtitle downloading for hidive 2024-03-08 08:56:27 -08:00
AnidlSupport
15b616c880
Merge pull request #582 from DAREK0N/mpd-support
Updated GUI
2024-02-20 21:18:23 +01:00
DAREKON
fcba060c90 [GUI] Fixed Hardsubs 2024-02-20 21:17:56 +01:00
DAREKON
275b559f06 delete unessesary 2024-02-16 22:31:59 +01:00
DAREKON
1f4d73aa0e Updated GUI 2024-02-16 22:29:20 +01:00
DAREKON
8d312025c1 Merge branch 'mpd-support' of https://github.com/DAREK0N/multi-downloader-nx into mpd-support 2024-02-16 17:58:23 +01:00
DAREKON
f0cf404c1d GUI update baseline 2024-02-16 17:57:24 +01:00
DAREKON
fcb192f7fe initial 2024-02-15 21:06:15 +01:00
AnimeDL
5417db41fd [CR] Improve version detection
Improves the version selection and detection to work for more than just --srz. This should help prevent API errors on non --srz requests
2024-02-10 08:55:22 -08:00
AnimeDL
7cab2cbab3 [CR] Fix old chapter API
Could fail to mux into the file if time didn't contain milliseconds
2024-02-08 18:36:15 -08:00
AnimeDL
3582f1829f [CR] Chapter Fix
Chapters could fail if for whatever reason the start and end times weren't defined.
2024-02-07 15:59:22 -08:00
AnimeDL
d2b4adf09b [CR] Improve Captions support
Renames what was previously called CC to `Signs`.

Also prevents a possible collision where not all subs would be downloaded
2024-02-06 19:46:14 -08:00
AnimeDL
66e1ee5702 [CR] Add CC Subtitles
CC Subtitles looks to be possible in the API now (it's not just always empty), so I've written some code to handle CC subtitles properly and download them

Fixes #580
2024-02-06 19:10:11 -08:00
AnimeDL
205232f6d9 Switch to linuxstatic for linux build
This should allow the linux build to run on a much larger variety of linux distros.
2024-02-06 17:44:36 -08:00
AnimeDL
03e3c96f3c Rework Building
Should now be more modular, at the cost of human-readable names. It can now be used to build android versions, alpine versions, as well as arm for each supported platform
2024-02-06 08:41:54 -08:00
AnimeDL
103b17f449 Simplify API switching code
Should also fix API switching on GUI
2024-02-05 10:12:47 -08:00
AnimeDL
27e02a8548 Improve readme
Adds instruction for DRM decryption, as well as some examples on how to use the CLI. It also splits up the dependencies as they are needed.
2024-02-04 16:13:22 -08:00
AnimeDL
b146ee71ae Linting fixes 2024-01-30 15:10:43 -08:00
AnimeDL
3502141c62 [CR] Improve chapter support
If new chapter request fails, use old request.

Also adds new option to enable chapter downloading, `--chapters`
2024-01-30 10:06:40 -08:00
AnimeDL
9bc62ba91e Increment version + Documentation 2024-01-30 06:51:22 +00:00
AnimeDL
1e2b396ed5 Increment version 2024-01-29 22:50:55 -08:00
AnimeDL
2e77593e14 [CR] Fixes subtitles when directory doesn't exist
Now creates the directory before making the files, similar to how videos work.
2024-01-29 22:50:05 -08:00
AnimeDL
4d9b1c7480 Formatting fix 2024-01-29 22:49:11 -08:00
AnimeDL
1e0e414ae6 Ignore guistate.json 2024-01-29 22:49:10 -08:00
AnimeDL
b491ba1917 Fix command line length issue
Fixes long standing issue on windows where if the command line length was over 8191 characters, this was achieved by switching node.exec to use powershell.exe instead of cmd.exe on Windows
2024-01-29 22:49:10 -08:00
AnimeDL
02620ec5b5 [CR] Initial commit to support chapters
TODO:
1) Add flag for chapters
2) Add ffmpeg merging for chapters
3) Add fallback to old CR API
2024-01-29 22:47:56 -08:00
AnimeDL
7be22ec132 [CR] Fixes subtitles when directory doesn't exist
Now creates the directory before making the files, similar to how videos work.
2024-01-29 22:45:36 -08:00
AnimeDL
ec8d8b3ab8 Formatting fix 2024-01-29 21:31:28 -08:00
AnimeDL
388204cc43 Ignore guistate.json 2024-01-29 21:31:23 -08:00
AnimeDL
169d48aa47 make --nocleanup work for encrypted files 2024-01-29 20:29:59 -08:00
AnimeDL
1b5dfa0f3e Fix command line length issue
Fixes long standing issue on windows where if the command line length was over 8191 characters, this was achieved by switching node.exec to use powershell.exe instead of cmd.exe on Windows
2024-01-29 19:30:43 -08:00
AnimeDL
615cef700a Fix issue with GUI decrypting 2024-01-28 09:46:15 -08:00
AnimeDL
f204b068ea Only show DRM streams if they can be decrypted 2024-01-26 10:38:25 -08:00
AnimeDL
d07603de8b Fix edge case with vtt2ass 2024-01-11 14:33:55 -08:00
AnimeDL
fe997ecb32 Fix edge case with vtt2ass 2024-01-11 11:38:33 -08:00
AnimeDL
1996e44ef5 Add Telugu (India)
Fixes #567 + Documentation
2024-01-10 19:19:38 +00:00
AnimeDL
1c6e4640c5 Add Telugu (India)
Fixes #567
2024-01-10 11:19:09 -08:00
AnimeDL
0e91c59197 Add Telugu (India)
Fixes #567
2024-01-10 11:18:40 -08:00
AnimeDL
acc91a0eed Fixed fallback request 2024-01-09 12:33:04 -08:00
AnimeDL
a1a9729134 Add new CR languages + Documentation 2024-01-09 16:47:30 +00:00
AnimeDL
70f8e36ffc Add new CR languages 2024-01-09 08:47:02 -08:00
AnimeDL
5ab4d3e0c4 Add new CR languages 2024-01-09 08:46:49 -08:00
AnimeDL
7998395c56 Increment version + Documentation 2024-01-09 16:40:17 +00:00
AnimeDL
60da30c92b Increment version 2024-01-09 08:39:53 -08:00
AnimeDL
fb0559abd3 Hotfix for CR
Fixes authentication in CR
Fixes #573
2024-01-09 08:38:01 -08:00
AnimeDL
3174a4db5e switch to yao-pkg 2024-01-09 08:38:01 -08:00
AnimeDL
e1c5ad2f2a Hotfix for CR
Fixes authentication in CR
Fixes #573
2024-01-09 08:34:34 -08:00
AnimeDL
5c751eb551 switch to yao-pkg 2024-01-07 22:07:24 -08:00
AnimeDL
350337fdda Fix episode selection on web API 2023-12-30 15:40:54 -08:00
AnimeDL
c3335db366 remove util 2023-12-29 21:17:55 -08:00
AnimeDL
f9adcdea7f Fix bug with kstream 3 2023-12-29 21:14:27 -08:00
AnimeDL
ca01c04961 API switching 2023-12-29 21:14:12 -08:00
AnimeDL
942f673934 Fix WV relative pathing 2023-12-25 11:11:55 -08:00
AnidlSupport
ee38658d0d Some refactoring 2023-12-25 10:46:13 -08:00
AnimeDL
7524f4c08f Remove uneeded console logs 2023-12-24 19:40:42 -08:00
AnimeDL
3b37703d7a Fix object downloading + Documentation 2023-12-24 19:37:35 -08:00
AnimeDL
64b2bf0b08 Fix object downloading 2023-12-24 19:37:35 -08:00
AnimeDL
28518bb461 Initial commit for WV Decryption 2023-12-24 19:29:01 -08:00
AnimeDL
d1d9840629 Fix muxing for MPD 2023-12-24 12:09:52 -08:00
AnimeDL
63e1d1dda8 Fix object downloading + Documentation 2023-12-24 19:54:30 +00:00
AnimeDL
788afbc6f5 Fix object downloading 2023-12-24 11:53:22 -08:00
AnidlSupport
7121d254f6 Linting Fixes 2023-12-24 11:20:52 -08:00
AnidlSupport
abc5747614 MPD Refactoring 2023-12-24 11:19:01 -08:00
AnimeDL
a27c0209c1 Initial support for mpd 2023-12-23 20:28:37 -08:00
AnimeDL
5b25b89622 Version bump + Documentation 2023-12-16 20:07:53 +00:00
AnimeDL
dd20ff0a7d Version bump 2023-12-16 12:07:26 -08:00
AnimeDL
ef2de8e372 [CR] Fix fallback for playlist failure 2023-12-16 12:07:11 -08:00
AnimeDL
49a1932930 Change the default kstream to 2 + Documentation 2023-12-15 17:48:53 +00:00
AnimeDL
834e157d70 Change the default kstream to 2 2023-12-15 09:48:23 -08:00
AnimeDL
5f082f10ee Fix Bucket Selection 2023-12-15 09:41:53 -08:00
AnimeDL
5ec231b951 Merge branch 'master' of github.com:anidl/multi-downloader-nx 2023-12-15 09:24:29 -08:00
AnimeDL
7e8bb06a8b Replace ffprobe static with ffprobe bin 2023-12-15 09:24:07 -08:00
AnimeDL
dfdd8c634e Hotfix for CR
This does not include DRM decryption, that will come later, however, this does work for however long CR allows it to. + Documentation
2023-12-15 17:21:55 +00:00
AnimeDL
df7dd06235 Hotfix for CR
This does not include DRM decryption, that will come later, however, this does work for however long CR allows it to.
2023-12-15 09:21:05 -08:00
AnimeDL
31867e216f GUI: hidive subtitles dropdown all/none missing + Documentation 2023-07-31 23:41:55 +00:00
AnimeDL
7de7f22775 GUI: hidive subtitles dropdown all/none missing 2023-07-31 16:41:08 -07:00
AnimeDL
b7643c15d2 Add new Hidive languages + Documentation 2023-07-30 23:10:51 +00:00
AnimeDL
24f330838e Add new Hidive languages 2023-07-30 16:09:56 -07:00
AnimeDL
8b6c488396 Typo + Documentation 2023-07-17 02:36:03 +00:00
AnimeDL
b8362a618c Typo 2023-07-16 19:35:22 -07:00
AnimeDL
c254b0d82c Add warning for pontially problematic subs + Documentation 2023-07-17 02:34:09 +00:00
AnimeDL
e02fd59f43 Add warning for pontially problematic subs 2023-07-16 19:32:18 -07:00
AnimeDL
485433cd74 Initial attempt to add --syncTiming flag
Add --syncTiming flag to attempt to detect timing differences in multi-dub downloads and sync them properly. Currently disabled by default since it's experimental and likely buggy. Potentially Resolved #471
2023-07-16 19:24:41 -07:00
AnimeDL
54d3a14bea Improve --keepAllVideos
Fixes issue where in not all cases keepAllVideos would work + Documentation
2023-07-16 20:26:23 +00:00
AnimeDL
5962c556ec Improve --keepAllVideos
Fixes issue where in not all cases keepAllVideos would work
2023-07-16 13:24:11 -07:00
AnimeDL
02b4385db3 Add --new to hidive
Adds --new to hidive to look at the ID's of recently added stuff
2023-07-16 13:23:31 -07:00
AnimeDL
4824367f39 Fixed duplicate episode entries in archive + Documentation 2023-07-16 15:59:29 +00:00
AnimeDL
a87c345fdc Fixed duplicate episode entries in archive 2023-07-16 08:57:54 -07:00
AnimeDL
dbf1640c1b Fix thumbnail when downloading in GUI
For Hidive
2023-07-15 21:28:29 -07:00
AnimeDL
e9e368f197 Add version info to help menu in GUI + Documentation 2023-07-16 02:13:06 +00:00
AnimeDL
b6187c48a8 Add version info to help menu in GUI 2023-07-15 19:12:24 -07:00
AnimeDL
7872bc343d Change no-unused-vars to warn + Documentation 2023-07-16 00:57:55 +00:00
AnimeDL
cf83eb18ba Change no-unused-vars to warn 2023-07-15 17:57:08 -07:00
AnimeDL
ac77e4b3e0 Fix crunchy archive
Should actually fix #501 this time
2023-07-15 17:56:31 -07:00
AnimeDL
b64a568352 Fix language order + Documentation 2023-07-15 23:12:56 +00:00
AnimeDL
2b969971e6 Fix language order 2023-07-15 16:11:58 -07:00
AnimeDL
8ddb23fc6b ESLint fixes 2023-07-15 15:43:36 -07:00
AnimeDL
3f5bc07de1 Add icons to service select + Documentation 2023-07-15 22:19:59 +00:00
AnimeDL
814dcb778d Add icons to service select 2023-07-15 15:19:17 -07:00
AnimeDL
ea5ba76eba Update dependencies 2023-07-15 15:09:06 -07:00
AnimeDL
11c32ba4bc eslint fixes 2023-07-15 11:28:43 -07:00
AnimeDL
f08c23a97b Add GUI States
This adds states for the GUI that get saved to a file, notably this means that queue's of services are no longer lost when changing services, or closing and reopening the program.

Also changed was a few spelling fixes + Documentation
2023-07-15 18:18:48 +00:00
AnimeDL
c3dab33f6b Add GUI States
This adds states for the GUI that get saved to a file, notably this means that queue's of services are no longer lost when changing services, or closing and reopening the program.

Also changed was a few spelling fixes
2023-07-15 11:16:55 -07:00
AnimeDL
f7ddaf1176 Add service name to handler 2023-07-14 19:40:40 -07:00
AnimeDL
b4d0c65cd2 Remove repetitive error log 2023-07-14 18:57:39 -07:00
AnimeDL
bd2574e05a Update pnpm/node version in workflows 2023-07-14 17:43:57 -07:00
AnimeDL
f73172b056 Update pnpm version in workflows 2023-07-14 17:39:35 -07:00
AnimeDL
ae2c9eedb3 Increment version + Documentation 2023-07-15 00:26:54 +00:00
AnimeDL
47078b2f93 Increment version 2023-07-14 17:25:01 -07:00
AnimeDL
f3144475f4 Remove 'Unknown' language entry 2023-07-14 17:24:30 -07:00
AnimeDL
f7f725392e Fix GUI service indication on load 2023-07-14 17:24:14 -07:00
AnimeDL
88d5c11032
Merge pull request #499 from anidl/4.2.0
4.2.0 - Bunch of fixes and additions
2023-07-13 15:27:38 -07:00
AnimeDL
3f3d9edaee + Documentation 2023-07-13 22:15:24 +00:00
AnimeDL
a25434b059 Add version statement on GUI boot 2023-07-13 15:14:35 -07:00
AnimeDL
2b491372fc Fix issue where specials weren't archived properly
Should fix #501
2023-07-13 15:12:19 -07:00
AnimeDL
9d8665dfbe Add --keepAllVideos option
If set to true, it will keep all videos in the merge process, rather than discarding the extra videos.
2023-07-13 14:28:44 -07:00
AnimeDL
1816c2521d Fix multiple like languages breaking hidive
Fixes issue where multiple of the same language and marked as the same version (broadcast vs home video) would crash the merger in Hidive
2023-07-13 14:12:29 -07:00
AnimeDL
ea06ad576a + Documentation 2023-07-13 20:57:31 +00:00
AnimeDL
a356e98aa8 Fix regression in Funimation merging
Fixes audio not being in the final video
2023-07-13 13:56:41 -07:00
AnimeDL
95374bd8f1 + Documentation 2023-07-13 04:54:42 +00:00
AnimeDL
8aa2323697 More graceful handling of unknown languages 2023-07-12 21:35:40 -07:00
AnimeDL
c29fd0e5f5 Add support for 6 more languages
Chinese (Taiwan),
Catalan,
Polish,
Thai,
Tamil (India),
Malay (Malaysia)
2023-07-12 21:19:19 -07:00
AnimeDL
e25f469fb9 Mark as b1 2023-07-10 17:24:39 -07:00
AnimeDL
573fa99626 Remove unused else 2023-07-10 16:07:52 -07:00
AnimeDL
0fbd6ec5f4 Increment Version 2023-07-10 16:07:10 -07:00
AnimeDL
57a777c4a3 Fix waittime in hidive 2023-07-10 16:05:48 -07:00
AnimeDL
2149df6fe5 Don't output user country on token refresh
Also should prevent from some excess API calls
2023-07-10 16:04:41 -07:00
AnimeDL
2dc6b3968c Add --waittime flag
Will wait between downloads, set in milliseconds. Setting this high should help when downloading large numbers of items.
2023-07-10 16:00:55 -07:00
AnimeDL
e32d2fdefe Fix debug command 2023-07-10 15:58:45 -07:00
AnimeDL
3c840e46f0 Add --nosubs support to CR/HD
Fixes #457
2023-07-10 09:05:57 -07:00
AnimeDL
4a135092e9 Add --extid for crunchyroll
Allows downloading or viewing of old Crunchyroll ID's such as: SRZ.282651 VOL.26453 EPI.842919 FLM.268837

Fixes #351 #249
2023-07-10 09:05:23 -07:00
AnimeDL
bfe4ae6164
Merge pull request #493 from anidl/4.1.0
4.1.0 -- Numerous fixes and improvements
2023-07-09 14:23:17 -07:00
AnimeDL
32fb7aea68 + Documentation 2023-07-09 21:15:32 +00:00
AnimeDL
4022909f14 Prevent episode selection from contextmenu
Episode would be selected if you selected anything in the contextmenu from the Episode List screen.

Co-authored-by: AnidlSupport <AnidlSupport@users.noreply.github.com>
2023-07-09 14:07:30 -07:00
AnimeDL
b9222325d1 Add context menus to Episode List
Adds Image and Description context menus to the Episode List

Co-authored-by: AnidlSupport <AnidlSupport@users.noreply.github.com>
2023-07-09 13:51:07 -07:00
AnimeDL
517b0cdc1b Add header to show which service is selected 2023-07-09 11:52:29 -07:00
AnidlSupport
fd10455400
Changes as discussed with sww 2023-07-09 17:19:44 +00:00
AnimeDL
3683d93691 Fix issue where sometimes episodes weren't listed
This fixes an issue in Crunchy where sometimes episodes weren't properly listed. Example series was: G8DHV741D
2023-07-08 18:18:32 -07:00
AnimeDL
46c1f9432f Increment version 2023-07-07 16:02:11 -07:00
AnimeDL
371b8336f1 add flag --originalFontSize
Flag to keep original font size for the service Hidive (which defines it's own expected fontsize per series)
2023-07-07 15:43:11 -07:00
AnimeDL
4eb3a4a916 Update to node 18 for prebuilds 2023-07-07 15:28:00 -07:00
AnimeDL
a0d0a7c27c
Add GUI instructions to ReadMe
Adds instructions on how to run the GUI after compiling the TypeScript
2023-07-07 11:20:25 -07:00
AnimeDL
94d12f0eb3 Increment version + Documentation 2023-07-06 04:38:25 +00:00
AnimeDL
2b05222cda Increment version 2023-07-05 21:36:55 -07:00
AnimeDL
a78461a2ac Disable CC subs from being default
Temporary measure until I can think of a better way to implement it so that CC subtitles are not set as default along with the normal subtitle
2023-07-05 21:35:44 -07:00
AnimeDL
5dfea78822 Fix text-shadow parsing error in vtt2ass
Should hopefully fix the warnings about text-shadow being improperly parsed
2023-07-05 21:21:48 -07:00
AnimeDL
0fcf1e95bf Fix hidive not working in GUI version
And increment version + Documentation
2023-07-05 22:53:45 +00:00
AnimeDL
3c422cfd3e Fix hidive not working in GUI version
And increment version
2023-07-05 15:52:50 -07:00
AnimeDL
61f17726a9 Increment version + Documentation 2023-07-05 22:18:50 +00:00
AnimeDL
88066690fd Increment version 2023-07-05 15:18:04 -07:00
AnimeDL
ad12b775bb Properly fix eslint error 2023-07-05 15:17:10 -07:00
AnimeDL
e12d9a30f6 Revert "eslint fixes"
This reverts commit a17677c3bf.
2023-07-05 15:05:20 -07:00
AnimeDL
a528378218
Merge pull request #491 from anidl/hidive
Add hidive to downloaders
2023-07-05 12:37:31 -07:00
AnimeDL
5c5f6cfb7a + Documentation 2023-07-05 18:53:11 +00:00
AnimeDL
580dbe2384 Improve documentation 2023-07-05 11:52:17 -07:00
AnimeDL
112cde5544 + Documentation 2023-07-05 18:16:29 +00:00
AnimeDL
a17677c3bf eslint fixes 2023-07-05 11:11:35 -07:00
AnidlSupport
abefc563d6
Fix vtt2ass module 2023-07-05 10:09:46 +00:00
AnimeDL
f6506fefcc set debug property on cr/hd 2023-07-04 22:45:52 -07:00
AnimeDL
4c870f0640 Respect user defined fontName for hidive 2023-07-04 19:58:34 -07:00
AnimeDL
38fe235b2c Respect user defined fontSize for hidive 2023-07-04 19:56:21 -07:00
AnimeDL
dd483dddb4 add hidive tag 2023-07-04 14:47:44 -07:00
AnimeDL
612bdff774 Initial commit to add hidive 2023-07-04 10:21:16 -07:00
AnimeDL
8414edfab9 Allow for half episodes 2023-07-04 08:35:48 -07:00
AnimeDL
a8bdef3bc7 Add skipmux functionality to CR 2023-07-03 22:16:18 -07:00
AnimeDL
0da2fc232d Implement search restrictions
This should prevent from sending too many API requests when searching via the GUI
2023-07-03 18:00:07 -07:00
AnimeDL
190c59d1a9 Update README.md
Improve syntax and update build instructions
2023-07-02 14:48:45 -07:00
AnidlSupport
3913d9dbe3 Increment version + Documentation 2023-06-18 19:30:45 +00:00
AnidlSupport
eab22402dc
Increment version 2023-06-18 19:30:20 +00:00
AnidlSupport
338ce2d904
Merge pull request #483 from Nicknakin/seriesTitle-templating
Series title templating for file names
2023-06-18 19:28:32 +00:00
Nicholas Kinney
adc21af7f1
Series title templatable now 2023-04-20 13:11:19 -05:00
AnimeDL
9d069b0586 fix lint issue 2023-04-16 19:07:55 -07:00
AnimeDL
ff034ed02c update lockfile + Documentation 2023-04-17 02:04:25 +00:00
AnimeDL
e2cd317a40 update lockfile 2023-04-16 19:03:00 -07:00
AnimeDL
47455e3807 increment version number 2023-04-16 19:01:23 -07:00
AnimeDL
92531715cd Fix downloading movies with -e 2023-04-16 18:49:23 -07:00
AnimeDL
c6f56d27bb update --flm to new API 2023-04-16 18:23:23 -07:00
AnidlSupport
be27f3c113 update version + Documentation 2023-04-07 20:45:09 +00:00
AnidlSupport
de192a21b4
update version 2023-04-07 22:44:38 +02:00
AnidlSupport
921f346bb4
Fix copy to clipboard for images 2023-04-07 22:39:23 +02:00
AnidlSupport
9468f928e7 CLI Default for GUI & Pnpm update + Documentation 2023-03-22 17:16:30 +00:00
AnidlSupport
f32e7b19a2 CLI Default for GUI & Pnpm update 2023-03-22 18:15:57 +01:00
AnidlSupport
1e99c80f81 Docker 2023-03-04 23:51:27 +01:00
AnidlSupport
f99deeb4ba Docker 2023-03-04 23:37:11 +01:00
AnidlSupport
758f1c4e4e
Merge pull request #474 from anidl/gui-rework
New GUI
2023-03-04 23:32:48 +01:00
AnidlSupport
fafa586905 CI 2023-03-04 23:25:26 +01:00
AnidlSupport
d873b6754e Merge branch 'gui-rework' of https://github.com/anidl/multi-downloader-nx into gui-rework 2023-03-04 23:06:03 +01:00
AnidlSupport
62f2e5dea2 Disable CI 2023-03-04 22:55:46 +01:00
AnidlSupport
d18a9c6d53 + Documentation 2023-03-04 21:48:44 +00:00
AnidlSupport
7e4f7e426a Version and pnpm update 2023-03-04 22:48:08 +01:00
AnidlSupport
9bdf49e6cf Fix ts issue 2023-03-03 21:17:12 +01:00
AnidlSupport
42ac8d85c4 New Logger + HLS-Download include 2023-03-03 19:10:20 +01:00
AnidlSupport
8f9a21c773 Loading display for current download if it hasn't started 2023-03-02 22:20:04 +01:00
AnidlSupport
bba20bb255 Fix docker 2023-03-01 23:42:42 +01:00
AnidlSupport
a249342007 Build docker on release 2023-03-01 22:35:37 +01:00
AnidlSupport
913012d1e7 Update workflows 2023-03-01 22:00:26 +01:00
AnidlSupport
db38051cf4 Update workflows 2023-03-01 21:57:41 +01:00
AnidlSupport
e0a6546df5 Fix eslint warnings 2023-03-01 20:40:36 +01:00
AnimeDL
2908e4f259 Fix some linting issues 2023-02-28 16:09:20 -08:00
AnimeDL
e27e5ff1b2
Fix linting 2023-02-28 15:44:22 -08:00
AnidlSupport
87e6990c7b Queue on server 2023-02-28 23:13:00 +01:00
AnimeDL
d1e5a77e3a
Update minimum requirements 2023-02-28 14:06:30 -08:00
AnidlSupport
50d48ca7cd Setup + Passwordless 2023-02-28 17:23:42 +01:00
AnidlSupport
d872bc1fdb Merge branch 'gui-rework' of https://github.com/anidl/multi-downloader-nx into gui-rework 2023-02-27 21:30:35 +01:00
AnidlSupport
dd9c112911 Update build 2023-02-27 21:30:24 +01:00
AnidlSupport
afe964c006 + Documentation 2023-02-27 20:24:14 +00:00
AnidlSupport
31a1c36ad7 Merge branch 'gui-rework' of https://github.com/anidl/multi-downloader-nx into gui-rework 2023-02-27 21:23:39 +01:00
AnidlSupport
54e150f713 Removed unused arg 2023-02-27 21:23:25 +01:00
AnidlSupport
3c2bde7431 + Documentation 2023-02-27 20:21:48 +00:00
AnidlSupport
cd62595518 New GUI 2023-02-27 21:20:06 +01:00
AnidlSupport
6707b7fdd4
Merge pull request #473 from anidl/pnpm-and-gui
Pnpm and gui
2023-02-26 19:34:21 +01:00
AnidlSupport
02a1874ef5 + Documentation 2023-02-26 18:33:41 +00:00
AnidlSupport
7fe6f61614 Increment version + eslint 2023-02-26 19:32:54 +01:00
AnidlSupport
08cbe31fc7 Remove action cache 2023-02-26 19:30:34 +01:00
AnidlSupport
ed82a60a84 Fix actions 2023-02-26 19:29:03 +01:00
AnidlSupport
6ceb80f949 Update setup-node action 2023-02-26 19:27:28 +01:00
AnidlSupport
be8d6a5b6c Update gui 2023-02-25 14:32:42 +01:00
AnidlSupport
c7a330a255 Remove unwanted files 2023-02-21 19:22:37 +01:00
AnidlSupport
bb623e52ea pnpm & GUI update 2023-02-21 19:17:44 +01:00
AnimeDL
fcaca8c394 Increment version number + Documentation 2023-02-17 18:28:59 +00:00
AnimeDL
1b2972066c Increment version number 2023-02-17 10:27:52 -08:00
AnimeDL
ba614d220e Remove duplicate Portuguese 2023-02-17 10:27:41 -08:00
AnimeDL
ff39e811b4 Fix Arabic language downloading 2023-02-17 10:27:22 -08:00
AnimeDL
ef8ce1d609 Fix LA Spanish downloading + Documentation 2023-02-16 22:49:30 +00:00
AnimeDL
99ff19e180 Fix LA Spanish downloading 2023-02-16 14:48:21 -08:00
AnimeDL
758f7e232d Increment version number + Documentation 2023-02-15 00:51:33 +00:00
AnimeDL
cceae5e1fe Increment version number 2023-02-14 16:50:44 -08:00
AnimeDL
98a074fd0d Fixes for -e bugs
Season would be undefined, and mediaId would be incorrect
2023-02-14 16:50:28 -08:00
AnimeDL
f0102c0279 Sanity check for selected language download
Prevents issue where it would say it's downloading in a language that isn't available
2023-02-14 16:49:56 -08:00
AnimeDL
68c19738e6 Fix -s and -e
Fix -s and -e from the API update
2023-02-14 16:49:02 -08:00
AnimeDL
ce41a4de79 Fix crunchyroll API change + Documentation 2023-02-14 22:42:06 +00:00
AnimeDL
43928b6246 Fix crunchyroll API change 2023-02-14 14:41:19 -08:00
AnimeDL
c7f404bc18
Update feature.yml 2023-01-24 19:06:30 -08:00
AnimeDL
0a785934b7 Add feature issue template 2023-01-24 19:04:05 -08:00
AnimeDL
144d2395de
Update bug.yml
Improve formatting for bug reports
2023-01-24 18:42:37 -08:00
AnimeDL
cb4d9ed621 Merge branch 'master' of https://github.com/anidl/multi-downloader-nx + Documentation 2023-01-24 18:16:14 +00:00
AnimeDL
16d661f6ef Merge branch 'master' of https://github.com/anidl/multi-downloader-nx 2023-01-24 10:15:02 -08:00
AnimeDL
54e8e26bd6 cleanup locales array 2023-01-24 10:14:23 -08:00
AnimeDL
24f24945a5 Add pt-PT locale 2023-01-24 10:11:16 -08:00
AnimeDL
df7cc55b64 increment version number + Documentation 2023-01-24 17:55:47 +00:00
AnimeDL
4d960cf726 increment version number 2023-01-24 09:54:49 -08:00
AnimeDL
5cd8986ac2 Add en-IN, fixes #456 2023-01-24 09:53:11 -08:00
AnimeDL
736d4ae0d1 make secondary token refreshes silent 2023-01-24 09:51:31 -08:00
AnimeDL
9d38278955 Actually fix #452 2023-01-23 21:21:48 -08:00
AnimeDL
a0873e4937 Update version number + Documentation 2023-01-24 04:20:14 +00:00
AnimeDL
e97e47d395 Update version number 2023-01-23 20:19:18 -08:00
AnimeDL
caeea00737 Improve detection of media types 2023-01-23 20:18:56 -08:00
AnimeDL
170f526cc2 Fixes #453: Multiple same seasons can exist
Previously if more than one season with the same number existed, it would squash and only the last one would be able to exist... this should hopefully fix that... or make everything worse.
2023-01-23 20:18:36 -08:00
AnimeDL
f23159fb71 Fixes all dubs not showing
Fixes all dubs not showing on old format series
2023-01-23 18:25:38 -08:00
AnimeDL
486c7d0666 Fix #452: logging details 2023-01-23 16:48:45 -08:00
AnimeDL
6afb8a45a6 Fix authentication error after long downloads 2023-01-23 16:05:03 -08:00
AnimeDL
28a85aa085 revert change to url requesting 2023-01-23 11:25:02 -08:00
AnimeDL
736c403575 Merge branch 'master' of https://github.com/anidl/multi-downloader-nx 2023-01-23 10:11:20 -08:00
AnimeDL
e07a4c4228 Update Author to include email 2023-01-23 10:10:55 -08:00
AnimeDL
5fc1c1ad2a
Merge pull request #451 from anidl/crunchyv2api
Update Crunchyroll to the latest API, including redoing the handling of dubs
2023-01-23 09:44:27 -08:00
AnidlSupport
40b5fde746 + Documentation 2023-01-23 07:46:41 +00:00
AnimeDL
47fcaeed50 update json5 2023-01-22 22:12:24 -08:00
AnimeDL
f97cc151e0 ignore dev script linting 2023-01-22 22:07:01 -08:00
AnimeDL
cdc5fae88e Update Maintainers 2023-01-22 21:38:55 -08:00
AnimeDL
64a69d953e Update Crunchy to new API 2023-01-22 21:37:53 -08:00
AnimeDL
614c382402 Update got 2023-01-21 21:19:13 -08:00
AnimeDL
d4fac8e6ce update electron builder 2023-01-21 11:28:32 -08:00
AnimeDL
56a66bdfbb linter + Documentation 2023-01-21 18:57:40 +00:00
AnimeDL
ab670d08e7 linter 2023-01-21 10:56:17 -08:00
AnimeDL
95b8c14b89 Prelim Crunchy Update 2023-01-21 10:49:05 -08:00
AnidlSupport
d66d86fb7c
Update dev.js 2023-01-21 12:53:02 +01:00
AnidlSupport
cf8b02421f
Add dev script 2023-01-20 23:36:23 +01:00
AnidlSupport
b34bb417b9 Merge pull request #401 from metaljerk/typo-fix
Fix minor typos + Documentation
2022-11-03 11:54:19 +00:00
AnidlSupport
8956926ea0
Merge pull request #401 from metaljerk/typo-fix
Fix minor typos
2022-11-03 12:53:36 +01:00
metaljerk
3b7cff85cf
Update DOCUMENTATION.md 2022-10-11 16:00:58 -05:00
metaljerk
31c2e6eb26
Update module.args.ts 2022-10-11 16:00:03 -05:00
AnimeDL
6c10aa1719 Merge pull request #381 from RavensRain/master
Ability to reduce unnecessary bandwidth usage + Documentation
2022-09-19 03:38:25 +00:00
AnimeDL
45b2fc2624
Merge pull request #381 from RavensRain/master
Ability to reduce unnecessary bandwidth usage
2022-09-18 20:37:52 -07:00
RavensRain
67ccb5daaa set dlVideoOnce to crunchy only 2022-09-12 19:14:51 +02:00
RavensRain
cce7956b07 remove useless console.log
and set dlVideoDownload as non-default
2022-09-11 13:22:05 +02:00
RavensRain
f6303c1a6e Ability to reduce unnecessary bandwidth usage 2022-09-11 10:41:21 +02:00
AnidlSupport
c3835cb79b
Merge pull request #370 from anidl/dependabot/npm_and_yarn/dotenv-16.0.2
Bump dotenv from 14.3.2 to 16.0.2
2022-08-31 13:38:26 +02:00
AnidlSupport
f7d9c9a1af
Merge pull request #369 from anidl/dependabot/npm_and_yarn/types/yargs-17.0.12
Bump @types/yargs from 17.0.10 to 17.0.12
2022-08-31 13:38:16 +02:00
AnidlSupport
e7ec2b9f92
Merge pull request #368 from anidl/dependabot/npm_and_yarn/eslint-8.23.0
Bump eslint from 8.20.0 to 8.23.0
2022-08-31 13:38:06 +02:00
AnidlSupport
b43375a377
Merge pull request #363 from anidl/dependabot/npm_and_yarn/fork-ts-checker-webpack-plugin-7.2.13
Bump fork-ts-checker-webpack-plugin from 6.5.2 to 7.2.13
2022-08-31 13:37:52 +02:00
AnidlSupport
efea1d9bd2
Merge pull request #362 from anidl/dependabot/npm_and_yarn/removeNPMAbsolutePaths-3.0.0
Bump removeNPMAbsolutePaths from 2.0.0 to 3.0.0
2022-08-31 13:37:43 +02:00
dependabot[bot]
1ec0b19db6
Bump dotenv from 14.3.2 to 16.0.2
Bumps [dotenv](https://github.com/motdotla/dotenv) from 14.3.2 to 16.0.2.
- [Release notes](https://github.com/motdotla/dotenv/releases)
- [Changelog](https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/motdotla/dotenv/compare/v14.3.2...v16.0.2)

---
updated-dependencies:
- dependency-name: dotenv
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-31 06:23:05 +00:00
dependabot[bot]
263728a7d7
Bump @types/yargs from 17.0.10 to 17.0.12
Bumps [@types/yargs](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/yargs) from 17.0.10 to 17.0.12.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/yargs)

---
updated-dependencies:
- dependency-name: "@types/yargs"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-30 06:24:01 +00:00
dependabot[bot]
115b2628e0
Bump eslint from 8.20.0 to 8.23.0
Bumps [eslint](https://github.com/eslint/eslint) from 8.20.0 to 8.23.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.20.0...v8.23.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-29 06:24:07 +00:00
AnimeDL
7a6b152b7a
Add Discord to readme
Add discord to readme, and spellcheck
2022-08-28 11:51:00 -07:00
dependabot[bot]
b7bbbe8be8
Bump fork-ts-checker-webpack-plugin from 6.5.2 to 7.2.13
Bumps [fork-ts-checker-webpack-plugin](https://github.com/TypeStrong/fork-ts-checker-webpack-plugin) from 6.5.2 to 7.2.13.
- [Release notes](https://github.com/TypeStrong/fork-ts-checker-webpack-plugin/releases)
- [Changelog](https://github.com/TypeStrong/fork-ts-checker-webpack-plugin/blob/main/CHANGELOG.md)
- [Commits](https://github.com/TypeStrong/fork-ts-checker-webpack-plugin/compare/v6.5.2...v7.2.13)

---
updated-dependencies:
- dependency-name: fork-ts-checker-webpack-plugin
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-19 18:22:42 +00:00
dependabot[bot]
effcec2db8
Bump removeNPMAbsolutePaths from 2.0.0 to 3.0.0
Bumps [removeNPMAbsolutePaths](https://github.com/juanjoDiaz/removeNPMAbsolutePaths) from 2.0.0 to 3.0.0.
- [Release notes](https://github.com/juanjoDiaz/removeNPMAbsolutePaths/releases)
- [Commits](https://github.com/juanjoDiaz/removeNPMAbsolutePaths/compare/v2.0.0...v3.0.0)

---
updated-dependencies:
- dependency-name: removeNPMAbsolutePaths
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-08-19 18:22:16 +00:00
anidl-git
ea4f202ee5
Add files via upload 2022-08-19 21:21:34 +03:00
Izuco
7658259836
Update Dockerfile 2022-07-28 15:20:41 +02:00
Izuco
663fe567ef
Update docker.yml 2022-07-28 15:03:58 +02:00
izu-co
9b4b968730 Update version + dependencies + Documentation 2022-07-23 19:33:59 +00:00
Izuco
8b02f3efc1
Update version + dependencies 2022-07-23 21:33:15 +02:00
izu-co
6413409ef3 Merge pull request #349 from acc029/master
Separate Latin American Spanish and European Spanish codes with region/territory tags + Documentation
2022-07-23 19:30:38 +00:00
Izuco
0acac420d9
Merge pull request #349 from acc029/master
Separate Latin American Spanish and European Spanish codes with region/territory tags
2022-07-23 21:30:06 +02:00
Izuco
28d1b835e3
Fix eslint errors 2022-07-23 21:24:24 +02:00
acc029
c0aa3fa5df
Merge branch 'anidl:master' into master 2022-07-23 12:36:13 -03:00
izu-co
e7abd13bb1 Ability to override the name of "cc" + Documentation 2022-07-22 16:48:33 +00:00
Izuco
5751676e16
Ability to override the name of "cc" 2022-07-22 18:47:37 +02:00
izu-co
4416e34398 Ability to change the default sub and/or dub langauge in merged file + Documentation 2022-07-21 18:07:00 +00:00
Izuco
c3fbc34478
Ability to change the default sub and/or dub langauge in merged file 2022-07-21 20:06:10 +02:00
acc029
1d00b42b0b Separate Latin American Spanish and European Spanish codes with region/territory tags 2022-07-21 14:36:25 -03:00
Izuco
e80dd03951
Merge pull request #346 from anidl/dependabot/npm_and_yarn/gui/react/terser-5.14.2
Bump terser from 5.13.1 to 5.14.2 in /gui/react
2022-07-21 18:45:08 +02:00
Izuco
55f34fb388
Merge pull request #347 from anidl/dependabot/npm_and_yarn/terser-5.14.2
Bump terser from 5.14.0 to 5.14.2
2022-07-21 18:44:58 +02:00
dependabot[bot]
01a490a730
Bump terser from 5.14.0 to 5.14.2
Bumps [terser](https://github.com/terser/terser) from 5.14.0 to 5.14.2.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-21 05:30:48 +00:00
dependabot[bot]
9fb131a3f1
Bump terser from 5.13.1 to 5.14.2 in /gui/react
Bumps [terser](https://github.com/terser/terser) from 5.13.1 to 5.14.2.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-21 03:53:47 +00:00
izu-co
5b29bd381c Merge pull request #343 from acc029/master + Documentation 2022-07-19 12:23:33 +00:00
Izuco
9dbd30a5f4
Merge pull request #343 from acc029/master 2022-07-19 14:22:28 +02:00
acc029
d8917d918a
Fix support for new dubs
Fix English/European Spanish dub downloads and add support for Hindi. European Spanish and Latin American Spanish still use the same download code.
2022-07-19 08:30:16 -03:00
Izuco
7e67a8f61a
CC for Crunchyroll 2022-07-05 23:03:19 +02:00
Izuco
b8432270b6
Fixed #339 2022-07-04 18:19:26 +02:00
Izuco
4984a2bfbf
Merge branch 'master' of https://github.com/anidl/multi-downloader-nx 2022-07-02 14:02:32 +02:00
Izuco
936f86b1f0
Sanitize filename parts; Fix #335 2022-07-02 14:02:10 +02:00
izu-co
b076d8f615 Added zh-CN + Documentation 2022-07-02 11:55:42 +00:00
Izuco
250709dc1d
Added zh-CN 2022-07-02 13:54:53 +02:00
Izuco
602d39e97b
Drop arm support in flavor of x64 2022-07-01 22:16:10 +02:00
Izuco
9fefe5ecff
Update Subtitle naming 2022-07-01 21:14:53 +02:00
Izuco
8f97971632
Build for x64 2022-07-01 20:48:42 +02:00
Izuco
4f3dda667c
Merge pull request #316 from anidl/dependabot/npm_and_yarn/electron-16.2.6
Bump electron from 16.0.7 to 16.2.6
2022-06-17 22:10:08 +02:00
Izuco
c87bd71dd9
Eslint 2022-06-17 21:49:10 +02:00
dependabot[bot]
8ae9cdef64
Bump electron from 16.0.7 to 16.2.6
Bumps [electron](https://github.com/electron/electron) from 16.0.7 to 16.2.6.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v16.0.7...v16.2.6)

---
updated-dependencies:
- dependency-name: electron
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-16 23:22:01 +00:00
izu-co
c7056418b2 Add novids and noaudio option to the gui + Documentation 2022-06-01 16:47:12 +00:00
Izuco
5a00353c11
Add novids and noaudio option to the gui 2022-06-01 18:46:13 +02:00
izu-co
48cb7434cc #311 + Documentation 2022-05-29 14:19:00 +00:00
Izuco
11372c041f
#311 2022-05-29 16:18:15 +02:00
Izuco
86728d08b9
Update package-lock.json 2022-05-09 16:26:28 +02:00
Izuco
f2dd58910b
Merge branch 'master' of https://github.com/anidl/multi-downloader-nx 2022-05-09 16:24:01 +02:00
Izuco
b8972a8526
Update dependencies 2022-05-09 16:23:53 +02:00
izu-co
24aae3e8e8 Move langsdata + Documentation 2022-05-09 14:23:32 +00:00
Izuco
8424e12449
Move langsdata 2022-05-09 16:22:31 +02:00
Izuco
32a52a1a1b
Eslint 2022-04-18 11:36:14 +02:00
Izuco
23ae20a3da
Remove debug console logs 2022-04-18 11:35:34 +02:00
Izuco
eaa24985ad
Fix Download Archive 2022-04-17 21:44:58 +02:00
Izuco
4caf8f87d0
Fix #307 2022-04-17 21:26:20 +02:00
izu-co
8902f20b81 Make jpn the default dub lang again + Documentation 2022-04-17 16:53:50 +00:00
Izuco
012db6ca76
Make jpn the default dub lang again 2022-04-17 18:52:40 +02:00
Izuco
c2284b85cc
Update index.ts 2022-04-15 20:58:18 +02:00
Izuco
89bd225391
Fix #298 2022-04-15 20:31:37 +02:00
Izuco
3854cb0d4e
Fixed an issue where no new archive items could be added 2022-04-15 20:09:27 +02:00
Izuco
552e16774b
Fix an issue where the updater would fail and added tsx support 2022-04-09 21:16:32 +02:00
izu-co
fb056aeb16 Update version + Documentation 2022-04-08 05:14:26 +00:00
Izuco
c40eb332ea
Update version 2022-04-08 07:13:23 +02:00
Izuco
ea3337b94a
Merge pull request #294 from anidl/dependabot/npm_and_yarn/gui/react/minimist-1.2.6
Bump minimist from 1.2.5 to 1.2.6 in /gui/react
2022-03-25 21:04:51 +01:00
Izuco
ad1876c800
Merge pull request #291 from anidl/dependabot/npm_and_yarn/gui/react/node-forge-1.3.0
Bump node-forge from 1.2.1 to 1.3.0 in /gui/react
2022-03-25 21:04:41 +01:00
izu-co
8ad994943d Fixed #293 + Documentation 2022-03-25 20:01:48 +00:00
Izuco
13e0cd7b07
Fixed #293 2022-03-25 21:00:54 +01:00
Izuco
bcd926048f
Fixing Docker erros when pull-requesting 2022-03-25 20:51:47 +01:00
Izuco
272177e176
Docker 2022-03-25 20:46:19 +01:00
dependabot[bot]
6f2096eb71
Bump minimist from 1.2.5 to 1.2.6 in /gui/react
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-25 19:32:29 +00:00
Izuco
78d999f2d4
Fix #292 2022-03-25 19:33:51 +01:00
dependabot[bot]
589d382569
Bump node-forge from 1.2.1 to 1.3.0 in /gui/react
Bumps [node-forge](https://github.com/digitalbazaar/forge) from 1.2.1 to 1.3.0.
- [Release notes](https://github.com/digitalbazaar/forge/releases)
- [Changelog](https://github.com/digitalbazaar/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/digitalbazaar/forge/compare/v1.2.1...v1.3.0)

---
updated-dependencies:
- dependency-name: node-forge
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-22 18:21:07 +00:00
izu-co
0218356073 Merge pull request #288 from anidl/ui-overhaul
Started Layout - Working on Queue + Documentation
2022-03-22 18:00:31 +00:00
Izuco
7d013462bd
Merge pull request #288 from anidl/ui-overhaul
Started Layout - Working on Queue
2022-03-22 18:59:42 +01:00
Izuco
2f2333051b
Update Version + Remove Beta announcment 2022-03-22 16:26:12 +01:00
Izuco
169376e9d7
Finished Queue 2022-03-22 16:21:11 +01:00
Izuco
9c5095f7a8
Started Layout - Working on Queue 2022-03-20 13:47:17 +01:00
izu-co
84da65120f Fix #232 + Documentation 2022-03-20 09:43:53 +00:00
Izuco
d4328eda0f
Fix #232 2022-03-20 10:43:02 +01:00
izu-co
a17d61d464 Eslint + Documentation 2022-03-19 09:51:54 +00:00
Izuco
9855faa91a
Eslint 2022-03-19 10:51:09 +01:00
Izuco
5931760449
Added the ability to set the option if the file has created programmaticly 2022-03-19 10:50:30 +01:00
Izuco
3271548987
Fix #282 2022-03-17 20:45:34 +01:00
Izuco
5386800473
UI improvments 2022-03-17 20:30:49 +01:00
Izuco
ef054dafed
Eslint 2022-03-17 17:57:39 +01:00
Izuco
8021dbea5e
Bug Fixes 2022-03-17 17:56:35 +01:00
Izuco
9a805f995a
Added #286 2022-03-17 17:26:43 +01:00
Izuco
431e840913
More debug information 2022-03-17 17:26:23 +01:00
izu-co
f1c2f84f66 Fixed #284 + Documentation 2022-03-16 18:34:58 +00:00
Izuco
d273d8f495
Fixed #284 2022-03-16 19:29:33 +01:00
izu-co
6e04f93110 Update version + Documentation 2022-03-14 18:50:55 +00:00
Izuco
5c28ff9402
Update version 2022-03-14 19:50:10 +01:00
Izuco
c39e3e40d8
Fixed #280 2022-03-14 19:48:24 +01:00
Izuco
eaf636061b
Fix #281 2022-03-14 19:40:53 +01:00
Izuco
1d1ffd324b
Merge pull request #279 from edward8e/master
Upgrade to include Docker
2022-03-13 11:46:47 +01:00
Izuco
1b697bf13b
Update docker builder 2022-03-13 11:38:58 +01:00
Izuco
8c6eafe552
Only push with commited to main 2022-03-13 11:16:46 +01:00
Izuco
fe835c2b37
Update secrets and removed unused build step 2022-03-13 11:12:53 +01:00
Edward Hernandez
3160d1ba55
Merge branch 'anidl:master' into master 2022-03-11 22:35:18 -08:00
Edward Hernandez
9440587b20 Upgrade to include Docker 2022-03-11 22:17:26 -08:00
Izuco
b8ff1105d6
Merge pull request #276 from oxixes/locale-fix
[CR] Fix only one locale per language being downloaded
2022-03-11 16:28:23 +01:00
oxixes
86fb936bf2 Add OR statement 2022-03-09 21:50:17 +01:00
oxixes
a1f89c8722 Fix locale issue
This allows to download multiple locales of the same language.
2022-03-06 20:15:01 +01:00
izu-co
4ac53d55f0 Update version + Documentation 2022-03-05 18:18:52 +00:00
Izuco
439c8d14ce
Update version 2022-03-05 19:18:02 +01:00
Izuco
dea190ec83
Fix #273 2022-03-05 19:16:17 +01:00
Izuco
6122a3eaa0
Eslint 2022-03-05 19:01:04 +01:00
Izuco
9ef4d43674
Fix crash after download 2022-03-05 19:00:33 +01:00
Izuco
acd427a251
Fix wrong episode from funimation 2022-03-05 19:00:15 +01:00
Izuco
2e6710dc34
Latest.log instead of all being a timestamp 2022-03-05 18:58:19 +01:00
Izuco
115781c92a
Fix issue template 2022-03-05 18:21:39 +01:00
izu-co
ebf683a9a8 Update default quality to max + Documentation 2022-03-05 12:34:13 +00:00
Izuco
54b031e516
Update default quality to max 2022-03-05 13:33:36 +01:00
Izuco
30b4b571a1
Progress bar 2022-03-05 13:32:49 +01:00
Izuco
aa1ec94f5d
Change service later 2022-03-04 20:11:27 +01:00
Izuco
2d139acffb
#261 2022-03-04 19:58:56 +01:00
Izuco
d1e88c120e
Update Github files 2022-03-04 19:52:16 +01:00
Izuco
ee6a302b82
Fix #271 2022-03-04 18:44:57 +01:00
izu-co
59b47a6e7c Build Documentaion 2022-03-03 20:16:48 +00:00
Izuco
0b82788b24
Fixed #232 2022-03-03 21:15:47 +01:00
Izuco
19800b59e3
Fix #270 2022-03-03 20:44:16 +01:00
izu-co
9f888a1cdb Build Documentaion 2022-03-01 16:13:58 +00:00
Izuco
abacbd2596
Merge branch 'master' of https://github.com/anidl/multi-downloader-nx 2022-03-01 17:13:06 +01:00
Izuco
e6555d51d5
--videoTitle for mkvmerge 2022-03-01 17:12:59 +01:00
izu-co
bc89271a69 Build Documentaion 2022-03-01 16:04:09 +00:00
Izuco
eac922863b
Merge branch 'master' of https://github.com/anidl/multi-downloader-nx 2022-03-01 17:03:25 +01:00
Izuco
a3c324aef5
Set coustom ffmpeg track title 2022-03-01 17:03:10 +01:00
izu-co
87ca363ff0 Build Documentaion 2022-03-01 15:33:49 +00:00
Izuco
56b595f05c
Eslint 2022-03-01 16:32:54 +01:00
Izuco
620e2085e2
Introducing --override
- Override OR set any filename variables
 - Use `toOverride='Override'`. Please note that the ' are required
2022-03-01 16:32:14 +01:00
Izuco
25079a2f79
Merge pull request #256 from anidl/dependabot/npm_and_yarn/gui/react/follow-redirects-1.14.8
Bump follow-redirects from 1.14.7 to 1.14.8 in /gui/react
2022-02-26 13:27:46 +01:00
Izuco
45c1810fe6
Fixed #232 2022-02-26 13:21:00 +01:00
Izuco
fed44e1cea
Fix #201 and #245 2022-02-26 11:11:21 +01:00
Izuco
9d8488ff24
Update README.md 2022-02-24 20:11:56 +01:00
Izuco
599f2572db Merge branch 'master' of https://github.com/anidl/multi-downloader-nx 2022-02-20 20:05:55 +01:00
Izuco
6562dede2a Update build.ts 2022-02-20 20:05:45 +01:00
izu-co
b8b355da04 Build Documentaion 2022-02-20 18:57:32 +00:00
Izuco
a7c7451b0d Update + Update Version + Eslint 2022-02-20 19:56:45 +01:00
Izuco
a307b9b1ed Fix #258 2022-02-20 19:51:42 +01:00
Izuco
750bcc0e8a
Use electron-builder 2022-02-20 15:44:20 +01:00
Izuco
59b87d22e9
Fix #259 2022-02-19 13:36:22 +01:00
Izuco
e6d5dab21a
Change application data path 2022-02-17 21:42:45 +01:00
dependabot[bot]
73f7a2dde0
Bump follow-redirects from 1.14.7 to 1.14.8 in /gui/react
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.7 to 1.14.8.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.7...v1.14.8)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-15 21:42:36 +00:00
Izuco
b24a55c8a3
New GUI - BETA 2022-02-15 22:33:51 +01:00
izu-co
7423fac392 Build Documentaion 2022-02-15 21:33:14 +00:00
Izuco
c5fd5cf313
Merge branch 'master' into gui 2022-02-15 22:32:21 +01:00
Izuco
f665a9b76b
Bump Version 2022-02-15 22:29:57 +01:00
Izuco
8b4e33f558
Merge pull request #254 from anidl/Build-Update
Build update
2022-02-15 22:22:14 +01:00
Izuco
9eaf940cb6
Linux fixes 2022-02-15 21:52:38 +01:00
Izuco
203bd9d8f5
Fix typo 2022-02-15 21:17:24 +01:00
Izuco
89f34f83f5
Fix tests 2022-02-15 21:08:37 +01:00
Izuco
66e97648da
Added missing $ 2022-02-15 21:08:03 +01:00
Izuco
511bd9a131
Bug fixes 2022-02-15 21:02:22 +01:00
Izuco
f85753fd9b
Bug fixes 2022-02-15 20:19:45 +01:00
Izuco
0a948788a4
Build GUI and CLI 2022-02-15 20:11:55 +01:00
Izuco
e8eeee5aa4
Bump vm2 from 3.9.5 to 3.9.7 2022-02-15 14:57:21 +01:00
dependabot[bot]
be25e331f6
Bump vm2 from 3.9.5 to 3.9.7
Bumps [vm2](https://github.com/patriksimek/vm2) from 3.9.5 to 3.9.7.
- [Release notes](https://github.com/patriksimek/vm2/releases)
- [Changelog](https://github.com/patriksimek/vm2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/patriksimek/vm2/compare/3.9.5...3.9.7)

---
updated-dependencies:
- dependency-name: vm2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-15 00:47:59 +00:00
Izuco
6fabaf489e
Stuff 2022-02-13 15:33:04 +01:00
Izuco
e9852ade1a
See todo for changes 2022-02-12 16:52:36 +01:00
Izuco
f076a7af7e
MWP 2022-02-08 19:28:32 +01:00
Izuco
40d07bb40d
Working downloads with queue system 2022-02-05 17:08:57 +01:00
Izuco
7dc72a1507
Downloading without visual feedback 2022-02-05 13:18:19 +01:00
Izuco
66c56f3522
Prepared feedback for downloading 2022-02-04 22:01:33 +01:00
Izuco
d7b43e398a
Add 'all' to dub select 2022-02-02 18:56:31 +01:00
Izuco
fde33eec76
Update text 2022-02-01 21:01:54 +01:00
Izuco
14de1b243a
Show episode listing 2022-02-01 20:57:41 +01:00
Izuco
ea9001d0b3
Path fix 2022-01-31 21:30:25 +01:00
Izuco
a9e2908ece
Use local server for react 2022-01-31 20:05:26 +01:00
Izuco
a011ef17be
Update tsc.ts 2022-01-31 19:09:11 +01:00
Izuco
64d9f64fb8
Update tsc.ts 2022-01-31 19:08:25 +01:00
Izuco
1a5939791e
Start of queue 2022-01-30 21:01:20 +01:00
Izuco
162a89efc9
Show default download options 2022-01-30 16:02:23 +01:00
Izuco
57836d7a91
Eslint + Crunchy implementation 2022-01-30 15:35:44 +01:00
Izuco
1546300832
Update tsc.ts 2022-01-29 20:33:47 +01:00
Izuco
df764a8620
Download options 2022-01-28 23:50:39 +01:00
Izuco
71f7eb2a69
Search bar 2022-01-27 20:55:21 +01:00
Izuco
75888c23a5
Working authentication for funimation 2022-01-26 22:14:55 +01:00
Izuco
0d5d0f0102
Build application 2022-01-25 21:04:57 +01:00
Izuco
3c34eb06dc
Restructe to be able to build the app later down the line 2022-01-25 17:51:04 +01:00
Izuco
3faf9e496f
funi.js to Class conversion 2022-01-24 20:53:54 +01:00
Izuco
52cff06388
More funi.ts to class conversion 2022-01-24 20:19:30 +01:00
Izuco
2e7271afcb
Start of gui 2022-01-23 20:26:38 +01:00
Izuco
a9a61c717e
Merge branch 'master' of https://github.com/anidl/multi-downloader-nx 2022-01-22 13:55:34 +01:00
Izuco
31628128b2
Fixed an issue where the path to the multi dl folder coud not contain a space 2022-01-22 13:55:14 +01:00
izu-co
c0595082d0 Build Documentaion 2022-01-18 20:53:25 +00:00
Izuco
362e40ab13
Typo 2022-01-18 21:52:48 +01:00
Izuco
d1df4909bc
Eslint 2022-01-18 15:45:09 +01:00
Izuco
d25fbe4f1c
Fixed #226 2022-01-18 15:42:03 +01:00
izu-co
e8ebaffef8 Build Documentaion 2022-01-17 15:06:34 +00:00
Izuco
401b64db05
Update fs handeling for EBUSY errors 2022-01-17 16:05:57 +01:00
izu-co
04c8d5a5d8 Build Documentaion 2022-01-16 15:06:49 +00:00
Izuco
5c5f0c73d2
FFmpeg can add fonts to mkv file 2022-01-16 16:06:10 +01:00
Izuco
595b407b22
Force Muxer command 2022-01-16 15:59:39 +01:00
izu-co
4e055418aa Build Documentaion 2022-01-15 16:02:51 +00:00
Izuco
6f7ea974fc
Update version 2022-01-15 17:02:24 +01:00
Izuco
5e89aa3dd3
Automaticlly build documentation 2022-01-15 16:50:57 +01:00
Izuco
0c2c021f8f
Update on trigger 2022-01-15 16:34:54 +01:00
Izuco
9513cead33
Eslint fix 2022-01-15 16:34:04 +01:00
Izuco
a3a50e97c1
Auto update documentation 2022-01-15 16:32:53 +01:00
Izuco
aca06d9a76
Update config 2022-01-15 15:25:45 +01:00
Izuco
06fbd7a4bc
Dont override config files 2022-01-15 15:25:33 +01:00
Izuco
1cd8c6ed19
Prevent duplicate archive entries 2022-01-15 14:54:51 +01:00
Izuco
af8ebbb92c
Logging + Fix unknown season title + Fix padding 2022-01-11 22:49:28 +01:00
Izuco
02bdbf1861
Hotfix as described in #217 2022-01-10 22:38:23 +01:00
Izuco
32b10e7ba1
Let every default option be set in cli-default 2022-01-08 22:28:21 +01:00
Izuco
1b001438d9
Let search-type be set in cli-default 2022-01-08 22:26:12 +01:00
Izuco
d61bad81b5
Added silentAuth + set username and password as args 2022-01-07 20:34:53 +01:00
Izuco
05d65d3d54
Added missing option partsize 2022-01-06 17:45:07 +01:00
Izuco
c89cea218c
Fixed typos as suggested by @Jaynator495 (thanks) 2021-12-29 01:26:03 +01:00
Izuco
57aaae1434
New args 2021-12-26 22:29:30 +01:00
Izuco
e1dbe9f1a6
Eslint 2021-12-26 22:26:43 +01:00
Izuco
fde01a049a
Full list + implementation 2021-12-26 22:25:30 +01:00
Izuco
50075dfbe3
Start 2021-12-26 20:10:39 +01:00
Izuco
b7ccefbddd
Added Encoding as requested in #202 2021-12-25 23:18:58 +01:00
Izuco
39818cfb07
Update bug template 2021-12-23 20:54:34 +01:00
Izuco
546550bd44
Fixed #199 2021-12-22 12:22:20 +01:00
Izuco
0824d4873a
Fixed typo (again) 2021-12-21 23:28:46 +01:00
Izuco
82b17c75bc
Update font managment 2021-12-21 20:55:44 +01:00
Izuco
78c529d072
Fix spelling and --dlFonts 2021-12-21 17:02:29 +01:00
Izuco
46ecd0922b
CC support as in #198 2021-12-21 14:35:15 +01:00
Izuco
6bde435a90
Eslint 2021-12-20 20:07:52 +01:00
Izuco
b681bc3491
Update langsdata to fix #192 2021-12-20 20:06:20 +01:00
Izuco
918e44f169
Fixed #194 2021-12-20 18:09:04 +01:00
Izuco
50bcc3e774
New templates 2021-12-19 21:31:46 +01:00
Izuco
41527822a6
Remove cli 2021-12-19 21:31:30 +01:00
Izuco
bda6beb13a
Bug 2021-12-19 21:30:46 +01:00
Izuco
8ee461605d
Fixed #190 and fixed auto updater 2021-12-19 19:16:33 +01:00
Izuco
7f03bd2d86
Fixed #189 2021-12-19 13:26:16 +01:00
Izuco
476b49c56c
Fixed #183 + Crunchy allSubs and allDubs 2021-12-11 12:11:41 +01:00
Izuco
f96936986a
Hot Fix: Build Script 2021-12-09 18:05:32 +01:00
Izuco
77985660c6
Style + Version 2021-12-09 17:57:32 +01:00
Izuco
24adba8b80
Changed default bin paths to use the path ENV 2021-12-09 17:56:01 +01:00
Izuco
a7a17d7408
Added fonts to default dir 2021-12-09 17:55:34 +01:00
Izuco
77ed8b6d1f
Added muxing to direct downloads 2021-12-09 17:55:20 +01:00
Izuco
a55cf7ae8e
Fix #182 2021-12-09 17:54:51 +01:00
Izuco
d262ce86bb
Added English language back #181 2021-12-07 22:03:23 +01:00
Izuco
667f02fae2
Eslint 2021-11-30 19:46:14 +01:00
Izuco
e642e73a52
Use Langs data in Funi 2021-11-30 19:44:23 +01:00
Izuco
5be607ff2f
Removed line that broke the auto updater 2021-11-28 18:04:49 +01:00
Izuco
a2070aa16d
Eslint 2021-11-28 14:59:42 +01:00
Izuco
d262972817
#177 Implemented 2021-11-28 14:58:48 +01:00
Izuco
77a9676eab
Episode select with media id 2021-11-27 19:50:35 +01:00
Izuco
d1972e3725
Crunchy season fix
- The number of the season will now be correctly inserted
 - The default filename templated has been altered to include the season
2021-11-25 11:58:08 +01:00
Izuco
3ae1269ac7
Fixed an issue where the downloader would crash 2021-11-23 19:07:59 +01:00
Izuco
0d051ac94d
Code Refactor + #172 Implementation 2021-11-23 16:55:17 +01:00
Izuco
5d0565010c
Style 2021-11-23 15:56:55 +01:00
Izuco
162e8656b6
Fixed #175 2021-11-23 15:56:19 +01:00
Izuco
0bfbeee6e3
--but and --downloadArchive 2021-11-21 15:00:51 +01:00
Izuco
e133821424
Revert unintended default config changes 2021-11-21 14:58:25 +01:00
Izuco
a10d84ca4b
Update version 2021-11-21 14:54:35 +01:00
Izuco
0cef1d95ee
Style 2021-11-21 14:53:50 +01:00
Izuco
8f6dcbed0f
Download Archve 2021-11-21 14:53:14 +01:00
Izuco
9586b412a1
Style 2021-11-20 15:28:44 +01:00
Izuco
72a38b975b
Crunchy but 2021-11-20 15:28:20 +01:00
Izuco
cb346284ba
Funimation but && crunchyroll all 2021-11-20 15:12:10 +01:00
Izuco
7beff58108
Update hls-download to display download speed 2021-11-20 14:02:40 +01:00
Izuco
07de7a29d4
Eslint 2021-11-19 23:53:46 +01:00
Izuco
846baea609
Crunchy multi dubs 2021-11-19 23:53:09 +01:00
157 changed files with 36691 additions and 9135 deletions

1
.dockerignore Normal file
View file

@ -0,0 +1 @@
**/node_modules

View file

@ -1,3 +0,0 @@
lib
/videos/*.ts
crunchy

View file

@ -1,33 +0,0 @@
{
"env": {
"es2021": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
"@typescript-eslint/no-explicit-any": "off",
"indent": [
"error",
2
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
]
}
}

View file

@ -1,18 +0,0 @@
---
name: CLI
about: Do you need help with the CLI? Than use this template :)
title: "[CLI] <Short description here>"
labels: cli
assignees: izu-co
---
<!-- Please fill in the placeholders.-->
** Main info: **
Script version:
Service:
What do you want to do:
** Additional Info: **

93
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View file

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

View file

@ -1,25 +0,0 @@
---
name: Bug report
about: Found a Bug? Than report it here :)
title: "[BUG] [Funimation/Crunchy] <Short description here>"
labels: bug
assignees: izu-co
---
<!-- Please fill in the placeholders.-->
** Main info: **
Script version:
Service:
Show ID:
Episode ID:
Command used:
<!-- Make sure you don't leak your token. This should only be a concern when you are using the --debug flag -->
Console output:
```text
```
** Additional Info: **

29
.github/ISSUE_TEMPLATE/feature.yml vendored Normal file
View file

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

6
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"

View file

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

32
.github/workflows/docker.yml vendored Normal file
View file

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

View file

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

View file

@ -11,23 +11,29 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 14
uses: actions/setup-node@v2
- uses: pnpm/action-setup@v2
with:
node-version: 14
cache: 'npm'
- run: npm i
version: 8.6.6
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: 20
check-latest: true
- run: pnpm i
- run: npx eslint .
test:
needs: eslint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 14
uses: actions/setup-node@v2
- uses: pnpm/action-setup@v2
with:
node-version: 14
cache: 'npm'
- run: npm i
- run: npm run test
version: 8.6.6
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: 20
check-latest: true
- run: pnpm i
- run: pnpm run test

27
.gitignore vendored
View file

@ -1,9 +1,11 @@
/bin/ff*
/bin/mkv*
/_builds/*
/node_modules/
**/node_modules/
/videos/*.json
/videos/*.ts
/videos/*.m4s
/videos/*.txt
.DS_Store
ffmpeg
mkvmerge
@ -19,4 +21,25 @@ token.yml
lib
test.*
updates.json
cr_token.yml
*_token.yml
*_profile.yml
*_sess.yml
archive.json
guistate.json
fonts
.webpack/
out/
dist/
gui/react/build/
docker-compose.yml
crunchyendpoints
.vscode
.idea
/logs
/tmp/*/
!videos/.gitkeep
/videos/*
/tmp/*.*
bin
widevine/*
!widevine/.gitkeep

50
@types/adnPlayerConfig.d.ts vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;
}

139
@types/crunchyAndroidEpisodes.d.ts vendored Normal file
View file

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

189
@types/crunchyAndroidObject.d.ts vendored Normal file
View file

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

93
@types/crunchyAndroidStreams.d.ts vendored Normal file
View file

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

26
@types/crunchyChapters.d.ts vendored Normal file
View file

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

View file

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

44
@types/crunchyPlayStreams.d.ts vendored Normal file
View 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;
}

View file

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

View file

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

View file

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

16
@types/enums.ts Normal file
View 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',
}

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

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

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

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

73
@types/hidiveDashboard.d.ts vendored Normal file
View file

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

84
@types/hidiveEpisodeList.d.ts vendored Normal file
View file

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

47
@types/hidiveSearch.d.ts vendored Normal file
View file

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

61
@types/hidiveTypes.d.ts vendored Normal file
View file

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

View file

@ -1,26 +0,0 @@
declare module 'hls-download' {
export default class hlsDownload {
constructor(options: {
m3u8json: {
segments: Record<string, unknown>[],
mediaSequence?: number,
},
output?: string,
threads?: number,
retries?: number,
offset?: number,
baseurl?: string,
proxy?: string,
skipInit?: boolean,
timeout?: number
})
async download() : Promise<{
ok: boolean,
parts: {
first: number,
total: number,
compleated: number
}
}>
}
}

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

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

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

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

View file

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

161
@types/messageHandler.d.ts vendored Normal file
View file

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

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

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

43
@types/newHidiveEpisode.d.ts vendored Normal file
View 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
View 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
View 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
View 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
View 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;
}

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

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

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

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

View file

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

15
@types/randomEvents.d.ts vendored Normal file
View file

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

View file

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

View file

@ -1,15 +1,14 @@
declare module 'sei-helper' {
export async function question(qStr: string): Promise<string>;
export function cleanupFilename(str: string): string;
export function exec(str: string, str1: string, str2: string);
export const cookie: {
parse: (data: Record<string, string>) => Record<string, {
value: string;
expires: Date;
path: string;
domain: string;
secure: boolean;
}>
};
export function formatTime(time: number): string
declare module 'sei-helper' {
export async function question(qStr: string): Promise<string>;
export function cleanupFilename(str: string): string;
export const cookie: {
parse: (data: Record<string, string>) => Record<string, {
value: string;
expires: Date;
path: string;
domain: string;
secure: boolean;
}>
};
export function formatTime(time: number): string
}

3
@types/serviceClassInterface.d.ts vendored Normal file
View file

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

View file

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

View file

@ -1,7 +0,0 @@
export type Subtitle = {
path: string,
ext: string,
langName: string,
language: string,
file?: string
}

View file

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

45
@types/ws.d.ts vendored Normal file
View file

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

39
Dockerfile Normal file
View file

@ -0,0 +1,39 @@
FROM node AS builder
WORKDIR "/app"
COPY . .
# Install 7z for packaging
RUN apt-get update
RUN apt-get install p7zip-full -y
# Update bin-path for docker/linux
RUN echo 'ffmpeg: "./bin/ffmpeg/ffmpeg"\nmkvmerge: "./bin/mkvtoolnix/mkvmerge"' > /app/config/bin-path.yml
#Build AniDL
RUN npm install -g pnpm
RUN pnpm i
RUN pnpm run build-linux-gui
# Move build to new Clean Image
FROM node
WORKDIR "/app"
COPY --from=builder /app/lib/_builds/multi-downloader-nx-linux-x64-gui ./
# Install mkvmerge and ffmpeg
RUN mkdir -p /app/bin/mkvtoolnix
RUN mkdir -p /app/bin/ffmpeg
RUN apt-get update
RUN apt-get install xdg-utils -y
RUN apt-get install mkvtoolnix -y
#RUN apt-get install ffmpeg -y
RUN mv /usr/bin/mkvmerge /app/bin/mkvtoolnix/mkvmerge
#RUN mv /usr/bin/ffmpeg /app/bin/ffmpeg/ffmpeg
CMD [ "/app/aniDL" ]

15
TODO.md Normal file
View file

@ -0,0 +1,15 @@
# Todo/Future Ideas list
- [ ] 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
View 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(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/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
View 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);
});
}
}

View file

@ -1,2 +1,4 @@
ffmpeg: "./bin/ffmpeg/ffmpeg.exe"
mkvmerge: "./bin/mkvtoolnix/mkvmerge.exe"
ffmpeg: "ffmpeg.exe"
mkvmerge: "mkvmerge.exe"
ffprobe: "ffprobe.exe"
mp4decrypt: "mp4decrypt.exe"

View file

@ -1,4 +1,24 @@
videoLayer: 7
nServer: 1
mp4mux: false
noCleanUp: false
# Set the quality of the stream, 0 is highest available.
q: 0
# 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"

View file

@ -1 +1,2 @@
content: ./videos/
fonts: ./fonts/

1
config/gui.yml Normal file
View file

@ -0,0 +1 @@
port: 3000

3726
crunchy.ts

File diff suppressed because it is too large Load diff

21
dev.js Normal file
View file

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

453
docs/DOCUMENTATION.md Normal file
View file

@ -0,0 +1,453 @@
# multi-downloader-nx (5.1.5v)
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 *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.
## CLI Options
### Legend
- `${someText}` shows that you should replace this text with your own
- e.g. `--username ${someText}` -> `--username Izuco`
### Authentication
#### `--auth`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
| --- | --- | --- | --- | --- | ---|
| All | `--auth ` | `boolean` | `No`| `NaN` | `NaN` |
Most of the shows on both services are only accessible if you payed for the service.
In order for them to know who you are you are required to log in.
If you trigger this command, you will be prompted for the username and password for the selected service
#### `--username`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | ---|
| All | `--username ${username}` | `string` | `No`| `NaN` | `undefined`| `username: ` |
Set the username to use for the authentication. If not provided, you will be prompted for the input
#### `--password`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | ---|
| All | `--password ${password}` | `string` | `No`| `NaN` | `undefined`| `password: ` |
Set the password to use for the authentication. If not provided, you will be prompted for the input
#### `--silentAuth`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | ---|
| 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**
| --- | --- | --- | --- | --- | ---|
| Crunchyroll | `--dlFonts ` | `boolean` | `No`| `NaN` | `NaN` |
Crunchyroll uses a variaty of fonts for the subtitles.
Use this command to download all the fonts and add them to the muxed **mkv** file.
#### `--fontName`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
| --- | --- | --- | --- | --- | ---|
| Hidive, AnimationDigitalNetwork | `--fontName ${fontName}` | `string` | `No`| `NaN` | `NaN` |
Set the font to use in subtiles
### Search
#### `--search`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
| --- | --- | --- | --- | --- | ---|
| All | `--search ${search}` | `string` | `No`| `-f` | `NaN` |
Search of an anime by the given string
#### `--search-type`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | --- | ---|
| Crunchyroll | `--search-type ${type}` | `string` | `No`| `NaN` | [`''`, `top_results`, `series`, `movie_listing`, `episode`] | ``| `search-type: ` |
Search only for type of anime listings (e.g. episodes, series)
#### `--page`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
| --- | --- | --- | --- | --- | ---|
| 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
#### `--locale`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | --- | ---|
| 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 local that will be used for the API.
#### `--new`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
| --- | --- | --- | --- | --- | ---|
| Crunchyroll, Hidive | `--new ` | `boolean` | `No`| `NaN` | `NaN` |
Get last updated series list
### Downloading
#### `--movie-listing`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
| --- | --- | --- | --- | --- | ---|
| Crunchyroll | `--movie-listing ${ID}` | `string` | `No`| `--flm` | `NaN` |
Get video list by Movie Listing ID
#### `--series`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
| --- | --- | --- | --- | --- | ---|
| Crunchyroll | `--series ${ID}` | `string` | `No`| `--srz` | `NaN` |
Requested is the ID of a show not a season.
#### `-s`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
| --- | --- | --- | --- | --- | ---|
| All | `-s ${ID}` | `string` | `No`| `NaN` | `NaN` |
Used to set the season ID to download from
#### `-e`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
| --- | --- | --- | --- | --- | ---|
| All | `-e ${selection}` | `string` | `No`| `--episode` | `NaN` |
Set the episode(s) to download from any given show.
For multiple selection: 1-4 OR 1,2,3,4
For special episodes: S1-4 OR S1,S2,S3,S4 where S is the special letter
#### `--extid`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
| --- | --- | --- | --- | --- | ---|
| Crunchyroll | `--extid ${selection}` | `string` | `No`| `--externalid` | `NaN` |
Set the external id to lookup/download.
Allows you to download or view legacy Crunchyroll Ids
#### `-q`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | ---|
| All | `-q ${qualityLevel}` | `number` | `No`| `NaN` | `0`| `q: ` |
Set the quality level. Use 0 to use the maximum quality.
#### `--dlVideoOnce`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | ---|
| 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**
| --- | --- | --- | --- | --- | --- | ---|
| Hidive | `--removeBumpers ` | `boolean` | `No`| `NaN` | `true`| `removeBumpers: ` |
If selected, it will remove the bumpers such as the hidive intro from the final file.
Currently disabling this sometimes results in bugs such as video/audio desync
#### `--originalFontSize`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | ---|
| Hidive | `--originalFontSize ` | `boolean` | `No`| `NaN` | `true`| `originalFontSize: ` |
If selected, it will prefer to keep the original Font Size defined by the service.
#### `-x`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | --- | ---|
| 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`] | `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`, `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`, `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)
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**
| --- | --- | --- | --- | --- | ---|
| All | `--novids ` | `boolean` | `No`| `NaN` | `NaN` |
Skip downloading videos
#### `--noaudio`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
| --- | --- | --- | --- | --- | ---|
| Crunchyroll, Hidive | `--noaudio ` | `boolean` | `No`| `NaN` | `NaN` |
Skip downloading audio
#### `--nosubs`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
| --- | --- | --- | --- | --- | ---|
| All | `--nosubs ` | `boolean` | `No`| `NaN` | `NaN` |
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`, `zh-HK`, `kor`, `cat`, `pol`, `tha`, `tam`, `may`, `vie`, `ind`, `tel`, `jpn`] | `jpn`| `dubLang: ` |
Set the language to download:
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**
| --- | --- | --- | --- | --- | --- | ---|
| All | `--all ` | `boolean` | `No`| `NaN` | `false`| `all: ` |
Used to download all episodes from the show
#### `--fontSize`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | ---|
| All | `--fontSize ${fontSize}` | `number` | `No`| `NaN` | `55`| `fontSize: ` |
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**
| --- | --- | --- | --- | --- | ---|
| All | `--allDubs ` | `boolean` | `No`| `NaN` | `NaN` |
If selected, all available dubs will get downloaded
#### `--timeout`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | ---|
| All | `--timeout ${timeout}` | `number` | `No`| `NaN` | `15000`| `timeout: ` |
Set the timeout of all download reqests. Set in millisecods
#### `--waittime`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | ---|
| Crunchyroll, Hidive | `--waittime ${waittime}` | `number` | `No`| `NaN` | `0`| `waittime: ` |
Set the time the program waits between downloads. Set in millisecods
#### `--simul`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | ---|
| Hidive | `--simul ` | `boolean` | `No`| `NaN` | `false`| `simul: ` |
Force downloading simulcast version instead of uncut version (if available).
#### `--but`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
| --- | --- | --- | --- | --- | ---|
| All | `--but ` | `boolean` | `No`| `NaN` | `NaN` |
Download everything but the -e selection
#### `--downloadArchive`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
| --- | --- | --- | --- | --- | ---|
| All | `--downloadArchive ` | `boolean` | `No`| `NaN` | `NaN` |
Used to download all archived shows
#### `--addArchive`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
| --- | --- | --- | --- | --- | ---|
| All | `--addArchive ` | `boolean` | `No`| `NaN` | `NaN` |
Used to add the `-s` and `--srz` to downloadArchive
#### `--partsize`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | ---|
| All | `--partsize ${amount}` | `number` | `No`| `NaN` | `10`| `partsize: ` |
Set the amount of parts to download at once
If you have a good connection try incresing this number to get a higher overall speed
#### `--fsRetryTime`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | ---|
| All | `--fsRetryTime ${time in seconds}` | `number` | `No`| `NaN` | `5`| `fsRetryTime: ` |
Set the time the downloader waits before retrying if an error while writing the file occurs
#### `--force`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **cli-default Entry**
| --- | --- | --- | --- | --- | --- | ---|
| All | `--force ${option}` | `string` | `No`| `NaN` | [`y`, `Y`, `n`, `N`, `c`, `C`] | `NaN` |
If a file already exists, the tool will ask you how to proceed. With this, you can answer in advance.
### Muxing
#### `--mp4`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | ---|
| All | `--mp4 ` | `boolean` | `No`| `NaN` | `false`| `mp4: ` |
If selected, the output file will be an mp4 file (not recommended tho)
#### `--keepAllVideos`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | ---|
| Crunchyroll, Hidive | `--keepAllVideos ` | `boolean` | `No`| `NaN` | `false`| `keepAllVideos: ` |
If set to true, it will keep all videos in the merge process, rather than discarding the extra videos.
#### `--syncTiming`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | ---|
| Crunchyroll, Hidive | `--syncTiming ` | `boolean` | `No`| `NaN` | `false`| `syncTiming: ` |
If enabled attempts to sync timing for multi-dub downloads.
NOTE: This is currently experimental and syncs audio and subtitles, though subtitles has a lot of guesswork
If you find bugs with this, please report it in the discord or github
#### `--skipmux`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
| --- | --- | --- | --- | --- | ---|
| All | `--skipmux ` | `boolean` | `No`| `NaN` | `NaN` |
Skip muxing video, audio and subtitles
#### `--nocleanup`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | ---|
| All | `--nocleanup ` | `boolean` | `No`| `NaN` | `false`| `nocleanup: ` |
Don't delete subtitle, audio and video files after muxing
#### `--skipSubMux`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | ---|
| All | `--skipSubMux ` | `boolean` | `No`| `NaN` | `false`| `skipSubMux: ` |
Skip muxing the subtitles
#### `--forceMuxer`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | --- | ---|
| All | `--forceMuxer ${muxer}` | `string` | `No`| `NaN` | [`ffmpeg`, `mkvmerge`] | `undefined`| `forceMuxer: ` |
Force the program to use said muxer or don't mux if the given muxer is not present
#### `--videoTitle`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
| --- | --- | --- | --- | --- | ---|
| All | `--videoTitle ${title}` | `string` | `No`| `NaN` | `NaN` |
Set the video track name of the merged file
#### `--mkvmergeOptions`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | ---|
| All | `--mkvmergeOptions ${args}` | `array` | `No`| `NaN` | `--no-date,--disable-track-statistics-tags,--engage no_variable_data`| `mkvmergeOptions: ` |
Set the options given to mkvmerge
#### `--ffmpegOptions`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | ---|
| All | `--ffmpegOptions ${args}` | `array` | `No`| `NaN` | ``| `ffmpegOptions: ` |
Set the options given to ffmpeg
#### `--defaultAudio`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | ---|
| 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, 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, zh-HK, kor, cat, pol, tha, tam, may, vie, ind, tel, jpn
### Filename Template
#### `--fileName`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | ---|
| All | `--fileName ${fileName}` | `string` | `No`| `NaN` | `[${service}] ${showTitle} - S${season}E${episode} [${height}p]`| `fileName: ` |
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', 'seriesTitle', 'season', 'width', 'height', 'service' as variables.
#### `--numbers`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | ---|
| All | `--numbers ${number}` | `number` | `No`| `NaN` | `2`| `numbers: ` |
Set how long a number in the title should be at least.
Set in config: 3; Episode number: 5; Output: 005
Set in config: 2; Episode number: 1; Output: 01
Set in config: 1; Episode number: 20; Output: 20
#### `--override`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | ---|
| All | `--override "${toOverride}='${value}'"` | `array` | `No`| `NaN` | ``| `override: ` |
Override a template variable
#### `--ccTag`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | ---|
| All | `--ccTag ${tag}` | `string` | `No`| `NaN` | `cc`| `ccTag: ` |
Used to set the name for subtitles that contain tranlations for none verbal communication (e.g. signs)
### Debug
#### `--nosess`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | ---|
| All | `--nosess ` | `boolean` | `No`| `NaN` | `false`| `nosess: ` |
Reset session cookie for testing purposes
#### `--debug`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | ---|
| All | `--debug ` | `boolean` | `No`| `NaN` | `false`| `debug: ` |
Debug mode (tokens may be revealed in the console output)
### Utilities
#### `--service`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | --- | ---|
| All | `--service ${service}` | `string` | `Yes`| `NaN` | [`crunchy`, `hidive`, `ao`, `adn`] | ``| `service: ` |
Set the service you want to use
#### `--update`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
| --- | --- | --- | --- | --- | ---|
| All | `--update ` | `boolean` | `No`| `NaN` | `NaN` |
Force the tool to check for updates (code version only)
#### `--skipUpdate`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
| --- | --- | --- | --- | --- | --- | ---|
| All | `--skipUpdate ` | `boolean` | `No`| `NaN` | `false`| `skipUpdate: ` |
If true, the tool won't check for updates
### Help
#### `--help`
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
| --- | --- | --- | --- | --- | ---|
| All | `--help ` | `boolean` | `No`| `-h` | `NaN` |
Show the help output
### GUI

View file

@ -1,88 +1,107 @@
# Anime Downloader NX by AniDL
This downloader can download anime from diffrent sites. Currently supported are *Funimation* and *Crunchyroll*.
[![Discord Shield](https://discord.com/api/guilds/884479461997805568/widget.png?style=banner2)](https://discord.gg/qEpbWen5vq)
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* or *Crunchyroll*. 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.
## Prerequisites
## Dependencies
* NodeJS >= 12.4.0 (https://nodejs.org/)
* NPM >= 6.9.0 (https://www.npmjs.org/)
* ffmpeg >= 4.0.0 (https://www.videohelp.com/software/ffmpeg)
* MKVToolNix >= 20.0.0 (https://www.videohelp.com/software/MKVToolNix)
* MKVToolNix >= 60.0.0 (https://www.videohelp.com/software/MKVToolNix)
### Paths Configuration
By default this application uses the following paths to programs (main executables):
* `./bin/ffmpeg/ffmpeg.exe`
* `./bin/mkvtoolnix/mkvmerge.exe`
* `ffmpeg.exe` (From PATH)
* `ffprobe.exe` (From PATH)
* `mkvmerge.exe` (From PATH)
* `mp4decrypt.exe` (From PATH)
To change these paths you need to edit `bin-path.yml` in `./config/` directory.
### Node Modules
## CLI Information
After installing NodeJS with NPM go to directory with `package.json` file and type: `npm i`. Afterwards run `npm run tsc`. You can now find a lib folder containing the js code execute.
* [check dependencies](https://david-dm.org/anidl/funimation-downloader-nx)
See [the documentation](https://github.com/anidl/multi-downloader-nx/blob/master/docs/DOCUMENTATION.md) for a complete list of what options are available. You can define defaults for the commands by editing the `cli-defaults.yml` file in the `./config/` directory.
## CLI Options
### Example usage
### Authentication
#### Logging in
* `--service ['funi', 'crunchy']` Set the service you want to use
* `--auth` enter auth mode
Most services require you to be logged in, in order to download from, an example of how you would login is:
### Get Show ID
```shell
AniDL --service {ServiceName} --auth
```
* `-f`, `--search <s>` sets the show title for search
#### Searching
### Download Video
In order to find the IDs to download, you can search from each service by using the `--search` flag like this:
* `-s <i> -e <s>` sets the show id and episode ids (comma-separated, hyphen-sequence)
* `-q <i>` sets the video layer quality [1...10] (optional, 0 is max)
* `--all` download all videos at once
* `--alt` alternative episode listing (if available)
* `--subLang` select one or more subtile language
* `--allSubs` If set to true, all available subs will get downloaded
* `--dub` select one or more dub languages
* `--allDubs` If set to true, all available dubs will get downloaded
* `--simul` force select simulcast version instead of uncut version
* `-x`, `--server` select server
* `--novids` skip download videos
* `--nosubs` skip download subtitles for Dub (if available)
* `--noaudio` skip downloading audio
```shell
AniDL --service {ServiceName} --search {SearchTerm}
```
### Proxy
#### Downloading
The proxy is currently unmainted. Use at your on risk.
Once you have the ID which you can obtain from using the search or other means, you are ready to download, which you can do like this:
* `--proxy <s>` http(s)/socks proxy WHATWG url (ex. https://myproxyhost:1080)
* `--proxy-auth <s>` Colon-separated username and password for proxy
* `--ssp` don't use proxy for stream downloading
```shell
AniDL --service {ServiceName} -s {SeasonID} -e {EpisodeNumber}
```
### Muxing
## Building and running from source
`[note] this application mux into mkv by default`
* `--mp4` mux into mp4
* `--mks` add subtitles to mkv or mp4 (if available)
### Build Dependencies
### Filenaming (optional)
Dependencies that are only required for running from code. These are not required if you are using the prebuilt binaries.
* `--fileName` Set the filename template. Use ${variable_name} to insert variables.
You may use 'title', 'episode', 'showTitle', 'season', 'width', 'height' as variables.
* NodeJS >= 18.0.0 (https://nodejs.org/)
* NPM >= 6.9.0 (https://www.npmjs.org/)
* PNPM >= 7.0.0 (https://pnpm.io/)
### Utility
### Build Setup
* `--nocleanup` don't delete the input files after the muxing
* `-h`, `--help` show all options
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)
## Filename Template
First clone this repo `git clone https://github.com/anidl/multi-downloader-nx.git`.
[${service}] ${showTitle} - ${episode} [${height}p]"]
`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.
## CLI Help
### Run from TypeScript
If you need help with the cli run `node index.js --help` or `aniDL[.exe] --help` .
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`
If you still don't get it please open up an issue with the CLI template.
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.
## DRM Decryption
### Decryption Requirements
* mp4decrypt >= Any (http://www.bento4.com/) - Only required for decrypting
### Instructions
In order to decrypt DRM content, you will need to have a dumped CDM, after that you will need to place the CDM files (`device_client_id_blob` and `device_private_key`) into the `./widevine/` directory. For legal reasons we do not include the CDM with the software, and you will have to source one yourself.

64
eslint.config.mjs Normal file
View 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
}
}
);

783
funi.ts
View file

@ -1,783 +0,0 @@
// modules build-in
import fs from 'fs';
import path from 'path';
// package json
import packageJson from './package.json';
// program name
const api_host = 'https://prod-api-funimationnow.dadcdigital.com/api';
// modules extra
import * as shlp from 'sei-helper';
import m3u8 from 'm3u8-parsed';
import hlsDownload from '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 { Item } from './@types/items';
// params
const cfg = yamlCfg.loadCfg();
const token = yamlCfg.loadFuniToken();
// cli
const argv = appYargs.appArgv(cfg.cli);
// Import modules after argv has been exported
import getData from './modules/module.getdata.js';
import merger, { SubtitleInput } from './modules/module.merger';
import parseSelect from './modules/module.parseSelect';
import { EpisodeData, MediaChild } from './@types/episode';
import { Subtitle } from './@types/subtitleObject';
import { StreamData } from './@types/streamData';
import { DownloadedFile } from './@types/downloadedFile';
import parseFileName, { Variable } from './modules/module.filename';
// check page
argv.p = 1;
// fn variables
let title = '',
showTitle = '',
fnEpNum: string|number = 0,
fnOutput: string[] = [],
season = 0,
tsDlPath: {
path: string,
lang: string,
}[] = [],
stDlPath: Subtitle[] = [];
// main
export default (async () => {
// load binaries
console.log(`\n=== Multi Downloader NX ${packageJson.version} ===\n`);
cfg.bin = await yamlCfg.loadBinCfg();
if (argv.allDubs) {
argv.dub = appYargs.dubLang;
}
if (argv.allSubs) {
argv.subLang = appYargs.subLang;
}
// select mode
if(argv.auth){
auth();
}
else if(argv.search){
searchShow();
}
else if(argv.s && !isNaN(parseInt(argv.s)) && parseInt(argv.s) > 0){
getShow();
}
else{
appYargs.showHelp();
}
});
// auth
async function auth(){
const authOpts = {
user: await shlp.question('[Q] LOGIN/EMAIL'),
pass: await shlp.question('[Q] PASSWORD ')
};
const authData = await getData({
baseUrl: api_host,
url: '/auth/login/',
auth: authOpts,
debug: argv.debug,
});
if(authData.ok && authData.res){
const resJSON = JSON.parse(authData.res.body);
if(resJSON.token){
console.log('[INFO] Authentication success, your token: %s%s\n', resJSON.token.slice(0,8),'*'.repeat(32));
yamlCfg.saveFuniToken({'token': resJSON.token});
} else {
console.log('[ERROR]%s\n', ' No token found');
if (argv.debug) {
console.log(resJSON);
}
process.exit(1);
}
}
}
// search show
async function searchShow(){
const qs = {unique: true, limit: 100, q: argv.search, offset: 0 };
const searchData = await getData({
baseUrl: api_host,
url: '/source/funimation/search/auto/',
querystring: qs,
token: token,
useToken: true,
debug: argv.debug,
});
if(!searchData.ok || !searchData.res){return;}
const searchDataJSON = JSON.parse(searchData.res.body);
if(searchDataJSON.detail){
console.log(`[ERROR] ${searchDataJSON.detail}`);
return;
}
if(searchDataJSON.items && searchDataJSON.items.hits){
const shows = searchDataJSON.items.hits;
console.log('[INFO] Search Results:');
for(const ssn in shows){
console.log(`[#${shows[ssn].id}] ${shows[ssn].title}` + (shows[ssn].tx_date?` (${shows[ssn].tx_date})`:''));
}
}
console.log('[INFO] Total shows found: %s\n',searchDataJSON.count);
}
// get show
async function getShow(){
// show main data
const showData = await getData({
baseUrl: api_host,
url: `/source/catalog/title/${argv.s}`,
token: token,
useToken: true,
debug: argv.debug,
});
// check errors
if(!showData.ok || !showData.res){return;}
const showDataJSON = JSON.parse(showData.res.body);
if(showDataJSON.status){
console.log('[ERROR] Error #%d: %s\n', showDataJSON.status, showDataJSON.data.errors[0].detail);
process.exit(1);
}
else if(!showDataJSON.items || showDataJSON.items.length<1){
console.log('[ERROR] Show not found\n');
process.exit(0);
}
const showDataItem = showDataJSON.items[0];
console.log('[#%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: parseInt(argv.s as string) };
if(argv.alt){ qs.language = 'English'; }
const episodesData = await getData({
baseUrl: api_host,
url: '/funimation/episodes/',
querystring: qs,
token: token,
useToken: true,
debug: argv.debug,
});
if(!episodesData.ok || !episodesData.res){return;}
let epsDataArr: Item[] = JSON.parse(episodesData.res.body).items;
const epNumRegex = /^([A-Z0-9]*[A-Z])?(\d+)$/i;
const epSelEpsTxt = []; let typeIdLen = 0, epIdLen = 4;
const parseEpStr = (epStr: string) => {
const match = epStr.match(epNumRegex);
if (!match) {
console.error('[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);
epIdLen = epMatch[1].length > epIdLen ? epMatch[1].length : epIdLen;
typeIdLen = epMatch[0].length > typeIdLen ? epMatch[0].length : typeIdLen;
e.id_split = epMatch;
}
else{
typeIdLen = 3 > typeIdLen? 3 : typeIdLen;
console.log('[ERROR] FAILED TO PARSE: ', e.id);
e.id_split = [ 'ZZZ', 9999 ];
}
return e;
});
const epSelList = parseSelect(argv.e as string);
const fnSlug: {
title: string,
episode: string
}[] = []; let is_selected = false;
const eps = epsDataArr;
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;
});
for(const e in eps){
eps[e].id_split[1] = parseInt(eps[e].id_split[1].toString()).toString().padStart(epIdLen, '0');
let epStrId = eps[e].id_split.join('');
// select
is_selected = false;
if (argv.all || epSelList.isSelected(epStrId)) {
fnSlug.push({title:eps[e].item.titleSlug,episode:eps[e].item.episodeSlug});
epSelEpsTxt.push(epStrId);
is_selected = true;
}
else{
is_selected = false;
}
// 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(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.log(conOut);
}
if(fnSlug.length < 1){
console.log('[INFO] Episodes not selected!\n');
process.exit();
}
else{
console.log('[INFO] Selected Episodes: %s\n',epSelEpsTxt.join(', '));
for(let fnEp=0;fnEp<fnSlug.length;fnEp++){
await getEpisode(fnSlug[fnEp]);
}
}
}
async function getEpisode(fnSlug: {
title: string,
episode: string
}) {
const episodeData = await getData({
baseUrl: api_host,
url: `/source/catalog/episode/${fnSlug.title}/${fnSlug.episode}/`,
token: token,
useToken: true,
debug: argv.debug,
});
if(!episodeData.ok || !episodeData.res){return;}
const ep = JSON.parse(episodeData.res.body).items[0] as EpisodeData, streamIds = [];
// build fn
showTitle = ep.parent.title;
title = ep.title;
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
console.log(
'[INFO] %s - S%sE%s - %s',
ep.parent.title,
(ep.parent.seasonNumber ? ep.parent.seasonNumber : '?'),
(ep.number ? ep.number : '?'),
ep.title
);
console.log('[INFO] Available streams (Non-Encrypted):');
// map medias
const media = ep.media.map(function(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: getSubsUrl(m.mediaChildren),
};
}
else{
return { id: 0, type: '' };
}
});
const dubType = {
'enUS': 'English',
'esLA': 'Spanish (Latin Am)',
'ptBR': 'Portuguese (Brazil)',
'zhMN': 'Chinese (Mandarin, PRC)',
'jaJP': 'Japanese'
};
// 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 = !argv.simul && uncut[dub_type] && m.version?.match(/uncut/i)
? true
: (!uncut[dub_type] || argv.simul && m.version?.match(/simulcast/i) ? true : false);
for (const curDub of (argv.dub as appYargs.possibleDubs)) {
if(dub_type == dubType[curDub] && selUncut){
streamIds.push({
id: m.id,
lang: merger.getLanguageCode(curDub, curDub.slice(0, -2))
});
stDlPath.push(...m.subtitles);
localSubs = m.subtitles;
selected = true;
}
}
console.log(`[#${m.id}] ${dub_type} [${m.version}]${(selected?' (selected)':'')}${
localSubs && localSubs.length > 0 && selected ? ` (using ${localSubs.map(a => `'${a.langName}'`).join(', ')} for subtitles)` : ''
}`);
}
}
const already: string[] = [];
stDlPath = stDlPath.filter(a => {
if (already.includes(a.language)) {
return false;
} else {
already.push(a.language);
return true;
}
});
if(streamIds.length < 1){
console.log('[ERROR] Track not selected\n');
return;
}
else{
tsDlPath = [];
for (const streamId of streamIds) {
const streamData = await getData({
baseUrl: api_host,
url: `/source/catalog/video/${streamId.id}/signed`,
token: token,
dinstid: 'uuid',
useToken: true,
debug: argv.debug,
});
if(!streamData.ok || !streamData.res){return;}
const streamDataRes = JSON.parse(streamData.res.body) as StreamData;
if(streamDataRes.errors){
console.log('[ERROR] Error #%s: %s\n',streamDataRes.errors[0].code,streamDataRes.errors[0].detail);
return;
}
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){
console.log('[ERROR] Unknown error\n');
return;
}
else{
await downloadStreams();
}
}
}
function getSubsUrl(m: MediaChild[]) : Subtitle[] {
if(argv.nosubs && !argv.sub){
return [];
}
let subLangs = argv.subLang as appYargs.possibleSubs;
const subType = {
'enUS': 'English',
'esLA': 'Spanish (Latin Am)',
'ptBR': 'Portuguese (Brazil)'
};
const subLangAvailable = m.some(a => subLangs.some(subLang => a.ext == 'vtt' && a.language === subType[subLang]));
if (!subLangAvailable) {
subLangs = [ 'enUS' ];
}
const found: Subtitle[] = [];
for(const i in m){
const fpp = m[i].filePath.split('.');
const fpe = fpp[fpp.length-1];
for (const lang of subLangs) {
if(fpe == 'vtt' && m[i].language === subType[lang]) {
found.push({
path: m[i].filePath,
ext: `.${lang}`,
langName: subType[lang],
language: m[i].languages[0].code || lang.slice(0, 2)
});
}
}
}
return found;
}
async function downloadStreams(){
// req playlist
const purvideo: DownloadedFile[] = [];
const puraudio: DownloadedFile[] = [];
const audioAndVideo: DownloadedFile[] = [];
for (const streamPath of tsDlPath) {
const plQualityReq = await getData({
url: streamPath.path,
debug: argv.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 = [],
plLayersRes: Record<string|number, {
width: number,
height: number
}> = {};
let plMaxLayer = 1,
plNewIds = 1,
plAud: {
uri: string,
langStr: string,
language: string
} = { uri: '', langStr: '', language: '' };
// 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.log('[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]];
plAud = { ...audioData, ...{ langStr: audioEl[0] } };
}
plQualityLinkList.playlists.sort((a, b) => {
const aMatch = a.uri.match(vplReg), bMatch = b.uri.match(vplReg);
if (!aMatch || !bMatch) {
console.log('[ERROR] 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.log(`[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.log(s.uri);
}
}
for(const s of mainServersList){
if(plServerList.includes(s)){
plServerList.splice(plServerList.indexOf(s), 1);
plServerList.unshift(s);
break;
}
}
argv.q = argv.q < 1 || argv.q > plMaxLayer ? plMaxLayer : argv.q;
const plSelectedServer = plServerList[argv.x-1];
const plSelectedList = plStreams[plSelectedServer];
const videoUrl = argv.x < plServerList.length+1 && plSelectedList[argv.q] ? plSelectedList[argv.q] : '';
plLayersStr.sort();
console.log(`[INFO] Servers available:\n\t${plServerList.join('\n\t')}`);
console.log(`[INFO] Available qualities:\n\t${plLayersStr.join('\n\t')}`);
if(videoUrl != ''){
console.log(`[INFO] Selected layer: ${argv.q} (${plLayersRes[argv.q].width}x${plLayersRes[argv.q].height}) @ ${plSelectedServer}`);
console.log('[INFO] Stream URL:',videoUrl);
fnOutput = parseFileName(argv.fileName, ([
['episode', fnEpNum],
['title', title],
['showTitle', showTitle],
['season', season],
['width', plLayersRes[argv.q].width],
['height', plLayersRes[argv.q].height],
['service', 'Funimation']
] as [appYargs.AvailableFilenameVars, string|number][]).map((a): Variable => {
return {
name: a[0],
replaceWith: a[1],
type: typeof a[1],
} as Variable;
}), argv.numbers);
if (fnOutput.length < 1)
throw new Error(`Invalid path generated for input ${argv.fileName}`);
console.log(`[INFO] Output filename: ${fnOutput.join(path.sep)}.ts`);
}
else if(argv.x > plServerList.length){
console.log('[ERROR] Server not selected!\n');
return;
}
else{
console.log('[ERROR] Layer not selected!\n');
return;
}
let dlFailed = false;
let dlFailedA = false;
await fs.promises.mkdir(path.join(cfg.dir.content, ...fnOutput.slice(0, -1)), { recursive: true });
video: if (!argv.novids) {
if (plAud.uri && (purvideo.length > 0 || audioAndVideo.length > 0)) {
break video;
} else if (!plAud.uri && (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: argv.debug,
});
if (!reqVideo.ok || !reqVideo.res) { break video; }
const chunkList = m3u8(reqVideo.res.body);
const tsFile = path.join(cfg.dir.content, ...fnOutput.slice(0, -1), `${fnOutput.slice(-1)}.video${(plAud.uri ? '' : '.' + streamPath.lang )}`);
dlFailed = !await downloadFile(tsFile, chunkList);
if (!dlFailed) {
if (plAud.uri) {
purvideo.push({
path: `${tsFile}.ts`,
lang: plAud.language
});
} else {
audioAndVideo.push({
path: `${tsFile}.ts`,
lang: streamPath.lang
});
}
}
}
else{
console.log('[INFO] Skip video downloading...\n');
}
audio: if (!argv.noaudio && plAud.uri) {
// 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: argv.debug,
});
if (!reqAudio.ok || !reqAudio.res) { return; }
const chunkListA = m3u8(reqAudio.res.body);
const tsFileA = path.join(cfg.dir.content, ...fnOutput.slice(0, -1), `${fnOutput.slice(-1)}.audio.${plAud.language}`);
dlFailedA = !await downloadFile(tsFileA, chunkListA);
if (!dlFailedA)
puraudio.push({
path: `${tsFileA}.ts`,
lang: plAud.language
});
}
}
// add subs
const subsExt = !argv.mp4 || argv.mp4 && argv.ass ? '.ass' : '.srt';
let addSubs = true;
// download subtitles
if(stDlPath.length > 0){
console.log('[INFO] Downloading subtitles...');
for (const subObject of stDlPath) {
const subsSrc = await getData({
url: subObject.path,
debug: argv.debug,
});
if(subsSrc.ok && subsSrc.res){
const assData = vttConvert(subsSrc.res.body, (subsExt == '.srt' ? true : false), subObject.langName, argv.fontSize, argv.fontName);
subObject.file = path.join(cfg.dir.content, ...fnOutput.slice(0, -1), `${fnOutput.slice(-1)}.subtitle${subObject.ext}${subsExt}`);
fs.writeFileSync(subObject.file, assData);
}
else{
console.log('[ERROR] Failed to download subtitles!');
addSubs = false;
break;
}
}
if (addSubs)
console.log('[INFO] Subtitles downloaded!');
}
if((puraudio.length < 1 && audioAndVideo.length < 1) || (purvideo.length < 1 && audioAndVideo.length < 1)){
console.log('\n[INFO] Unable to locate a video AND audio file\n');
return;
}
if(argv.skipmux){
console.log('[INFO] Skipping muxing...');
return;
}
// check exec
const mergerBin = merger.checkMerger(cfg.bin, argv.mp4);
if ( argv.novids ){
console.log('[INFO] Video not downloaded. Skip muxing video.');
}
// mergers
if(!argv.mp4 && !mergerBin.MKVmerge){
console.log('[WARN] MKVMerge not found...');
}
if(!mergerBin.MKVmerge && !mergerBin.FFmpeg || argv.mp4 && !mergerBin.MKVmerge){
console.log('[WARN] FFmpeg not found...');
}
const ffext = !argv.mp4 ? 'mkv' : 'mp4';
const mergeInstance = new merger({
onlyAudio: puraudio,
onlyVid: purvideo,
output: `${path.join(cfg.dir.content, ...fnOutput)}.${ffext}`,
subtitels: stDlPath as SubtitleInput[],
videoAndAudio: audioAndVideo,
simul: argv.simul
});
if(!argv.mp4 && mergerBin.MKVmerge){
const command = mergeInstance.MkvMerge();
shlp.exec('mkvmerge', `"${mergerBin.MKVmerge}"`, command);
}
else if(mergerBin.FFmpeg){
const command = mergeInstance.FFmpeg();
shlp.exec('ffmpeg',`"${mergerBin.FFmpeg}"`,command);
}
else{
console.log('\n[INFO] Done!\n');
return;
}
if (argv.nocleanup)
return;
audioAndVideo.concat(puraudio).concat(purvideo).forEach(a => fs.unlinkSync(a.path));
stDlPath.forEach(subObject => subObject.file && fs.unlinkSync(subObject.file));
console.log('\n[INFO] Done!\n');
}
async function downloadFile(filename: string, chunkList: {
segments: Record<string, unknown>[],
}) {
const downloadStatus = await new hlsDownload({
m3u8json: chunkList,
output: `${filename + '.ts'}`,
timeout: argv.timeout,
threads: argv.partsize
}).download();
return downloadStatus.ok;
}

3
gui.ts Normal file
View file

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

3
gui/react/.babelrc Normal file
View file

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

2
gui/react/.env Normal file
View file

@ -0,0 +1,2 @@
PORT=3002
CI=false

57
gui/react/package.json Normal file
View file

@ -0,0 +1,57 @@
{
"name": "anidl-gui",
"version": "1.0.0",
"private": true,
"dependencies": {
"@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.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": {
"build": "npx tsc && npx webpack",
"start": "npx concurrently -k npm:frontend npm:backend",
"frontend": "npx webpack-dev-server",
"backend": "npx ts-node -T ../../gui.ts"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

5482
gui/react/pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

3
gui/react/src/@types/FC.d.ts vendored Normal file
View file

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

10
gui/react/src/App.tsx Normal file
View file

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

38
gui/react/src/Layout.tsx Normal file
View file

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

19
gui/react/src/Style.tsx Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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