├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug.yml │ └── feature.yml ├── dependabot.yml └── workflows │ ├── auto-documentation.yml │ ├── docker.yml │ ├── release-matrix.yml │ └── test.yml ├── .gitignore ├── .prettierrc ├── @types ├── adnPlayerConfig.d.ts ├── adnSearch.d.ts ├── adnStreams.d.ts ├── adnSubtitles.d.ts ├── adnVideos.d.ts ├── animeOnegaiSearch.d.ts ├── animeOnegaiSeasons.d.ts ├── animeOnegaiSeries.d.ts ├── animeOnegaiStream.d.ts ├── crunchyAndroidEpisodes.d.ts ├── crunchyAndroidObject.d.ts ├── crunchyAndroidStreams.d.ts ├── crunchyChapters.d.ts ├── crunchyEpisodeList.d.ts ├── crunchyPlayStreams.d.ts ├── crunchySearch.d.ts ├── crunchyTypes.d.ts ├── downloadedFile.d.ts ├── enums.ts ├── episode.d.ts ├── github.d.ts ├── hidiveDashboard.d.ts ├── hidiveEpisodeList.d.ts ├── hidiveSearch.d.ts ├── hidiveTypes.d.ts ├── iso639.d.ts ├── items.d.ts ├── m3u8-parsed.d.ts ├── messageHandler.d.ts ├── mpd-parser.d.ts ├── newHidiveEpisode.d.ts ├── newHidivePlayback.d.ts ├── newHidiveSearch.d.ts ├── newHidiveSeason.d.ts ├── newHidiveSeries.d.ts ├── objectInfo.d.ts ├── pkg.d.ts ├── playbackData.d.ts ├── randomEvents.d.ts ├── removeNPMAbsolutePaths.d.ts ├── serviceClassInterface.d.ts ├── streamData.d.ts ├── updateFile.d.ts └── ws.d.ts ├── Dockerfile ├── LICENSE.md ├── TODO.md ├── adn.ts ├── ao.ts ├── config ├── bin-path.yml ├── cli-defaults.yml ├── dir-path.yml └── gui.yml ├── crunchy.ts ├── dev.js ├── docs ├── CHANGELOG.md ├── DOCUMENTATION.md └── README.md ├── eslint.config.mjs ├── gui.ts ├── gui ├── react │ ├── .babelrc │ ├── .env │ ├── package.json │ ├── pnpm-lock.yaml │ ├── public │ │ ├── favicon.webp │ │ ├── index.html │ │ └── notFound.png │ ├── src │ │ ├── @types │ │ │ └── FC.d.ts │ │ ├── App.tsx │ │ ├── Layout.tsx │ │ ├── Style.tsx │ │ ├── components │ │ │ ├── AddToQueue │ │ │ │ ├── AddToQueue.tsx │ │ │ │ ├── DownloadSelector │ │ │ │ │ ├── DownloadSelector.tsx │ │ │ │ │ └── Listing │ │ │ │ │ │ └── EpisodeListing.tsx │ │ │ │ └── SearchBox │ │ │ │ │ ├── SearchBox.css │ │ │ │ │ └── SearchBox.tsx │ │ │ ├── AuthButton.tsx │ │ │ ├── LogoutButton.tsx │ │ │ ├── MainFrame │ │ │ │ ├── DownloadManager │ │ │ │ │ └── DownloadManager.tsx │ │ │ │ ├── MainFrame.tsx │ │ │ │ └── Queue │ │ │ │ │ └── Queue.tsx │ │ │ ├── MenuBar │ │ │ │ └── MenuBar.tsx │ │ │ ├── Require.tsx │ │ │ ├── StartQueue.tsx │ │ │ └── reusable │ │ │ │ ├── ContextMenu.tsx │ │ │ │ ├── LinearProgressWithLabel.tsx │ │ │ │ └── MultiSelect.tsx │ │ ├── hooks │ │ │ └── useStore.tsx │ │ ├── index.tsx │ │ └── provider │ │ │ ├── ErrorHandler.tsx │ │ │ ├── MessageChannel.tsx │ │ │ ├── QueueProvider.tsx │ │ │ ├── ServiceProvider.tsx │ │ │ └── Store.tsx │ ├── tsconfig.json │ └── webpack.config.ts └── server │ ├── index.ts │ ├── serviceHandler.ts │ ├── services │ ├── adn.ts │ ├── animeonegai.ts │ ├── base.ts │ ├── crunchyroll.ts │ └── hidive.ts │ └── websocket.ts ├── hidive.ts ├── index.ts ├── modules ├── NotoSans-Regular.ttf ├── build-docs.ts ├── build.ts ├── cdm.ts ├── cmd-here.bat ├── hls-download-got.ts ├── hls-download.ts ├── log.ts ├── module.api-urls.ts ├── module.app-args.ts ├── module.args.ts ├── module.cfg-loader.ts ├── module.colors.json ├── module.cookieFile.ts ├── module.downloadArchive.ts ├── module.fetch.ts ├── module.filename.ts ├── module.fontsData.ts ├── module.helper.ts ├── module.langsData.ts ├── module.merger.ts ├── module.parseSelect.ts ├── module.transform-mpd.ts ├── module.updater.ts ├── module.vtt2ass.ts ├── module.vttconvert.ts ├── playready │ ├── bcert.ts │ ├── cdm.ts │ ├── device.ts │ ├── ecc_key.ts │ ├── elgamal.ts │ ├── key.ts │ ├── pssh.ts │ ├── wrmheader.ts │ ├── xml_key.ts │ └── xmrlicense.ts └── widevine │ ├── cmac.ts │ ├── license.ts │ └── license_protocol_pb3.ts ├── package.json ├── playready └── .gitkeep ├── pnpm-lock.yaml ├── tsc.ts ├── tsconfig.json ├── videos └── .gitkeep └── widevine └── .gitkeep /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug 2 | description: File a bug report 3 | assignees: 4 | - AnimeDL 5 | - AnidlSupport 6 | labels: 7 | - bug 8 | title: "[BUG]: " 9 | body: 10 | - type: markdown 11 | attributes: 12 | value: | 13 | Thank you for reporting the issues you found and have with this program. 14 | This template will guide you through all the information we need. 15 | - type: input 16 | id: version 17 | attributes: 18 | label: Program version 19 | description: "Which version of the program do you use?" 20 | placeholder: "1.0.0" 21 | validations: 22 | required: true 23 | - type: dropdown 24 | id: opsystem 25 | attributes: 26 | label: Operating System 27 | description: "Please tell us what OS you are using." 28 | options: 29 | - Windows 30 | - Linux 31 | - MacOS 32 | validations: 33 | required: true 34 | - type: dropdown 35 | id: gui 36 | attributes: 37 | label: Type 38 | description: "Please tell us if you are using the gui or the cli version." 39 | options: 40 | - CLI 41 | - GUI 42 | validations: 43 | required: true 44 | - type: dropdown 45 | id: service 46 | attributes: 47 | label: Service 48 | description: "Please tell us what service the bug occured in." 49 | options: 50 | - Crunchyroll 51 | - Hidive 52 | - AnimationDigitalNetwork 53 | - AnimeOnegai 54 | - All 55 | - Irrelevant 56 | validations: 57 | required: true 58 | - type: input 59 | id: command 60 | attributes: 61 | label: Command used 62 | description: "Please tell us what command you used." 63 | validations: 64 | required: true 65 | - type: input 66 | id: ShowID 67 | attributes: 68 | label: Show ID 69 | description: "Please provide the ID of an example show." 70 | placeholder: "1234" 71 | validations: 72 | required: true 73 | - type: input 74 | id: Episode 75 | attributes: 76 | label: Episode 77 | description: "Please provide the episode ID you used as an example." 78 | placeholder: "1" 79 | validations: 80 | required: true 81 | - type: textarea 82 | id: output 83 | attributes: 84 | label: Console Output 85 | 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." 86 | render: Shell 87 | validations: 88 | required: true 89 | - type: textarea 90 | id: additionalInfos 91 | attributes: 92 | label: Additional Information 93 | description: "Do you have any additional information you can provide?" 94 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yml: -------------------------------------------------------------------------------- 1 | name: Enhancement 2 | description: Suggest a enhancement or feature 3 | labels: 4 | - enhancement 5 | title: "[Feedback]: " 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thank you for giving feedback with this program. 11 | This template will guide you through all the information we need. 12 | - type: dropdown 13 | id: programversion 14 | attributes: 15 | label: Type 16 | description: "Is this suggestion for the CLI, GUI, or Both?" 17 | options: 18 | - CLI 19 | - GUI 20 | - Both 21 | validations: 22 | required: true 23 | - type: textarea 24 | id: suggestion 25 | attributes: 26 | label: Suggestion 27 | description: "What is your suggestion?" 28 | validations: 29 | required: true 30 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/auto-documentation.yml: -------------------------------------------------------------------------------- 1 | name: auto-documentation 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | documentation: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | with: 14 | ref: ${{ github.head_ref }} 15 | - uses: pnpm/action-setup@v2 16 | with: 17 | version: latest 18 | - name: Use Node.js 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 20 22 | - run: pnpm i 23 | - run: pnpm run docs 24 | - uses: stefanzweifel/git-auto-commit-action@v4 25 | with: 26 | commit_message: ${{ github.event.head_commit.message }} + Documentation -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Node project with Docker 2 | 3 | name: build and push docker image 4 | 5 | on: 6 | push: 7 | branches: [ master ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build-node: 12 | 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up Docker Buildx 17 | id: buildx 18 | uses: docker/setup-buildx-action@v1 19 | - name: Login to DockerHub 20 | if: ${{ github.ref == 'refs/heads/master' }} 21 | uses: docker/login-action@v1 22 | with: 23 | username: ${{ secrets.DOCKERHUB_USERNAME }} 24 | password: ${{ secrets.DOCKERHUB_TOKEN }} 25 | - name: Build and push Docker images 26 | uses: docker/build-push-action@v2.9.0 27 | with: 28 | github-token: ${{ github.token }} 29 | push: ${{ github.ref == 'refs/heads/master' }} 30 | tags: | 31 | "multidl/multi-downloader-nx:latest" 32 | - name: Image digest 33 | run: echo ${{ steps.docker_build.outputs.digest }} 34 | -------------------------------------------------------------------------------- /.github/workflows/release-matrix.yml: -------------------------------------------------------------------------------- 1 | name: Release Builds 2 | 3 | on: 4 | release: 5 | types: [ published ] 6 | 7 | jobs: 8 | build: 9 | strategy: 10 | matrix: 11 | build_type: [ linux, macos, windows ] 12 | build_arch: [ x64 ] 13 | gui: [ gui, cli ] 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | - uses: pnpm/action-setup@v2 19 | with: 20 | version: latest 21 | - name: Set up Node.js 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 20 25 | check-latest: true 26 | - name: Install Node modules 27 | run: | 28 | pnpm install 29 | - name: Get name and version from package.json 30 | run: | 31 | test -n $(node -p -e "require('./package.json').name") && 32 | test -n $(node -p -e "require('./package.json').version") && 33 | echo PACKAGE_NAME=$(node -p -e "require('./package.json').name") >> $GITHUB_ENV && 34 | echo PACKAGE_VERSION=$(node -p -e "require('./package.json').version") >> $GITHUB_ENV || exit 1 35 | - name: Make build 36 | run: pnpm run build-${{ matrix.build_type }}-${{ matrix.gui }} 37 | - name: Upload release 38 | uses: actions/upload-release-asset@v1 39 | with: 40 | upload_url: ${{ github.event.release.upload_url }} 41 | asset_name: multi-downloader-nx-${{ matrix.build_type }}-${{ matrix.gui }}.7z 42 | asset_path: ./lib/_builds/multi-downloader-nx-${{ matrix.build_type }}-${{ matrix.build_arch }}-${{ matrix.gui }}.7z 43 | asset_content_type: application/x-7z-compressed 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | build-docker: 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v2 50 | - name: Set up Docker Buildx 51 | id: buildx 52 | uses: docker/setup-buildx-action@v1 53 | - name: Login to DockerHub 54 | uses: docker/login-action@v1 55 | with: 56 | username: ${{ secrets.DOCKERHUB_USERNAME }} 57 | password: ${{ secrets.DOCKERHUB_TOKEN }} 58 | - name: Build and push Docker images 59 | uses: docker/build-push-action@v2.9.0 60 | with: 61 | github-token: ${{ github.token }} 62 | push: true 63 | tags: | 64 | "multidl/multi-downloader-nx:${{ github.event.release.tag_name }}" 65 | - name: Image digest 66 | run: echo ${{ steps.docker_build.outputs.digest }} 67 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Style and build test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | eslint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: pnpm/action-setup@v2 15 | with: 16 | version: latest 17 | - name: Set up Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 20 21 | check-latest: true 22 | - run: pnpm i 23 | - run: npx eslint . --quiet 24 | test: 25 | needs: eslint 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: pnpm/action-setup@v2 30 | with: 31 | version: latest 32 | - name: Set up Node.js 33 | uses: actions/setup-node@v3 34 | with: 35 | node-version: 20 36 | check-latest: true 37 | - run: pnpm i 38 | - run: pnpm run test 39 | 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ff* 2 | /bin/mkv* 3 | /_builds/* 4 | **/node_modules/ 5 | /videos/*.json 6 | /videos/*.ts 7 | /videos/*.m4s 8 | /videos/*.txt 9 | .DS_Store 10 | ffmpeg 11 | mkvmerge 12 | token.yml 13 | *.exe 14 | *.dll 15 | *.mkv 16 | *.mp4 17 | *.ass 18 | *.srt 19 | *.resume 20 | *.user.yml 21 | lib 22 | test.* 23 | updates.json 24 | *_token.yml 25 | *_profile.yml 26 | *_sess.yml 27 | archive.json 28 | guistate.json 29 | fonts 30 | .webpack/ 31 | out/ 32 | dist/ 33 | gui/react/build/ 34 | docker-compose.yml 35 | crunchyendpoints 36 | .vscode 37 | .idea 38 | /logs 39 | /tmp/*/ 40 | !videos/.gitkeep 41 | /videos/* 42 | /tmp/*.* 43 | bin 44 | widevine/* 45 | !widevine/.gitkeep 46 | playready/* 47 | !playready/.gitkeep -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSameLine": false, 4 | "bracketSpacing": true, 5 | "embeddedLanguageFormatting": "auto", 6 | "htmlWhitespaceSensitivity": "strict", 7 | "insertPragma": false, 8 | "jsxSingleQuote": false, 9 | "proseWrap": "never", 10 | "quoteProps": "as-needed", 11 | "requirePragma": false, 12 | "semi": true, 13 | "singleQuote": true, 14 | "tabWidth": 2, 15 | "trailingComma": "none", 16 | "useTabs": false, 17 | "vueIndentScriptAndStyle": false, 18 | "printWidth": 180, 19 | "endOfLine": "auto" 20 | } 21 | -------------------------------------------------------------------------------- /@types/adnPlayerConfig.d.ts: -------------------------------------------------------------------------------- 1 | export interface ADNPlayerConfig { 2 | player: Player; 3 | } 4 | 5 | export interface Player { 6 | image: string; 7 | options: Options; 8 | } 9 | 10 | export interface Options { 11 | user: User; 12 | chromecast: Chromecast; 13 | ios: Ios; 14 | video: Video; 15 | dock: any[]; 16 | preference: Preference; 17 | } 18 | 19 | export interface Chromecast { 20 | appId: string; 21 | refreshTokenUrl: string; 22 | } 23 | 24 | export interface Ios { 25 | videoUrl: string; 26 | appUrl: string; 27 | title: string; 28 | } 29 | 30 | export interface Preference { 31 | quality: string; 32 | autoplay: boolean; 33 | language: string; 34 | green: boolean; 35 | } 36 | 37 | export interface User { 38 | hasAccess: boolean; 39 | profileId: number; 40 | refreshToken: string; 41 | refreshTokenUrl: string; 42 | } 43 | 44 | export interface Video { 45 | startDate: null; 46 | currentDate: Date; 47 | available: boolean; 48 | free: boolean; 49 | url: string; 50 | } 51 | -------------------------------------------------------------------------------- /@types/adnSearch.d.ts: -------------------------------------------------------------------------------- 1 | export interface ADNSearch { 2 | shows: ADNSearchShow[]; 3 | total: number; 4 | } 5 | 6 | export interface ADNSearchShow { 7 | id: number; 8 | title: string; 9 | type: string; 10 | originalTitle: string; 11 | shortTitle: string; 12 | reference: string; 13 | age: string; 14 | languages: string[]; 15 | summary: string; 16 | image: string; 17 | image2x: string; 18 | imageHorizontal: string; 19 | imageHorizontal2x: string; 20 | url: string; 21 | urlPath: string; 22 | episodeCount: number; 23 | genres: string[]; 24 | copyright: string; 25 | rating: number; 26 | ratingsCount: number; 27 | commentsCount: number; 28 | qualities: string[]; 29 | simulcast: boolean; 30 | free: boolean; 31 | available: boolean; 32 | download: boolean; 33 | basedOn: string; 34 | tagline: null; 35 | firstReleaseYear: string; 36 | productionStudio: string; 37 | countryOfOrigin: string; 38 | productionTeam: ProductionTeam[]; 39 | nextVideoReleaseDate: null; 40 | indexable: boolean; 41 | } 42 | 43 | export interface ProductionTeam { 44 | role: string; 45 | name: string; 46 | } 47 | -------------------------------------------------------------------------------- /@types/adnStreams.d.ts: -------------------------------------------------------------------------------- 1 | export interface ADNStreams { 2 | links: Links; 3 | video: Video; 4 | metadata: Metadata; 5 | } 6 | 7 | export interface Links { 8 | streaming: Streaming; 9 | subtitles: Subtitles; 10 | history: string; 11 | nextVideoUrl: string; 12 | previousVideoUrl: string; 13 | } 14 | 15 | export interface Streaming { 16 | [streams: string]: Streams; 17 | } 18 | 19 | export interface Streams { 20 | mobile: string; 21 | sd: string; 22 | hd: string; 23 | fhd: string; 24 | auto: string; 25 | } 26 | 27 | export interface Subtitles { 28 | all: string; 29 | } 30 | 31 | export interface Metadata { 32 | title: string; 33 | subtitle: string; 34 | summary: null; 35 | rating: number; 36 | } 37 | 38 | export interface Video { 39 | guid: string; 40 | id: number; 41 | currentTime: number; 42 | duration: number; 43 | url: string; 44 | image: string; 45 | tcEpisodeStart?:string; 46 | tcEpisodeEnd?: string; 47 | tcIntroStart?: string; 48 | tcIntroEnd?: string; 49 | tcEndingStart?: string; 50 | tcEndingEnd?: string; 51 | } 52 | -------------------------------------------------------------------------------- /@types/adnSubtitles.d.ts: -------------------------------------------------------------------------------- 1 | export interface ADNSubtitles { 2 | [subtitleLang: string]: Subtitle[]; 3 | } 4 | 5 | export interface Subtitle { 6 | startTime: number; 7 | endTime: number; 8 | positionAlign: string; 9 | lineAlign: string; 10 | text: string; 11 | } 12 | -------------------------------------------------------------------------------- /@types/adnVideos.d.ts: -------------------------------------------------------------------------------- 1 | export interface ADNVideos { 2 | videos: ADNVideo[]; 3 | } 4 | 5 | export interface ADNVideo { 6 | id: number; 7 | title: string; 8 | name: string; 9 | number: string; 10 | shortNumber: string; 11 | season: string; 12 | reference: string; 13 | type: string; 14 | order: number; 15 | image: string; 16 | image2x: string; 17 | summary: string; 18 | releaseDate: Date; 19 | duration: number; 20 | url: string; 21 | urlPath: string; 22 | embeddedUrl: string; 23 | languages: string[]; 24 | qualities: string[]; 25 | rating: number; 26 | ratingsCount: number; 27 | commentsCount: number; 28 | available: boolean; 29 | download: boolean; 30 | free: boolean; 31 | freeWithAds: boolean; 32 | show: Show; 33 | indexable: boolean; 34 | isSelected?: boolean; 35 | } 36 | 37 | export interface Show { 38 | id: number; 39 | title: string; 40 | type: string; 41 | originalTitle: string; 42 | shortTitle: string; 43 | reference: string; 44 | age: string; 45 | languages: string[]; 46 | summary: string; 47 | image: string; 48 | image2x: string; 49 | imageHorizontal: string; 50 | imageHorizontal2x: string; 51 | url: string; 52 | urlPath: string; 53 | episodeCount: number; 54 | genres: string[]; 55 | copyright: string; 56 | rating: number; 57 | ratingsCount: number; 58 | commentsCount: number; 59 | qualities: string[]; 60 | simulcast: boolean; 61 | free: boolean; 62 | available: boolean; 63 | download: boolean; 64 | basedOn: string; 65 | tagline: string; 66 | firstReleaseYear: string; 67 | productionStudio: string; 68 | countryOfOrigin: string; 69 | productionTeam: ProductionTeam[]; 70 | nextVideoReleaseDate: Date; 71 | indexable: boolean; 72 | } 73 | 74 | export interface ProductionTeam { 75 | role: string; 76 | name: string; 77 | } 78 | -------------------------------------------------------------------------------- /@types/animeOnegaiSearch.d.ts: -------------------------------------------------------------------------------- 1 | export interface AnimeOnegaiSearch { 2 | text: string; 3 | list: AOSearchResult[]; 4 | } 5 | 6 | export interface AOSearchResult { 7 | /** 8 | * Asset ID 9 | */ 10 | ID: number; 11 | CreatedAt: Date; 12 | UpdatedAt: Date; 13 | DeletedAt: null; 14 | title: string; 15 | active: boolean; 16 | excerpt: string; 17 | description: string; 18 | bg: string; 19 | poster: string; 20 | entry: string; 21 | code_name: string; 22 | /** 23 | * The Video ID required to get the streams 24 | */ 25 | video_entry: string; 26 | trailer: string; 27 | year: number; 28 | /** 29 | * Asset Type, Known Possibilities 30 | * * 1 - Video 31 | * * 2 - Series 32 | */ 33 | asset_type: 1 | 2; 34 | status: number; 35 | permalink: string; 36 | duration: string; 37 | subtitles: boolean; 38 | price: number; 39 | rent_price: number; 40 | rating: number; 41 | color: number | null; 42 | classification: number; 43 | brazil_classification: null | string; 44 | likes: number; 45 | views: number; 46 | button: string; 47 | stream_url: string; 48 | stream_url_backup: string; 49 | copyright: null | string; 50 | skip_intro: null | string; 51 | ending: null | string; 52 | bumper_intro: string; 53 | ads: string; 54 | age_restriction: boolean | null; 55 | epg: null; 56 | allow_languages: string[] | null; 57 | allow_countries: string[] | null; 58 | classification_text: string; 59 | locked: boolean; 60 | resign: boolean; 61 | favorite: boolean; 62 | actors_list: null; 63 | voiceactors_list: null; 64 | artdirectors_list: null; 65 | audios_list: null; 66 | awards_list: null; 67 | companies_list: null; 68 | countries_list: null; 69 | directors_list: null; 70 | edition_list: null; 71 | genres_list: null; 72 | music_list: null; 73 | photograpy_list: null; 74 | producer_list: null; 75 | screenwriter_list: null; 76 | season_list: null; 77 | tags_list: null; 78 | chapter_id: number; 79 | chapter_entry: string; 80 | chapter_poster: string; 81 | progress_time: number; 82 | progress_percent: number; 83 | included_subscription: number; 84 | paid_content: number; 85 | rent_content: number; 86 | objectID: string; 87 | lang: string; 88 | } -------------------------------------------------------------------------------- /@types/animeOnegaiSeasons.d.ts: -------------------------------------------------------------------------------- 1 | export interface AnimeOnegaiSeasons { 2 | ID: number; 3 | CreatedAt: Date; 4 | UpdatedAt: Date; 5 | DeletedAt: null; 6 | name: string; 7 | number: number; 8 | asset_id: number; 9 | entry: string; 10 | description: string; 11 | active: boolean; 12 | allow_languages: string[]; 13 | allow_countries: string[]; 14 | list: Episode[]; 15 | } 16 | 17 | export interface Episode { 18 | ID: number; 19 | CreatedAt: Date; 20 | UpdatedAt: Date; 21 | DeletedAt: null; 22 | name: string; 23 | number: number; 24 | description: string; 25 | thumbnail: string; 26 | entry: string; 27 | video_entry: string; 28 | active: boolean; 29 | season_id: number; 30 | stream_url: string; 31 | skip_intro: null; 32 | ending: null; 33 | open_free: boolean; 34 | asset_id: number; 35 | age_restriction: boolean; 36 | } 37 | -------------------------------------------------------------------------------- /@types/animeOnegaiSeries.d.ts: -------------------------------------------------------------------------------- 1 | export interface AnimeOnegaiSeries { 2 | ID: number; 3 | CreatedAt: Date; 4 | UpdatedAt: Date; 5 | DeletedAt: null; 6 | title: string; 7 | active: boolean; 8 | excerpt: string; 9 | description: string; 10 | bg: string; 11 | poster: string; 12 | entry: string; 13 | code_name: string; 14 | /** 15 | * The Video ID required to get the streams 16 | */ 17 | video_entry: string; 18 | trailer: string; 19 | year: number; 20 | asset_type: number; 21 | status: number; 22 | permalink: string; 23 | duration: string; 24 | subtitles: boolean; 25 | price: number; 26 | rent_price: number; 27 | rating: number; 28 | color: number; 29 | classification: number; 30 | brazil_classification: string; 31 | likes: number; 32 | views: number; 33 | button: string; 34 | stream_url: string; 35 | stream_url_backup: string; 36 | copyright: string; 37 | skip_intro: null; 38 | ending: null; 39 | bumper_intro: string; 40 | ads: string; 41 | age_restriction: boolean; 42 | epg: null; 43 | allow_languages: string[]; 44 | allow_countries: string[]; 45 | classification_text: string; 46 | locked: boolean; 47 | resign: boolean; 48 | favorite: boolean; 49 | actors_list: CtorsList[]; 50 | voiceactors_list: CtorsList[]; 51 | artdirectors_list: any[]; 52 | audios_list: SList[]; 53 | awards_list: any[]; 54 | companies_list: any[]; 55 | countries_list: any[]; 56 | directors_list: CtorsList[]; 57 | edition_list: any[]; 58 | genres_list: SList[]; 59 | music_list: any[]; 60 | photograpy_list: any[]; 61 | producer_list: any[]; 62 | screenwriter_list: any[]; 63 | season_list: any[]; 64 | tags_list: TagsList[]; 65 | chapter_id: number; 66 | chapter_entry: string; 67 | chapter_poster: string; 68 | progress_time: number; 69 | progress_percent: number; 70 | included_subscription: number; 71 | paid_content: number; 72 | rent_content: number; 73 | objectID: string; 74 | lang: string; 75 | } 76 | 77 | export interface CtorsList { 78 | ID: number; 79 | CreatedAt: Date; 80 | UpdatedAt: Date; 81 | DeletedAt: null; 82 | name: string; 83 | Permalink?: string; 84 | country: number | null; 85 | year: number | null; 86 | death: number | null; 87 | image: string; 88 | genre: null; 89 | description: string; 90 | permalink?: string; 91 | background?: string; 92 | } 93 | 94 | export interface SList { 95 | ID: number; 96 | CreatedAt: Date; 97 | UpdatedAt: Date; 98 | DeletedAt: null; 99 | name: string; 100 | age_restriction?: number; 101 | } 102 | 103 | export interface TagsList { 104 | ID: number; 105 | CreatedAt: Date; 106 | UpdatedAt: Date; 107 | DeletedAt: null; 108 | name: string; 109 | position: number; 110 | status: boolean; 111 | } 112 | -------------------------------------------------------------------------------- /@types/animeOnegaiStream.d.ts: -------------------------------------------------------------------------------- 1 | export interface AnimeOnegaiStream { 2 | ID: number; 3 | CreatedAt: Date; 4 | UpdatedAt: Date; 5 | DeletedAt: null; 6 | name: string; 7 | source_url: string; 8 | backup_url: string; 9 | live: boolean; 10 | token_handler: number; 11 | entry: string; 12 | job: string; 13 | drm: boolean; 14 | transcoding_content_id: string; 15 | transcoding_asset_id: string; 16 | status: number; 17 | thumbnail: string; 18 | hls: string; 19 | dash: string; 20 | widevine_proxy: string; 21 | playready_proxy: string; 22 | apple_licence: string; 23 | apple_certificate: string; 24 | dpath: string; 25 | dbin: string; 26 | subtitles: Subtitle[]; 27 | origin: number; 28 | offline_entry: string; 29 | offline_status: boolean; 30 | } 31 | 32 | export interface Subtitle { 33 | ID: number; 34 | CreatedAt: Date; 35 | UpdatedAt: Date; 36 | DeletedAt: null; 37 | name: string; 38 | lang: string; 39 | entry_id: string; 40 | url: string; 41 | } 42 | -------------------------------------------------------------------------------- /@types/crunchyAndroidEpisodes.d.ts: -------------------------------------------------------------------------------- 1 | import { Images } from './crunchyEpisodeList'; 2 | 3 | export interface CrunchyAndroidEpisodes { 4 | __class__: string; 5 | __href__: string; 6 | __resource_key__: string; 7 | __links__: object; 8 | __actions__: object; 9 | total: number; 10 | items: CrunchyAndroidEpisode[]; 11 | } 12 | 13 | export interface CrunchyAndroidEpisode { 14 | __class__: string; 15 | __href__: string; 16 | __resource_key__: string; 17 | __links__: Links; 18 | __actions__: Actions; 19 | playback: string; 20 | id: string; 21 | channel_id: ChannelID; 22 | series_id: string; 23 | series_title: string; 24 | series_slug_title: string; 25 | season_id: string; 26 | season_title: string; 27 | season_slug_title: string; 28 | season_number: number; 29 | episode: string; 30 | episode_number: number; 31 | sequence_number: number; 32 | production_episode_id: string; 33 | title: string; 34 | slug_title: string; 35 | description: string; 36 | next_episode_id: string; 37 | next_episode_title: string; 38 | hd_flag: boolean; 39 | maturity_ratings: MaturityRating[]; 40 | extended_maturity_rating: Actions; 41 | is_mature: boolean; 42 | mature_blocked: boolean; 43 | episode_air_date: Date; 44 | upload_date: Date; 45 | availability_starts: Date; 46 | availability_ends: Date; 47 | eligible_region: string; 48 | available_date: Date; 49 | free_available_date: Date; 50 | premium_date: Date; 51 | premium_available_date: Date; 52 | is_subbed: boolean; 53 | is_dubbed: boolean; 54 | is_clip: boolean; 55 | seo_title: string; 56 | seo_description: string; 57 | season_tags: string[]; 58 | available_offline: boolean; 59 | subtitle_locales: Locale[]; 60 | availability_notes: string; 61 | audio_locale: Locale; 62 | versions: Version[]; 63 | closed_captions_available: boolean; 64 | identifier: string; 65 | media_type: MediaType; 66 | slug: string; 67 | images: Images; 68 | duration_ms: number; 69 | is_premium_only: boolean; 70 | listing_id: string; 71 | hide_season_title?: boolean; 72 | hide_season_number?: boolean; 73 | isSelected?: boolean; 74 | seq_id: string; 75 | } 76 | 77 | export interface Links { 78 | 'episode/channel': Link; 79 | 'episode/next_episode': Link; 80 | 'episode/season': Link; 81 | 'episode/series': Link; 82 | streams: Link; 83 | } 84 | 85 | export interface Link { 86 | href: string; 87 | } 88 | 89 | export interface Thumbnail { 90 | width: number; 91 | height: number; 92 | type: string; 93 | source: string; 94 | } 95 | 96 | export enum Locale { 97 | enUS = 'en-US', 98 | esLA = 'es-LA', 99 | es419 = 'es-419', 100 | esES = 'es-ES', 101 | ptBR = 'pt-BR', 102 | frFR = 'fr-FR', 103 | deDE = 'de-DE', 104 | arME = 'ar-ME', 105 | arSA = 'ar-SA', 106 | itIT = 'it-IT', 107 | ruRU = 'ru-RU', 108 | trTR = 'tr-TR', 109 | hiIN = 'hi-IN', 110 | zhCN = 'zh-CN', 111 | koKR = 'ko-KR', 112 | jaJP = 'ja-JP', 113 | } 114 | 115 | export enum MediaType { 116 | Episode = 'episode', 117 | } 118 | 119 | export enum ChannelID { 120 | Crunchyroll = 'crunchyroll', 121 | } 122 | 123 | export enum MaturityRating { 124 | Tv14 = 'TV-14', 125 | } 126 | 127 | export interface Version { 128 | audio_locale: Locale; 129 | guid: string; 130 | original: boolean; 131 | variant: string; 132 | season_guid: string; 133 | media_guid: string; 134 | is_premium_only: boolean; 135 | } 136 | 137 | -------------------------------------------------------------------------------- /@types/crunchyAndroidStreams.d.ts: -------------------------------------------------------------------------------- 1 | export interface CrunchyAndroidStreams { 2 | __class__: string; 3 | __href__: string; 4 | __resource_key__: string; 5 | __links__: Links; 6 | __actions__: Record; 7 | media_id: string; 8 | audio_locale: Locale; 9 | subtitles: Subtitles; 10 | closed_captions: Subtitles; 11 | streams: Streams; 12 | bifs: string[]; 13 | versions: Version[]; 14 | captions: Record; 15 | } 16 | 17 | export interface Subtitles { 18 | '': Subtitle; 19 | 'en-US'?: Subtitle; 20 | 'es-LA'?: Subtitle; 21 | 'es-419'?: Subtitle; 22 | 'es-ES'?: Subtitle; 23 | 'pt-BR'?: Subtitle; 24 | 'fr-FR'?: Subtitle; 25 | 'de-DE'?: Subtitle; 26 | 'ar-ME'?: Subtitle; 27 | 'ar-SA'?: Subtitle; 28 | 'it-IT'?: Subtitle; 29 | 'ru-RU'?: Subtitle; 30 | 'tr-TR'?: Subtitle; 31 | 'hi-IN'?: Subtitle; 32 | 'zh-CN'?: Subtitle; 33 | 'ko-KR'?: Subtitle; 34 | 'ja-JP'?: Subtitle; 35 | } 36 | 37 | export interface Links { 38 | resource: Resource; 39 | } 40 | 41 | export interface Resource { 42 | href: string; 43 | } 44 | 45 | export interface Streams { 46 | [key: string]: { [key: string]: Download }; 47 | } 48 | 49 | export interface Download { 50 | hardsub_locale: Locale; 51 | hardsub_lang?: string; 52 | url: string; 53 | } 54 | 55 | export interface Urls { 56 | '': Download; 57 | } 58 | 59 | export interface Subtitle { 60 | locale: Locale; 61 | url: string; 62 | format: string; 63 | } 64 | 65 | export interface Version { 66 | audio_locale: Locale; 67 | guid: string; 68 | original: boolean; 69 | variant: string; 70 | season_guid: string; 71 | media_guid: string; 72 | is_premium_only: boolean; 73 | } 74 | 75 | export enum Locale { 76 | default = '', 77 | enUS = 'en-US', 78 | esLA = 'es-LA', 79 | es419 = 'es-419', 80 | esES = 'es-ES', 81 | ptBR = 'pt-BR', 82 | frFR = 'fr-FR', 83 | deDE = 'de-DE', 84 | arME = 'ar-ME', 85 | arSA = 'ar-SA', 86 | itIT = 'it-IT', 87 | ruRU = 'ru-RU', 88 | trTR = 'tr-TR', 89 | hiIN = 'hi-IN', 90 | zhCN = 'zh-CN', 91 | koKR = 'ko-KR', 92 | jaJP = 'ja-JP', 93 | } -------------------------------------------------------------------------------- /@types/crunchyChapters.d.ts: -------------------------------------------------------------------------------- 1 | export interface CrunchyChapters { 2 | [key: string]: CrunchyChapter; 3 | lastUpdate: Date; 4 | mediaId: string; 5 | } 6 | 7 | export interface CrunchyChapter { 8 | approverId: string; 9 | distributionNumber: string; 10 | end: number; 11 | start: number; 12 | title: string; 13 | seriesId: string; 14 | new: boolean; 15 | type: string; 16 | } 17 | 18 | export interface CrunchyOldChapter { 19 | media_id: string; 20 | startTime: number; 21 | endTime: number; 22 | duration: number; 23 | comparedWith: string; 24 | ordering: string; 25 | last_updated: Date; 26 | } -------------------------------------------------------------------------------- /@types/crunchyEpisodeList.d.ts: -------------------------------------------------------------------------------- 1 | import { Links } from './crunchyAndroidEpisodes'; 2 | 3 | export interface CrunchyEpisodeList { 4 | total: number; 5 | data: CrunchyEpisode[]; 6 | meta: Meta; 7 | } 8 | 9 | export interface CrunchyEpisode { 10 | next_episode_id: string; 11 | series_id: string; 12 | season_number: number; 13 | next_episode_title: string; 14 | availability_notes: string; 15 | duration_ms: number; 16 | series_slug_title: string; 17 | series_title: string; 18 | is_dubbed: boolean; 19 | versions: Version[] | null; 20 | identifier: string; 21 | sequence_number: number; 22 | eligible_region: Record; 23 | availability_starts: Date; 24 | images: Images; 25 | season_id: string; 26 | seo_title: string; 27 | is_premium_only: boolean; 28 | extended_maturity_rating: Record; 29 | title: string; 30 | production_episode_id: string; 31 | premium_available_date: Date; 32 | season_title: string; 33 | seo_description: string; 34 | audio_locale: Locale; 35 | id: string; 36 | media_type: MediaType; 37 | availability_ends: Date; 38 | free_available_date: Date; 39 | playback: string; 40 | channel_id: ChannelID; 41 | episode: string; 42 | is_mature: boolean; 43 | listing_id: string; 44 | episode_air_date: Date; 45 | slug: string; 46 | available_date: Date; 47 | subtitle_locales: Locale[]; 48 | slug_title: string; 49 | available_offline: boolean; 50 | description: string; 51 | is_subbed: boolean; 52 | premium_date: Date; 53 | upload_date: Date; 54 | season_slug_title: string; 55 | closed_captions_available: boolean; 56 | episode_number: number; 57 | season_tags: any[]; 58 | maturity_ratings: MaturityRating[]; 59 | streams_link?: string; 60 | mature_blocked: boolean; 61 | is_clip: boolean; 62 | hd_flag: boolean; 63 | hide_season_title?: boolean; 64 | hide_season_number?: boolean; 65 | isSelected?: boolean; 66 | seq_id: string; 67 | __links__?: Links; 68 | } 69 | 70 | export enum Locale { 71 | enUS = 'en-US', 72 | esLA = 'es-LA', 73 | es419 = 'es-419', 74 | esES = 'es-ES', 75 | ptBR = 'pt-BR', 76 | frFR = 'fr-FR', 77 | deDE = 'de-DE', 78 | arME = 'ar-ME', 79 | arSA = 'ar-SA', 80 | itIT = 'it-IT', 81 | ruRU = 'ru-RU', 82 | trTR = 'tr-TR', 83 | hiIN = 'hi-IN', 84 | zhCN = 'zh-CN', 85 | koKR = 'ko-KR', 86 | jaJP = 'ja-JP', 87 | } 88 | 89 | export enum ChannelID { 90 | Crunchyroll = 'crunchyroll', 91 | } 92 | 93 | export interface Images { 94 | poster_tall?: Array; 95 | poster_wide?: Array; 96 | promo_image?: Array; 97 | thumbnail?: Array; 98 | } 99 | 100 | export interface Image { 101 | height: number; 102 | source: string; 103 | type: ImageType; 104 | width: number; 105 | } 106 | 107 | export enum ImageType { 108 | PosterTall = 'poster_tall', 109 | PosterWide = 'poster_wide', 110 | PromoImage = 'promo_image', 111 | Thumbnail = 'thumbnail', 112 | } 113 | 114 | export enum MaturityRating { 115 | Tv14 = 'TV-14', 116 | } 117 | 118 | export enum MediaType { 119 | Episode = 'episode', 120 | } 121 | 122 | export interface Version { 123 | audio_locale: Locale; 124 | guid: string; 125 | is_premium_only: boolean; 126 | media_guid: string; 127 | original: boolean; 128 | season_guid: string; 129 | variant: string; 130 | } 131 | 132 | export interface Meta { 133 | versions_considered?: boolean; 134 | } -------------------------------------------------------------------------------- /@types/crunchyPlayStreams.d.ts: -------------------------------------------------------------------------------- 1 | import { Locale } from './playbackData'; 2 | 3 | export interface CrunchyPlayStream { 4 | assetId: string; 5 | audioLocale: Locale; 6 | bifs: string; 7 | burnedInLocale: string; 8 | captions: { [key: string]: Caption }; 9 | hardSubs: { [key: string]: HardSub }; 10 | playbackType: string; 11 | session: Session; 12 | subtitles: { [key: string]: Subtitle }; 13 | token: string; 14 | url: string; 15 | versions: any[]; 16 | } 17 | 18 | export interface Caption { 19 | format: string; 20 | language: string; 21 | url: string; 22 | } 23 | 24 | export interface HardSub { 25 | hlang: string; 26 | url: string; 27 | quality: string; 28 | } 29 | 30 | export interface Session { 31 | renewSeconds: number; 32 | noNetworkRetryIntervalSeconds: number; 33 | noNetworkTimeoutSeconds: number; 34 | maximumPauseSeconds: number; 35 | endOfVideoUnloadSeconds: number; 36 | sessionExpirationSeconds: number; 37 | usesStreamLimits: boolean; 38 | } 39 | 40 | export interface Subtitle { 41 | format: string; 42 | language: string; 43 | url: string; 44 | } 45 | -------------------------------------------------------------------------------- /@types/downloadedFile.d.ts: -------------------------------------------------------------------------------- 1 | import { LanguageItem } from '../modules/module.langsData'; 2 | 3 | export type DownloadedFile = { 4 | path: string, 5 | lang: LanguageItem 6 | } -------------------------------------------------------------------------------- /@types/enums.ts: -------------------------------------------------------------------------------- 1 | export enum CrunchyPlayStreams { 2 | 'chrome' = 'web/chrome', 3 | 'firefox' = 'web/firefox', 4 | 'safari' = 'web/safari', 5 | 'edge' = 'web/edge', 6 | 'fallback' = 'web/fallback', 7 | 'ps4' = 'console/ps4', 8 | 'ps5' = 'console/ps5', 9 | 'switch' = 'console/switch', 10 | 'xboxone' = 'console/xbox_one', 11 | 'vidaa' = 'tv/vidaa', 12 | 'samsungtv' = 'tv/samsung', 13 | 'lgtv' = 'tv/lg', 14 | 'rokutv' = 'tv/roku', 15 | 'android' = 'android/phone', 16 | 'androidt' = 'android/tablet', 17 | 'iphone' = 'ios/iphone', 18 | 'ipad' = 'ios/ipad', 19 | 'vision' = 'ios/vision', 20 | } -------------------------------------------------------------------------------- /@types/github.d.ts: -------------------------------------------------------------------------------- 1 | export type GithubTag = { 2 | name: string, 3 | zipball_url: string, 4 | tarball_url: string, 5 | commit: { 6 | sha: string, 7 | url: string 8 | }, 9 | node_id: string 10 | } 11 | 12 | export interface TagCompare { 13 | url: string; 14 | html_url: string; 15 | permalink_url: string; 16 | diff_url: string; 17 | patch_url: string; 18 | base_commit: BaseCommitClass; 19 | merge_base_commit: BaseCommitClass; 20 | status: string; 21 | ahead_by: number; 22 | behind_by: number; 23 | total_commits: number; 24 | commits: BaseCommitClass[]; 25 | files: File[]; 26 | } 27 | 28 | export interface BaseCommitClass { 29 | sha: string; 30 | node_id: string; 31 | commit: BaseCommitCommit; 32 | url: string; 33 | html_url: string; 34 | comments_url: string; 35 | author: BaseCommitAuthor; 36 | committer: BaseCommitAuthor; 37 | parents: Parent[]; 38 | } 39 | 40 | export interface BaseCommitAuthor { 41 | login: string; 42 | id: number; 43 | node_id: string; 44 | avatar_url: string; 45 | gravatar_id: string; 46 | url: string; 47 | html_url: string; 48 | followers_url: string; 49 | following_url: string; 50 | gists_url: string; 51 | starred_url: string; 52 | subscriptions_url: string; 53 | organizations_url: string; 54 | repos_url: string; 55 | events_url: string; 56 | received_events_url: string; 57 | type: string; 58 | site_admin: boolean; 59 | } 60 | 61 | export interface BaseCommitCommit { 62 | author: PurpleAuthor; 63 | committer: PurpleAuthor; 64 | message: string; 65 | tree: Tree; 66 | url: string; 67 | comment_count: number; 68 | verification: Verification; 69 | } 70 | 71 | export interface PurpleAuthor { 72 | name: string; 73 | email: string; 74 | date: string; 75 | } 76 | 77 | export interface Tree { 78 | sha: string; 79 | url: string; 80 | } 81 | 82 | export interface Verification { 83 | verified: boolean; 84 | reason: string; 85 | signature: string; 86 | payload: string; 87 | } 88 | 89 | export interface Parent { 90 | sha: string; 91 | url: string; 92 | html_url: string; 93 | } 94 | 95 | export interface File { 96 | sha: string; 97 | filename: string; 98 | status: string; 99 | additions: number; 100 | deletions: number; 101 | changes: number; 102 | blob_url: string; 103 | raw_url: string; 104 | contents_url: string; 105 | patch: string; 106 | } 107 | -------------------------------------------------------------------------------- /@types/hidiveDashboard.d.ts: -------------------------------------------------------------------------------- 1 | export interface HidiveDashboard { 2 | Code: number; 3 | Status: string; 4 | Message: null; 5 | Messages: object; 6 | Data: Data; 7 | Timestamp: string; 8 | IPAddress: string; 9 | } 10 | 11 | export interface Data { 12 | TitleRows: TitleRow[]; 13 | LoadTime: number; 14 | } 15 | 16 | export interface TitleRow { 17 | Name: string; 18 | Titles: Title[]; 19 | LoadTime: number; 20 | } 21 | 22 | export interface Title { 23 | Id: number; 24 | Name: string; 25 | ShortSynopsis: string; 26 | MediumSynopsis: string; 27 | LongSynopsis: string; 28 | KeyArtUrl: string; 29 | MasterArtUrl: string; 30 | Rating: null | string; 31 | OverallRating: number; 32 | RatingCount: number; 33 | MALScore: null; 34 | UserRating: number; 35 | RunTime: number | null; 36 | ShowInfoTitle: string; 37 | FirstPremiereDate: Date; 38 | EpisodeCount: number; 39 | SeasonName: string; 40 | RokuHDArtUrl: string; 41 | RokuSDArtUrl: string; 42 | IsRateable: boolean; 43 | InQueue: boolean; 44 | IsFavorite: boolean; 45 | IsContinueWatching: boolean; 46 | ContinueWatching: ContinueWatching; 47 | Episodes: any[]; 48 | LoadTime: number; 49 | } 50 | 51 | export interface ContinueWatching { 52 | Id: string; 53 | ProfileId: number; 54 | EpisodeId: number; 55 | Status: Status | null; 56 | CurrentTime: number; 57 | UserId: number; 58 | TitleId: number; 59 | SeasonId: number; 60 | VideoId: number; 61 | TotalSeconds: number; 62 | CreatedDT: Date; 63 | ModifiedDT: Date | null; 64 | } 65 | 66 | export enum Status { 67 | Paused = 'Paused', 68 | Playing = 'Playing', 69 | Watching = 'Watching', 70 | } -------------------------------------------------------------------------------- /@types/hidiveEpisodeList.d.ts: -------------------------------------------------------------------------------- 1 | export interface HidiveEpisodeList { 2 | Code: number; 3 | Status: string; 4 | Message: null; 5 | Messages: Record; 6 | Data: Data; 7 | Timestamp: string; 8 | IPAddress: string; 9 | } 10 | 11 | export interface Data { 12 | Title: HidiveTitle; 13 | } 14 | 15 | export interface HidiveTitle { 16 | Id: number; 17 | Name: string; 18 | ShortSynopsis: string; 19 | MediumSynopsis: string; 20 | LongSynopsis: string; 21 | KeyArtUrl: string; 22 | MasterArtUrl: string; 23 | Rating: string; 24 | OverallRating: number; 25 | RatingCount: number; 26 | MALScore: null; 27 | UserRating: number; 28 | RunTime: number; 29 | ShowInfoTitle: string; 30 | FirstPremiereDate: Date; 31 | EpisodeCount: number; 32 | SeasonName: string; 33 | RokuHDArtUrl: string; 34 | RokuSDArtUrl: string; 35 | IsRateable: boolean; 36 | InQueue: boolean; 37 | IsFavorite: boolean; 38 | IsContinueWatching: boolean; 39 | ContinueWatching: ContinueWatching; 40 | Episodes: HidiveEpisode[]; 41 | LoadTime: number; 42 | } 43 | 44 | export interface ContinueWatching { 45 | Id: string; 46 | ProfileId: number; 47 | EpisodeId: number; 48 | Status: string; 49 | CurrentTime: number; 50 | UserId: number; 51 | TitleId: number; 52 | SeasonId: number; 53 | VideoId: number; 54 | TotalSeconds: number; 55 | CreatedDT: Date; 56 | ModifiedDT: Date; 57 | } 58 | 59 | export interface HidiveEpisode { 60 | Id: number; 61 | Number: number; 62 | Name: string; 63 | Summary: string; 64 | HIDIVEPremiereDate: Date; 65 | ScreenShotSmallUrl: string; 66 | ScreenShotCompressedUrl: string; 67 | SeasonNumber: number; 68 | TitleId: number; 69 | SeasonNumberValue: number; 70 | EpisodeNumberValue: number; 71 | VideoKey: string; 72 | DisplayNameLong: string; 73 | PercentProgress: number; 74 | LoadTime: number; 75 | } 76 | 77 | export interface HidiveEpisodeExtra extends HidiveEpisode { 78 | titleId: number; 79 | epKey: string; 80 | nameLong: string; 81 | seriesTitle: string; 82 | seriesId?: number; 83 | isSelected: boolean; 84 | } -------------------------------------------------------------------------------- /@types/hidiveSearch.d.ts: -------------------------------------------------------------------------------- 1 | export interface HidiveSearch { 2 | Code: number; 3 | Status: string; 4 | Message: null; 5 | Messages: Record; 6 | Data: HidiveSearchData; 7 | Timestamp: string; 8 | IPAddress: string; 9 | } 10 | 11 | export interface HidiveSearchData { 12 | Query: string; 13 | Slug: string; 14 | TitleResults: HidiveSearchItem[]; 15 | SearchId: number; 16 | IsSearchPinned: boolean; 17 | IsPinnedSearchAvailable: boolean; 18 | } 19 | 20 | export interface HidiveSearchItem { 21 | Id: number; 22 | Name: string; 23 | ShortSynopsis: string; 24 | MediumSynopsis: string; 25 | LongSynopsis: string; 26 | KeyArtUrl: string; 27 | MasterArtUrl: string; 28 | Rating: string; 29 | OverallRating: number; 30 | RatingCount: number; 31 | MALScore: null; 32 | UserRating: number; 33 | RunTime: number | null; 34 | ShowInfoTitle: string; 35 | FirstPremiereDate: Date; 36 | EpisodeCount: number; 37 | SeasonName: string; 38 | RokuHDArtUrl: string; 39 | RokuSDArtUrl: string; 40 | IsRateable: boolean; 41 | InQueue: boolean; 42 | IsFavorite: boolean; 43 | IsContinueWatching: boolean; 44 | ContinueWatching: null; 45 | Episodes: any[]; 46 | LoadTime: number; 47 | } -------------------------------------------------------------------------------- /@types/hidiveTypes.d.ts: -------------------------------------------------------------------------------- 1 | export interface HidiveVideoList { 2 | Code: number; 3 | Status: string; 4 | Message: null; 5 | Messages: Record; 6 | Data: HidiveVideo; 7 | Timestamp: string; 8 | IPAddress: string; 9 | } 10 | 11 | export interface HidiveVideo { 12 | ShowAds: boolean; 13 | CaptionCssUrl: string; 14 | FontSize: number; 15 | FontScale: number; 16 | CaptionLanguages: string[]; 17 | CaptionLanguage: string; 18 | CaptionVttUrls: Record; 19 | VideoLanguages: string[]; 20 | VideoLanguage: string; 21 | VideoUrls: Record; 22 | FontColorName: string; 23 | AutoPlayNextEpisode: boolean; 24 | MaxStreams: number; 25 | CurrentTime: number; 26 | FontColorCode: string; 27 | RunTime: number; 28 | AdUrl: null; 29 | } 30 | 31 | export interface HidiveStreamList { 32 | hls: string[]; 33 | drm: string[]; 34 | drmEnabled: boolean; 35 | } 36 | 37 | export interface HidiveStreamInfo extends HidiveStreamList { 38 | language?: string; 39 | episodeTitle?: string; 40 | seriesTitle?: string; 41 | season?: number; 42 | episodeNumber?: number; 43 | uncut?: boolean; 44 | image?: string; 45 | } 46 | 47 | export interface HidiveSubtitleInfo { 48 | language: string; 49 | cc: boolean; 50 | url: string; 51 | } 52 | 53 | export type DownloadedMedia = { 54 | type: 'Video', 55 | lang: LanguageItem, 56 | path: string, 57 | uncut: boolean 58 | } | ({ 59 | type: 'Subtitle', 60 | cc: boolean 61 | } & sxItem ) -------------------------------------------------------------------------------- /@types/iso639.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'iso-639' { 2 | export type iso639Type = { 3 | [key: string]: { 4 | '639-1'?: string, 5 | '639-2'?: string 6 | } 7 | } 8 | export const iso_639_2: iso639Type; 9 | } -------------------------------------------------------------------------------- /@types/m3u8-parsed.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'm3u8-parsed' { 2 | export type M3U8 = { 3 | allowCache: boolean, 4 | discontinuityStarts: [], 5 | segments: { 6 | duration: number, 7 | byterange?: { 8 | length: number, 9 | offset: number 10 | }, 11 | uri: string, 12 | key: { 13 | method: string, 14 | uri: string, 15 | }, 16 | timeline: number 17 | }[], 18 | version: number, 19 | mediaGroups: { 20 | [type: string]: { 21 | [index: string]: { 22 | [language: string]: { 23 | default: boolean, 24 | autoselect: boolean, 25 | language: string, 26 | uri: string 27 | } 28 | } 29 | } 30 | }, 31 | playlists: { 32 | uri: string, 33 | timeline: number, 34 | attributes: { 35 | 'CLOSED-CAPTIONS': string, 36 | 'AUDIO': string, 37 | 'FRAME-RATE': number, 38 | 'RESOLUTION': { 39 | width: number, 40 | height: number 41 | }, 42 | 'CODECS': string, 43 | 'AVERAGE-BANDWIDTH': string, 44 | 'BANDWIDTH': number 45 | } 46 | }[], 47 | } 48 | export default function (data: string): M3U8; 49 | } -------------------------------------------------------------------------------- /@types/mpd-parser.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'mpd-parser' { 2 | export type Segment = { 3 | uri: string, 4 | timeline: number, 5 | duration: number, 6 | resolvedUri: string, 7 | map: { 8 | uri: string, 9 | resolvedUri: string, 10 | byterange?: { 11 | length: number, 12 | offset: number 13 | } 14 | }, 15 | byterange?: { 16 | length: number, 17 | offset: number 18 | }, 19 | number: number, 20 | presentationTime: number 21 | } 22 | 23 | export type Sidx = { 24 | uri: string, 25 | resolvedUri: string, 26 | byterange: { 27 | length: number, 28 | offset: number 29 | }, 30 | map: { 31 | uri: string, 32 | resolvedUri: string, 33 | byterange: { 34 | length: number, 35 | offset: number 36 | } 37 | }, 38 | duration: number, 39 | timeline: number, 40 | presentationTime: number, 41 | number: number 42 | } 43 | 44 | export type Playlist = { 45 | attributes: { 46 | NAME: string, 47 | BANDWIDTH: number, 48 | CODECS: string, 49 | 'PROGRAM-ID': number, 50 | // Following for video only 51 | 'FRAME-RATE'?: number, 52 | AUDIO?: string, // audio stream name 53 | SUBTITLES?: string, 54 | RESOLUTION?: { 55 | width: number, 56 | height: number 57 | } 58 | }, 59 | uri: string, 60 | endList: boolean, 61 | timeline: number, 62 | resolvedUri: string, 63 | targetDuration: number, 64 | discontinuitySequence: number, 65 | discontinuityStarts: [], 66 | timelineStarts: { 67 | start: number, 68 | timeline: number 69 | }[], 70 | mediaSequence: number, 71 | contentProtection?: { 72 | [type: string]: { 73 | pssh?: Uint8Array 74 | } 75 | } 76 | segments: Segment[] 77 | sidx?: Sidx 78 | } 79 | 80 | export type Manifest = { 81 | allowCache: boolean, 82 | discontinuityStarts: [], 83 | segments: [], 84 | endList: true, 85 | duration: number, 86 | playlists: Playlist[], 87 | mediaGroups: { 88 | AUDIO: { 89 | audio: { 90 | [name: string]: { 91 | language: string, 92 | autoselect: boolean, 93 | default: boolean, 94 | playlists: Playlist[] 95 | } 96 | } 97 | } 98 | } 99 | } 100 | export function parse(manifest: string): Manifest 101 | } 102 | -------------------------------------------------------------------------------- /@types/newHidiveEpisode.d.ts: -------------------------------------------------------------------------------- 1 | export interface NewHidiveEpisode { 2 | description: string; 3 | duration: number; 4 | title: string; 5 | categories: string[]; 6 | contentDownload: ContentDownload; 7 | favourite: boolean; 8 | subEvents: any[]; 9 | thumbnailUrl: string; 10 | longDescription: string; 11 | posterUrl: string; 12 | offlinePlaybackLanguages: string[]; 13 | externalAssetId: string; 14 | maxHeight: number; 15 | rating: Rating; 16 | episodeInformation: EpisodeInformation; 17 | id: number; 18 | accessLevel: string; 19 | playerUrlCallback: string; 20 | thumbnailsPreview: string; 21 | displayableTags: any[]; 22 | plugins: any[]; 23 | watchStatus: string; 24 | computedReleases: any[]; 25 | licences: any[]; 26 | type: string; 27 | } 28 | 29 | export interface ContentDownload { 30 | permission: string; 31 | period: string; 32 | } 33 | 34 | export interface EpisodeInformation { 35 | seasonNumber: number; 36 | episodeNumber: number; 37 | season: number; 38 | } 39 | 40 | export interface Rating { 41 | rating: string; 42 | descriptors: any[]; 43 | } -------------------------------------------------------------------------------- /@types/newHidivePlayback.d.ts: -------------------------------------------------------------------------------- 1 | export interface NewHidivePlayback { 2 | watermark: null; 3 | skipMarkers: any[]; 4 | annotations: null; 5 | dash: Format[]; 6 | hls: Format[]; 7 | } 8 | 9 | export interface Format { 10 | subtitles: Subtitle[]; 11 | url: string; 12 | drm: DRM; 13 | } 14 | 15 | export interface DRM { 16 | encryptionMode: string; 17 | containerType: string; 18 | jwtToken: string; 19 | url: string; 20 | keySystems: string[]; 21 | } 22 | 23 | export interface Subtitle { 24 | format: Formats; 25 | language: string; 26 | url: string; 27 | } 28 | 29 | export enum Formats { 30 | Scc = 'scc', 31 | Srt = 'srt', 32 | Vtt = 'vtt', 33 | } 34 | -------------------------------------------------------------------------------- /@types/newHidiveSearch.d.ts: -------------------------------------------------------------------------------- 1 | export interface NewHidiveSearch { 2 | results: Result[]; 3 | } 4 | 5 | export interface Result { 6 | hits: Hit[]; 7 | nbHits: number; 8 | page: number; 9 | nbPages: number; 10 | hitsPerPage: number; 11 | exhaustiveNbHits: boolean; 12 | exhaustiveTypo: boolean; 13 | exhaustive: Exhaustive; 14 | query: string; 15 | params: string; 16 | index: string; 17 | renderingContent: object; 18 | processingTimeMS: number; 19 | processingTimingsMS: ProcessingTimingsMS; 20 | serverTimeMS: number; 21 | } 22 | 23 | export interface Exhaustive { 24 | nbHits: boolean; 25 | typo: boolean; 26 | } 27 | 28 | export interface Hit { 29 | type: string; 30 | weight: number; 31 | id: number; 32 | name: string; 33 | description: string; 34 | meta: object; 35 | coverUrl: string; 36 | smallCoverUrl: string; 37 | seasonsCount: number; 38 | tags: string[]; 39 | localisations: HitLocalisations; 40 | ratings: Ratings; 41 | objectID: string; 42 | _highlightResult: HighlightResult; 43 | } 44 | 45 | export interface HighlightResult { 46 | name: Description; 47 | description: Description; 48 | tags: Description[]; 49 | localisations: HighlightResultLocalisations; 50 | } 51 | 52 | export interface Description { 53 | value: string; 54 | matchLevel: string; 55 | matchedWords: string[]; 56 | fullyHighlighted?: boolean; 57 | } 58 | 59 | export interface HighlightResultLocalisations { 60 | en_US: PurpleEnUS; 61 | } 62 | 63 | export interface PurpleEnUS { 64 | title: Description; 65 | description: Description; 66 | } 67 | 68 | export interface HitLocalisations { 69 | [language: string]: HitLocalization; 70 | } 71 | 72 | export interface HitLocalization { 73 | title: string; 74 | description: string; 75 | } 76 | 77 | export interface Ratings { 78 | US: string[]; 79 | } 80 | 81 | export interface ProcessingTimingsMS { 82 | _request: Request; 83 | } 84 | 85 | export interface Request { 86 | queue: number; 87 | roundTrip: number; 88 | } 89 | -------------------------------------------------------------------------------- /@types/newHidiveSeason.d.ts: -------------------------------------------------------------------------------- 1 | export interface NewHidiveSeason { 2 | title: string; 3 | description: string; 4 | longDescription: string; 5 | smallCoverUrl: string; 6 | coverUrl: string; 7 | titleUrl: string; 8 | posterUrl: string; 9 | seasonNumber: number; 10 | episodeCount: number; 11 | displayableTags: any[]; 12 | rating: Rating; 13 | contentRating: Rating; 14 | id: number; 15 | series: Series; 16 | episodes: Episode[]; 17 | paging: Paging; 18 | licences: any[]; 19 | } 20 | 21 | export interface Rating { 22 | rating: string; 23 | descriptors: any[]; 24 | } 25 | 26 | export interface Episode { 27 | accessLevel: string; 28 | availablePurchases?: any[]; 29 | licenceIds?: any[]; 30 | type: string; 31 | id: number; 32 | title: string; 33 | description: string; 34 | thumbnailUrl: string; 35 | posterUrl: string; 36 | duration: number; 37 | favourite: boolean; 38 | contentDownload: ContentDownload; 39 | offlinePlaybackLanguages: string[]; 40 | externalAssetId: string; 41 | subEvents: any[]; 42 | maxHeight: number; 43 | thumbnailsPreview: string; 44 | longDescription: string; 45 | episodeInformation: EpisodeInformation; 46 | categories: string[]; 47 | displayableTags: any[]; 48 | watchStatus: string; 49 | computedReleases: any[]; 50 | } 51 | 52 | export interface ContentDownload { 53 | permission: string; 54 | } 55 | 56 | export interface EpisodeInformation { 57 | seasonNumber: number; 58 | episodeNumber: number; 59 | season: number; 60 | } 61 | 62 | export interface Paging { 63 | moreDataAvailable: boolean; 64 | lastSeen: number; 65 | } 66 | 67 | export interface Series { 68 | seriesId: number; 69 | title: string; 70 | description: string; 71 | longDescription: string; 72 | displayableTags: any[]; 73 | rating: Rating; 74 | contentRating: Rating; 75 | } 76 | 77 | export interface NewHidiveSeriesExtra extends Series { 78 | season: NewHidiveSeason; 79 | } 80 | 81 | export interface NewHidiveEpisodeExtra extends Episode { 82 | titleId: number; 83 | nameLong: string; 84 | seasonTitle: string; 85 | seriesTitle: string; 86 | seriesId?: number; 87 | isSelected: boolean; 88 | jwtToken?: string; 89 | } -------------------------------------------------------------------------------- /@types/newHidiveSeries.d.ts: -------------------------------------------------------------------------------- 1 | export interface NewHidiveSeries { 2 | id: number; 3 | title: string; 4 | description: string; 5 | longDescription: string; 6 | smallCoverUrl: string; 7 | coverUrl: string; 8 | titleUrl: string; 9 | posterUrl: string; 10 | seasons: Season[]; 11 | rating: Rating; 12 | contentRating: Rating; 13 | displayableTags: any[]; 14 | paging: Paging; 15 | } 16 | 17 | export interface Rating { 18 | rating: string; 19 | descriptors: any[]; 20 | } 21 | 22 | export interface Paging { 23 | moreDataAvailable: boolean; 24 | lastSeen: number; 25 | } 26 | 27 | export interface Season { 28 | title: string; 29 | description: string; 30 | longDescription: string; 31 | seasonNumber: number; 32 | episodeCount: number; 33 | displayableTags: any[]; 34 | id: number; 35 | } 36 | -------------------------------------------------------------------------------- /@types/pkg.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'pkg' { 2 | export async function exec(config: string[]); 3 | } -------------------------------------------------------------------------------- /@types/playbackData.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by https://quicktype.io 2 | export interface PlaybackData { 3 | total: number; 4 | data: { [key: string]: { [key: string]: StreamDetails } }[]; 5 | meta: Meta; 6 | } 7 | 8 | export interface StreamList { 9 | download_hls: CrunchyStreams; 10 | drm_adaptive_hls: CrunchyStreams; 11 | multitrack_adaptive_hls_v2: CrunchyStreams; 12 | vo_adaptive_hls: CrunchyStreams; 13 | vo_drm_adaptive_hls: CrunchyStreams; 14 | adaptive_hls: CrunchyStreams; 15 | drm_download_dash: CrunchyStreams; 16 | drm_download_hls: CrunchyStreams; 17 | drm_multitrack_adaptive_hls_v2: CrunchyStreams; 18 | vo_drm_adaptive_dash: CrunchyStreams; 19 | adaptive_dash: CrunchyStreams; 20 | urls: CrunchyStreams; 21 | vo_adaptive_dash: CrunchyStreams; 22 | download_dash: CrunchyStreams; 23 | drm_adaptive_dash: CrunchyStreams; 24 | } 25 | 26 | export interface CrunchyStreams { 27 | '': StreamDetails; 28 | 'en-US'?: StreamDetails; 29 | 'es-LA'?: StreamDetails; 30 | 'es-419'?: StreamDetails; 31 | 'es-ES'?: StreamDetails; 32 | 'pt-BR'?: StreamDetails; 33 | 'fr-FR'?: StreamDetails; 34 | 'de-DE'?: StreamDetails; 35 | 'ar-ME'?: StreamDetails; 36 | 'ar-SA'?: StreamDetails; 37 | 'it-IT'?: StreamDetails; 38 | 'ru-RU'?: StreamDetails; 39 | 'tr-TR'?: StreamDetails; 40 | 'hi-IN'?: StreamDetails; 41 | 'zh-CN'?: StreamDetails; 42 | 'ko-KR'?: StreamDetails; 43 | 'ja-JP'?: StreamDetails; 44 | [string: string]: StreamDetails; 45 | } 46 | 47 | export interface StreamDetails { 48 | //hardsub_locale: Locale; 49 | hardsub_locale: string; 50 | url: string; 51 | hardsub_lang?: string; 52 | audio_lang?: string; 53 | type?: string; 54 | } 55 | export interface Meta { 56 | media_id: string; 57 | subtitles: Subtitles; 58 | bifs: string[]; 59 | versions: Version[]; 60 | audio_locale: Locale; 61 | closed_captions: Subtitles; 62 | captions: Subtitles; 63 | } 64 | 65 | export interface Subtitles { 66 | ''?: SubtitleInfo; 67 | 'en-US'?: SubtitleInfo; 68 | 'es-LA'?: SubtitleInfo; 69 | 'es-419'?: SubtitleInfo; 70 | 'es-ES'?: SubtitleInfo; 71 | 'pt-BR'?: SubtitleInfo; 72 | 'fr-FR'?: SubtitleInfo; 73 | 'de-DE'?: SubtitleInfo; 74 | 'ar-ME'?: SubtitleInfo; 75 | 'ar-SA'?: SubtitleInfo; 76 | 'it-IT'?: SubtitleInfo; 77 | 'ru-RU'?: SubtitleInfo; 78 | 'tr-TR'?: SubtitleInfo; 79 | 'hi-IN'?: SubtitleInfo; 80 | 'zh-CN'?: SubtitleInfo; 81 | 'ko-KR'?: SubtitleInfo; 82 | 'ja-JP'?: SubtitleInfo; 83 | } 84 | 85 | 86 | export interface SubtitleInfo { 87 | format: string; 88 | locale: Locale; 89 | url: string; 90 | } 91 | export interface Version { 92 | audio_locale: Locale; 93 | guid: string; 94 | is_premium_only: boolean; 95 | media_guid: string; 96 | original: boolean; 97 | season_guid: string; 98 | variant: string; 99 | } 100 | 101 | export enum Locale { 102 | default = '', 103 | enUS = 'en-US', 104 | esLA = 'es-LA', 105 | es419 = 'es-419', 106 | esES = 'es-ES', 107 | ptBR = 'pt-BR', 108 | frFR = 'fr-FR', 109 | deDE = 'de-DE', 110 | arME = 'ar-ME', 111 | arSA = 'ar-SA', 112 | itIT = 'it-IT', 113 | ruRU = 'ru-RU', 114 | trTR = 'tr-TR', 115 | hiIN = 'hi-IN', 116 | zhCN = 'zh-CN', 117 | koKR = 'ko-KR', 118 | jaJP = 'ja-JP', 119 | } -------------------------------------------------------------------------------- /@types/randomEvents.d.ts: -------------------------------------------------------------------------------- 1 | import { ExtendedProgress, QueueItem } from './messageHandler'; 2 | 3 | export type RandomEvents = { 4 | progress: ExtendedProgress, 5 | finish: undefined, 6 | queueChange: QueueItem[], 7 | current: QueueItem|undefined 8 | } 9 | 10 | export interface RandomEvent { 11 | name: T, 12 | data: RandomEvents[T] 13 | } 14 | 15 | export type Handler = (data: RandomEvent) => unknown; -------------------------------------------------------------------------------- /@types/removeNPMAbsolutePaths.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'removeNPMAbsolutePaths' { 2 | export default async function modulesCleanup(path: string); 3 | } -------------------------------------------------------------------------------- /@types/serviceClassInterface.d.ts: -------------------------------------------------------------------------------- 1 | export interface ServiceClass { 2 | cli: () => Promise 3 | } -------------------------------------------------------------------------------- /@types/streamData.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by https://quicktype.io 2 | 3 | export interface StreamData { 4 | items: Item[]; 5 | watchHistorySaveInterval: number; 6 | errors?: Error[] 7 | } 8 | 9 | export interface Error { 10 | detail: string, 11 | code: number 12 | } 13 | 14 | export interface Item { 15 | src: string; 16 | kind: string; 17 | isPromo: boolean; 18 | videoType: string; 19 | aips: Aip[]; 20 | experienceId: string; 21 | showAds: boolean; 22 | id: number; 23 | } 24 | 25 | export interface Aip { 26 | out: number; 27 | in: number; 28 | } 29 | -------------------------------------------------------------------------------- /@types/updateFile.d.ts: -------------------------------------------------------------------------------- 1 | export type UpdateFile = { 2 | lastCheck: number, 3 | nextCheck: number 4 | } -------------------------------------------------------------------------------- /@types/ws.d.ts: -------------------------------------------------------------------------------- 1 | import { GUIConfig } from '../modules/module.cfg-loader'; 2 | import { AuthResponse, CheckTokenResponse, EpisodeListResponse, FolderTypes, QueueItem, ResolveItemsData, SearchData, SearchResponse } from './messageHandler'; 3 | 4 | export type WSMessage = { 5 | name: T, 6 | data: MessageTypes[T][P] 7 | } 8 | 9 | export type WSMessageWithID = WSMessage & { 10 | id: string 11 | } 12 | 13 | export type UnknownWSMessage = { 14 | name: keyof MessageTypes, 15 | data: MessageTypes[keyof MessageTypes][0], 16 | id: string 17 | } 18 | 19 | export type MessageTypes = { 20 | 'auth': [AuthData, AuthResponse], 21 | 'version': [undefined, string], 22 | 'checkToken': [undefined, CheckTokenResponse], 23 | 'search': [SearchData, SearchResponse], 24 | 'default': [string, unknown], 25 | 'availableDubCodes': [undefined, string[]], 26 | 'availableSubCodes': [undefined, string[]], 27 | 'resolveItems': [ResolveItemsData, boolean], 28 | 'listEpisodes': [string, EpisodeListResponse], 29 | 'downloadItem': [QueueItem, undefined], 30 | 'isDownloading': [undefined, boolean], 31 | 'openFolder': [FolderTypes, undefined], 32 | 'changeProvider': [undefined, boolean], 33 | 'type': [undefined, 'crunchy'|'hidive'|'ao'|'adn'|undefined], 34 | 'setup': ['crunchy'|'hidive'|'ao'|'adn'|undefined, undefined], 35 | 'openFile': [[FolderTypes, string], undefined], 36 | 'openURL': [string, undefined], 37 | 'isSetup': [undefined, boolean], 38 | 'setupServer': [GUIConfig, boolean], 39 | 'requirePassword': [undefined, boolean], 40 | 'getQueue': [undefined, QueueItem[]], 41 | 'removeFromQueue': [number, undefined], 42 | 'clearQueue': [undefined, undefined], 43 | 'setDownloadQueue': [boolean, undefined], 44 | 'getDownloadQueue': [undefined, boolean] 45 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node AS builder 2 | WORKDIR "/app" 3 | COPY . . 4 | 5 | # Install 7z for packaging 6 | 7 | RUN apt-get update 8 | RUN apt-get install p7zip-full -y 9 | 10 | # Update bin-path for docker/linux 11 | 12 | RUN echo 'ffmpeg: "./bin/ffmpeg/ffmpeg"\nmkvmerge: "./bin/mkvtoolnix/mkvmerge"' > /app/config/bin-path.yml 13 | 14 | #Build AniDL 15 | 16 | RUN npm install -g pnpm 17 | RUN pnpm i 18 | RUN pnpm run build-linux-gui 19 | 20 | # Move build to new Clean Image 21 | 22 | FROM node 23 | WORKDIR "/app" 24 | COPY --from=builder /app/lib/_builds/multi-downloader-nx-linux-x64-gui ./ 25 | 26 | # Install mkvmerge and ffmpeg 27 | 28 | RUN mkdir -p /app/bin/mkvtoolnix 29 | RUN mkdir -p /app/bin/ffmpeg 30 | 31 | RUN apt-get update 32 | RUN apt-get install xdg-utils -y 33 | RUN apt-get install mkvtoolnix -y 34 | #RUN apt-get install ffmpeg -y 35 | 36 | RUN mv /usr/bin/mkvmerge /app/bin/mkvtoolnix/mkvmerge 37 | #RUN mv /usr/bin/ffmpeg /app/bin/ffmpeg/ffmpeg 38 | 39 | CMD [ "/app/aniDL" ] -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2021 AniDL 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Todo/Future Ideas list 2 | 3 | - [ ] Look into implementing wvd file support 4 | - [ ] Merge sync branch with latest master 5 | - [ ] Finish implementing old algorithm 6 | - [ ] Look into adding suggested algorithm [#599](https://github.com/anidl/multi-downloader-nx/issues/599) 7 | - [ ] Remove Funimation 8 | - [ ] Remove old hidive API or find a way to make it work 9 | - [ ] Look into adding other services 10 | - [ ] Refactor downloading code 11 | - [ ] Allow audio and video download at the same time 12 | - [ ] Reduce/Refactor the amount of duplicate/boilerplate code required 13 | - [ ] Create a generic service class for the CLI with set inputs/outputs 14 | - [ ] Modularize site modules to ease addition of new sites 15 | - [ ] Create generic MPD/M3U8 playlist downloader 16 | -------------------------------------------------------------------------------- /config/bin-path.yml: -------------------------------------------------------------------------------- 1 | ffmpeg: "ffmpeg.exe" 2 | mkvmerge: "mkvmerge.exe" 3 | ffprobe: "ffprobe.exe" 4 | mp4decrypt: "mp4decrypt.exe" 5 | shaka: "shaka-packager.exe" 6 | -------------------------------------------------------------------------------- /config/cli-defaults.yml: -------------------------------------------------------------------------------- 1 | # Set the quality of the stream, 0 is highest available. 2 | q: 0 3 | # Set which stream to use 4 | kstream: 1 5 | # Set which server to use 6 | server: 1 7 | # How many parts to download at once. Increasing may improve download speed. 8 | partsize: 10 9 | # Set whether to mux into an mp4 or not. Not recommended. 10 | mp4: false 11 | # Whether to delete any created files or not 12 | nocleanup: false 13 | # Whether to only download the relevant video once 14 | dlVideoOnce: false 15 | # Whether to keep all downloaded videos or only a single copy 16 | keepAllVideos: false 17 | # What to use as the file name template 18 | fileName: "[${service}] ${showTitle} - S${season}E${episode} [${height}p]" 19 | # What Audio languages to download 20 | dubLang: ["jpn"] 21 | # What Subtitle languages to download 22 | dlsubs: ["all"] 23 | # What language Audio to set as default 24 | defaultAudio: "jpn" -------------------------------------------------------------------------------- /config/dir-path.yml: -------------------------------------------------------------------------------- 1 | content: ./videos/ 2 | fonts: ./fonts/ 3 | -------------------------------------------------------------------------------- /config/gui.yml: -------------------------------------------------------------------------------- 1 | port: 3000 2 | -------------------------------------------------------------------------------- /dev.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process'); 2 | const path = require('path'); 3 | const toRun = process.argv.slice(2).join(' ').split('---'); 4 | 5 | const waitForProcess = async (proc) => { 6 | return new Promise((resolve, reject) => { 7 | proc.stdout?.on('data', (data) => process.stdout.write(data)); 8 | proc.stderr?.on('data', (data) => process.stderr.write(data)); 9 | proc.on('close', resolve); 10 | proc.on('error', reject); 11 | }); 12 | }; 13 | 14 | (async () => { 15 | await waitForProcess(exec('pnpm run tsc test false')); 16 | for (let command of toRun) { 17 | await waitForProcess(exec(`node index.js --service hidive ${command}`, { 18 | cwd: path.join(__dirname, 'lib') 19 | })); 20 | } 21 | })(); -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Change Log 2 | 3 | This changelog is out of date and wont be continued. Please see the releases comments, or if not present the commit comments. 4 | 5 | ### 4.7.0 (unreleased) 6 | - Change subtitles parser from ttml to vtt 7 | - Improve help command 8 | - Update modules 9 | 10 | #### Known issues: 11 | - Proxy not supported 12 | 13 | ### 4.6.1 (2020/09/19) 14 | - Update modules 15 | 16 | #### Known issues: 17 | - Proxy not supported 18 | 19 | ### 4.6.0 (2020/06/03) 20 | - Bug fixes and improvements 21 | 22 | #### Known issues: 23 | - Proxy not supported 24 | 25 | ### 4.5.1 (2020/03/10) 26 | - Better binary files handling 27 | - Binary build for windows 28 | 29 | #### Known issues: 30 | - Proxy not supported 31 | 32 | ### 4.5.0 (2020/01/21) 33 | - Resume downloading 34 | 35 | #### Known issues: 36 | - Proxy not supported 37 | 38 | ### 4.4.2 (2019/07/21) 39 | - Better proxy handling for stream download 40 | 41 | ### 4.4.1 (2019/07/21) 42 | - Fixed proxy for stream download 43 | 44 | ### 4.4.0 (2019/06/04) 45 | - Added `--novids` option (Thanks to @subdiox) 46 | - Update modules 47 | 48 | ### 4.3.2 (2019/05/09) 49 | - Code improvements 50 | - Fix `hls-download` error printing 51 | 52 | ### 4.3.1 (2019/05/09) 53 | - Fix auto detection max quality (Regression in d7d280c) 54 | 55 | ### 4.3.0 (2019/05/09) 56 | - Better server selection (Closes #42) 57 | 58 | ### 4.2.1 (2019/05/04) 59 | - Filter duplicate urls for cloudfront.net (Closes #40) 60 | 61 | ### 4.2.0 (2019/05/02) 62 | - Replace `request` module with `got` 63 | - Changed proxy cli options 64 | - Changed `login` option name to `auth` 65 | - Changed `hls-download` parallel download configuration from 5 parts to 10 66 | - Update modules 67 | 68 | ### 4.1.0 (2019/04/05) 69 | - CLI options for login moved to CUI 70 | - Removed showing set token at startup 71 | 72 | ### 4.0.5 (2019/02/09) 73 | - Fix downloading shows with autoselect max quality 74 | 75 | ### 4.0.4 (2019/01/26) 76 | - Fix search when shows not found 77 | - Update modules 78 | 79 | ### 4.0.3 (2018/12/06) 80 | - Select only non-encrypted (HLS) streams, encrypted streams is MPEG-DASH 81 | 82 | ### 4.0.2 (2018/11/25) 83 | - Fix typos and update modules 84 | 85 | ### 4.0.1 (2018/11/23) 86 | - Code refactoring and small fixes 87 | 88 | ### 4.0.0 RC 1 (2018/11/17) 89 | - Select range of episodes using hyphen-sequence 90 | - Skip muxing if executables not found 91 | - Fixed typos and duplicate options 92 | 93 | ### 4.0.0 Beta 2 (2018/11/12) 94 | - Select alternative server 95 | - Updated readme 96 | 97 | ### 4.0.0 Beta 1 (2018/11/10) 98 | - Rearrange folders structure 99 | - Configuration changed to yaml format 100 | - Muxing changed to MKV by default 101 | - tsMuxeR+mp4box replaced with FFMPEG 102 | - Updated commands help and readme 103 | - Fixed typos and duplicate options 104 | - `ttml2srt` moved to separate module 105 | - Drop `m3u8-stream-list` module 106 | - Code improvements 107 | 108 | ### 3.2.8 (2018/06/16) 109 | - Fix video request when token not specified 110 | 111 | ### 3.2.7 (2018/06/15) 112 | - Update modules 113 | 114 | ### 3.2.6 (2018/02/18) 115 | - Fix commands help 116 | 117 | ### 3.2.5 (2018/02/12) 118 | - Fixes and update modules 119 | 120 | ### 3.2.4 (2018/02/01) 121 | - Update modules 122 | 123 | ### 3.2.3 (2018/01/31) 124 | - Rearrange folders structure 125 | 126 | ### 3.2.2 (2018/01/16) 127 | - Update modules 128 | 129 | ### 3.2.1 (2018/01/16) 130 | - Update modules 131 | - Small fixes 132 | 133 | ### 3.2.0 (2018/01/16) 134 | - `hls-download` module moved to independent module 135 | - Auth for socks proxy 136 | 137 | ### 3.1.0 (2017/12/30) 138 | - Convert DXFP (TTML) subtitles to SRT format 139 | 140 | ### 3.0.1 (2017/12/05) 141 | - Check subtitles availability 142 | - Download subtitles in SRT format instead of VTT 143 | - Extended hls download progress info 144 | 145 | ### 3.0.0 Beta 3 (2017/12/03) 146 | - Restored MKV and MP4 muxing 147 | - Convert VTT subtitles to SRT format 148 | 149 | ### 3.0.0 Beta 2 (2017/10/18) 150 | - Fix video downloading 151 | 152 | ### 3.0.0 Beta 1 (2017/10/17) 153 | - Major code changes and improvements 154 | - Drop Streamlink and added own module for hls download 155 | 156 | ### 2.5.0 (2017/09/04) 157 | - `nosubs` option 158 | - Request video with app api 159 | 160 | ### 2.4.1 (2017/09/02) 161 | - Fixed typo in package.json 162 | - Fix #11: URL for getting video stream url was changed 163 | 164 | ### 2.4.0 (2017/07/04) 165 | - IPv4 Socks5 proxy support 166 | 167 | ### 2.3.3 (2017/06/19) 168 | - Removed forgotten debug code 169 | 170 | ### 2.3.2 (2017/06/19) 171 | - Fix #5: Script fails to multiplex unique file names 172 | 173 | ### 2.3.1 (2017/04/29) 174 | - Code improvements 175 | 176 | ### 2.3.0 (2017/04/27) 177 | - Code improvements 178 | 179 | ### 2.2.5 (2017/04/17) 180 | - Minor code improvements and fixes 181 | 182 | ### 2.1.4 (2017/04/10) 183 | - Minor changes 184 | 185 | ### 2.1.3 (2017/04/10) 186 | - Minor changes and fixes 187 | 188 | ### 2.1.2 (2017/04/10) 189 | - Fix config path 190 | 191 | ### 2.1.1 (2017/04/10) 192 | - Minor text changes 193 | - Fix config 194 | - Minor changes 195 | 196 | ### 2.1.0 (2017/04/10) 197 | - First stable release 198 | 199 | ### 2.0.0 Beta (lost in time) 200 | - First public release -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from '@eslint/js'; 4 | import tseslint from 'typescript-eslint'; 5 | 6 | export default tseslint.config( 7 | eslint.configs.recommended, 8 | ...tseslint.configs.recommended, 9 | { 10 | rules: { 11 | 'no-console': 2, 12 | 'react/prop-types': 0, 13 | 'react-hooks/exhaustive-deps': 0, 14 | '@typescript-eslint/no-explicit-any': 'off', 15 | '@typescript-eslint/no-unsafe-declaration-merging': 'warn', 16 | '@typescript-eslint/no-unused-vars' : 'warn', 17 | '@typescript-eslint/no-unused-expressions': 'warn', 18 | 'indent': [ 19 | 'error', 20 | 2 21 | ], 22 | 'linebreak-style': [ 23 | 'warn', 24 | 'windows' 25 | ], 26 | 'quotes': [ 27 | 'error', 28 | 'single' 29 | ], 30 | 'semi': [ 31 | 'error', 32 | 'always' 33 | ] 34 | }, 35 | languageOptions: { 36 | parserOptions: { 37 | ecmaFeatures: { 38 | jsx: true, 39 | }, 40 | ecmaVersion: 2020, 41 | sourceType: 'module' 42 | }, 43 | parser: tseslint.parser 44 | } 45 | }, 46 | { 47 | ignores: [ 48 | '**/lib', 49 | '**/videos/*.ts', 50 | '**/build', 51 | 'dev.js', 52 | 'tsc.ts' 53 | ] 54 | }, 55 | { 56 | files: ['gui/react/**/*'], 57 | rules: { 58 | 'no-console': 0 59 | } 60 | } 61 | ); -------------------------------------------------------------------------------- /gui.ts: -------------------------------------------------------------------------------- 1 | process.env.isGUI = 'true'; 2 | import './modules/log'; 3 | import './gui/server/index'; -------------------------------------------------------------------------------- /gui/react/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env","@babel/preset-react", "@babel/preset-typescript"] 3 | } -------------------------------------------------------------------------------- /gui/react/.env: -------------------------------------------------------------------------------- 1 | PORT=3002 2 | CI=false -------------------------------------------------------------------------------- /gui/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "anidl-gui", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "^11.14.0", 7 | "@emotion/styled": "^11.14.0", 8 | "@mui/icons-material": "^7.1.0", 9 | "@mui/lab": "7.0.0-beta.12", 10 | "@mui/material": "^7.1.0", 11 | "concurrently": "^9.1.2", 12 | "notistack": "^3.0.2", 13 | "react": "^19.1.0", 14 | "react-dom": "^19.1.0", 15 | "typescript": "^5.8.3", 16 | "uuid": "^11.1.0", 17 | "ws": "^8.18.2" 18 | }, 19 | "devDependencies": { 20 | "@babel/cli": "^7.27.2", 21 | "@babel/core": "^7.27.1", 22 | "@babel/preset-env": "^7.27.2", 23 | "@babel/preset-react": "^7.27.1", 24 | "@babel/preset-typescript": "^7.27.1", 25 | "@types/node": "^22.15.17", 26 | "@types/react": "^19.1.3", 27 | "@types/react-dom": "^19.1.3", 28 | "@types/uuid": "^10.0.0", 29 | "babel-loader": "^10.0.0", 30 | "css-loader": "^7.1.2", 31 | "html-webpack-plugin": "^5.6.3", 32 | "style-loader": "^4.0.0", 33 | "ts-node": "^10.9.2", 34 | "webpack": "^5.99.8", 35 | "webpack-cli": "^6.0.1", 36 | "webpack-dev-server": "^5.2.1" 37 | }, 38 | "proxy": "http://localhost:3000", 39 | "scripts": { 40 | "build": "npx tsc && npx webpack", 41 | "start": "npx concurrently -k npm:frontend npm:backend", 42 | "frontend": "npx webpack-dev-server", 43 | "backend": "npx ts-node -T ../../gui.ts" 44 | }, 45 | "browserslist": { 46 | "production": [ 47 | ">0.2%", 48 | "not dead", 49 | "not op_mini all" 50 | ], 51 | "development": [ 52 | "last 1 chrome version", 53 | "last 1 firefox version", 54 | "last 1 safari version" 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /gui/react/public/favicon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anidl/multi-downloader-nx/0133a774b6a702481d1d671908d80031a58b2d51/gui/react/public/favicon.webp -------------------------------------------------------------------------------- /gui/react/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Multi Downloader 5 | 6 | 7 | 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /gui/react/public/notFound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anidl/multi-downloader-nx/0133a774b6a702481d1d671908d80031a58b2d51/gui/react/public/notFound.png -------------------------------------------------------------------------------- /gui/react/src/@types/FC.d.ts: -------------------------------------------------------------------------------- 1 | type FCWithChildren = React.FC<{ 2 | children?: React.ReactNode[]|React.ReactNode 3 | } & T> -------------------------------------------------------------------------------- /gui/react/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Layout from './Layout'; 3 | 4 | const App: React.FC = () => { 5 | return ( 6 | 7 | ); 8 | }; 9 | 10 | export default App; 11 | -------------------------------------------------------------------------------- /gui/react/src/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AuthButton from './components/AuthButton'; 3 | import { Box, Button } from '@mui/material'; 4 | import MainFrame from './components/MainFrame/MainFrame'; 5 | import LogoutButton from './components/LogoutButton'; 6 | import AddToQueue from './components/AddToQueue/AddToQueue'; 7 | import { messageChannelContext } from './provider/MessageChannel'; 8 | import { ClearAll, Folder } from '@mui/icons-material'; 9 | import StartQueueButton from './components/StartQueue'; 10 | import MenuBar from './components/MenuBar/MenuBar'; 11 | 12 | const Layout: React.FC = () => { 13 | 14 | const messageHandler = React.useContext(messageChannelContext); 15 | 16 | return 17 | 18 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ; 36 | }; 37 | 38 | export default Layout; -------------------------------------------------------------------------------- /gui/react/src/Style.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Container, Box, ThemeProvider, createTheme, Theme } from '@mui/material'; 3 | 4 | const makeTheme = (mode: 'dark'|'light') : Partial => { 5 | return createTheme({ 6 | palette: { 7 | mode, 8 | }, 9 | }); 10 | }; 11 | 12 | const Style: FCWithChildren = ({children}) => { 13 | return 14 | 15 | {children} 16 | ; 17 | }; 18 | 19 | export default Style; -------------------------------------------------------------------------------- /gui/react/src/components/AddToQueue/AddToQueue.tsx: -------------------------------------------------------------------------------- 1 | import { Add } from '@mui/icons-material'; 2 | import { Box, Button, Dialog, Divider, Typography } from '@mui/material'; 3 | import React from 'react'; 4 | import DownloadSelector from './DownloadSelector/DownloadSelector'; 5 | import EpisodeListing from './DownloadSelector/Listing/EpisodeListing'; 6 | import SearchBox from './SearchBox/SearchBox'; 7 | 8 | const AddToQueue: React.FC = () => { 9 | const [isOpen, setOpen] = React.useState(false); 10 | 11 | return 12 | 13 | setOpen(false)} maxWidth='md' PaperProps={{ elevation:4 }}> 14 | 15 | 16 | 17 | setOpen(false)} /> 18 | 19 | 20 | 24 | ; 25 | }; 26 | 27 | export default AddToQueue; -------------------------------------------------------------------------------- /gui/react/src/components/AddToQueue/SearchBox/SearchBox.css: -------------------------------------------------------------------------------- 1 | .listitem-hover:hover { 2 | -webkit-filter: brightness(70%); 3 | filter: brightness(70%); 4 | } 5 | 6 | .listitem-hover { 7 | transition: filter 0.1s ease-in; 8 | } -------------------------------------------------------------------------------- /gui/react/src/components/AuthButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField } from '@mui/material'; 2 | import { Check, Close } from '@mui/icons-material'; 3 | import React from 'react'; 4 | import { messageChannelContext } from '../provider/MessageChannel'; 5 | import Require from './Require'; 6 | import { useSnackbar } from 'notistack'; 7 | 8 | const AuthButton: React.FC = () => { 9 | const snackbar = useSnackbar(); 10 | 11 | const [open, setOpen] = React.useState(false); 12 | 13 | const [username, setUsername] = React.useState(''); 14 | const [password, setPassword] = React.useState(''); 15 | 16 | const [usernameError, setUsernameError] = React.useState(false); 17 | const [passwordError, setPasswordError] = React.useState(false); 18 | 19 | const messageChannel = React.useContext(messageChannelContext); 20 | 21 | const [loading, setLoading] = React.useState(false); 22 | const [error, setError] = React.useState(undefined); 23 | const [authed, setAuthed] = React.useState(false); 24 | 25 | const checkAuth = async () => { 26 | setAuthed((await messageChannel?.checkToken())?.isOk ?? false); 27 | }; 28 | 29 | React.useEffect(() => { checkAuth(); }, []); 30 | 31 | const handleSubmit = async () => { 32 | if (!messageChannel) 33 | throw new Error('Invalid state'); //The components to confirm only render if the messageChannel is not undefinded 34 | if (username.trim().length === 0) 35 | return setUsernameError(true); 36 | if (password.trim().length === 0) 37 | return setPasswordError(true); 38 | setUsernameError(false); 39 | setPasswordError(false); 40 | setLoading(true); 41 | 42 | const res = await messageChannel.auth({ username, password }); 43 | if (res.isOk) { 44 | setOpen(false); 45 | snackbar.enqueueSnackbar('Logged in', { 46 | variant: 'success' 47 | }); 48 | setUsername(''); 49 | setPassword(''); 50 | } else { 51 | setError(res.reason); 52 | } 53 | await checkAuth(); 54 | setLoading(false); 55 | }; 56 | 57 | return 58 | 59 | 60 | Error during Authentication 61 | 62 | {error?.name} 63 | {error?.message} 64 | 65 | 66 | 67 | 68 | 69 | Authentication 70 | 71 | 72 | Here, you need to enter your username (most likely your Email) and your password.
73 | These information are not stored anywhere and are only used to authenticate with the service once. 74 |
75 | setUsername(e.target.value)} 86 | disabled={loading} 87 | /> 88 | setPassword(e.target.value)} 99 | disabled={loading} 100 | /> 101 |
102 | 103 | {loading && } 104 | 105 | 106 | 107 |
108 | 109 |
; 110 | }; 111 | 112 | export default AuthButton; -------------------------------------------------------------------------------- /gui/react/src/components/LogoutButton.tsx: -------------------------------------------------------------------------------- 1 | import { ExitToApp } from '@mui/icons-material'; 2 | import { Button } from '@mui/material'; 3 | import React from 'react'; 4 | import useStore from '../hooks/useStore'; 5 | import { messageChannelContext } from '../provider/MessageChannel'; 6 | import Require from './Require'; 7 | 8 | const LogoutButton: React.FC = () => { 9 | const messageChannel = React.useContext(messageChannelContext); 10 | const [, dispatch] = useStore(); 11 | 12 | const logout = async () => { 13 | if (await messageChannel?.isDownloading()) 14 | return alert('You are currently downloading. Please finish the download first.'); 15 | if (await messageChannel?.logout()) 16 | dispatch({ 17 | type: 'service', 18 | payload: undefined 19 | }); 20 | else 21 | alert('Unable to change service'); 22 | }; 23 | 24 | return 25 | 33 | ; 34 | 35 | }; 36 | 37 | export default LogoutButton; -------------------------------------------------------------------------------- /gui/react/src/components/MainFrame/DownloadManager/DownloadManager.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ExtendedProgress, QueueItem } from '../../../../../../@types/messageHandler'; 3 | import { RandomEvent } from '../../../../../../@types/randomEvents'; 4 | import { messageChannelContext } from '../../../provider/MessageChannel'; 5 | 6 | const useDownloadManager = () => { 7 | const messageHandler = React.useContext(messageChannelContext); 8 | 9 | const [progressData, setProgressData] = React.useState(); 10 | const [current, setCurrent] = React.useState(); 11 | 12 | React.useEffect(() => { 13 | const handler = (ev: RandomEvent<'progress'>) => { 14 | console.log(ev.data); 15 | setProgressData(ev.data); 16 | }; 17 | 18 | const currentHandler = (ev: RandomEvent<'current'>) => { 19 | setCurrent(ev.data); 20 | }; 21 | 22 | const finishHandler = () => { 23 | setProgressData(undefined); 24 | }; 25 | 26 | messageHandler?.randomEvents.on('progress', handler); 27 | messageHandler?.randomEvents.on('current', currentHandler); 28 | messageHandler?.randomEvents.on('finish', finishHandler); 29 | 30 | return () => { 31 | messageHandler?.randomEvents.removeListener('progress', handler); 32 | messageHandler?.randomEvents.removeListener('finish', finishHandler); 33 | messageHandler?.randomEvents.removeListener('current', currentHandler); 34 | }; 35 | }, [messageHandler]); 36 | 37 | return { data: progressData, current}; 38 | }; 39 | 40 | export default useDownloadManager; -------------------------------------------------------------------------------- /gui/react/src/components/MainFrame/MainFrame.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from '@mui/material'; 2 | import React from 'react'; 3 | import Queue from './Queue/Queue'; 4 | 5 | const MainFrame: React.FC = () => { 6 | return 7 | 8 | ; 9 | }; 10 | 11 | export default MainFrame; -------------------------------------------------------------------------------- /gui/react/src/components/MenuBar/MenuBar.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Button, Menu, MenuItem, Typography } from '@mui/material'; 2 | import React from 'react'; 3 | import { messageChannelContext } from '../../provider/MessageChannel'; 4 | import useStore from '../../hooks/useStore'; 5 | import { StoreState } from '../../provider/Store'; 6 | 7 | const MenuBar: React.FC = () => { 8 | const [ openMenu, setMenuOpen ] = React.useState<'settings'|'help'|undefined>(); 9 | const [anchorEl, setAnchorEl] = React.useState(null); 10 | const [store, dispatch] = useStore(); 11 | 12 | const messageChannel = React.useContext(messageChannelContext); 13 | 14 | React.useEffect(() => { 15 | (async () => { 16 | if (!messageChannel || store.version !== '') 17 | return; 18 | dispatch({ 19 | type: 'version', 20 | payload: await messageChannel.version() 21 | }); 22 | })(); 23 | }, [messageChannel]); 24 | 25 | const transformService = (service: StoreState['service']) => { 26 | switch(service) { 27 | case 'crunchy': 28 | return 'Crunchyroll'; 29 | case 'hidive': 30 | return 'Hidive'; 31 | case 'ao': 32 | return 'AnimeOnegai'; 33 | case 'adn': 34 | return 'AnimationDigitalNetwork'; 35 | } 36 | }; 37 | 38 | const msg = React.useContext(messageChannelContext); 39 | 40 | const handleClick = (event: React.MouseEvent, n: 'settings'|'help') => { 41 | setAnchorEl(event.currentTarget); 42 | setMenuOpen(n); 43 | }; 44 | const handleClose = () => { 45 | setAnchorEl(null); 46 | setMenuOpen(undefined); 47 | }; 48 | 49 | if (!msg) 50 | return <>; 51 | 52 | return 53 | 54 | 57 | 60 | 61 | 62 | { 63 | msg.openFolder('config'); 64 | handleClose(); 65 | }}> 66 | Open settings folder 67 | 68 | { 69 | msg.openFile(['config', 'bin-path.yml']); 70 | handleClose(); 71 | }}> 72 | Open FFmpeg/Mkvmerge file 73 | 74 | { 75 | msg.openFile(['config', 'cli-defaults.yml']); 76 | handleClose(); 77 | }}> 78 | Open advanced options 79 | 80 | { 81 | msg.openFolder('content'); 82 | handleClose(); 83 | }}> 84 | Open output path 85 | 86 | 87 | 88 | { 89 | msg.openURL('https://github.com/anidl/multi-downloader-nx'); 90 | handleClose(); 91 | }}> 92 | GitHub 93 | 94 | { 95 | msg.openURL('https://github.com/anidl/multi-downloader-nx/issues/new?assignees=AnimeDL,AnidlSupport&labels=bug&template=bug.yml&title=BUG'); 96 | handleClose(); 97 | }}> 98 | Report a bug 99 | 100 | { 101 | msg.openURL('https://github.com/anidl/multi-downloader-nx/graphs/contributors'); 102 | handleClose(); 103 | }}> 104 | Contributors 105 | 106 | { 107 | msg.openURL('https://discord.gg/qEpbWen5vq'); 108 | handleClose(); 109 | }}> 110 | Discord 111 | 112 | { 113 | handleClose(); 114 | }}> 115 | Version: {store.version} 116 | 117 | 118 | 119 | {transformService(store.service)} 120 | 121 | ; 122 | }; 123 | 124 | export default MenuBar; 125 | -------------------------------------------------------------------------------- /gui/react/src/components/Require.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Backdrop, CircularProgress } from '@mui/material'; 3 | 4 | export type RequireType = { 5 | value?: T 6 | } 7 | 8 | const Require = (props: React.PropsWithChildren>) => { 9 | return props.value === undefined ? 10 | 11 | : {props.children}; 12 | }; 13 | 14 | export default Require; -------------------------------------------------------------------------------- /gui/react/src/components/StartQueue.tsx: -------------------------------------------------------------------------------- 1 | import { PauseCircleFilled, PlayCircleFilled } from '@mui/icons-material'; 2 | import { Button } from '@mui/material'; 3 | import React from 'react'; 4 | import { messageChannelContext } from '../provider/MessageChannel'; 5 | import Require from './Require'; 6 | 7 | const StartQueueButton: React.FC = () => { 8 | const messageChannel = React.useContext(messageChannelContext); 9 | const [start, setStart] = React.useState(false); 10 | const msg = React.useContext(messageChannelContext); 11 | 12 | React.useEffect(() => { 13 | (async () => { 14 | if (!msg) 15 | return alert('Invalid state: msg not found'); 16 | setStart(await msg.getDownloadQueue()); 17 | })(); 18 | }, []); 19 | 20 | const change = async () => { 21 | if (await messageChannel?.isDownloading()) 22 | alert('The current download will be finished before the queue stops'); 23 | msg?.setDownloadQueue(!start); 24 | setStart(!start); 25 | }; 26 | 27 | return 28 | 38 | ; 39 | 40 | }; 41 | 42 | export default StartQueueButton; -------------------------------------------------------------------------------- /gui/react/src/components/reusable/ContextMenu.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Button, Divider, List, SxProps } from '@mui/material'; 2 | import React from 'react'; 3 | 4 | export type Option = { 5 | text: string, 6 | onClick: () => unknown 7 | } 8 | 9 | export type ContextMenuProps = { 10 | options: ('divider'|Option)[], 11 | popupItem: React.RefObject 12 | } 13 | 14 | const buttonSx: SxProps = { 15 | '&:hover': { 16 | background: 'rgb(0, 30, 60)' 17 | }, 18 | fontSize: '0.7rem', 19 | minHeight: '30px', 20 | justifyContent: 'center', 21 | p: 0 22 | }; 23 | 24 | function ContextMenu(props: ContextMenuProps) { 25 | const [anchor, setAnchor] = React.useState( { x: 0, y: 0 } ); 26 | 27 | const [show, setShow] = React.useState(false); 28 | 29 | React.useEffect(() => { 30 | const { popupItem: ref } = props; 31 | if (ref.current === null) 32 | return; 33 | const listener = (ev: MouseEvent) => { 34 | ev.preventDefault(); 35 | setAnchor({ x: ev.x + 10, y: ev.y + 10 }); 36 | setShow(true); 37 | }; 38 | ref.current.addEventListener('contextmenu', listener); 39 | 40 | return () => { 41 | if (ref.current) 42 | ref.current.removeEventListener('contextmenu', listener); 43 | }; 44 | }, [ props.popupItem ]); 45 | 46 | return show ? 47 | 48 | {props.options.map((item, i) => { 49 | return item === 'divider' ? : 50 | ; 56 | })} 57 | 58 | 61 | 62 | : <>; 63 | } 64 | 65 | export default ContextMenu; 66 | -------------------------------------------------------------------------------- /gui/react/src/components/reusable/LinearProgressWithLabel.tsx: -------------------------------------------------------------------------------- 1 | import { LinearProgressProps, Box, LinearProgress, Typography } from '@mui/material'; 2 | import React from 'react'; 3 | 4 | // The following code has been taken from the mui example 5 | // Thanks for that mui 6 | 7 | export type LinearProgressWithLabelProps = LinearProgressProps & { value: number }; 8 | 9 | const LinearProgressWithLabel: React.FC = (props) => { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | {`${Math.round( 17 | props.value, 18 | )}%`} 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default LinearProgressWithLabel; -------------------------------------------------------------------------------- /gui/react/src/components/reusable/MultiSelect.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormControl, InputLabel, MenuItem, OutlinedInput, Select, Theme, useTheme } from '@mui/material'; 3 | 4 | export type MultiSelectProps = { 5 | values: string[], 6 | selected: string[], 7 | onChange: (values: string[]) => unknown, 8 | title: string, 9 | allOption?: boolean 10 | } 11 | 12 | const ITEM_HEIGHT = 48; 13 | const ITEM_PADDING_TOP = 8; 14 | const MenuProps = { 15 | PaperProps: { 16 | style: { 17 | maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, 18 | width: 250 19 | } 20 | } 21 | }; 22 | 23 | function getStyles(name: string, personName: readonly string[], theme: Theme) { 24 | return { 25 | fontWeight: 26 | (personName ?? []).indexOf(name) === -1 27 | ? theme.typography.fontWeightRegular 28 | : theme.typography.fontWeightMedium 29 | }; 30 | } 31 | 32 | const MultiSelect: React.FC = (props) => { 33 | const theme = useTheme(); 34 | 35 | return
36 | 37 | {props.title} 38 | 70 | 71 |
; 72 | }; 73 | 74 | export default MultiSelect; -------------------------------------------------------------------------------- /gui/react/src/hooks/useStore.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StoreAction, StoreContext, StoreState } from '../provider/Store'; 3 | 4 | const useStore = () => { 5 | const context = React.useContext(StoreContext as unknown as React.Context<[StoreState, React.Dispatch>]>); 6 | if (!context) { 7 | throw new Error('useStore must be used under Store'); 8 | } 9 | return context; 10 | }; 11 | 12 | export default useStore; -------------------------------------------------------------------------------- /gui/react/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import App from './App'; 4 | import ServiceProvider from './provider/ServiceProvider'; 5 | import Style from './Style'; 6 | import MessageChannel from './provider/MessageChannel'; 7 | import { Box, IconButton } from '@mui/material'; 8 | import { CloseOutlined } from '@mui/icons-material'; 9 | import { SnackbarProvider, SnackbarKey } from 'notistack'; 10 | import Store from './provider/Store'; 11 | import ErrorHandler from './provider/ErrorHandler'; 12 | import QueueProvider from './provider/QueueProvider'; 13 | 14 | document.body.style.backgroundColor = 'rgb(0, 30, 60)'; 15 | document.body.style.display = 'flex'; 16 | document.body.style.justifyContent = 'center'; 17 | 18 | const notistackRef = React.createRef(); 19 | const onClickDismiss = (key: SnackbarKey | undefined) => () => { 20 | if (notistackRef.current) 21 | notistackRef.current.closeSnackbar(key); 22 | }; 23 | 24 | const container = document.getElementById('root'); 25 | const root = createRoot(container as HTMLElement); 26 | root.render( 27 | 28 | 29 | ( 32 | 33 | 34 | 35 | )} 36 | > 37 | 48 | 49 | 50 | 51 | ); -------------------------------------------------------------------------------- /gui/react/src/provider/ErrorHandler.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Typography } from '@mui/material'; 2 | import React from 'react'; 3 | 4 | export default class ErrorHandler extends React.Component<{ 5 | children: React.ReactNode|React.ReactNode[] 6 | }, { 7 | error?: { 8 | er: Error, 9 | stack: React.ErrorInfo 10 | } 11 | }> { 12 | 13 | constructor(props: { 14 | children: React.ReactNode|React.ReactNode[] 15 | }) { 16 | super(props); 17 | this.state = { error: undefined }; 18 | } 19 | 20 | componentDidCatch(er: Error, stack: React.ErrorInfo) { 21 | this.setState({ error: { er, stack } }); 22 | } 23 | 24 | render(): React.ReactNode { 25 | return this.state.error ? 26 | 27 | 28 | {`${this.state.error.er.name}: ${this.state.error.er.message}`} 29 |
30 | {this.state.error.stack.componentStack?.split('\n').map(a => { 31 | return <> 32 | {a} 33 |
34 | ; 35 | })} 36 |
37 |
: this.props.children; 38 | } 39 | } -------------------------------------------------------------------------------- /gui/react/src/provider/QueueProvider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { QueueItem } from '../../../../@types/messageHandler'; 3 | import { messageChannelContext } from './MessageChannel'; 4 | import { RandomEvent } from '../../../../@types/randomEvents'; 5 | 6 | export const queueContext = React.createContext([]); 7 | 8 | const QueueProvider: FCWithChildren = ({ children }) => { 9 | const msg = React.useContext(messageChannelContext); 10 | 11 | const [ready, setReady] = React.useState(false); 12 | const [queue, setQueue] = React.useState([]); 13 | 14 | React.useEffect(() => { 15 | if (msg && !ready) { 16 | msg.getQueue().then(data => { 17 | setQueue(data); 18 | setReady(true); 19 | }); 20 | } 21 | const listener = (ev: RandomEvent<'queueChange'>) => { 22 | setQueue(ev.data); 23 | }; 24 | msg?.randomEvents.on('queueChange', listener); 25 | return () => { 26 | msg?.randomEvents.removeListener('queueChange', listener); 27 | }; 28 | }, [ msg ]); 29 | 30 | return 31 | {children} 32 | ; 33 | }; 34 | 35 | export default QueueProvider; -------------------------------------------------------------------------------- /gui/react/src/provider/ServiceProvider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Divider, Box, Button, Typography, Avatar} from '@mui/material'; 3 | import useStore from '../hooks/useStore'; 4 | import { StoreState } from './Store'; 5 | 6 | type Services = 'crunchy'|'hidive'|'ao'|'adn'; 7 | 8 | export const serviceContext = React.createContext(undefined); 9 | 10 | const ServiceProvider: FCWithChildren = ({ children }) => { 11 | const [ { service }, dispatch ] = useStore(); 12 | 13 | const setService = (s: StoreState['service']) => { 14 | dispatch({ 15 | type: 'service', 16 | payload: s 17 | }); 18 | }; 19 | 20 | return service === undefined ? 21 | 22 | Please select your service 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | : 31 | {children} 32 | ; 33 | }; 34 | 35 | export default ServiceProvider; -------------------------------------------------------------------------------- /gui/react/src/provider/Store.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Episode } from '../../../../@types/messageHandler'; 3 | import { dubLanguageCodes } from '../../../../modules/module.langsData'; 4 | 5 | export type DownloadOptions = { 6 | q: number, 7 | id: string, 8 | e: string, 9 | dubLang: typeof dubLanguageCodes, 10 | dlsubs: string[], 11 | fileName: string, 12 | dlVideoOnce: boolean, 13 | all: boolean, 14 | but: boolean, 15 | novids: boolean, 16 | hslang?: string, 17 | simul: boolean, 18 | noaudio: boolean 19 | } 20 | 21 | export type StoreState = { 22 | episodeListing: Episode[]; 23 | downloadOptions: DownloadOptions, 24 | service: 'crunchy'|'hidive'|'ao'|'adn'|undefined, 25 | version: string, 26 | } 27 | 28 | export type StoreAction = { 29 | type: T, 30 | payload: StoreState[T], 31 | extraInfo?: Record 32 | } 33 | 34 | const Reducer = (state: StoreState, action: StoreAction): StoreState => { 35 | switch(action.type) { 36 | default: 37 | return { ...state, [action.type]: action.payload }; 38 | } 39 | }; 40 | 41 | const initialState: StoreState = { 42 | downloadOptions: { 43 | id: '', 44 | q: 0, 45 | e: '', 46 | dubLang: [ 'jpn' ], 47 | dlsubs: [ 'all' ], 48 | fileName: '', 49 | dlVideoOnce: false, 50 | all: false, 51 | but: false, 52 | noaudio: false, 53 | novids: false, 54 | simul: false 55 | }, 56 | service: undefined, 57 | episodeListing: [], 58 | version: '', 59 | }; 60 | 61 | const Store: FCWithChildren = ({children}) => { 62 | const [state, dispatch] = React.useReducer(Reducer, initialState); 63 | /*React.useEffect(() => { 64 | if (!state.unsavedChanges.has) 65 | return; 66 | const unsavedChanges = (ev: BeforeUnloadEvent, lang: LanguageContextType) => { 67 | ev.preventDefault(); 68 | ev.returnValue = lang.getLang('unsaved_changes'); 69 | return lang.getLang('unsaved_changes'); 70 | }; 71 | 72 | 73 | const windowListener = (ev: BeforeUnloadEvent) => { 74 | return unsavedChanges(ev, state.lang); 75 | }; 76 | 77 | window.addEventListener('beforeunload', windowListener); 78 | 79 | return () => window.removeEventListener('beforeunload', windowListener); 80 | }, [state.unsavedChanges.has]);*/ 81 | 82 | return ( 83 | 84 | {children} 85 | 86 | ); 87 | }; 88 | 89 | /* Importent Notice -- The 'queue' generic will be overriden */ 90 | export const StoreContext = React.createContext<[StoreState, React.Dispatch>]>([initialState, undefined as any]); 91 | export default Store; -------------------------------------------------------------------------------- /gui/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./build", 4 | "target": "es5", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "module": "CommonJS", 18 | "moduleResolution": "node", 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | //"noEmit": true, 22 | "jsx": "react-jsx", 23 | "downlevelIteration": true 24 | }, 25 | "include": [ 26 | "./src", 27 | "./webpack.config.ts" 28 | ] 29 | } -------------------------------------------------------------------------------- /gui/react/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import type { Configuration } from 'webpack'; 2 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 3 | import path from 'path'; 4 | import type { Configuration as DevServerConfig } from 'webpack-dev-server'; 5 | 6 | const config: Configuration & DevServerConfig = { 7 | devServer: { 8 | proxy: [ 9 | { 10 | target: 'http://localhost:3000', 11 | context: ['/public', '/private'], 12 | ws: true 13 | } 14 | ], 15 | }, 16 | entry: './src/index.tsx', 17 | mode: 'production', 18 | output: { 19 | path: path.resolve(process.cwd(), './build'), 20 | filename: 'index.js', 21 | }, 22 | target: 'web', 23 | resolve: { 24 | extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'], 25 | }, 26 | performance: false, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.(ts|tsx)$/, 31 | exclude: /node_modules/, 32 | use: { 33 | 'loader': 'babel-loader', 34 | options: { 35 | presets: [ 36 | '@babel/typescript', 37 | '@babel/preset-react', 38 | ['@babel/preset-env', { 39 | targets: 'defaults' 40 | }] 41 | ] 42 | } 43 | }, 44 | }, 45 | { 46 | test: /\.css$/i, 47 | use: ['style-loader', 'css-loader'], 48 | }, 49 | ], 50 | }, 51 | plugins: [ 52 | new HtmlWebpackPlugin({ 53 | template: path.join(process.cwd(), 'public', 'index.html') 54 | }) 55 | ] 56 | }; 57 | 58 | export default config; 59 | -------------------------------------------------------------------------------- /gui/server/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { ensureConfig, loadCfg, workingDir } from '../../modules/module.cfg-loader'; 3 | import cors from 'cors'; 4 | import ServiceHandler from './serviceHandler'; 5 | import open from 'open'; 6 | import path from 'path'; 7 | import { PublicWebSocket } from './websocket'; 8 | import { console } from '../../modules/log'; 9 | import packageJson from '../../package.json'; 10 | 11 | process.title = 'AniDL'; 12 | 13 | ensureConfig(); 14 | 15 | const cfg = loadCfg(); 16 | 17 | const app = express(); 18 | 19 | export { app, cfg }; 20 | 21 | app.use(express.json()); 22 | app.use(cors()); 23 | app.use(express.static(path.join(workingDir, 'gui', 'server', 'build'), { maxAge: 1000 * 60 * 20 })); 24 | 25 | console.info(`\n=== Multi Downloader NX GUI ${packageJson.version} ===\n`); 26 | 27 | const server = app.listen(cfg.gui.port, () => { 28 | console.info(`GUI server started on port ${cfg.gui.port}`); 29 | }); 30 | 31 | new PublicWebSocket(server); 32 | new ServiceHandler(server); 33 | 34 | open(`http://localhost:${cfg.gui.port}`); -------------------------------------------------------------------------------- /gui/server/services/base.ts: -------------------------------------------------------------------------------- 1 | import { DownloadInfo, FolderTypes, GuiState, ProgressData, QueueItem } from '../../../@types/messageHandler'; 2 | import { RandomEvent, RandomEvents } from '../../../@types/randomEvents'; 3 | import WebSocketHandler from '../websocket'; 4 | import open from 'open'; 5 | import { cfg } from '..'; 6 | import path from 'path'; 7 | import { console } from '../../../modules/log'; 8 | import { getState, setState } from '../../../modules/module.cfg-loader'; 9 | import packageJson from '../../../package.json'; 10 | 11 | export default class Base { 12 | private state: GuiState; 13 | public name = 'default'; 14 | constructor(private ws: WebSocketHandler) { 15 | this.state = getState(); 16 | } 17 | 18 | private downloading = false; 19 | 20 | private queue: QueueItem[] = []; 21 | private workOnQueue = false; 22 | 23 | version(): Promise { 24 | return new Promise(() => { 25 | return packageJson.version; 26 | }); 27 | } 28 | 29 | initState() { 30 | if (this.state.services[this.name]) { 31 | this.queue = this.state.services[this.name].queue; 32 | this.queueChange(); 33 | } else { 34 | this.state.services[this.name] = { 35 | 'queue': [] 36 | }; 37 | } 38 | } 39 | 40 | setDownloading(downloading: boolean) { 41 | this.downloading = downloading; 42 | } 43 | 44 | getDownloading() { 45 | return this.downloading; 46 | } 47 | 48 | alertError(error: Error) { 49 | console.error(`${error}`); 50 | } 51 | 52 | makeProgressHandler(videoInfo: DownloadInfo) { 53 | return ((data: ProgressData) => { 54 | this.sendMessage({ 55 | name: 'progress', 56 | data: { 57 | downloadInfo: videoInfo, 58 | progress: data 59 | } 60 | }); 61 | }); 62 | } 63 | 64 | sendMessage(data: RandomEvent) { 65 | this.ws.sendMessage(data); 66 | } 67 | 68 | async isDownloading() { 69 | return this.downloading; 70 | } 71 | 72 | async openFolder(folderType: FolderTypes) { 73 | switch (folderType) { 74 | case 'content': 75 | open(cfg.dir.content); 76 | break; 77 | case 'config': 78 | open(cfg.dir.config); 79 | break; 80 | } 81 | } 82 | 83 | async openFile(data: [FolderTypes, string]) { 84 | switch (data[0]) { 85 | case 'config': 86 | open(path.join(cfg.dir.config, data[1])); 87 | break; 88 | case 'content': 89 | throw new Error('No subfolders'); 90 | } 91 | } 92 | 93 | async openURL(data: string) { 94 | open(data); 95 | } 96 | 97 | public async getQueue(): Promise { 98 | return this.queue; 99 | } 100 | 101 | public async removeFromQueue(index: number) { 102 | this.queue.splice(index, 1); 103 | this.queueChange(); 104 | } 105 | 106 | public async clearQueue() { 107 | this.queue = []; 108 | this.queueChange(); 109 | } 110 | 111 | public addToQueue(data: QueueItem[]) { 112 | this.queue = this.queue.concat(...data); 113 | this.queueChange(); 114 | } 115 | 116 | public setDownloadQueue(data: boolean) { 117 | this.workOnQueue = data; 118 | this.queueChange(); 119 | } 120 | 121 | public async getDownloadQueue(): Promise { 122 | return this.workOnQueue; 123 | } 124 | 125 | private async queueChange() { 126 | this.sendMessage({ name: 'queueChange', data: this.queue }); 127 | if (this.workOnQueue && this.queue.length > 0 && !await this.isDownloading()) { 128 | this.setDownloading(true); 129 | this.sendMessage({ name: 'current', data: this.queue[0] }); 130 | this.downloadItem(this.queue[0]); 131 | this.queue = this.queue.slice(1); 132 | this.queueChange(); 133 | } 134 | this.state.services[this.name].queue = this.queue; 135 | setState(this.state); 136 | } 137 | 138 | public async onFinish() { 139 | this.sendMessage({ name: 'current', data: undefined }); 140 | this.queueChange(); 141 | } 142 | 143 | //Overriten 144 | // eslint-disable-next-line 145 | public async downloadItem(_: QueueItem) { 146 | throw new Error('downloadItem not overriden'); 147 | } 148 | } -------------------------------------------------------------------------------- /gui/server/services/crunchyroll.ts: -------------------------------------------------------------------------------- 1 | import { AuthData, CheckTokenResponse, DownloadData, EpisodeListResponse, MessageHandler, ResolveItemsData, SearchData, SearchResponse } from '../../../@types/messageHandler'; 2 | import Crunchy from '../../../crunchy'; 3 | import { getDefault } from '../../../modules/module.args'; 4 | import { languages, subtitleLanguagesFilter } from '../../../modules/module.langsData'; 5 | import WebSocketHandler from '../websocket'; 6 | import Base from './base'; 7 | import { console } from '../../../modules/log'; 8 | import * as yargs from '../../../modules/module.app-args'; 9 | 10 | class CrunchyHandler extends Base implements MessageHandler { 11 | private crunchy: Crunchy; 12 | public name = 'crunchy'; 13 | constructor(ws: WebSocketHandler) { 14 | super(ws); 15 | this.crunchy = new Crunchy(); 16 | this.crunchy.refreshToken(); 17 | this.initState(); 18 | this.getDefaults(); 19 | } 20 | 21 | public getDefaults() { 22 | const _default = yargs.appArgv(this.crunchy.cfg.cli, true); 23 | this.crunchy.api = _default.crapi; 24 | this.crunchy.locale = _default.locale; 25 | } 26 | 27 | public async listEpisodes (id: string): Promise { 28 | this.getDefaults(); 29 | await this.crunchy.refreshToken(true); 30 | return { isOk: true, value: (await this.crunchy.listSeriesID(id)).list }; 31 | } 32 | 33 | public async handleDefault(name: string) { 34 | return getDefault(name, this.crunchy.cfg.cli); 35 | } 36 | 37 | public async availableDubCodes(): Promise { 38 | const dubLanguageCodesArray: string[] = []; 39 | for(const language of languages){ 40 | if (language.cr_locale) 41 | dubLanguageCodesArray.push(language.code); 42 | } 43 | return [...new Set(dubLanguageCodesArray)]; 44 | } 45 | 46 | public async availableSubCodes(): Promise { 47 | return subtitleLanguagesFilter; 48 | } 49 | 50 | public async resolveItems(data: ResolveItemsData): Promise { 51 | this.getDefaults(); 52 | await this.crunchy.refreshToken(true); 53 | console.debug(`Got resolve options: ${JSON.stringify(data)}`); 54 | const res = await this.crunchy.downloadFromSeriesID(data.id, data); 55 | if (!res.isOk) 56 | return res.isOk; 57 | this.addToQueue(res.value.map(a => { 58 | return { 59 | ...data, 60 | 61 | ids: a.data.map(a => a.mediaId), 62 | title: a.episodeTitle, 63 | parent: { 64 | title: a.seasonTitle, 65 | season: a.season.toString() 66 | }, 67 | e: a.e, 68 | image: a.image, 69 | episode: a.episodeNumber 70 | }; 71 | })); 72 | return true; 73 | } 74 | 75 | public async search(data: SearchData): Promise { 76 | this.getDefaults(); 77 | await this.crunchy.refreshToken(true); 78 | if (!data['search-type']) data['search-type'] = 'series'; 79 | console.debug(`Got search options: ${JSON.stringify(data)}`); 80 | const crunchySearch = await this.crunchy.doSearch(data); 81 | if (!crunchySearch.isOk) { 82 | this.crunchy.refreshToken(); 83 | return crunchySearch; 84 | } 85 | return { isOk: true, value: crunchySearch.value }; 86 | } 87 | 88 | public async checkToken(): Promise { 89 | if (await this.crunchy.getProfile()) { 90 | return { isOk: true, value: undefined }; 91 | } else { 92 | return { isOk: false, reason: new Error('') }; 93 | } 94 | } 95 | 96 | public auth(data: AuthData) { 97 | return this.crunchy.doAuth(data); 98 | } 99 | 100 | public async downloadItem(data: DownloadData) { 101 | this.getDefaults(); 102 | await this.crunchy.refreshToken(true); 103 | console.debug(`Got download options: ${JSON.stringify(data)}`); 104 | this.setDownloading(true); 105 | const _default = yargs.appArgv(this.crunchy.cfg.cli, true); 106 | this.crunchy.api = _default.crapi; 107 | const res = await this.crunchy.downloadFromSeriesID(data.id, { 108 | dubLang: data.dubLang, 109 | e: data.e 110 | }); 111 | if (res.isOk) { 112 | for (const select of res.value) { 113 | if (!(await this.crunchy.downloadEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y', 114 | novids: data.novids, noaudio: data.noaudio, hslang: data.hslang || 'none' }))) { 115 | const er = new Error(`Unable to download episode ${data.e} from ${data.id}`); 116 | er.name = 'Download error'; 117 | this.alertError(er); 118 | } 119 | } 120 | } else { 121 | this.alertError(res.reason); 122 | } 123 | this.sendMessage({ name: 'finish', data: undefined }); 124 | this.setDownloading(false); 125 | this.onFinish(); 126 | } 127 | } 128 | 129 | export default CrunchyHandler; -------------------------------------------------------------------------------- /gui/server/services/hidive.ts: -------------------------------------------------------------------------------- 1 | import { AuthData, CheckTokenResponse, DownloadData, EpisodeListResponse, MessageHandler, ResolveItemsData, SearchData, SearchResponse } from '../../../@types/messageHandler'; 2 | import Hidive from '../../../hidive'; 3 | import { getDefault } from '../../../modules/module.args'; 4 | import { languages } from '../../../modules/module.langsData'; 5 | import WebSocketHandler from '../websocket'; 6 | import Base from './base'; 7 | import { console } from '../../../modules/log'; 8 | import * as yargs from '../../../modules/module.app-args'; 9 | 10 | class HidiveHandler extends Base implements MessageHandler { 11 | private hidive: Hidive; 12 | public name = 'hidive'; 13 | constructor(ws: WebSocketHandler) { 14 | super(ws); 15 | this.hidive = new Hidive(); 16 | this.initState(); 17 | } 18 | 19 | public async auth(data: AuthData) { 20 | return this.hidive.doAuth(data); 21 | } 22 | 23 | public async checkToken(): Promise { 24 | //TODO: implement proper method to check token 25 | return { isOk: true, value: undefined }; 26 | } 27 | 28 | public async search(data: SearchData): Promise { 29 | console.debug(`Got search options: ${JSON.stringify(data)}`); 30 | const hidiveSearch = await this.hidive.doSearch(data); 31 | if (!hidiveSearch.isOk) { 32 | return hidiveSearch; 33 | } 34 | return { isOk: true, value: hidiveSearch.value }; 35 | } 36 | 37 | public async handleDefault(name: string) { 38 | return getDefault(name, this.hidive.cfg.cli); 39 | } 40 | 41 | public async availableDubCodes(): Promise { 42 | const dubLanguageCodesArray: string[] = []; 43 | for(const language of languages){ 44 | if (language.new_hd_locale) 45 | dubLanguageCodesArray.push(language.code); 46 | } 47 | return [...new Set(dubLanguageCodesArray)]; 48 | } 49 | 50 | public async availableSubCodes(): Promise { 51 | const subLanguageCodesArray: string[] = []; 52 | for(const language of languages){ 53 | if (language.new_hd_locale) 54 | subLanguageCodesArray.push(language.locale); 55 | } 56 | return ['all', 'none', ...new Set(subLanguageCodesArray)]; 57 | } 58 | 59 | public async resolveItems(data: ResolveItemsData): Promise { 60 | const parse = parseInt(data.id); 61 | if (isNaN(parse) || parse <= 0) 62 | return false; 63 | console.debug(`Got resolve options: ${JSON.stringify(data)}`); 64 | const res = await this.hidive.selectSeries(parseInt(data.id), data.e, data.but, data.all); 65 | if (!res.isOk || !res.value) 66 | return res.isOk; 67 | this.addToQueue(res.value.map(item => { 68 | return { 69 | ...data, 70 | ids: [item.id], 71 | title: item.title, 72 | parent: { 73 | title: item.seriesTitle, 74 | season: item.episodeInformation.seasonNumber+'' 75 | }, 76 | image: item.thumbnailUrl, 77 | e: item.episodeInformation.episodeNumber+'', 78 | episode: item.episodeInformation.episodeNumber+'', 79 | }; 80 | })); 81 | return true; 82 | } 83 | 84 | public async listEpisodes(id: string): Promise { 85 | const parse = parseInt(id); 86 | if (isNaN(parse) || parse <= 0) 87 | return { isOk: false, reason: new Error('The ID is invalid') }; 88 | 89 | const request = await this.hidive.listSeries(parse); 90 | if (!request.isOk || !request.value) 91 | return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')}; 92 | 93 | return { isOk: true, value: request.value.map(function(item) { 94 | const description = item.description.split('\r\n'); 95 | return { 96 | e: item.episodeInformation.episodeNumber+'', 97 | lang: [], 98 | name: item.title, 99 | season: item.episodeInformation.seasonNumber+'', 100 | seasonTitle: request.series.seasons[item.episodeInformation.seasonNumber-1]?.title ?? request.series.title, 101 | episode: item.episodeInformation.episodeNumber+'', 102 | id: item.id+'', 103 | img: item.thumbnailUrl, 104 | description: description ? description[0] : '', 105 | time: '' 106 | }; 107 | })}; 108 | } 109 | 110 | public async downloadItem(data: DownloadData) { 111 | this.setDownloading(true); 112 | console.debug(`Got download options: ${JSON.stringify(data)}`); 113 | const _default = yargs.appArgv(this.hidive.cfg.cli, true); 114 | const res = await this.hidive.selectSeries(parseInt(data.id), data.e, false, false); 115 | if (!res.isOk || !res.showData) 116 | return this.alertError(new Error('Download failed upstream, check for additional logs')); 117 | 118 | for (const ep of res.value) { 119 | await this.hidive.downloadEpisode(ep, {..._default, callbackMaker: this.makeProgressHandler.bind(this), dubLang: data.dubLang, dlsubs: data.dlsubs, fileName: data.fileName, q: data.q, force: 'y', noaudio: data.noaudio, novids: data.novids }); 120 | } 121 | this.sendMessage({ name: 'finish', data: undefined }); 122 | this.setDownloading(false); 123 | this.onFinish(); 124 | } 125 | } 126 | 127 | export default HidiveHandler; -------------------------------------------------------------------------------- /gui/server/websocket.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage, Server } from 'http'; 2 | import ws, { WebSocket } from 'ws'; 3 | import { RandomEvent, RandomEvents } from '../../@types/randomEvents'; 4 | import { MessageTypes, UnknownWSMessage, WSMessage } from '../../@types/ws'; 5 | import { EventEmitter } from 'events'; 6 | import { cfg } from '.'; 7 | import { getState } from '../../modules/module.cfg-loader'; 8 | import { console } from '../../modules/log'; 9 | 10 | declare interface ExternalEvent { 11 | on(event: T, listener: (msg: WSMessage, respond: (data: MessageTypes[T][1]) => void) => void): this; 12 | emit(event: T, msg: WSMessage, respond: (data: MessageTypes[T][1]) => void): boolean; 13 | } 14 | 15 | class ExternalEvent extends EventEmitter {} 16 | 17 | export default class WebSocketHandler { 18 | 19 | private wsServer: ws.Server; 20 | 21 | public events: ExternalEvent = new ExternalEvent(); 22 | 23 | constructor(server: Server) { 24 | this.wsServer = new ws.WebSocketServer({ noServer: true, path: '/private' }); 25 | 26 | this.wsServer.on('connection', (socket, req) => { 27 | console.info(`[WS] Connection from '${req.socket.remoteAddress}'`); 28 | socket.on('error', (er) => console.error(`[WS] ${er}`)); 29 | socket.on('message', (data) => { 30 | const json = JSON.parse(data.toString()) as UnknownWSMessage; 31 | this.events.emit(json.name, json as any, (data) => { 32 | this.wsServer.clients.forEach(client => { 33 | if (client.readyState !== WebSocket.OPEN) 34 | return; 35 | client.send(JSON.stringify({ 36 | data, 37 | id: json.id, 38 | name: json.name 39 | }), (er) => { 40 | if (er) 41 | console.error(`[WS] ${er}`); 42 | }); 43 | }); 44 | }); 45 | }); 46 | }); 47 | 48 | server.on('upgrade', (request, socket, head) => { 49 | if (!this.wsServer.shouldHandle(request)) 50 | return; 51 | if (!this.authenticate(request)) { 52 | socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); 53 | socket.destroy(); 54 | console.info(`[WS] ${request.socket.remoteAddress} tried to connect but used a wrong password.`); 55 | return; 56 | } 57 | this.wsServer.handleUpgrade(request, socket, head, socket => { 58 | this.wsServer.emit('connection', socket, request); 59 | }); 60 | }); 61 | } 62 | 63 | public sendMessage(data: RandomEvent) { 64 | this.wsServer.clients.forEach(client => { 65 | if (client.readyState !== WebSocket.OPEN) 66 | return; 67 | client.send(JSON.stringify(data), (er) => { 68 | if (er) 69 | console.error(`[WS] ${er}`); 70 | }); 71 | }); 72 | } 73 | 74 | private authenticate(request: IncomingMessage): boolean { 75 | const search = new URL(`http://${request.headers.host}${request.url}`).searchParams; 76 | return cfg.gui.password === (search.get('password') ?? undefined); 77 | } 78 | 79 | } 80 | 81 | export class PublicWebSocket { 82 | private wsServer: ws.Server; 83 | 84 | private state = getState(); 85 | constructor(server: Server) { 86 | this.wsServer = new ws.WebSocketServer({ noServer: true, path: '/public' }); 87 | 88 | this.wsServer.on('connection', (socket, req) => { 89 | console.info(`[WS] Connection to public ws from '${req.socket.remoteAddress}'`); 90 | socket.on('error', (er) => console.error(`[WS] ${er}`)); 91 | socket.on('message', (msg) => { 92 | const data = JSON.parse(msg.toString()) as UnknownWSMessage; 93 | switch (data.name) { 94 | case 'isSetup': 95 | this.send(socket, data.id, data.name, this.state.setup); 96 | break; 97 | case 'requirePassword': 98 | this.send(socket, data.id, data.name, cfg.gui.password !== undefined); 99 | break; 100 | } 101 | }); 102 | }); 103 | 104 | server.on('upgrade', (request, socket, head) => { 105 | if (!this.wsServer.shouldHandle(request)) 106 | return; 107 | this.wsServer.handleUpgrade(request, socket, head, socket => { 108 | this.wsServer.emit('connection', socket, request); 109 | }); 110 | }); 111 | } 112 | 113 | private send(client: ws.WebSocket, id: string, name: string, data: any) { 114 | client.send(JSON.stringify({ 115 | data, 116 | id, 117 | name 118 | }), (er) => { 119 | if (er) 120 | console.error(`[WS] ${er}`); 121 | }); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { console } from './modules/log'; 2 | import { ServiceClass } from './@types/serviceClassInterface'; 3 | import { appArgv, overrideArguments } from './modules/module.app-args'; 4 | import * as yamlCfg from './modules/module.cfg-loader'; 5 | import { makeCommand, addToArchive } from './modules/module.downloadArchive'; 6 | 7 | import update from './modules/module.updater'; 8 | 9 | (async () => { 10 | const cfg = yamlCfg.loadCfg(); 11 | const argv = appArgv(cfg.cli); 12 | if (!argv.skipUpdate) 13 | await update(argv.update); 14 | 15 | if (argv.all && argv.but) { 16 | console.error('--all and --but exclude each other!'); 17 | return; 18 | } 19 | 20 | if (argv.addArchive) { 21 | if (argv.service === 'crunchy') { 22 | if (argv.s === undefined && argv.series === undefined) 23 | return console.error('`-s` or `--srz` not found'); 24 | if (argv.s && argv.series) 25 | return console.error('Both `-s` and `--srz` found'); 26 | addToArchive({ 27 | service: 'crunchy', 28 | type: argv.s === undefined ? 'srz' : 's' 29 | }, (argv.s === undefined ? argv.series : argv.s) as string); 30 | console.info('Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s)); 31 | } else if (argv.service === 'hidive') { 32 | if (argv.s === undefined) 33 | return console.error('`-s` not found'); 34 | addToArchive({ 35 | service: 'hidive', 36 | //type: argv.s === undefined ? 'srz' : 's' 37 | type: 's' 38 | }, (argv.s === undefined ? argv.series : argv.s) as string); 39 | console.info('Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s)); 40 | } else if (argv.service === 'ao') { 41 | if (argv.s === undefined) 42 | return console.error('`-s` not found'); 43 | addToArchive({ 44 | service: 'hidive', 45 | //type: argv.s === undefined ? 'srz' : 's' 46 | type: 's' 47 | }, (argv.s === undefined ? argv.series : argv.s) as string); 48 | console.info('Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s)); 49 | } 50 | } else if (argv.downloadArchive) { 51 | const ids = makeCommand(argv.service); 52 | for (const id of ids) { 53 | overrideArguments(cfg.cli, id); 54 | /* Reimport module to override appArgv */ 55 | Object.keys(require.cache).forEach(key => { 56 | if (key.endsWith('crunchy.js') || key.endsWith('hidive.js') || key.endsWith('ao.js')) 57 | delete require.cache[key]; 58 | }); 59 | let service: ServiceClass; 60 | switch(argv.service) { 61 | case 'crunchy': 62 | service = new (await import('./crunchy')).default; 63 | break; 64 | case 'hidive': 65 | service = new (await import('./hidive')).default; 66 | break; 67 | case 'ao': 68 | service = new (await import('./ao')).default; 69 | break; 70 | case 'adn': 71 | service = new (await import('./adn')).default; 72 | break; 73 | default: 74 | service = new (await import(`./${argv.service}`)).default; 75 | break; 76 | } 77 | await service.cli(); 78 | } 79 | } else { 80 | let service: ServiceClass; 81 | switch(argv.service) { 82 | case 'crunchy': 83 | service = new (await import('./crunchy')).default; 84 | break; 85 | case 'hidive': 86 | service = new (await import('./hidive')).default; 87 | break; 88 | case 'ao': 89 | service = new (await import('./ao')).default; 90 | break; 91 | case 'adn': 92 | service = new (await import('./adn')).default; 93 | break; 94 | default: 95 | service = new (await import(`./${argv.service}`)).default; 96 | break; 97 | } 98 | await service.cli(); 99 | } 100 | })(); -------------------------------------------------------------------------------- /modules/NotoSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anidl/multi-downloader-nx/0133a774b6a702481d1d671908d80031a58b2d51/modules/NotoSans-Regular.ttf -------------------------------------------------------------------------------- /modules/build-docs.ts: -------------------------------------------------------------------------------- 1 | import packageJSON from '../package.json'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import { args, groups } from './module.args'; 5 | 6 | const transformService = (str: Array<'crunchy'|'hidive'|'ao'|'adn'|'all'>) => { 7 | const services: string[] = []; 8 | str.forEach(function(part) { 9 | switch(part) { 10 | case 'crunchy': 11 | services.push('Crunchyroll'); 12 | break; 13 | case 'hidive': 14 | services.push('Hidive'); 15 | break; 16 | case 'ao': 17 | services.push('AnimeOnegai'); 18 | break; 19 | case 'adn': 20 | services.push('AnimationDigitalNetwork'); 21 | break; 22 | case 'all': 23 | services.push('All'); 24 | break; 25 | } 26 | }); 27 | return services.join(', '); 28 | }; 29 | 30 | let docs = `# ${packageJSON.name} (v${packageJSON.version}) 31 | 32 | If you find any bugs in this documentation or in the program itself please report it [over on GitHub](${packageJSON.bugs.url}). 33 | 34 | ## Legal Warning 35 | 36 | This application is not endorsed by or affiliated with *Crunchyroll*, *Hidive*, *AnimeOnegai*, or *AnimationDigitalNetwork*. 37 | This application enables you to download videos for offline viewing which may be forbidden by law in your country. 38 | The usage of this application may also cause a violation of the *Terms of Service* between you and the stream provider. 39 | This tool is not responsible for your actions; please make an informed decision before using this application. 40 | 41 | ## CLI Options 42 | ### Legend 43 | - \`\${someText}\` shows that you should replace this text with your own 44 | - e.g. \`--username \${someText}\` -> \`--username Izuco\` 45 | `; 46 | 47 | Object.entries(groups).forEach(([key, value]) => { 48 | docs += `\n### ${value.slice(0, -1)}\n`; 49 | 50 | docs += args.filter(a => a.group === key).map(argument => { 51 | return [`#### \`${argument.name.length > 1 ? '--' : '-'}${argument.name}\``, 52 | `| **Service** | **Usage** | **Type** | **Required** | **Alias** | ${argument.choices ? '**Choices** |' : ''} ${argument.default ? '**Default** |' : ''}**cli-default Entry**`, 53 | `| --- | --- | --- | --- | --- | ${argument.choices ? '--- | ' : ''}${argument.default ? '--- | ' : ''}---| `, 54 | `| ${transformService(argument.service)} | \`${argument.name.length > 1 ? '--' : '-'}${argument.name} ${argument.usage}\` | \`${argument.type}\` | \`${argument.demandOption ? 'Yes' : 'No'}\`|` 55 | + ` \`${(argument.alias ? `${argument.alias.length > 1 ? '--' : '-'}${argument.alias}` : undefined) ?? 'NaN'}\` |` 56 | + `${argument.choices ? ` [${argument.choices.map(a => `\`${a || '\'\''}\``).join(', ')}] |` : ''}` 57 | + `${argument.default ? ` \`${ 58 | typeof argument.default === 'object' 59 | ? Array.isArray(argument.default) 60 | ? JSON.stringify(argument.default) 61 | : (argument.default as any).default 62 | : argument.default 63 | }\`|` : ''}` 64 | + ` ${typeof argument.default === 'object' && !Array.isArray(argument.default) 65 | ? `\`${argument.default.name || argument.name}: \`` 66 | : '`NaN`' 67 | } |`, 68 | '', 69 | argument.docDescribe === true ? argument.describe : argument.docDescribe 70 | ].join('\n'); 71 | }).join('\n'); 72 | }); 73 | 74 | 75 | 76 | fs.writeFileSync(path.resolve(__dirname, '..', 'docs', 'DOCUMENTATION.md'), docs); -------------------------------------------------------------------------------- /modules/build.ts: -------------------------------------------------------------------------------- 1 | // build requirements 2 | import fs from 'fs-extra'; 3 | import pkg from '../package.json'; 4 | import modulesCleanup from 'removeNPMAbsolutePaths'; 5 | import { exec } from '@yao-pkg/pkg'; 6 | import { execSync } from 'child_process'; 7 | import { console } from './log'; 8 | import esbuild from 'esbuild'; 9 | import path from 'path'; 10 | import { builtinModules } from 'module'; 11 | 12 | const buildsDir = './_builds'; 13 | const nodeVer = 'node20-'; 14 | 15 | type BuildTypes = `${'windows'|'macos'|'linux'|'linuxstatic'|'alpine'}-${'x64'|'arm64'}`|'linuxstatic-armv7' 16 | 17 | (async () => { 18 | const buildType = process.argv[2] as BuildTypes; 19 | const isGUI = process.argv[3] === 'true'; 20 | 21 | buildBinary(buildType, isGUI); 22 | })(); 23 | 24 | // main 25 | async function buildBinary(buildType: BuildTypes, gui: boolean) { 26 | const buildStr = 'multi-downloader-nx'; 27 | const acceptablePlatforms = ['windows','linux','linuxstatic','macos','alpine']; 28 | const acceptableArchs = ['x64','arm64']; 29 | const acceptableBuilds: string[] = ['linuxstatic-armv7']; 30 | for (const platform of acceptablePlatforms) { 31 | for (const arch of acceptableArchs) { 32 | acceptableBuilds.push(platform+'-'+arch); 33 | } 34 | } 35 | if(!acceptableBuilds.includes(buildType)){ 36 | console.error('Unknown build type!'); 37 | process.exit(1); 38 | } 39 | await modulesCleanup('.'); 40 | if(!fs.existsSync(buildsDir)){ 41 | fs.mkdirSync(buildsDir); 42 | } 43 | const buildFull = `${buildStr}-${getFriendlyName(buildType)}-${gui ? 'gui' : 'cli'}`; 44 | const buildDir = `${buildsDir}/${buildFull}`; 45 | if(fs.existsSync(buildDir)){ 46 | fs.removeSync(buildDir); 47 | } 48 | fs.mkdirSync(buildDir); 49 | console.info('Running esbuild'); 50 | 51 | const build = await esbuild.build({ 52 | entryPoints: [ 53 | gui ? 'gui.js' : 'index.js', 54 | ], 55 | sourceRoot: './', 56 | bundle: true, 57 | platform: 'node', 58 | format: 'cjs', 59 | treeShaking: true, 60 | // External source map for debugging 61 | sourcemap: true, 62 | // Minify and keep the original names 63 | minify: true, 64 | keepNames: true, 65 | outfile: path.join(buildsDir, 'index.cjs'), 66 | metafile: true, 67 | external: ['cheerio', 'sleep', ...builtinModules] 68 | }); 69 | 70 | if (build.errors?.length > 0) console.error(build.errors); 71 | if (build.warnings?.length > 0) console.warn(build.warnings); 72 | 73 | const buildConfig = [ 74 | `${buildsDir}/index.cjs`, 75 | '--target', nodeVer + buildType, 76 | '--output', `${buildDir}/${pkg.short_name}`, 77 | '--compress', 'GZip' 78 | ]; 79 | console.info(`[Build] Build configuration: ${buildFull}`); 80 | try { 81 | await exec(buildConfig); 82 | } 83 | catch(e){ 84 | console.info(e); 85 | process.exit(1); 86 | } 87 | fs.mkdirSync(`${buildDir}/config`); 88 | fs.mkdirSync(`${buildDir}/videos`); 89 | fs.mkdirSync(`${buildDir}/widevine`); 90 | fs.mkdirSync(`${buildDir}/playready`); 91 | fs.copySync('./config/bin-path.yml', `${buildDir}/config/bin-path.yml`); 92 | fs.copySync('./config/cli-defaults.yml', `${buildDir}/config/cli-defaults.yml`); 93 | fs.copySync('./config/dir-path.yml', `${buildDir}/config/dir-path.yml`); 94 | fs.copySync('./config/gui.yml', `${buildDir}/config/gui.yml`); 95 | fs.copySync('./modules/cmd-here.bat', `${buildDir}/cmd-here.bat`); 96 | fs.copySync('./modules/NotoSans-Regular.ttf', `${buildDir}/NotoSans-Regular.ttf`); 97 | fs.copySync('./package.json', `${buildDir}/package.json`); 98 | fs.copySync('./docs/', `${buildDir}/docs/`); 99 | fs.copySync('./LICENSE.md', `${buildDir}/docs/LICENSE.md`); 100 | if (gui) { 101 | fs.copySync('./gui', `${buildDir}/gui`); 102 | fs.copySync('./node_modules/open/xdg-open', `${buildDir}/xdg-open`); 103 | } 104 | if(fs.existsSync(`${buildsDir}/${buildFull}.7z`)){ 105 | fs.removeSync(`${buildsDir}/${buildFull}.7z`); 106 | } 107 | execSync(`7z a -t7z "${buildsDir}/${buildFull}.7z" "${buildDir}"`,{stdio:[0,1,2]}); 108 | } 109 | 110 | function getFriendlyName(buildString: string): string { 111 | if (buildString.includes('armv7')) { 112 | return 'android'; 113 | } 114 | if (buildString.includes('linuxstatic')) { 115 | buildString = buildString.replace('linuxstatic', 'linux'); 116 | } 117 | return buildString; 118 | } -------------------------------------------------------------------------------- /modules/cmd-here.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | title CmdHere 3 | cmd /k PROMPT @$S$P$_$_$G$S 4 | -------------------------------------------------------------------------------- /modules/log.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { workingDir } from './module.cfg-loader'; 4 | import log4js from 'log4js'; 5 | 6 | const logFolder = path.join(workingDir, 'logs'); 7 | const latest = path.join(logFolder, 'latest.log'); 8 | 9 | const makeLogFolder = () => { 10 | if (!fs.existsSync(logFolder)) 11 | fs.mkdirSync(logFolder); 12 | if (fs.existsSync(latest)) { 13 | const stats = fs.statSync(latest); 14 | fs.renameSync(latest, path.join(logFolder, `${stats.mtimeMs}.log`)); 15 | } 16 | }; 17 | 18 | const makeLogger = () => { 19 | global.console.log = 20 | global.console.info = 21 | global.console.warn = 22 | global.console.error = 23 | global.console.debug = (...data: any[]) => { 24 | console.info((data.length >= 1 ? data.shift() : ''), ...data); 25 | }; 26 | makeLogFolder(); 27 | log4js.configure({ 28 | appenders: { 29 | console: { 30 | type: 'console', layout: { 31 | type: 'pattern', 32 | pattern: process.env.isGUI === 'true' ? '%[%x{info}%m%]' : '%x{info}%m', 33 | tokens: { 34 | info: (ev) => { 35 | return ev.level.levelStr === 'INFO' ? '' : `[${ev.level.levelStr}] `; 36 | } 37 | } 38 | } 39 | }, 40 | file: { 41 | type: 'file', 42 | filename: latest, 43 | layout: { 44 | type: 'pattern', 45 | pattern: '%x{info}%m', 46 | tokens: { 47 | info: (ev) => { 48 | return ev.level.levelStr === 'INFO' ? '' : `[${ev.level.levelStr}] `; 49 | } 50 | } 51 | } 52 | } 53 | }, 54 | categories: { 55 | default: { 56 | appenders: ['console', 'file'], 57 | level: 'all', 58 | } 59 | } 60 | }); 61 | }; 62 | 63 | const getLogger = () => { 64 | if (!log4js.isConfigured()) 65 | makeLogger(); 66 | return log4js.getLogger(); 67 | }; 68 | 69 | export const console = getLogger(); -------------------------------------------------------------------------------- /modules/module.api-urls.ts: -------------------------------------------------------------------------------- 1 | // api domains 2 | const domain = { 3 | www: 'https://www.crunchyroll.com', 4 | api: 'https://api.crunchyroll.com', 5 | api_beta: 'https://beta-api.crunchyroll.com', 6 | hd_www: 'https://www.hidive.com', 7 | hd_api: 'https://api.hidive.com', 8 | hd_new: 'https://dce-frontoffice.imggaming.com' 9 | }; 10 | 11 | export type APIType = { 12 | bundlejs: string, 13 | newani: string, 14 | search1: string, 15 | search2: string, 16 | rss_cid: string, 17 | rss_gid: string 18 | media_page: string 19 | series_page: string 20 | auth: string 21 | // mobile api 22 | search3: string 23 | session: string 24 | collections: string 25 | // beta api 26 | defaultUserAgent: string, 27 | beta_profile: string 28 | beta_cmsToken: string 29 | search: string 30 | cms: string 31 | beta_browse: string 32 | beta_cms: string, 33 | drm: string; 34 | drm_widevine: string; 35 | drm_playready: string; 36 | /** 37 | * Header 38 | */ 39 | crunchyDefHeader: Record, 40 | crunchyAuthHeader: Record, 41 | hd_apikey: string, 42 | hd_devName: string, 43 | hd_appId: string, 44 | hd_clientWeb: string, 45 | hd_clientExo: string, 46 | hd_api: string, 47 | hd_new_api: string, 48 | hd_new_apiKey: string, 49 | hd_new_version: string, 50 | } 51 | 52 | // api urls 53 | const api: APIType = { 54 | // web 55 | bundlejs: 'https://static.crunchyroll.com/vilos-v2/web/vilos/js/bundle.js', 56 | newani: `${domain.www}/rss/anime`, 57 | search1: `${domain.www}/ajax/?req=RpcApiSearch_GetSearchCandidates`, 58 | search2: `${domain.www}/search_page`, 59 | rss_cid: `${domain.www}/syndication/feed?type=episodes&id=`, // &lang=enUS 60 | rss_gid: `${domain.www}/syndication/feed?type=episodes&group_id=`, // &lang=enUS 61 | media_page: `${domain.www}/media-`, 62 | series_page: `${domain.www}/series-`, 63 | auth: `${domain.www}/auth/v1/token`, 64 | // mobile api 65 | search3: `${domain.api}/autocomplete.0.json`, 66 | session: `${domain.api}/start_session.0.json`, 67 | collections: `${domain.api}/list_collections.0.json`, 68 | // This User-Agent bypasses Cloudflare security of the newer Endpoint 69 | defaultUserAgent: 'Crunchyroll/4.77.3 (bundle_identifier:com.crunchyroll.iphone; build_number:4148147.285670380) iOS/18.3.2 Gravity/4.77.3', 70 | beta_profile: `${domain.api_beta}/accounts/v1/me/profile`, 71 | beta_cmsToken: `${domain.api_beta}/index/v2`, 72 | search: `${domain.api_beta}/content/v2/discover/search`, 73 | cms: `${domain.api_beta}/content/v2/cms`, 74 | beta_browse: `${domain.api_beta}/content/v1/browse`, 75 | beta_cms: `${domain.api_beta}/cms/v2`, 76 | // beta api 77 | // broken - deprecated since 06.05.2025 78 | drm: `${domain.api_beta}/drm/v1/auth`, 79 | // new drm endpoints 80 | drm_widevine: `${domain.www}/license/v1/license/widevine`, 81 | // playready endpoint currently broken 82 | drm_playready: `${domain.www}/license/v1/license/playReady`, 83 | crunchyDefHeader: {}, 84 | crunchyAuthHeader: {}, 85 | //hidive API 86 | hd_apikey: '508efd7b42d546e19cc24f4d0b414e57e351ca73', 87 | hd_devName: 'Android', 88 | hd_appId: '24i-Android', 89 | hd_clientWeb: 'okhttp/3.4.1', 90 | hd_clientExo: 'smartexoplayer/1.6.0.R (Linux;Android 6.0) ExoPlayerLib/2.6.0', 91 | hd_api: `${domain.hd_api}/api/v1`, 92 | //Hidive New API 93 | hd_new_api: `${domain.hd_new}/api`, 94 | hd_new_apiKey: '857a1e5d-e35e-4fdf-805b-a87b6f8364bf', 95 | hd_new_version: '6.0.1.bbf09a2' 96 | }; 97 | 98 | api.crunchyDefHeader = { 99 | 'User-Agent': api.defaultUserAgent, 100 | 'Accept': '*/*', 101 | 'Accept-Encoding': 'gzip;q=1.0, compress;q=0.5', 102 | 'Accept-Language': 'de-IT;q=1.0, it-IT;q=0.9, en-GB;q=0.8', 103 | 'Connection': 'keep-alive', 104 | 'Host': 'www.crunchyroll.com' 105 | }; 106 | 107 | // set header 108 | api.crunchyAuthHeader = { 109 | 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', 110 | ...api.crunchyDefHeader 111 | }; 112 | 113 | export { 114 | domain, api 115 | }; 116 | -------------------------------------------------------------------------------- /modules/module.colors.json: -------------------------------------------------------------------------------- 1 | { 2 | "aliceblue": "#f0f8ff", 3 | "antiquewhite": "#faebd7", 4 | "aqua": "#00ffff", 5 | "aquamarine": "#7fffd4", 6 | "azure": "#f0ffff", 7 | "beige": "#f5f5dc", 8 | "bisque": "#ffe4c4", 9 | "black": "#000000", 10 | "blanchedalmond": "#ffebcd", 11 | "blue": "#0000ff", 12 | "blueviolet": "#8a2be2", 13 | "brown": "#a52a2a", 14 | "burlywood": "#deb887", 15 | "cadetblue": "#5f9ea0", 16 | "chartreuse": "#7fff00", 17 | "chocolate": "#d2691e", 18 | "coral": "#ff7f50", 19 | "cornflowerblue": "#6495ed", 20 | "cornsilk": "#fff8dc", 21 | "crimson": "#dc143c", 22 | "cyan": "#00ffff", 23 | "darkblue": "#00008b", 24 | "darkcyan": "#008b8b", 25 | "darkgoldenrod": "#b8860b", 26 | "darkgray": "#a9a9a9", 27 | "darkgreen": "#006400", 28 | "darkgrey": "#a9a9a9", 29 | "darkkhaki": "#bdb76b", 30 | "darkmagenta": "#8b008b", 31 | "darkolivegreen": "#556b2f", 32 | "darkorange": "#ff8c00", 33 | "darkorchid": "#9932cc", 34 | "darkred": "#8b0000", 35 | "darksalmon": "#e9967a", 36 | "darkseagreen": "#8fbc8f", 37 | "darkslateblue": "#483d8b", 38 | "darkslategray": "#2f4f4f", 39 | "darkslategrey": "#2f4f4f", 40 | "darkturquoise": "#00ced1", 41 | "darkviolet": "#9400d3", 42 | "deeppink": "#ff1493", 43 | "deepskyblue": "#00bfff", 44 | "dimgray": "#696969", 45 | "dimgrey": "#696969", 46 | "dodgerblue": "#1e90ff", 47 | "firebrick": "#b22222", 48 | "floralwhite": "#fffaf0", 49 | "forestgreen": "#228b22", 50 | "fuchsia": "#ff00ff", 51 | "gainsboro": "#dcdcdc", 52 | "ghostwhite": "#f8f8ff", 53 | "goldenrod": "#daa520", 54 | "gold": "#ffd700", 55 | "gray": "#808080", 56 | "green": "#008000", 57 | "greenyellow": "#adff2f", 58 | "grey": "#808080", 59 | "honeydew": "#f0fff0", 60 | "hotpink": "#ff69b4", 61 | "indianred": "#cd5c5c", 62 | "indigo": "#4b0082", 63 | "ivory": "#fffff0", 64 | "khaki": "#f0e68c", 65 | "lavenderblush": "#fff0f5", 66 | "lavender": "#e6e6fa", 67 | "lawngreen": "#7cfc00", 68 | "lemonchiffon": "#fffacd", 69 | "lightblue": "#add8e6", 70 | "lightcoral": "#f08080", 71 | "lightcyan": "#e0ffff", 72 | "lightgoldenrodyellow": "#fafad2", 73 | "lightgray": "#d3d3d3", 74 | "lightgreen": "#90ee90", 75 | "lightgrey": "#d3d3d3", 76 | "lightpink": "#ffb6c1", 77 | "lightsalmon": "#ffa07a", 78 | "lightseagreen": "#20b2aa", 79 | "lightskyblue": "#87cefa", 80 | "lightslategray": "#778899", 81 | "lightslategrey": "#778899", 82 | "lightsteelblue": "#b0c4de", 83 | "lightyellow": "#ffffe0", 84 | "lime": "#00ff00", 85 | "limegreen": "#32cd32", 86 | "linen": "#faf0e6", 87 | "magenta": "#ff00ff", 88 | "maroon": "#800000", 89 | "mediumaquamarine": "#66cdaa", 90 | "mediumblue": "#0000cd", 91 | "mediumorchid": "#ba55d3", 92 | "mediumpurple": "#9370db", 93 | "mediumseagreen": "#3cb371", 94 | "mediumslateblue": "#7b68ee", 95 | "mediumspringgreen": "#00fa9a", 96 | "mediumturquoise": "#48d1cc", 97 | "mediumvioletred": "#c71585", 98 | "midnightblue": "#191970", 99 | "mintcream": "#f5fffa", 100 | "mistyrose": "#ffe4e1", 101 | "moccasin": "#ffe4b5", 102 | "navajowhite": "#ffdead", 103 | "navy": "#000080", 104 | "oldlace": "#fdf5e6", 105 | "olive": "#808000", 106 | "olivedrab": "#6b8e23", 107 | "orange": "#ffa500", 108 | "orangered": "#ff4500", 109 | "orchid": "#da70d6", 110 | "palegoldenrod": "#eee8aa", 111 | "palegreen": "#98fb98", 112 | "paleturquoise": "#afeeee", 113 | "palevioletred": "#db7093", 114 | "papayawhip": "#ffefd5", 115 | "peachpuff": "#ffdab9", 116 | "peru": "#cd853f", 117 | "pink": "#ffc0cb", 118 | "plum": "#dda0dd", 119 | "powderblue": "#b0e0e6", 120 | "purple": "#800080", 121 | "red": "#ff0000", 122 | "rosybrown": "#bc8f8f", 123 | "royalblue": "#4169e1", 124 | "saddlebrown": "#8b4513", 125 | "salmon": "#fa8072", 126 | "sandybrown": "#f4a460", 127 | "seagreen": "#2e8b57", 128 | "seashell": "#fff5ee", 129 | "sienna": "#a0522d", 130 | "silver": "#c0c0c0", 131 | "skyblue": "#87ceeb", 132 | "slateblue": "#6a5acd", 133 | "slategray": "#708090", 134 | "slategrey": "#708090", 135 | "snow": "#fffafa", 136 | "springgreen": "#00ff7f", 137 | "steelblue": "#4682b4", 138 | "tan": "#d2b48c", 139 | "teal": "#008080", 140 | "thistle": "#d8bfd8", 141 | "tomato": "#ff6347", 142 | "turquoise": "#40e0d0", 143 | "violet": "#ee82ee", 144 | "wheat": "#f5deb3", 145 | "white": "#ffffff", 146 | "whitesmoke": "#f5f5f5", 147 | "yellow": "#ffff00", 148 | "yellowgreen": "#9acd32" 149 | } 150 | -------------------------------------------------------------------------------- /modules/module.cookieFile.ts: -------------------------------------------------------------------------------- 1 | const parse = (data: string) => { 2 | const res: Record = {}; 9 | const split = data.replace(/\r/g,'').split('\n'); 10 | for (const line of split) { 11 | const c = line.split('\t'); 12 | if(c.length < 7){ 13 | continue; 14 | } 15 | res[c[5]] = { 16 | value: c[6], 17 | expires: new Date(parseInt(c[4])*1000), 18 | path: c[2], 19 | domain: c[0].replace(/^\./,''), 20 | secure: c[3] == 'TRUE' ? true : false 21 | }; 22 | } 23 | return res; 24 | }; 25 | 26 | export default parse; 27 | -------------------------------------------------------------------------------- /modules/module.downloadArchive.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs'; 3 | import { ArgvType } from './module.app-args'; 4 | import { workingDir } from './module.cfg-loader'; 5 | 6 | export const archiveFile = path.join(workingDir, 'config', 'archive.json'); 7 | 8 | export type ItemType = { 9 | id: string, 10 | already: string[] 11 | }[] 12 | 13 | export type DataType = { 14 | hidive: { 15 | s: ItemType 16 | }, 17 | ao: { 18 | s: ItemType 19 | }, 20 | adn: { 21 | s: ItemType 22 | }, 23 | crunchy: { 24 | srz: ItemType, 25 | s: ItemType 26 | } 27 | } 28 | 29 | const addToArchive = (kind: { 30 | service: 'crunchy', 31 | type: 's'|'srz' 32 | } | { 33 | service: 'hidive', 34 | type: 's' 35 | } | { 36 | service: 'ao', 37 | type: 's' 38 | } | { 39 | service: 'adn', 40 | type: 's' 41 | }, ID: string) => { 42 | const data = loadData(); 43 | 44 | if (Object.prototype.hasOwnProperty.call(data, kind.service)) { 45 | const items = kind.service === 'crunchy' ? data[kind.service][kind.type] : data[kind.service][kind.type]; 46 | if (items.findIndex(a => a.id === ID) >= 0) // Prevent duplicate 47 | return; 48 | items.push({ 49 | id: ID, 50 | already: [] 51 | }); 52 | (data as any)[kind.service][kind.type] = items; 53 | } else { 54 | if (kind.service === 'ao') { 55 | data['ao'] = { 56 | s: [ 57 | { 58 | id: ID, 59 | already: [] 60 | } 61 | ] 62 | }; 63 | } else if (kind.service === 'crunchy') { 64 | data['crunchy'] = { 65 | s: ([] as ItemType).concat(kind.type === 's' ? { 66 | id: ID, 67 | already: [] as string[] 68 | } : []), 69 | srz: ([] as ItemType).concat(kind.type === 'srz' ? { 70 | id: ID, 71 | already: [] as string[] 72 | } : []), 73 | }; 74 | } else if (kind.service === 'adn') { 75 | data['adn'] = { 76 | s: [ 77 | { 78 | id: ID, 79 | already: [] 80 | } 81 | ] 82 | }; 83 | } else { 84 | data['hidive'] = { 85 | s: [ 86 | { 87 | id: ID, 88 | already: [] 89 | } 90 | ] 91 | }; 92 | } 93 | } 94 | fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4)); 95 | }; 96 | 97 | const downloaded = (kind: { 98 | service: 'crunchy', 99 | type: 's'|'srz' 100 | } | { 101 | service: 'hidive', 102 | type: 's' 103 | } | { 104 | service: 'ao', 105 | type: 's' 106 | } | { 107 | service: 'adn', 108 | type: 's' 109 | }, ID: string, episode: string[]) => { 110 | let data = loadData(); 111 | if (!Object.prototype.hasOwnProperty.call(data, kind.service) || !Object.prototype.hasOwnProperty.call(data[kind.service], kind.type) 112 | || !Object.prototype.hasOwnProperty.call((data as any)[kind.service][kind.type], ID)) { 113 | addToArchive(kind, ID); 114 | data = loadData(); // Load updated version 115 | } 116 | 117 | const archivedata = (kind.service == 'crunchy' ? data[kind.service][kind.type] : data[kind.service][kind.type]); 118 | const alreadyData = archivedata.find(a => a.id === ID)?.already; 119 | for (const ep of episode) { 120 | if (alreadyData?.includes(ep)) continue; 121 | alreadyData?.push(ep); 122 | } 123 | fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4)); 124 | }; 125 | 126 | const makeCommand = (service: 'crunchy'|'hidive'|'ao'|'adn') : Partial[] => { 127 | const data = loadData(); 128 | const ret: Partial[] = []; 129 | const kind = data[service]; 130 | for (const type of Object.keys(kind)) { 131 | const item = kind[type as 's']; // 'srz' is also possible but will be ignored for the compiler 132 | item.forEach(i => ret.push({ 133 | but: true, 134 | all: false, 135 | service, 136 | e: i.already.join(','), 137 | ...(type === 's' ? { 138 | s: i.id, 139 | series: undefined 140 | } : { 141 | series: i.id, 142 | s: undefined 143 | }) 144 | })); 145 | } 146 | return ret; 147 | }; 148 | 149 | const loadData = () : DataType => { 150 | if (fs.existsSync(archiveFile)) 151 | return JSON.parse(fs.readFileSync(archiveFile).toString()) as DataType; 152 | return {} as DataType; 153 | }; 154 | 155 | export { addToArchive, downloaded, makeCommand }; -------------------------------------------------------------------------------- /modules/module.filename.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { AvailableFilenameVars } from './module.args'; 3 | import { console } from './log'; 4 | import Helper from './module.helper'; 5 | 6 | export type Variable = ({ 7 | type: 'number', 8 | replaceWith: number 9 | } | { 10 | type: 'string', 11 | replaceWith: string 12 | }) & { 13 | name: T, 14 | sanitize?: boolean 15 | } 16 | 17 | const parseFileName = (input: string, variables: Variable[], numbers: number, override: string[]): string[] => { 18 | const varRegex = /\${[A-Za-z1-9]+}/g; 19 | const vars = input.match(varRegex); 20 | const overridenVars = parseOverride(variables, override); 21 | if (!vars) 22 | return [input]; 23 | for (let i = 0; i < vars.length; i++) { 24 | const type = vars[i]; 25 | const varName = type.slice(2, -1); 26 | let use = overridenVars.find(a => a.name === varName); 27 | if (use === undefined && type === '${height}') { 28 | use = { type: 'number', replaceWith: 0 } as Variable; 29 | } 30 | if (use === undefined) { 31 | console.info(`[ERROR] Found variable '${type}' in fileName but no values was internally found!`); 32 | continue; 33 | } 34 | 35 | if (use.type === 'number') { 36 | const len = use.replaceWith.toFixed(0).length; 37 | const replaceStr = len < numbers ? '0'.repeat(numbers - len) + use.replaceWith : use.replaceWith+''; 38 | input = input.replace(type, replaceStr); 39 | } else { 40 | if (use.sanitize) 41 | use.replaceWith = Helper.cleanupFilename(use.replaceWith); 42 | input = input.replace(type, use.replaceWith); 43 | } 44 | } 45 | return input.split(path.sep).map(a => Helper.cleanupFilename(a)); 46 | }; 47 | 48 | const parseOverride = (variables: Variable[], override: string[]): Variable[] => { 49 | const vars: Variable[] = variables; 50 | override.forEach(item => { 51 | const index = item.indexOf('='); 52 | if (index === -1) 53 | return logError(item, 'invalid'); 54 | const parts = [ item.slice(0, index), item.slice(index + 1) ]; 55 | if (!(parts[1].startsWith('\'') && parts[1].endsWith('\'') && parts[1].length >= 2)) 56 | return logError(item, 'invalid'); 57 | parts[1] = parts[1].slice(1, -1); 58 | const already = vars.findIndex(a => a.name === parts[0]); 59 | if (already > -1) { 60 | if (vars[already].type === 'number') { 61 | if (isNaN(parseFloat(parts[1]))) 62 | return logError(item, 'wrongType'); 63 | vars[already].replaceWith = parseFloat(parts[1]); 64 | } else { 65 | vars[already].replaceWith = parts[1]; 66 | } 67 | } else { 68 | const isNumber = !isNaN(parseFloat(parts[1])); 69 | vars.push({ 70 | name: parts[0], 71 | replaceWith: isNumber ? parseFloat(parts[1]) : parts[1], 72 | type: isNumber ? 'number' : 'string' 73 | } as Variable); 74 | } 75 | }); 76 | 77 | return variables; 78 | }; 79 | 80 | const logError = (override: string, reason: 'invalid'|'wrongType') => { 81 | switch (reason) { 82 | case 'wrongType': 83 | console.error(`[ERROR] Invalid type on \`${override}\`. Expected number but found string. It has been ignored`); 84 | break; 85 | case 'invalid': 86 | default: 87 | console.error(`[ERROR] Invalid override \`${override}\`. It has been ignored`); 88 | } 89 | }; 90 | 91 | export default parseFileName; -------------------------------------------------------------------------------- /modules/module.fontsData.ts: -------------------------------------------------------------------------------- 1 | // fonts src 2 | const root = 'https://static.crunchyroll.com/vilos-v2/web/vilos/assets/libass-fonts/'; 3 | 4 | // file list 5 | const fontFamilies = { 6 | 'Adobe Arabic': [ 'AdobeArabic-Bold.otf', ], 7 | 'Andale Mono': [ 'andalemo.ttf', ], 8 | 'Arial': [ 'arial.ttf', 'arialbd.ttf', 'arialbi.ttf', 'ariali.ttf', ], 9 | 'Arial Unicode MS': [ 'arialuni.ttf', ], 10 | 'Arial Black': [ 'ariblk.ttf', ], 11 | 'Comic Sans MS': [ 'comic.ttf', 'comicbd.ttf', ], 12 | 'Courier New': [ 'cour.ttf', 'courbd.ttf', 'courbi.ttf', 'couri.ttf', ], 13 | 'DejaVu LGC Sans Mono': [ 'DejaVuLGCSansMono-Bold.ttf', 'DejaVuLGCSansMono-BoldOblique.ttf', 'DejaVuLGCSansMono-Oblique.ttf', 'DejaVuLGCSansMono.ttf', ], 14 | 'DejaVu Sans': [ 'DejaVuSans-Bold.ttf', 'DejaVuSans-BoldOblique.ttf', 'DejaVuSans-ExtraLight.ttf', 'DejaVuSans-Oblique.ttf', 'DejaVuSans.ttf', ], 15 | 'DejaVu Sans Condensed': [ 'DejaVuSansCondensed-Bold.ttf', 'DejaVuSansCondensed-BoldOblique.ttf', 'DejaVuSansCondensed-Oblique.ttf', 'DejaVuSansCondensed.ttf', ], 16 | 'DejaVu Sans Mono': [ 'DejaVuSansMono-Bold.ttf', 'DejaVuSansMono-BoldOblique.ttf', 'DejaVuSansMono-Oblique.ttf', 'DejaVuSansMono.ttf', ], 17 | 'Georgia': [ 'georgia.ttf', 'georgiab.ttf', 'georgiai.ttf', 'georgiaz.ttf', ], 18 | 'Impact': [ 'impact.ttf', ], 19 | 'Rubik Black': [ 'Rubik-Black.ttf', 'Rubik-BlackItalic.ttf', ], 20 | 'Rubik': [ 'Rubik-Bold.ttf', 'Rubik-BoldItalic.ttf', 'Rubik-Italic.ttf', 'Rubik-Light.ttf', 'Rubik-LightItalic.ttf', 'Rubik-Medium.ttf', 'Rubik-MediumItalic.ttf', 'Rubik-Regular.ttf', ], 21 | 'Tahoma': [ 'tahoma.ttf', ], 22 | 'Times New Roman': [ 'times.ttf', 'timesbd.ttf', 'timesbi.ttf', 'timesi.ttf', ], 23 | 'Trebuchet MS': [ 'trebuc.ttf', 'trebucbd.ttf', 'trebucbi.ttf', 'trebucit.ttf', ], 24 | 'Verdana': [ 'verdana.ttf', 'verdanab.ttf', 'verdanai.ttf', 'verdanaz.ttf', ], 25 | 'Webdings': [ 'webdings.ttf', ], 26 | }; 27 | 28 | // collect styles from ass string 29 | function assFonts(ass: string){ 30 | const strings = ass.replace(/\r/g,'').split('\n'); 31 | const styles: string[] = []; 32 | for(const s of strings){ 33 | if(s.match(/^Style: /)){ 34 | const addStyle = s.split(','); 35 | styles.push(addStyle[1]); 36 | } 37 | } 38 | const fontMatches = ass.matchAll(/\\fn([^\\}]+)/g); 39 | for (const match of fontMatches) { 40 | styles.push(match[1]); 41 | } 42 | return [...new Set(styles)]; 43 | } 44 | 45 | // font mime type 46 | function fontMime(fontFile: string){ 47 | if(fontFile.match(/\.otf$/)){ 48 | return 'application/vnd.ms-opentype'; 49 | } 50 | if(fontFile.match(/\.ttf$/)){ 51 | return 'application/x-truetype-font'; 52 | } 53 | return 'application/octet-stream'; 54 | } 55 | 56 | export type AvailableFonts = keyof typeof fontFamilies; 57 | 58 | // output 59 | export { root, fontFamilies, assFonts, fontMime }; 60 | -------------------------------------------------------------------------------- /modules/module.helper.ts: -------------------------------------------------------------------------------- 1 | // Helper functions 2 | import readline from 'readline/promises'; 3 | import { stdin as input, stdout as output } from 'process'; 4 | import childProcess from 'child_process'; 5 | import { console } from './log'; 6 | 7 | export default class Helper { 8 | static async question(q: string) { 9 | const rl = readline.createInterface({ input, output }); 10 | const a = await rl.question(q); 11 | rl.close(); 12 | return a; 13 | } 14 | static formatTime(t: number) { 15 | const days = Math.floor(t / 86400); 16 | const hours = Math.floor((t % 86400) / 3600); 17 | const minutes = Math.floor(((t % 86400) % 3600) / 60); 18 | const seconds = t % 60; 19 | const daysS = days > 0 ? `${days}d` : ''; 20 | const hoursS = daysS || hours ? `${daysS}${daysS && hours < 10 ? '0' : ''}${hours}h` : ''; 21 | const minutesS = minutes || hoursS ? `${hoursS}${hoursS && minutes < 10 ? '0' : ''}${minutes}m` : ''; 22 | const secondsS = `${minutesS}${minutesS && seconds < 10 ? '0' : ''}${seconds}s`; 23 | return secondsS; 24 | } 25 | 26 | static cleanupFilename(n: string) { 27 | /* eslint-disable no-extra-boolean-cast, no-useless-escape, no-control-regex */ 28 | const fixingChar = '_'; 29 | const illegalRe = /[\/\?<>\\:\*\|":]/g; 30 | const controlRe = /[\x00-\x1f\x80-\x9f]/g; 31 | const reservedRe = /^\.+$/; 32 | const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i; 33 | const windowsTrailingRe = /[\. ]+$/; 34 | return n 35 | .replace(illegalRe, fixingChar) 36 | .replace(controlRe, fixingChar) 37 | .replace(reservedRe, fixingChar) 38 | .replace(windowsReservedRe, fixingChar) 39 | .replace(windowsTrailingRe, fixingChar); 40 | } 41 | 42 | static exec( 43 | pname: string, 44 | fpath: string, 45 | pargs: string, 46 | spc = false 47 | ): 48 | | { 49 | isOk: true; 50 | } 51 | | { 52 | isOk: false; 53 | err: Error & { code: number }; 54 | } { 55 | pargs = pargs ? ' ' + pargs : ''; 56 | console.info(`\n> "${pname}"${pargs}${spc ? '\n' : ''}`); 57 | try { 58 | if (process.platform === 'win32') { 59 | childProcess.execSync('& ' + fpath + pargs, { stdio: 'inherit', shell: 'powershell.exe', windowsHide: true }); 60 | } else { 61 | childProcess.execSync(fpath + pargs, { stdio: 'inherit' }); 62 | } 63 | return { 64 | isOk: true 65 | }; 66 | } catch (er) { 67 | const err = er as Error & { status: number }; 68 | return { 69 | isOk: false, 70 | err: { 71 | ...err, 72 | code: err.status 73 | } 74 | }; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /modules/module.parseSelect.ts: -------------------------------------------------------------------------------- 1 | import { console } from './log'; 2 | 3 | const parseSelect = (selectString: string, but = false) : { 4 | isSelected: (val: string|string[]) => boolean, 5 | values: string[] 6 | } => { 7 | if (!selectString) 8 | return { 9 | values: [], 10 | isSelected: () => but 11 | }; 12 | const parts = selectString.split(','); 13 | const select: string[] = []; 14 | 15 | parts.forEach(part => { 16 | if (part.includes('-')) { 17 | const splits = part.split('-'); 18 | if (splits.length !== 2) { 19 | console.warn(`[WARN] Unable to parse input "${part}"`); 20 | return; 21 | } 22 | 23 | const firstPart = splits[0]; 24 | const match = firstPart.match(/[A-Za-z]+/); 25 | if (match && match.length > 0) { 26 | if (match.index && match.index !== 0) { 27 | console.warn(`[WARN] Unable to parse input "${part}"`); 28 | return; 29 | } 30 | const letters = firstPart.substring(0, match[0].length); 31 | const number = parseFloat(firstPart.substring(match[0].length)); 32 | const b = parseFloat(splits[1]); 33 | if (isNaN(number) || isNaN(b)) { 34 | console.warn(`[WARN] Unable to parse input "${part}"`); 35 | return; 36 | } 37 | for (let i = number; i <= b; i++) { 38 | select.push(`${letters}${i}`); 39 | } 40 | 41 | } else { 42 | const a = parseFloat(firstPart); 43 | const b = parseFloat(splits[1]); 44 | if (isNaN(a) || isNaN(b)) { 45 | console.warn(`[WARN] Unable to parse input "${part}"`); 46 | return; 47 | } 48 | for (let i = a; i <= b; i++) { 49 | select.push(`${i}`); 50 | } 51 | } 52 | 53 | } else { 54 | if (part.match(/[0-9A-Z]{9}/)) { 55 | select.push(part); 56 | return; 57 | } else if (part.match(/[A-Z]{3}\.[0-9]*/)) { 58 | select.push(part); 59 | return; 60 | } 61 | const match = part.match(/[A-Za-z]+/); 62 | if (match && match.length > 0) { 63 | if (match.index && match.index !== 0) { 64 | console.warn(`[WARN] Unable to parse input "${part}"`); 65 | return; 66 | } 67 | const letters = part.substring(0, match[0].length); 68 | const number = parseFloat(part.substring(match[0].length)); 69 | if (isNaN(number)) { 70 | console.warn(`[WARN] Unable to parse input "${part}"`); 71 | return; 72 | } 73 | select.push(`${letters}${number}`); 74 | } else { 75 | select.push(`${parseFloat(part)}`); 76 | } 77 | } 78 | }); 79 | 80 | return { 81 | values: select, 82 | isSelected: (st) => { 83 | if (typeof st === 'string') 84 | st = [st]; 85 | return st.some(st => { 86 | const match = st.match(/[A-Za-z]+/); 87 | if (st.match(/[0-9A-Z]{9}/)) { 88 | const included = select.includes(st); 89 | return but ? !included : included; 90 | } else if (match && match.length > 0) { 91 | if (match.index && match.index !== 0) { 92 | return false; 93 | } 94 | const letter = st.substring(0, match[0].length); 95 | const number = parseFloat(st.substring(match[0].length)); 96 | if (isNaN(number)) { 97 | return false; 98 | } 99 | const included = select.includes(`${letter}${number}`); 100 | return but ? !included : included; 101 | } else { 102 | const included = select.includes(`${parseFloat(st)}`); 103 | return but ? !included : included; 104 | } 105 | }); 106 | } 107 | }; 108 | }; 109 | 110 | export default parseSelect; -------------------------------------------------------------------------------- /modules/playready/device.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from 'binary-parser-encoder'; 2 | import { CertificateChain } from './bcert'; 3 | import ECCKey from './ecc_key'; 4 | import * as fs from 'fs'; 5 | 6 | type RawDeviceV2 = { 7 | signature: string; 8 | version: number; 9 | group_certificate_length: number; 10 | group_certificate: Buffer; 11 | encryption_key: Buffer; 12 | signing_key: Buffer; 13 | }; 14 | 15 | class DeviceStructs { 16 | static magic = 'PRD'; 17 | 18 | static v1 = new Parser() 19 | .string('signature', { length: 3, assert: DeviceStructs.magic }) 20 | .uint8('version') 21 | .uint32('group_key_length') 22 | .buffer('group_key', { length: 'group_key_length' }) 23 | .uint32('group_certificate_length') 24 | .buffer('group_certificate', { length: 'group_certificate_length' }); 25 | 26 | static v2 = new Parser() 27 | .string('signature', { length: 3, assert: DeviceStructs.magic }) 28 | .uint8('version') 29 | .uint32('group_certificate_length') 30 | .buffer('group_certificate', { length: 'group_certificate_length' }) 31 | .buffer('encryption_key', { length: 96 }) 32 | .buffer('signing_key', { length: 96 }); 33 | 34 | static v3 = new Parser() 35 | .string('signature', { length: 3, assert: DeviceStructs.magic }) 36 | .uint8('version') 37 | .buffer('group_key', { length: 96 }) 38 | .buffer('encryption_key', { length: 96 }) 39 | .buffer('signing_key', { length: 96 }) 40 | .uint32('group_certificate_length') 41 | .buffer('group_certificate', { length: 'group_certificate_length' }); 42 | } 43 | 44 | export class Device { 45 | static CURRENT_STRUCT = DeviceStructs.v3; 46 | 47 | group_certificate: CertificateChain; 48 | encryption_key: ECCKey; 49 | signing_key: ECCKey; 50 | security_level: number; 51 | 52 | constructor(parsedData: RawDeviceV2) { 53 | this.group_certificate = CertificateChain.loads( 54 | parsedData.group_certificate 55 | ); 56 | this.encryption_key = ECCKey.loads(parsedData.encryption_key); 57 | this.signing_key = ECCKey.loads(parsedData.signing_key); 58 | this.security_level = this.group_certificate.get_security_level(); 59 | } 60 | 61 | static loads(data: Buffer): Device { 62 | const parsedData = Device.CURRENT_STRUCT.parse(data); 63 | return new Device(parsedData); 64 | } 65 | 66 | static load(filePath: string): Device { 67 | const data = fs.readFileSync(filePath); 68 | return Device.loads(data); 69 | } 70 | 71 | dumps(): Buffer { 72 | const groupCertBytes = this.group_certificate.dumps(); 73 | const encryptionKeyBytes = this.encryption_key.dumps(); 74 | const signingKeyBytes = this.signing_key.dumps(); 75 | 76 | const buildData = { 77 | signature: DeviceStructs.magic, 78 | version: 2, 79 | group_certificate_length: groupCertBytes.length, 80 | group_certificate: groupCertBytes, 81 | encryption_key: encryptionKeyBytes, 82 | signing_key: signingKeyBytes, 83 | }; 84 | 85 | return Device.CURRENT_STRUCT.encode(buildData); 86 | } 87 | 88 | dump(filePath: string): void { 89 | const data = this.dumps(); 90 | fs.writeFileSync(filePath, data); 91 | } 92 | 93 | get_name(): string { 94 | const name = `${this.group_certificate.get_name()}_sl${ 95 | this.security_level 96 | }`; 97 | return name.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase(); 98 | } 99 | } 100 | 101 | // Device V2 disabled because unstable provisioning 102 | // export class Device { 103 | // group_certificate: CertificateChain 104 | // encryption_key: ECCKey 105 | // signing_key: ECCKey 106 | // security_level: number 107 | 108 | // constructor(group_certificate: Buffer, group_key: Buffer) { 109 | // this.group_certificate = CertificateChain.loads(group_certificate) 110 | 111 | // this.encryption_key = ECCKey.generate() 112 | // this.signing_key = ECCKey.generate() 113 | 114 | // this.security_level = this.group_certificate.get_security_level() 115 | 116 | // const new_certificate = Certificate.new_key_cert( 117 | // randomBytes(16), 118 | // this.group_certificate.get_security_level(), 119 | // randomBytes(16), 120 | // this.signing_key, 121 | // this.encryption_key, 122 | // ECCKey.loads(group_key), 123 | // this.group_certificate 124 | // ) 125 | 126 | // this.group_certificate.prepend(new_certificate) 127 | // } 128 | // } 129 | -------------------------------------------------------------------------------- /modules/playready/ecc_key.ts: -------------------------------------------------------------------------------- 1 | import elliptic from 'elliptic'; 2 | import { createHash } from 'crypto'; 3 | import * as fs from 'fs'; 4 | 5 | export default class ECCKey { 6 | keyPair: elliptic.ec.KeyPair; 7 | 8 | constructor(keyPair: elliptic.ec.KeyPair) { 9 | this.keyPair = keyPair; 10 | } 11 | 12 | static generate(): ECCKey { 13 | const EC = new elliptic.ec('p256'); 14 | const keyPair = EC.genKeyPair(); 15 | return new ECCKey(keyPair); 16 | } 17 | 18 | static construct(privateKey: Buffer | string | number): ECCKey { 19 | if (Buffer.isBuffer(privateKey)) { 20 | privateKey = privateKey.toString('hex'); 21 | } else if (typeof privateKey === 'number') { 22 | privateKey = privateKey.toString(16); 23 | } 24 | 25 | const EC = new elliptic.ec('p256'); 26 | const keyPair = EC.keyFromPrivate(privateKey, 'hex'); 27 | 28 | return new ECCKey(keyPair); 29 | } 30 | 31 | static loads(data: string | Buffer): ECCKey { 32 | if (typeof data === 'string') { 33 | data = Buffer.from(data, 'base64'); 34 | } 35 | if (!Buffer.isBuffer(data)) { 36 | throw new Error(`Expecting Bytes or Base64 input, got ${data}`); 37 | } 38 | 39 | if (data.length !== 96 && data.length !== 32) { 40 | throw new Error( 41 | `Invalid data length. Expecting 96 or 32 bytes, got ${data.length}` 42 | ); 43 | } 44 | 45 | const privateKey = data.subarray(0, 32); 46 | return ECCKey.construct(privateKey); 47 | } 48 | 49 | static load(filePath: string): ECCKey { 50 | const data = fs.readFileSync(filePath); 51 | return ECCKey.loads(data); 52 | } 53 | 54 | dumps(): Buffer { 55 | return Buffer.concat([this.privateBytes(), this.publicBytes()]); 56 | } 57 | 58 | dump(filePath: string): void { 59 | fs.writeFileSync(filePath, this.dumps()); 60 | } 61 | 62 | getPoint(): { x: string; y: string } { 63 | const publicKey = this.keyPair.getPublic(); 64 | return { 65 | x: publicKey.getX().toString('hex'), 66 | y: publicKey.getY().toString('hex'), 67 | }; 68 | } 69 | 70 | privateBytes(): Buffer { 71 | const privateKey = this.keyPair.getPrivate(); 72 | return Buffer.from(privateKey.toArray('be', 32)); 73 | } 74 | 75 | privateSha256Digest(): Buffer { 76 | const hash = createHash('sha256'); 77 | hash.update(this.privateBytes()); 78 | return hash.digest(); 79 | } 80 | 81 | publicBytes(): Buffer { 82 | const publicKey = this.keyPair.getPublic(); 83 | const x = publicKey.getX().toArray('be', 32); 84 | const y = publicKey.getY().toArray('be', 32); 85 | return Buffer.concat([Buffer.from(x), Buffer.from(y)]); 86 | } 87 | 88 | publicSha256Digest(): Buffer { 89 | const hash = createHash('sha256'); 90 | hash.update(this.publicBytes()); 91 | return hash.digest(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /modules/playready/elgamal.ts: -------------------------------------------------------------------------------- 1 | import { ec as EC } from 'elliptic'; 2 | import { randomBytes } from 'crypto'; 3 | import BN from 'bn.js'; 4 | 5 | export interface Point { 6 | getY(): BN; 7 | getX(): BN; 8 | add(point: Point): Point; 9 | mul(n: BN | bigint | number): Point; 10 | neg(): Point; 11 | } 12 | 13 | export default class ElGamal { 14 | curve: EC; 15 | 16 | constructor(curve: EC) { 17 | this.curve = curve; 18 | } 19 | 20 | static toBytes(n: BN): Uint8Array { 21 | const byteArray = n.toString(16).padStart(2, '0'); 22 | if (byteArray.length % 2 !== 0) { 23 | return Uint8Array.from(Buffer.from('0' + byteArray, 'hex')); 24 | } 25 | return Uint8Array.from(Buffer.from(byteArray, 'hex')); 26 | } 27 | 28 | encrypt(messagePoint: Point, publicKey: Point): [Point, Point] { 29 | const ephemeralKey = new BN(randomBytes(32).toString('hex'), 16).mod( 30 | this.curve.n! 31 | ); 32 | const ephemeralKeyBigInt = BigInt(ephemeralKey.toString(10)); 33 | const point1 = this.curve.g.mul(ephemeralKeyBigInt); 34 | const point2 = messagePoint.add(publicKey.mul(ephemeralKeyBigInt)); 35 | 36 | return [point1, point2]; 37 | } 38 | 39 | static decrypt(encrypted: [Point, Point], privateKey: BN): Point { 40 | const [point1, point2] = encrypted; 41 | const sharedSecret = point1.mul(privateKey); 42 | const decryptedMessage = point2.add(sharedSecret.neg()); 43 | return decryptedMessage; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /modules/playready/key.ts: -------------------------------------------------------------------------------- 1 | enum KeyType { 2 | Invalid = 0x0000, 3 | AES128CTR = 0x0001, 4 | RC4 = 0x0002, 5 | AES128ECB = 0x0003, 6 | Cocktail = 0x0004, 7 | AESCBC = 0x0005, 8 | UNKNOWN = 0xffff, 9 | } 10 | 11 | function getKeyType(value: number): KeyType { 12 | switch (value) { 13 | case KeyType.Invalid: 14 | case KeyType.AES128CTR: 15 | case KeyType.RC4: 16 | case KeyType.AES128ECB: 17 | case KeyType.Cocktail: 18 | case KeyType.AESCBC: 19 | return value; 20 | default: 21 | return KeyType.UNKNOWN; 22 | } 23 | } 24 | 25 | enum CipherType { 26 | Invalid = 0x0000, 27 | RSA128 = 0x0001, 28 | ChainedLicense = 0x0002, 29 | ECC256 = 0x0003, 30 | ECCforScalableLicenses = 0x0004, 31 | Scalable = 0x0005, 32 | UNKNOWN = 0xffff, 33 | } 34 | 35 | function getCipherType(value: number): CipherType { 36 | switch (value) { 37 | case CipherType.Invalid: 38 | case CipherType.RSA128: 39 | case CipherType.ChainedLicense: 40 | case CipherType.ECC256: 41 | case CipherType.ECCforScalableLicenses: 42 | case CipherType.Scalable: 43 | return value; 44 | default: 45 | return CipherType.UNKNOWN; 46 | } 47 | } 48 | 49 | export class Key { 50 | key_id: string; 51 | key_type: KeyType; 52 | cipher_type: CipherType; 53 | key_length: number; 54 | key: string; 55 | 56 | constructor( 57 | key_id: string, 58 | key_type: number, 59 | cipher_type: number, 60 | key_length: number, 61 | key: Buffer 62 | ) { 63 | this.key_id = key_id; 64 | this.key_type = getKeyType(key_type); 65 | this.cipher_type = getCipherType(cipher_type); 66 | this.key_length = key_length; 67 | this.key = key.toString('hex'); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /modules/playready/pssh.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from 'binary-parser'; 2 | import { Buffer } from 'buffer'; 3 | import WRMHeader from './wrmheader'; 4 | 5 | const SYSTEM_ID = Buffer.from('9a04f07998404286ab92e65be0885f95', 'hex'); 6 | 7 | const PSSHBox = new Parser() 8 | .uint32('length') 9 | .string('pssh', { length: 4, assert: 'pssh' }) 10 | .uint32('fullbox') 11 | .buffer('system_id', { length: 16 }) 12 | .uint32('data_length') 13 | .buffer('data', { 14 | length: 'data_length', 15 | }); 16 | 17 | const PlayreadyObject = new Parser() 18 | .useContextVars() 19 | .uint16('type') 20 | .uint16('length') 21 | .choice('data', { 22 | tag: 'type', 23 | choices: { 24 | 1: new Parser().string('data', { 25 | length: function () { 26 | return (this as any).$parent.length; 27 | }, 28 | encoding: 'utf16le', 29 | }), 30 | }, 31 | defaultChoice: new Parser().buffer('data', { 32 | length: function () { 33 | return (this as any).$parent.length; 34 | }, 35 | }), 36 | }); 37 | 38 | const PlayreadyHeader = new Parser() 39 | .uint32('length') 40 | .uint16('record_count') 41 | .array('records', { 42 | length: 'record_count', 43 | type: PlayreadyObject, 44 | }); 45 | 46 | function isPlayreadyPsshBox(data: Buffer): boolean { 47 | if (data.length < 28) return false; 48 | return data.subarray(12, 28).equals(SYSTEM_ID); 49 | } 50 | 51 | function isUtf16(data: Buffer): boolean { 52 | for (let i = 1; i < data.length; i += 2) { 53 | if (data[i] !== 0) { 54 | return false; 55 | } 56 | } 57 | return true; 58 | } 59 | 60 | function* getWrmHeaders(wrm_header: any): IterableIterator { 61 | for (const record of wrm_header.records) { 62 | if (record.type === 1 && typeof record.data === 'string') { 63 | yield record.data; 64 | } 65 | } 66 | } 67 | 68 | export class PSSH { 69 | public wrm_headers: string[]; 70 | 71 | constructor(data: string | Buffer) { 72 | if (!data) { 73 | throw new Error('Data must not be empty'); 74 | } 75 | 76 | if (typeof data === 'string') { 77 | try { 78 | data = Buffer.from(data, 'base64'); 79 | } catch (e) { 80 | throw new Error(`Could not decode data as Base64: ${e}`); 81 | } 82 | } 83 | 84 | try { 85 | if (isPlayreadyPsshBox(data)) { 86 | const pssh_box = PSSHBox.parse(data); 87 | const psshData = pssh_box.data; 88 | 89 | if (isUtf16(psshData)) { 90 | this.wrm_headers = [psshData.toString('utf16le')]; 91 | } else if (isUtf16(psshData.subarray(6))) { 92 | this.wrm_headers = [psshData.subarray(6).toString('utf16le')]; 93 | } else if (isUtf16(psshData.subarray(10))) { 94 | this.wrm_headers = [psshData.subarray(10).toString('utf16le')]; 95 | } else { 96 | const playready_header = PlayreadyHeader.parse(psshData); 97 | this.wrm_headers = Array.from(getWrmHeaders(playready_header)); 98 | } 99 | } else { 100 | if (isUtf16(data)) { 101 | this.wrm_headers = [data.toString('utf16le')]; 102 | } else if (isUtf16(data.subarray(6))) { 103 | this.wrm_headers = [data.subarray(6).toString('utf16le')]; 104 | } else if (isUtf16(data.subarray(10))) { 105 | this.wrm_headers = [data.subarray(10).toString('utf16le')]; 106 | } else { 107 | const playready_header = PlayreadyHeader.parse(data); 108 | this.wrm_headers = Array.from(getWrmHeaders(playready_header)); 109 | } 110 | } 111 | } catch (e) { 112 | throw new Error( 113 | 'Could not parse data as a PSSH Box nor a PlayReadyHeader' 114 | ); 115 | } 116 | } 117 | 118 | // Header downgrade 119 | public get_wrm_headers(downgrade_to_v4: boolean = false): string[] { 120 | return this.wrm_headers.map( 121 | downgrade_to_v4 ? this.downgradePSSH : (_) => _ 122 | ); 123 | } 124 | 125 | private downgradePSSH(wrm_header: string): string { 126 | const header = new WRMHeader(wrm_header); 127 | return header.to_v4_0_0_0(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /modules/playready/wrmheader.ts: -------------------------------------------------------------------------------- 1 | import { XMLParser } from 'fast-xml-parser'; 2 | 3 | export class SignedKeyID { 4 | constructor( 5 | public alg_id: string, 6 | public value: string, 7 | public checksum?: string 8 | ) {} 9 | } 10 | 11 | export type Version = '4.0.0.0' | '4.1.0.0' | '4.2.0.0' | '4.3.0.0' | 'UNKNOWN'; 12 | 13 | export type ReturnStructure = [ 14 | SignedKeyID[], 15 | string | null, 16 | string | null, 17 | string | null 18 | ]; 19 | 20 | interface ParsedWRMHeader { 21 | WRMHEADER: { 22 | '@_version': string; 23 | DATA?: any; 24 | }; 25 | } 26 | 27 | export default class WRMHeader { 28 | private header: ParsedWRMHeader['WRMHEADER']; 29 | version: Version; 30 | 31 | constructor(data: string) { 32 | if (!data) throw new Error('Data must not be empty'); 33 | 34 | const parser = new XMLParser({ 35 | ignoreAttributes: false, 36 | removeNSPrefix: true, 37 | attributeNamePrefix: '@_', 38 | }); 39 | const parsed = parser.parse(data) as ParsedWRMHeader; 40 | 41 | if (!parsed.WRMHEADER) throw new Error('Data is not a valid WRMHEADER'); 42 | 43 | this.header = parsed.WRMHEADER; 44 | this.version = WRMHeader.fromString(this.header['@_version']); 45 | } 46 | 47 | private static fromString(value: string): Version { 48 | if (['4.0.0.0', '4.1.0.0', '4.2.0.0', '4.3.0.0'].includes(value)) { 49 | return value as Version; 50 | } 51 | return 'UNKNOWN'; 52 | } 53 | 54 | to_v4_0_0_0(): string { 55 | const [key_ids, la_url, lui_url, ds_id] = this.readAttributes(); 56 | if (key_ids.length === 0) throw new Error('No Key IDs available'); 57 | const key_id = key_ids[0]; 58 | return `16AESCTR${ 59 | key_id.value 60 | }${la_url ? `${la_url}` : ''}${ 61 | lui_url ? `${lui_url}` : '' 62 | }${ds_id ? `${ds_id}` : ''}${ 63 | key_id.checksum ? `${key_id.checksum}` : '' 64 | }`; 65 | } 66 | 67 | readAttributes(): ReturnStructure { 68 | const data = this.header.DATA; 69 | if (!data) 70 | throw new Error( 71 | 'Not a valid PlayReady Header Record, WRMHEADER/DATA required' 72 | ); 73 | switch (this.version) { 74 | case '4.0.0.0': 75 | return WRMHeader.read_v4(data); 76 | case '4.1.0.0': 77 | case '4.2.0.0': 78 | case '4.3.0.0': 79 | return WRMHeader.read_vX(data); 80 | default: 81 | throw new Error(`Unsupported version: ${this.version}`); 82 | } 83 | } 84 | 85 | private static read_v4(data: any): ReturnStructure { 86 | const protectInfo = data.PROTECTINFO; 87 | return [ 88 | [new SignedKeyID(protectInfo.ALGID, data.KID, data.CHECKSUM)], 89 | data.LA_URL || null, 90 | data.LUI_URL || null, 91 | data.DS_ID || null, 92 | ]; 93 | } 94 | 95 | private static read_vX(data: any): ReturnStructure { 96 | const protectInfo = data.PROTECTINFO; 97 | 98 | const signedKeyID: SignedKeyID | undefined = protectInfo.KIDS.KID 99 | ? new SignedKeyID( 100 | protectInfo.KIDS.KID['@_ALGID'] || '', 101 | protectInfo.KIDS.KID['@_VALUE'], 102 | protectInfo.KIDS.KID['@_CHECKSUM'] 103 | ) 104 | : undefined; 105 | return [ 106 | signedKeyID ? [signedKeyID] : [], 107 | data.LA_URL || null, 108 | data.LUI_URL || null, 109 | data.DS_ID || null, 110 | ]; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /modules/playready/xml_key.ts: -------------------------------------------------------------------------------- 1 | import BN from 'bn.js'; 2 | import { ec as EC } from 'elliptic'; 3 | import ECCKey from './ecc_key'; 4 | import ElGamal, { Point } from './elgamal'; 5 | 6 | export default class XmlKey { 7 | private _sharedPoint: ECCKey; 8 | public sharedKeyX: BN; 9 | public sharedKeyY: BN; 10 | public _shared_key_x_bytes: Uint8Array; 11 | public aesIv: Uint8Array; 12 | public aesKey: Uint8Array; 13 | 14 | constructor() { 15 | this._sharedPoint = ECCKey.generate(); 16 | this.sharedKeyX = this._sharedPoint.keyPair.getPublic().getX(); 17 | this.sharedKeyY = this._sharedPoint.keyPair.getPublic().getY(); 18 | this._shared_key_x_bytes = ElGamal.toBytes(this.sharedKeyX); 19 | this.aesIv = this._shared_key_x_bytes.subarray(0, 16); 20 | this.aesKey = this._shared_key_x_bytes.subarray(16, 32); 21 | } 22 | 23 | getPoint(curve: EC): Point { 24 | return curve.curve.point(this.sharedKeyX, this.sharedKeyY); 25 | } 26 | } 27 | 28 | // Make it more undetectable (not working right now) 29 | // import { randomBytes } from 'crypto' 30 | // export default class XmlKey { 31 | // public aesIv: Uint8Array 32 | // public aesKey: Uint8Array 33 | // public bytes: Uint8Array 34 | 35 | // constructor() { 36 | // this.aesIv = randomBytes(16) 37 | // this.aesKey = randomBytes(16) 38 | // this.bytes = new Uint8Array([...this.aesIv, ...this.aesKey]) 39 | 40 | // console.log('XML key (AES/CBC)') 41 | // console.log('iv:', Buffer.from(this.aesIv).toString('hex')) 42 | // console.log('key:', Buffer.from(this.aesKey).toString('hex')) 43 | // console.log('bytes:', this.bytes) 44 | // } 45 | // } 46 | -------------------------------------------------------------------------------- /modules/widevine/cmac.ts: -------------------------------------------------------------------------------- 1 | // Modified version of https://github.com/Frooastside/node-widevine 2 | import crypto from 'crypto'; 3 | 4 | export class AES_CMAC { 5 | private readonly BLOCK_SIZE = 16; 6 | private readonly XOR_RIGHT = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87]); 7 | private readonly EMPTY_BLOCK_SIZE_BUFFER = Buffer.alloc(this.BLOCK_SIZE); 8 | 9 | private _key: Buffer; 10 | private _subkeys: { first: Buffer; second: Buffer }; 11 | 12 | public constructor(key: Buffer) { 13 | if (![16, 24, 32].includes(key.length)) { 14 | throw new Error('Key size must be 128, 192, or 256 bits.'); 15 | } 16 | this._key = key; 17 | this._subkeys = this._generateSubkeys(); 18 | } 19 | 20 | public calculate(message: Buffer): Buffer { 21 | const blockCount = this._getBlockCount(message); 22 | 23 | let x = this.EMPTY_BLOCK_SIZE_BUFFER; 24 | let y; 25 | 26 | for (let i = 0; i < blockCount - 1; i++) { 27 | const from = i * this.BLOCK_SIZE; 28 | const block = message.subarray(from, from + this.BLOCK_SIZE); 29 | y = this._xor(x, block); 30 | x = this._aes(y); 31 | } 32 | 33 | y = this._xor(x, this._getLastBlock(message)); 34 | x = this._aes(y); 35 | 36 | return x; 37 | } 38 | 39 | private _generateSubkeys(): { first: Buffer; second: Buffer } { 40 | const l = this._aes(this.EMPTY_BLOCK_SIZE_BUFFER); 41 | 42 | let first = this._bitShiftLeft(l); 43 | if (l[0] & 0x80) { 44 | first = this._xor(first, this.XOR_RIGHT); 45 | } 46 | 47 | let second = this._bitShiftLeft(first); 48 | if (first[0] & 0x80) { 49 | second = this._xor(second, this.XOR_RIGHT); 50 | } 51 | 52 | return { first: first, second: second }; 53 | } 54 | 55 | private _getBlockCount(message: Buffer): number { 56 | const blockCount = Math.ceil(message.length / this.BLOCK_SIZE); 57 | return blockCount === 0 ? 1 : blockCount; 58 | } 59 | 60 | private _aes(message: Buffer): Buffer { 61 | const cipher = crypto.createCipheriv(`aes-${this._key.length * 8}-cbc`, this._key, Buffer.alloc(this.BLOCK_SIZE)); 62 | const result = cipher.update(message).subarray(0, 16); 63 | cipher.destroy(); 64 | return result; 65 | } 66 | 67 | private _getLastBlock(message: Buffer): Buffer { 68 | const blockCount = this._getBlockCount(message); 69 | const paddedBlock = this._padding(message, blockCount - 1); 70 | 71 | let complete = false; 72 | if (message.length > 0) { 73 | complete = message.length % this.BLOCK_SIZE === 0; 74 | } 75 | 76 | const key = complete ? this._subkeys.first : this._subkeys.second; 77 | return this._xor(paddedBlock, key); 78 | } 79 | 80 | private _padding(message: Buffer, blockIndex: number): Buffer { 81 | const block = Buffer.alloc(this.BLOCK_SIZE); 82 | 83 | const from = blockIndex * this.BLOCK_SIZE; 84 | 85 | const slice = message.subarray(from, from + this.BLOCK_SIZE); 86 | block.set(slice); 87 | 88 | if (slice.length !== this.BLOCK_SIZE) { 89 | block[slice.length] = 0x80; 90 | } 91 | 92 | return block; 93 | } 94 | 95 | private _bitShiftLeft(input: Buffer): Buffer { 96 | const output = Buffer.alloc(input.length); 97 | let overflow = 0; 98 | for (let i = input.length - 1; i >= 0; i--) { 99 | output[i] = (input[i] << 1) | overflow; 100 | overflow = input[i] & 0x80 ? 1 : 0; 101 | } 102 | return output; 103 | } 104 | 105 | private _xor(a: Buffer, b: Buffer): Buffer { 106 | const length = Math.min(a.length, b.length); 107 | const output = Buffer.alloc(length); 108 | for (let i = 0; i < length; i++) { 109 | output[i] = a[i] ^ b[i]; 110 | } 111 | return output; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multi-downloader-nx", 3 | "short_name": "aniDL", 4 | "version": "5.4.5", 5 | "description": "Downloader for Crunchyroll, Hidive, AnimeOnegai, and AnimationDigitalNetwork with CLI and GUI", 6 | "keywords": [ 7 | "download", 8 | "downloader", 9 | "hidive", 10 | "crunchy", 11 | "crunchyroll", 12 | "util", 13 | "utility", 14 | "cli", 15 | "gui" 16 | ], 17 | "engines": { 18 | "node": ">=18", 19 | "pnpm": ">=7" 20 | }, 21 | "author": "AnimeDL ", 22 | "contributors": [ 23 | { 24 | "name": "AnimeDL " 25 | }, 26 | { 27 | "name": "AniDL " 28 | }, 29 | { 30 | "name": "AnidlSupport " 31 | } 32 | ], 33 | "homepage": "https://github.com/anidl/multi-downloader-nx", 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/anidl/multi-downloader-nx.git" 37 | }, 38 | "bugs": { 39 | "url": "https://github.com/anidl/multi-downloader-nx/issues" 40 | }, 41 | "license": "MIT", 42 | "dependencies": { 43 | "@bufbuild/buf": "^1.54.0", 44 | "@bufbuild/protobuf": "^2.4.0", 45 | "@bufbuild/protoc-gen-es": "^2.4.0", 46 | "@yao-pkg/pkg": "^6.5.1", 47 | "binary-parser": "^2.2.1", 48 | "binary-parser-encoder": "^1.5.3", 49 | "bn.js": "^5.2.2", 50 | "cors": "^2.8.5", 51 | "elliptic": "^6.6.1", 52 | "esbuild": "^0.25.4", 53 | "express": "^5.1.0", 54 | "fast-xml-parser": "^5.2.3", 55 | "ffprobe": "^1.1.2", 56 | "fs-extra": "^11.3.0", 57 | "iso-639": "^0.2.2", 58 | "leven": "^3.1.0", 59 | "log4js": "^6.9.1", 60 | "long": "^5.3.2", 61 | "lookpath": "^1.2.3", 62 | "m3u8-parsed": "^2.0.0", 63 | "mpd-parser": "^1.3.1", 64 | "node-forge": "^1.3.1", 65 | "ofetch": "^1.4.1", 66 | "open": "^8.4.2", 67 | "protobufjs": "^7.5.2", 68 | "puppeteer-real-browser": "^1.4.2", 69 | "ws": "^8.18.2", 70 | "yaml": "^2.8.0", 71 | "yargs": "^17.7.2" 72 | }, 73 | "devDependencies": { 74 | "@eslint/js": "^9.27.0", 75 | "@types/bn.js": "^5.1.6", 76 | "@types/cors": "^2.8.18", 77 | "@types/elliptic": "^6.4.18", 78 | "@types/express": "^5.0.2", 79 | "@types/ffprobe": "^1.1.8", 80 | "@types/fs-extra": "^11.0.4", 81 | "@types/node": "^22.15.18", 82 | "@types/node-forge": "^1.3.11", 83 | "@types/ws": "^8.18.1", 84 | "@types/yargs": "^17.0.33", 85 | "@typescript-eslint/eslint-plugin": "^8.32.1", 86 | "@typescript-eslint/parser": "^8.32.1", 87 | "eslint": "^9.27.0", 88 | "protoc": "^1.1.3", 89 | "removeNPMAbsolutePaths": "^3.0.1", 90 | "ts-node": "^10.9.2", 91 | "typescript": "5.8.3", 92 | "typescript-eslint": "8.32.1" 93 | }, 94 | "scripts": { 95 | "prestart": "pnpm run tsc test", 96 | "start": "pnpm prestart && cd lib && node gui.js", 97 | "gui": "cd ./gui/react/ && pnpm start", 98 | "docs": "ts-node modules/build-docs.ts", 99 | "tsc": "ts-node tsc.ts", 100 | "proto:compile": "protoc --plugin=protoc-gen-ts_proto=.\\node_modules\\.bin\\protoc-gen-ts_proto.cmd --ts_proto_opt=\"esModuleInterop=true\" --ts_proto_opt=\"forceLong=long\" --ts_proto_opt=\"env=node\" --ts_proto_out=. modules/*.proto", 101 | "prebuild-cli": "pnpm run tsc false false", 102 | "build-windows-cli": "pnpm run prebuild-cli && cd lib && node modules/build windows-x64", 103 | "build-linux-cli": "pnpm run prebuild-cli && cd lib && node modules/build linuxstatic-x64", 104 | "build-arm-cli": "pnpm run prebuild-cli && cd lib && node modules/build linux-arm64", 105 | "build-macos-cli": "pnpm run prebuild-cli && cd lib && node modules/build macos-x64", 106 | "build-alpine-cli": "pnpm run prebuild-cli && cd lib && node modules/build alpine-x64", 107 | "build-android-cli": "pnpm run prebuild-cli && cd lib && node modules/build linuxstatic-armv7", 108 | "prebuild-gui": "pnpm run tsc", 109 | "build-windows-gui": "pnpm run prebuild-gui && cd lib && node modules/build windows-x64 true", 110 | "build-linux-gui": "pnpm run prebuild-gui && cd lib && node modules/build linuxstatic-x64 true", 111 | "build-arm-gui": "pnpm run prebuild-gui && cd lib && node modules/build linux-arm64 true", 112 | "build-macos-gui": "pnpm run prebuild-gui && cd lib && node modules/build macos-x64 true", 113 | "build-alpine-gui": "pnpm run prebuild-gui && cd lib && node modules/build alpine-x64 true", 114 | "build-android-gui": "pnpm run prebuild-gui && cd lib && node modules/build linuxstatic-armv7 true", 115 | "eslint": "npx eslint .", 116 | "eslint-fix": "npx eslint . --fix", 117 | "pretest": "pnpm run tsc", 118 | "test": "pnpm run pretest && cd lib && node modules/build windows-x64 && node modules/build linuxstatic-x64 && node modules/build macos-x64" 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /playready/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anidl/multi-downloader-nx/0133a774b6a702481d1d671908d80031a58b2d51/playready/.gitkeep -------------------------------------------------------------------------------- /tsc.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess, exec } from 'child_process'; 2 | import fs from 'fs-extra'; 3 | import path from 'path'; 4 | import { removeSync, copyFileSync } from 'fs-extra'; 5 | 6 | const argv = process.argv.slice(2); 7 | let buildIgnore: string[] = []; 8 | 9 | const isTest = argv.length > 0 && argv[0] === 'test'; 10 | const isGUI = !(argv.length > 1 && argv[1] === 'false'); 11 | 12 | if (!isTest) 13 | buildIgnore = [ 14 | '*/\\.env', 15 | './config/setup.json' 16 | ]; 17 | 18 | if (!isGUI) 19 | buildIgnore = buildIgnore.concat([ 20 | './gui*', 21 | './build*', 22 | 'gui.ts' 23 | ]); 24 | 25 | 26 | const ignore = [ 27 | ...buildIgnore, 28 | '*/\\.git*', 29 | './lib*', 30 | '*/@types*', 31 | './out*', 32 | './bin/mkvtoolnix*', 33 | './config/token.yml$', 34 | './config/updates.json$', 35 | './config/*_token.yml$', 36 | './config/*_sess.yml$', 37 | './config/*_profile.yml$', 38 | '*/\\.eslint*', 39 | '*/*\\.tsx?$', 40 | './fonts*', 41 | './gui/react*', 42 | './dev.js$', 43 | '*/node_modules/*', 44 | './widevine/*', 45 | './playready/*', 46 | './videos/*', 47 | './logs/*', 48 | ].map(a => a.replace(/\*/g, '[^]*').replace(/\.\//g, escapeRegExp(__dirname) + '/').replace(/\//g, path.sep === '\\' ? '\\\\' : '/')).map(a => new RegExp(a, 'i')); 49 | 50 | export { ignore }; 51 | 52 | (async () => { 53 | 54 | const waitForProcess = async (proc: ChildProcess) => { 55 | return new Promise((resolve, reject) => { 56 | proc.stdout?.on('data', console.log); 57 | proc.stderr?.on('data', console.error); 58 | proc.on('close', resolve); 59 | proc.on('error', reject); 60 | }); 61 | }; 62 | 63 | process.stdout.write('Removing lib dir... '); 64 | removeSync('lib'); 65 | process.stdout.write('✓\nRunning tsc... '); 66 | const tsc = exec('npx tsc'); 67 | 68 | await waitForProcess(tsc); 69 | 70 | if (!isGUI) { 71 | fs.emptyDirSync(path.join('lib', 'gui')); 72 | fs.rmdirSync(path.join('lib', 'gui')); 73 | } 74 | 75 | if (!isTest && isGUI) { 76 | process.stdout.write('✓\nBuilding react... '); 77 | 78 | const installReactDependencies = exec('pnpm install', { 79 | cwd: path.join(__dirname, 'gui', 'react'), 80 | }); 81 | 82 | await waitForProcess(installReactDependencies); 83 | 84 | const react = exec('pnpm run build', { 85 | cwd: path.join(__dirname, 'gui', 'react'), 86 | env: { 87 | ...process.env, 88 | CI: 'false' 89 | } 90 | }); 91 | 92 | await waitForProcess(react); 93 | } 94 | 95 | process.stdout.write('✓\nCopying files... '); 96 | if (!isTest && isGUI) { 97 | copyDir(path.join(__dirname, 'gui', 'react', 'build'), path.join(__dirname, 'lib', 'gui', 'server', 'build')); 98 | } 99 | 100 | const files = readDir(__dirname); 101 | files.forEach(item => { 102 | const itemPath = path.join(__dirname, 'lib', item.path.replace(__dirname, '')); 103 | if (item.stats.isDirectory()) { 104 | if (!fs.existsSync(itemPath)) 105 | fs.mkdirSync(itemPath); 106 | } else { 107 | copyFileSync(item.path, itemPath); 108 | } 109 | }); 110 | 111 | process.stdout.write('✓\nInstalling dependencies... '); 112 | if (!isTest) { 113 | const dependencies = exec(`pnpm install ${isGUI ? '' : '-P'}`, { 114 | cwd: path.join(__dirname, 'lib') 115 | }); 116 | await waitForProcess(dependencies); 117 | } 118 | 119 | process.stdout.write('✓\n'); 120 | })(); 121 | 122 | function readDir (dir: string): { 123 | path: string, 124 | stats: fs.Stats 125 | }[] { 126 | const items: { 127 | path: string, 128 | stats: fs.Stats 129 | }[] = []; 130 | const content = fs.readdirSync(dir); 131 | itemLoop: for (const item of content) { 132 | const itemPath = path.join(dir, item); 133 | for (const ignoreItem of ignore) { 134 | if (ignoreItem.test(itemPath)) 135 | continue itemLoop; 136 | } 137 | const stats = fs.statSync(itemPath); 138 | items.push({ 139 | path: itemPath, 140 | stats 141 | }); 142 | if (stats.isDirectory()) { 143 | items.push(...readDir(itemPath)); 144 | } 145 | } 146 | return items; 147 | } 148 | 149 | async function copyDir(src: string, dest: string) { 150 | await fs.promises.mkdir(dest, { recursive: true }); 151 | const entries = await fs.promises.readdir(src, { withFileTypes: true }); 152 | 153 | for (const entry of entries) { 154 | const srcPath = path.join(src, entry.name); 155 | const destPath = path.join(dest, entry.name); 156 | 157 | entry.isDirectory() ? 158 | await copyDir(srcPath, destPath) : 159 | await fs.promises.copyFile(srcPath, destPath); 160 | } 161 | } 162 | 163 | function escapeRegExp(string: string): string { 164 | return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string 165 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "outDir": "./lib", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "downlevelIteration": true, 12 | "jsx": "react" 13 | }, 14 | "exclude": [ 15 | "./videos", 16 | "./tsc.ts", 17 | "lib/**/*", 18 | "gui/react/**/*" 19 | ] 20 | } -------------------------------------------------------------------------------- /videos/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /widevine/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anidl/multi-downloader-nx/0133a774b6a702481d1d671908d80031a58b2d51/widevine/.gitkeep --------------------------------------------------------------------------------