From 130d964b647a3afaa3680ab12d585b3494229f0c Mon Sep 17 00:00:00 2001 From: stratumadev Date: Tue, 30 Sep 2025 14:43:25 +0200 Subject: [PATCH] prettier fix + updated to node 22 + added new workflow + crunchyroll updates + --version fix --- .github/ISSUE_TEMPLATE/bug.yml | 178 +- .github/ISSUE_TEMPLATE/feature.yml | 50 +- .github/dependabot.yml | 13 +- .github/workflows/auto-documentation.yml | 40 +- .github/workflows/docker.yml | 51 +- .github/workflows/release-matrix.yml | 122 +- .github/workflows/test.yml | 34 +- .prettierignore | 23 + .prettierrc | 36 +- @types/adnPlayerConfig.d.ts | 34 +- @types/adnSearch.d.ts | 66 +- @types/adnStreams.d.ts | 48 +- @types/adnSubtitles.d.ts | 8 +- @types/adnVideos.d.ts | 122 +- @types/animeOnegaiSearch.d.ts | 138 +- @types/animeOnegaiSeasons.d.ts | 56 +- @types/animeOnegaiSeries.d.ts | 178 +- @types/animeOnegaiStream.d.ts | 72 +- @types/crunchyAndroidEpisodes.d.ts | 165 +- @types/crunchyAndroidObject.d.ts | 258 +- @types/crunchyAndroidStreams.d.ts | 70 +- @types/crunchyChapters.d.ts | 38 +- @types/crunchyEpisodeList.d.ts | 148 +- @types/crunchyPlayStreams.d.ts | 56 +- @types/crunchySearch.d.ts | 206 +- @types/crunchyTypes.d.ts | 317 +- @types/downloadedFile.d.ts | 6 +- @types/enums.ts | 10 +- @types/episode.d.ts | 598 +- @types/github.d.ts | 156 +- @types/hidiveDashboard.d.ts | 92 +- @types/hidiveEpisodeList.d.ts | 124 +- @types/hidiveSearch.d.ts | 72 +- @types/hidiveTypes.d.ts | 82 +- @types/iso639.d.ts | 10 +- @types/items.d.ts | 229 +- @types/m3u8-parsed.d.ts | 74 +- @types/messageHandler.d.ts | 265 +- @types/mpd-parser.d.ts | 152 +- @types/newHidiveEpisode.d.ts | 58 +- @types/newHidivePlayback.d.ts | 24 +- @types/newHidiveSearch.d.ts | 74 +- @types/newHidiveSeason.d.ts | 108 +- @types/newHidiveSeries.d.ts | 36 +- @types/objectInfo.d.ts | 266 +- @types/pkg.d.ts | 2 +- @types/playbackData.d.ts | 131 +- @types/randomEvents.d.ts | 16 +- @types/removeNPMAbsolutePaths.d.ts | 2 +- @types/serviceClassInterface.d.ts | 4 +- @types/streamData.d.ts | 24 +- @types/updateFile.d.ts | 6 +- @types/ws.d.ts | 74 +- adn.ts | 1717 ++--- ao.ts | 1568 +++-- config/bin-path.yml | 10 +- config/cli-defaults.yml | 12 +- crunchy.ts | 6130 +++++++++-------- dev.js | 28 +- eslint.config.mjs | 97 +- gui.ts | 2 +- gui/react/.babelrc | 4 +- gui/react/package.json | 2 +- gui/react/public/index.html | 11 +- gui/react/src/@types/FC.d.ts | 8 +- gui/react/src/App.tsx | 4 +- gui/react/src/Layout.tsx | 53 +- gui/react/src/Style.tsx | 26 +- .../src/components/AddToQueue/AddToQueue.tsx | 34 +- .../DownloadSelector/DownloadSelector.tsx | 708 +- .../Listing/EpisodeListing.tsx | 358 +- .../AddToQueue/SearchBox/SearchBox.css | 8 +- .../AddToQueue/SearchBox/SearchBox.tsx | 230 +- gui/react/src/components/AuthButton.tsx | 192 +- gui/react/src/components/LogoutButton.tsx | 44 +- .../DownloadManager/DownloadManager.tsx | 56 +- .../src/components/MainFrame/MainFrame.tsx | 10 +- .../src/components/MainFrame/Queue/Queue.tsx | 929 +-- gui/react/src/components/MenuBar/MenuBar.tsx | 228 +- gui/react/src/components/Require.tsx | 20 +- gui/react/src/components/StartQueue.tsx | 52 +- .../src/components/reusable/ContextMenu.tsx | 114 +- .../reusable/LinearProgressWithLabel.tsx | 24 +- .../src/components/reusable/MultiSelect.tsx | 105 +- gui/react/src/hooks/useStore.tsx | 12 +- gui/react/src/index.tsx | 55 +- gui/react/src/provider/ErrorHandler.tsx | 77 +- gui/react/src/provider/MessageChannel.tsx | 406 +- gui/react/src/provider/QueueProvider.tsx | 44 +- gui/react/src/provider/ServiceProvider.tsx | 73 +- gui/react/src/provider/Store.tsx | 106 +- gui/react/tsconfig.json | 13 +- gui/react/webpack.config.ts | 101 +- gui/server/index.ts | 4 +- gui/server/serviceHandler.ts | 221 +- gui/server/services/adn.ts | 242 +- gui/server/services/animeonegai.ts | 275 +- gui/server/services/base.ts | 230 +- gui/server/services/crunchyroll.ts | 227 +- gui/server/services/hidive.ts | 214 +- gui/server/websocket.ts | 185 +- hidive.ts | 2109 +++--- index.ts | 181 +- modules/build-docs.ts | 101 +- modules/build.ts | 177 +- modules/cdm.ts | 307 +- modules/hls-download-got.ts | 12 +- modules/hls-download.ts | 766 +- modules/log.ts | 104 +- modules/module.api-urls.ts | 172 +- modules/module.app-args.ts | 343 +- modules/module.args.ts | 1979 +++--- modules/module.cfg-loader.ts | 633 +- modules/module.cookieFile.ts | 47 +- modules/module.downloadArchive.ts | 302 +- modules/module.fetch.ts | 287 +- modules/module.ffmpegChapter.ts | 70 +- modules/module.filename.ts | 150 +- modules/module.fontsData.ts | 174 +- modules/module.helper.ts | 134 +- modules/module.langsData.ts | 316 +- modules/module.merger.ts | 751 +- modules/module.parseSelect.ts | 204 +- modules/module.transform-mpd.ts | 423 +- modules/module.updater.ts | 302 +- modules/module.vtt2ass.ts | 940 +-- modules/module.vttconvert.ts | 286 +- modules/playready/bcert.ts | 790 +-- modules/playready/cdm.ts | 402 +- modules/playready/device.ts | 144 +- modules/playready/ecc_key.ts | 140 +- modules/playready/elgamal.ts | 60 +- modules/playready/key.ts | 102 +- modules/playready/pssh.ts | 195 +- modules/playready/wrmheader.ts | 154 +- modules/playready/xml_key.ts | 34 +- modules/playready/xmrlicense.ts | 397 +- modules/widevine/cmac.ts | 170 +- modules/widevine/license.ts | 476 +- modules/widevine/license_protocol_pb3.ts | 3154 ++++----- package.json | 239 +- pnpm-lock.yaml | 429 +- tsc.ts | 50 +- tsconfig.json | 9 +- 144 files changed, 19708 insertions(+), 18998 deletions(-) create mode 100644 .prettierignore diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index b4b1904..48c3dd9 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -1,93 +1,93 @@ name: Bug description: File a bug report -assignees: - - AnimeDL - - AnidlSupport +assignees: + - AnimeDL + - AnidlSupport labels: - - bug -title: "[BUG]: " + - bug +title: '[BUG]: ' body: - - type: markdown - attributes: - value: | - Thank you for reporting the issues you found and have with this program. - This template will guide you through all the information we need. - - type: input - id: version - attributes: - label: Program version - description: "Which version of the program do you use?" - placeholder: "1.0.0" - validations: - required: true - - type: dropdown - id: opsystem - attributes: - label: Operating System - description: "Please tell us what OS you are using." - options: - - Windows - - Linux - - MacOS - validations: - required: true - - type: dropdown - id: gui - attributes: - label: Type - description: "Please tell us if you are using the gui or the cli version." - options: - - CLI - - GUI - validations: - required: true - - type: dropdown - id: service - attributes: - label: Service - description: "Please tell us what service the bug occured in." - options: - - Crunchyroll - - Hidive - - AnimationDigitalNetwork - - AnimeOnegai - - All - - Irrelevant - validations: - required: true - - type: input - id: command - attributes: - label: Command used - description: "Please tell us what command you used." - validations: - required: true - - type: input - id: ShowID - attributes: - label: Show ID - description: "Please provide the ID of an example show." - placeholder: "1234" - validations: - required: true - - type: input - id: Episode - attributes: - label: Episode - description: "Please provide the episode ID you used as an example." - placeholder: "1" - validations: - required: true - - type: textarea - id: output - attributes: - label: Console Output - description: "Please paste the console output from the beginning till termination here. If you are using the gui open the log folder under 'Debug > Open Log Folder' in the Menu. Please copy the content of latest.log here." - render: Shell - validations: - required: true - - type: textarea - id: additionalInfos - attributes: - label: Additional Information - description: "Do you have any additional information you can provide?" + - type: markdown + attributes: + value: | + Thank you for reporting the issues you found and have with this program. + This template will guide you through all the information we need. + - type: input + id: version + attributes: + label: Program version + description: 'Which version of the program do you use?' + placeholder: '1.0.0' + validations: + required: true + - type: dropdown + id: opsystem + attributes: + label: Operating System + description: 'Please tell us what OS you are using.' + options: + - Windows + - Linux + - MacOS + validations: + required: true + - type: dropdown + id: gui + attributes: + label: Type + description: 'Please tell us if you are using the gui or the cli version.' + options: + - CLI + - GUI + validations: + required: true + - type: dropdown + id: service + attributes: + label: Service + description: 'Please tell us what service the bug occured in.' + options: + - Crunchyroll + - Hidive + - AnimationDigitalNetwork + - AnimeOnegai + - All + - Irrelevant + validations: + required: true + - type: input + id: command + attributes: + label: Command used + description: 'Please tell us what command you used.' + validations: + required: true + - type: input + id: ShowID + attributes: + label: Show ID + description: 'Please provide the ID of an example show.' + placeholder: '1234' + validations: + required: true + - type: input + id: Episode + attributes: + label: Episode + description: 'Please provide the episode ID you used as an example.' + placeholder: '1' + validations: + required: true + - type: textarea + id: output + attributes: + label: Console Output + description: "Please paste the console output from the beginning till termination here. If you are using the gui open the log folder under 'Debug > Open Log Folder' in the Menu. Please copy the content of latest.log here." + render: Shell + validations: + required: true + - type: textarea + id: additionalInfos + attributes: + label: Additional Information + description: 'Do you have any additional information you can provide?' diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index 829c37c..39200d9 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -1,29 +1,29 @@ name: Enhancement description: Suggest a enhancement or feature labels: - - enhancement -title: "[Feedback]: " + - enhancement +title: '[Feedback]: ' body: - - type: markdown - attributes: - value: | - Thank you for giving feedback with this program. - This template will guide you through all the information we need. - - type: dropdown - id: programversion - attributes: - label: Type - description: "Is this suggestion for the CLI, GUI, or Both?" - options: - - CLI - - GUI - - Both - validations: - required: true - - type: textarea - id: suggestion - attributes: - label: Suggestion - description: "What is your suggestion?" - validations: - required: true + - type: markdown + attributes: + value: | + Thank you for giving feedback with this program. + This template will guide you through all the information we need. + - type: dropdown + id: programversion + attributes: + label: Type + description: 'Is this suggestion for the CLI, GUI, or Both?' + options: + - CLI + - GUI + - Both + validations: + required: true + - type: textarea + id: suggestion + attributes: + label: Suggestion + description: 'What is your suggestion?' + validations: + required: true diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 47167bb..d65de6c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,11 @@ version: 2 updates: - - package-ecosystem: "npm" - directory: "/" - schedule: - interval: "daily" + - package-ecosystem: 'npm' + directory: '/' + schedule: + interval: 'daily' + open-pull-requests-limit: 0 + allow: + - dependency-type: 'direct' + ignore: + - dependency-type: 'all' diff --git a/.github/workflows/auto-documentation.yml b/.github/workflows/auto-documentation.yml index fd0c6a0..da32f25 100644 --- a/.github/workflows/auto-documentation.yml +++ b/.github/workflows/auto-documentation.yml @@ -1,26 +1,26 @@ name: auto-documentation on: - push: - branches: [ master ] + push: + branches: [master] jobs: - documentation: - runs-on: ubuntu-latest + documentation: + runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - ref: ${{ github.head_ref }} - - uses: pnpm/action-setup@v2 - with: - version: latest - - name: Use Node.js - uses: actions/setup-node@v3 - with: - node-version: 20 - - run: pnpm i - - run: pnpm run docs - - uses: stefanzweifel/git-auto-commit-action@v4 - with: - commit_message: ${{ github.event.head_commit.message }} + Documentation \ No newline at end of file + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.head_ref }} + - uses: pnpm/action-setup@v2 + with: + version: latest + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: 22 + - run: pnpm i + - run: pnpm run docs + - uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: ${{ github.event.head_commit.message }} + Documentation diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 3ade905..e204b4a 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -3,31 +3,30 @@ name: build and push docker image on: - push: - branches: [ master ] - workflow_dispatch: + push: + branches: [master] + workflow_dispatch: jobs: - build-node: - - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v1 - - name: Login to DockerHub - if: ${{ github.ref == 'refs/heads/master' }} - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push Docker images - uses: docker/build-push-action@v2.9.0 - with: - github-token: ${{ github.token }} - push: ${{ github.ref == 'refs/heads/master' }} - tags: | - "multidl/multi-downloader-nx:latest" - - name: Image digest - run: echo ${{ steps.docker_build.outputs.digest }} + build-node: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + - name: Login to DockerHub + if: ${{ github.ref == 'refs/heads/master' }} + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push Docker images + uses: docker/build-push-action@v2.9.0 + with: + github-token: ${{ github.token }} + push: ${{ github.ref == 'refs/heads/master' }} + tags: | + "multidl/multi-downloader-nx:latest" + - name: Image digest + run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/.github/workflows/release-matrix.yml b/.github/workflows/release-matrix.yml index 24acdde..07ab0ee 100644 --- a/.github/workflows/release-matrix.yml +++ b/.github/workflows/release-matrix.yml @@ -1,66 +1,66 @@ name: Release Builds on: - release: - types: [ published ] + release: + types: [published] jobs: - build: - strategy: - matrix: - build_type: [ linux, macos, windows ] - build_arch: [ x64 ] - gui: [ gui, cli ] - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - uses: pnpm/action-setup@v2 - with: - version: latest - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: 20 - check-latest: true - - name: Install Node modules - run: | - pnpm install - - name: Get name and version from package.json - run: | - test -n $(node -p -e "require('./package.json').name") && - test -n $(node -p -e "require('./package.json').version") && - echo PACKAGE_NAME=$(node -p -e "require('./package.json').name") >> $GITHUB_ENV && - echo PACKAGE_VERSION=$(node -p -e "require('./package.json').version") >> $GITHUB_ENV || exit 1 - - name: Make build - run: pnpm run build-${{ matrix.build_type }}-${{ matrix.gui }} - - name: Upload release - uses: actions/upload-release-asset@v1 - with: - upload_url: ${{ github.event.release.upload_url }} - asset_name: multi-downloader-nx-${{ matrix.build_type }}-${{ matrix.gui }}.7z - asset_path: ./lib/_builds/multi-downloader-nx-${{ matrix.build_type }}-${{ matrix.build_arch }}-${{ matrix.gui }}.7z - asset_content_type: application/x-7z-compressed - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - build-docker: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v1 - - name: Login to DockerHub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push Docker images - uses: docker/build-push-action@v2.9.0 - with: - github-token: ${{ github.token }} - push: true - tags: | - "multidl/multi-downloader-nx:${{ github.event.release.tag_name }}" - - name: Image digest - run: echo ${{ steps.docker_build.outputs.digest }} + build: + strategy: + matrix: + build_type: [linux, macos, windows] + build_arch: [x64] + gui: [gui, cli] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - uses: pnpm/action-setup@v2 + with: + version: latest + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: 22 + check-latest: true + - name: Install Node modules + run: | + pnpm install + - name: Get name and version from package.json + run: | + test -n $(node -p -e "require('./package.json').name") && + test -n $(node -p -e "require('./package.json').version") && + echo PACKAGE_NAME=$(node -p -e "require('./package.json').name") >> $GITHUB_ENV && + echo PACKAGE_VERSION=$(node -p -e "require('./package.json').version") >> $GITHUB_ENV || exit 1 + - name: Make build + run: pnpm run build-${{ matrix.build_type }}-${{ matrix.gui }} + - name: Upload release + uses: actions/upload-release-asset@v1 + with: + upload_url: ${{ github.event.release.upload_url }} + asset_name: multi-downloader-nx-${{ matrix.build_type }}-${{ matrix.gui }}.7z + asset_path: ./lib/_builds/multi-downloader-nx-${{ matrix.build_type }}-${{ matrix.build_arch }}-${{ matrix.gui }}.7z + asset_content_type: application/x-7z-compressed + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + build-docker: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push Docker images + uses: docker/build-push-action@v2.9.0 + with: + github-token: ${{ github.token }} + push: true + tags: | + "multidl/multi-downloader-nx:${{ github.event.release.tag_name }}" + - name: Image digest + run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f86bbfc..20433a9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,6 +7,20 @@ on: branches: [ master ] jobs: + tsc: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: pnpm/action-setup@v2 + with: + version: latest + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: 22 + check-latest: true + - run: pnpm i + - run: npx tsc eslint: runs-on: ubuntu-latest steps: @@ -17,10 +31,24 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v3 with: - node-version: 20 + node-version: 22 check-latest: true - run: pnpm i - - run: npx eslint . --quiet + - run: pnpm run eslint + prettier: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: pnpm/action-setup@v2 + with: + version: latest + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: 22 + check-latest: true + - run: pnpm i + - run: pnpm run prettier test: needs: eslint runs-on: ubuntu-latest @@ -32,7 +60,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v3 with: - node-version: 20 + node-version: 22 check-latest: true - run: pnpm i - run: pnpm run test diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..91231e9 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,23 @@ +# Build +build +lib + +# Local +.vscode +.idea +.DS_Store +node_modules +logs +ffmpeg +mkvmerge +fonts +*.json +*.md +*.yaml +!package.json +!tsconfig.json +docker-compose.yml + +# Auto generated +docs + diff --git a/.prettierrc b/.prettierrc index 0fdfbf3..a2d20b1 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,20 +1,20 @@ { - "arrowParens": "always", - "bracketSameLine": false, - "bracketSpacing": true, - "embeddedLanguageFormatting": "auto", - "htmlWhitespaceSensitivity": "strict", - "insertPragma": false, - "jsxSingleQuote": false, - "proseWrap": "never", - "quoteProps": "as-needed", - "requirePragma": false, - "semi": true, - "singleQuote": true, - "tabWidth": 4, - "trailingComma": "none", - "useTabs": true, - "vueIndentScriptAndStyle": false, - "printWidth": 180, - "endOfLine": "auto" + "arrowParens": "always", + "bracketSameLine": false, + "bracketSpacing": true, + "embeddedLanguageFormatting": "auto", + "htmlWhitespaceSensitivity": "strict", + "insertPragma": false, + "jsxSingleQuote": false, + "proseWrap": "never", + "quoteProps": "as-needed", + "requirePragma": false, + "semi": true, + "singleQuote": true, + "tabWidth": 4, + "trailingComma": "none", + "useTabs": true, + "vueIndentScriptAndStyle": false, + "printWidth": 180, + "endOfLine": "auto" } diff --git a/@types/adnPlayerConfig.d.ts b/@types/adnPlayerConfig.d.ts index 62634f5..231b440 100644 --- a/@types/adnPlayerConfig.d.ts +++ b/@types/adnPlayerConfig.d.ts @@ -3,48 +3,48 @@ export interface ADNPlayerConfig { } export interface Player { - image: string; + image: string; options: Options; } export interface Options { - user: User; + user: User; chromecast: Chromecast; - ios: Ios; - video: Video; - dock: any[]; + ios: Ios; + video: Video; + dock: any[]; preference: Preference; } export interface Chromecast { - appId: string; + appId: string; refreshTokenUrl: string; } export interface Ios { videoUrl: string; - appUrl: string; - title: string; + appUrl: string; + title: string; } export interface Preference { - quality: string; + quality: string; autoplay: boolean; language: string; - green: boolean; + green: boolean; } export interface User { - hasAccess: boolean; - profileId: number; - refreshToken: string; + hasAccess: boolean; + profileId: number; + refreshToken: string; refreshTokenUrl: string; } export interface Video { - startDate: null; + startDate: null; currentDate: Date; - available: boolean; - free: boolean; - url: string; + available: boolean; + free: boolean; + url: string; } diff --git a/@types/adnSearch.d.ts b/@types/adnSearch.d.ts index c47b33a..0337c5c 100644 --- a/@types/adnSearch.d.ts +++ b/@types/adnSearch.d.ts @@ -4,40 +4,40 @@ export interface ADNSearch { } export interface ADNSearchShow { - id: number; - title: string; - type: string; - originalTitle: string; - shortTitle: string; - reference: string; - age: string; - languages: string[]; - summary: string; - image: string; - image2x: string; - imageHorizontal: string; - imageHorizontal2x: string; - url: string; - urlPath: string; - episodeCount: number; - genres: string[]; - copyright: string; - rating: number; - ratingsCount: number; - commentsCount: number; - qualities: string[]; - simulcast: boolean; - free: boolean; - available: boolean; - download: boolean; - basedOn: string; - tagline: null; - firstReleaseYear: string; - productionStudio: string; - countryOfOrigin: string; - productionTeam: ProductionTeam[]; + id: number; + title: string; + type: string; + originalTitle: string; + shortTitle: string; + reference: string; + age: string; + languages: string[]; + summary: string; + image: string; + image2x: string; + imageHorizontal: string; + imageHorizontal2x: string; + url: string; + urlPath: string; + episodeCount: number; + genres: string[]; + copyright: string; + rating: number; + ratingsCount: number; + commentsCount: number; + qualities: string[]; + simulcast: boolean; + free: boolean; + available: boolean; + download: boolean; + basedOn: string; + tagline: null; + firstReleaseYear: string; + productionStudio: string; + countryOfOrigin: string; + productionTeam: ProductionTeam[]; nextVideoReleaseDate: null; - indexable: boolean; + indexable: boolean; } export interface ProductionTeam { diff --git a/@types/adnStreams.d.ts b/@types/adnStreams.d.ts index 48dd969..f5fc62b 100644 --- a/@types/adnStreams.d.ts +++ b/@types/adnStreams.d.ts @@ -1,14 +1,14 @@ export interface ADNStreams { - links: Links; - video: Video; + links: Links; + video: Video; metadata: Metadata; } export interface Links { - streaming: Streaming; - subtitles: Subtitles; - history: string; - nextVideoUrl: string; + streaming: Streaming; + subtitles: Subtitles; + history: string; + nextVideoUrl: string; previousVideoUrl: string; } @@ -18,10 +18,10 @@ export interface Streaming { export interface Streams { mobile: string; - sd: string; - hd: string; - fhd: string; - auto: string; + sd: string; + hd: string; + fhd: string; + auto: string; } export interface Subtitles { @@ -29,23 +29,23 @@ export interface Subtitles { } export interface Metadata { - title: string; + title: string; subtitle: string; - summary: null; - rating: number; + summary: null; + rating: number; } export interface Video { - guid: string; - id: number; - currentTime: number; - duration: number; - url: string; - image: string; - tcEpisodeStart?:string; - tcEpisodeEnd?: string; - tcIntroStart?: string; - tcIntroEnd?: string; + guid: string; + id: number; + currentTime: number; + duration: number; + url: string; + image: string; + tcEpisodeStart?: string; + tcEpisodeEnd?: string; + tcIntroStart?: string; + tcIntroEnd?: string; tcEndingStart?: string; - tcEndingEnd?: string; + tcEndingEnd?: string; } diff --git a/@types/adnSubtitles.d.ts b/@types/adnSubtitles.d.ts index dc2cec1..e79eb65 100644 --- a/@types/adnSubtitles.d.ts +++ b/@types/adnSubtitles.d.ts @@ -3,9 +3,9 @@ export interface ADNSubtitles { } export interface Subtitle { - startTime: number; - endTime: number; + startTime: number; + endTime: number; positionAlign: string; - lineAlign: string; - text: string; + lineAlign: string; + text: string; } diff --git a/@types/adnVideos.d.ts b/@types/adnVideos.d.ts index 27a2332..0574357 100644 --- a/@types/adnVideos.d.ts +++ b/@types/adnVideos.d.ts @@ -3,72 +3,72 @@ export interface ADNVideos { } export interface ADNVideo { - id: number; - title: string; - name: string; - number: string; - shortNumber: string; - season: string; - reference: string; - type: string; - order: number; - image: string; - image2x: string; - summary: string; - releaseDate: Date; - duration: number; - url: string; - urlPath: string; - embeddedUrl: string; - languages: string[]; - qualities: string[]; - rating: number; - ratingsCount: number; + id: number; + title: string; + name: string; + number: string; + shortNumber: string; + season: string; + reference: string; + type: string; + order: number; + image: string; + image2x: string; + summary: string; + releaseDate: Date; + duration: number; + url: string; + urlPath: string; + embeddedUrl: string; + languages: string[]; + qualities: string[]; + rating: number; + ratingsCount: number; commentsCount: number; - available: boolean; - download: boolean; - free: boolean; - freeWithAds: boolean; - show: Show; - indexable: boolean; - isSelected?: boolean; + available: boolean; + download: boolean; + free: boolean; + freeWithAds: boolean; + show: Show; + indexable: boolean; + isSelected?: boolean; } export interface Show { - id: number; - title: string; - type: string; - originalTitle: string; - shortTitle: string; - reference: string; - age: string; - languages: string[]; - summary: string; - image: string; - image2x: string; - imageHorizontal: string; - imageHorizontal2x: string; - url: string; - urlPath: string; - episodeCount: number; - genres: string[]; - copyright: string; - rating: number; - ratingsCount: number; - commentsCount: number; - qualities: string[]; - simulcast: boolean; - free: boolean; - available: boolean; - download: boolean; - basedOn: string; - tagline: string; - firstReleaseYear: string; - productionStudio: string; - countryOfOrigin: string; - productionTeam: ProductionTeam[]; + id: number; + title: string; + type: string; + originalTitle: string; + shortTitle: string; + reference: string; + age: string; + languages: string[]; + summary: string; + image: string; + image2x: string; + imageHorizontal: string; + imageHorizontal2x: string; + url: string; + urlPath: string; + episodeCount: number; + genres: string[]; + copyright: string; + rating: number; + ratingsCount: number; + commentsCount: number; + qualities: string[]; + simulcast: boolean; + free: boolean; + available: boolean; + download: boolean; + basedOn: string; + tagline: string; + firstReleaseYear: string; + productionStudio: string; + countryOfOrigin: string; + productionTeam: ProductionTeam[]; nextVideoReleaseDate: Date; - indexable: boolean; + indexable: boolean; } export interface ProductionTeam { diff --git a/@types/animeOnegaiSearch.d.ts b/@types/animeOnegaiSearch.d.ts index e030157..ce23799 100644 --- a/@types/animeOnegaiSearch.d.ts +++ b/@types/animeOnegaiSearch.d.ts @@ -7,82 +7,82 @@ export interface AOSearchResult { /** * Asset ID */ - ID: number; - CreatedAt: Date; - UpdatedAt: Date; - DeletedAt: null; - title: string; - active: boolean; - excerpt: string; - description: string; - bg: string; - poster: string; - entry: string; - code_name: string; + ID: number; + CreatedAt: Date; + UpdatedAt: Date; + DeletedAt: null; + title: string; + active: boolean; + excerpt: string; + description: string; + bg: string; + poster: string; + entry: string; + code_name: string; /** * The Video ID required to get the streams */ - video_entry: string; - trailer: string; - year: number; + video_entry: string; + trailer: string; + year: number; /** * Asset Type, Known Possibilities * * 1 - Video * * 2 - Series */ - asset_type: 1 | 2; - status: number; - permalink: string; - duration: string; - subtitles: boolean; - price: number; - rent_price: number; - rating: number; - color: number | null; - classification: number; + asset_type: 1 | 2; + status: number; + permalink: string; + duration: string; + subtitles: boolean; + price: number; + rent_price: number; + rating: number; + color: number | null; + classification: number; brazil_classification: null | string; - likes: number; - views: number; - button: string; - stream_url: string; - stream_url_backup: string; - copyright: null | string; - skip_intro: null | string; - ending: null | string; - bumper_intro: string; - ads: string; - age_restriction: boolean | null; - epg: null; - allow_languages: string[] | null; - allow_countries: string[] | null; - classification_text: string; - locked: boolean; - resign: boolean; - favorite: boolean; - actors_list: null; - voiceactors_list: null; - artdirectors_list: null; - audios_list: null; - awards_list: null; - companies_list: null; - countries_list: null; - directors_list: null; - edition_list: null; - genres_list: null; - music_list: null; - photograpy_list: null; - producer_list: null; - screenwriter_list: null; - season_list: null; - tags_list: null; - chapter_id: number; - chapter_entry: string; - chapter_poster: string; - progress_time: number; - progress_percent: number; + likes: number; + views: number; + button: string; + stream_url: string; + stream_url_backup: string; + copyright: null | string; + skip_intro: null | string; + ending: null | string; + bumper_intro: string; + ads: string; + age_restriction: boolean | null; + epg: null; + allow_languages: string[] | null; + allow_countries: string[] | null; + classification_text: string; + locked: boolean; + resign: boolean; + favorite: boolean; + actors_list: null; + voiceactors_list: null; + artdirectors_list: null; + audios_list: null; + awards_list: null; + companies_list: null; + countries_list: null; + directors_list: null; + edition_list: null; + genres_list: null; + music_list: null; + photograpy_list: null; + producer_list: null; + screenwriter_list: null; + season_list: null; + tags_list: null; + chapter_id: number; + chapter_entry: string; + chapter_poster: string; + progress_time: number; + progress_percent: number; included_subscription: number; - paid_content: number; - rent_content: number; - objectID: string; - lang: string; -} \ No newline at end of file + paid_content: number; + rent_content: number; + objectID: string; + lang: string; +} diff --git a/@types/animeOnegaiSeasons.d.ts b/@types/animeOnegaiSeasons.d.ts index 90e851d..8a0b176 100644 --- a/@types/animeOnegaiSeasons.d.ts +++ b/@types/animeOnegaiSeasons.d.ts @@ -1,36 +1,36 @@ export interface AnimeOnegaiSeasons { - ID: number; - CreatedAt: Date; - UpdatedAt: Date; - DeletedAt: null; - name: string; - number: number; - asset_id: number; - entry: string; - description: string; - active: boolean; + ID: number; + CreatedAt: Date; + UpdatedAt: Date; + DeletedAt: null; + name: string; + number: number; + asset_id: number; + entry: string; + description: string; + active: boolean; allow_languages: string[]; allow_countries: string[]; - list: Episode[]; + list: Episode[]; } export interface Episode { - ID: number; - CreatedAt: Date; - UpdatedAt: Date; - DeletedAt: null; - name: string; - number: number; - description: string; - thumbnail: string; - entry: string; - video_entry: string; - active: boolean; - season_id: number; - stream_url: string; - skip_intro: null; - ending: null; - open_free: boolean; - asset_id: number; + ID: number; + CreatedAt: Date; + UpdatedAt: Date; + DeletedAt: null; + name: string; + number: number; + description: string; + thumbnail: string; + entry: string; + video_entry: string; + active: boolean; + season_id: number; + stream_url: string; + skip_intro: null; + ending: null; + open_free: boolean; + asset_id: number; age_restriction: boolean; } diff --git a/@types/animeOnegaiSeries.d.ts b/@types/animeOnegaiSeries.d.ts index f8e5b21..301eb48 100644 --- a/@types/animeOnegaiSeries.d.ts +++ b/@types/animeOnegaiSeries.d.ts @@ -1,111 +1,111 @@ export interface AnimeOnegaiSeries { - ID: number; - CreatedAt: Date; - UpdatedAt: Date; - DeletedAt: null; - title: string; - active: boolean; - excerpt: string; - description: string; - bg: string; - poster: string; - entry: string; - code_name: string; + ID: number; + CreatedAt: Date; + UpdatedAt: Date; + DeletedAt: null; + title: string; + active: boolean; + excerpt: string; + description: string; + bg: string; + poster: string; + entry: string; + code_name: string; /** * The Video ID required to get the streams */ - video_entry: string; - trailer: string; - year: number; - asset_type: number; - status: number; - permalink: string; - duration: string; - subtitles: boolean; - price: number; - rent_price: number; - rating: number; - color: number; - classification: number; + video_entry: string; + trailer: string; + year: number; + asset_type: number; + status: number; + permalink: string; + duration: string; + subtitles: boolean; + price: number; + rent_price: number; + rating: number; + color: number; + classification: number; brazil_classification: string; - likes: number; - views: number; - button: string; - stream_url: string; - stream_url_backup: string; - copyright: string; - skip_intro: null; - ending: null; - bumper_intro: string; - ads: string; - age_restriction: boolean; - epg: null; - allow_languages: string[]; - allow_countries: string[]; - classification_text: string; - locked: boolean; - resign: boolean; - favorite: boolean; - actors_list: CtorsList[]; - voiceactors_list: CtorsList[]; - artdirectors_list: any[]; - audios_list: SList[]; - awards_list: any[]; - companies_list: any[]; - countries_list: any[]; - directors_list: CtorsList[]; - edition_list: any[]; - genres_list: SList[]; - music_list: any[]; - photograpy_list: any[]; - producer_list: any[]; - screenwriter_list: any[]; - season_list: any[]; - tags_list: TagsList[]; - chapter_id: number; - chapter_entry: string; - chapter_poster: string; - progress_time: number; - progress_percent: number; + likes: number; + views: number; + button: string; + stream_url: string; + stream_url_backup: string; + copyright: string; + skip_intro: null; + ending: null; + bumper_intro: string; + ads: string; + age_restriction: boolean; + epg: null; + allow_languages: string[]; + allow_countries: string[]; + classification_text: string; + locked: boolean; + resign: boolean; + favorite: boolean; + actors_list: CtorsList[]; + voiceactors_list: CtorsList[]; + artdirectors_list: any[]; + audios_list: SList[]; + awards_list: any[]; + companies_list: any[]; + countries_list: any[]; + directors_list: CtorsList[]; + edition_list: any[]; + genres_list: SList[]; + music_list: any[]; + photograpy_list: any[]; + producer_list: any[]; + screenwriter_list: any[]; + season_list: any[]; + tags_list: TagsList[]; + chapter_id: number; + chapter_entry: string; + chapter_poster: string; + progress_time: number; + progress_percent: number; included_subscription: number; - paid_content: number; - rent_content: number; - objectID: string; - lang: string; + paid_content: number; + rent_content: number; + objectID: string; + lang: string; } export interface CtorsList { - ID: number; - CreatedAt: Date; - UpdatedAt: Date; - DeletedAt: null; - name: string; - Permalink?: string; - country: number | null; - year: number | null; - death: number | null; - image: string; - genre: null; + ID: number; + CreatedAt: Date; + UpdatedAt: Date; + DeletedAt: null; + name: string; + Permalink?: string; + country: number | null; + year: number | null; + death: number | null; + image: string; + genre: null; description: string; - permalink?: string; + permalink?: string; background?: string; } export interface SList { - ID: number; - CreatedAt: Date; - UpdatedAt: Date; - DeletedAt: null; - name: string; + ID: number; + CreatedAt: Date; + UpdatedAt: Date; + DeletedAt: null; + name: string; age_restriction?: number; } export interface TagsList { - ID: number; + ID: number; CreatedAt: Date; UpdatedAt: Date; DeletedAt: null; - name: string; - position: number; - status: boolean; + name: string; + position: number; + status: boolean; } diff --git a/@types/animeOnegaiStream.d.ts b/@types/animeOnegaiStream.d.ts index 9eb3519..8215b76 100644 --- a/@types/animeOnegaiStream.d.ts +++ b/@types/animeOnegaiStream.d.ts @@ -1,41 +1,41 @@ export interface AnimeOnegaiStream { - ID: number; - CreatedAt: Date; - UpdatedAt: Date; - DeletedAt: null; - name: string; - source_url: string; - backup_url: string; - live: boolean; - token_handler: number; - entry: string; - job: string; - drm: boolean; - transcoding_content_id: string; - transcoding_asset_id: string; - status: number; - thumbnail: string; - hls: string; - dash: string; - widevine_proxy: string; - playready_proxy: string; - apple_licence: string; - apple_certificate: string; - dpath: string; - dbin: string; - subtitles: Subtitle[]; - origin: number; - offline_entry: string; - offline_status: boolean; -} - -export interface Subtitle { - ID: number; + ID: number; CreatedAt: Date; UpdatedAt: Date; DeletedAt: null; - name: string; - lang: string; - entry_id: string; - url: string; + name: string; + source_url: string; + backup_url: string; + live: boolean; + token_handler: number; + entry: string; + job: string; + drm: boolean; + transcoding_content_id: string; + transcoding_asset_id: string; + status: number; + thumbnail: string; + hls: string; + dash: string; + widevine_proxy: string; + playready_proxy: string; + apple_licence: string; + apple_certificate: string; + dpath: string; + dbin: string; + subtitles: Subtitle[]; + origin: number; + offline_entry: string; + offline_status: boolean; +} + +export interface Subtitle { + ID: number; + CreatedAt: Date; + UpdatedAt: Date; + DeletedAt: null; + name: string; + lang: string; + entry_id: string; + url: string; } diff --git a/@types/crunchyAndroidEpisodes.d.ts b/@types/crunchyAndroidEpisodes.d.ts index 9ee26b0..b070af3 100644 --- a/@types/crunchyAndroidEpisodes.d.ts +++ b/@types/crunchyAndroidEpisodes.d.ts @@ -1,85 +1,85 @@ import { Images } from './crunchyEpisodeList'; export interface CrunchyAndroidEpisodes { - __class__: string; - __href__: string; + __class__: string; + __href__: string; __resource_key__: string; - __links__: object; - __actions__: object; - total: number; - items: CrunchyAndroidEpisode[]; + __links__: object; + __actions__: object; + total: number; + items: CrunchyAndroidEpisode[]; } export interface CrunchyAndroidEpisode { - __class__: string; - __href__: string; - __resource_key__: string; - __links__: Links; - __actions__: Actions; - playback: string; - id: string; - channel_id: ChannelID; - series_id: string; - series_title: string; - series_slug_title: string; - season_id: string; - season_title: string; - season_slug_title: string; - season_number: number; - episode: string; - episode_number: number; - sequence_number: number; - production_episode_id: string; - title: string; - slug_title: string; - description: string; - next_episode_id: string; - next_episode_title: string; - hd_flag: boolean; - maturity_ratings: MaturityRating[]; - extended_maturity_rating: Actions; - is_mature: boolean; - mature_blocked: boolean; - episode_air_date: Date; - upload_date: Date; - availability_starts: Date; - availability_ends: Date; - eligible_region: string; - available_date: Date; - free_available_date: Date; - premium_date: Date; - premium_available_date: Date; - is_subbed: boolean; - is_dubbed: boolean; - is_clip: boolean; - seo_title: string; - seo_description: string; - season_tags: string[]; - available_offline: boolean; - subtitle_locales: Locale[]; - availability_notes: string; - audio_locale: Locale; - versions: Version[]; + __class__: string; + __href__: string; + __resource_key__: string; + __links__: Links; + __actions__: Actions; + playback: string; + id: string; + channel_id: ChannelID; + series_id: string; + series_title: string; + series_slug_title: string; + season_id: string; + season_title: string; + season_slug_title: string; + season_number: number; + episode: string; + episode_number: number; + sequence_number: number; + production_episode_id: string; + title: string; + slug_title: string; + description: string; + next_episode_id: string; + next_episode_title: string; + hd_flag: boolean; + maturity_ratings: MaturityRating[]; + extended_maturity_rating: Actions; + is_mature: boolean; + mature_blocked: boolean; + episode_air_date: Date; + upload_date: Date; + availability_starts: Date; + availability_ends: Date; + eligible_region: string; + available_date: Date; + free_available_date: Date; + premium_date: Date; + premium_available_date: Date; + is_subbed: boolean; + is_dubbed: boolean; + is_clip: boolean; + seo_title: string; + seo_description: string; + season_tags: string[]; + available_offline: boolean; + subtitle_locales: Locale[]; + availability_notes: string; + audio_locale: Locale; + versions: Version[]; closed_captions_available: boolean; - identifier: string; - media_type: MediaType; - slug: string; - images: Images; - duration_ms: number; - is_premium_only: boolean; - listing_id: string; - hide_season_title?: boolean; - hide_season_number?: boolean; - isSelected?: boolean; - seq_id: string; + identifier: string; + media_type: MediaType; + slug: string; + images: Images; + duration_ms: number; + is_premium_only: boolean; + listing_id: string; + hide_season_title?: boolean; + hide_season_number?: boolean; + isSelected?: boolean; + seq_id: string; } export interface Links { - 'episode/channel': Link; + 'episode/channel': Link; 'episode/next_episode': Link; - 'episode/season': Link; - 'episode/series': Link; - streams: Link; + 'episode/season': Link; + 'episode/series': Link; + streams: Link; } export interface Link { @@ -87,9 +87,9 @@ export interface Link { } export interface Thumbnail { - width: number; + width: number; height: number; - type: string; + type: string; source: string; } @@ -109,28 +109,27 @@ export enum Locale { hiIN = 'hi-IN', zhCN = 'zh-CN', koKR = 'ko-KR', - jaJP = 'ja-JP', + jaJP = 'ja-JP' } export enum MediaType { - Episode = 'episode', + Episode = 'episode' } export enum ChannelID { - Crunchyroll = 'crunchyroll', + Crunchyroll = 'crunchyroll' } export enum MaturityRating { - Tv14 = 'TV-14', + Tv14 = 'TV-14' } export interface Version { - audio_locale: Locale; - guid: string; - original: boolean; - variant: string; - season_guid: string; - media_guid: string; + audio_locale: Locale; + guid: string; + original: boolean; + variant: string; + season_guid: string; + media_guid: string; is_premium_only: boolean; } - diff --git a/@types/crunchyAndroidObject.d.ts b/@types/crunchyAndroidObject.d.ts index 5bccbb2..45c871a 100644 --- a/@types/crunchyAndroidObject.d.ts +++ b/@types/crunchyAndroidObject.d.ts @@ -1,49 +1,49 @@ import { ImageType, Images, Image } from './objectInfo'; export interface CrunchyAndroidObject { - __class__: string; - __href__: string; + __class__: string; + __href__: string; __resource_key__: string; - __links__: object; - __actions__: object; - total: number; - items: AndroidObject[]; + __links__: object; + __actions__: object; + total: number; + items: AndroidObject[]; } export interface AndroidObject { - __class__: string; - __href__: string; - __links__: Links; - __actions__: Actions; - id: string; - external_id: string; - channel_id: string; - title: string; - description: string; - promo_title: string; - promo_description: string; - type: string; - slug: string; - slug_title: string; - images: Images; + __class__: string; + __href__: string; + __links__: Links; + __actions__: Actions; + id: string; + external_id: string; + channel_id: string; + title: string; + description: string; + promo_title: string; + promo_description: string; + type: string; + slug: string; + slug_title: string; + images: Images; movie_listing_metadata?: MovieListingMetadata; - movie_metadata?: MovieMetadata; - playback?: string; - episode_metadata?: EpisodeMetadata; - streams_link?: string; - season_metadata?: SeasonMetadata; - linked_resource_key: string; - isSelected?: boolean; - f_num: string; - s_num: string; + movie_metadata?: MovieMetadata; + playback?: string; + episode_metadata?: EpisodeMetadata; + streams_link?: string; + season_metadata?: SeasonMetadata; + linked_resource_key: string; + isSelected?: boolean; + f_num: string; + s_num: string; } export interface Links { - 'episode/season': LinkData; - 'episode/series': LinkData; - resource: LinkData; + 'episode/season': LinkData; + 'episode/series': LinkData; + resource: LinkData; 'resource/channel': LinkData; - streams: LinkData; + streams: LinkData; } export interface LinkData { @@ -51,119 +51,119 @@ export interface LinkData { } export interface EpisodeMetadata { - audio_locale: Locale; - availability_ends: Date; - availability_notes: string; - availability_starts: Date; - available_date: null; - available_offline: boolean; + audio_locale: Locale; + availability_ends: Date; + availability_notes: string; + availability_starts: Date; + available_date: null; + available_offline: boolean; closed_captions_available: boolean; - duration_ms: number; - eligible_region: string; - episode: string; - episode_air_date: Date; - episode_number: number; - extended_maturity_rating: Record; - free_available_date: Date; - identifier: string; - is_clip: boolean; - is_dubbed: boolean; - is_mature: boolean; - is_premium_only: boolean; - is_subbed: boolean; - mature_blocked: boolean; - maturity_ratings: string[]; - premium_available_date: Date; - premium_date: null; - season_id: string; - season_number: number; - season_slug_title: string; - season_title: string; - sequence_number: number; - series_id: string; - series_slug_title: string; - series_title: string; - subtitle_locales: Locale[]; - tenant_categories?: string[]; - upload_date: Date; - versions: EpisodeMetadataVersion[]; + duration_ms: number; + eligible_region: string; + episode: string; + episode_air_date: Date; + episode_number: number; + extended_maturity_rating: Record; + free_available_date: Date; + identifier: string; + is_clip: boolean; + is_dubbed: boolean; + is_mature: boolean; + is_premium_only: boolean; + is_subbed: boolean; + mature_blocked: boolean; + maturity_ratings: string[]; + premium_available_date: Date; + premium_date: null; + season_id: string; + season_number: number; + season_slug_title: string; + season_title: string; + sequence_number: number; + series_id: string; + series_slug_title: string; + series_title: string; + subtitle_locales: Locale[]; + tenant_categories?: string[]; + upload_date: Date; + versions: EpisodeMetadataVersion[]; } export interface MovieListingMetadata { - availability_notes: string; - available_date: null; - available_offline: boolean; - duration_ms: number; - extended_description: string; + availability_notes: string; + available_date: null; + available_offline: boolean; + duration_ms: number; + extended_description: string; extended_maturity_rating: Record; - first_movie_id: string; - free_available_date: Date; - is_dubbed: boolean; - is_mature: boolean; - is_premium_only: boolean; - is_subbed: boolean; - mature_blocked: boolean; - maturity_ratings: string[]; - movie_release_year: number; - premium_available_date: Date; - premium_date: null; - subtitle_locales: Locale[]; - tenant_categories: string[]; + first_movie_id: string; + free_available_date: Date; + is_dubbed: boolean; + is_mature: boolean; + is_premium_only: boolean; + is_subbed: boolean; + mature_blocked: boolean; + maturity_ratings: string[]; + movie_release_year: number; + premium_available_date: Date; + premium_date: null; + subtitle_locales: Locale[]; + tenant_categories: string[]; } export interface MovieMetadata { - availability_notes: string; - available_offline: boolean; + availability_notes: string; + available_offline: boolean; closed_captions_available: boolean; - duration_ms: number; - extended_maturity_rating: Record; - is_dubbed: boolean; - is_mature: boolean; - is_premium_only: boolean; - is_subbed: boolean; - mature_blocked: boolean; - maturity_ratings: string[]; - movie_listing_id: string; - movie_listing_slug_title: string; - movie_listing_title: string; + duration_ms: number; + extended_maturity_rating: Record; + is_dubbed: boolean; + is_mature: boolean; + is_premium_only: boolean; + is_subbed: boolean; + mature_blocked: boolean; + maturity_ratings: string[]; + movie_listing_id: string; + movie_listing_slug_title: string; + movie_listing_title: string; } export interface SeasonMetadata { - audio_locale: Locale; - audio_locales: Locale[]; + audio_locale: Locale; + audio_locales: Locale[]; extended_maturity_rating: Record; - identifier: string; - is_mature: boolean; - mature_blocked: boolean; - maturity_ratings: string[]; - season_display_number: string; - season_sequence_number: number; - subtitle_locales: Locale[]; - versions: SeasonMetadataVersion[]; + identifier: string; + is_mature: boolean; + mature_blocked: boolean; + maturity_ratings: string[]; + season_display_number: string; + season_sequence_number: number; + subtitle_locales: Locale[]; + versions: SeasonMetadataVersion[]; } export interface SeasonMetadataVersion { audio_locale: Locale; - guid: string; - original: boolean; - variant: string; + guid: string; + original: boolean; + variant: string; } export interface SeriesMetadata { - audio_locales: Locale[]; - availability_notes: string; - episode_count: number; - extended_description: string; + audio_locales: Locale[]; + availability_notes: string; + episode_count: number; + extended_description: string; extended_maturity_rating: Record; - is_dubbed: boolean; - is_mature: boolean; - is_simulcast: boolean; - is_subbed: boolean; - mature_blocked: boolean; - maturity_ratings: string[]; - season_count: number; - series_launch_year: number; - subtitle_locales: Locale[]; - tenant_categories?: string[]; + is_dubbed: boolean; + is_mature: boolean; + is_simulcast: boolean; + is_subbed: boolean; + mature_blocked: boolean; + maturity_ratings: string[]; + season_count: number; + series_launch_year: number; + subtitle_locales: Locale[]; + tenant_categories?: string[]; } export enum Locale { @@ -182,5 +182,5 @@ export enum Locale { hiIN = 'hi-IN', zhCN = 'zh-CN', koKR = 'ko-KR', - jaJP = 'ja-JP', -} \ No newline at end of file + jaJP = 'ja-JP' +} diff --git a/@types/crunchyAndroidStreams.d.ts b/@types/crunchyAndroidStreams.d.ts index f3674fd..81ddb6f 100644 --- a/@types/crunchyAndroidStreams.d.ts +++ b/@types/crunchyAndroidStreams.d.ts @@ -1,37 +1,21 @@ -export interface CrunchyAndroidStreams { - __class__: string; - __href__: string; - __resource_key__: string; - __links__: Links; - __actions__: Record; - media_id: string; - audio_locale: Locale; - subtitles: Subtitles; - closed_captions: Subtitles; - streams: Streams; - bifs: string[]; - versions: Version[]; - captions: Record; -} - export interface Subtitles { - '': Subtitle; - 'en-US'?: Subtitle; - 'es-LA'?: Subtitle; + '': Subtitle; + 'en-US'?: Subtitle; + 'es-LA'?: Subtitle; 'es-419'?: Subtitle; - 'es-ES'?: Subtitle; - 'pt-BR'?: Subtitle; - 'fr-FR'?: Subtitle; - 'de-DE'?: Subtitle; - 'ar-ME'?: Subtitle; - 'ar-SA'?: Subtitle; - 'it-IT'?: Subtitle; - 'ru-RU'?: Subtitle; - 'tr-TR'?: Subtitle; - 'hi-IN'?: Subtitle; - 'zh-CN'?: Subtitle; - 'ko-KR'?: Subtitle; - 'ja-JP'?: Subtitle; + 'es-ES'?: Subtitle; + 'pt-BR'?: Subtitle; + 'fr-FR'?: Subtitle; + 'de-DE'?: Subtitle; + 'ar-ME'?: Subtitle; + 'ar-SA'?: Subtitle; + 'it-IT'?: Subtitle; + 'ru-RU'?: Subtitle; + 'tr-TR'?: Subtitle; + 'hi-IN'?: Subtitle; + 'zh-CN'?: Subtitle; + 'ko-KR'?: Subtitle; + 'ja-JP'?: Subtitle; } export interface Links { @@ -48,8 +32,8 @@ export interface Streams { export interface Download { hardsub_locale: Locale; - hardsub_lang?: string; - url: string; + hardsub_lang?: string; + url: string; } export interface Urls { @@ -58,17 +42,17 @@ export interface Urls { export interface Subtitle { locale: Locale; - url: string; + url: string; format: string; } export interface Version { - audio_locale: Locale; - guid: string; - original: boolean; - variant: string; - season_guid: string; - media_guid: string; + audio_locale: Locale; + guid: string; + original: boolean; + variant: string; + season_guid: string; + media_guid: string; is_premium_only: boolean; } @@ -89,5 +73,5 @@ export enum Locale { hiIN = 'hi-IN', zhCN = 'zh-CN', koKR = 'ko-KR', - jaJP = 'ja-JP', -} \ No newline at end of file + jaJP = 'ja-JP' +} diff --git a/@types/crunchyChapters.d.ts b/@types/crunchyChapters.d.ts index 5aafdb3..c49299c 100644 --- a/@types/crunchyChapters.d.ts +++ b/@types/crunchyChapters.d.ts @@ -1,26 +1,26 @@ export interface CrunchyChapters { - [key: string]: CrunchyChapter; - lastUpdate: Date; - mediaId: string; + [key: string]: CrunchyChapter; + lastUpdate: Date; + mediaId: string; } export interface CrunchyChapter { - approverId: string; - distributionNumber: string; - end: number; - start: number; - title: string; - seriesId: string; - new: boolean; - type: string; + approverId: string; + distributionNumber: string; + end: number; + start: number; + title: string; + seriesId: string; + new: boolean; + type: string; } export interface CrunchyOldChapter { - media_id: string; - startTime: number; - endTime: number; - duration: number; - comparedWith: string; - ordering: string; - last_updated: Date; -} \ No newline at end of file + media_id: string; + startTime: number; + endTime: number; + duration: number; + comparedWith: string; + ordering: string; + last_updated: Date; +} diff --git a/@types/crunchyEpisodeList.d.ts b/@types/crunchyEpisodeList.d.ts index 8a573fd..acf4105 100644 --- a/@types/crunchyEpisodeList.d.ts +++ b/@types/crunchyEpisodeList.d.ts @@ -2,69 +2,69 @@ import { Links } from './crunchyAndroidEpisodes'; export interface CrunchyEpisodeList { total: number; - data: CrunchyEpisode[]; - meta: Meta; + data: CrunchyEpisode[]; + meta: Meta; } export interface CrunchyEpisode { - next_episode_id: string; - series_id: string; - season_number: number; - next_episode_title: string; - availability_notes: string; - duration_ms: number; - series_slug_title: string; - series_title: string; - is_dubbed: boolean; - versions: Version[] | null; - identifier: string; - sequence_number: number; - eligible_region: Record; - availability_starts: Date; - images: Images; - season_id: string; - seo_title: string; - is_premium_only: boolean; - extended_maturity_rating: Record; - title: string; - production_episode_id: string; - premium_available_date: Date; - season_title: string; - seo_description: string; - audio_locale: Locale; - id: string; - media_type: MediaType; - availability_ends: Date; - free_available_date: Date; - playback: string; - channel_id: ChannelID; - episode: string; - is_mature: boolean; - listing_id: string; - episode_air_date: Date; - slug: string; - available_date: Date; - subtitle_locales: Locale[]; - slug_title: string; - available_offline: boolean; - description: string; - is_subbed: boolean; - premium_date: Date; - upload_date: Date; - season_slug_title: string; + next_episode_id: string; + series_id: string; + season_number: number; + next_episode_title: string; + availability_notes: string; + duration_ms: number; + series_slug_title: string; + series_title: string; + is_dubbed: boolean; + versions: Version[] | null; + identifier: string; + sequence_number: number; + eligible_region: Record; + availability_starts: Date; + images: Images; + season_id: string; + seo_title: string; + is_premium_only: boolean; + extended_maturity_rating: Record; + title: string; + production_episode_id: string; + premium_available_date: Date; + season_title: string; + seo_description: string; + audio_locale: Locale; + id: string; + media_type: MediaType; + availability_ends: Date; + free_available_date: Date; + playback: string; + channel_id: ChannelID; + episode: string; + is_mature: boolean; + listing_id: string; + episode_air_date: Date; + slug: string; + available_date: Date; + subtitle_locales: Locale[]; + slug_title: string; + available_offline: boolean; + description: string; + is_subbed: boolean; + premium_date: Date; + upload_date: Date; + season_slug_title: string; closed_captions_available: boolean; - episode_number: number; - season_tags: any[]; - maturity_ratings: MaturityRating[]; - streams_link?: string; - mature_blocked: boolean; - is_clip: boolean; - hd_flag: boolean; - hide_season_title?: boolean; - hide_season_number?: boolean; - isSelected?: boolean; - seq_id: string; - __links__?: Links; + episode_number: number; + season_tags: any[]; + maturity_ratings: MaturityRating[]; + streams_link?: string; + mature_blocked: boolean; + is_clip: boolean; + hd_flag: boolean; + hide_season_title?: boolean; + hide_season_number?: boolean; + isSelected?: boolean; + seq_id: string; + __links__?: Links; } export enum Locale { @@ -83,52 +83,52 @@ export enum Locale { hiIN = 'hi-IN', zhCN = 'zh-CN', koKR = 'ko-KR', - jaJP = 'ja-JP', + jaJP = 'ja-JP' } export enum ChannelID { - Crunchyroll = 'crunchyroll', + Crunchyroll = 'crunchyroll' } export interface Images { poster_tall?: Array; poster_wide?: Array; promo_image?: Array; - thumbnail?: Array; + thumbnail?: Array; } export interface Image { height: number; source: string; - type: ImageType; - width: number; + type: ImageType; + width: number; } export enum ImageType { PosterTall = 'poster_tall', PosterWide = 'poster_wide', PromoImage = 'promo_image', - Thumbnail = 'thumbnail', + Thumbnail = 'thumbnail' } export enum MaturityRating { - Tv14 = 'TV-14', + Tv14 = 'TV-14' } export enum MediaType { - Episode = 'episode', + Episode = 'episode' } export interface Version { - audio_locale: Locale; - guid: string; + audio_locale: Locale; + guid: string; is_premium_only: boolean; - media_guid: string; - original: boolean; - season_guid: string; - variant: string; + media_guid: string; + original: boolean; + season_guid: string; + variant: string; } export interface Meta { versions_considered?: boolean; -} \ No newline at end of file +} diff --git a/@types/crunchyPlayStreams.d.ts b/@types/crunchyPlayStreams.d.ts index 88abd2f..79f0fac 100644 --- a/@types/crunchyPlayStreams.d.ts +++ b/@types/crunchyPlayStreams.d.ts @@ -1,44 +1,44 @@ import { Locale } from './playbackData'; export interface CrunchyPlayStream { - assetId: string; - audioLocale: Locale; - bifs: string; - burnedInLocale: string; - captions: { [key: string]: Caption }; - hardSubs: { [key: string]: HardSub }; - playbackType: string; - session: Session; - subtitles: { [key: string]: Subtitle }; - token: string; - url: string; - versions: any[]; + assetId: string; + audioLocale: Locale; + bifs: string; + burnedInLocale: string; + captions: { [key: string]: Caption }; + hardSubs: { [key: string]: HardSub }; + playbackType: string; + session: Session; + subtitles: { [key: string]: Subtitle }; + token: string; + url: string; + versions: any[]; } export interface Caption { - format: string; - language: string; - url: string; + format: string; + language: string; + url: string; } export interface HardSub { - hlang: string; - url: string; - quality: string; + hlang: string; + url: string; + quality: string; } export interface Session { - renewSeconds: number; - noNetworkRetryIntervalSeconds: number; - noNetworkTimeoutSeconds: number; - maximumPauseSeconds: number; - endOfVideoUnloadSeconds: number; - sessionExpirationSeconds: number; - usesStreamLimits: boolean; + renewSeconds: number; + noNetworkRetryIntervalSeconds: number; + noNetworkTimeoutSeconds: number; + maximumPauseSeconds: number; + endOfVideoUnloadSeconds: number; + sessionExpirationSeconds: number; + usesStreamLimits: boolean; } export interface Subtitle { - format: string; - language: string; - url: string; + format: string; + language: string; + url: string; } diff --git a/@types/crunchySearch.d.ts b/@types/crunchySearch.d.ts index 9a61e7d..c00142b 100644 --- a/@types/crunchySearch.d.ts +++ b/@types/crunchySearch.d.ts @@ -2,79 +2,79 @@ export interface CrunchySearch { total: number; - data: CrunchySearchData[]; - meta: Record; + data: CrunchySearchData[]; + meta: Record; } export interface CrunchySearchData { - type: string; + type: string; count: number; items: CrunchySearchItem[]; } export interface CrunchySearchItem { - title: string; - images: Images; - series_metadata?: SeriesMetadata; - promo_description: string; - external_id: string; - slug: string; - new: boolean; - slug_title: string; - channel_id: ChannelID; - description: string; - linked_resource_key: string; - type: ItemType; - id: string; - promo_title: string; - search_metadata: SearchMetadata; + title: string; + images: Images; + series_metadata?: SeriesMetadata; + promo_description: string; + external_id: string; + slug: string; + new: boolean; + slug_title: string; + channel_id: ChannelID; + description: string; + linked_resource_key: string; + type: ItemType; + id: string; + promo_title: string; + search_metadata: SearchMetadata; movie_listing_metadata?: MovieListingMetadata; - playback?: string; - streams_link?: string; - episode_metadata?: EpisodeMetadata; + playback?: string; + streams_link?: string; + episode_metadata?: EpisodeMetadata; } export enum ChannelID { - Crunchyroll = 'crunchyroll', + Crunchyroll = 'crunchyroll' } export interface EpisodeMetadata { - audio_locale: Locale; - availability_ends: Date; - availability_notes: string; - availability_starts: Date; - available_date: null; - available_offline: boolean; + audio_locale: Locale; + availability_ends: Date; + availability_notes: string; + availability_starts: Date; + available_date: null; + available_offline: boolean; closed_captions_available: boolean; - duration_ms: number; - eligible_region: string[]; - episode: string; - episode_air_date: Date; - episode_number: number; - extended_maturity_rating: Record; - free_available_date: Date; - identifier: string; - is_clip: boolean; - is_dubbed: boolean; - is_mature: boolean; - is_premium_only: boolean; - is_subbed: boolean; - mature_blocked: boolean; - maturity_ratings: MaturityRating[]; - premium_available_date: Date; - premium_date: null; - season_id: string; - season_number: number; - season_slug_title: string; - season_title: string; - sequence_number: number; - series_id: string; - series_slug_title: string; - series_title: string; - subtitle_locales: Locale[]; - upload_date: Date; - versions: Version[] | null; - tenant_categories?: string[]; + duration_ms: number; + eligible_region: string[]; + episode: string; + episode_air_date: Date; + episode_number: number; + extended_maturity_rating: Record; + free_available_date: Date; + identifier: string; + is_clip: boolean; + is_dubbed: boolean; + is_mature: boolean; + is_premium_only: boolean; + is_subbed: boolean; + mature_blocked: boolean; + maturity_ratings: MaturityRating[]; + premium_available_date: Date; + premium_date: null; + season_id: string; + season_number: number; + season_slug_title: string; + season_title: string; + sequence_number: number; + series_id: string; + series_slug_title: string; + series_title: string; + subtitle_locales: Locale[]; + upload_date: Date; + versions: Version[] | null; + tenant_categories?: string[]; } export enum Locale { @@ -93,65 +93,65 @@ export enum Locale { hiIN = 'hi-IN', zhCN = 'zh-CN', koKR = 'ko-KR', - jaJP = 'ja-JP', + jaJP = 'ja-JP' } export enum MaturityRating { Tv14 = 'TV-14', - TvMa = 'TV-MA', + TvMa = 'TV-MA' } export interface Version { - audio_locale: Locale; - guid: string; + audio_locale: Locale; + guid: string; is_premium_only: boolean; - media_guid: string; - original: boolean; - season_guid: string; - variant: string; + media_guid: string; + original: boolean; + season_guid: string; + variant: string; } export interface Images { poster_tall?: Array; poster_wide?: Array; promo_image?: Array; - thumbnail?: Array; + thumbnail?: Array; } export interface Image { height: number; source: string; - type: ImageType; - width: number; + type: ImageType; + width: number; } export enum ImageType { PosterTall = 'poster_tall', PosterWide = 'poster_wide', PromoImage = 'promo_image', - Thumbnail = 'thumbnail', + Thumbnail = 'thumbnail' } export interface MovieListingMetadata { - availability_notes: string; - available_date: null; - available_offline: boolean; - duration_ms: number; - extended_description: string; + availability_notes: string; + available_date: null; + available_offline: boolean; + duration_ms: number; + extended_description: string; extended_maturity_rating: Record; - first_movie_id: string; - free_available_date: Date; - is_dubbed: boolean; - is_mature: boolean; - is_premium_only: boolean; - is_subbed: boolean; - mature_blocked: boolean; - maturity_ratings: string[]; - movie_release_year: number; - premium_available_date: Date; - premium_date: null; - subtitle_locales: any[]; - tenant_categories: string[]; + first_movie_id: string; + free_available_date: Date; + is_dubbed: boolean; + is_mature: boolean; + is_premium_only: boolean; + is_subbed: boolean; + mature_blocked: boolean; + maturity_ratings: string[]; + movie_release_year: number; + premium_available_date: Date; + premium_date: null; + subtitle_locales: any[]; + tenant_categories: string[]; } export interface SearchMetadata { @@ -159,25 +159,25 @@ export interface SearchMetadata { } export interface SeriesMetadata { - audio_locales: Locale[]; - availability_notes: string; - episode_count: number; - extended_description: string; + audio_locales: Locale[]; + availability_notes: string; + episode_count: number; + extended_description: string; extended_maturity_rating: Record; - is_dubbed: boolean; - is_mature: boolean; - is_simulcast: boolean; - is_subbed: boolean; - mature_blocked: boolean; - maturity_ratings: MaturityRating[]; - season_count: number; - series_launch_year: number; - subtitle_locales: Locale[]; - tenant_categories?: string[]; + is_dubbed: boolean; + is_mature: boolean; + is_simulcast: boolean; + is_subbed: boolean; + mature_blocked: boolean; + maturity_ratings: MaturityRating[]; + season_count: number; + series_launch_year: number; + subtitle_locales: Locale[]; + tenant_categories?: string[]; } export enum ItemType { Episode = 'episode', MovieListing = 'movie_listing', - Series = 'series', -} \ No newline at end of file + Series = 'series' +} diff --git a/@types/crunchyTypes.d.ts b/@types/crunchyTypes.d.ts index a84d861..957a3ee 100644 --- a/@types/crunchyTypes.d.ts +++ b/@types/crunchyTypes.d.ts @@ -5,180 +5,191 @@ import { DownloadInfo } from './messageHandler'; import { CrunchyVideoPlayStreams, CrunchyAudioPlayStreams } from './enums'; export type CrunchyDownloadOptions = { - hslang: string, + hslang: string; // kstream: number, - cstream: keyof typeof CrunchyVideoPlayStreams, - vstream: keyof typeof CrunchyVideoPlayStreams, - astream: keyof typeof CrunchyAudioPlayStreams, - tsd?: boolean, - novids?: boolean, - noaudio?: boolean, - x: number, - q: number, - fileName: string, - numbers: number, - partsize: number, - callbackMaker?: (data: DownloadInfo) => HLSCallback, - timeout: number, - waittime: number, - fsRetryTime: number, - dlsubs: string[], - skipsubs: boolean, - nosubs?: boolean, - mp4: boolean, - override: string[], - videoTitle: string, - force: 'Y'|'y'|'N'|'n'|'C'|'c', - ffmpegOptions: string[], - mkvmergeOptions: string[], - defaultSub: LanguageItem, - defaultAudio: LanguageItem, - ccTag: string, - dlVideoOnce: boolean, - skipmux?: boolean, - syncTiming: boolean, - nocleanup: boolean, - chapters: boolean, - fontName: string | undefined, - originalFontSize: boolean, - fontSize: number, - dubLang: string[], -} + cstream: keyof typeof CrunchyVideoPlayStreams; + vstream: keyof typeof CrunchyVideoPlayStreams; + astream: keyof typeof CrunchyAudioPlayStreams; + tsd?: boolean; + novids?: boolean; + noaudio?: boolean; + x: number; + q: number; + fileName: string; + numbers: number; + partsize: number; + callbackMaker?: (data: DownloadInfo) => HLSCallback; + timeout: number; + waittime: number; + fsRetryTime: number; + dlsubs: string[]; + skipsubs: boolean; + nosubs?: boolean; + mp4: boolean; + override: string[]; + videoTitle: string; + force: 'Y' | 'y' | 'N' | 'n' | 'C' | 'c'; + ffmpegOptions: string[]; + mkvmergeOptions: string[]; + defaultSub: LanguageItem; + defaultAudio: LanguageItem; + ccTag: string; + dlVideoOnce: boolean; + skipmux?: boolean; + syncTiming: boolean; + nocleanup: boolean; + chapters: boolean; + fontName: string | undefined; + originalFontSize: boolean; + fontSize: number; + dubLang: string[]; + // Subtitle Fix Options + srtAssFix: boolean; + layoutResFix: boolean; + scaledBorderAndShadowFix: boolean; + scaledBorderAndShadow: 'yes' | 'no'; + originalScriptFix: boolean; +}; export type CrunchyMultiDownload = { - absolute?: boolean, - dubLang: string[], - all?: boolean, - but?: boolean, - e?: string, - s?: string -} + absolute?: boolean; + dubLang: string[]; + all?: boolean; + but?: boolean; + e?: string; + s?: string; +}; export type CrunchyMuxOptions = { - output: string, - skipSubMux?: boolean - keepAllVideos?: bolean - novids?: boolean, - mp4: boolean, - forceMuxer?: 'ffmpeg'|'mkvmerge', - nocleanup?: boolean, - videoTitle: string, - ffmpegOptions: string[], - mkvmergeOptions: string[], - defaultSub: LanguageItem, - defaultAudio: LanguageItem, - ccTag: string, - syncTiming: boolean, -} + output: string; + skipSubMux?: boolean; + keepAllVideos?: bolean; + novids?: boolean; + mp4: boolean; + forceMuxer?: 'ffmpeg' | 'mkvmerge'; + nocleanup?: boolean; + videoTitle: string; + ffmpegOptions: string[]; + mkvmergeOptions: string[]; + defaultSub: LanguageItem; + defaultAudio: LanguageItem; + ccTag: string; + syncTiming: boolean; +}; export type CrunchyEpMeta = { data: { - mediaId: string, - lang?: LanguageItem, - playback?: string, - versions?: EpisodeVersion[] | null, - isSubbed: boolean, - isDubbed: boolean, - }[], - seriesTitle: string, - seasonTitle: string, - episodeNumber: string, - episodeTitle: string, - seasonID: string, - season: number, - showID: string, - e: string, - image: string, -} + mediaId: string; + lang?: LanguageItem; + playback?: string; + versions?: EpisodeVersion[] | null; + isSubbed: boolean; + isDubbed: boolean; + }[]; + seriesTitle: string; + seasonTitle: string; + episodeNumber: string; + episodeTitle: string; + seasonID: string; + season: number; + showID: string; + e: string; + image: string; +}; -export type DownloadedMedia = { - type: 'Video', - lang: LanguageItem, - path: string, - isPrimary?: boolean -} | { - type: 'Audio', - lang: LanguageItem, - path: string, - isPrimary?: boolean -} | { - type: 'Chapters', - lang: LanguageItem, - path: string -} | ({ - type: 'Subtitle', - signs: boolean, - cc: boolean -} & sxItem ) +export type DownloadedMedia = + | { + type: 'Video'; + lang: LanguageItem; + path: string; + isPrimary?: boolean; + } + | { + type: 'Audio'; + lang: LanguageItem; + path: string; + isPrimary?: boolean; + } + | { + type: 'Chapters'; + lang: LanguageItem; + path: string; + } + | ({ + type: 'Subtitle'; + signs: boolean; + cc: boolean; + } & sxItem); export type ParseItem = { __class__?: string; - isSelected?: boolean, - type?: string, - id: string, - title: string, - playback?: string, - season_number?: number|string, - episode_number?: number|string, - season_count?: number|string, - is_premium_only?: boolean, - hide_metadata?: boolean, - seq_id?: string, - f_num?: string, - s_num?: string - external_id?: string, - ep_num?: string - last_public?: string, - subtitle_locales?: string[], - availability_notes?: string, - identifier?: string, - versions?: Version[] | null, - media_type?: string | null, - movie_release_year?: number | null, -} + isSelected?: boolean; + type?: string; + id: string; + title: string; + playback?: string; + season_number?: number | string; + episode_number?: number | string; + season_count?: number | string; + is_premium_only?: boolean; + hide_metadata?: boolean; + seq_id?: string; + f_num?: string; + s_num?: string; + external_id?: string; + ep_num?: string; + last_public?: string; + subtitle_locales?: string[]; + availability_notes?: string; + identifier?: string; + versions?: Version[] | null; + media_type?: string | null; + movie_release_year?: number | null; +}; export interface SeriesSearch { total: number; - data: SeriesSearchItem[]; - meta: Meta; + data: SeriesSearchItem[]; + meta: Meta; } export interface SeriesSearchItem { - description: string; - seo_description: string; - number_of_episodes: number; - is_dubbed: boolean; - identifier: string; - channel_id: string; - slug_title: string; - season_sequence_number: number; - season_tags: string[]; + description: string; + seo_description: string; + number_of_episodes: number; + is_dubbed: boolean; + identifier: string; + channel_id: string; + slug_title: string; + season_sequence_number: number; + season_tags: string[]; extended_maturity_rating: Record; - is_mature: boolean; - audio_locale: string; - season_number: number; - images: Record; - mature_blocked: boolean; - versions: Version[]; - title: string; - is_subbed: boolean; - id: string; - audio_locales: string[]; - subtitle_locales: string[]; - availability_notes: string; - series_id: string; - season_display_number: string; - is_complete: boolean; - keywords: any[]; - maturity_ratings: string[]; - is_simulcast: boolean; - seo_title: string; + is_mature: boolean; + audio_locale: string; + season_number: number; + images: Record; + mature_blocked: boolean; + versions: Version[]; + title: string; + is_subbed: boolean; + id: string; + audio_locales: string[]; + subtitle_locales: string[]; + availability_notes: string; + series_id: string; + season_display_number: string; + is_complete: boolean; + keywords: any[]; + maturity_ratings: string[]; + is_simulcast: boolean; + seo_title: string; } + export interface Version { audio_locale: Locale; - guid: string; - original: boolean; - variant: string; + guid: string; + original: boolean; + variant: string; } export interface EpisodeVersion { @@ -207,7 +218,7 @@ export enum Locale { hiIN = 'hi-IN', zhCN = 'zh-CN', koKR = 'ko-KR', - jaJP = 'ja-JP', + jaJP = 'ja-JP' } export interface Meta { diff --git a/@types/downloadedFile.d.ts b/@types/downloadedFile.d.ts index a4240a5..c3bec2d 100644 --- a/@types/downloadedFile.d.ts +++ b/@types/downloadedFile.d.ts @@ -1,6 +1,6 @@ import { LanguageItem } from '../modules/module.langsData'; export type DownloadedFile = { - path: string, - lang: LanguageItem -} \ No newline at end of file + path: string; + lang: LanguageItem; +}; diff --git a/@types/enums.ts b/@types/enums.ts index 31f337e..ef022a2 100644 --- a/@types/enums.ts +++ b/@types/enums.ts @@ -1,11 +1,11 @@ export enum CrunchyVideoPlayStreams { 'androidtv' = 'tv/android_tv', - 'android' = 'android/phone', - 'androidtab'= 'android/tablet' + 'android' = 'android/phone', + 'androidtab' = 'android/tablet' } export enum CrunchyAudioPlayStreams { 'androidtv' = 'tv/android_tv', - 'android' = 'android/phone', - 'androidtab'= 'android/tablet' -} \ No newline at end of file + 'android' = 'android/phone', + 'androidtab' = 'android/tablet' +} diff --git a/@types/episode.d.ts b/@types/episode.d.ts index 291cf6d..9f7d651 100644 --- a/@types/episode.d.ts +++ b/@types/episode.d.ts @@ -1,391 +1,391 @@ // Generated by https://quicktype.io export interface EpisodeData { - id: number; - title: string; - mediaDict: { [key: string]: string }; - episodeSlug: string; - starRating: number; - parent: EpisodeDataParent; - number: string; - description: string; - filename: string; - seriesBanner: string; - media: Media[]; - externalItemId: string; - contentId: string; - metaItems: MetaItems; - thumb: string; - type: Type; - default: { [key: string]: Default }; - published: boolean; - versions: VersionClass[]; - mediaCategory: string; - order: number; - seriesVersions: any[]; - source: Source; - ids: EpisodeDataIDS; - runtime: string; - siblings: PreviousSeasonEpisode[]; - seriesTitle: string; - seriesSlug: string; - next: Next; - previousSeasonEpisode: PreviousSeasonEpisode; - seasonTitle: string; - quality: Quality; - ratings: Array; - languages: TitleElement[]; - releaseDate: string; - historicalSelections: HistoricalSelections; - userRating: UserRating; + id: number; + title: string; + mediaDict: { [key: string]: string }; + episodeSlug: string; + starRating: number; + parent: EpisodeDataParent; + number: string; + description: string; + filename: string; + seriesBanner: string; + media: Media[]; + externalItemId: string; + contentId: string; + metaItems: MetaItems; + thumb: string; + type: Type; + default: { [key: string]: Default }; + published: boolean; + versions: VersionClass[]; + mediaCategory: string; + order: number; + seriesVersions: any[]; + source: Source; + ids: EpisodeDataIDS; + runtime: string; + siblings: PreviousSeasonEpisode[]; + seriesTitle: string; + seriesSlug: string; + next: Next; + previousSeasonEpisode: PreviousSeasonEpisode; + seasonTitle: string; + quality: Quality; + ratings: Array; + languages: TitleElement[]; + releaseDate: string; + historicalSelections: HistoricalSelections; + userRating: UserRating; } export interface Default { - items: DefaultItem[]; + items: DefaultItem[]; } export interface DefaultItem { - languages: string[]; - territories: string[]; - version: null; - value: Value[]; - devices: any[]; + languages: string[]; + territories: string[]; + version: null; + value: Value[]; + devices: any[]; } export interface Value { - name: MetaType; - value: string; - label: Label; + name: MetaType; + value: string; + label: Label; } export enum Label { - Rating = 'Rating', - RatingSystem = 'Rating System', - ReleaseDate = 'Release Date', - Synopsis = 'Synopsis', - SynopsisType = 'Synopsis Type', + Rating = 'Rating', + RatingSystem = 'Rating System', + ReleaseDate = 'Release Date', + Synopsis = 'Synopsis', + SynopsisType = 'Synopsis Type' } export enum MetaType { - Rating = 'rating', - RatingSystemType = 'RatingSystemType', - ReleaseDate = 'release-date', - Synopsis = 'synopsis', - Synopsistype = 'synopsistype', - VideoRatingType = 'VideoRatingType', + Rating = 'rating', + RatingSystemType = 'RatingSystemType', + ReleaseDate = 'release-date', + Synopsis = 'synopsis', + Synopsistype = 'synopsistype', + VideoRatingType = 'VideoRatingType' } export interface HistoricalSelections { - version: string; - language: string; + version: string; + language: string; } export interface EpisodeDataIDS { - externalShowId: string; - externalSeasonId: string; - externalEpisodeId: string; + externalShowId: string; + externalSeasonId: string; + externalEpisodeId: string; } export enum TitleElement { - Empty = '', - English = 'English', + Empty = '', + English = 'English' } export interface Media { - id: number; - title: string; - experienceType: string; - created: string; - createdBy: string; - itemFieldData: Next; - keyPath: string; - filename: string; - complianceStatus: null; - events: any[]; - clients: string[]; - qcStatus: null; - qcStatusDate: null; - image: string; - thumb: string; - ext: string; - avails: Avail[]; - version: string; - startTimecode: null; - endTimecode: null; - versionId: string; - mediaType: string; - status: string; - languages: LanguageClass[]; - territories: any[]; - devices: any[]; - keyType: string; - purpose: null; - externalItemId: null | string; - proxyId: null; - externalDbId: null; - mediaChildren: MediaChild[]; - isDefault: boolean; - parent: MediaChildParent; - filePath: null | string; - mediaInfo: Next; - type: string; - approved: boolean; - mediaKey: string; - itemFields: any[]; - source: Source; - fieldData: Next; - sourceId: null | string; - timecodeOverride: null; - seriesTitle: string; - episodeTitle: string; - genre: any[]; - txDate: string; - description: string; - synopsis: string; - resolution: null; - restrictedAccess: boolean; - createdById: string; - userIdsWithAccess: any[]; - runtime?: number; - language?: TitleElement; - purchased: boolean; + id: number; + title: string; + experienceType: string; + created: string; + createdBy: string; + itemFieldData: Next; + keyPath: string; + filename: string; + complianceStatus: null; + events: any[]; + clients: string[]; + qcStatus: null; + qcStatusDate: null; + image: string; + thumb: string; + ext: string; + avails: Avail[]; + version: string; + startTimecode: null; + endTimecode: null; + versionId: string; + mediaType: string; + status: string; + languages: LanguageClass[]; + territories: any[]; + devices: any[]; + keyType: string; + purpose: null; + externalItemId: null | string; + proxyId: null; + externalDbId: null; + mediaChildren: MediaChild[]; + isDefault: boolean; + parent: MediaChildParent; + filePath: null | string; + mediaInfo: Next; + type: string; + approved: boolean; + mediaKey: string; + itemFields: any[]; + source: Source; + fieldData: Next; + sourceId: null | string; + timecodeOverride: null; + seriesTitle: string; + episodeTitle: string; + genre: any[]; + txDate: string; + description: string; + synopsis: string; + resolution: null; + restrictedAccess: boolean; + createdById: string; + userIdsWithAccess: any[]; + runtime?: number; + language?: TitleElement; + purchased: boolean; } export interface Avail { - id: number; - description: string; - endDate: string; - startDate: string; - ids: AvailIDS; - originalAirDate: null; - physicalReleaseDate: null; - preorderDate: null; - language: TitleElement; - territory: string; - territoryCode: string; - license: string; - parentAvail: null; - item: number; - version: string; - applyToLevel: null; - availLevel: string; - availDisplayCode: string; - availStatus: string; - bundleOnly: boolean; - contentOwnerOrganization: string; - currency: null; - price: null; - purchase: string; - priceValue: string; - resolutionFormat: null; - runtimeMilliseconds: null; - seasonOrEpisodeNumber: null; - tmsid: null; - deviceList: string; - tvodSku: null; + id: number; + description: string; + endDate: string; + startDate: string; + ids: AvailIDS; + originalAirDate: null; + physicalReleaseDate: null; + preorderDate: null; + language: TitleElement; + territory: string; + territoryCode: string; + license: string; + parentAvail: null; + item: number; + version: string; + applyToLevel: null; + availLevel: string; + availDisplayCode: string; + availStatus: string; + bundleOnly: boolean; + contentOwnerOrganization: string; + currency: null; + price: null; + purchase: string; + priceValue: string; + resolutionFormat: null; + runtimeMilliseconds: null; + seasonOrEpisodeNumber: null; + tmsid: null; + deviceList: string; + tvodSku: null; } export interface AvailIDS { - externalSeasonId: string; - externalAsianId: null; - externalShowId: string; - externalEpisodeId: string; - externalEnglishId: string; - externalAlphaId: string; + externalSeasonId: string; + externalAsianId: null; + externalShowId: string; + externalEpisodeId: string; + externalEnglishId: string; + externalAlphaId: string; } -export type Next = Record +export type Next = Record; export interface LanguageClass { - code: string; - id: number; - title: TitleElement; + code: string; + id: number; + title: TitleElement; } export interface MediaChild { - id: number; - title: string; - experienceType: string; - created: string; - createdBy: string; - itemFieldData: Next; - keyPath: null; - filename: string; - complianceStatus: null; - events: any[]; - clients: string[]; - qcStatus: null; - qcStatusDate: null; - image: string; - ext: string; - avails: any[]; - version: string; - startTimecode: null; - endTimecode: null; - versionId: string; - mediaType: string; - status: string; - languages: LanguageClass[]; - territories: any[]; - devices: any[]; - keyType: string; - purpose: null; - externalItemId: string; - proxyId: null; - externalDbId: null; - mediaChildren: any[]; - isDefault: boolean; - parent: MediaChildParent; - filePath: string; - mediaInfo: MediaInfo; - type: string; - approved: boolean; - mediaKey: null; - itemFields: any[]; - source: Source; - fieldData: Next; - sourceId: null; - timecodeOverride: null; - seriesTitle: string; - episodeTitle: string; - genre: any[]; - txDate: string; - description: string; - synopsis: string; - resolution: null | string; - restrictedAccess: boolean; - createdById: string; - userIdsWithAccess: any[]; - language: TitleElement; + id: number; + title: string; + experienceType: string; + created: string; + createdBy: string; + itemFieldData: Next; + keyPath: null; + filename: string; + complianceStatus: null; + events: any[]; + clients: string[]; + qcStatus: null; + qcStatusDate: null; + image: string; + ext: string; + avails: any[]; + version: string; + startTimecode: null; + endTimecode: null; + versionId: string; + mediaType: string; + status: string; + languages: LanguageClass[]; + territories: any[]; + devices: any[]; + keyType: string; + purpose: null; + externalItemId: string; + proxyId: null; + externalDbId: null; + mediaChildren: any[]; + isDefault: boolean; + parent: MediaChildParent; + filePath: string; + mediaInfo: MediaInfo; + type: string; + approved: boolean; + mediaKey: null; + itemFields: any[]; + source: Source; + fieldData: Next; + sourceId: null; + timecodeOverride: null; + seriesTitle: string; + episodeTitle: string; + genre: any[]; + txDate: string; + description: string; + synopsis: string; + resolution: null | string; + restrictedAccess: boolean; + createdById: string; + userIdsWithAccess: any[]; + language: TitleElement; } export interface MediaInfo { - imageAspectRatio: null | string; - format: string; - scanMode: null | string; - burnedInSubtitleLanguage: string; - screenAspectRatio: null | string; - subtitleFormat: null | string; - subtitleContent: null | string; - frameHeight: number | null; - frameWidth: number | null; - video: Video; + imageAspectRatio: null | string; + format: string; + scanMode: null | string; + burnedInSubtitleLanguage: string; + screenAspectRatio: null | string; + subtitleFormat: null | string; + subtitleContent: null | string; + frameHeight: number | null; + frameWidth: number | null; + video: Video; } export interface Video { - codecId: null | string; - container: null | string; - encodingRate: number | null; - frameRate: null | string; - height: number | null; - width: number | null; - duration: number | null; - bitRate: number | null; + codecId: null | string; + container: null | string; + encodingRate: number | null; + frameRate: null | string; + height: number | null; + width: number | null; + duration: number | null; + bitRate: number | null; } export interface MediaChildParent { - title: string; - type: string; - catalogParent: CatalogParent; - slug: string; - grandparentId: number; - id: number; + title: string; + type: string; + catalogParent: CatalogParent; + slug: string; + grandparentId: number; + id: number; } export interface CatalogParent { - id: number; - title: string; + id: number; + title: string; } export enum Source { - Dbb = 'dbb', + Dbb = 'dbb' } export interface MetaItems { - items: Items; - filters: Filters; + items: Items; + filters: Filters; } export interface Filters { - territory: any[]; - language: any[]; + territory: any[]; + language: any[]; } export interface Items { - 'release-date': AnimationProductionStudio; - rating: AnimationProductionStudio; - synopsis: AnimationProductionStudio; - 'animation-production-studio': AnimationProductionStudio; + 'release-date': AnimationProductionStudio; + rating: AnimationProductionStudio; + synopsis: AnimationProductionStudio; + 'animation-production-studio': AnimationProductionStudio; } export interface AnimationProductionStudio { - items: AnimationProductionStudioItem[]; - label: string; - id: number; - slug: string; + items: AnimationProductionStudioItem[]; + label: string; + id: number; + slug: string; } export interface AnimationProductionStudioItem { - id: number; - metaType: MetaType; - metaTypeId: string; - client: null; - languages: TitleElement; - territories: string; - devices: string; - isDefault: boolean; - value: Value[]; - approved: boolean; - version: null; - source: Source; + id: number; + metaType: MetaType; + metaTypeId: string; + client: null; + languages: TitleElement; + territories: string; + devices: string; + isDefault: boolean; + value: Value[]; + approved: boolean; + version: null; + source: Source; } export interface EpisodeDataParent { - seasonId: number; - seasonNumber: string; - title: string; - titleSlug: string; - titleType: string; - titleId: number; + seasonId: number; + seasonNumber: string; + title: string; + titleSlug: string; + titleType: string; + titleId: number; } export interface PreviousSeasonEpisode { - seasonTitle?: string; - mediaCategory: Type; - thumb: string; - title: string; - image: string; - number: string; - id: number; - version: string[]; - order: number; - slug: string; - season?: number; - languages?: TitleElement[]; + seasonTitle?: string; + mediaCategory: Type; + thumb: string; + title: string; + image: string; + number: string; + id: number; + version: string[]; + order: number; + slug: string; + season?: number; + languages?: TitleElement[]; } export enum Type { - Episode = 'episode', - Ova = 'ova', + Episode = 'episode', + Ova = 'ova' } export interface Quality { - quality: string; - height: number; + quality: string; + height: number; } export interface UserRating { - overall: number; - ja: number; - eng: number; + overall: number; + ja: number; + eng: number; } export interface VersionClass { - compliance_approved: boolean; - title: string; - version_id: string; - is_default: boolean; - runtime: string; - external_id: string; - id: number; + compliance_approved: boolean; + title: string; + version_id: string; + is_default: boolean; + runtime: string; + external_id: string; + id: number; } diff --git a/@types/github.d.ts b/@types/github.d.ts index b597cef..a2c8f33 100644 --- a/@types/github.d.ts +++ b/@types/github.d.ts @@ -1,106 +1,106 @@ export type GithubTag = { - name: string, - zipball_url: string, - tarball_url: string, - commit: { - sha: string, - url: string - }, - node_id: string -} + name: string; + zipball_url: string; + tarball_url: string; + commit: { + sha: string; + url: string; + }; + node_id: string; +}; export interface TagCompare { - url: string; - html_url: string; - permalink_url: string; - diff_url: string; - patch_url: string; - base_commit: BaseCommitClass; - merge_base_commit: BaseCommitClass; - status: string; - ahead_by: number; - behind_by: number; - total_commits: number; - commits: BaseCommitClass[]; - files: File[]; + url: string; + html_url: string; + permalink_url: string; + diff_url: string; + patch_url: string; + base_commit: BaseCommitClass; + merge_base_commit: BaseCommitClass; + status: string; + ahead_by: number; + behind_by: number; + total_commits: number; + commits: BaseCommitClass[]; + files: File[]; } export interface BaseCommitClass { - sha: string; - node_id: string; - commit: BaseCommitCommit; - url: string; - html_url: string; - comments_url: string; - author: BaseCommitAuthor; - committer: BaseCommitAuthor; - parents: Parent[]; + sha: string; + node_id: string; + commit: BaseCommitCommit; + url: string; + html_url: string; + comments_url: string; + author: BaseCommitAuthor; + committer: BaseCommitAuthor; + parents: Parent[]; } export interface BaseCommitAuthor { - login: string; - id: number; - node_id: string; - avatar_url: string; - gravatar_id: string; - url: string; - html_url: string; - followers_url: string; - following_url: string; - gists_url: string; - starred_url: string; - subscriptions_url: string; - organizations_url: string; - repos_url: string; - events_url: string; - received_events_url: string; - type: string; - site_admin: boolean; + login: string; + id: number; + node_id: string; + avatar_url: string; + gravatar_id: string; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: boolean; } export interface BaseCommitCommit { - author: PurpleAuthor; - committer: PurpleAuthor; - message: string; - tree: Tree; - url: string; - comment_count: number; - verification: Verification; + author: PurpleAuthor; + committer: PurpleAuthor; + message: string; + tree: Tree; + url: string; + comment_count: number; + verification: Verification; } export interface PurpleAuthor { - name: string; - email: string; - date: string; + name: string; + email: string; + date: string; } export interface Tree { - sha: string; - url: string; + sha: string; + url: string; } export interface Verification { - verified: boolean; - reason: string; - signature: string; - payload: string; + verified: boolean; + reason: string; + signature: string; + payload: string; } export interface Parent { - sha: string; - url: string; - html_url: string; + sha: string; + url: string; + html_url: string; } export interface File { - sha: string; - filename: string; - status: string; - additions: number; - deletions: number; - changes: number; - blob_url: string; - raw_url: string; - contents_url: string; - patch: string; + sha: string; + filename: string; + status: string; + additions: number; + deletions: number; + changes: number; + blob_url: string; + raw_url: string; + contents_url: string; + patch: string; } diff --git a/@types/hidiveDashboard.d.ts b/@types/hidiveDashboard.d.ts index e6fab33..5c7e37c 100644 --- a/@types/hidiveDashboard.d.ts +++ b/@types/hidiveDashboard.d.ts @@ -1,70 +1,70 @@ export interface HidiveDashboard { - Code: number; - Status: string; - Message: null; - Messages: object; - Data: Data; + Code: number; + Status: string; + Message: null; + Messages: object; + Data: Data; Timestamp: string; IPAddress: string; } export interface Data { TitleRows: TitleRow[]; - LoadTime: number; + LoadTime: number; } export interface TitleRow { - Name: string; - Titles: Title[]; + Name: string; + Titles: Title[]; LoadTime: number; } export interface Title { - Id: number; - Name: string; - ShortSynopsis: string; - MediumSynopsis: string; - LongSynopsis: string; - KeyArtUrl: string; - MasterArtUrl: string; - Rating: null | string; - OverallRating: number; - RatingCount: number; - MALScore: null; - UserRating: number; - RunTime: number | null; - ShowInfoTitle: string; - FirstPremiereDate: Date; - EpisodeCount: number; - SeasonName: string; - RokuHDArtUrl: string; - RokuSDArtUrl: string; - IsRateable: boolean; - InQueue: boolean; - IsFavorite: boolean; + Id: number; + Name: string; + ShortSynopsis: string; + MediumSynopsis: string; + LongSynopsis: string; + KeyArtUrl: string; + MasterArtUrl: string; + Rating: null | string; + OverallRating: number; + RatingCount: number; + MALScore: null; + UserRating: number; + RunTime: number | null; + ShowInfoTitle: string; + FirstPremiereDate: Date; + EpisodeCount: number; + SeasonName: string; + RokuHDArtUrl: string; + RokuSDArtUrl: string; + IsRateable: boolean; + InQueue: boolean; + IsFavorite: boolean; IsContinueWatching: boolean; - ContinueWatching: ContinueWatching; - Episodes: any[]; - LoadTime: number; + ContinueWatching: ContinueWatching; + Episodes: any[]; + LoadTime: number; } export interface ContinueWatching { - Id: string; - ProfileId: number; - EpisodeId: number; - Status: Status | null; - CurrentTime: number; - UserId: number; - TitleId: number; - SeasonId: number; - VideoId: number; + Id: string; + ProfileId: number; + EpisodeId: number; + Status: Status | null; + CurrentTime: number; + UserId: number; + TitleId: number; + SeasonId: number; + VideoId: number; TotalSeconds: number; - CreatedDT: Date; - ModifiedDT: Date | null; + CreatedDT: Date; + ModifiedDT: Date | null; } export enum Status { Paused = 'Paused', Playing = 'Playing', - Watching = 'Watching', -} \ No newline at end of file + Watching = 'Watching' +} diff --git a/@types/hidiveEpisodeList.d.ts b/@types/hidiveEpisodeList.d.ts index b6de9ec..cb93ea9 100644 --- a/@types/hidiveEpisodeList.d.ts +++ b/@types/hidiveEpisodeList.d.ts @@ -1,9 +1,9 @@ export interface HidiveEpisodeList { - Code: number; - Status: string; - Message: null; - Messages: Record; - Data: Data; + Code: number; + Status: string; + Message: null; + Messages: Record; + Data: Data; Timestamp: string; IPAddress: string; } @@ -13,72 +13,72 @@ export interface Data { } export interface HidiveTitle { - Id: number; - Name: string; - ShortSynopsis: string; - MediumSynopsis: string; - LongSynopsis: string; - KeyArtUrl: string; - MasterArtUrl: string; - Rating: string; - OverallRating: number; - RatingCount: number; - MALScore: null; - UserRating: number; - RunTime: number; - ShowInfoTitle: string; - FirstPremiereDate: Date; - EpisodeCount: number; - SeasonName: string; - RokuHDArtUrl: string; - RokuSDArtUrl: string; - IsRateable: boolean; - InQueue: boolean; - IsFavorite: boolean; + Id: number; + Name: string; + ShortSynopsis: string; + MediumSynopsis: string; + LongSynopsis: string; + KeyArtUrl: string; + MasterArtUrl: string; + Rating: string; + OverallRating: number; + RatingCount: number; + MALScore: null; + UserRating: number; + RunTime: number; + ShowInfoTitle: string; + FirstPremiereDate: Date; + EpisodeCount: number; + SeasonName: string; + RokuHDArtUrl: string; + RokuSDArtUrl: string; + IsRateable: boolean; + InQueue: boolean; + IsFavorite: boolean; IsContinueWatching: boolean; - ContinueWatching: ContinueWatching; - Episodes: HidiveEpisode[]; - LoadTime: number; + ContinueWatching: ContinueWatching; + Episodes: HidiveEpisode[]; + LoadTime: number; } export interface ContinueWatching { - Id: string; - ProfileId: number; - EpisodeId: number; - Status: string; - CurrentTime: number; - UserId: number; - TitleId: number; - SeasonId: number; - VideoId: number; + Id: string; + ProfileId: number; + EpisodeId: number; + Status: string; + CurrentTime: number; + UserId: number; + TitleId: number; + SeasonId: number; + VideoId: number; TotalSeconds: number; - CreatedDT: Date; - ModifiedDT: Date; + CreatedDT: Date; + ModifiedDT: Date; } export interface HidiveEpisode { - Id: number; - Number: number; - Name: string; - Summary: string; - HIDIVEPremiereDate: Date; - ScreenShotSmallUrl: string; + Id: number; + Number: number; + Name: string; + Summary: string; + HIDIVEPremiereDate: Date; + ScreenShotSmallUrl: string; ScreenShotCompressedUrl: string; - SeasonNumber: number; - TitleId: number; - SeasonNumberValue: number; - EpisodeNumberValue: number; - VideoKey: string; - DisplayNameLong: string; - PercentProgress: number; - LoadTime: number; + SeasonNumber: number; + TitleId: number; + SeasonNumberValue: number; + EpisodeNumberValue: number; + VideoKey: string; + DisplayNameLong: string; + PercentProgress: number; + LoadTime: number; } export interface HidiveEpisodeExtra extends HidiveEpisode { - titleId: number; - epKey: string; - nameLong: string; - seriesTitle: string; - seriesId?: number; - isSelected: boolean; -} \ No newline at end of file + titleId: number; + epKey: string; + nameLong: string; + seriesTitle: string; + seriesId?: number; + isSelected: boolean; +} diff --git a/@types/hidiveSearch.d.ts b/@types/hidiveSearch.d.ts index 32186d0..a121a5a 100644 --- a/@types/hidiveSearch.d.ts +++ b/@types/hidiveSearch.d.ts @@ -1,47 +1,47 @@ export interface HidiveSearch { - Code: number; - Status: string; - Message: null; - Messages: Record; - Data: HidiveSearchData; + Code: number; + Status: string; + Message: null; + Messages: Record; + Data: HidiveSearchData; Timestamp: string; IPAddress: string; } export interface HidiveSearchData { - Query: string; - Slug: string; - TitleResults: HidiveSearchItem[]; - SearchId: number; - IsSearchPinned: boolean; + Query: string; + Slug: string; + TitleResults: HidiveSearchItem[]; + SearchId: number; + IsSearchPinned: boolean; IsPinnedSearchAvailable: boolean; } export interface HidiveSearchItem { - Id: number; - Name: string; - ShortSynopsis: string; - MediumSynopsis: string; - LongSynopsis: string; - KeyArtUrl: string; - MasterArtUrl: string; - Rating: string; - OverallRating: number; - RatingCount: number; - MALScore: null; - UserRating: number; - RunTime: number | null; - ShowInfoTitle: string; - FirstPremiereDate: Date; - EpisodeCount: number; - SeasonName: string; - RokuHDArtUrl: string; - RokuSDArtUrl: string; - IsRateable: boolean; - InQueue: boolean; - IsFavorite: boolean; + Id: number; + Name: string; + ShortSynopsis: string; + MediumSynopsis: string; + LongSynopsis: string; + KeyArtUrl: string; + MasterArtUrl: string; + Rating: string; + OverallRating: number; + RatingCount: number; + MALScore: null; + UserRating: number; + RunTime: number | null; + ShowInfoTitle: string; + FirstPremiereDate: Date; + EpisodeCount: number; + SeasonName: string; + RokuHDArtUrl: string; + RokuSDArtUrl: string; + IsRateable: boolean; + InQueue: boolean; + IsFavorite: boolean; IsContinueWatching: boolean; - ContinueWatching: null; - Episodes: any[]; - LoadTime: number; -} \ No newline at end of file + ContinueWatching: null; + Episodes: any[]; + LoadTime: number; +} diff --git a/@types/hidiveTypes.d.ts b/@types/hidiveTypes.d.ts index 4eca332..102d952 100644 --- a/@types/hidiveTypes.d.ts +++ b/@types/hidiveTypes.d.ts @@ -1,61 +1,63 @@ export interface HidiveVideoList { - Code: number; - Status: string; - Message: null; - Messages: Record; - Data: HidiveVideo; + Code: number; + Status: string; + Message: null; + Messages: Record; + Data: HidiveVideo; Timestamp: string; IPAddress: string; } export interface HidiveVideo { - ShowAds: boolean; - CaptionCssUrl: string; - FontSize: number; - FontScale: number; - CaptionLanguages: string[]; - CaptionLanguage: string; - CaptionVttUrls: Record; - VideoLanguages: string[]; - VideoLanguage: string; - VideoUrls: Record; - FontColorName: string; + ShowAds: boolean; + CaptionCssUrl: string; + FontSize: number; + FontScale: number; + CaptionLanguages: string[]; + CaptionLanguage: string; + CaptionVttUrls: Record; + VideoLanguages: string[]; + VideoLanguage: string; + VideoUrls: Record; + FontColorName: string; AutoPlayNextEpisode: boolean; - MaxStreams: number; - CurrentTime: number; - FontColorCode: string; - RunTime: number; - AdUrl: null; + MaxStreams: number; + CurrentTime: number; + FontColorCode: string; + RunTime: number; + AdUrl: null; } export interface HidiveStreamList { - hls: string[]; - drm: string[]; + hls: string[]; + drm: string[]; drmEnabled: boolean; } export interface HidiveStreamInfo extends HidiveStreamList { - language?: string; - episodeTitle?: string; - seriesTitle?: string; - season?: number; + language?: string; + episodeTitle?: string; + seriesTitle?: string; + season?: number; episodeNumber?: number; - uncut?: boolean; - image?: string; + uncut?: boolean; + image?: string; } export interface HidiveSubtitleInfo { language: string; - cc: boolean; - url: string; + cc: boolean; + url: string; } -export type DownloadedMedia = { - type: 'Video', - lang: LanguageItem, - path: string, - uncut: boolean -} | ({ - type: 'Subtitle', - cc: boolean -} & sxItem ) \ No newline at end of file +export type DownloadedMedia = + | { + type: 'Video'; + lang: LanguageItem; + path: string; + uncut: boolean; + } + | ({ + type: 'Subtitle'; + cc: boolean; + } & sxItem); diff --git a/@types/iso639.d.ts b/@types/iso639.d.ts index 39ef820..65931c5 100644 --- a/@types/iso639.d.ts +++ b/@types/iso639.d.ts @@ -1,9 +1,9 @@ declare module 'iso-639' { export type iso639Type = { [key: string]: { - '639-1'?: string, - '639-2'?: string - } - } + '639-1'?: string; + '639-2'?: string; + }; + }; export const iso_639_2: iso639Type; -} \ No newline at end of file +} diff --git a/@types/items.d.ts b/@types/items.d.ts index 548d17f..cba6d85 100644 --- a/@types/items.d.ts +++ b/@types/items.d.ts @@ -1,169 +1,168 @@ export interface Item { // Added later - id: string, - id_split: (number|string)[] + id: string; + id_split: (number | string)[]; // Added from the start - mostRecentSvodJpnUs: MostRecentSvodJpnUs; - synopsis: string; - mediaCategory: ContentType; - mostRecentSvodUsEndTimestamp: number; - quality: QualityClass; - genres: Genre[]; - titleImages: TitleImages; - engAllTerritoryAvail: EngAllTerritoryAvail; - thumb: string; + mostRecentSvodJpnUs: MostRecentSvodJpnUs; + synopsis: string; + mediaCategory: ContentType; + mostRecentSvodUsEndTimestamp: number; + quality: QualityClass; + genres: Genre[]; + titleImages: TitleImages; + engAllTerritoryAvail: EngAllTerritoryAvail; + thumb: string; mostRecentSvodJpnAllTerrStartTimestamp: number; - title: string; - starRating: number; - primaryAvail: PrimaryAvail; - access: Access[]; - version: Version[]; - mostRecentSvodJpnAllTerrEndTimestamp: number; - itemId: number; - versionAudio: VersionAudio; - contentType: ContentType; - mostRecentSvodUsStartTimestamp: number; - poster: string; - mostRecentSvodEngAllTerrEndTimestamp: number; - mostRecentSvodJpnUsStartTimestamp: number; - mostRecentSvodJpnUsEndTimestamp: number; - mostRecentSvodStartTimestamp: number; - mostRecentSvod: MostRecent; - altAvail: AltAvail; - ids: IDs; - mostRecentSvodUs: MostRecent; - item: Item; + title: string; + starRating: number; + primaryAvail: PrimaryAvail; + access: Access[]; + version: Version[]; + mostRecentSvodJpnAllTerrEndTimestamp: number; + itemId: number; + versionAudio: VersionAudio; + contentType: ContentType; + mostRecentSvodUsStartTimestamp: number; + poster: string; + mostRecentSvodEngAllTerrEndTimestamp: number; + mostRecentSvodJpnUsStartTimestamp: number; + mostRecentSvodJpnUsEndTimestamp: number; + mostRecentSvodStartTimestamp: number; + mostRecentSvod: MostRecent; + altAvail: AltAvail; + ids: IDs; + mostRecentSvodUs: MostRecent; + item: Item; mostRecentSvodEngAllTerrStartTimestamp: number; - audio: string[]; - mostRecentAvod: MostRecent; + audio: string[]; + mostRecentAvod: MostRecent; } export enum ContentType { Episode = 'episode', - Ova = 'ova', + Ova = 'ova' } export interface IDs { - externalShowId: ID; - externalSeasonId: ExternalSeasonID; + externalShowId: ID; + externalSeasonId: ExternalSeasonID; externalEpisodeId: string; - externalAsianId?: string + externalAsianId?: string; } export interface Item { - seasonTitle: string; - seasonId: number; - episodeOrder: number; - episodeSlug: string; - created: Date; - titleSlug: string; - episodeNum: string; - episodeId: number; - titleId: number; - seasonNum: string; - ratings: Array; - showImage: string; - titleName: string; - runtime: string; - episodeName: string; - seasonOrder: number; + seasonTitle: string; + seasonId: number; + episodeOrder: number; + episodeSlug: string; + created: Date; + titleSlug: string; + episodeNum: string; + episodeId: number; + titleId: number; + seasonNum: string; + ratings: Array; + showImage: string; + titleName: string; + runtime: string; + episodeName: string; + seasonOrder: number; titleExternalId: string; } - export interface MostRecent { - image?: string; + image?: string; siblingStartTimestamp?: string; - devices?: Device[]; - availId?: number; - distributor?: Distributor; - quality?: MostRecentAvodQuality; - endTimestamp?: string; - mediaCategory?: ContentType; - isPromo?: boolean; - siblingType?: Purchase; - version?: Version; - territory?: Territory; - startDate?: Date; - endDate?: Date; - versionId?: number; - tier?: Device | null; - purchase?: Purchase; - startTimestamp?: string; - language?: Audio; - itemTitle?: string; - ids?: MostRecentAvodIDS; - experience?: number; - siblingEndTimestamp?: string; - item?: Item; - subscriptionRequired?: boolean; - purchased?: boolean; + devices?: Device[]; + availId?: number; + distributor?: Distributor; + quality?: MostRecentAvodQuality; + endTimestamp?: string; + mediaCategory?: ContentType; + isPromo?: boolean; + siblingType?: Purchase; + version?: Version; + territory?: Territory; + startDate?: Date; + endDate?: Date; + versionId?: number; + tier?: Device | null; + purchase?: Purchase; + startTimestamp?: string; + language?: Audio; + itemTitle?: string; + ids?: MostRecentAvodIDS; + experience?: number; + siblingEndTimestamp?: string; + item?: Item; + subscriptionRequired?: boolean; + purchased?: boolean; } export interface MostRecentAvodIDS { - externalSeasonId: ExternalSeasonID; - externalAsianId: null; - externalShowId: ID; + externalSeasonId: ExternalSeasonID; + externalAsianId: null; + externalShowId: ID; externalEpisodeId: string; externalEnglishId: string; - externalAlphaId: string; + externalAlphaId: string; } export enum Purchase { AVOD = 'A-VOD', Dfov = 'DFOV', Est = 'EST', - Svod = 'SVOD', + Svod = 'SVOD' } export enum Version { Simulcast = 'Simulcast', - Uncut = 'Uncut', + Uncut = 'Uncut' } -export type MostRecentSvodJpnUs = Record +export type MostRecentSvodJpnUs = Record; export interface QualityClass { quality: QualityQuality; - height: number; + height: number; } export enum QualityQuality { HD = 'HD', - SD = 'SD', + SD = 'SD' } export interface TitleImages { - showThumbnail: string; - showBackgroundSite: string; - showDetailHeaderDesktop: string; - continueWatchingDesktop: string; - showDetailHeroSite: string; - appleHorizontalBannerShow: string; - backgroundImageXbox_360: string; - applePosterCover: string; - showDetailBoxArtTablet: string; + showThumbnail: string; + showBackgroundSite: string; + showDetailHeaderDesktop: string; + continueWatchingDesktop: string; + showDetailHeroSite: string; + appleHorizontalBannerShow: string; + backgroundImageXbox_360: string; + applePosterCover: string; + showDetailBoxArtTablet: string; featuredShowBackgroundTablet: string; backgroundImageAppletvfiretv: string; - newShowDetailHero: string; - showDetailHeroDesktop: string; - showKeyart: string; - continueWatchingMobile: string; - featuredSpotlightShowPhone: string; - appleHorizontalBannerMovie: string; - featuredSpotlightShowTablet: string; - showDetailBoxArtPhone: string; - featuredShowBackgroundPhone: string; - appleSquareCover: string; - backgroundVideo: string; - showMasterKeyArt: string; - newShowDetailHeroPhone: string; - showDetailBoxArtXbox_360: string; - showDetailHeaderMobile: string; - showLogo: string; + newShowDetailHero: string; + showDetailHeroDesktop: string; + showKeyart: string; + continueWatchingMobile: string; + featuredSpotlightShowPhone: string; + appleHorizontalBannerMovie: string; + featuredSpotlightShowTablet: string; + showDetailBoxArtPhone: string; + featuredShowBackgroundPhone: string; + appleSquareCover: string; + backgroundVideo: string; + showMasterKeyArt: string; + newShowDetailHeroPhone: string; + showDetailBoxArtXbox_360: string; + showDetailHeaderMobile: string; + showLogo: string; } export interface VersionAudio { - Uncut?: Audio[]; + Uncut?: Audio[]; Simulcast: Audio[]; -} \ No newline at end of file +} diff --git a/@types/m3u8-parsed.d.ts b/@types/m3u8-parsed.d.ts index e42d532..2c7c616 100644 --- a/@types/m3u8-parsed.d.ts +++ b/@types/m3u8-parsed.d.ts @@ -1,49 +1,49 @@ declare module 'm3u8-parsed' { export type M3U8 = { - allowCache: boolean, - discontinuityStarts: [], + allowCache: boolean; + discontinuityStarts: []; segments: { - duration: number, + duration: number; byterange?: { - length: number, - offset: number - }, - uri: string, + length: number; + offset: number; + }; + uri: string; key: { - method: string, - uri: string, - }, - timeline: number - }[], - version: number, + method: string; + uri: string; + }; + timeline: number; + }[]; + version: number; mediaGroups: { [type: string]: { [index: string]: { [language: string]: { - default: boolean, - autoselect: boolean, - language: string, - uri: string - } - } - } - }, + default: boolean; + autoselect: boolean; + language: string; + uri: string; + }; + }; + }; + }; playlists: { - uri: string, - timeline: number, + uri: string; + timeline: number; attributes: { - 'CLOSED-CAPTIONS': string, - 'AUDIO': string, - 'FRAME-RATE': number, - 'RESOLUTION': { - width: number, - height: number - }, - 'CODECS': string, - 'AVERAGE-BANDWIDTH': string, - 'BANDWIDTH': number - } - }[], - } + 'CLOSED-CAPTIONS': string; + AUDIO: string; + 'FRAME-RATE': number; + RESOLUTION: { + width: number; + height: number; + }; + CODECS: string; + 'AVERAGE-BANDWIDTH': string; + BANDWIDTH: number; + }; + }[]; + }; export default function (data: string): M3U8; -} \ No newline at end of file +} diff --git a/@types/messageHandler.d.ts b/@types/messageHandler.d.ts index 6f16ea9..e8a36de 100644 --- a/@types/messageHandler.d.ts +++ b/@types/messageHandler.d.ts @@ -4,111 +4,145 @@ import type { AvailableMuxer } from '../modules/module.args'; import { LanguageItem } from '../modules/module.langsData'; export interface MessageHandler { - name: string + name: string; auth: (data: AuthData) => Promise; version: () => Promise; checkToken: () => Promise; - search: (data: SearchData) => Promise, - availableDubCodes: () => Promise, - availableSubCodes: () => Promise, - handleDefault: (name: string) => Promise, - resolveItems: (data: ResolveItemsData) => Promise, - listEpisodes: (id: string) => Promise, - downloadItem: (data: QueueItem) => void, - isDownloading: () => Promise, - openFolder: (path: FolderTypes) => void, - openFile: (data: [FolderTypes, string]) => void, + search: (data: SearchData) => Promise; + availableDubCodes: () => Promise; + availableSubCodes: () => Promise; + handleDefault: (name: string) => Promise; + resolveItems: (data: ResolveItemsData) => Promise; + listEpisodes: (id: string) => Promise; + downloadItem: (data: QueueItem) => void; + isDownloading: () => Promise; + openFolder: (path: FolderTypes) => void; + openFile: (data: [FolderTypes, string]) => void; openURL: (data: string) => void; - getQueue: () => Promise, - removeFromQueue: (index: number) => void, - clearQueue: () => void, - setDownloadQueue: (data: boolean) => void, - getDownloadQueue: () => Promise + getQueue: () => Promise; + removeFromQueue: (index: number) => void; + clearQueue: () => void; + setDownloadQueue: (data: boolean) => void; + getDownloadQueue: () => Promise; } export type FolderTypes = 'content' | 'config'; export type QueueItem = { - title: string, - episode: string, - fileName: string, - dlsubs: string[], + title: string; + episode: string; + fileName: string; + dlsubs: string[]; parent: { - title: string, - season: string - }, - q: number, - dlVideoOnce: boolean, - dubLang: string[], - image: string, -} & ResolveItemsData + title: string; + season: string; + }; + q: number; + dlVideoOnce: boolean; + dubLang: string[]; + image: string; +} & ResolveItemsData; export type ResolveItemsData = { - id: string, - dubLang: string[], - all: boolean, - but: boolean, - novids: boolean, - noaudio: boolean - dlVideoOnce: boolean, - e: string, - fileName: string, - q: number, - dlsubs: string[] -} + id: string; + dubLang: string[]; + all: boolean; + but: boolean; + novids: boolean; + noaudio: boolean; + dlVideoOnce: boolean; + e: string; + fileName: string; + q: number; + dlsubs: string[]; +}; export type SearchResponseItem = { - image: string, - name: string, - desc?: string, - id: string, - lang?: string[], - rating: number + image: string; + name: string; + desc?: string; + id: string; + lang?: string[]; + rating: number; }; export type Episode = { - e: string, - lang: string[], - name: string, - season: string, - seasonTitle: string, - episode: string, - id: string, - img: string, - description: string, - time: string -} - -export type SearchResponse = ResponseBase -export type EpisodeListResponse = ResponseBase - -export type FuniEpisodeData = { - title: string, - episode: string, - epsiodeNumber: string, - episodeID: string, - seasonTitle: string, - seasonNumber: string, - ids: { - episode: string, - show: string, - season: string - }, - image: string + e: string; + lang: string[]; + name: string; + season: string; + seasonTitle: string; + episode: string; + id: string; + img: string; + description: string; + time: string; }; -export type AuthData = { username: string, password: string }; -export type SearchData = { search: string, page?: number, 'search-type'?: string, 'search-locale'?: string }; -export type FuniGetShowData = { id: number, e?: string, but: boolean, all: boolean }; -export type FuniGetEpisodeData = { subs: FuniSubsData, fnSlug: FuniEpisodeData, simul?: boolean; dubLang: string[], s: string } -export type FuniStreamData = { force?: 'Y'|'y'|'N'|'n'|'C'|'c', callbackMaker?: (data: DownloadInfo) => HLSCallback, q: number, x: number, fileName: string, numbers: number, novids?: boolean, - timeout: number, partsize: number, fsRetryTime: number, noaudio?: boolean, mp4: boolean, ass: boolean, fontSize: number, fontName?: string, skipmux?: boolean, - forceMuxer: AvailableMuxer | undefined, simul: boolean, skipSubMux: boolean, nocleanup: boolean, override: string[], videoTitle: string, - ffmpegOptions: string[], mkvmergeOptions: string[], defaultAudio: LanguageItem, defaultSub: LanguageItem, ccTag: string } -export type FuniSubsData = { nosubs?: boolean, sub: boolean, dlsubs: string[], ccTag: string } +export type SearchResponse = ResponseBase; +export type EpisodeListResponse = ResponseBase; + +export type FuniEpisodeData = { + title: string; + episode: string; + epsiodeNumber: string; + episodeID: string; + seasonTitle: string; + seasonNumber: string; + ids: { + episode: string; + show: string; + season: string; + }; + image: string; +}; + +export type AuthData = { username: string; password: string }; +export type SearchData = { search: string; page?: number; 'search-type'?: string; 'search-locale'?: string }; +export type FuniGetShowData = { id: number; e?: string; but: boolean; all: boolean }; +export type FuniGetEpisodeData = { subs: FuniSubsData; fnSlug: FuniEpisodeData; simul?: boolean; dubLang: string[]; s: string }; +export type FuniStreamData = { + force?: 'Y' | 'y' | 'N' | 'n' | 'C' | 'c'; + callbackMaker?: (data: DownloadInfo) => HLSCallback; + q: number; + x: number; + fileName: string; + numbers: number; + novids?: boolean; + timeout: number; + partsize: number; + fsRetryTime: number; + noaudio?: boolean; + mp4: boolean; + ass: boolean; + fontSize: number; + fontName?: string; + skipmux?: boolean; + forceMuxer: AvailableMuxer | undefined; + simul: boolean; + skipSubMux: boolean; + nocleanup: boolean; + override: string[]; + videoTitle: string; + ffmpegOptions: string[]; + mkvmergeOptions: string[]; + defaultAudio: LanguageItem; + defaultSub: LanguageItem; + ccTag: string; +}; +export type FuniSubsData = { nosubs?: boolean; sub: boolean; dlsubs: string[]; ccTag: string }; export type DownloadData = { - hslang?: string; id: string, e: string, dubLang: string[], dlsubs: string[], fileName: string, q: number, novids: boolean, noaudio: boolean, dlVideoOnce: boolean -} + hslang?: string; + id: string; + e: string; + dubLang: string[]; + dlsubs: string[]; + fileName: string; + q: number; + novids: boolean; + noaudio: boolean; + dlVideoOnce: boolean; +}; export type AuthResponse = ResponseBase; export type FuniSearchReponse = ResponseBase; @@ -116,46 +150,47 @@ export type FuniShowResponse = ResponseBase; export type FuniGetEpisodeResponse = ResponseBase; export type CheckTokenResponse = ResponseBase; - -export type ResponseBase = ({ - isOk: true, - value: T -} | { - isOk: false, - reason: Error -}); +export type ResponseBase = + | { + isOk: true; + value: T; + } + | { + isOk: false; + reason: Error; + }; export type ProgressData = { - total: number, - cur: number, - percent: number|string, - time: number, - downloadSpeed: number, - bytes: number + total: number; + cur: number; + percent: number | string; + time: number; + downloadSpeed: number; + bytes: number; }; export type PossibleMessages = keyof ServiceHandler; -export type DownloadInfo = { - image: string, +export type DownloadInfo = { + image: string; parent: { - title: string - }, - title: string, - language: LanguageItem, - fileName: string -} + title: string; + }; + title: string; + language: LanguageItem; + fileName: string; +}; export type ExtendedProgress = { - progress: ProgressData, - downloadInfo: DownloadInfo -} + progress: ProgressData; + downloadInfo: DownloadInfo; +}; export type GuiState = { - setup: boolean, - services: Record -} + setup: boolean; + services: Record; +}; export type GuiStateService = { - queue: QueueItem[] -} \ No newline at end of file + queue: QueueItem[]; +}; diff --git a/@types/mpd-parser.d.ts b/@types/mpd-parser.d.ts index 54688a1..ec607fd 100644 --- a/@types/mpd-parser.d.ts +++ b/@types/mpd-parser.d.ts @@ -1,101 +1,101 @@ declare module 'mpd-parser' { export type Segment = { - uri: string, - timeline: number, - duration: number, - resolvedUri: string, + uri: string; + timeline: number; + duration: number; + resolvedUri: string; map: { - uri: string, - resolvedUri: string, + uri: string; + resolvedUri: string; byterange?: { - length: number, - offset: number - } - }, + length: number; + offset: number; + }; + }; byterange?: { - length: number, - offset: number - }, - number: number, - presentationTime: number - } + length: number; + offset: number; + }; + number: number; + presentationTime: number; + }; export type Sidx = { - uri: string, - resolvedUri: string, + uri: string; + resolvedUri: string; byterange: { - length: number, - offset: number - }, + length: number; + offset: number; + }; map: { - uri: string, - resolvedUri: string, + uri: string; + resolvedUri: string; byterange: { - length: number, - offset: number - } - }, - duration: number, - timeline: number, - presentationTime: number, - number: number - } + length: number; + offset: number; + }; + }; + duration: number; + timeline: number; + presentationTime: number; + number: number; + }; export type Playlist = { attributes: { - NAME: string, - BANDWIDTH: number, - CODECS: string, - 'PROGRAM-ID': number, + NAME: string; + BANDWIDTH: number; + CODECS: string; + 'PROGRAM-ID': number; // Following for video only - 'FRAME-RATE'?: number, - AUDIO?: string, // audio stream name - SUBTITLES?: string, + 'FRAME-RATE'?: number; + AUDIO?: string; // audio stream name + SUBTITLES?: string; RESOLUTION?: { - width: number, - height: number - } - }, - uri: string, - endList: boolean, - timeline: number, - resolvedUri: string, - targetDuration: number, - discontinuitySequence: number, - discontinuityStarts: [], + width: number; + height: number; + }; + }; + uri: string; + endList: boolean; + timeline: number; + resolvedUri: string; + targetDuration: number; + discontinuitySequence: number; + discontinuityStarts: []; timelineStarts: { - start: number, - timeline: number - }[], - mediaSequence: number, + start: number; + timeline: number; + }[]; + mediaSequence: number; contentProtection?: { [type: string]: { - pssh?: Uint8Array - } - } - segments: Segment[] - sidx?: Sidx - } + pssh?: Uint8Array; + }; + }; + segments: Segment[]; + sidx?: Sidx; + }; export type Manifest = { - allowCache: boolean, - discontinuityStarts: [], - segments: [], - endList: true, - duration: number, - playlists: Playlist[], + allowCache: boolean; + discontinuityStarts: []; + segments: []; + endList: true; + duration: number; + playlists: Playlist[]; mediaGroups: { AUDIO: { audio: { [name: string]: { - language: string, - autoselect: boolean, - default: boolean, - playlists: Playlist[] - } - } - } - } - } - export function parse(manifest: string): Manifest + language: string; + autoselect: boolean; + default: boolean; + playlists: Playlist[]; + }; + }; + }; + }; + }; + export function parse(manifest: string): Manifest; } diff --git a/@types/newHidiveEpisode.d.ts b/@types/newHidiveEpisode.d.ts index 9ccf705..eca2085 100644 --- a/@types/newHidiveEpisode.d.ts +++ b/@types/newHidiveEpisode.d.ts @@ -1,43 +1,43 @@ export interface NewHidiveEpisode { - description: string; - duration: number; - title: string; - categories: string[]; - contentDownload: ContentDownload; - favourite: boolean; - subEvents: any[]; - thumbnailUrl: string; - longDescription: string; - posterUrl: string; + description: string; + duration: number; + title: string; + categories: string[]; + contentDownload: ContentDownload; + favourite: boolean; + subEvents: any[]; + thumbnailUrl: string; + longDescription: string; + posterUrl: string; offlinePlaybackLanguages: string[]; - externalAssetId: string; - maxHeight: number; - rating: Rating; - episodeInformation: EpisodeInformation; - id: number; - accessLevel: string; - playerUrlCallback: string; - thumbnailsPreview: string; - displayableTags: any[]; - plugins: any[]; - watchStatus: string; - computedReleases: any[]; - licences: any[]; - type: string; + externalAssetId: string; + maxHeight: number; + rating: Rating; + episodeInformation: EpisodeInformation; + id: number; + accessLevel: string; + playerUrlCallback: string; + thumbnailsPreview: string; + displayableTags: any[]; + plugins: any[]; + watchStatus: string; + computedReleases: any[]; + licences: any[]; + type: string; } export interface ContentDownload { permission: string; - period: string; + period: string; } export interface EpisodeInformation { - seasonNumber: number; + seasonNumber: number; episodeNumber: number; - season: number; + season: number; } export interface Rating { - rating: string; + rating: string; descriptors: any[]; -} \ No newline at end of file +} diff --git a/@types/newHidivePlayback.d.ts b/@types/newHidivePlayback.d.ts index 3094c0c..696eaa8 100644 --- a/@types/newHidivePlayback.d.ts +++ b/@types/newHidivePlayback.d.ts @@ -1,33 +1,33 @@ export interface NewHidivePlayback { - watermark: null; + watermark: null; skipMarkers: any[]; annotations: null; - dash: Format[]; - hls: Format[]; + dash: Format[]; + hls: Format[]; } export interface Format { subtitles: Subtitle[]; - url: string; - drm: DRM; + url: string; + drm: DRM; } export interface DRM { encryptionMode: string; - containerType: string; - jwtToken: string; - url: string; - keySystems: string[]; + containerType: string; + jwtToken: string; + url: string; + keySystems: string[]; } export interface Subtitle { - format: Formats; + format: Formats; language: string; - url: string; + url: string; } export enum Formats { Scc = 'scc', Srt = 'srt', - Vtt = 'vtt', + Vtt = 'vtt' } diff --git a/@types/newHidiveSearch.d.ts b/@types/newHidiveSearch.d.ts index 44cd903..8b1c615 100644 --- a/@types/newHidiveSearch.d.ts +++ b/@types/newHidiveSearch.d.ts @@ -3,56 +3,56 @@ export interface NewHidiveSearch { } export interface Result { - hits: Hit[]; - nbHits: number; - page: number; - nbPages: number; - hitsPerPage: number; - exhaustiveNbHits: boolean; - exhaustiveTypo: boolean; - exhaustive: Exhaustive; - query: string; - params: string; - index: string; - renderingContent: object; - processingTimeMS: number; + hits: Hit[]; + nbHits: number; + page: number; + nbPages: number; + hitsPerPage: number; + exhaustiveNbHits: boolean; + exhaustiveTypo: boolean; + exhaustive: Exhaustive; + query: string; + params: string; + index: string; + renderingContent: object; + processingTimeMS: number; processingTimingsMS: ProcessingTimingsMS; - serverTimeMS: number; + serverTimeMS: number; } export interface Exhaustive { nbHits: boolean; - typo: boolean; + typo: boolean; } export interface Hit { - type: string; - weight: number; - id: number; - name: string; - description: string; - meta: object; - coverUrl: string; - smallCoverUrl: string; - seasonsCount: number; - tags: string[]; - localisations: HitLocalisations; - ratings: Ratings; - objectID: string; + type: string; + weight: number; + id: number; + name: string; + description: string; + meta: object; + coverUrl: string; + smallCoverUrl: string; + seasonsCount: number; + tags: string[]; + localisations: HitLocalisations; + ratings: Ratings; + objectID: string; _highlightResult: HighlightResult; } export interface HighlightResult { - name: Description; - description: Description; - tags: Description[]; + name: Description; + description: Description; + tags: Description[]; localisations: HighlightResultLocalisations; } export interface Description { - value: string; - matchLevel: string; - matchedWords: string[]; + value: string; + matchLevel: string; + matchedWords: string[]; fullyHighlighted?: boolean; } @@ -61,7 +61,7 @@ export interface HighlightResultLocalisations { } export interface PurpleEnUS { - title: Description; + title: Description; description: Description; } @@ -70,7 +70,7 @@ export interface HitLocalisations { } export interface HitLocalization { - title: string; + title: string; description: string; } @@ -83,6 +83,6 @@ export interface ProcessingTimingsMS { } export interface Request { - queue: number; + queue: number; roundTrip: number; } diff --git a/@types/newHidiveSeason.d.ts b/@types/newHidiveSeason.d.ts index 973c725..5d70ad1 100644 --- a/@types/newHidiveSeason.d.ts +++ b/@types/newHidiveSeason.d.ts @@ -1,52 +1,52 @@ export interface NewHidiveSeason { - title: string; - description: string; + title: string; + description: string; longDescription: string; - smallCoverUrl: string; - coverUrl: string; - titleUrl: string; - posterUrl: string; - seasonNumber: number; - episodeCount: number; + smallCoverUrl: string; + coverUrl: string; + titleUrl: string; + posterUrl: string; + seasonNumber: number; + episodeCount: number; displayableTags: any[]; - rating: Rating; - contentRating: Rating; - id: number; - series: Series; - episodes: Episode[]; - paging: Paging; - licences: any[]; + rating: Rating; + contentRating: Rating; + id: number; + series: Series; + episodes: Episode[]; + paging: Paging; + licences: any[]; } export interface Rating { - rating: string; + rating: string; descriptors: any[]; } export interface Episode { - accessLevel: string; - availablePurchases?: any[]; - licenceIds?: any[]; - type: string; - id: number; - title: string; - description: string; - thumbnailUrl: string; - posterUrl: string; - duration: number; - favourite: boolean; - contentDownload: ContentDownload; + accessLevel: string; + availablePurchases?: any[]; + licenceIds?: any[]; + type: string; + id: number; + title: string; + description: string; + thumbnailUrl: string; + posterUrl: string; + duration: number; + favourite: boolean; + contentDownload: ContentDownload; offlinePlaybackLanguages: string[]; - externalAssetId: string; - subEvents: any[]; - maxHeight: number; - thumbnailsPreview: string; - longDescription: string; - episodeInformation: EpisodeInformation; - categories: string[]; - displayableTags: any[]; - watchStatus: string; - computedReleases: any[]; + externalAssetId: string; + subEvents: any[]; + maxHeight: number; + thumbnailsPreview: string; + longDescription: string; + episodeInformation: EpisodeInformation; + categories: string[]; + displayableTags: any[]; + watchStatus: string; + computedReleases: any[]; } export interface ContentDownload { @@ -54,24 +54,24 @@ export interface ContentDownload { } export interface EpisodeInformation { - seasonNumber: number; + seasonNumber: number; episodeNumber: number; - season: number; + season: number; } export interface Paging { moreDataAvailable: boolean; - lastSeen: number; + lastSeen: number; } export interface Series { - seriesId: number; - title: string; - description: string; + seriesId: number; + title: string; + description: string; longDescription: string; displayableTags: any[]; - rating: Rating; - contentRating: Rating; + rating: Rating; + contentRating: Rating; } export interface NewHidiveSeriesExtra extends Series { @@ -79,11 +79,11 @@ export interface NewHidiveSeriesExtra extends Series { } export interface NewHidiveEpisodeExtra extends Episode { - titleId: number; - nameLong: string; - seasonTitle: string; - seriesTitle: string; - seriesId?: number; - isSelected: boolean; - jwtToken?: string; -} \ No newline at end of file + titleId: number; + nameLong: string; + seasonTitle: string; + seriesTitle: string; + seriesId?: number; + isSelected: boolean; + jwtToken?: string; +} diff --git a/@types/newHidiveSeries.d.ts b/@types/newHidiveSeries.d.ts index 31935d2..280f9a4 100644 --- a/@types/newHidiveSeries.d.ts +++ b/@types/newHidiveSeries.d.ts @@ -1,35 +1,35 @@ export interface NewHidiveSeries { - id: number; - title: string; - description: string; + id: number; + title: string; + description: string; longDescription: string; - smallCoverUrl: string; - coverUrl: string; - titleUrl: string; - posterUrl: string; - seasons: Season[]; - rating: Rating; - contentRating: Rating; + smallCoverUrl: string; + coverUrl: string; + titleUrl: string; + posterUrl: string; + seasons: Season[]; + rating: Rating; + contentRating: Rating; displayableTags: any[]; - paging: Paging; + paging: Paging; } export interface Rating { - rating: string; + rating: string; descriptors: any[]; } export interface Paging { moreDataAvailable: boolean; - lastSeen: number; + lastSeen: number; } export interface Season { - title: string; - description: string; + title: string; + description: string; longDescription: string; - seasonNumber: number; - episodeCount: number; + seasonNumber: number; + episodeCount: number; displayableTags: any[]; - id: number; + id: number; } diff --git a/@types/objectInfo.d.ts b/@types/objectInfo.d.ts index 7a9a9e9..338ffbe 100644 --- a/@types/objectInfo.d.ts +++ b/@types/objectInfo.d.ts @@ -2,42 +2,42 @@ export interface ObjectInfo { total: number; - data: CrunchyObject[]; - meta: Record; + data: CrunchyObject[]; + meta: Record; } export interface CrunchyObject { - __links__?: Links; - channel_id: string; - slug: string; - images: Images; - linked_resource_key: string; - description: string; - promo_description: string; - external_id: string; - title: string; - series_metadata?: SeriesMetadata; - id: string; - slug_title: string; - type: string; - promo_title: string; + __links__?: Links; + channel_id: string; + slug: string; + images: Images; + linked_resource_key: string; + description: string; + promo_description: string; + external_id: string; + title: string; + series_metadata?: SeriesMetadata; + id: string; + slug_title: string; + type: string; + promo_title: string; movie_listing_metadata?: MovieListingMetadata; - movie_metadata?: MovieMetadata; - playback?: string; - episode_metadata?: EpisodeMetadata; - streams_link?: string; - season_metadata?: SeasonMetadata; - isSelected?: boolean; - f_num: string; - s_num: string; + movie_metadata?: MovieMetadata; + playback?: string; + episode_metadata?: EpisodeMetadata; + streams_link?: string; + season_metadata?: SeasonMetadata; + isSelected?: boolean; + f_num: string; + s_num: string; } export interface Links { - 'episode/season': LinkData; - 'episode/series': LinkData; - resource: LinkData; + 'episode/season': LinkData; + 'episode/series': LinkData; + resource: LinkData; 'resource/channel': LinkData; - streams: LinkData; + streams: LinkData; } export interface LinkData { @@ -45,150 +45,150 @@ export interface LinkData { } export interface EpisodeMetadata { - audio_locale: Locale; - availability_ends: Date; - availability_notes: string; - availability_starts: Date; - available_date: null; - available_offline: boolean; + audio_locale: Locale; + availability_ends: Date; + availability_notes: string; + availability_starts: Date; + available_date: null; + available_offline: boolean; closed_captions_available: boolean; - duration_ms: number; - eligible_region: string; - episode: string; - episode_air_date: Date; - episode_number: number; - extended_maturity_rating: Record; - free_available_date: Date; - identifier: string; - is_clip: boolean; - is_dubbed: boolean; - is_mature: boolean; - is_premium_only: boolean; - is_subbed: boolean; - mature_blocked: boolean; - maturity_ratings: string[]; - premium_available_date: Date; - premium_date: null; - season_id: string; - season_number: number; - season_slug_title: string; - season_title: string; - sequence_number: number; - series_id: string; - series_slug_title: string; - series_title: string; - subtitle_locales: Locale[]; - tenant_categories?: string[]; - upload_date: Date; - versions: EpisodeMetadataVersion[]; + duration_ms: number; + eligible_region: string; + episode: string; + episode_air_date: Date; + episode_number: number; + extended_maturity_rating: Record; + free_available_date: Date; + identifier: string; + is_clip: boolean; + is_dubbed: boolean; + is_mature: boolean; + is_premium_only: boolean; + is_subbed: boolean; + mature_blocked: boolean; + maturity_ratings: string[]; + premium_available_date: Date; + premium_date: null; + season_id: string; + season_number: number; + season_slug_title: string; + season_title: string; + sequence_number: number; + series_id: string; + series_slug_title: string; + series_title: string; + subtitle_locales: Locale[]; + tenant_categories?: string[]; + upload_date: Date; + versions: EpisodeMetadataVersion[]; } export interface EpisodeMetadataVersion { - audio_locale: Locale; - guid: string; + audio_locale: Locale; + guid: string; is_premium_only: boolean; - media_guid: string; - original: boolean; - season_guid: string; - variant: string; + media_guid: string; + original: boolean; + season_guid: string; + variant: string; } export interface Images { poster_tall?: Array; poster_wide?: Array; promo_image?: Array; - thumbnail?: Array; + thumbnail?: Array; } export interface Image { height: number; source: string; - type: ImageType; - width: number; + type: ImageType; + width: number; } export enum ImageType { PosterTall = 'poster_tall', PosterWide = 'poster_wide', PromoImage = 'promo_image', - Thumbnail = 'thumbnail', + Thumbnail = 'thumbnail' } export interface MovieListingMetadata { - availability_notes: string; - available_date: null; - available_offline: boolean; - duration_ms: number; - extended_description: string; + availability_notes: string; + available_date: null; + available_offline: boolean; + duration_ms: number; + extended_description: string; extended_maturity_rating: Record; - first_movie_id: string; - free_available_date: Date; - is_dubbed: boolean; - is_mature: boolean; - is_premium_only: boolean; - is_subbed: boolean; - mature_blocked: boolean; - maturity_ratings: string[]; - movie_release_year: number; - premium_available_date: Date; - premium_date: null; - subtitle_locales: Locale[]; - tenant_categories: string[]; + first_movie_id: string; + free_available_date: Date; + is_dubbed: boolean; + is_mature: boolean; + is_premium_only: boolean; + is_subbed: boolean; + mature_blocked: boolean; + maturity_ratings: string[]; + movie_release_year: number; + premium_available_date: Date; + premium_date: null; + subtitle_locales: Locale[]; + tenant_categories: string[]; } export interface MovieMetadata { - availability_notes: string; - available_offline: boolean; + availability_notes: string; + available_offline: boolean; closed_captions_available: boolean; - duration_ms: number; - extended_maturity_rating: Record; - is_dubbed: boolean; - is_mature: boolean; - is_premium_only: boolean; - is_subbed: boolean; - mature_blocked: boolean; - maturity_ratings: string[]; - movie_listing_id: string; - movie_listing_slug_title: string; - movie_listing_title: string; + duration_ms: number; + extended_maturity_rating: Record; + is_dubbed: boolean; + is_mature: boolean; + is_premium_only: boolean; + is_subbed: boolean; + mature_blocked: boolean; + maturity_ratings: string[]; + movie_listing_id: string; + movie_listing_slug_title: string; + movie_listing_title: string; } export interface SeasonMetadata { - audio_locale: Locale; - audio_locales: Locale[]; + audio_locale: Locale; + audio_locales: Locale[]; extended_maturity_rating: Record; - identifier: string; - is_mature: boolean; - mature_blocked: boolean; - maturity_ratings: string[]; - season_display_number: string; - season_sequence_number: number; - subtitle_locales: Locale[]; - versions: SeasonMetadataVersion[]; + identifier: string; + is_mature: boolean; + mature_blocked: boolean; + maturity_ratings: string[]; + season_display_number: string; + season_sequence_number: number; + subtitle_locales: Locale[]; + versions: SeasonMetadataVersion[]; } export interface SeasonMetadataVersion { audio_locale: Locale; - guid: string; - original: boolean; - variant: string; + guid: string; + original: boolean; + variant: string; } export interface SeriesMetadata { - audio_locales: Locale[]; - availability_notes: string; - episode_count: number; - extended_description: string; + audio_locales: Locale[]; + availability_notes: string; + episode_count: number; + extended_description: string; extended_maturity_rating: Record; - is_dubbed: boolean; - is_mature: boolean; - is_simulcast: boolean; - is_subbed: boolean; - mature_blocked: boolean; - maturity_ratings: string[]; - season_count: number; - series_launch_year: number; - subtitle_locales: Locale[]; - tenant_categories?: string[]; + is_dubbed: boolean; + is_mature: boolean; + is_simulcast: boolean; + is_subbed: boolean; + mature_blocked: boolean; + maturity_ratings: string[]; + season_count: number; + series_launch_year: number; + subtitle_locales: Locale[]; + tenant_categories?: string[]; } export enum Locale { @@ -207,5 +207,5 @@ export enum Locale { hiIN = 'hi-IN', zhCN = 'zh-CN', koKR = 'ko-KR', - jaJP = 'ja-JP', -} \ No newline at end of file + jaJP = 'ja-JP' +} diff --git a/@types/pkg.d.ts b/@types/pkg.d.ts index ba03e2a..ae11367 100644 --- a/@types/pkg.d.ts +++ b/@types/pkg.d.ts @@ -1,3 +1,3 @@ declare module 'pkg' { export async function exec(config: string[]); -} \ No newline at end of file +} diff --git a/@types/playbackData.d.ts b/@types/playbackData.d.ts index 6223ab1..f579647 100644 --- a/@types/playbackData.d.ts +++ b/@types/playbackData.d.ts @@ -7,96 +7,95 @@ export interface PlaybackData { } export interface StreamList { - download_hls: CrunchyStreams; - drm_adaptive_hls: CrunchyStreams; - multitrack_adaptive_hls_v2: CrunchyStreams; - vo_adaptive_hls: CrunchyStreams; - vo_drm_adaptive_hls: CrunchyStreams; - adaptive_hls: CrunchyStreams; - drm_download_dash: CrunchyStreams; - drm_download_hls: CrunchyStreams; + download_hls: CrunchyStreams; + drm_adaptive_hls: CrunchyStreams; + multitrack_adaptive_hls_v2: CrunchyStreams; + vo_adaptive_hls: CrunchyStreams; + vo_drm_adaptive_hls: CrunchyStreams; + adaptive_hls: CrunchyStreams; + drm_download_dash: CrunchyStreams; + drm_download_hls: CrunchyStreams; drm_multitrack_adaptive_hls_v2: CrunchyStreams; - vo_drm_adaptive_dash: CrunchyStreams; - adaptive_dash: CrunchyStreams; - urls: CrunchyStreams; - vo_adaptive_dash: CrunchyStreams; - download_dash: CrunchyStreams; - drm_adaptive_dash: CrunchyStreams; + vo_drm_adaptive_dash: CrunchyStreams; + adaptive_dash: CrunchyStreams; + urls: CrunchyStreams; + vo_adaptive_dash: CrunchyStreams; + download_dash: CrunchyStreams; + drm_adaptive_dash: CrunchyStreams; } export interface CrunchyStreams { - '': StreamDetails; - 'en-US'?: StreamDetails; - 'es-LA'?: StreamDetails; + '': StreamDetails; + 'en-US'?: StreamDetails; + 'es-LA'?: StreamDetails; 'es-419'?: StreamDetails; - 'es-ES'?: StreamDetails; - 'pt-BR'?: StreamDetails; - 'fr-FR'?: StreamDetails; - 'de-DE'?: StreamDetails; - 'ar-ME'?: StreamDetails; - 'ar-SA'?: StreamDetails; - 'it-IT'?: StreamDetails; - 'ru-RU'?: StreamDetails; - 'tr-TR'?: StreamDetails; - 'hi-IN'?: StreamDetails; - 'zh-CN'?: StreamDetails; - 'ko-KR'?: StreamDetails; - 'ja-JP'?: StreamDetails; + 'es-ES'?: StreamDetails; + 'pt-BR'?: StreamDetails; + 'fr-FR'?: StreamDetails; + 'de-DE'?: StreamDetails; + 'ar-ME'?: StreamDetails; + 'ar-SA'?: StreamDetails; + 'it-IT'?: StreamDetails; + 'ru-RU'?: StreamDetails; + 'tr-TR'?: StreamDetails; + 'hi-IN'?: StreamDetails; + 'zh-CN'?: StreamDetails; + 'ko-KR'?: StreamDetails; + 'ja-JP'?: StreamDetails; [string: string]: StreamDetails; } export interface StreamDetails { //hardsub_locale: Locale; hardsub_locale: string; - url: string; - hardsub_lang?: string; - audio_lang?: string; - type?: string; + url: string; + hardsub_lang?: string; + audio_lang?: string; + type?: string; } export interface Meta { - media_id: string; - subtitles: Subtitles; - bifs: string[]; - versions: Version[]; - audio_locale: Locale; + media_id: string; + subtitles: Subtitles; + bifs: string[]; + versions: Version[]; + audio_locale: Locale; closed_captions: Subtitles; - captions: Subtitles; + captions: Subtitles; } export interface Subtitles { - ''?: SubtitleInfo; - 'en-US'?: SubtitleInfo; - 'es-LA'?: SubtitleInfo; + ''?: SubtitleInfo; + 'en-US'?: SubtitleInfo; + 'es-LA'?: SubtitleInfo; 'es-419'?: SubtitleInfo; - 'es-ES'?: SubtitleInfo; - 'pt-BR'?: SubtitleInfo; - 'fr-FR'?: SubtitleInfo; - 'de-DE'?: SubtitleInfo; - 'ar-ME'?: SubtitleInfo; - 'ar-SA'?: SubtitleInfo; - 'it-IT'?: SubtitleInfo; - 'ru-RU'?: SubtitleInfo; - 'tr-TR'?: SubtitleInfo; - 'hi-IN'?: SubtitleInfo; - 'zh-CN'?: SubtitleInfo; - 'ko-KR'?: SubtitleInfo; - 'ja-JP'?: SubtitleInfo; + 'es-ES'?: SubtitleInfo; + 'pt-BR'?: SubtitleInfo; + 'fr-FR'?: SubtitleInfo; + 'de-DE'?: SubtitleInfo; + 'ar-ME'?: SubtitleInfo; + 'ar-SA'?: SubtitleInfo; + 'it-IT'?: SubtitleInfo; + 'ru-RU'?: SubtitleInfo; + 'tr-TR'?: SubtitleInfo; + 'hi-IN'?: SubtitleInfo; + 'zh-CN'?: SubtitleInfo; + 'ko-KR'?: SubtitleInfo; + 'ja-JP'?: SubtitleInfo; } - export interface SubtitleInfo { format: string; locale: Locale; - url: string; + url: string; } export interface Version { - audio_locale: Locale; - guid: string; + audio_locale: Locale; + guid: string; is_premium_only: boolean; - media_guid: string; - original: boolean; - season_guid: string; - variant: string; + media_guid: string; + original: boolean; + season_guid: string; + variant: string; } export enum Locale { @@ -116,5 +115,5 @@ export enum Locale { hiIN = 'hi-IN', zhCN = 'zh-CN', koKR = 'ko-KR', - jaJP = 'ja-JP', -} \ No newline at end of file + jaJP = 'ja-JP' +} diff --git a/@types/randomEvents.d.ts b/@types/randomEvents.d.ts index 357029f..1ee35fb 100644 --- a/@types/randomEvents.d.ts +++ b/@types/randomEvents.d.ts @@ -1,15 +1,15 @@ import { ExtendedProgress, QueueItem } from './messageHandler'; export type RandomEvents = { - progress: ExtendedProgress, - finish: undefined, - queueChange: QueueItem[], - current: QueueItem|undefined -} + progress: ExtendedProgress; + finish: undefined; + queueChange: QueueItem[]; + current: QueueItem | undefined; +}; export interface RandomEvent { - name: T, - data: RandomEvents[T] + name: T; + data: RandomEvents[T]; } -export type Handler = (data: RandomEvent) => unknown; \ No newline at end of file +export type Handler = (data: RandomEvent) => unknown; diff --git a/@types/removeNPMAbsolutePaths.d.ts b/@types/removeNPMAbsolutePaths.d.ts index f141c00..eab4c83 100644 --- a/@types/removeNPMAbsolutePaths.d.ts +++ b/@types/removeNPMAbsolutePaths.d.ts @@ -1,3 +1,3 @@ declare module 'removeNPMAbsolutePaths' { export default async function modulesCleanup(path: string); -} \ No newline at end of file +} diff --git a/@types/serviceClassInterface.d.ts b/@types/serviceClassInterface.d.ts index 4115bd6..89e33be 100644 --- a/@types/serviceClassInterface.d.ts +++ b/@types/serviceClassInterface.d.ts @@ -1,3 +1,3 @@ export interface ServiceClass { - cli: () => Promise -} \ No newline at end of file + cli: () => Promise; +} diff --git a/@types/streamData.d.ts b/@types/streamData.d.ts index 16ed2e7..7722335 100644 --- a/@types/streamData.d.ts +++ b/@types/streamData.d.ts @@ -1,28 +1,28 @@ // Generated by https://quicktype.io export interface StreamData { - items: Item[]; + items: Item[]; watchHistorySaveInterval: number; - errors?: Error[] + errors?: Error[]; } export interface Error { - detail: string, - code: number + detail: string; + code: number; } export interface Item { - src: string; - kind: string; - isPromo: boolean; - videoType: string; - aips: Aip[]; + src: string; + kind: string; + isPromo: boolean; + videoType: string; + aips: Aip[]; experienceId: string; - showAds: boolean; - id: number; + showAds: boolean; + id: number; } export interface Aip { out: number; - in: number; + in: number; } diff --git a/@types/updateFile.d.ts b/@types/updateFile.d.ts index 3960260..8dfd657 100644 --- a/@types/updateFile.d.ts +++ b/@types/updateFile.d.ts @@ -1,4 +1,4 @@ export type UpdateFile = { - lastCheck: number, - nextCheck: number -} \ No newline at end of file + lastCheck: number; + nextCheck: number; +}; diff --git a/@types/ws.d.ts b/@types/ws.d.ts index 1c85447..2dd80a2 100644 --- a/@types/ws.d.ts +++ b/@types/ws.d.ts @@ -1,45 +1,45 @@ import { GUIConfig } from '../modules/module.cfg-loader'; import { AuthResponse, CheckTokenResponse, EpisodeListResponse, FolderTypes, QueueItem, ResolveItemsData, SearchData, SearchResponse } from './messageHandler'; -export type WSMessage = { - name: T, - data: MessageTypes[T][P] -} +export type WSMessage = { + name: T; + data: MessageTypes[T][P]; +}; -export type WSMessageWithID = WSMessage & { - id: string -} +export type WSMessageWithID = WSMessage & { + id: string; +}; export type UnknownWSMessage = { - name: keyof MessageTypes, - data: MessageTypes[keyof MessageTypes][0], - id: string -} + name: keyof MessageTypes; + data: MessageTypes[keyof MessageTypes][0]; + id: string; +}; export type MessageTypes = { - 'auth': [AuthData, AuthResponse], - 'version': [undefined, string], - 'checkToken': [undefined, CheckTokenResponse], - 'search': [SearchData, SearchResponse], - 'default': [string, unknown], - 'availableDubCodes': [undefined, string[]], - 'availableSubCodes': [undefined, string[]], - 'resolveItems': [ResolveItemsData, boolean], - 'listEpisodes': [string, EpisodeListResponse], - 'downloadItem': [QueueItem, undefined], - 'isDownloading': [undefined, boolean], - 'openFolder': [FolderTypes, undefined], - 'changeProvider': [undefined, boolean], - 'type': [undefined, 'crunchy'|'hidive'|'ao'|'adn'|undefined], - 'setup': ['crunchy'|'hidive'|'ao'|'adn'|undefined, undefined], - 'openFile': [[FolderTypes, string], undefined], - 'openURL': [string, undefined], - 'isSetup': [undefined, boolean], - 'setupServer': [GUIConfig, boolean], - 'requirePassword': [undefined, boolean], - 'getQueue': [undefined, QueueItem[]], - 'removeFromQueue': [number, undefined], - 'clearQueue': [undefined, undefined], - 'setDownloadQueue': [boolean, undefined], - 'getDownloadQueue': [undefined, boolean] -} \ No newline at end of file + auth: [AuthData, AuthResponse]; + version: [undefined, string]; + checkToken: [undefined, CheckTokenResponse]; + search: [SearchData, SearchResponse]; + default: [string, unknown]; + availableDubCodes: [undefined, string[]]; + availableSubCodes: [undefined, string[]]; + resolveItems: [ResolveItemsData, boolean]; + listEpisodes: [string, EpisodeListResponse]; + downloadItem: [QueueItem, undefined]; + isDownloading: [undefined, boolean]; + openFolder: [FolderTypes, undefined]; + changeProvider: [undefined, boolean]; + type: [undefined, 'crunchy' | 'hidive' | 'ao' | 'adn' | undefined]; + setup: ['crunchy' | 'hidive' | 'ao' | 'adn' | undefined, undefined]; + openFile: [[FolderTypes, string], undefined]; + openURL: [string, undefined]; + isSetup: [undefined, boolean]; + setupServer: [GUIConfig, boolean]; + requirePassword: [undefined, boolean]; + getQueue: [undefined, QueueItem[]]; + removeFromQueue: [number, undefined]; + clearQueue: [undefined, undefined]; + setDownloadQueue: [boolean, undefined]; + getDownloadQueue: [undefined, boolean]; +}; diff --git a/adn.ts b/adn.ts index 7e0f40e..7737266 100644 --- a/adn.ts +++ b/adn.ts @@ -36,903 +36,912 @@ import { ADNPlayerConfig } from './@types/adnPlayerConfig'; import { ADNStreams } from './@types/adnStreams'; import { ADNSubtitles } from './@types/adnSubtitles'; -export default class AnimationDigitalNetwork implements ServiceClass { - public cfg: yamlCfg.ConfigObject; - public locale: string; - private token: Record; - private req: reqModule.Req; - private posAlignMap: { [key: string]: number } = { - 'start': 1, - 'end': 3 - }; - private lineAlignMap: { [key: string]: number } = { - 'middle': 8, - 'end': 4 - }; - private jpnStrings: string[] = [ - 'vostf', - 'vostde' - ]; - private deuStrings: string[] = [ - 'vde' - ]; - private fraStrings: string[] = [ - 'vf' - ]; - private deuSubStrings: string[] = [ - 'vde', - 'vostde' - ]; - private fraSubStrings: string[] = [ - 'vf', - 'vostf' - ]; - - constructor(private debug = false) { - this.cfg = yamlCfg.loadCfg(); - this.token = yamlCfg.loadADNToken(); - this.req = new reqModule.Req(domain, debug, false, 'adn'); - this.locale = 'fr'; - } +export default class AnimationDigitalNetwork implements ServiceClass { + public cfg: yamlCfg.ConfigObject; + public locale: string; + private token: Record; + private req: reqModule.Req; + private posAlignMap: { [key: string]: number } = { + start: 1, + end: 3 + }; + private lineAlignMap: { [key: string]: number } = { + middle: 8, + end: 4 + }; + private jpnStrings: string[] = ['vostf', 'vostde']; + private deuStrings: string[] = ['vde']; + private fraStrings: string[] = ['vf']; + private deuSubStrings: string[] = ['vde', 'vostde']; + private fraSubStrings: string[] = ['vf', 'vostf']; - public async cli() { - console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`); - const argv = yargs.appArgv(this.cfg.cli); - if (['fr', 'de'].includes(argv.locale)) - this.locale = argv.locale; - if (argv.debug) - this.debug = true; + constructor(private debug = false) { + this.cfg = yamlCfg.loadCfg(); + this.token = yamlCfg.loadADNToken(); + this.req = new reqModule.Req(domain, debug, false, 'adn'); + this.locale = 'fr'; + } - // load binaries - this.cfg.bin = await yamlCfg.loadBinCfg(); - if (argv.allDubs) { - argv.dubLang = langsData.dubLanguageCodes; - } - if (argv.auth) { - //Authenticate - await this.doAuth({ - username: argv.username ?? await Helper.question('[Q] LOGIN/EMAIL: '), - password: argv.password ?? await Helper.question('[Q] PASSWORD: ') - }); - } else if (argv.search && argv.search.length > 2) { - //Search - await this.doSearch({ ...argv, search: argv.search as string }); - } else if (argv.s && !isNaN(parseInt(argv.s,10)) && parseInt(argv.s,10) > 0) { - const selected = await this.selectShow(parseInt(argv.s), argv.e, argv.but, argv.all); - if (selected.isOk) { - for (const select of selected.value) { - if (!(await this.getEpisode(select, {...argv, skipsubs: false}))) { - console.error(`Unable to download selected episode ${select.shortNumber}`); - return false; - } - } - } - return true; - } else { - console.info('No option selected or invalid value entered. Try --help.'); - } - } + public async cli() { + console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`); + const argv = yargs.appArgv(this.cfg.cli); + if (['fr', 'de'].includes(argv.locale)) this.locale = argv.locale; + if (argv.debug) this.debug = true; - private generateRandomString(length: number) { - const characters = '0123456789abcdef'; - let result = ''; - for (let i = 0; i < length; i++) { - result += characters.charAt(Math.floor(Math.random() * characters.length)); - } - return result; - } + // load binaries + this.cfg.bin = await yamlCfg.loadBinCfg(); + if (argv.allDubs) { + argv.dubLang = langsData.dubLanguageCodes; + } + if (argv.auth) { + //Authenticate + await this.doAuth({ + username: argv.username ?? (await Helper.question('[Q] LOGIN/EMAIL: ')), + password: argv.password ?? (await Helper.question('[Q] PASSWORD: ')) + }); + } else if (argv.search && argv.search.length > 2) { + //Search + await this.doSearch({ ...argv, search: argv.search as string }); + } else if (argv.s && !isNaN(parseInt(argv.s, 10)) && parseInt(argv.s, 10) > 0) { + const selected = await this.selectShow(parseInt(argv.s), argv.e, argv.but, argv.all); + if (selected.isOk) { + for (const select of selected.value) { + if (!(await this.getEpisode(select, { ...argv, skipsubs: false }))) { + console.error(`Unable to download selected episode ${select.shortNumber}`); + return false; + } + } + } + return true; + } else { + console.info('No option selected or invalid value entered. Try --help.'); + } + } - private parseCookies(cookiesString: string | null): Record { - const cookies: Record = {}; - if (cookiesString) { - cookiesString.split(';').forEach(cookie => { - const parts = cookie.split('='); - const name = parts.shift()?.trim(); - const value = decodeURIComponent(parts.join('=')); - if (name) { - cookies[name] = value; - } - }); - } - return cookies; - } + private generateRandomString(length: number) { + const characters = '0123456789abcdef'; + let result = ''; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * characters.length)); + } + return result; + } - private convertToSSATimestamp(timestamp: number): string { - const seconds = Math.floor(timestamp); - const centiseconds = Math.round((timestamp - seconds) * 100); + private parseCookies(cookiesString: string | null): Record { + const cookies: Record = {}; + if (cookiesString) { + cookiesString.split(';').forEach((cookie) => { + const parts = cookie.split('='); + const name = parts.shift()?.trim(); + const value = decodeURIComponent(parts.join('=')); + if (name) { + cookies[name] = value; + } + }); + } + return cookies; + } - const hours = Math.floor(seconds / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - const remainingSeconds = seconds % 60; + private convertToSSATimestamp(timestamp: number): string { + const seconds = Math.floor(timestamp); + const centiseconds = Math.round((timestamp - seconds) * 100); - return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`; - } + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const remainingSeconds = seconds % 60; - public async doSearch(data: SearchData): Promise { - const limit = 12; - const offset = data.page ? data.page * limit : 0; - const searchReq = await this.req.getData(`https://gw.api.animationdigitalnetwork.com/show/catalog?maxAgeCategory=18&offset=${offset}&limit=${limit}&search=${encodeURIComponent(data.search)}`, { - 'headers': { - 'X-Target-Distribution': this.locale - } - }); - if (!searchReq.ok || !searchReq.res) { - console.error('Search FAILED!'); - return { isOk: false, reason: new Error('Search failed. No more information provided') }; - } - const searchData = await searchReq.res.json() as ADNSearch; - const searchItems: ADNSearchShow[] = []; - console.info('Search Results:'); - for (const show of searchData.shows) { - searchItems.push(show); - let fullType: string; - if (show.type == 'EPS') { - fullType = `S.${show.id}`; - } else if (show.type == 'MOV' || show.type == 'OAV') { - fullType = `E.${show.id}`; - } else { - fullType = 'Unknown'; - console.warn(`Unknown type ${show.type}, please report this.`); - } - console.log(`[${fullType}] ${show.title}`); - } - return { isOk: true, value: searchItems.flatMap((a): SearchResponseItem => { - return { - id: a.id+'', - image: a.image ?? '/notFound.png', - name: a.title, - rating: a.rating, - desc: a.summary - }; - })}; - } + return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`; + } - public async doAuth(data: AuthData): Promise { - const authData = JSON.stringify({ - 'username': data.username, - 'password': data.password, - 'source': 'Web' - }); - const authReqOpts: reqModule.Params = { - method: 'POST', - body: authData, - headers: { - 'content-type': 'application/json', - 'x-target-distribution': this.locale, - } - }; - const authReq = await this.req.getData('https://gw.api.animationdigitalnetwork.com/authentication/login', authReqOpts); - if(!authReq.ok || !authReq.res){ - console.error('Authentication failed!'); - return { isOk: false, reason: new Error('Authentication failed') }; - } - this.token = await authReq.res.json(); - yamlCfg.saveADNToken(this.token); - console.info('Authentication Success'); - return { isOk: true, value: undefined }; - } + public async doSearch(data: SearchData): Promise { + const limit = 12; + const offset = data.page ? data.page * limit : 0; + const searchReq = await this.req.getData( + `https://gw.api.animationdigitalnetwork.com/show/catalog?maxAgeCategory=18&offset=${offset}&limit=${limit}&search=${encodeURIComponent(data.search)}`, + { + headers: { + 'X-Target-Distribution': this.locale + } + } + ); + if (!searchReq.ok || !searchReq.res) { + console.error('Search FAILED!'); + return { isOk: false, reason: new Error('Search failed. No more information provided') }; + } + const searchData = (await searchReq.res.json()) as ADNSearch; + const searchItems: ADNSearchShow[] = []; + console.info('Search Results:'); + for (const show of searchData.shows) { + searchItems.push(show); + let fullType: string; + if (show.type == 'EPS') { + fullType = `S.${show.id}`; + } else if (show.type == 'MOV' || show.type == 'OAV') { + fullType = `E.${show.id}`; + } else { + fullType = 'Unknown'; + console.warn(`Unknown type ${show.type}, please report this.`); + } + console.log(`[${fullType}] ${show.title}`); + } + return { + isOk: true, + value: searchItems.flatMap((a): SearchResponseItem => { + return { + id: a.id + '', + image: a.image ?? '/notFound.png', + name: a.title, + rating: a.rating, + desc: a.summary + }; + }) + }; + } - public async refreshToken() { - const authReq = await this.req.getData('https://gw.api.animationdigitalnetwork.com/authentication/refresh', { - method: 'POST', - headers: { - Authorization: `Bearer ${this.token.accessToken}`, - 'X-Access-Token': this.token.accessToken, - 'content-type': 'application/json', - 'x-target-distribution': this.locale - }, - body: JSON.stringify({refreshToken: this.token.refreshToken}) - }); - if(!authReq.ok || !authReq.res){ - console.error('Token refresh failed!'); - return { isOk: false, reason: new Error('Token refresh failed') }; - } - this.token = await authReq.res.json(); - yamlCfg.saveADNToken(this.token); - return { isOk: true, value: undefined }; - } + public async doAuth(data: AuthData): Promise { + const authData = JSON.stringify({ + username: data.username, + password: data.password, + source: 'Web' + }); + const authReqOpts: reqModule.Params = { + method: 'POST', + body: authData, + headers: { + 'content-type': 'application/json', + 'x-target-distribution': this.locale + } + }; + const authReq = await this.req.getData('https://gw.api.animationdigitalnetwork.com/authentication/login', authReqOpts); + if (!authReq.ok || !authReq.res) { + console.error('Authentication failed!'); + return { isOk: false, reason: new Error('Authentication failed') }; + } + this.token = await authReq.res.json(); + yamlCfg.saveADNToken(this.token); + console.info('Authentication Success'); + return { isOk: true, value: undefined }; + } - public async getShow(id: number) { - const getShowData = await this.req.getData(`https://gw.api.animationdigitalnetwork.com/video/show/${id}?maxAgeCategory=18&limit=-1&order=asc`, { - 'headers': { - 'X-Target-Distribution': this.locale - } - }); - if (!getShowData.ok || !getShowData.res) { - console.error('Failed to get Series Data'); - return { isOk: false }; - } - const showData = await getShowData.res.json() as ADNVideos; - return { isOk: true, value: showData }; - } + public async refreshToken() { + const authReq = await this.req.getData('https://gw.api.animationdigitalnetwork.com/authentication/refresh', { + method: 'POST', + headers: { + Authorization: `Bearer ${this.token.accessToken}`, + 'X-Access-Token': this.token.accessToken, + 'content-type': 'application/json', + 'x-target-distribution': this.locale + }, + body: JSON.stringify({ refreshToken: this.token.refreshToken }) + }); + if (!authReq.ok || !authReq.res) { + console.error('Token refresh failed!'); + return { isOk: false, reason: new Error('Token refresh failed') }; + } + this.token = await authReq.res.json(); + yamlCfg.saveADNToken(this.token); + return { isOk: true, value: undefined }; + } - public async listShow(id: number) { - const show = await this.getShow(id); - if (!show.isOk || !show.value) { - console.error('Failed to list show data: Failed to get show'); - return { isOk: false }; - } - if (show.value.videos.length == 0) { - console.error('No episodes found!'); - return { isOk: false }; - } - const showData = show.value.videos[0].show; - console.info(`[S.${showData.id}] ${showData.title}`); - const specials: ADNVideo[] = []; - const ncs: ADNVideo[] = []; - let episodeIndex = 0, specialIndex = 0, ncIndex = 0; - for (const episode of show.value.videos) { - episode.season = episode.season+''; - const seasonNumberTitleParse = episode.season.match(/\d+/); - const seriesNumberTitleParse = episode.show.title.match(/\d+/); - const episodeNumber = parseInt(episode.shortNumber); - if (seasonNumberTitleParse && !isNaN(parseInt(seasonNumberTitleParse[0]))) { - episode.season = seasonNumberTitleParse[0]; - } else if (seriesNumberTitleParse && !isNaN(parseInt(seriesNumberTitleParse[0]))) { - episode.season = seriesNumberTitleParse[0]; - } else { - episode.season = '1'; - } - show.value.videos[episodeIndex].season = episode.season; - show.value.videos[episodeIndex].shortNumber = episodeIndex+''; - if (!episodeNumber) { - specialIndex++; - episode.shortNumber = 'S'+specialIndex; - specials.push(episode); - episodeIndex--; - } else if (episode.number.includes('(NC)')) { - ncIndex++; - episode.shortNumber = 'NC'+ncIndex; - ncs.push(episode); - episodeIndex--; - } else { - console.info(` (${episode.id}) [E${episode.shortNumber}] ${episode.number} - ${episode.name}`); - } - episodeIndex++; - } - for (const special of specials) { - console.info(` (Special) (${special.id}) [${special.shortNumber}] ${special.number} - ${special.name}`); - show.value.videos.splice(show.value.videos.findIndex(i => i.id === special.id), 1); - } - for (const nc of ncs) { - console.info(` (NC) (${nc.id}) [${nc.shortNumber}] ${nc.number} - ${nc.name}`); - show.value.videos.splice(show.value.videos.findIndex(i => i.id === nc.id), 1); - } - show.value.videos.push(...specials); - show.value.videos.push(...ncs); - return { isOk: true, value: show.value }; - } + public async getShow(id: number) { + const getShowData = await this.req.getData(`https://gw.api.animationdigitalnetwork.com/video/show/${id}?maxAgeCategory=18&limit=-1&order=asc`, { + headers: { + 'X-Target-Distribution': this.locale + } + }); + if (!getShowData.ok || !getShowData.res) { + console.error('Failed to get Series Data'); + return { isOk: false }; + } + const showData = (await getShowData.res.json()) as ADNVideos; + return { isOk: true, value: showData }; + } - public async selectShow(id: number, e: string | undefined, but: boolean, all: boolean) { - const getShowData = await this.listShow(id); - if (!getShowData.isOk || !getShowData.value) { - return { isOk: false, value: [] }; - } - console.info(''); - console.info('-'.repeat(30)); - console.info(''); - const showData = getShowData.value; - const doEpsFilter = parseSelect(e as string); - const selEpsArr: ADNVideo[] = []; - for (const episode of showData.videos) { - if ( - all || - but && !doEpsFilter.isSelected([episode.shortNumber, episode.id+'']) || - !but && doEpsFilter.isSelected([episode.shortNumber, episode.id+'']) - ) { - selEpsArr.push({ isSelected: true, ...episode }); - console.info('%s[S%sE%s] %s', - '✓ ', - episode.season, - episode.shortNumber, - episode.name, - ); - } - } - return { isOk: true, value: selEpsArr }; - } + public async listShow(id: number) { + const show = await this.getShow(id); + if (!show.isOk || !show.value) { + console.error('Failed to list show data: Failed to get show'); + return { isOk: false }; + } + if (show.value.videos.length == 0) { + console.error('No episodes found!'); + return { isOk: false }; + } + const showData = show.value.videos[0].show; + console.info(`[S.${showData.id}] ${showData.title}`); + const specials: ADNVideo[] = []; + const ncs: ADNVideo[] = []; + let episodeIndex = 0, + specialIndex = 0, + ncIndex = 0; + for (const episode of show.value.videos) { + episode.season = episode.season + ''; + const seasonNumberTitleParse = episode.season.match(/\d+/); + const seriesNumberTitleParse = episode.show.title.match(/\d+/); + const episodeNumber = parseInt(episode.shortNumber); + if (seasonNumberTitleParse && !isNaN(parseInt(seasonNumberTitleParse[0]))) { + episode.season = seasonNumberTitleParse[0]; + } else if (seriesNumberTitleParse && !isNaN(parseInt(seriesNumberTitleParse[0]))) { + episode.season = seriesNumberTitleParse[0]; + } else { + episode.season = '1'; + } + show.value.videos[episodeIndex].season = episode.season; + show.value.videos[episodeIndex].shortNumber = episodeIndex + ''; + if (!episodeNumber) { + specialIndex++; + episode.shortNumber = 'S' + specialIndex; + specials.push(episode); + episodeIndex--; + } else if (episode.number.includes('(NC)')) { + ncIndex++; + episode.shortNumber = 'NC' + ncIndex; + ncs.push(episode); + episodeIndex--; + } else { + console.info(` (${episode.id}) [E${episode.shortNumber}] ${episode.number} - ${episode.name}`); + } + episodeIndex++; + } + for (const special of specials) { + console.info(` (Special) (${special.id}) [${special.shortNumber}] ${special.number} - ${special.name}`); + show.value.videos.splice( + show.value.videos.findIndex((i) => i.id === special.id), + 1 + ); + } + for (const nc of ncs) { + console.info(` (NC) (${nc.id}) [${nc.shortNumber}] ${nc.number} - ${nc.name}`); + show.value.videos.splice( + show.value.videos.findIndex((i) => i.id === nc.id), + 1 + ); + } + show.value.videos.push(...specials); + show.value.videos.push(...ncs); + return { isOk: true, value: show.value }; + } - public async muxStreams(data: DownloadedMedia[], options: yargs.ArgvType) { - this.cfg.bin = await yamlCfg.loadBinCfg(); - let hasAudioStreams = false; - if (options.novids || data.filter(a => a.type === 'Video').length === 0) - return console.info('Skip muxing since no vids are downloaded'); - if (data.some(a => a.type === 'Audio')) { - hasAudioStreams = true; - } - const merger = new Merger({ - onlyVid: hasAudioStreams ? data.filter(a => a.type === 'Video').map((a) : MergerInput => { - if (a.type === 'Subtitle') - throw new Error('Never'); - return { - lang: a.lang, - path: a.path, - }; - }) : [], - skipSubMux: options.skipSubMux, - inverseTrackOrder: false, - keepAllVideos: options.keepAllVideos, - onlyAudio: hasAudioStreams ? data.filter(a => a.type === 'Audio').map((a) : MergerInput => { - if (a.type === 'Subtitle') - throw new Error('Never'); - return { - lang: a.lang, - path: a.path, - }; - }) : [], - output: `${options.output}.${options.mp4 ? 'mp4' : 'mkv'}`, - subtitles: data.filter(a => a.type === 'Subtitle').map((a) : SubtitleInput => { - if (a.type === 'Video') - throw new Error('Never'); - if (a.type === 'Audio') - throw new Error('Never'); - return { - file: a.path, - language: a.language, - closedCaption: a.cc - }; - }), - simul: data.filter(a => a.type === 'Video').map((a) : boolean => { - if (a.type === 'Subtitle') - throw new Error('Never'); - return !a.uncut as boolean; - })[0], - fonts: Merger.makeFontsList(this.cfg.dir.fonts, data.filter(a => a.type === 'Subtitle') as sxItem[]), - videoAndAudio: hasAudioStreams ? [] : data.filter(a => a.type === 'Video').map((a) : MergerInput => { - if (a.type === 'Subtitle') - throw new Error('Never'); - return { - lang: a.lang, - path: a.path, - }; - }), - chapters: data.filter(a => a.type === 'Chapters').map((a) : MergerInput => { - return { - path: a.path, - lang: a.lang - }; - }), - videoTitle: options.videoTitle, - options: { - ffmpeg: options.ffmpegOptions, - mkvmerge: options.mkvmergeOptions - }, - defaults: { - audio: options.defaultAudio, - sub: options.defaultSub - }, - ccTag: options.ccTag - }); - const bin = Merger.checkMerger(this.cfg.bin, options.mp4, options.forceMuxer); - // collect fonts info - // mergers - let isMuxed = false; - if (options.syncTiming) { - await merger.createDelays(); - } - if (bin.MKVmerge) { - await merger.merge('mkvmerge', bin.MKVmerge); - isMuxed = true; - } else if (bin.FFmpeg) { - await merger.merge('ffmpeg', bin.FFmpeg); - isMuxed = true; - } else{ - console.info('\nDone!\n'); - return; - } - if (isMuxed && !options.nocleanup) - merger.cleanUp(); - } + public async selectShow(id: number, e: string | undefined, but: boolean, all: boolean) { + const getShowData = await this.listShow(id); + if (!getShowData.isOk || !getShowData.value) { + return { isOk: false, value: [] }; + } + console.info(''); + console.info('-'.repeat(30)); + console.info(''); + const showData = getShowData.value; + const doEpsFilter = parseSelect(e as string); + const selEpsArr: ADNVideo[] = []; + for (const episode of showData.videos) { + if (all || (but && !doEpsFilter.isSelected([episode.shortNumber, episode.id + ''])) || (!but && doEpsFilter.isSelected([episode.shortNumber, episode.id + '']))) { + selEpsArr.push({ isSelected: true, ...episode }); + console.info('%s[S%sE%s] %s', '✓ ', episode.season, episode.shortNumber, episode.name); + } + } + return { isOk: true, value: selEpsArr }; + } + public async muxStreams(data: DownloadedMedia[], options: yargs.ArgvType) { + this.cfg.bin = await yamlCfg.loadBinCfg(); + let hasAudioStreams = false; + if (options.novids || data.filter((a) => a.type === 'Video').length === 0) return console.info('Skip muxing since no vids are downloaded'); + if (data.some((a) => a.type === 'Audio')) { + hasAudioStreams = true; + } + const merger = new Merger({ + onlyVid: hasAudioStreams + ? data + .filter((a) => a.type === 'Video') + .map((a): MergerInput => { + if (a.type === 'Subtitle') throw new Error('Never'); + return { + lang: a.lang, + path: a.path + }; + }) + : [], + skipSubMux: options.skipSubMux, + inverseTrackOrder: false, + keepAllVideos: options.keepAllVideos, + onlyAudio: hasAudioStreams + ? data + .filter((a) => a.type === 'Audio') + .map((a): MergerInput => { + if (a.type === 'Subtitle') throw new Error('Never'); + return { + lang: a.lang, + path: a.path + }; + }) + : [], + output: `${options.output}.${options.mp4 ? 'mp4' : 'mkv'}`, + subtitles: data + .filter((a) => a.type === 'Subtitle') + .map((a): SubtitleInput => { + if (a.type === 'Video') throw new Error('Never'); + if (a.type === 'Audio') throw new Error('Never'); + return { + file: a.path, + language: a.language, + closedCaption: a.cc + }; + }), + simul: data + .filter((a) => a.type === 'Video') + .map((a): boolean => { + if (a.type === 'Subtitle') throw new Error('Never'); + return !a.uncut as boolean; + })[0], + fonts: Merger.makeFontsList(this.cfg.dir.fonts, data.filter((a) => a.type === 'Subtitle') as sxItem[]), + videoAndAudio: hasAudioStreams + ? [] + : data + .filter((a) => a.type === 'Video') + .map((a): MergerInput => { + if (a.type === 'Subtitle') throw new Error('Never'); + return { + lang: a.lang, + path: a.path + }; + }), + chapters: data + .filter((a) => a.type === 'Chapters') + .map((a): MergerInput => { + return { + path: a.path, + lang: a.lang + }; + }), + videoTitle: options.videoTitle, + options: { + ffmpeg: options.ffmpegOptions, + mkvmerge: options.mkvmergeOptions + }, + defaults: { + audio: options.defaultAudio, + sub: options.defaultSub + }, + ccTag: options.ccTag + }); + const bin = Merger.checkMerger(this.cfg.bin, options.mp4, options.forceMuxer); + // collect fonts info + // mergers + let isMuxed = false; + if (options.syncTiming) { + await merger.createDelays(); + } + if (bin.MKVmerge) { + await merger.merge('mkvmerge', bin.MKVmerge); + isMuxed = true; + } else if (bin.FFmpeg) { + await merger.merge('ffmpeg', bin.FFmpeg); + isMuxed = true; + } else { + console.info('\nDone!\n'); + return; + } + if (isMuxed && !options.nocleanup) merger.cleanUp(); + } - public async getEpisode(data: ADNVideo, options: yargs.ArgvType) { - //TODO: Move all the requests for getting the m3u8 here - const res = await this.downloadEpisode(data, options); - if (res === undefined || res.error) { - console.error('Failed to download media list'); - return { isOk: false, reason: new Error('Failed to download media list') }; - } else { - if (!options.skipmux) { - await this.muxStreams(res.data, { ...options, output: res.fileName }); - } else { - console.info('Skipping mux'); - } - downloaded({ - service: 'adn', - type: 's' - }, data.id+'', [data.shortNumber]); - return { isOk: res, value: undefined }; - } - } + public async getEpisode(data: ADNVideo, options: yargs.ArgvType) { + //TODO: Move all the requests for getting the m3u8 here + const res = await this.downloadEpisode(data, options); + if (res === undefined || res.error) { + console.error('Failed to download media list'); + return { isOk: false, reason: new Error('Failed to download media list') }; + } else { + if (!options.skipmux) { + await this.muxStreams(res.data, { ...options, output: res.fileName }); + } else { + console.info('Skipping mux'); + } + downloaded( + { + service: 'adn', + type: 's' + }, + data.id + '', + [data.shortNumber] + ); + return { isOk: res, value: undefined }; + } + } - public async downloadEpisode(data: ADNVideo, options: yargs.ArgvType) { - if(!this.token.accessToken){ - console.error('Authentication required!'); - return; - } + public async downloadEpisode(data: ADNVideo, options: yargs.ArgvType) { + if (!this.token.accessToken) { + console.error('Authentication required!'); + return; + } - if (!this.cfg.bin.ffmpeg) - this.cfg.bin = await yamlCfg.loadBinCfg(); + if (!this.cfg.bin.ffmpeg) this.cfg.bin = await yamlCfg.loadBinCfg(); - let mediaName = '...'; - let fileName; - const variables: Variable[] = []; - if(data.show.title && data.shortNumber && data.title){ - mediaName = `${data.show.shortTitle ?? data.show.title} - ${data.shortNumber} - ${data.title}`; - } + let mediaName = '...'; + let fileName; + const variables: Variable[] = []; + if (data.show.title && data.shortNumber && data.title) { + mediaName = `${data.show.shortTitle ?? data.show.title} - ${data.shortNumber} - ${data.title}`; + } - const files: DownloadedMedia[] = []; + const files: DownloadedMedia[] = []; - let dlFailed = false; - let dlVideoOnce = false; // Variable to save if best selected video quality was downloaded + let dlFailed = false; + let dlVideoOnce = false; // Variable to save if best selected video quality was downloaded - const refreshToken = await this.refreshToken(); - if (!refreshToken.isOk) { - console.error('Failed to refresh token'); - return undefined; - } + const refreshToken = await this.refreshToken(); + if (!refreshToken.isOk) { + console.error('Failed to refresh token'); + return undefined; + } - const configReq = await this.req.getData(`https://gw.api.animationdigitalnetwork.com/player/video/${data.id}/configuration`, { - headers: { - Authorization: `Bearer ${this.token.accessToken}`, - 'X-Target-Distribution': this.locale - } - }); - if(!configReq.ok || !configReq.res){ - console.error('Player Config Request failed!'); - return undefined; - } - const configuration = await configReq.res.json() as ADNPlayerConfig; - if (!configuration.player.options.user.hasAccess) { - console.error('You don\'t have access to this video!'); - return undefined; - } - const tokenReq = await this.req.getData(configuration.player.options.user.refreshTokenUrl || 'https://gw.api.animationdigitalnetwork.com/player/refresh/token', { - method: 'POST', - headers: { - 'X-Player-Refresh-Token': `${configuration.player.options.user.refreshToken}` - } - }); - if(!tokenReq.ok || !tokenReq.res){ - console.error('Player Token Request failed!'); - return undefined; - } - const token = await tokenReq.res.json() as { - refreshToken: string, - accessToken: string, - token: string - }; + const configReq = await this.req.getData(`https://gw.api.animationdigitalnetwork.com/player/video/${data.id}/configuration`, { + headers: { + Authorization: `Bearer ${this.token.accessToken}`, + 'X-Target-Distribution': this.locale + } + }); + if (!configReq.ok || !configReq.res) { + console.error('Player Config Request failed!'); + return undefined; + } + const configuration = (await configReq.res.json()) as ADNPlayerConfig; + if (!configuration.player.options.user.hasAccess) { + console.error("You don't have access to this video!"); + return undefined; + } + const tokenReq = await this.req.getData(configuration.player.options.user.refreshTokenUrl || 'https://gw.api.animationdigitalnetwork.com/player/refresh/token', { + method: 'POST', + headers: { + 'X-Player-Refresh-Token': `${configuration.player.options.user.refreshToken}` + } + }); + if (!tokenReq.ok || !tokenReq.res) { + console.error('Player Token Request failed!'); + return undefined; + } + const token = (await tokenReq.res.json()) as { + refreshToken: string; + accessToken: string; + token: string; + }; - const linksUrl = configuration.player.options.video.url || `https://gw.api.animationdigitalnetwork.com/player/video/${data.id}/link`; - const key = this.generateRandomString(16); - const decryptionKey = key + '7fac1178830cfe0c'; + const linksUrl = configuration.player.options.video.url || `https://gw.api.animationdigitalnetwork.com/player/video/${data.id}/link`; + const key = this.generateRandomString(16); + const decryptionKey = key + '7fac1178830cfe0c'; - const authorization = crypto.publicEncrypt({ - 'key': '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCbQrCJBRmaXM4gJidDmcpWDssg\nnumHinCLHAgS4buMtdH7dEGGEUfBofLzoEdt1jqcrCDT6YNhM0aFCqbLOPFtx9cg\n/X2G/G5bPVu8cuFM0L+ehp8s6izK1kjx3OOPH/kWzvstM5tkqgJkNyNEvHdeJl6\nKhS+IFEqwvZqgbBpKuwIDAQAB\n-----END PUBLIC KEY-----', - padding: crypto.constants.RSA_PKCS1_PADDING - }, Buffer.from(JSON.stringify({ - k: key, - t: token.token - }), 'utf-8')).toString('base64'); + const authorization = crypto + .publicEncrypt( + { + key: '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCbQrCJBRmaXM4gJidDmcpWDssg\nnumHinCLHAgS4buMtdH7dEGGEUfBofLzoEdt1jqcrCDT6YNhM0aFCqbLOPFtx9cg\n/X2G/G5bPVu8cuFM0L+ehp8s6izK1kjx3OOPH/kWzvstM5tkqgJkNyNEvHdeJl6\nKhS+IFEqwvZqgbBpKuwIDAQAB\n-----END PUBLIC KEY-----', + padding: crypto.constants.RSA_PKCS1_PADDING + }, + Buffer.from( + JSON.stringify({ + k: key, + t: token.token + }), + 'utf-8' + ) + ) + .toString('base64'); - //TODO: Add chapter support - const streamsRequest = await this.req.getData(linksUrl+'?freeWithAds=true&adaptive=true&withMetadata=true&source=Web', { - 'headers': { - 'X-Player-Token': authorization, - 'X-Target-Distribution': this.locale - } - }); - if(!streamsRequest.ok || !streamsRequest.res){ - if (streamsRequest.error?.res!.status == 403 || streamsRequest.res?.status == 403) { - console.error('Georestricted!'); - } else { - console.error('Streams request failed!'); - } - return undefined; - } - const streams = await streamsRequest.res.json() as ADNStreams; - for (const streamName in streams.links.streaming) { - let audDub: langsData.LanguageItem; - if (this.jpnStrings.includes(streamName)) { - audDub = langsData.languages.find(a=>a.code == 'jpn') as langsData.LanguageItem; - } else if (this.deuStrings.includes(streamName)) { - audDub = langsData.languages.find(a=>a.code == 'deu') as langsData.LanguageItem; - } else if (this.fraStrings.includes(streamName)) { - audDub = langsData.languages.find(a=>a.code == 'fra') as langsData.LanguageItem; - } else { - console.error(`Language ${streamName} not recognized, please report this.`); - continue; - } + //TODO: Add chapter support + const streamsRequest = await this.req.getData(linksUrl + '?freeWithAds=true&adaptive=true&withMetadata=true&source=Web', { + headers: { + 'X-Player-Token': authorization, + 'X-Target-Distribution': this.locale + } + }); + if (!streamsRequest.ok || !streamsRequest.res) { + if (streamsRequest.error?.res!.status == 403 || streamsRequest.res?.status == 403) { + console.error('Georestricted!'); + } else { + console.error('Streams request failed!'); + } + return undefined; + } + const streams = (await streamsRequest.res.json()) as ADNStreams; + for (const streamName in streams.links.streaming) { + let audDub: langsData.LanguageItem; + if (this.jpnStrings.includes(streamName)) { + audDub = langsData.languages.find((a) => a.code == 'jpn') as langsData.LanguageItem; + } else if (this.deuStrings.includes(streamName)) { + audDub = langsData.languages.find((a) => a.code == 'deu') as langsData.LanguageItem; + } else if (this.fraStrings.includes(streamName)) { + audDub = langsData.languages.find((a) => a.code == 'fra') as langsData.LanguageItem; + } else { + console.error(`Language ${streamName} not recognized, please report this.`); + continue; + } - if (!options.dubLang.includes(audDub.code)) { - continue; - } + if (!options.dubLang.includes(audDub.code)) { + continue; + } - console.info(`Requesting: [${data.id}] ${mediaName} (${audDub.name})`); + console.info(`Requesting: [${data.id}] ${mediaName} (${audDub.name})`); - variables.push(...([ - ['title', data.title, true], - ['episode', isNaN(parseFloat(data.shortNumber)) ? data.shortNumber : parseFloat(data.shortNumber), false], - ['service', 'ADN', false], - ['seriesTitle', data.show.shortTitle ?? data.show.title, true], - ['showTitle', data.show.title, true], - ['season', isNaN(parseFloat(data.season)) ? data.season : parseFloat(data.season), false] - ] as [AvailableFilenameVars, string|number, boolean][]).map((a): Variable => { - return { - name: a[0], - replaceWith: a[1], - type: typeof a[1], - sanitize: a[2] - } as Variable; - })); + variables.push( + ...( + [ + ['title', data.title, true], + ['episode', isNaN(parseFloat(data.shortNumber)) ? data.shortNumber : parseFloat(data.shortNumber), false], + ['service', 'ADN', false], + ['seriesTitle', data.show.shortTitle ?? data.show.title, true], + ['showTitle', data.show.title, true], + ['season', isNaN(parseFloat(data.season)) ? data.season : parseFloat(data.season), false] + ] as [AvailableFilenameVars, string | number, boolean][] + ).map((a): Variable => { + return { + name: a[0], + replaceWith: a[1], + type: typeof a[1], + sanitize: a[2] + } as Variable; + }) + ); - console.info('Playlists URL: %s', streams.links.streaming[streamName].auto); + console.info('Playlists URL: %s', streams.links.streaming[streamName].auto); - let tsFile = undefined; + let tsFile = undefined; - if (!dlFailed && !options.novids) { - const streamPlaylistsLocationReq = await this.req.getData(streams.links.streaming[streamName].auto); - if (!streamPlaylistsLocationReq.ok || !streamPlaylistsLocationReq.res) { - console.error('CAN\'T FETCH VIDEO PLAYLIST LOCATION!'); - return undefined; - } - const streamPlaylistLocation = await streamPlaylistsLocationReq.res.json() as {'location': string}; - const streamPlaylistsReq = await this.req.getData(streamPlaylistLocation.location); - if (!streamPlaylistsReq.ok || !streamPlaylistsReq.res) { - console.error('CAN\'T FETCH VIDEO PLAYLISTS!'); - dlFailed = true; - } else { - const streamPlaylistBody = await streamPlaylistsReq.res.text(); - const streamPlaylists = m3u8(streamPlaylistBody); - const plServerList: string[] = [], - plStreams: Record> = {}, - plQuality: { - str: string, - dim: string, - CODECS: string, - RESOLUTION: { - width: number, - height: number - } - }[] = []; - for(const pl of streamPlaylists.playlists){ - // set quality - const plResolution = pl.attributes.RESOLUTION; - const plResolutionText = `${plResolution.width}x${plResolution.height}`; - // set codecs - const plCodecs = pl.attributes.CODECS; - // parse uri - const plUri = new URL(pl.uri); - let plServer = plUri.hostname; - // set server list - if (plUri.searchParams.get('cdn')){ - plServer += ` (${plUri.searchParams.get('cdn')})`; - } - if (!plServerList.includes(plServer)){ - plServerList.push(plServer); - } - // add to server - if (!Object.keys(plStreams).includes(plServer)){ - plStreams[plServer] = {}; - } - if( - plStreams[plServer][plResolutionText] - && plStreams[plServer][plResolutionText] != pl.uri - && typeof plStreams[plServer][plResolutionText] != 'undefined' - ) { - console.error(`Non duplicate url for ${plServer} detected, please report to developer!`); - } else{ - plStreams[plServer][plResolutionText] = pl.uri; - } - // set plQualityStr - const plBandwidth = Math.round(pl.attributes.BANDWIDTH/1024); - const qualityStrAdd = `${plResolutionText} (${plBandwidth}KiB/s)`; - const qualityStrRegx = new RegExp(qualityStrAdd.replace(/([:()/])/g, '\\$1'), 'm'); - const qualityStrMatch = !plQuality.map(a => a.str).join('\r\n').match(qualityStrRegx); - if(qualityStrMatch){ - plQuality.push({ - str: qualityStrAdd, - dim: plResolutionText, - CODECS: plCodecs, - RESOLUTION: plResolution - }); - } - } + if (!dlFailed && !options.novids) { + const streamPlaylistsLocationReq = await this.req.getData(streams.links.streaming[streamName].auto); + if (!streamPlaylistsLocationReq.ok || !streamPlaylistsLocationReq.res) { + console.error("CAN'T FETCH VIDEO PLAYLIST LOCATION!"); + return undefined; + } + const streamPlaylistLocation = (await streamPlaylistsLocationReq.res.json()) as { location: string }; + const streamPlaylistsReq = await this.req.getData(streamPlaylistLocation.location); + if (!streamPlaylistsReq.ok || !streamPlaylistsReq.res) { + console.error("CAN'T FETCH VIDEO PLAYLISTS!"); + dlFailed = true; + } else { + const streamPlaylistBody = await streamPlaylistsReq.res.text(); + const streamPlaylists = m3u8(streamPlaylistBody); + const plServerList: string[] = [], + plStreams: Record> = {}, + plQuality: { + str: string; + dim: string; + CODECS: string; + RESOLUTION: { + width: number; + height: number; + }; + }[] = []; + for (const pl of streamPlaylists.playlists) { + // set quality + const plResolution = pl.attributes.RESOLUTION; + const plResolutionText = `${plResolution.width}x${plResolution.height}`; + // set codecs + const plCodecs = pl.attributes.CODECS; + // parse uri + const plUri = new URL(pl.uri); + let plServer = plUri.hostname; + // set server list + if (plUri.searchParams.get('cdn')) { + plServer += ` (${plUri.searchParams.get('cdn')})`; + } + if (!plServerList.includes(plServer)) { + plServerList.push(plServer); + } + // add to server + if (!Object.keys(plStreams).includes(plServer)) { + plStreams[plServer] = {}; + } + if ( + plStreams[plServer][plResolutionText] && + plStreams[plServer][plResolutionText] != pl.uri && + typeof plStreams[plServer][plResolutionText] != 'undefined' + ) { + console.error(`Non duplicate url for ${plServer} detected, please report to developer!`); + } else { + plStreams[plServer][plResolutionText] = pl.uri; + } + // set plQualityStr + const plBandwidth = Math.round(pl.attributes.BANDWIDTH / 1024); + const qualityStrAdd = `${plResolutionText} (${plBandwidth}KiB/s)`; + const qualityStrRegx = new RegExp(qualityStrAdd.replace(/([:()/])/g, '\\$1'), 'm'); + const qualityStrMatch = !plQuality + .map((a) => a.str) + .join('\r\n') + .match(qualityStrRegx); + if (qualityStrMatch) { + plQuality.push({ + str: qualityStrAdd, + dim: plResolutionText, + CODECS: plCodecs, + RESOLUTION: plResolution + }); + } + } - options.x = options.x > plServerList.length ? 1 : options.x; + options.x = options.x > plServerList.length ? 1 : options.x; - const plSelectedServer = plServerList[options.x - 1]; - const plSelectedList = plStreams[plSelectedServer]; - plQuality.sort((a, b) => { - const aMatch: RegExpMatchArray | never[] = a.dim.match(/[0-9]+/) || []; - const bMatch: RegExpMatchArray | never[] = b.dim.match(/[0-9]+/) || []; - return parseInt(aMatch[0]) - parseInt(bMatch[0]); - }); - let quality = options.q === 0 ? plQuality.length : options.q; - if(quality > plQuality.length) { - console.warn(`The requested quality of ${options.q} is greater than the maximum ${plQuality.length}.\n[WARN] Therefor the maximum will be capped at ${plQuality.length}.`); - quality = plQuality.length; - } - // When best selected video quality is already downloaded - if(dlVideoOnce && options.dlVideoOnce) { - // Select the lowest resolution with the same codecs - while(quality !=1 && plQuality[quality - 1].CODECS == plQuality[quality - 2].CODECS) { - quality--; - } - } - const selPlUrl = plSelectedList[plQuality.map(a => a.dim)[quality - 1]] ? plSelectedList[plQuality.map(a => a.dim)[quality - 1]] : ''; - console.info(`Servers available:\n\t${plServerList.join('\n\t')}`); - console.info(`Available qualities:\n\t${plQuality.map((a, ind) => `[${ind+1}] ${a.str}`).join('\n\t')}`); + const plSelectedServer = plServerList[options.x - 1]; + const plSelectedList = plStreams[plSelectedServer]; + plQuality.sort((a, b) => { + const aMatch: RegExpMatchArray | never[] = a.dim.match(/[0-9]+/) || []; + const bMatch: RegExpMatchArray | never[] = b.dim.match(/[0-9]+/) || []; + return parseInt(aMatch[0]) - parseInt(bMatch[0]); + }); + let quality = options.q === 0 ? plQuality.length : options.q; + if (quality > plQuality.length) { + console.warn( + `The requested quality of ${options.q} is greater than the maximum ${plQuality.length}.\n[WARN] Therefor the maximum will be capped at ${plQuality.length}.` + ); + quality = plQuality.length; + } + // When best selected video quality is already downloaded + if (dlVideoOnce && options.dlVideoOnce) { + // Select the lowest resolution with the same codecs + while (quality != 1 && plQuality[quality - 1].CODECS == plQuality[quality - 2].CODECS) { + quality--; + } + } + const selPlUrl = plSelectedList[plQuality.map((a) => a.dim)[quality - 1]] ? plSelectedList[plQuality.map((a) => a.dim)[quality - 1]] : ''; + console.info(`Servers available:\n\t${plServerList.join('\n\t')}`); + console.info(`Available qualities:\n\t${plQuality.map((a, ind) => `[${ind + 1}] ${a.str}`).join('\n\t')}`); - if(selPlUrl != ''){ - variables.push({ - name: 'height', - type: 'number', - replaceWith: quality === 0 ? plQuality[plQuality.length - 1].RESOLUTION.height as number : plQuality[quality - 1].RESOLUTION.height - }, { - name: 'width', - type: 'number', - replaceWith: quality === 0 ? plQuality[plQuality.length - 1].RESOLUTION.width as number : plQuality[quality - 1].RESOLUTION.width - }); + if (selPlUrl != '') { + variables.push( + { + name: 'height', + type: 'number', + replaceWith: quality === 0 ? (plQuality[plQuality.length - 1].RESOLUTION.height as number) : plQuality[quality - 1].RESOLUTION.height + }, + { + name: 'width', + type: 'number', + replaceWith: quality === 0 ? (plQuality[plQuality.length - 1].RESOLUTION.width as number) : plQuality[quality - 1].RESOLUTION.width + } + ); - console.info(`Selected quality: ${Object.keys(plSelectedList).find(a => plSelectedList[a] === selPlUrl)} @ ${plSelectedServer}`); - console.info('Stream URL:', selPlUrl); - // TODO check filename - fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); - const outFile = parseFileName(options.fileName + '.' + audDub.name, variables, options.numbers, options.override).join(path.sep); - console.info(`Output filename: ${outFile}`); - const chunkPage = await this.req.getData(selPlUrl); - if(!chunkPage.ok || !chunkPage.res){ - console.error('CAN\'T FETCH VIDEO PLAYLIST!'); - dlFailed = true; - } else { - const chunkPageBody = await chunkPage.res.text(); - const chunkPlaylist = m3u8(chunkPageBody); - const totalParts = chunkPlaylist.segments.length; - const mathParts = Math.ceil(totalParts / options.partsize); - const mathMsg = `(${mathParts}*${options.partsize})`; - console.info('Total parts in stream:', totalParts, mathMsg); - tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); - const dirName = path.dirname(tsFile); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }); - } - const dlStreamByPl = await new streamdl({ - output: `${tsFile}.ts`, - timeout: options.timeout, - m3u8json: chunkPlaylist, - baseurl: selPlUrl.replace('playlist.m3u8',''), - threads: options.partsize, - fsRetryTime: options.fsRetryTime * 1000, - override: options.force, - callback: options.callbackMaker ? options.callbackMaker({ - fileName: `${path.isAbsolute(outFile) ? outFile.slice(this.cfg.dir.content.length) : outFile}`, - image: data.image, - parent: { - title: data.show.title - }, - title: data.title, - language: audDub - }) : undefined - }).download(); - if (!dlStreamByPl.ok) { - console.error(`DL Stats: ${JSON.stringify(dlStreamByPl.parts)}\n`); - dlFailed = true; - } - files.push({ - type: 'Video', - path: `${tsFile}.ts`, - lang: audDub - }); - dlVideoOnce = true; - } - } else{ - console.error('Quality not selected!\n'); - dlFailed = true; - } - } - } else if (options.novids) { - console.info('Downloading skipped!'); - fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); - } - await this.sleep(options.waittime); - } + console.info(`Selected quality: ${Object.keys(plSelectedList).find((a) => plSelectedList[a] === selPlUrl)} @ ${plSelectedServer}`); + console.info('Stream URL:', selPlUrl); + // TODO check filename + fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); + const outFile = parseFileName(options.fileName + '.' + audDub.name, variables, options.numbers, options.override).join(path.sep); + console.info(`Output filename: ${outFile}`); + const chunkPage = await this.req.getData(selPlUrl); + if (!chunkPage.ok || !chunkPage.res) { + console.error("CAN'T FETCH VIDEO PLAYLIST!"); + dlFailed = true; + } else { + const chunkPageBody = await chunkPage.res.text(); + const chunkPlaylist = m3u8(chunkPageBody); + const totalParts = chunkPlaylist.segments.length; + const mathParts = Math.ceil(totalParts / options.partsize); + const mathMsg = `(${mathParts}*${options.partsize})`; + console.info('Total parts in stream:', totalParts, mathMsg); + tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); + const dirName = path.dirname(tsFile); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + const dlStreamByPl = await new streamdl({ + output: `${tsFile}.ts`, + timeout: options.timeout, + m3u8json: chunkPlaylist, + baseurl: selPlUrl.replace('playlist.m3u8', ''), + threads: options.partsize, + fsRetryTime: options.fsRetryTime * 1000, + override: options.force, + callback: options.callbackMaker + ? options.callbackMaker({ + fileName: `${path.isAbsolute(outFile) ? outFile.slice(this.cfg.dir.content.length) : outFile}`, + image: data.image, + parent: { + title: data.show.title + }, + title: data.title, + language: audDub + }) + : undefined + }).download(); + if (!dlStreamByPl.ok) { + console.error(`DL Stats: ${JSON.stringify(dlStreamByPl.parts)}\n`); + dlFailed = true; + } + files.push({ + type: 'Video', + path: `${tsFile}.ts`, + lang: audDub + }); + dlVideoOnce = true; + } + } else { + console.error('Quality not selected!\n'); + dlFailed = true; + } + } + } else if (options.novids) { + console.info('Downloading skipped!'); + fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); + } + await this.sleep(options.waittime); + } - const compiledChapters: string[] = []; - if (options.chapters) { - if (streams.video.tcIntroStart) { - if (streams.video.tcIntroStart != '00:00:00') { - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=00:00:00.00`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=Prologue` - ); - } - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=${streams.video.tcIntroStart+'.00'}`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=Opening` - ); - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=${streams.video.tcIntroEnd+'.00'}`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=Episode` - ); - } else { - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=00:00:00.00`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=Episode` - ); - } + const compiledChapters: string[] = []; + if (options.chapters) { + if (streams.video.tcIntroStart) { + if (streams.video.tcIntroStart != '00:00:00') { + compiledChapters.push(`CHAPTER${compiledChapters.length / 2 + 1}=00:00:00.00`, `CHAPTER${compiledChapters.length / 2 + 1}NAME=Prologue`); + } + compiledChapters.push(`CHAPTER${compiledChapters.length / 2 + 1}=${streams.video.tcIntroStart + '.00'}`, `CHAPTER${compiledChapters.length / 2 + 1}NAME=Opening`); + compiledChapters.push(`CHAPTER${compiledChapters.length / 2 + 1}=${streams.video.tcIntroEnd + '.00'}`, `CHAPTER${compiledChapters.length / 2 + 1}NAME=Episode`); + } else { + compiledChapters.push(`CHAPTER${compiledChapters.length / 2 + 1}=00:00:00.00`, `CHAPTER${compiledChapters.length / 2 + 1}NAME=Episode`); + } - if (streams.video.tcEndingStart) { - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=${streams.video.tcEndingStart+'.00'}`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=Ending Start` - ); - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=${streams.video.tcEndingEnd+'.00'}`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=Ending End` - ); - } + if (streams.video.tcEndingStart) { + compiledChapters.push( + `CHAPTER${compiledChapters.length / 2 + 1}=${streams.video.tcEndingStart + '.00'}`, + `CHAPTER${compiledChapters.length / 2 + 1}NAME=Ending Start` + ); + compiledChapters.push(`CHAPTER${compiledChapters.length / 2 + 1}=${streams.video.tcEndingEnd + '.00'}`, `CHAPTER${compiledChapters.length / 2 + 1}NAME=Ending End`); + } - if (compiledChapters.length > 0) { - try { - fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); - const outFile = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); - const tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); - const dirName = path.dirname(tsFile); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }); - } - fs.writeFileSync(`${tsFile}.txt`, compiledChapters.join('\r\n')); - files.push({ - path: `${tsFile}.txt`, - lang: langsData.languages.find(a=>a.code=='jpn'), - type: 'Chapters' - }); - } catch { - console.error('Failed to write chapter file'); - } - } - } + if (compiledChapters.length > 0) { + try { + fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); + const outFile = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); + const tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); + const dirName = path.dirname(tsFile); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + fs.writeFileSync(`${tsFile}.txt`, compiledChapters.join('\r\n')); + files.push({ + path: `${tsFile}.txt`, + lang: langsData.languages.find((a) => a.code == 'jpn'), + type: 'Chapters' + }); + } catch { + console.error('Failed to write chapter file'); + } + } + } - if(options.dlsubs.indexOf('all') > -1){ - options.dlsubs = ['all']; - } + if (options.dlsubs.indexOf('all') > -1) { + options.dlsubs = ['all']; + } - if (options.nosubs) { - console.info('Subtitles downloading disabled from nosubs flag.'); - options.skipsubs = true; - } + if (options.nosubs) { + console.info('Subtitles downloading disabled from nosubs flag.'); + options.skipsubs = true; + } - if(!options.skipsubs && options.dlsubs.indexOf('none') == -1) { - if (Object.keys(streams.links.subtitles).length !== 0) { - const subtitlesUrlReq = await this.req.getData(streams.links.subtitles.all); - if(!subtitlesUrlReq.ok || !subtitlesUrlReq.res){ - console.error('Subtitle location request failed!'); - return undefined; - } - const subtitleUrl = await subtitlesUrlReq.res.json() as {'location': string}; - const encryptedSubtitlesReq = await this.req.getData(subtitleUrl.location); - if(!encryptedSubtitlesReq.ok || !encryptedSubtitlesReq.res){ - console.error('Subtitle request failed!'); - return undefined; - } - const encryptedSubtitles = await encryptedSubtitlesReq.res.text(); - const iv = Buffer.from(encryptedSubtitles.slice(0, 24), 'base64'); - const derivedKey = Buffer.from(decryptionKey, 'hex'); - const encryptedData = Buffer.from(encryptedSubtitles.slice(24), 'base64'); - const decipher = crypto.createDecipheriv('aes-128-cbc', derivedKey, iv); - const decryptedData = Buffer.concat([decipher.update(encryptedData), decipher.final()]).toString('utf8'); - - let subIndex = 0; - const subtitles = JSON.parse(decryptedData) as ADNSubtitles; - if (Object.keys(subtitles).length === 0) { - console.warn('No subtitles found.'); - } - for (const subName in subtitles) { - let subLang: langsData.LanguageItem; - if (this.deuSubStrings.includes(subName)) { - subLang = langsData.languages.find(a=>a.code == 'deu') as langsData.LanguageItem; - } else if (this.fraSubStrings.includes(subName)) { - subLang = langsData.languages.find(a=>a.code == 'fra') as langsData.LanguageItem; - } else { - console.error(`Language ${subName} not recognized, please report this.`); - continue; - } + if (!options.skipsubs && options.dlsubs.indexOf('none') == -1) { + if (Object.keys(streams.links.subtitles).length !== 0) { + const subtitlesUrlReq = await this.req.getData(streams.links.subtitles.all); + if (!subtitlesUrlReq.ok || !subtitlesUrlReq.res) { + console.error('Subtitle location request failed!'); + return undefined; + } + const subtitleUrl = (await subtitlesUrlReq.res.json()) as { location: string }; + const encryptedSubtitlesReq = await this.req.getData(subtitleUrl.location); + if (!encryptedSubtitlesReq.ok || !encryptedSubtitlesReq.res) { + console.error('Subtitle request failed!'); + return undefined; + } + const encryptedSubtitles = await encryptedSubtitlesReq.res.text(); + const iv = Buffer.from(encryptedSubtitles.slice(0, 24), 'base64'); + const derivedKey = Buffer.from(decryptionKey, 'hex'); + const encryptedData = Buffer.from(encryptedSubtitles.slice(24), 'base64'); + const decipher = crypto.createDecipheriv('aes-128-cbc', derivedKey, iv); + const decryptedData = Buffer.concat([decipher.update(encryptedData), decipher.final()]).toString('utf8'); - if (!options.dlsubs.includes(subLang.locale) && !options.dlsubs.includes('all')) { - continue; - } - - const sxData: Partial = {}; - sxData.file = langsData.subsFile(fileName as string, subIndex+'', subLang, false, options.ccTag); - if (path.isAbsolute(sxData.file)) { - sxData.path = sxData.file; - } else { - sxData.path = path.join(this.cfg.dir.content, sxData.file); - } - const dirName = path.dirname(sxData.path); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }); - } - sxData.language = subLang; - if(options.dlsubs.includes('all') || options.dlsubs.includes(subLang.locale)) { - let subBody = '[Script Info]' - + '\nScriptType:V4.00+' - + '\nWrapStyle: 0' - + '\nPlayResX: 1280' - + '\nPlayResY: 720' - + '\nScaledBorderAndShadow: yes' - + '' - + '\n[V4+ Styles]' - + '\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding' - + `\nStyle: Default,${options.fontName ?? 'Arial'},${options.fontSize ?? 50},&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,-1,0,0,0,100,100,0,0,1,1.95,0,2,0,0,70,0` - + '\n[Events]' - + '\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text'; + let subIndex = 0; + const subtitles = JSON.parse(decryptedData) as ADNSubtitles; + if (Object.keys(subtitles).length === 0) { + console.warn('No subtitles found.'); + } + for (const subName in subtitles) { + let subLang: langsData.LanguageItem; + if (this.deuSubStrings.includes(subName)) { + subLang = langsData.languages.find((a) => a.code == 'deu') as langsData.LanguageItem; + } else if (this.fraSubStrings.includes(subName)) { + subLang = langsData.languages.find((a) => a.code == 'fra') as langsData.LanguageItem; + } else { + console.error(`Language ${subName} not recognized, please report this.`); + continue; + } - for (const sub of subtitles[subName]) { - const [start, end, text, lineAlign, positionAlign] = - [sub.startTime, sub.endTime, sub.text, sub.lineAlign, sub.positionAlign]; - for (const subProp in sub) { - switch (subProp) { - case 'startTime': - case 'endTime': - case 'text': - case 'lineAlign': - case 'positionAlign': - break; - default: - console.warn(`json2ass: Unknown style: ${subProp}`); - } - } - const alignment = (this.posAlignMap[positionAlign] || 2) + (this.lineAlignMap[lineAlign] || 0); - const xtext = text - .replace(/ \\N$/g, '\\N') - .replace(/\\N$/, '') - .replace(/\r/g, '') - .replace(/\n/g, '\\N') - .replace(/\\N +/g, '\\N') - .replace(/ +\\N/g, '\\N') - .replace(/(\\N)+/g, '\\N') - .replace(/]*>([^<]*)<\/b>/g, '{\\b1}$1{\\b0}') - .replace(/]*>([^<]*)<\/i>/g, '{\\i1}$1{\\i0}') - .replace(/]*>([^<]*)<\/u>/g, '{\\u1}$1{\\u0}') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/&/g, '&') - .replace(/<[^>]>/g, '') - .replace(/\\N$/, '') - .replace(/ +$/, ''); - subBody += `\nDialogue: 0,${this.convertToSSATimestamp(start)},${this.convertToSSATimestamp(end)},Default,,0,0,0,,${(alignment !== 2 ? `{\\a${alignment}}` : '')}${xtext}`; - } - sxData.title = `${subLang.language}`; - sxData.fonts = fontsData.assFonts(subBody) as Font[]; - fs.writeFileSync(sxData.path, subBody); - console.info(`Subtitle converted: ${sxData.file}`); - files.push({ - type: 'Subtitle', - ...sxData as sxItem, - cc: false - }); - } - subIndex++; - } - } else { - console.warn('Couldn\'t find subtitles.'); - } - } else{ - console.info('Subtitles downloading skipped!'); - } + if (!options.dlsubs.includes(subLang.locale) && !options.dlsubs.includes('all')) { + continue; + } - return { - error: dlFailed, - data: files, - fileName: fileName ? (path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName)) || './unknown' : './unknown' - }; - } + const sxData: Partial = {}; + sxData.file = langsData.subsFile(fileName as string, subIndex + '', subLang, false, options.ccTag); + if (path.isAbsolute(sxData.file)) { + sxData.path = sxData.file; + } else { + sxData.path = path.join(this.cfg.dir.content, sxData.file); + } + const dirName = path.dirname(sxData.path); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + sxData.language = subLang; + if (options.dlsubs.includes('all') || options.dlsubs.includes(subLang.locale)) { + let subBody = + '[Script Info]' + + '\nScriptType:V4.00+' + + '\nWrapStyle: 0' + + '\nPlayResX: 1280' + + '\nPlayResY: 720' + + '\nScaledBorderAndShadow: yes' + + '' + + '\n[V4+ Styles]' + + '\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding' + + `\nStyle: Default,${options.fontName ?? 'Arial'},${options.fontSize ?? 50},&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,-1,0,0,0,100,100,0,0,1,1.95,0,2,0,0,70,0` + + '\n[Events]' + + '\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text'; - public sleep(ms: number) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); - } -} \ No newline at end of file + for (const sub of subtitles[subName]) { + const [start, end, text, lineAlign, positionAlign] = [sub.startTime, sub.endTime, sub.text, sub.lineAlign, sub.positionAlign]; + for (const subProp in sub) { + switch (subProp) { + case 'startTime': + case 'endTime': + case 'text': + case 'lineAlign': + case 'positionAlign': + break; + default: + console.warn(`json2ass: Unknown style: ${subProp}`); + } + } + const alignment = (this.posAlignMap[positionAlign] || 2) + (this.lineAlignMap[lineAlign] || 0); + const xtext = text + .replace(/ \\N$/g, '\\N') + .replace(/\\N$/, '') + .replace(/\r/g, '') + .replace(/\n/g, '\\N') + .replace(/\\N +/g, '\\N') + .replace(/ +\\N/g, '\\N') + .replace(/(\\N)+/g, '\\N') + .replace(/]*>([^<]*)<\/b>/g, '{\\b1}$1{\\b0}') + .replace(/]*>([^<]*)<\/i>/g, '{\\i1}$1{\\i0}') + .replace(/]*>([^<]*)<\/u>/g, '{\\u1}$1{\\u0}') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&') + .replace(/<[^>]>/g, '') + .replace(/\\N$/, '') + .replace(/ +$/, ''); + subBody += `\nDialogue: 0,${this.convertToSSATimestamp(start)},${this.convertToSSATimestamp(end)},Default,,0,0,0,,${alignment !== 2 ? `{\\a${alignment}}` : ''}${xtext}`; + } + sxData.title = `${subLang.language}`; + sxData.fonts = fontsData.assFonts(subBody) as Font[]; + fs.writeFileSync(sxData.path, subBody); + console.info(`Subtitle converted: ${sxData.file}`); + files.push({ + type: 'Subtitle', + ...(sxData as sxItem), + cc: false + }); + } + subIndex++; + } + } else { + console.warn("Couldn't find subtitles."); + } + } else { + console.info('Subtitles downloading skipped!'); + } + + return { + error: dlFailed, + data: files, + fileName: fileName ? (path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName)) || './unknown' : './unknown' + }; + } + + public sleep(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + } +} diff --git a/ao.ts b/ao.ts index 1095335..151086e 100644 --- a/ao.ts +++ b/ao.ts @@ -34,815 +34,875 @@ import { AnimeOnegaiStream } from './@types/animeOnegaiStream'; import { sxItem } from './crunchy'; type parsedMultiDubDownload = { - data: { - lang: string, - videoId: string - episode: Episode - }[], - seriesTitle: string, - seasonTitle: string, - episodeTitle: string, - episodeNumber: number, - seasonNumber: number, - seriesID: number, - seasonID: number, - image: string, -} + data: { + lang: string; + videoId: string; + episode: Episode; + }[]; + seriesTitle: string; + seasonTitle: string; + episodeTitle: string; + episodeNumber: number; + seasonNumber: number; + seriesID: number; + seasonID: number; + image: string; +}; -export default class AnimeOnegai implements ServiceClass { - public cfg: yamlCfg.ConfigObject; - private token: Record; - private req: reqModule.Req; - public locale: string; - public jpnStrings: string[] = [ - 'japonés con subtítulos en español', - 'japonés con subtítulos en portugués', - 'japonês com legendas em espanhol', - 'japonês com legendas em português', - 'japonés' - ]; - public spaStrings: string[] = [ - 'doblaje en español', - 'dublagem em espanhol', - 'español', - ]; - public porStrings: string[] = [ - 'doblaje en portugués', - 'dublagem em português' - ]; - private defaultOptions: RequestInit = { - 'headers': { - 'origin': 'https://www.animeonegai.com', - 'referer': 'https://www.animeonegai.com/', - } - }; +export default class AnimeOnegai implements ServiceClass { + public cfg: yamlCfg.ConfigObject; + private token: Record; + private req: reqModule.Req; + public locale: string; + public jpnStrings: string[] = [ + 'japonés con subtítulos en español', + 'japonés con subtítulos en portugués', + 'japonês com legendas em espanhol', + 'japonês com legendas em português', + 'japonés' + ]; + public spaStrings: string[] = ['doblaje en español', 'dublagem em espanhol', 'español']; + public porStrings: string[] = ['doblaje en portugués', 'dublagem em português']; + private defaultOptions: RequestInit = { + headers: { + origin: 'https://www.animeonegai.com', + referer: 'https://www.animeonegai.com/' + } + }; - constructor(private debug = false) { - this.cfg = yamlCfg.loadCfg(); - this.token = yamlCfg.loadAOToken(); - this.req = new reqModule.Req(domain, debug, false, 'ao'); - this.locale = 'es'; - } + constructor(private debug = false) { + this.cfg = yamlCfg.loadCfg(); + this.token = yamlCfg.loadAOToken(); + this.req = new reqModule.Req(domain, debug, false, 'ao'); + this.locale = 'es'; + } - public async cli() { - console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`); - const argv = yargs.appArgv(this.cfg.cli); - if (['pt', 'es'].includes(argv.locale)) - this.locale = argv.locale; - if (argv.debug) - this.debug = true; + public async cli() { + console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`); + const argv = yargs.appArgv(this.cfg.cli); + if (['pt', 'es'].includes(argv.locale)) this.locale = argv.locale; + if (argv.debug) this.debug = true; - // load binaries - this.cfg.bin = await yamlCfg.loadBinCfg(); - if (argv.allDubs) { - argv.dubLang = langsData.dubLanguageCodes; - } - if (argv.auth) { - //Authenticate - await this.doAuth({ - username: argv.username ?? await Helper.question('[Q] LOGIN/EMAIL: '), - password: argv.password ?? await Helper.question('[Q] PASSWORD: ') - }); - } else if (argv.search && argv.search.length > 2) { - //Search - await this.doSearch({ ...argv, search: argv.search as string }); - } else if (argv.s && !isNaN(parseInt(argv.s,10)) && parseInt(argv.s,10) > 0) { - const selected = await this.selectShow(parseInt(argv.s), argv.e, argv.but, argv.all, argv); - if (selected.isOk) { - for (const select of selected.value) { - if (!(await this.downloadEpisode(select, {...argv, skipsubs: false}))) { - console.error(`Unable to download selected episode ${select.episodeNumber}`); - return false; - } - } - } - return true; - } else if (argv.token) { - this.token = {token: argv.token}; - yamlCfg.saveAOToken(this.token); - console.info('Saved token'); - } else { - console.info('No option selected or invalid value entered. Try --help.'); - } - } + // load binaries + this.cfg.bin = await yamlCfg.loadBinCfg(); + if (argv.allDubs) { + argv.dubLang = langsData.dubLanguageCodes; + } + if (argv.auth) { + //Authenticate + await this.doAuth({ + username: argv.username ?? (await Helper.question('[Q] LOGIN/EMAIL: ')), + password: argv.password ?? (await Helper.question('[Q] PASSWORD: ')) + }); + } else if (argv.search && argv.search.length > 2) { + //Search + await this.doSearch({ ...argv, search: argv.search as string }); + } else if (argv.s && !isNaN(parseInt(argv.s, 10)) && parseInt(argv.s, 10) > 0) { + const selected = await this.selectShow(parseInt(argv.s), argv.e, argv.but, argv.all, argv); + if (selected.isOk) { + for (const select of selected.value) { + if (!(await this.downloadEpisode(select, { ...argv, skipsubs: false }))) { + console.error(`Unable to download selected episode ${select.episodeNumber}`); + return false; + } + } + } + return true; + } else if (argv.token) { + this.token = { token: argv.token }; + yamlCfg.saveAOToken(this.token); + console.info('Saved token'); + } else { + console.info('No option selected or invalid value entered. Try --help.'); + } + } - public async doSearch(data: SearchData): Promise { - const searchReq = await this.req.getData(`https://api.animeonegai.com/v1/search/algolia/${encodeURIComponent(data.search)}?lang=${this.locale}`, this.defaultOptions); - if (!searchReq.ok || !searchReq.res) { - console.error('Search FAILED!'); - return { isOk: false, reason: new Error('Search failed. No more information provided') }; - } - const searchData = await searchReq.res.json() as AnimeOnegaiSearch; - const searchItems: AOSearchResult[] = []; - console.info('Search Results:'); - for (const hit of searchData.list) { - searchItems.push(hit); - let fullType: string; - if (hit.asset_type == 2) { - fullType = `S.${hit.ID}`; - } else if (hit.asset_type == 1) { - fullType = `E.${hit.ID}`; - } else { - fullType = 'Unknown'; - console.warn(`Unknown asset type ${hit.asset_type}, please report this.`); - } - console.log(`[${fullType}] ${hit.title}`); - } - return { isOk: true, value: searchItems.filter(a => a.asset_type == 2).flatMap((a): SearchResponseItem => { - return { - id: a.ID+'', - image: a.poster ?? '/notFound.png', - name: a.title, - rating: a.likes, - desc: a.description - }; - })}; - } + public async doSearch(data: SearchData): Promise { + const searchReq = await this.req.getData(`https://api.animeonegai.com/v1/search/algolia/${encodeURIComponent(data.search)}?lang=${this.locale}`, this.defaultOptions); + if (!searchReq.ok || !searchReq.res) { + console.error('Search FAILED!'); + return { isOk: false, reason: new Error('Search failed. No more information provided') }; + } + const searchData = (await searchReq.res.json()) as AnimeOnegaiSearch; + const searchItems: AOSearchResult[] = []; + console.info('Search Results:'); + for (const hit of searchData.list) { + searchItems.push(hit); + let fullType: string; + if (hit.asset_type == 2) { + fullType = `S.${hit.ID}`; + } else if (hit.asset_type == 1) { + fullType = `E.${hit.ID}`; + } else { + fullType = 'Unknown'; + console.warn(`Unknown asset type ${hit.asset_type}, please report this.`); + } + console.log(`[${fullType}] ${hit.title}`); + } + return { + isOk: true, + value: searchItems + .filter((a) => a.asset_type == 2) + .flatMap((a): SearchResponseItem => { + return { + id: a.ID + '', + image: a.poster ?? '/notFound.png', + name: a.title, + rating: a.likes, + desc: a.description + }; + }) + }; + } - public async doAuth(data: AuthData): Promise { - data; - console.error('Authentication not possible, manual authentication required due to recaptcha. In order to login use the --token flag. You can get the token by logging into the website, and opening the dev console and running the command "localStorage.ott_token"'); - return { isOk: false, reason: new Error('Authentication not possible, manual authentication required do to recaptcha.') }; - } + public async doAuth(data: AuthData): Promise { + data; + console.error( + 'Authentication not possible, manual authentication required due to recaptcha. In order to login use the --token flag. You can get the token by logging into the website, and opening the dev console and running the command "localStorage.ott_token"' + ); + return { isOk: false, reason: new Error('Authentication not possible, manual authentication required do to recaptcha.') }; + } - public async getShow(id: number) { - const getSeriesData = await this.req.getData(`https://api.animeonegai.com/v1/asset/${id}?lang=${this.locale}`, this.defaultOptions); - if (!getSeriesData.ok || !getSeriesData.res) { - console.error('Failed to get Show Data'); - return { isOk: false }; - } - const seriesData = await getSeriesData.res.json() as AnimeOnegaiSeries; + public async getShow(id: number) { + const getSeriesData = await this.req.getData(`https://api.animeonegai.com/v1/asset/${id}?lang=${this.locale}`, this.defaultOptions); + if (!getSeriesData.ok || !getSeriesData.res) { + console.error('Failed to get Show Data'); + return { isOk: false }; + } + const seriesData = (await getSeriesData.res.json()) as AnimeOnegaiSeries; - const getSeasonData = await this.req.getData(`https://api.animeonegai.com/v1/asset/content/${id}?lang=${this.locale}`, this.defaultOptions); - if (!getSeasonData.ok || !getSeasonData.res) { - console.error('Failed to get Show Data'); - return { isOk: false }; - } - const seasonData = await getSeasonData.res.json() as AnimeOnegaiSeasons[]; + const getSeasonData = await this.req.getData(`https://api.animeonegai.com/v1/asset/content/${id}?lang=${this.locale}`, this.defaultOptions); + if (!getSeasonData.ok || !getSeasonData.res) { + console.error('Failed to get Show Data'); + return { isOk: false }; + } + const seasonData = (await getSeasonData.res.json()) as AnimeOnegaiSeasons[]; - return { isOk: true, data: seriesData, seasons: seasonData }; - } + return { isOk: true, data: seriesData, seasons: seasonData }; + } - public async listShow(id: number, outputEpisode: boolean = true) { - const series = await this.getShow(id); - if (!series.isOk || !series.data) { - console.error('Failed to list series data: Failed to get series'); - return { isOk: false }; - } - console.info(`[S.${series.data.ID}] ${series.data.title} (${series.seasons.length} Seasons)`); - if (series.seasons.length === 0 && series.data.asset_type !== 1) { - console.info(' No Seasons found!'); - return { isOk: false }; - } - const episodes: { [key: string]: (Episode & { lang?: string })[] } = {}; - for (const season of series.seasons) { - let lang: string | undefined = undefined; - if (this.jpnStrings.includes(season.name.trim().toLowerCase())) lang = 'ja'; - else if (this.porStrings.includes(season.name.trim().toLowerCase())) lang = 'pt'; - else if (this.spaStrings.includes(season.name.trim().toLowerCase())) lang = 'es'; - else {lang = 'unknown';console.error(`Language (${season.name.trim()}) not known, please report this!`);} - for (const episode of season.list) { - if (!episodes[episode.number]) { - episodes[episode.number] = []; - } - /*if (!episodes[episode.number].find(a=>a.lang == lang))*/ episodes[episode.number].push({...episode, lang}); - } - } - //Item is movie, lets define it manually - if (series.data.asset_type === 1 && series.seasons.length === 0) { - let lang: string | undefined; - if (this.jpnStrings.some(str => series.data.title.includes(str.toLowerCase()))) lang = 'ja'; - else if (this.porStrings.some(str => series.data.title.includes(str.toLowerCase()))) lang = 'pt'; - else if (this.spaStrings.some(str => series.data.title.includes(str.toLowerCase()))) lang = 'es'; - else {lang = 'unknown';console.error('Language could not be parsed from movie title, please report this!');} - episodes[1] = [{ - 'video_entry': series.data.video_entry, - 'number': 1, - 'season_id': 1, - 'name': series.data.title, - 'ID': series.data.ID, - 'CreatedAt': series.data.CreatedAt, - 'DeletedAt': series.data.DeletedAt, - 'UpdatedAt': series.data.UpdatedAt, - 'active': series.data.active, - 'description': series.data.description, - 'age_restriction': series.data.age_restriction, - 'asset_id': series.data.ID, - 'ending': null, - 'entry': series.data.entry, - 'stream_url': series.data.stream_url, - 'skip_intro': null, - 'thumbnail': series.data.bg, - 'open_free': false, - lang - }]; // as unknown as (Episode & { lang?: string })[]; - // The above needs to be uncommented if the episode number should be M1 instead of 1 - } - //Enable to output episodes seperate from selection - if (outputEpisode) { - for (const episodeKey in episodes) { - const episode = episodes[episodeKey][0]; - const langs = Array.from(new Set(episodes[episodeKey].map(a=>a.lang))); - console.info(` [E.${episode.ID}] E${episode.number} - ${episode.name} (${langs.map(a=>{ - if (a) return langsData.languages.find(b=>b.ao_locale === a)?.name; - return 'Unknown'; - }).join(', ')})`); - } - } - return { isOk: true, value: episodes, series: series }; - } + public async listShow(id: number, outputEpisode: boolean = true) { + const series = await this.getShow(id); + if (!series.isOk || !series.data) { + console.error('Failed to list series data: Failed to get series'); + return { isOk: false }; + } + console.info(`[S.${series.data.ID}] ${series.data.title} (${series.seasons.length} Seasons)`); + if (series.seasons.length === 0 && series.data.asset_type !== 1) { + console.info(' No Seasons found!'); + return { isOk: false }; + } + const episodes: { [key: string]: (Episode & { lang?: string })[] } = {}; + for (const season of series.seasons) { + let lang: string | undefined = undefined; + if (this.jpnStrings.includes(season.name.trim().toLowerCase())) lang = 'ja'; + else if (this.porStrings.includes(season.name.trim().toLowerCase())) lang = 'pt'; + else if (this.spaStrings.includes(season.name.trim().toLowerCase())) lang = 'es'; + else { + lang = 'unknown'; + console.error(`Language (${season.name.trim()}) not known, please report this!`); + } + for (const episode of season.list) { + if (!episodes[episode.number]) { + episodes[episode.number] = []; + } + /*if (!episodes[episode.number].find(a=>a.lang == lang))*/ episodes[episode.number].push({ ...episode, lang }); + } + } + //Item is movie, lets define it manually + if (series.data.asset_type === 1 && series.seasons.length === 0) { + let lang: string | undefined; + if (this.jpnStrings.some((str) => series.data.title.includes(str.toLowerCase()))) lang = 'ja'; + else if (this.porStrings.some((str) => series.data.title.includes(str.toLowerCase()))) lang = 'pt'; + else if (this.spaStrings.some((str) => series.data.title.includes(str.toLowerCase()))) lang = 'es'; + else { + lang = 'unknown'; + console.error('Language could not be parsed from movie title, please report this!'); + } + episodes[1] = [ + { + video_entry: series.data.video_entry, + number: 1, + season_id: 1, + name: series.data.title, + ID: series.data.ID, + CreatedAt: series.data.CreatedAt, + DeletedAt: series.data.DeletedAt, + UpdatedAt: series.data.UpdatedAt, + active: series.data.active, + description: series.data.description, + age_restriction: series.data.age_restriction, + asset_id: series.data.ID, + ending: null, + entry: series.data.entry, + stream_url: series.data.stream_url, + skip_intro: null, + thumbnail: series.data.bg, + open_free: false, + lang + } + ]; // as unknown as (Episode & { lang?: string })[]; + // The above needs to be uncommented if the episode number should be M1 instead of 1 + } + //Enable to output episodes seperate from selection + if (outputEpisode) { + for (const episodeKey in episodes) { + const episode = episodes[episodeKey][0]; + const langs = Array.from(new Set(episodes[episodeKey].map((a) => a.lang))); + console.info( + ` [E.${episode.ID}] E${episode.number} - ${episode.name} (${langs + .map((a) => { + if (a) return langsData.languages.find((b) => b.ao_locale === a)?.name; + return 'Unknown'; + }) + .join(', ')})` + ); + } + } + return { isOk: true, value: episodes, series: series }; + } - public async selectShow(id: number, e: string | undefined, but: boolean, all: boolean, options: yargs.ArgvType) { - const getShowData = await this.listShow(id, false); - if (!getShowData.isOk || !getShowData.value) { - return { isOk: false, value: [] }; - } - //const showData = getShowData.value; - const doEpsFilter = parseSelect(e as string); - // build selected episodes - const selEpsArr: parsedMultiDubDownload[] = []; - const episodes = getShowData.value; - const seasonNumberTitleParse = getShowData.series.data.title.match(/\d+/); - const seasonNumber = seasonNumberTitleParse ? parseInt(seasonNumberTitleParse[0]) : 1; - for (const episodeKey in getShowData.value) { - const episode = episodes[episodeKey][0]; - const selectedLangs: string[] = []; - const selected: { - lang: string, - videoId: string - episode: Episode - }[] = []; - for (const episode of episodes[episodeKey]) { - const lang = langsData.languages.find(a=>a.ao_locale === episode.lang); - let isSelected = false; - if (typeof selected.find(a=>a.lang == episode.lang) == 'undefined') { - if (options.dubLang.includes(lang?.code ?? 'Unknown')) { - if ((but && !doEpsFilter.isSelected([episode.number+'', episode.ID+''])) || all || (!but && doEpsFilter.isSelected([episode.number+'', episode.ID+'']))) { - isSelected = true; - selected.push({lang: episode.lang as string, videoId: episode.video_entry, episode: episode }); - } - } - const selectedLang = isSelected ? `✓ ${lang?.name ?? 'Unknown'}` : `${lang?.name ?? 'Unknown'}`; - if (!selectedLangs.includes(selectedLang)) { - selectedLangs.push(selectedLang); - } - } - } - if (selected.length > 0) { - selEpsArr.push({ - 'data': selected, - 'seasonNumber': seasonNumber, - 'episodeNumber': episode.number, - 'episodeTitle': episode.name, - 'image': episode.thumbnail, - 'seasonID': episode.season_id, - 'seasonTitle': getShowData.series.data.title, - 'seriesTitle': getShowData.series.data.title, - 'seriesID': getShowData.series.data.ID - }); - } - console.info(` [S${seasonNumber}E${episode.number}] - ${episode.name} (${selectedLangs.join(', ')})`); - } - return { isOk: true, value: selEpsArr, showData: getShowData.series }; - } + public async selectShow(id: number, e: string | undefined, but: boolean, all: boolean, options: yargs.ArgvType) { + const getShowData = await this.listShow(id, false); + if (!getShowData.isOk || !getShowData.value) { + return { isOk: false, value: [] }; + } + //const showData = getShowData.value; + const doEpsFilter = parseSelect(e as string); + // build selected episodes + const selEpsArr: parsedMultiDubDownload[] = []; + const episodes = getShowData.value; + const seasonNumberTitleParse = getShowData.series.data.title.match(/\d+/); + const seasonNumber = seasonNumberTitleParse ? parseInt(seasonNumberTitleParse[0]) : 1; + for (const episodeKey in getShowData.value) { + const episode = episodes[episodeKey][0]; + const selectedLangs: string[] = []; + const selected: { + lang: string; + videoId: string; + episode: Episode; + }[] = []; + for (const episode of episodes[episodeKey]) { + const lang = langsData.languages.find((a) => a.ao_locale === episode.lang); + let isSelected = false; + if (typeof selected.find((a) => a.lang == episode.lang) == 'undefined') { + if (options.dubLang.includes(lang?.code ?? 'Unknown')) { + if ( + (but && !doEpsFilter.isSelected([episode.number + '', episode.ID + ''])) || + all || + (!but && doEpsFilter.isSelected([episode.number + '', episode.ID + ''])) + ) { + isSelected = true; + selected.push({ lang: episode.lang as string, videoId: episode.video_entry, episode: episode }); + } + } + const selectedLang = isSelected ? `✓ ${lang?.name ?? 'Unknown'}` : `${lang?.name ?? 'Unknown'}`; + if (!selectedLangs.includes(selectedLang)) { + selectedLangs.push(selectedLang); + } + } + } + if (selected.length > 0) { + selEpsArr.push({ + data: selected, + seasonNumber: seasonNumber, + episodeNumber: episode.number, + episodeTitle: episode.name, + image: episode.thumbnail, + seasonID: episode.season_id, + seasonTitle: getShowData.series.data.title, + seriesTitle: getShowData.series.data.title, + seriesID: getShowData.series.data.ID + }); + } + console.info(` [S${seasonNumber}E${episode.number}] - ${episode.name} (${selectedLangs.join(', ')})`); + } + return { isOk: true, value: selEpsArr, showData: getShowData.series }; + } - public async downloadEpisode(data: parsedMultiDubDownload, options: yargs.ArgvType): Promise { - const res = await this.downloadMediaList(data, options); - if (res === undefined || res.error) { - return false; - } else { - if (!options.skipmux) { - await this.muxStreams(res.data, { ...options, output: res.fileName }); - } else { - console.info('Skipping mux'); - } - downloaded({ - service: 'ao', - type: 's' - }, data.seasonID+'', [data.episodeNumber+'']); - } - return true; - } + public async downloadEpisode(data: parsedMultiDubDownload, options: yargs.ArgvType): Promise { + const res = await this.downloadMediaList(data, options); + if (res === undefined || res.error) { + return false; + } else { + if (!options.skipmux) { + await this.muxStreams(res.data, { ...options, output: res.fileName }); + } else { + console.info('Skipping mux'); + } + downloaded( + { + service: 'ao', + type: 's' + }, + data.seasonID + '', + [data.episodeNumber + ''] + ); + } + return true; + } - public async muxStreams(data: DownloadedMedia[], options: yargs.ArgvType) { - this.cfg.bin = await yamlCfg.loadBinCfg(); - let hasAudioStreams = false; - if (options.novids || data.filter(a => a.type === 'Video').length === 0) - return console.info('Skip muxing since no vids are downloaded'); - if (data.some(a => a.type === 'Audio')) { - hasAudioStreams = true; - } - const merger = new Merger({ - onlyVid: hasAudioStreams ? data.filter(a => a.type === 'Video').map((a) : MergerInput => { - if (a.type === 'Subtitle') - throw new Error('Never'); - return { - lang: a.lang, - path: a.path, - }; - }) : [], - skipSubMux: options.skipSubMux, - inverseTrackOrder: false, - keepAllVideos: options.keepAllVideos, - onlyAudio: hasAudioStreams ? data.filter(a => a.type === 'Audio').map((a) : MergerInput => { - if (a.type === 'Subtitle') - throw new Error('Never'); - return { - lang: a.lang, - path: a.path, - }; - }) : [], - output: `${options.output}.${options.mp4 ? 'mp4' : 'mkv'}`, - subtitles: data.filter(a => a.type === 'Subtitle').map((a) : SubtitleInput => { - if (a.type === 'Video') - throw new Error('Never'); - if (a.type === 'Audio') - throw new Error('Never'); - return { - file: a.path, - language: a.language, - closedCaption: a.cc - }; - }), - simul: data.filter(a => a.type === 'Video').map((a) : boolean => { - if (a.type === 'Subtitle') - throw new Error('Never'); - return !a.uncut as boolean; - })[0], - fonts: Merger.makeFontsList(this.cfg.dir.fonts, data.filter(a => a.type === 'Subtitle') as sxItem[]), - videoAndAudio: hasAudioStreams ? [] : data.filter(a => a.type === 'Video').map((a) : MergerInput => { - if (a.type === 'Subtitle') - throw new Error('Never'); - return { - lang: a.lang, - path: a.path, - }; - }), - videoTitle: options.videoTitle, - options: { - ffmpeg: options.ffmpegOptions, - mkvmerge: options.mkvmergeOptions - }, - defaults: { - audio: options.defaultAudio, - sub: options.defaultSub - }, - ccTag: options.ccTag - }); - const bin = Merger.checkMerger(this.cfg.bin, options.mp4, options.forceMuxer); - // collect fonts info - // mergers - let isMuxed = false; - if (options.syncTiming) { - await merger.createDelays(); - } - if (bin.MKVmerge) { - await merger.merge('mkvmerge', bin.MKVmerge); - isMuxed = true; - } else if (bin.FFmpeg) { - await merger.merge('ffmpeg', bin.FFmpeg); - isMuxed = true; - } else{ - console.info('\nDone!\n'); - return; - } - if (isMuxed && !options.nocleanup) - merger.cleanUp(); - } + public async muxStreams(data: DownloadedMedia[], options: yargs.ArgvType) { + this.cfg.bin = await yamlCfg.loadBinCfg(); + let hasAudioStreams = false; + if (options.novids || data.filter((a) => a.type === 'Video').length === 0) return console.info('Skip muxing since no vids are downloaded'); + if (data.some((a) => a.type === 'Audio')) { + hasAudioStreams = true; + } + const merger = new Merger({ + onlyVid: hasAudioStreams + ? data + .filter((a) => a.type === 'Video') + .map((a): MergerInput => { + if (a.type === 'Subtitle') throw new Error('Never'); + return { + lang: a.lang, + path: a.path + }; + }) + : [], + skipSubMux: options.skipSubMux, + inverseTrackOrder: false, + keepAllVideos: options.keepAllVideos, + onlyAudio: hasAudioStreams + ? data + .filter((a) => a.type === 'Audio') + .map((a): MergerInput => { + if (a.type === 'Subtitle') throw new Error('Never'); + return { + lang: a.lang, + path: a.path + }; + }) + : [], + output: `${options.output}.${options.mp4 ? 'mp4' : 'mkv'}`, + subtitles: data + .filter((a) => a.type === 'Subtitle') + .map((a): SubtitleInput => { + if (a.type === 'Video') throw new Error('Never'); + if (a.type === 'Audio') throw new Error('Never'); + return { + file: a.path, + language: a.language, + closedCaption: a.cc + }; + }), + simul: data + .filter((a) => a.type === 'Video') + .map((a): boolean => { + if (a.type === 'Subtitle') throw new Error('Never'); + return !a.uncut as boolean; + })[0], + fonts: Merger.makeFontsList(this.cfg.dir.fonts, data.filter((a) => a.type === 'Subtitle') as sxItem[]), + videoAndAudio: hasAudioStreams + ? [] + : data + .filter((a) => a.type === 'Video') + .map((a): MergerInput => { + if (a.type === 'Subtitle') throw new Error('Never'); + return { + lang: a.lang, + path: a.path + }; + }), + videoTitle: options.videoTitle, + options: { + ffmpeg: options.ffmpegOptions, + mkvmerge: options.mkvmergeOptions + }, + defaults: { + audio: options.defaultAudio, + sub: options.defaultSub + }, + ccTag: options.ccTag + }); + const bin = Merger.checkMerger(this.cfg.bin, options.mp4, options.forceMuxer); + // collect fonts info + // mergers + let isMuxed = false; + if (options.syncTiming) { + await merger.createDelays(); + } + if (bin.MKVmerge) { + await merger.merge('mkvmerge', bin.MKVmerge); + isMuxed = true; + } else if (bin.FFmpeg) { + await merger.merge('ffmpeg', bin.FFmpeg); + isMuxed = true; + } else { + console.info('\nDone!\n'); + return; + } + if (isMuxed && !options.nocleanup) merger.cleanUp(); + } - public async downloadMediaList(medias: parsedMultiDubDownload, options: yargs.ArgvType) : Promise<{ - data: DownloadedMedia[], - fileName: string, - error: boolean - } | undefined> { - if(!this.token.token){ - console.error('Authentication required!'); - return; - } + public async downloadMediaList( + medias: parsedMultiDubDownload, + options: yargs.ArgvType + ): Promise< + | { + data: DownloadedMedia[]; + fileName: string; + error: boolean; + } + | undefined + > { + if (!this.token.token) { + console.error('Authentication required!'); + return; + } - if (!this.cfg.bin.ffmpeg) - this.cfg.bin = await yamlCfg.loadBinCfg(); + if (!this.cfg.bin.ffmpeg) this.cfg.bin = await yamlCfg.loadBinCfg(); - let mediaName = '...'; - let fileName; - const variables: Variable[] = []; - if(medias.seasonTitle && medias.episodeNumber && medias.episodeTitle){ - mediaName = `${medias.seasonTitle} - ${medias.episodeNumber} - ${medias.episodeTitle}`; - } + let mediaName = '...'; + let fileName; + const variables: Variable[] = []; + if (medias.seasonTitle && medias.episodeNumber && medias.episodeTitle) { + mediaName = `${medias.seasonTitle} - ${medias.episodeNumber} - ${medias.episodeTitle}`; + } - const files: DownloadedMedia[] = []; + const files: DownloadedMedia[] = []; - let subIndex = 0; - let dlFailed = false; - let dlVideoOnce = false; // Variable to save if best selected video quality was downloaded + let subIndex = 0; + let dlFailed = false; + let dlVideoOnce = false; // Variable to save if best selected video quality was downloaded - for (const media of medias.data) { - console.info(`Requesting: [E.${media.episode.ID}] ${mediaName}`); + for (const media of medias.data) { + console.info(`Requesting: [E.${media.episode.ID}] ${mediaName}`); - const AuthHeaders = { - headers: { - Authorization: `Bearer ${this.token.token}`, - 'Referer': 'https://www.animeonegai.com/', - 'Origin': 'https://www.animeonegai.com', - 'Referrer-Policy': 'strict-origin-when-cross-origin', - 'Content-Type': 'application/json' - } - }; + const AuthHeaders = { + headers: { + Authorization: `Bearer ${this.token.token}`, + Referer: 'https://www.animeonegai.com/', + Origin: 'https://www.animeonegai.com', + 'Referrer-Policy': 'strict-origin-when-cross-origin', + 'Content-Type': 'application/json' + } + }; - const playbackReq = await this.req.getData(`https://api.animeonegai.com/v1/media/${media.videoId}?lang=${this.locale}`, AuthHeaders); - if(!playbackReq.ok || !playbackReq.res){ - console.error('Request Stream URLs FAILED!'); - return undefined; - } - const streamData = await playbackReq.res.json() as AnimeOnegaiStream; + const playbackReq = await this.req.getData(`https://api.animeonegai.com/v1/media/${media.videoId}?lang=${this.locale}`, AuthHeaders); + if (!playbackReq.ok || !playbackReq.res) { + console.error('Request Stream URLs FAILED!'); + return undefined; + } + const streamData = (await playbackReq.res.json()) as AnimeOnegaiStream; - variables.push(...([ - ['title', medias.episodeTitle, true], - ['episode', medias.episodeNumber, false], - ['service', 'AO', false], - ['seriesTitle', medias.seriesTitle, true], - ['showTitle', medias.seasonTitle, true], - ['season', medias.seasonNumber, false] - ] as [AvailableFilenameVars, string|number, boolean][]).map((a): Variable => { - return { - name: a[0], - replaceWith: a[1], - type: typeof a[1], - sanitize: a[2] - } as Variable; - })); + variables.push( + ...( + [ + ['title', medias.episodeTitle, true], + ['episode', medias.episodeNumber, false], + ['service', 'AO', false], + ['seriesTitle', medias.seriesTitle, true], + ['showTitle', medias.seasonTitle, true], + ['season', medias.seasonNumber, false] + ] as [AvailableFilenameVars, string | number, boolean][] + ).map((a): Variable => { + return { + name: a[0], + replaceWith: a[1], + type: typeof a[1], + sanitize: a[2] + } as Variable; + }) + ); - if (!canDecrypt) { - console.error('No valid Widevine or PlayReady CDM detected. Please ensure a supported and functional CDM is installed.'); - return undefined; - } - - if (!this.cfg.bin.mp4decrypt && !this.cfg.bin.shaka) { - console.error('Neither Shaka nor MP4Decrypt found. Please ensure at least one of them is installed.'); - return undefined; - } + if (!canDecrypt) { + console.error('No valid Widevine or PlayReady CDM detected. Please ensure a supported and functional CDM is installed.'); + return undefined; + } - const lang = langsData.languages.find(a=>a.ao_locale == media.lang) as langsData.LanguageItem; - if (!lang) { - console.error(`Unable to find language for code ${media.lang}`); - return; - } - let tsFile = undefined; + if (!this.cfg.bin.mp4decrypt && !this.cfg.bin.shaka) { + console.error('Neither Shaka nor MP4Decrypt found. Please ensure at least one of them is installed.'); + return undefined; + } - if (!streamData.dash) { - console.error('You don\'t have access to download this content'); - continue; - } + const lang = langsData.languages.find((a) => a.ao_locale == media.lang) as langsData.LanguageItem; + if (!lang) { + console.error(`Unable to find language for code ${media.lang}`); + return; + } + let tsFile = undefined; - console.info('Playlists URL: %s', streamData.dash); + if (!streamData.dash) { + console.error("You don't have access to download this content"); + continue; + } - if(!dlFailed && !(options.novids && options.noaudio)){ - const streamPlaylistsReq = await this.req.getData(streamData.dash, AuthHeaders); - if(!streamPlaylistsReq.ok || !streamPlaylistsReq.res){ - console.error('CAN\'T FETCH VIDEO PLAYLISTS!'); - dlFailed = true; - } else { - const streamPlaylistBody = (await streamPlaylistsReq.res.text()).replace(/(.*?)<\/BaseURL>/g, `${streamData.dash.split('/dash/')[0]}/dash/$1`); - //Parse MPD Playlists - const streamPlaylists = await parse(streamPlaylistBody, lang as langsData.LanguageItem, streamData.dash.split('/dash/')[0]+'/dash/'); + console.info('Playlists URL: %s', streamData.dash); - //Get name of CDNs/Servers - const streamServers = Object.keys(streamPlaylists); + if (!dlFailed && !(options.novids && options.noaudio)) { + const streamPlaylistsReq = await this.req.getData(streamData.dash, AuthHeaders); + if (!streamPlaylistsReq.ok || !streamPlaylistsReq.res) { + console.error("CAN'T FETCH VIDEO PLAYLISTS!"); + dlFailed = true; + } else { + const streamPlaylistBody = (await streamPlaylistsReq.res.text()).replace( + /(.*?)<\/BaseURL>/g, + `${streamData.dash.split('/dash/')[0]}/dash/$1` + ); + //Parse MPD Playlists + const streamPlaylists = await parse(streamPlaylistBody, lang as langsData.LanguageItem, streamData.dash.split('/dash/')[0] + '/dash/'); - options.x = options.x > streamServers.length ? 1 : options.x; + //Get name of CDNs/Servers + const streamServers = Object.keys(streamPlaylists); - const selectedServer = streamServers[options.x - 1]; - const selectedList = streamPlaylists[selectedServer]; + options.x = options.x > streamServers.length ? 1 : options.x; - //set Video Qualities - const videos = selectedList.video.map(item => { - return { - ...item, - resolutionText: `${item.quality.width}x${item.quality.height} (${Math.round(item.bandwidth/1024)}KiB/s)` - }; - }); + const selectedServer = streamServers[options.x - 1]; + const selectedList = streamPlaylists[selectedServer]; - const audios = selectedList.audio.map(item => { - return { - ...item, - resolutionText: `${Math.round(item.bandwidth/1024)}kB/s` - }; - }); + //set Video Qualities + const videos = selectedList.video.map((item) => { + return { + ...item, + resolutionText: `${item.quality.width}x${item.quality.height} (${Math.round(item.bandwidth / 1024)}KiB/s)` + }; + }); - videos.sort((a, b) => { - return a.quality.width - b.quality.width; - }); + const audios = selectedList.audio.map((item) => { + return { + ...item, + resolutionText: `${Math.round(item.bandwidth / 1024)}kB/s` + }; + }); - audios.sort((a, b) => { - return a.bandwidth - b.bandwidth; - }); + videos.sort((a, b) => { + return a.quality.width - b.quality.width; + }); - let chosenVideoQuality = options.q === 0 ? videos.length : options.q; - if(chosenVideoQuality > videos.length) { - console.warn(`The requested quality of ${options.q} is greater than the maximum ${videos.length}.\n[WARN] Therefor the maximum will be capped at ${videos.length}.`); - chosenVideoQuality = videos.length; - } - chosenVideoQuality--; + audios.sort((a, b) => { + return a.bandwidth - b.bandwidth; + }); - let chosenAudioQuality = options.q === 0 ? audios.length : options.q; - if(chosenAudioQuality > audios.length) { - chosenAudioQuality = audios.length; - } - chosenAudioQuality--; + let chosenVideoQuality = options.q === 0 ? videos.length : options.q; + if (chosenVideoQuality > videos.length) { + console.warn( + `The requested quality of ${options.q} is greater than the maximum ${videos.length}.\n[WARN] Therefor the maximum will be capped at ${videos.length}.` + ); + chosenVideoQuality = videos.length; + } + chosenVideoQuality--; - const chosenVideoSegments = videos[chosenVideoQuality]; - const chosenAudioSegments = audios[chosenAudioQuality]; + let chosenAudioQuality = options.q === 0 ? audios.length : options.q; + if (chosenAudioQuality > audios.length) { + chosenAudioQuality = audios.length; + } + chosenAudioQuality--; - console.info(`Servers available:\n\t${streamServers.join('\n\t')}`); - console.info(`Available Video Qualities:\n\t${videos.map((a, ind) => `[${ind+1}] ${a.resolutionText}`).join('\n\t')}`); - console.info(`Available Audio Qualities:\n\t${audios.map((a, ind) => `[${ind+1}] ${a.resolutionText}`).join('\n\t')}`); + const chosenVideoSegments = videos[chosenVideoQuality]; + const chosenAudioSegments = audios[chosenAudioQuality]; - variables.push({ - name: 'height', - type: 'number', - replaceWith: chosenVideoSegments.quality.height - }, { - name: 'width', - type: 'number', - replaceWith: chosenVideoSegments.quality.width - }); + console.info(`Servers available:\n\t${streamServers.join('\n\t')}`); + console.info(`Available Video Qualities:\n\t${videos.map((a, ind) => `[${ind + 1}] ${a.resolutionText}`).join('\n\t')}`); + console.info(`Available Audio Qualities:\n\t${audios.map((a, ind) => `[${ind + 1}] ${a.resolutionText}`).join('\n\t')}`); - console.info(`Selected quality: \n\tVideo: ${chosenVideoSegments.resolutionText}\n\tAudio: ${chosenAudioSegments.resolutionText}\n\tServer: ${selectedServer}`); - //console.info('Stream URL:', chosenVideoSegments.segments[0].uri); - // TODO check filename - fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); - const outFile = parseFileName(options.fileName + '.' + lang.name, variables, options.numbers, options.override).join(path.sep); - const tempFile = parseFileName(`temp-${media.videoId}`, variables, options.numbers, options.override).join(path.sep); - const tempTsFile = path.isAbsolute(tempFile as string) ? tempFile : path.join(this.cfg.dir.content, tempFile); + variables.push( + { + name: 'height', + type: 'number', + replaceWith: chosenVideoSegments.quality.height + }, + { + name: 'width', + type: 'number', + replaceWith: chosenVideoSegments.quality.width + } + ); - let [audioDownloaded, videoDownloaded] = [false, false]; + console.info(`Selected quality: \n\tVideo: ${chosenVideoSegments.resolutionText}\n\tAudio: ${chosenAudioSegments.resolutionText}\n\tServer: ${selectedServer}`); + //console.info('Stream URL:', chosenVideoSegments.segments[0].uri); + // TODO check filename + fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); + const outFile = parseFileName(options.fileName + '.' + lang.name, variables, options.numbers, options.override).join(path.sep); + const tempFile = parseFileName(`temp-${media.videoId}`, variables, options.numbers, options.override).join(path.sep); + const tempTsFile = path.isAbsolute(tempFile as string) ? tempFile : path.join(this.cfg.dir.content, tempFile); - // When best selected video quality is already downloaded - if(dlVideoOnce && options.dlVideoOnce) { - console.info('Already downloaded video, skipping video download...'); - } else if (options.novids) { - console.info('Skipping video download...'); - } else { - //Download Video - const totalParts = chosenVideoSegments.segments.length; - const mathParts = Math.ceil(totalParts / options.partsize); - const mathMsg = `(${mathParts}*${options.partsize})`; - console.info('Total parts in video stream:', totalParts, mathMsg); - tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); - const dirName = path.dirname(tsFile); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }); - } - const videoJson: M3U8Json = { - segments: chosenVideoSegments.segments - }; - try { - const videoDownload = await new streamdl({ - output: chosenVideoSegments.pssh_wvd ? `${tempTsFile}.video.enc.m4s` : `${tsFile}.video.m4s`, - timeout: options.timeout, - m3u8json: videoJson, - // baseurl: chunkPlaylist.baseUrl, - threads: options.partsize, - fsRetryTime: options.fsRetryTime * 1000, - override: options.force, - callback: options.callbackMaker ? options.callbackMaker({ - fileName: `${path.isAbsolute(outFile) ? outFile.slice(this.cfg.dir.content.length) : outFile}`, - image: medias.image, - parent: { - title: medias.seasonTitle - }, - title: medias.episodeTitle, - language: lang - }) : undefined - }).download(); - if(!videoDownload.ok){ - console.error(`DL Stats: ${JSON.stringify(videoDownload.parts)}\n`); - dlFailed = true; - } else { - dlVideoOnce = true; - videoDownloaded = true; - } - } catch (e) { - console.error(e); - dlFailed = true; - } - } + let [audioDownloaded, videoDownloaded] = [false, false]; - if (chosenAudioSegments && !options.noaudio) { - //Download Audio (if available) - const totalParts = chosenAudioSegments.segments.length; - const mathParts = Math.ceil(totalParts / options.partsize); - const mathMsg = `(${mathParts}*${options.partsize})`; - console.info('Total parts in audio stream:', totalParts, mathMsg); - tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); - const dirName = path.dirname(tsFile); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }); - } - const audioJson: M3U8Json = { - segments: chosenAudioSegments.segments - }; - try { - const audioDownload = await new streamdl({ - output: chosenAudioSegments.pssh_wvd ? `${tempTsFile}.audio.enc.m4s` : `${tsFile}.audio.m4s`, - timeout: options.timeout, - m3u8json: audioJson, - // baseurl: chunkPlaylist.baseUrl, - threads: options.partsize, - fsRetryTime: options.fsRetryTime * 1000, - override: options.force, - callback: options.callbackMaker ? options.callbackMaker({ - fileName: `${path.isAbsolute(outFile) ? outFile.slice(this.cfg.dir.content.length) : outFile}`, - image: medias.image, - parent: { - title: medias.seasonTitle - }, - title: medias.episodeTitle, - language: lang - }) : undefined - }).download(); - if(!audioDownload.ok){ - console.error(`DL Stats: ${JSON.stringify(audioDownload.parts)}\n`); - dlFailed = true; - } else { - audioDownloaded = true; - } - } catch (e) { - console.error(e); - dlFailed = true; - } - } else if (options.noaudio) { - console.info('Skipping audio download...'); - } + // When best selected video quality is already downloaded + if (dlVideoOnce && options.dlVideoOnce) { + console.info('Already downloaded video, skipping video download...'); + } else if (options.novids) { + console.info('Skipping video download...'); + } else { + //Download Video + const totalParts = chosenVideoSegments.segments.length; + const mathParts = Math.ceil(totalParts / options.partsize); + const mathMsg = `(${mathParts}*${options.partsize})`; + console.info('Total parts in video stream:', totalParts, mathMsg); + tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); + const dirName = path.dirname(tsFile); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + const videoJson: M3U8Json = { + segments: chosenVideoSegments.segments + }; + try { + const videoDownload = await new streamdl({ + output: chosenVideoSegments.pssh_wvd ? `${tempTsFile}.video.enc.m4s` : `${tsFile}.video.m4s`, + timeout: options.timeout, + m3u8json: videoJson, + // baseurl: chunkPlaylist.baseUrl, + threads: options.partsize, + fsRetryTime: options.fsRetryTime * 1000, + override: options.force, + callback: options.callbackMaker + ? options.callbackMaker({ + fileName: `${path.isAbsolute(outFile) ? outFile.slice(this.cfg.dir.content.length) : outFile}`, + image: medias.image, + parent: { + title: medias.seasonTitle + }, + title: medias.episodeTitle, + language: lang + }) + : undefined + }).download(); + if (!videoDownload.ok) { + console.error(`DL Stats: ${JSON.stringify(videoDownload.parts)}\n`); + dlFailed = true; + } else { + dlVideoOnce = true; + videoDownloaded = true; + } + } catch (e) { + console.error(e); + dlFailed = true; + } + } - //Handle Decryption if needed - if ((chosenVideoSegments.pssh_wvd || chosenAudioSegments.pssh_wvd) && (videoDownloaded || audioDownloaded)) { - console.info('Decryption Needed, attempting to decrypt'); - let encryptionKeys; + if (chosenAudioSegments && !options.noaudio) { + //Download Audio (if available) + const totalParts = chosenAudioSegments.segments.length; + const mathParts = Math.ceil(totalParts / options.partsize); + const mathMsg = `(${mathParts}*${options.partsize})`; + console.info('Total parts in audio stream:', totalParts, mathMsg); + tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); + const dirName = path.dirname(tsFile); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + const audioJson: M3U8Json = { + segments: chosenAudioSegments.segments + }; + try { + const audioDownload = await new streamdl({ + output: chosenAudioSegments.pssh_wvd ? `${tempTsFile}.audio.enc.m4s` : `${tsFile}.audio.m4s`, + timeout: options.timeout, + m3u8json: audioJson, + // baseurl: chunkPlaylist.baseUrl, + threads: options.partsize, + fsRetryTime: options.fsRetryTime * 1000, + override: options.force, + callback: options.callbackMaker + ? options.callbackMaker({ + fileName: `${path.isAbsolute(outFile) ? outFile.slice(this.cfg.dir.content.length) : outFile}`, + image: medias.image, + parent: { + title: medias.seasonTitle + }, + title: medias.episodeTitle, + language: lang + }) + : undefined + }).download(); + if (!audioDownload.ok) { + console.error(`DL Stats: ${JSON.stringify(audioDownload.parts)}\n`); + dlFailed = true; + } else { + audioDownloaded = true; + } + } catch (e) { + console.error(e); + dlFailed = true; + } + } else if (options.noaudio) { + console.info('Skipping audio download...'); + } - if (cdm === 'widevine') { - encryptionKeys = await getKeysWVD(chosenVideoSegments.pssh_wvd, streamData.widevine_proxy, {}); - } - if (cdm === 'playready') { - encryptionKeys = await getKeysPRD(chosenVideoSegments.pssh_prd, streamData.playready_proxy, {}); - } + //Handle Decryption if needed + if ((chosenVideoSegments.pssh_wvd || chosenAudioSegments.pssh_wvd) && (videoDownloaded || audioDownloaded)) { + console.info('Decryption Needed, attempting to decrypt'); + let encryptionKeys; - if (!encryptionKeys || encryptionKeys.length == 0) { - console.error('Failed to get encryption keys'); - return undefined; - } - /*const keys = {} as Record; + if (cdm === 'widevine') { + encryptionKeys = await getKeysWVD(chosenVideoSegments.pssh_wvd, streamData.widevine_proxy, {}); + } + if (cdm === 'playready') { + encryptionKeys = await getKeysPRD(chosenVideoSegments.pssh_prd, streamData.playready_proxy, {}); + } + + if (!encryptionKeys || encryptionKeys.length == 0) { + console.error('Failed to get encryption keys'); + return undefined; + } + /*const keys = {} as Record; encryptionKeys.forEach(function(key) { keys[key.kid] = key.key; });*/ - if (this.cfg.bin.mp4decrypt || this.cfg.bin.shaka) { - let commandBase = `--show-progress --key ${encryptionKeys[cdm === 'playready' ? 0 : 1].kid}:${encryptionKeys[cdm === 'playready' ? 0 : 1].key} `; - let commandVideo = commandBase+`"${tempTsFile}.video.enc.m4s" "${tempTsFile}.video.m4s"`; - let commandAudio = commandBase+`"${tempTsFile}.audio.enc.m4s" "${tempTsFile}.audio.m4s"`; + if (this.cfg.bin.mp4decrypt || this.cfg.bin.shaka) { + let commandBase = `--show-progress --key ${encryptionKeys[cdm === 'playready' ? 0 : 1].kid}:${encryptionKeys[cdm === 'playready' ? 0 : 1].key} `; + let commandVideo = commandBase + `"${tempTsFile}.video.enc.m4s" "${tempTsFile}.video.m4s"`; + let commandAudio = commandBase + `"${tempTsFile}.audio.enc.m4s" "${tempTsFile}.audio.m4s"`; - if (this.cfg.bin.shaka) { - commandBase = ` --enable_raw_key_decryption ${encryptionKeys.map(kb => '--keys key_id='+kb.kid+':key='+kb.key).join(' ')}`; - commandVideo = `input="${tempTsFile}.video.enc.m4s",stream=video,output="${tempTsFile}.video.m4s"`+commandBase; - commandAudio = `input="${tempTsFile}.audio.enc.m4s",stream=audio,output="${tempTsFile}.audio.m4s"`+commandBase; - } + if (this.cfg.bin.shaka) { + commandBase = ` --enable_raw_key_decryption ${encryptionKeys.map((kb) => '--keys key_id=' + kb.kid + ':key=' + kb.key).join(' ')}`; + commandVideo = `input="${tempTsFile}.video.enc.m4s",stream=video,output="${tempTsFile}.video.m4s"` + commandBase; + commandAudio = `input="${tempTsFile}.audio.enc.m4s",stream=audio,output="${tempTsFile}.audio.m4s"` + commandBase; + } - if (videoDownloaded) { - console.info('Started decrypting video,', this.cfg.bin.shaka ? 'using shaka' : 'using mp4decrypt'); - const decryptVideo = Helper.exec(this.cfg.bin.shaka ? 'shaka-packager' : 'mp4decrypt', this.cfg.bin.shaka ? `"${this.cfg.bin.shaka}"` : `"${this.cfg.bin.mp4decrypt}"`, commandVideo); - if (!decryptVideo.isOk) { - console.error(decryptVideo.err); - console.error(`Decryption failed with exit code ${decryptVideo.err.code}`); - fs.renameSync(`${tempTsFile}.video.enc.m4s`, `${tsFile}.video.enc.m4s`); - return undefined; - } else { - console.info('Decryption done for video'); - if (!options.nocleanup) { - fs.removeSync(`${tempTsFile}.video.enc.m4s`); - } - fs.copyFileSync(`${tempTsFile}.video.m4s`, `${tsFile}.video.m4s`); - fs.unlinkSync(`${tempTsFile}.video.m4s`); - files.push({ - type: 'Video', - path: `${tsFile}.video.m4s`, - lang: lang - }); - } - } + if (videoDownloaded) { + console.info('Started decrypting video,', this.cfg.bin.shaka ? 'using shaka' : 'using mp4decrypt'); + const decryptVideo = Helper.exec( + this.cfg.bin.shaka ? 'shaka-packager' : 'mp4decrypt', + this.cfg.bin.shaka ? `"${this.cfg.bin.shaka}"` : `"${this.cfg.bin.mp4decrypt}"`, + commandVideo + ); + if (!decryptVideo.isOk) { + console.error(decryptVideo.err); + console.error(`Decryption failed with exit code ${decryptVideo.err.code}`); + if (this.cfg.bin.shaka) { + console.error(`Downgrade to Shaka-Packager v2.6.1 (https://github.com/shaka-project/shaka-packager/releases/tag/v2.6.1) and try again`); + } + fs.renameSync(`${tempTsFile}.video.enc.m4s`, `${tsFile}.video.enc.m4s`); + return undefined; + } else { + console.info('Decryption done for video'); + if (!options.nocleanup) { + fs.removeSync(`${tempTsFile}.video.enc.m4s`); + } + fs.copyFileSync(`${tempTsFile}.video.m4s`, `${tsFile}.video.m4s`); + fs.unlinkSync(`${tempTsFile}.video.m4s`); + files.push({ + type: 'Video', + path: `${tsFile}.video.m4s`, + lang: lang + }); + } + } - if (audioDownloaded) { - console.info('Started decrypting audio,', this.cfg.bin.shaka ? 'using shaka' : 'using mp4decrypt'); - const decryptAudio = Helper.exec(this.cfg.bin.shaka ? 'shaka-packager' : 'mp4decrypt', this.cfg.bin.shaka ? `"${this.cfg.bin.shaka}"` : `"${this.cfg.bin.mp4decrypt}"`, commandAudio); - if (!decryptAudio.isOk) { - console.error(decryptAudio.err); - console.error(`Decryption failed with exit code ${decryptAudio.err.code}`); - fs.renameSync(`${tempTsFile}.audio.enc.m4s`, `${tsFile}.audio.enc.m4s`); - return undefined; - } else { - if (!options.nocleanup) { - fs.removeSync(`${tempTsFile}.audio.enc.m4s`); - } - fs.copyFileSync(`${tempTsFile}.audio.m4s`, `${tsFile}.audio.m4s`); - fs.unlinkSync(`${tempTsFile}.audio.m4s`); - files.push({ - type: 'Audio', - path: `${tsFile}.audio.m4s`, - lang: lang - }); - console.info('Decryption done for audio'); - } - } - } else { - console.warn('mp4decrypt/shaka not found, files need decryption. Decryption Keys:', encryptionKeys); - } - } else { - if (videoDownloaded) { - files.push({ - type: 'Video', - path: `${tsFile}.video.m4s`, - lang: lang - }); - } - if (audioDownloaded) { - files.push({ - type: 'Audio', - path: `${tsFile}.audio.m4s`, - lang: lang - }); - } - } - } - } else if (options.novids && options.noaudio) { - fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); - } + if (audioDownloaded) { + console.info('Started decrypting audio,', this.cfg.bin.shaka ? 'using shaka' : 'using mp4decrypt'); + const decryptAudio = Helper.exec( + this.cfg.bin.shaka ? 'shaka-packager' : 'mp4decrypt', + this.cfg.bin.shaka ? `"${this.cfg.bin.shaka}"` : `"${this.cfg.bin.mp4decrypt}"`, + commandAudio + ); + if (!decryptAudio.isOk) { + console.error(decryptAudio.err); + console.error(`Decryption failed with exit code ${decryptAudio.err.code}`); + if (this.cfg.bin.shaka) { + console.error(`Downgrade to Shaka-Packager v2.6.1 (https://github.com/shaka-project/shaka-packager/releases/tag/v2.6.1) and try again`); + } + fs.renameSync(`${tempTsFile}.audio.enc.m4s`, `${tsFile}.audio.enc.m4s`); + return undefined; + } else { + if (!options.nocleanup) { + fs.removeSync(`${tempTsFile}.audio.enc.m4s`); + } + fs.copyFileSync(`${tempTsFile}.audio.m4s`, `${tsFile}.audio.m4s`); + fs.unlinkSync(`${tempTsFile}.audio.m4s`); + files.push({ + type: 'Audio', + path: `${tsFile}.audio.m4s`, + lang: lang + }); + console.info('Decryption done for audio'); + } + } + } else { + console.warn('mp4decrypt/shaka not found, files need decryption. Decryption Keys:', encryptionKeys); + } + } else { + if (videoDownloaded) { + files.push({ + type: 'Video', + path: `${tsFile}.video.m4s`, + lang: lang + }); + } + if (audioDownloaded) { + files.push({ + type: 'Audio', + path: `${tsFile}.audio.m4s`, + lang: lang + }); + } + } + } + } else if (options.novids && options.noaudio) { + fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); + } - if(options.dlsubs.indexOf('all') > -1){ - options.dlsubs = ['all']; - } + if (options.dlsubs.indexOf('all') > -1) { + options.dlsubs = ['all']; + } - if (options.nosubs) { - console.info('Subtitles downloading disabled from nosubs flag.'); - options.skipsubs = true; - } + if (options.nosubs) { + console.info('Subtitles downloading disabled from nosubs flag.'); + options.skipsubs = true; + } - if (!options.skipsubs && options.dlsubs.indexOf('none') == -1) { - if(streamData.subtitles.length > 0) { - for(const sub of streamData.subtitles) { - const subLang = langsData.languages.find(a => a.ao_locale === sub.lang); - if (!subLang) { - console.warn(`Language not found for subtitle language: ${sub.lang}, Skipping`); - continue; - } - const sxData: Partial = {}; - sxData.file = langsData.subsFile(fileName as string, subIndex+'', subLang, false, options.ccTag); - if (path.isAbsolute(sxData.file)) { - sxData.path = sxData.file; - } else { - sxData.path = path.join(this.cfg.dir.content, sxData.file); - } - const dirName = path.dirname(sxData.path); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }); - } - sxData.language = subLang; - if((options.dlsubs.includes('all') || options.dlsubs.includes(subLang.locale)) && sub.url.includes('.ass')) { - const getSubtitle = await this.req.getData(sub.url, AuthHeaders); - if (getSubtitle.ok && getSubtitle.res) { - console.info(`Subtitle Downloaded: ${sub.url}`); - const sBody = await getSubtitle.res.text(); - sxData.title = `${subLang.language}`; - sxData.fonts = fontsData.assFonts(sBody) as Font[]; - fs.writeFileSync(sxData.path, sBody); - files.push({ - type: 'Subtitle', - ...sxData as sxItem, - cc: false - }); - } else{ - console.warn(`Failed to download subtitle: ${sxData.file}`); - } - } - subIndex++; - } - } else{ - console.warn('Can\'t find urls for subtitles!'); - } - } - else{ - console.info('Subtitles downloading skipped!'); - } - await this.sleep(options.waittime); - } - return { - error: dlFailed, - data: files, - fileName: fileName ? (path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName)) || './unknown' : './unknown' - }; - } + if (!options.skipsubs && options.dlsubs.indexOf('none') == -1) { + if (streamData.subtitles.length > 0) { + for (const sub of streamData.subtitles) { + const subLang = langsData.languages.find((a) => a.ao_locale === sub.lang); + if (!subLang) { + console.warn(`Language not found for subtitle language: ${sub.lang}, Skipping`); + continue; + } + const sxData: Partial = {}; + sxData.file = langsData.subsFile(fileName as string, subIndex + '', subLang, false, options.ccTag); + if (path.isAbsolute(sxData.file)) { + sxData.path = sxData.file; + } else { + sxData.path = path.join(this.cfg.dir.content, sxData.file); + } + const dirName = path.dirname(sxData.path); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + sxData.language = subLang; + if ((options.dlsubs.includes('all') || options.dlsubs.includes(subLang.locale)) && sub.url.includes('.ass')) { + const getSubtitle = await this.req.getData(sub.url, AuthHeaders); + if (getSubtitle.ok && getSubtitle.res) { + console.info(`Subtitle Downloaded: ${sub.url}`); + const sBody = await getSubtitle.res.text(); + sxData.title = `${subLang.language}`; + sxData.fonts = fontsData.assFonts(sBody) as Font[]; + fs.writeFileSync(sxData.path, sBody); + files.push({ + type: 'Subtitle', + ...(sxData as sxItem), + cc: false + }); + } else { + console.warn(`Failed to download subtitle: ${sxData.file}`); + } + } + subIndex++; + } + } else { + console.warn("Can't find urls for subtitles!"); + } + } else { + console.info('Subtitles downloading skipped!'); + } + await this.sleep(options.waittime); + } + return { + error: dlFailed, + data: files, + fileName: fileName ? (path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName)) || './unknown' : './unknown' + }; + } - public sleep(ms: number) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); - } + public sleep(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + } } diff --git a/config/bin-path.yml b/config/bin-path.yml index e09af5e..0a27a57 100644 --- a/config/bin-path.yml +++ b/config/bin-path.yml @@ -1,5 +1,5 @@ -ffmpeg: "ffmpeg.exe" -mkvmerge: "mkvmerge.exe" -ffprobe: "ffprobe.exe" -mp4decrypt: "mp4decrypt.exe" -shaka: "shaka-packager.exe" +ffmpeg: 'ffmpeg.exe' +mkvmerge: 'mkvmerge.exe' +ffprobe: 'ffprobe.exe' +mp4decrypt: 'mp4decrypt.exe' +shaka: 'shaka-packager.exe' diff --git a/config/cli-defaults.yml b/config/cli-defaults.yml index ea5a158..99b095f 100644 --- a/config/cli-defaults.yml +++ b/config/cli-defaults.yml @@ -13,16 +13,16 @@ dlVideoOnce: false # Whether to keep all downloaded videos or only a single copy keepAllVideos: false # What to use as the file name template -fileName: "[${service}] ${showTitle} - S${season}E${episode} [${height}p]" +fileName: '[${service}] ${showTitle} - S${season}E${episode} [${height}p]' # What Audio languages to download -dubLang: ["jpn"] +dubLang: ['jpn'] # What Subtitle languages to download -dlsubs: ["all"] +dlsubs: ['all'] # What language Audio to set as default -defaultAudio: "jpn" +defaultAudio: 'jpn' # Video Playback Endpoint (Crunchyroll) -vstream: "androidtv" +vstream: 'androidtv' # Audio Playback Endpoint (Crunchyroll) -astream: "android" +astream: 'android' # Total Session Death (Could kill active streaming sessions from watching users if account shared, use with caution) (Crunchyroll) tsd: false diff --git a/crunchy.ts b/crunchy.ts index f8d8632..fd54348 100644 --- a/crunchy.ts +++ b/crunchy.ts @@ -18,9 +18,6 @@ import * as yamlCfg from './modules/module.cfg-loader'; import * as yargs from './modules/module.app-args'; import Merger, { Font, MergerInput, SubtitleInput } from './modules/module.merger'; import { canDecrypt, getKeysPRD, getKeysWVD, cdm } from './modules/cdm'; -//import vttConvert from './modules/module.vttconvert'; - -// args // load req import { domain, api } from './modules/module.api-urls'; @@ -36,7 +33,6 @@ import parseSelect from './modules/module.parseSelect'; import { AvailableFilenameVars, getDefault } from './modules/module.args'; import { AuthData, AuthResponse, Episode, ResponseBase, SearchData, SearchResponse, SearchResponseItem } from './@types/messageHandler'; import { ServiceClass } from './@types/serviceClassInterface'; -import { CrunchyAndroidStreams } from './@types/crunchyAndroidStreams'; import { CrunchyAndroidEpisodes } from './@types/crunchyAndroidEpisodes'; import { parse } from './modules/module.transform-mpd'; import { CrunchyAndroidObject } from './@types/crunchyAndroidObject'; @@ -47,3022 +43,3182 @@ import { CrunchyVideoPlayStreams, CrunchyAudioPlayStreams } from './@types/enums import { randomUUID } from 'node:crypto'; export type sxItem = { - language: langsData.LanguageItem, - path: string, - file: string - title: string, - fonts: Font[] -} + language: langsData.LanguageItem; + path: string; + file: string; + title: string; + fonts: Font[]; +}; export default class Crunchy implements ServiceClass { - public cfg: yamlCfg.ConfigObject; - public locale: string; - private token: Record; - private req: reqModule.Req; - private cmsToken: { - cms?: Record, - cms_beta?: Record, - cms_web?: Record - } = {}; + public cfg: yamlCfg.ConfigObject; + public locale: string; + private token: Record; + private req: reqModule.Req; + private cmsToken: { + cms?: Record; + cms_beta?: Record; + cms_web?: Record; + } = {}; - constructor(private debug = false) { - this.cfg = yamlCfg.loadCfg(); - this.token = yamlCfg.loadCRToken(); - this.req = new reqModule.Req(domain, debug, false, 'cr'); - this.locale = 'en-US'; - } + constructor(private debug = false) { + this.cfg = yamlCfg.loadCfg(); + this.token = yamlCfg.loadCRToken(); + this.req = new reqModule.Req(domain, debug, false, 'cr'); + this.locale = 'en-US'; + } - public checkToken(): boolean { - return Object.keys(this.cmsToken.cms_web ?? {}).length > 0; - } + public checkToken(): boolean { + return Object.keys(this.cmsToken.cms_web ?? {}).length > 0; + } - public async cli() { - console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`); - const argv = yargs.appArgv(this.cfg.cli); - this.locale = argv.locale; - if (argv.debug) - this.debug = true; + public async cli() { + console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`); + const argv = yargs.appArgv(this.cfg.cli); + this.locale = argv.locale; + if (argv.debug) this.debug = true; - // load binaries - this.cfg.bin = await yamlCfg.loadBinCfg(); - if (argv.allDubs) { - argv.dubLang = langsData.dubLanguageCodes; - } - // select mode - if (argv.silentAuth && !argv.auth) { - await this.doAuth({ - username: argv.username ?? await Helper.question('[Q] LOGIN/EMAIL: '), - password: argv.password ?? await Helper.question('[Q] PASSWORD: ') - }); - } - if(argv.dlFonts){ - await this.getFonts(); - } - else if(argv.auth){ - await this.doAuth({ - username: argv.username ?? await Helper.question('[Q] LOGIN/EMAIL: '), - password: argv.password ?? await Helper.question('[Q] PASSWORD: ') - }); - } - else if (argv.token) { - await this.loginWithToken(argv.token); - } - else if(argv.cmsindex){ - await this.refreshToken(); - await this.getCmsData(); - } - else if(argv.new){ - await this.refreshToken(); - await this.getNewlyAdded(argv.page, argv['search-type'], argv.raw, argv.rawoutput); - } - else if(argv.search && argv.search.length > 2){ - await this.refreshToken(); - await this.doSearch({ ...argv, search: argv.search as string }); - } - else if(argv.series && argv.series.match(/^[0-9A-Z]{9,}$/)){ - await this.refreshToken(); - await this.logSeriesById(argv.series as string); - const selected = await this.downloadFromSeriesID(argv.series, { ...argv }); - if (selected.isOk) { - for (const select of selected.value) { - if (!(await this.downloadEpisode(select, {...argv, skipsubs: false}, true))) { - console.error(`Unable to download selected episode ${select.episodeNumber}`); - return false; - } - } - } - return true; - } - else if(argv['movie-listing'] && argv['movie-listing'].match(/^[0-9A-Z]{9,}$/)){ - await this.refreshToken(); - await this.logMovieListingById(argv['movie-listing'] as string); - } - else if(argv['show-raw'] && argv['show-raw'].match(/^[0-9A-Z]{9,}$/)){ - await this.refreshToken(); - await this.logShowRawById(argv['show-raw'] as string); - } - else if(argv['season-raw'] && argv['season-raw'].match(/^[0-9A-Z]{9,}$/)){ - await this.refreshToken(); - await this.logSeasonRawById(argv['season-raw'] as string); - } - else if(argv['show-list-raw']){ - await this.refreshToken(); - await this.logShowListRaw(); - } - else if(argv.s && argv.s.match(/^[0-9A-Z]{9,}$/)){ - await this.refreshToken(); - if (argv.dubLang.length > 1) { - console.info('One show can only be downloaded with one dub. Use --srz instead.'); - } - argv.dubLang = [argv.dubLang[0]]; - const selected = await this.getSeasonById(argv.s, argv.numbers, argv.e, argv.but, argv.all); - if (selected.isOk) { - for (const select of selected.value) { - if (!(await this.downloadEpisode(select, {...argv, skipsubs: false }))) { - console.error(`Unable to download selected episode ${select.episodeNumber}`); - return false; - } - } - } - return true; - } - else if(argv.e){ - await this.refreshToken(); - if (argv.dubLang.length > 1) { - console.info('One show can only be downloaded with one dub. Use --srz instead.'); - } - argv.dubLang = [argv.dubLang[0]]; - const selected = await this.getObjectById(argv.e, false); - for (const select of selected as Partial[]) { - if (!(await this.downloadEpisode(select as CrunchyEpMeta, {...argv, skipsubs: false}))) { - console.error(`Unable to download selected episode ${select.episodeNumber}`); - return false; - } - } - return true; - } else if (argv.extid) { - await this.refreshToken(); - if (argv.dubLang.length > 1) { - console.info('One show can only be downloaded with one dub. Use --srz instead.'); - } - argv.dubLang = [argv.dubLang[0]]; - const selected = await this.getObjectById(argv.extid, false, true); - for (const select of selected as Partial[]) { - if (!(await this.downloadEpisode(select as CrunchyEpMeta, {...argv, skipsubs: false}))) { - console.error(`Unable to download selected episode ${select.episodeNumber}`); - return false; - } - } - return true; - } - else{ - console.info('No option selected or invalid value entered. Try --help.'); - } - } + // load binaries + this.cfg.bin = await yamlCfg.loadBinCfg(); + if (argv.allDubs) { + argv.dubLang = langsData.dubLanguageCodes; + } + // select mode + if (argv.silentAuth && !argv.auth) { + await this.doAuth({ + username: argv.username ?? (await Helper.question('[Q] LOGIN/EMAIL: ')), + password: argv.password ?? (await Helper.question('[Q] PASSWORD: ')) + }); + } + if (argv.dlFonts) { + await this.getFonts(); + } else if (argv.auth) { + await this.doAuth({ + username: argv.username ?? (await Helper.question('[Q] LOGIN/EMAIL: ')), + password: argv.password ?? (await Helper.question('[Q] PASSWORD: ')) + }); + } else if (argv.token) { + await this.loginWithToken(argv.token); + } else if (argv.cmsindex) { + await this.refreshToken(); + await this.getCmsData(); + } else if (argv.new) { + await this.refreshToken(); + await this.getNewlyAdded(argv.page, argv['search-type'], argv.raw, argv.rawoutput); + } else if (argv.search && argv.search.length > 2) { + await this.refreshToken(); + await this.doSearch({ ...argv, search: argv.search as string }); + } else if (argv.series && argv.series.match(/^[0-9A-Z]{9,}$/)) { + await this.refreshToken(); + await this.logSeriesById(argv.series as string); + const selected = await this.downloadFromSeriesID(argv.series, { ...argv }); + if (selected.isOk) { + for (const select of selected.value) { + if (!(await this.downloadEpisode(select, { ...argv, skipsubs: false }, true))) { + console.error(`Unable to download selected episode ${select.episodeNumber}`); + return false; + } + } + } + return true; + } else if (argv['movie-listing'] && argv['movie-listing'].match(/^[0-9A-Z]{9,}$/)) { + await this.refreshToken(); + await this.logMovieListingById(argv['movie-listing'] as string); + } else if (argv['show-raw'] && argv['show-raw'].match(/^[0-9A-Z]{9,}$/)) { + await this.refreshToken(); + await this.logShowRawById(argv['show-raw'] as string); + } else if (argv['season-raw'] && argv['season-raw'].match(/^[0-9A-Z]{9,}$/)) { + await this.refreshToken(); + await this.logSeasonRawById(argv['season-raw'] as string); + } else if (argv['show-list-raw']) { + await this.refreshToken(); + await this.logShowListRaw(); + } else if (argv.s && argv.s.match(/^[0-9A-Z]{9,}$/)) { + await this.refreshToken(); + if (argv.dubLang.length > 1) { + console.info('One show can only be downloaded with one dub. Use --srz instead.'); + } + argv.dubLang = [argv.dubLang[0]]; + const selected = await this.getSeasonById(argv.s, argv.numbers, argv.e, argv.but, argv.all); + if (selected.isOk) { + for (const select of selected.value) { + if (!(await this.downloadEpisode(select, { ...argv, skipsubs: false }))) { + console.error(`Unable to download selected episode ${select.episodeNumber}`); + return false; + } + } + } + return true; + } else if (argv.e) { + await this.refreshToken(); + if (argv.dubLang.length > 1) { + console.info('One show can only be downloaded with one dub. Use --srz instead.'); + } + argv.dubLang = [argv.dubLang[0]]; + const selected = await this.getObjectById(argv.e, false); + for (const select of selected as Partial[]) { + if (!(await this.downloadEpisode(select as CrunchyEpMeta, { ...argv, skipsubs: false }))) { + console.error(`Unable to download selected episode ${select.episodeNumber}`); + return false; + } + } + return true; + } else if (argv.extid) { + await this.refreshToken(); + if (argv.dubLang.length > 1) { + console.info('One show can only be downloaded with one dub. Use --srz instead.'); + } + argv.dubLang = [argv.dubLang[0]]; + const selected = await this.getObjectById(argv.extid, false, true); + for (const select of selected as Partial[]) { + if (!(await this.downloadEpisode(select as CrunchyEpMeta, { ...argv, skipsubs: false }))) { + console.error(`Unable to download selected episode ${select.episodeNumber}`); + return false; + } + } + return true; + } else { + console.info('No option selected or invalid value entered. Try --help.'); + } + } - public async logShowRawById(id: string){ - // check token - if(!this.cmsToken.cms_web){ - console.error('Authentication required!'); - return; - } - // opts - const AuthHeaders = { - headers: { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader - }, - useProxy: true - }; - // seasons list - const seriesSeasonListReq = await this.req.getData(`${api.content_cms}/series/${id}/seasons?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); - if(!seriesSeasonListReq.ok || !seriesSeasonListReq.res){ - console.error('Series Request FAILED!'); - return; - } - const seriesData = await seriesSeasonListReq.res.json(); - for (const item of seriesData.data) { - // stringify each object, then a newline - console.log(JSON.stringify(item)); - } - return seriesData.data; - } + public async logShowRawById(id: string) { + // check token + if (!this.cmsToken.cms_web) { + console.error('Authentication required!'); + return; + } + // opts + const AuthHeaders = { + headers: { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader + }, + useProxy: true + }; + // seasons list + const seriesSeasonListReq = await this.req.getData( + `${api.content_cms}/series/${id}/seasons?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, + AuthHeaders + ); + if (!seriesSeasonListReq.ok || !seriesSeasonListReq.res) { + console.error('Series Request FAILED!'); + return; + } + const seriesData = await seriesSeasonListReq.res.json(); + for (const item of seriesData.data) { + // stringify each object, then a newline + console.log(JSON.stringify(item)); + } + return seriesData.data; + } + public async logSeasonRawById(id: string) { + // check token + if (!this.cmsToken.cms_web) { + console.error('Authentication required!'); + return; + } + // opts + const AuthHeaders = { + headers: { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader + }, + useProxy: true + }; + // seasons list + let episodeList = { total: 0, data: [], meta: {} } as CrunchyEpisodeList; + //get episode info + const reqEpsListOpts = [ + api.cms_bucket, + this.cmsToken.cms_web.bucket, + '/episodes?', + new URLSearchParams({ + force_locale: '', + preferred_audio_language: 'ja-JP', + locale: this.locale, + season_id: id, + Policy: this.cmsToken.cms_web.policy, + Signature: this.cmsToken.cms_web.signature, + 'Key-Pair-Id': this.cmsToken.cms_web.key_pair_id + }) + ].join(''); + const reqEpsList = await this.req.getData(reqEpsListOpts, AuthHeaders); + if (!reqEpsList.ok || !reqEpsList.res) { + console.error('Episode List Request FAILED!'); + return { isOk: false, reason: new Error('Episode List request failed. No more information provided.') }; + } + //CrunchyEpisodeList + const episodeListAndroid = (await reqEpsList.res.json()) as CrunchyAndroidEpisodes; + episodeList = { + total: episodeListAndroid.total, + data: episodeListAndroid.items, + meta: {} + }; + for (const item of episodeList.data) { + // stringify each object, then a newline + console.log(JSON.stringify(item)); + } - public async logSeasonRawById(id: string){ - // check token - if(!this.cmsToken.cms_web){ - console.error('Authentication required!'); - return; - } - // opts - const AuthHeaders = { - headers: { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader - }, - useProxy: true - }; - // seasons list - let episodeList = { total: 0, data: [], meta: {} } as CrunchyEpisodeList; - //get episode info - const reqEpsListOpts = [ - api.cms_bucket, - this.cmsToken.cms_web.bucket, - '/episodes?', - new URLSearchParams({ - 'force_locale': '', - 'preferred_audio_language': 'ja-JP', - 'locale': this.locale, - 'season_id': id, - 'Policy': this.cmsToken.cms_web.policy, - 'Signature': this.cmsToken.cms_web.signature, - 'Key-Pair-Id': this.cmsToken.cms_web.key_pair_id, - }), - ].join(''); - const reqEpsList = await this.req.getData(reqEpsListOpts, AuthHeaders); - if(!reqEpsList.ok || !reqEpsList.res){ - console.error('Episode List Request FAILED!'); - return { isOk: false, reason: new Error('Episode List request failed. No more information provided.') }; - } - //CrunchyEpisodeList - const episodeListAndroid = await reqEpsList.res.json() as CrunchyAndroidEpisodes; - episodeList = { - total: episodeListAndroid.total, - data: episodeListAndroid.items, - meta: {} - }; - for (const item of episodeList.data) { - // stringify each object, then a newline - console.log(JSON.stringify(item)); - } + // Return the data directly if this function is called by other code + return episodeList.data; + } - // Return the data directly if this function is called by other code - return episodeList.data; - } + public async logShowListRaw() { + // check token + if (!this.cmsToken.cms_web) { + console.error('Authentication required!'); + return; + } - public async logShowListRaw() { - // check token - if(!this.cmsToken.cms_web){ - console.error('Authentication required!'); - return; - } + // opts + const AuthHeaders = { + headers: { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader + }, + useProxy: true + }; - // opts - const AuthHeaders = { - headers: { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader - }, - useProxy: true - }; + const allShows: any[] = []; + let page = 1; + let hasMorePages = true; - const allShows: any[] = []; - let page = 1; - let hasMorePages = true; + if (this.debug) { + console.info('Retrieving complete show list...'); + } - if(this.debug){ - console.info('Retrieving complete show list...'); - } + while (hasMorePages) { + const searchStart = (page - 1) * 50; + const params = new URLSearchParams({ + preferred_audio_language: 'ja-JP', + locale: this.locale, + ratings: 'true', + sort_by: 'alphabetical', + n: '50', + start: searchStart.toString() + }).toString(); - while (hasMorePages) { - const searchStart = (page - 1) * 50; - const params = new URLSearchParams({ - 'preferred_audio_language': 'ja-JP', - 'locale': this.locale, - 'ratings': 'true', - 'sort_by': 'alphabetical', - 'n': '50', - 'start': searchStart.toString() - }).toString(); + const showListReq = await this.req.getData(`${api.browse_all_series}?${params}`, AuthHeaders); - const showListReq = await this.req.getData(`${api.browse_all_series}?${params}`, AuthHeaders); + if (!showListReq.ok || !showListReq.res) { + console.error(`Show List Request FAILED on page ${page}!`); + return allShows; + } - if (!showListReq.ok || !showListReq.res) { - console.error(`Show List Request FAILED on page ${page}!`); - return allShows; - } + const showListData = await showListReq.res.json(); - const showListData = await showListReq.res.json(); + // Add current page data + for (const item of showListData.data) { + // stringify each object, then a newline + console.log(JSON.stringify(item)); + allShows.push(item); + } - // Add current page data - for (const item of showListData.data) { - // stringify each object, then a newline - console.log(JSON.stringify(item)); - allShows.push(item); - } + // Calculate pagination info + const totalItems = showListData.total; + const totalPages = Math.ceil(totalItems / 50); + if (this.debug) { + console.info(`Retrieved page ${page}/${totalPages} (${allShows.length}/${totalItems} items)`); + } - // Calculate pagination info - const totalItems = showListData.total; - const totalPages = Math.ceil(totalItems / 50); - if(this.debug){ - console.info(`Retrieved page ${page}/${totalPages} (${allShows.length}/${totalItems} items)`); - } + // Check if we need to fetch more pages + if (page >= totalPages) { + hasMorePages = false; + } else { + page++; + // Add a small delay to avoid rate limiting + await this.sleep(1000); + } + } - // Check if we need to fetch more pages - if (page >= totalPages) { - hasMorePages = false; - } else { - page++; - // Add a small delay to avoid rate limiting - await this.sleep(1000); - } - } + if (this.debug) { + console.info(`Complete show list retrieved: ${allShows.length} items`); + } + return allShows; + } - if(this.debug){ - console.info(`Complete show list retrieved: ${allShows.length} items`); - } - return allShows; - } + public async getFonts() { + console.info('Downloading fonts...'); + const fonts = Object.values(fontsData.fontFamilies).reduce((pre, curr) => pre.concat(curr)); + for (const f of fonts) { + const fontLoc = path.join(this.cfg.dir.fonts, f); + if (fs.existsSync(fontLoc) && fs.statSync(fontLoc).size != 0) { + console.info(`${f} already downloaded!`); + } else { + const fontFolder = path.dirname(fontLoc); + if (fs.existsSync(fontLoc) && fs.statSync(fontLoc).size == 0) { + fs.unlinkSync(fontLoc); + } + try { + fs.ensureDirSync(fontFolder); + } catch (e) { + console.info(''); + } + const fontUrl = fontsData.root + f; + const getFont = await this.req.getData(fontUrl, { + headers: api.crunchyDefHeader + }); + if (getFont.ok && getFont.res) { + fs.writeFileSync(fontLoc, Buffer.from(await getFont.res.arrayBuffer())); + console.info(`Downloaded: ${f}`); + } else { + console.warn(`Failed to download: ${f}`); + } + } + } + console.info('All required fonts downloaded!'); + } - public async getFonts() { - console.info('Downloading fonts...'); - const fonts = Object.values(fontsData.fontFamilies).reduce((pre, curr) => pre.concat(curr)); - for(const f of fonts) { - const fontLoc = path.join(this.cfg.dir.fonts, f); - if(fs.existsSync(fontLoc) && fs.statSync(fontLoc).size != 0){ - console.info(`${f} already downloaded!`); - } - else{ - const fontFolder = path.dirname(fontLoc); - if(fs.existsSync(fontLoc) && fs.statSync(fontLoc).size == 0){ - fs.unlinkSync(fontLoc); - } - try{ - fs.ensureDirSync(fontFolder); - } - catch(e){ - console.info(''); - } - const fontUrl = fontsData.root + f; - const getFont = await this.req.getData(fontUrl, { - headers: api.crunchyDefHeader - }); - if(getFont.ok && getFont.res){ - fs.writeFileSync(fontLoc, Buffer.from(await getFont.res.arrayBuffer())); - console.info(`Downloaded: ${f}`); - } - else{ - console.warn(`Failed to download: ${f}`); - } - } - } - console.info('All required fonts downloaded!'); - } + // private async productionToken() { + // const tokenReq = await this.req.getData(api.bundlejs); - // private async productionToken() { - // const tokenReq = await this.req.getData(api.bundlejs); + // if (!tokenReq.ok || !tokenReq.res) { + // console.error('Failed to get Production Token!'); + // return { isOk: false, reason: new Error('Failed to get Production Token') }; + // } - // if (!tokenReq.ok || !tokenReq.res) { - // console.error('Failed to get Production Token!'); - // return { isOk: false, reason: new Error('Failed to get Production Token') }; - // } + // const rawjs = await tokenReq.res.text(); - // const rawjs = await tokenReq.res.text(); + // const tokens = rawjs.match(/prod="([\w-]+:[\w-]+)"/); - // const tokens = rawjs.match(/prod="([\w-]+:[\w-]+)"/); + // if (!tokens) { + // console.error('Failed to find Production Token in js!'); + // return { isOk: false, reason: new Error('Failed to find Production Token in js') }; + // } - // if (!tokens) { - // console.error('Failed to find Production Token in js!'); - // return { isOk: false, reason: new Error('Failed to find Production Token in js') }; - // } + // return Buffer.from(tokens[1], 'latin1').toString('base64'); + // } - // return Buffer.from(tokens[1], 'latin1').toString('base64'); - // } + public async doAuth(data: AuthData): Promise { + const uuid = randomUUID(); + const authData = new URLSearchParams({ + username: data.username, + password: data.password, + grant_type: 'password', + scope: 'offline_access', + device_id: uuid, + device_name: 'iPhone', + device_type: 'iPhone 13' + }).toString(); + const authReqOpts: reqModule.Params = { + method: 'POST', + headers: { ...api.crunchyAuthHeader, 'ETP-Anonymous-ID': uuid }, + body: authData + }; + const authReq = await this.req.getData(api.auth, authReqOpts); + if (!authReq.ok || !authReq.res) { + console.error('Authentication failed!'); + return { isOk: false, reason: new Error('Authentication failed') }; + } + // To prevent any Cloudflare errors in the future + if (authReq.res.headers.get('Set-Cookie')) { + api.crunchyDefHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; + api.crunchyAuthHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; + } + if (authReq.headers && authReq.headers['Set-Cookie']) { + api.crunchyDefHeader['Cookie'] = authReq.headers['Set-Cookie']; + api.crunchyAuthHeader['Cookie'] = authReq.headers['Set-Cookie']; + api.crunchyDefHeader['User-Agent'] = authReq.headers['User-Agent']; + api.crunchyAuthHeader['User-Agent'] = authReq.headers['User-Agent']; + } + this.token = await authReq.res.json(); + this.token.device_id = uuid; + this.token.expires = new Date(Date.now() + this.token.expires_in); + yamlCfg.saveCRToken(this.token); + await this.getProfile(); + console.info('Your Country: %s', this.token.country); + return { isOk: true, value: undefined }; + } - public async doAuth(data: AuthData): Promise { - const uuid = randomUUID(); - const authData = new URLSearchParams({ - 'username': data.username, - 'password': data.password, - 'grant_type': 'password', - 'scope': 'offline_access', - 'device_id': uuid, - 'device_name': 'iPhone', - 'device_type': 'iPhone 13' - }).toString(); - const authReqOpts: reqModule.Params = { - method: 'POST', - headers: { ...api.crunchyAuthHeader, 'ETP-Anonymous-ID': uuid }, - body: authData - }; - const authReq = await this.req.getData(api.auth, authReqOpts); - if(!authReq.ok || !authReq.res){ - console.error('Authentication failed!'); - return { isOk: false, reason: new Error('Authentication failed') }; - } - // To prevent any Cloudflare errors in the future - if (authReq.res.headers.get('Set-Cookie')) { - api.crunchyDefHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; - api.crunchyAuthHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; - } - if (authReq.headers && authReq.headers['Set-Cookie']) { - api.crunchyDefHeader['Cookie'] = authReq.headers['Set-Cookie']; - api.crunchyAuthHeader['Cookie'] = authReq.headers['Set-Cookie']; - api.crunchyDefHeader['User-Agent'] = authReq.headers['User-Agent']; - api.crunchyAuthHeader['User-Agent'] = authReq.headers['User-Agent']; - } - this.token = await authReq.res.json(); - this.token.device_id = uuid; - this.token.expires = new Date(Date.now() + this.token.expires_in); - yamlCfg.saveCRToken(this.token); - await this.getProfile(); - console.info('Your Country: %s', this.token.country); - return { isOk: true, value: undefined }; - } + public async doAnonymousAuth() { + const uuid = randomUUID(); + const authData = new URLSearchParams({ + grant_type: 'client_id', + scope: 'offline_access', + device_id: uuid, + device_name: 'iPhone', + device_type: 'iPhone 13' + }).toString(); + const authReqOpts: reqModule.Params = { + method: 'POST', + headers: { ...api.crunchyAuthHeader, 'ETP-Anonymous-ID': uuid }, + body: authData + }; + const authReq = await this.req.getData(api.auth, authReqOpts); + if (!authReq.ok || !authReq.res) { + console.error('Anonymous Authentication failed!'); + return; + } + // To prevent any Cloudflare errors in the future + if (authReq.res.headers.get('Set-Cookie')) { + api.crunchyDefHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; + api.crunchyAuthHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; + } + if (authReq.headers && authReq.headers['Set-Cookie']) { + api.crunchyDefHeader['Cookie'] = authReq.headers['Set-Cookie']; + api.crunchyAuthHeader['Cookie'] = authReq.headers['Set-Cookie']; + api.crunchyDefHeader['User-Agent'] = authReq.headers['User-Agent']; + api.crunchyAuthHeader['User-Agent'] = authReq.headers['User-Agent']; + } + this.token = await authReq.res.json(); + this.token.device_id = uuid; + this.token.expires = new Date(Date.now() + this.token.expires_in); + yamlCfg.saveCRToken(this.token); + } - public async doAnonymousAuth(){ - const uuid = randomUUID(); - const authData = new URLSearchParams({ - 'grant_type': 'client_id', - 'scope': 'offline_access', - 'device_id': uuid, - 'device_name': 'iPhone', - 'device_type': 'iPhone 13' - }).toString(); - const authReqOpts: reqModule.Params = { - method: 'POST', - headers: { ...api.crunchyAuthHeader, 'ETP-Anonymous-ID': uuid }, - body: authData - }; - const authReq = await this.req.getData(api.auth, authReqOpts); - if(!authReq.ok || !authReq.res){ - console.error('Anonymous Authentication failed!'); - return; - } - // To prevent any Cloudflare errors in the future - if (authReq.res.headers.get('Set-Cookie')) { - api.crunchyDefHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; - api.crunchyAuthHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; - } - if (authReq.headers && authReq.headers['Set-Cookie']) { - api.crunchyDefHeader['Cookie'] = authReq.headers['Set-Cookie']; - api.crunchyAuthHeader['Cookie'] = authReq.headers['Set-Cookie']; - api.crunchyDefHeader['User-Agent'] = authReq.headers['User-Agent']; - api.crunchyAuthHeader['User-Agent'] = authReq.headers['User-Agent']; - } - this.token = await authReq.res.json(); - this.token.device_id = uuid; - this.token.expires = new Date(Date.now() + this.token.expires_in); - yamlCfg.saveCRToken(this.token); - } + public async getProfile(silent = false): Promise { + if (!this.token.access_token) { + console.error('No access token!'); + return false; + } + const profileReqOptions = { + headers: { + ...api.crunchyDefHeader, + Authorization: `Bearer ${this.token.access_token}` + }, + useProxy: true + }; + const profileReq = await this.req.getData(api.profile, profileReqOptions); + if (!profileReq.ok || !profileReq.res) { + console.error('Get profile failed!'); + return false; + } + const profile = await profileReq.res.json(); + if (!silent) { + console.info('USER: %s (%s)', profile.username, profile.email); + } + return true; + } - public async getProfile(silent = false) : Promise { - if(!this.token.access_token){ - console.error('No access token!'); - return false; - } - const profileReqOptions = { - headers: { - ...api.crunchyDefHeader, - Authorization: `Bearer ${this.token.access_token}` - }, - useProxy: true - }; - const profileReq = await this.req.getData(api.profile, profileReqOptions); - if(!profileReq.ok || !profileReq.res){ - console.error('Get profile failed!'); - return false; - } - const profile = await profileReq.res.json(); - if (!silent) { - console.info('USER: %s (%s)', profile.username, profile.email); - } - return true; - } + public sleep(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + } - public sleep(ms: number) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); - } + public async loginWithToken(refreshToken: string) { + const uuid = randomUUID(); + const authData = new URLSearchParams({ + refresh_token: this.token.refresh_token, + grant_type: 'refresh_token', + //'grant_type': 'etp_rt_cookie', + scope: 'offline_access', + device_id: uuid, + device_name: 'iPhone', + device_type: 'iPhone 13' + }).toString(); + const authReqOpts: reqModule.Params = { + method: 'POST', + headers: { ...api.crunchyAuthHeader, 'ETP-Anonymous-ID': uuid, Cookie: `etp_rt=${refreshToken}` }, + body: authData + }; + const authReq = await this.req.getData(api.auth, authReqOpts); + if (!authReq.ok || !authReq.res) { + console.error('Token Authentication failed!'); + if (authReq.res?.status == 400) { + console.warn('Token is likely wrong (Or invalid for given API), please login again!'); + } + return; + } + // To prevent any Cloudflare errors in the future + if (authReq.res.headers.get('Set-Cookie')) { + api.crunchyDefHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; + api.crunchyAuthHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; + } + if (authReq.headers && authReq.headers['Set-Cookie']) { + api.crunchyDefHeader['Cookie'] = authReq.headers['Set-Cookie']; + api.crunchyAuthHeader['Cookie'] = authReq.headers['Set-Cookie']; + api.crunchyDefHeader['User-Agent'] = authReq.headers['User-Agent']; + api.crunchyAuthHeader['User-Agent'] = authReq.headers['User-Agent']; + } + this.token = await authReq.res.json(); + this.token.device_id = uuid; + this.token.expires = new Date(Date.now() + this.token.expires_in); + yamlCfg.saveCRToken(this.token); + await this.getProfile(false); + await this.getCMStoken(true); + } - public async loginWithToken(refreshToken: string) { - const uuid = randomUUID(); - const authData = new URLSearchParams({ - 'refresh_token': this.token.refresh_token, - 'grant_type': 'refresh_token', - //'grant_type': 'etp_rt_cookie', - 'scope': 'offline_access', - 'device_id': uuid, - 'device_name': 'iPhone', - 'device_type': 'iPhone 13' - }).toString(); - const authReqOpts: reqModule.Params = { - method: 'POST', - headers: {...api.crunchyAuthHeader, 'ETP-Anonymous-ID': uuid, Cookie: `etp_rt=${refreshToken}`}, - body: authData - }; - const authReq = await this.req.getData(api.auth, authReqOpts); - if(!authReq.ok || !authReq.res){ - console.error('Token Authentication failed!'); - if (authReq.res?.status == 400) { - console.warn('Token is likely wrong (Or invalid for given API), please login again!'); - } - return; - } - // To prevent any Cloudflare errors in the future - if (authReq.res.headers.get('Set-Cookie')) { - api.crunchyDefHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; - api.crunchyAuthHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; - } - if (authReq.headers && authReq.headers['Set-Cookie']) { - api.crunchyDefHeader['Cookie'] = authReq.headers['Set-Cookie']; - api.crunchyAuthHeader['Cookie'] = authReq.headers['Set-Cookie']; - api.crunchyDefHeader['User-Agent'] = authReq.headers['User-Agent']; - api.crunchyAuthHeader['User-Agent'] = authReq.headers['User-Agent']; - } - this.token = await authReq.res.json(); - this.token.device_id = uuid; - this.token.expires = new Date(Date.now() + this.token.expires_in); - yamlCfg.saveCRToken(this.token); - await this.getProfile(false); - await this.getCMStoken(true); - } - - public async refreshToken(ifNeeded = false, silent = false) { - if(!this.token.access_token && !this.token.refresh_token || this.token.access_token && !this.token.refresh_token){ - await this.doAnonymousAuth(); - } - else{ - /*if (ifNeeded) + public async refreshToken(ifNeeded = false, silent = false) { + if ((!this.token.access_token && !this.token.refresh_token) || (this.token.access_token && !this.token.refresh_token)) { + await this.doAnonymousAuth(); + } else { + /*if (ifNeeded) return;*/ - if (!(Date.now() > new Date(this.token.expires).getTime()) && ifNeeded) { - return; - } else { - //console.info('[WARN] The token has expired compleatly. I will try to refresh the token anyway, but you might have to reauth.'); - } - const uuid = this.token.device_id || randomUUID(); - const authData = new URLSearchParams({ - 'refresh_token': this.token.refresh_token, - 'grant_type': 'refresh_token', - 'device_id': uuid, - 'device_name': 'iPhone', - 'device_type': 'iPhone 13' - }).toString(); - const authReqOpts: reqModule.Params = { - method: 'POST', - headers: { ...api.crunchyAuthHeader, 'ETP-Anonymous-ID': uuid }, - body: authData - }; - const authReq = await this.req.getData(api.auth, authReqOpts); - if(!authReq.ok || !authReq.res){ - console.error('Token Refresh Failed!'); - if (authReq.res?.status == 400) { - console.warn('Token is likely wrong, please login again!'); - } - return; - } - // To prevent any Cloudflare errors in the future - if (authReq.res.headers.get('Set-Cookie')) { - api.crunchyDefHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; - api.crunchyAuthHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; - } - if (authReq.headers && authReq.headers['Set-Cookie']) { - api.crunchyDefHeader['Cookie'] = authReq.headers['Set-Cookie']; - api.crunchyAuthHeader['Cookie'] = authReq.headers['Set-Cookie']; - api.crunchyDefHeader['User-Agent'] = authReq.headers['User-Agent']; - api.crunchyAuthHeader['User-Agent'] = authReq.headers['User-Agent']; - } - this.token = await authReq.res.json(); - this.token.device_id = uuid; - this.token.expires = new Date(Date.now() + this.token.expires_in); - yamlCfg.saveCRToken(this.token); - } - if(this.token.refresh_token) { - await this.getProfile(silent); - } else { - console.info('USER: Anonymous'); - } - await this.getCMStoken(ifNeeded); - } - - public async getCMStoken(ifNeeded = false) { - if(!this.token.access_token){ - console.error('No access token!'); - return; - } - - if (ifNeeded && this.cmsToken.cms_web) { - if (!(Date.now() >= new Date(this.cmsToken.cms_web.expires).getTime())) { - return; - } - } - - const cmsTokenReqOpts = { - headers: { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader - }, - useProxy: true - }; - const cmsTokenReq = await this.req.getData(api.cms_auth, cmsTokenReqOpts); - if(!cmsTokenReq.ok || !cmsTokenReq.res){ - console.error('Authentication CMS token failed!'); - return; - } - this.cmsToken = await cmsTokenReq.res.json(); - console.info('Your Country: %s\n', this.cmsToken.cms_web?.bucket.split('/')[1]); - } - - public async getCmsData(){ - // check token - if(!this.cmsToken.cms_web){ - console.error('Authentication required!'); - return; - } - // opts - const indexReqOpts = [ - api.cms_bucket, - this.cmsToken.cms_web.bucket, - '/index?', - new URLSearchParams({ - 'force_locale': '', - 'preferred_audio_language': 'ja-JP', - 'locale': this.locale, - 'Policy': this.cmsToken.cms_web.policy, - 'Signature': this.cmsToken.cms_web.signature, - 'Key-Pair-Id': this.cmsToken.cms_web.key_pair_id, - }), - ].join(''); - const indexReq = await this.req.getData(indexReqOpts, { - headers: { - 'User-Agent': api.crunchyDefUserAgent - } - }); - if(!indexReq.ok || ! indexReq.res){ - console.error('Get CMS index FAILED!'); - return; - } - console.info(await indexReq.res.json()); - } - - public async doSearch(data: SearchData): Promise{ - if(!this.token.access_token){ - console.error('Authentication required!'); - return { isOk: false, reason: new Error('Not authenticated') }; - } - const searchReqOpts = { - headers: { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader - }, - useProxy: true - }; - const searchStart = data.page ? (data.page-1)*5 : 0; - const searchParams = new URLSearchParams({ - q: data.search, - n: '5', - start: data.page ? `${(data.page-1)*5}` : '0', - type: data['search-type'] ?? getDefault('search-type', this.cfg.cli), - locale: this.locale, - }).toString(); - const searchReq = await this.req.getData(`${api.search}?${searchParams}`, searchReqOpts); - if(!searchReq.ok || ! searchReq.res){ - console.error('Search FAILED!'); - return { isOk: false, reason: new Error('Search failed. No more information provided') }; - } - const searchResults = await searchReq.res.json() as CrunchySearch; - if(searchResults.total < 1){ - console.info('Nothing Found!'); - return { isOk: true, value: [] }; - } - - const searchTypesInfo = { - 'top_results': 'Top results', - 'series': 'Found series', - 'movie_listing': 'Found movie lists', - 'episode': 'Found episodes' - }; - for(const search_item of searchResults.data){ - console.info('%s:', searchTypesInfo[search_item.type as keyof typeof searchTypesInfo]); - // calculate pages - const pageCur = searchStart > 0 ? Math.ceil(searchStart/5) + 1 : 1; - const pageMax = Math.ceil(search_item.count/5); - // pages per category - if(search_item.count < 1){ - console.info(' Nothing Found...'); - } - if(search_item.count > 0){ - if(pageCur > pageMax){ - console.info(' Last page is %s...', pageMax); - continue; - } - for(const item of search_item.items){ - await this.logObject(item); - } - console.info(` Total results: ${search_item.count} (Page: ${pageCur}/${pageMax})`); - } - } - const toSend = searchResults.data.filter(a => a.type === 'series' || a.type === 'movie_listing'); - return { isOk: true, value: toSend.map(a => { - return a.items.map((a): SearchResponseItem => { - const images = (a.images.poster_tall ?? [[ { source: '/notFound.png' } ]])[0]; - return { - id: a.id, - image: images[Math.floor(images.length / 2)].source, - name: a.title, - rating: -1, - desc: a.description - }; - }); - }).reduce((pre, cur) => pre.concat(cur))}; - } - - public async logObject(item: ParseItem, pad?: number, getSeries?: boolean, getMovieListing?: boolean){ - if(this.debug){ - console.info(item); - } - pad = pad ?? 2; - getSeries = getSeries === undefined ? true : getSeries; - getMovieListing = getMovieListing === undefined ? true : getMovieListing; - item.isSelected = item.isSelected === undefined ? false : item.isSelected; - if(!item.type) { - item.type = item.__class__; - } - - //guess item type - //TODO: look into better methods of getting item type - let iType = item.type; - if (!iType) { - if (item.episode_number) { - iType = 'episode'; - } else if (item.season_number) { - iType = 'season'; - } else if (item.season_count) { - iType = 'series'; - } else if (item.media_type == 'movie') { - iType = 'movie'; - } else if (item.movie_release_year) { - iType = 'movie_listing'; - } else { - if (item.identifier !== '') { - const iTypeCheck = item.identifier?.split('|'); - if (iTypeCheck) { - if (iTypeCheck[1] == 'M') { - iType = 'movie'; - } else if (!iTypeCheck[2]) { - iType = 'season'; - } else { - iType = 'episode'; - } - } else { - iType = 'series'; - } - } else { - iType = 'movie_listing'; - } - } - item.type = iType; - } - - const oTypes = { - 'series': 'Z', // SRZ - 'season': 'S', // VOL - 'episode': 'E', // EPI - 'movie_listing': 'F', // FLM - 'movie': 'M', // MED - }; - // check title - item.title = item.title != '' ? item.title : 'NO_TITLE'; - // static data - const oMetadata: string[] = [], - oBooleans: string[] = [], - tMetadata = item.type + '_metadata', - iMetadata = (Object.prototype.hasOwnProperty.call(item, tMetadata) ? item[tMetadata as keyof ParseItem] : item) as Record, - iTitle = [ item.title ]; - - const audio_languages: string[] = []; - - // set object booleans - if(iMetadata.duration_ms){ - oBooleans.push(Helper.formatTime(iMetadata.duration_ms/1000)); - } - if(iMetadata.is_simulcast) { - oBooleans.push('SIMULCAST'); - } - if(iMetadata.is_mature) { - oBooleans.push('MATURE'); - } - if (item.versions) { - for(const version of item.versions) { - audio_languages.push(version.audio_locale); - if (version.original) { - oBooleans.push('SUB'); - } else { - if (!oBooleans.includes('DUB')) { - oBooleans.push('DUB'); - } - } - } - } else { - if(iMetadata.is_subbed){ - oBooleans.push('SUB'); - } - if(iMetadata.is_dubbed){ - oBooleans.push('DUB'); - } - } - if(item.playback && item.type != 'movie_listing') { - oBooleans.push('STREAM'); - } - // set object metadata - if(iMetadata.season_count){ - oMetadata.push(`Seasons: ${iMetadata.season_count}`); - } - if(iMetadata.episode_count){ - oMetadata.push(`EPs: ${iMetadata.episode_count}`); - } - if(item.season_number && !iMetadata.hide_season_title && !iMetadata.hide_season_number){ - oMetadata.push(`Season: ${item.season_number}`); - } - if(item.type == 'episode'){ - if(iMetadata.episode){ - iTitle.unshift(iMetadata.episode); - } - if(!iMetadata.hide_season_title && iMetadata.season_title){ - iTitle.unshift(iMetadata.season_title); - } - } - if(item.is_premium_only){ - iTitle[0] = `☆ ${iTitle[0]}`; - } - // display metadata - if(item.hide_metadata){ - iMetadata.hide_metadata = item.hide_metadata; - } - const showObjectMetadata = oMetadata.length > 0 && !iMetadata.hide_metadata; - const showObjectBooleans = oBooleans.length > 0 && !iMetadata.hide_metadata; - // make obj ids - const objects_ids: string[] = []; - objects_ids.push(oTypes[item.type as keyof typeof oTypes] + ':' + item.id); - if(item.seq_id){ - objects_ids.unshift(item.seq_id); - } - if(item.f_num){ - objects_ids.unshift(item.f_num); - } - if(item.s_num){ - objects_ids.unshift(item.s_num); - } - if(item.external_id){ - objects_ids.push(item.external_id); - } - if(item.ep_num){ - objects_ids.push(item.ep_num); - } - - // show entry - console.info( - '%s%s[%s] %s%s%s', - ''.padStart(item.isSelected ? pad-1 : pad, ' '), - item.isSelected ? '✓' : '', - objects_ids.join('|'), - iTitle.join(' - '), - showObjectMetadata ? ` (${oMetadata.join(', ')})` : '', - showObjectBooleans ? ` [${oBooleans.join(', ')}]` : '', - - ); - if(item.last_public){ - console.info(''.padStart(pad+1, ' '), '- Last updated:', item.last_public); - } - if(item.subtitle_locales){ - iMetadata.subtitle_locales = item.subtitle_locales; - } - if (item.versions && audio_languages.length > 0) { - console.info( - '%s- Versions: %s', - ''.padStart(pad + 2, ' '), - langsData.parseSubtitlesArray(audio_languages) - ); - } - if(iMetadata.subtitle_locales && iMetadata.subtitle_locales.length > 0){ - console.info( - '%s- Subtitles: %s', - ''.padStart(pad + 2, ' '), - langsData.parseSubtitlesArray(iMetadata.subtitle_locales) - ); - } - if(item.availability_notes){ - console.info( - '%s- Availability notes: %s', - ''.padStart(pad + 2, ' '), - item.availability_notes.replace(/\[[^\]]*]?/gm, '') - ); - } - if(item.type == 'series' && getSeries){ - await this.logSeriesById(item.id, pad, true); - console.info(''); - } - if(item.type == 'movie_listing' && getMovieListing){ - await this.logMovieListingById(item.id, pad+2); - console.info(''); - } - } - - public async logSeriesById(id: string, pad?: number, hideSeriesTitle?: boolean){ - // parse - pad = pad || 0; - hideSeriesTitle = hideSeriesTitle !== undefined ? hideSeriesTitle : false; - // check token - if(!this.cmsToken.cms_web){ - console.error('Authentication required!'); - return; - } - // opts - const AuthHeaders = { - headers: { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader - }, - useProxy: true - }; - // reqs - if(!hideSeriesTitle){ - const seriesReq = await this.req.getData(`${api.content_cms}/series/${id}?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); - if(!seriesReq.ok || !seriesReq.res){ - console.error('Series Request FAILED!'); - return; - } - const seriesData = await seriesReq.res.json(); - await this.logObject(seriesData.data[0], pad, false); - } - // seasons list - const seriesSeasonListReq = await this.req.getData(`${api.content_cms}/series/${id}/seasons?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); - if(!seriesSeasonListReq.ok || !seriesSeasonListReq.res){ - console.error('Series Request FAILED!'); - return; - } - // parse data - const seasonsList = await seriesSeasonListReq.res.json() as SeriesSearch; - if(seasonsList.total < 1){ - console.info('Series is empty!'); - return; - } - for(const item of seasonsList.data){ - await this.logObject(item, pad+2); - } - } - - public async logMovieListingById(id: string, pad?: number){ - pad = pad || 2; - if(!this.cmsToken.cms_web){ - console.error('Authentication required!'); - return; - } - - // opts - const AuthHeaders = { - headers: { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader - }, - useProxy: true - }; - - //Movie Listing - const movieListingReq = await this.req.getData(`${api.content_cms}/movie_listings/${id}?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); - if(!movieListingReq.ok || !movieListingReq.res){ - console.error('Movie Listing Request FAILED!'); - return; - } - const movieListing = await movieListingReq.res.json(); - if(movieListing.total < 1){ - console.info('Movie Listing is empty!'); - return; - } - for(const item of movieListing.data){ - await this.logObject(item, pad, false, false); - } - - //Movies - const moviesListReq = await this.req.getData(`${api.content_cms}/movie_listings/${id}/movies?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); - if(!moviesListReq.ok || !moviesListReq.res){ - console.error('Movies List Request FAILED!'); - return; - } - const moviesList = await moviesListReq.res.json(); - for(const item of moviesList.data){ - await this.logObject(item, pad + 2); - } - } - - public async getNewlyAdded(page: number = 1, type: string, raw: boolean = false, rawoutput?: string) { - if(!this.token.access_token){ - console.error('Authentication required!'); - return; - } - const newlyAddedReqOpts = { - headers: { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader - }, - useProxy: true - }; - const newlyAddedParams = new URLSearchParams({ - sort_by: 'newly_added', - type: type || 'series', - n: '50', - start: (page ? (page-1)*25 : 0).toString(), - preferred_audio_language: 'ja-JP', - force_locale: '', - locale: this.locale - }).toString(); - const newlyAddedReq = await this.req.getData(`${api.browse}?${newlyAddedParams}`, newlyAddedReqOpts); - if(!newlyAddedReq.ok || !newlyAddedReq.res){ - console.error('Get newly added FAILED!'); - return; - } - const newlyAddedResults = await newlyAddedReq.res.json(); - - if (raw) { - console.info(JSON.stringify(newlyAddedResults, null, 2)); - if (rawoutput) { - try { - fs.writeFileSync(rawoutput, JSON.stringify(newlyAddedResults), { encoding: 'utf-8' }); - console.info(`Raw output saved to ${rawoutput}`); - } catch (e) { - console.error(`Failed to save raw output to ${rawoutput}:`, e); - } - } - return; - } - - console.info('Newly added:'); - for(const i of newlyAddedResults.items){ - await this.logObject(i, 2); - } - // calculate pages - const itemPad = parseInt(new URL(newlyAddedResults.__href__, domain.cr_www).searchParams.get('start') as string); - const pageCur = itemPad > 0 ? Math.ceil(itemPad/25) + 1 : 1; - const pageMax = Math.ceil(newlyAddedResults.total/25); - console.info(` Total results: ${newlyAddedResults.total} (Page: ${pageCur}/${pageMax})`); - } - - public async getSeasonById(id: string, numbers: number, e: string|undefined, but: boolean, all: boolean) : Promise> { - if(!this.cmsToken.cms_web){ - console.error('Authentication required!'); - return { isOk: false, reason: new Error('Authentication required') }; - } - - const AuthHeaders = { - headers: { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader - }, - useProxy: true - }; - - - //get show info - const showInfoReq = await this.req.getData(`${api.content_cms}/seasons/${id}?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); - if(!showInfoReq.ok || !showInfoReq.res){ - console.error('Show Request FAILED!'); - return { isOk: false, reason: new Error('Show request failed. No more information provided.') }; - } - const showInfo = await showInfoReq.res.json(); - await this.logObject(showInfo.data[0], 0); - - let episodeList = { total: 0, data: [], meta: {} } as CrunchyEpisodeList; - //get episode info - const reqEpsListOpts = [ - api.cms_bucket, - this.cmsToken.cms_web.bucket, - '/episodes?', - new URLSearchParams({ - 'force_locale': '', - 'preferred_audio_language': 'ja-JP', - 'locale': this.locale, - 'season_id': id, - 'Policy': this.cmsToken.cms_web.policy, - 'Signature': this.cmsToken.cms_web.signature, - 'Key-Pair-Id': this.cmsToken.cms_web.key_pair_id, - }), - ].join(''); - const reqEpsList = await this.req.getData(reqEpsListOpts, AuthHeaders); - if(!reqEpsList.ok || !reqEpsList.res){ - console.error('Episode List Request FAILED!'); - return { isOk: false, reason: new Error('Episode List request failed. No more information provided.') }; - } - //CrunchyEpisodeList - const episodeListAndroid = await reqEpsList.res.json() as CrunchyAndroidEpisodes; - episodeList = { - total: episodeListAndroid.total, - data: episodeListAndroid.items, - meta: {} - }; - - const epNumList: { - ep: number[], - sp: number - } = { ep: [], sp: 0 }; - const epNumLen = numbers; - - if(episodeList.total < 1){ - console.info(' Season is empty!'); - return { isOk: true, value: [] }; - } - - const doEpsFilter = parseSelect(e as string); - const selectedMedia: CrunchyEpMeta[] = []; - - episodeList.data.forEach((item) => { - item.hide_season_title = true; - if(item.season_title == '' && item.series_title != ''){ - item.season_title = item.series_title; - item.hide_season_title = false; - item.hide_season_number = true; - } - if(item.season_title == '' && item.series_title == ''){ - item.season_title = 'NO_TITLE'; - item.series_title = 'NO_TITLE'; - } - const epNum = item.episode; - let isSpecial = false; - item.isSelected = false; - if(!epNum.match(/^\d+$/) || epNumList.ep.indexOf(parseInt(epNum, 10)) > -1){ - isSpecial = true; - epNumList.sp++; - } - else{ - epNumList.ep.push(parseInt(epNum, 10)); - } - const selEpId = ( - isSpecial - ? 'S' + epNumList.sp.toString().padStart(epNumLen, '0') - : '' + parseInt(epNum, 10).toString().padStart(epNumLen, '0') - ); - // set data - const images = (item.images.thumbnail ?? [[ { source: '/notFound.png' } ]])[0]; - const epMeta: CrunchyEpMeta = { - data: [ - { - mediaId: item.id, - versions: null, - lang: langsData.languages.find(a => a.code == yargs.appArgv(this.cfg.cli).dubLang[0]), - isSubbed: item.is_subbed, - isDubbed: item.is_dubbed - } - ], - seriesTitle: item.series_title, - seasonTitle: item.season_title, - episodeNumber: item.episode, - episodeTitle: item.title, - seasonID: item.season_id, - season: item.season_number, - showID: id, - e: selEpId, - image: images[Math.floor(images.length / 2)].source - }; - // Check for streams_link and update playback var if needed - if (item.__links__?.streams?.href) { - epMeta.data[0].playback = item.__links__.streams.href; - if(!item.playback) { - item.playback = item.__links__.streams.href; - } - } - if (item.streams_link) { - epMeta.data[0].playback = item.streams_link; - if(!item.playback) { - item.playback = item.streams_link; - } - } - if (item.versions) { - epMeta.data[0].versions = item.versions; - } - // find episode numbers - if((but && item.playback && !doEpsFilter.isSelected([selEpId, item.id])) || (all && item.playback) || (!but && doEpsFilter.isSelected([selEpId, item.id]) && !item.isSelected && item.playback)){ - selectedMedia.push(epMeta); - item.isSelected = true; - } - // show ep - item.seq_id = selEpId; - this.logObject(item); - }); - - // display - if(selectedMedia.length < 1){ - console.info('\nEpisodes not selected!\n'); - } - - console.info(''); - return { isOk: true, value: selectedMedia }; - } - - public async downloadEpisode(data: CrunchyEpMeta, options: CrunchyDownloadOptions, isSeries?: boolean): Promise { - const res = await this.downloadMediaList(data, options); - if (res === undefined || res.error) { - return false; - } else { - if (!options.skipmux) { - await this.muxStreams(res.data, { ...options, output: res.fileName }); - } else { - console.info('Skipping mux'); - } - if (!isSeries) { - downloaded({ - service: 'crunchy', - type: 's' - }, data.seasonID, [data.e]); - } else { - downloaded({ - service: 'crunchy', - type: 'srz' - }, data.showID, [data.e]); - } - } - return true; - } - - public async getObjectById(e?: string, earlyReturn?: boolean, external_id?: boolean): Promise[]|undefined> { - if(!this.cmsToken.cms_web){ - console.error('Authentication required!'); - return []; - } - - let convertedObjects; - if (external_id) { - const epFilter = parseSelect(e as string); - const objectIds = []; - for (const ob of epFilter.values) { - const extIdReqOpts = [ - api.cms_bucket, - this.cmsToken.cms_web.bucket, - '/channels/crunchyroll/objects', - '?', - new URLSearchParams({ - 'force_locale': '', - 'preferred_audio_language': 'ja-JP', - 'locale': this.locale, - 'external_id': ob, - 'Policy': this.cmsToken.cms_web.policy, - 'Signature': this.cmsToken.cms_web.signature, - 'Key-Pair-Id': this.cmsToken.cms_web.key_pair_id, - }), - ].join(''); - - const extIdReq = await this.req.getData(extIdReqOpts, { - headers: { - 'User-Agent': api.crunchyDefUserAgent - } - }); - if (!extIdReq.ok || !extIdReq.res) { - console.error('Objects Request FAILED!'); - if (extIdReq.error && extIdReq.error.res && extIdReq.error.res.body) { - console.info('[INFO] Body:', extIdReq.error.res.body); - } - continue; - } - - const oldObjectInfo = await extIdReq.res.json() as Record; - for (const object of oldObjectInfo.items) { - objectIds.push(object.id); - } - } - convertedObjects = objectIds.join(','); - } - - const doEpsFilter = parseSelect(convertedObjects ?? e as string); - - if(doEpsFilter.values.length < 1){ - console.info('\nObjects not selected!\n'); - return []; - } - - // node index.js --service crunchy -e G6497Z43Y,GRZXCMN1W,G62PEZ2E6,G25FVGDEK,GZ7UVPVX5 - console.info('Requested object ID: %s', doEpsFilter.values.join(', ')); - - const AuthHeaders = { - headers: { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader - }, - useProxy: true - }; - - // reqs - let objectInfo: ObjectInfo = { total: 0, data: [], meta: {} }; - const objectReqOpts = [ - api.cms_bucket, - this.cmsToken.cms_web.bucket, - '/objects/', - doEpsFilter.values.join(','), - '?', - new URLSearchParams({ - 'force_locale': '', - 'preferred_audio_language': 'ja-JP', - 'locale': this.locale, - 'Policy': this.cmsToken.cms_web.policy, - 'Signature': this.cmsToken.cms_web.signature, - 'Key-Pair-Id': this.cmsToken.cms_web.key_pair_id, - }), - ].join(''); - const objectReq = await this.req.getData(objectReqOpts, AuthHeaders); - if(!objectReq.ok || !objectReq.res){ - console.error('Objects Request FAILED!'); - if(objectReq.error && objectReq.error.res && objectReq.error.res.body){ - const objectInfo = await objectReq.error.res.json(); - console.info('Body:', JSON.stringify(objectInfo, null, '\t')); - objectInfo.error = true; - return objectInfo; - } - return []; - } - const objectInfoAndroid = await objectReq.res.json() as CrunchyAndroidObject; - objectInfo = { - total: objectInfoAndroid.total, - data: objectInfoAndroid.items, - meta: {} - }; - - if(earlyReturn){ - return objectInfo; - } - - const selectedMedia: Partial[] = []; - - for(const item of objectInfo.data){ - if(item.type != 'episode' && item.type != 'movie'){ - await this.logObject(item, 2, true, false); - continue; - } - const epMeta: Partial = {}; - - epMeta.data = []; - if (item.episode_metadata) { - item.s_num = 'S:' + item.episode_metadata.season_id; - epMeta.data = [ - { - mediaId: 'E:'+ item.id, - versions: item.episode_metadata.versions, - isSubbed: item.episode_metadata.is_subbed, - isDubbed: item.episode_metadata.is_dubbed - } - ]; - epMeta.seriesTitle = item.episode_metadata.series_title; - epMeta.seasonTitle = item.episode_metadata.season_title; - epMeta.episodeNumber = item.episode_metadata.episode; - epMeta.episodeTitle = item.title; - epMeta.season = item.episode_metadata.season_number; - } else if (item.movie_listing_metadata) { - item.f_num = 'F:' + item.id; - epMeta.data = [ - { - mediaId: 'M:'+ item.id, - isSubbed: item.movie_listing_metadata.is_subbed, - isDubbed: item.movie_listing_metadata.is_dubbed - } - ]; - epMeta.seriesTitle = item.title; - epMeta.seasonTitle = item.title; - epMeta.episodeNumber = 'Movie'; - epMeta.episodeTitle = item.title; - } else if (item.movie_metadata) { - item.f_num = 'F:' + item.id; - epMeta.data = [ - { - mediaId: 'M:'+ item.id, - isSubbed: item.movie_metadata.is_subbed, - isDubbed: item.movie_metadata.is_dubbed - } - ]; - epMeta.season = 0; - epMeta.seriesTitle = item.title; - epMeta.seasonTitle = item.title; - epMeta.episodeNumber = 'Movie'; - epMeta.episodeTitle = item.title; - } - if (item.streams_link) { - epMeta.data[0].playback = item.streams_link; - if(!item.playback) { - item.playback = item.streams_link; - } - selectedMedia.push(epMeta); - item.isSelected = true; - } else if (item.__links__) { - epMeta.data[0].playback = item.__links__.streams.href; - if(!item.playback) { - item.playback = item.__links__.streams.href; - } - selectedMedia.push(epMeta); - item.isSelected = true; - } - await this.logObject(item, 2); - } - console.info(''); - return selectedMedia; - } - - private convertDownloadToPlayback(audioUrl: string, videoUrl: string): string { - try { - const url = new URL(audioUrl); - const urla = new URL(videoUrl); - url.pathname = url.pathname.replace('/manifest/download/', '/manifest/'); - url.searchParams.delete('downloadGuid'); - url.searchParams.set('playbackGuid', urla.searchParams.get('playbackGuid') as string); - - return url.toString(); - } catch (err) { - return audioUrl; - } - } - - public async downloadMediaList(medias: CrunchyEpMeta, options: CrunchyDownloadOptions) : Promise<{ - data: DownloadedMedia[], - fileName: string, - error: boolean - } | undefined> { - if(!this.cmsToken.cms_web){ - console.error('Authentication required!'); - return; - } - - if (!this.cfg.bin.ffmpeg) - this.cfg.bin = await yamlCfg.loadBinCfg(); - - let mediaName = '...'; - let fileName; - const variables: Variable[] = []; - if(medias.seasonTitle && medias.episodeNumber && medias.episodeTitle){ - mediaName = `${medias.seasonTitle} - ${medias.episodeNumber} - ${medias.episodeTitle}`; - } - - const files: DownloadedMedia[] = []; - - if(medias.data.every(a => !a.playback)){ - console.warn('Video not available!'); - return undefined; - } - - let dlFailed = false; - let dlVideoOnce = false; // Variable to save if best selected video quality was downloaded - - - for (const mMeta of medias.data) { - console.info(`Requesting: [${mMeta.mediaId}] ${mediaName}`); - - // Make sure we have a media id without a : in it - const currentMediaId = (mMeta.mediaId.includes(':') ? mMeta.mediaId.split(':')[1] : mMeta.mediaId); - - //Make sure token is up-to-date - await this.refreshToken(true, true); - let currentVersion; - let isPrimary = mMeta.isSubbed; - const AuthHeaders: RequestInit = { - headers: { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader - } - }; - - //Get Media GUID - let mediaId = mMeta.mediaId; - if (mMeta.versions) { - if (mMeta.lang) { - currentVersion = mMeta.versions.find(a => a.audio_locale == mMeta.lang?.cr_locale); - } else if (options.dubLang.length == 1) { - const currentLang = langsData.languages.find(a => a.code == options.dubLang[0]); - currentVersion = mMeta.versions.find(a => a.audio_locale == currentLang?.cr_locale); - } else if (mMeta.versions.length == 1) { - currentVersion = mMeta.versions[0]; - } - if (!currentVersion?.media_guid) { - console.error('Selected language not found in versions.'); - continue; - } - isPrimary = currentVersion.original; - mediaId = currentVersion?.media_guid; - } - - // If for whatever reason mediaId has a :, return the ID only - if (mediaId.includes(':')) - mediaId = mediaId.split(':')[1]; - - const compiledChapters: string[] = []; - if (options.chapters) { - //Make Chapter Request - const chapterRequest = await this.req.getData(`https://static.crunchyroll.com/skip-events/production/${currentMediaId}.json`, { - headers: api.crunchyDefHeader - }); - if(!chapterRequest.ok || !chapterRequest.res){ - //Old Chapter Request Fallback - console.warn('Chapter request failed, attempting old API'); - const oldChapterRequest = await this.req.getData(`https://static.crunchyroll.com/datalab-intro-v2/${currentMediaId}.json`, { - headers: api.crunchyDefHeader - }); - if(!oldChapterRequest.ok || !oldChapterRequest.res) { - console.warn('Old Chapter API request failed'); - } else { - console.info('Old Chapter request successful'); - const chapterData = await oldChapterRequest.res.json() as CrunchyOldChapter; - - //Generate Timestamps - const startTime = new Date(0), endTime = new Date(0); - startTime.setSeconds(chapterData.startTime); - endTime.setSeconds(chapterData.endTime); - const startTimeMS = String(chapterData.startTime).split('.')[1], endTimeMS = String(chapterData.endTime).split('.')[1]; - const startMS = startTimeMS ? startTimeMS : '00', endMS = endTimeMS ? endTimeMS : '00'; - const startFormatted = startTime.toISOString().substring(11, 19)+'.'+startMS; - const endFormatted = endTime.toISOString().substring(11, 19)+'.'+endMS; - - //Push Generated Chapters - if (chapterData.startTime > 1) { - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=00:00:00.00`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=Prologue` - ); - } - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=${startFormatted}`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=Intro` - ); - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=${endFormatted}`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=Episode` - ); - } - } else { - //Chapter request succeeded, now let's parse them - console.info('Chapter request successful'); - const chapterData = await chapterRequest.res.json() as CrunchyChapters; - const chapters: CrunchyChapter[] = []; - - //Make a format more usable for the crunchy chapters - for (const chapter in chapterData) { - if (typeof chapterData[chapter] == 'object') { - chapters.push(chapterData[chapter]); - } - } - - if (chapters.length > 0) { - chapters.sort((a, b) => a.start - b.start); - //Check if chapters has an intro - //if (!(chapters.find(c => c.type === 'intro') || chapters.find(c => c.type === 'recap'))) { - if (!(chapters.find(c => c.type === 'intro'))) { - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=00:00:00.00`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=Episode` - ); - } - - //Loop through all the chapters - for (const chapter of chapters) { - if (typeof chapter.start == 'undefined' || typeof chapter.end == 'undefined') continue; - //Generate timestamps - const startTime = new Date(0), endTime = new Date(0); - startTime.setSeconds(chapter.start); - endTime.setSeconds(chapter.end); - const startFormatted = startTime.toISOString().substring(11, 19)+'.00'; - const endFormatted = endTime.toISOString().substring(11, 19)+'.00'; - //Find the max start time from the chapters - const maxStart = Math.max( - ...chapters - .map(obj => obj.start) - .filter((start): start is number => start !== null && start !== undefined) - ); - //We need the duration of the ep - let epDuration: number | undefined; - const epiMeta = await this.req.getData(`${api.content_cms}/objects/${currentMediaId}?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); - if(!epiMeta.ok || !epiMeta.res){ - epDuration = 7200; - } else { - epDuration = Math.floor((await epiMeta.res.json()).data[0].episode_metadata.duration_ms / 1000 - 3); - } - - //Push generated chapters - if (chapter.type == 'intro') { - if (chapter.start > 0) { - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=00:00:00.00`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=Episode` - ); - } - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=${startFormatted}`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=${chapter.type.charAt(0).toUpperCase() + chapter.type.slice(1)}` - ); - if (chapter.end < epDuration && chapter.end != maxStart) { - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=${endFormatted}`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=Episode` - ); - } - } else { - if (chapter.type !== 'recap') { - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=${startFormatted}`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=${chapter.type.charAt(0).toUpperCase() + chapter.type.slice(1)}` - ); - if (chapter.end < epDuration && chapter.end != maxStart) { - compiledChapters.push( - `CHAPTER${(compiledChapters.length/2)+1}=${endFormatted}`, - `CHAPTER${(compiledChapters.length/2)+1}NAME=Episode` - ); - } - } - } - } - } - } - } - - const pbData = { total: 0, vpb: {}, apb: {}, meta: {} } as PlaybackData; - - let videoStream: CrunchyPlayStream | null = null; - let audioStream: CrunchyPlayStream | null = null; - let isDLVideoBypass: boolean = options.vstream === 'android' || options.vstream === 'androidtab' ? true : false; - let isDLAudioBypass: boolean = options.astream === 'android' || options.astream === 'androidtab' ? true : false; - let isDLBypassCapable: boolean = true; - - if (isDLVideoBypass || isDLAudioBypass) { - const me = await this.req.getData(api.me, AuthHeaders); - if (me.ok && me.res) { - const data_me = await me.res.json(); - const benefits = await this.req.getData(`https://www.crunchyroll.com/subs/v1/subscriptions/${data_me.external_id}/benefits`, AuthHeaders); - if (benefits.ok && benefits.res) { - const data_benefits = await benefits.res.json() as { items: { benefit: string }[] }; - if (data_benefits?.items && !data_benefits.items.find(i => i.benefit === 'offline_viewing')) { - isDLBypassCapable = false; - } - } else { - isDLBypassCapable = false; - } - } else { - isDLBypassCapable = false; - } - } - - if (isDLVideoBypass && !isDLBypassCapable) { - isDLVideoBypass = false; - options.vstream = 'androidtv'; - console.warn('VBR video downloads are not available on your current Crunchyroll plan. Please upgrade to the "Mega Fan" plan to enable this feature. Falling back to CBR video stream.'); - } - - if (isDLAudioBypass && !isDLBypassCapable) { - isDLAudioBypass = false; - options.astream = 'androidtv'; - console.warn('192 kb/s audio downloads are not available on your current Crunchyroll plan. Please upgrade to the "Mega Fan" plan to enable this feature. Falling back to 128 kb/s CBR stream.'); - } - - if (options.tsd) { - console.warn('Total Session Death Active'); - const activeStreamsReq = await this.req.getData(api.streaming_sessions, AuthHeaders); - if (activeStreamsReq.ok && activeStreamsReq.res){ - const data = await activeStreamsReq.res.json(); - for (const s of data.items) { - await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${s.contentId}/${s.token}`, {...{method: 'DELETE'}, ...AuthHeaders}); - } - console.warn(`Killed ${data.items?.length ?? 0} Sessions`); - } - } - - const videoPlaybackReq = await this.req.getData(`https://www.crunchyroll.com/playback/v3/${currentVersion ? currentVersion.guid : currentMediaId}/${CrunchyVideoPlayStreams['androidtv']}/play?queue=0`, AuthHeaders); - if (!videoPlaybackReq.ok || !videoPlaybackReq.res) { - console.warn('Request Video Stream URLs FAILED!'); - } else { - videoStream = await videoPlaybackReq.res.json() as CrunchyPlayStream; - const derivedPlaystreams = {} as CrunchyStreams; - for (const hardsub in videoStream.hardSubs) { - const stream = videoStream.hardSubs[hardsub]; - derivedPlaystreams[hardsub] = { - url: stream.url, - 'hardsub_locale': stream.hlang - }; - } - if (isDLVideoBypass) { - const videoDLReq = await this.req.getData(`https://www.crunchyroll.com/playback/v3/${currentVersion ? currentVersion.guid : currentMediaId}/${CrunchyVideoPlayStreams[options.vstream]}/download`, AuthHeaders); - if (videoDLReq.ok && videoDLReq.res) { - const data = await videoDLReq.res.json() as CrunchyPlayStream; - derivedPlaystreams[''] = { - url: this.convertDownloadToPlayback(data.url, videoStream.url), - hardsub_locale: '' - }; - } else { - derivedPlaystreams[''] = { - url: videoStream.url, - hardsub_locale: '' - }; - } - } else { - derivedPlaystreams[''] = { - url: videoStream.url, - hardsub_locale: '' - }; - } - pbData.meta = { - audio_locale: videoStream.audioLocale, - bifs: [videoStream.bifs], - captions: videoStream.captions, - closed_captions: videoStream.captions, - media_id: videoStream.assetId, - subtitles: videoStream.subtitles, - versions: videoStream.versions - }; - pbData.vpb[`adaptive_${options.vstream}_${videoStream.url.includes('m3u8') ? 'hls' : 'dash'}_drm`] = { - ...derivedPlaystreams - }; - } - - if (!options.cstream && (options.vstream !== options.astream) && videoStream) { - const audioPlaybackReq = await this.req.getData(`https://www.crunchyroll.com/playback/v3/${currentVersion ? currentVersion.guid : currentMediaId}/${CrunchyAudioPlayStreams[options.astream]}/${isDLAudioBypass ? 'download' : 'play?queue=1'}`, AuthHeaders); - if (!audioPlaybackReq.ok || !audioPlaybackReq.res) { - console.warn('Request Audio Stream URLs FAILED!'); - } else { - audioStream = await audioPlaybackReq.res.json() as CrunchyPlayStream; - const derivedPlaystreams = {} as CrunchyStreams; - for (const hardsub in audioStream.hardSubs) { - const stream = audioStream.hardSubs[hardsub]; - derivedPlaystreams[hardsub] = { - url: stream.url, - 'hardsub_locale': stream.hlang - }; - } - if (isDLAudioBypass) { - audioStream.token = videoStream.token; - derivedPlaystreams[''] = { - url: this.convertDownloadToPlayback(audioStream.url, videoStream.url), - hardsub_locale: '' - }; - } else { - derivedPlaystreams[''] = { - url: audioStream.url, - hardsub_locale: '' - }; - }; - pbData.meta = { - audio_locale: audioStream.audioLocale, - bifs: [audioStream.bifs], - captions: audioStream.captions, - closed_captions: audioStream.captions, - media_id: audioStream.assetId, - subtitles: audioStream.subtitles, - versions: audioStream.versions - }; - pbData.apb[`adaptive_${options.astream}_${audioStream.url.includes('m3u8') ? 'hls' : 'dash'}_drm`] = { - ...derivedPlaystreams - }; - } - } else { - pbData.apb = pbData.vpb; - } - - variables.push(...([ - ['title', medias.episodeTitle, true], - ['episode', isNaN(parseFloat(medias.episodeNumber)) ? medias.episodeNumber : parseFloat(medias.episodeNumber), false], - ['service', 'CR', false], - ['seriesTitle', medias.seriesTitle, true], - ['showTitle', medias.seriesTitle ?? medias.seasonTitle, true], - ['season', medias.season, false] - ] as [AvailableFilenameVars, string|number, boolean][]).map((a): Variable => { - return { - name: a[0], - replaceWith: a[1], - type: typeof a[1], - sanitize: a[2] - } as Variable; - })); - - let vstreams: any[] = []; - let astreams: any[] = []; - let hsLangs: string[] = []; - const vpbStreams = pbData.vpb; - const apbStreams = pbData.apb; - - if (!canDecrypt && (!options.novids || !options.noaudio)) { - console.error('No valid Widevine or PlayReady CDM detected. Please ensure a supported and functional CDM is installed.'); - return undefined; - } - - if (!this.cfg.bin.mp4decrypt && !this.cfg.bin.shaka && (!options.novids || !options.noaudio)) { - console.error('Neither Shaka nor MP4Decrypt found. Please ensure at least one of them is installed.'); - return undefined; - } - - for (const s of Object.keys(pbData.vpb)) { - if ( - (s.match(/hls/) || s.match(/dash/)) - && !(s.match(/hls/) && s.match(/drm/)) - && !s.match(/trailer/) - ) { - const pb = Object.values(vpbStreams[s]).map(v => { - v.hardsub_lang = v.hardsub_locale - ? langsData.fixAndFindCrLC(v.hardsub_locale).locale - : v.hardsub_locale; - if(v.hardsub_lang && hsLangs.indexOf(v.hardsub_lang) < 0){ - hsLangs.push(v.hardsub_lang); - } - return { - ...v, - ...{ format: s } - }; - }); - vstreams.push(...pb); - } - } - - for (const s of Object.keys(pbData.apb)) { - if ( - (s.match(/hls/) || s.match(/dash/)) - && !(s.match(/hls/) && s.match(/drm/)) - && !s.match(/trailer/) - ) { - const pb = Object.values(apbStreams[s]).map(v => { - v.hardsub_lang = v.hardsub_locale - ? langsData.fixAndFindCrLC(v.hardsub_locale).locale - : v.hardsub_locale; - if(v.hardsub_lang && hsLangs.indexOf(v.hardsub_lang) < 0){ - hsLangs.push(v.hardsub_lang); - } - return { - ...v, - ...{ format: s } - }; - }); - astreams.push(...pb); - } - } - - if (vstreams.length < 1) { - console.warn('No full video streams found!'); - return undefined; - } - - if (astreams.length < 1) { - console.warn('No full audio streams found!'); - return undefined; - } - - const audDub = langsData.findLang(langsData.fixLanguageTag(pbData.meta.audio_locale as string) || '').code; - hsLangs = langsData.sortTags(hsLangs); - - vstreams = vstreams.map((s) => { - s.audio_lang = audDub; - s.hardsub_lang = s.hardsub_lang ? s.hardsub_lang : '-'; - s.type = `${s.format}/${s.audio_lang}/${s.hardsub_lang}`; - return s; - }); - - vstreams = vstreams.sort((a, b) => { - if (a.type < b.type) { - return -1; - } - return 0; - }); - - astreams = astreams.map((s) => { - s.audio_lang = audDub; - s.hardsub_lang = s.hardsub_lang ? s.hardsub_lang : '-'; - s.type = `${s.format}/${s.audio_lang}/${s.hardsub_lang}`; - return s; - }); - - astreams = astreams.sort((a, b) => { - if (a.type < b.type) { - return -1; - } - return 0; - }); - - if(options.hslang != 'none'){ - if(hsLangs.indexOf(options.hslang) > -1){ - console.info('Selecting stream with %s hardsubs', langsData.locale2language(options.hslang).language); - vstreams = vstreams.filter((s) => { - if(s.hardsub_lang == '-'){ - return false; - } - return s.hardsub_lang == options.hslang; - }); - astreams = astreams.filter((s) => { - if(s.hardsub_lang == '-'){ - return false; - } - return s.hardsub_lang == options.hslang; - }); - } - else{ - console.warn('Selected stream with %s hardsubs not available', langsData.locale2language(options.hslang).language); - if(hsLangs.length > 0){ - console.warn('Try other hardsubs stream:', hsLangs.join(', ')); - } - dlFailed = true; - } - } else { - vstreams = vstreams.filter((s) => { - return s.hardsub_lang == '-'; - }); - astreams = astreams.filter((s) => { - return s.hardsub_lang == '-'; - }); - if(vstreams.length < 1){ - console.warn('Raw video streams not available!'); - if(hsLangs.length > 0){ - console.warn('Try hardsubs stream:', hsLangs.join(', ')); - } - dlFailed = true; - } - if(astreams.length < 1){ - console.warn('Raw audio streams not available!'); - if(hsLangs.length > 0){ - console.warn('Try hardsubs stream:', hsLangs.join(', ')); - } - dlFailed = true; - } - console.info('Selecting raw stream'); - } - - let vcurStream: - undefined|typeof vstreams[0] - = undefined; - let acurStream: - undefined|typeof astreams[0] - = undefined; - - if (!dlFailed) { - console.info('Downloading...'); - vcurStream = vstreams[0]; - acurStream = astreams[0]; - - console.info('Video Playlists URL: %s (%s)', vcurStream.url, vcurStream.type); - console.info('Audio Playlists URL: %s (%s)', acurStream.url, acurStream.type); - } - - let tsFile = undefined; - - // Delete the stream if it's not needed - if (options.novids && options.noaudio) { - if (videoStream) { - await this.refreshToken(true, true); - await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${videoStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders}); - } - if (audioStream && (videoStream?.token !== audioStream.token)) { - await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${audioStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders}); - } - } - - if(!dlFailed && vcurStream && acurStream && vcurStream !== undefined && acurStream !== undefined && !(options.novids && options.noaudio)){ - const vstreamPlaylistsReq = await this.req.getData(vcurStream.url, AuthHeaders); - const astreamPlaylistsReq = vcurStream.url !== acurStream.url ? await this.req.getData(acurStream.url, AuthHeaders) : vstreamPlaylistsReq; - if(!vstreamPlaylistsReq.ok || !vstreamPlaylistsReq.res || !astreamPlaylistsReq.ok || !astreamPlaylistsReq.res){ - console.error('CAN\'T FETCH VIDEO PLAYLISTS!'); - dlFailed = true; - } else { - const vstreamPlaylistBody = await vstreamPlaylistsReq.res.text(); - const astreamPlaylistBody = vcurStream.url !== acurStream.url ? await astreamPlaylistsReq.res.text() : vstreamPlaylistBody; - if (vstreamPlaylistBody.match('MPD') && astreamPlaylistBody.match('MPD')) { - //Parse MPD Playlists - const vstreamPlaylists = await parse(vstreamPlaylistBody, langsData.findLang(langsData.fixLanguageTag(pbData.meta.audio_locale as string) || ''), vcurStream.url.match(/.*\.urlset\//)?.[0]); - const astreamPlaylists = vcurStream.url !== acurStream.url ? await parse(astreamPlaylistBody, langsData.findLang(langsData.fixLanguageTag(pbData.meta.audio_locale as string) || ''), acurStream.url.match(/.*\.urlset\//)?.[0]) : vstreamPlaylists; - - //Get name of CDNs/Servers - const vstreamServers = Object.keys(vstreamPlaylists); - const astreamServers = Object.keys(astreamPlaylists); - - options.x = options.x > vstreamServers.length ? 1 : options.x; - - const vselectedServer = vstreamServers[options.x - 1]; - const vselectedList = vstreamPlaylists[vselectedServer]; - - const aselectedServer = astreamServers[options.x - 1]; - const aselectedList = astreamPlaylists[aselectedServer]; - - //set Video Qualities - const videos = vselectedList.video.map(item => { - return { - ...item, - resolutionText: `${item.quality.width}x${item.quality.height} (${Math.round(item.bandwidth/1024)}KiB/s)` - }; - }); - - const audios = aselectedList.audio.map(item => { - return { - ...item, - resolutionText: `${Math.round(item.bandwidth/1000)}kB/s` - }; - }); - - - videos.sort((a, b) => { - return a.quality.width - b.quality.width; - }); - - audios.sort((a, b) => { - return a.bandwidth - b.bandwidth; - }); - - let chosenVideoQuality = options.q === 0 ? videos.length : options.q; - if(chosenVideoQuality > videos.length) { - console.warn(`The requested quality of ${options.q} is greater than the maximum ${videos.length}.\n[WARN] Therefor the maximum will be capped at ${videos.length}.`); - chosenVideoQuality = videos.length; - } - chosenVideoQuality--; - - let chosenAudioQuality = options.q === 0 ? audios.length : options.q; - if(chosenAudioQuality > audios.length) { - chosenAudioQuality = audios.length; - } - chosenAudioQuality--; - - - const chosenVideoSegments = videos[chosenVideoQuality]; - const chosenAudioSegments = audios[chosenAudioQuality]; - - console.info(`Available Video Qualities:\n\t${videos.map((a, ind) => `[${ind+1}] ${a.resolutionText}`).join('\n\t')}`); - console.info(`Available Audio Qualities:\n\t${audios.map((a, ind) => `[${ind+1}] ${a.resolutionText}`).join('\n\t')}`); - - variables.push({ - name: 'height', - type: 'number', - replaceWith: chosenVideoSegments.quality.height - }, { - name: 'width', - type: 'number', - replaceWith: chosenVideoSegments.quality.width - }); - - const lang = langsData.languages.find(a => a.code === acurStream?.audio_lang); - if (!lang) { - console.error(`Unable to find language for code ${acurStream.audio_lang}`); - return; - } - console.info(`Selected quality: \n\tVideo: ${chosenVideoSegments.resolutionText}\n\tAudio: ${chosenAudioSegments.resolutionText}\n\tVideo Server: ${vselectedServer}\n\tAudio Server: ${aselectedServer}`); - console.info('Stream URL:', chosenVideoSegments.segments[0].uri.split(',.urlset')[0]); - // TODO check filename - fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); - const outFile = parseFileName(options.fileName + '.' + (mMeta.lang?.name || lang.name), variables, options.numbers, options.override).join(path.sep); - const tempFile = parseFileName(`temp-${currentVersion ? currentVersion.guid : currentMediaId}`, variables, options.numbers, options.override).join(path.sep); - const tempTsFile = path.isAbsolute(tempFile as string) ? tempFile : path.join(this.cfg.dir.content, tempFile); - - let encryptionKeysVideo; - let encryptionKeysAudio; - - //Handle Getting Decryption Keys if needed - if ((chosenVideoSegments.pssh_wvd ||chosenVideoSegments.pssh_prd || chosenAudioSegments.pssh_wvd || chosenAudioSegments.pssh_prd)) { - await this.refreshToken(true, true); - if (videoStream) { - await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${videoStream.token}/keepAlive?playhead=1`, {...{method: 'PATCH'}, ...AuthHeaders}); - } - if (audioStream && (videoStream?.token !== audioStream.token)) { - await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${audioStream.token}/keepAlive?playhead=1`, {...{method: 'PATCH'}, ...AuthHeaders}); - } - - console.info(`Getting decryption keys with ${cdm}`); - // New Crunchyroll DRM endpoint for Widevine - if (cdm === 'widevine') { - encryptionKeysVideo = await getKeysWVD(chosenVideoSegments.pssh_wvd, api.drm_widevine, { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader, - Pragma: 'no-cache', - 'Cache-Control': 'no-cache', - 'content-type': 'application/octet-stream', - 'x-cr-content-id': currentVersion ? currentVersion.guid : currentMediaId, - 'x-cr-video-token': videoStream!.token - }); - - // Check if the audio pssh is different since Crunchyroll started to have different dec keys for audio tracks - if (chosenAudioSegments.pssh_wvd && chosenAudioSegments.pssh_wvd !== chosenVideoSegments.pssh_wvd) { - encryptionKeysAudio = await getKeysWVD(chosenAudioSegments.pssh_wvd, api.drm_widevine, { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader, - Pragma: 'no-cache', - 'Cache-Control': 'no-cache', - 'content-type': 'application/octet-stream', - 'x-cr-content-id': currentVersion ? currentVersion.guid : currentMediaId, - 'x-cr-video-token': audioStream!.token - }); - } else { - encryptionKeysAudio = encryptionKeysVideo; - } - } - - // New Crunchyroll DRM endpoint for Playready - if (cdm === 'playready') { - encryptionKeysVideo = await getKeysPRD(chosenVideoSegments.pssh_prd, api.drm_playready, { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader, - Pragma: 'no-cache', - 'Cache-Control': 'no-cache', - 'content-type': 'application/octet-stream', - 'x-cr-content-id': currentVersion ? currentVersion.guid : currentMediaId, - 'x-cr-video-token': videoStream!.token - }); - - // Check if the audio pssh is different since Crunchyroll started to have different dec keys for audio tracks - if (chosenAudioSegments.pssh_prd && chosenAudioSegments.pssh_prd !== chosenVideoSegments.pssh_prd) { - encryptionKeysAudio = await getKeysPRD(chosenAudioSegments.pssh_prd, api.drm_playready, { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader, - Pragma: 'no-cache', - 'Cache-Control': 'no-cache', - 'content-type': 'application/octet-stream', - 'x-cr-content-id': currentVersion ? currentVersion.guid : currentMediaId, - 'x-cr-video-token': audioStream!.token - }); - } else { - encryptionKeysAudio = encryptionKeysVideo; - } - } - - if (!encryptionKeysVideo || encryptionKeysVideo.length == 0 || !encryptionKeysAudio || encryptionKeysAudio.length == 0) { - console.error('Failed to get encryption keys'); - return undefined; - } - - console.info('Got decryption keys'); - } - - if (videoStream) { - await this.refreshToken(true, true); - await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${videoStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders}); - } - if (audioStream && (videoStream?.token !== audioStream.token)) { - await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${audioStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders}); - } - - let [audioDownloaded, videoDownloaded] = [false, false]; - - // When best selected video quality is already downloaded - if(dlVideoOnce && options.dlVideoOnce) { - console.info('Already downloaded video, skipping video download...'); - } else if (options.novids) { - console.info('Skipping video download...'); - } else { - //Download Video - const totalParts = chosenVideoSegments.segments.length; - const mathParts = Math.ceil(totalParts / options.partsize); - const mathMsg = `(${mathParts}*${options.partsize})`; - console.info('Total parts in video stream:', totalParts, mathMsg); - tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); - const dirName = path.dirname(tsFile); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }); - } - const videoJson: M3U8Json = { - segments: chosenVideoSegments.segments - }; - const videoDownload = await new streamdl({ - output: chosenVideoSegments.pssh_wvd || chosenVideoSegments.pssh_prd ? `${tempTsFile}.video.enc.m4s` : `${tsFile}.video.m4s`, - timeout: options.timeout, - m3u8json: videoJson, - // baseurl: chunkPlaylist.baseUrl, - threads: options.partsize, - fsRetryTime: options.fsRetryTime * 1000, - override: options.force, - callback: options.callbackMaker ? options.callbackMaker({ - fileName: `${path.isAbsolute(outFile) ? outFile.slice(this.cfg.dir.content.length) : outFile}`, - image: medias.image, - parent: { - title: medias.seasonTitle - }, - title: medias.episodeTitle, - language: lang - }) : undefined - }).download(); - if(!videoDownload.ok){ - console.error(`DL Stats: ${JSON.stringify(videoDownload.parts)}\n`); - dlFailed = true; - } - dlVideoOnce = true; - videoDownloaded = true; - } - - if (chosenAudioSegments && !options.noaudio) { - //Download Audio (if available) - const totalParts = chosenAudioSegments.segments.length; - const mathParts = Math.ceil(totalParts / options.partsize); - const mathMsg = `(${mathParts}*${options.partsize})`; - console.info('Total parts in audio stream:', totalParts, mathMsg); - tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); - const dirName = path.dirname(tsFile); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }); - } - const audioJson: M3U8Json = { - segments: chosenAudioSegments.segments - }; - const audioDownload = await new streamdl({ - output: chosenVideoSegments.pssh_wvd || chosenVideoSegments.pssh_prd ? `${tempTsFile}.audio.enc.m4s` : `${tsFile}.audio.m4s`, - timeout: options.timeout, - m3u8json: audioJson, - // baseurl: chunkPlaylist.baseUrl, - threads: options.partsize, - fsRetryTime: options.fsRetryTime * 1000, - override: options.force, - callback: options.callbackMaker ? options.callbackMaker({ - fileName: `${path.isAbsolute(outFile) ? outFile.slice(this.cfg.dir.content.length) : outFile}`, - image: medias.image, - parent: { - title: medias.seasonTitle - }, - title: medias.episodeTitle, - language: lang - }) : undefined - }).download(); - if(!audioDownload.ok){ - console.error(`DL Stats: ${JSON.stringify(audioDownload.parts)}\n`); - dlFailed = true; - } - audioDownloaded = true; - } else if (options.noaudio) { - console.info('Skipping audio download...'); - } - - //Handle Decryption if needed - if ((chosenVideoSegments.pssh_wvd || chosenVideoSegments.pssh_prd || chosenAudioSegments.pssh_wvd || chosenAudioSegments.pssh_prd) && (videoDownloaded || audioDownloaded) && !dlFailed) { - console.info('Decryption Needed, attempting to decrypt'); - if (this.cfg.bin.mp4decrypt || this.cfg.bin.shaka) { - let commandBaseVideo = `--show-progress --key ${encryptionKeysVideo?.[cdm === 'playready' ? 0 : 1].kid}:${encryptionKeysVideo?.[cdm === 'playready' ? 0 : 1].key} `; - let commandBaseAudio = `--show-progress --key ${encryptionKeysAudio?.[cdm === 'playready' ? 0 : 1].kid}:${encryptionKeysAudio?.[cdm === 'playready' ? 0 : 1].key} `; - let commandVideo = commandBaseVideo+`"${tempTsFile}.video.enc.m4s" "${tempTsFile}.video.m4s"`; - let commandAudio = commandBaseAudio+`"${tempTsFile}.audio.enc.m4s" "${tempTsFile}.audio.m4s"`; - - if (this.cfg.bin.shaka) { - commandBaseVideo = ` --enable_raw_key_decryption ${encryptionKeysVideo?.map(kb => '--keys key_id='+kb.kid+':key='+kb.key).join(' ')}`; - commandBaseAudio = ` --enable_raw_key_decryption ${encryptionKeysAudio?.map(kb => '--keys key_id='+kb.kid+':key='+kb.key).join(' ')}`; - commandVideo = `input="${tempTsFile}.video.enc.m4s",stream=video,output="${tempTsFile}.video.m4s"`+commandBaseVideo; - commandAudio = `input="${tempTsFile}.audio.enc.m4s",stream=audio,output="${tempTsFile}.audio.m4s"`+commandBaseAudio; - } - - if (videoDownloaded) { - console.info('Started decrypting video,', this.cfg.bin.shaka ? 'using shaka' : 'using mp4decrypt'); - const decryptVideo = Helper.exec(this.cfg.bin.shaka ? 'shaka-packager' : 'mp4decrypt', this.cfg.bin.shaka ? `"${this.cfg.bin.shaka}"` : `"${this.cfg.bin.mp4decrypt}"`, commandVideo); - if (!decryptVideo.isOk) { - console.error(decryptVideo.err); - console.error(`Decryption failed with exit code ${decryptVideo.err.code}`); - fs.renameSync(`${tempTsFile}.video.enc.m4s`, `${tsFile}.video.enc.m4s`); - return undefined; - } else { - console.info('Decryption done for video'); - if (!options.nocleanup) { - fs.removeSync(`${tempTsFile}.video.enc.m4s`); - } - fs.copyFileSync(`${tempTsFile}.video.m4s`, `${tsFile}.video.m4s`); - fs.unlinkSync(`${tempTsFile}.video.m4s`); - files.push({ - type: 'Video', - path: `${tsFile}.video.m4s`, - lang: lang, - isPrimary: isPrimary - }); - } - } - - if (audioDownloaded) { - console.info('Started decrypting audio,', this.cfg.bin.shaka ? 'using shaka' : 'using mp4decrypt'); - const decryptAudio = Helper.exec(this.cfg.bin.shaka ? 'shaka-packager' : 'mp4decrypt', this.cfg.bin.shaka ? `"${this.cfg.bin.shaka}"` : `"${this.cfg.bin.mp4decrypt}"`, commandAudio); - if (!decryptAudio.isOk) { - console.error(decryptAudio.err); - console.error(`Decryption failed with exit code ${decryptAudio.err.code}`); - fs.renameSync(`${tempTsFile}.audio.enc.m4s`, `${tsFile}.audio.enc.m4s`); - return undefined; - } else { - if (!options.nocleanup) { - fs.removeSync(`${tempTsFile}.audio.enc.m4s`); - } - fs.copyFileSync(`${tempTsFile}.audio.m4s`, `${tsFile}.audio.m4s`); - fs.unlinkSync(`${tempTsFile}.audio.m4s`); - files.push({ - type: 'Audio', - path: `${tsFile}.audio.m4s`, - lang: lang, - isPrimary: isPrimary - }); - console.info('Decryption done for audio'); - } - } - } else { - console.warn('mp4decrypt/shaka not found, files need decryption. Decryption Keys:', encryptionKeysVideo, encryptionKeysAudio); - } - } else if (dlFailed) { - console.error('Download failed, skipping decryption'); - } else { - if (videoDownloaded) { - files.push({ - type: 'Video', - path: `${tsFile}.video.m4s`, - lang: lang, - isPrimary: isPrimary - }); - } - if (audioDownloaded) { - files.push({ - type: 'Audio', - path: `${tsFile}.audio.m4s`, - lang: lang, - isPrimary: isPrimary - }); - } - } - } else if (!options.novids) { - const streamPlaylists = m3u8(vstreamPlaylistBody); - const plServerList: string[] = [], - plStreams: Record> = {}, - plQuality: { - str: string, - dim: string, - CODECS: string, - RESOLUTION: { - width: number, - height: number - } - }[] = []; - for(const pl of streamPlaylists.playlists){ - // set quality - const plResolution = pl.attributes.RESOLUTION; - const plResolutionText = `${plResolution.width}x${plResolution.height}`; - // set codecs - const plCodecs = pl.attributes.CODECS; - // parse uri - const plUri = new URL(pl.uri); - let plServer = plUri.hostname; - // set server list - if (plUri.searchParams.get('cdn')){ - plServer += ` (${plUri.searchParams.get('cdn')})`; - } - if (!plServerList.includes(plServer)){ - plServerList.push(plServer); - } - // add to server - if (!Object.keys(plStreams).includes(plServer)){ - plStreams[plServer] = {}; - } - if( - plStreams[plServer][plResolutionText] - && plStreams[plServer][plResolutionText] != pl.uri - && typeof plStreams[plServer][plResolutionText] != 'undefined' - ) { - console.error(`Non duplicate url for ${plServer} detected, please report to developer!`); - } else{ - plStreams[plServer][plResolutionText] = pl.uri; - } - // set plQualityStr - const plBandwidth = Math.round(pl.attributes.BANDWIDTH/1024); - const qualityStrAdd = `${plResolutionText} (${plBandwidth}KiB/s)`; - const qualityStrRegx = new RegExp(qualityStrAdd.replace(/([:()/])/g, '\\$1'), 'm'); - const qualityStrMatch = !plQuality.map(a => a.str).join('\r\n').match(qualityStrRegx); - if(qualityStrMatch){ - plQuality.push({ - str: qualityStrAdd, - dim: plResolutionText, - CODECS: plCodecs, - RESOLUTION: plResolution - }); - } - } - - const plSelectedServer = plServerList[0]; - const plSelectedList = plStreams[plSelectedServer]; - plQuality.sort((a, b) => { - const aMatch: RegExpMatchArray | never[] = a.dim.match(/[0-9]+/) || []; - const bMatch: RegExpMatchArray | never[] = b.dim.match(/[0-9]+/) || []; - return parseInt(aMatch[0]) - parseInt(bMatch[0]); - }); - let quality = options.q === 0 ? plQuality.length : options.q; - if(quality > plQuality.length) { - console.warn(`The requested quality of ${options.q} is greater than the maximum ${plQuality.length}.\n[WARN] Therefor the maximum will be capped at ${plQuality.length}.`); - quality = plQuality.length; - } - // When best selected video quality is already downloaded - if(dlVideoOnce && options.dlVideoOnce) { - // Select the lowest resolution with the same codecs - while(quality !=1 && plQuality[quality - 1].CODECS == plQuality[quality - 2].CODECS) { - quality--; - } - } - const selPlUrl = plSelectedList[plQuality.map(a => a.dim)[quality - 1]] ? plSelectedList[plQuality.map(a => a.dim)[quality - 1]] : ''; - console.info(`Servers available:\n\t${plServerList.join('\n\t')}`); - console.info(`Available qualities:\n\t${plQuality.map((a, ind) => `[${ind+1}] ${a.str}`).join('\n\t')}`); - - if(selPlUrl != ''){ - variables.push({ - name: 'height', - type: 'number', - replaceWith: quality === 0 ? plQuality[plQuality.length - 1].RESOLUTION.height as number : plQuality[quality - 1].RESOLUTION.height - }, { - name: 'width', - type: 'number', - replaceWith: quality === 0 ? plQuality[plQuality.length - 1].RESOLUTION.width as number : plQuality[quality - 1].RESOLUTION.width - }); - const lang = langsData.languages.find(a => a.code === vcurStream?.audio_lang); - if (!lang) { - console.error(`Unable to find language for code ${vcurStream.audio_lang}`); - return; - } - console.info(`Selected quality: ${Object.keys(plSelectedList).find(a => plSelectedList[a] === selPlUrl)} @ ${plSelectedServer}`); - console.info('Stream URL:', selPlUrl); - // TODO check filename - fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); - const outFile = parseFileName(options.fileName + '.' + (mMeta.lang?.name || lang.name), variables, options.numbers, options.override).join(path.sep); - console.info(`Output filename: ${outFile}`); - const chunkPage = await this.req.getData(selPlUrl, { - headers: api.crunchyDefHeader, - }); - if(!chunkPage.ok || !chunkPage.res){ - console.error('CAN\'T FETCH VIDEO PLAYLIST!'); - dlFailed = true; - } else { - // We have the stream, so go ahead and delete the active stream - if (videoStream) { - await this.refreshToken(true, true); - await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${videoStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders}); - } - if (audioStream && (videoStream?.token !== audioStream.token)) { - await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${audioStream.token}`, {...{method: 'DELETE'}, ...AuthHeaders}); - } - - const chunkPageBody = await chunkPage.res.text(); - const chunkPlaylist = m3u8(chunkPageBody); - const totalParts = chunkPlaylist.segments.length; - const mathParts = Math.ceil(totalParts / options.partsize); - const mathMsg = `(${mathParts}*${options.partsize})`; - console.info('Total parts in stream:', totalParts, mathMsg); - tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); - const dirName = path.dirname(tsFile); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }); - } - const dlStreamByPl = await new streamdl({ - output: `${tsFile}.ts`, - timeout: options.timeout, - m3u8json: chunkPlaylist, - // baseurl: chunkPlaylist.baseUrl, - threads: options.partsize, - fsRetryTime: options.fsRetryTime * 1000, - override: options.force, - callback: options.callbackMaker ? options.callbackMaker({ - fileName: `${path.isAbsolute(outFile) ? outFile.slice(this.cfg.dir.content.length) : outFile}`, - image: medias.image, - parent: { - title: medias.seasonTitle - }, - title: medias.episodeTitle, - language: lang - }) : undefined - }).download(); - if (!dlStreamByPl.ok) { - console.error(`DL Stats: ${JSON.stringify(dlStreamByPl.parts)}\n`); - dlFailed = true; - } - files.push({ - type: 'Video', - path: `${tsFile}.ts`, - lang: lang, - isPrimary: isPrimary - }); - dlVideoOnce = true; - } - } else{ - console.error('Quality not selected!\n'); - dlFailed = true; - } - } else if (options.novids) { - fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); - console.info('Downloading skipped!'); - } - } - } else if (options.novids && options.noaudio) { - fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); - } - - if (compiledChapters.length > 0) { - try { - fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); - const outFile = parseFileName(options.fileName + '.' + mMeta.lang?.name, variables, options.numbers, options.override).join(path.sep); - tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); - const dirName = path.dirname(tsFile); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }); - } - const lang = langsData.languages.find(a => a.code === vcurStream?.audio_lang); - if (!lang) { - console.error(`Unable to find language for code ${vcurStream.audio_lang}`); - return; - } - fs.writeFileSync(`${tsFile}.txt`, compiledChapters.join('\r\n')); - files.push({ - path: `${tsFile}.txt`, - lang: lang, - type: 'Chapters' - }); - } catch { - console.error('Failed to write chapter file'); - } - } - - if(options.dlsubs.indexOf('all') > -1){ - options.dlsubs = ['all']; - } - - if(options.hslang != 'none'){ - console.warn('Subtitles downloading disabled for hardsubs streams.'); - options.skipsubs = true; - } - - if (options.nosubs) { - console.info('Subtitles downloading disabled from nosubs flag.'); - options.skipsubs = true; - } - - if (!options.skipsubs && options.dlsubs.indexOf('none') == -1){ - if ((pbData.meta.subtitles && Object.values(pbData.meta.subtitles).length) || (pbData.meta.closed_captions && Object.values(pbData.meta.closed_captions).length > 0)) { - const subsData = Object.values(pbData.meta.subtitles); - const capsData = Object.values(pbData.meta.closed_captions); - const subsDataMapped = subsData.map((s) => { - const subLang = langsData.fixAndFindCrLC(s.language); - return { - ...s, - isCC: false, - locale: subLang, - language: subLang.locale - }; - }).concat( - capsData.map((s) => { - const subLang = langsData.fixAndFindCrLC(s.language); - return { - ...s, - isCC: true, - locale: subLang, - language: subLang.locale - }; - }) - ); - const subsArr = langsData.sortSubtitles(subsDataMapped, 'language'); - for(const subsIndex in subsArr){ - const subsItem = subsArr[subsIndex]; - const langItem = subsItem.locale; - const sxData: Partial = {}; - sxData.language = langItem; - const isSigns = langItem.code === audDub && !subsItem.isCC; - const isCC = subsItem.isCC; - sxData.file = langsData.subsFile(fileName as string, subsIndex, langItem, isCC, options.ccTag, isSigns, subsItem.format); - if (path.isAbsolute(sxData.file)) { - sxData.path = sxData.file; - } else { - sxData.path = path.join(this.cfg.dir.content, sxData.file); - } - const dirName = path.dirname(sxData.path); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }); - } - if (files.some(a => a.type === 'Subtitle' && (a.language.cr_locale == langItem.cr_locale || a.language.locale == langItem.locale) && a.cc === isCC && a.signs === isSigns)) - continue; - if(options.dlsubs.includes('all') || options.dlsubs.includes(langItem.locale)){ - const subsAssReq = await this.req.getData(subsItem.url, { - headers: api.crunchyDefHeader - }); - if(subsAssReq.ok && subsAssReq.res){ - let sBody = await subsAssReq.res.text(); - if (subsItem.format == 'vtt') { - const chosenFontSize = options.originalFontSize ? undefined : options.fontSize; - if (!options.originalFontSize) sBody = sBody.replace(/( font-size:.+?;)/g, '').replace(/(font-size:.+?;)/g, ''); - sBody = vtt2ass(undefined, chosenFontSize, sBody, '', undefined, options.fontName); - sxData.fonts = fontsData.assFonts(sBody) as Font[]; - sxData.file = sxData.file.replace('.vtt','.ass'); - } else { - sBody = '\ufeff' + sBody; - const sBodySplit = sBody.split('\r\n'); - sBodySplit.splice(2, 0, 'ScaledBorderAndShadow: yes'); - sBody = sBodySplit.join('\r\n'); - sxData.title = sBody.split('\r\n')[1].replace(/^Title: /, ''); - sxData.title = `${langItem.language} / ${sxData.title}`; - sxData.fonts = fontsData.assFonts(sBody) as Font[]; - } - fs.writeFileSync(sxData.path, sBody); - console.info(`Subtitle downloaded: ${sxData.file}`); - files.push({ - type: 'Subtitle', - ...sxData as sxItem, - cc: isCC, - signs: isSigns, - }); - } - else{ - console.warn(`Failed to download subtitle: ${sxData.file}`); - } - } - } - } - else{ - console.warn('Can\'t find urls for subtitles!'); - } - } else{ - console.info('Subtitles downloading skipped!'); - } - - await this.sleep(options.waittime); - } - return { - error: dlFailed, - data: files, - fileName: fileName ? (path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName)) || './unknown' : './unknown' - }; - } - - public async muxStreams(data: DownloadedMedia[], options: CrunchyMuxOptions) { - this.cfg.bin = await yamlCfg.loadBinCfg(); - let hasAudioStreams = false; - if (options.novids || data.filter(a => a.type === 'Video').length === 0) - return console.info('Skip muxing since no vids are downloaded'); - if (data.some(a => a.type === 'Audio')) { - hasAudioStreams = true; - } - const merger = new Merger({ - onlyVid: hasAudioStreams ? data.filter(a => a.type === 'Video').map((a) : MergerInput => { - return { - lang: a.lang, - path: a.path, - }; - }) : [], - skipSubMux: options.skipSubMux, - onlyAudio: hasAudioStreams ? data.filter(a => a.type === 'Audio').map((a) : MergerInput => { - return { - lang: a.lang, - path: a.path, - }; - }) : [], - output: `${options.output}.${options.mp4 ? 'mp4' : 'mkv'}`, - subtitles: data.filter(a => a.type === 'Subtitle').map((a) : SubtitleInput => { - return { - file: a.path, - language: a.language, - closedCaption: a.cc, - signs: a.signs, - }; - }), - simul: false, - keepAllVideos: options.keepAllVideos, - fonts: Merger.makeFontsList(this.cfg.dir.fonts, data.filter(a => a.type === 'Subtitle') as sxItem[]), - videoAndAudio: hasAudioStreams ? [] : data.filter(a => a.type === 'Video').map((a) : MergerInput => { - return { - lang: a.lang, - path: a.path, - }; - }), - chapters: data.filter(a => a.type === 'Chapters').map((a) : MergerInput => { - return { - path: a.path, - lang: a.lang - }; - }), - videoTitle: options.videoTitle, - options: { - ffmpeg: options.ffmpegOptions, - mkvmerge: options.mkvmergeOptions - }, - defaults: { - audio: options.defaultAudio, - sub: options.defaultSub - }, - ccTag: options.ccTag - }); - const bin = Merger.checkMerger(this.cfg.bin, options.mp4, options.forceMuxer); - // collect fonts info - // mergers - let isMuxed = false; - if (options.syncTiming) { - await merger.createDelays(); - } - if (bin.MKVmerge) { - await merger.merge('mkvmerge', bin.MKVmerge); - isMuxed = true; - } else if (bin.FFmpeg) { - await merger.merge('ffmpeg', bin.FFmpeg); - isMuxed = true; - } else{ - console.info('\nDone!\n'); - return; - } - if (isMuxed && !options.nocleanup) - merger.cleanUp(); - } - - public async listSeriesID(id: string, data?: CrunchyMultiDownload): Promise<{ list: Episode[], data: Record}> { - await this.refreshToken(true, true); - let serieshasversions = true; - const parsed = await this.parseSeriesById(id); - if (!parsed) - return { data: {}, list: [] }; - const result = this.parseSeriesResult(parsed); - const episodes : Record = {}; - for(const season of Object.keys(result) as unknown as number[]) { - for (const key of Object.keys(result[season])) { - const s = result[season][key]; - if (data?.s && s.id !== data.s) continue; - (await this.getSeasonDataById(s))?.data?.forEach(episode => { - //TODO: Make sure the below code is ok - //Prepare the episode array - let item; - const seasonIdentifier = s.identifier ? s.identifier.split('|')[1] : `S${episode.season_number}`; - if (!(Object.prototype.hasOwnProperty.call(episodes, `${seasonIdentifier}E${episode.episode || episode.episode_number}`))) { - item = episodes[`${seasonIdentifier}E${episode.episode || episode.episode_number}`] = { - items: [] as CrunchyEpisode[], - langs: [] as langsData.LanguageItem[] - }; - } else { - item = episodes[`${seasonIdentifier}E${episode.episode || episode.episode_number}`]; - } - - if (episode.versions) { - //Iterate over episode versions for audio languages - for (const version of episode.versions) { - //Make sure there is only one of the same language - if (!item.langs.find(a => a?.cr_locale == version.audio_locale)) { - //Push to arrays if there is no duplicates of the same language. - item.items.push(episode); - item.langs.push(langsData.languages.find(a => a.cr_locale == version.audio_locale) as langsData.LanguageItem); - } - } - //Sort audio tracks according to the order of languages passed to the 'dubLang' option - const argv = yargs.appArgv(this.cfg.cli); - if(!argv.allDubs) { - item.langs.sort((a,b) => argv.dubLang.indexOf(a.code) - argv.dubLang.indexOf(b.code)); - } - } else { - //Episode didn't have versions, mark it as such to be logged. - serieshasversions = false; - //Make sure there is only one of the same language - if (!item.langs.find(a => a?.cr_locale == episode.audio_locale)) { - //Push to arrays if there is no duplicates of the same language. - item.items.push(episode); - item.langs.push(langsData.languages.find(a => a.cr_locale == episode.audio_locale) as langsData.LanguageItem); - } - } - }); - } - } - - const itemIndexes = { - sp: 1, - no: 1 - }; - - for (const key of Object.keys(episodes)) { - const item = episodes[key]; - const isSpecial = !item.items[0].episode.match(/^\d+$/); - episodes[`${isSpecial ? 'S' : 'E'}${itemIndexes[isSpecial ? 'sp' : 'no']}`] = item; - if (isSpecial) - itemIndexes.sp++; - else - itemIndexes.no++; - delete episodes[key]; - } - - // Sort episodes to have specials at the end - const specials = Object.entries(episodes).filter(a => a[0].startsWith('S')), - normal = Object.entries(episodes).filter(a => a[0].startsWith('E')), - sortedEpisodes = Object.fromEntries([...normal, ...specials]); - - for (const key of Object.keys(sortedEpisodes)) { - const item = sortedEpisodes[key]; - const epNum = key.startsWith('E') ? (`E${data?.absolute ? (item.items[0].episode_number?.toString() || item.items[0].episode) : key.slice(1)}`) : key; - console.info(`[${data?.absolute ? epNum : key}] [${item.items[0].upload_date ? new Date(item.items[0].upload_date).toISOString().slice(0, 10) : '0000-00-00'}] ${ - item.items.find(a => !a.season_title.match(/\(\w+ Dub\)/))?.season_title ?? item.items[0].season_title.replace(/\(\w+ Dub\)/g, '').trimEnd() - } - Season ${item.items[0].season_number} - ${item.items[0].title} [${ - item.items.map((a, index) => { - return `${a.is_premium_only ? '☆ ' : ''}${item.langs[index]?.name ?? 'Unknown'}`; - }).join(', ') - }]`); - } - - if (!serieshasversions) { - console.warn('Couldn\'t find versions on some episodes, fell back to old method.'); - } - - return { data: sortedEpisodes, list: Object.entries(sortedEpisodes).map(([key, value]) => { - const images = (value.items[0].images.thumbnail ?? [[ { source: '/notFound.png' } ]])[0]; - const seconds = Math.floor(value.items[0].duration_ms / 1000); - let epNum; - if (data?.absolute) { - epNum = value.items[0].episode_number !== null && value.items[0].episode_number !== undefined ? value.items[0].episode_number.toString() : (value.items[0].episode !== null && value.items[0].episode !== undefined ? value.items[0].episode : (key.startsWith('E') ? key.slice(1) : key)); - } else { - epNum = key.startsWith('E') ? key.slice(1) : key; - } - return { - e: epNum, - lang: value.langs.map(a => a?.code), - name: value.items[0].title, - season: value.items[0].season_number.toString(), - seriesTitle: value.items[0].series_title.replace(/\(\w+ Dub\)/g, '').trimEnd(), - seasonTitle: value.items[0].season_title.replace(/\(\w+ Dub\)/g, '').trimEnd(), - episode: value.items[0].episode_number?.toString() ?? value.items[0].episode ?? '?', - id: value.items[0].season_id, - img: images[Math.floor(images.length / 2)].source, - description: value.items[0].description, - time: `${Math.floor(seconds / 60)}:${seconds % 60}` - }; - })}; - } - - public async downloadFromSeriesID(id: string, data: CrunchyMultiDownload) : Promise> { - const { data: episodes } = await this.listSeriesID(id, data); - console.info(''); - console.info('-'.repeat(30)); - console.info(''); - const selected = this.itemSelectMultiDub(episodes, data.dubLang, data.but, data.all, data.e, data.absolute); - for (const key of Object.keys(selected)) { - const item = selected[key]; - console.info(`[S${item.season}E${item.episodeNumber}] - ${item.episodeTitle} [${ - item.data.map(a => { - return `✓ ${a.lang?.name || 'Unknown Language'}`; - }).join(', ') - }]`); - } - return { isOk: true, value: Object.values(selected) }; - } - - public itemSelectMultiDub (eps: Record, dubLang: string[], but?: boolean, all?: boolean, e?: string, absolute?: boolean) { - const doEpsFilter = parseSelect(e as string); - - const ret: Record = {}; - - for (const key of Object.keys(eps)) { - const itemE = eps[key]; - itemE.items.forEach((item, index) => { - if (!dubLang.includes(itemE.langs[index]?.code)) - return; - item.hide_season_title = true; - if(item.season_title == '' && item.series_title != ''){ - item.season_title = item.series_title; - item.hide_season_title = false; - item.hide_season_number = true; - } - if(item.season_title == '' && item.series_title == ''){ - item.season_title = 'NO_TITLE'; - item.series_title = 'NO_TITLE'; - } - - let epNum; - if (absolute) { - epNum = item.episode_number !== null && item.episode_number !== undefined ? item.episode_number.toString() : (item.episode !== null && item.episode !== undefined ? item.episode : (key.startsWith('E') ? key.slice(1) : key)); - } else { - epNum = key.startsWith('E') ? key.slice(1) : key; - } - - // set data - const images = (item.images.thumbnail ?? [[ { source: '/notFound.png' } ]])[0]; - const epMeta: CrunchyEpMeta = { - data: [ - { - mediaId: item.id, - versions: item.versions, - isSubbed: item.is_subbed, - isDubbed: item.is_dubbed - } - ], - seriesTitle: itemE.items.find(a => !a.series_title.match(/\(\w+ Dub\)/))?.series_title ?? itemE.items[0].series_title.replace(/\(\w+ Dub\)/g, '').trimEnd(), - seasonTitle: itemE.items.find(a => !a.season_title.match(/\(\w+ Dub\)/))?.season_title ?? itemE.items[0].season_title.replace(/\(\w+ Dub\)/g, '').trimEnd(), - episodeNumber: item.episode, - episodeTitle: item.title, - seasonID: item.season_id, - season: item.season_number, - showID: item.series_id, - e: epNum, - image: images[Math.floor(images.length / 2)].source, - }; - if (item.__links__?.streams?.href) { - epMeta.data[0].playback = item.__links__.streams.href; - if(!item.playback) { - item.playback = item.__links__.streams.href; - } - } - if (item.streams_link) { - epMeta.data[0].playback = item.streams_link; - if(!item.playback) { - item.playback = item.streams_link; - } - } - - if(item.playback && ((but && !doEpsFilter.isSelected([epNum, item.id])) || (all || (doEpsFilter.isSelected([epNum, item.id])) && !but))) { - if (Object.prototype.hasOwnProperty.call(ret, key)) { - const epMe = ret[key]; - epMe.data.push({ - lang: itemE.langs[index], - ...epMeta.data[0] - }); - } else { - epMeta.data[0].lang = itemE.langs[index]; - ret[key] = { - ...epMeta - }; - } - } - // show ep - item.seq_id = epNum; - }); - } - return ret; - } - - public parseSeriesResult (seasonsList: SeriesSearch) : Record> { - const ret: Record> = {}; - let i = 0; - for (const item of seasonsList.data) { - i++; - for (const lang of langsData.languages) { - //TODO: Make sure the below code is fine - let season_number = item.season_number; - if (item.versions) { - season_number = i; - } - if (!Object.prototype.hasOwnProperty.call(ret, season_number)) - ret[season_number] = {}; - if (item.title.includes(`(${lang.name} Dub)`) || item.title.includes(`(${lang.name})`)) { - ret[season_number][lang.code] = item; - } else if (item.is_subbed && !item.is_dubbed && lang.code == 'jpn') { - ret[season_number][lang.code] = item; - } else if (item.is_dubbed && lang.code === 'eng' && !langsData.languages.some(a => item.title.includes(`(${a.name})`) || item.title.includes(`(${a.name} Dub)`))) { // Dubbed with no more infos will be treated as eng dubs - ret[season_number][lang.code] = item; - //TODO: look into if below is stable - } else if (item.audio_locale == lang.cr_locale) { - ret[season_number][lang.code] = item; - } - } - } - return ret; - } - - public async parseSeriesById(id: string) { - if(!this.cmsToken.cms_web){ - console.error('Authentication required!'); - return; - } - - const AuthHeaders = { - headers: { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader - }, - useProxy: true - }; - - // seasons list - const seriesSeasonListReq = await this.req.getData(`${api.content_cms}/series/${id}/seasons?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); - if(!seriesSeasonListReq.ok || !seriesSeasonListReq.res){ - console.error('Series Request FAILED!'); - return; - } - // parse data - const seasonsList = await seriesSeasonListReq.res.json() as SeriesSearch; - if(seasonsList.total < 1){ - console.info('Series is empty!'); - return; - } - return seasonsList; - } - - public async getSeasonDataById(item: SeriesSearchItem, log = false){ - if(!this.cmsToken.cms_web){ - console.error('Authentication required!'); - return; - } - - const AuthHeaders = { - headers: { - Authorization: `Bearer ${this.token.access_token}`, - ...api.crunchyDefHeader - }, - useProxy: true - }; - - //get show info - const showInfoReq = await this.req.getData(`${api.content_cms}/seasons/${item.id}?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); - if(!showInfoReq.ok || !showInfoReq.res){ - console.error('Show Request FAILED!'); - return; - } - const showInfo = await showInfoReq.res.json(); - if (log) - await this.logObject(showInfo, 0); - - let episodeList = { total: 0, data: [], meta: {} } as CrunchyEpisodeList; - //get episode info - for (const s of showInfo.data) { - const original_id = s.versions?.find((v: { original: boolean; }) => v.original)?.guid; - const id = original_id ? original_id : s.id; - - const reqEpsListOpts = [ - api.cms_bucket, - this.cmsToken.cms_web.bucket, - '/episodes?', - new URLSearchParams({ - 'force_locale': '', - 'preferred_audio_language': 'ja-JP', - 'locale': this.locale, - 'season_id': id, - 'Policy': this.cmsToken.cms_web.policy, - 'Signature': this.cmsToken.cms_web.signature, - 'Key-Pair-Id': this.cmsToken.cms_web.key_pair_id, - }), - ].join(''); - const reqEpsList = await this.req.getData(reqEpsListOpts, AuthHeaders); - if(!reqEpsList.ok || !reqEpsList.res){ - console.error('Episode List Request FAILED!'); - return; - } - - const episodeListAndroid = await reqEpsList.res.json() as CrunchyAndroidEpisodes; - episodeList = { - total: episodeList.total + episodeListAndroid.total, - data: [...episodeList.data, ...episodeListAndroid.items], - meta: {} - }; - } - - if(episodeList.total < 1){ - console.info(' Season is empty!'); - return; - } - return episodeList; - } - + if (!(Date.now() > new Date(this.token.expires).getTime()) && ifNeeded) { + return; + } else { + //console.info('[WARN] The token has expired compleatly. I will try to refresh the token anyway, but you might have to reauth.'); + } + const uuid = this.token.device_id || randomUUID(); + const authData = new URLSearchParams({ + refresh_token: this.token.refresh_token, + grant_type: 'refresh_token', + device_id: uuid, + device_name: 'iPhone', + device_type: 'iPhone 13' + }).toString(); + const authReqOpts: reqModule.Params = { + method: 'POST', + headers: { ...api.crunchyAuthHeader, 'ETP-Anonymous-ID': uuid }, + body: authData + }; + const authReq = await this.req.getData(api.auth, authReqOpts); + if (!authReq.ok || !authReq.res) { + console.error('Token Refresh Failed!'); + if (authReq.res?.status == 400) { + console.warn('Token is likely wrong, please login again!'); + } + return; + } + // To prevent any Cloudflare errors in the future + if (authReq.res.headers.get('Set-Cookie')) { + api.crunchyDefHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; + api.crunchyAuthHeader['Cookie'] = authReq.res.headers.get('Set-Cookie') as string; + } + if (authReq.headers && authReq.headers['Set-Cookie']) { + api.crunchyDefHeader['Cookie'] = authReq.headers['Set-Cookie']; + api.crunchyAuthHeader['Cookie'] = authReq.headers['Set-Cookie']; + api.crunchyDefHeader['User-Agent'] = authReq.headers['User-Agent']; + api.crunchyAuthHeader['User-Agent'] = authReq.headers['User-Agent']; + } + this.token = await authReq.res.json(); + this.token.device_id = uuid; + this.token.expires = new Date(Date.now() + this.token.expires_in); + yamlCfg.saveCRToken(this.token); + } + if (this.token.refresh_token) { + await this.getProfile(silent); + } else { + console.info('USER: Anonymous'); + } + await this.getCMStoken(ifNeeded); + } + + public async getCMStoken(ifNeeded = false) { + if (!this.token.access_token) { + console.error('No access token!'); + return; + } + + if (ifNeeded && this.cmsToken.cms_web) { + if (!(Date.now() >= new Date(this.cmsToken.cms_web.expires).getTime())) { + return; + } + } + + const cmsTokenReqOpts = { + headers: { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader + }, + useProxy: true + }; + const cmsTokenReq = await this.req.getData(api.cms_auth, cmsTokenReqOpts); + if (!cmsTokenReq.ok || !cmsTokenReq.res) { + console.error('Authentication CMS token failed!'); + return; + } + this.cmsToken = await cmsTokenReq.res.json(); + console.info('Your Country: %s\n', this.cmsToken.cms_web?.bucket.split('/')[1]); + } + + public async getCmsData() { + // check token + if (!this.cmsToken.cms_web) { + console.error('Authentication required!'); + return; + } + // opts + const indexReqOpts = [ + api.cms_bucket, + this.cmsToken.cms_web.bucket, + '/index?', + new URLSearchParams({ + force_locale: '', + preferred_audio_language: 'ja-JP', + locale: this.locale, + Policy: this.cmsToken.cms_web.policy, + Signature: this.cmsToken.cms_web.signature, + 'Key-Pair-Id': this.cmsToken.cms_web.key_pair_id + }) + ].join(''); + const indexReq = await this.req.getData(indexReqOpts, { + headers: { + 'User-Agent': api.crunchyDefUserAgent + } + }); + if (!indexReq.ok || !indexReq.res) { + console.error('Get CMS index FAILED!'); + return; + } + console.info(await indexReq.res.json()); + } + + public async doSearch(data: SearchData): Promise { + if (!this.token.access_token) { + console.error('Authentication required!'); + return { isOk: false, reason: new Error('Not authenticated') }; + } + const searchReqOpts = { + headers: { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader + }, + useProxy: true + }; + const searchStart = data.page ? (data.page - 1) * 5 : 0; + const searchParams = new URLSearchParams({ + q: data.search, + n: '5', + start: data.page ? `${(data.page - 1) * 5}` : '0', + type: data['search-type'] ?? getDefault('search-type', this.cfg.cli), + locale: this.locale + }).toString(); + const searchReq = await this.req.getData(`${api.search}?${searchParams}`, searchReqOpts); + if (!searchReq.ok || !searchReq.res) { + console.error('Search FAILED!'); + return { isOk: false, reason: new Error('Search failed. No more information provided') }; + } + const searchResults = (await searchReq.res.json()) as CrunchySearch; + if (searchResults.total < 1) { + console.info('Nothing Found!'); + return { isOk: true, value: [] }; + } + + const searchTypesInfo = { + top_results: 'Top results', + series: 'Found series', + movie_listing: 'Found movie lists', + episode: 'Found episodes' + }; + for (const search_item of searchResults.data) { + console.info('%s:', searchTypesInfo[search_item.type as keyof typeof searchTypesInfo]); + // calculate pages + const pageCur = searchStart > 0 ? Math.ceil(searchStart / 5) + 1 : 1; + const pageMax = Math.ceil(search_item.count / 5); + // pages per category + if (search_item.count < 1) { + console.info(' Nothing Found...'); + } + if (search_item.count > 0) { + if (pageCur > pageMax) { + console.info(' Last page is %s...', pageMax); + continue; + } + for (const item of search_item.items) { + await this.logObject(item); + } + console.info(` Total results: ${search_item.count} (Page: ${pageCur}/${pageMax})`); + } + } + const toSend = searchResults.data.filter((a) => a.type === 'series' || a.type === 'movie_listing'); + return { + isOk: true, + value: toSend + .map((a) => { + return a.items.map((a): SearchResponseItem => { + const images = (a.images.poster_tall ?? [[{ source: '/notFound.png' }]])[0]; + return { + id: a.id, + image: images[Math.floor(images.length / 2)].source, + name: a.title, + rating: -1, + desc: a.description + }; + }); + }) + .reduce((pre, cur) => pre.concat(cur)) + }; + } + + public async logObject(item: ParseItem, pad?: number, getSeries?: boolean, getMovieListing?: boolean) { + if (this.debug) { + console.info(item); + } + pad = pad ?? 2; + getSeries = getSeries === undefined ? true : getSeries; + getMovieListing = getMovieListing === undefined ? true : getMovieListing; + item.isSelected = item.isSelected === undefined ? false : item.isSelected; + if (!item.type) { + item.type = item.__class__; + } + + //guess item type + //TODO: look into better methods of getting item type + let iType = item.type; + if (!iType) { + if (item.episode_number) { + iType = 'episode'; + } else if (item.season_number) { + iType = 'season'; + } else if (item.season_count) { + iType = 'series'; + } else if (item.media_type == 'movie') { + iType = 'movie'; + } else if (item.movie_release_year) { + iType = 'movie_listing'; + } else { + if (item.identifier !== '') { + const iTypeCheck = item.identifier?.split('|'); + if (iTypeCheck) { + if (iTypeCheck[1] == 'M') { + iType = 'movie'; + } else if (!iTypeCheck[2]) { + iType = 'season'; + } else { + iType = 'episode'; + } + } else { + iType = 'series'; + } + } else { + iType = 'movie_listing'; + } + } + item.type = iType; + } + + const oTypes = { + series: 'Z', // SRZ + season: 'S', // VOL + episode: 'E', // EPI + movie_listing: 'F', // FLM + movie: 'M' // MED + }; + // check title + item.title = item.title != '' ? item.title : 'NO_TITLE'; + // static data + const oMetadata: string[] = [], + oBooleans: string[] = [], + tMetadata = item.type + '_metadata', + iMetadata = (Object.prototype.hasOwnProperty.call(item, tMetadata) ? item[tMetadata as keyof ParseItem] : item) as Record, + iTitle = [item.title]; + + const audio_languages: string[] = []; + + // set object booleans + if (iMetadata.duration_ms) { + oBooleans.push(Helper.formatTime(iMetadata.duration_ms / 1000)); + } + if (iMetadata.is_simulcast) { + oBooleans.push('SIMULCAST'); + } + if (iMetadata.is_mature) { + oBooleans.push('MATURE'); + } + if (item.versions) { + for (const version of item.versions) { + audio_languages.push(version.audio_locale); + if (version.original) { + oBooleans.push('SUB'); + } else { + if (!oBooleans.includes('DUB')) { + oBooleans.push('DUB'); + } + } + } + } else { + if (iMetadata.is_subbed) { + oBooleans.push('SUB'); + } + if (iMetadata.is_dubbed) { + oBooleans.push('DUB'); + } + } + if (item.playback && item.type != 'movie_listing') { + oBooleans.push('STREAM'); + } + // set object metadata + if (iMetadata.season_count) { + oMetadata.push(`Seasons: ${iMetadata.season_count}`); + } + if (iMetadata.episode_count) { + oMetadata.push(`EPs: ${iMetadata.episode_count}`); + } + if (item.season_number && !iMetadata.hide_season_title && !iMetadata.hide_season_number) { + oMetadata.push(`Season: ${item.season_number}`); + } + if (item.type == 'episode') { + if (iMetadata.episode) { + iTitle.unshift(iMetadata.episode); + } + if (!iMetadata.hide_season_title && iMetadata.season_title) { + iTitle.unshift(iMetadata.season_title); + } + } + if (item.is_premium_only) { + iTitle[0] = `☆ ${iTitle[0]}`; + } + // display metadata + if (item.hide_metadata) { + iMetadata.hide_metadata = item.hide_metadata; + } + const showObjectMetadata = oMetadata.length > 0 && !iMetadata.hide_metadata; + const showObjectBooleans = oBooleans.length > 0 && !iMetadata.hide_metadata; + // make obj ids + const objects_ids: string[] = []; + objects_ids.push(oTypes[item.type as keyof typeof oTypes] + ':' + item.id); + if (item.seq_id) { + objects_ids.unshift(item.seq_id); + } + if (item.f_num) { + objects_ids.unshift(item.f_num); + } + if (item.s_num) { + objects_ids.unshift(item.s_num); + } + if (item.external_id) { + objects_ids.push(item.external_id); + } + if (item.ep_num) { + objects_ids.push(item.ep_num); + } + + // show entry + console.info( + '%s%s[%s] %s%s%s', + ''.padStart(item.isSelected ? pad - 1 : pad, ' '), + item.isSelected ? '✓' : '', + objects_ids.join('|'), + iTitle.join(' - '), + showObjectMetadata ? ` (${oMetadata.join(', ')})` : '', + showObjectBooleans ? ` [${oBooleans.join(', ')}]` : '' + ); + if (item.last_public) { + console.info(''.padStart(pad + 1, ' '), '- Last updated:', item.last_public); + } + if (item.subtitle_locales) { + iMetadata.subtitle_locales = item.subtitle_locales; + } + if (item.versions && audio_languages.length > 0) { + console.info('%s- Versions: %s', ''.padStart(pad + 2, ' '), langsData.parseSubtitlesArray(audio_languages)); + } + if (iMetadata.subtitle_locales && iMetadata.subtitle_locales.length > 0) { + console.info('%s- Subtitles: %s', ''.padStart(pad + 2, ' '), langsData.parseSubtitlesArray(iMetadata.subtitle_locales)); + } + if (item.availability_notes) { + console.info('%s- Availability notes: %s', ''.padStart(pad + 2, ' '), item.availability_notes.replace(/\[[^\]]*]?/gm, '')); + } + if (item.type == 'series' && getSeries) { + await this.logSeriesById(item.id, pad, true); + console.info(''); + } + if (item.type == 'movie_listing' && getMovieListing) { + await this.logMovieListingById(item.id, pad + 2); + console.info(''); + } + } + + public async logSeriesById(id: string, pad?: number, hideSeriesTitle?: boolean) { + // parse + pad = pad || 0; + hideSeriesTitle = hideSeriesTitle !== undefined ? hideSeriesTitle : false; + // check token + if (!this.cmsToken.cms_web) { + console.error('Authentication required!'); + return; + } + // opts + const AuthHeaders = { + headers: { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader + }, + useProxy: true + }; + // reqs + if (!hideSeriesTitle) { + const seriesReq = await this.req.getData(`${api.content_cms}/series/${id}?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); + if (!seriesReq.ok || !seriesReq.res) { + console.error('Series Request FAILED!'); + return; + } + const seriesData = await seriesReq.res.json(); + await this.logObject(seriesData.data[0], pad, false); + } + // seasons list + const seriesSeasonListReq = await this.req.getData( + `${api.content_cms}/series/${id}/seasons?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, + AuthHeaders + ); + if (!seriesSeasonListReq.ok || !seriesSeasonListReq.res) { + console.error('Series Request FAILED!'); + return; + } + // parse data + const seasonsList = (await seriesSeasonListReq.res.json()) as SeriesSearch; + if (seasonsList.total < 1) { + console.info('Series is empty!'); + return; + } + for (const item of seasonsList.data) { + await this.logObject(item, pad + 2); + } + } + + public async logMovieListingById(id: string, pad?: number) { + pad = pad || 2; + if (!this.cmsToken.cms_web) { + console.error('Authentication required!'); + return; + } + + // opts + const AuthHeaders = { + headers: { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader + }, + useProxy: true + }; + + //Movie Listing + const movieListingReq = await this.req.getData(`${api.content_cms}/movie_listings/${id}?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); + if (!movieListingReq.ok || !movieListingReq.res) { + console.error('Movie Listing Request FAILED!'); + return; + } + const movieListing = await movieListingReq.res.json(); + if (movieListing.total < 1) { + console.info('Movie Listing is empty!'); + return; + } + for (const item of movieListing.data) { + await this.logObject(item, pad, false, false); + } + + //Movies + const moviesListReq = await this.req.getData( + `${api.content_cms}/movie_listings/${id}/movies?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, + AuthHeaders + ); + if (!moviesListReq.ok || !moviesListReq.res) { + console.error('Movies List Request FAILED!'); + return; + } + const moviesList = await moviesListReq.res.json(); + for (const item of moviesList.data) { + await this.logObject(item, pad + 2); + } + } + + public async getNewlyAdded(page: number = 1, type: string, raw: boolean = false, rawoutput?: string) { + if (!this.token.access_token) { + console.error('Authentication required!'); + return; + } + const newlyAddedReqOpts = { + headers: { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader + }, + useProxy: true + }; + const newlyAddedParams = new URLSearchParams({ + sort_by: 'newly_added', + type: type || 'series', + n: '50', + start: (page ? (page - 1) * 25 : 0).toString(), + preferred_audio_language: 'ja-JP', + force_locale: '', + locale: this.locale + }).toString(); + const newlyAddedReq = await this.req.getData(`${api.browse}?${newlyAddedParams}`, newlyAddedReqOpts); + if (!newlyAddedReq.ok || !newlyAddedReq.res) { + console.error('Get newly added FAILED!'); + return; + } + const newlyAddedResults = await newlyAddedReq.res.json(); + + if (raw) { + console.info(JSON.stringify(newlyAddedResults, null, 2)); + if (rawoutput) { + try { + fs.writeFileSync(rawoutput, JSON.stringify(newlyAddedResults), { encoding: 'utf-8' }); + console.info(`Raw output saved to ${rawoutput}`); + } catch (e) { + console.error(`Failed to save raw output to ${rawoutput}:`, e); + } + } + return; + } + + console.info('Newly added:'); + for (const i of newlyAddedResults.items) { + await this.logObject(i, 2); + } + // calculate pages + const itemPad = parseInt(new URL(newlyAddedResults.__href__, domain.cr_www).searchParams.get('start') as string); + const pageCur = itemPad > 0 ? Math.ceil(itemPad / 25) + 1 : 1; + const pageMax = Math.ceil(newlyAddedResults.total / 25); + console.info(` Total results: ${newlyAddedResults.total} (Page: ${pageCur}/${pageMax})`); + } + + public async getSeasonById(id: string, numbers: number, e: string | undefined, but: boolean, all: boolean): Promise> { + if (!this.cmsToken.cms_web) { + console.error('Authentication required!'); + return { isOk: false, reason: new Error('Authentication required') }; + } + + const AuthHeaders = { + headers: { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader + }, + useProxy: true + }; + + //get show info + const showInfoReq = await this.req.getData(`${api.content_cms}/seasons/${id}?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); + if (!showInfoReq.ok || !showInfoReq.res) { + console.error('Show Request FAILED!'); + return { isOk: false, reason: new Error('Show request failed. No more information provided.') }; + } + const showInfo = await showInfoReq.res.json(); + await this.logObject(showInfo.data[0], 0); + + let episodeList = { total: 0, data: [], meta: {} } as CrunchyEpisodeList; + //get episode info + const reqEpsListOpts = [ + api.cms_bucket, + this.cmsToken.cms_web.bucket, + '/episodes?', + new URLSearchParams({ + force_locale: '', + preferred_audio_language: 'ja-JP', + locale: this.locale, + season_id: id, + Policy: this.cmsToken.cms_web.policy, + Signature: this.cmsToken.cms_web.signature, + 'Key-Pair-Id': this.cmsToken.cms_web.key_pair_id + }) + ].join(''); + const reqEpsList = await this.req.getData(reqEpsListOpts, AuthHeaders); + if (!reqEpsList.ok || !reqEpsList.res) { + console.error('Episode List Request FAILED!'); + return { isOk: false, reason: new Error('Episode List request failed. No more information provided.') }; + } + //CrunchyEpisodeList + const episodeListAndroid = (await reqEpsList.res.json()) as CrunchyAndroidEpisodes; + episodeList = { + total: episodeListAndroid.total, + data: episodeListAndroid.items, + meta: {} + }; + + const epNumList: { + ep: number[]; + sp: number; + } = { ep: [], sp: 0 }; + const epNumLen = numbers; + + if (episodeList.total < 1) { + console.info(' Season is empty!'); + return { isOk: true, value: [] }; + } + + const doEpsFilter = parseSelect(e as string); + const selectedMedia: CrunchyEpMeta[] = []; + + episodeList.data.forEach((item) => { + item.hide_season_title = true; + if (item.season_title == '' && item.series_title != '') { + item.season_title = item.series_title; + item.hide_season_title = false; + item.hide_season_number = true; + } + if (item.season_title == '' && item.series_title == '') { + item.season_title = 'NO_TITLE'; + item.series_title = 'NO_TITLE'; + } + const epNum = item.episode; + let isSpecial = false; + item.isSelected = false; + if (!epNum.match(/^\d+$/) || epNumList.ep.indexOf(parseInt(epNum, 10)) > -1) { + isSpecial = true; + epNumList.sp++; + } else { + epNumList.ep.push(parseInt(epNum, 10)); + } + const selEpId = isSpecial ? 'S' + epNumList.sp.toString().padStart(epNumLen, '0') : '' + parseInt(epNum, 10).toString().padStart(epNumLen, '0'); + // set data + const images = (item.images.thumbnail ?? [[{ source: '/notFound.png' }]])[0]; + const epMeta: CrunchyEpMeta = { + data: [ + { + mediaId: item.id, + versions: null, + lang: langsData.languages.find((a) => a.code == yargs.appArgv(this.cfg.cli).dubLang[0]), + isSubbed: item.is_subbed, + isDubbed: item.is_dubbed + } + ], + seriesTitle: item.series_title, + seasonTitle: item.season_title, + episodeNumber: item.episode, + episodeTitle: item.title, + seasonID: item.season_id, + season: item.season_number, + showID: id, + e: selEpId, + image: images[Math.floor(images.length / 2)].source + }; + // Check for streams_link and update playback var if needed + if (item.__links__?.streams?.href) { + epMeta.data[0].playback = item.__links__.streams.href; + if (!item.playback) { + item.playback = item.__links__.streams.href; + } + } + if (item.streams_link) { + epMeta.data[0].playback = item.streams_link; + if (!item.playback) { + item.playback = item.streams_link; + } + } + if (item.versions) { + epMeta.data[0].versions = item.versions; + } + // find episode numbers + if ( + (but && item.playback && !doEpsFilter.isSelected([selEpId, item.id])) || + (all && item.playback) || + (!but && doEpsFilter.isSelected([selEpId, item.id]) && !item.isSelected && item.playback) + ) { + selectedMedia.push(epMeta); + item.isSelected = true; + } + // show ep + item.seq_id = selEpId; + this.logObject(item); + }); + + // display + if (selectedMedia.length < 1) { + console.info('\nEpisodes not selected!\n'); + } + + console.info(''); + return { isOk: true, value: selectedMedia }; + } + + public async downloadEpisode(data: CrunchyEpMeta, options: CrunchyDownloadOptions, isSeries?: boolean): Promise { + const res = await this.downloadMediaList(data, options); + if (res === undefined || res.error) { + return false; + } else { + if (!options.skipmux) { + await this.muxStreams(res.data, { ...options, output: res.fileName }); + } else { + console.info('Skipping mux'); + } + if (!isSeries) { + downloaded( + { + service: 'crunchy', + type: 's' + }, + data.seasonID, + [data.e] + ); + } else { + downloaded( + { + service: 'crunchy', + type: 'srz' + }, + data.showID, + [data.e] + ); + } + } + return true; + } + + public async getObjectById(e?: string, earlyReturn?: boolean, external_id?: boolean): Promise[] | undefined> { + if (!this.cmsToken.cms_web) { + console.error('Authentication required!'); + return []; + } + + let convertedObjects; + if (external_id) { + const epFilter = parseSelect(e as string); + const objectIds = []; + for (const ob of epFilter.values) { + const extIdReqOpts = [ + api.cms_bucket, + this.cmsToken.cms_web.bucket, + '/channels/crunchyroll/objects', + '?', + new URLSearchParams({ + force_locale: '', + preferred_audio_language: 'ja-JP', + locale: this.locale, + external_id: ob, + Policy: this.cmsToken.cms_web.policy, + Signature: this.cmsToken.cms_web.signature, + 'Key-Pair-Id': this.cmsToken.cms_web.key_pair_id + }) + ].join(''); + + const extIdReq = await this.req.getData(extIdReqOpts, { + headers: { + 'User-Agent': api.crunchyDefUserAgent + } + }); + if (!extIdReq.ok || !extIdReq.res) { + console.error('Objects Request FAILED!'); + if (extIdReq.error && extIdReq.error.res && extIdReq.error.res.body) { + console.info('[INFO] Body:', extIdReq.error.res.body); + } + continue; + } + + const oldObjectInfo = (await extIdReq.res.json()) as Record; + for (const object of oldObjectInfo.items) { + objectIds.push(object.id); + } + } + convertedObjects = objectIds.join(','); + } + + const doEpsFilter = parseSelect(convertedObjects ?? (e as string)); + + if (doEpsFilter.values.length < 1) { + console.info('\nObjects not selected!\n'); + return []; + } + + // node index.js --service crunchy -e G6497Z43Y,GRZXCMN1W,G62PEZ2E6,G25FVGDEK,GZ7UVPVX5 + console.info('Requested object ID: %s', doEpsFilter.values.join(', ')); + + const AuthHeaders = { + headers: { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader + }, + useProxy: true + }; + + // reqs + let objectInfo: ObjectInfo = { total: 0, data: [], meta: {} }; + const objectReqOpts = [ + api.cms_bucket, + this.cmsToken.cms_web.bucket, + '/objects/', + doEpsFilter.values.join(','), + '?', + new URLSearchParams({ + force_locale: '', + preferred_audio_language: 'ja-JP', + locale: this.locale, + Policy: this.cmsToken.cms_web.policy, + Signature: this.cmsToken.cms_web.signature, + 'Key-Pair-Id': this.cmsToken.cms_web.key_pair_id + }) + ].join(''); + const objectReq = await this.req.getData(objectReqOpts, AuthHeaders); + if (!objectReq.ok || !objectReq.res) { + console.error('Objects Request FAILED!'); + if (objectReq.error && objectReq.error.res && objectReq.error.res.body) { + const objectInfo = await objectReq.error.res.json(); + console.info('Body:', JSON.stringify(objectInfo, null, '\t')); + objectInfo.error = true; + return objectInfo; + } + return []; + } + const objectInfoAndroid = (await objectReq.res.json()) as CrunchyAndroidObject; + objectInfo = { + total: objectInfoAndroid.total, + data: objectInfoAndroid.items, + meta: {} + }; + + if (earlyReturn) { + return objectInfo; + } + + const selectedMedia: Partial[] = []; + + for (const item of objectInfo.data) { + if (item.type != 'episode' && item.type != 'movie') { + await this.logObject(item, 2, true, false); + continue; + } + const epMeta: Partial = {}; + + epMeta.data = []; + if (item.episode_metadata) { + item.s_num = 'S:' + item.episode_metadata.season_id; + epMeta.data = [ + { + mediaId: 'E:' + item.id, + versions: item.episode_metadata.versions, + isSubbed: item.episode_metadata.is_subbed, + isDubbed: item.episode_metadata.is_dubbed + } + ]; + epMeta.seriesTitle = item.episode_metadata.series_title; + epMeta.seasonTitle = item.episode_metadata.season_title; + epMeta.episodeNumber = item.episode_metadata.episode; + epMeta.episodeTitle = item.title; + epMeta.season = item.episode_metadata.season_number; + } else if (item.movie_listing_metadata) { + item.f_num = 'F:' + item.id; + epMeta.data = [ + { + mediaId: 'M:' + item.id, + isSubbed: item.movie_listing_metadata.is_subbed, + isDubbed: item.movie_listing_metadata.is_dubbed + } + ]; + epMeta.seriesTitle = item.title; + epMeta.seasonTitle = item.title; + epMeta.episodeNumber = 'Movie'; + epMeta.episodeTitle = item.title; + } else if (item.movie_metadata) { + item.f_num = 'F:' + item.id; + epMeta.data = [ + { + mediaId: 'M:' + item.id, + isSubbed: item.movie_metadata.is_subbed, + isDubbed: item.movie_metadata.is_dubbed + } + ]; + epMeta.season = 0; + epMeta.seriesTitle = item.title; + epMeta.seasonTitle = item.title; + epMeta.episodeNumber = 'Movie'; + epMeta.episodeTitle = item.title; + } + if (item.streams_link) { + epMeta.data[0].playback = item.streams_link; + if (!item.playback) { + item.playback = item.streams_link; + } + selectedMedia.push(epMeta); + item.isSelected = true; + } else if (item.__links__) { + epMeta.data[0].playback = item.__links__.streams.href; + if (!item.playback) { + item.playback = item.__links__.streams.href; + } + selectedMedia.push(epMeta); + item.isSelected = true; + } + await this.logObject(item, 2); + } + console.info(''); + return selectedMedia; + } + + private convertDownloadToPlayback(audioUrl: string, videoUrl: string): string { + try { + const url = new URL(audioUrl); + const urla = new URL(videoUrl); + url.pathname = url.pathname.replace('/manifest/download/', '/manifest/'); + url.searchParams.delete('downloadGuid'); + url.searchParams.set('playbackGuid', urla.searchParams.get('playbackGuid') as string); + + return url.toString(); + } catch (err) { + return audioUrl; + } + } + + public async downloadMediaList( + medias: CrunchyEpMeta, + options: CrunchyDownloadOptions + ): Promise< + | { + data: DownloadedMedia[]; + fileName: string; + error: boolean; + } + | undefined + > { + if (!this.cmsToken.cms_web) { + console.error('Authentication required!'); + return; + } + + if (!this.cfg.bin.ffmpeg) this.cfg.bin = await yamlCfg.loadBinCfg(); + + let mediaName = '...'; + let fileName; + const variables: Variable[] = []; + if (medias.seasonTitle && medias.episodeNumber && medias.episodeTitle) { + mediaName = `${medias.seasonTitle} - ${medias.episodeNumber} - ${medias.episodeTitle}`; + } + + const files: DownloadedMedia[] = []; + + if (medias.data.every((a) => !a.playback)) { + console.warn('Video not available!'); + return undefined; + } + + let dlFailed = false; + let dlVideoOnce = false; // Variable to save if best selected video quality was downloaded + + for (const mMeta of medias.data) { + console.info(`Requesting: [${mMeta.mediaId}] ${mediaName}`); + + // Make sure we have a media id without a : in it + const currentMediaId = mMeta.mediaId.includes(':') ? mMeta.mediaId.split(':')[1] : mMeta.mediaId; + + //Make sure token is up-to-date + await this.refreshToken(true, true); + let currentVersion; + let isPrimary = mMeta.isSubbed; + const AuthHeaders: RequestInit = { + headers: { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader + } + }; + + //Get Media GUID + let mediaId = mMeta.mediaId; + if (mMeta.versions) { + if (mMeta.lang) { + currentVersion = mMeta.versions.find((a) => a.audio_locale == mMeta.lang?.cr_locale); + } else if (options.dubLang.length == 1) { + const currentLang = langsData.languages.find((a) => a.code == options.dubLang[0]); + currentVersion = mMeta.versions.find((a) => a.audio_locale == currentLang?.cr_locale); + } else if (mMeta.versions.length == 1) { + currentVersion = mMeta.versions[0]; + } + if (!currentVersion?.media_guid) { + console.error('Selected language not found in versions.'); + continue; + } + isPrimary = currentVersion.original; + mediaId = currentVersion?.media_guid; + } + + // If for whatever reason mediaId has a :, return the ID only + if (mediaId.includes(':')) mediaId = mediaId.split(':')[1]; + + const compiledChapters: string[] = []; + if (options.chapters) { + //Make Chapter Request + const chapterRequest = await this.req.getData(`https://static.crunchyroll.com/skip-events/production/${currentMediaId}.json`, { + headers: api.crunchyDefHeader + }); + if (!chapterRequest.ok || !chapterRequest.res) { + //Old Chapter Request Fallback + console.warn('Chapter request failed, attempting old API'); + const oldChapterRequest = await this.req.getData(`https://static.crunchyroll.com/datalab-intro-v2/${currentMediaId}.json`, { + headers: api.crunchyDefHeader + }); + if (!oldChapterRequest.ok || !oldChapterRequest.res) { + console.warn('Old Chapter API request failed'); + } else { + console.info('Old Chapter request successful'); + const chapterData = (await oldChapterRequest.res.json()) as CrunchyOldChapter; + + //Generate Timestamps + const startTime = new Date(0), + endTime = new Date(0); + startTime.setSeconds(chapterData.startTime); + endTime.setSeconds(chapterData.endTime); + const startTimeMS = String(chapterData.startTime).split('.')[1], + endTimeMS = String(chapterData.endTime).split('.')[1]; + const startMS = startTimeMS ? startTimeMS : '00', + endMS = endTimeMS ? endTimeMS : '00'; + const startFormatted = startTime.toISOString().substring(11, 19) + '.' + startMS; + const endFormatted = endTime.toISOString().substring(11, 19) + '.' + endMS; + + //Push Generated Chapters + if (chapterData.startTime > 1) { + compiledChapters.push(`CHAPTER${compiledChapters.length / 2 + 1}=00:00:00.00`, `CHAPTER${compiledChapters.length / 2 + 1}NAME=Prologue`); + } + compiledChapters.push(`CHAPTER${compiledChapters.length / 2 + 1}=${startFormatted}`, `CHAPTER${compiledChapters.length / 2 + 1}NAME=Intro`); + compiledChapters.push(`CHAPTER${compiledChapters.length / 2 + 1}=${endFormatted}`, `CHAPTER${compiledChapters.length / 2 + 1}NAME=Episode`); + } + } else { + //Chapter request succeeded, now let's parse them + console.info('Chapter request successful'); + const chapterData = (await chapterRequest.res.json()) as CrunchyChapters; + const chapters: CrunchyChapter[] = []; + + //Make a format more usable for the crunchy chapters + for (const chapter in chapterData) { + if (typeof chapterData[chapter] == 'object') { + chapters.push(chapterData[chapter]); + } + } + + if (chapters.length > 0) { + chapters.sort((a, b) => a.start - b.start); + //Check if chapters has an intro + //if (!(chapters.find(c => c.type === 'intro') || chapters.find(c => c.type === 'recap'))) { + if (!chapters.find((c) => c.type === 'intro')) { + compiledChapters.push(`CHAPTER${compiledChapters.length / 2 + 1}=00:00:00.00`, `CHAPTER${compiledChapters.length / 2 + 1}NAME=Episode`); + } + + //Loop through all the chapters + for (const chapter of chapters) { + if (typeof chapter.start == 'undefined' || typeof chapter.end == 'undefined') continue; + //Generate timestamps + const startTime = new Date(0), + endTime = new Date(0); + startTime.setSeconds(chapter.start); + endTime.setSeconds(chapter.end); + const startFormatted = startTime.toISOString().substring(11, 19) + '.00'; + const endFormatted = endTime.toISOString().substring(11, 19) + '.00'; + //Find the max start time from the chapters + const maxStart = Math.max(...chapters.map((obj) => obj.start).filter((start): start is number => start !== null && start !== undefined)); + //We need the duration of the ep + let epDuration: number | undefined; + const epiMeta = await this.req.getData( + `${api.content_cms}/objects/${currentMediaId}?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, + AuthHeaders + ); + if (!epiMeta.ok || !epiMeta.res) { + epDuration = 7200; + } else { + epDuration = Math.floor((await epiMeta.res.json()).data[0].episode_metadata.duration_ms / 1000 - 3); + } + + //Push generated chapters + if (chapter.type == 'intro') { + if (chapter.start > 0) { + compiledChapters.push(`CHAPTER${compiledChapters.length / 2 + 1}=00:00:00.00`, `CHAPTER${compiledChapters.length / 2 + 1}NAME=Episode`); + } + compiledChapters.push( + `CHAPTER${compiledChapters.length / 2 + 1}=${startFormatted}`, + `CHAPTER${compiledChapters.length / 2 + 1}NAME=${chapter.type.charAt(0).toUpperCase() + chapter.type.slice(1)}` + ); + if (chapter.end < epDuration && chapter.end != maxStart) { + compiledChapters.push(`CHAPTER${compiledChapters.length / 2 + 1}=${endFormatted}`, `CHAPTER${compiledChapters.length / 2 + 1}NAME=Episode`); + } + } else { + if (chapter.type !== 'recap') { + compiledChapters.push( + `CHAPTER${compiledChapters.length / 2 + 1}=${startFormatted}`, + `CHAPTER${compiledChapters.length / 2 + 1}NAME=${chapter.type.charAt(0).toUpperCase() + chapter.type.slice(1)}` + ); + if (chapter.end < epDuration && chapter.end != maxStart) { + compiledChapters.push(`CHAPTER${compiledChapters.length / 2 + 1}=${endFormatted}`, `CHAPTER${compiledChapters.length / 2 + 1}NAME=Episode`); + } + } + } + } + } + } + } + + const pbData = { total: 0, vpb: {}, apb: {}, meta: {} } as PlaybackData; + + let videoStream: CrunchyPlayStream | null = null; + let audioStream: CrunchyPlayStream | null = null; + let isDLVideoBypass: boolean = options.vstream === 'android' || options.vstream === 'androidtab' ? true : false; + let isDLAudioBypass: boolean = options.astream === 'android' || options.astream === 'androidtab' ? true : false; + let isDLBypassCapable: boolean = true; + + if (isDLVideoBypass || isDLAudioBypass) { + const me = await this.req.getData(api.me, AuthHeaders); + if (me.ok && me.res) { + const data_me = await me.res.json(); + const benefits = await this.req.getData(`https://www.crunchyroll.com/subs/v1/subscriptions/${data_me.external_id}/benefits`, AuthHeaders); + if (benefits.ok && benefits.res) { + const data_benefits = (await benefits.res.json()) as { items: { benefit: string }[] }; + if (data_benefits?.items && !data_benefits.items.find((i) => i.benefit === 'offline_viewing')) { + isDLBypassCapable = false; + } + } else { + isDLBypassCapable = false; + } + } else { + isDLBypassCapable = false; + } + } + + if (isDLVideoBypass && !isDLBypassCapable) { + isDLVideoBypass = false; + options.vstream = 'androidtv'; + console.warn( + 'VBR video downloads are not available on your current Crunchyroll plan. Please upgrade to the "Mega Fan" plan to enable this feature. Falling back to CBR video stream.' + ); + } + + if (isDLAudioBypass && !isDLBypassCapable) { + isDLAudioBypass = false; + options.astream = 'androidtv'; + console.warn( + '192 kb/s audio downloads are not available on your current Crunchyroll plan. Please upgrade to the "Mega Fan" plan to enable this feature. Falling back to 128 kb/s CBR stream.' + ); + } + + if (options.tsd) { + console.warn('Total Session Death Active'); + const activeStreamsReq = await this.req.getData(api.streaming_sessions, AuthHeaders); + if (activeStreamsReq.ok && activeStreamsReq.res) { + const data = await activeStreamsReq.res.json(); + for (const s of data.items) { + await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${s.contentId}/${s.token}`, { ...{ method: 'DELETE' }, ...AuthHeaders }); + } + console.warn(`Killed ${data.items?.length ?? 0} Sessions`); + } + } + + const videoPlaybackReq = await this.req.getData( + `https://www.crunchyroll.com/playback/v3/${currentVersion ? currentVersion.guid : currentMediaId}/${CrunchyVideoPlayStreams['androidtv']}/play?queue=0`, + AuthHeaders + ); + if (!videoPlaybackReq.ok || !videoPlaybackReq.res) { + console.warn('Request Video Stream URLs FAILED!'); + } else { + videoStream = (await videoPlaybackReq.res.json()) as CrunchyPlayStream; + const derivedPlaystreams = {} as CrunchyStreams; + for (const hardsub in videoStream.hardSubs) { + const stream = videoStream.hardSubs[hardsub]; + derivedPlaystreams[hardsub] = { + url: stream.url, + hardsub_locale: stream.hlang + }; + } + if (isDLVideoBypass) { + const videoDLReq = await this.req.getData( + `https://www.crunchyroll.com/playback/v3/${currentVersion ? currentVersion.guid : currentMediaId}/${CrunchyVideoPlayStreams[options.vstream]}/download`, + AuthHeaders + ); + if (videoDLReq.ok && videoDLReq.res) { + const data = (await videoDLReq.res.json()) as CrunchyPlayStream; + derivedPlaystreams[''] = { + url: this.convertDownloadToPlayback(data.url, videoStream.url), + hardsub_locale: '' + }; + } else { + derivedPlaystreams[''] = { + url: videoStream.url, + hardsub_locale: '' + }; + } + } else { + derivedPlaystreams[''] = { + url: videoStream.url, + hardsub_locale: '' + }; + } + pbData.meta = { + audio_locale: videoStream.audioLocale, + bifs: [videoStream.bifs], + captions: videoStream.captions, + closed_captions: videoStream.captions, + media_id: videoStream.assetId, + subtitles: videoStream.subtitles, + versions: videoStream.versions + }; + pbData.vpb[`adaptive_${options.vstream}_${videoStream.url.includes('m3u8') ? 'hls' : 'dash'}_drm`] = { + ...derivedPlaystreams + }; + } + + if (!options.cstream && options.vstream !== options.astream && videoStream) { + const audioPlaybackReq = await this.req.getData( + `https://www.crunchyroll.com/playback/v3/${currentVersion ? currentVersion.guid : currentMediaId}/${CrunchyAudioPlayStreams[options.astream]}/${isDLAudioBypass ? 'download' : 'play?queue=1'}`, + AuthHeaders + ); + if (!audioPlaybackReq.ok || !audioPlaybackReq.res) { + console.warn('Request Audio Stream URLs FAILED!'); + } else { + audioStream = (await audioPlaybackReq.res.json()) as CrunchyPlayStream; + const derivedPlaystreams = {} as CrunchyStreams; + for (const hardsub in audioStream.hardSubs) { + const stream = audioStream.hardSubs[hardsub]; + derivedPlaystreams[hardsub] = { + url: stream.url, + hardsub_locale: stream.hlang + }; + } + if (isDLAudioBypass) { + audioStream.token = videoStream.token; + derivedPlaystreams[''] = { + url: this.convertDownloadToPlayback(audioStream.url, videoStream.url), + hardsub_locale: '' + }; + } else { + derivedPlaystreams[''] = { + url: audioStream.url, + hardsub_locale: '' + }; + } + pbData.meta = { + audio_locale: audioStream.audioLocale, + bifs: [audioStream.bifs], + captions: audioStream.captions, + closed_captions: audioStream.captions, + media_id: audioStream.assetId, + subtitles: audioStream.subtitles, + versions: audioStream.versions + }; + pbData.apb[`adaptive_${options.astream}_${audioStream.url.includes('m3u8') ? 'hls' : 'dash'}_drm`] = { + ...derivedPlaystreams + }; + } + } else { + pbData.apb = pbData.vpb; + } + + variables.push( + ...( + [ + ['title', medias.episodeTitle, true], + ['episode', isNaN(parseFloat(medias.episodeNumber)) ? medias.episodeNumber : parseFloat(medias.episodeNumber), false], + ['service', 'CR', false], + ['seriesTitle', medias.seriesTitle, true], + ['showTitle', medias.seriesTitle ?? medias.seasonTitle, true], + ['season', medias.season, false] + ] as [AvailableFilenameVars, string | number, boolean][] + ).map((a): Variable => { + return { + name: a[0], + replaceWith: a[1], + type: typeof a[1], + sanitize: a[2] + } as Variable; + }) + ); + + let vstreams: any[] = []; + let astreams: any[] = []; + let hsLangs: string[] = []; + const vpbStreams = pbData.vpb; + const apbStreams = pbData.apb; + + if (!canDecrypt && (!options.novids || !options.noaudio)) { + console.error('No valid Widevine or PlayReady CDM detected. Please ensure a supported and functional CDM is installed.'); + return undefined; + } + + if (!this.cfg.bin.mp4decrypt && !this.cfg.bin.shaka && (!options.novids || !options.noaudio)) { + console.error('Neither Shaka nor MP4Decrypt found. Please ensure at least one of them is installed.'); + return undefined; + } + + for (const s of Object.keys(pbData.vpb)) { + if ((s.match(/hls/) || s.match(/dash/)) && !(s.match(/hls/) && s.match(/drm/)) && !s.match(/trailer/)) { + const pb = Object.values(vpbStreams[s]).map((v) => { + v.hardsub_lang = v.hardsub_locale ? langsData.fixAndFindCrLC(v.hardsub_locale).locale : v.hardsub_locale; + if (v.hardsub_lang && hsLangs.indexOf(v.hardsub_lang) < 0) { + hsLangs.push(v.hardsub_lang); + } + return { + ...v, + ...{ format: s } + }; + }); + vstreams.push(...pb); + } + } + + for (const s of Object.keys(pbData.apb)) { + if ((s.match(/hls/) || s.match(/dash/)) && !(s.match(/hls/) && s.match(/drm/)) && !s.match(/trailer/)) { + const pb = Object.values(apbStreams[s]).map((v) => { + v.hardsub_lang = v.hardsub_locale ? langsData.fixAndFindCrLC(v.hardsub_locale).locale : v.hardsub_locale; + if (v.hardsub_lang && hsLangs.indexOf(v.hardsub_lang) < 0) { + hsLangs.push(v.hardsub_lang); + } + return { + ...v, + ...{ format: s } + }; + }); + astreams.push(...pb); + } + } + + if (vstreams.length < 1) { + console.warn('No full video streams found!'); + return undefined; + } + + if (astreams.length < 1) { + console.warn('No full audio streams found!'); + return undefined; + } + + const audDub = langsData.findLang(langsData.fixLanguageTag(pbData.meta.audio_locale as string) || '').code; + hsLangs = langsData.sortTags(hsLangs); + + vstreams = vstreams.map((s) => { + s.audio_lang = audDub; + s.hardsub_lang = s.hardsub_lang ? s.hardsub_lang : '-'; + s.type = `${s.format}/${s.audio_lang}/${s.hardsub_lang}`; + return s; + }); + + vstreams = vstreams.sort((a, b) => { + if (a.type < b.type) { + return -1; + } + return 0; + }); + + astreams = astreams.map((s) => { + s.audio_lang = audDub; + s.hardsub_lang = s.hardsub_lang ? s.hardsub_lang : '-'; + s.type = `${s.format}/${s.audio_lang}/${s.hardsub_lang}`; + return s; + }); + + astreams = astreams.sort((a, b) => { + if (a.type < b.type) { + return -1; + } + return 0; + }); + + if (options.hslang != 'none') { + if (hsLangs.indexOf(options.hslang) > -1) { + console.info('Selecting stream with %s hardsubs', langsData.locale2language(options.hslang).language); + vstreams = vstreams.filter((s) => { + if (s.hardsub_lang == '-') { + return false; + } + return s.hardsub_lang == options.hslang; + }); + astreams = astreams.filter((s) => { + if (s.hardsub_lang == '-') { + return false; + } + return s.hardsub_lang == options.hslang; + }); + } else { + console.warn('Selected stream with %s hardsubs not available', langsData.locale2language(options.hslang).language); + if (hsLangs.length > 0) { + console.warn('Try other hardsubs stream:', hsLangs.join(', ')); + } + dlFailed = true; + } + } else { + vstreams = vstreams.filter((s) => { + return s.hardsub_lang == '-'; + }); + astreams = astreams.filter((s) => { + return s.hardsub_lang == '-'; + }); + if (vstreams.length < 1) { + console.warn('Raw video streams not available!'); + if (hsLangs.length > 0) { + console.warn('Try hardsubs stream:', hsLangs.join(', ')); + } + dlFailed = true; + } + if (astreams.length < 1) { + console.warn('Raw audio streams not available!'); + if (hsLangs.length > 0) { + console.warn('Try hardsubs stream:', hsLangs.join(', ')); + } + dlFailed = true; + } + console.info('Selecting raw stream'); + } + + let vcurStream: undefined | (typeof vstreams)[0] = undefined; + let acurStream: undefined | (typeof astreams)[0] = undefined; + + if (!dlFailed) { + console.info('Downloading...'); + vcurStream = vstreams[0]; + acurStream = astreams[0]; + + console.info('Video Playlists URL: %s (%s)', vcurStream.url, vcurStream.type); + console.info('Audio Playlists URL: %s (%s)', acurStream.url, acurStream.type); + } + + let tsFile = undefined; + + // Delete the stream if it's not needed + if (options.novids && options.noaudio) { + if (videoStream) { + await this.refreshToken(true, true); + await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${videoStream.token}`, { + ...{ method: 'DELETE' }, + ...AuthHeaders + }); + } + if (audioStream && videoStream?.token !== audioStream.token) { + await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${audioStream.token}`, { + ...{ method: 'DELETE' }, + ...AuthHeaders + }); + } + } + + if (!dlFailed && vcurStream && acurStream && vcurStream !== undefined && acurStream !== undefined && !(options.novids && options.noaudio)) { + const vstreamPlaylistsReq = await this.req.getData(vcurStream.url, AuthHeaders); + const astreamPlaylistsReq = vcurStream.url !== acurStream.url ? await this.req.getData(acurStream.url, AuthHeaders) : vstreamPlaylistsReq; + if (!vstreamPlaylistsReq.ok || !vstreamPlaylistsReq.res || !astreamPlaylistsReq.ok || !astreamPlaylistsReq.res) { + console.error("CAN'T FETCH VIDEO PLAYLISTS!"); + dlFailed = true; + } else { + const vstreamPlaylistBody = await vstreamPlaylistsReq.res.text(); + const astreamPlaylistBody = vcurStream.url !== acurStream.url ? await astreamPlaylistsReq.res.text() : vstreamPlaylistBody; + if (vstreamPlaylistBody.match('MPD') && astreamPlaylistBody.match('MPD')) { + //Parse MPD Playlists + const vstreamPlaylists = await parse( + vstreamPlaylistBody, + langsData.findLang(langsData.fixLanguageTag(pbData.meta.audio_locale as string) || ''), + vcurStream.url.match(/.*\.urlset\//)?.[0] + ); + const astreamPlaylists = + vcurStream.url !== acurStream.url + ? await parse( + astreamPlaylistBody, + langsData.findLang(langsData.fixLanguageTag(pbData.meta.audio_locale as string) || ''), + acurStream.url.match(/.*\.urlset\//)?.[0] + ) + : vstreamPlaylists; + + //Get name of CDNs/Servers + const vstreamServers = Object.keys(vstreamPlaylists); + const astreamServers = Object.keys(astreamPlaylists); + + options.x = options.x > vstreamServers.length ? 1 : options.x; + + const vselectedServer = vstreamServers[options.x - 1]; + const vselectedList = vstreamPlaylists[vselectedServer]; + + const aselectedServer = astreamServers[options.x - 1]; + const aselectedList = astreamPlaylists[aselectedServer]; + + //set Video Qualities + const videos = vselectedList.video.map((item) => { + return { + ...item, + resolutionText: `${item.quality.width}x${item.quality.height} (${Math.round(item.bandwidth / 1024)}KiB/s)` + }; + }); + + const audios = aselectedList.audio.map((item) => { + return { + ...item, + resolutionText: `${Math.round(item.bandwidth / 1000)}kB/s` + }; + }); + + videos.sort((a, b) => { + return a.quality.width - b.quality.width; + }); + + audios.sort((a, b) => { + return a.bandwidth - b.bandwidth; + }); + + let chosenVideoQuality = options.q === 0 ? videos.length : options.q; + if (chosenVideoQuality > videos.length) { + console.warn( + `The requested quality of ${options.q} is greater than the maximum ${videos.length}.\n[WARN] Therefor the maximum will be capped at ${videos.length}.` + ); + chosenVideoQuality = videos.length; + } + chosenVideoQuality--; + + let chosenAudioQuality = options.q === 0 ? audios.length : options.q; + if (chosenAudioQuality > audios.length) { + chosenAudioQuality = audios.length; + } + chosenAudioQuality--; + + const chosenVideoSegments = videos[chosenVideoQuality]; + const chosenAudioSegments = audios[chosenAudioQuality]; + + console.info(`Available Video Qualities:\n\t${videos.map((a, ind) => `[${ind + 1}] ${a.resolutionText}`).join('\n\t')}`); + console.info(`Available Audio Qualities:\n\t${audios.map((a, ind) => `[${ind + 1}] ${a.resolutionText}`).join('\n\t')}`); + + variables.push( + { + name: 'height', + type: 'number', + replaceWith: chosenVideoSegments.quality.height + }, + { + name: 'width', + type: 'number', + replaceWith: chosenVideoSegments.quality.width + } + ); + + const lang = langsData.languages.find((a) => a.code === acurStream?.audio_lang); + if (!lang) { + console.error(`Unable to find language for code ${acurStream.audio_lang}`); + return; + } + console.info( + `Selected quality: \n\tVideo: ${chosenVideoSegments.resolutionText}\n\tAudio: ${chosenAudioSegments.resolutionText}\n\tVideo Server: ${vselectedServer}\n\tAudio Server: ${aselectedServer}` + ); + console.info('Stream URL:', chosenVideoSegments.segments[0].uri.split(',.urlset')[0]); + // TODO check filename + fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); + const outFile = parseFileName(options.fileName + '.' + (mMeta.lang?.name || lang.name), variables, options.numbers, options.override).join(path.sep); + const tempFile = parseFileName(`temp-${currentVersion ? currentVersion.guid : currentMediaId}`, variables, options.numbers, options.override).join( + path.sep + ); + const tempTsFile = path.isAbsolute(tempFile as string) ? tempFile : path.join(this.cfg.dir.content, tempFile); + + let encryptionKeysVideo; + let encryptionKeysAudio; + + //Handle Getting Decryption Keys if needed + if (chosenVideoSegments.pssh_wvd || chosenVideoSegments.pssh_prd || chosenAudioSegments.pssh_wvd || chosenAudioSegments.pssh_prd) { + await this.refreshToken(true, true); + if (videoStream) { + await this.req.getData( + `https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${videoStream.token}/keepAlive?playhead=1`, + { ...{ method: 'PATCH' }, ...AuthHeaders } + ); + } + if (audioStream && videoStream?.token !== audioStream.token) { + await this.req.getData( + `https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${audioStream.token}/keepAlive?playhead=1`, + { ...{ method: 'PATCH' }, ...AuthHeaders } + ); + } + + console.info(`Getting decryption keys with ${cdm}`); + // New Crunchyroll DRM endpoint for Widevine + if (cdm === 'widevine') { + encryptionKeysVideo = await getKeysWVD(chosenVideoSegments.pssh_wvd, api.drm_widevine, { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader, + Pragma: 'no-cache', + 'Cache-Control': 'no-cache', + 'content-type': 'application/octet-stream', + 'x-cr-content-id': currentVersion ? currentVersion.guid : currentMediaId, + 'x-cr-video-token': videoStream!.token + }); + + // Check if the audio pssh is different since Crunchyroll started to have different dec keys for audio tracks + if (chosenAudioSegments.pssh_wvd && chosenAudioSegments.pssh_wvd !== chosenVideoSegments.pssh_wvd) { + encryptionKeysAudio = await getKeysWVD(chosenAudioSegments.pssh_wvd, api.drm_widevine, { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader, + Pragma: 'no-cache', + 'Cache-Control': 'no-cache', + 'content-type': 'application/octet-stream', + 'x-cr-content-id': currentVersion ? currentVersion.guid : currentMediaId, + 'x-cr-video-token': audioStream!.token + }); + } else { + encryptionKeysAudio = encryptionKeysVideo; + } + } + + // New Crunchyroll DRM endpoint for Playready + if (cdm === 'playready') { + encryptionKeysVideo = await getKeysPRD(chosenVideoSegments.pssh_prd, api.drm_playready, { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader, + Pragma: 'no-cache', + 'Cache-Control': 'no-cache', + 'content-type': 'application/octet-stream', + 'x-cr-content-id': currentVersion ? currentVersion.guid : currentMediaId, + 'x-cr-video-token': videoStream!.token + }); + + // Check if the audio pssh is different since Crunchyroll started to have different dec keys for audio tracks + if (chosenAudioSegments.pssh_prd && chosenAudioSegments.pssh_prd !== chosenVideoSegments.pssh_prd) { + encryptionKeysAudio = await getKeysPRD(chosenAudioSegments.pssh_prd, api.drm_playready, { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader, + Pragma: 'no-cache', + 'Cache-Control': 'no-cache', + 'content-type': 'application/octet-stream', + 'x-cr-content-id': currentVersion ? currentVersion.guid : currentMediaId, + 'x-cr-video-token': audioStream!.token + }); + } else { + encryptionKeysAudio = encryptionKeysVideo; + } + } + + if (!encryptionKeysVideo || encryptionKeysVideo.length == 0 || !encryptionKeysAudio || encryptionKeysAudio.length == 0) { + console.error('Failed to get encryption keys'); + return undefined; + } + + console.info('Got decryption keys'); + } + + if (videoStream) { + await this.refreshToken(true, true); + await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${videoStream.token}`, { + ...{ method: 'DELETE' }, + ...AuthHeaders + }); + } + if (audioStream && videoStream?.token !== audioStream.token) { + await this.req.getData(`https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${audioStream.token}`, { + ...{ method: 'DELETE' }, + ...AuthHeaders + }); + } + + let [audioDownloaded, videoDownloaded] = [false, false]; + + // When best selected video quality is already downloaded + if (dlVideoOnce && options.dlVideoOnce) { + console.info('Already downloaded video, skipping video download...'); + } else if (options.novids) { + console.info('Skipping video download...'); + } else { + //Download Video + const totalParts = chosenVideoSegments.segments.length; + const mathParts = Math.ceil(totalParts / options.partsize); + const mathMsg = `(${mathParts}*${options.partsize})`; + console.info('Total parts in video stream:', totalParts, mathMsg); + tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); + const dirName = path.dirname(tsFile); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + const videoJson: M3U8Json = { + segments: chosenVideoSegments.segments + }; + const videoDownload = await new streamdl({ + output: chosenVideoSegments.pssh_wvd || chosenVideoSegments.pssh_prd ? `${tempTsFile}.video.enc.m4s` : `${tsFile}.video.m4s`, + timeout: options.timeout, + m3u8json: videoJson, + // baseurl: chunkPlaylist.baseUrl, + threads: options.partsize, + fsRetryTime: options.fsRetryTime * 1000, + override: options.force, + callback: options.callbackMaker + ? options.callbackMaker({ + fileName: `${path.isAbsolute(outFile) ? outFile.slice(this.cfg.dir.content.length) : outFile}`, + image: medias.image, + parent: { + title: medias.seasonTitle + }, + title: medias.episodeTitle, + language: lang + }) + : undefined + }).download(); + if (!videoDownload.ok) { + console.error(`DL Stats: ${JSON.stringify(videoDownload.parts)}\n`); + dlFailed = true; + } + dlVideoOnce = true; + videoDownloaded = true; + } + + if (chosenAudioSegments && !options.noaudio) { + //Download Audio (if available) + const totalParts = chosenAudioSegments.segments.length; + const mathParts = Math.ceil(totalParts / options.partsize); + const mathMsg = `(${mathParts}*${options.partsize})`; + console.info('Total parts in audio stream:', totalParts, mathMsg); + tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); + const dirName = path.dirname(tsFile); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + const audioJson: M3U8Json = { + segments: chosenAudioSegments.segments + }; + const audioDownload = await new streamdl({ + output: chosenVideoSegments.pssh_wvd || chosenVideoSegments.pssh_prd ? `${tempTsFile}.audio.enc.m4s` : `${tsFile}.audio.m4s`, + timeout: options.timeout, + m3u8json: audioJson, + // baseurl: chunkPlaylist.baseUrl, + threads: options.partsize, + fsRetryTime: options.fsRetryTime * 1000, + override: options.force, + callback: options.callbackMaker + ? options.callbackMaker({ + fileName: `${path.isAbsolute(outFile) ? outFile.slice(this.cfg.dir.content.length) : outFile}`, + image: medias.image, + parent: { + title: medias.seasonTitle + }, + title: medias.episodeTitle, + language: lang + }) + : undefined + }).download(); + if (!audioDownload.ok) { + console.error(`DL Stats: ${JSON.stringify(audioDownload.parts)}\n`); + dlFailed = true; + } + audioDownloaded = true; + } else if (options.noaudio) { + console.info('Skipping audio download...'); + } + + //Handle Decryption if needed + if ( + (chosenVideoSegments.pssh_wvd || chosenVideoSegments.pssh_prd || chosenAudioSegments.pssh_wvd || chosenAudioSegments.pssh_prd) && + (videoDownloaded || audioDownloaded) && + !dlFailed + ) { + console.info('Decryption Needed, attempting to decrypt'); + if (this.cfg.bin.mp4decrypt || this.cfg.bin.shaka) { + let commandBaseVideo = `--show-progress --key ${encryptionKeysVideo?.[cdm === 'playready' ? 0 : 1].kid}:${encryptionKeysVideo?.[cdm === 'playready' ? 0 : 1].key} `; + let commandBaseAudio = `--show-progress --key ${encryptionKeysAudio?.[cdm === 'playready' ? 0 : 1].kid}:${encryptionKeysAudio?.[cdm === 'playready' ? 0 : 1].key} `; + let commandVideo = commandBaseVideo + `"${tempTsFile}.video.enc.m4s" "${tempTsFile}.video.m4s"`; + let commandAudio = commandBaseAudio + `"${tempTsFile}.audio.enc.m4s" "${tempTsFile}.audio.m4s"`; + + if (this.cfg.bin.shaka) { + commandBaseVideo = ` --enable_raw_key_decryption ${encryptionKeysVideo?.map((kb) => '--keys key_id=' + kb.kid + ':key=' + kb.key).join(' ')}`; + commandBaseAudio = ` --enable_raw_key_decryption ${encryptionKeysAudio?.map((kb) => '--keys key_id=' + kb.kid + ':key=' + kb.key).join(' ')}`; + commandVideo = `input="${tempTsFile}.video.enc.m4s",stream=video,output="${tempTsFile}.video.m4s"` + commandBaseVideo; + commandAudio = `input="${tempTsFile}.audio.enc.m4s",stream=audio,output="${tempTsFile}.audio.m4s"` + commandBaseAudio; + } + + if (videoDownloaded) { + console.info('Started decrypting video,', this.cfg.bin.shaka ? 'using shaka' : 'using mp4decrypt'); + const decryptVideo = Helper.exec( + this.cfg.bin.shaka ? 'shaka-packager' : 'mp4decrypt', + this.cfg.bin.shaka ? `"${this.cfg.bin.shaka}"` : `"${this.cfg.bin.mp4decrypt}"`, + commandVideo + ); + if (!decryptVideo.isOk) { + console.error(decryptVideo.err); + console.error(`Decryption failed with exit code ${decryptVideo.err.code}`); + if (this.cfg.bin.shaka) { + console.error(`Downgrade to Shaka-Packager v2.6.1 (https://github.com/shaka-project/shaka-packager/releases/tag/v2.6.1) and try again`); + } + fs.renameSync(`${tempTsFile}.video.enc.m4s`, `${tsFile}.video.enc.m4s`); + return undefined; + } else { + console.info('Decryption done for video'); + if (!options.nocleanup) { + fs.removeSync(`${tempTsFile}.video.enc.m4s`); + } + fs.copyFileSync(`${tempTsFile}.video.m4s`, `${tsFile}.video.m4s`); + fs.unlinkSync(`${tempTsFile}.video.m4s`); + files.push({ + type: 'Video', + path: `${tsFile}.video.m4s`, + lang: lang, + isPrimary: isPrimary + }); + } + } + + if (audioDownloaded) { + console.info('Started decrypting audio,', this.cfg.bin.shaka ? 'using shaka' : 'using mp4decrypt'); + const decryptAudio = Helper.exec( + this.cfg.bin.shaka ? 'shaka-packager' : 'mp4decrypt', + this.cfg.bin.shaka ? `"${this.cfg.bin.shaka}"` : `"${this.cfg.bin.mp4decrypt}"`, + commandAudio + ); + if (!decryptAudio.isOk) { + console.error(decryptAudio.err); + console.error(`Decryption failed with exit code ${decryptAudio.err.code}`); + if (this.cfg.bin.shaka) { + console.error(`Downgrade to Shaka-Packager v2.6.1 (https://github.com/shaka-project/shaka-packager/releases/tag/v2.6.1) and try again`); + } + fs.renameSync(`${tempTsFile}.audio.enc.m4s`, `${tsFile}.audio.enc.m4s`); + return undefined; + } else { + if (!options.nocleanup) { + fs.removeSync(`${tempTsFile}.audio.enc.m4s`); + } + fs.copyFileSync(`${tempTsFile}.audio.m4s`, `${tsFile}.audio.m4s`); + fs.unlinkSync(`${tempTsFile}.audio.m4s`); + files.push({ + type: 'Audio', + path: `${tsFile}.audio.m4s`, + lang: lang, + isPrimary: isPrimary + }); + console.info('Decryption done for audio'); + } + } + } else { + console.warn('mp4decrypt/shaka not found, files need decryption. Decryption Keys:', encryptionKeysVideo, encryptionKeysAudio); + } + } else if (dlFailed) { + console.error('Download failed, skipping decryption'); + } else { + if (videoDownloaded) { + files.push({ + type: 'Video', + path: `${tsFile}.video.m4s`, + lang: lang, + isPrimary: isPrimary + }); + } + if (audioDownloaded) { + files.push({ + type: 'Audio', + path: `${tsFile}.audio.m4s`, + lang: lang, + isPrimary: isPrimary + }); + } + } + } else if (!options.novids) { + const streamPlaylists = m3u8(vstreamPlaylistBody); + const plServerList: string[] = [], + plStreams: Record> = {}, + plQuality: { + str: string; + dim: string; + CODECS: string; + RESOLUTION: { + width: number; + height: number; + }; + }[] = []; + for (const pl of streamPlaylists.playlists) { + // set quality + const plResolution = pl.attributes.RESOLUTION; + const plResolutionText = `${plResolution.width}x${plResolution.height}`; + // set codecs + const plCodecs = pl.attributes.CODECS; + // parse uri + const plUri = new URL(pl.uri); + let plServer = plUri.hostname; + // set server list + if (plUri.searchParams.get('cdn')) { + plServer += ` (${plUri.searchParams.get('cdn')})`; + } + if (!plServerList.includes(plServer)) { + plServerList.push(plServer); + } + // add to server + if (!Object.keys(plStreams).includes(plServer)) { + plStreams[plServer] = {}; + } + if ( + plStreams[plServer][plResolutionText] && + plStreams[plServer][plResolutionText] != pl.uri && + typeof plStreams[plServer][plResolutionText] != 'undefined' + ) { + console.error(`Non duplicate url for ${plServer} detected, please report to developer!`); + } else { + plStreams[plServer][plResolutionText] = pl.uri; + } + // set plQualityStr + const plBandwidth = Math.round(pl.attributes.BANDWIDTH / 1024); + const qualityStrAdd = `${plResolutionText} (${plBandwidth}KiB/s)`; + const qualityStrRegx = new RegExp(qualityStrAdd.replace(/([:()/])/g, '\\$1'), 'm'); + const qualityStrMatch = !plQuality + .map((a) => a.str) + .join('\r\n') + .match(qualityStrRegx); + if (qualityStrMatch) { + plQuality.push({ + str: qualityStrAdd, + dim: plResolutionText, + CODECS: plCodecs, + RESOLUTION: plResolution + }); + } + } + + const plSelectedServer = plServerList[0]; + const plSelectedList = plStreams[plSelectedServer]; + plQuality.sort((a, b) => { + const aMatch: RegExpMatchArray | never[] = a.dim.match(/[0-9]+/) || []; + const bMatch: RegExpMatchArray | never[] = b.dim.match(/[0-9]+/) || []; + return parseInt(aMatch[0]) - parseInt(bMatch[0]); + }); + let quality = options.q === 0 ? plQuality.length : options.q; + if (quality > plQuality.length) { + console.warn( + `The requested quality of ${options.q} is greater than the maximum ${plQuality.length}.\n[WARN] Therefor the maximum will be capped at ${plQuality.length}.` + ); + quality = plQuality.length; + } + // When best selected video quality is already downloaded + if (dlVideoOnce && options.dlVideoOnce) { + // Select the lowest resolution with the same codecs + while (quality != 1 && plQuality[quality - 1].CODECS == plQuality[quality - 2].CODECS) { + quality--; + } + } + const selPlUrl = plSelectedList[plQuality.map((a) => a.dim)[quality - 1]] ? plSelectedList[plQuality.map((a) => a.dim)[quality - 1]] : ''; + console.info(`Servers available:\n\t${plServerList.join('\n\t')}`); + console.info(`Available qualities:\n\t${plQuality.map((a, ind) => `[${ind + 1}] ${a.str}`).join('\n\t')}`); + + if (selPlUrl != '') { + variables.push( + { + name: 'height', + type: 'number', + replaceWith: quality === 0 ? (plQuality[plQuality.length - 1].RESOLUTION.height as number) : plQuality[quality - 1].RESOLUTION.height + }, + { + name: 'width', + type: 'number', + replaceWith: quality === 0 ? (plQuality[plQuality.length - 1].RESOLUTION.width as number) : plQuality[quality - 1].RESOLUTION.width + } + ); + const lang = langsData.languages.find((a) => a.code === vcurStream?.audio_lang); + if (!lang) { + console.error(`Unable to find language for code ${vcurStream.audio_lang}`); + return; + } + console.info(`Selected quality: ${Object.keys(plSelectedList).find((a) => plSelectedList[a] === selPlUrl)} @ ${plSelectedServer}`); + console.info('Stream URL:', selPlUrl); + // TODO check filename + fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); + const outFile = parseFileName(options.fileName + '.' + (mMeta.lang?.name || lang.name), variables, options.numbers, options.override).join(path.sep); + console.info(`Output filename: ${outFile}`); + const chunkPage = await this.req.getData(selPlUrl, { + headers: api.crunchyDefHeader + }); + if (!chunkPage.ok || !chunkPage.res) { + console.error("CAN'T FETCH VIDEO PLAYLIST!"); + dlFailed = true; + } else { + // We have the stream, so go ahead and delete the active stream + if (videoStream) { + await this.refreshToken(true, true); + await this.req.getData( + `https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${videoStream.token}`, + { ...{ method: 'DELETE' }, ...AuthHeaders } + ); + } + if (audioStream && videoStream?.token !== audioStream.token) { + await this.req.getData( + `https://www.crunchyroll.com/playback/v1/token/${currentVersion ? currentVersion.guid : currentMediaId}/${audioStream.token}`, + { ...{ method: 'DELETE' }, ...AuthHeaders } + ); + } + + const chunkPageBody = await chunkPage.res.text(); + const chunkPlaylist = m3u8(chunkPageBody); + const totalParts = chunkPlaylist.segments.length; + const mathParts = Math.ceil(totalParts / options.partsize); + const mathMsg = `(${mathParts}*${options.partsize})`; + console.info('Total parts in stream:', totalParts, mathMsg); + tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); + const dirName = path.dirname(tsFile); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + const dlStreamByPl = await new streamdl({ + output: `${tsFile}.ts`, + timeout: options.timeout, + m3u8json: chunkPlaylist, + // baseurl: chunkPlaylist.baseUrl, + threads: options.partsize, + fsRetryTime: options.fsRetryTime * 1000, + override: options.force, + callback: options.callbackMaker + ? options.callbackMaker({ + fileName: `${path.isAbsolute(outFile) ? outFile.slice(this.cfg.dir.content.length) : outFile}`, + image: medias.image, + parent: { + title: medias.seasonTitle + }, + title: medias.episodeTitle, + language: lang + }) + : undefined + }).download(); + if (!dlStreamByPl.ok) { + console.error(`DL Stats: ${JSON.stringify(dlStreamByPl.parts)}\n`); + dlFailed = true; + } + files.push({ + type: 'Video', + path: `${tsFile}.ts`, + lang: lang, + isPrimary: isPrimary + }); + dlVideoOnce = true; + } + } else { + console.error('Quality not selected!\n'); + dlFailed = true; + } + } else if (options.novids) { + fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); + console.info('Downloading skipped!'); + } + } + } else if (options.novids && options.noaudio) { + fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); + } + + if (compiledChapters.length > 0) { + try { + fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); + const outFile = parseFileName(options.fileName + '.' + mMeta.lang?.name, variables, options.numbers, options.override).join(path.sep); + tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); + const dirName = path.dirname(tsFile); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + const lang = langsData.languages.find((a) => a.code === vcurStream?.audio_lang); + if (!lang) { + console.error(`Unable to find language for code ${vcurStream.audio_lang}`); + return; + } + fs.writeFileSync(`${tsFile}.txt`, compiledChapters.join('\r\n')); + files.push({ + path: `${tsFile}.txt`, + lang: lang, + type: 'Chapters' + }); + } catch { + console.error('Failed to write chapter file'); + } + } + + if (options.dlsubs.indexOf('all') > -1) { + options.dlsubs = ['all']; + } + + if (options.hslang != 'none') { + console.warn('Subtitles downloading disabled for hardsubs streams.'); + options.skipsubs = true; + } + + if (options.nosubs) { + console.info('Subtitles downloading disabled from nosubs flag.'); + options.skipsubs = true; + } + + if (!options.skipsubs && options.dlsubs.indexOf('none') == -1) { + if ( + (pbData.meta.subtitles && Object.values(pbData.meta.subtitles).length) || + (pbData.meta.closed_captions && Object.values(pbData.meta.closed_captions).length > 0) + ) { + const subsData = Object.values(pbData.meta.subtitles); + const capsData = Object.values(pbData.meta.closed_captions); + const subsDataMapped = subsData + .map((s) => { + const subLang = langsData.fixAndFindCrLC(s.language); + return { + ...s, + isCC: false, + locale: subLang, + language: subLang.locale + }; + }) + .concat( + capsData.map((s) => { + const subLang = langsData.fixAndFindCrLC(s.language); + return { + ...s, + isCC: true, + locale: subLang, + language: subLang.locale + }; + }) + ); + const subsArr = langsData.sortSubtitles<(typeof subsDataMapped)[0]>(subsDataMapped, 'language'); + for (const subsIndex in subsArr) { + const subsItem = subsArr[subsIndex]; + const langItem = subsItem.locale; + const sxData: Partial = {}; + sxData.language = langItem; + const isSigns = langItem.code === audDub && !subsItem.isCC; + const isCC = subsItem.isCC; + sxData.file = langsData.subsFile(fileName as string, subsIndex, langItem, isCC, options.ccTag, isSigns, subsItem.format); + if (path.isAbsolute(sxData.file)) { + sxData.path = sxData.file; + } else { + sxData.path = path.join(this.cfg.dir.content, sxData.file); + } + const dirName = path.dirname(sxData.path); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + if ( + files.some( + (a) => + a.type === 'Subtitle' && + (a.language.cr_locale == langItem.cr_locale || a.language.locale == langItem.locale) && + a.cc === isCC && + a.signs === isSigns + ) + ) + continue; + if (options.dlsubs.includes('all') || options.dlsubs.includes(langItem.locale)) { + const subsAssReq = await this.req.getData(subsItem.url, { + headers: api.crunchyDefHeader + }); + if (subsAssReq.ok && subsAssReq.res) { + let sBody = await subsAssReq.res.text(); + if (subsItem.format == 'vtt') { + const chosenFontSize = options.originalFontSize ? undefined : options.fontSize; + if (!options.originalFontSize) sBody = sBody.replace(/( font-size:.+?;)/g, '').replace(/(font-size:.+?;)/g, ''); + sBody = vtt2ass(undefined, chosenFontSize, sBody, '', undefined, options.fontName); + sxData.fonts = fontsData.assFonts(sBody) as Font[]; + sxData.file = sxData.file.replace('.vtt', '.ass'); + } else { + // Extract PlayRes + const mX = sBody.match(/^PlayResX:\s*(\d+)/m); + const mY = sBody.match(/^PlayResY:\s*(\d+)/m); + let playResX = Number(mX?.[1]); + let playResY = Number(mY?.[1]); + + // Fix for Crunchyroll CCC SRT ASS + if (sBody.includes('www.closedcaptionconverter.com') && options.srtAssFix) { + playResX = 640; + playResY = 360; + + // Fix invalid Dialogue and remove PlayDepth + sBody = sBody.replace(/,,,,25.00,,/g, ',,0,0,0,,').replace('PlayDepth: 0\n', ''); + + // Fix fonts (default crunchyroll german sub font) + sBody = sBody.replace(/^Style:\s*([^,]+),.*?,(\d+),0,0,0,0$/gm, (match, name, align) => { + return `Style: ${name},Arial,23,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,-1,0,0,0,100,100,0,0,1,2,0,${align},0,0,20,1`; + }); + + // Removing CCC credits + sBody = sBody.replace('; Script generated by Closed Caption Converter | www.closedcaptionconverter.com\n', ''); + + const lines = sBody.split('\n'); + + // Add PlayResX, PlayResY, Timer and WrapStyle + const idx = lines.findIndex((l) => l.trim() === '[Script Info]'); + if (idx !== -1) { + lines.splice(idx + 3, 0, `PlayResX: ${playResX}`, `PlayResY: ${playResY}`, 'Timer: 0.0000', 'WrapStyle: 0'); + } + + sBody = lines.join('\n'); + } + + if (!sBody.includes('www.closedcaptionconverter.com')) { + // LayoutRes Fix + if (options.layoutResFix) { + sBody = sBody.replace(/^(PlayResY:\s*\d+)/m, `$1\nLayoutResX: ${playResX}\nLayoutResY: ${playResY}`); + } + + // ScaleBorderAndShadow Fix + if (options.scaledBorderAndShadowFix) { + sBody = sBody.replace(/^(WrapStyle:.*)$/m, `$1\nScaledBorderAndShadow: ${options.scaledBorderAndShadow}`); + } + + // Fix VLC wrong parsing if URL not avaiable + if (options.originalScriptFix) { + sBody = sBody.replace(/^Original Script:.*$/gm, 'Original Script: Crunchyroll'); + } + } + + sxData.title = sBody.split('\r\n')[1].replace(/^Title: /, ''); + sxData.title = `${langItem.language} / ${sxData.title}`; + sxData.fonts = fontsData.assFonts(sBody) as Font[]; + } + fs.writeFileSync(sxData.path, sBody); + console.info(`Subtitle downloaded: ${sxData.file}`); + files.push({ + type: 'Subtitle', + ...(sxData as sxItem), + cc: isCC, + signs: isSigns + }); + } else { + console.warn(`Failed to download subtitle: ${sxData.file}`); + } + } + } + } else { + console.warn("Can't find urls for subtitles!"); + } + } else { + console.info('Subtitles downloading skipped!'); + } + + await this.sleep(options.waittime); + } + return { + error: dlFailed, + data: files, + fileName: fileName ? (path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName)) || './unknown' : './unknown' + }; + } + + public async muxStreams(data: DownloadedMedia[], options: CrunchyMuxOptions) { + this.cfg.bin = await yamlCfg.loadBinCfg(); + let hasAudioStreams = false; + if (options.novids || data.filter((a) => a.type === 'Video').length === 0) return console.info('Skip muxing since no vids are downloaded'); + if (data.some((a) => a.type === 'Audio')) { + hasAudioStreams = true; + } + const merger = new Merger({ + onlyVid: hasAudioStreams + ? data + .filter((a) => a.type === 'Video') + .map((a): MergerInput => { + return { + lang: a.lang, + path: a.path + }; + }) + : [], + skipSubMux: options.skipSubMux, + onlyAudio: hasAudioStreams + ? data + .filter((a) => a.type === 'Audio') + .map((a): MergerInput => { + return { + lang: a.lang, + path: a.path + }; + }) + : [], + output: `${options.output}.${options.mp4 ? 'mp4' : 'mkv'}`, + subtitles: data + .filter((a) => a.type === 'Subtitle') + .map((a): SubtitleInput => { + return { + file: a.path, + language: a.language, + closedCaption: a.cc, + signs: a.signs + }; + }), + simul: false, + keepAllVideos: options.keepAllVideos, + fonts: Merger.makeFontsList(this.cfg.dir.fonts, data.filter((a) => a.type === 'Subtitle') as sxItem[]), + videoAndAudio: hasAudioStreams + ? [] + : data + .filter((a) => a.type === 'Video') + .map((a): MergerInput => { + return { + lang: a.lang, + path: a.path + }; + }), + chapters: data + .filter((a) => a.type === 'Chapters') + .map((a): MergerInput => { + return { + path: a.path, + lang: a.lang + }; + }), + videoTitle: options.videoTitle, + options: { + ffmpeg: options.ffmpegOptions, + mkvmerge: options.mkvmergeOptions + }, + defaults: { + audio: options.defaultAudio, + sub: options.defaultSub + }, + ccTag: options.ccTag + }); + const bin = Merger.checkMerger(this.cfg.bin, options.mp4, options.forceMuxer); + // collect fonts info + // mergers + let isMuxed = false; + if (options.syncTiming) { + await merger.createDelays(); + } + if (bin.MKVmerge) { + await merger.merge('mkvmerge', bin.MKVmerge); + isMuxed = true; + } else if (bin.FFmpeg) { + await merger.merge('ffmpeg', bin.FFmpeg); + isMuxed = true; + } else { + console.info('\nDone!\n'); + return; + } + if (isMuxed && !options.nocleanup) merger.cleanUp(); + } + + public async listSeriesID( + id: string, + data?: CrunchyMultiDownload + ): Promise<{ + list: Episode[]; + data: Record< + string, + { + items: CrunchyEpisode[]; + langs: langsData.LanguageItem[]; + } + >; + }> { + await this.refreshToken(true, true); + let serieshasversions = true; + const parsed = await this.parseSeriesById(id); + if (!parsed) return { data: {}, list: [] }; + const result = this.parseSeriesResult(parsed); + const episodes: Record< + string, + { + items: CrunchyEpisode[]; + langs: langsData.LanguageItem[]; + } + > = {}; + for (const season of Object.keys(result) as unknown as number[]) { + for (const key of Object.keys(result[season])) { + const s = result[season][key]; + if (data?.s && s.id !== data.s) continue; + (await this.getSeasonDataById(s))?.data?.forEach((episode) => { + //TODO: Make sure the below code is ok + //Prepare the episode array + let item; + const seasonIdentifier = s.identifier ? s.identifier.split('|')[1] : `S${episode.season_number}`; + if (!Object.prototype.hasOwnProperty.call(episodes, `${seasonIdentifier}E${episode.episode || episode.episode_number}`)) { + item = episodes[`${seasonIdentifier}E${episode.episode || episode.episode_number}`] = { + items: [] as CrunchyEpisode[], + langs: [] as langsData.LanguageItem[] + }; + } else { + item = episodes[`${seasonIdentifier}E${episode.episode || episode.episode_number}`]; + } + + if (episode.versions) { + //Iterate over episode versions for audio languages + for (const version of episode.versions) { + //Make sure there is only one of the same language + if (!item.langs.find((a) => a?.cr_locale == version.audio_locale)) { + //Push to arrays if there is no duplicates of the same language. + item.items.push(episode); + item.langs.push(langsData.languages.find((a) => a.cr_locale == version.audio_locale) as langsData.LanguageItem); + } + } + //Sort audio tracks according to the order of languages passed to the 'dubLang' option + const argv = yargs.appArgv(this.cfg.cli); + if (!argv.allDubs) { + item.langs.sort((a, b) => argv.dubLang.indexOf(a.code) - argv.dubLang.indexOf(b.code)); + } + } else { + //Episode didn't have versions, mark it as such to be logged. + serieshasversions = false; + //Make sure there is only one of the same language + if (!item.langs.find((a) => a?.cr_locale == episode.audio_locale)) { + //Push to arrays if there is no duplicates of the same language. + item.items.push(episode); + item.langs.push(langsData.languages.find((a) => a.cr_locale == episode.audio_locale) as langsData.LanguageItem); + } + } + }); + } + } + + const itemIndexes = { + sp: 1, + no: 1 + }; + + for (const key of Object.keys(episodes)) { + const item = episodes[key]; + const isSpecial = !item.items[0].episode.match(/^\d+$/); + episodes[`${isSpecial ? 'S' : 'E'}${itemIndexes[isSpecial ? 'sp' : 'no']}`] = item; + if (isSpecial) itemIndexes.sp++; + else itemIndexes.no++; + delete episodes[key]; + } + + // Sort episodes to have specials at the end + const specials = Object.entries(episodes).filter((a) => a[0].startsWith('S')), + normal = Object.entries(episodes).filter((a) => a[0].startsWith('E')), + sortedEpisodes = Object.fromEntries([...normal, ...specials]); + + for (const key of Object.keys(sortedEpisodes)) { + const item = sortedEpisodes[key]; + const epNum = key.startsWith('E') ? `E${data?.absolute ? item.items[0].episode_number?.toString() || item.items[0].episode : key.slice(1)}` : key; + console.info( + `[${data?.absolute ? epNum : key}] [${item.items[0].upload_date ? new Date(item.items[0].upload_date).toISOString().slice(0, 10) : '0000-00-00'}] ${ + item.items.find((a) => !a.season_title.match(/\(\w+ Dub\)/))?.season_title ?? item.items[0].season_title.replace(/\(\w+ Dub\)/g, '').trimEnd() + } - Season ${item.items[0].season_number} - ${item.items[0].title} [${item.items + .map((a, index) => { + return `${a.is_premium_only ? '☆ ' : ''}${item.langs[index]?.name ?? 'Unknown'}`; + }) + .join(', ')}]` + ); + } + + if (!serieshasversions) { + console.warn("Couldn't find versions on some episodes, fell back to old method."); + } + + return { + data: sortedEpisodes, + list: Object.entries(sortedEpisodes).map(([key, value]) => { + const images = (value.items[0].images.thumbnail ?? [[{ source: '/notFound.png' }]])[0]; + const seconds = Math.floor(value.items[0].duration_ms / 1000); + let epNum; + if (data?.absolute) { + epNum = + value.items[0].episode_number !== null && value.items[0].episode_number !== undefined + ? value.items[0].episode_number.toString() + : value.items[0].episode !== null && value.items[0].episode !== undefined + ? value.items[0].episode + : key.startsWith('E') + ? key.slice(1) + : key; + } else { + epNum = key.startsWith('E') ? key.slice(1) : key; + } + return { + e: epNum, + lang: value.langs.map((a) => a?.code), + name: value.items[0].title, + season: value.items[0].season_number.toString(), + seriesTitle: value.items[0].series_title.replace(/\(\w+ Dub\)/g, '').trimEnd(), + seasonTitle: value.items[0].season_title.replace(/\(\w+ Dub\)/g, '').trimEnd(), + episode: value.items[0].episode_number?.toString() ?? value.items[0].episode ?? '?', + id: value.items[0].season_id, + img: images[Math.floor(images.length / 2)].source, + description: value.items[0].description, + time: `${Math.floor(seconds / 60)}:${seconds % 60}` + }; + }) + }; + } + + public async downloadFromSeriesID(id: string, data: CrunchyMultiDownload): Promise> { + const { data: episodes } = await this.listSeriesID(id, data); + console.info(''); + console.info('-'.repeat(30)); + console.info(''); + const selected = this.itemSelectMultiDub(episodes, data.dubLang, data.but, data.all, data.e, data.absolute); + for (const key of Object.keys(selected)) { + const item = selected[key]; + console.info( + `[S${item.season}E${item.episodeNumber}] - ${item.episodeTitle} [${item.data + .map((a) => { + return `✓ ${a.lang?.name || 'Unknown Language'}`; + }) + .join(', ')}]` + ); + } + return { isOk: true, value: Object.values(selected) }; + } + + public itemSelectMultiDub( + eps: Record< + string, + { + items: CrunchyEpisode[]; + langs: langsData.LanguageItem[]; + } + >, + dubLang: string[], + but?: boolean, + all?: boolean, + e?: string, + absolute?: boolean + ) { + const doEpsFilter = parseSelect(e as string); + + const ret: Record = {}; + + for (const key of Object.keys(eps)) { + const itemE = eps[key]; + itemE.items.forEach((item, index) => { + if (!dubLang.includes(itemE.langs[index]?.code)) return; + item.hide_season_title = true; + if (item.season_title == '' && item.series_title != '') { + item.season_title = item.series_title; + item.hide_season_title = false; + item.hide_season_number = true; + } + if (item.season_title == '' && item.series_title == '') { + item.season_title = 'NO_TITLE'; + item.series_title = 'NO_TITLE'; + } + + let epNum; + if (absolute) { + epNum = + item.episode_number !== null && item.episode_number !== undefined + ? item.episode_number.toString() + : item.episode !== null && item.episode !== undefined + ? item.episode + : key.startsWith('E') + ? key.slice(1) + : key; + } else { + epNum = key.startsWith('E') ? key.slice(1) : key; + } + + // set data + const images = (item.images.thumbnail ?? [[{ source: '/notFound.png' }]])[0]; + const epMeta: CrunchyEpMeta = { + data: [ + { + mediaId: item.id, + versions: item.versions, + isSubbed: item.is_subbed, + isDubbed: item.is_dubbed + } + ], + seriesTitle: itemE.items.find((a) => !a.series_title.match(/\(\w+ Dub\)/))?.series_title ?? itemE.items[0].series_title.replace(/\(\w+ Dub\)/g, '').trimEnd(), + seasonTitle: itemE.items.find((a) => !a.season_title.match(/\(\w+ Dub\)/))?.season_title ?? itemE.items[0].season_title.replace(/\(\w+ Dub\)/g, '').trimEnd(), + episodeNumber: item.episode, + episodeTitle: item.title, + seasonID: item.season_id, + season: item.season_number, + showID: item.series_id, + e: epNum, + image: images[Math.floor(images.length / 2)].source + }; + if (item.__links__?.streams?.href) { + epMeta.data[0].playback = item.__links__.streams.href; + if (!item.playback) { + item.playback = item.__links__.streams.href; + } + } + if (item.streams_link) { + epMeta.data[0].playback = item.streams_link; + if (!item.playback) { + item.playback = item.streams_link; + } + } + + if (item.playback && ((but && !doEpsFilter.isSelected([epNum, item.id])) || all || (doEpsFilter.isSelected([epNum, item.id]) && !but))) { + if (Object.prototype.hasOwnProperty.call(ret, key)) { + const epMe = ret[key]; + epMe.data.push({ + lang: itemE.langs[index], + ...epMeta.data[0] + }); + } else { + epMeta.data[0].lang = itemE.langs[index]; + ret[key] = { + ...epMeta + }; + } + } + // show ep + item.seq_id = epNum; + }); + } + return ret; + } + + public parseSeriesResult(seasonsList: SeriesSearch): Record> { + const ret: Record> = {}; + let i = 0; + for (const item of seasonsList.data) { + i++; + for (const lang of langsData.languages) { + //TODO: Make sure the below code is fine + let season_number = item.season_number; + if (item.versions) { + season_number = i; + } + if (!Object.prototype.hasOwnProperty.call(ret, season_number)) ret[season_number] = {}; + if (item.title.includes(`(${lang.name} Dub)`) || item.title.includes(`(${lang.name})`)) { + ret[season_number][lang.code] = item; + } else if (item.is_subbed && !item.is_dubbed && lang.code == 'jpn') { + ret[season_number][lang.code] = item; + } else if ( + item.is_dubbed && + lang.code === 'eng' && + !langsData.languages.some((a) => item.title.includes(`(${a.name})`) || item.title.includes(`(${a.name} Dub)`)) + ) { + // Dubbed with no more infos will be treated as eng dubs + ret[season_number][lang.code] = item; + //TODO: look into if below is stable + } else if (item.audio_locale == lang.cr_locale) { + ret[season_number][lang.code] = item; + } + } + } + return ret; + } + + public async parseSeriesById(id: string) { + if (!this.cmsToken.cms_web) { + console.error('Authentication required!'); + return; + } + + const AuthHeaders = { + headers: { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader + }, + useProxy: true + }; + + // seasons list + const seriesSeasonListReq = await this.req.getData( + `${api.content_cms}/series/${id}/seasons?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, + AuthHeaders + ); + if (!seriesSeasonListReq.ok || !seriesSeasonListReq.res) { + console.error('Series Request FAILED!'); + return; + } + // parse data + const seasonsList = (await seriesSeasonListReq.res.json()) as SeriesSearch; + if (seasonsList.total < 1) { + console.info('Series is empty!'); + return; + } + return seasonsList; + } + + public async getSeasonDataById(item: SeriesSearchItem, log = false) { + if (!this.cmsToken.cms_web) { + console.error('Authentication required!'); + return; + } + + const AuthHeaders = { + headers: { + Authorization: `Bearer ${this.token.access_token}`, + ...api.crunchyDefHeader + }, + useProxy: true + }; + + //get show info + const showInfoReq = await this.req.getData(`${api.content_cms}/seasons/${item.id}?force_locale=&preferred_audio_language=ja-JP&locale=${this.locale}`, AuthHeaders); + if (!showInfoReq.ok || !showInfoReq.res) { + console.error('Show Request FAILED!'); + return; + } + const showInfo = await showInfoReq.res.json(); + if (log) await this.logObject(showInfo, 0); + + let episodeList = { total: 0, data: [], meta: {} } as CrunchyEpisodeList; + //get episode info + for (const s of showInfo.data) { + const original_id = s.versions?.find((v: { original: boolean }) => v.original)?.guid; + const id = original_id ? original_id : s.id; + + const reqEpsListOpts = [ + api.cms_bucket, + this.cmsToken.cms_web.bucket, + '/episodes?', + new URLSearchParams({ + force_locale: '', + preferred_audio_language: 'ja-JP', + locale: this.locale, + season_id: id, + Policy: this.cmsToken.cms_web.policy, + Signature: this.cmsToken.cms_web.signature, + 'Key-Pair-Id': this.cmsToken.cms_web.key_pair_id + }) + ].join(''); + const reqEpsList = await this.req.getData(reqEpsListOpts, AuthHeaders); + if (!reqEpsList.ok || !reqEpsList.res) { + console.error('Episode List Request FAILED!'); + return; + } + + const episodeListAndroid = (await reqEpsList.res.json()) as CrunchyAndroidEpisodes; + episodeList = { + total: episodeList.total + episodeListAndroid.total, + data: [...episodeList.data, ...episodeListAndroid.items], + meta: {} + }; + } + + if (episodeList.total < 1) { + console.info(' Season is empty!'); + return; + } + return episodeList; + } } - - diff --git a/dev.js b/dev.js index 5342348..1234ccf 100644 --- a/dev.js +++ b/dev.js @@ -3,19 +3,21 @@ const path = require('path'); const toRun = process.argv.slice(2).join(' ').split('---'); const waitForProcess = async (proc) => { - return new Promise((resolve, reject) => { - proc.stdout?.on('data', (data) => process.stdout.write(data)); - proc.stderr?.on('data', (data) => process.stderr.write(data)); - proc.on('close', resolve); - proc.on('error', reject); - }); + return new Promise((resolve, reject) => { + proc.stdout?.on('data', (data) => process.stdout.write(data)); + proc.stderr?.on('data', (data) => process.stderr.write(data)); + proc.on('close', resolve); + proc.on('error', reject); + }); }; (async () => { - await waitForProcess(exec('pnpm run tsc test false')); - for (let command of toRun) { - await waitForProcess(exec(`node index.js --service hidive ${command}`, { - cwd: path.join(__dirname, 'lib') - })); - } -})(); \ No newline at end of file + await waitForProcess(exec('pnpm run tsc test false')); + for (let command of toRun) { + await waitForProcess( + exec(`node index.js --service hidive ${command}`, { + cwd: path.join(__dirname, 'lib') + }) + ); + } +})(); diff --git a/eslint.config.mjs b/eslint.config.mjs index b4a7b34..c9aeeae 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -2,61 +2,46 @@ import eslint from '@eslint/js'; import tseslint from 'typescript-eslint'; +import prettier from 'eslint-config-prettier'; export default tseslint.config( - eslint.configs.recommended, - ...tseslint.configs.recommended, - { - rules: { - 'no-console': 2, - 'react/prop-types': 0, - 'react-hooks/exhaustive-deps': 0, - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-unsafe-declaration-merging': 'warn', - '@typescript-eslint/no-unused-vars' : 'warn', - '@typescript-eslint/no-unused-expressions': 'warn', - 'indent': [ - 'error', - 4 - ], - 'linebreak-style': [ - 'warn', - 'windows' - ], - 'quotes': [ - 'error', - 'single' - ], - 'semi': [ - 'error', - 'always' - ] - }, - languageOptions: { - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - ecmaVersion: 2020, - sourceType: 'module' - }, - parser: tseslint.parser - } - }, - { - ignores: [ - '**/lib', - '**/videos/*.ts', - '**/build', - 'dev.js', - 'tsc.ts' - ] - }, - { - files: ['gui/react/**/*'], - rules: { - 'no-console': 0, - 'indent': 'off' - } - } -); \ No newline at end of file + eslint.configs.recommended, + ...tseslint.configs.recommended, + { + rules: { + 'no-console': 2, + 'react/prop-types': 0, + 'react-hooks/exhaustive-deps': 0, + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unsafe-declaration-merging': 'warn', + '@typescript-eslint/no-unused-vars': 'warn', + '@typescript-eslint/no-unused-expressions': 'warn', + indent: ['error', 4], + 'linebreak-style': ['warn', 'windows'], + quotes: ['error', 'single', { avoidEscape: true }], + semi: ['error', 'always'] + }, + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true + }, + ecmaVersion: 2020, + sourceType: 'module' + }, + parser: tseslint.parser + } + }, + { + ignores: ['**/lib', '**/videos', '**/build', 'dev.js', 'tsc.ts'] + }, + { + files: ['gui/react/**/*'], + rules: { + 'no-console': 0, + indent: 'off' + } + }, + // Disables all rules that conflict with prettier + prettier +); diff --git a/gui.ts b/gui.ts index 5bb963e..e621c9b 100644 --- a/gui.ts +++ b/gui.ts @@ -1,3 +1,3 @@ process.env.isGUI = 'true'; import './modules/log'; -import './gui/server/index'; \ No newline at end of file +import './gui/server/index'; diff --git a/gui/react/.babelrc b/gui/react/.babelrc index 0761517..18f3008 100644 --- a/gui/react/.babelrc +++ b/gui/react/.babelrc @@ -1,3 +1,3 @@ { - "presets": ["@babel/preset-env","@babel/preset-react", "@babel/preset-typescript"] -} \ No newline at end of file + "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"] +} diff --git a/gui/react/package.json b/gui/react/package.json index a2b704c..52e0ddd 100644 --- a/gui/react/package.json +++ b/gui/react/package.json @@ -54,4 +54,4 @@ "last 1 safari version" ] } -} \ No newline at end of file +} diff --git a/gui/react/public/index.html b/gui/react/public/index.html index 08d1831..59c13ba 100644 --- a/gui/react/public/index.html +++ b/gui/react/public/index.html @@ -1,13 +1,10 @@ - + Multi Downloader - - - + + +
diff --git a/gui/react/src/@types/FC.d.ts b/gui/react/src/@types/FC.d.ts index 11963df..91291f5 100644 --- a/gui/react/src/@types/FC.d.ts +++ b/gui/react/src/@types/FC.d.ts @@ -1,3 +1,5 @@ -type FCWithChildren = React.FC<{ - children?: React.ReactNode[]|React.ReactNode -} & T> \ No newline at end of file +type FCWithChildren = React.FC< + { + children?: React.ReactNode[] | React.ReactNode; + } & T +>; diff --git a/gui/react/src/App.tsx b/gui/react/src/App.tsx index 42b08bf..5c7b67e 100644 --- a/gui/react/src/App.tsx +++ b/gui/react/src/App.tsx @@ -2,9 +2,7 @@ import React from 'react'; import Layout from './Layout'; const App: React.FC = () => { - return ( - - ); + return ; }; export default App; diff --git a/gui/react/src/Layout.tsx b/gui/react/src/Layout.tsx index d9b50f1..89da39f 100644 --- a/gui/react/src/Layout.tsx +++ b/gui/react/src/Layout.tsx @@ -10,29 +10,36 @@ import StartQueueButton from './components/StartQueue'; import MenuBar from './components/MenuBar/MenuBar'; const Layout: React.FC = () => { + const messageHandler = React.useContext(messageChannelContext); - const messageHandler = React.useContext(messageChannelContext); - - return - - - - - - - - - - - ; + return ( + + + + + + + + + + + + + ); }; -export default Layout; \ No newline at end of file +export default Layout; diff --git a/gui/react/src/Style.tsx b/gui/react/src/Style.tsx index 643ba40..a6b23b4 100644 --- a/gui/react/src/Style.tsx +++ b/gui/react/src/Style.tsx @@ -1,19 +1,21 @@ import React from 'react'; import { Container, Box, ThemeProvider, createTheme, Theme } from '@mui/material'; -const makeTheme = (mode: 'dark'|'light') : Partial => { - return createTheme({ - palette: { - mode, - }, - }); +const makeTheme = (mode: 'dark' | 'light'): Partial => { + return createTheme({ + palette: { + mode + } + }); }; -const Style: FCWithChildren = ({children}) => { - return - - {children} - ; +const Style: FCWithChildren = ({ children }) => { + return ( + + + {children} + + ); }; -export default Style; \ No newline at end of file +export default Style; diff --git a/gui/react/src/components/AddToQueue/AddToQueue.tsx b/gui/react/src/components/AddToQueue/AddToQueue.tsx index 251f4d5..dfa1824 100644 --- a/gui/react/src/components/AddToQueue/AddToQueue.tsx +++ b/gui/react/src/components/AddToQueue/AddToQueue.tsx @@ -6,22 +6,24 @@ import EpisodeListing from './DownloadSelector/Listing/EpisodeListing'; import SearchBox from './SearchBox/SearchBox'; const AddToQueue: React.FC = () => { - const [isOpen, setOpen] = React.useState(false); + const [isOpen, setOpen] = React.useState(false); - return - - setOpen(false)} maxWidth='md' PaperProps={{ elevation:4 }}> - - - - setOpen(false)} /> - - - - ; + return ( + + + setOpen(false)} maxWidth="md" PaperProps={{ elevation: 4 }}> + + + + setOpen(false)} /> + + + + + ); }; -export default AddToQueue; \ No newline at end of file +export default AddToQueue; diff --git a/gui/react/src/components/AddToQueue/DownloadSelector/DownloadSelector.tsx b/gui/react/src/components/AddToQueue/DownloadSelector/DownloadSelector.tsx index bac174f..7274ca3 100644 --- a/gui/react/src/components/AddToQueue/DownloadSelector/DownloadSelector.tsx +++ b/gui/react/src/components/AddToQueue/DownloadSelector/DownloadSelector.tsx @@ -8,320 +8,398 @@ import { useSnackbar } from 'notistack'; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; type DownloadSelectorProps = { - onFinish?: () => unknown -} - -const DownloadSelector: React.FC = ({ onFinish }) => { - const messageHandler = React.useContext(messageChannelContext); - const [store, dispatch] = useStore(); - const [availableDubs, setAvailableDubs] = React.useState([]); - const [availableSubs, setAvailableSubs ] = React.useState([]); - const [ loading, setLoading ] = React.useState(false); - const { enqueueSnackbar } = useSnackbar(); - const ITEM_HEIGHT = 48; - const ITEM_PADDING_TOP = 8; - - React.useEffect(() => { - (async () => { - /* If we don't wait the response is undefined? */ - await new Promise((resolve) => setTimeout(() => resolve(undefined), 100)); - const dubLang = messageHandler?.handleDefault('dubLang'); - const subLang = messageHandler?.handleDefault('dlsubs'); - const q = messageHandler?.handleDefault('q'); - const fileName = messageHandler?.handleDefault('fileName'); - const dlVideoOnce = messageHandler?.handleDefault('dlVideoOnce'); - const result = await Promise.all([dubLang, subLang, q, fileName, dlVideoOnce]); - dispatch({ - type: 'downloadOptions', - payload: { - ...store.downloadOptions, - dubLang: result[0], - dlsubs: result[1], - q: result[2], - fileName: result[3], - dlVideoOnce: result[4], - } - }); - setAvailableDubs(await messageHandler?.availableDubCodes() ?? []); - setAvailableSubs(await messageHandler?.availableSubCodes() ?? []); - })(); - }, []); - - const addToQueue = async () => { - setLoading(true); - const res = await messageHandler?.resolveItems(store.downloadOptions); - if (!res) - return enqueueSnackbar('The request failed. Please check if the ID is correct.', { - variant: 'error' - }); - setLoading(false); - if (onFinish) - onFinish(); - }; - - const listEpisodes = async () => { - if (!store.downloadOptions.id) { - return enqueueSnackbar('Please enter a ID', { - variant: 'error' - }); - } - setLoading(true); - const res = await messageHandler?.listEpisodes(store.downloadOptions.id); - if (!res || !res.isOk) { - setLoading(false); - return enqueueSnackbar('The request failed. Please check if the ID is correct.', { - variant: 'error' - }); - } else { - dispatch({ - type: 'episodeListing', - payload: res.value - }); - } - setLoading(false); - }; - - return - - - - - General Options - - { - dispatch({ - type: 'downloadOptions', - payload: { ...store.downloadOptions, id: e.target.value } - }); - }} label='Show ID'/> - { - const parsed = parseInt(e.target.value); - if (isNaN(parsed) || parsed < 0 || parsed > 10) - return; - dispatch({ - type: 'downloadOptions', - payload: { ...store.downloadOptions, q: parsed } - }); - }} label='Quality Level (0 for max)'/> - - - - - - - Simulcast is only supported on Hidive - } - arrow placement='top' - > - - - - - - - - Episode Options - - - - { - dispatch({ - type: 'downloadOptions', - payload: { ...store.downloadOptions, e: e.target.value } - }); - }} placeholder='Episode Select'/> - - List
Episodes
-
-
- - -
- - - Language Options - - { - dispatch({ - type: 'downloadOptions', - payload: { ...store.downloadOptions, dubLang: e } - }); - }} - allOption - /> - - { - dispatch({ - type: 'downloadOptions', - payload: { ...store.downloadOptions, dlsubs: e } - }); - }} - /> - - Hardsubs are only supported on Crunchyroll - - } - arrow placement='top'> - - - - - Hardsub Language - - - - - - - Downloads the hardsub version of the selected subtitle.
Subtitles are displayed PERMANENTLY!
You can choose only 1 subtitle per video! - - } arrow placement='top'> - -
-
-
-
-
- - - { - dispatch({ - type: 'downloadOptions', - payload: { ...store.downloadOptions, fileName: e.target.value } - }); - }} sx={{ width: '87%' }} label='Filename Overwrite' /> - - Click here to see the documentation - - } arrow placement='top'> - - - - - - - - - Add to Queue - - ; + onFinish?: () => unknown; }; -export default DownloadSelector; \ No newline at end of file +const DownloadSelector: React.FC = ({ onFinish }) => { + const messageHandler = React.useContext(messageChannelContext); + const [store, dispatch] = useStore(); + const [availableDubs, setAvailableDubs] = React.useState([]); + const [availableSubs, setAvailableSubs] = React.useState([]); + const [loading, setLoading] = React.useState(false); + const { enqueueSnackbar } = useSnackbar(); + const ITEM_HEIGHT = 48; + const ITEM_PADDING_TOP = 8; + + React.useEffect(() => { + (async () => { + /* If we don't wait the response is undefined? */ + await new Promise((resolve) => setTimeout(() => resolve(undefined), 100)); + const dubLang = messageHandler?.handleDefault('dubLang'); + const subLang = messageHandler?.handleDefault('dlsubs'); + const q = messageHandler?.handleDefault('q'); + const fileName = messageHandler?.handleDefault('fileName'); + const dlVideoOnce = messageHandler?.handleDefault('dlVideoOnce'); + const result = await Promise.all([dubLang, subLang, q, fileName, dlVideoOnce]); + dispatch({ + type: 'downloadOptions', + payload: { + ...store.downloadOptions, + dubLang: result[0], + dlsubs: result[1], + q: result[2], + fileName: result[3], + dlVideoOnce: result[4] + } + }); + setAvailableDubs((await messageHandler?.availableDubCodes()) ?? []); + setAvailableSubs((await messageHandler?.availableSubCodes()) ?? []); + })(); + }, []); + + const addToQueue = async () => { + setLoading(true); + const res = await messageHandler?.resolveItems(store.downloadOptions); + if (!res) + return enqueueSnackbar('The request failed. Please check if the ID is correct.', { + variant: 'error' + }); + setLoading(false); + if (onFinish) onFinish(); + }; + + const listEpisodes = async () => { + if (!store.downloadOptions.id) { + return enqueueSnackbar('Please enter a ID', { + variant: 'error' + }); + } + setLoading(true); + const res = await messageHandler?.listEpisodes(store.downloadOptions.id); + if (!res || !res.isOk) { + setLoading(false); + return enqueueSnackbar('The request failed. Please check if the ID is correct.', { + variant: 'error' + }); + } else { + dispatch({ + type: 'episodeListing', + payload: res.value + }); + } + setLoading(false); + }; + + return ( + + + + + General Options + { + dispatch({ + type: 'downloadOptions', + payload: { ...store.downloadOptions, id: e.target.value } + }); + }} + label="Show ID" + /> + { + const parsed = parseInt(e.target.value); + if (isNaN(parsed) || parsed < 0 || parsed > 10) return; + dispatch({ + type: 'downloadOptions', + payload: { ...store.downloadOptions, q: parsed } + }); + }} + label="Quality Level (0 for max)" + /> + + + + + + Simulcast is only supported on Hidive} arrow placement="top"> + + + + + + + Episode Options + + + { + dispatch({ + type: 'downloadOptions', + payload: { ...store.downloadOptions, e: e.target.value } + }); + }} + placeholder="Episode Select" + /> + + + + List +
+ Episodes +
+
+
+
+ + +
+ + Language Options + { + dispatch({ + type: 'downloadOptions', + payload: { ...store.downloadOptions, dubLang: e } + }); + }} + allOption + /> + + { + dispatch({ + type: 'downloadOptions', + payload: { ...store.downloadOptions, dlsubs: e } + }); + }} + /> + Hardsubs are only supported on Crunchyroll} arrow placement="top"> + + + + Hardsub Language + + + + + + Downloads the hardsub version of the selected subtitle. +
+ Subtitles are displayed PERMANENTLY! +
+ You can choose only 1 subtitle per video! + + } + arrow + placement="top" + > + +
+
+
+
+
+ + + { + dispatch({ + type: 'downloadOptions', + payload: { ...store.downloadOptions, fileName: e.target.value } + }); + }} + sx={{ width: '87%' }} + label="Filename Overwrite" + /> + Click here to see the documentation} arrow placement="top"> + + + + + + + + + + Add to Queue + + + ); +}; + +export default DownloadSelector; diff --git a/gui/react/src/components/AddToQueue/DownloadSelector/Listing/EpisodeListing.tsx b/gui/react/src/components/AddToQueue/DownloadSelector/Listing/EpisodeListing.tsx index a59cf77..e3b60f1 100644 --- a/gui/react/src/components/AddToQueue/DownloadSelector/Listing/EpisodeListing.tsx +++ b/gui/react/src/components/AddToQueue/DownloadSelector/Listing/EpisodeListing.tsx @@ -5,187 +5,205 @@ import useStore from '../../../../hooks/useStore'; import ContextMenu from '../../../reusable/ContextMenu'; import { useSnackbar } from 'notistack'; - const EpisodeListing: React.FC = () => { - const [store, dispatch] = useStore(); + const [store, dispatch] = useStore(); - const [season, setSeason] = React.useState<'all'|string>('all'); - const { enqueueSnackbar } = useSnackbar(); + const [season, setSeason] = React.useState<'all' | string>('all'); + const { enqueueSnackbar } = useSnackbar(); - const seasons = React.useMemo(() => { - const s: string[] = []; - for (const {season} of store.episodeListing) { - if (s.includes(season)) - continue; - s.push(season); - } - return s; - }, [ store.episodeListing ]); + const seasons = React.useMemo(() => { + const s: string[] = []; + for (const { season } of store.episodeListing) { + if (s.includes(season)) continue; + s.push(season); + } + return s; + }, [store.episodeListing]); - const [selected, setSelected] = React.useState([]); + const [selected, setSelected] = React.useState([]); - React.useEffect(() => { - setSelected(parseSelect(store.downloadOptions.e)); - }, [ store.episodeListing ]); + React.useEffect(() => { + setSelected(parseSelect(store.downloadOptions.e)); + }, [store.episodeListing]); - const close = () => { - dispatch({ - type: 'episodeListing', - payload: [] - }); - dispatch({ - type: 'downloadOptions', - payload: { - ...store.downloadOptions, - e: `${([...new Set([...parseSelect(store.downloadOptions.e), ...selected])]).join(',')}` - } - }); - }; + const close = () => { + dispatch({ + type: 'episodeListing', + payload: [] + }); + dispatch({ + type: 'downloadOptions', + payload: { + ...store.downloadOptions, + e: `${[...new Set([...parseSelect(store.downloadOptions.e), ...selected])].join(',')}` + } + }); + }; - const getEpisodesForSeason = (season: string|'all') => { - return store.episodeListing.filter((a) => season === 'all' ? true : a.season === season); - }; + const getEpisodesForSeason = (season: string | 'all') => { + return store.episodeListing.filter((a) => (season === 'all' ? true : a.season === season)); + }; - return 0} onClose={close} scroll='paper' maxWidth='xl' sx={{ p: 2 }}> - - - Episodes - - - Season - - - - - - selected.includes(a.e)) && !store.episodeListing.every(a => selected.includes(a.e))} - checked={store.episodeListing.every(a => selected.includes(a.e))} - onChange={() => { - if (selected.length > 0) { - setSelected([]); - } else { - setSelected(getEpisodesForSeason(season).map(a => a.e)); - } - }} - /> - - {getEpisodesForSeason(season).map((item, index, { length }) => { - const e = isNaN(parseInt(item.e)) ? item.e : parseInt(item.e); - const idStr = `S${item.season}E${e}`; - const isSelected = selected.includes(e.toString()); - const imageRef = React.createRef(); - const summaryRef = React.createRef(); - return - { - let arr: string[] = []; - if (isSelected) { - arr = [...selected.filter(a => a !== e.toString())]; - } else { - arr = [...selected, e.toString()]; - } - setSelected(arr.filter(a => a.length > 0)); - }}> - { isSelected ? : } - - {idStr} - - thumbnail - - - - {item.name} - - - {item.time.startsWith('00:') ? item.time.slice(3) : item.time} - - - - {item.description} - - - -
- Available audio languages: {item.lang.join(', ')} -
-
-
-
- { - await navigator.clipboard.writeText(item.img); - enqueueSnackbar('Copied URL to clipboard', { - variant: 'info' - }); - }}, - { - text: 'Open image in new tab', - onClick: () => { - window.open(item.img); - } - } ]} popupItem={imageRef as RefObject} /> - { - await navigator.clipboard.writeText(item.description!); - enqueueSnackbar('Copied summary to clipboard', { - variant: 'info' - }); - }, - text: 'Copy summary to clipboard' - } - ]} popupItem={summaryRef as RefObject} /> - {index < length - 1 && } -
; - })} -
-
; + return ( + 0} onClose={close} scroll="paper" maxWidth="xl" sx={{ p: 2 }}> + + + Episodes + + + Season + + + + + + selected.includes(a.e)) && !store.episodeListing.every((a) => selected.includes(a.e))} + checked={store.episodeListing.every((a) => selected.includes(a.e))} + onChange={() => { + if (selected.length > 0) { + setSelected([]); + } else { + setSelected(getEpisodesForSeason(season).map((a) => a.e)); + } + }} + /> + + {getEpisodesForSeason(season).map((item, index, { length }) => { + const e = isNaN(parseInt(item.e)) ? item.e : parseInt(item.e); + const idStr = `S${item.season}E${e}`; + const isSelected = selected.includes(e.toString()); + const imageRef = React.createRef(); + const summaryRef = React.createRef(); + return ( + + { + let arr: string[] = []; + if (isSelected) { + arr = [...selected.filter((a) => a !== e.toString())]; + } else { + arr = [...selected, e.toString()]; + } + setSelected(arr.filter((a) => a.length > 0)); + }} + > + {isSelected ? : } + + {idStr} + + thumbnail + + + + {item.name} + + {item.time.startsWith('00:') ? item.time.slice(3) : item.time} + + + {item.description} + + + +
+ Available audio languages: {item.lang.join(', ')} +
+
+
+
+ { + await navigator.clipboard.writeText(item.img); + enqueueSnackbar('Copied URL to clipboard', { + variant: 'info' + }); + } + }, + { + text: 'Open image in new tab', + onClick: () => { + window.open(item.img); + } + } + ]} + popupItem={imageRef as RefObject} + /> + { + await navigator.clipboard.writeText(item.description!); + enqueueSnackbar('Copied summary to clipboard', { + variant: 'info' + }); + }, + text: 'Copy summary to clipboard' + } + ]} + popupItem={summaryRef as RefObject} + /> + {index < length - 1 && } +
+ ); + })} +
+
+ ); }; const parseSelect = (s: string): string[] => { - const ret: string[] = []; - s.split(',').forEach(item => { - if (item.includes('-')) { - const split = item.split('-'); - if (split.length !== 2) - return; - const match = split[0].match(/[A-Za-z]+/); - if (match && match.length > 0) { - if (match.index && match.index !== 0) { - return; - } - const letters = split[0].substring(0, match[0].length); - const number = parseInt(split[0].substring(match[0].length)); - const b = parseInt(split[1]); - if (isNaN(number) || isNaN(b)) { - return; - } - for (let i = number; i <= b; i++) { - ret.push(`${letters}${i}`); - } - - } else { - const a = parseInt(split[0]); - const b = parseInt(split[1]); - if (isNaN(a) || isNaN(b)) { - return; - } - for (let i = a; i <= b; i++) { - ret.push(`${i}`); - } - } - } else { - ret.push(item); - } - }); - return [...new Set(ret)]; + const ret: string[] = []; + s.split(',').forEach((item) => { + if (item.includes('-')) { + const split = item.split('-'); + if (split.length !== 2) return; + const match = split[0].match(/[A-Za-z]+/); + if (match && match.length > 0) { + if (match.index && match.index !== 0) { + return; + } + const letters = split[0].substring(0, match[0].length); + const number = parseInt(split[0].substring(match[0].length)); + const b = parseInt(split[1]); + if (isNaN(number) || isNaN(b)) { + return; + } + for (let i = number; i <= b; i++) { + ret.push(`${letters}${i}`); + } + } else { + const a = parseInt(split[0]); + const b = parseInt(split[1]); + if (isNaN(a) || isNaN(b)) { + return; + } + for (let i = a; i <= b; i++) { + ret.push(`${i}`); + } + } + } else { + ret.push(item); + } + }); + return [...new Set(ret)]; }; export default EpisodeListing; diff --git a/gui/react/src/components/AddToQueue/SearchBox/SearchBox.css b/gui/react/src/components/AddToQueue/SearchBox/SearchBox.css index 8964e52..e1d4479 100644 --- a/gui/react/src/components/AddToQueue/SearchBox/SearchBox.css +++ b/gui/react/src/components/AddToQueue/SearchBox/SearchBox.css @@ -1,8 +1,8 @@ .listitem-hover:hover { - -webkit-filter: brightness(70%); - filter: brightness(70%); + -webkit-filter: brightness(70%); + filter: brightness(70%); } .listitem-hover { - transition: filter 0.1s ease-in; -} \ No newline at end of file + transition: filter 0.1s ease-in; +} diff --git a/gui/react/src/components/AddToQueue/SearchBox/SearchBox.tsx b/gui/react/src/components/AddToQueue/SearchBox/SearchBox.tsx index b494f33..8e667af 100644 --- a/gui/react/src/components/AddToQueue/SearchBox/SearchBox.tsx +++ b/gui/react/src/components/AddToQueue/SearchBox/SearchBox.tsx @@ -8,112 +8,144 @@ import ContextMenu from '../../reusable/ContextMenu'; import { useSnackbar } from 'notistack'; const SearchBox: React.FC = () => { - const messageHandler = React.useContext(messageChannelContext); - const [store, dispatch] = useStore(); - const [search, setSearch] = React.useState(''); + const messageHandler = React.useContext(messageChannelContext); + const [store, dispatch] = useStore(); + const [search, setSearch] = React.useState(''); - const [focus, setFocus] = React.useState(false); + const [focus, setFocus] = React.useState(false); - const [searchResult, setSearchResult] = React.useState(); - const anchor = React.useRef(null); + const [searchResult, setSearchResult] = React.useState(); + const anchor = React.useRef(null); - const { enqueueSnackbar } = useSnackbar(); + const { enqueueSnackbar } = useSnackbar(); - const selectItem = (id: string) => { - dispatch({ - type: 'downloadOptions', - payload: { - ...store.downloadOptions, - id - } - }); - }; + const selectItem = (id: string) => { + dispatch({ + type: 'downloadOptions', + payload: { + ...store.downloadOptions, + id + } + }); + }; - React.useEffect(() => { - if (search.trim().length === 0) - return setSearchResult({ isOk: true, value: [] }); + React.useEffect(() => { + if (search.trim().length === 0) return setSearchResult({ isOk: true, value: [] }); - const timeOutId = setTimeout(async () => { - if (search.trim().length > 3) { - const s = await messageHandler?.search({search}); - if (s && s.isOk) - s.value = s.value.slice(0, 10); - setSearchResult(s); - } - }, 500); - return () => clearTimeout(timeOutId); - }, [search]); + const timeOutId = setTimeout(async () => { + if (search.trim().length > 3) { + const s = await messageHandler?.search({ search }); + if (s && s.isOk) s.value = s.value.slice(0, 10); + setSearchResult(s); + } + }, 500); + return () => clearTimeout(timeOutId); + }, [search]); - const anchorBounding = anchor.current?.getBoundingClientRect(); - return setFocus(false)}> - - setFocus(true)} onChange={e => setSearch(e.target.value)} variant='outlined' label='Search' fullWidth /> - {searchResult !== undefined && searchResult.isOk && searchResult.value.length > 0 && focus && - - - {searchResult && searchResult.isOk ? - searchResult.value.map((a, ind, arr) => { - const imageRef = React.createRef(); - const summaryRef = React.createRef(); - return - { - selectItem(a.id); - setFocus(false); - }}> - - - thumbnail - - - - {a.name} - - {a.desc && - {a.desc} - } - {a.lang && - Languages: {a.lang.join(', ')} - } - - ID: {a.id} - - - - - { - await navigator.clipboard.writeText(a.image); - enqueueSnackbar('Copied URL to clipboard', { - variant: 'info' - }); - }}, - { - text: 'Open image in new tab', - onClick: () => { - window.open(a.image); - } - } ]} popupItem={imageRef as RefObject} /> - {a.desc && - { - await navigator.clipboard.writeText(a.desc!); - enqueueSnackbar('Copied summary to clipboard', { - variant: 'info' - }); - }, - text: 'Copy summary to clipboard' - } - ]} popupItem={summaryRef as RefObject} /> - } - {(ind < arr.length - 1) && } - ; - }) - : <>} - - } - - ; + const anchorBounding = anchor.current?.getBoundingClientRect(); + return ( + setFocus(false)}> + + setFocus(true)} onChange={(e) => setSearch(e.target.value)} variant="outlined" label="Search" fullWidth /> + {searchResult !== undefined && searchResult.isOk && searchResult.value.length > 0 && focus && ( + + + {searchResult && searchResult.isOk ? ( + searchResult.value.map((a, ind, arr) => { + const imageRef = React.createRef(); + const summaryRef = React.createRef(); + return ( + + { + selectItem(a.id); + setFocus(false); + }} + > + + + thumbnail + + + + {a.name} + + {a.desc && ( + + {a.desc} + + )} + {a.lang && ( + + Languages: {a.lang.join(', ')} + + )} + + ID: {a.id} + + + + + { + await navigator.clipboard.writeText(a.image); + enqueueSnackbar('Copied URL to clipboard', { + variant: 'info' + }); + } + }, + { + text: 'Open image in new tab', + onClick: () => { + window.open(a.image); + } + } + ]} + popupItem={imageRef as RefObject} + /> + {a.desc && ( + { + await navigator.clipboard.writeText(a.desc!); + enqueueSnackbar('Copied summary to clipboard', { + variant: 'info' + }); + }, + text: 'Copy summary to clipboard' + } + ]} + popupItem={summaryRef as RefObject} + /> + )} + {ind < arr.length - 1 && } + + ); + }) + ) : ( + <> + )} + + + )} + + + ); }; export default SearchBox; diff --git a/gui/react/src/components/AuthButton.tsx b/gui/react/src/components/AuthButton.tsx index a218afb..f18519c 100644 --- a/gui/react/src/components/AuthButton.tsx +++ b/gui/react/src/components/AuthButton.tsx @@ -6,107 +6,115 @@ import Require from './Require'; import { useSnackbar } from 'notistack'; const AuthButton: React.FC = () => { - const snackbar = useSnackbar(); + const snackbar = useSnackbar(); - const [open, setOpen] = React.useState(false); + const [open, setOpen] = React.useState(false); - const [username, setUsername] = React.useState(''); - const [password, setPassword] = React.useState(''); + const [username, setUsername] = React.useState(''); + const [password, setPassword] = React.useState(''); - const [usernameError, setUsernameError] = React.useState(false); - const [passwordError, setPasswordError] = React.useState(false); + const [usernameError, setUsernameError] = React.useState(false); + const [passwordError, setPasswordError] = React.useState(false); - const messageChannel = React.useContext(messageChannelContext); + const messageChannel = React.useContext(messageChannelContext); - const [loading, setLoading] = React.useState(false); - const [error, setError] = React.useState(undefined); - const [authed, setAuthed] = React.useState(false); + const [loading, setLoading] = React.useState(false); + const [error, setError] = React.useState(undefined); + const [authed, setAuthed] = React.useState(false); - const checkAuth = async () => { - setAuthed((await messageChannel?.checkToken())?.isOk ?? false); - }; + const checkAuth = async () => { + setAuthed((await messageChannel?.checkToken())?.isOk ?? false); + }; - React.useEffect(() => { checkAuth(); }, []); + React.useEffect(() => { + checkAuth(); + }, []); - const handleSubmit = async () => { - if (!messageChannel) - throw new Error('Invalid state'); //The components to confirm only render if the messageChannel is not undefinded - if (username.trim().length === 0) - return setUsernameError(true); - if (password.trim().length === 0) - return setPasswordError(true); - setUsernameError(false); - setPasswordError(false); - setLoading(true); + const handleSubmit = async () => { + if (!messageChannel) throw new Error('Invalid state'); //The components to confirm only render if the messageChannel is not undefinded + if (username.trim().length === 0) return setUsernameError(true); + if (password.trim().length === 0) return setPasswordError(true); + setUsernameError(false); + setPasswordError(false); + setLoading(true); - const res = await messageChannel.auth({ username, password }); - if (res.isOk) { - setOpen(false); - snackbar.enqueueSnackbar('Logged in', { - variant: 'success' - }); - setUsername(''); - setPassword(''); - } else { - setError(res.reason); - } - await checkAuth(); - setLoading(false); - }; + const res = await messageChannel.auth({ username, password }); + if (res.isOk) { + setOpen(false); + snackbar.enqueueSnackbar('Logged in', { + variant: 'success' + }); + setUsername(''); + setPassword(''); + } else { + setError(res.reason); + } + await checkAuth(); + setLoading(false); + }; - return - - - Error during Authentication - - {error?.name} - {error?.message} - - - - - - Authentication - - - Here, you need to enter your username (most likely your Email) and your password.
- These information are not stored anywhere and are only used to authenticate with the service once. -
- setUsername(e.target.value)} - disabled={loading} - /> - setPassword(e.target.value)} - disabled={loading} - /> -
- - {loading && } - - - -
- -
; + return ( + + + + Error during Authentication + + {error?.name} + {error?.message} + + + + + + Authentication + + + Here, you need to enter your username (most likely your Email) and your password. +
+ These information are not stored anywhere and are only used to authenticate with the service once. +
+ setUsername(e.target.value)} + disabled={loading} + /> + setPassword(e.target.value)} + disabled={loading} + /> +
+ + {loading && } + + + +
+ +
+ ); }; -export default AuthButton; \ No newline at end of file +export default AuthButton; diff --git a/gui/react/src/components/LogoutButton.tsx b/gui/react/src/components/LogoutButton.tsx index 83283d7..0e91cca 100644 --- a/gui/react/src/components/LogoutButton.tsx +++ b/gui/react/src/components/LogoutButton.tsx @@ -6,32 +6,26 @@ import { messageChannelContext } from '../provider/MessageChannel'; import Require from './Require'; const LogoutButton: React.FC = () => { - const messageChannel = React.useContext(messageChannelContext); - const [, dispatch] = useStore(); + const messageChannel = React.useContext(messageChannelContext); + const [, dispatch] = useStore(); - const logout = async () => { - if (await messageChannel?.isDownloading()) - return alert('You are currently downloading. Please finish the download first.'); - if (await messageChannel?.logout()) - dispatch({ - type: 'service', - payload: undefined - }); - else - alert('Unable to change service'); - }; - - return - - ; + const logout = async () => { + if (await messageChannel?.isDownloading()) return alert('You are currently downloading. Please finish the download first.'); + if (await messageChannel?.logout()) + dispatch({ + type: 'service', + payload: undefined + }); + else alert('Unable to change service'); + }; + return ( + + + + ); }; -export default LogoutButton; \ No newline at end of file +export default LogoutButton; diff --git a/gui/react/src/components/MainFrame/DownloadManager/DownloadManager.tsx b/gui/react/src/components/MainFrame/DownloadManager/DownloadManager.tsx index ee78ac5..0118b68 100644 --- a/gui/react/src/components/MainFrame/DownloadManager/DownloadManager.tsx +++ b/gui/react/src/components/MainFrame/DownloadManager/DownloadManager.tsx @@ -4,37 +4,37 @@ import { RandomEvent } from '../../../../../../@types/randomEvents'; import { messageChannelContext } from '../../../provider/MessageChannel'; const useDownloadManager = () => { - const messageHandler = React.useContext(messageChannelContext); + const messageHandler = React.useContext(messageChannelContext); - const [progressData, setProgressData] = React.useState(); - const [current, setCurrent] = React.useState(); - - React.useEffect(() => { - const handler = (ev: RandomEvent<'progress'>) => { - console.log(ev.data); - setProgressData(ev.data); - }; + const [progressData, setProgressData] = React.useState(); + const [current, setCurrent] = React.useState(); - const currentHandler = (ev: RandomEvent<'current'>) => { - setCurrent(ev.data); - }; - - const finishHandler = () => { - setProgressData(undefined); - }; + React.useEffect(() => { + const handler = (ev: RandomEvent<'progress'>) => { + console.log(ev.data); + setProgressData(ev.data); + }; - messageHandler?.randomEvents.on('progress', handler); - messageHandler?.randomEvents.on('current', currentHandler); - messageHandler?.randomEvents.on('finish', finishHandler); + const currentHandler = (ev: RandomEvent<'current'>) => { + setCurrent(ev.data); + }; - return () => { - messageHandler?.randomEvents.removeListener('progress', handler); - messageHandler?.randomEvents.removeListener('finish', finishHandler); - messageHandler?.randomEvents.removeListener('current', currentHandler); - }; - }, [messageHandler]); - - return { data: progressData, current}; + const finishHandler = () => { + setProgressData(undefined); + }; + + messageHandler?.randomEvents.on('progress', handler); + messageHandler?.randomEvents.on('current', currentHandler); + messageHandler?.randomEvents.on('finish', finishHandler); + + return () => { + messageHandler?.randomEvents.removeListener('progress', handler); + messageHandler?.randomEvents.removeListener('finish', finishHandler); + messageHandler?.randomEvents.removeListener('current', currentHandler); + }; + }, [messageHandler]); + + return { data: progressData, current }; }; -export default useDownloadManager; \ No newline at end of file +export default useDownloadManager; diff --git a/gui/react/src/components/MainFrame/MainFrame.tsx b/gui/react/src/components/MainFrame/MainFrame.tsx index 1e387e5..25fcd78 100644 --- a/gui/react/src/components/MainFrame/MainFrame.tsx +++ b/gui/react/src/components/MainFrame/MainFrame.tsx @@ -3,9 +3,11 @@ import React from 'react'; import Queue from './Queue/Queue'; const MainFrame: React.FC = () => { - return - - ; + return ( + + + + ); }; -export default MainFrame; \ No newline at end of file +export default MainFrame; diff --git a/gui/react/src/components/MainFrame/Queue/Queue.tsx b/gui/react/src/components/MainFrame/Queue/Queue.tsx index 96aa825..eddea59 100644 --- a/gui/react/src/components/MainFrame/Queue/Queue.tsx +++ b/gui/react/src/components/MainFrame/Queue/Queue.tsx @@ -7,414 +7,537 @@ import DeleteIcon from '@mui/icons-material/Delete'; import useDownloadManager from '../DownloadManager/DownloadManager'; const Queue: React.FC = () => { - const { data, current } = useDownloadManager(); - const queue = React.useContext(queueContext); - const msg = React.useContext(messageChannelContext); + const { data, current } = useDownloadManager(); + const queue = React.useContext(queueContext); + const msg = React.useContext(messageChannelContext); + if (!msg) return <>Never; - if (!msg) - return <>Never; - - return data || queue.length > 0 ? <> - {data && <> - - - Thumbnail - - - - - - {data.downloadInfo.parent.title} - - - {data.downloadInfo.title} - - - - - - Downloading: {data.downloadInfo.language.name} - - - - - - - - {data.progress.cur} / {(data.progress.total)} parts ({data.progress.percent}% | {formatTime(data.progress.time)} | {(data.progress.downloadSpeed / 1024 / 1024).toFixed(2)} MB/s | {(data.progress.bytes / 1024 / 1024).toFixed(2)}MB) - - - - - - - - } - { - current && !data && <> - - - Thumbnail - - - - - - {current.parent.title} - - - {current.title} - - - - - - - Downloading: - - - - - - - - - - 0 / ? parts (0% | XX:XX | 0 MB/s | 0MB) - - - - - - - - } - {queue.map((queueItem, index, { length }) => { - return - - Thumbnail - - - - {queueItem.parent.title} - - - S{queueItem.parent.season}E{queueItem.episode} - - - {queueItem.title} - - - - - Dub(s): {queueItem.dubLang.join(', ')} - - - Sub(s): {queueItem.dlsubs.join(', ')} - - - Quality: {queueItem.q} - - - - - { - msg.removeFromQueue(index); - }} - sx={{ - backgroundColor: '#ff573a25', - height: '40px', - transition: '250ms', - '&:hover' : { - backgroundColor: '#ff573a', - } - }}> - - - - - - - - ; - })} - : - - Selected episodes will be shown here - - - - - - - - - - - - - - - - ; + return data || queue.length > 0 ? ( + <> + {data && ( + <> + + + Thumbnail + + + + + + {data.downloadInfo.parent.title} + + + {data.downloadInfo.title} + + + + + + Downloading: {data.downloadInfo.language.name} + + + + + + + + {data.progress.cur} / {data.progress.total} parts ({data.progress.percent}% | {formatTime(data.progress.time)} |{' '} + {(data.progress.downloadSpeed / 1024 / 1024).toFixed(2)} MB/s | {(data.progress.bytes / 1024 / 1024).toFixed(2)}MB) + + + + + + + + )} + {current && !data && ( + <> + + + Thumbnail + + + + + + {current.parent.title} + + + {current.title} + + + + + + + Downloading: + + + + + + + + + + 0 / ? parts (0% | XX:XX | 0 MB/s | 0MB) + + + + + + + + )} + {queue.map((queueItem, index, { length }) => { + return ( + + + Thumbnail + + + + {queueItem.parent.title} + + + S{queueItem.parent.season}E{queueItem.episode} + + + {queueItem.title} + + + + + Dub(s): {queueItem.dubLang.join(', ')} + + + Sub(s): {queueItem.dlsubs.join(', ')} + + + Quality: {queueItem.q} + + + + + { + msg.removeFromQueue(index); + }} + sx={{ + backgroundColor: '#ff573a25', + height: '40px', + transition: '250ms', + '&:hover': { + backgroundColor: '#ff573a' + } + }} + > + + + + + + + + ); + })} + + ) : ( + + + Selected episodes will be shown here + + + + + + + + + + + + + + + + + ); }; const formatTime = (time: number) => { - time = Math.floor(time / 1000); - const minutes = Math.floor(time / 60); - time = time % 60; + time = Math.floor(time / 1000); + const minutes = Math.floor(time / 60); + time = time % 60; - return `${minutes.toFixed(0).length < 2 ? `0${minutes}` : minutes}m${time.toFixed(0).length < 2 ? `0${time}` : time}s`; + return `${minutes.toFixed(0).length < 2 ? `0${minutes}` : minutes}m${time.toFixed(0).length < 2 ? `0${time}` : time}s`; }; -export default Queue; \ No newline at end of file +export default Queue; diff --git a/gui/react/src/components/MenuBar/MenuBar.tsx b/gui/react/src/components/MenuBar/MenuBar.tsx index dc48ab7..a79498d 100644 --- a/gui/react/src/components/MenuBar/MenuBar.tsx +++ b/gui/react/src/components/MenuBar/MenuBar.tsx @@ -5,120 +5,134 @@ import useStore from '../../hooks/useStore'; import { StoreState } from '../../provider/Store'; const MenuBar: React.FC = () => { - const [ openMenu, setMenuOpen ] = React.useState<'settings'|'help'|undefined>(); - const [anchorEl, setAnchorEl] = React.useState(null); - const [store, dispatch] = useStore(); + const [openMenu, setMenuOpen] = React.useState<'settings' | 'help' | undefined>(); + const [anchorEl, setAnchorEl] = React.useState(null); + const [store, dispatch] = useStore(); - const messageChannel = React.useContext(messageChannelContext); + const messageChannel = React.useContext(messageChannelContext); - React.useEffect(() => { - (async () => { - if (!messageChannel || store.version !== '') - return; - dispatch({ - type: 'version', - payload: await messageChannel.version() - }); - })(); - }, [messageChannel]); + React.useEffect(() => { + (async () => { + if (!messageChannel || store.version !== '') return; + dispatch({ + type: 'version', + payload: await messageChannel.version() + }); + })(); + }, [messageChannel]); - const transformService = (service: StoreState['service']) => { - switch(service) { - case 'crunchy': - return 'Crunchyroll'; - case 'hidive': - return 'Hidive'; - case 'ao': - return 'AnimeOnegai'; - case 'adn': - return 'AnimationDigitalNetwork'; - } - }; + const transformService = (service: StoreState['service']) => { + switch (service) { + case 'crunchy': + return 'Crunchyroll'; + case 'hidive': + return 'Hidive'; + case 'ao': + return 'AnimeOnegai'; + case 'adn': + return 'AnimationDigitalNetwork'; + } + }; - const msg = React.useContext(messageChannelContext); + const msg = React.useContext(messageChannelContext); - const handleClick = (event: React.MouseEvent, n: 'settings'|'help') => { - setAnchorEl(event.currentTarget); - setMenuOpen(n); - }; - const handleClose = () => { - setAnchorEl(null); - setMenuOpen(undefined); - }; + const handleClick = (event: React.MouseEvent, n: 'settings' | 'help') => { + setAnchorEl(event.currentTarget); + setMenuOpen(n); + }; + const handleClose = () => { + setAnchorEl(null); + setMenuOpen(undefined); + }; - if (!msg) - return <>; + if (!msg) return <>; - return - - - - - - { - msg.openFolder('config'); - handleClose(); - }}> - Open settings folder - - { - msg.openFile(['config', 'bin-path.yml']); - handleClose(); - }}> - Open FFmpeg/Mkvmerge file - - { - msg.openFile(['config', 'cli-defaults.yml']); - handleClose(); - }}> - Open advanced options - - { - msg.openFolder('content'); - handleClose(); - }}> - Open output path - - - - { - msg.openURL('https://github.com/anidl/multi-downloader-nx'); - handleClose(); - }}> - GitHub - - { - msg.openURL('https://github.com/anidl/multi-downloader-nx/issues/new?assignees=AnimeDL,AnidlSupport&labels=bug&template=bug.yml&title=BUG'); - handleClose(); - }}> - Report a bug - - { - msg.openURL('https://github.com/anidl/multi-downloader-nx/graphs/contributors'); - handleClose(); - }}> - Contributors - - { - msg.openURL('https://discord.gg/qEpbWen5vq'); - handleClose(); - }}> - Discord - - { - handleClose(); - }}> - Version: {store.version} - - - - {transformService(store.service)} - - ; + return ( + + + + + + + { + msg.openFolder('config'); + handleClose(); + }} + > + Open settings folder + + { + msg.openFile(['config', 'bin-path.yml']); + handleClose(); + }} + > + Open FFmpeg/Mkvmerge file + + { + msg.openFile(['config', 'cli-defaults.yml']); + handleClose(); + }} + > + Open advanced options + + { + msg.openFolder('content'); + handleClose(); + }} + > + Open output path + + + + { + msg.openURL('https://github.com/anidl/multi-downloader-nx'); + handleClose(); + }} + > + GitHub + + { + msg.openURL('https://github.com/anidl/multi-downloader-nx/issues/new?assignees=AnimeDL,AnidlSupport&labels=bug&template=bug.yml&title=BUG'); + handleClose(); + }} + > + Report a bug + + { + msg.openURL('https://github.com/anidl/multi-downloader-nx/graphs/contributors'); + handleClose(); + }} + > + Contributors + + { + msg.openURL('https://discord.gg/qEpbWen5vq'); + handleClose(); + }} + > + Discord + + { + handleClose(); + }} + > + Version: {store.version} + + + + {transformService(store.service)} + + + ); }; export default MenuBar; diff --git a/gui/react/src/components/Require.tsx b/gui/react/src/components/Require.tsx index 8c2827d..59f6afa 100644 --- a/gui/react/src/components/Require.tsx +++ b/gui/react/src/components/Require.tsx @@ -2,13 +2,17 @@ import React from 'react'; import { Box, Backdrop, CircularProgress } from '@mui/material'; export type RequireType = { - value?: T -} - -const Require = (props: React.PropsWithChildren>) => { - return props.value === undefined ? - - : {props.children}; + value?: T; }; -export default Require; \ No newline at end of file +const Require = (props: React.PropsWithChildren>) => { + return props.value === undefined ? ( + + + + ) : ( + {props.children} + ); +}; + +export default Require; diff --git a/gui/react/src/components/StartQueue.tsx b/gui/react/src/components/StartQueue.tsx index 71eb834..e2ec080 100644 --- a/gui/react/src/components/StartQueue.tsx +++ b/gui/react/src/components/StartQueue.tsx @@ -5,38 +5,30 @@ import { messageChannelContext } from '../provider/MessageChannel'; import Require from './Require'; const StartQueueButton: React.FC = () => { - const messageChannel = React.useContext(messageChannelContext); - const [start, setStart] = React.useState(false); - const msg = React.useContext(messageChannelContext); + const messageChannel = React.useContext(messageChannelContext); + const [start, setStart] = React.useState(false); + const msg = React.useContext(messageChannelContext); - React.useEffect(() => { - (async () => { - if (!msg) - return alert('Invalid state: msg not found'); - setStart(await msg.getDownloadQueue()); - })(); - }, []); + React.useEffect(() => { + (async () => { + if (!msg) return alert('Invalid state: msg not found'); + setStart(await msg.getDownloadQueue()); + })(); + }, []); - const change = async () => { - if (await messageChannel?.isDownloading()) - alert('The current download will be finished before the queue stops'); - msg?.setDownloadQueue(!start); - setStart(!start); - }; - - return - - ; + const change = async () => { + if (await messageChannel?.isDownloading()) alert('The current download will be finished before the queue stops'); + msg?.setDownloadQueue(!start); + setStart(!start); + }; + return ( + + + + ); }; -export default StartQueueButton; \ No newline at end of file +export default StartQueueButton; diff --git a/gui/react/src/components/reusable/ContextMenu.tsx b/gui/react/src/components/reusable/ContextMenu.tsx index 41fa4a6..f621ca9 100644 --- a/gui/react/src/components/reusable/ContextMenu.tsx +++ b/gui/react/src/components/reusable/ContextMenu.tsx @@ -2,64 +2,74 @@ import { Box, Button, Divider, List, SxProps } from '@mui/material'; import React from 'react'; export type Option = { - text: string, - onClick: () => unknown -} - -export type ContextMenuProps = { - options: ('divider'|Option)[], - popupItem: React.RefObject -} - -const buttonSx: SxProps = { - '&:hover': { - background: 'rgb(0, 30, 60)' - }, - fontSize: '0.7rem', - minHeight: '30px', - justifyContent: 'center', - p: 0 + text: string; + onClick: () => unknown; }; -function ContextMenu(props: ContextMenuProps) { - const [anchor, setAnchor] = React.useState( { x: 0, y: 0 } ); +export type ContextMenuProps = { + options: ('divider' | Option)[]; + popupItem: React.RefObject; +}; - const [show, setShow] = React.useState(false); +const buttonSx: SxProps = { + '&:hover': { + background: 'rgb(0, 30, 60)' + }, + fontSize: '0.7rem', + minHeight: '30px', + justifyContent: 'center', + p: 0 +}; - React.useEffect(() => { - const { popupItem: ref } = props; - if (ref.current === null) - return; - const listener = (ev: MouseEvent) => { - ev.preventDefault(); - setAnchor({ x: ev.x + 10, y: ev.y + 10 }); - setShow(true); - }; - ref.current.addEventListener('contextmenu', listener); +function ContextMenu(props: ContextMenuProps) { + const [anchor, setAnchor] = React.useState({ x: 0, y: 0 }); - return () => { - if (ref.current) - ref.current.removeEventListener('contextmenu', listener); - }; - }, [ props.popupItem ]); + const [show, setShow] = React.useState(false); - return show ? - - {props.options.map((item, i) => { - return item === 'divider' ? : - ; - })} - - - - : <>; + React.useEffect(() => { + const { popupItem: ref } = props; + if (ref.current === null) return; + const listener = (ev: MouseEvent) => { + ev.preventDefault(); + setAnchor({ x: ev.x + 10, y: ev.y + 10 }); + setShow(true); + }; + ref.current.addEventListener('contextmenu', listener); + + return () => { + if (ref.current) ref.current.removeEventListener('contextmenu', listener); + }; + }, [props.popupItem]); + + return show ? ( + + + {props.options.map((item, i) => { + return item === 'divider' ? ( + + ) : ( + + ); + })} + + + + + ) : ( + <> + ); } export default ContextMenu; diff --git a/gui/react/src/components/reusable/LinearProgressWithLabel.tsx b/gui/react/src/components/reusable/LinearProgressWithLabel.tsx index 295251b..3ded1e2 100644 --- a/gui/react/src/components/reusable/LinearProgressWithLabel.tsx +++ b/gui/react/src/components/reusable/LinearProgressWithLabel.tsx @@ -7,18 +7,16 @@ import React from 'react'; export type LinearProgressWithLabelProps = LinearProgressProps & { value: number }; const LinearProgressWithLabel: React.FC = (props) => { - return ( - - - - - - {`${Math.round( - props.value, - )}%`} - - - ); + return ( + + + + + + {`${Math.round(props.value)}%`} + + + ); }; -export default LinearProgressWithLabel; \ No newline at end of file +export default LinearProgressWithLabel; diff --git a/gui/react/src/components/reusable/MultiSelect.tsx b/gui/react/src/components/reusable/MultiSelect.tsx index 56ae3e7..ae0638f 100644 --- a/gui/react/src/components/reusable/MultiSelect.tsx +++ b/gui/react/src/components/reusable/MultiSelect.tsx @@ -2,73 +2,64 @@ import React from 'react'; import { FormControl, InputLabel, MenuItem, OutlinedInput, Select, Theme, useTheme } from '@mui/material'; export type MultiSelectProps = { - values: string[], - selected: string[], - onChange: (values: string[]) => unknown, - title: string, - allOption?: boolean -} + values: string[]; + selected: string[]; + onChange: (values: string[]) => unknown; + title: string; + allOption?: boolean; +}; const ITEM_HEIGHT = 48; const ITEM_PADDING_TOP = 8; const MenuProps = { - PaperProps: { - style: { - maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, - width: 250 - } - } + PaperProps: { + style: { + maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, + width: 250 + } + } }; function getStyles(name: string, personName: readonly string[], theme: Theme) { - return { - fontWeight: - (personName ?? []).indexOf(name) === -1 - ? theme.typography.fontWeightRegular - : theme.typography.fontWeightMedium - }; + return { + fontWeight: (personName ?? []).indexOf(name) === -1 ? theme.typography.fontWeightRegular : theme.typography.fontWeightMedium + }; } const MultiSelect: React.FC = (props) => { - const theme = useTheme(); + const theme = useTheme(); - return
- - {props.title} - - -
; + return ( +
+ + {props.title} + + +
+ ); }; -export default MultiSelect; \ No newline at end of file +export default MultiSelect; diff --git a/gui/react/src/hooks/useStore.tsx b/gui/react/src/hooks/useStore.tsx index 3c3699d..66edf14 100644 --- a/gui/react/src/hooks/useStore.tsx +++ b/gui/react/src/hooks/useStore.tsx @@ -2,11 +2,11 @@ import React from 'react'; import { StoreAction, StoreContext, StoreState } from '../provider/Store'; const useStore = () => { - const context = React.useContext(StoreContext as unknown as React.Context<[StoreState, React.Dispatch>]>); - if (!context) { - throw new Error('useStore must be used under Store'); - } - return context; + const context = React.useContext(StoreContext as unknown as React.Context<[StoreState, React.Dispatch>]>); + if (!context) { + throw new Error('useStore must be used under Store'); + } + return context; }; -export default useStore; \ No newline at end of file +export default useStore; diff --git a/gui/react/src/index.tsx b/gui/react/src/index.tsx index ebaa1cd..46c61f9 100644 --- a/gui/react/src/index.tsx +++ b/gui/react/src/index.tsx @@ -16,36 +16,35 @@ document.body.style.display = 'flex'; document.body.style.justifyContent = 'center'; const notistackRef = React.createRef(); -const onClickDismiss = (key: SnackbarKey | undefined) => () => { - if (notistackRef.current) - notistackRef.current.closeSnackbar(key); +const onClickDismiss = (key: SnackbarKey | undefined) => () => { + if (notistackRef.current) notistackRef.current.closeSnackbar(key); }; const container = document.getElementById('root'); const root = createRoot(container as HTMLElement); root.render( - - - ( - - - - )} - > - - - - -); \ No newline at end of file + + + ( + + + + )} + > + + + + +); diff --git a/gui/react/src/provider/ErrorHandler.tsx b/gui/react/src/provider/ErrorHandler.tsx index e5135ba..f1ee830 100644 --- a/gui/react/src/provider/ErrorHandler.tsx +++ b/gui/react/src/provider/ErrorHandler.tsx @@ -1,39 +1,44 @@ import { Box, Typography } from '@mui/material'; import React from 'react'; -export default class ErrorHandler extends React.Component<{ - children: React.ReactNode|React.ReactNode[] -}, { - error?: { - er: Error, - stack: React.ErrorInfo - } -}> { - - constructor(props: { - children: React.ReactNode|React.ReactNode[] - }) { - super(props); - this.state = { error: undefined }; - } - - componentDidCatch(er: Error, stack: React.ErrorInfo) { - this.setState({ error: { er, stack } }); - } - - render(): React.ReactNode { - return this.state.error ? - - - {`${this.state.error.er.name}: ${this.state.error.er.message}`} -
- {this.state.error.stack.componentStack?.split('\n').map(a => { - return <> - {a} -
- ; - })} -
-
: this.props.children; - } -} \ No newline at end of file +export default class ErrorHandler extends React.Component< + { + children: React.ReactNode | React.ReactNode[]; + }, + { + error?: { + er: Error; + stack: React.ErrorInfo; + }; + } +> { + constructor(props: { children: React.ReactNode | React.ReactNode[] }) { + super(props); + this.state = { error: undefined }; + } + + componentDidCatch(er: Error, stack: React.ErrorInfo) { + this.setState({ error: { er, stack } }); + } + + render(): React.ReactNode { + return this.state.error ? ( + + + {`${this.state.error.er.name}: ${this.state.error.er.message}`} +
+ {this.state.error.stack.componentStack?.split('\n').map((a) => { + return ( + <> + {a} +
+ + ); + })} +
+
+ ) : ( + this.props.children + ); + } +} diff --git a/gui/react/src/provider/MessageChannel.tsx b/gui/react/src/provider/MessageChannel.tsx index f0f0768..c5726fd 100644 --- a/gui/react/src/provider/MessageChannel.tsx +++ b/gui/react/src/provider/MessageChannel.tsx @@ -9,236 +9,236 @@ import { useSnackbar } from 'notistack'; import { LockOutlined, PowerSettingsNew } from '@mui/icons-material'; import { GUIConfig } from '../../../../modules/module.cfg-loader'; -export type FrontEndMessages = (MessageHandler & { randomEvents: RandomEventHandler, logout: () => Promise }); +export type FrontEndMessages = MessageHandler & { randomEvents: RandomEventHandler; logout: () => Promise }; export class RandomEventHandler { - private handler: { - [eventName in keyof RandomEvents]: Handler[] - } = { - progress: [], - finish: [], - queueChange: [], - current: [] - }; + private handler: { + [eventName in keyof RandomEvents]: Handler[]; + } = { + progress: [], + finish: [], + queueChange: [], + current: [] + }; - public on(name: T, listener: Handler) { - if (Object.prototype.hasOwnProperty.call(this.handler, name)) { - this.handler[name].push(listener as any); - } else { - this.handler[name] = [ listener as any ]; - } - } + public on(name: T, listener: Handler) { + if (Object.prototype.hasOwnProperty.call(this.handler, name)) { + this.handler[name].push(listener as any); + } else { + this.handler[name] = [listener as any]; + } + } - public emit(name: keyof RandomEvents, data: RandomEvent) { - (this.handler[name] ?? []).forEach(handler => handler(data as any)); - } + public emit(name: keyof RandomEvents, data: RandomEvent) { + (this.handler[name] ?? []).forEach((handler) => handler(data as any)); + } - public removeListener(name: T, listener: Handler) { - this.handler[name] = (this.handler[name] as Handler[]).filter(a => a !== listener) as any; - } + public removeListener(name: T, listener: Handler) { + this.handler[name] = (this.handler[name] as Handler[]).filter((a) => a !== listener) as any; + } } -export const messageChannelContext = React.createContext(undefined); +export const messageChannelContext = React.createContext(undefined); async function messageAndResponse(socket: WebSocket, msg: WSMessage): Promise> { - const id = v4(); - const ret = new Promise>((resolve) => { - const handler = function({ data }: MessageEvent) { - const parsed = JSON.parse(data.toString()) as WSMessageWithID; - if (parsed.id === id) { - socket.removeEventListener('message', handler); - resolve(parsed); - } - }; - socket.addEventListener('message', handler); - }); - const toSend = msg as WSMessageWithID; - toSend.id = id; + const id = v4(); + const ret = new Promise>((resolve) => { + const handler = function ({ data }: MessageEvent) { + const parsed = JSON.parse(data.toString()) as WSMessageWithID; + if (parsed.id === id) { + socket.removeEventListener('message', handler); + resolve(parsed); + } + }; + socket.addEventListener('message', handler); + }); + const toSend = msg as WSMessageWithID; + toSend.id = id; - socket.send(JSON.stringify(toSend)); - return ret; + socket.send(JSON.stringify(toSend)); + return ret; } const MessageChannelProvider: FCWithChildren = ({ children }) => { + const [store, dispatch] = useStore(); + const [socket, setSocket] = React.useState(); + const [publicWS, setPublicWS] = React.useState(); + const [usePassword, setUsePassword] = React.useState<'waiting' | 'yes' | 'no'>('waiting'); + const [isSetup, setIsSetup] = React.useState<'waiting' | 'yes' | 'no'>('waiting'); - const [store, dispatch] = useStore(); - const [socket, setSocket] = React.useState(); - const [publicWS, setPublicWS] = React.useState(); - const [usePassword, setUsePassword] = React.useState<'waiting'|'yes'|'no'>('waiting'); - const [isSetup, setIsSetup] = React.useState<'waiting'|'yes'|'no'>('waiting'); + const { enqueueSnackbar } = useSnackbar(); - const { enqueueSnackbar } = useSnackbar(); + React.useEffect(() => { + const wss = new WebSocket(`${location.protocol == 'https:' ? 'wss' : 'ws'}://${process.env.NODE_ENV === 'development' ? 'localhost:3000' : window.location.host}/public`); + wss.addEventListener('open', () => { + setPublicWS(wss); + }); + wss.addEventListener('error', () => { + enqueueSnackbar('Unable to connect to server. Please reload the page to try again.', { variant: 'error' }); + }); + }, []); - React.useEffect(() => { - const wss = new WebSocket(`${location.protocol == 'https:' ? 'wss' : 'ws'}://${process.env.NODE_ENV === 'development' ? 'localhost:3000' : window.location.host}/public`); - wss.addEventListener('open', () => { - setPublicWS(wss); - }); - wss.addEventListener('error', () => { - enqueueSnackbar('Unable to connect to server. Please reload the page to try again.', { variant: 'error' }); - }); - }, []); + React.useEffect(() => { + (async () => { + if (!publicWS) return; + setUsePassword((await messageAndResponse(publicWS, { name: 'requirePassword', data: undefined })).data ? 'yes' : 'no'); + setIsSetup((await messageAndResponse(publicWS, { name: 'isSetup', data: undefined })).data ? 'yes' : 'no'); + })(); + }, [publicWS]); - React.useEffect(() => { - (async () => { - if (!publicWS) - return; - setUsePassword((await messageAndResponse(publicWS, { name: 'requirePassword', data: undefined })).data ? 'yes' : 'no'); - setIsSetup((await messageAndResponse(publicWS, { name: 'isSetup', data: undefined })).data ? 'yes' : 'no'); - })(); - }, [publicWS]); + const connect = (ev?: React.FormEvent) => { + let search = new URLSearchParams(); + if (ev) { + ev.preventDefault(); + const formData = new FormData(ev.currentTarget); + const password = formData.get('password')?.toString(); + if (!password) + return enqueueSnackbar('Please provide both a username and password', { + variant: 'error' + }); + search = new URLSearchParams({ + password + }); + } - const connect = (ev?: React.FormEvent) => { - let search = new URLSearchParams(); - if (ev) { - ev.preventDefault(); - const formData = new FormData(ev.currentTarget); - const password = formData.get('password')?.toString(); - if (!password) - return enqueueSnackbar('Please provide both a username and password', { - variant: 'error' - }); - search = new URLSearchParams({ - password - }); - } + const wws = new WebSocket( + `${location.protocol == 'https:' ? 'wss' : 'ws'}://${process.env.NODE_ENV === 'development' ? 'localhost:3000' : window.location.host}/private?${search}` + ); + wws.addEventListener('open', () => { + console.log('[INFO] [WS] Connected'); + setSocket(wws); + }); + wws.addEventListener('error', (er) => { + console.error('[ERROR] [WS]', er); + enqueueSnackbar('Unable to connect to server. Please check the password and try again.', { + variant: 'error' + }); + }); + }; - const wws = new WebSocket(`${location.protocol == 'https:' ? 'wss' : 'ws'}://${process.env.NODE_ENV === 'development' ? 'localhost:3000' : window.location.host}/private?${search}`, ); - wws.addEventListener('open', () => { - console.log('[INFO] [WS] Connected'); - setSocket(wws); - }); - wws.addEventListener('error', (er) => { - console.error('[ERROR] [WS]', er); - enqueueSnackbar('Unable to connect to server. Please check the password and try again.', { - variant: 'error' - }); - }); - }; + const setup = async (ev: React.FormEvent) => { + ev.preventDefault(); + if (!socket) return enqueueSnackbar('Invalid state: socket not found', { variant: 'error' }); + const formData = new FormData(ev.currentTarget); + const password = formData.get('password'); + const data = { + port: parseInt(formData.get('port')?.toString() ?? '') ?? 3000, + password: password ? password.toString() : undefined + } as GUIConfig; + await messageAndResponse(socket, { name: 'setupServer', data }); + enqueueSnackbar(`The following settings have been set: Port=${data.port}, Password=${data.password ?? 'noPasswordRequired'}`, { + variant: 'success', + persist: true + }); + enqueueSnackbar('Please restart the server now.', { + variant: 'info', + persist: true + }); + }; - const setup = async (ev: React.FormEvent) => { - ev.preventDefault(); - if (!socket) - return enqueueSnackbar('Invalid state: socket not found', { variant: 'error' }); - const formData = new FormData(ev.currentTarget); - const password = formData.get('password'); - const data = { - port: parseInt(formData.get('port')?.toString() ?? '') ?? 3000, - password: password ? password.toString() : undefined - } as GUIConfig; - await messageAndResponse(socket, { name: 'setupServer', data }); - enqueueSnackbar(`The following settings have been set: Port=${data.port}, Password=${data.password ?? 'noPasswordRequired'}`, { - variant: 'success', - persist: true - }); - enqueueSnackbar('Please restart the server now.', { - variant: 'info', - persist: true - }); - }; + const randomEventHandler = React.useMemo(() => new RandomEventHandler(), []); - const randomEventHandler = React.useMemo(() => new RandomEventHandler(), []); + React.useEffect(() => { + (async () => { + if (!socket) return; + const currentService = await messageAndResponse(socket, { name: 'type', data: undefined }); + if (currentService.data !== undefined) return dispatch({ type: 'service', payload: currentService.data }); + if (store.service !== currentService.data) messageAndResponse(socket, { name: 'setup', data: store.service }); + })(); + }, [store.service, dispatch, socket]); - React.useEffect(() => { - (async () => { - if (!socket) - return; - const currentService = await messageAndResponse(socket, { name: 'type', data: undefined }); - if (currentService.data !== undefined) - return dispatch({ type: 'service', payload: currentService.data }); - if (store.service !== currentService.data) - messageAndResponse(socket, { name: 'setup', data: store.service }); - })(); - }, [store.service, dispatch, socket]); + React.useEffect(() => { + if (!socket) return; + /* finish is a placeholder */ + const listener = (initalData: MessageEvent) => { + const data = JSON.parse(initalData.data) as RandomEvent<'finish'>; + randomEventHandler.emit(data.name, data); + }; + socket.addEventListener('message', listener); + return () => { + socket.removeEventListener('message', listener); + }; + }, [socket]); - React.useEffect(() => { - if (!socket) - return; - /* finish is a placeholder */ - const listener = (initalData: MessageEvent) => { - const data = JSON.parse(initalData.data) as RandomEvent<'finish'>; - randomEventHandler.emit(data.name, data); - }; - socket.addEventListener('message', listener); - return () => { - socket.removeEventListener('message', listener); - }; - }, [ socket ]); + if (usePassword === 'waiting') return <>; - if (usePassword === 'waiting') - return <>; + if (socket === undefined) { + if (usePassword === 'no') { + connect(undefined); + return <>; + } + return ( + + + + + + Login + + + + + + You need to login in order to use this tool. + + + + ); + } - if (socket === undefined) { - if (usePassword === 'no') { - connect(undefined); - return <>; - } - return - - - - - Login - - - - - - You need to login in order to use this tool. - - - ; - } + if (isSetup === 'no') { + return ( + + + + + + Confirm + + + + + + + Please enter data that will be set to use this tool. +
+ Leave blank to use no password (NOT RECOMMENDED)! +
+
+
+ ); + } - if (isSetup === 'no') { - return - - - - - Confirm - - - - - - - Please enter data that will be set to use this tool. -
- Leave blank to use no password (NOT RECOMMENDED)! -
-
-
; - } + const messageHandler: FrontEndMessages = { + name: 'default', + auth: async (data) => (await messageAndResponse(socket, { name: 'auth', data })).data, + version: async () => (await messageAndResponse(socket, { name: 'version', data: undefined })).data, + checkToken: async () => (await messageAndResponse(socket, { name: 'checkToken', data: undefined })).data, + search: async (data) => (await messageAndResponse(socket, { name: 'search', data })).data, + handleDefault: async (data) => (await messageAndResponse(socket, { name: 'default', data })).data, + availableDubCodes: async () => (await messageAndResponse(socket, { name: 'availableDubCodes', data: undefined })).data, + availableSubCodes: async () => (await messageAndResponse(socket, { name: 'availableSubCodes', data: undefined })).data, + resolveItems: async (data) => (await messageAndResponse(socket, { name: 'resolveItems', data })).data, + listEpisodes: async (data) => (await messageAndResponse(socket, { name: 'listEpisodes', data })).data, + randomEvents: randomEventHandler, + downloadItem: (data) => messageAndResponse(socket, { name: 'downloadItem', data }), + isDownloading: async () => (await messageAndResponse(socket, { name: 'isDownloading', data: undefined })).data, + openFolder: async (data) => messageAndResponse(socket, { name: 'openFolder', data }), + logout: async () => (await messageAndResponse(socket, { name: 'changeProvider', data: undefined })).data, + openFile: async (data) => await messageAndResponse(socket, { name: 'openFile', data }), + openURL: async (data) => await messageAndResponse(socket, { name: 'openURL', data }), + getQueue: async () => (await messageAndResponse(socket, { name: 'getQueue', data: undefined })).data, + removeFromQueue: async (data) => await messageAndResponse(socket, { name: 'removeFromQueue', data }), + clearQueue: async () => await messageAndResponse(socket, { name: 'clearQueue', data: undefined }), + setDownloadQueue: async (data) => await messageAndResponse(socket, { name: 'setDownloadQueue', data }), + getDownloadQueue: async () => (await messageAndResponse(socket, { name: 'getDownloadQueue', data: undefined })).data + }; - const messageHandler: FrontEndMessages = { - name: 'default', - auth: async (data) => (await messageAndResponse(socket, { name: 'auth', data })).data, - version: async () => (await messageAndResponse(socket, { name: 'version', data: undefined })).data, - checkToken: async () => (await messageAndResponse(socket, { name: 'checkToken', data: undefined })).data, - search: async (data) => (await messageAndResponse(socket, { name: 'search', data })).data, - handleDefault: async (data) => (await messageAndResponse(socket, { name: 'default', data })).data, - availableDubCodes: async () => (await messageAndResponse(socket, { name: 'availableDubCodes', data: undefined})).data, - availableSubCodes: async () => (await messageAndResponse(socket, { name: 'availableSubCodes', data: undefined })).data, - resolveItems: async (data) => (await messageAndResponse(socket, { name: 'resolveItems', data })).data, - listEpisodes: async (data) => (await messageAndResponse(socket, { name: 'listEpisodes', data })).data, - randomEvents: randomEventHandler, - downloadItem: (data) => messageAndResponse(socket, { name: 'downloadItem', data }), - isDownloading: async () => (await messageAndResponse(socket, { name: 'isDownloading', data: undefined })).data, - openFolder: async (data) => messageAndResponse(socket, { name: 'openFolder', data }), - logout: async () => (await messageAndResponse(socket, { name: 'changeProvider', data: undefined })).data, - openFile: async (data) => await messageAndResponse(socket, { name: 'openFile', data }), - openURL: async (data) => await messageAndResponse(socket, { name: 'openURL', data }), - getQueue: async () => (await messageAndResponse(socket, { name: 'getQueue', data: undefined })).data, - removeFromQueue: async (data) => await messageAndResponse(socket, { name: 'removeFromQueue', data }), - clearQueue: async () => await messageAndResponse(socket, { name: 'clearQueue', data: undefined }), - setDownloadQueue: async (data) => await messageAndResponse(socket, { name: 'setDownloadQueue', data }), - getDownloadQueue: async () => (await messageAndResponse(socket, { name: 'getDownloadQueue', data: undefined })).data, - }; - - return - {children} - ; + return {children}; }; export default MessageChannelProvider; diff --git a/gui/react/src/provider/QueueProvider.tsx b/gui/react/src/provider/QueueProvider.tsx index 833ff17..a17c4f2 100644 --- a/gui/react/src/provider/QueueProvider.tsx +++ b/gui/react/src/provider/QueueProvider.tsx @@ -6,30 +6,28 @@ import { RandomEvent } from '../../../../@types/randomEvents'; export const queueContext = React.createContext([]); const QueueProvider: FCWithChildren = ({ children }) => { - const msg = React.useContext(messageChannelContext); + const msg = React.useContext(messageChannelContext); - const [ready, setReady] = React.useState(false); - const [queue, setQueue] = React.useState([]); + const [ready, setReady] = React.useState(false); + const [queue, setQueue] = React.useState([]); - React.useEffect(() => { - if (msg && !ready) { - msg.getQueue().then(data => { - setQueue(data); - setReady(true); - }); - } - const listener = (ev: RandomEvent<'queueChange'>) => { - setQueue(ev.data); - }; - msg?.randomEvents.on('queueChange', listener); - return () => { - msg?.randomEvents.removeListener('queueChange', listener); - }; - }, [ msg ]); - - return - {children} - ; + React.useEffect(() => { + if (msg && !ready) { + msg.getQueue().then((data) => { + setQueue(data); + setReady(true); + }); + } + const listener = (ev: RandomEvent<'queueChange'>) => { + setQueue(ev.data); + }; + msg?.randomEvents.on('queueChange', listener); + return () => { + msg?.randomEvents.removeListener('queueChange', listener); + }; + }, [msg]); + + return {children}; }; -export default QueueProvider; \ No newline at end of file +export default QueueProvider; diff --git a/gui/react/src/provider/ServiceProvider.tsx b/gui/react/src/provider/ServiceProvider.tsx index 16cc5ca..ff6da5e 100644 --- a/gui/react/src/provider/ServiceProvider.tsx +++ b/gui/react/src/provider/ServiceProvider.tsx @@ -1,35 +1,60 @@ import React from 'react'; -import {Divider, Box, Button, Typography, Avatar} from '@mui/material'; +import { Divider, Box, Button, Typography, Avatar } from '@mui/material'; import useStore from '../hooks/useStore'; import { StoreState } from './Store'; -type Services = 'crunchy'|'hidive'|'ao'|'adn'; +type Services = 'crunchy' | 'hidive' | 'ao' | 'adn'; -export const serviceContext = React.createContext(undefined); +export const serviceContext = React.createContext(undefined); const ServiceProvider: FCWithChildren = ({ children }) => { - const [ { service }, dispatch ] = useStore(); + const [{ service }, dispatch] = useStore(); - const setService = (s: StoreState['service']) => { - dispatch({ - type: 'service', - payload: s - }); - }; + const setService = (s: StoreState['service']) => { + dispatch({ + type: 'service', + payload: s + }); + }; - return service === undefined ? - - Please select your service - - - - - - - - : - {children} - ; + return service === undefined ? ( + + + Please select your service + + + + + + + + + ) : ( + {children} + ); }; -export default ServiceProvider; \ No newline at end of file +export default ServiceProvider; diff --git a/gui/react/src/provider/Store.tsx b/gui/react/src/provider/Store.tsx index 2e0f8f7..1f16a4d 100644 --- a/gui/react/src/provider/Store.tsx +++ b/gui/react/src/provider/Store.tsx @@ -3,64 +3,64 @@ import { Episode } from '../../../../@types/messageHandler'; import { dubLanguageCodes } from '../../../../modules/module.langsData'; export type DownloadOptions = { - q: number, - id: string, - e: string, - dubLang: typeof dubLanguageCodes, - dlsubs: string[], - fileName: string, - dlVideoOnce: boolean, - all: boolean, - but: boolean, - novids: boolean, - hslang?: string, - simul: boolean, - noaudio: boolean -} + q: number; + id: string; + e: string; + dubLang: typeof dubLanguageCodes; + dlsubs: string[]; + fileName: string; + dlVideoOnce: boolean; + all: boolean; + but: boolean; + novids: boolean; + hslang?: string; + simul: boolean; + noaudio: boolean; +}; export type StoreState = { - episodeListing: Episode[]; - downloadOptions: DownloadOptions, - service: 'crunchy'|'hidive'|'ao'|'adn'|undefined, - version: string, -} + episodeListing: Episode[]; + downloadOptions: DownloadOptions; + service: 'crunchy' | 'hidive' | 'ao' | 'adn' | undefined; + version: string; +}; -export type StoreAction = { - type: T, - payload: StoreState[T], - extraInfo?: Record -} +export type StoreAction = { + type: T; + payload: StoreState[T]; + extraInfo?: Record; +}; -const Reducer = (state: StoreState, action: StoreAction): StoreState => { - switch(action.type) { - default: - return { ...state, [action.type]: action.payload }; - } +const Reducer = (state: StoreState, action: StoreAction): StoreState => { + switch (action.type) { + default: + return { ...state, [action.type]: action.payload }; + } }; const initialState: StoreState = { - downloadOptions: { - id: '', - q: 0, - e: '', - dubLang: [ 'jpn' ], - dlsubs: [ 'all' ], - fileName: '', - dlVideoOnce: false, - all: false, - but: false, - noaudio: false, - novids: false, - simul: false - }, - service: undefined, - episodeListing: [], - version: '', + downloadOptions: { + id: '', + q: 0, + e: '', + dubLang: ['jpn'], + dlsubs: ['all'], + fileName: '', + dlVideoOnce: false, + all: false, + but: false, + noaudio: false, + novids: false, + simul: false + }, + service: undefined, + episodeListing: [], + version: '' }; -const Store: FCWithChildren = ({children}) => { - const [state, dispatch] = React.useReducer(Reducer, initialState); - /*React.useEffect(() => { +const Store: FCWithChildren = ({ children }) => { + const [state, dispatch] = React.useReducer(Reducer, initialState); + /*React.useEffect(() => { if (!state.unsavedChanges.has) return; const unsavedChanges = (ev: BeforeUnloadEvent, lang: LanguageContextType) => { @@ -79,13 +79,9 @@ const Store: FCWithChildren = ({children}) => { return () => window.removeEventListener('beforeunload', windowListener); }, [state.unsavedChanges.has]);*/ - return ( - - {children} - - ); + return {children}; }; /* Importent Notice -- The 'queue' generic will be overriden */ export const StoreContext = React.createContext<[StoreState, React.Dispatch>]>([initialState, undefined as any]); -export default Store; \ No newline at end of file +export default Store; diff --git a/gui/react/tsconfig.json b/gui/react/tsconfig.json index 5eecbe8..187a160 100644 --- a/gui/react/tsconfig.json +++ b/gui/react/tsconfig.json @@ -2,11 +2,7 @@ "compilerOptions": { "outDir": "./build", "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, @@ -22,8 +18,5 @@ "jsx": "react-jsx", "downlevelIteration": true }, - "include": [ - "./src", - "./webpack.config.ts" - ] -} \ No newline at end of file + "include": ["./src", "./webpack.config.ts"] +} diff --git a/gui/react/webpack.config.ts b/gui/react/webpack.config.ts index 0901205..83df4b8 100644 --- a/gui/react/webpack.config.ts +++ b/gui/react/webpack.config.ts @@ -4,55 +4,58 @@ import path from 'path'; import type { Configuration as DevServerConfig } from 'webpack-dev-server'; const config: Configuration & DevServerConfig = { - devServer: { - proxy: [ - { - target: 'http://localhost:3000', - context: ['/public', '/private'], - ws: true - } - ], - }, - entry: './src/index.tsx', - mode: 'production', - output: { - path: path.resolve(process.cwd(), './build'), - filename: 'index.js', - }, - target: 'web', - resolve: { - extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'], - }, - performance: false, - module: { - rules: [ - { - test: /\.(ts|tsx)$/, - exclude: /node_modules/, - use: { - 'loader': 'babel-loader', - options: { - presets: [ - '@babel/typescript', - '@babel/preset-react', - ['@babel/preset-env', { - targets: 'defaults' - }] - ] - } - }, - }, - { - test: /\.css$/i, - use: ['style-loader', 'css-loader'], - }, - ], - }, - plugins: [ - new HtmlWebpackPlugin({ - template: path.join(process.cwd(), 'public', 'index.html') - }) - ] + devServer: { + proxy: [ + { + target: 'http://localhost:3000', + context: ['/public', '/private'], + ws: true + } + ] + }, + entry: './src/index.tsx', + mode: 'production', + output: { + path: path.resolve(process.cwd(), './build'), + filename: 'index.js' + }, + target: 'web', + resolve: { + extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'] + }, + performance: false, + module: { + rules: [ + { + test: /\.(ts|tsx)$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + options: { + presets: [ + '@babel/typescript', + '@babel/preset-react', + [ + '@babel/preset-env', + { + targets: 'defaults' + } + ] + ] + } + } + }, + { + test: /\.css$/i, + use: ['style-loader', 'css-loader'] + } + ] + }, + plugins: [ + new HtmlWebpackPlugin({ + template: path.join(process.cwd(), 'public', 'index.html') + }) + ] }; export default config; diff --git a/gui/server/index.ts b/gui/server/index.ts index c1bb446..34e484d 100644 --- a/gui/server/index.ts +++ b/gui/server/index.ts @@ -25,10 +25,10 @@ app.use(express.static(path.join(workingDir, 'gui', 'server', 'build'), { maxAge console.info(`\n=== Multi Downloader NX GUI ${packageJson.version} ===\n`); const server = app.listen(cfg.gui.port, () => { - console.info(`GUI server started on port ${cfg.gui.port}`); + console.info(`GUI server started on port ${cfg.gui.port}`); }); new PublicWebSocket(server); new ServiceHandler(server); -open(`http://localhost:${cfg.gui.port}`); \ No newline at end of file +open(`http://localhost:${cfg.gui.port}`); diff --git a/gui/server/serviceHandler.ts b/gui/server/serviceHandler.ts index 17e997e..918c109 100644 --- a/gui/server/serviceHandler.ts +++ b/gui/server/serviceHandler.ts @@ -11,124 +11,113 @@ import WebSocketHandler from './websocket'; import packageJson from '../../package.json'; export default class ServiceHandler { + private service: MessageHandler | undefined = undefined; + private ws: WebSocketHandler; + private state: GuiState; - private service: MessageHandler|undefined = undefined; - private ws: WebSocketHandler; - private state: GuiState; + constructor(server: Server) { + this.ws = new WebSocketHandler(server); + this.handleMessages(); + this.state = getState(); + } - constructor(server: Server) { - this.ws = new WebSocketHandler(server); - this.handleMessages(); - this.state = getState(); - } + private handleMessages() { + this.ws.events.on('setupServer', ({ data }, respond) => { + writeYamlCfgFile('gui', data); + this.state.setup = true; + setState(this.state); + respond(true); + process.exit(0); + }); - private handleMessages() { - this.ws.events.on('setupServer', ({ data }, respond) => { - writeYamlCfgFile('gui', data); - this.state.setup = true; - setState(this.state); - respond(true); - process.exit(0); - }); + this.ws.events.on('setup', ({ data }) => { + if (data === 'crunchy') { + this.service = new CrunchyHandler(this.ws); + } else if (data === 'hidive') { + this.service = new HidiveHandler(this.ws); + } else if (data === 'ao') { + this.service = new AnimeOnegaiHandler(this.ws); + } else if (data === 'adn') { + this.service = new ADNHandler(this.ws); + } + }); - this.ws.events.on('setup', ({ data }) => { - if (data === 'crunchy') { - this.service = new CrunchyHandler(this.ws); - } else if (data === 'hidive') { - this.service = new HidiveHandler(this.ws); - } else if (data === 'ao') { - this.service = new AnimeOnegaiHandler(this.ws); - } else if (data === 'adn') { - this.service = new ADNHandler(this.ws); - } - }); - - this.ws.events.on('changeProvider', async (_, respond) => { - if (await this.service?.isDownloading()) - return respond(false); - this.service = undefined; - respond(true); - }); + this.ws.events.on('changeProvider', async (_, respond) => { + if (await this.service?.isDownloading()) return respond(false); + this.service = undefined; + respond(true); + }); - this.ws.events.on('auth', async ({ data }, respond) => { - if (this.service === undefined) - return respond({ isOk: false, reason: new Error('No service selected') }); - respond(await this.service.auth(data)); - }); - this.ws.events.on('version', async (_, respond) => { - respond(packageJson.version); - }); - this.ws.events.on('type', async (_, respond) => respond(this.service === undefined ? undefined : this.service.name as 'hidive'|'crunchy'|'ao'|'adn')); - this.ws.events.on('checkToken', async (_, respond) => { - if (this.service === undefined) - return respond({ isOk: false, reason: new Error('No service selected') }); - respond(await this.service.checkToken()); - }); - this.ws.events.on('search', async ({ data }, respond) => { - if (this.service === undefined) - return respond({ isOk: false, reason: new Error('No service selected') }); - respond(await this.service.search(data)); - }); - this.ws.events.on('default', async ({ data }, respond) => { - if (this.service === undefined) - return respond({ isOk: false, reason: new Error('No service selected') }); - respond(await this.service.handleDefault(data)); - }); - this.ws.events.on('availableDubCodes', async (_, respond) => { - if (this.service === undefined) - return respond([]); - respond(await this.service.availableDubCodes()); - }); - this.ws.events.on('availableSubCodes', async (_, respond) => { - if (this.service === undefined) - return respond([]); - respond(await this.service.availableSubCodes()); - }); - this.ws.events.on('resolveItems', async ({ data }, respond) => { - if (this.service === undefined) - return respond(false); - respond(await this.service.resolveItems(data)); - }); - this.ws.events.on('listEpisodes', async ({ data }, respond) => { - if (this.service === undefined) - return respond({ isOk: false, reason: new Error('No service selected') }); - respond(await this.service.listEpisodes(data)); - }); - this.ws.events.on('downloadItem', async ({ data }, respond) => { - this.service?.downloadItem(data); - respond(undefined); - }); - this.ws.events.on('openFolder', async ({ data }, respond) => { - this.service?.openFolder(data); - respond(undefined); - }); - this.ws.events.on('openFile', async ({ data }, respond) => { - this.service?.openFile(data); - respond(undefined); - }); - this.ws.events.on('openURL', async ({ data }, respond) => { - this.service?.openURL(data); - respond(undefined); - }); - this.ws.events.on('getQueue', async (_, respond) => { - respond(await this.service?.getQueue() ?? []); - }); - this.ws.events.on('removeFromQueue', async ({ data }, respond) => { - this.service?.removeFromQueue(data); - respond(undefined); - }); - this.ws.events.on('clearQueue', async (_, respond) => { - this.service?.clearQueue(); - respond(undefined); - }); - this.ws.events.on('setDownloadQueue', async ({ data }, respond) => { - this.service?.setDownloadQueue(data); - respond(undefined); - }); - this.ws.events.on('getDownloadQueue', async (_, respond) => { - respond(await this.service?.getDownloadQueue() ?? false); - }); - this.ws.events.on('isDownloading', async (_, respond) => respond(await this.service?.isDownloading() ?? false)); - } - -} \ No newline at end of file + this.ws.events.on('auth', async ({ data }, respond) => { + if (this.service === undefined) return respond({ isOk: false, reason: new Error('No service selected') }); + respond(await this.service.auth(data)); + }); + this.ws.events.on('version', async (_, respond) => { + respond(packageJson.version); + }); + this.ws.events.on('type', async (_, respond) => respond(this.service === undefined ? undefined : (this.service.name as 'hidive' | 'crunchy' | 'ao' | 'adn'))); + this.ws.events.on('checkToken', async (_, respond) => { + if (this.service === undefined) return respond({ isOk: false, reason: new Error('No service selected') }); + respond(await this.service.checkToken()); + }); + this.ws.events.on('search', async ({ data }, respond) => { + if (this.service === undefined) return respond({ isOk: false, reason: new Error('No service selected') }); + respond(await this.service.search(data)); + }); + this.ws.events.on('default', async ({ data }, respond) => { + if (this.service === undefined) return respond({ isOk: false, reason: new Error('No service selected') }); + respond(await this.service.handleDefault(data)); + }); + this.ws.events.on('availableDubCodes', async (_, respond) => { + if (this.service === undefined) return respond([]); + respond(await this.service.availableDubCodes()); + }); + this.ws.events.on('availableSubCodes', async (_, respond) => { + if (this.service === undefined) return respond([]); + respond(await this.service.availableSubCodes()); + }); + this.ws.events.on('resolveItems', async ({ data }, respond) => { + if (this.service === undefined) return respond(false); + respond(await this.service.resolveItems(data)); + }); + this.ws.events.on('listEpisodes', async ({ data }, respond) => { + if (this.service === undefined) return respond({ isOk: false, reason: new Error('No service selected') }); + respond(await this.service.listEpisodes(data)); + }); + this.ws.events.on('downloadItem', async ({ data }, respond) => { + this.service?.downloadItem(data); + respond(undefined); + }); + this.ws.events.on('openFolder', async ({ data }, respond) => { + this.service?.openFolder(data); + respond(undefined); + }); + this.ws.events.on('openFile', async ({ data }, respond) => { + this.service?.openFile(data); + respond(undefined); + }); + this.ws.events.on('openURL', async ({ data }, respond) => { + this.service?.openURL(data); + respond(undefined); + }); + this.ws.events.on('getQueue', async (_, respond) => { + respond((await this.service?.getQueue()) ?? []); + }); + this.ws.events.on('removeFromQueue', async ({ data }, respond) => { + this.service?.removeFromQueue(data); + respond(undefined); + }); + this.ws.events.on('clearQueue', async (_, respond) => { + this.service?.clearQueue(); + respond(undefined); + }); + this.ws.events.on('setDownloadQueue', async ({ data }, respond) => { + this.service?.setDownloadQueue(data); + respond(undefined); + }); + this.ws.events.on('getDownloadQueue', async (_, respond) => { + respond((await this.service?.getDownloadQueue()) ?? false); + }); + this.ws.events.on('isDownloading', async (_, respond) => respond((await this.service?.isDownloading()) ?? false)); + } +} diff --git a/gui/server/services/adn.ts b/gui/server/services/adn.ts index e9dee9a..04441e3 100644 --- a/gui/server/services/adn.ts +++ b/gui/server/services/adn.ts @@ -8,132 +8,144 @@ import { console } from '../../../modules/log'; import * as yargs from '../../../modules/module.app-args'; class ADNHandler extends Base implements MessageHandler { - private adn: AnimationDigitalNetwork; - public name = 'adn'; - constructor(ws: WebSocketHandler) { - super(ws); - this.adn = new AnimationDigitalNetwork(); - this.initState(); - this.getDefaults(); - } + private adn: AnimationDigitalNetwork; + public name = 'adn'; + constructor(ws: WebSocketHandler) { + super(ws); + this.adn = new AnimationDigitalNetwork(); + this.initState(); + this.getDefaults(); + } - public getDefaults() { - const _default = yargs.appArgv(this.adn.cfg.cli, true); - if (['fr', 'de'].includes(_default.locale)) - this.adn.locale = _default.locale; - } + public getDefaults() { + const _default = yargs.appArgv(this.adn.cfg.cli, true); + if (['fr', 'de'].includes(_default.locale)) this.adn.locale = _default.locale; + } - public async auth(data: AuthData) { - return this.adn.doAuth(data); - } + public async auth(data: AuthData) { + return this.adn.doAuth(data); + } - public async checkToken(): Promise { - //TODO: implement proper method to check token - return { isOk: true, value: undefined }; - } + public async checkToken(): Promise { + //TODO: implement proper method to check token + return { isOk: true, value: undefined }; + } - public async search(data: SearchData): Promise { - console.debug(`Got search options: ${JSON.stringify(data)}`); - const search = await this.adn.doSearch(data); - if (!search.isOk) { - return search; - } - return { isOk: true, value: search.value }; - } + public async search(data: SearchData): Promise { + console.debug(`Got search options: ${JSON.stringify(data)}`); + const search = await this.adn.doSearch(data); + if (!search.isOk) { + return search; + } + return { isOk: true, value: search.value }; + } - public async handleDefault(name: string) { - return getDefault(name, this.adn.cfg.cli); - } + public async handleDefault(name: string) { + return getDefault(name, this.adn.cfg.cli); + } - public async availableDubCodes(): Promise { - const dubLanguageCodesArray: string[] = []; - for(const language of languages){ - if (language.adn_locale) - dubLanguageCodesArray.push(language.code); - } - return [...new Set(dubLanguageCodesArray)]; - } + public async availableDubCodes(): Promise { + const dubLanguageCodesArray: string[] = []; + for (const language of languages) { + if (language.adn_locale) dubLanguageCodesArray.push(language.code); + } + return [...new Set(dubLanguageCodesArray)]; + } - public async availableSubCodes(): Promise { - const subLanguageCodesArray: string[] = []; - for(const language of languages){ - if (language.adn_locale) - subLanguageCodesArray.push(language.locale); - } - return ['all', 'none', ...new Set(subLanguageCodesArray)]; - } + public async availableSubCodes(): Promise { + const subLanguageCodesArray: string[] = []; + for (const language of languages) { + if (language.adn_locale) subLanguageCodesArray.push(language.locale); + } + return ['all', 'none', ...new Set(subLanguageCodesArray)]; + } - public async resolveItems(data: ResolveItemsData): Promise { - const parse = parseInt(data.id); - if (isNaN(parse) || parse <= 0) - return false; - console.debug(`Got resolve options: ${JSON.stringify(data)}`); - const res = await this.adn.selectShow(parseInt(data.id), data.e, data.but, data.all); - if (!res.isOk || !res.value) - return res.isOk; - this.addToQueue(res.value.map(a => { - return { - ...data, - ids: [a.id], - title: a.title, - parent: { - title: a.show.shortTitle, - season: a.season - }, - e: a.shortNumber, - image: a.image, - episode: a.shortNumber - }; - })); - return true; - } + public async resolveItems(data: ResolveItemsData): Promise { + const parse = parseInt(data.id); + if (isNaN(parse) || parse <= 0) return false; + console.debug(`Got resolve options: ${JSON.stringify(data)}`); + const res = await this.adn.selectShow(parseInt(data.id), data.e, data.but, data.all); + if (!res.isOk || !res.value) return res.isOk; + this.addToQueue( + res.value.map((a) => { + return { + ...data, + ids: [a.id], + title: a.title, + parent: { + title: a.show.shortTitle, + season: a.season + }, + e: a.shortNumber, + image: a.image, + episode: a.shortNumber + }; + }) + ); + return true; + } - public async listEpisodes(id: string): Promise { - const parse = parseInt(id); - if (isNaN(parse) || parse <= 0) - return { isOk: false, reason: new Error('The ID is invalid') }; + public async listEpisodes(id: string): Promise { + const parse = parseInt(id); + if (isNaN(parse) || parse <= 0) return { isOk: false, reason: new Error('The ID is invalid') }; - const request = await this.adn.listShow(parse); - if (!request.isOk || !request.value) - return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')}; + const request = await this.adn.listShow(parse); + if (!request.isOk || !request.value) return { isOk: false, reason: new Error('Unknown upstream error, check for additional logs') }; - return { isOk: true, value: request.value.videos.map(function(item) { - return { - e: item.shortNumber, - lang: [], - name: item.title, - season: item.season, - seasonTitle: item.show.title, - episode: item.shortNumber, - id: item.id+'', - img: item.image, - description: item.summary, - time: item.duration+'' - }; - })}; - } + return { + isOk: true, + value: request.value.videos.map(function (item) { + return { + e: item.shortNumber, + lang: [], + name: item.title, + season: item.season, + seasonTitle: item.show.title, + episode: item.shortNumber, + id: item.id + '', + img: item.image, + description: item.summary, + time: item.duration + '' + }; + }) + }; + } - public async downloadItem(data: DownloadData) { - this.setDownloading(true); - console.debug(`Got download options: ${JSON.stringify(data)}`); - const _default = yargs.appArgv(this.adn.cfg.cli, true); - const res = await this.adn.selectShow(parseInt(data.id), data.e, false, false); - if (res.isOk) { - for (const select of res.value) { - if (!(await this.adn.getEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y', - novids: data.novids, noaudio: data.noaudio, hslang: data.hslang || 'none', dubLang: data.dubLang }))) { - const er = new Error(`Unable to download episode ${data.e} from ${data.id}`); - er.name = 'Download error'; - this.alertError(er); - } - } - } else { - this.alertError(new Error('Failed to download episode, check for additional logs.')); - } - this.sendMessage({ name: 'finish', data: undefined }); - this.setDownloading(false); - this.onFinish(); - } + public async downloadItem(data: DownloadData) { + this.setDownloading(true); + console.debug(`Got download options: ${JSON.stringify(data)}`); + const _default = yargs.appArgv(this.adn.cfg.cli, true); + const res = await this.adn.selectShow(parseInt(data.id), data.e, false, false); + if (res.isOk) { + for (const select of res.value) { + if ( + !(await this.adn.getEpisode(select, { + ..._default, + skipsubs: false, + callbackMaker: this.makeProgressHandler.bind(this), + q: data.q, + fileName: data.fileName, + dlsubs: data.dlsubs, + dlVideoOnce: data.dlVideoOnce, + force: 'y', + novids: data.novids, + noaudio: data.noaudio, + hslang: data.hslang || 'none', + dubLang: data.dubLang + })) + ) { + const er = new Error(`Unable to download episode ${data.e} from ${data.id}`); + er.name = 'Download error'; + this.alertError(er); + } + } + } else { + this.alertError(new Error('Failed to download episode, check for additional logs.')); + } + this.sendMessage({ name: 'finish', data: undefined }); + this.setDownloading(false); + this.onFinish(); + } } -export default ADNHandler; \ No newline at end of file +export default ADNHandler; diff --git a/gui/server/services/animeonegai.ts b/gui/server/services/animeonegai.ts index cfe7ccb..58ccd15 100644 --- a/gui/server/services/animeonegai.ts +++ b/gui/server/services/animeonegai.ts @@ -1,4 +1,14 @@ -import { AuthData, CheckTokenResponse, DownloadData, Episode, EpisodeListResponse, MessageHandler, ResolveItemsData, SearchData, SearchResponse } from '../../../@types/messageHandler'; +import { + AuthData, + CheckTokenResponse, + DownloadData, + Episode, + EpisodeListResponse, + MessageHandler, + ResolveItemsData, + SearchData, + SearchResponse +} from '../../../@types/messageHandler'; import AnimeOnegai from '../../../ao'; import { getDefault } from '../../../modules/module.args'; import { languages } from '../../../modules/module.langsData'; @@ -8,144 +18,153 @@ import { console } from '../../../modules/log'; import * as yargs from '../../../modules/module.app-args'; class AnimeOnegaiHandler extends Base implements MessageHandler { - private ao: AnimeOnegai; - public name = 'ao'; - constructor(ws: WebSocketHandler) { - super(ws); - this.ao = new AnimeOnegai(); - this.initState(); - this.getDefaults(); - } + private ao: AnimeOnegai; + public name = 'ao'; + constructor(ws: WebSocketHandler) { + super(ws); + this.ao = new AnimeOnegai(); + this.initState(); + this.getDefaults(); + } - public getDefaults() { - const _default = yargs.appArgv(this.ao.cfg.cli, true); - if (['es', 'pt'].includes(_default.locale)) - this.ao.locale = _default.locale; - } + public getDefaults() { + const _default = yargs.appArgv(this.ao.cfg.cli, true); + if (['es', 'pt'].includes(_default.locale)) this.ao.locale = _default.locale; + } - public async auth(data: AuthData) { - return this.ao.doAuth(data); - } + public async auth(data: AuthData) { + return this.ao.doAuth(data); + } - public async checkToken(): Promise { - //TODO: implement proper method to check token - return { isOk: true, value: undefined }; - } + public async checkToken(): Promise { + //TODO: implement proper method to check token + return { isOk: true, value: undefined }; + } - public async search(data: SearchData): Promise { - console.debug(`Got search options: ${JSON.stringify(data)}`); - const search = await this.ao.doSearch(data); - if (!search.isOk) { - return search; - } - return { isOk: true, value: search.value }; - } + public async search(data: SearchData): Promise { + console.debug(`Got search options: ${JSON.stringify(data)}`); + const search = await this.ao.doSearch(data); + if (!search.isOk) { + return search; + } + return { isOk: true, value: search.value }; + } - public async handleDefault(name: string) { - return getDefault(name, this.ao.cfg.cli); - } + public async handleDefault(name: string) { + return getDefault(name, this.ao.cfg.cli); + } - public async availableDubCodes(): Promise { - const dubLanguageCodesArray: string[] = []; - for(const language of languages){ - if (language.ao_locale) - dubLanguageCodesArray.push(language.code); - } - return [...new Set(dubLanguageCodesArray)]; - } + public async availableDubCodes(): Promise { + const dubLanguageCodesArray: string[] = []; + for (const language of languages) { + if (language.ao_locale) dubLanguageCodesArray.push(language.code); + } + return [...new Set(dubLanguageCodesArray)]; + } - public async availableSubCodes(): Promise { - const subLanguageCodesArray: string[] = []; - for(const language of languages){ - if (language.ao_locale) - subLanguageCodesArray.push(language.locale); - } - return ['all', 'none', ...new Set(subLanguageCodesArray)]; - } + public async availableSubCodes(): Promise { + const subLanguageCodesArray: string[] = []; + for (const language of languages) { + if (language.ao_locale) subLanguageCodesArray.push(language.locale); + } + return ['all', 'none', ...new Set(subLanguageCodesArray)]; + } - public async resolveItems(data: ResolveItemsData): Promise { - const parse = parseInt(data.id); - if (isNaN(parse) || parse <= 0) - return false; - console.debug(`Got resolve options: ${JSON.stringify(data)}`); - const _default = yargs.appArgv(this.ao.cfg.cli, true); - const res = await this.ao.selectShow(parseInt(data.id), data.e, data.but, data.all, _default); - if (!res.isOk || !res.value) - return res.isOk; - this.addToQueue(res.value.map(a => { - return { - ...data, - ids: a.data.map(a => a.videoId), - title: a.episodeTitle, - parent: { - title: a.seasonTitle, - season: a.seasonTitle - }, - e: a.episodeNumber+'', - image: a.image, - episode: a.episodeNumber+'' - }; - })); - return true; - } + public async resolveItems(data: ResolveItemsData): Promise { + const parse = parseInt(data.id); + if (isNaN(parse) || parse <= 0) return false; + console.debug(`Got resolve options: ${JSON.stringify(data)}`); + const _default = yargs.appArgv(this.ao.cfg.cli, true); + const res = await this.ao.selectShow(parseInt(data.id), data.e, data.but, data.all, _default); + if (!res.isOk || !res.value) return res.isOk; + this.addToQueue( + res.value.map((a) => { + return { + ...data, + ids: a.data.map((a) => a.videoId), + title: a.episodeTitle, + parent: { + title: a.seasonTitle, + season: a.seasonTitle + }, + e: a.episodeNumber + '', + image: a.image, + episode: a.episodeNumber + '' + }; + }) + ); + return true; + } - public async listEpisodes(id: string): Promise { - const parse = parseInt(id); - if (isNaN(parse) || parse <= 0) - return { isOk: false, reason: new Error('The ID is invalid') }; + public async listEpisodes(id: string): Promise { + const parse = parseInt(id); + if (isNaN(parse) || parse <= 0) return { isOk: false, reason: new Error('The ID is invalid') }; - const request = await this.ao.listShow(parse); - if (!request.isOk || !request.value) - return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')}; + const request = await this.ao.listShow(parse); + if (!request.isOk || !request.value) return { isOk: false, reason: new Error('Unknown upstream error, check for additional logs') }; - const episodes: Episode[] = []; - const seasonNumberTitleParse = request.series.data.title.match(/\d+$/); - const seasonNumber = seasonNumberTitleParse ? parseInt(seasonNumberTitleParse[0]) : 1; - //request.value - for (const episodeKey in request.value) { - const episode = request.value[episodeKey][0]; - const langs = Array.from(new Set(request.value[episodeKey].map(a=>a.lang))); - episodes.push({ - e: episode.number+'', - lang: langs as string[], - name: episode.name, - season: seasonNumber+'', - seasonTitle: '', - episode: episode.number+'', - id: episode.video_entry+'', - img: episode.thumbnail, - description: episode.description, - time: '' - }); - } - return { isOk: true, value: episodes }; - } + const episodes: Episode[] = []; + const seasonNumberTitleParse = request.series.data.title.match(/\d+$/); + const seasonNumber = seasonNumberTitleParse ? parseInt(seasonNumberTitleParse[0]) : 1; + //request.value + for (const episodeKey in request.value) { + const episode = request.value[episodeKey][0]; + const langs = Array.from(new Set(request.value[episodeKey].map((a) => a.lang))); + episodes.push({ + e: episode.number + '', + lang: langs as string[], + name: episode.name, + season: seasonNumber + '', + seasonTitle: '', + episode: episode.number + '', + id: episode.video_entry + '', + img: episode.thumbnail, + description: episode.description, + time: '' + }); + } + return { isOk: true, value: episodes }; + } - public async downloadItem(data: DownloadData) { - this.setDownloading(true); - console.debug(`Got download options: ${JSON.stringify(data)}`); - const _default = yargs.appArgv(this.ao.cfg.cli, true); - const res = await this.ao.selectShow(parseInt(data.id), data.e, false, false, { - ..._default, - dubLang: data.dubLang, - e: data.e - }); - if (res.isOk) { - for (const select of res.value) { - if (!(await this.ao.downloadEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y', - novids: data.novids, noaudio: data.noaudio, hslang: data.hslang || 'none', dubLang: data.dubLang }))) { - const er = new Error(`Unable to download episode ${data.e} from ${data.id}`); - er.name = 'Download error'; - this.alertError(er); - } - } - } else { - this.alertError(new Error('Failed to download episode, check for additional logs.')); - } - this.sendMessage({ name: 'finish', data: undefined }); - this.setDownloading(false); - this.onFinish(); - } + public async downloadItem(data: DownloadData) { + this.setDownloading(true); + console.debug(`Got download options: ${JSON.stringify(data)}`); + const _default = yargs.appArgv(this.ao.cfg.cli, true); + const res = await this.ao.selectShow(parseInt(data.id), data.e, false, false, { + ..._default, + dubLang: data.dubLang, + e: data.e + }); + if (res.isOk) { + for (const select of res.value) { + if ( + !(await this.ao.downloadEpisode(select, { + ..._default, + skipsubs: false, + callbackMaker: this.makeProgressHandler.bind(this), + q: data.q, + fileName: data.fileName, + dlsubs: data.dlsubs, + dlVideoOnce: data.dlVideoOnce, + force: 'y', + novids: data.novids, + noaudio: data.noaudio, + hslang: data.hslang || 'none', + dubLang: data.dubLang + })) + ) { + const er = new Error(`Unable to download episode ${data.e} from ${data.id}`); + er.name = 'Download error'; + this.alertError(er); + } + } + } else { + this.alertError(new Error('Failed to download episode, check for additional logs.')); + } + this.sendMessage({ name: 'finish', data: undefined }); + this.setDownloading(false); + this.onFinish(); + } } -export default AnimeOnegaiHandler; \ No newline at end of file +export default AnimeOnegaiHandler; diff --git a/gui/server/services/base.ts b/gui/server/services/base.ts index 93a4bd9..5f998d0 100644 --- a/gui/server/services/base.ts +++ b/gui/server/services/base.ts @@ -9,140 +9,140 @@ import { getState, setState } from '../../../modules/module.cfg-loader'; import packageJson from '../../../package.json'; export default class Base { - private state: GuiState; - public name = 'default'; - constructor(private ws: WebSocketHandler) { - this.state = getState(); - } + private state: GuiState; + public name = 'default'; + constructor(private ws: WebSocketHandler) { + this.state = getState(); + } - private downloading = false; + private downloading = false; - private queue: QueueItem[] = []; - private workOnQueue = false; + private queue: QueueItem[] = []; + private workOnQueue = false; - version(): Promise { - return new Promise(() => { - return packageJson.version; - }); - } + version(): Promise { + return new Promise(() => { + return packageJson.version; + }); + } - initState() { - if (this.state.services[this.name]) { - this.queue = this.state.services[this.name].queue; - this.queueChange(); - } else { - this.state.services[this.name] = { - 'queue': [] - }; - } - } + initState() { + if (this.state.services[this.name]) { + this.queue = this.state.services[this.name].queue; + this.queueChange(); + } else { + this.state.services[this.name] = { + queue: [] + }; + } + } - setDownloading(downloading: boolean) { - this.downloading = downloading; - } + setDownloading(downloading: boolean) { + this.downloading = downloading; + } - getDownloading() { - return this.downloading; - } + getDownloading() { + return this.downloading; + } - alertError(error: Error) { - console.error(`${error}`); - } + alertError(error: Error) { + console.error(`${error}`); + } - makeProgressHandler(videoInfo: DownloadInfo) { - return ((data: ProgressData) => { - this.sendMessage({ - name: 'progress', - data: { - downloadInfo: videoInfo, - progress: data - } - }); - }); - } + makeProgressHandler(videoInfo: DownloadInfo) { + return (data: ProgressData) => { + this.sendMessage({ + name: 'progress', + data: { + downloadInfo: videoInfo, + progress: data + } + }); + }; + } - sendMessage(data: RandomEvent) { - this.ws.sendMessage(data); - } + sendMessage(data: RandomEvent) { + this.ws.sendMessage(data); + } - async isDownloading() { - return this.downloading; - } + async isDownloading() { + return this.downloading; + } - async openFolder(folderType: FolderTypes) { - switch (folderType) { - case 'content': - open(cfg.dir.content); - break; - case 'config': - open(cfg.dir.config); - break; - } - } + async openFolder(folderType: FolderTypes) { + switch (folderType) { + case 'content': + open(cfg.dir.content); + break; + case 'config': + open(cfg.dir.config); + break; + } + } - async openFile(data: [FolderTypes, string]) { - switch (data[0]) { - case 'config': - open(path.join(cfg.dir.config, data[1])); - break; - case 'content': - throw new Error('No subfolders'); - } - } + async openFile(data: [FolderTypes, string]) { + switch (data[0]) { + case 'config': + open(path.join(cfg.dir.config, data[1])); + break; + case 'content': + throw new Error('No subfolders'); + } + } - async openURL(data: string) { - open(data); - } + async openURL(data: string) { + open(data); + } - public async getQueue(): Promise { - return this.queue; - } + public async getQueue(): Promise { + return this.queue; + } - public async removeFromQueue(index: number) { - this.queue.splice(index, 1); - this.queueChange(); - } + public async removeFromQueue(index: number) { + this.queue.splice(index, 1); + this.queueChange(); + } - public async clearQueue() { - this.queue = []; - this.queueChange(); - } + public async clearQueue() { + this.queue = []; + this.queueChange(); + } - public addToQueue(data: QueueItem[]) { - this.queue = this.queue.concat(...data); - this.queueChange(); - } + public addToQueue(data: QueueItem[]) { + this.queue = this.queue.concat(...data); + this.queueChange(); + } - public setDownloadQueue(data: boolean) { - this.workOnQueue = data; - this.queueChange(); - } + public setDownloadQueue(data: boolean) { + this.workOnQueue = data; + this.queueChange(); + } - public async getDownloadQueue(): Promise { - return this.workOnQueue; - } + public async getDownloadQueue(): Promise { + return this.workOnQueue; + } - private async queueChange() { - this.sendMessage({ name: 'queueChange', data: this.queue }); - if (this.workOnQueue && this.queue.length > 0 && !await this.isDownloading()) { - this.setDownloading(true); - this.sendMessage({ name: 'current', data: this.queue[0] }); - this.downloadItem(this.queue[0]); - this.queue = this.queue.slice(1); - this.queueChange(); - } - this.state.services[this.name].queue = this.queue; - setState(this.state); - } + private async queueChange() { + this.sendMessage({ name: 'queueChange', data: this.queue }); + if (this.workOnQueue && this.queue.length > 0 && !(await this.isDownloading())) { + this.setDownloading(true); + this.sendMessage({ name: 'current', data: this.queue[0] }); + this.downloadItem(this.queue[0]); + this.queue = this.queue.slice(1); + this.queueChange(); + } + this.state.services[this.name].queue = this.queue; + setState(this.state); + } - public async onFinish() { - this.sendMessage({ name: 'current', data: undefined }); - this.queueChange(); - } + public async onFinish() { + this.sendMessage({ name: 'current', data: undefined }); + this.queueChange(); + } - //Overriten - // eslint-disable-next-line - public async downloadItem(_: QueueItem) { - throw new Error('downloadItem not overriden'); - } -} \ No newline at end of file + //Overriten + // eslint-disable-next-line + public async downloadItem(_: QueueItem) { + throw new Error('downloadItem not overriden'); + } +} diff --git a/gui/server/services/crunchyroll.ts b/gui/server/services/crunchyroll.ts index 3535a3c..5ffcb2f 100644 --- a/gui/server/services/crunchyroll.ts +++ b/gui/server/services/crunchyroll.ts @@ -8,120 +8,133 @@ import { console } from '../../../modules/log'; import * as yargs from '../../../modules/module.app-args'; class CrunchyHandler extends Base implements MessageHandler { - private crunchy: Crunchy; - public name = 'crunchy'; - constructor(ws: WebSocketHandler) { - super(ws); - this.crunchy = new Crunchy(); - this.crunchy.refreshToken(); - this.initState(); - this.getDefaults(); - } + private crunchy: Crunchy; + public name = 'crunchy'; + constructor(ws: WebSocketHandler) { + super(ws); + this.crunchy = new Crunchy(); + this.crunchy.refreshToken(); + this.initState(); + this.getDefaults(); + } - public getDefaults() { - const _default = yargs.appArgv(this.crunchy.cfg.cli, true); - this.crunchy.locale = _default.locale; - } - - public async listEpisodes (id: string): Promise { - this.getDefaults(); - await this.crunchy.refreshToken(true); - return { isOk: true, value: (await this.crunchy.listSeriesID(id)).list }; - } - - public async handleDefault(name: string) { - return getDefault(name, this.crunchy.cfg.cli); - } + public getDefaults() { + const _default = yargs.appArgv(this.crunchy.cfg.cli, true); + this.crunchy.locale = _default.locale; + } - public async availableDubCodes(): Promise { - const dubLanguageCodesArray: string[] = []; - for(const language of languages){ - if (language.cr_locale) - dubLanguageCodesArray.push(language.code); - } - return [...new Set(dubLanguageCodesArray)]; - } + public async listEpisodes(id: string): Promise { + this.getDefaults(); + await this.crunchy.refreshToken(true); + return { isOk: true, value: (await this.crunchy.listSeriesID(id)).list }; + } - public async availableSubCodes(): Promise { - return subtitleLanguagesFilter; - } + public async handleDefault(name: string) { + return getDefault(name, this.crunchy.cfg.cli); + } - public async resolveItems(data: ResolveItemsData): Promise { - this.getDefaults(); - await this.crunchy.refreshToken(true); - console.debug(`Got resolve options: ${JSON.stringify(data)}`); - const res = await this.crunchy.downloadFromSeriesID(data.id, data); - if (!res.isOk) - return res.isOk; - this.addToQueue(res.value.map(a => { - return { - ...data, - - ids: a.data.map(a => a.mediaId), - title: a.episodeTitle, - parent: { - title: a.seasonTitle, - season: a.season.toString() - }, - e: a.e, - image: a.image, - episode: a.episodeNumber - }; - })); - return true; - } + public async availableDubCodes(): Promise { + const dubLanguageCodesArray: string[] = []; + for (const language of languages) { + if (language.cr_locale) dubLanguageCodesArray.push(language.code); + } + return [...new Set(dubLanguageCodesArray)]; + } - public async search(data: SearchData): Promise { - this.getDefaults(); - await this.crunchy.refreshToken(true); - if (!data['search-type']) data['search-type'] = 'series'; - console.debug(`Got search options: ${JSON.stringify(data)}`); - const crunchySearch = await this.crunchy.doSearch(data); - if (!crunchySearch.isOk) { - this.crunchy.refreshToken(); - return crunchySearch; - } - return { isOk: true, value: crunchySearch.value }; - } + public async availableSubCodes(): Promise { + return subtitleLanguagesFilter; + } - public async checkToken(): Promise { - if (await this.crunchy.getProfile()) { - return { isOk: true, value: undefined }; - } else { - return { isOk: false, reason: new Error('') }; - } - } + public async resolveItems(data: ResolveItemsData): Promise { + this.getDefaults(); + await this.crunchy.refreshToken(true); + console.debug(`Got resolve options: ${JSON.stringify(data)}`); + const res = await this.crunchy.downloadFromSeriesID(data.id, data); + if (!res.isOk) return res.isOk; + this.addToQueue( + res.value.map((a) => { + return { + ...data, - public auth(data: AuthData) { - return this.crunchy.doAuth(data); - } + ids: a.data.map((a) => a.mediaId), + title: a.episodeTitle, + parent: { + title: a.seasonTitle, + season: a.season.toString() + }, + e: a.e, + image: a.image, + episode: a.episodeNumber + }; + }) + ); + return true; + } - public async downloadItem(data: DownloadData) { - this.getDefaults(); - await this.crunchy.refreshToken(true); - console.debug(`Got download options: ${JSON.stringify(data)}`); - this.setDownloading(true); - const _default = yargs.appArgv(this.crunchy.cfg.cli, true); - const res = await this.crunchy.downloadFromSeriesID(data.id, { - dubLang: data.dubLang, - e: data.e - }); - if (res.isOk) { - for (const select of res.value) { - if (!(await this.crunchy.downloadEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y', - novids: data.novids, noaudio: data.noaudio, hslang: data.hslang || 'none' }))) { - const er = new Error(`Unable to download episode ${data.e} from ${data.id}`); - er.name = 'Download error'; - this.alertError(er); - } - } - } else { - this.alertError(res.reason); - } - this.sendMessage({ name: 'finish', data: undefined }); - this.setDownloading(false); - this.onFinish(); - } + public async search(data: SearchData): Promise { + this.getDefaults(); + await this.crunchy.refreshToken(true); + if (!data['search-type']) data['search-type'] = 'series'; + console.debug(`Got search options: ${JSON.stringify(data)}`); + const crunchySearch = await this.crunchy.doSearch(data); + if (!crunchySearch.isOk) { + this.crunchy.refreshToken(); + return crunchySearch; + } + return { isOk: true, value: crunchySearch.value }; + } + + public async checkToken(): Promise { + if (await this.crunchy.getProfile()) { + return { isOk: true, value: undefined }; + } else { + return { isOk: false, reason: new Error('') }; + } + } + + public auth(data: AuthData) { + return this.crunchy.doAuth(data); + } + + public async downloadItem(data: DownloadData) { + this.getDefaults(); + await this.crunchy.refreshToken(true); + console.debug(`Got download options: ${JSON.stringify(data)}`); + this.setDownloading(true); + const _default = yargs.appArgv(this.crunchy.cfg.cli, true); + const res = await this.crunchy.downloadFromSeriesID(data.id, { + dubLang: data.dubLang, + e: data.e + }); + if (res.isOk) { + for (const select of res.value) { + if ( + !(await this.crunchy.downloadEpisode(select, { + ..._default, + skipsubs: false, + callbackMaker: this.makeProgressHandler.bind(this), + q: data.q, + fileName: data.fileName, + dlsubs: data.dlsubs, + dlVideoOnce: data.dlVideoOnce, + force: 'y', + novids: data.novids, + noaudio: data.noaudio, + hslang: data.hslang || 'none' + })) + ) { + const er = new Error(`Unable to download episode ${data.e} from ${data.id}`); + er.name = 'Download error'; + this.alertError(er); + } + } + } else { + this.alertError(res.reason); + } + this.sendMessage({ name: 'finish', data: undefined }); + this.setDownloading(false); + this.onFinish(); + } } -export default CrunchyHandler; \ No newline at end of file +export default CrunchyHandler; diff --git a/gui/server/services/hidive.ts b/gui/server/services/hidive.ts index 5fc9c5f..469b3ae 100644 --- a/gui/server/services/hidive.ts +++ b/gui/server/services/hidive.ts @@ -8,120 +8,128 @@ import { console } from '../../../modules/log'; import * as yargs from '../../../modules/module.app-args'; class HidiveHandler extends Base implements MessageHandler { - private hidive: Hidive; - public name = 'hidive'; - constructor(ws: WebSocketHandler) { - super(ws); - this.hidive = new Hidive(); - this.initState(); - } + private hidive: Hidive; + public name = 'hidive'; + constructor(ws: WebSocketHandler) { + super(ws); + this.hidive = new Hidive(); + this.initState(); + } - public async auth(data: AuthData) { - return this.hidive.doAuth(data); - } + public async auth(data: AuthData) { + return this.hidive.doAuth(data); + } - public async checkToken(): Promise { - //TODO: implement proper method to check token - return { isOk: true, value: undefined }; - } + public async checkToken(): Promise { + //TODO: implement proper method to check token + return { isOk: true, value: undefined }; + } - public async search(data: SearchData): Promise { - console.debug(`Got search options: ${JSON.stringify(data)}`); - const hidiveSearch = await this.hidive.doSearch(data); - if (!hidiveSearch.isOk) { - return hidiveSearch; - } - return { isOk: true, value: hidiveSearch.value }; - } + public async search(data: SearchData): Promise { + console.debug(`Got search options: ${JSON.stringify(data)}`); + const hidiveSearch = await this.hidive.doSearch(data); + if (!hidiveSearch.isOk) { + return hidiveSearch; + } + return { isOk: true, value: hidiveSearch.value }; + } - public async handleDefault(name: string) { - return getDefault(name, this.hidive.cfg.cli); - } + public async handleDefault(name: string) { + return getDefault(name, this.hidive.cfg.cli); + } - public async availableDubCodes(): Promise { - const dubLanguageCodesArray: string[] = []; - for(const language of languages){ - if (language.new_hd_locale) - dubLanguageCodesArray.push(language.code); - } - return [...new Set(dubLanguageCodesArray)]; - } + public async availableDubCodes(): Promise { + const dubLanguageCodesArray: string[] = []; + for (const language of languages) { + if (language.new_hd_locale) dubLanguageCodesArray.push(language.code); + } + return [...new Set(dubLanguageCodesArray)]; + } - public async availableSubCodes(): Promise { - const subLanguageCodesArray: string[] = []; - for(const language of languages){ - if (language.new_hd_locale) - subLanguageCodesArray.push(language.locale); - } - return ['all', 'none', ...new Set(subLanguageCodesArray)]; - } + public async availableSubCodes(): Promise { + const subLanguageCodesArray: string[] = []; + for (const language of languages) { + if (language.new_hd_locale) subLanguageCodesArray.push(language.locale); + } + return ['all', 'none', ...new Set(subLanguageCodesArray)]; + } - public async resolveItems(data: ResolveItemsData): Promise { - const parse = parseInt(data.id); - if (isNaN(parse) || parse <= 0) - return false; - console.debug(`Got resolve options: ${JSON.stringify(data)}`); - const res = await this.hidive.selectSeries(parseInt(data.id), data.e, data.but, data.all); - if (!res.isOk || !res.value) - return res.isOk; - this.addToQueue(res.value.map(item => { - return { - ...data, - ids: [item.id], - title: item.title, - parent: { - title: item.seriesTitle, - season: item.episodeInformation.seasonNumber+'' - }, - image: item.thumbnailUrl, - e: item.episodeInformation.episodeNumber+'', - episode: item.episodeInformation.episodeNumber+'', - }; - })); - return true; - } + public async resolveItems(data: ResolveItemsData): Promise { + const parse = parseInt(data.id); + if (isNaN(parse) || parse <= 0) return false; + console.debug(`Got resolve options: ${JSON.stringify(data)}`); + const res = await this.hidive.selectSeries(parseInt(data.id), data.e, data.but, data.all); + if (!res.isOk || !res.value) return res.isOk; + this.addToQueue( + res.value.map((item) => { + return { + ...data, + ids: [item.id], + title: item.title, + parent: { + title: item.seriesTitle, + season: item.episodeInformation.seasonNumber + '' + }, + image: item.thumbnailUrl, + e: item.episodeInformation.episodeNumber + '', + episode: item.episodeInformation.episodeNumber + '' + }; + }) + ); + return true; + } - public async listEpisodes(id: string): Promise { - const parse = parseInt(id); - if (isNaN(parse) || parse <= 0) - return { isOk: false, reason: new Error('The ID is invalid') }; + public async listEpisodes(id: string): Promise { + const parse = parseInt(id); + if (isNaN(parse) || parse <= 0) return { isOk: false, reason: new Error('The ID is invalid') }; - const request = await this.hidive.listSeries(parse); - if (!request.isOk || !request.value) - return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')}; + const request = await this.hidive.listSeries(parse); + if (!request.isOk || !request.value) return { isOk: false, reason: new Error('Unknown upstream error, check for additional logs') }; - return { isOk: true, value: request.value.map(function(item) { - const description = item.description.split('\r\n'); - return { - e: item.episodeInformation.episodeNumber+'', - lang: [], - name: item.title, - season: item.episodeInformation.seasonNumber+'', - seasonTitle: request.series.seasons[item.episodeInformation.seasonNumber-1]?.title ?? request.series.title, - episode: item.episodeInformation.episodeNumber+'', - id: item.id+'', - img: item.thumbnailUrl, - description: description ? description[0] : '', - time: '' - }; - })}; - } + return { + isOk: true, + value: request.value.map(function (item) { + const description = item.description.split('\r\n'); + return { + e: item.episodeInformation.episodeNumber + '', + lang: [], + name: item.title, + season: item.episodeInformation.seasonNumber + '', + seasonTitle: request.series.seasons[item.episodeInformation.seasonNumber - 1]?.title ?? request.series.title, + episode: item.episodeInformation.episodeNumber + '', + id: item.id + '', + img: item.thumbnailUrl, + description: description ? description[0] : '', + time: '' + }; + }) + }; + } - public async downloadItem(data: DownloadData) { - this.setDownloading(true); - console.debug(`Got download options: ${JSON.stringify(data)}`); - const _default = yargs.appArgv(this.hidive.cfg.cli, true); - const res = await this.hidive.selectSeries(parseInt(data.id), data.e, false, false); - if (!res.isOk || !res.showData) - return this.alertError(new Error('Download failed upstream, check for additional logs')); + public async downloadItem(data: DownloadData) { + this.setDownloading(true); + console.debug(`Got download options: ${JSON.stringify(data)}`); + const _default = yargs.appArgv(this.hidive.cfg.cli, true); + const res = await this.hidive.selectSeries(parseInt(data.id), data.e, false, false); + if (!res.isOk || !res.showData) return this.alertError(new Error('Download failed upstream, check for additional logs')); - for (const ep of res.value) { - await this.hidive.downloadEpisode(ep, {..._default, callbackMaker: this.makeProgressHandler.bind(this), dubLang: data.dubLang, dlsubs: data.dlsubs, fileName: data.fileName, q: data.q, force: 'y', noaudio: data.noaudio, novids: data.novids }); - } - this.sendMessage({ name: 'finish', data: undefined }); - this.setDownloading(false); - this.onFinish(); - } + for (const ep of res.value) { + await this.hidive.downloadEpisode(ep, { + ..._default, + callbackMaker: this.makeProgressHandler.bind(this), + dubLang: data.dubLang, + dlsubs: data.dlsubs, + fileName: data.fileName, + q: data.q, + force: 'y', + noaudio: data.noaudio, + novids: data.novids + }); + } + this.sendMessage({ name: 'finish', data: undefined }); + this.setDownloading(false); + this.onFinish(); + } } -export default HidiveHandler; \ No newline at end of file +export default HidiveHandler; diff --git a/gui/server/websocket.ts b/gui/server/websocket.ts index 68e5312..23e2bb9 100644 --- a/gui/server/websocket.ts +++ b/gui/server/websocket.ts @@ -8,116 +8,113 @@ import { getState } from '../../modules/module.cfg-loader'; import { console } from '../../modules/log'; declare interface ExternalEvent { - on(event: T, listener: (msg: WSMessage, respond: (data: MessageTypes[T][1]) => void) => void): this; - emit(event: T, msg: WSMessage, respond: (data: MessageTypes[T][1]) => void): boolean; + on(event: T, listener: (msg: WSMessage, respond: (data: MessageTypes[T][1]) => void) => void): this; + emit(event: T, msg: WSMessage, respond: (data: MessageTypes[T][1]) => void): boolean; } class ExternalEvent extends EventEmitter {} export default class WebSocketHandler { + private wsServer: ws.Server; - private wsServer: ws.Server; + public events: ExternalEvent = new ExternalEvent(); - public events: ExternalEvent = new ExternalEvent(); + constructor(server: Server) { + this.wsServer = new ws.WebSocketServer({ noServer: true, path: '/private' }); - constructor(server: Server) { - this.wsServer = new ws.WebSocketServer({ noServer: true, path: '/private' }); + this.wsServer.on('connection', (socket, req) => { + console.info(`[WS] Connection from '${req.socket.remoteAddress}'`); + socket.on('error', (er) => console.error(`[WS] ${er}`)); + socket.on('message', (data) => { + const json = JSON.parse(data.toString()) as UnknownWSMessage; + this.events.emit(json.name, json as any, (data) => { + this.wsServer.clients.forEach((client) => { + if (client.readyState !== WebSocket.OPEN) return; + client.send( + JSON.stringify({ + data, + id: json.id, + name: json.name + }), + (er) => { + if (er) console.error(`[WS] ${er}`); + } + ); + }); + }); + }); + }); - this.wsServer.on('connection', (socket, req) => { - console.info(`[WS] Connection from '${req.socket.remoteAddress}'`); - socket.on('error', (er) => console.error(`[WS] ${er}`)); - socket.on('message', (data) => { - const json = JSON.parse(data.toString()) as UnknownWSMessage; - this.events.emit(json.name, json as any, (data) => { - this.wsServer.clients.forEach(client => { - if (client.readyState !== WebSocket.OPEN) - return; - client.send(JSON.stringify({ - data, - id: json.id, - name: json.name - }), (er) => { - if (er) - console.error(`[WS] ${er}`); - }); - }); - }); - }); - }); + server.on('upgrade', (request, socket, head) => { + if (!this.wsServer.shouldHandle(request)) return; + if (!this.authenticate(request)) { + socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); + socket.destroy(); + console.info(`[WS] ${request.socket.remoteAddress} tried to connect but used a wrong password.`); + return; + } + this.wsServer.handleUpgrade(request, socket, head, (socket) => { + this.wsServer.emit('connection', socket, request); + }); + }); + } - server.on('upgrade', (request, socket, head) => { - if (!this.wsServer.shouldHandle(request)) - return; - if (!this.authenticate(request)) { - socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); - socket.destroy(); - console.info(`[WS] ${request.socket.remoteAddress} tried to connect but used a wrong password.`); - return; - } - this.wsServer.handleUpgrade(request, socket, head, socket => { - this.wsServer.emit('connection', socket, request); - }); - }); - } - - public sendMessage(data: RandomEvent) { - this.wsServer.clients.forEach(client => { - if (client.readyState !== WebSocket.OPEN) - return; - client.send(JSON.stringify(data), (er) => { - if (er) - console.error(`[WS] ${er}`); - }); - }); - } - - private authenticate(request: IncomingMessage): boolean { - const search = new URL(`http://${request.headers.host}${request.url}`).searchParams; - return cfg.gui.password === (search.get('password') ?? undefined); - } + public sendMessage(data: RandomEvent) { + this.wsServer.clients.forEach((client) => { + if (client.readyState !== WebSocket.OPEN) return; + client.send(JSON.stringify(data), (er) => { + if (er) console.error(`[WS] ${er}`); + }); + }); + } + private authenticate(request: IncomingMessage): boolean { + const search = new URL(`http://${request.headers.host}${request.url}`).searchParams; + return cfg.gui.password === (search.get('password') ?? undefined); + } } export class PublicWebSocket { - private wsServer: ws.Server; + private wsServer: ws.Server; - private state = getState(); - constructor(server: Server) { - this.wsServer = new ws.WebSocketServer({ noServer: true, path: '/public' }); + private state = getState(); + constructor(server: Server) { + this.wsServer = new ws.WebSocketServer({ noServer: true, path: '/public' }); - this.wsServer.on('connection', (socket, req) => { - console.info(`[WS] Connection to public ws from '${req.socket.remoteAddress}'`); - socket.on('error', (er) => console.error(`[WS] ${er}`)); - socket.on('message', (msg) => { - const data = JSON.parse(msg.toString()) as UnknownWSMessage; - switch (data.name) { - case 'isSetup': - this.send(socket, data.id, data.name, this.state.setup); - break; - case 'requirePassword': - this.send(socket, data.id, data.name, cfg.gui.password !== undefined); - break; - } - }); - }); + this.wsServer.on('connection', (socket, req) => { + console.info(`[WS] Connection to public ws from '${req.socket.remoteAddress}'`); + socket.on('error', (er) => console.error(`[WS] ${er}`)); + socket.on('message', (msg) => { + const data = JSON.parse(msg.toString()) as UnknownWSMessage; + switch (data.name) { + case 'isSetup': + this.send(socket, data.id, data.name, this.state.setup); + break; + case 'requirePassword': + this.send(socket, data.id, data.name, cfg.gui.password !== undefined); + break; + } + }); + }); - server.on('upgrade', (request, socket, head) => { - if (!this.wsServer.shouldHandle(request)) - return; - this.wsServer.handleUpgrade(request, socket, head, socket => { - this.wsServer.emit('connection', socket, request); - }); - }); - } + server.on('upgrade', (request, socket, head) => { + if (!this.wsServer.shouldHandle(request)) return; + this.wsServer.handleUpgrade(request, socket, head, (socket) => { + this.wsServer.emit('connection', socket, request); + }); + }); + } - private send(client: ws.WebSocket, id: string, name: string, data: any) { - client.send(JSON.stringify({ - data, - id, - name - }), (er) => { - if (er) - console.error(`[WS] ${er}`); - }); - } + private send(client: ws.WebSocket, id: string, name: string, data: any) { + client.send( + JSON.stringify({ + data, + id, + name + }), + (er) => { + if (er) console.error(`[WS] ${er}`); + } + ); + } } diff --git a/hidive.ts b/hidive.ts index 7ccc794..09b7548 100644 --- a/hidive.ts +++ b/hidive.ts @@ -38,1075 +38,1174 @@ import { MPDParsed, parse } from './modules/module.transform-mpd'; import { canDecrypt, getKeysWVD, cdm, getKeysPRD } from './modules/cdm'; import { KeyContainer } from './modules/widevine/license'; -export default class Hidive implements ServiceClass { - public cfg: yamlCfg.ConfigObject; - private token: Record; - private req: reqModule.Req; +export default class Hidive implements ServiceClass { + public cfg: yamlCfg.ConfigObject; + private token: Record; + private req: reqModule.Req; - constructor(private debug = false) { - this.cfg = yamlCfg.loadCfg(); - this.token = yamlCfg.loadNewHDToken(); - this.req = new reqModule.Req(domain, debug, false, 'hd'); - } + constructor(private debug = false) { + this.cfg = yamlCfg.loadCfg(); + this.token = yamlCfg.loadNewHDToken(); + this.req = new reqModule.Req(domain, debug, false, 'hd'); + } - public async cli() { - console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`); - const argv = yargs.appArgv(this.cfg.cli); - if (argv.debug) - this.debug = true; + public async cli() { + console.info(`\n=== Multi Downloader NX ${packageJson.version} ===\n`); + const argv = yargs.appArgv(this.cfg.cli); + if (argv.debug) this.debug = true; - //below is for quickly testing API calls - /*const apiTest = await this.apiReq('/v4/season/18871', '', 'auth', 'GET'); + //below is for quickly testing API calls + /*const apiTest = await this.apiReq('/v4/season/18871', '', 'auth', 'GET'); if(!apiTest.ok || !apiTest.res){return;} console.info(apiTest.res.body); fs.writeFileSync('apitest.json', JSON.stringify(JSON.parse(apiTest.res.body), null, 2)); return console.info('test done');*/ - // load binaries - this.cfg.bin = await yamlCfg.loadBinCfg(); - if (argv.allDubs) { - argv.dubLang = langsData.dubLanguageCodes; - } - if (argv.auth) { - //Authenticate - await this.doAuth({ - username: argv.username ?? await Helper.question('[Q] LOGIN/EMAIL: '), - password: argv.password ?? await Helper.question('[Q] PASSWORD: ') - }); - } else if (argv.search && argv.search.length > 2){ - await this.doSearch({ ...argv, search: argv.search as string }); - } else if (argv.s && !isNaN(parseInt(argv.s,10)) && parseInt(argv.s,10) > 0) { - const selected = await this.selectSeason(parseInt(argv.s), argv.e, argv.but, argv.all); - if (selected.isOk && selected.showData) { - for (const select of selected.value) { - //download episode - if (!(await this.downloadEpisode(select, {...argv}))) { - console.error(`Unable to download selected episode ${select.episodeInformation.episodeNumber}`); - return false; - } - } - } - return true; - } else if (argv.srz && !isNaN(parseInt(argv.srz,10)) && parseInt(argv.srz,10) > 0) { - const selected = await this.selectSeries(parseInt(argv.srz), argv.e, argv.but, argv.all); - if (selected.isOk && selected.showData) { - for (const select of selected.value) { - //download episode - if (!(await this.downloadEpisode(select, {...argv}))) { - console.error(`Unable to download selected episode ${select.episodeInformation.episodeNumber}`); - return false; - } - } - } - } else if (argv.new) { - console.error('--new is not yet implemented in the new API'); - } else if(argv.e) { - if (!(await this.downloadSingleEpisode(parseInt(argv.e), {...argv}))) { - console.error(`Unable to download selected episode ${argv.e}`); - return false; - } - } else { - console.info('No option selected or invalid value entered. Try --help.'); - } - } + // load binaries + this.cfg.bin = await yamlCfg.loadBinCfg(); + if (argv.allDubs) { + argv.dubLang = langsData.dubLanguageCodes; + } + if (argv.auth) { + //Authenticate + await this.doAuth({ + username: argv.username ?? (await Helper.question('[Q] LOGIN/EMAIL: ')), + password: argv.password ?? (await Helper.question('[Q] PASSWORD: ')) + }); + } else if (argv.search && argv.search.length > 2) { + await this.doSearch({ ...argv, search: argv.search as string }); + } else if (argv.s && !isNaN(parseInt(argv.s, 10)) && parseInt(argv.s, 10) > 0) { + const selected = await this.selectSeason(parseInt(argv.s), argv.e, argv.but, argv.all); + if (selected.isOk && selected.showData) { + for (const select of selected.value) { + //download episode + if (!(await this.downloadEpisode(select, { ...argv }))) { + console.error(`Unable to download selected episode ${select.episodeInformation.episodeNumber}`); + return false; + } + } + } + return true; + } else if (argv.srz && !isNaN(parseInt(argv.srz, 10)) && parseInt(argv.srz, 10) > 0) { + const selected = await this.selectSeries(parseInt(argv.srz), argv.e, argv.but, argv.all); + if (selected.isOk && selected.showData) { + for (const select of selected.value) { + //download episode + if (!(await this.downloadEpisode(select, { ...argv }))) { + console.error(`Unable to download selected episode ${select.episodeInformation.episodeNumber}`); + return false; + } + } + } + } else if (argv.new) { + console.error('--new is not yet implemented in the new API'); + } else if (argv.e) { + if (!(await this.downloadSingleEpisode(parseInt(argv.e), { ...argv }))) { + console.error(`Unable to download selected episode ${argv.e}`); + return false; + } + } else { + console.info('No option selected or invalid value entered. Try --help.'); + } + } + public async apiReq( + endpoint: string, + body: string | object = '', + authType: 'refresh' | 'auth' | 'both' | 'other' | 'none' = 'none', + method: 'GET' | 'POST' = 'POST', + authHeader?: string + ) { + const options = { + headers: { + 'X-Api-Key': api.hd_new_apiKey, + 'X-App-Var': api.hd_new_version, + realm: 'dce.hidive', + Referer: 'https://www.hidive.com/', + Origin: 'https://www.hidive.com' + } as Record, + method: method as 'GET' | 'POST', + url: (api.hd_new_api + endpoint) as string, + body: body, + useProxy: true + }; + // get request type + const isGet = method == 'GET'; + if (!isGet) { + options.body = body == '' ? body : JSON.stringify(body); + options.headers['Content-Type'] = 'application/json'; + } + if (authType == 'other') { + options.headers['Authorization'] = authHeader; + } else if (authType == 'auth') { + options.headers['Authorization'] = `Bearer ${this.token.authorisationToken}`; + } else if (authType == 'refresh') { + options.headers['Authorization'] = `Bearer ${this.token.refreshToken}`; + } else if (authType == 'both') { + options.headers['Authorization'] = `Mixed ${this.token.authorisationToken} ${this.token.refreshToken}`; + } + if (this.debug) { + console.debug('[DEBUG] Request params:'); + console.debug(options); + } + const apiReqOpts: reqModule.Params = { + method: options.method, + headers: options.headers as Record, + body: options.body as string + }; + let apiReq = await this.req.getData(options.url, apiReqOpts); + if (!apiReq.ok || !apiReq.res) { + if ((apiReq.error && apiReq.error.res?.status == 401) || (apiReq.res && apiReq.res.status == 401)) { + console.warn('Token expired, refreshing token and retrying.'); + if (await this.refreshToken()) { + if (authType == 'other') { + options.headers['Authorization'] = authHeader; + } else if (authType == 'auth') { + options.headers['Authorization'] = `Bearer ${this.token.authorisationToken}`; + } else if (authType == 'refresh') { + options.headers['Authorization'] = `Bearer ${this.token.refreshToken}`; + } else if (authType == 'both') { + options.headers['Authorization'] = `Mixed ${this.token.authorisationToken} ${this.token.refreshToken}`; + } + apiReq = await this.req.getData(options.url, apiReqOpts); + if (!apiReq.ok || !apiReq.res) { + console.error('API Request Failed!'); + return { + ok: false, + res: apiReq.res, + error: apiReq.error + }; + } + } else { + console.error('Failed to refresh token...'); + return { + ok: false, + res: apiReq.res, + error: apiReq.error + }; + } + } else { + console.error('API Request Failed!'); + return { + ok: false, + res: apiReq.res, + error: apiReq.error + }; + } + } + return { + ok: true, + res: apiReq.res + }; + } - public async apiReq(endpoint: string, body: string | object = '', authType: 'refresh' | 'auth' | 'both' | 'other' | 'none' = 'none', method: 'GET' | 'POST' = 'POST', authHeader?: string) { - const options = { - headers: { - 'X-Api-Key': api.hd_new_apiKey, - 'X-App-Var': api.hd_new_version, - 'realm': 'dce.hidive', - 'Referer': 'https://www.hidive.com/', - 'Origin': 'https://www.hidive.com' - } as Record, - method: method as 'GET'|'POST', - url: api.hd_new_api+endpoint as string, - body: body, - useProxy: true - }; - // get request type - const isGet = method == 'GET'; - if(!isGet){ - options.body = body == '' ? body : JSON.stringify(body); - options.headers['Content-Type'] = 'application/json'; - } - if (authType == 'other') { - options.headers['Authorization'] = authHeader; - } else if (authType == 'auth') { - options.headers['Authorization'] = `Bearer ${this.token.authorisationToken}`; - } else if (authType == 'refresh') { - options.headers['Authorization'] = `Bearer ${this.token.refreshToken}`; - } else if (authType == 'both') { - options.headers['Authorization'] = `Mixed ${this.token.authorisationToken} ${this.token.refreshToken}`; - } - if (this.debug) { - console.debug('[DEBUG] Request params:'); - console.debug(options); - } - const apiReqOpts: reqModule.Params = { - method: options.method, - headers: options.headers as Record, - body: options.body as string - }; - let apiReq = await this.req.getData(options.url, apiReqOpts); - if(!apiReq.ok || !apiReq.res){ - if ((apiReq.error && apiReq.error.res?.status == 401) || (apiReq.res && apiReq.res.status == 401)) { - console.warn('Token expired, refreshing token and retrying.'); - if (await this.refreshToken()) { - if (authType == 'other') { - options.headers['Authorization'] = authHeader; - } else if (authType == 'auth') { - options.headers['Authorization'] = `Bearer ${this.token.authorisationToken}`; - } else if (authType == 'refresh') { - options.headers['Authorization'] = `Bearer ${this.token.refreshToken}`; - } else if (authType == 'both') { - options.headers['Authorization'] = `Mixed ${this.token.authorisationToken} ${this.token.refreshToken}`; - } - apiReq = await this.req.getData(options.url, apiReqOpts); - if(!apiReq.ok || !apiReq.res) { - console.error('API Request Failed!'); - return { - ok: false, - res: apiReq.res, - error: apiReq.error - }; - } - } else { - console.error('Failed to refresh token...'); - return { - ok: false, - res: apiReq.res, - error: apiReq.error - }; - } - } else { - console.error('API Request Failed!'); - return { - ok: false, - res: apiReq.res, - error: apiReq.error - }; - } - } - return { - ok: true, - res: apiReq.res, - }; - } + public async doAuth(data: AuthData): Promise { + if (!this.token.refreshToken || !this.token.authorisationToken) { + await this.doAnonymousAuth(); + } + const authReq = await this.apiReq( + '/v2/login', + { + id: data.username, + secret: data.password + }, + 'auth' + ); + if (!authReq.ok || !authReq.res) { + console.error('Authentication failed!'); + return { isOk: false, reason: new Error('Authentication failed') }; + } + const tokens: Record = JSON.parse(await authReq.res.text()); + for (const token in tokens) { + this.token[token] = tokens[token]; + } + this.token.guest = false; + yamlCfg.saveNewHDToken(this.token); + console.info('Auth complete!'); + return { isOk: true, value: undefined }; + } - public async doAuth(data: AuthData): Promise { - if (!this.token.refreshToken || !this.token.authorisationToken) { - await this.doAnonymousAuth(); - } - const authReq = await this.apiReq('/v2/login', { - id: data.username, - secret: data.password - }, 'auth'); - if(!authReq.ok || !authReq.res){ - console.error('Authentication failed!'); - return { isOk: false, reason: new Error('Authentication failed') }; - } - const tokens: Record = JSON.parse(await authReq.res.text()); - for (const token in tokens) { - this.token[token] = tokens[token]; - } - this.token.guest = false; - yamlCfg.saveNewHDToken(this.token); - console.info('Auth complete!'); - return { isOk: true, value: undefined }; - } - - public async doAnonymousAuth() { - const authReq = await this.apiReq('/v2/login/guest/checkin'); - if(!authReq.ok || !authReq.res){ - console.error('Authentication failed!'); - return false; - } - const tokens: Record = JSON.parse(await authReq.res.text()); - for (const token in tokens) { - this.token[token] = tokens[token]; - } - //this.token.expires = new Date(Date.now() + 300); - this.token.guest = true; - yamlCfg.saveNewHDToken(this.token); - return true; - } + public async doAnonymousAuth() { + const authReq = await this.apiReq('/v2/login/guest/checkin'); + if (!authReq.ok || !authReq.res) { + console.error('Authentication failed!'); + return false; + } + const tokens: Record = JSON.parse(await authReq.res.text()); + for (const token in tokens) { + this.token[token] = tokens[token]; + } + //this.token.expires = new Date(Date.now() + 300); + this.token.guest = true; + yamlCfg.saveNewHDToken(this.token); + return true; + } - public async refreshToken() { - if (!this.token.refreshToken || !this.token.authorisationToken) { - return await this.doAnonymousAuth(); - } else { - const authReq = await this.apiReq('/v2/token/refresh', { - 'refreshToken': this.token.refreshToken - }, 'auth'); - if(!authReq.ok || !authReq.res){ - console.error('Token refresh failed, reinitializing session...'); - return this.initSession(); - } - const tokens: Record = JSON.parse(await authReq.res.text()); - for (const token in tokens) { - this.token[token] = tokens[token]; - } - yamlCfg.saveNewHDToken(this.token); - return true; - } - } + public async refreshToken() { + if (!this.token.refreshToken || !this.token.authorisationToken) { + return await this.doAnonymousAuth(); + } else { + const authReq = await this.apiReq( + '/v2/token/refresh', + { + refreshToken: this.token.refreshToken + }, + 'auth' + ); + if (!authReq.ok || !authReq.res) { + console.error('Token refresh failed, reinitializing session...'); + return this.initSession(); + } + const tokens: Record = JSON.parse(await authReq.res.text()); + for (const token in tokens) { + this.token[token] = tokens[token]; + } + yamlCfg.saveNewHDToken(this.token); + return true; + } + } - public async initSession() { - const authReq = await this.apiReq('/v1/init/', '', 'both', 'GET'); - if(!authReq.ok || !authReq.res){ - console.error('Failed to initialize session.'); - return false; - } - const tokens: Record = JSON.parse(await authReq.res.text()).authentication; - for (const token in tokens) { - this.token[token] = tokens[token]; - } - yamlCfg.saveNewHDToken(this.token); - return true; - } + public async initSession() { + const authReq = await this.apiReq('/v1/init/', '', 'both', 'GET'); + if (!authReq.ok || !authReq.res) { + console.error('Failed to initialize session.'); + return false; + } + const tokens: Record = JSON.parse(await authReq.res.text()).authentication; + for (const token in tokens) { + this.token[token] = tokens[token]; + } + yamlCfg.saveNewHDToken(this.token); + return true; + } - public async doSearch(data: SearchData): Promise { - const searchReq = await this.req.getData('https://h99xldr8mj-dsn.algolia.net/1/indexes/*/queries?x-algolia-agent=Algolia%20for%20JavaScript%20(3.35.1)%3B%20Browser&x-algolia-application-id=H99XLDR8MJ&x-algolia-api-key=e55ccb3db0399eabe2bfc37a0314c346', { - method: 'POST', - body: JSON.stringify({'requests': - [ - {'indexName':'prod-dce.hidive-livestreaming-events','params':'query='+encodeURIComponent(data.search)+'&facetFilters=%5B%22type%3ALIVE_EVENT%22%5D&hitsPerPage=25'+(data.page ? '&page='+(data.page-1) : '')}, - {'indexName':'prod-dce.hidive-livestreaming-events','params':'query='+encodeURIComponent(data.search)+'&facetFilters=%5B%22type%3AVOD_VIDEO%22%5D&hitsPerPage=25'+(data.page ? '&page='+(data.page-1) : '')}, - {'indexName':'prod-dce.hidive-livestreaming-events','params':'query='+encodeURIComponent(data.search)+'&facetFilters=%5B%22type%3AVOD_PLAYLIST%22%5D&hitsPerPage=25'+(data.page ? '&page='+(data.page-1) : '')}, - {'indexName':'prod-dce.hidive-livestreaming-events','params':'query='+encodeURIComponent(data.search)+'&facetFilters=%5B%22type%3AVOD_SERIES%22%5D&hitsPerPage=25'+(data.page ? '&page='+(data.page-1) : '')} - ] - }) - }); - if(!searchReq.ok || !searchReq.res){ - console.error('Search FAILED!'); - return { isOk: false, reason: new Error('Search failed. No more information provided') }; - } - const searchData = JSON.parse(await searchReq.res.text()) as NewHidiveSearch; - const searchItems: Hit[] = []; - console.info('Search Results:'); - for (const category of searchData.results) { - for (const hit of category.hits) { - searchItems.push(hit); - let fullType: string; - if (hit.type == 'VOD_SERIES') { - fullType = `Z.${hit.id}`; - } else if (hit.type == 'VOD_VIDEO') { - fullType = `E.${hit.id}`; - } else { - fullType = `${hit.type} #${hit.id}`; - } - console.log(`[${fullType}] ${hit.name} ${hit.seasonsCount ? '('+hit.seasonsCount+' Seasons)' : ''}`); - } - } - return { isOk: true, value: searchItems.filter(a => a.type == 'VOD_SERIES').flatMap((a): SearchResponseItem => { - return { - id: a.id+'', - image: a.coverUrl ?? '/notFound.png', - name: a.name, - rating: -1, - desc: a.description - }; - })}; - } + public async doSearch(data: SearchData): Promise { + const searchReq = await this.req.getData( + 'https://h99xldr8mj-dsn.algolia.net/1/indexes/*/queries?x-algolia-agent=Algolia%20for%20JavaScript%20(3.35.1)%3B%20Browser&x-algolia-application-id=H99XLDR8MJ&x-algolia-api-key=e55ccb3db0399eabe2bfc37a0314c346', + { + method: 'POST', + body: JSON.stringify({ + requests: [ + { + indexName: 'prod-dce.hidive-livestreaming-events', + params: + 'query=' + + encodeURIComponent(data.search) + + '&facetFilters=%5B%22type%3ALIVE_EVENT%22%5D&hitsPerPage=25' + + (data.page ? '&page=' + (data.page - 1) : '') + }, + { + indexName: 'prod-dce.hidive-livestreaming-events', + params: + 'query=' + + encodeURIComponent(data.search) + + '&facetFilters=%5B%22type%3AVOD_VIDEO%22%5D&hitsPerPage=25' + + (data.page ? '&page=' + (data.page - 1) : '') + }, + { + indexName: 'prod-dce.hidive-livestreaming-events', + params: + 'query=' + + encodeURIComponent(data.search) + + '&facetFilters=%5B%22type%3AVOD_PLAYLIST%22%5D&hitsPerPage=25' + + (data.page ? '&page=' + (data.page - 1) : '') + }, + { + indexName: 'prod-dce.hidive-livestreaming-events', + params: + 'query=' + + encodeURIComponent(data.search) + + '&facetFilters=%5B%22type%3AVOD_SERIES%22%5D&hitsPerPage=25' + + (data.page ? '&page=' + (data.page - 1) : '') + } + ] + }) + } + ); + if (!searchReq.ok || !searchReq.res) { + console.error('Search FAILED!'); + return { isOk: false, reason: new Error('Search failed. No more information provided') }; + } + const searchData = JSON.parse(await searchReq.res.text()) as NewHidiveSearch; + const searchItems: Hit[] = []; + console.info('Search Results:'); + for (const category of searchData.results) { + for (const hit of category.hits) { + searchItems.push(hit); + let fullType: string; + if (hit.type == 'VOD_SERIES') { + fullType = `Z.${hit.id}`; + } else if (hit.type == 'VOD_VIDEO') { + fullType = `E.${hit.id}`; + } else { + fullType = `${hit.type} #${hit.id}`; + } + console.log(`[${fullType}] ${hit.name} ${hit.seasonsCount ? '(' + hit.seasonsCount + ' Seasons)' : ''}`); + } + } + return { + isOk: true, + value: searchItems + .filter((a) => a.type == 'VOD_SERIES') + .flatMap((a): SearchResponseItem => { + return { + id: a.id + '', + image: a.coverUrl ?? '/notFound.png', + name: a.name, + rating: -1, + desc: a.description + }; + }) + }; + } - public async getSeries(id: number) { - const getSeriesData = await this.apiReq(`/v4/series/${id}?rpp=20`, '', 'auth', 'GET'); - if (!getSeriesData.ok || !getSeriesData.res) { - console.error('Failed to get Series Data'); - return { isOk: false }; - } - const seriesData = JSON.parse(await getSeriesData.res.text()) as NewHidiveSeries; - return { isOk: true, value: seriesData }; - } + public async getSeries(id: number) { + const getSeriesData = await this.apiReq(`/v4/series/${id}?rpp=20`, '', 'auth', 'GET'); + if (!getSeriesData.ok || !getSeriesData.res) { + console.error('Failed to get Series Data'); + return { isOk: false }; + } + const seriesData = JSON.parse(await getSeriesData.res.text()) as NewHidiveSeries; + return { isOk: true, value: seriesData }; + } - /** - * Function to get the season data from the API - * @param id ID of the season - * @param lastSeen Last episode ID seen, used for paging - * @returns - */ - public async getSeason(id: number, lastSeen?: number) { - const getSeasonData = await this.apiReq(`/v4/season/${id}?rpp=20${lastSeen ? '&lastSeen='+lastSeen : ''}`, '', 'auth', 'GET'); - if (!getSeasonData.ok || !getSeasonData.res) { - console.error('Failed to get Season Data'); - return { isOk: false }; - } - const seasonData = JSON.parse(await getSeasonData.res.text()) as NewHidiveSeason; - return { isOk: true, value: seasonData }; - } + /** + * Function to get the season data from the API + * @param id ID of the season + * @param lastSeen Last episode ID seen, used for paging + * @returns + */ + public async getSeason(id: number, lastSeen?: number) { + const getSeasonData = await this.apiReq(`/v4/season/${id}?rpp=20${lastSeen ? '&lastSeen=' + lastSeen : ''}`, '', 'auth', 'GET'); + if (!getSeasonData.ok || !getSeasonData.res) { + console.error('Failed to get Season Data'); + return { isOk: false }; + } + const seasonData = JSON.parse(await getSeasonData.res.text()) as NewHidiveSeason; + return { isOk: true, value: seasonData }; + } - public async listSeries(id: number) { - const series = await this.getSeries(id); - if (!series.isOk || !series.value) { - console.error('Failed to list series data: Failed to get series'); - return { isOk: false }; - } - console.info(`[Z.${series.value.id}] ${series.value.title} (${series.value.seasons.length} Seasons)`); - if (series.value.seasons.length === 0) { - console.info(' No Seasons found!'); - return { isOk: false }; - } - const episodes: Episode[] = []; - for (const seasonData of series.value.seasons) { - const season = await this.getSeason(seasonData.id); - if (!season.isOk || !season.value) { - console.error('Failed to list series data: Failed to get season '+seasonData.id); - return { isOk: false }; - } - console.info(` [S.${season.value.id}] ${season.value.title} (${season.value.episodeCount} Episodes)`); - while (season.value.paging.moreDataAvailable) { - const seasonPage = await this.getSeason(seasonData.id, season.value.paging.lastSeen); - if (!seasonPage.isOk || !seasonPage.value) break; - season.value.episodes = season.value.episodes.concat(seasonPage.value.episodes); - season.value.paging.lastSeen = seasonPage.value.paging.lastSeen; - season.value.paging.moreDataAvailable = seasonPage.value.paging.moreDataAvailable; - } - for (const episode of season.value.episodes) { - const datePattern = /\d{1,2}\/\d{1,2}\/\d{2,4} \d{1,2}:\d{2} UTC/; - if (episode.title.includes(' - ')) { - episode.episodeInformation.episodeNumber = parseFloat(episode.title.split(' - ')[0].replace('E', '')); - episode.title = episode.title.split(' - ')[1]; - } - //S${episode.episodeInformation.seasonNumber}E${episode.episodeInformation.episodeNumber} - - if (!datePattern.test(episode.title) && episode.duration !== 10) { - episodes.push(episode); - } - console.info(` [E.${episode.id}] ${episode.title}`); - } - } - return { isOk: true, value: episodes, series: series.value }; - } + public async listSeries(id: number) { + const series = await this.getSeries(id); + if (!series.isOk || !series.value) { + console.error('Failed to list series data: Failed to get series'); + return { isOk: false }; + } + console.info(`[Z.${series.value.id}] ${series.value.title} (${series.value.seasons.length} Seasons)`); + if (series.value.seasons.length === 0) { + console.info(' No Seasons found!'); + return { isOk: false }; + } + const episodes: Episode[] = []; + for (const seasonData of series.value.seasons) { + const season = await this.getSeason(seasonData.id); + if (!season.isOk || !season.value) { + console.error('Failed to list series data: Failed to get season ' + seasonData.id); + return { isOk: false }; + } + console.info(` [S.${season.value.id}] ${season.value.title} (${season.value.episodeCount} Episodes)`); + while (season.value.paging.moreDataAvailable) { + const seasonPage = await this.getSeason(seasonData.id, season.value.paging.lastSeen); + if (!seasonPage.isOk || !seasonPage.value) break; + season.value.episodes = season.value.episodes.concat(seasonPage.value.episodes); + season.value.paging.lastSeen = seasonPage.value.paging.lastSeen; + season.value.paging.moreDataAvailable = seasonPage.value.paging.moreDataAvailable; + } + for (const episode of season.value.episodes) { + const datePattern = /\d{1,2}\/\d{1,2}\/\d{2,4} \d{1,2}:\d{2} UTC/; + if (episode.title.includes(' - ')) { + episode.episodeInformation.episodeNumber = parseFloat(episode.title.split(' - ')[0].replace('E', '')); + episode.title = episode.title.split(' - ')[1]; + } + //S${episode.episodeInformation.seasonNumber}E${episode.episodeInformation.episodeNumber} - + if (!datePattern.test(episode.title) && episode.duration !== 10) { + episodes.push(episode); + } + console.info(` [E.${episode.id}] ${episode.title}`); + } + } + return { isOk: true, value: episodes, series: series.value }; + } - public async listSeason(id: number) { - const season = await this.getSeason(id); - if (!season.isOk || !season.value) { - console.error('Failed to list series data: Failed to get season '+id); - return { isOk: false }; - } - console.info(` [S.${season.value.id}] ${season.value.title} (${season.value.episodeCount} Episodes)`); - while (season.value.paging.moreDataAvailable) { - const seasonPage = await this.getSeason(id, season.value.paging.lastSeen); - if (!seasonPage.isOk || !seasonPage.value) break; - season.value.episodes = season.value.episodes.concat(seasonPage.value.episodes); - season.value.paging.lastSeen = seasonPage.value.paging.lastSeen; - season.value.paging.moreDataAvailable = seasonPage.value.paging.moreDataAvailable; - } - const episodes: Episode[] = []; - for (const episode of season.value.episodes) { - const datePattern = /\d{1,2}\/\d{1,2}\/\d{2,4} \d{1,2}:\d{2} UTC/; - if (episode.title.includes(' - ')) { - episode.episodeInformation.episodeNumber = parseFloat(episode.title.split(' - ')[0].replace('E', '')); - episode.title = episode.title.split(' - ')[1]; - } - //S${episode.episodeInformation.seasonNumber}E${episode.episodeInformation.episodeNumber} - - if (!datePattern.test(episode.title) && episode.duration !== 10) { - episodes.push(episode); - } - console.info(` [E.${episode.id}] ${episode.title}`); - } - const series: NewHidiveSeriesExtra = {...season.value.series, season: season.value}; - return { isOk: true, value: episodes, series: series }; - } + public async listSeason(id: number) { + const season = await this.getSeason(id); + if (!season.isOk || !season.value) { + console.error('Failed to list series data: Failed to get season ' + id); + return { isOk: false }; + } + console.info(` [S.${season.value.id}] ${season.value.title} (${season.value.episodeCount} Episodes)`); + while (season.value.paging.moreDataAvailable) { + const seasonPage = await this.getSeason(id, season.value.paging.lastSeen); + if (!seasonPage.isOk || !seasonPage.value) break; + season.value.episodes = season.value.episodes.concat(seasonPage.value.episodes); + season.value.paging.lastSeen = seasonPage.value.paging.lastSeen; + season.value.paging.moreDataAvailable = seasonPage.value.paging.moreDataAvailable; + } + const episodes: Episode[] = []; + for (const episode of season.value.episodes) { + const datePattern = /\d{1,2}\/\d{1,2}\/\d{2,4} \d{1,2}:\d{2} UTC/; + if (episode.title.includes(' - ')) { + episode.episodeInformation.episodeNumber = parseFloat(episode.title.split(' - ')[0].replace('E', '')); + episode.title = episode.title.split(' - ')[1]; + } + //S${episode.episodeInformation.seasonNumber}E${episode.episodeInformation.episodeNumber} - + if (!datePattern.test(episode.title) && episode.duration !== 10) { + episodes.push(episode); + } + console.info(` [E.${episode.id}] ${episode.title}`); + } + const series: NewHidiveSeriesExtra = { ...season.value.series, season: season.value }; + return { isOk: true, value: episodes, series: series }; + } - /** - * Lists the requested series, and returns the selected episodes - * @param id Series ID - * @param e Selector - * @param but Download all but selected videos - * @param all Whether to download all available videos - * @returns - */ - public async selectSeries(id: number, e: string | undefined, but: boolean, all: boolean) { - const getShowData = await this.listSeries(id); - if (!getShowData.isOk || !getShowData.value) { - return { isOk: false, value: [] }; - } - const showData = getShowData.value; - const doEpsFilter = parseSelect(e as string); - // build selected episodes - const selEpsArr: NewHidiveEpisodeExtra[] = []; let ovaSeq = 1; let movieSeq = 1; - for (let i = 0; i < showData.length; i++) { - const titleId = showData[i].id; - const seriesTitle = getShowData.series.title; - const seasonTitle = getShowData.series.seasons[showData[i].episodeInformation.seasonNumber-1]?.title ?? seriesTitle; - let nameLong = showData[i].title; - if (nameLong.match(/OVA/i)) { - nameLong = 'ova' + (('0' + ovaSeq).slice(-2)); ovaSeq++; - } else if (nameLong.match(/Theatrical/i)) { - nameLong = 'movie' + (('0' + movieSeq).slice(-2)); movieSeq++; - } - let selMark = ''; - if (all || - but && !doEpsFilter.isSelected([parseFloat(showData[i].episodeInformation.episodeNumber+'')+'', showData[i].id+'']) || - !but && doEpsFilter.isSelected([parseFloat(showData[i].episodeInformation.episodeNumber+'')+'', showData[i].id+'']) - ) { - selEpsArr.push({ isSelected: true, titleId, nameLong, seasonTitle, seriesTitle, ...showData[i] }); - selMark = '✓ '; - } - console.info('%s[%s] %s', - selMark, - 'S'+parseFloat(showData[i].episodeInformation.seasonNumber+'')+'E'+parseFloat(showData[i].episodeInformation.episodeNumber+''), - showData[i].title, - ); - } - return { isOk: true, value: selEpsArr, showData: getShowData.series }; - } + /** + * Lists the requested series, and returns the selected episodes + * @param id Series ID + * @param e Selector + * @param but Download all but selected videos + * @param all Whether to download all available videos + * @returns + */ + public async selectSeries(id: number, e: string | undefined, but: boolean, all: boolean) { + const getShowData = await this.listSeries(id); + if (!getShowData.isOk || !getShowData.value) { + return { isOk: false, value: [] }; + } + const showData = getShowData.value; + const doEpsFilter = parseSelect(e as string); + // build selected episodes + const selEpsArr: NewHidiveEpisodeExtra[] = []; + let ovaSeq = 1; + let movieSeq = 1; + for (let i = 0; i < showData.length; i++) { + const titleId = showData[i].id; + const seriesTitle = getShowData.series.title; + const seasonTitle = getShowData.series.seasons[showData[i].episodeInformation.seasonNumber - 1]?.title ?? seriesTitle; + let nameLong = showData[i].title; + if (nameLong.match(/OVA/i)) { + nameLong = 'ova' + ('0' + ovaSeq).slice(-2); + ovaSeq++; + } else if (nameLong.match(/Theatrical/i)) { + nameLong = 'movie' + ('0' + movieSeq).slice(-2); + movieSeq++; + } + let selMark = ''; + if ( + all || + (but && !doEpsFilter.isSelected([parseFloat(showData[i].episodeInformation.episodeNumber + '') + '', showData[i].id + ''])) || + (!but && doEpsFilter.isSelected([parseFloat(showData[i].episodeInformation.episodeNumber + '') + '', showData[i].id + ''])) + ) { + selEpsArr.push({ isSelected: true, titleId, nameLong, seasonTitle, seriesTitle, ...showData[i] }); + selMark = '✓ '; + } + console.info( + '%s[%s] %s', + selMark, + 'S' + parseFloat(showData[i].episodeInformation.seasonNumber + '') + 'E' + parseFloat(showData[i].episodeInformation.episodeNumber + ''), + showData[i].title + ); + } + return { isOk: true, value: selEpsArr, showData: getShowData.series }; + } - /** - * Lists the requested season, and returns the selected episodes - * @param id Season ID - * @param e Selector - * @param but Download all but selected videos - * @param all Whether to download all available videos - * @returns - */ - public async selectSeason(id: number, e: string | undefined, but: boolean, all: boolean) { - const getShowData = await this.listSeason(id); - if (!getShowData.isOk || !getShowData.value) { - return { isOk: false, value: [] }; - } - const showData = getShowData.value; - const doEpsFilter = parseSelect(e as string); - // build selected episodes - const selEpsArr: NewHidiveEpisodeExtra[] = []; let ovaSeq = 1; let movieSeq = 1; - for (let i = 0; i < showData.length; i++) { - const titleId = showData[i].id; - const seriesTitle = getShowData.series.title; - const seasonTitle = getShowData.series.season.title; - let nameLong = showData[i].title; - if (nameLong.match(/OVA/i)) { - nameLong = 'ova' + (('0' + ovaSeq).slice(-2)); ovaSeq++; - } else if (nameLong.match(/Theatrical/i)) { - nameLong = 'movie' + (('0' + movieSeq).slice(-2)); movieSeq++; - } - let selMark = ''; - if (all || - but && !doEpsFilter.isSelected([parseFloat(showData[i].episodeInformation.episodeNumber+'')+'', showData[i].id+'']) || - !but && doEpsFilter.isSelected([parseFloat(showData[i].episodeInformation.episodeNumber+'')+'', showData[i].id+'']) - ) { - selEpsArr.push({ isSelected: true, titleId, nameLong, seasonTitle, seriesTitle, ...showData[i] }); - selMark = '✓ '; - } - console.info('%s[%s] %s', - selMark, - 'S'+parseFloat(showData[i].episodeInformation.seasonNumber+'')+'E'+parseFloat(showData[i].episodeInformation.episodeNumber+''), - showData[i].title, - ); - } - return { isOk: true, value: selEpsArr, showData: getShowData.series }; - } + /** + * Lists the requested season, and returns the selected episodes + * @param id Season ID + * @param e Selector + * @param but Download all but selected videos + * @param all Whether to download all available videos + * @returns + */ + public async selectSeason(id: number, e: string | undefined, but: boolean, all: boolean) { + const getShowData = await this.listSeason(id); + if (!getShowData.isOk || !getShowData.value) { + return { isOk: false, value: [] }; + } + const showData = getShowData.value; + const doEpsFilter = parseSelect(e as string); + // build selected episodes + const selEpsArr: NewHidiveEpisodeExtra[] = []; + let ovaSeq = 1; + let movieSeq = 1; + for (let i = 0; i < showData.length; i++) { + const titleId = showData[i].id; + const seriesTitle = getShowData.series.title; + const seasonTitle = getShowData.series.season.title; + let nameLong = showData[i].title; + if (nameLong.match(/OVA/i)) { + nameLong = 'ova' + ('0' + ovaSeq).slice(-2); + ovaSeq++; + } else if (nameLong.match(/Theatrical/i)) { + nameLong = 'movie' + ('0' + movieSeq).slice(-2); + movieSeq++; + } + let selMark = ''; + if ( + all || + (but && !doEpsFilter.isSelected([parseFloat(showData[i].episodeInformation.episodeNumber + '') + '', showData[i].id + ''])) || + (!but && doEpsFilter.isSelected([parseFloat(showData[i].episodeInformation.episodeNumber + '') + '', showData[i].id + ''])) + ) { + selEpsArr.push({ isSelected: true, titleId, nameLong, seasonTitle, seriesTitle, ...showData[i] }); + selMark = '✓ '; + } + console.info( + '%s[%s] %s', + selMark, + 'S' + parseFloat(showData[i].episodeInformation.seasonNumber + '') + 'E' + parseFloat(showData[i].episodeInformation.episodeNumber + ''), + showData[i].title + ); + } + return { isOk: true, value: selEpsArr, showData: getShowData.series }; + } - public async downloadEpisode(selectedEpisode: NewHidiveEpisodeExtra, options: Record) { - //Get Episode data - const episodeDataReq = await this.apiReq(`/v4/vod/${selectedEpisode.id}?includePlaybackDetails=URL`, '', 'auth', 'GET'); - if (!episodeDataReq.ok || !episodeDataReq.res) { - console.error('Failed to get episode data'); - return { isOk: false, reason: new Error('Failed to get Episode Data') }; - } - const episodeData = JSON.parse(await episodeDataReq.res.text()) as NewHidiveEpisode; + public async downloadEpisode(selectedEpisode: NewHidiveEpisodeExtra, options: Record) { + //Get Episode data + const episodeDataReq = await this.apiReq(`/v4/vod/${selectedEpisode.id}?includePlaybackDetails=URL`, '', 'auth', 'GET'); + if (!episodeDataReq.ok || !episodeDataReq.res) { + console.error('Failed to get episode data'); + return { isOk: false, reason: new Error('Failed to get Episode Data') }; + } + const episodeData = JSON.parse(await episodeDataReq.res.text()) as NewHidiveEpisode; - if (!episodeData.playerUrlCallback) { - console.error('Failed to download episode: You do not have access to this'); - return { isOk: false, reason: new Error('You do not have access to this') }; - } + if (!episodeData.playerUrlCallback) { + console.error('Failed to download episode: You do not have access to this'); + return { isOk: false, reason: new Error('You do not have access to this') }; + } - //Get Playback data - const playbackReq = await this.req.getData(episodeData.playerUrlCallback); - if(!playbackReq.ok || !playbackReq.res){ - console.error('Playback Request Failed'); - return { isOk: false, reason: new Error('Playback request failed') }; - } - const playbackData = JSON.parse(await playbackReq.res.text()) as NewHidivePlayback; + //Get Playback data + const playbackReq = await this.req.getData(episodeData.playerUrlCallback); + if (!playbackReq.ok || !playbackReq.res) { + console.error('Playback Request Failed'); + return { isOk: false, reason: new Error('Playback request failed') }; + } + const playbackData = JSON.parse(await playbackReq.res.text()) as NewHidivePlayback; - //Get actual MPD - const mpdRequest = await this.req.getData(playbackData.dash[0].url); - if(!mpdRequest.ok || !mpdRequest.res){ - console.error('MPD Request Failed'); - return { isOk: false, reason: new Error('MPD request failed') }; - } - const mpd = await mpdRequest.res.text() as string; + //Get actual MPD + const mpdRequest = await this.req.getData(playbackData.dash[0].url); + if (!mpdRequest.ok || !mpdRequest.res) { + console.error('MPD Request Failed'); + return { isOk: false, reason: new Error('MPD request failed') }; + } + const mpd = (await mpdRequest.res.text()) as string; - selectedEpisode.jwtToken = playbackData.dash[0].drm.jwtToken; + selectedEpisode.jwtToken = playbackData.dash[0].drm.jwtToken; - //Output metadata and prepare for download - const availableSubs = playbackData.dash[0].subtitles.filter(a => a.format === 'vtt'); - const showTitle = `${selectedEpisode.seriesTitle} S${selectedEpisode.episodeInformation.seasonNumber}`; - console.info(`[INFO] ${showTitle} - ${selectedEpisode.episodeInformation.episodeNumber}`); - console.info('[INFO] Available dubs and subtitles:'); - console.info('\tAudios: ' + episodeData.offlinePlaybackLanguages.map(a => langsData.languages.find(b => b.code == a)?.name).join('\n\t\t')); - console.info('\tSubs : ' + availableSubs.map(a => langsData.languages.find(b => b.new_hd_locale == a.language)?.name).join('\n\t\t')); - console.info(`[INFO] Selected dub(s): ${options.dubLang.join(', ')}`); - const baseUrl = playbackData.dash[0].url.split('master')[0]; - const parsedmpd = await parse(mpd, undefined, baseUrl); - const res = await this.downloadMPD(parsedmpd, availableSubs, selectedEpisode, options); - if (res === undefined || res.error) { - console.error('Failed to download media list'); - return { isOk: false, reason: new Error('Failed to download media list') }; - } else { - if (!options.skipmux) { - await this.muxStreams(res.data, { ...options, output: res.fileName }, false); - } else { - console.info('Skipping mux'); - } - downloaded({ - service: 'hidive', - type: 's' - }, selectedEpisode.titleId+'', [selectedEpisode.episodeInformation.episodeNumber+'']); - return { isOk: res, value: undefined }; - } - } + //Output metadata and prepare for download + const availableSubs = playbackData.dash[0].subtitles.filter((a) => a.format === 'vtt'); + const showTitle = `${selectedEpisode.seriesTitle} S${selectedEpisode.episodeInformation.seasonNumber}`; + console.info(`[INFO] ${showTitle} - ${selectedEpisode.episodeInformation.episodeNumber}`); + console.info('[INFO] Available dubs and subtitles:'); + console.info('\tAudios: ' + episodeData.offlinePlaybackLanguages.map((a) => langsData.languages.find((b) => b.code == a)?.name).join('\n\t\t')); + console.info('\tSubs : ' + availableSubs.map((a) => langsData.languages.find((b) => b.new_hd_locale == a.language)?.name).join('\n\t\t')); + console.info(`[INFO] Selected dub(s): ${options.dubLang.join(', ')}`); + const baseUrl = playbackData.dash[0].url.split('master')[0]; + const parsedmpd = await parse(mpd, undefined, baseUrl); + const res = await this.downloadMPD(parsedmpd, availableSubs, selectedEpisode, options); + if (res === undefined || res.error) { + console.error('Failed to download media list'); + return { isOk: false, reason: new Error('Failed to download media list') }; + } else { + if (!options.skipmux) { + await this.muxStreams(res.data, { ...options, output: res.fileName }, false); + } else { + console.info('Skipping mux'); + } + downloaded( + { + service: 'hidive', + type: 's' + }, + selectedEpisode.titleId + '', + [selectedEpisode.episodeInformation.episodeNumber + ''] + ); + return { isOk: res, value: undefined }; + } + } - public async downloadSingleEpisode(id: number, options: Record) { - //Get Episode data - const episodeDataReq = await this.apiReq(`/v4/vod/${id}?includePlaybackDetails=URL`, '', 'auth', 'GET'); - if (!episodeDataReq.ok || !episodeDataReq.res) { - console.error('Failed to get episode data'); - return { isOk: false, reason: new Error('Failed to get Episode Data') }; - } - const episodeData = JSON.parse(await episodeDataReq.res.text()) as NewHidiveEpisode; + public async downloadSingleEpisode(id: number, options: Record) { + //Get Episode data + const episodeDataReq = await this.apiReq(`/v4/vod/${id}?includePlaybackDetails=URL`, '', 'auth', 'GET'); + if (!episodeDataReq.ok || !episodeDataReq.res) { + console.error('Failed to get episode data'); + return { isOk: false, reason: new Error('Failed to get Episode Data') }; + } + const episodeData = JSON.parse(await episodeDataReq.res.text()) as NewHidiveEpisode; - if (episodeData.title.includes(' - ') && episodeData.episodeInformation) { - episodeData.episodeInformation.episodeNumber = parseFloat(episodeData.title.split(' - ')[0].replace('E', '')); - episodeData.title = episodeData.title.split(' - ')[1]; - } + if (episodeData.title.includes(' - ') && episodeData.episodeInformation) { + episodeData.episodeInformation.episodeNumber = parseFloat(episodeData.title.split(' - ')[0].replace('E', '')); + episodeData.title = episodeData.title.split(' - ')[1]; + } - if (!episodeData.playerUrlCallback) { - console.error('Failed to download episode: You do not have access to this'); - return { isOk: false, reason: new Error('You do not have access to this') }; - } + if (!episodeData.playerUrlCallback) { + console.error('Failed to download episode: You do not have access to this'); + return { isOk: false, reason: new Error('You do not have access to this') }; + } - let seasonData: Awaited> | undefined = undefined; - if (episodeData.episodeInformation) { - seasonData = await this.getSeason(episodeData.episodeInformation.season); - if (!seasonData.isOk || !seasonData.value) { - console.error('Failed to get season data'); - return { isOk: false, reason: new Error('Failed to get season data') }; - } - } else { - episodeData.episodeInformation = { - season: 0, - seasonNumber: 0, - episodeNumber: 0, - }; - } + let seasonData: Awaited> | undefined = undefined; + if (episodeData.episodeInformation) { + seasonData = await this.getSeason(episodeData.episodeInformation.season); + if (!seasonData.isOk || !seasonData.value) { + console.error('Failed to get season data'); + return { isOk: false, reason: new Error('Failed to get season data') }; + } + } else { + episodeData.episodeInformation = { + season: 0, + seasonNumber: 0, + episodeNumber: 0 + }; + } - //Get Playback data - const playbackReq = await this.req.getData(episodeData.playerUrlCallback); - if(!playbackReq.ok || !playbackReq.res){ - console.error('Playback Request Failed'); - return { isOk: false, reason: new Error('Playback request failed') }; - } - const playbackData = JSON.parse(await playbackReq.res.text()) as NewHidivePlayback; + //Get Playback data + const playbackReq = await this.req.getData(episodeData.playerUrlCallback); + if (!playbackReq.ok || !playbackReq.res) { + console.error('Playback Request Failed'); + return { isOk: false, reason: new Error('Playback request failed') }; + } + const playbackData = JSON.parse(await playbackReq.res.text()) as NewHidivePlayback; - //Get actual MPD - const mpdRequest = await this.req.getData(playbackData.dash[0].url); - if(!mpdRequest.ok || !mpdRequest.res){ - console.error('MPD Request Failed'); - return { isOk: false, reason: new Error('MPD request failed') }; - } - const mpd = await mpdRequest.res.text() as string; + //Get actual MPD + const mpdRequest = await this.req.getData(playbackData.dash[0].url); + if (!mpdRequest.ok || !mpdRequest.res) { + console.error('MPD Request Failed'); + return { isOk: false, reason: new Error('MPD request failed') }; + } + const mpd = (await mpdRequest.res.text()) as string; - const selectedEpisode: NewHidiveEpisodeExtra = { - ...episodeData, - nameLong: episodeData.title, - titleId: episodeData.id, - seasonTitle: seasonData?.value.title ?? episodeData.title, - seriesTitle: seasonData?.value.series.title ?? episodeData.title, - isSelected: true - }; - - selectedEpisode.jwtToken = playbackData.dash[0].drm.jwtToken; + const selectedEpisode: NewHidiveEpisodeExtra = { + ...episodeData, + nameLong: episodeData.title, + titleId: episodeData.id, + seasonTitle: seasonData?.value.title ?? episodeData.title, + seriesTitle: seasonData?.value.series.title ?? episodeData.title, + isSelected: true + }; - //Output metadata and prepare for download - const availableSubs = playbackData.dash[0].subtitles.filter(a => a.format === 'vtt'); - const showTitle = `${selectedEpisode.seriesTitle} S${selectedEpisode.episodeInformation.seasonNumber}`; - console.info(`[INFO] ${showTitle} - ${selectedEpisode.episodeInformation.episodeNumber}`); - console.info('[INFO] Available dubs and subtitles:'); - console.info('\tAudios: ' + episodeData.offlinePlaybackLanguages.map(a => langsData.languages.find(b => b.code == a)?.name).join('\n\t\t')); - console.info('\tSubs : ' + availableSubs.map(a => langsData.languages.find(b => b.new_hd_locale == a.language)?.name).join('\n\t\t')); - console.info(`[INFO] Selected dub(s): ${options.dubLang.join(', ')}`); - const baseUrl = playbackData.dash[0].url.split('master')[0]; - const parsedmpd = await parse(mpd, undefined, baseUrl); - const res = await this.downloadMPD(parsedmpd, availableSubs, selectedEpisode, options); - if (res === undefined || res.error) { - console.error('Failed to download media list'); - return { isOk: false, reason: new Error('Failed to download media list') }; - } else { - if (!options.skipmux) { - await this.muxStreams(res.data, { ...options, output: res.fileName }, false); - } else { - console.info('Skipping mux'); - } - downloaded({ - service: 'hidive', - type: 's' - }, selectedEpisode.titleId+'', [selectedEpisode.episodeInformation.episodeNumber+'']); - return { isOk: res, value: undefined }; - } - } + selectedEpisode.jwtToken = playbackData.dash[0].drm.jwtToken; - public async downloadMPD(streamPlaylists: MPDParsed, subs: Subtitle[], selectedEpisode: NewHidiveEpisodeExtra, options: Record) { - //let fileName: string; - const files: DownloadedMedia[] = []; - const variables: Variable[] = []; - let dlFailed = false; - const subsMargin = 0; - const chosenFontSize = options.originalFontSize ? undefined : options.fontSize; - let encryptionKeys: KeyContainer[] = []; - if (!canDecrypt && (!options.novids || !options.noaudio)) { - console.error('No valid Widevine or PlayReady CDM detected. Please ensure a supported and functional CDM is installed.'); - return undefined; - } + //Output metadata and prepare for download + const availableSubs = playbackData.dash[0].subtitles.filter((a) => a.format === 'vtt'); + const showTitle = `${selectedEpisode.seriesTitle} S${selectedEpisode.episodeInformation.seasonNumber}`; + console.info(`[INFO] ${showTitle} - ${selectedEpisode.episodeInformation.episodeNumber}`); + console.info('[INFO] Available dubs and subtitles:'); + console.info('\tAudios: ' + episodeData.offlinePlaybackLanguages.map((a) => langsData.languages.find((b) => b.code == a)?.name).join('\n\t\t')); + console.info('\tSubs : ' + availableSubs.map((a) => langsData.languages.find((b) => b.new_hd_locale == a.language)?.name).join('\n\t\t')); + console.info(`[INFO] Selected dub(s): ${options.dubLang.join(', ')}`); + const baseUrl = playbackData.dash[0].url.split('master')[0]; + const parsedmpd = await parse(mpd, undefined, baseUrl); + const res = await this.downloadMPD(parsedmpd, availableSubs, selectedEpisode, options); + if (res === undefined || res.error) { + console.error('Failed to download media list'); + return { isOk: false, reason: new Error('Failed to download media list') }; + } else { + if (!options.skipmux) { + await this.muxStreams(res.data, { ...options, output: res.fileName }, false); + } else { + console.info('Skipping mux'); + } + downloaded( + { + service: 'hidive', + type: 's' + }, + selectedEpisode.titleId + '', + [selectedEpisode.episodeInformation.episodeNumber + ''] + ); + return { isOk: res, value: undefined }; + } + } - if (!this.cfg.bin.ffmpeg) - this.cfg.bin = await yamlCfg.loadBinCfg(); + public async downloadMPD(streamPlaylists: MPDParsed, subs: Subtitle[], selectedEpisode: NewHidiveEpisodeExtra, options: Record) { + //let fileName: string; + const files: DownloadedMedia[] = []; + const variables: Variable[] = []; + let dlFailed = false; + const subsMargin = 0; + const chosenFontSize = options.originalFontSize ? undefined : options.fontSize; + let encryptionKeys: KeyContainer[] = []; + if (!canDecrypt && (!options.novids || !options.noaudio)) { + console.error('No valid Widevine or PlayReady CDM detected. Please ensure a supported and functional CDM is installed.'); + return undefined; + } - if (!this.cfg.bin.mp4decrypt && !this.cfg.bin.shaka && (!options.novids || !options.noaudio)) { - console.error('Neither Shaka nor MP4Decrypt found. Please ensure at least one of them is installed.'); - return undefined; - } + if (!this.cfg.bin.ffmpeg) this.cfg.bin = await yamlCfg.loadBinCfg(); - variables.push(...([ - ['title', selectedEpisode.title, true], - ['episode', selectedEpisode.episodeInformation.episodeNumber, false], - ['service', 'HD', false], - ['seriesTitle', selectedEpisode.seasonTitle, true], - ['showTitle', selectedEpisode.seriesTitle, true], - ['season', selectedEpisode.episodeInformation.seasonNumber, false] - ] as [AvailableFilenameVars, string|number, boolean][]).map((a): Variable => { - return { - name: a[0], - replaceWith: a[1], - type: typeof a[1], - sanitize: a[2] - } as Variable; - })); + if (!this.cfg.bin.mp4decrypt && !this.cfg.bin.shaka && (!options.novids || !options.noaudio)) { + console.error('Neither Shaka nor MP4Decrypt found. Please ensure at least one of them is installed.'); + return undefined; + } - //Get name of CDNs/Servers - const streamServers = Object.keys(streamPlaylists); + variables.push( + ...( + [ + ['title', selectedEpisode.title, true], + ['episode', selectedEpisode.episodeInformation.episodeNumber, false], + ['service', 'HD', false], + ['seriesTitle', selectedEpisode.seasonTitle, true], + ['showTitle', selectedEpisode.seriesTitle, true], + ['season', selectedEpisode.episodeInformation.seasonNumber, false] + ] as [AvailableFilenameVars, string | number, boolean][] + ).map((a): Variable => { + return { + name: a[0], + replaceWith: a[1], + type: typeof a[1], + sanitize: a[2] + } as Variable; + }) + ); - options.x = options.x > streamServers.length ? 1 : options.x; + //Get name of CDNs/Servers + const streamServers = Object.keys(streamPlaylists); - const selectedServer = streamServers[options.x - 1]; - const selectedList = streamPlaylists[selectedServer]; + options.x = options.x > streamServers.length ? 1 : options.x; - //set Video Qualities - const videos = selectedList.video.map(item => { - return { - ...item, - resolutionText: `${item.quality.width}x${item.quality.height} (${Math.round(item.bandwidth/1024)}KiB/s)` - }; - }); + const selectedServer = streamServers[options.x - 1]; + const selectedList = streamPlaylists[selectedServer]; - const audios = selectedList.audio.map(item => { - return { - ...item, - resolutionText: `${Math.round(item.bandwidth/1000)}kB/s` - }; - }); + //set Video Qualities + const videos = selectedList.video.map((item) => { + return { + ...item, + resolutionText: `${item.quality.width}x${item.quality.height} (${Math.round(item.bandwidth / 1024)}KiB/s)` + }; + }); + const audios = selectedList.audio.map((item) => { + return { + ...item, + resolutionText: `${Math.round(item.bandwidth / 1000)}kB/s` + }; + }); - videos.sort((a, b) => { - return a.bandwidth - b.bandwidth; - }); + videos.sort((a, b) => { + return a.bandwidth - b.bandwidth; + }); - videos.sort((a, b) => { - return a.quality.width - b.quality.width; - }); + videos.sort((a, b) => { + return a.quality.width - b.quality.width; + }); - audios.sort((a, b) => { - return a.bandwidth - b.bandwidth; - }); + audios.sort((a, b) => { + return a.bandwidth - b.bandwidth; + }); - let chosenVideoQuality = options.q === 0 ? videos.length : options.q; - if(chosenVideoQuality > videos.length) { - console.warn(`The requested quality of ${options.q} is greater than the maximum ${videos.length}.\n[WARN] Therefor the maximum will be capped at ${videos.length}.`); - chosenVideoQuality = videos.length; - } - chosenVideoQuality--; + let chosenVideoQuality = options.q === 0 ? videos.length : options.q; + if (chosenVideoQuality > videos.length) { + console.warn(`The requested quality of ${options.q} is greater than the maximum ${videos.length}.\n[WARN] Therefor the maximum will be capped at ${videos.length}.`); + chosenVideoQuality = videos.length; + } + chosenVideoQuality--; - const chosenVideoSegments = videos[chosenVideoQuality]; + const chosenVideoSegments = videos[chosenVideoQuality]; - console.info(`Servers available:\n\t${streamServers.join('\n\t')}`); - console.info(`Available Video Qualities:\n\t${videos.map((a, ind) => `[${ind+1}] ${a.resolutionText}`).join('\n\t')}`); - console.info(`Available Audio Qualities:\n\t${audios.map((a, ind) => `[${ind+1}] ${a.resolutionText}`).join('\n\t')}`); + console.info(`Servers available:\n\t${streamServers.join('\n\t')}`); + console.info(`Available Video Qualities:\n\t${videos.map((a, ind) => `[${ind + 1}] ${a.resolutionText}`).join('\n\t')}`); + console.info(`Available Audio Qualities:\n\t${audios.map((a, ind) => `[${ind + 1}] ${a.resolutionText}`).join('\n\t')}`); - variables.push({ - name: 'height', - type: 'number', - replaceWith: chosenVideoSegments.quality.height - }, { - name: 'width', - type: 'number', - replaceWith: chosenVideoSegments.quality.width - }); + variables.push( + { + name: 'height', + type: 'number', + replaceWith: chosenVideoSegments.quality.height + }, + { + name: 'width', + type: 'number', + replaceWith: chosenVideoSegments.quality.width + } + ); - const chosenAudios: typeof audios[0][] = []; - const audioByLanguage: Record = {}; - for (const audio of audios) { - if (!audioByLanguage[audio.language.code]) audioByLanguage[audio.language.code] = []; - audioByLanguage[audio.language.code].push(audio); - } - for (const dubLang of options.dubLang as string[]) { - if (audioByLanguage[dubLang]) { - let chosenAudioQuality = options.q === 0 ? audios.length : options.q; - if(chosenAudioQuality > audioByLanguage[dubLang].length) { - chosenAudioQuality = audioByLanguage[dubLang].length; - } - chosenAudioQuality--; - chosenAudios.push(audioByLanguage[dubLang][chosenAudioQuality]); - } - } - if (chosenAudios.length == 0) { - console.error(`Chosen audio language(s) does not exist for episode ${selectedEpisode.episodeInformation.episodeNumber}`); - return undefined; - } + const chosenAudios: (typeof audios)[0][] = []; + const audioByLanguage: Record = {}; + for (const audio of audios) { + if (!audioByLanguage[audio.language.code]) audioByLanguage[audio.language.code] = []; + audioByLanguage[audio.language.code].push(audio); + } + for (const dubLang of options.dubLang as string[]) { + if (audioByLanguage[dubLang]) { + let chosenAudioQuality = options.q === 0 ? audios.length : options.q; + if (chosenAudioQuality > audioByLanguage[dubLang].length) { + chosenAudioQuality = audioByLanguage[dubLang].length; + } + chosenAudioQuality--; + chosenAudios.push(audioByLanguage[dubLang][chosenAudioQuality]); + } + } + if (chosenAudios.length == 0) { + console.error(`Chosen audio language(s) does not exist for episode ${selectedEpisode.episodeInformation.episodeNumber}`); + return undefined; + } - const fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); + const fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep); - console.info(`Selected quality: \n\tVideo: ${chosenVideoSegments.resolutionText}\n\tAudio: ${chosenAudios[0].resolutionText}\n\tServer: ${selectedServer}`); - console.info(`Selected (Available) Audio Languages: ${chosenAudios.map(a => a.language.name).join(', ')}`); - console.info('Stream URL:', chosenVideoSegments.segments[0].map.uri.split('/init.mp4')[0]); + console.info(`Selected quality: \n\tVideo: ${chosenVideoSegments.resolutionText}\n\tAudio: ${chosenAudios[0].resolutionText}\n\tServer: ${selectedServer}`); + console.info(`Selected (Available) Audio Languages: ${chosenAudios.map((a) => a.language.name).join(', ')}`); + console.info('Stream URL:', chosenVideoSegments.segments[0].map.uri.split('/init.mp4')[0]); - if (chosenAudios[0].pssh_wvd && cdm === 'widevine' || chosenVideoSegments.pssh_wvd && cdm === 'widevine') { - encryptionKeys = await getKeysWVD(chosenVideoSegments.pssh_wvd, 'https://shield-drm.imggaming.com/api/v2/license', { - 'Authorization': `Bearer ${selectedEpisode.jwtToken}`, - 'X-Drm-Info': 'eyJzeXN0ZW0iOiJjb20ud2lkZXZpbmUuYWxwaGEifQ==', - }); - } + if ((chosenAudios[0].pssh_wvd && cdm === 'widevine') || (chosenVideoSegments.pssh_wvd && cdm === 'widevine')) { + encryptionKeys = await getKeysWVD(chosenVideoSegments.pssh_wvd, 'https://shield-drm.imggaming.com/api/v2/license', { + Authorization: `Bearer ${selectedEpisode.jwtToken}`, + 'X-Drm-Info': 'eyJzeXN0ZW0iOiJjb20ud2lkZXZpbmUuYWxwaGEifQ==' + }); + } - if (chosenAudios[0].pssh_prd && cdm === 'playready' || chosenVideoSegments.pssh_prd && cdm === 'playready') { - encryptionKeys = await getKeysPRD(chosenVideoSegments.pssh_prd, 'https://shield-drm.imggaming.com/api/v2/license', { - 'Authorization': `Bearer ${selectedEpisode.jwtToken}`, - 'X-Drm-Info': 'eyJzeXN0ZW0iOiJjb20ubWljcm9zb2Z0LnBsYXlyZWFkeSJ9', - }); - } - - if (!options.novids) { - //Download Video - const totalParts = chosenVideoSegments.segments.length; - const mathParts = Math.ceil(totalParts / options.partsize); - const mathMsg = `(${mathParts}*${options.partsize})`; - console.info('Total parts in video stream:', totalParts, mathMsg); - const tsFile = path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName); - const tempFile = parseFileName(`temp-${selectedEpisode.id}`, variables, options.numbers, options.override).join(path.sep); - const tempTsFile = path.isAbsolute(tempFile as string) ? tempFile : path.join(this.cfg.dir.content, tempFile); - const dirName = path.dirname(tsFile); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }); - } - const videoJson: M3U8Json = { - segments: chosenVideoSegments.segments - }; - const videoDownload = await new streamdl({ - output: `${tempTsFile}.video.enc.m4s`, - timeout: options.timeout, - m3u8json: videoJson, - // baseurl: chunkPlaylist.baseUrl, - threads: options.partsize, - fsRetryTime: options.fsRetryTime * 1000, - override: options.force, - callback: options.callbackMaker ? options.callbackMaker({ - fileName: `${path.isAbsolute(fileName) ? fileName.slice(this.cfg.dir.content.length) : fileName}`, - image: selectedEpisode.thumbnailUrl, - parent: { - title: selectedEpisode.seriesTitle - }, - title: selectedEpisode.title, - language: chosenAudios[0].language - }) : undefined - }).download(); - if(!videoDownload.ok){ - console.error(`DL Stats: ${JSON.stringify(videoDownload.parts)}\n`); - dlFailed = true; - } else { - if (chosenVideoSegments.pssh_wvd || chosenVideoSegments.pssh_prd) { - console.info('Decryption Needed, attempting to decrypt'); - if (encryptionKeys.length == 0) { - console.error('Failed to get encryption keys'); - return undefined; - } - if (this.cfg.bin.mp4decrypt || this.cfg.bin.shaka) { - let commandBase = `--show-progress --key ${encryptionKeys[cdm === 'playready' ? 0 : 1].kid}:${encryptionKeys[cdm === 'playready' ? 0 : 1].key} `; - let commandVideo = commandBase+`"${tempTsFile}.video.enc.m4s" "${tempTsFile}.video.m4s"`; + if ((chosenAudios[0].pssh_prd && cdm === 'playready') || (chosenVideoSegments.pssh_prd && cdm === 'playready')) { + encryptionKeys = await getKeysPRD(chosenVideoSegments.pssh_prd, 'https://shield-drm.imggaming.com/api/v2/license', { + Authorization: `Bearer ${selectedEpisode.jwtToken}`, + 'X-Drm-Info': 'eyJzeXN0ZW0iOiJjb20ubWljcm9zb2Z0LnBsYXlyZWFkeSJ9' + }); + } - if (this.cfg.bin.shaka) { - commandBase = ` --enable_raw_key_decryption ${encryptionKeys.map(kb => '--keys key_id='+kb.kid+':key='+kb.key).join(' ')}`; - commandVideo = `input="${tempTsFile}.video.enc.m4s",stream=video,output="${tempTsFile}.video.m4s"`+commandBase; - } + if (!options.novids) { + //Download Video + const totalParts = chosenVideoSegments.segments.length; + const mathParts = Math.ceil(totalParts / options.partsize); + const mathMsg = `(${mathParts}*${options.partsize})`; + console.info('Total parts in video stream:', totalParts, mathMsg); + const tsFile = path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName); + const tempFile = parseFileName(`temp-${selectedEpisode.id}`, variables, options.numbers, options.override).join(path.sep); + const tempTsFile = path.isAbsolute(tempFile as string) ? tempFile : path.join(this.cfg.dir.content, tempFile); + const dirName = path.dirname(tsFile); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + const videoJson: M3U8Json = { + segments: chosenVideoSegments.segments + }; + const videoDownload = await new streamdl({ + output: `${tempTsFile}.video.enc.m4s`, + timeout: options.timeout, + m3u8json: videoJson, + // baseurl: chunkPlaylist.baseUrl, + threads: options.partsize, + fsRetryTime: options.fsRetryTime * 1000, + override: options.force, + callback: options.callbackMaker + ? options.callbackMaker({ + fileName: `${path.isAbsolute(fileName) ? fileName.slice(this.cfg.dir.content.length) : fileName}`, + image: selectedEpisode.thumbnailUrl, + parent: { + title: selectedEpisode.seriesTitle + }, + title: selectedEpisode.title, + language: chosenAudios[0].language + }) + : undefined + }).download(); + if (!videoDownload.ok) { + console.error(`DL Stats: ${JSON.stringify(videoDownload.parts)}\n`); + dlFailed = true; + } else { + if (chosenVideoSegments.pssh_wvd || chosenVideoSegments.pssh_prd) { + console.info('Decryption Needed, attempting to decrypt'); + if (encryptionKeys.length == 0) { + console.error('Failed to get encryption keys'); + return undefined; + } + if (this.cfg.bin.mp4decrypt || this.cfg.bin.shaka) { + let commandBase = `--show-progress --key ${encryptionKeys[cdm === 'playready' ? 0 : 1].kid}:${encryptionKeys[cdm === 'playready' ? 0 : 1].key} `; + let commandVideo = commandBase + `"${tempTsFile}.video.enc.m4s" "${tempTsFile}.video.m4s"`; - console.info('Started decrypting video,', this.cfg.bin.shaka ? 'using shaka' : 'using mp4decrypt'); - const decryptVideo = Helper.exec(this.cfg.bin.shaka ? 'shaka-packager' : 'mp4decrypt', this.cfg.bin.shaka ? `"${this.cfg.bin.shaka}"` : `"${this.cfg.bin.mp4decrypt}"`, commandVideo); - if (!decryptVideo.isOk) { - console.error(decryptVideo.err); - console.error(`Decryption failed with exit code ${decryptVideo.err.code}`); - fs.renameSync(`${tempTsFile}.video.enc.m4s`, `${tsFile}.video.enc.m4s`); - return undefined; - } else { - console.info('Decryption done for video'); - if (!options.nocleanup) { - fs.removeSync(`${tempTsFile}.video.enc.m4s`); - } - fs.copyFileSync(`${tempTsFile}.video.m4s`, `${tsFile}.video.m4s`); - fs.unlinkSync(`${tempTsFile}.video.m4s`); - files.push({ - type: 'Video', - path: `${tsFile}.video.m4s`, - lang: chosenAudios[0].language, - isPrimary: true - }); - } - } else { - console.warn('mp4decrypt/shaka not found, files need decryption. Decryption Keys:', encryptionKeys); - } - } - } - } else { - console.info('Skipping Video'); - } + if (this.cfg.bin.shaka) { + commandBase = ` --enable_raw_key_decryption ${encryptionKeys.map((kb) => '--keys key_id=' + kb.kid + ':key=' + kb.key).join(' ')}`; + commandVideo = `input="${tempTsFile}.video.enc.m4s",stream=video,output="${tempTsFile}.video.m4s"` + commandBase; + } - if (!options.noaudio) { - for (const audio of chosenAudios) { - const chosenAudioSegments = audio; - //Download Audio (if available) - const totalParts = chosenAudioSegments.segments.length; - const mathParts = Math.ceil(totalParts / options.partsize); - const mathMsg = `(${mathParts}*${options.partsize})`; - console.info('Total parts in audio stream:', totalParts, mathMsg); - const tempFile = parseFileName(`temp-${selectedEpisode.id}.${chosenAudioSegments.language.name}`, variables, options.numbers, options.override).join(path.sep); - const tempTsFile = path.isAbsolute(tempFile as string) ? tempFile : path.join(this.cfg.dir.content, tempFile); - const outFile = parseFileName(options.fileName + '.' + (chosenAudioSegments.language.name), variables, options.numbers, options.override).join(path.sep); - const tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); - const dirName = path.dirname(tsFile); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }); - } - const audioJson: M3U8Json = { - segments: chosenAudioSegments.segments - }; - const audioDownload = await new streamdl({ - output: `${tempTsFile}.audio.enc.m4s`, - timeout: options.timeout, - m3u8json: audioJson, - // baseurl: chunkPlaylist.baseUrl, - threads: options.partsize, - fsRetryTime: options.fsRetryTime * 1000, - override: options.force, - callback: options.callbackMaker ? options.callbackMaker({ - fileName: `${path.isAbsolute(outFile) ? outFile.slice(this.cfg.dir.content.length) : outFile}`, - image: selectedEpisode.thumbnailUrl, - parent: { - title: selectedEpisode.seriesTitle - }, - title: selectedEpisode.title, - language: chosenAudioSegments.language - }) : undefined - }).download(); - if(!audioDownload.ok){ - console.error(`DL Stats: ${JSON.stringify(audioDownload.parts)}\n`); - dlFailed = true; - } - if (chosenAudioSegments.pssh_wvd || chosenAudioSegments.pssh_prd) { - console.info('Decryption Needed, attempting to decrypt'); - if (encryptionKeys.length == 0) { - console.error('Failed to get encryption keys'); - return undefined; - } - if (this.cfg.bin.mp4decrypt || this.cfg.bin.shaka) { - let commandBase = `--show-progress --key ${encryptionKeys[cdm === 'playready' ? 0 : 1].kid}:${encryptionKeys[cdm === 'playready' ? 0 : 1].key} `; - let commandAudio = commandBase+`"${tempTsFile}.audio.enc.m4s" "${tempTsFile}.audio.m4s"`; + console.info('Started decrypting video,', this.cfg.bin.shaka ? 'using shaka' : 'using mp4decrypt'); + const decryptVideo = Helper.exec( + this.cfg.bin.shaka ? 'shaka-packager' : 'mp4decrypt', + this.cfg.bin.shaka ? `"${this.cfg.bin.shaka}"` : `"${this.cfg.bin.mp4decrypt}"`, + commandVideo + ); + if (!decryptVideo.isOk) { + console.error(decryptVideo.err); + console.error(`Decryption failed with exit code ${decryptVideo.err.code}`); + if (this.cfg.bin.shaka) { + console.error(`Downgrade to Shaka-Packager v2.6.1 (https://github.com/shaka-project/shaka-packager/releases/tag/v2.6.1) and try again`); + } + fs.renameSync(`${tempTsFile}.video.enc.m4s`, `${tsFile}.video.enc.m4s`); + return undefined; + } else { + console.info('Decryption done for video'); + if (!options.nocleanup) { + fs.removeSync(`${tempTsFile}.video.enc.m4s`); + } + fs.copyFileSync(`${tempTsFile}.video.m4s`, `${tsFile}.video.m4s`); + fs.unlinkSync(`${tempTsFile}.video.m4s`); + files.push({ + type: 'Video', + path: `${tsFile}.video.m4s`, + lang: chosenAudios[0].language, + isPrimary: true + }); + } + } else { + console.warn('mp4decrypt/shaka not found, files need decryption. Decryption Keys:', encryptionKeys); + } + } + } + } else { + console.info('Skipping Video'); + } - if (this.cfg.bin.shaka) { - commandBase = ` --enable_raw_key_decryption ${encryptionKeys.map(kb => '--keys key_id='+kb.kid+':key='+kb.key).join(' ')}`; - commandAudio = `input="${tempTsFile}.audio.enc.m4s",stream=audio,output="${tempTsFile}.audio.m4s"`+commandBase; - } + if (!options.noaudio) { + for (const audio of chosenAudios) { + const chosenAudioSegments = audio; + //Download Audio (if available) + const totalParts = chosenAudioSegments.segments.length; + const mathParts = Math.ceil(totalParts / options.partsize); + const mathMsg = `(${mathParts}*${options.partsize})`; + console.info('Total parts in audio stream:', totalParts, mathMsg); + const tempFile = parseFileName(`temp-${selectedEpisode.id}.${chosenAudioSegments.language.name}`, variables, options.numbers, options.override).join(path.sep); + const tempTsFile = path.isAbsolute(tempFile as string) ? tempFile : path.join(this.cfg.dir.content, tempFile); + const outFile = parseFileName(options.fileName + '.' + chosenAudioSegments.language.name, variables, options.numbers, options.override).join(path.sep); + const tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile); + const dirName = path.dirname(tsFile); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + const audioJson: M3U8Json = { + segments: chosenAudioSegments.segments + }; + const audioDownload = await new streamdl({ + output: `${tempTsFile}.audio.enc.m4s`, + timeout: options.timeout, + m3u8json: audioJson, + // baseurl: chunkPlaylist.baseUrl, + threads: options.partsize, + fsRetryTime: options.fsRetryTime * 1000, + override: options.force, + callback: options.callbackMaker + ? options.callbackMaker({ + fileName: `${path.isAbsolute(outFile) ? outFile.slice(this.cfg.dir.content.length) : outFile}`, + image: selectedEpisode.thumbnailUrl, + parent: { + title: selectedEpisode.seriesTitle + }, + title: selectedEpisode.title, + language: chosenAudioSegments.language + }) + : undefined + }).download(); + if (!audioDownload.ok) { + console.error(`DL Stats: ${JSON.stringify(audioDownload.parts)}\n`); + dlFailed = true; + } + if (chosenAudioSegments.pssh_wvd || chosenAudioSegments.pssh_prd) { + console.info('Decryption Needed, attempting to decrypt'); + if (encryptionKeys.length == 0) { + console.error('Failed to get encryption keys'); + return undefined; + } + if (this.cfg.bin.mp4decrypt || this.cfg.bin.shaka) { + let commandBase = `--show-progress --key ${encryptionKeys[cdm === 'playready' ? 0 : 1].kid}:${encryptionKeys[cdm === 'playready' ? 0 : 1].key} `; + let commandAudio = commandBase + `"${tempTsFile}.audio.enc.m4s" "${tempTsFile}.audio.m4s"`; - console.info('Started decrypting audio'); - const decryptAudio = Helper.exec(this.cfg.bin.shaka ? 'shaka-packager' : 'mp4decrypt', this.cfg.bin.shaka ? `"${this.cfg.bin.shaka}"` : `"${this.cfg.bin.mp4decrypt}"`, commandAudio); - if (!decryptAudio.isOk) { - console.error(decryptAudio.err); - console.error(`Decryption failed with exit code ${decryptAudio.err.code}`); - fs.renameSync(`${tempTsFile}.audio.enc.m4s`, `${tsFile}.audio.enc.m4s`); - return undefined; - } else { - if (!options.nocleanup) { - fs.removeSync(`${tempTsFile}.audio.enc.m4s`); - } - fs.copyFileSync(`${tempTsFile}.audio.m4s`, `${tsFile}.audio.m4s`); - fs.unlinkSync(`${tempTsFile}.audio.m4s`); - files.push({ - type: 'Audio', - path: `${tsFile}.audio.m4s`, - lang: chosenAudioSegments.language, - isPrimary: chosenAudioSegments.default - }); - console.info('Decryption done for audio'); - } - } else { - console.warn('mp4decrypt not found, files need decryption. Decryption Keys:', encryptionKeys); - } - } - } - } else { - console.info('Skipping Audio'); - } + if (this.cfg.bin.shaka) { + commandBase = ` --enable_raw_key_decryption ${encryptionKeys.map((kb) => '--keys key_id=' + kb.kid + ':key=' + kb.key).join(' ')}`; + commandAudio = `input="${tempTsFile}.audio.enc.m4s",stream=audio,output="${tempTsFile}.audio.m4s"` + commandBase; + } - if(options.dlsubs.indexOf('all') > -1){ - options.dlsubs = ['all']; - } + console.info('Started decrypting audio'); + const decryptAudio = Helper.exec( + this.cfg.bin.shaka ? 'shaka-packager' : 'mp4decrypt', + this.cfg.bin.shaka ? `"${this.cfg.bin.shaka}"` : `"${this.cfg.bin.mp4decrypt}"`, + commandAudio + ); + if (!decryptAudio.isOk) { + console.error(decryptAudio.err); + console.error(`Decryption failed with exit code ${decryptAudio.err.code}`); + if (this.cfg.bin.shaka) { + console.error(`Downgrade to Shaka-Packager v2.6.1 (https://github.com/shaka-project/shaka-packager/releases/tag/v2.6.1) and try again`); + } + fs.renameSync(`${tempTsFile}.audio.enc.m4s`, `${tsFile}.audio.enc.m4s`); + return undefined; + } else { + if (!options.nocleanup) { + fs.removeSync(`${tempTsFile}.audio.enc.m4s`); + } + fs.copyFileSync(`${tempTsFile}.audio.m4s`, `${tsFile}.audio.m4s`); + fs.unlinkSync(`${tempTsFile}.audio.m4s`); + files.push({ + type: 'Audio', + path: `${tsFile}.audio.m4s`, + lang: chosenAudioSegments.language, + isPrimary: chosenAudioSegments.default + }); + console.info('Decryption done for audio'); + } + } else { + console.warn('mp4decrypt not found, files need decryption. Decryption Keys:', encryptionKeys); + } + } + } + } else { + console.info('Skipping Audio'); + } - if (options.nosubs) { - console.info('Subtitles downloading disabled from nosubs flag.'); - options.skipsubs = true; - } + if (options.dlsubs.indexOf('all') > -1) { + options.dlsubs = ['all']; + } - if(!options.skipsubs && options.dlsubs.indexOf('none') == -1) { - if(subs.length > 0) { - let subIndex = 0; - for(const sub of subs) { - const subLang = langsData.languages.find(a => a.new_hd_locale === sub.language); - if (!subLang) { - console.warn(`Language not found for subtitle language: ${sub.language}, Skipping`); - continue; - } - const sxData: Partial = {}; - sxData.file = langsData.subsFile(fileName as string, subIndex+'', subLang, false, options.ccTag); - if (path.isAbsolute(sxData.file)) { - sxData.path = sxData.file; - } else { - sxData.path = path.join(this.cfg.dir.content, sxData.file); - } - const dirName = path.dirname(sxData.path); - if (!fs.existsSync(dirName)) { - fs.mkdirSync(dirName, { recursive: true }); - } - sxData.language = subLang; - if(options.dlsubs.includes('all') || options.dlsubs.includes(subLang.locale)) { - const getVttContent = await this.req.getData(sub.url); - if (getVttContent.ok && getVttContent.res) { - console.info(`Subtitle Downloaded: ${sub.url}`); - //vttConvert(getVttContent.res.body, false, subLang.name, fontSize); - const sBody = vtt2ass(undefined, chosenFontSize, await getVttContent.res.text(), '', subsMargin, options.fontName, options.combineLines); - sxData.title = `${subLang.language} / ${sxData.title}`; - sxData.fonts = fontsData.assFonts(sBody) as Font[]; - fs.writeFileSync(sxData.path, sBody); - console.info(`Subtitle converted: ${sxData.file}`); - files.push({ - type: 'Subtitle', - ...sxData as sxItem, - cc: false - }); - } else{ - console.warn(`Failed to download subtitle: ${sxData.file}`); - } - } - subIndex++; - } - } else{ - console.warn('Can\'t find urls for subtitles!'); - } - } else{ - console.info('Subtitles downloading skipped!'); - } + if (options.nosubs) { + console.info('Subtitles downloading disabled from nosubs flag.'); + options.skipsubs = true; + } - return { - error: dlFailed, - data: files, - fileName: fileName ? (path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName)) || './unknown' : './unknown' - }; - } + if (!options.skipsubs && options.dlsubs.indexOf('none') == -1) { + if (subs.length > 0) { + let subIndex = 0; + for (const sub of subs) { + const subLang = langsData.languages.find((a) => a.new_hd_locale === sub.language); + if (!subLang) { + console.warn(`Language not found for subtitle language: ${sub.language}, Skipping`); + continue; + } + const sxData: Partial = {}; + sxData.file = langsData.subsFile(fileName as string, subIndex + '', subLang, false, options.ccTag); + if (path.isAbsolute(sxData.file)) { + sxData.path = sxData.file; + } else { + sxData.path = path.join(this.cfg.dir.content, sxData.file); + } + const dirName = path.dirname(sxData.path); + if (!fs.existsSync(dirName)) { + fs.mkdirSync(dirName, { recursive: true }); + } + sxData.language = subLang; + if (options.dlsubs.includes('all') || options.dlsubs.includes(subLang.locale)) { + const getVttContent = await this.req.getData(sub.url); + if (getVttContent.ok && getVttContent.res) { + console.info(`Subtitle Downloaded: ${sub.url}`); + //vttConvert(getVttContent.res.body, false, subLang.name, fontSize); + const sBody = vtt2ass(undefined, chosenFontSize, await getVttContent.res.text(), '', subsMargin, options.fontName, options.combineLines); + sxData.title = `${subLang.language} / ${sxData.title}`; + sxData.fonts = fontsData.assFonts(sBody) as Font[]; + fs.writeFileSync(sxData.path, sBody); + console.info(`Subtitle converted: ${sxData.file}`); + files.push({ + type: 'Subtitle', + ...(sxData as sxItem), + cc: false + }); + } else { + console.warn(`Failed to download subtitle: ${sxData.file}`); + } + } + subIndex++; + } + } else { + console.warn("Can't find urls for subtitles!"); + } + } else { + console.info('Subtitles downloading skipped!'); + } - public async muxStreams(data: DownloadedMedia[], options: Record, inverseTrackOrder: boolean = true) { - this.cfg.bin = await yamlCfg.loadBinCfg(); - let hasAudioStreams = false; - if (options.novids || data.filter(a => a.type === 'Video').length === 0) - return console.info('Skip muxing since no vids are downloaded'); - if (data.some(a => a.type === 'Audio')) { - hasAudioStreams = true; - } - const merger = new Merger({ - onlyVid: hasAudioStreams ? data.filter(a => a.type === 'Video').map((a) : MergerInput => { - if (a.type === 'Subtitle') - throw new Error('Never'); - return { - lang: a.lang, - path: a.path, - }; - }) : [], - skipSubMux: options.skipSubMux, - inverseTrackOrder: inverseTrackOrder, - keepAllVideos: options.keepAllVideos, - onlyAudio: hasAudioStreams ? data.filter(a => a.type === 'Audio').map((a) : MergerInput => { - if (a.type === 'Subtitle') - throw new Error('Never'); - return { - lang: a.lang, - path: a.path, - }; - }) : [], - output: `${options.output}.${options.mp4 ? 'mp4' : 'mkv'}`, - subtitles: data.filter(a => a.type === 'Subtitle').map((a) : SubtitleInput => { - if (a.type === 'Video') - throw new Error('Never'); - if (a.type === 'Audio') - throw new Error('Never'); - return { - file: a.path, - language: a.language, - closedCaption: a.cc - }; - }), - simul: data.filter(a => a.type === 'Video').map((a) : boolean => { - if (a.type === 'Subtitle') - throw new Error('Never'); - return !a.uncut as boolean; - })[0], - fonts: Merger.makeFontsList(this.cfg.dir.fonts, data.filter(a => a.type === 'Subtitle') as sxItem[]), - videoAndAudio: hasAudioStreams ? [] : data.filter(a => a.type === 'Video').map((a) : MergerInput => { - if (a.type === 'Subtitle') - throw new Error('Never'); - return { - lang: a.lang, - path: a.path, - }; - }), - videoTitle: options.videoTitle, - options: { - ffmpeg: options.ffmpegOptions, - mkvmerge: options.mkvmergeOptions - }, - defaults: { - audio: options.defaultAudio, - sub: options.defaultSub - }, - ccTag: options.ccTag - }); - const bin = Merger.checkMerger(this.cfg.bin, options.mp4, options.forceMuxer); - // collect fonts info - // mergers - let isMuxed = false; - if (options.syncTiming) { - await merger.createDelays(); - } - if (bin.MKVmerge) { - await merger.merge('mkvmerge', bin.MKVmerge); - isMuxed = true; - } else if (bin.FFmpeg) { - await merger.merge('ffmpeg', bin.FFmpeg); - isMuxed = true; - } else{ - console.info('\nDone!\n'); - return; - } - if (isMuxed && !options.nocleanup) - merger.cleanUp(); - } - - public sleep(ms: number) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); - } + return { + error: dlFailed, + data: files, + fileName: fileName ? (path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName)) || './unknown' : './unknown' + }; + } + + public async muxStreams(data: DownloadedMedia[], options: Record, inverseTrackOrder: boolean = true) { + this.cfg.bin = await yamlCfg.loadBinCfg(); + let hasAudioStreams = false; + if (options.novids || data.filter((a) => a.type === 'Video').length === 0) return console.info('Skip muxing since no vids are downloaded'); + if (data.some((a) => a.type === 'Audio')) { + hasAudioStreams = true; + } + const merger = new Merger({ + onlyVid: hasAudioStreams + ? data + .filter((a) => a.type === 'Video') + .map((a): MergerInput => { + if (a.type === 'Subtitle') throw new Error('Never'); + return { + lang: a.lang, + path: a.path + }; + }) + : [], + skipSubMux: options.skipSubMux, + inverseTrackOrder: inverseTrackOrder, + keepAllVideos: options.keepAllVideos, + onlyAudio: hasAudioStreams + ? data + .filter((a) => a.type === 'Audio') + .map((a): MergerInput => { + if (a.type === 'Subtitle') throw new Error('Never'); + return { + lang: a.lang, + path: a.path + }; + }) + : [], + output: `${options.output}.${options.mp4 ? 'mp4' : 'mkv'}`, + subtitles: data + .filter((a) => a.type === 'Subtitle') + .map((a): SubtitleInput => { + if (a.type === 'Video') throw new Error('Never'); + if (a.type === 'Audio') throw new Error('Never'); + return { + file: a.path, + language: a.language, + closedCaption: a.cc + }; + }), + simul: data + .filter((a) => a.type === 'Video') + .map((a): boolean => { + if (a.type === 'Subtitle') throw new Error('Never'); + return !a.uncut as boolean; + })[0], + fonts: Merger.makeFontsList(this.cfg.dir.fonts, data.filter((a) => a.type === 'Subtitle') as sxItem[]), + videoAndAudio: hasAudioStreams + ? [] + : data + .filter((a) => a.type === 'Video') + .map((a): MergerInput => { + if (a.type === 'Subtitle') throw new Error('Never'); + return { + lang: a.lang, + path: a.path + }; + }), + videoTitle: options.videoTitle, + options: { + ffmpeg: options.ffmpegOptions, + mkvmerge: options.mkvmergeOptions + }, + defaults: { + audio: options.defaultAudio, + sub: options.defaultSub + }, + ccTag: options.ccTag + }); + const bin = Merger.checkMerger(this.cfg.bin, options.mp4, options.forceMuxer); + // collect fonts info + // mergers + let isMuxed = false; + if (options.syncTiming) { + await merger.createDelays(); + } + if (bin.MKVmerge) { + await merger.merge('mkvmerge', bin.MKVmerge); + isMuxed = true; + } else if (bin.FFmpeg) { + await merger.merge('ffmpeg', bin.FFmpeg); + isMuxed = true; + } else { + console.info('\nDone!\n'); + return; + } + if (isMuxed && !options.nocleanup) merger.cleanUp(); + } + + public sleep(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + } } diff --git a/index.ts b/index.ts index 6b6e90e..45fe889 100644 --- a/index.ts +++ b/index.ts @@ -7,94 +7,97 @@ import { makeCommand, addToArchive } from './modules/module.downloadArchive'; import update from './modules/module.updater'; (async () => { - const cfg = yamlCfg.loadCfg(); - const argv = appArgv(cfg.cli); - if (!argv.skipUpdate) - await update(argv.update); + const cfg = yamlCfg.loadCfg(); + const argv = appArgv(cfg.cli); + if (!argv.skipUpdate) await update(argv.update); - if (argv.all && argv.but) { - console.error('--all and --but exclude each other!'); - return; - } + if (argv.all && argv.but) { + console.error('--all and --but exclude each other!'); + return; + } - if (argv.addArchive) { - if (argv.service === 'crunchy') { - if (argv.s === undefined && argv.series === undefined) - return console.error('`-s` or `--srz` not found'); - if (argv.s && argv.series) - return console.error('Both `-s` and `--srz` found'); - addToArchive({ - service: 'crunchy', - type: argv.s === undefined ? 'srz' : 's' - }, (argv.s === undefined ? argv.series : argv.s) as string); - console.info('Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s)); - } else if (argv.service === 'hidive') { - if (argv.s === undefined) - return console.error('`-s` not found'); - addToArchive({ - service: 'hidive', - //type: argv.s === undefined ? 'srz' : 's' - type: 's' - }, (argv.s === undefined ? argv.series : argv.s) as string); - console.info('Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s)); - } else if (argv.service === 'ao') { - if (argv.s === undefined) - return console.error('`-s` not found'); - addToArchive({ - service: 'hidive', - //type: argv.s === undefined ? 'srz' : 's' - type: 's' - }, (argv.s === undefined ? argv.series : argv.s) as string); - console.info('Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s)); - } - } else if (argv.downloadArchive) { - const ids = makeCommand(argv.service); - for (const id of ids) { - overrideArguments(cfg.cli, id); - /* Reimport module to override appArgv */ - Object.keys(require.cache).forEach(key => { - if (key.endsWith('crunchy.js') || key.endsWith('hidive.js') || key.endsWith('ao.js')) - delete require.cache[key]; - }); - let service: ServiceClass; - switch(argv.service) { - case 'crunchy': - service = new (await import('./crunchy')).default; - break; - case 'hidive': - service = new (await import('./hidive')).default; - break; - case 'ao': - service = new (await import('./ao')).default; - break; - case 'adn': - service = new (await import('./adn')).default; - break; - default: - service = new (await import(`./${argv.service}`)).default; - break; - } - await service.cli(); - } - } else { - let service: ServiceClass; - switch(argv.service) { - case 'crunchy': - service = new (await import('./crunchy')).default; - break; - case 'hidive': - service = new (await import('./hidive')).default; - break; - case 'ao': - service = new (await import('./ao')).default; - break; - case 'adn': - service = new (await import('./adn')).default; - break; - default: - service = new (await import(`./${argv.service}`)).default; - break; - } - await service.cli(); - } -})(); \ No newline at end of file + if (argv.addArchive) { + if (argv.service === 'crunchy') { + if (argv.s === undefined && argv.series === undefined) return console.error('`-s` or `--srz` not found'); + if (argv.s && argv.series) return console.error('Both `-s` and `--srz` found'); + addToArchive( + { + service: 'crunchy', + type: argv.s === undefined ? 'srz' : 's' + }, + (argv.s === undefined ? argv.series : argv.s) as string + ); + console.info('Added %s to the downloadArchive list', argv.s === undefined ? argv.series : argv.s); + } else if (argv.service === 'hidive') { + if (argv.s === undefined) return console.error('`-s` not found'); + addToArchive( + { + service: 'hidive', + //type: argv.s === undefined ? 'srz' : 's' + type: 's' + }, + (argv.s === undefined ? argv.series : argv.s) as string + ); + console.info('Added %s to the downloadArchive list', argv.s === undefined ? argv.series : argv.s); + } else if (argv.service === 'ao') { + if (argv.s === undefined) return console.error('`-s` not found'); + addToArchive( + { + service: 'hidive', + //type: argv.s === undefined ? 'srz' : 's' + type: 's' + }, + (argv.s === undefined ? argv.series : argv.s) as string + ); + console.info('Added %s to the downloadArchive list', argv.s === undefined ? argv.series : argv.s); + } + } else if (argv.downloadArchive) { + const ids = makeCommand(argv.service); + for (const id of ids) { + overrideArguments(cfg.cli, id); + /* Reimport module to override appArgv */ + Object.keys(require.cache).forEach((key) => { + if (key.endsWith('crunchy.js') || key.endsWith('hidive.js') || key.endsWith('ao.js')) delete require.cache[key]; + }); + let service: ServiceClass; + switch (argv.service) { + case 'crunchy': + service = new (await import('./crunchy')).default(); + break; + case 'hidive': + service = new (await import('./hidive')).default(); + break; + case 'ao': + service = new (await import('./ao')).default(); + break; + case 'adn': + service = new (await import('./adn')).default(); + break; + default: + service = new (await import(`./${argv.service}`)).default(); + break; + } + await service.cli(); + } + } else { + let service: ServiceClass; + switch (argv.service) { + case 'crunchy': + service = new (await import('./crunchy')).default(); + break; + case 'hidive': + service = new (await import('./hidive')).default(); + break; + case 'ao': + service = new (await import('./ao')).default(); + break; + case 'adn': + service = new (await import('./adn')).default(); + break; + default: + service = new (await import(`./${argv.service}`)).default(); + break; + } + await service.cli(); + } +})(); diff --git a/modules/build-docs.ts b/modules/build-docs.ts index 07efb8d..0cbc7b8 100644 --- a/modules/build-docs.ts +++ b/modules/build-docs.ts @@ -3,28 +3,28 @@ import fs from 'fs'; import path from 'path'; import { args, groups } from './module.args'; -const transformService = (str: Array<'crunchy'|'hidive'|'ao'|'adn'|'all'>) => { - const services: string[] = []; - str.forEach(function(part) { - switch(part) { - case 'crunchy': - services.push('Crunchyroll'); - break; - case 'hidive': - services.push('Hidive'); - break; - case 'ao': - services.push('AnimeOnegai'); - break; - case 'adn': - services.push('AnimationDigitalNetwork'); - break; - case 'all': - services.push('All'); - break; - } - }); - return services.join(', '); +const transformService = (str: Array<'crunchy' | 'hidive' | 'ao' | 'adn' | 'all'>) => { + const services: string[] = []; + str.forEach(function (part) { + switch (part) { + case 'crunchy': + services.push('Crunchyroll'); + break; + case 'hidive': + services.push('Hidive'); + break; + case 'ao': + services.push('AnimeOnegai'); + break; + case 'adn': + services.push('AnimationDigitalNetwork'); + break; + case 'all': + services.push('All'); + break; + } + }); + return services.join(', '); }; let docs = `# ${packageJSON.name} (v${packageJSON.version}) @@ -45,32 +45,35 @@ This tool is not responsible for your actions; please make an informed decision `; Object.entries(groups).forEach(([key, value]) => { - docs += `\n### ${value.slice(0, -1)}\n`; - - docs += args.filter(a => a.group === key).map(argument => { - return [`#### \`${argument.name.length > 1 ? '--' : '-'}${argument.name}\``, - `| **Service** | **Usage** | **Type** | **Required** | **Alias** | ${argument.choices ? '**Choices** |' : ''} ${argument.default ? '**Default** |' : ''}**cli-default Entry**`, - `| --- | --- | --- | --- | --- | ${argument.choices ? '--- | ' : ''}${argument.default ? '--- | ' : ''}---| `, - `| ${transformService(argument.service)} | \`${argument.name.length > 1 ? '--' : '-'}${argument.name} ${argument.usage}\` | \`${argument.type}\` | \`${argument.demandOption ? 'Yes' : 'No'}\`|` - + ` \`${(argument.alias ? `${argument.alias.length > 1 ? '--' : '-'}${argument.alias}` : undefined) ?? 'NaN'}\` |` - + `${argument.choices ? ` [${argument.choices.map(a => `\`${a || '\'\''}\``).join(', ')}] |` : ''}` - + `${argument.default ? ` \`${ - typeof argument.default === 'object' - ? Array.isArray(argument.default) - ? JSON.stringify(argument.default) - : (argument.default as any).default - : argument.default - }\`|` : ''}` - + ` ${typeof argument.default === 'object' && !Array.isArray(argument.default) - ? `\`${argument.default.name || argument.name}: \`` - : '`NaN`' - } |`, - '', - argument.docDescribe === true ? argument.describe : argument.docDescribe - ].join('\n'); - }).join('\n'); + docs += `\n### ${value.slice(0, -1)}\n`; + + docs += args + .filter((a) => a.group === key) + .map((argument) => { + return [ + `#### \`${argument.name.length > 1 ? '--' : '-'}${argument.name}\``, + `| **Service** | **Usage** | **Type** | **Required** | **Alias** | ${argument.choices ? '**Choices** |' : ''} ${argument.default ? '**Default** |' : ''}**cli-default Entry**`, + `| --- | --- | --- | --- | --- | ${argument.choices ? '--- | ' : ''}${argument.default ? '--- | ' : ''}---| `, + `| ${transformService(argument.service)} | \`${argument.name.length > 1 ? '--' : '-'}${argument.name} ${argument.usage}\` | \`${argument.type}\` | \`${argument.demandOption ? 'Yes' : 'No'}\`|` + + ` \`${(argument.alias ? `${argument.alias.length > 1 ? '--' : '-'}${argument.alias}` : undefined) ?? 'NaN'}\` |` + + `${argument.choices ? ` [${argument.choices.map((a) => `\`${a || "''"}\``).join(', ')}] |` : ''}` + + `${ + argument.default + ? ` \`${ + typeof argument.default === 'object' + ? Array.isArray(argument.default) + ? JSON.stringify(argument.default) + : (argument.default as any).default + : argument.default + }\`|` + : '' + }` + + ` ${typeof argument.default === 'object' && !Array.isArray(argument.default) ? `\`${argument.default.name || argument.name}: \`` : '`NaN`'} |`, + '', + argument.docDescribe === true ? argument.describe : argument.docDescribe + ].join('\n'); + }) + .join('\n'); }); - - -fs.writeFileSync(path.resolve(__dirname, '..', 'docs', 'DOCUMENTATION.md'), docs); \ No newline at end of file +fs.writeFileSync(path.resolve(__dirname, '..', 'docs', 'DOCUMENTATION.md'), docs); diff --git a/modules/build.ts b/modules/build.ts index cb8a925..8465c84 100644 --- a/modules/build.ts +++ b/modules/build.ts @@ -7,112 +7,103 @@ import { execSync } from 'child_process'; import { console } from './log'; import esbuild from 'esbuild'; import path from 'path'; -import { builtinModules } from 'module'; const buildsDir = './_builds'; -const nodeVer = 'node20-'; +const nodeVer = 'node22-'; -type BuildTypes = `${'windows'|'macos'|'linux'|'linuxstatic'|'alpine'}-${'x64'|'arm64'}`|'linuxstatic-armv7' +type BuildTypes = `${'windows' | 'macos' | 'linux' | 'linuxstatic' | 'alpine'}-${'x64' | 'arm64'}` | 'linuxstatic-armv7'; (async () => { - const buildType = process.argv[2] as BuildTypes; - const isGUI = process.argv[3] === 'true'; + const buildType = process.argv[2] as BuildTypes; + const isGUI = process.argv[3] === 'true'; - buildBinary(buildType, isGUI); + buildBinary(buildType, isGUI); })(); // main async function buildBinary(buildType: BuildTypes, gui: boolean) { - const buildStr = 'multi-downloader-nx'; - const acceptablePlatforms = ['windows','linux','linuxstatic','macos','alpine']; - const acceptableArchs = ['x64','arm64']; - const acceptableBuilds: string[] = ['linuxstatic-armv7']; - for (const platform of acceptablePlatforms) { - for (const arch of acceptableArchs) { - acceptableBuilds.push(platform+'-'+arch); - } - } - if(!acceptableBuilds.includes(buildType)){ - console.error('Unknown build type!'); - process.exit(1); - } - await modulesCleanup('.'); - if(!fs.existsSync(buildsDir)){ - fs.mkdirSync(buildsDir); - } - const buildFull = `${buildStr}-${getFriendlyName(buildType)}-${gui ? 'gui' : 'cli'}`; - const buildDir = `${buildsDir}/${buildFull}`; - if(fs.existsSync(buildDir)){ - fs.removeSync(buildDir); - } - fs.mkdirSync(buildDir); - console.info('Running esbuild'); + const buildStr = 'multi-downloader-nx'; + const acceptablePlatforms = ['windows', 'linux', 'linuxstatic', 'macos', 'alpine']; + const acceptableArchs = ['x64', 'arm64']; + const acceptableBuilds: string[] = ['linuxstatic-armv7']; + for (const platform of acceptablePlatforms) { + for (const arch of acceptableArchs) { + acceptableBuilds.push(platform + '-' + arch); + } + } + if (!acceptableBuilds.includes(buildType)) { + console.error('Unknown build type!'); + process.exit(1); + } + await modulesCleanup('.'); + if (!fs.existsSync(buildsDir)) { + fs.mkdirSync(buildsDir); + } + const buildFull = `${buildStr}-${getFriendlyName(buildType)}-${gui ? 'gui' : 'cli'}`; + const buildDir = `${buildsDir}/${buildFull}`; + if (fs.existsSync(buildDir)) { + fs.removeSync(buildDir); + } + fs.mkdirSync(buildDir); + console.info('Running esbuild'); - const build = await esbuild.build({ - entryPoints: [ - gui ? 'gui.js' : 'index.js', - ], - sourceRoot: './', - bundle: true, - platform: 'node', - format: 'cjs', - treeShaking: true, - // External source map for debugging - sourcemap: true, - // Minify and keep the original names - minify: true, - keepNames: true, - outfile: path.join(buildsDir, 'index.cjs'), - metafile: true, - external: ['cheerio', 'sleep', ...builtinModules] - }); + const build = await esbuild.build({ + entryPoints: [gui ? 'gui.js' : 'index.js'], + sourceRoot: './', + bundle: true, + platform: 'node', + format: 'cjs', + treeShaking: true, + // External source map for debugging + sourcemap: true, + // Minify and keep the original names + minify: true, + keepNames: true, + outfile: path.join(buildsDir, 'index.cjs'), + metafile: true, + external: ['cheerio', 'sleep', 'readline/promises'] + }); - if (build.errors?.length > 0) console.error(build.errors); - if (build.warnings?.length > 0) console.warn(build.warnings); + if (build.errors?.length > 0) console.error(build.errors); + if (build.warnings?.length > 0) console.warn(build.warnings); - const buildConfig = [ - `${buildsDir}/index.cjs`, - '--target', nodeVer + buildType, - '--output', `${buildDir}/${pkg.short_name}`, - '--compress', 'GZip' - ]; - console.info(`[Build] Build configuration: ${buildFull}`); - try { - await exec(buildConfig); - } - catch(e){ - console.info(e); - process.exit(1); - } - fs.mkdirSync(`${buildDir}/config`); - fs.mkdirSync(`${buildDir}/videos`); - fs.mkdirSync(`${buildDir}/widevine`); - fs.mkdirSync(`${buildDir}/playready`); - fs.copySync('./config/bin-path.yml', `${buildDir}/config/bin-path.yml`); - fs.copySync('./config/cli-defaults.yml', `${buildDir}/config/cli-defaults.yml`); - fs.copySync('./config/dir-path.yml', `${buildDir}/config/dir-path.yml`); - fs.copySync('./config/gui.yml', `${buildDir}/config/gui.yml`); - fs.copySync('./modules/cmd-here.bat', `${buildDir}/cmd-here.bat`); - fs.copySync('./modules/NotoSans-Regular.ttf', `${buildDir}/NotoSans-Regular.ttf`); - fs.copySync('./package.json', `${buildDir}/package.json`); - fs.copySync('./docs/', `${buildDir}/docs/`); - fs.copySync('./LICENSE.md', `${buildDir}/docs/LICENSE.md`); - if (gui) { - fs.copySync('./gui', `${buildDir}/gui`); - fs.copySync('./node_modules/open/xdg-open', `${buildDir}/xdg-open`); - } - if(fs.existsSync(`${buildsDir}/${buildFull}.7z`)){ - fs.removeSync(`${buildsDir}/${buildFull}.7z`); - } - execSync(`7z a -t7z "${buildsDir}/${buildFull}.7z" "${buildDir}"`,{stdio:[0,1,2]}); + const buildConfig = [`${buildsDir}/index.cjs`, '--target', nodeVer + buildType, '--output', `${buildDir}/${pkg.short_name}`, '--public', '--compress', 'GZip']; + console.info(`[Build] Build configuration: ${buildFull}`); + try { + await exec(buildConfig); + } catch (e) { + console.info(e); + process.exit(1); + } + fs.mkdirSync(`${buildDir}/config`); + fs.mkdirSync(`${buildDir}/videos`); + fs.mkdirSync(`${buildDir}/widevine`); + fs.mkdirSync(`${buildDir}/playready`); + fs.copySync('./config/bin-path.yml', `${buildDir}/config/bin-path.yml`); + fs.copySync('./config/cli-defaults.yml', `${buildDir}/config/cli-defaults.yml`); + fs.copySync('./config/dir-path.yml', `${buildDir}/config/dir-path.yml`); + fs.copySync('./config/gui.yml', `${buildDir}/config/gui.yml`); + fs.copySync('./modules/cmd-here.bat', `${buildDir}/cmd-here.bat`); + fs.copySync('./modules/NotoSans-Regular.ttf', `${buildDir}/NotoSans-Regular.ttf`); + fs.copySync('./package.json', `${buildDir}/package.json`); + fs.copySync('./docs/', `${buildDir}/docs/`); + fs.copySync('./LICENSE.md', `${buildDir}/docs/LICENSE.md`); + if (gui) { + fs.copySync('./gui', `${buildDir}/gui`); + fs.copySync('./node_modules/open/xdg-open', `${buildDir}/xdg-open`); + } + if (fs.existsSync(`${buildsDir}/${buildFull}.7z`)) { + fs.removeSync(`${buildsDir}/${buildFull}.7z`); + } + execSync(`7z a -t7z "${buildsDir}/${buildFull}.7z" "${buildDir}"`, { stdio: [0, 1, 2] }); } function getFriendlyName(buildString: string): string { - if (buildString.includes('armv7')) { - return 'android'; - } - if (buildString.includes('linuxstatic')) { - buildString = buildString.replace('linuxstatic', 'linux'); - } - return buildString; -} \ No newline at end of file + if (buildString.includes('armv7')) { + return 'android'; + } + if (buildString.includes('linuxstatic')) { + buildString = buildString.replace('linuxstatic', 'linux'); + } + return buildString; +} diff --git a/modules/cdm.ts b/modules/cdm.ts index 66b413d..5e39145 100644 --- a/modules/cdm.ts +++ b/modules/cdm.ts @@ -10,176 +10,181 @@ import { ofetch } from 'ofetch'; //read cdm files located in the same directory let privateKey: Buffer = Buffer.from([]), - identifierBlob: Buffer = Buffer.from([]), - prd: Buffer = Buffer.from([]), - prd_cdm: Cdm | undefined; + identifierBlob: Buffer = Buffer.from([]), + prd: Buffer = Buffer.from([]), + prd_cdm: Cdm | undefined; export let cdm: 'widevine' | 'playready'; export let canDecrypt: boolean; try { - const files_prd = fs.readdirSync(path.join(workingDir, 'playready')); - const prd_file_found = files_prd.find((f) => f.includes('.prd')); - try { - if (prd_file_found) { - const file_prd = path.join(workingDir, 'playready', prd_file_found); - const stats = fs.statSync(file_prd); - if (stats.size < 1024 * 8 && stats.isFile()) { - const fileContents = fs.readFileSync(file_prd, { - encoding: 'utf8' - }); - if (fileContents.includes('CERT')) { - prd = fs.readFileSync(file_prd); - const device = Device.loads(prd); - prd_cdm = Cdm.fromDevice(device); - } - } - } - } catch (e) { - console.error('Error loading Playready CDM, ensure the CDM is provisioned as a V3 Device and not malformed. For more informations read the readme.'); - prd = Buffer.from([]); - } + const files_prd = fs.readdirSync(path.join(workingDir, 'playready')); + const prd_file_found = files_prd.find((f) => f.includes('.prd')); + try { + if (prd_file_found) { + const file_prd = path.join(workingDir, 'playready', prd_file_found); + const stats = fs.statSync(file_prd); + if (stats.size < 1024 * 8 && stats.isFile()) { + const fileContents = fs.readFileSync(file_prd, { + encoding: 'utf8' + }); + if (fileContents.includes('CERT')) { + prd = fs.readFileSync(file_prd); + const device = Device.loads(prd); + prd_cdm = Cdm.fromDevice(device); + } + } + } + } catch (e) { + console.error('Error loading Playready CDM, ensure the CDM is provisioned as a V3 Device and not malformed. For more informations read the readme.'); + prd = Buffer.from([]); + } - const files_wvd = fs.readdirSync(path.join(workingDir, 'widevine')); - try { - files_wvd.forEach(function (file) { - file = path.join(workingDir, 'widevine', file); - const stats = fs.statSync(file); - if (stats.size < 1024 * 8 && stats.isFile()) { - const fileContents = fs.readFileSync(file, { encoding: 'utf8' }); - if ((fileContents.startsWith('-----BEGIN RSA PRIVATE KEY-----') && fileContents.endsWith('-----END RSA PRIVATE KEY-----')) || (fileContents.startsWith('-----BEGIN PRIVATE KEY-----') && fileContents.endsWith('-----END PRIVATE KEY-----'))) { - privateKey = fs.readFileSync(file); - } - if (fileContents.includes('widevine_cdm_version') && fileContents.includes('oem_crypto_security_patch_level') && !fileContents.startsWith('WVD')) { - identifierBlob = fs.readFileSync(file); - } - if (fileContents.startsWith('WVD')) { - console.warn('Found WVD file in folder, AniDL currently only supports device_client_id_blob and device_private_key, make sure to have them in the widevine folder.'); - } - } - }); - } catch (e) { - console.error('Error loading Widevine CDM, malformed client blob or private key.'); - privateKey = Buffer.from([]); - identifierBlob = Buffer.from([]); - } + const files_wvd = fs.readdirSync(path.join(workingDir, 'widevine')); + try { + files_wvd.forEach(function (file) { + file = path.join(workingDir, 'widevine', file); + const stats = fs.statSync(file); + if (stats.size < 1024 * 8 && stats.isFile()) { + const fileContents = fs.readFileSync(file, { encoding: 'utf8' }); + if ( + (fileContents.startsWith('-----BEGIN RSA PRIVATE KEY-----') && fileContents.endsWith('-----END RSA PRIVATE KEY-----')) || + (fileContents.startsWith('-----BEGIN PRIVATE KEY-----') && fileContents.endsWith('-----END PRIVATE KEY-----')) + ) { + privateKey = fs.readFileSync(file); + } + if (fileContents.includes('widevine_cdm_version') && fileContents.includes('oem_crypto_security_patch_level') && !fileContents.startsWith('WVD')) { + identifierBlob = fs.readFileSync(file); + } + if (fileContents.startsWith('WVD')) { + console.warn( + 'Found WVD file in folder, AniDL currently only supports device_client_id_blob and device_private_key, make sure to have them in the widevine folder.' + ); + } + } + }); + } catch (e) { + console.error('Error loading Widevine CDM, malformed client blob or private key.'); + privateKey = Buffer.from([]); + identifierBlob = Buffer.from([]); + } - if (privateKey.length !== 0 && identifierBlob.length !== 0) { - cdm = 'widevine'; - canDecrypt = true; - } else if (prd.length !== 0) { - cdm = 'playready'; - canDecrypt = true; - } else if (privateKey.length === 0 && identifierBlob.length !== 0) { - console.warn('Private key missing'); - canDecrypt = false; - } else if (identifierBlob.length === 0 && privateKey.length !== 0) { - console.warn('Identifier blob missing'); - canDecrypt = false; - } else if (prd.length == 0) { - canDecrypt = false; - } else { - canDecrypt = false; - } + if (privateKey.length !== 0 && identifierBlob.length !== 0) { + cdm = 'widevine'; + canDecrypt = true; + } else if (prd.length !== 0) { + cdm = 'playready'; + canDecrypt = true; + } else if (privateKey.length === 0 && identifierBlob.length !== 0) { + console.warn('Private key missing'); + canDecrypt = false; + } else if (identifierBlob.length === 0 && privateKey.length !== 0) { + console.warn('Identifier blob missing'); + canDecrypt = false; + } else if (prd.length == 0) { + canDecrypt = false; + } else { + canDecrypt = false; + } } catch (e) { - console.error(e); - canDecrypt = false; + console.error(e); + canDecrypt = false; } export async function getKeysWVD(pssh: string | undefined, licenseServer: string, authData: Record): Promise { - if (!pssh || !canDecrypt) return []; - //pssh found in the mpd manifest - const psshBuffer = Buffer.from(pssh, 'base64'); + if (!pssh || !canDecrypt) return []; + //pssh found in the mpd manifest + const psshBuffer = Buffer.from(pssh, 'base64'); - //Create a new widevine session - const session = new Session({ privateKey, identifierBlob }, psshBuffer); + //Create a new widevine session + const session = new Session({ privateKey, identifierBlob }, psshBuffer); - //Generate license - const data = await ofetch(licenseServer, { - method: 'POST', - body: session.createLicenseRequest(), - headers: authData, - responseType: 'arrayBuffer' - }).catch((error) => { - if (error.status && error.statusText) { - console.error(`${error.name} ${error.status}: ${error.statusText}`); - } else { - console.error(`${error.name}: ${error.message}`); - } + //Generate license + const data = await ofetch(licenseServer, { + method: 'POST', + body: session.createLicenseRequest(), + headers: authData, + responseType: 'arrayBuffer' + }).catch((error) => { + if (error.status && error.statusText) { + console.error(`${error.name} ${error.status}: ${error.statusText}`); + } else { + console.error(`${error.name}: ${error.message}`); + } - if (!error.data) return; - const data = error.data instanceof ArrayBuffer ? new TextDecoder().decode(error.data) : error.data; - if (data) { - const docTitle = data.match(/(.*)<\/title>/); - if (docTitle) { - console.error(docTitle[1]); - } - if (error.status && error.status != 404 && error.status != 403) { - console.error('Body:', data); - } - } - }); + if (!error.data) return; + const data = error.data instanceof ArrayBuffer ? new TextDecoder().decode(error.data) : error.data; + if (data) { + const docTitle = data.match(/<title>(.*)<\/title>/); + if (docTitle) { + console.error(docTitle[1]); + } + if (error.status && error.status != 404 && error.status != 403) { + console.error('Body:', data); + } + } + }); - if (data) { - //Parse License and return keys - const text = new TextDecoder().decode(data); - try { - const json = JSON.parse(text); - return session.parseLicense(Buffer.from(json['license'], 'base64')) as KeyContainer[]; - } catch { - return session.parseLicense(Buffer.from(new Uint8Array(data))) as KeyContainer[]; - } - } else { - console.error('License request failed'); - return []; - } + if (data) { + //Parse License and return keys + const text = new TextDecoder().decode(data); + try { + const json = JSON.parse(text); + return session.parseLicense(Buffer.from(json['license'], 'base64')) as KeyContainer[]; + } catch { + return session.parseLicense(Buffer.from(new Uint8Array(data))) as KeyContainer[]; + } + } else { + console.error('License request failed'); + return []; + } } export async function getKeysPRD(pssh: string | undefined, licenseServer: string, authData: Record<string, string>): Promise<KeyContainer[]> { - if (!pssh || !canDecrypt || !prd_cdm) return []; - const pssh_parsed = new PSSH(pssh); + if (!pssh || !canDecrypt || !prd_cdm) return []; + const pssh_parsed = new PSSH(pssh); - //Create a new playready session - const session = prd_cdm.getLicenseChallenge(pssh_parsed.get_wrm_headers(true)[0]); + //Create a new playready session + const session = prd_cdm.getLicenseChallenge(pssh_parsed.get_wrm_headers(true)[0]); - //Generate license - const data = await ofetch(licenseServer, { - method: 'POST', - body: session, - headers: authData, - responseType: 'text' - }).catch((error) => { - if (error && error.status && error.statusText) { - console.error(`${error.name} ${error.status}: ${error.statusText}`); - } else { - console.error(`${error.name}: ${error.message}`); - } + //Generate license + const data = await ofetch(licenseServer, { + method: 'POST', + body: session, + headers: authData, + responseType: 'text' + }).catch((error) => { + if (error && error.status && error.statusText) { + console.error(`${error.name} ${error.status}: ${error.statusText}`); + } else { + console.error(`${error.name}: ${error.message}`); + } - if (!error.data) return; - const docTitle = error.data.match(/<title>(.*)<\/title>/); - if (docTitle) { - console.error(docTitle[1]); - } - if (error.status && error.status != 404 && error.status != 403) { - console.error('Body:', error.data); - } - }); + if (!error.data) return; + const docTitle = error.data.match(/<title>(.*)<\/title>/); + if (docTitle) { + console.error(docTitle[1]); + } + if (error.status && error.status != 404 && error.status != 403) { + console.error('Body:', error.data); + } + }); - if (data) { - //Parse License and return keys - try { - const keys = prd_cdm.parseLicense(data); + if (data) { + //Parse License and return keys + try { + const keys = prd_cdm.parseLicense(data); - return keys.map((k) => { - return { - kid: k.key_id, - key: k.key - }; - }); - } catch { - console.error('License parsing failed'); - return []; - } - } else { - console.error('License request failed'); - return []; - } + return keys.map((k) => { + return { + kid: k.key_id, + key: k.key + }; + }); + } catch { + console.error('License parsing failed'); + return []; + } + } else { + console.error('License request failed'); + return []; + } } diff --git a/modules/hls-download-got.ts b/modules/hls-download-got.ts index c9c8d2e..2280145 100644 --- a/modules/hls-download-got.ts +++ b/modules/hls-download-got.ts @@ -17,7 +17,7 @@ // const isResponseOk = (response: Response) => { // const {statusCode} = response; // const limitStatusCode = response.request.options.followRedirect ? 299 : 399; - + // return (statusCode >= 200 && statusCode <= limitStatusCode) || statusCode === 304; // }; // if (isResponseOk(res)) { @@ -94,8 +94,8 @@ // // check playlist // if( // !options -// || !options.m3u8json -// || !options.m3u8json.segments +// || !options.m3u8json +// || !options.m3u8json.segments // || options.m3u8json.segments.length === 0 // ){ // throw new Error('Playlist is empty!'); @@ -386,7 +386,7 @@ // return crypto.createDecipheriv('aes-128-cbc', this.data.keys[kURI], iv); // } // } - + // const extFn = { // getURI: (uri: string, baseurl?: string) => { // const httpURI = /^https{0,1}:/.test(uri); @@ -453,5 +453,5 @@ // return got(uri, options); // } // }; - -// export default hlsDownload; \ No newline at end of file + +// export default hlsDownload; diff --git a/modules/hls-download.ts b/modules/hls-download.ts index 82d4551..abca990 100644 --- a/modules/hls-download.ts +++ b/modules/hls-download.ts @@ -12,412 +12,412 @@ import Helper from './module.helper'; export type HLSCallback = (data: ProgressData) => unknown; export type M3U8Json = { - segments: Record<string, unknown>[]; - mediaSequence?: number; + segments: Record<string, unknown>[]; + mediaSequence?: number; }; type Segment = { - uri: string; - key: Key; - byterange?: { - offset: number; - length: number; - }; + uri: string; + key: Key; + byterange?: { + offset: number; + length: number; + }; }; type Key = { - uri: string; - iv: number[]; + uri: string; + iv: number[]; }; export type HLSOptions = { - m3u8json: M3U8Json; - output?: string; - threads?: number; - retries?: number; - offset?: number; - baseurl?: string; - skipInit?: boolean; - timeout?: number; - fsRetryTime?: number; - override?: 'Y' | 'y' | 'N' | 'n' | 'C' | 'c'; - callback?: HLSCallback; + m3u8json: M3U8Json; + output?: string; + threads?: number; + retries?: number; + offset?: number; + baseurl?: string; + skipInit?: boolean; + timeout?: number; + fsRetryTime?: number; + override?: 'Y' | 'y' | 'N' | 'n' | 'C' | 'c'; + callback?: HLSCallback; }; type Data = { - parts: { - first: number; - total: number; - completed: number; - }; - m3u8json: M3U8Json; - outputFile: string; - threads: number; - retries: number; - offset: number; - baseurl?: string; - skipInit?: boolean; - keys: { - [uri: string]: Buffer | string; - }; - timeout: number; - checkPartLength: boolean; - isResume: boolean; - bytesDownloaded: number; - waitTime: number; - callback?: HLSCallback; - override?: string; - dateStart: number; + parts: { + first: number; + total: number; + completed: number; + }; + m3u8json: M3U8Json; + outputFile: string; + threads: number; + retries: number; + offset: number; + baseurl?: string; + skipInit?: boolean; + keys: { + [uri: string]: Buffer | string; + }; + timeout: number; + checkPartLength: boolean; + isResume: boolean; + bytesDownloaded: number; + waitTime: number; + callback?: HLSCallback; + override?: string; + dateStart: number; }; // hls class class hlsDownload { - private data: Data; - constructor(options: HLSOptions) { - // check playlist - if (!options || !options.m3u8json || !options.m3u8json.segments || options.m3u8json.segments.length === 0) { - throw new Error('Playlist is empty!'); - } - // init options - this.data = { - parts: { - first: options.m3u8json.mediaSequence || 0, - total: options.m3u8json.segments.length, - completed: 0 - }, - m3u8json: options.m3u8json, - outputFile: options.output || 'stream.ts', - threads: options.threads || 5, - retries: options.retries || 4, - offset: options.offset || 0, - baseurl: options.baseurl, - skipInit: options.skipInit, - keys: {}, - timeout: options.timeout ? options.timeout : 60 * 1000, - checkPartLength: false, - isResume: options.offset ? options.offset > 0 : false, - bytesDownloaded: 0, - waitTime: options.fsRetryTime ?? 1000 * 5, - callback: options.callback, - override: options.override, - dateStart: 0 - }; - } - async download() { - // set output - const fn = this.data.outputFile; - // try load resume file - if (fsp.existsSync(fn) && fsp.existsSync(`${fn}.resume`) && this.data.offset < 1) { - try { - console.info('Resume data found! Trying to resume...'); - const resumeData = JSON.parse(await fs.readFile(`${fn}.resume`, 'utf-8')); - if (resumeData.total == this.data.m3u8json.segments.length && resumeData.completed != resumeData.total && !isNaN(resumeData.completed)) { - console.info('Resume data is ok!'); - this.data.offset = resumeData.completed; - this.data.isResume = true; - } else { - console.warn(' Resume data is wrong!'); - console.warn({ - resume: { total: resumeData.total, dled: resumeData.completed }, - current: { total: this.data.m3u8json.segments.length } - }); - } - } catch (e) { - console.error('Resume failed, downloading will be not resumed!'); - console.error(e); - } - } - // ask before rewrite file - if (fsp.existsSync(`${fn}`) && !this.data.isResume) { - let rwts = this.data.override ?? (await Helper.question(`[Q] File «${fn}» already exists! Rewrite? ([y]es/[N]o/[c]ontinue)`)); - rwts = rwts || 'N'; - if (['Y', 'y'].includes(rwts[0])) { - console.info(`Deleting «${fn}»...`); - await fs.unlink(fn); - } else if (['C', 'c'].includes(rwts[0])) { - return { ok: true, parts: this.data.parts }; - } else { - return { ok: false, parts: this.data.parts }; - } - } - // show output filename - if (fsp.existsSync(fn) && this.data.isResume) { - console.info(`Adding content to «${fn}»...`); - } else { - console.info(`Saving stream to «${fn}»...`); - } - // start time - this.data.dateStart = Date.now(); - let segments = this.data.m3u8json.segments; - // download init part - if (segments[0].map && this.data.offset === 0 && !this.data.skipInit) { - console.info('Download and save init part...'); - const initSeg = segments[0].map as Segment; - if (segments[0].key) { - initSeg.key = segments[0].key as Key; - } - try { - const initDl = await this.downloadPart(initSeg, 0, 0); - await fs.writeFile(fn, initDl.dec, { flag: 'a' }); - await fs.writeFile( - `${fn}.resume`, - JSON.stringify({ - completed: 0, - total: this.data.m3u8json.segments.length - }) - ); - console.info('Init part downloaded.'); - } catch (e: any) { - console.error(`Part init download error:\n\t${e.message}`); - return { ok: false, parts: this.data.parts }; - } - } else if (segments[0].map && this.data.offset === 0 && this.data.skipInit) { - console.warn('Skipping init part can lead to broken video!'); - } - // resuming ... - if (this.data.offset > 0) { - segments = segments.slice(this.data.offset); - console.info(`Resuming download from part ${this.data.offset + 1}...`); - this.data.parts.completed = this.data.offset; - } - // dl process - for (let p = 0; p < segments.length / this.data.threads; p++) { - // set offsets - const offset = p * this.data.threads; - const dlOffset = offset + this.data.threads; - // map download threads - const krq = new Map(), - prq = new Map(); - const res: any[] = []; - let errcnt = 0; - for (let px = offset; px < dlOffset && px < segments.length; px++) { - const curp = segments[px]; - const key = curp.key as Key; - if (key && !krq.has(key.uri) && !this.data.keys[key.uri as string]) { - krq.set(key.uri, this.downloadKey(key, px, this.data.offset)); - } - } - try { - await Promise.all(krq.values()); - } catch (er: any) { - console.error(`Key ${er.p + 1} download error:\n\t${er.message}`); - return { ok: false, parts: this.data.parts }; - } - for (let px = offset; px < dlOffset && px < segments.length; px++) { - const curp = segments[px] as Segment; - prq.set(px, () => this.downloadPart(curp, px, this.data.offset)); - } - // Parallelized part download with retry logic and optional concurrency limit - const maxConcurrency = this.data.threads; - const partEntries = [...prq.entries()]; - let index = 0; + private data: Data; + constructor(options: HLSOptions) { + // check playlist + if (!options || !options.m3u8json || !options.m3u8json.segments || options.m3u8json.segments.length === 0) { + throw new Error('Playlist is empty!'); + } + // init options + this.data = { + parts: { + first: options.m3u8json.mediaSequence || 0, + total: options.m3u8json.segments.length, + completed: 0 + }, + m3u8json: options.m3u8json, + outputFile: options.output || 'stream.ts', + threads: options.threads || 5, + retries: options.retries || 4, + offset: options.offset || 0, + baseurl: options.baseurl, + skipInit: options.skipInit, + keys: {}, + timeout: options.timeout ? options.timeout : 60 * 1000, + checkPartLength: false, + isResume: options.offset ? options.offset > 0 : false, + bytesDownloaded: 0, + waitTime: options.fsRetryTime ?? 1000 * 5, + callback: options.callback, + override: options.override, + dateStart: 0 + }; + } + async download() { + // set output + const fn = this.data.outputFile; + // try load resume file + if (fsp.existsSync(fn) && fsp.existsSync(`${fn}.resume`) && this.data.offset < 1) { + try { + console.info('Resume data found! Trying to resume...'); + const resumeData = JSON.parse(await fs.readFile(`${fn}.resume`, 'utf-8')); + if (resumeData.total == this.data.m3u8json.segments.length && resumeData.completed != resumeData.total && !isNaN(resumeData.completed)) { + console.info('Resume data is ok!'); + this.data.offset = resumeData.completed; + this.data.isResume = true; + } else { + console.warn(' Resume data is wrong!'); + console.warn({ + resume: { total: resumeData.total, dled: resumeData.completed }, + current: { total: this.data.m3u8json.segments.length } + }); + } + } catch (e) { + console.error('Resume failed, downloading will be not resumed!'); + console.error(e); + } + } + // ask before rewrite file + if (fsp.existsSync(`${fn}`) && !this.data.isResume) { + let rwts = this.data.override ?? (await Helper.question(`[Q] File «${fn}» already exists! Rewrite? ([y]es/[N]o/[c]ontinue)`)); + rwts = rwts || 'N'; + if (['Y', 'y'].includes(rwts[0])) { + console.info(`Deleting «${fn}»...`); + await fs.unlink(fn); + } else if (['C', 'c'].includes(rwts[0])) { + return { ok: true, parts: this.data.parts }; + } else { + return { ok: false, parts: this.data.parts }; + } + } + // show output filename + if (fsp.existsSync(fn) && this.data.isResume) { + console.info(`Adding content to «${fn}»...`); + } else { + console.info(`Saving stream to «${fn}»...`); + } + // start time + this.data.dateStart = Date.now(); + let segments = this.data.m3u8json.segments; + // download init part + if (segments[0].map && this.data.offset === 0 && !this.data.skipInit) { + console.info('Download and save init part...'); + const initSeg = segments[0].map as Segment; + if (segments[0].key) { + initSeg.key = segments[0].key as Key; + } + try { + const initDl = await this.downloadPart(initSeg, 0, 0); + await fs.writeFile(fn, initDl.dec, { flag: 'a' }); + await fs.writeFile( + `${fn}.resume`, + JSON.stringify({ + completed: 0, + total: this.data.m3u8json.segments.length + }) + ); + console.info('Init part downloaded.'); + } catch (e: any) { + console.error(`Part init download error:\n\t${e.message}`); + return { ok: false, parts: this.data.parts }; + } + } else if (segments[0].map && this.data.offset === 0 && this.data.skipInit) { + console.warn('Skipping init part can lead to broken video!'); + } + // resuming ... + if (this.data.offset > 0) { + segments = segments.slice(this.data.offset); + console.info(`Resuming download from part ${this.data.offset + 1}...`); + this.data.parts.completed = this.data.offset; + } + // dl process + for (let p = 0; p < segments.length / this.data.threads; p++) { + // set offsets + const offset = p * this.data.threads; + const dlOffset = offset + this.data.threads; + // map download threads + const krq = new Map(), + prq = new Map(); + const res: any[] = []; + let errcnt = 0; + for (let px = offset; px < dlOffset && px < segments.length; px++) { + const curp = segments[px]; + const key = curp.key as Key; + if (key && !krq.has(key.uri) && !this.data.keys[key.uri as string]) { + krq.set(key.uri, this.downloadKey(key, px, this.data.offset)); + } + } + try { + await Promise.all(krq.values()); + } catch (er: any) { + console.error(`Key ${er.p + 1} download error:\n\t${er.message}`); + return { ok: false, parts: this.data.parts }; + } + for (let px = offset; px < dlOffset && px < segments.length; px++) { + const curp = segments[px] as Segment; + prq.set(px, () => this.downloadPart(curp, px, this.data.offset)); + } + // Parallelized part download with retry logic and optional concurrency limit + const maxConcurrency = this.data.threads; + const partEntries = [...prq.entries()]; + let index = 0; - async function worker(this: hlsDownload) { - while (index < partEntries.length) { - const i = index++; - const [px, downloadFn] = partEntries[i]; + async function worker(this: hlsDownload) { + while (index < partEntries.length) { + const i = index++; + const [px, downloadFn] = partEntries[i]; - let retriesLeft = this.data.retries; - let success = false; - while (retriesLeft > 0 && !success) { - try { - const r = await downloadFn(); - res[px - offset] = r.dec; - success = true; - } catch (error: any) { - retriesLeft--; - console.warn(`Retrying part ${error.p + 1 + this.data.offset} (${this.data.retries - retriesLeft}/${this.data.retries})`); - if (retriesLeft > 0) { - await new Promise((resolve) => setTimeout(resolve, 1000)); - } else { - console.error(`Part ${error.p + 1 + this.data.offset} download failed after ${this.data.retries} retries:\n\t${error.message}`); - errcnt++; - } - } - } - } - } + let retriesLeft = this.data.retries; + let success = false; + while (retriesLeft > 0 && !success) { + try { + const r = await downloadFn(); + res[px - offset] = r.dec; + success = true; + } catch (error: any) { + retriesLeft--; + console.warn(`Retrying part ${error.p + 1 + this.data.offset} (${this.data.retries - retriesLeft}/${this.data.retries})`); + if (retriesLeft > 0) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + } else { + console.error(`Part ${error.p + 1 + this.data.offset} download failed after ${this.data.retries} retries:\n\t${error.message}`); + errcnt++; + } + } + } + } + } - const workers = []; - for (let i = 0; i < maxConcurrency; i++) { - workers.push(worker.call(this)); - } - await Promise.all(workers); + const workers = []; + for (let i = 0; i < maxConcurrency; i++) { + workers.push(worker.call(this)); + } + await Promise.all(workers); - // catch error - if (errcnt > 0) { - console.error(`${errcnt} parts not downloaded`); - return { ok: false, parts: this.data.parts }; - } - // write downloaded - for (const r of res) { - let error = 0; - while (error < 3) { - try { - await fs.writeFile(fn, r, { flag: 'a' }); - break; - } catch (err) { - console.error(err); - console.error(`Unable to write to file '${fn}' (Attempt ${error + 1}/3)`); - console.info(`Waiting ${Math.round(this.data.waitTime / 1000)}s before retrying`); - await new Promise<void>((resolve) => setTimeout(() => resolve(), this.data.waitTime)); - } - error++; - } - if (error === 3) { - console.error(`Unable to write content to '${fn}'.`); - return { ok: false, parts: this.data.parts }; - } - } - // log downloaded - const totalSeg = segments.length + this.data.offset; // Add the sliced lenght back so the resume data will be correct even if an resumed download fails - const downloadedSeg = dlOffset < totalSeg ? dlOffset : totalSeg; - this.data.parts.completed = downloadedSeg + this.data.offset; - const data = extFn.getDownloadInfo(this.data.dateStart, downloadedSeg, totalSeg, this.data.bytesDownloaded); - await fs.writeFile( - `${fn}.resume`, - JSON.stringify({ - completed: this.data.parts.completed, - total: totalSeg - }) - ); - function formatDLSpeedB(s: number) { - if (s < 1000000) return `${(s / 1000).toFixed(2)} KB/s`; - if (s < 1000000000) return `${(s / 1000000).toFixed(2)} MB/s`; - return `${(s / 1000000000).toFixed(2)} GB/s`; - } - function formatDLSpeedBit(s: number) { - if (s * 8 < 1000000) return `${(s * 8 / 1000).toFixed(2)} KBit/s`; - if (s * 8 < 1000000000) return `${(s * 8 / 1000000).toFixed(2)} MBit/s`; - return `${(s * 8 / 1000000000).toFixed(2)} GBit/s`; - } - console.info( - `${downloadedSeg} of ${totalSeg} parts downloaded [${data.percent}%] (${Helper.formatTime(parseInt((data.time / 1000).toFixed(0)))} | ${formatDLSpeedB(data.downloadSpeed)} / ${formatDLSpeedBit(data.downloadSpeed)})` - ); - if (this.data.callback) - this.data.callback({ - total: this.data.parts.total, - cur: this.data.parts.completed, - bytes: this.data.bytesDownloaded, - percent: data.percent, - time: data.time, - downloadSpeed: data.downloadSpeed - }); - } - // return result - await fs.unlink(`${fn}.resume`); - return { ok: true, parts: this.data.parts }; - } - async downloadPart(seg: Segment, segIndex: number, segOffset: number) { - const sURI = extFn.getURI(seg.uri, this.data.baseurl); - let decipher, part, dec; - const p = segIndex; - try { - if (seg.key != undefined) { - decipher = await this.getKey(seg.key, p, segOffset); - } - part = await extFn.getData( - p, - sURI, - { - ...(seg.byterange - ? { - Range: `bytes=${seg.byterange.offset}-${seg.byterange.offset + seg.byterange.length - 1}` - } - : {}) - }, - segOffset, - false - ); - // if (this.data.checkPartLength) { - // this.data.checkPartLength = false; - // console.warn(`Part ${segIndex + segOffset + 1}: can't check parts size!`); - // } - if (decipher == undefined) { - this.data.bytesDownloaded += Buffer.from(part).byteLength; - return { dec: Buffer.from(part), p }; - } - dec = decipher.update(Buffer.from(part)); - dec = Buffer.concat([dec, decipher.final()]); - this.data.bytesDownloaded += dec.byteLength; - } catch (error: any) { - error.p = p; - throw error; - } - return { dec, p }; - } - async downloadKey(key: Key, segIndex: number, segOffset: number) { - const kURI = extFn.getURI(key.uri, this.data.baseurl); - if (!this.data.keys[kURI]) { - try { - const rkey = await extFn.getData(segIndex, kURI, {}, segOffset, true); - return rkey; - } catch (error: any) { - error.p = segIndex; - throw error; - } - } - } - async getKey(key: Key, segIndex: number, segOffset: number) { - const kURI = extFn.getURI(key.uri, this.data.baseurl); - const p = segIndex; - if (!this.data.keys[kURI]) { - try { - const rkey = await this.downloadKey(key, segIndex, segOffset); - if (!rkey) throw new Error(); - this.data.keys[kURI] = Buffer.from(rkey); - } catch (error: any) { - error.p = p; - throw error; - } - } - // get ivs - const iv = Buffer.alloc(16); - const ivs = key.iv ? key.iv : [0, 0, 0, p + 1]; - for (let i = 0; i < ivs.length; i++) { - iv.writeUInt32BE(ivs[i], i * 4); - } - return crypto.createDecipheriv('aes-128-cbc', this.data.keys[kURI], iv); - } + // catch error + if (errcnt > 0) { + console.error(`${errcnt} parts not downloaded`); + return { ok: false, parts: this.data.parts }; + } + // write downloaded + for (const r of res) { + let error = 0; + while (error < 3) { + try { + await fs.writeFile(fn, r, { flag: 'a' }); + break; + } catch (err) { + console.error(err); + console.error(`Unable to write to file '${fn}' (Attempt ${error + 1}/3)`); + console.info(`Waiting ${Math.round(this.data.waitTime / 1000)}s before retrying`); + await new Promise<void>((resolve) => setTimeout(() => resolve(), this.data.waitTime)); + } + error++; + } + if (error === 3) { + console.error(`Unable to write content to '${fn}'.`); + return { ok: false, parts: this.data.parts }; + } + } + // log downloaded + const totalSeg = segments.length + this.data.offset; // Add the sliced lenght back so the resume data will be correct even if an resumed download fails + const downloadedSeg = dlOffset < totalSeg ? dlOffset : totalSeg; + this.data.parts.completed = downloadedSeg + this.data.offset; + const data = extFn.getDownloadInfo(this.data.dateStart, downloadedSeg, totalSeg, this.data.bytesDownloaded); + await fs.writeFile( + `${fn}.resume`, + JSON.stringify({ + completed: this.data.parts.completed, + total: totalSeg + }) + ); + function formatDLSpeedB(s: number) { + if (s < 1000000) return `${(s / 1000).toFixed(2)} KB/s`; + if (s < 1000000000) return `${(s / 1000000).toFixed(2)} MB/s`; + return `${(s / 1000000000).toFixed(2)} GB/s`; + } + function formatDLSpeedBit(s: number) { + if (s * 8 < 1000000) return `${((s * 8) / 1000).toFixed(2)} KBit/s`; + if (s * 8 < 1000000000) return `${((s * 8) / 1000000).toFixed(2)} MBit/s`; + return `${((s * 8) / 1000000000).toFixed(2)} GBit/s`; + } + console.info( + `${downloadedSeg} of ${totalSeg} parts downloaded [${data.percent}%] (${Helper.formatTime(parseInt((data.time / 1000).toFixed(0)))} | ${formatDLSpeedB(data.downloadSpeed)} / ${formatDLSpeedBit(data.downloadSpeed)})` + ); + if (this.data.callback) + this.data.callback({ + total: this.data.parts.total, + cur: this.data.parts.completed, + bytes: this.data.bytesDownloaded, + percent: data.percent, + time: data.time, + downloadSpeed: data.downloadSpeed + }); + } + // return result + await fs.unlink(`${fn}.resume`); + return { ok: true, parts: this.data.parts }; + } + async downloadPart(seg: Segment, segIndex: number, segOffset: number) { + const sURI = extFn.getURI(seg.uri, this.data.baseurl); + let decipher, part, dec; + const p = segIndex; + try { + if (seg.key != undefined) { + decipher = await this.getKey(seg.key, p, segOffset); + } + part = await extFn.getData( + p, + sURI, + { + ...(seg.byterange + ? { + Range: `bytes=${seg.byterange.offset}-${seg.byterange.offset + seg.byterange.length - 1}` + } + : {}) + }, + segOffset, + false + ); + // if (this.data.checkPartLength) { + // this.data.checkPartLength = false; + // console.warn(`Part ${segIndex + segOffset + 1}: can't check parts size!`); + // } + if (decipher == undefined) { + this.data.bytesDownloaded += Buffer.from(part).byteLength; + return { dec: Buffer.from(part), p }; + } + dec = decipher.update(Buffer.from(part)); + dec = Buffer.concat([dec, decipher.final()]); + this.data.bytesDownloaded += dec.byteLength; + } catch (error: any) { + error.p = p; + throw error; + } + return { dec, p }; + } + async downloadKey(key: Key, segIndex: number, segOffset: number) { + const kURI = extFn.getURI(key.uri, this.data.baseurl); + if (!this.data.keys[kURI]) { + try { + const rkey = await extFn.getData(segIndex, kURI, {}, segOffset, true); + return rkey; + } catch (error: any) { + error.p = segIndex; + throw error; + } + } + } + async getKey(key: Key, segIndex: number, segOffset: number) { + const kURI = extFn.getURI(key.uri, this.data.baseurl); + const p = segIndex; + if (!this.data.keys[kURI]) { + try { + const rkey = await this.downloadKey(key, segIndex, segOffset); + if (!rkey) throw new Error(); + this.data.keys[kURI] = Buffer.from(rkey); + } catch (error: any) { + error.p = p; + throw error; + } + } + // get ivs + const iv = Buffer.alloc(16); + const ivs = key.iv ? key.iv : [0, 0, 0, p + 1]; + for (let i = 0; i < ivs.length; i++) { + iv.writeUInt32BE(ivs[i], i * 4); + } + return crypto.createDecipheriv('aes-128-cbc', this.data.keys[kURI], iv); + } } const extFn = { - getURI: (uri: string, baseurl?: string) => { - const httpURI = /^https{0,1}:/.test(uri); - if (!baseurl && !httpURI) { - throw new Error('No base and not http(s) uri'); - } else if (httpURI) { - return uri; - } - return baseurl + uri; - }, - getDownloadInfo: (dateStart: number, partsDL: number, partsTotal: number, downloadedBytes: number) => { - const dateElapsed = Date.now() - dateStart; - const percentFxd = parseInt(((partsDL / partsTotal) * 100).toFixed()); - const percent = percentFxd < 100 ? percentFxd : partsTotal == partsDL ? 100 : 99; - const revParts = dateElapsed * (partsTotal / partsDL - 1); - const downloadSpeed = downloadedBytes / (dateElapsed / 1000); //Bytes per second - return { percent, time: revParts, downloadSpeed }; - }, - getData: async (partIndex: number, uri: string, headers: Record<string, string>, segOffset: number, isKey: boolean) => { - // get file if uri is local - if (uri.startsWith('file://')) { - const buffer = await fs.readFile(url.fileURLToPath(uri)); - return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); - } - // do request - return await ofetch(uri, { - method: 'GET', - headers: headers, - responseType: 'arrayBuffer', - retry: 0, - async onRequestError({ error }) { - const partType = isKey ? 'Key' : 'Part'; - const partIndx = partIndex + 1 + segOffset; - console.warn(`%s %s: ${error.message}`, partType, partIndx); - } - }); - } + getURI: (uri: string, baseurl?: string) => { + const httpURI = /^https{0,1}:/.test(uri); + if (!baseurl && !httpURI) { + throw new Error('No base and not http(s) uri'); + } else if (httpURI) { + return uri; + } + return baseurl + uri; + }, + getDownloadInfo: (dateStart: number, partsDL: number, partsTotal: number, downloadedBytes: number) => { + const dateElapsed = Date.now() - dateStart; + const percentFxd = parseInt(((partsDL / partsTotal) * 100).toFixed()); + const percent = percentFxd < 100 ? percentFxd : partsTotal == partsDL ? 100 : 99; + const revParts = dateElapsed * (partsTotal / partsDL - 1); + const downloadSpeed = downloadedBytes / (dateElapsed / 1000); //Bytes per second + return { percent, time: revParts, downloadSpeed }; + }, + getData: async (partIndex: number, uri: string, headers: Record<string, string>, segOffset: number, isKey: boolean) => { + // get file if uri is local + if (uri.startsWith('file://')) { + const buffer = await fs.readFile(url.fileURLToPath(uri)); + return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); + } + // do request + return await ofetch(uri, { + method: 'GET', + headers: headers, + responseType: 'arrayBuffer', + retry: 0, + async onRequestError({ error }) { + const partType = isKey ? 'Key' : 'Part'; + const partIndx = partIndex + 1 + segOffset; + console.warn(`%s %s: ${error.message}`, partType, partIndx); + } + }); + } }; export default hlsDownload; diff --git a/modules/log.ts b/modules/log.ts index e747e45..b9214d0 100644 --- a/modules/log.ts +++ b/modules/log.ts @@ -7,63 +7,63 @@ const logFolder = path.join(workingDir, 'logs'); const latest = path.join(logFolder, 'latest.log'); const makeLogFolder = () => { - if (!fs.existsSync(logFolder)) - fs.mkdirSync(logFolder); - if (fs.existsSync(latest)) { - const stats = fs.statSync(latest); - fs.renameSync(latest, path.join(logFolder, `${stats.mtimeMs}.log`)); - } + if (!fs.existsSync(logFolder)) fs.mkdirSync(logFolder); + if (fs.existsSync(latest)) { + const stats = fs.statSync(latest); + fs.renameSync(latest, path.join(logFolder, `${stats.mtimeMs}.log`)); + } }; const makeLogger = () => { - global.console.log = - global.console.info = - global.console.warn = - global.console.error = - global.console.debug = (...data: any[]) => { - console.info((data.length >= 1 ? data.shift() : ''), ...data); - }; - makeLogFolder(); - log4js.configure({ - appenders: { - console: { - type: 'console', layout: { - type: 'pattern', - pattern: process.env.isGUI === 'true' ? '%[%x{info}%m%]' : '%x{info}%m', - tokens: { - info: (ev) => { - return ev.level.levelStr === 'INFO' ? '' : `[${ev.level.levelStr}] `; - } - } - } - }, - file: { - type: 'file', - filename: latest, - layout: { - type: 'pattern', - pattern: '%x{info}%m', - tokens: { - info: (ev) => { - return ev.level.levelStr === 'INFO' ? '' : `[${ev.level.levelStr}] `; - } - } - } - } - }, - categories: { - default: { - appenders: ['console', 'file'], - level: 'all', - } - } - }); + global.console.log = + global.console.info = + global.console.warn = + global.console.error = + global.console.debug = + (...data: any[]) => { + console.info(data.length >= 1 ? data.shift() : '', ...data); + }; + makeLogFolder(); + log4js.configure({ + appenders: { + console: { + type: 'console', + layout: { + type: 'pattern', + pattern: process.env.isGUI === 'true' ? '%[%x{info}%m%]' : '%x{info}%m', + tokens: { + info: (ev) => { + return ev.level.levelStr === 'INFO' ? '' : `[${ev.level.levelStr}] `; + } + } + } + }, + file: { + type: 'file', + filename: latest, + layout: { + type: 'pattern', + pattern: '%x{info}%m', + tokens: { + info: (ev) => { + return ev.level.levelStr === 'INFO' ? '' : `[${ev.level.levelStr}] `; + } + } + } + } + }, + categories: { + default: { + appenders: ['console', 'file'], + level: 'all' + } + } + }); }; const getLogger = () => { - if (!log4js.isConfigured()) - makeLogger(); - return log4js.getLogger(); + if (!log4js.isConfigured()) makeLogger(); + return log4js.getLogger(); }; -export const console = getLogger(); \ No newline at end of file +export const console = getLogger(); diff --git a/modules/module.api-urls.ts b/modules/module.api-urls.ts index 214d2c9..90b8cb0 100644 --- a/modules/module.api-urls.ts +++ b/modules/module.api-urls.ts @@ -1,104 +1,104 @@ // api domains const domain = { - cr_www: 'https://www.crunchyroll.com', - cr_api: 'https://api.crunchyroll.com', - hd_www: 'https://www.hidive.com', - hd_api: 'https://api.hidive.com', - hd_new: 'https://dce-frontoffice.imggaming.com' + cr_www: 'https://www.crunchyroll.com', + cr_api: 'https://api.crunchyroll.com', + hd_www: 'https://www.hidive.com', + hd_api: 'https://api.hidive.com', + hd_new: 'https://dce-frontoffice.imggaming.com' }; export type APIType = { - // Crunchyroll Vilos bundle.js - bundlejs: string; - // Crunchyroll API - basic_auth_token: string; - auth: string; - me: string; - profile: string; - search: string; - content_cms: string; - browse: string; - browse_all_series: string; - streaming_sessions: string; - drm_widevine: string; - drm_playready: string; - // Crunchyroll Bucket - cms_bucket: string; - cms_auth: string; - // Crunchyroll Headers - crunchyDefUserAgent: string; - crunchyDefHeader: Record<string, string>; - crunchyAuthHeader: Record<string, string>; - // Hidive - hd_apikey: string; - hd_devName: string; - hd_appId: string; - hd_clientWeb: string; - hd_clientExo: string; - hd_api: string; - hd_new_api: string; - hd_new_apiKey: string; - hd_new_version: string; + // Crunchyroll Vilos bundle.js + bundlejs: string; + // Crunchyroll API + basic_auth_token: string; + auth: string; + me: string; + profile: string; + search: string; + content_cms: string; + browse: string; + browse_all_series: string; + streaming_sessions: string; + drm_widevine: string; + drm_playready: string; + // Crunchyroll Bucket + cms_bucket: string; + cms_auth: string; + // Crunchyroll Headers + crunchyDefUserAgent: string; + crunchyDefHeader: Record<string, string>; + crunchyAuthHeader: Record<string, string>; + // Hidive + hd_apikey: string; + hd_devName: string; + hd_appId: string; + hd_clientWeb: string; + hd_clientExo: string; + hd_api: string; + hd_new_api: string; + hd_new_apiKey: string; + hd_new_version: string; }; const api: APIType = { - // - // - // Crunchyroll - // Vilos bundle.js (where we can extract the basic token thats needed for the initial auth) - bundlejs: 'https://static.crunchyroll.com/vilos-v2/web/vilos/js/bundle.js', - // - // Crunchyroll API - basic_auth_token: 'Y2I5bnpybWh0MzJ2Z3RleHlna286S1V3bU1qSlh4eHVyc0hJVGQxenZsMkMyeVFhUW84TjQ=', - auth: `${domain.cr_www}/auth/v1/token`, - me: `${domain.cr_www}/accounts/v1/me`, - profile: `${domain.cr_www}/accounts/v1/me/profile`, - search: `${domain.cr_www}/content/v2/discover/search`, - content_cms: `${domain.cr_www}/content/v2/cms`, - browse: `${domain.cr_www}/content/v1/browse`, - browse_all_series: `${domain.cr_www}/content/v2/discover/browse`, - streaming_sessions: `${domain.cr_www}/playback/v1/sessions/streaming`, - drm_widevine: `${domain.cr_www}/license/v1/license/widevine`, - drm_playready: `${domain.cr_www}/license/v1/license/playReady`, - // - // Crunchyroll Bucket - cms_bucket: `${domain.cr_www}/cms/v2`, - cms_auth: `${domain.cr_www}/index/v2`, - // - // Crunchyroll Headers - crunchyDefUserAgent: 'Crunchyroll/ANDROIDTV/3.45.2_22274 (Android 12; en-US; SHIELD Android TV Build/SR1A.211012.001)', - crunchyDefHeader: {}, - crunchyAuthHeader: {}, - // - // - // Hidive - // Hidive API - hd_apikey: '508efd7b42d546e19cc24f4d0b414e57e351ca73', - hd_devName: 'Android', - hd_appId: '24i-Android', - hd_clientWeb: 'okhttp/3.4.1', - hd_clientExo: 'smartexoplayer/1.6.0.R (Linux;Android 6.0) ExoPlayerLib/2.6.0', - hd_api: `${domain.hd_api}/api/v1`, - // Hidive New API - hd_new_api: `${domain.hd_new}/api`, - hd_new_apiKey: '857a1e5d-e35e-4fdf-805b-a87b6f8364bf', - hd_new_version: '6.0.1.bbf09a2' + // + // + // Crunchyroll + // Vilos bundle.js (where we can extract the basic token thats needed for the initial auth) + bundlejs: 'https://static.crunchyroll.com/vilos-v2/web/vilos/js/bundle.js', + // + // Crunchyroll API + basic_auth_token: 'YW55ZGF6d2F4Y2xyb2NhbndobzM6ODhnbklzdWNWLVE3c1lyWTI5dU9XX0pHbE1xeDFtQk4=', + auth: `${domain.cr_www}/auth/v1/token`, + me: `${domain.cr_www}/accounts/v1/me`, + profile: `${domain.cr_www}/accounts/v1/me/profile`, + search: `${domain.cr_www}/content/v2/discover/search`, + content_cms: `${domain.cr_www}/content/v2/cms`, + browse: `${domain.cr_www}/content/v1/browse`, + browse_all_series: `${domain.cr_www}/content/v2/discover/browse`, + streaming_sessions: `${domain.cr_www}/playback/v1/sessions/streaming`, + drm_widevine: `${domain.cr_www}/license/v1/license/widevine`, + drm_playready: `${domain.cr_www}/license/v1/license/playReady`, + // + // Crunchyroll Bucket + cms_bucket: `${domain.cr_www}/cms/v2`, + cms_auth: `${domain.cr_www}/index/v2`, + // + // Crunchyroll Headers + crunchyDefUserAgent: 'Crunchyroll/ANDROIDTV/3.46.0_22275 (Android 12; en-US; SHIELD Android TV Build/SR1A.211012.001)', + crunchyDefHeader: {}, + crunchyAuthHeader: {}, + // + // + // Hidive + // Hidive API + hd_apikey: '508efd7b42d546e19cc24f4d0b414e57e351ca73', + hd_devName: 'Android', + hd_appId: '24i-Android', + hd_clientWeb: 'okhttp/3.4.1', + hd_clientExo: 'smartexoplayer/1.6.0.R (Linux;Android 6.0) ExoPlayerLib/2.6.0', + hd_api: `${domain.hd_api}/api/v1`, + // Hidive New API + hd_new_api: `${domain.hd_new}/api`, + hd_new_apiKey: '857a1e5d-e35e-4fdf-805b-a87b6f8364bf', + hd_new_version: '6.0.1.bbf09a2' }; api.crunchyDefHeader = { - 'User-Agent': api.crunchyDefUserAgent, - Accept: '*/*', - 'Accept-Encoding': 'gzip', - Connection: 'Keep-Alive', - Host: 'www.crunchyroll.com' + 'User-Agent': api.crunchyDefUserAgent, + Accept: '*/*', + 'Accept-Encoding': 'gzip', + Connection: 'Keep-Alive', + Host: 'www.crunchyroll.com' }; // set header api.crunchyAuthHeader = { - Authorization: `Basic ${api.basic_auth_token}`, - 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', - 'Request-Type': 'SignIn', - ...api.crunchyDefHeader + Authorization: `Basic ${api.basic_auth_token}`, + 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', + 'Request-Type': 'SignIn', + ...api.crunchyDefHeader }; export { domain, api }; diff --git a/modules/module.app-args.ts b/modules/module.app-args.ts index 0a8ff1d..79005f5 100644 --- a/modules/module.app-args.ts +++ b/modules/module.app-args.ts @@ -6,179 +6,182 @@ import { HLSCallback } from './hls-download'; import leven from 'leven'; import { console } from './log'; import { CrunchyVideoPlayStreams, CrunchyAudioPlayStreams } from '../@types/enums'; +import pj from '../package.json'; -let argvC: { - [x: string]: unknown; - ccTag: string, - defaultAudio: LanguageItem, - defaultSub: LanguageItem, - ffmpegOptions: string[], - mkvmergeOptions: string[], - force: 'Y'|'y'|'N'|'n'|'C'|'c', - skipUpdate: boolean, - videoTitle: string, - override: string[], - fsRetryTime: number, - forceMuxer: AvailableMuxer|undefined; - username: string|undefined, - password: string|undefined, - token: string|undefined, - silentAuth: boolean, - skipSubMux: boolean, - downloadArchive: boolean, - addArchive: boolean, - but: boolean, - auth: boolean | undefined; - dlFonts: boolean | undefined; - search: string | undefined; - 'search-type': string; - page: number | undefined; - locale: string; - new: boolean | undefined; - 'movie-listing': string | undefined; - 'show-raw': string | undefined; - 'season-raw': string | undefined; - series: string | undefined; - s: string | undefined; - srz: string | undefined; - e: string | undefined; - extid: string | undefined; - q: number; - x: number; - // kstream: number; - cstream: keyof typeof CrunchyVideoPlayStreams; - vstream: keyof typeof CrunchyVideoPlayStreams; - astream: keyof typeof CrunchyAudioPlayStreams; - tsd: boolean | undefined; - partsize: number; - hslang: string; - dlsubs: string[]; - novids: boolean | undefined; - noaudio: boolean | undefined; - nosubs: boolean | undefined; - dubLang: string[]; - all: boolean; - fontSize: number; - combineLines: boolean; - allDubs: boolean; - timeout: number; - waittime: number; - simul: boolean; - mp4: boolean; - skipmux: boolean | undefined; - fileName: string; - numbers: number; - nosess: string; - debug: boolean | undefined; - raw: boolean; - rawoutput: string; - nocleanup: boolean; - help: boolean | undefined; - service: 'crunchy' | 'hidive' | 'ao' | 'adn'; - update: boolean; - fontName: string | undefined; - _: (string | number)[]; - $0: string; - dlVideoOnce: boolean; - chapters: boolean; - // crapi: 'android' | 'web'; - removeBumpers: boolean; - originalFontSize: boolean; - keepAllVideos: boolean; - syncTiming: boolean; - callbackMaker?: (data: DownloadInfo) => HLSCallback; -}; - -export type ArgvType = typeof argvC; - -const appArgv = (cfg: { - [key: string]: unknown -}, isGUI = false) => { - if (argvC) - return argvC; - yargs(process.argv.slice(2)); - const argv = getArgv(cfg, isGUI) - .parseSync(); - argvC = argv; - return argv; +let argvC: { + [x: string]: unknown; + ccTag: string; + defaultAudio: LanguageItem; + defaultSub: LanguageItem; + ffmpegOptions: string[]; + mkvmergeOptions: string[]; + force: 'Y' | 'y' | 'N' | 'n' | 'C' | 'c'; + skipUpdate: boolean; + videoTitle: string; + override: string[]; + fsRetryTime: number; + forceMuxer: AvailableMuxer | undefined; + username: string | undefined; + password: string | undefined; + token: string | undefined; + silentAuth: boolean; + skipSubMux: boolean; + downloadArchive: boolean; + addArchive: boolean; + but: boolean; + auth: boolean | undefined; + dlFonts: boolean | undefined; + search: string | undefined; + 'search-type': string; + page: number | undefined; + locale: string; + new: boolean | undefined; + 'movie-listing': string | undefined; + 'show-raw': string | undefined; + 'season-raw': string | undefined; + series: string | undefined; + s: string | undefined; + srz: string | undefined; + e: string | undefined; + extid: string | undefined; + q: number; + x: number; + // kstream: number; + cstream: keyof typeof CrunchyVideoPlayStreams; + vstream: keyof typeof CrunchyVideoPlayStreams; + astream: keyof typeof CrunchyAudioPlayStreams; + tsd: boolean | undefined; + partsize: number; + hslang: string; + dlsubs: string[]; + novids: boolean | undefined; + noaudio: boolean | undefined; + nosubs: boolean | undefined; + dubLang: string[]; + all: boolean; + fontSize: number; + combineLines: boolean; + allDubs: boolean; + timeout: number; + waittime: number; + simul: boolean; + mp4: boolean; + skipmux: boolean | undefined; + fileName: string; + numbers: number; + nosess: string; + debug: boolean | undefined; + raw: boolean; + rawoutput: string; + nocleanup: boolean; + help: boolean | undefined; + service: 'crunchy' | 'hidive' | 'ao' | 'adn'; + update: boolean; + fontName: string | undefined; + _: (string | number)[]; + $0: string; + dlVideoOnce: boolean; + chapters: boolean; + // crapi: 'android' | 'web'; + removeBumpers: boolean; + originalFontSize: boolean; + keepAllVideos: boolean; + syncTiming: boolean; + callbackMaker?: (data: DownloadInfo) => HLSCallback; + // Subtitle Fix Options + srtAssFix: boolean; + layoutResFix: boolean; + scaledBorderAndShadowFix: boolean; + scaledBorderAndShadow: 'yes' | 'no'; + originalScriptFix: boolean; }; +export type ArgvType = typeof argvC; -const overrideArguments = (cfg: { [key:string]: unknown }, override: Partial<typeof argvC>, isGUI = false) => { - const argv = getArgv(cfg, isGUI).middleware((ar) => { - for (const key of Object.keys(override)) { - ar[key] = override[key]; - } - }).parseSync(); - argvC = argv; +const appArgv = ( + cfg: { + [key: string]: unknown; + }, + isGUI = false +) => { + if (argvC) return argvC; + yargs(process.argv.slice(2)); + const argv = getArgv(cfg, isGUI).parseSync(); + argvC = argv; + return argv; }; - -export { - appArgv, - overrideArguments -}; - -const getArgv = (cfg: { [key:string]: unknown }, isGUI: boolean) => { - const parseDefault = <T = unknown>(key: string, _default: T) : T=> { - if (Object.prototype.hasOwnProperty.call(cfg, key)) { - return cfg[key] as T; - } else - return _default; - }; - const argv = yargs.parserConfiguration({ - 'duplicate-arguments-array': false, - 'camel-case-expansion': false, - }) - .wrap(yargs.terminalWidth()) - .usage('Usage: $0 [options]') - .help(true); - //.strictOptions() - const data = args.map(a => { - return { - ...a, - demandOption: !isGUI && a.demandOption, - group: groups[a.group], - default: typeof a.default === 'object' && !Array.isArray(a.default) ? - parseDefault((a.default as any).name || a.name, (a.default as any).default) : a.default - }; - }); - for (const item of data) - argv.option(item.name, { - ...item, - coerce: (value) => { - if (item.transformer) { - return item.transformer(value); - } else { - return value; - } - }, - choices: item.name === 'service' && isGUI ? undefined : item.choices as unknown as Choices - }); - // Custom logic for suggesting corrections for misspelled options - argv.middleware((argv: Record<string, any>) => { - // List of valid options - const validOptions = [ - ...args.map(a => a.name), - ...args.map(a => a.alias).filter(alias => alias !== undefined) as string[] - ]; - const unknownOptions = Object.keys(argv).filter(key => !validOptions.includes(key) && key !== '_' && key !== '$0'); // Filter out known options - - const suggestedOptions: Record<string, boolean> = {}; - unknownOptions.forEach(actualOption => { - const closestOption = validOptions.find(option => { - const levenVal = leven(option, actualOption); - return levenVal <= 2 && levenVal > 0; - }); - - if (closestOption && !suggestedOptions[closestOption]) { - suggestedOptions[closestOption] = true; - console.info(`Unknown option ${actualOption}, did you mean ${closestOption}?`); - } else if (!suggestedOptions[actualOption]) { - suggestedOptions[actualOption] = true; - console.info(`Unknown option ${actualOption}`); - } - }); - }); - return argv as unknown as yargs.Argv<typeof argvC>; -}; \ No newline at end of file +const overrideArguments = (cfg: { [key: string]: unknown }, override: Partial<typeof argvC>, isGUI = false) => { + const argv = getArgv(cfg, isGUI) + .middleware((ar) => { + for (const key of Object.keys(override)) { + ar[key] = override[key]; + } + }) + .parseSync(); + argvC = argv; +}; + +export { appArgv, overrideArguments }; + +const getArgv = (cfg: { [key: string]: unknown }, isGUI: boolean) => { + const parseDefault = <T = unknown>(key: string, _default: T): T => { + if (Object.prototype.hasOwnProperty.call(cfg, key)) { + return cfg[key] as T; + } else return _default; + }; + const argv = yargs + .parserConfiguration({ + 'duplicate-arguments-array': false, + 'camel-case-expansion': false + }) + .wrap(yargs.terminalWidth()) + .usage('Usage: $0 [options]') + .version(pj.version) + .help(true); + //.strictOptions() + const data = args.map((a) => { + return { + ...a, + demandOption: !isGUI && a.demandOption, + group: groups[a.group], + default: typeof a.default === 'object' && !Array.isArray(a.default) ? parseDefault((a.default as any).name || a.name, (a.default as any).default) : a.default + }; + }); + for (const item of data) + argv.option(item.name, { + ...item, + coerce: (value) => { + if (item.transformer) { + return item.transformer(value); + } else { + return value; + } + }, + choices: item.name === 'service' && isGUI ? undefined : (item.choices as unknown as Choices) + }); + + // Custom logic for suggesting corrections for misspelled options + argv.middleware((argv: Record<string, any>) => { + // List of valid options + const validOptions = [...args.map((a) => a.name), ...(args.map((a) => a.alias).filter((alias) => alias !== undefined) as string[])]; + const unknownOptions = Object.keys(argv).filter((key) => !validOptions.includes(key) && key !== '_' && key !== '$0'); // Filter out known options + + const suggestedOptions: Record<string, boolean> = {}; + unknownOptions.forEach((actualOption) => { + const closestOption = validOptions.find((option) => { + const levenVal = leven(option, actualOption); + return levenVal <= 2 && levenVal > 0; + }); + + if (closestOption && !suggestedOptions[closestOption]) { + suggestedOptions[closestOption] = true; + console.info(`Unknown option ${actualOption}, did you mean ${closestOption}?`); + } else if (!suggestedOptions[actualOption]) { + suggestedOptions[actualOption] = true; + console.info(`Unknown option ${actualOption}`); + } + }); + }); + return argv as unknown as yargs.Argv<typeof argvC>; +}; diff --git a/modules/module.args.ts b/modules/module.args.ts index 33eece1..da21abf 100644 --- a/modules/module.args.ts +++ b/modules/module.args.ts @@ -2,989 +2,1016 @@ import { aoSearchLocales, dubLanguageCodes, languages, searchLocales, subtitleLa import { CrunchyVideoPlayStreams, CrunchyAudioPlayStreams } from '../@types/enums'; const groups = { - 'auth': 'Authentication:', - 'fonts': 'Fonts:', - 'search': 'Search:', - 'dl': 'Downloading:', - 'mux': 'Muxing:', - 'fileName': 'Filename Template:', - 'debug': 'Debug:', - 'util': 'Utilities:', - 'help': 'Help:', - 'gui': 'GUI:' + auth: 'Authentication:', + fonts: 'Fonts:', + search: 'Search:', + dl: 'Downloading:', + mux: 'Muxing:', + fileName: 'Filename Template:', + debug: 'Debug:', + util: 'Utilities:', + help: 'Help:', + gui: 'GUI:' }; -export type AvailableFilenameVars = 'title' | 'episode' | 'showTitle' | 'seriesTitle' | 'season' | 'width' | 'height' | 'service' +export type AvailableFilenameVars = 'title' | 'episode' | 'showTitle' | 'seriesTitle' | 'season' | 'width' | 'height' | 'service'; -const availableFilenameVars: AvailableFilenameVars[] = [ - 'title', - 'episode', - 'showTitle', - 'seriesTitle', - 'season', - 'width', - 'height', - 'service' +const availableFilenameVars: AvailableFilenameVars[] = ['title', 'episode', 'showTitle', 'seriesTitle', 'season', 'width', 'height', 'service']; + +export type AvailableMuxer = 'ffmpeg' | 'mkvmerge'; +export const muxer: AvailableMuxer[] = ['ffmpeg', 'mkvmerge']; + +export type TAppArg<T extends boolean | string | number | unknown[], K = any> = { + name: string; + group: keyof typeof groups; + type: 'boolean' | 'string' | 'number' | 'array'; + choices?: T[]; + alias?: string; + describe: string; + docDescribe: true | string; // true means use describe for the docs + default?: + | T + | { + default: T | undefined; + name?: string; + }; + service: Array<'crunchy' | 'hidive' | 'ao' | 'adn' | 'all'>; + usage: string; // -(-)${name} will be added for each command, + demandOption?: true; + transformer?: (value: T) => K; +}; + +const args: TAppArg<boolean | number | string | unknown[]>[] = [ + { + name: 'absolute', + describe: 'Use absolute numbers for the episode', + docDescribe: 'Use absolute numbers for the episode. If not set, it will use the default index numbers', + group: 'dl', + service: ['crunchy'], + type: 'boolean', + usage: '' + }, + { + name: 'auth', + describe: 'Enter authentication mode', + type: 'boolean', + group: 'auth', + service: ['all'], + docDescribe: + 'Most of the shows on both services are only accessible if you payed for the service.' + + '\nIn order for them to know who you are you are required to log in.' + + '\nIf you trigger this command, you will be prompted for the username and password for the selected service', + usage: '' + }, + { + name: 'dlFonts', + group: 'fonts', + describe: 'Download all required fonts for mkv muxing', + docDescribe: 'Crunchyroll uses a variaty of fonts for the subtitles.' + '\nUse this command to download all the fonts and add them to the muxed **mkv** file.', + service: ['crunchy'], + type: 'boolean', + usage: '' + }, + { + name: 'search', + group: 'search', + alias: 'f', + describe: 'Search of an anime by the given string', + type: 'string', + docDescribe: true, + service: ['all'], + usage: '${search}' + }, + { + name: 'search-type', + describe: 'Search by type', + docDescribe: 'Search only for type of anime listings (e.g. episodes, series)', + group: 'search', + service: ['crunchy'], + type: 'string', + usage: '${type}', + choices: ['', 'top_results', 'series', 'movie_listing', 'episode'], + default: { + default: '' + } + }, + { + name: 'page', + alias: 'p', + describe: 'Set the page number for search results', + docDescribe: 'The output is organized in pages. Use this command to output the items for the given page', + group: 'search', + service: ['crunchy', 'hidive'], + type: 'number', + usage: '${page}' + }, + { + name: 'locale', + describe: 'Set the service locale', + docDescribe: 'Set the local that will be used for the API.', + group: 'search', + choices: [...searchLocales.filter((a) => a !== undefined), ...aoSearchLocales.filter((a) => a !== undefined)] as string[], + default: { + default: 'en-US' + }, + type: 'string', + service: ['crunchy', 'ao', 'adn'], + usage: '${locale}' + }, + { + group: 'search', + name: 'new', + describe: 'Get last updated series list', + docDescribe: true, + service: ['crunchy', 'hidive'], + type: 'boolean', + usage: '' + }, + { + group: 'dl', + alias: 'flm', + name: 'movie-listing', + describe: 'Get video list by Movie Listing ID', + docDescribe: true, + service: ['crunchy'], + type: 'string', + usage: '${ID}' + }, + { + group: 'dl', + alias: 'sraw', + name: 'show-raw', + describe: 'Get Raw Show data', + docDescribe: true, + service: ['crunchy'], + type: 'string', + usage: '${ID}' + }, + { + group: 'dl', + alias: 'seraw', + name: 'season-raw', + describe: 'Get Raw Season data', + docDescribe: true, + service: ['crunchy'], + type: 'string', + usage: '${ID}' + }, + { + group: 'dl', + alias: 'slraw', + name: 'show-list-raw', + describe: 'Get Raw Show list data', + docDescribe: true, + service: ['crunchy'], + type: 'boolean', + usage: '' + }, + { + name: 'series', + group: 'dl', + alias: 'srz', + describe: 'Get season list by series ID', + docDescribe: 'Requested is the ID of a show not a season.', + service: ['crunchy'], + type: 'string', + usage: '${ID}' + }, + { + name: 's', + group: 'dl', + type: 'string', + describe: 'Set the season ID', + docDescribe: 'Used to set the season ID to download from', + service: ['all'], + usage: '${ID}' + }, + { + name: 'e', + group: 'dl', + describe: 'Set the episode(s) to download from any given show', + docDescribe: + 'Set the episode(s) to download from any given show.' + + '\nFor multiple selection: 1-4 OR 1,2,3,4 ' + + '\nFor special episodes: S1-4 OR S1,S2,S3,S4 where S is the special letter', + service: ['all'], + type: 'string', + usage: '${selection}', + alias: 'episode' + }, + { + name: 'extid', + group: 'dl', + describe: 'Set the external id to lookup/download', + docDescribe: 'Set the external id to lookup/download.' + '\nAllows you to download or view legacy Crunchyroll Ids ', + service: ['crunchy'], + type: 'string', + usage: '${selection}', + alias: 'externalid' + }, + { + name: 'q', + group: 'dl', + describe: 'Set the quality level. Use 0 to use the maximum quality.', + default: { + default: 0 + }, + docDescribe: true, + service: ['all'], + type: 'number', + usage: '${qualityLevel}' + }, + { + name: 'dlVideoOnce', + describe: 'Download only once the video with the best selected quality', + type: 'boolean', + group: 'dl', + service: ['crunchy', 'ao'], + docDescribe: + 'If selected, the best selected quality will be downloaded only for the first language,' + + '\nthen the worst video quality with the same audio quality will be downloaded for every other language.' + + '\nBy the later merge of the videos, no quality difference will be present.' + + '\nThis will speed up the download speed, if multiple languages are selected.', + usage: '', + default: { + default: false + } + }, + { + name: 'chapters', + describe: 'Will fetch the chapters and add them into the final video', + type: 'boolean', + group: 'dl', + service: ['crunchy', 'adn'], + docDescribe: 'Will fetch the chapters and add them into the final video.', + usage: '', + default: { + default: true + } + }, + { + name: 'removeBumpers', + describe: 'Remove bumpers from final video', + type: 'boolean', + group: 'dl', + service: ['hidive'], + docDescribe: + 'If selected, it will remove the bumpers such as the hidive intro from the final file.' + + '\nCurrently disabling this sometimes results in bugs such as video/audio desync', + usage: '', + default: { + default: true + } + }, + { + name: 'originalFontSize', + describe: 'Keep original font size', + type: 'boolean', + group: 'dl', + service: ['hidive'], + docDescribe: 'If selected, it will prefer to keep the original Font Size defined by the service.', + usage: '', + default: { + default: true + } + }, + { + name: 'x', + group: 'dl', + describe: 'Select the server to use', + choices: [1, 2, 3, 4], + default: { + default: 1 + }, + type: 'number', + alias: 'server', + docDescribe: true, + service: ['all'], + usage: '${server}' + }, + { + name: 'cstream', + group: 'dl', + alias: 'cs', + service: ['crunchy'], + type: 'string', + describe: + '(Please use --vstream and --astream instead, this will deprecate soon) Select a specific Crunchyroll playback endpoint by device. Since Crunchyroll has started rolling out their new VBR encodes, we highly recommend using a TV endpoint (e.g. vidaa, samsungtv, lgtv, rokutv, chromecast, firetv, androidtv) to access the old CBR encodes. Please note: The older encodes do not include the new 192 kbps audio, the new audio is only available with the new VBR encodes.', + choices: [...Object.keys(CrunchyVideoPlayStreams), 'none'], + docDescribe: true, + usage: '${device}' + }, + { + name: 'vstream', + group: 'dl', + alias: 'vs', + service: ['crunchy'], + type: 'string', + describe: 'Select a specific Crunchyroll video playback endpoint by device.', + choices: [...Object.keys(CrunchyVideoPlayStreams), 'none'], + default: { + default: 'androidtv' + }, + docDescribe: true, + usage: '${device}' + }, + { + name: 'astream', + group: 'dl', + alias: 'as', + service: ['crunchy'], + type: 'string', + describe: 'Select a specific Crunchyroll audio playback endpoint by device.', + choices: [...Object.keys(CrunchyAudioPlayStreams), 'none'], + default: { + default: 'android' + }, + docDescribe: true, + usage: '${device}' + }, + { + name: 'tsd', + group: 'dl', + describe: '(Total Session Death) Kills all active Crunchyroll Streaming Sessions to prevent getting the "TOO_MANY_ACTIVE_STREAMS" error.', + docDescribe: true, + service: ['crunchy'], + type: 'boolean', + usage: '', + default: { + default: false + } + }, + { + name: 'hslang', + group: 'dl', + describe: 'Download video with specific hardsubs', + choices: subtitleLanguagesFilter.slice(1), + default: { + default: 'none' + }, + type: 'string', + usage: '${hslang}', + docDescribe: true, + service: ['crunchy'] + }, + { + name: 'dlsubs', + group: 'dl', + describe: + 'Download subtitles by language tag (space-separated)' + + `\nCrunchy Only: ${languages + .filter((a) => a.cr_locale) + .map((a) => a.locale) + .join(', ')}`, + docDescribe: true, + service: ['all'], + type: 'array', + choices: subtitleLanguagesFilter, + default: { + default: ['all'] + }, + usage: '${sub1} ${sub2}' + }, + { + name: 'srtAssFix', + group: 'dl', + describe: 'Fixes the recently changed Crunchyroll subtitles provided by Closed Caption Converter.', + docDescribe: true, + service: ['crunchy'], + type: 'boolean', + usage: '', + default: { + default: true + } + }, + { + name: 'layoutResFix', + group: 'dl', + describe: 'Applies the LayoutRes Fix to all ASS subtitles.', + docDescribe: true, + service: ['crunchy'], + type: 'boolean', + usage: '', + default: { + default: true + } + }, + { + name: 'scaledBorderAndShadowFix', + group: 'dl', + describe: 'Applies the ScaledBorderAndShadow Fix to all ASS subtitles.', + docDescribe: true, + service: ['crunchy'], + type: 'boolean', + usage: '', + default: { + default: true + } + }, + { + name: 'scaledBorderAndShadow', + group: 'dl', + service: ['crunchy'], + type: 'string', + describe: 'Select if ScaledBorderAndShadow should be set to "yes" or "no".', + choices: ['yes', 'no'], + default: { + default: 'yes' + }, + docDescribe: true, + usage: '${yes/no}' + }, + { + name: 'originalScriptFix', + group: 'dl', + describe: 'Removes the URL in the Original Script line of the ASS subtitles, it prevents from bricking the subs in VLC (Fonts not loading when url not returning 200).', + docDescribe: true, + service: ['crunchy'], + type: 'boolean', + usage: '', + default: { + default: true + } + }, + { + name: 'novids', + group: 'dl', + describe: 'Skip downloading videos', + docDescribe: true, + service: ['all'], + type: 'boolean', + usage: '' + }, + { + name: 'noaudio', + group: 'dl', + describe: 'Skip downloading audio', + docDescribe: true, + service: ['crunchy', 'hidive'], + type: 'boolean', + usage: '' + }, + { + name: 'nosubs', + group: 'dl', + describe: 'Skip downloading subtitles', + docDescribe: true, + service: ['all'], + type: 'boolean', + usage: '' + }, + { + name: 'dubLang', + describe: + 'Set the language to download: ' + + `\nCrunchy Only: ${languages + .filter((a) => a.cr_locale) + .map((a) => a.code) + .join(', ')}`, + docDescribe: true, + group: 'dl', + choices: dubLanguageCodes, + default: { + default: [dubLanguageCodes.slice(-1)[0]] + }, + service: ['all'], + type: 'array', + usage: '${dub1} ${dub2}' + }, + { + name: 'all', + describe: 'Used to download all episodes from the show', + docDescribe: true, + group: 'dl', + service: ['all'], + default: { + default: false + }, + type: 'boolean', + usage: '' + }, + { + name: 'fontSize', + describe: 'Used to set the fontsize of the subtitles', + default: { + default: 55 + }, + docDescribe: 'When converting the subtitles to ass, this will change the font size' + '\nIn most cases, requires "--originaFontSize false" to take effect', + group: 'dl', + service: ['all'], + type: 'number', + usage: '${fontSize}' + }, + { + name: 'combineLines', + describe: 'Merge adjacent lines with same style and text', + docDescribe: 'If selected, will prevent a line from shifting downwards', + group: 'dl', + service: ['hidive'], + type: 'boolean', + usage: '' + }, + { + name: 'allDubs', + describe: 'If selected, all available dubs will get downloaded', + docDescribe: true, + group: 'dl', + service: ['all'], + type: 'boolean', + usage: '' + }, + { + name: 'timeout', + group: 'dl', + type: 'number', + describe: 'Set the timeout of all download reqests. Set in millisecods', + docDescribe: true, + service: ['all'], + usage: '${timeout}', + default: { + default: 15 * 1000 + } + }, + { + name: 'waittime', + group: 'dl', + type: 'number', + describe: 'Set the time the program waits between downloads. Set in millisecods', + docDescribe: true, + service: ['crunchy', 'hidive'], + usage: '${waittime}', + default: { + default: 0 * 1000 + } + }, + { + name: 'simul', + group: 'dl', + describe: 'Force downloading simulcast version instead of uncut version (if available).', + docDescribe: true, + service: ['hidive'], + type: 'boolean', + usage: '', + default: { + default: false + } + }, + { + name: 'mp4', + group: 'mux', + describe: 'Mux video into mp4', + docDescribe: 'If selected, the output file will be an mp4 file (not recommended tho)', + service: ['all'], + type: 'boolean', + usage: '', + default: { + default: false + } + }, + { + name: 'keepAllVideos', + group: 'mux', + describe: 'Keeps all videos when merging instead of discarding extras', + docDescribe: 'If set to true, it will keep all videos in the merge process, rather than discarding the extra videos.', + service: ['crunchy', 'hidive'], + type: 'boolean', + usage: '', + default: { + default: false + } + }, + { + name: 'syncTiming', + group: 'mux', + describe: 'Attempts to sync timing for multi-dub downloads EXPERIMENTAL', + docDescribe: + 'If enabled attempts to sync timing for multi-dub downloads.' + + '\nNOTE: This is currently experimental and syncs audio and subtitles, though subtitles has a lot of guesswork' + + '\nIf you find bugs with this, please report it in the discord or github', + service: ['crunchy', 'hidive'], + type: 'boolean', + usage: '', + default: { + default: false + } + }, + { + name: 'skipmux', + describe: 'Skip muxing video, audio and subtitles', + docDescribe: true, + group: 'mux', + service: ['all'], + type: 'boolean', + usage: '' + }, + { + name: 'fileName', + group: 'fileName', + describe: `Set the filename template. Use \${variable_name} to insert variables.\nYou can also create folders by inserting a path seperator in the filename\nYou may use ${availableFilenameVars + .map((a) => `'${a}'`) + .join(', ')} as variables.`, + docDescribe: true, + service: ['all'], + type: 'string', + usage: '${fileName}', + default: { + default: '[${service}] ${showTitle} - S${season}E${episode} [${height}p]' + } + }, + { + name: 'numbers', + group: 'fileName', + describe: `Set how long a number in the title should be at least.\n${[ + [3, 5, '005'], + [2, 1, '01'], + [1, 20, '20'] + ] + .map((val) => `Set in config: ${val[0]}; Episode number: ${val[1]}; Output: ${val[2]}`) + .join('\n')}`, + type: 'number', + default: { + default: 2 + }, + docDescribe: true, + service: ['all'], + usage: '${number}' + }, + { + name: 'nosess', + group: 'debug', + describe: 'Reset session cookie for testing purposes', + docDescribe: true, + service: ['all'], + type: 'boolean', + usage: '', + default: { + default: false + } + }, + { + name: 'debug', + group: 'debug', + describe: 'Debug mode (tokens may be revealed in the console output)', + docDescribe: true, + service: ['all'], + type: 'boolean', + usage: '', + default: { + default: false + } + }, + { + name: 'nocleanup', + describe: "Don't delete subtitle, audio and video files after muxing", + docDescribe: true, + group: 'mux', + service: ['all'], + type: 'boolean', + default: { + default: false + }, + usage: '' + }, + { + name: 'help', + alias: 'h', + describe: 'Show the help output', + docDescribe: true, + group: 'help', + service: ['all'], + type: 'boolean', + usage: '' + }, + { + name: 'service', + describe: 'Set the service you want to use', + docDescribe: true, + group: 'util', + service: ['all'], + type: 'string', + choices: ['crunchy', 'hidive', 'ao', 'adn'], + usage: '${service}', + default: { + default: '' + }, + demandOption: true + }, + { + name: 'update', + group: 'util', + describe: 'Force the tool to check for updates (code version only)', + docDescribe: true, + service: ['all'], + type: 'boolean', + usage: '' + }, + { + name: 'fontName', + group: 'fonts', + describe: 'Set the font to use in subtiles', + docDescribe: true, + service: ['hidive', 'adn'], + type: 'string', + usage: '${fontName}' + }, + { + name: 'but', + describe: 'Download everything but the -e selection', + docDescribe: true, + group: 'dl', + service: ['all'], + type: 'boolean', + usage: '' + }, + { + name: 'downloadArchive', + describe: 'Used to download all archived shows', + group: 'dl', + docDescribe: true, + service: ['all'], + type: 'boolean', + usage: '' + }, + { + name: 'addArchive', + describe: 'Used to add the `-s` and `--srz` to downloadArchive', + group: 'dl', + docDescribe: true, + service: ['all'], + type: 'boolean', + usage: '' + }, + { + name: 'skipSubMux', + describe: 'Skip muxing the subtitles', + docDescribe: true, + group: 'mux', + service: ['all'], + type: 'boolean', + usage: '', + default: { + default: false + } + }, + { + name: 'partsize', + describe: 'Set the amount of parts to download at once', + docDescribe: 'Set the amount of parts to download at once\nIf you have a good connection try incresing this number to get a higher overall speed', + group: 'dl', + service: ['all'], + type: 'number', + usage: '${amount}', + default: { + default: 10 + } + }, + { + name: 'username', + describe: 'Set the username to use for the authentication. If not provided, you will be prompted for the input', + docDescribe: true, + group: 'auth', + service: ['all'], + type: 'string', + usage: '${username}', + default: { + default: undefined + } + }, + { + name: 'password', + describe: 'Set the password to use for the authentication. If not provided, you will be prompted for the input', + docDescribe: true, + group: 'auth', + service: ['all'], + type: 'string', + usage: '${password}', + default: { + default: undefined + } + }, + { + name: 'silentAuth', + describe: 'Authenticate every time the script runs. Use at your own risk.', + docDescribe: true, + group: 'auth', + service: ['crunchy'], + type: 'boolean', + usage: '', + default: { + default: false + } + }, + { + name: 'token', + describe: 'Allows you to login with your token (Example on crunchy is Refresh Token/etp-rt cookie)', + docDescribe: true, + group: 'auth', + service: ['crunchy', 'ao'], + type: 'string', + usage: '${token}', + default: { + default: undefined + } + }, + { + name: 'forceMuxer', + describe: "Force the program to use said muxer or don't mux if the given muxer is not present", + docDescribe: true, + group: 'mux', + service: ['all'], + type: 'string', + usage: '${muxer}', + choices: muxer, + default: { + default: undefined + } + }, + { + name: 'fsRetryTime', + describe: 'Set the time the downloader waits before retrying if an error while writing the file occurs', + docDescribe: true, + group: 'dl', + service: ['all'], + type: 'number', + usage: '${time in seconds}', + default: { + default: 5 + } + }, + { + name: 'override', + describe: 'Override a template variable', + docDescribe: true, + group: 'fileName', + service: ['all'], + type: 'array', + usage: '"${toOverride}=\'${value}\'"', + default: { + default: [] + } + }, + { + name: 'videoTitle', + describe: 'Set the video track name of the merged file', + docDescribe: true, + group: 'mux', + service: ['all'], + type: 'string', + usage: '${title}' + }, + { + name: 'skipUpdate', + describe: "If true, the tool won't check for updates", + docDescribe: true, + group: 'util', + service: ['all'], + type: 'boolean', + usage: '', + default: { + default: false + } + }, + { + name: 'raw', + describe: 'If true, the tool will output the raw data from the API (Where applicable, the feature is a WIP)', + docDescribe: true, + group: 'util', + service: ['all'], + type: 'boolean', + usage: '', + default: { + default: false + } + }, + { + name: 'rawoutput', + describe: 'Provide a path to output the raw data from the API into a file (Where applicable, the feature is a WIP)', + docDescribe: true, + group: 'util', + service: ['all'], + type: 'string', + usage: '', + default: { + default: '' + } + }, + { + name: 'force', + describe: "Set the default option for the 'alredy exists' prompt", + docDescribe: 'If a file already exists, the tool will ask you how to proceed. With this, you can answer in advance.', + group: 'dl', + service: ['all'], + type: 'string', + usage: '${option}', + choices: ['y', 'Y', 'n', 'N', 'c', 'C'] + }, + { + name: 'mkvmergeOptions', + describe: 'Set the options given to mkvmerge', + docDescribe: true, + group: 'mux', + service: ['all'], + type: 'array', + usage: '${args}', + default: { + default: ['--no-date', '--disable-track-statistics-tags', '--engage no_variable_data'] + } + }, + { + name: 'ffmpegOptions', + describe: 'Set the options given to ffmpeg', + docDescribe: true, + group: 'mux', + service: ['all'], + type: 'array', + usage: '${args}', + default: { + default: [] + } + }, + { + name: 'defaultAudio', + describe: `Set the default audio track by language code\nPossible Values: ${languages.map((a) => a.code).join(', ')}`, + docDescribe: true, + group: 'mux', + service: ['all'], + type: 'string', + usage: '${args}', + default: { + default: 'eng' + }, + transformer: (val) => { + const item = languages.find((a) => a.code === val); + if (!item) { + throw new Error(`Unable to find language code ${val}!`); + } + return item; + } + }, + { + name: 'defaultSub', + describe: `Set the default subtitle track by language code\nPossible Values: ${languages.map((a) => a.code).join(', ')}`, + docDescribe: true, + group: 'mux', + service: ['all'], + type: 'string', + usage: '${args}', + default: { + default: 'eng' + }, + transformer: (val) => { + const item = languages.find((a) => a.code === val); + if (!item) { + throw new Error(`Unable to find language code ${val}!`); + } + return item; + } + }, + { + name: 'ccTag', + describe: 'Used to set the name for subtitles that contain tranlations for none verbal communication (e.g. signs)', + docDescribe: true, + group: 'fileName', + service: ['all'], + type: 'string', + usage: '${tag}', + default: { + default: 'cc' + } + } ]; -export type AvailableMuxer = 'ffmpeg' | 'mkvmerge' -export const muxer: AvailableMuxer[] = [ 'ffmpeg', 'mkvmerge' ]; - -export type TAppArg<T extends boolean|string|number|unknown[], K = any> = { - name: string, - group: keyof typeof groups, - type: 'boolean'|'string'|'number'|'array', - choices?: T[], - alias?: string, - describe: string, - docDescribe: true|string, // true means use describe for the docs - default?: T|{ - default: T|undefined, - name?: string - }, - service: Array<'crunchy'|'hidive'|'ao'|'adn'|'all'>, - usage: string // -(-)${name} will be added for each command, - demandOption?: true, - transformer?: (value: T) => K -} - -const args: TAppArg<boolean|number|string|unknown[]>[] = [ - { - name: 'absolute', - describe: 'Use absolute numbers for the episode', - docDescribe: 'Use absolute numbers for the episode. If not set, it will use the default index numbers', - group: 'dl', - service: ['crunchy'], - type: 'boolean', - usage: '', - }, - { - name: 'auth', - describe: 'Enter authentication mode', - type: 'boolean', - group: 'auth', - service: ['all'], - docDescribe: 'Most of the shows on both services are only accessible if you payed for the service.' - + '\nIn order for them to know who you are you are required to log in.' - + '\nIf you trigger this command, you will be prompted for the username and password for the selected service', - usage: '' - }, - { - name: 'dlFonts', - group: 'fonts', - describe: 'Download all required fonts for mkv muxing', - docDescribe: 'Crunchyroll uses a variaty of fonts for the subtitles.' - + '\nUse this command to download all the fonts and add them to the muxed **mkv** file.', - service: ['crunchy'], - type: 'boolean', - usage: '' - }, - { - name: 'search', - group: 'search', - alias: 'f', - describe: 'Search of an anime by the given string', - type: 'string', - docDescribe: true, - service: ['all'], - usage: '${search}' - }, - { - name: 'search-type', - describe: 'Search by type', - docDescribe: 'Search only for type of anime listings (e.g. episodes, series)', - group: 'search', - service: ['crunchy'], - type: 'string', - usage: '${type}', - choices: [ '', 'top_results', 'series', 'movie_listing', 'episode' ], - default: { - default: '' - } - }, - { - name: 'page', - alias: 'p', - describe: 'Set the page number for search results', - docDescribe: 'The output is organized in pages. Use this command to output the items for the given page', - group: 'search', - service: ['crunchy', 'hidive'], - type: 'number', - usage: '${page}' - }, - { - name: 'locale', - describe: 'Set the service locale', - docDescribe: 'Set the local that will be used for the API.', - group: 'search', - choices: ([...searchLocales.filter(a => a !== undefined), ...aoSearchLocales.filter(a => a !== undefined)] as string[]), - default: { - default: 'en-US' - }, - type: 'string', - service: ['crunchy', 'ao', 'adn'], - usage: '${locale}' - }, - { - group: 'search', - name: 'new', - describe: 'Get last updated series list', - docDescribe: true, - service: ['crunchy', 'hidive'], - type: 'boolean', - usage: '', - }, - { - group: 'dl', - alias: 'flm', - name: 'movie-listing', - describe: 'Get video list by Movie Listing ID', - docDescribe: true, - service: ['crunchy'], - type: 'string', - usage: '${ID}', - }, - { - group: 'dl', - alias: 'sraw', - name: 'show-raw', - describe: 'Get Raw Show data', - docDescribe: true, - service: ['crunchy'], - type: 'string', - usage: '${ID}', - }, - { - group: 'dl', - alias: 'seraw', - name: 'season-raw', - describe: 'Get Raw Season data', - docDescribe: true, - service: ['crunchy'], - type: 'string', - usage: '${ID}', - }, - { - group: 'dl', - alias: 'slraw', - name: 'show-list-raw', - describe: 'Get Raw Show list data', - docDescribe: true, - service: ['crunchy'], - type: 'boolean', - usage: '', - }, - { - name: 'series', - group: 'dl', - alias: 'srz', - describe: 'Get season list by series ID', - docDescribe: 'Requested is the ID of a show not a season.', - service: ['crunchy'], - type: 'string', - usage: '${ID}' - }, - { - name: 's', - group: 'dl', - type: 'string', - describe: 'Set the season ID', - docDescribe: 'Used to set the season ID to download from', - service: ['all'], - usage: '${ID}' - }, - { - name: 'e', - group: 'dl', - describe: 'Set the episode(s) to download from any given show', - docDescribe: 'Set the episode(s) to download from any given show.' - + '\nFor multiple selection: 1-4 OR 1,2,3,4 ' - + '\nFor special episodes: S1-4 OR S1,S2,S3,S4 where S is the special letter', - service: ['all'], - type: 'string', - usage: '${selection}', - alias: 'episode' - }, - { - name: 'extid', - group: 'dl', - describe: 'Set the external id to lookup/download', - docDescribe: 'Set the external id to lookup/download.' - + '\nAllows you to download or view legacy Crunchyroll Ids ', - service: ['crunchy'], - type: 'string', - usage: '${selection}', - alias: 'externalid' - }, - { - name: 'q', - group: 'dl', - describe: 'Set the quality level. Use 0 to use the maximum quality.', - default: { - default: 0 - }, - docDescribe: true, - service: ['all'], - type: 'number', - usage: '${qualityLevel}' - }, - { - name: 'dlVideoOnce', - describe: 'Download only once the video with the best selected quality', - type: 'boolean', - group: 'dl', - service: ['crunchy', 'ao'], - docDescribe: 'If selected, the best selected quality will be downloaded only for the first language,' - + '\nthen the worst video quality with the same audio quality will be downloaded for every other language.' - + '\nBy the later merge of the videos, no quality difference will be present.' - + '\nThis will speed up the download speed, if multiple languages are selected.', - usage: '', - default: { - default: false - } - }, - { - name: 'chapters', - describe: 'Will fetch the chapters and add them into the final video', - type: 'boolean', - group: 'dl', - service: ['crunchy', 'adn'], - docDescribe: 'Will fetch the chapters and add them into the final video.', - usage: '', - default: { - default: true - } - }, - // Deprecated - // { - // name: 'crapi', - // describe: 'Selects the API type for Crunchyroll', - // type: 'string', - // group: 'dl', - // service: ['crunchy'], - // docDescribe: 'If set to Android, it has lower quality, but Non-DRM streams,' - // + '\nIf set to Web, it has a higher quality adaptive stream, but everything is DRM.', - // usage: '', - // choices: ['android', 'web'], - // default: { - // default: 'web' - // } - // }, - { - name: 'removeBumpers', - describe: 'Remove bumpers from final video', - type: 'boolean', - group: 'dl', - service: ['hidive'], - docDescribe: 'If selected, it will remove the bumpers such as the hidive intro from the final file.' - + '\nCurrently disabling this sometimes results in bugs such as video/audio desync', - usage: '', - default: { - default: true - } - }, - { - name: 'originalFontSize', - describe: 'Keep original font size', - type: 'boolean', - group: 'dl', - service: ['hidive'], - docDescribe: 'If selected, it will prefer to keep the original Font Size defined by the service.', - usage: '', - default: { - default: true - } - }, - { - name: 'x', - group: 'dl', - describe: 'Select the server to use', - choices: [1, 2, 3, 4], - default: { - default: 1 - }, - type: 'number', - alias: 'server', - docDescribe: true, - service: ['all'], - usage: '${server}' - }, - // Deprecated - // { - // name: 'kstream', - // group: 'dl', - // alias: 'k', - // describe: 'Select specific stream', - // choices: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], - // default: { - // default: 1 - // }, - // docDescribe: true, - // service: ['crunchy'], - // type: 'number', - // usage: '${stream}' - // }, - // About to Deprecate - { - name: 'cstream', - group: 'dl', - alias: 'cs', - service: ['crunchy'], - type: 'string', - describe: '(Please use --vstream and --astream instead, this will deprecate soon) Select a specific Crunchyroll playback endpoint by device. Since Crunchyroll has started rolling out their new VBR encodes, we highly recommend using a TV endpoint (e.g. vidaa, samsungtv, lgtv, rokutv, chromecast, firetv, androidtv) to access the old CBR encodes. Please note: The older encodes do not include the new 192 kbps audio, the new audio is only available with the new VBR encodes.', - choices: [...Object.keys(CrunchyVideoPlayStreams), 'none'], - docDescribe: true, - usage: '${device}' - }, - { - name: 'vstream', - group: 'dl', - alias: 'vs', - service: ['crunchy'], - type: 'string', - describe: 'Select a specific Crunchyroll video playback endpoint by device.', - choices: [...Object.keys(CrunchyVideoPlayStreams), 'none'], - default: { - default: 'androidtv' - }, - docDescribe: true, - usage: '${device}' - }, - { - name: 'astream', - group: 'dl', - alias: 'as', - service: ['crunchy'], - type: 'string', - describe: 'Select a specific Crunchyroll audio playback endpoint by device.', - choices: [...Object.keys(CrunchyAudioPlayStreams), 'none'], - default: { - default: 'android' - }, - docDescribe: true, - usage: '${device}' - }, - { - name: 'tsd', - group: 'dl', - describe: '(Total Session Death) Kills all active Crunchyroll Streaming Sessions to prevent getting the "TOO_MANY_ACTIVE_STREAMS" error.', - docDescribe: true, - service: ['crunchy'], - type: 'boolean', - usage: '', - default: { - default: false - } - }, - { - name: 'hslang', - group: 'dl', - describe: 'Download video with specific hardsubs', - choices: subtitleLanguagesFilter.slice(1), - default: { - default: 'none' - }, - type: 'string', - usage: '${hslang}', - docDescribe: true, - service: ['crunchy'] - }, - { - name: 'dlsubs', - group: 'dl', - describe: 'Download subtitles by language tag (space-separated)' - + `\nCrunchy Only: ${languages.filter(a => a.cr_locale).map(a => a.locale).join(', ')}`, - docDescribe: true, - service: ['all'], - type: 'array', - choices: subtitleLanguagesFilter, - default: { - default: [ 'all' ] - }, - usage: '${sub1} ${sub2}' - }, - { - name: 'novids', - group: 'dl', - describe: 'Skip downloading videos', - docDescribe: true, - service: ['all'], - type: 'boolean', - usage: '' - }, - { - name: 'noaudio', - group: 'dl', - describe: 'Skip downloading audio', - docDescribe: true, - service: ['crunchy', 'hidive'], - type: 'boolean', - usage: '' - }, - { - name: 'nosubs', - group: 'dl', - describe: 'Skip downloading subtitles', - docDescribe: true, - service: ['all'], - type: 'boolean', - usage: '' - }, - { - name: 'dubLang', - describe: 'Set the language to download: ' - + `\nCrunchy Only: ${languages.filter(a => a.cr_locale).map(a => a.code).join(', ')}`, - docDescribe: true, - group: 'dl', - choices: dubLanguageCodes, - default: { - default: [dubLanguageCodes.slice(-1)[0]] - }, - service: ['all'], - type: 'array', - usage: '${dub1} ${dub2}', - }, - { - name: 'all', - describe: 'Used to download all episodes from the show', - docDescribe: true, - group: 'dl', - service: ['all'], - default: { - default: false - }, - type: 'boolean', - usage: '' - }, - { - name: 'fontSize', - describe: 'Used to set the fontsize of the subtitles', - default: { - default: 55 - }, - docDescribe: 'When converting the subtitles to ass, this will change the font size' - + '\nIn most cases, requires "--originaFontSize false" to take effect', - group: 'dl', - service: ['all'], - type: 'number', - usage: '${fontSize}' - }, - { - name: 'combineLines', - describe: 'Merge adjacent lines with same style and text', - docDescribe: 'If selected, will prevent a line from shifting downwards', - group: 'dl', - service: ['hidive'], - type: 'boolean', - usage: '' - }, - { - name: 'allDubs', - describe: 'If selected, all available dubs will get downloaded', - docDescribe: true, - group: 'dl', - service: ['all'], - type: 'boolean', - usage: '' - }, - { - name: 'timeout', - group: 'dl', - type: 'number', - describe: 'Set the timeout of all download reqests. Set in millisecods', - docDescribe: true, - service: ['all'], - usage: '${timeout}', - default: { - default: 15 * 1000 - } - }, - { - name: 'waittime', - group: 'dl', - type: 'number', - describe: 'Set the time the program waits between downloads. Set in millisecods', - docDescribe: true, - service: ['crunchy','hidive'], - usage: '${waittime}', - default: { - default: 0 * 1000 - } - }, - { - name: 'simul', - group: 'dl', - describe: 'Force downloading simulcast version instead of uncut version (if available).', - docDescribe: true, - service: ['hidive'], - type: 'boolean', - usage: '', - default: { - default: false - } - }, - { - name: 'mp4', - group: 'mux', - describe: 'Mux video into mp4', - docDescribe: 'If selected, the output file will be an mp4 file (not recommended tho)', - service: ['all'], - type: 'boolean', - usage: '', - default: { - default: false - } - }, - { - name: 'keepAllVideos', - group: 'mux', - describe: 'Keeps all videos when merging instead of discarding extras', - docDescribe: 'If set to true, it will keep all videos in the merge process, rather than discarding the extra videos.', - service: ['crunchy','hidive'], - type: 'boolean', - usage: '', - default: { - default: false - } - }, - { - name: 'syncTiming', - group: 'mux', - describe: 'Attempts to sync timing for multi-dub downloads EXPERIMENTAL', - docDescribe: 'If enabled attempts to sync timing for multi-dub downloads.' - + '\nNOTE: This is currently experimental and syncs audio and subtitles, though subtitles has a lot of guesswork' - + '\nIf you find bugs with this, please report it in the discord or github', - service: ['crunchy','hidive'], - type: 'boolean', - usage: '', - default: { - default: false - } - }, - { - name: 'skipmux', - describe: 'Skip muxing video, audio and subtitles', - docDescribe: true, - group: 'mux', - service: ['all'], - type: 'boolean', - usage: '' - }, - { - name: 'fileName', - group: 'fileName', - describe: `Set the filename template. Use \${variable_name} to insert variables.\nYou can also create folders by inserting a path seperator in the filename\nYou may use ${availableFilenameVars - .map(a => `'${a}'`).join(', ')} as variables.`, - docDescribe: true, - service: ['all'], - type: 'string', - usage: '${fileName}', - default: { - default: '[${service}] ${showTitle} - S${season}E${episode} [${height}p]' - } - }, - { - name: 'numbers', - group: 'fileName', - describe: `Set how long a number in the title should be at least.\n${[[3, 5, '005'], [2, 1, '01'], [1, 20, '20']] - .map(val => `Set in config: ${val[0]}; Episode number: ${val[1]}; Output: ${val[2]}`).join('\n')}`, - type: 'number', - default: { - default: 2 - }, - docDescribe: true, - service: ['all'], - usage: '${number}' - }, - { - name: 'nosess', - group: 'debug', - describe: 'Reset session cookie for testing purposes', - docDescribe: true, - service: ['all'], - type: 'boolean', - usage: '', - default: { - default: false - } - }, - { - name: 'debug', - group: 'debug', - describe: 'Debug mode (tokens may be revealed in the console output)', - docDescribe: true, - service: ['all'], - type: 'boolean', - usage: '', - default: { - default: false - } - }, - { - name: 'nocleanup', - describe: 'Don\'t delete subtitle, audio and video files after muxing', - docDescribe: true, - group: 'mux', - service: ['all'], - type: 'boolean', - default: { - default: false - }, - usage: '' - }, - { - name: 'help', - alias: 'h', - describe: 'Show the help output', - docDescribe: true, - group: 'help', - service: ['all'], - type: 'boolean', - usage: '' - }, - { - name: 'service', - describe: 'Set the service you want to use', - docDescribe: true, - group: 'util', - service: ['all'], - type: 'string', - choices: ['crunchy', 'hidive', 'ao', 'adn'], - usage: '${service}', - default: { - default: '' - }, - demandOption: true - }, - { - name: 'update', - group: 'util', - describe: 'Force the tool to check for updates (code version only)', - docDescribe: true, - service: ['all'], - type: 'boolean', - usage: '' - }, - { - name: 'fontName', - group: 'fonts', - describe: 'Set the font to use in subtiles', - docDescribe: true, - service: ['hidive', 'adn'], - type: 'string', - usage: '${fontName}', - }, - { - name: 'but', - describe: 'Download everything but the -e selection', - docDescribe: true, - group: 'dl', - service: ['all'], - type: 'boolean', - usage: '' - }, - { - name: 'downloadArchive', - describe: 'Used to download all archived shows', - group: 'dl', - docDescribe: true, - service: ['all'], - type: 'boolean', - usage: '' - }, - { - name: 'addArchive', - describe: 'Used to add the `-s` and `--srz` to downloadArchive', - group: 'dl', - docDescribe: true, - service: ['all'], - type: 'boolean', - usage: '' - }, - { - name: 'skipSubMux', - describe: 'Skip muxing the subtitles', - docDescribe: true, - group: 'mux', - service: ['all'], - type: 'boolean', - usage: '', - default: { - default: false - } - }, - { - name: 'partsize', - describe: 'Set the amount of parts to download at once', - docDescribe: 'Set the amount of parts to download at once\nIf you have a good connection try incresing this number to get a higher overall speed', - group: 'dl', - service: ['all'], - type: 'number', - usage: '${amount}', - default: { - default: 10 - } - }, - { - name: 'username', - describe: 'Set the username to use for the authentication. If not provided, you will be prompted for the input', - docDescribe: true, - group: 'auth', - service: ['all'], - type: 'string', - usage: '${username}', - default: { - default: undefined - } - }, - { - name: 'password', - describe: 'Set the password to use for the authentication. If not provided, you will be prompted for the input', - docDescribe: true, - group: 'auth', - service: ['all'], - type: 'string', - usage: '${password}', - default: { - default: undefined - } - }, - { - name: 'silentAuth', - describe: 'Authenticate every time the script runs. Use at your own risk.', - docDescribe: true, - group: 'auth', - service: ['crunchy'], - type: 'boolean', - usage: '', - default: { - default: false - } - }, - { - name: 'token', - describe: 'Allows you to login with your token (Example on crunchy is Refresh Token/etp-rt cookie)', - docDescribe: true, - group: 'auth', - service: ['crunchy', 'ao'], - type: 'string', - usage: '${token}', - default: { - default: undefined - } - }, - { - name: 'forceMuxer', - describe: 'Force the program to use said muxer or don\'t mux if the given muxer is not present', - docDescribe: true, - group: 'mux', - service: ['all'], - type: 'string', - usage: '${muxer}', - choices: muxer, - default: { - default: undefined - } - }, - { - name: 'fsRetryTime', - describe: 'Set the time the downloader waits before retrying if an error while writing the file occurs', - docDescribe: true, - group: 'dl', - service: ['all'], - type: 'number', - usage: '${time in seconds}', - default: { - default: 5 - }, - }, - { - name: 'override', - describe: 'Override a template variable', - docDescribe: true, - group: 'fileName', - service: ['all'], - type: 'array', - usage: '"${toOverride}=\'${value}\'"', - default: { - default: [ ] - } - }, - { - name: 'videoTitle', - describe: 'Set the video track name of the merged file', - docDescribe: true, - group: 'mux', - service: ['all'], - type: 'string', - usage: '${title}' - }, - { - name: 'skipUpdate', - describe: 'If true, the tool won\'t check for updates', - docDescribe: true, - group: 'util', - service: ['all'], - type: 'boolean', - usage: '', - default: { - default: false - } - }, - { - name: 'raw', - describe: 'If true, the tool will output the raw data from the API (Where applicable, the feature is a WIP)', - docDescribe: true, - group: 'util', - service: ['all'], - type: 'boolean', - usage: '', - default: { - default: false - } - }, - { - name: 'rawoutput', - describe: 'Provide a path to output the raw data from the API into a file (Where applicable, the feature is a WIP)', - docDescribe: true, - group: 'util', - service: ['all'], - type: 'string', - usage: '', - default: { - default: '' - } - }, - { - name: 'force', - describe: 'Set the default option for the \'alredy exists\' prompt', - docDescribe: 'If a file already exists, the tool will ask you how to proceed. With this, you can answer in advance.', - group: 'dl', - service: ['all'], - type: 'string', - usage: '${option}', - choices: [ 'y', 'Y', 'n', 'N', 'c', 'C' ] - }, - { - name: 'mkvmergeOptions', - describe: 'Set the options given to mkvmerge', - docDescribe: true, - group: 'mux', - service: ['all'], - type: 'array', - usage: '${args}', - default: { - default: [ - '--no-date', - '--disable-track-statistics-tags', - '--engage no_variable_data' - ] - } - }, - { - name: 'ffmpegOptions', - describe: 'Set the options given to ffmpeg', - docDescribe: true, - group: 'mux', - service: ['all'], - type: 'array', - usage: '${args}', - default: { - default: [] - } - }, - { - name: 'defaultAudio', - describe: `Set the default audio track by language code\nPossible Values: ${languages.map(a => a.code).join(', ')}`, - docDescribe: true, - group: 'mux', - service: ['all'], - type: 'string', - usage: '${args}', - default: { - default: 'eng' - }, - transformer: (val) => { - const item = languages.find(a => a.code === val); - if (!item) { - throw new Error(`Unable to find language code ${val}!`); - } - return item; - } - }, - { - name: 'defaultSub', - describe: `Set the default subtitle track by language code\nPossible Values: ${languages.map(a => a.code).join(', ')}`, - docDescribe: true, - group: 'mux', - service: ['all'], - type: 'string', - usage: '${args}', - default: { - default: 'eng' - }, - transformer: (val) => { - const item = languages.find(a => a.code === val); - if (!item) { - throw new Error(`Unable to find language code ${val}!`); - } - return item; - } - }, - { - name: 'ccTag', - describe: 'Used to set the name for subtitles that contain tranlations for none verbal communication (e.g. signs)', - docDescribe: true, - group: 'fileName', - service: ['all'], - type: 'string', - usage: '${tag}', - default: { - default: 'cc' - } - } -]; - -const getDefault = <T extends boolean|string|number|unknown[]>(name: string, cfg: Record<string, T>): T => { - const option = args.find(item => item.name === name); - if (!option) - throw new Error(`Unable to find option ${name}`); - if (option.default === undefined) - throw new Error(`Option ${name} has no default`); - if (typeof option.default === 'object') { - if (Array.isArray(option.default)) - return option.default as T; - if (Object.prototype.hasOwnProperty.call(cfg, (option.default as any).name ?? option.name)) { - return cfg[(option.default as any).name ?? option.name]; - } else { - return (option.default as any).default as T; - } - } else { - return option.default as T; - } +const getDefault = <T extends boolean | string | number | unknown[]>(name: string, cfg: Record<string, T>): T => { + const option = args.find((item) => item.name === name); + if (!option) throw new Error(`Unable to find option ${name}`); + if (option.default === undefined) throw new Error(`Option ${name} has no default`); + if (typeof option.default === 'object') { + if (Array.isArray(option.default)) return option.default as T; + if (Object.prototype.hasOwnProperty.call(cfg, (option.default as any).name ?? option.name)) { + return cfg[(option.default as any).name ?? option.name]; + } else { + return (option.default as any).default as T; + } + } else { + return option.default as T; + } }; const buildDefault = () => { - const data: Record<string, unknown> = {}; - const defaultArgs = args.filter(a => a.default); - defaultArgs.forEach(item => { - if (typeof item.default === 'object') { - if (Array.isArray(item.default)) { - data[item.name] = item.default; - } else { - data[(item.default as any).name ?? item.name] = (item.default as any).default; - } - } else { - data[item.name] = item.default; - } - }); - return data; + const data: Record<string, unknown> = {}; + const defaultArgs = args.filter((a) => a.default); + defaultArgs.forEach((item) => { + if (typeof item.default === 'object') { + if (Array.isArray(item.default)) { + data[item.name] = item.default; + } else { + data[(item.default as any).name ?? item.name] = (item.default as any).default; + } + } else { + data[item.name] = item.default; + } + }); + return data; }; -export { - getDefault, - buildDefault, - args, - groups, - availableFilenameVars -}; +export { getDefault, buildDefault, args, groups, availableFilenameVars }; diff --git a/modules/module.cfg-loader.ts b/modules/module.cfg-loader.ts index 3e61964..6a0ca68 100644 --- a/modules/module.cfg-loader.ts +++ b/modules/module.cfg-loader.ts @@ -6,407 +6,398 @@ import { console } from './log'; import { GuiState } from '../@types/messageHandler'; // new-cfg -const workingDir = (process as NodeJS.Process & { - pkg?: unknown -}).pkg ? path.dirname(process.execPath) : process.env.contentDirectory ? process.env.contentDirectory : path.join(__dirname, '/..'); +const workingDir = ( + process as NodeJS.Process & { + pkg?: unknown; + } +).pkg + ? path.dirname(process.execPath) + : process.env.contentDirectory + ? process.env.contentDirectory + : path.join(__dirname, '/..'); export { workingDir }; -const binCfgFile = path.join(workingDir, 'config', 'bin-path'); -const dirCfgFile = path.join(workingDir, 'config', 'dir-path'); -const guiCfgFile = path.join(workingDir, 'config', 'gui'); -const cliCfgFile = path.join(workingDir, 'config', 'cli-defaults'); +const binCfgFile = path.join(workingDir, 'config', 'bin-path'); +const dirCfgFile = path.join(workingDir, 'config', 'dir-path'); +const guiCfgFile = path.join(workingDir, 'config', 'gui'); +const cliCfgFile = path.join(workingDir, 'config', 'cli-defaults'); const hdPflCfgFile = path.join(workingDir, 'config', 'hd_profile'); -const sessCfgFile = { - cr: path.join(workingDir, 'config', 'cr_sess'), - hd: path.join(workingDir, 'config', 'hd_sess'), - ao: path.join(workingDir, 'config', 'ao_sess'), - adn: path.join(workingDir, 'config', 'adn_sess') +const sessCfgFile = { + cr: path.join(workingDir, 'config', 'cr_sess'), + hd: path.join(workingDir, 'config', 'hd_sess'), + ao: path.join(workingDir, 'config', 'ao_sess'), + adn: path.join(workingDir, 'config', 'adn_sess') }; -const stateFile = path.join(workingDir, 'config', 'guistate'); -const tokenFile = { - cr: path.join(workingDir, 'config', 'cr_token'), - hd: path.join(workingDir, 'config', 'hd_token'), - hdNew:path.join(workingDir, 'config', 'hd_new_token'), - ao: path.join(workingDir, 'config', 'ao_token'), - adn: path.join(workingDir, 'config', 'adn_token') +const stateFile = path.join(workingDir, 'config', 'guistate'); +const tokenFile = { + cr: path.join(workingDir, 'config', 'cr_token'), + hd: path.join(workingDir, 'config', 'hd_token'), + hdNew: path.join(workingDir, 'config', 'hd_new_token'), + ao: path.join(workingDir, 'config', 'ao_token'), + adn: path.join(workingDir, 'config', 'adn_token') }; export const ensureConfig = () => { - if (!fs.existsSync(path.join(workingDir, 'config'))) - fs.mkdirSync(path.join(workingDir, 'config')); - if (process.env.contentDirectory) - [binCfgFile, dirCfgFile, cliCfgFile, guiCfgFile].forEach(a => { - if (!fs.existsSync(`${a}.yml`)) - fs.copyFileSync(path.join(__dirname, '..', 'config', `${path.basename(a)}.yml`), `${a}.yml`); - }); + if (!fs.existsSync(path.join(workingDir, 'config'))) fs.mkdirSync(path.join(workingDir, 'config')); + if (process.env.contentDirectory) + [binCfgFile, dirCfgFile, cliCfgFile, guiCfgFile].forEach((a) => { + if (!fs.existsSync(`${a}.yml`)) fs.copyFileSync(path.join(__dirname, '..', 'config', `${path.basename(a)}.yml`), `${a}.yml`); + }); }; const loadYamlCfgFile = <T extends Record<string, any>>(file: string, isSess?: boolean): T => { - if(fs.existsSync(`${file}.user.yml`) && !isSess){ - file += '.user'; - } - file += '.yml'; - if(fs.existsSync(file)){ - try{ - return yaml.parse(fs.readFileSync(file, 'utf8')); - } - catch(e){ - console.error('[ERROR]', e); - return {} as T; - } - } - return {} as T; + if (fs.existsSync(`${file}.user.yml`) && !isSess) { + file += '.user'; + } + file += '.yml'; + if (fs.existsSync(file)) { + try { + return yaml.parse(fs.readFileSync(file, 'utf8')); + } catch (e) { + console.error('[ERROR]', e); + return {} as T; + } + } + return {} as T; }; export type WriteObjects = { - gui: GUIConfig -} + gui: GUIConfig; +}; const writeYamlCfgFile = <T extends keyof WriteObjects>(file: T, data: WriteObjects[T]) => { - const fn = path.join(workingDir, 'config', `${file}.yml`); - if (fs.existsSync(fn)) - fs.removeSync(fn); - fs.writeFileSync(fn, yaml.stringify(data)); + const fn = path.join(workingDir, 'config', `${file}.yml`); + if (fs.existsSync(fn)) fs.removeSync(fn); + fs.writeFileSync(fn, yaml.stringify(data)); }; export type GUIConfig = { - port: number, - password?: string + port: number; + password?: string; }; export type ConfigObject = { - dir: { - content: string, - trash: string, - fonts: string; - config: string; - }, - bin: { - ffmpeg?: string, - mkvmerge?: string, - ffprobe?: string, - mp4decrypt?: string - shaka?: string - }, - cli: { - [key: string]: any - }, - gui: GUIConfig -} + dir: { + content: string; + trash: string; + fonts: string; + config: string; + }; + bin: { + ffmpeg?: string; + mkvmerge?: string; + ffprobe?: string; + mp4decrypt?: string; + shaka?: string; + }; + cli: { + [key: string]: any; + }; + gui: GUIConfig; +}; -const loadCfg = () : ConfigObject => { - // load cfgs - const defaultCfg: ConfigObject = { - bin: {}, - dir: loadYamlCfgFile<{ - content: string, - trash: string, - fonts: string - config: string - }>(dirCfgFile), - cli: loadYamlCfgFile<{ - [key: string]: any - }>(cliCfgFile), - gui: loadYamlCfgFile<GUIConfig>(guiCfgFile) - }; - const defaultDirs = { - fonts: '${wdir}/fonts/', - content: '${wdir}/videos/', - trash: '${wdir}/videos/_trash/', - config: '${wdir}/config' - }; - if (typeof defaultCfg.dir !== 'object' || defaultCfg.dir === null || Array.isArray(defaultCfg.dir)) { - defaultCfg.dir = defaultDirs; - } - - const keys = Object.keys(defaultDirs) as (keyof typeof defaultDirs)[]; - for (const key of keys) { - if (!Object.prototype.hasOwnProperty.call(defaultCfg.dir, key) || typeof defaultCfg.dir[key] !== 'string') { - defaultCfg.dir[key] = defaultDirs[key]; - } - if (!path.isAbsolute(defaultCfg.dir[key])) { - defaultCfg.dir[key] = path.join(workingDir, defaultCfg.dir[key].replace(/^\${wdir}/, '')); - } - } - if(!fs.existsSync(defaultCfg.dir.content)){ - try{ - fs.ensureDirSync(defaultCfg.dir.content); - } - catch(e){ - console.error('Content directory not accessible!'); - return defaultCfg; - } - } - if(!fs.existsSync(defaultCfg.dir.trash)){ - defaultCfg.dir.trash = defaultCfg.dir.content; - } - // output - return defaultCfg; +const loadCfg = (): ConfigObject => { + // load cfgs + const defaultCfg: ConfigObject = { + bin: {}, + dir: loadYamlCfgFile<{ + content: string; + trash: string; + fonts: string; + config: string; + }>(dirCfgFile), + cli: loadYamlCfgFile<{ + [key: string]: any; + }>(cliCfgFile), + gui: loadYamlCfgFile<GUIConfig>(guiCfgFile) + }; + const defaultDirs = { + fonts: '${wdir}/fonts/', + content: '${wdir}/videos/', + trash: '${wdir}/videos/_trash/', + config: '${wdir}/config' + }; + if (typeof defaultCfg.dir !== 'object' || defaultCfg.dir === null || Array.isArray(defaultCfg.dir)) { + defaultCfg.dir = defaultDirs; + } + + const keys = Object.keys(defaultDirs) as (keyof typeof defaultDirs)[]; + for (const key of keys) { + if (!Object.prototype.hasOwnProperty.call(defaultCfg.dir, key) || typeof defaultCfg.dir[key] !== 'string') { + defaultCfg.dir[key] = defaultDirs[key]; + } + if (!path.isAbsolute(defaultCfg.dir[key])) { + defaultCfg.dir[key] = path.join(workingDir, defaultCfg.dir[key].replace(/^\${wdir}/, '')); + } + } + if (!fs.existsSync(defaultCfg.dir.content)) { + try { + fs.ensureDirSync(defaultCfg.dir.content); + } catch (e) { + console.error('Content directory not accessible!'); + return defaultCfg; + } + } + if (!fs.existsSync(defaultCfg.dir.trash)) { + defaultCfg.dir.trash = defaultCfg.dir.content; + } + // output + return defaultCfg; }; const loadBinCfg = async () => { - const binCfg = loadYamlCfgFile<ConfigObject['bin']>(binCfgFile); - // binaries - const defaultBin = { - ffmpeg: 'ffmpeg', - mkvmerge: 'mkvmerge', - ffprobe: 'ffprobe', - mp4decrypt: 'mp4decrypt', - shaka: 'shaka-packager' - }; - const keys = Object.keys(defaultBin) as (keyof typeof defaultBin)[]; - for(const dir of keys){ - if(!Object.prototype.hasOwnProperty.call(binCfg, dir) || typeof binCfg[dir] != 'string'){ - binCfg[dir] = defaultBin[dir]; - } - if ((binCfg[dir] as string).match(/^\${wdir}/)) { - binCfg[dir] = (binCfg[dir] as string).replace(/^\${wdir}/, ''); - binCfg[dir] = path.join(workingDir, binCfg[dir] as string); - } - if (!path.isAbsolute(binCfg[dir] as string)){ - binCfg[dir] = path.join(workingDir, binCfg[dir] as string); - } - binCfg[dir] = await lookpath(binCfg[dir] as string); - binCfg[dir] = binCfg[dir] ? binCfg[dir] : undefined; - if(!binCfg[dir]){ - const binFile = await lookpath(path.basename(defaultBin[dir])); - binCfg[dir] = binFile ? binFile : binCfg[dir]; - } - } - return binCfg; + const binCfg = loadYamlCfgFile<ConfigObject['bin']>(binCfgFile); + // binaries + const defaultBin = { + ffmpeg: 'ffmpeg', + mkvmerge: 'mkvmerge', + ffprobe: 'ffprobe', + mp4decrypt: 'mp4decrypt', + shaka: 'shaka-packager' + }; + const keys = Object.keys(defaultBin) as (keyof typeof defaultBin)[]; + for (const dir of keys) { + if (!Object.prototype.hasOwnProperty.call(binCfg, dir) || typeof binCfg[dir] != 'string') { + binCfg[dir] = defaultBin[dir]; + } + if ((binCfg[dir] as string).match(/^\${wdir}/)) { + binCfg[dir] = (binCfg[dir] as string).replace(/^\${wdir}/, ''); + binCfg[dir] = path.join(workingDir, binCfg[dir] as string); + } + if (!path.isAbsolute(binCfg[dir] as string)) { + binCfg[dir] = path.join(workingDir, binCfg[dir] as string); + } + binCfg[dir] = await lookpath(binCfg[dir] as string); + binCfg[dir] = binCfg[dir] ? binCfg[dir] : undefined; + if (!binCfg[dir]) { + const binFile = await lookpath(path.basename(defaultBin[dir])); + binCfg[dir] = binFile ? binFile : binCfg[dir]; + } + } + return binCfg; }; const loadCRSession = () => { - let session = loadYamlCfgFile(sessCfgFile.cr, true); - if(typeof session !== 'object' || session === null || Array.isArray(session)){ - session = {}; - } - for(const cv of Object.keys(session)){ - if(typeof session[cv] !== 'object' || session[cv] === null || Array.isArray(session[cv])){ - session[cv] = {}; - } - } - return session; + let session = loadYamlCfgFile(sessCfgFile.cr, true); + if (typeof session !== 'object' || session === null || Array.isArray(session)) { + session = {}; + } + for (const cv of Object.keys(session)) { + if (typeof session[cv] !== 'object' || session[cv] === null || Array.isArray(session[cv])) { + session[cv] = {}; + } + } + return session; }; const saveCRSession = (data: Record<string, unknown>) => { - const cfgFolder = path.dirname(sessCfgFile.cr); - try{ - fs.ensureDirSync(cfgFolder); - fs.writeFileSync(`${sessCfgFile.cr}.yml`, yaml.stringify(data)); - } - catch(e){ - console.error('Can\'t save session file to disk!'); - } + const cfgFolder = path.dirname(sessCfgFile.cr); + try { + fs.ensureDirSync(cfgFolder); + fs.writeFileSync(`${sessCfgFile.cr}.yml`, yaml.stringify(data)); + } catch (e) { + console.error("Can't save session file to disk!"); + } }; const loadCRToken = () => { - let token = loadYamlCfgFile(tokenFile.cr, true); - if(typeof token !== 'object' || token === null || Array.isArray(token)){ - token = {}; - } - return token; + let token = loadYamlCfgFile(tokenFile.cr, true); + if (typeof token !== 'object' || token === null || Array.isArray(token)) { + token = {}; + } + return token; }; const saveCRToken = (data: Record<string, unknown>) => { - const cfgFolder = path.dirname(tokenFile.cr); - try{ - fs.ensureDirSync(cfgFolder); - fs.writeFileSync(`${tokenFile.cr}.yml`, yaml.stringify(data)); - } - catch(e){ - console.error('Can\'t save token file to disk!'); - } + const cfgFolder = path.dirname(tokenFile.cr); + try { + fs.ensureDirSync(cfgFolder); + fs.writeFileSync(`${tokenFile.cr}.yml`, yaml.stringify(data)); + } catch (e) { + console.error("Can't save token file to disk!"); + } }; - + const loadADNToken = () => { - let token = loadYamlCfgFile(tokenFile.adn, true); - if(typeof token !== 'object' || token === null || Array.isArray(token)){ - token = {}; - } - return token; + let token = loadYamlCfgFile(tokenFile.adn, true); + if (typeof token !== 'object' || token === null || Array.isArray(token)) { + token = {}; + } + return token; }; const saveADNToken = (data: Record<string, unknown>) => { - const cfgFolder = path.dirname(tokenFile.adn); - try{ - fs.ensureDirSync(cfgFolder); - fs.writeFileSync(`${tokenFile.adn}.yml`, yaml.stringify(data)); - } - catch(e){ - console.error('Can\'t save token file to disk!'); - } + const cfgFolder = path.dirname(tokenFile.adn); + try { + fs.ensureDirSync(cfgFolder); + fs.writeFileSync(`${tokenFile.adn}.yml`, yaml.stringify(data)); + } catch (e) { + console.error("Can't save token file to disk!"); + } }; - + const loadAOToken = () => { - let token = loadYamlCfgFile(tokenFile.ao, true); - if(typeof token !== 'object' || token === null || Array.isArray(token)){ - token = {}; - } - return token; + let token = loadYamlCfgFile(tokenFile.ao, true); + if (typeof token !== 'object' || token === null || Array.isArray(token)) { + token = {}; + } + return token; }; const saveAOToken = (data: Record<string, unknown>) => { - const cfgFolder = path.dirname(tokenFile.ao); - try{ - fs.ensureDirSync(cfgFolder); - fs.writeFileSync(`${tokenFile.ao}.yml`, yaml.stringify(data)); - } - catch(e){ - console.error('Can\'t save token file to disk!'); - } + const cfgFolder = path.dirname(tokenFile.ao); + try { + fs.ensureDirSync(cfgFolder); + fs.writeFileSync(`${tokenFile.ao}.yml`, yaml.stringify(data)); + } catch (e) { + console.error("Can't save token file to disk!"); + } }; const loadHDSession = () => { - let session = loadYamlCfgFile(sessCfgFile.hd, true); - if(typeof session !== 'object' || session === null || Array.isArray(session)){ - session = {}; - } - for(const cv of Object.keys(session)){ - if(typeof session[cv] !== 'object' || session[cv] === null || Array.isArray(session[cv])){ - session[cv] = {}; - } - } - return session; + let session = loadYamlCfgFile(sessCfgFile.hd, true); + if (typeof session !== 'object' || session === null || Array.isArray(session)) { + session = {}; + } + for (const cv of Object.keys(session)) { + if (typeof session[cv] !== 'object' || session[cv] === null || Array.isArray(session[cv])) { + session[cv] = {}; + } + } + return session; }; const saveHDSession = (data: Record<string, unknown>) => { - const cfgFolder = path.dirname(sessCfgFile.hd); - try{ - fs.ensureDirSync(cfgFolder); - fs.writeFileSync(`${sessCfgFile.hd}.yml`, yaml.stringify(data)); - } - catch(e){ - console.error('Can\'t save session file to disk!'); - } + const cfgFolder = path.dirname(sessCfgFile.hd); + try { + fs.ensureDirSync(cfgFolder); + fs.writeFileSync(`${sessCfgFile.hd}.yml`, yaml.stringify(data)); + } catch (e) { + console.error("Can't save session file to disk!"); + } }; - const loadHDToken = () => { - let token = loadYamlCfgFile(tokenFile.hd, true); - if(typeof token !== 'object' || token === null || Array.isArray(token)){ - token = {}; - } - return token; + let token = loadYamlCfgFile(tokenFile.hd, true); + if (typeof token !== 'object' || token === null || Array.isArray(token)) { + token = {}; + } + return token; }; const saveHDToken = (data: Record<string, unknown>) => { - const cfgFolder = path.dirname(tokenFile.hd); - try{ - fs.ensureDirSync(cfgFolder); - fs.writeFileSync(`${tokenFile.hd}.yml`, yaml.stringify(data)); - } - catch(e){ - console.error('Can\'t save token file to disk!'); - } + const cfgFolder = path.dirname(tokenFile.hd); + try { + fs.ensureDirSync(cfgFolder); + fs.writeFileSync(`${tokenFile.hd}.yml`, yaml.stringify(data)); + } catch (e) { + console.error("Can't save token file to disk!"); + } }; const saveHDProfile = (data: Record<string, unknown>) => { - const cfgFolder = path.dirname(hdPflCfgFile); - try{ - fs.ensureDirSync(cfgFolder); - fs.writeFileSync(`${hdPflCfgFile}.yml`, yaml.stringify(data)); - } - catch(e){ - console.error('Can\'t save profile file to disk!'); - } + const cfgFolder = path.dirname(hdPflCfgFile); + try { + fs.ensureDirSync(cfgFolder); + fs.writeFileSync(`${hdPflCfgFile}.yml`, yaml.stringify(data)); + } catch (e) { + console.error("Can't save profile file to disk!"); + } }; const loadHDProfile = () => { - let profile = loadYamlCfgFile(hdPflCfgFile, true); - if(typeof profile !== 'object' || profile === null || Array.isArray(profile) || Object.keys(profile).length === 0){ - profile = { - // base - ipAddress : '', - xNonce : '', - xSignature: '', - // personal - visitId : '', - // profile data - profile: { - userId : 0, - profileId: 0, - deviceId : '', - }, - }; - } - return profile; + let profile = loadYamlCfgFile(hdPflCfgFile, true); + if (typeof profile !== 'object' || profile === null || Array.isArray(profile) || Object.keys(profile).length === 0) { + profile = { + // base + ipAddress: '', + xNonce: '', + xSignature: '', + // personal + visitId: '', + // profile data + profile: { + userId: 0, + profileId: 0, + deviceId: '' + } + }; + } + return profile; }; const loadNewHDToken = () => { - let token = loadYamlCfgFile(tokenFile.hdNew, true); - if(typeof token !== 'object' || token === null || Array.isArray(token)){ - token = {}; - } - return token; + let token = loadYamlCfgFile(tokenFile.hdNew, true); + if (typeof token !== 'object' || token === null || Array.isArray(token)) { + token = {}; + } + return token; }; const saveNewHDToken = (data: Record<string, unknown>) => { - const cfgFolder = path.dirname(tokenFile.hdNew); - try{ - fs.ensureDirSync(cfgFolder); - fs.writeFileSync(`${tokenFile.hdNew}.yml`, yaml.stringify(data)); - } - catch(e){ - console.error('Can\'t save token file to disk!'); - } + const cfgFolder = path.dirname(tokenFile.hdNew); + try { + fs.ensureDirSync(cfgFolder); + fs.writeFileSync(`${tokenFile.hdNew}.yml`, yaml.stringify(data)); + } catch (e) { + console.error("Can't save token file to disk!"); + } }; const cfgDir = path.join(workingDir, 'config'); const getState = (): GuiState => { - const fn = `${stateFile}.json`; - if (!fs.existsSync(fn)) { - return { - 'setup': false, - 'services': {} - }; - } - try { - return JSON.parse(fs.readFileSync(fn).toString()); - } catch(e) { - console.error('Invalid state file, regenerating'); - return { - 'setup': false, - 'services': {} - }; - } + const fn = `${stateFile}.json`; + if (!fs.existsSync(fn)) { + return { + setup: false, + services: {} + }; + } + try { + return JSON.parse(fs.readFileSync(fn).toString()); + } catch (e) { + console.error('Invalid state file, regenerating'); + return { + setup: false, + services: {} + }; + } }; const setState = (state: GuiState) => { - const fn = `${stateFile}.json`; - try { - fs.writeFileSync(fn, JSON.stringify(state, null, 2)); - } catch(e) { - console.error('Failed to write state file.'); - } + const fn = `${stateFile}.json`; + try { + fs.writeFileSync(fn, JSON.stringify(state, null, 2)); + } catch (e) { + console.error('Failed to write state file.'); + } }; - export { - loadBinCfg, - loadCfg, - saveCRSession, - loadCRSession, - saveCRToken, - loadCRToken, - saveADNToken, - loadADNToken, - saveHDSession, - loadHDSession, - saveHDToken, - loadHDToken, - saveNewHDToken, - loadNewHDToken, - saveHDProfile, - loadHDProfile, - saveAOToken, - loadAOToken, - getState, - setState, - writeYamlCfgFile, - sessCfgFile, - hdPflCfgFile, - cfgDir -}; \ No newline at end of file + loadBinCfg, + loadCfg, + saveCRSession, + loadCRSession, + saveCRToken, + loadCRToken, + saveADNToken, + loadADNToken, + saveHDSession, + loadHDSession, + saveHDToken, + loadHDToken, + saveNewHDToken, + loadNewHDToken, + saveHDProfile, + loadHDProfile, + saveAOToken, + loadAOToken, + getState, + setState, + writeYamlCfgFile, + sessCfgFile, + hdPflCfgFile, + cfgDir +}; diff --git a/modules/module.cookieFile.ts b/modules/module.cookieFile.ts index f355084..6e14d1d 100644 --- a/modules/module.cookieFile.ts +++ b/modules/module.cookieFile.ts @@ -1,26 +1,29 @@ const parse = (data: string) => { - const res: Record<string, { - value: string, - expires: Date, - path: string, - domain: string, - secure: boolean - }> = {}; - const split = data.replace(/\r/g,'').split('\n'); - for (const line of split) { - const c = line.split('\t'); - if(c.length < 7){ - continue; - } - res[c[5]] = { - value: c[6], - expires: new Date(parseInt(c[4])*1000), - path: c[2], - domain: c[0].replace(/^\./,''), - secure: c[3] == 'TRUE' ? true : false - }; - } - return res; + const res: Record< + string, + { + value: string; + expires: Date; + path: string; + domain: string; + secure: boolean; + } + > = {}; + const split = data.replace(/\r/g, '').split('\n'); + for (const line of split) { + const c = line.split('\t'); + if (c.length < 7) { + continue; + } + res[c[5]] = { + value: c[6], + expires: new Date(parseInt(c[4]) * 1000), + path: c[2], + domain: c[0].replace(/^\./, ''), + secure: c[3] == 'TRUE' ? true : false + }; + } + return res; }; export default parse; diff --git a/modules/module.downloadArchive.ts b/modules/module.downloadArchive.ts index 76f999a..ceacc8b 100644 --- a/modules/module.downloadArchive.ts +++ b/modules/module.downloadArchive.ts @@ -6,150 +6,180 @@ import { workingDir } from './module.cfg-loader'; export const archiveFile = path.join(workingDir, 'config', 'archive.json'); export type ItemType = { - id: string, - already: string[] -}[] + id: string; + already: string[]; +}[]; export type DataType = { - hidive: { - s: ItemType - }, - ao: { - s: ItemType - }, - adn: { - s: ItemType - }, - crunchy: { - srz: ItemType, - s: ItemType - } -} - -const addToArchive = (kind: { - service: 'crunchy', - type: 's'|'srz' -} | { - service: 'hidive', - type: 's' -} | { - service: 'ao', - type: 's' -} | { - service: 'adn', - type: 's' -}, ID: string) => { - const data = loadData(); - - if (Object.prototype.hasOwnProperty.call(data, kind.service)) { - const items = kind.service === 'crunchy' ? data[kind.service][kind.type] : data[kind.service][kind.type]; - if (items.findIndex(a => a.id === ID) >= 0) // Prevent duplicate - return; - items.push({ - id: ID, - already: [] - }); - (data as any)[kind.service][kind.type] = items; - } else { - if (kind.service === 'ao') { - data['ao'] = { - s: [ - { - id: ID, - already: [] - } - ] - }; - } else if (kind.service === 'crunchy') { - data['crunchy'] = { - s: ([] as ItemType).concat(kind.type === 's' ? { - id: ID, - already: [] as string[] - } : []), - srz: ([] as ItemType).concat(kind.type === 'srz' ? { - id: ID, - already: [] as string[] - } : []), - }; - } else if (kind.service === 'adn') { - data['adn'] = { - s: [ - { - id: ID, - already: [] - } - ] - }; - } else { - data['hidive'] = { - s: [ - { - id: ID, - already: [] - } - ] - }; - } - } - fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4)); + hidive: { + s: ItemType; + }; + ao: { + s: ItemType; + }; + adn: { + s: ItemType; + }; + crunchy: { + srz: ItemType; + s: ItemType; + }; }; -const downloaded = (kind: { - service: 'crunchy', - type: 's'|'srz' -} | { - service: 'hidive', - type: 's' -} | { - service: 'ao', - type: 's' -} | { - service: 'adn', - type: 's' -}, ID: string, episode: string[]) => { - let data = loadData(); - if (!Object.prototype.hasOwnProperty.call(data, kind.service) || !Object.prototype.hasOwnProperty.call(data[kind.service], kind.type) - || !Object.prototype.hasOwnProperty.call((data as any)[kind.service][kind.type], ID)) { - addToArchive(kind, ID); - data = loadData(); // Load updated version - } +const addToArchive = ( + kind: + | { + service: 'crunchy'; + type: 's' | 'srz'; + } + | { + service: 'hidive'; + type: 's'; + } + | { + service: 'ao'; + type: 's'; + } + | { + service: 'adn'; + type: 's'; + }, + ID: string +) => { + const data = loadData(); - const archivedata = (kind.service == 'crunchy' ? data[kind.service][kind.type] : data[kind.service][kind.type]); - const alreadyData = archivedata.find(a => a.id === ID)?.already; - for (const ep of episode) { - if (alreadyData?.includes(ep)) continue; - alreadyData?.push(ep); - } - fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4)); + if (Object.prototype.hasOwnProperty.call(data, kind.service)) { + const items = kind.service === 'crunchy' ? data[kind.service][kind.type] : data[kind.service][kind.type]; + if (items.findIndex((a) => a.id === ID) >= 0) + // Prevent duplicate + return; + items.push({ + id: ID, + already: [] + }); + (data as any)[kind.service][kind.type] = items; + } else { + if (kind.service === 'ao') { + data['ao'] = { + s: [ + { + id: ID, + already: [] + } + ] + }; + } else if (kind.service === 'crunchy') { + data['crunchy'] = { + s: ([] as ItemType).concat( + kind.type === 's' + ? { + id: ID, + already: [] as string[] + } + : [] + ), + srz: ([] as ItemType).concat( + kind.type === 'srz' + ? { + id: ID, + already: [] as string[] + } + : [] + ) + }; + } else if (kind.service === 'adn') { + data['adn'] = { + s: [ + { + id: ID, + already: [] + } + ] + }; + } else { + data['hidive'] = { + s: [ + { + id: ID, + already: [] + } + ] + }; + } + } + fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4)); }; -const makeCommand = (service: 'crunchy'|'hidive'|'ao'|'adn') : Partial<ArgvType>[] => { - const data = loadData(); - const ret: Partial<ArgvType>[] = []; - const kind = data[service]; - for (const type of Object.keys(kind)) { - const item = kind[type as 's']; // 'srz' is also possible but will be ignored for the compiler - item.forEach(i => ret.push({ - but: true, - all: false, - service, - e: i.already.join(','), - ...(type === 's' ? { - s: i.id, - series: undefined - } : { - series: i.id, - s: undefined - }) - })); - } - return ret; +const downloaded = ( + kind: + | { + service: 'crunchy'; + type: 's' | 'srz'; + } + | { + service: 'hidive'; + type: 's'; + } + | { + service: 'ao'; + type: 's'; + } + | { + service: 'adn'; + type: 's'; + }, + ID: string, + episode: string[] +) => { + let data = loadData(); + if ( + !Object.prototype.hasOwnProperty.call(data, kind.service) || + !Object.prototype.hasOwnProperty.call(data[kind.service], kind.type) || + !Object.prototype.hasOwnProperty.call((data as any)[kind.service][kind.type], ID) + ) { + addToArchive(kind, ID); + data = loadData(); // Load updated version + } + + const archivedata = kind.service == 'crunchy' ? data[kind.service][kind.type] : data[kind.service][kind.type]; + const alreadyData = archivedata.find((a) => a.id === ID)?.already; + for (const ep of episode) { + if (alreadyData?.includes(ep)) continue; + alreadyData?.push(ep); + } + fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4)); }; -const loadData = () : DataType => { - if (fs.existsSync(archiveFile)) - return JSON.parse(fs.readFileSync(archiveFile).toString()) as DataType; - return {} as DataType; +const makeCommand = (service: 'crunchy' | 'hidive' | 'ao' | 'adn'): Partial<ArgvType>[] => { + const data = loadData(); + const ret: Partial<ArgvType>[] = []; + const kind = data[service]; + for (const type of Object.keys(kind)) { + const item = kind[type as 's']; // 'srz' is also possible but will be ignored for the compiler + item.forEach((i) => + ret.push({ + but: true, + all: false, + service, + e: i.already.join(','), + ...(type === 's' + ? { + s: i.id, + series: undefined + } + : { + series: i.id, + s: undefined + }) + }) + ); + } + return ret; }; -export { addToArchive, downloaded, makeCommand }; \ No newline at end of file +const loadData = (): DataType => { + if (fs.existsSync(archiveFile)) return JSON.parse(fs.readFileSync(archiveFile).toString()) as DataType; + return {} as DataType; +}; + +export { addToArchive, downloaded, makeCommand }; diff --git a/modules/module.fetch.ts b/modules/module.fetch.ts index e716981..bbb3bf7 100644 --- a/modules/module.fetch.ts +++ b/modules/module.fetch.ts @@ -3,174 +3,179 @@ import { console } from './log'; import { connect } from 'puppeteer-real-browser'; export type Params = { - method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; - headers?: Record<string, string>; - body?: BodyInit | undefined; - binary?: boolean; - followRedirect?: 'follow' | 'error' | 'manual'; + method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; + headers?: Record<string, string>; + body?: BodyInit | undefined; + binary?: boolean; + followRedirect?: 'follow' | 'error' | 'manual'; }; type GetDataResponse = { - ok: boolean; - res?: Response; - headers?: Record<string, string>; - error?: { - name: string; - } & TypeError & { - res?: Response; - }; + ok: boolean; + res?: Response; + headers?: Record<string, string>; + error?: { + name: string; + } & TypeError & { + res?: Response; + }; }; function hasDisplay(): boolean { - if (process.platform === 'linux') { - return !!process.env.DISPLAY || !!process.env.WAYLAND_DISPLAY; - } - // Win and Mac true by default - return true; + if (process.platform === 'linux') { + return !!process.env.DISPLAY || !!process.env.WAYLAND_DISPLAY; + } + // Win and Mac true by default + return true; } // req export class Req { - private sessCfg: string; - private service: 'cr' | 'hd' | 'ao' | 'adn'; - private session: Record< - string, - { - value: string; - expires: Date; - path: string; - domain: string; - secure: boolean; - 'Max-Age'?: string; - } - > = {}; - private cfgDir = yamlCfg.cfgDir; - private curl: boolean | string = false; + private sessCfg: string; + private service: 'cr' | 'hd' | 'ao' | 'adn'; + private session: Record< + string, + { + value: string; + expires: Date; + path: string; + domain: string; + secure: boolean; + 'Max-Age'?: string; + } + > = {}; + private cfgDir = yamlCfg.cfgDir; + private curl: boolean | string = false; - constructor(private domain: Record<string, unknown>, private debug: boolean, private nosess = false, private type: 'cr' | 'hd' | 'ao' | 'adn') { - this.sessCfg = yamlCfg.sessCfgFile[type]; - this.service = type; - } + constructor( + private domain: Record<string, unknown>, + private debug: boolean, + private nosess = false, + private type: 'cr' | 'hd' | 'ao' | 'adn' + ) { + this.sessCfg = yamlCfg.sessCfgFile[type]; + this.service = type; + } - async getData(durl: string, params?: RequestInit): Promise<GetDataResponse> { - params = params || {}; - // options - const options: RequestInit = { - method: params.method ? params.method : 'GET' - }; - // additional params - if (params.headers) { - options.headers = params.headers; - } - if (params.body) { - options.body = params.body; - } - if (typeof params.redirect == 'string') { - options.redirect = params.redirect; - } - // debug - if (this.debug) { - console.debug('[DEBUG] FETCH OPTIONS:'); - console.debug(options); - } - // try do request - try { - const res = await fetch(durl, options); - if (!res.ok) { - console.error(`${res.status}: ${res.statusText}`); - const body = await res.text(); - const docTitle = body.match(/<title>(.*)<\/title>/); - if (body && docTitle) { - if (docTitle[1] === 'Just a moment...' && durl.includes('crunchyroll') && hasDisplay()) { - console.warn('Cloudflare triggered, trying to get cookies...'); + async getData(durl: string, params?: RequestInit): Promise<GetDataResponse> { + params = params || {}; + // options + const options: RequestInit = { + method: params.method ? params.method : 'GET' + }; + // additional params + if (params.headers) { + options.headers = params.headers; + } + if (params.body) { + options.body = params.body; + } + if (typeof params.redirect == 'string') { + options.redirect = params.redirect; + } + // debug + if (this.debug) { + console.debug('[DEBUG] FETCH OPTIONS:'); + console.debug(options); + } + // try do request + try { + const res = await fetch(durl, options); + if (!res.ok) { + console.error(`${res.status}: ${res.statusText}`); + const body = await res.text(); + const docTitle = body.match(/<title>(.*)<\/title>/); + if (body && docTitle) { + if (docTitle[1] === 'Just a moment...' && durl.includes('crunchyroll') && hasDisplay()) { + console.warn('Cloudflare triggered, trying to get cookies...'); - const { page } = await connect({ - headless: false, - turnstile: true - }); + const { page } = await connect({ + headless: false, + turnstile: true + }); - await page.goto('https://www.crunchyroll.com/', { - waitUntil: 'networkidle2' - }); + await page.goto('https://www.crunchyroll.com/', { + waitUntil: 'networkidle2' + }); - await page.waitForRequest('https://www.crunchyroll.com/auth/v1/token'); + await page.waitForRequest('https://www.crunchyroll.com/auth/v1/token'); - const cookies = await page.cookies(); + const cookies = await page.cookies(); - await page.close(); + await page.close(); - params.headers = { - ...params.headers, - Cookie: cookies.map((c) => `${c.name}=${c.value}`).join('; '), - 'Set-Cookie': cookies.map((c) => `${c.name}=${c.value}`).join('; ') - }; + params.headers = { + ...params.headers, + Cookie: cookies.map((c) => `${c.name}=${c.value}`).join('; '), + 'Set-Cookie': cookies.map((c) => `${c.name}=${c.value}`).join('; ') + }; - (params as any).headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36'; + (params as any).headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36'; - return await this.getData(durl, params); - } else { - console.error(docTitle[1]); - } - } else { - console.error(body); - } - } - return { - ok: res.ok, - res, - headers: params.headers as Record<string, string> - }; - } catch (_error) { - const error = _error as { - name: string; - } & TypeError & { - res: Response; - }; - if (error.res && error.res.status && error.res.statusText) { - console.error(`${error.name} ${error.res.status}: ${error.res.statusText}`); - } else { - console.error(`${error.name}: ${error.res?.statusText || error.message}`); - } - if (error.res) { - const body = await error.res.text(); - const docTitle = body.match(/<title>(.*)<\/title>/); - if (body && docTitle) { - console.error(docTitle[1]); - } - } - return { - ok: false, - error - }; - } - } + return await this.getData(durl, params); + } else { + console.error(docTitle[1]); + } + } else { + console.error(body); + } + } + return { + ok: res.ok, + res, + headers: params.headers as Record<string, string> + }; + } catch (_error) { + const error = _error as { + name: string; + } & TypeError & { + res: Response; + }; + if (error.res && error.res.status && error.res.statusText) { + console.error(`${error.name} ${error.res.status}: ${error.res.statusText}`); + } else { + console.error(`${error.name}: ${error.res?.statusText || error.message}`); + } + if (error.res) { + const body = await error.res.text(); + const docTitle = body.match(/<title>(.*)<\/title>/); + if (body && docTitle) { + console.error(docTitle[1]); + } + } + return { + ok: false, + error + }; + } + } } export function buildProxy(proxyBaseUrl: string, proxyAuth: string) { - if (!proxyBaseUrl.match(/^(https?|socks4|socks5):/)) { - proxyBaseUrl = 'http://' + proxyBaseUrl; - } + if (!proxyBaseUrl.match(/^(https?|socks4|socks5):/)) { + proxyBaseUrl = 'http://' + proxyBaseUrl; + } - const proxyCfg = new URL(proxyBaseUrl); - let proxyStr = `${proxyCfg.protocol}//`; + const proxyCfg = new URL(proxyBaseUrl); + let proxyStr = `${proxyCfg.protocol}//`; - if (typeof proxyCfg.hostname != 'string' || proxyCfg.hostname == '') { - throw new Error('[ERROR] Hostname and port required for proxy!'); - } + if (typeof proxyCfg.hostname != 'string' || proxyCfg.hostname == '') { + throw new Error('[ERROR] Hostname and port required for proxy!'); + } - if (proxyAuth && typeof proxyAuth == 'string' && proxyAuth.match(':')) { - proxyCfg.username = proxyAuth.split(':')[0]; - proxyCfg.password = proxyAuth.split(':')[1]; - proxyStr += `${proxyCfg.username}:${proxyCfg.password}@`; - } + if (proxyAuth && typeof proxyAuth == 'string' && proxyAuth.match(':')) { + proxyCfg.username = proxyAuth.split(':')[0]; + proxyCfg.password = proxyAuth.split(':')[1]; + proxyStr += `${proxyCfg.username}:${proxyCfg.password}@`; + } - proxyStr += proxyCfg.hostname; + proxyStr += proxyCfg.hostname; - if (!proxyCfg.port && proxyCfg.protocol == 'http:') { - proxyStr += ':80'; - } else if (!proxyCfg.port && proxyCfg.protocol == 'https:') { - proxyStr += ':443'; - } + if (!proxyCfg.port && proxyCfg.protocol == 'http:') { + proxyStr += ':80'; + } else if (!proxyCfg.port && proxyCfg.protocol == 'https:') { + proxyStr += ':443'; + } - return proxyStr; + return proxyStr; } diff --git a/modules/module.ffmpegChapter.ts b/modules/module.ffmpegChapter.ts index c701a78..82df6cf 100644 --- a/modules/module.ffmpegChapter.ts +++ b/modules/module.ffmpegChapter.ts @@ -1,51 +1,51 @@ import fs from 'fs'; export function convertChaptersToFFmpegFormat(inputFilePath: string): string { - const content = fs.readFileSync(inputFilePath, 'utf-8'); + const content = fs.readFileSync(inputFilePath, 'utf-8'); - const chapterMatches = Array.from(content.matchAll(/CHAPTER(\d+)=([\d:.]+)/g)); - const nameMatches = Array.from(content.matchAll(/CHAPTER(\d+)NAME=([^\n]+)/g)); + const chapterMatches = Array.from(content.matchAll(/CHAPTER(\d+)=([\d:.]+)/g)); + const nameMatches = Array.from(content.matchAll(/CHAPTER(\d+)NAME=([^\n]+)/g)); - const chapters = chapterMatches.map((m) => ({ - index: parseInt(m[1], 10), - time: m[2], - })).sort((a, b) => a.index - b.index); + const chapters = chapterMatches + .map((m) => ({ + index: parseInt(m[1], 10), + time: m[2] + })) + .sort((a, b) => a.index - b.index); - const nameDict: Record<number, string> = {}; - nameMatches.forEach((m) => { - nameDict[parseInt(m[1], 10)] = m[2]; - }); + const nameDict: Record<number, string> = {}; + nameMatches.forEach((m) => { + nameDict[parseInt(m[1], 10)] = m[2]; + }); - let ffmpegContent = ';FFMETADATA1\n'; - let startTimeInNs = 0; + let ffmpegContent = ';FFMETADATA1\n'; + let startTimeInNs = 0; - for (let i = 0; i < chapters.length; i++) { - const chapterStartTime = timeToNanoSeconds(chapters[i].time); - const chapterEndTime = (i + 1 < chapters.length) - ? timeToNanoSeconds(chapters[i + 1].time) - : chapterStartTime + 1000000000; + for (let i = 0; i < chapters.length; i++) { + const chapterStartTime = timeToNanoSeconds(chapters[i].time); + const chapterEndTime = i + 1 < chapters.length ? timeToNanoSeconds(chapters[i + 1].time) : chapterStartTime + 1000000000; - const chapterName = nameDict[chapters[i].index] || `Chapter ${chapters[i].index}`; + const chapterName = nameDict[chapters[i].index] || `Chapter ${chapters[i].index}`; - ffmpegContent += '[CHAPTER]\n'; - ffmpegContent += 'TIMEBASE=1/1000000000\n'; - ffmpegContent += `START=${startTimeInNs}\n`; - ffmpegContent += `END=${chapterEndTime}\n`; - ffmpegContent += `title=${chapterName}\n`; + ffmpegContent += '[CHAPTER]\n'; + ffmpegContent += 'TIMEBASE=1/1000000000\n'; + ffmpegContent += `START=${startTimeInNs}\n`; + ffmpegContent += `END=${chapterEndTime}\n`; + ffmpegContent += `title=${chapterName}\n`; - startTimeInNs = chapterEndTime; - } + startTimeInNs = chapterEndTime; + } - return ffmpegContent; + return ffmpegContent; } export function timeToNanoSeconds(time: string): number { - const parts = time.split(':'); - const hours = parseInt(parts[0], 10); - const minutes = parseInt(parts[1], 10); - const secondsAndMs = parts[2].split('.'); - const seconds = parseInt(secondsAndMs[0], 10); - const milliseconds = parseInt(secondsAndMs[1], 10); + const parts = time.split(':'); + const hours = parseInt(parts[0], 10); + const minutes = parseInt(parts[1], 10); + const secondsAndMs = parts[2].split('.'); + const seconds = parseInt(secondsAndMs[0], 10); + const milliseconds = parseInt(secondsAndMs[1], 10); - return (hours * 3600 + minutes * 60 + seconds) * 1000000000 + milliseconds * 1000000; -} \ No newline at end of file + return (hours * 3600 + minutes * 60 + seconds) * 1000000000 + milliseconds * 1000000; +} diff --git a/modules/module.filename.ts b/modules/module.filename.ts index ec7346b..ac7fcd1 100644 --- a/modules/module.filename.ts +++ b/modules/module.filename.ts @@ -3,89 +3,87 @@ import { AvailableFilenameVars } from './module.args'; import { console } from './log'; import Helper from './module.helper'; -export type Variable<T extends string = AvailableFilenameVars> = ({ - type: 'number', - replaceWith: number -} | { - type: 'string', - replaceWith: string -}) & { - name: T, - sanitize?: boolean -} +export type Variable<T extends string = AvailableFilenameVars> = ( + | { + type: 'number'; + replaceWith: number; + } + | { + type: 'string'; + replaceWith: string; + } +) & { + name: T; + sanitize?: boolean; +}; const parseFileName = (input: string, variables: Variable[], numbers: number, override: string[]): string[] => { - const varRegex = /\${[A-Za-z1-9]+}/g; - const vars = input.match(varRegex); - const overridenVars = parseOverride(variables, override); - if (!vars) - return [input]; - for (let i = 0; i < vars.length; i++) { - const type = vars[i]; - const varName = type.slice(2, -1); - let use = overridenVars.find(a => a.name === varName); - if (use === undefined && type === '${height}') { - use = { type: 'number', replaceWith: 0 } as Variable<string>; - } - if (use === undefined) { - console.info(`[ERROR] Found variable '${type}' in fileName but no values was internally found!`); - continue; - } - - if (use.type === 'number') { - const len = use.replaceWith.toFixed(0).length; - const replaceStr = len < numbers ? '0'.repeat(numbers - len) + use.replaceWith : use.replaceWith+''; - input = input.replace(type, replaceStr); - } else { - if (use.sanitize) - use.replaceWith = Helper.cleanupFilename(use.replaceWith); - input = input.replace(type, use.replaceWith); - } - } - return input.split(path.sep).map(a => Helper.cleanupFilename(a)); + const varRegex = /\${[A-Za-z1-9]+}/g; + const vars = input.match(varRegex); + const overridenVars = parseOverride(variables, override); + if (!vars) return [input]; + for (let i = 0; i < vars.length; i++) { + const type = vars[i]; + const varName = type.slice(2, -1); + let use = overridenVars.find((a) => a.name === varName); + if (use === undefined && type === '${height}') { + use = { type: 'number', replaceWith: 0 } as Variable<string>; + } + if (use === undefined) { + console.info(`[ERROR] Found variable '${type}' in fileName but no values was internally found!`); + continue; + } + + if (use.type === 'number') { + const len = use.replaceWith.toFixed(0).length; + const replaceStr = len < numbers ? '0'.repeat(numbers - len) + use.replaceWith : use.replaceWith + ''; + input = input.replace(type, replaceStr); + } else { + if (use.sanitize) use.replaceWith = Helper.cleanupFilename(use.replaceWith); + input = input.replace(type, use.replaceWith); + } + } + return input.split(path.sep).map((a) => Helper.cleanupFilename(a)); }; const parseOverride = (variables: Variable[], override: string[]): Variable<string>[] => { - const vars: Variable<string>[] = variables; - override.forEach(item => { - const index = item.indexOf('='); - if (index === -1) - return logError(item, 'invalid'); - const parts = [ item.slice(0, index), item.slice(index + 1) ]; - if (!(parts[1].startsWith('\'') && parts[1].endsWith('\'') && parts[1].length >= 2)) - return logError(item, 'invalid'); - parts[1] = parts[1].slice(1, -1); - const already = vars.findIndex(a => a.name === parts[0]); - if (already > -1) { - if (vars[already].type === 'number') { - if (isNaN(parseFloat(parts[1]))) - return logError(item, 'wrongType'); - vars[already].replaceWith = parseFloat(parts[1]); - } else { - vars[already].replaceWith = parts[1]; - } - } else { - const isNumber = !isNaN(parseFloat(parts[1])); - vars.push({ - name: parts[0], - replaceWith: isNumber ? parseFloat(parts[1]) : parts[1], - type: isNumber ? 'number' : 'string' - } as Variable<string>); - } - }); + const vars: Variable<string>[] = variables; + override.forEach((item) => { + const index = item.indexOf('='); + if (index === -1) return logError(item, 'invalid'); + const parts = [item.slice(0, index), item.slice(index + 1)]; + if (!(parts[1].startsWith("'") && parts[1].endsWith("'") && parts[1].length >= 2)) return logError(item, 'invalid'); + parts[1] = parts[1].slice(1, -1); + const already = vars.findIndex((a) => a.name === parts[0]); + if (already > -1) { + if (vars[already].type === 'number') { + if (isNaN(parseFloat(parts[1]))) return logError(item, 'wrongType'); + vars[already].replaceWith = parseFloat(parts[1]); + } else { + vars[already].replaceWith = parts[1]; + } + } else { + const isNumber = !isNaN(parseFloat(parts[1])); + vars.push({ + name: parts[0], + replaceWith: isNumber ? parseFloat(parts[1]) : parts[1], + type: isNumber ? 'number' : 'string' + } as Variable<string>); + } + }); - return variables; + return variables; }; -const logError = (override: string, reason: 'invalid'|'wrongType') => { - switch (reason) { - case 'wrongType': - console.error(`[ERROR] Invalid type on \`${override}\`. Expected number but found string. It has been ignored`); - break; - case 'invalid': - default: - console.error(`[ERROR] Invalid override \`${override}\`. It has been ignored`); - } +const logError = (override: string, reason: 'invalid' | 'wrongType') => { + switch (reason) { + case 'wrongType': + console.error(`[ERROR] Invalid type on \`${override}\`. Expected number but found string. It has been ignored`); + break; + case 'invalid': + default: + console.error(`[ERROR] Invalid override \`${override}\`. It has been ignored`); + } }; -export default parseFileName; \ No newline at end of file +export default parseFileName; diff --git a/modules/module.fontsData.ts b/modules/module.fontsData.ts index 7fc69d9..5c848d7 100644 --- a/modules/module.fontsData.ts +++ b/modules/module.fontsData.ts @@ -3,99 +3,99 @@ const root = 'https://static.crunchyroll.com/vilos-v2/web/vilos/assets/libass-fo // file list const fontFamilies = { - 'Adobe Arabic': ['AdobeArabic-Bold.otf'], - 'Andale Mono': ['andalemo.ttf'], - 'Arial': ['arial.ttf'], - 'Arial Black': ['ariblk.ttf'], - 'Arial Bold': ['arialbd.ttf'], - 'Arial Bold Italic': ['arialbi.ttf'], - 'Arial Italic': ['ariali.ttf'], - 'Arial Unicode MS': ['arialuni.ttf'], - 'Comic Sans MS': ['comic.ttf'], - 'Comic Sans MS Bold': ['comicbd.ttf'], - 'Courier New': ['cour.ttf'], - 'Courier New Bold': ['courbd.ttf'], - 'Courier New Bold Italic': ['courbi.ttf'], - 'Courier New Italic': ['couri.ttf'], - 'DejaVu LGC Sans Mono': ['DejaVuLGCSansMono.ttf'], - 'DejaVu LGC Sans Mono Bold': ['DejaVuLGCSansMono-Bold.ttf'], - 'DejaVu LGC Sans Mono Bold Oblique': ['DejaVuLGCSansMono-BoldOblique.ttf'], - 'DejaVu LGC Sans Mono Oblique': ['DejaVuLGCSansMono-Oblique.ttf'], - 'DejaVu Sans': ['DejaVuSans.ttf'], - 'DejaVu Sans Bold': ['DejaVuSans-Bold.ttf'], - 'DejaVu Sans Bold Oblique': ['DejaVuSans-BoldOblique.ttf'], - 'DejaVu Sans Condensed': ['DejaVuSansCondensed.ttf'], - 'DejaVu Sans Condensed Bold': ['DejaVuSansCondensed-Bold.ttf'], - 'DejaVu Sans Condensed Bold Oblique': ['DejaVuSansCondensed-BoldOblique.ttf'], - 'DejaVu Sans Condensed Oblique': ['DejaVuSansCondensed-Oblique.ttf'], - 'DejaVu Sans ExtraLight': ['DejaVuSans-ExtraLight.ttf'], - 'DejaVu Sans Mono': ['DejaVuSansMono.ttf'], - 'DejaVu Sans Mono Bold': ['DejaVuSansMono-Bold.ttf'], - 'DejaVu Sans Mono Bold Oblique': ['DejaVuSansMono-BoldOblique.ttf'], - 'DejaVu Sans Mono Oblique': ['DejaVuSansMono-Oblique.ttf'], - 'DejaVu Sans Oblique': ['DejaVuSans-Oblique.ttf'], - 'Gautami': ['gautami.ttf'], - 'Georgia': ['georgia.ttf'], - 'Georgia Bold': ['georgiab.ttf'], - 'Georgia Bold Italic': ['georgiaz.ttf'], - 'Georgia Italic': ['georgiai.ttf'], - 'Impact': ['impact.ttf'], - 'Meera Inimai': ['MeeraInimai-Regular.ttf'], - 'Noto Sans Thai': ['NotoSansThai.ttf'], - 'Rubik': ['Rubik-Regular.ttf'], - 'Rubik Black': ['Rubik-Black.ttf'], - 'Rubik Black Italic': ['Rubik-BlackItalic.ttf'], - 'Rubik Bold': ['Rubik-Bold.ttf'], - 'Rubik Bold Italic': ['Rubik-BoldItalic.ttf'], - 'Rubik Italic': ['Rubik-Italic.ttf'], - 'Rubik Light': ['Rubik-Light.ttf'], - 'Rubik Light Italic': ['Rubik-LightItalic.ttf'], - 'Rubik Medium': ['Rubik-Medium.ttf'], - 'Rubik Medium Italic': ['Rubik-MediumItalic.ttf'], - 'Tahoma': ['tahoma.ttf'], - 'Times New Roman': ['times.ttf'], - 'Times New Roman Bold': ['timesbd.ttf'], - 'Times New Roman Bold Italic': ['timesbi.ttf'], - 'Times New Roman Italic': ['timesi.ttf'], - 'Trebuchet MS': ['trebuc.ttf'], - 'Trebuchet MS Bold': ['trebucbd.ttf'], - 'Trebuchet MS Bold Italic': ['trebucbi.ttf'], - 'Trebuchet MS Italic': ['trebucit.ttf'], - 'Verdana': ['verdana.ttf'], - 'Verdana Bold': ['verdanab.ttf'], - 'Verdana Bold Italic': ['verdanaz.ttf'], - 'Verdana Italic': ['verdanai.ttf'], - 'Vrinda': ['vrinda.ttf'], - 'Vrinda Bold': ['vrindab.ttf'], - 'Webdings': ['webdings.ttf'], + 'Adobe Arabic': ['AdobeArabic-Bold.otf'], + 'Andale Mono': ['andalemo.ttf'], + Arial: ['arial.ttf'], + 'Arial Black': ['ariblk.ttf'], + 'Arial Bold': ['arialbd.ttf'], + 'Arial Bold Italic': ['arialbi.ttf'], + 'Arial Italic': ['ariali.ttf'], + 'Arial Unicode MS': ['arialuni.ttf'], + 'Comic Sans MS': ['comic.ttf'], + 'Comic Sans MS Bold': ['comicbd.ttf'], + 'Courier New': ['cour.ttf'], + 'Courier New Bold': ['courbd.ttf'], + 'Courier New Bold Italic': ['courbi.ttf'], + 'Courier New Italic': ['couri.ttf'], + 'DejaVu LGC Sans Mono': ['DejaVuLGCSansMono.ttf'], + 'DejaVu LGC Sans Mono Bold': ['DejaVuLGCSansMono-Bold.ttf'], + 'DejaVu LGC Sans Mono Bold Oblique': ['DejaVuLGCSansMono-BoldOblique.ttf'], + 'DejaVu LGC Sans Mono Oblique': ['DejaVuLGCSansMono-Oblique.ttf'], + 'DejaVu Sans': ['DejaVuSans.ttf'], + 'DejaVu Sans Bold': ['DejaVuSans-Bold.ttf'], + 'DejaVu Sans Bold Oblique': ['DejaVuSans-BoldOblique.ttf'], + 'DejaVu Sans Condensed': ['DejaVuSansCondensed.ttf'], + 'DejaVu Sans Condensed Bold': ['DejaVuSansCondensed-Bold.ttf'], + 'DejaVu Sans Condensed Bold Oblique': ['DejaVuSansCondensed-BoldOblique.ttf'], + 'DejaVu Sans Condensed Oblique': ['DejaVuSansCondensed-Oblique.ttf'], + 'DejaVu Sans ExtraLight': ['DejaVuSans-ExtraLight.ttf'], + 'DejaVu Sans Mono': ['DejaVuSansMono.ttf'], + 'DejaVu Sans Mono Bold': ['DejaVuSansMono-Bold.ttf'], + 'DejaVu Sans Mono Bold Oblique': ['DejaVuSansMono-BoldOblique.ttf'], + 'DejaVu Sans Mono Oblique': ['DejaVuSansMono-Oblique.ttf'], + 'DejaVu Sans Oblique': ['DejaVuSans-Oblique.ttf'], + Gautami: ['gautami.ttf'], + Georgia: ['georgia.ttf'], + 'Georgia Bold': ['georgiab.ttf'], + 'Georgia Bold Italic': ['georgiaz.ttf'], + 'Georgia Italic': ['georgiai.ttf'], + Impact: ['impact.ttf'], + 'Meera Inimai': ['MeeraInimai-Regular.ttf'], + 'Noto Sans Thai': ['NotoSansThai.ttf'], + Rubik: ['Rubik-Regular.ttf'], + 'Rubik Black': ['Rubik-Black.ttf'], + 'Rubik Black Italic': ['Rubik-BlackItalic.ttf'], + 'Rubik Bold': ['Rubik-Bold.ttf'], + 'Rubik Bold Italic': ['Rubik-BoldItalic.ttf'], + 'Rubik Italic': ['Rubik-Italic.ttf'], + 'Rubik Light': ['Rubik-Light.ttf'], + 'Rubik Light Italic': ['Rubik-LightItalic.ttf'], + 'Rubik Medium': ['Rubik-Medium.ttf'], + 'Rubik Medium Italic': ['Rubik-MediumItalic.ttf'], + Tahoma: ['tahoma.ttf'], + 'Times New Roman': ['times.ttf'], + 'Times New Roman Bold': ['timesbd.ttf'], + 'Times New Roman Bold Italic': ['timesbi.ttf'], + 'Times New Roman Italic': ['timesi.ttf'], + 'Trebuchet MS': ['trebuc.ttf'], + 'Trebuchet MS Bold': ['trebucbd.ttf'], + 'Trebuchet MS Bold Italic': ['trebucbi.ttf'], + 'Trebuchet MS Italic': ['trebucit.ttf'], + Verdana: ['verdana.ttf'], + 'Verdana Bold': ['verdanab.ttf'], + 'Verdana Bold Italic': ['verdanaz.ttf'], + 'Verdana Italic': ['verdanai.ttf'], + Vrinda: ['vrinda.ttf'], + 'Vrinda Bold': ['vrindab.ttf'], + Webdings: ['webdings.ttf'] }; // collect styles from ass string -function assFonts(ass: string){ - const strings = ass.replace(/\r/g,'').split('\n'); - const styles: string[] = []; - for(const s of strings){ - if(s.match(/^Style: /)){ - const addStyle = s.split(','); - styles.push(addStyle[1]); - } - } - const fontMatches = ass.matchAll(/\\fn([^\\}]+)/g); - for (const match of fontMatches) { - styles.push(match[1]); - } - return [...new Set(styles)]; +function assFonts(ass: string) { + const strings = ass.replace(/\r/g, '').split('\n'); + const styles: string[] = []; + for (const s of strings) { + if (s.match(/^Style: /)) { + const addStyle = s.split(','); + styles.push(addStyle[1]); + } + } + const fontMatches = ass.matchAll(/\\fn([^\\}]+)/g); + for (const match of fontMatches) { + styles.push(match[1]); + } + return [...new Set(styles)]; } // font mime type -function fontMime(fontFile: string){ - if(fontFile.match(/\.otf$/)){ - return 'application/vnd.ms-opentype'; - } - if(fontFile.match(/\.ttf$/)){ - return 'application/x-truetype-font'; - } - return 'application/octet-stream'; +function fontMime(fontFile: string) { + if (fontFile.match(/\.otf$/)) { + return 'application/vnd.ms-opentype'; + } + if (fontFile.match(/\.ttf$/)) { + return 'application/x-truetype-font'; + } + return 'application/octet-stream'; } export type AvailableFonts = keyof typeof fontFamilies; diff --git a/modules/module.helper.ts b/modules/module.helper.ts index da60eab..41cc141 100644 --- a/modules/module.helper.ts +++ b/modules/module.helper.ts @@ -5,73 +5,73 @@ import childProcess from 'child_process'; import { console } from './log'; export default class Helper { - static async question(q: string) { - const rl = readline.createInterface({ input, output }); - const a = await rl.question(q); - rl.close(); - return a; - } - static formatTime(t: number) { - const days = Math.floor(t / 86400); - const hours = Math.floor((t % 86400) / 3600); - const minutes = Math.floor(((t % 86400) % 3600) / 60); - const seconds = t % 60; - const daysS = days > 0 ? `${days}d` : ''; - const hoursS = daysS || hours ? `${daysS}${daysS && hours < 10 ? '0' : ''}${hours}h` : ''; - const minutesS = minutes || hoursS ? `${hoursS}${hoursS && minutes < 10 ? '0' : ''}${minutes}m` : ''; - const secondsS = `${minutesS}${minutesS && seconds < 10 ? '0' : ''}${seconds}s`; - return secondsS; - } + static async question(q: string) { + const rl = readline.createInterface({ input, output }); + const a = await rl.question(q); + rl.close(); + return a; + } + static formatTime(t: number) { + const days = Math.floor(t / 86400); + const hours = Math.floor((t % 86400) / 3600); + const minutes = Math.floor(((t % 86400) % 3600) / 60); + const seconds = t % 60; + const daysS = days > 0 ? `${days}d` : ''; + const hoursS = daysS || hours ? `${daysS}${daysS && hours < 10 ? '0' : ''}${hours}h` : ''; + const minutesS = minutes || hoursS ? `${hoursS}${hoursS && minutes < 10 ? '0' : ''}${minutes}m` : ''; + const secondsS = `${minutesS}${minutesS && seconds < 10 ? '0' : ''}${seconds}s`; + return secondsS; + } - static cleanupFilename(n: string) { - /* eslint-disable no-useless-escape, no-control-regex */ - const fixingChar = '_'; - const illegalRe = /[\/\?<>\\:\*\|":]/g; - const controlRe = /[\x00-\x1f\x80-\x9f]/g; - const reservedRe = /^\.+$/; - const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i; - const windowsTrailingRe = /[\. ]+$/; - return n - .replace(illegalRe, fixingChar) - .replace(controlRe, fixingChar) - .replace(reservedRe, fixingChar) - .replace(windowsReservedRe, fixingChar) - .replace(windowsTrailingRe, fixingChar); - } + static cleanupFilename(n: string) { + /* eslint-disable no-useless-escape, no-control-regex */ + const fixingChar = '_'; + const illegalRe = /[\/\?<>\\:\*\|":]/g; + const controlRe = /[\x00-\x1f\x80-\x9f]/g; + const reservedRe = /^\.+$/; + const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i; + const windowsTrailingRe = /[\. ]+$/; + return n + .replace(illegalRe, fixingChar) + .replace(controlRe, fixingChar) + .replace(reservedRe, fixingChar) + .replace(windowsReservedRe, fixingChar) + .replace(windowsTrailingRe, fixingChar); + } - static exec( - pname: string, - fpath: string, - pargs: string, - spc = false - ): - | { - isOk: true; - } - | { - isOk: false; - err: Error & { code: number }; - } { - pargs = pargs ? ' ' + pargs : ''; - console.info(`\n> "${pname}"${pargs}${spc ? '\n' : ''}`); - try { - if (process.platform === 'win32') { - childProcess.execSync('& ' + fpath + pargs, { stdio: 'inherit', shell: 'powershell.exe', windowsHide: true }); - } else { - childProcess.execSync(fpath + pargs, { stdio: 'inherit' }); - } - return { - isOk: true - }; - } catch (er) { - const err = er as Error & { status: number }; - return { - isOk: false, - err: { - ...err, - code: err.status - } - }; - } - } + static exec( + pname: string, + fpath: string, + pargs: string, + spc = false + ): + | { + isOk: true; + } + | { + isOk: false; + err: Error & { code: number }; + } { + pargs = pargs ? ' ' + pargs : ''; + console.info(`\n> "${pname}"${pargs}${spc ? '\n' : ''}`); + try { + if (process.platform === 'win32') { + childProcess.execSync('& ' + fpath + pargs, { stdio: 'inherit', shell: 'powershell.exe', windowsHide: true }); + } else { + childProcess.execSync(fpath + pargs, { stdio: 'inherit' }); + } + return { + isOk: true + }; + } catch (er) { + const err = er as Error & { status: number }; + return { + isOk: false, + err: { + ...err, + code: err.status + } + }; + } + } } diff --git a/modules/module.langsData.ts b/modules/module.langsData.ts index 043b32e..c37976e 100644 --- a/modules/module.langsData.ts +++ b/modules/module.langsData.ts @@ -1,204 +1,250 @@ // available langs export type LanguageItem = { - cr_locale?: string, - hd_locale?: string, - adn_locale?: string, - new_hd_locale?: string, - ao_locale?: string, - locale: string, - code: string, - name: string, - language?: string -} + cr_locale?: string; + hd_locale?: string; + adn_locale?: string; + new_hd_locale?: string; + ao_locale?: string; + locale: string; + code: string; + name: string; + language?: string; +}; const languages: LanguageItem[] = [ - { locale: 'un', code: 'und', name: 'Undetermined', language: 'Undetermined', new_hd_locale: 'und', cr_locale: 'und', adn_locale: 'und', ao_locale: 'und' }, - { cr_locale: 'en-US', new_hd_locale: 'en-US', hd_locale: 'English', locale: 'en', code: 'eng', name: 'English' }, - { cr_locale: 'en-IN', locale: 'en-IN', code: 'eng', name: 'English (India)', }, - { cr_locale: 'es-LA', new_hd_locale: 'es-MX', hd_locale: 'Spanish LatAm', locale: 'es-419', code: 'spa', name: 'Spanish', language: 'Latin American Spanish' }, - { cr_locale: 'es-419',ao_locale: 'es',hd_locale: 'Spanish', locale: 'es-419', code: 'spa-419', name: 'Spanish', language: 'Latin American Spanish' }, - { cr_locale: 'es-ES', new_hd_locale: 'es-ES', hd_locale: 'Spanish Europe', locale: 'es-ES', code: 'spa-ES', name: 'Castilian', language: 'European Spanish' }, - { cr_locale: 'pt-BR', ao_locale: 'pt',new_hd_locale: 'pt-BR', hd_locale: 'Portuguese', locale: 'pt-BR', code: 'por', name: 'Portuguese', language: 'Brazilian Portuguese' }, - { cr_locale: 'pt-PT', locale: 'pt-PT', code: 'por', name: 'Portuguese (Portugal)', language: 'Portugues (Portugal)' }, - { cr_locale: 'fr-FR', adn_locale: 'fr', hd_locale: 'French', locale: 'fr', code: 'fra', name: 'French' }, - { cr_locale: 'de-DE', adn_locale: 'de', hd_locale: 'German', locale: 'de', code: 'deu', name: 'German' }, - { cr_locale: 'ar-ME', locale: 'ar', code: 'ara-ME', name: 'Arabic' }, - { cr_locale: 'ar-SA', hd_locale: 'Arabic', locale: 'ar', code: 'ara', name: 'Arabic (Saudi Arabia)' }, - { cr_locale: 'it-IT', hd_locale: 'Italian', locale: 'it', code: 'ita', name: 'Italian' }, - { cr_locale: 'ru-RU', hd_locale: 'Russian', locale: 'ru', code: 'rus', name: 'Russian' }, - { cr_locale: 'tr-TR', hd_locale: 'Turkish', locale: 'tr', code: 'tur', name: 'Turkish' }, - { cr_locale: 'hi-IN', locale: 'hi', code: 'hin', name: 'Hindi' }, - { locale: 'zh', code: 'cmn', name: 'Chinese (Mandarin, PRC)' }, - { cr_locale: 'zh-CN', locale: 'zh-CN', code: 'zho', name: 'Chinese (Mainland China)' }, - { cr_locale: 'zh-TW', locale: 'zh-TW', code: 'chi', name: 'Chinese (Taiwan)' }, - { cr_locale: 'zh-HK', locale: 'zh-HK', code: 'zh-HK', name: 'Chinese (Hong-Kong)', language: '中文 (粵語)' }, - { cr_locale: 'ko-KR', hd_locale: 'Korean', locale: 'ko', code: 'kor', name: 'Korean' }, - { cr_locale: 'ca-ES', locale: 'ca-ES', code: 'cat', name: 'Catalan' }, - { cr_locale: 'pl-PL', locale: 'pl-PL', code: 'pol', name: 'Polish' }, - { cr_locale: 'th-TH', locale: 'th-TH', code: 'tha', name: 'Thai', language: 'ไทย' }, - { cr_locale: 'ta-IN', locale: 'ta-IN', code: 'tam', name: 'Tamil (India)', language: 'தமிழ்' }, - { cr_locale: 'ms-MY', locale: 'ms-MY', code: 'may', name: 'Malay (Malaysia)', language: 'Bahasa Melayu' }, - { cr_locale: 'vi-VN', locale: 'vi-VN', code: 'vie', name: 'Vietnamese', language: 'Tiếng Việt' }, - { cr_locale: 'id-ID', locale: 'id-ID', code: 'ind', name: 'Indonesian', language: 'Bahasa Indonesia' }, - { cr_locale: 'te-IN', locale: 'te-IN', code: 'tel', name: 'Telugu (India)', language: 'తెలుగు' }, - { cr_locale: 'ja-JP', adn_locale: 'ja', ao_locale: 'ja', hd_locale: 'Japanese', locale: 'ja', code: 'jpn', name: 'Japanese' }, + { locale: 'un', code: 'und', name: 'Undetermined', language: 'Undetermined', new_hd_locale: 'und', cr_locale: 'und', adn_locale: 'und', ao_locale: 'und' }, + { cr_locale: 'en-US', new_hd_locale: 'en-US', hd_locale: 'English', locale: 'en', code: 'eng', name: 'English' }, + { cr_locale: 'en-IN', locale: 'en-IN', code: 'eng', name: 'English (India)' }, + { cr_locale: 'es-LA', new_hd_locale: 'es-MX', hd_locale: 'Spanish LatAm', locale: 'es-419', code: 'spa', name: 'Spanish', language: 'Latin American Spanish' }, + { cr_locale: 'es-419', ao_locale: 'es', hd_locale: 'Spanish', locale: 'es-419', code: 'spa-419', name: 'Spanish', language: 'Latin American Spanish' }, + { cr_locale: 'es-ES', new_hd_locale: 'es-ES', hd_locale: 'Spanish Europe', locale: 'es-ES', code: 'spa-ES', name: 'Castilian', language: 'European Spanish' }, + { cr_locale: 'pt-BR', ao_locale: 'pt', new_hd_locale: 'pt-BR', hd_locale: 'Portuguese', locale: 'pt-BR', code: 'por', name: 'Portuguese', language: 'Brazilian Portuguese' }, + { cr_locale: 'pt-PT', locale: 'pt-PT', code: 'por', name: 'Portuguese (Portugal)', language: 'Portugues (Portugal)' }, + { cr_locale: 'fr-FR', adn_locale: 'fr', hd_locale: 'French', locale: 'fr', code: 'fra', name: 'French' }, + { cr_locale: 'de-DE', adn_locale: 'de', hd_locale: 'German', locale: 'de', code: 'deu', name: 'German' }, + { cr_locale: 'ar-ME', locale: 'ar', code: 'ara-ME', name: 'Arabic' }, + { cr_locale: 'ar-SA', hd_locale: 'Arabic', locale: 'ar', code: 'ara', name: 'Arabic (Saudi Arabia)' }, + { cr_locale: 'it-IT', hd_locale: 'Italian', locale: 'it', code: 'ita', name: 'Italian' }, + { cr_locale: 'ru-RU', hd_locale: 'Russian', locale: 'ru', code: 'rus', name: 'Russian' }, + { cr_locale: 'tr-TR', hd_locale: 'Turkish', locale: 'tr', code: 'tur', name: 'Turkish' }, + { cr_locale: 'hi-IN', locale: 'hi', code: 'hin', name: 'Hindi' }, + { locale: 'zh', code: 'cmn', name: 'Chinese (Mandarin, PRC)' }, + { cr_locale: 'zh-CN', locale: 'zh-CN', code: 'zho', name: 'Chinese (Mainland China)' }, + { cr_locale: 'zh-TW', locale: 'zh-TW', code: 'chi', name: 'Chinese (Taiwan)' }, + { cr_locale: 'zh-HK', locale: 'zh-HK', code: 'zh-HK', name: 'Chinese (Hong-Kong)', language: '中文 (粵語)' }, + { cr_locale: 'ko-KR', hd_locale: 'Korean', locale: 'ko', code: 'kor', name: 'Korean' }, + { cr_locale: 'ca-ES', locale: 'ca-ES', code: 'cat', name: 'Catalan' }, + { cr_locale: 'pl-PL', locale: 'pl-PL', code: 'pol', name: 'Polish' }, + { cr_locale: 'th-TH', locale: 'th-TH', code: 'tha', name: 'Thai', language: 'ไทย' }, + { cr_locale: 'ta-IN', locale: 'ta-IN', code: 'tam', name: 'Tamil (India)', language: 'தமிழ்' }, + { cr_locale: 'ms-MY', locale: 'ms-MY', code: 'may', name: 'Malay (Malaysia)', language: 'Bahasa Melayu' }, + { cr_locale: 'vi-VN', locale: 'vi-VN', code: 'vie', name: 'Vietnamese', language: 'Tiếng Việt' }, + { cr_locale: 'id-ID', locale: 'id-ID', code: 'ind', name: 'Indonesian', language: 'Bahasa Indonesia' }, + { cr_locale: 'te-IN', locale: 'te-IN', code: 'tel', name: 'Telugu (India)', language: 'తెలుగు' }, + { cr_locale: 'ja-JP', adn_locale: 'ja', ao_locale: 'ja', hd_locale: 'Japanese', locale: 'ja', code: 'jpn', name: 'Japanese' } ]; // add en language names -(() =>{ - for(const languageIndex in languages){ - if(!languages[languageIndex].language){ - languages[languageIndex].language = languages[languageIndex].name; - } - } +(() => { + for (const languageIndex in languages) { + if (!languages[languageIndex].language) { + languages[languageIndex].language = languages[languageIndex].name; + } + } })(); // construct dub language codes const dubLanguageCodes = (() => { - const dubLanguageCodesArray: string[] = []; - for(const language of languages){ - dubLanguageCodesArray.push(language.code); - } - return [...new Set(dubLanguageCodesArray)]; + const dubLanguageCodesArray: string[] = []; + for (const language of languages) { + dubLanguageCodesArray.push(language.code); + } + return [...new Set(dubLanguageCodesArray)]; })(); // construct subtitle languages filter const subtitleLanguagesFilter = (() => { - const subtitleLanguagesExtraParameters = ['all', 'none']; - return [...subtitleLanguagesExtraParameters, ...new Set(languages.map(l => { return l.locale; }))]; + const subtitleLanguagesExtraParameters = ['all', 'none']; + return [ + ...subtitleLanguagesExtraParameters, + ...new Set( + languages.map((l) => { + return l.locale; + }) + ) + ]; })(); const searchLocales = (() => { - return ['', ...new Set(languages.map(l => { return l.cr_locale; }).slice(0, -1)), ...new Set(languages.map(l => { return l.adn_locale; }).slice(0, -1))]; + return [ + '', + ...new Set( + languages + .map((l) => { + return l.cr_locale; + }) + .slice(0, -1) + ), + ...new Set( + languages + .map((l) => { + return l.adn_locale; + }) + .slice(0, -1) + ) + ]; })(); export const aoSearchLocales = (() => { - return ['', ...new Set(languages.map(l => { return l.ao_locale; }).slice(0, -1))]; + return [ + '', + ...new Set( + languages + .map((l) => { + return l.ao_locale; + }) + .slice(0, -1) + ) + ]; })(); // convert const fixLanguageTag = (tag: string) => { - tag = typeof tag == 'string' ? tag : 'und'; - const tagLangLC = tag.match(/^(\w{2})-?(\w{2})$/); - if(tagLangLC){ - const tagLang = `${tagLangLC[1]}-${tagLangLC[2].toUpperCase()}`; - if(findLang(tagLang).cr_locale != 'und'){ - return findLang(tagLang).cr_locale; - } - else{ - return tagLang; - } - } - else{ - return tag; - } + tag = typeof tag == 'string' ? tag : 'und'; + const tagLangLC = tag.match(/^(\w{2})-?(\w{2})$/); + if (tagLangLC) { + const tagLang = `${tagLangLC[1]}-${tagLangLC[2].toUpperCase()}`; + if (findLang(tagLang).cr_locale != 'und') { + return findLang(tagLang).cr_locale; + } else { + return tagLang; + } + } else { + return tag; + } }; // find lang by cr_locale const findLang = (cr_locale: string) => { - const lang = languages.find(l => { return l.cr_locale == cr_locale; }); - return lang ? lang : languages.find(l => l.code === 'und') || { cr_locale: 'und', locale: 'un', code: 'und', name: 'Undetermined', language: 'Undetermined' }; + const lang = languages.find((l) => { + return l.cr_locale == cr_locale; + }); + return lang ? lang : languages.find((l) => l.code === 'und') || { cr_locale: 'und', locale: 'un', code: 'und', name: 'Undetermined', language: 'Undetermined' }; }; const fixAndFindCrLC = (cr_locale: string) => { - const str = fixLanguageTag(cr_locale); - return findLang(str || ''); + const str = fixLanguageTag(cr_locale); + return findLang(str || ''); }; // rss subs lang parser const parseRssSubtitlesString = (subs: string) => { - const splitMap = subs.replace(/\s/g, '').split(',').map((s) => { - return fixAndFindCrLC(s).locale; - }); - const sort = sortTags(splitMap); - return sort.join(', '); + const splitMap = subs + .replace(/\s/g, '') + .split(',') + .map((s) => { + return fixAndFindCrLC(s).locale; + }); + const sort = sortTags(splitMap); + return sort.join(', '); }; - // parse subtitles Array const parseSubtitlesArray = (tags: string[]) => { - const sort = sortSubtitles(tags.map((t) => { - return { locale: fixAndFindCrLC(t).locale }; - })); - return sort.map((t) => { return t.locale; }).join(', '); + const sort = sortSubtitles( + tags.map((t) => { + return { locale: fixAndFindCrLC(t).locale }; + }) + ); + return sort + .map((t) => { + return t.locale; + }) + .join(', '); }; // sort subtitles -const sortSubtitles = <T extends { - [key: string]: unknown -} = Record<string, string>> (data: T[], sortkey?: keyof T) : T[] => { - const idx: Record<string, number> = {}; - const key = sortkey || 'locale' as keyof T; - const tags = [...new Set(Object.values(languages).map(e => e.locale))]; - for(const l of tags){ - idx[l] = Object.keys(idx).length + 1; - } - data.sort((a, b) => { - const ia = idx[a[key] as string] ? idx[a[key] as string] : 50; - const ib = idx[b[key] as string] ? idx[b[key] as string] : 50; - return ia - ib; - }); - return data; +const sortSubtitles = < + T extends { + [key: string]: unknown; + } = Record<string, string> +>( + data: T[], + sortkey?: keyof T +): T[] => { + const idx: Record<string, number> = {}; + const key = sortkey || ('locale' as keyof T); + const tags = [...new Set(Object.values(languages).map((e) => e.locale))]; + for (const l of tags) { + idx[l] = Object.keys(idx).length + 1; + } + data.sort((a, b) => { + const ia = idx[a[key] as string] ? idx[a[key] as string] : 50; + const ib = idx[b[key] as string] ? idx[b[key] as string] : 50; + return ia - ib; + }); + return data; }; const sortTags = (data: string[]) => { - const retData = data.map(e => { return { locale: e }; }); - const sort = sortSubtitles(retData); - return sort.map(e => e.locale as string); + const retData = data.map((e) => { + return { locale: e }; + }); + const sort = sortSubtitles(retData); + return sort.map((e) => e.locale as string); }; -const subsFile = (fnOutput:string, subsIndex: string, langItem: LanguageItem, isCC: boolean, ccTag: string, isSigns?: boolean, format?: string) => { - subsIndex = (parseInt(subsIndex) + 1).toString().padStart(2, '0'); - return `${fnOutput}.${subsIndex}.${langItem.code}.${langItem.language}${isCC ? `.${ccTag}` : ''}${isSigns ? '.signs' : ''}.${format ? format : 'ass'}`; +const subsFile = (fnOutput: string, subsIndex: string, langItem: LanguageItem, isCC: boolean, ccTag: string, isSigns?: boolean, format?: string) => { + subsIndex = (parseInt(subsIndex) + 1).toString().padStart(2, '0'); + return `${fnOutput}.${subsIndex}.${langItem.code}.${langItem.language}${isCC ? `.${ccTag}` : ''}${isSigns ? '.signs' : ''}.${format ? format : 'ass'}`; }; // construct dub langs const const dubLanguages = (() => { - const dubDb: Record<string, string> = {}; - for(const lang of languages){ - if(!Object.keys(dubDb).includes(lang.name)){ - dubDb[lang.name] = lang.code; - } - } - return dubDb; + const dubDb: Record<string, string> = {}; + for (const lang of languages) { + if (!Object.keys(dubDb).includes(lang.name)) { + dubDb[lang.name] = lang.code; + } + } + return dubDb; })(); // dub regex -const dubRegExpStr = - `\\((${Object.keys(dubLanguages).join('|')})(?: (Dub|VO))?\\)$`; +const dubRegExpStr = `\\((${Object.keys(dubLanguages).join('|')})(?: (Dub|VO))?\\)$`; const dubRegExp = new RegExp(dubRegExpStr); // code to lang name const langCode2name = (code: string) => { - const codeIdx = dubLanguageCodes.indexOf(code); - return Object.keys(dubLanguages)[codeIdx]; + const codeIdx = dubLanguageCodes.indexOf(code); + return Object.keys(dubLanguages)[codeIdx]; }; // locale to lang name const locale2language = (locale: string) => { - const filteredLocale = languages.filter(l => { - return l.locale == locale; - }); - return filteredLocale[0]; + const filteredLocale = languages.filter((l) => { + return l.locale == locale; + }); + return filteredLocale[0]; }; // output export { - languages, - dubLanguageCodes, - dubLanguages, - langCode2name, - locale2language, - dubRegExp, - subtitleLanguagesFilter, - searchLocales, - fixLanguageTag, - findLang, - fixAndFindCrLC, - parseRssSubtitlesString, - parseSubtitlesArray, - sortSubtitles, - sortTags, - subsFile, + languages, + dubLanguageCodes, + dubLanguages, + langCode2name, + locale2language, + dubRegExp, + subtitleLanguagesFilter, + searchLocales, + fixLanguageTag, + findLang, + fixAndFindCrLC, + parseRssSubtitlesString, + parseSubtitlesArray, + sortSubtitles, + sortTags, + subsFile }; diff --git a/modules/module.merger.ts b/modules/module.merger.ts index b8f585b..620a32e 100644 --- a/modules/module.merger.ts +++ b/modules/module.merger.ts @@ -11,420 +11,411 @@ import Helper from './module.helper'; import { convertChaptersToFFmpegFormat } from './module.ffmpegChapter'; export type MergerInput = { - path: string, - lang: LanguageItem, - duration?: number, - delay?: number, - isPrimary?: boolean, -} + path: string; + lang: LanguageItem; + duration?: number; + delay?: number; + isPrimary?: boolean; +}; export type SubtitleInput = { - language: LanguageItem, - file: string, - closedCaption?: boolean, - signs?: boolean, - delay?: number -} + language: LanguageItem; + file: string; + closedCaption?: boolean; + signs?: boolean; + delay?: number; +}; export type Font = keyof typeof fontFamilies; export type ParsedFont = { - name: string, - path: string, - mime: string, -} + name: string; + path: string; + mime: string; +}; export type MergerOptions = { - videoAndAudio: MergerInput[], - onlyVid: MergerInput[], - onlyAudio: MergerInput[], - subtitles: SubtitleInput[], - chapters?: MergerInput[], - ccTag: string, - output: string, - videoTitle?: string, - simul?: boolean, - inverseTrackOrder?: boolean, - keepAllVideos?: boolean, - fonts?: ParsedFont[], - skipSubMux?: boolean, - options: { - ffmpeg: string[], - mkvmerge: string[] - }, - defaults: { - audio: LanguageItem, - sub: LanguageItem - } -} + videoAndAudio: MergerInput[]; + onlyVid: MergerInput[]; + onlyAudio: MergerInput[]; + subtitles: SubtitleInput[]; + chapters?: MergerInput[]; + ccTag: string; + output: string; + videoTitle?: string; + simul?: boolean; + inverseTrackOrder?: boolean; + keepAllVideos?: boolean; + fonts?: ParsedFont[]; + skipSubMux?: boolean; + options: { + ffmpeg: string[]; + mkvmerge: string[]; + }; + defaults: { + audio: LanguageItem; + sub: LanguageItem; + }; +}; class Merger { - - constructor(private options: MergerOptions) { - if (this.options.skipSubMux) - this.options.subtitles = []; - if (this.options.videoTitle) - this.options.videoTitle = this.options.videoTitle.replace(/"/g, '\''); - } + constructor(private options: MergerOptions) { + if (this.options.skipSubMux) this.options.subtitles = []; + if (this.options.videoTitle) this.options.videoTitle = this.options.videoTitle.replace(/"/g, "'"); + } - public async createDelays() { - //Don't bother scanning it if there is only 1 vna stream - if (this.options.videoAndAudio.length > 1) { - const bin = await yamlCfg.loadBinCfg(); - const vnas = this.options.videoAndAudio; - //get and set durations on each videoAndAudio Stream - for (const [vnaIndex, vna] of vnas.entries()) { - const streamInfo = await ffprobe(vna.path, { path: bin.ffprobe as string }); - const videoInfo = streamInfo.streams.filter(stream => stream.codec_type == 'video'); - vnas[vnaIndex].duration = parseInt(videoInfo[0].duration as string); - } - //Sort videoAndAudio streams by duration (shortest first) - vnas.sort((a,b) => { - if (!a.duration || !b.duration) return -1; - return a.duration - b.duration; - }); - //Set Delays - const shortestDuration = vnas[0].duration; - for (const [vnaIndex, vna] of vnas.entries()) { - //Don't calculate the shortestDuration track - if (vnaIndex == 0) { - if (!vna.isPrimary && vna.isPrimary !== undefined) - console.warn('Shortest video isn\'t primary, this might lead to problems with subtitles. Please report on github or discord if you experience issues.'); - continue; - } - if (vna.duration && shortestDuration) { - //Calculate the tracks delay - vna.delay = Math.ceil((vna.duration-shortestDuration) * 1000) / 1000; - //TODO: set primary language for audio so it can be used to determine which track needs the delay - //The above is a problem in the event that it isn't the dub that needs the delay, but rather the sub. - //Alternatively: Might not work: it could be checked if there are multiple of the same video language, and if there is - //more than 1 of the same video language, then do the subtitle delay on CC, else normal language. - const subtitles = this.options.subtitles.filter(sub => sub.language.code == vna.lang.code); - for (const [subIndex, sub] of subtitles.entries()) { - if (vna.isPrimary) subtitles[subIndex].delay = vna.delay; - else if (sub.closedCaption) subtitles[subIndex].delay = vna.delay; - } - } - } - } - } + public async createDelays() { + //Don't bother scanning it if there is only 1 vna stream + if (this.options.videoAndAudio.length > 1) { + const bin = await yamlCfg.loadBinCfg(); + const vnas = this.options.videoAndAudio; + //get and set durations on each videoAndAudio Stream + for (const [vnaIndex, vna] of vnas.entries()) { + const streamInfo = await ffprobe(vna.path, { path: bin.ffprobe as string }); + const videoInfo = streamInfo.streams.filter((stream) => stream.codec_type == 'video'); + vnas[vnaIndex].duration = parseInt(videoInfo[0].duration as string); + } + //Sort videoAndAudio streams by duration (shortest first) + vnas.sort((a, b) => { + if (!a.duration || !b.duration) return -1; + return a.duration - b.duration; + }); + //Set Delays + const shortestDuration = vnas[0].duration; + for (const [vnaIndex, vna] of vnas.entries()) { + //Don't calculate the shortestDuration track + if (vnaIndex == 0) { + if (!vna.isPrimary && vna.isPrimary !== undefined) + console.warn("Shortest video isn't primary, this might lead to problems with subtitles. Please report on github or discord if you experience issues."); + continue; + } + if (vna.duration && shortestDuration) { + //Calculate the tracks delay + vna.delay = Math.ceil((vna.duration - shortestDuration) * 1000) / 1000; + //TODO: set primary language for audio so it can be used to determine which track needs the delay + //The above is a problem in the event that it isn't the dub that needs the delay, but rather the sub. + //Alternatively: Might not work: it could be checked if there are multiple of the same video language, and if there is + //more than 1 of the same video language, then do the subtitle delay on CC, else normal language. + const subtitles = this.options.subtitles.filter((sub) => sub.language.code == vna.lang.code); + for (const [subIndex, sub] of subtitles.entries()) { + if (vna.isPrimary) subtitles[subIndex].delay = vna.delay; + else if (sub.closedCaption) subtitles[subIndex].delay = vna.delay; + } + } + } + } + } - public FFmpeg() : string { - const args: string[] = []; - const metaData: string[] = []; + public FFmpeg(): string { + const args: string[] = []; + const metaData: string[] = []; - let index = 0; - let audioIndex = 0; - let hasVideo = false; + let index = 0; + let audioIndex = 0; + let hasVideo = false; - for (const vid of this.options.videoAndAudio) { - if (vid.delay && hasVideo) { - args.push( - `-itsoffset -${Math.ceil(vid.delay*1000)}ms` - ); - } - args.push(`-i "${vid.path}"`); - if (!hasVideo || this.options.keepAllVideos) { - metaData.push(`-map ${index}:a -map ${index}:v`); - metaData.push(`-metadata:s:a:${audioIndex} language=${vid.lang.code}`); - metaData.push(`-metadata:s:v:${index} title="${this.options.videoTitle}"`); - hasVideo = true; - } else { - metaData.push(`-map ${index}:a`); - metaData.push(`-metadata:s:a:${audioIndex} language=${vid.lang.code}`); - } - audioIndex++; - index++; - } + for (const vid of this.options.videoAndAudio) { + if (vid.delay && hasVideo) { + args.push(`-itsoffset -${Math.ceil(vid.delay * 1000)}ms`); + } + args.push(`-i "${vid.path}"`); + if (!hasVideo || this.options.keepAllVideos) { + metaData.push(`-map ${index}:a -map ${index}:v`); + metaData.push(`-metadata:s:a:${audioIndex} language=${vid.lang.code}`); + metaData.push(`-metadata:s:v:${index} title="${this.options.videoTitle}"`); + hasVideo = true; + } else { + metaData.push(`-map ${index}:a`); + metaData.push(`-metadata:s:a:${audioIndex} language=${vid.lang.code}`); + } + audioIndex++; + index++; + } - for (const vid of this.options.onlyVid) { - if (!hasVideo || this.options.keepAllVideos) { - args.push(`-i "${vid.path}"`); - metaData.push(`-map ${index} -map -${index}:a`); - metaData.push(`-metadata:s:v:${index} title="${this.options.videoTitle}"`); - hasVideo = true; - index++; - } - } + for (const vid of this.options.onlyVid) { + if (!hasVideo || this.options.keepAllVideos) { + args.push(`-i "${vid.path}"`); + metaData.push(`-map ${index} -map -${index}:a`); + metaData.push(`-metadata:s:v:${index} title="${this.options.videoTitle}"`); + hasVideo = true; + index++; + } + } - for (const aud of this.options.onlyAudio) { - args.push(`-i "${aud.path}"`); - metaData.push(`-map ${index}`); - metaData.push(`-metadata:s:a:${audioIndex} language=${aud.lang.code}`); - index++; - audioIndex++; - } + for (const aud of this.options.onlyAudio) { + args.push(`-i "${aud.path}"`); + metaData.push(`-map ${index}`); + metaData.push(`-metadata:s:a:${audioIndex} language=${aud.lang.code}`); + index++; + audioIndex++; + } - for (const index in this.options.subtitles) { - const sub = this.options.subtitles[index]; - if (sub.delay) { - args.push( - `-itsoffset -${Math.ceil(sub.delay*1000)}ms` - ); - } - args.push(`-i "${sub.file}"`); - } + for (const index in this.options.subtitles) { + const sub = this.options.subtitles[index]; + if (sub.delay) { + args.push(`-itsoffset -${Math.ceil(sub.delay * 1000)}ms`); + } + args.push(`-i "${sub.file}"`); + } - if (this.options.chapters && this.options.chapters.length > 0) { - const chapterFilePath = this.options.chapters[0].path; - const chapterData = convertChaptersToFFmpegFormat(this.options.chapters[0].path); - fs.writeFileSync(chapterFilePath, chapterData, 'utf-8'); - args.push(`-i "${chapterFilePath}" -map_metadata 1`); - } + if (this.options.chapters && this.options.chapters.length > 0) { + const chapterFilePath = this.options.chapters[0].path; + const chapterData = convertChaptersToFFmpegFormat(this.options.chapters[0].path); + fs.writeFileSync(chapterFilePath, chapterData, 'utf-8'); + args.push(`-i "${chapterFilePath}" -map_metadata 1`); + } - if (this.options.output.split('.').pop() === 'mkv') { - if (this.options.fonts) { - let fontIndex = 0; - for (const font of this.options.fonts) { - args.push(`-attach ${font.path} -metadata:s:t:${fontIndex} mimetype=${font.mime} -metadata:s:t:${fontIndex} filename=${font.name}`); - fontIndex++; - } - } - } + if (this.options.output.split('.').pop() === 'mkv') { + if (this.options.fonts) { + let fontIndex = 0; + for (const font of this.options.fonts) { + args.push(`-attach ${font.path} -metadata:s:t:${fontIndex} mimetype=${font.mime} -metadata:s:t:${fontIndex} filename=${font.name}`); + fontIndex++; + } + } + } - args.push(...metaData); - args.push(...this.options.subtitles.map((_, subIndex) => `-map ${subIndex + index}`)); - args.push( - '-c:v copy', - '-c:a copy', - this.options.output.split('.').pop()?.toLowerCase() === 'mp4' ? '-c:s mov_text' : '-c:s ass', - ...this.options.subtitles.map((sub, subindex) => `-metadata:s:s:${subindex} title="${ - (sub.language.language || sub.language.name) + `${sub.closedCaption === true ? ` ${this.options.ccTag}` : ''}` + `${sub.signs === true ? ' Signs' : ''}` - }" -metadata:s:s:${subindex} language=${sub.language.code}`) - ); - args.push(...this.options.options.ffmpeg); - args.push(`"${this.options.output}"`); - return args.join(' '); - } + args.push(...metaData); + args.push(...this.options.subtitles.map((_, subIndex) => `-map ${subIndex + index}`)); + args.push( + '-c:v copy', + '-c:a copy', + this.options.output.split('.').pop()?.toLowerCase() === 'mp4' ? '-c:s mov_text' : '-c:s ass', + ...this.options.subtitles.map( + (sub, subindex) => + `-metadata:s:s:${subindex} title="${ + (sub.language.language || sub.language.name) + `${sub.closedCaption === true ? ` ${this.options.ccTag}` : ''}` + `${sub.signs === true ? ' Signs' : ''}` + }" -metadata:s:s:${subindex} language=${sub.language.code}` + ) + ); + args.push(...this.options.options.ffmpeg); + args.push(`"${this.options.output}"`); + return args.join(' '); + } - public static getLanguageCode = (from: string, _default = 'eng'): string => { - if (from === 'cmn') return 'chi'; - for (const lang in iso639.iso_639_2) { - const langObj = iso639.iso_639_2[lang]; - if (Object.prototype.hasOwnProperty.call(langObj, '639-1') && langObj['639-1'] === from) { - return langObj['639-2'] as string; - } - } - return _default; - }; + public static getLanguageCode = (from: string, _default = 'eng'): string => { + if (from === 'cmn') return 'chi'; + for (const lang in iso639.iso_639_2) { + const langObj = iso639.iso_639_2[lang]; + if (Object.prototype.hasOwnProperty.call(langObj, '639-1') && langObj['639-1'] === from) { + return langObj['639-2'] as string; + } + } + return _default; + }; - public MkvMerge = () => { - const args: string[] = []; + public MkvMerge = () => { + const args: string[] = []; - let hasVideo = false; + let hasVideo = false; - args.push(`-o "${this.options.output}"`); - args.push(...this.options.options.mkvmerge); + args.push(`-o "${this.options.output}"`); + args.push(...this.options.options.mkvmerge); - for (const vid of this.options.onlyVid) { - if (!hasVideo || this.options.keepAllVideos) { - args.push( - '--video-tracks 0', - '--no-audio' - ); - const trackName = ((this.options.videoTitle ?? vid.lang.name) + (this.options.simul ? ' [Simulcast]' : ' [Uncut]')); - args.push('--track-name', `0:"${trackName}"`); - args.push(`--language 0:${vid.lang.code}`); - hasVideo = true; - args.push(`"${vid.path}"`); - } - } + for (const vid of this.options.onlyVid) { + if (!hasVideo || this.options.keepAllVideos) { + args.push('--video-tracks 0', '--no-audio'); + const trackName = (this.options.videoTitle ?? vid.lang.name) + (this.options.simul ? ' [Simulcast]' : ' [Uncut]'); + args.push('--track-name', `0:"${trackName}"`); + args.push(`--language 0:${vid.lang.code}`); + hasVideo = true; + args.push(`"${vid.path}"`); + } + } - for (const vid of this.options.videoAndAudio) { - const audioTrackNum = this.options.inverseTrackOrder ? '0' : '1'; - const videoTrackNum = this.options.inverseTrackOrder ? '1' : '0'; - if (vid.delay) { - args.push( - `--sync ${audioTrackNum}:-${Math.ceil(vid.delay*1000)}` - ); - } - if (!hasVideo || this.options.keepAllVideos) { - args.push( - `--video-tracks ${videoTrackNum}`, - `--audio-tracks ${audioTrackNum}` - ); - const trackName = ((this.options.videoTitle ?? vid.lang.name) + (this.options.simul ? ' [Simulcast]' : ' [Uncut]')); - args.push('--track-name', `0:"${trackName}"`); - //args.push('--track-name', `1:"${trackName}"`); - args.push(`--language ${audioTrackNum}:${vid.lang.code}`); - if (this.options.defaults.audio.code === vid.lang.code) { - args.push(`--default-track ${audioTrackNum}`); - } else { - args.push(`--default-track ${audioTrackNum}:0`); - } - hasVideo = true; - } else { - args.push( - '--no-video', - `--audio-tracks ${audioTrackNum}` - ); - if (this.options.defaults.audio.code === vid.lang.code) { - args.push(`--default-track ${audioTrackNum}`); - } else { - args.push(`--default-track ${audioTrackNum}:0`); - } - args.push('--track-name', `${audioTrackNum}:"${vid.lang.name}"`); - args.push(`--language ${audioTrackNum}:${vid.lang.code}`); - } - args.push(`"${vid.path}"`); - } + for (const vid of this.options.videoAndAudio) { + const audioTrackNum = this.options.inverseTrackOrder ? '0' : '1'; + const videoTrackNum = this.options.inverseTrackOrder ? '1' : '0'; + if (vid.delay) { + args.push(`--sync ${audioTrackNum}:-${Math.ceil(vid.delay * 1000)}`); + } + if (!hasVideo || this.options.keepAllVideos) { + args.push(`--video-tracks ${videoTrackNum}`, `--audio-tracks ${audioTrackNum}`); + const trackName = (this.options.videoTitle ?? vid.lang.name) + (this.options.simul ? ' [Simulcast]' : ' [Uncut]'); + args.push('--track-name', `0:"${trackName}"`); + //args.push('--track-name', `1:"${trackName}"`); + args.push(`--language ${audioTrackNum}:${vid.lang.code}`); + if (this.options.defaults.audio.code === vid.lang.code) { + args.push(`--default-track ${audioTrackNum}`); + } else { + args.push(`--default-track ${audioTrackNum}:0`); + } + hasVideo = true; + } else { + args.push('--no-video', `--audio-tracks ${audioTrackNum}`); + if (this.options.defaults.audio.code === vid.lang.code) { + args.push(`--default-track ${audioTrackNum}`); + } else { + args.push(`--default-track ${audioTrackNum}:0`); + } + args.push('--track-name', `${audioTrackNum}:"${vid.lang.name}"`); + args.push(`--language ${audioTrackNum}:${vid.lang.code}`); + } + args.push(`"${vid.path}"`); + } - for (const aud of this.options.onlyAudio) { - const trackName = aud.lang.name; - args.push('--track-name', `0:"${trackName}"`); - args.push(`--language 0:${aud.lang.code}`); - args.push( - '--no-video', - '--audio-tracks 0' - ); - if (this.options.defaults.audio.code === aud.lang.code) { - args.push('--default-track 0'); - } else { - args.push('--default-track 0:0'); - } - args.push(`"${aud.path}"`); - } + for (const aud of this.options.onlyAudio) { + const trackName = aud.lang.name; + args.push('--track-name', `0:"${trackName}"`); + args.push(`--language 0:${aud.lang.code}`); + args.push('--no-video', '--audio-tracks 0'); + if (this.options.defaults.audio.code === aud.lang.code) { + args.push('--default-track 0'); + } else { + args.push('--default-track 0:0'); + } + args.push(`"${aud.path}"`); + } - if (this.options.subtitles.length > 0) { - for (const subObj of this.options.subtitles) { - if (subObj.delay) { - args.push( - `--sync 0:-${Math.ceil(subObj.delay*1000)}` - ); - } - args.push('--track-name', `0:"${(subObj.language.language || subObj.language.name) + `${subObj.closedCaption === true ? ` ${this.options.ccTag}` : ''}` + `${subObj.signs === true ? ' Signs' : ''}`}"`); - args.push('--language', `0:"${subObj.language.code}"`); - //TODO: look into making Closed Caption default if it's the only sub of the default language downloaded - if (this.options.defaults.sub.code === subObj.language.code && !subObj.closedCaption) { - args.push('--default-track 0'); - } else { - args.push('--default-track 0:0'); - } - args.push(`"${subObj.file}"`); - } - } else { - args.push( - '--no-subtitles', - ); - } + if (this.options.subtitles.length > 0) { + for (const subObj of this.options.subtitles) { + if (subObj.delay) { + args.push(`--sync 0:-${Math.ceil(subObj.delay * 1000)}`); + } + args.push( + '--track-name', + `0:"${(subObj.language.language || subObj.language.name) + `${subObj.closedCaption === true ? ` ${this.options.ccTag}` : ''}` + `${subObj.signs === true ? ' Signs' : ''}`}"` + ); + args.push('--language', `0:"${subObj.language.code}"`); + //TODO: look into making Closed Caption default if it's the only sub of the default language downloaded + if (this.options.defaults.sub.code === subObj.language.code && !subObj.closedCaption) { + args.push('--default-track 0'); + } else { + args.push('--default-track 0:0'); + } + args.push(`"${subObj.file}"`); + } + } else { + args.push('--no-subtitles'); + } - if (this.options.fonts && this.options.fonts.length > 0) { - for (const f of this.options.fonts) { - args.push('--attachment-name', f.name); - args.push('--attachment-mime-type', f.mime); - args.push('--attach-file', `"${f.path}"`); - } - } else { - args.push( - '--no-attachments' - ); - } + if (this.options.fonts && this.options.fonts.length > 0) { + for (const f of this.options.fonts) { + args.push('--attachment-name', f.name); + args.push('--attachment-mime-type', f.mime); + args.push('--attach-file', `"${f.path}"`); + } + } else { + args.push('--no-attachments'); + } - if (this.options.chapters && this.options.chapters.length > 0) { - args.push(`--chapters "${this.options.chapters[0].path}"`); - } + if (this.options.chapters && this.options.chapters.length > 0) { + args.push(`--chapters "${this.options.chapters[0].path}"`); + } - return args.join(' '); - }; + return args.join(' '); + }; - public static checkMerger(bin: { - mkvmerge?: string, - ffmpeg?: string, - }, useMP4format: boolean, force: AvailableMuxer|undefined) : { - MKVmerge?: string, - FFmpeg?: string - } { - if (force && bin[force]) { - return { - FFmpeg: force === 'ffmpeg' ? bin.ffmpeg : undefined, - MKVmerge: force === 'mkvmerge' ? bin.mkvmerge : undefined - }; - } - if (useMP4format && bin.ffmpeg) { - return { - FFmpeg: bin.ffmpeg - }; - } else if (!useMP4format && (bin.mkvmerge || bin.ffmpeg)) { - return { - MKVmerge: bin.mkvmerge, - FFmpeg: bin.ffmpeg - }; - } else if (useMP4format) { - console.warn('FFmpeg not found, skip muxing...'); - } else if (!bin.mkvmerge) { - console.warn('MKVMerge not found, skip muxing...'); - } - return {}; - } + public static checkMerger( + bin: { + mkvmerge?: string; + ffmpeg?: string; + }, + useMP4format: boolean, + force: AvailableMuxer | undefined + ): { + MKVmerge?: string; + FFmpeg?: string; + } { + if (force && bin[force]) { + return { + FFmpeg: force === 'ffmpeg' ? bin.ffmpeg : undefined, + MKVmerge: force === 'mkvmerge' ? bin.mkvmerge : undefined + }; + } + if (useMP4format && bin.ffmpeg) { + return { + FFmpeg: bin.ffmpeg + }; + } else if (!useMP4format && (bin.mkvmerge || bin.ffmpeg)) { + return { + MKVmerge: bin.mkvmerge, + FFmpeg: bin.ffmpeg + }; + } else if (useMP4format) { + console.warn('FFmpeg not found, skip muxing...'); + } else if (!bin.mkvmerge) { + console.warn('MKVMerge not found, skip muxing...'); + } + return {}; + } - public static makeFontsList (fontsDir: string, subs: { - language: LanguageItem, - fonts: Font[] - }[]) : ParsedFont[] { - let fontsNameList: Font[] = []; const fontsList: { name: string, path: string, mime: string }[] = [], subsList: string[] = []; let isNstr = true; - for(const s of subs){ - fontsNameList.push(...s.fonts); - subsList.push(s.language.locale); - } - fontsNameList = [...new Set(fontsNameList)]; - if(subsList.length > 0){ - console.info('\nSubtitles: %s (Total: %s)', subsList.join(', '), subsList.length); - isNstr = false; - } - if(fontsNameList.length > 0){ - console.info((isNstr ? '\n' : '') + 'Required fonts: %s (Total: %s)', fontsNameList.join(', '), fontsNameList.length); - } - for(const f of fontsNameList){ - const fontFiles = fontFamilies[f]; - if(fontFiles){ - for (const fontFile of fontFiles) { - const fontPath = path.join(fontsDir, fontFile); - const mime = fontMime(fontFile); - if(fs.existsSync(fontPath) && fs.statSync(fontPath).size != 0){ - fontsList.push({ - name: fontFile, - path: fontPath, - mime: mime, - }); - } - } - } - } - return fontsList; - } + public static makeFontsList( + fontsDir: string, + subs: { + language: LanguageItem; + fonts: Font[]; + }[] + ): ParsedFont[] { + let fontsNameList: Font[] = []; + const fontsList: { name: string; path: string; mime: string }[] = [], + subsList: string[] = []; + let isNstr = true; + for (const s of subs) { + fontsNameList.push(...s.fonts); + subsList.push(s.language.locale); + } + fontsNameList = [...new Set(fontsNameList)]; + if (subsList.length > 0) { + console.info('\nSubtitles: %s (Total: %s)', subsList.join(', '), subsList.length); + isNstr = false; + } + if (fontsNameList.length > 0) { + console.info((isNstr ? '\n' : '') + 'Required fonts: %s (Total: %s)', fontsNameList.join(', '), fontsNameList.length); + } + for (const f of fontsNameList) { + const fontFiles = fontFamilies[f]; + if (fontFiles) { + for (const fontFile of fontFiles) { + const fontPath = path.join(fontsDir, fontFile); + const mime = fontMime(fontFile); + if (fs.existsSync(fontPath) && fs.statSync(fontPath).size != 0) { + fontsList.push({ + name: fontFile, + path: fontPath, + mime: mime + }); + } + } + } + } + return fontsList; + } - public async merge(type: 'ffmpeg'|'mkvmerge', bin: string) { - let command: string|undefined = undefined; - switch (type) { - case 'ffmpeg': - command = this.FFmpeg(); - break; - case 'mkvmerge': - command = this.MkvMerge(); - break; - } - if (command === undefined) { - console.warn('Unable to merge files.'); - return; - } - console.info(`[${type}] Started merging`); - const res = Helper.exec(type, `"${bin}"`, command); - if (!res.isOk && type === 'mkvmerge' && res.err.code === 1) { - console.info(`[${type}] Mkvmerge finished with at least one warning`); - } else if (!res.isOk) { - console.error(res.err); - console.error(`[${type}] Merging failed with exit code ${res.err.code}`); - } else { - console.info(`[${type} Done]`); - } - } - - public cleanUp() { - this.options.onlyAudio.concat(this.options.onlyVid).concat(this.options.videoAndAudio).forEach(a => fs.unlinkSync(a.path)); - this.options.chapters?.forEach(a => fs.unlinkSync(a.path)); - this.options.subtitles.forEach(a => fs.unlinkSync(a.file)); - } + public async merge(type: 'ffmpeg' | 'mkvmerge', bin: string) { + let command: string | undefined = undefined; + switch (type) { + case 'ffmpeg': + command = this.FFmpeg(); + break; + case 'mkvmerge': + command = this.MkvMerge(); + break; + } + if (command === undefined) { + console.warn('Unable to merge files.'); + return; + } + console.info(`[${type}] Started merging`); + const res = Helper.exec(type, `"${bin}"`, command); + if (!res.isOk && type === 'mkvmerge' && res.err.code === 1) { + console.info(`[${type}] Mkvmerge finished with at least one warning`); + } else if (!res.isOk) { + console.error(res.err); + console.error(`[${type}] Merging failed with exit code ${res.err.code}`); + } else { + console.info(`[${type} Done]`); + } + } + public cleanUp() { + this.options.onlyAudio + .concat(this.options.onlyVid) + .concat(this.options.videoAndAudio) + .forEach((a) => fs.unlinkSync(a.path)); + this.options.chapters?.forEach((a) => fs.unlinkSync(a.path)); + this.options.subtitles.forEach((a) => fs.unlinkSync(a.file)); + } } -export default Merger; \ No newline at end of file +export default Merger; diff --git a/modules/module.parseSelect.ts b/modules/module.parseSelect.ts index d438294..14e11d6 100644 --- a/modules/module.parseSelect.ts +++ b/modules/module.parseSelect.ts @@ -1,110 +1,110 @@ import { console } from './log'; -const parseSelect = (selectString: string, but = false) : { - isSelected: (val: string|string[]) => boolean, - values: string[] +const parseSelect = ( + selectString: string, + but = false +): { + isSelected: (val: string | string[]) => boolean; + values: string[]; } => { - if (!selectString) - return { - values: [], - isSelected: () => but - }; - const parts = selectString.split(','); - const select: string[] = []; + if (!selectString) + return { + values: [], + isSelected: () => but + }; + const parts = selectString.split(','); + const select: string[] = []; - parts.forEach(part => { - if (part.includes('-')) { - const splits = part.split('-'); - if (splits.length !== 2) { - console.warn(`[WARN] Unable to parse input "${part}"`); - return; - } + parts.forEach((part) => { + if (part.includes('-')) { + const splits = part.split('-'); + if (splits.length !== 2) { + console.warn(`[WARN] Unable to parse input "${part}"`); + return; + } - const firstPart = splits[0]; - const match = firstPart.match(/[A-Za-z]+/); - if (match && match.length > 0) { - if (match.index && match.index !== 0) { - console.warn(`[WARN] Unable to parse input "${part}"`); - return; - } - const letters = firstPart.substring(0, match[0].length); - const number = parseFloat(firstPart.substring(match[0].length)); - const b = parseFloat(splits[1]); - if (isNaN(number) || isNaN(b)) { - console.warn(`[WARN] Unable to parse input "${part}"`); - return; - } - for (let i = number; i <= b; i++) { - select.push(`${letters}${i}`); - } + const firstPart = splits[0]; + const match = firstPart.match(/[A-Za-z]+/); + if (match && match.length > 0) { + if (match.index && match.index !== 0) { + console.warn(`[WARN] Unable to parse input "${part}"`); + return; + } + const letters = firstPart.substring(0, match[0].length); + const number = parseFloat(firstPart.substring(match[0].length)); + const b = parseFloat(splits[1]); + if (isNaN(number) || isNaN(b)) { + console.warn(`[WARN] Unable to parse input "${part}"`); + return; + } + for (let i = number; i <= b; i++) { + select.push(`${letters}${i}`); + } + } else { + const a = parseFloat(firstPart); + const b = parseFloat(splits[1]); + if (isNaN(a) || isNaN(b)) { + console.warn(`[WARN] Unable to parse input "${part}"`); + return; + } + for (let i = a; i <= b; i++) { + select.push(`${i}`); + } + } + } else { + if (part.match(/[0-9A-Z]{9}/)) { + select.push(part); + return; + } else if (part.match(/[A-Z]{3}\.[0-9]*/)) { + select.push(part); + return; + } + const match = part.match(/[A-Za-z]+/); + if (match && match.length > 0) { + if (match.index && match.index !== 0) { + console.warn(`[WARN] Unable to parse input "${part}"`); + return; + } + const letters = part.substring(0, match[0].length); + const number = parseFloat(part.substring(match[0].length)); + if (isNaN(number)) { + console.warn(`[WARN] Unable to parse input "${part}"`); + return; + } + select.push(`${letters}${number}`); + } else { + select.push(`${parseFloat(part)}`); + } + } + }); - } else { - const a = parseFloat(firstPart); - const b = parseFloat(splits[1]); - if (isNaN(a) || isNaN(b)) { - console.warn(`[WARN] Unable to parse input "${part}"`); - return; - } - for (let i = a; i <= b; i++) { - select.push(`${i}`); - } - } - - } else { - if (part.match(/[0-9A-Z]{9}/)) { - select.push(part); - return; - } else if (part.match(/[A-Z]{3}\.[0-9]*/)) { - select.push(part); - return; - } - const match = part.match(/[A-Za-z]+/); - if (match && match.length > 0) { - if (match.index && match.index !== 0) { - console.warn(`[WARN] Unable to parse input "${part}"`); - return; - } - const letters = part.substring(0, match[0].length); - const number = parseFloat(part.substring(match[0].length)); - if (isNaN(number)) { - console.warn(`[WARN] Unable to parse input "${part}"`); - return; - } - select.push(`${letters}${number}`); - } else { - select.push(`${parseFloat(part)}`); - } - } - }); - - return { - values: select, - isSelected: (st) => { - if (typeof st === 'string') - st = [st]; - return st.some(st => { - const match = st.match(/[A-Za-z]+/); - if (st.match(/[0-9A-Z]{9}/)) { - const included = select.includes(st); - return but ? !included : included; - } else if (match && match.length > 0) { - if (match.index && match.index !== 0) { - return false; - } - const letter = st.substring(0, match[0].length); - const number = parseFloat(st.substring(match[0].length)); - if (isNaN(number)) { - return false; - } - const included = select.includes(`${letter}${number}`); - return but ? !included : included; - } else { - const included = select.includes(`${parseFloat(st)}`); - return but ? !included : included; - } - }); - } - }; + return { + values: select, + isSelected: (st) => { + if (typeof st === 'string') st = [st]; + return st.some((st) => { + const match = st.match(/[A-Za-z]+/); + if (st.match(/[0-9A-Z]{9}/)) { + const included = select.includes(st); + return but ? !included : included; + } else if (match && match.length > 0) { + if (match.index && match.index !== 0) { + return false; + } + const letter = st.substring(0, match[0].length); + const number = parseFloat(st.substring(match[0].length)); + if (isNaN(number)) { + return false; + } + const included = select.includes(`${letter}${number}`); + return but ? !included : included; + } else { + const included = select.includes(`${parseFloat(st)}`); + return but ? !included : included; + } + }); + } + }; }; -export default parseSelect; \ No newline at end of file +export default parseSelect; diff --git a/modules/module.transform-mpd.ts b/modules/module.transform-mpd.ts index c15fe5d..817d0f2 100644 --- a/modules/module.transform-mpd.ts +++ b/modules/module.transform-mpd.ts @@ -3,258 +3,237 @@ import { LanguageItem, findLang, languages } from './module.langsData'; import { console } from './log'; type Segment = { - uri: string; - timeline: number; - duration: number; - map: { - uri: string; - byterange?: { - length: number, - offset: number - }; - }; - byterange?: { - length: number, - offset: number - }; - number?: number; - presentationTime?: number; -} + uri: string; + timeline: number; + duration: number; + map: { + uri: string; + byterange?: { + length: number; + offset: number; + }; + }; + byterange?: { + length: number; + offset: number; + }; + number?: number; + presentationTime?: number; +}; export type PlaylistItem = { - pssh_wvd?: string, - pssh_prd?: string, - bandwidth: number, - segments: Segment[] -} - + pssh_wvd?: string; + pssh_prd?: string; + bandwidth: number; + segments: Segment[]; +}; type AudioPlayList = { - language: LanguageItem, - default: boolean -} & PlaylistItem + language: LanguageItem; + default: boolean; +} & PlaylistItem; type VideoPlayList = { - quality: { - width: number, - height: number - } -} & PlaylistItem + quality: { + width: number; + height: number; + }; +} & PlaylistItem; export type MPDParsed = { - [server: string]: { - audio: AudioPlayList[], - video: VideoPlayList[] - } -} + [server: string]: { + audio: AudioPlayList[]; + video: VideoPlayList[]; + }; +}; -function extractPSSH( - manifest: string, - schemeIdUri: string, - psshTagNames: string[] -): string | null { - const regex = new RegExp( - `<ContentProtection[^>]*schemeIdUri=["']${schemeIdUri}["'][^>]*>([\\s\\S]*?)</ContentProtection>`, - 'i' - ); - const match = regex.exec(manifest); - if (match && match[1]) { - const innerContent = match[1]; - for (const tagName of psshTagNames) { - const psshRegex = new RegExp(`<${tagName}[^>]*>([\\s\\S]*?)</${tagName}>`, 'i'); - const psshMatch = psshRegex.exec(innerContent); - if (psshMatch && psshMatch[1]) { - return psshMatch[1].trim(); - } - } - } - return null; +function extractPSSH(manifest: string, schemeIdUri: string, psshTagNames: string[]): string | null { + const regex = new RegExp(`<ContentProtection[^>]*schemeIdUri=["']${schemeIdUri}["'][^>]*>([\\s\\S]*?)</ContentProtection>`, 'i'); + const match = regex.exec(manifest); + if (match && match[1]) { + const innerContent = match[1]; + for (const tagName of psshTagNames) { + const psshRegex = new RegExp(`<${tagName}[^>]*>([\\s\\S]*?)</${tagName}>`, 'i'); + const psshMatch = psshRegex.exec(innerContent); + if (psshMatch && psshMatch[1]) { + return psshMatch[1].trim(); + } + } + } + return null; } export async function parse(manifest: string, language?: LanguageItem, url?: string) { - if (!manifest.includes('BaseURL') && url) { - manifest = manifest.replace(/(<MPD*\b[^>]*>)/gm, `$1<BaseURL>${url}</BaseURL>`); - } - const parsed = mpdParse(manifest); - const ret: MPDParsed = {}; + if (!manifest.includes('BaseURL') && url) { + manifest = manifest.replace(/(<MPD*\b[^>]*>)/gm, `$1<BaseURL>${url}</BaseURL>`); + } + const parsed = mpdParse(manifest); + const ret: MPDParsed = {}; - // Audio Loop - for (const item of Object.values(parsed.mediaGroups.AUDIO.audio)){ - for (const playlist of item.playlists) { - const host = new URL(playlist.resolvedUri).hostname; - if (!Object.prototype.hasOwnProperty.call(ret, host)) - ret[host] = { audio: [], video: [] }; + // Audio Loop + for (const item of Object.values(parsed.mediaGroups.AUDIO.audio)) { + for (const playlist of item.playlists) { + const host = new URL(playlist.resolvedUri).hostname; + if (!Object.prototype.hasOwnProperty.call(ret, host)) ret[host] = { audio: [], video: [] }; + if (playlist.sidx && playlist.segments.length == 0) { + const options: RequestInit = { + method: 'head' + }; + if (playlist.sidx.uri.includes('animecdn')) + options.headers = { + origin: 'https://www.animeonegai.com', + referer: 'https://www.animeonegai.com/' + }; + const item = await fetch(playlist.sidx.uri, options); + if (!item.ok) + console.warn(`${item.status}: ${item.statusText}, Unable to fetch byteLength for audio stream ${Math.round(playlist.attributes.BANDWIDTH / 1024)}KiB/s`); + const byteLength = parseInt(item.headers.get('content-length') as string); + let currentByte = playlist.sidx.map.byterange.length; + while (currentByte <= byteLength) { + playlist.segments.push({ + duration: 0, + map: { + uri: playlist.resolvedUri, + resolvedUri: playlist.resolvedUri, + byterange: playlist.sidx.map.byterange + }, + uri: playlist.resolvedUri, + resolvedUri: playlist.resolvedUri, + byterange: { + length: 500000, + offset: currentByte + }, + timeline: 0, + number: 0, + presentationTime: 0 + }); + currentByte = currentByte + 500000; + } + } - if (playlist.sidx && playlist.segments.length == 0) { - const options: RequestInit = { - method: 'head' - }; - if (playlist.sidx.uri.includes('animecdn')) options.headers = { - 'origin': 'https://www.animeonegai.com', - 'referer': 'https://www.animeonegai.com/', - }; - const item = await fetch(playlist.sidx.uri, options); - if (!item.ok) console.warn(`${item.status}: ${item.statusText}, Unable to fetch byteLength for audio stream ${Math.round(playlist.attributes.BANDWIDTH/1024)}KiB/s`); - const byteLength = parseInt(item.headers.get('content-length') as string); - let currentByte = playlist.sidx.map.byterange.length; - while (currentByte <= byteLength) { - playlist.segments.push({ - 'duration': 0, - 'map': { - 'uri': playlist.resolvedUri, - 'resolvedUri': playlist.resolvedUri, - 'byterange': playlist.sidx.map.byterange - }, - 'uri': playlist.resolvedUri, - 'resolvedUri': playlist.resolvedUri, - 'byterange': { - 'length': 500000, - 'offset': currentByte - }, - timeline: 0, - number: 0, - presentationTime: 0 - }); - currentByte = currentByte + 500000; - } - } + //Find and add audio language if it is found in the MPD + let audiolang: LanguageItem; + const foundlanguage = findLang(languages.find((a) => a.code === item.language)?.cr_locale ?? 'unknown'); + if (item.language) { + audiolang = foundlanguage; + } else { + audiolang = language ? language : foundlanguage; + } + const pItem: AudioPlayList = { + bandwidth: playlist.attributes.BANDWIDTH, + language: audiolang, + default: item.default, + segments: playlist.segments.map((segment): Segment => { + const uri = segment.resolvedUri; + const map_uri = segment.map.resolvedUri; + return { + duration: segment.duration, + map: { uri: map_uri, byterange: segment.map.byterange }, + number: segment.number, + presentationTime: segment.presentationTime, + timeline: segment.timeline, + byterange: segment.byterange, + uri + }; + }) + }; - //Find and add audio language if it is found in the MPD - let audiolang: LanguageItem; - const foundlanguage = findLang(languages.find(a => a.code === item.language)?.cr_locale ?? 'unknown'); - if (item.language) { - audiolang = foundlanguage; - } else { - audiolang = language ? language : foundlanguage; - } - const pItem: AudioPlayList = { - bandwidth: playlist.attributes.BANDWIDTH, - language: audiolang, - default: item.default, - segments: playlist.segments.map((segment): Segment => { - const uri = segment.resolvedUri; - const map_uri = segment.map.resolvedUri; - return { - duration: segment.duration, - map: { uri: map_uri, byterange: segment.map.byterange }, - number: segment.number, - presentationTime: segment.presentationTime, - timeline: segment.timeline, - byterange: segment.byterange, - uri - }; - }) - }; + const playreadyPssh = extractPSSH(manifest, 'urn:uuid:9A04F079-9840-4286-AB92-E65BE0885F95', ['cenc:pssh', 'mspr:pro']); - const playreadyPssh = extractPSSH( - manifest, - 'urn:uuid:9A04F079-9840-4286-AB92-E65BE0885F95', - ['cenc:pssh', 'mspr:pro'] - ); + const widevinePssh = extractPSSH(manifest, 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', ['cenc:pssh']); - const widevinePssh = extractPSSH( - manifest, - 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', - ['cenc:pssh'] - ); + if (widevinePssh) { + pItem.pssh_wvd = widevinePssh; + } - if (widevinePssh) { - pItem.pssh_wvd = widevinePssh; - } - - if (playreadyPssh) { - pItem.pssh_prd = playreadyPssh; - } + if (playreadyPssh) { + pItem.pssh_prd = playreadyPssh; + } - ret[host].audio.push(pItem); - } - } + ret[host].audio.push(pItem); + } + } - // Video Loop - for (const playlist of parsed.playlists) { - const host = new URL(playlist.resolvedUri).hostname; - if (!Object.prototype.hasOwnProperty.call(ret, host)) - ret[host] = { audio: [], video: [] }; + // Video Loop + for (const playlist of parsed.playlists) { + const host = new URL(playlist.resolvedUri).hostname; + if (!Object.prototype.hasOwnProperty.call(ret, host)) ret[host] = { audio: [], video: [] }; - if (playlist.sidx && playlist.segments.length == 0) { - const options: RequestInit = { - method: 'head' - }; - if (playlist.sidx.uri.includes('animecdn')) options.headers = { - 'origin': 'https://www.animeonegai.com', - 'referer': 'https://www.animeonegai.com/', - }; - const item = await fetch(playlist.sidx.uri, options); - if (!item.ok) console.warn(`${item.status}: ${item.statusText}, Unable to fetch byteLength for video stream ${playlist.attributes.RESOLUTION?.height}x${playlist.attributes.RESOLUTION?.width}@${Math.round(playlist.attributes.BANDWIDTH/1024)}KiB/s`); - const byteLength = parseInt(item.headers.get('content-length') as string); - let currentByte = playlist.sidx.map.byterange.length; - while (currentByte <= byteLength) { - playlist.segments.push({ - 'duration': 0, - 'map': { - 'uri': playlist.resolvedUri, - 'resolvedUri': playlist.resolvedUri, - 'byterange': playlist.sidx.map.byterange - }, - 'uri': playlist.resolvedUri, - 'resolvedUri': playlist.resolvedUri, - 'byterange': { - 'length': 2000000, - 'offset': currentByte - }, - timeline: 0, - number: 0, - presentationTime: 0 - }); - currentByte = currentByte + 2000000; - } - } + if (playlist.sidx && playlist.segments.length == 0) { + const options: RequestInit = { + method: 'head' + }; + if (playlist.sidx.uri.includes('animecdn')) + options.headers = { + origin: 'https://www.animeonegai.com', + referer: 'https://www.animeonegai.com/' + }; + const item = await fetch(playlist.sidx.uri, options); + if (!item.ok) + console.warn( + `${item.status}: ${item.statusText}, Unable to fetch byteLength for video stream ${playlist.attributes.RESOLUTION?.height}x${playlist.attributes.RESOLUTION?.width}@${Math.round(playlist.attributes.BANDWIDTH / 1024)}KiB/s` + ); + const byteLength = parseInt(item.headers.get('content-length') as string); + let currentByte = playlist.sidx.map.byterange.length; + while (currentByte <= byteLength) { + playlist.segments.push({ + duration: 0, + map: { + uri: playlist.resolvedUri, + resolvedUri: playlist.resolvedUri, + byterange: playlist.sidx.map.byterange + }, + uri: playlist.resolvedUri, + resolvedUri: playlist.resolvedUri, + byterange: { + length: 2000000, + offset: currentByte + }, + timeline: 0, + number: 0, + presentationTime: 0 + }); + currentByte = currentByte + 2000000; + } + } - const pItem: VideoPlayList = { - bandwidth: playlist.attributes.BANDWIDTH, - quality: playlist.attributes.RESOLUTION!, - segments: playlist.segments.map((segment): Segment => { - const uri = segment.resolvedUri; - const map_uri = segment.map.resolvedUri; - return { - duration: segment.duration, - map: { uri: map_uri, byterange: segment.map.byterange }, - number: segment.number, - presentationTime: segment.presentationTime, - timeline: segment.timeline, - byterange: segment.byterange, - uri - }; - }) - }; + const pItem: VideoPlayList = { + bandwidth: playlist.attributes.BANDWIDTH, + quality: playlist.attributes.RESOLUTION!, + segments: playlist.segments.map((segment): Segment => { + const uri = segment.resolvedUri; + const map_uri = segment.map.resolvedUri; + return { + duration: segment.duration, + map: { uri: map_uri, byterange: segment.map.byterange }, + number: segment.number, + presentationTime: segment.presentationTime, + timeline: segment.timeline, + byterange: segment.byterange, + uri + }; + }) + }; - const playreadyPssh = extractPSSH( - manifest, - 'urn:uuid:9A04F079-9840-4286-AB92-E65BE0885F95', - ['cenc:pssh', 'mspr:pro'] - ); + const playreadyPssh = extractPSSH(manifest, 'urn:uuid:9A04F079-9840-4286-AB92-E65BE0885F95', ['cenc:pssh', 'mspr:pro']); - const widevinePssh = extractPSSH( - manifest, - 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', - ['cenc:pssh'] - ); + const widevinePssh = extractPSSH(manifest, 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', ['cenc:pssh']); - if (widevinePssh) { - pItem.pssh_wvd = widevinePssh; - } + if (widevinePssh) { + pItem.pssh_wvd = widevinePssh; + } - if (playreadyPssh) { - pItem.pssh_prd = playreadyPssh; - } + if (playreadyPssh) { + pItem.pssh_prd = playreadyPssh; + } - ret[host].video.push(pItem); - } + ret[host].video.push(pItem); + } - return ret; + return ret; } function arrayBufferToBase64(buffer: Uint8Array): string { - return Buffer.from(buffer).toString('base64'); + return Buffer.from(buffer).toString('base64'); } diff --git a/modules/module.updater.ts b/modules/module.updater.ts index b6add24..372367b 100644 --- a/modules/module.updater.ts +++ b/modules/module.updater.ts @@ -12,186 +12,186 @@ import Helper from './module.helper'; const updateFilePlace = path.join(workingDir, 'config', 'updates.json'); const updateIgnore = [ - '*.d.ts', - '.git', - 'lib', - 'node_modules', - '@types', - path.join('bin', 'mkvtoolnix'), - path.join('config', 'token.yml'), - '.eslint', - 'tsconfig.json', - 'updates.json', - 'tsc.ts' + '*.d.ts', + '.git', + 'lib', + 'node_modules', + '@types', + path.join('bin', 'mkvtoolnix'), + path.join('config', 'token.yml'), + '.eslint', + 'tsconfig.json', + 'updates.json', + 'tsc.ts' ]; const askBeforeUpdate = ['*.yml']; enum ApplyType { - DELETE, - ADD, - UPDATE + DELETE, + ADD, + UPDATE } export type ApplyItem = { - type: ApplyType; - path: string; - content: string; + type: ApplyType; + path: string; + content: string; }; export default async (force = false) => { - const isPackaged = ( - process as NodeJS.Process & { - pkg?: unknown; - } - ).pkg - ? true - : !!process.env.contentDirectory; - if (isPackaged) { - return; - } - let updateFile: UpdateFile | undefined; - if (fs.existsSync(updateFilePlace)) { - updateFile = JSON.parse(fs.readFileSync(updateFilePlace).toString()) as UpdateFile; - if (new Date() < new Date(updateFile.nextCheck) && !force) { - return; - } - } - console.info('Checking for updates...'); - const tagRequest = await fetch('https://api.github.com/repos/anidl/multi-downloader-nx/tags'); - const tags = JSON.parse(await tagRequest.text()) as GithubTag[]; + const isPackaged = ( + process as NodeJS.Process & { + pkg?: unknown; + } + ).pkg + ? true + : !!process.env.contentDirectory; + if (isPackaged) { + return; + } + let updateFile: UpdateFile | undefined; + if (fs.existsSync(updateFilePlace)) { + updateFile = JSON.parse(fs.readFileSync(updateFilePlace).toString()) as UpdateFile; + if (new Date() < new Date(updateFile.nextCheck) && !force) { + return; + } + } + console.info('Checking for updates...'); + const tagRequest = await fetch('https://api.github.com/repos/anidl/multi-downloader-nx/tags'); + const tags = JSON.parse(await tagRequest.text()) as GithubTag[]; - if (tags.length > 0) { - const newer = tags.filter((a) => { - return isNewer(packageJson.version, a.name); - }); - console.info(`Found ${tags.length} release tags and ${newer.length} that are new.`); + if (tags.length > 0) { + const newer = tags.filter((a) => { + return isNewer(packageJson.version, a.name); + }); + console.info(`Found ${tags.length} release tags and ${newer.length} that are new.`); - if (newer.length < 1) { - console.info('No new tags found'); - return done(); - } - const newest = newer.sort((a, b) => (a.name < b.name ? 1 : a.name > b.name ? -1 : 0))[0]; - const compareRequest = await fetch(`https://api.github.com/repos/anidl/multi-downloader-nx/compare/${packageJson.version}...${newest.name}`); + if (newer.length < 1) { + console.info('No new tags found'); + return done(); + } + const newest = newer.sort((a, b) => (a.name < b.name ? 1 : a.name > b.name ? -1 : 0))[0]; + const compareRequest = await fetch(`https://api.github.com/repos/anidl/multi-downloader-nx/compare/${packageJson.version}...${newest.name}`); - const compareJSON = JSON.parse(await compareRequest.text()) as TagCompare; + const compareJSON = JSON.parse(await compareRequest.text()) as TagCompare; - console.info(`You are behind by ${compareJSON.ahead_by} releases!`); - const changedFiles = compareJSON.files - .map((a) => ({ - ...a, - filename: path.join(...a.filename.split('/')) - })) - .filter((a) => { - return !updateIgnore.some((_filter) => matchString(_filter, a.filename)); - }); - if (changedFiles.length < 1) { - console.info('No file changes found... updating package.json. If you think this is an error please get the newst version yourself.'); - return done(newest.name); - } - console.info(`Found file changes: \n${changedFiles.map((a) => ` [${a.status === 'modified' ? '*' : a.status === 'added' ? '+' : '-'}] ${a.filename}`).join('\n')}`); + console.info(`You are behind by ${compareJSON.ahead_by} releases!`); + const changedFiles = compareJSON.files + .map((a) => ({ + ...a, + filename: path.join(...a.filename.split('/')) + })) + .filter((a) => { + return !updateIgnore.some((_filter) => matchString(_filter, a.filename)); + }); + if (changedFiles.length < 1) { + console.info('No file changes found... updating package.json. If you think this is an error please get the newst version yourself.'); + return done(newest.name); + } + console.info(`Found file changes: \n${changedFiles.map((a) => ` [${a.status === 'modified' ? '*' : a.status === 'added' ? '+' : '-'}] ${a.filename}`).join('\n')}`); - const remove: string[] = []; + const remove: string[] = []; - for (const a of changedFiles.filter((a) => a.status !== 'added')) { - if (!askBeforeUpdate.some((pattern) => matchString(pattern, a.filename))) continue; - const answer = await Helper.question( - `The developer decided that the file '${a.filename}' may contain information you changed yourself. Should they be overriden to be updated? [y/N]` - ); - if (answer.toLowerCase() === 'y') remove.push(a.sha); - } + for (const a of changedFiles.filter((a) => a.status !== 'added')) { + if (!askBeforeUpdate.some((pattern) => matchString(pattern, a.filename))) continue; + const answer = await Helper.question( + `The developer decided that the file '${a.filename}' may contain information you changed yourself. Should they be overriden to be updated? [y/N]` + ); + if (answer.toLowerCase() === 'y') remove.push(a.sha); + } - const changesToApply = await Promise.all( - changedFiles - .filter((a) => !remove.includes(a.sha)) - .map(async (a): Promise<ApplyItem> => { - if (a.filename.endsWith('.ts') || a.filename.endsWith('tsx')) { - const isTSX = a.filename.endsWith('tsx'); - const ret = { - path: a.filename.slice(0, isTSX ? -3 : -2) + `js${isTSX ? 'x' : ''}`, - content: transpileModule(await (await fetch(a.raw_url)).text(), { - compilerOptions: tsConfig.compilerOptions as unknown as CompilerOptions - }).outputText, - type: a.status === 'modified' ? ApplyType.UPDATE : a.status === 'added' ? ApplyType.ADD : ApplyType.DELETE - }; - console.info('✓ Transpiled %s', ret.path); - return ret; - } else { - const ret = { - path: a.filename, - content: await (await fetch(a.raw_url)).text(), - type: a.status === 'modified' ? ApplyType.UPDATE : a.status === 'added' ? ApplyType.ADD : ApplyType.DELETE - }; - console.info('✓ Got %s', ret.path); - return ret; - } - }) - ); + const changesToApply = await Promise.all( + changedFiles + .filter((a) => !remove.includes(a.sha)) + .map(async (a): Promise<ApplyItem> => { + if (a.filename.endsWith('.ts') || a.filename.endsWith('tsx')) { + const isTSX = a.filename.endsWith('tsx'); + const ret = { + path: a.filename.slice(0, isTSX ? -3 : -2) + `js${isTSX ? 'x' : ''}`, + content: transpileModule(await (await fetch(a.raw_url)).text(), { + compilerOptions: tsConfig.compilerOptions as unknown as CompilerOptions + }).outputText, + type: a.status === 'modified' ? ApplyType.UPDATE : a.status === 'added' ? ApplyType.ADD : ApplyType.DELETE + }; + console.info('✓ Transpiled %s', ret.path); + return ret; + } else { + const ret = { + path: a.filename, + content: await (await fetch(a.raw_url)).text(), + type: a.status === 'modified' ? ApplyType.UPDATE : a.status === 'added' ? ApplyType.ADD : ApplyType.DELETE + }; + console.info('✓ Got %s', ret.path); + return ret; + } + }) + ); - changesToApply.forEach((a) => { - try { - fsextra.ensureDirSync(path.dirname(a.path)); - fs.writeFileSync(path.join(__dirname, '..', a.path), a.content); - console.info('✓ Written %s', a.path); - } catch (er) { - console.info('✗ Error while writing %s', a.path); - } - }); + changesToApply.forEach((a) => { + try { + fsextra.ensureDirSync(path.dirname(a.path)); + fs.writeFileSync(path.join(__dirname, '..', a.path), a.content); + console.info('✓ Written %s', a.path); + } catch (er) { + console.info('✗ Error while writing %s', a.path); + } + }); - console.info('Done'); - return done(); - } + console.info('Done'); + return done(); + } }; function done(newVersion?: string) { - const next = new Date(Date.now() + 1000 * 60 * 60 * 24); - fs.writeFileSync( - updateFilePlace, - JSON.stringify( - { - lastCheck: Date.now(), - nextCheck: next.getTime() - } as UpdateFile, - null, - 2 - ) - ); - if (newVersion) { - fs.writeFileSync( - '../package.json', - JSON.stringify( - { - ...packageJson, - version: newVersion - }, - null, - 4 - ) - ); - } - console.info('[INFO] Searching for update finished. Next time running on the ' + next.toLocaleDateString() + ' at ' + next.toLocaleTimeString() + '.'); + const next = new Date(Date.now() + 1000 * 60 * 60 * 24); + fs.writeFileSync( + updateFilePlace, + JSON.stringify( + { + lastCheck: Date.now(), + nextCheck: next.getTime() + } as UpdateFile, + null, + 2 + ) + ); + if (newVersion) { + fs.writeFileSync( + '../package.json', + JSON.stringify( + { + ...packageJson, + version: newVersion + }, + null, + 4 + ) + ); + } + console.info('[INFO] Searching for update finished. Next time running on the ' + next.toLocaleDateString() + ' at ' + next.toLocaleTimeString() + '.'); } function isNewer(curr: string, compare: string): boolean { - const currParts = curr.split('.').map((a) => parseInt(a)); - const compareParts = compare.split('.').map((a) => parseInt(a)); + const currParts = curr.split('.').map((a) => parseInt(a)); + const compareParts = compare.split('.').map((a) => parseInt(a)); - for (let i = 0; i < Math.max(currParts.length, compareParts.length); i++) { - if (currParts.length <= i) return true; - if (compareParts.length <= i) return false; - if (currParts[i] !== compareParts[i]) return compareParts[i] > currParts[i]; - } + for (let i = 0; i < Math.max(currParts.length, compareParts.length); i++) { + if (currParts.length <= i) return true; + if (compareParts.length <= i) return false; + if (currParts[i] !== compareParts[i]) return compareParts[i] > currParts[i]; + } - return false; + return false; } function matchString(pattern: string, toMatch: string): boolean { - const filter = path.join('..', pattern); - if (pattern.startsWith('*')) { - return toMatch.endsWith(pattern.slice(1)); - } else if (filter.split(path.sep).pop()?.indexOf('.') === -1) { - return toMatch.startsWith(filter); - } else { - return toMatch.split(path.sep).pop() === pattern; - } + const filter = path.join('..', pattern); + if (pattern.startsWith('*')) { + return toMatch.endsWith(pattern.slice(1)); + } else if (filter.split(path.sep).pop()?.indexOf('.') === -1) { + return toMatch.startsWith(filter); + } else { + return toMatch.split(path.sep).pop() === pattern; + } } diff --git a/modules/module.vtt2ass.ts b/modules/module.vtt2ass.ts index 5846b73..2b8dee5 100644 --- a/modules/module.vtt2ass.ts +++ b/modules/module.vtt2ass.ts @@ -15,514 +15,534 @@ let tmMrg = 0; let rFont = ''; let doCombineLines = false; -type Css = Record<string, { - params: string; - list: string[]; -}> +type Css = Record< + string, + { + params: string; + list: string[]; + } +>; type Vtt = { - caption: string; - time: { - start: string; - end: string; - ext: unknown; - }; - text?: string | undefined; + caption: string; + time: { + start: string; + end: string; + ext: unknown; + }; + text?: string | undefined; }; function loadCSS(cssStr: string): Css { - const css = cssStr.replace(cssPrefixRx, '').replace(/[\r\n]+/g, '\n').split('\n'); - const defaultSFont = rFont == '' ? defaultStyleFont : rFont; - let defaultStyle = `${defaultSFont},40,&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,1,0,2,20,20,20,1`; //base for nonDialog - const styles: Record<string, { - params: string, - list: string[] - }> = { [defaultStyleName]: { params: defaultStyle, list: [] } }; - const classList: Record<string, number> = { [defaultStyleName]: 1 }; - for (const i in css) { - let clx, clz, clzx, rgx; - const l = css[i]; - if (l === '') continue; - const m = l.match(/^(.*)\{(.*)\}$/); - if (!m) { - console.error(`VTT2ASS: Invalid css in line ${i}: ${l}`); - continue; - } + const css = cssStr + .replace(cssPrefixRx, '') + .replace(/[\r\n]+/g, '\n') + .split('\n'); + const defaultSFont = rFont == '' ? defaultStyleFont : rFont; + let defaultStyle = `${defaultSFont},40,&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,1,0,2,20,20,20,1`; //base for nonDialog + const styles: Record< + string, + { + params: string; + list: string[]; + } + > = { [defaultStyleName]: { params: defaultStyle, list: [] } }; + const classList: Record<string, number> = { [defaultStyleName]: 1 }; + for (const i in css) { + let clx, clz, clzx, rgx; + const l = css[i]; + if (l === '') continue; + const m = l.match(/^(.*)\{(.*)\}$/); + if (!m) { + console.error(`VTT2ASS: Invalid css in line ${i}: ${l}`); + continue; + } - if (m[1] === '') { - const style = parseStyle(defaultStyleName, m[2], defaultStyle); - styles[defaultStyleName].params = style; - defaultStyle = style; - } else { - clx = m[1].replace(/\./g, '').split(','); - clz = clx[0].replace(/-C(\d+)_(\d+)$/i, '').replace(/-(\d+)$/i, ''); - classList[clz] = (classList[clz] || 0) + 1; - rgx = classList[clz]; - const classSubNum = rgx > 1 ? `-${rgx}` : ''; - clzx = clz + classSubNum; - const style = parseStyle(clzx, m[2], defaultStyle); - styles[clzx] = { params: style, list: clx }; - } - } - return styles; + if (m[1] === '') { + const style = parseStyle(defaultStyleName, m[2], defaultStyle); + styles[defaultStyleName].params = style; + defaultStyle = style; + } else { + clx = m[1].replace(/\./g, '').split(','); + clz = clx[0].replace(/-C(\d+)_(\d+)$/i, '').replace(/-(\d+)$/i, ''); + classList[clz] = (classList[clz] || 0) + 1; + rgx = classList[clz]; + const classSubNum = rgx > 1 ? `-${rgx}` : ''; + clzx = clz + classSubNum; + const style = parseStyle(clzx, m[2], defaultStyle); + styles[clzx] = { params: style, list: clx }; + } + } + return styles; } function parseStyle(stylegroup: string, line: string, style: any) { - const defaultSFont = rFont == '' ? defaultStyleFont : rFont; //redeclare cause of let + const defaultSFont = rFont == '' ? defaultStyleFont : rFont; //redeclare cause of let - if (stylegroup.startsWith('Subtitle') || stylegroup.startsWith('Song') || stylegroup.startsWith('Q') || stylegroup.startsWith('Default')) { //base for dialog, everything else use defaultStyle - style = `${defaultSFont},${fontSize},&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2.6,0,2,20,20,46,1`; - } + if (stylegroup.startsWith('Subtitle') || stylegroup.startsWith('Song') || stylegroup.startsWith('Q') || stylegroup.startsWith('Default')) { + //base for dialog, everything else use defaultStyle + style = `${defaultSFont},${fontSize},&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2.6,0,2,20,20,46,1`; + } - // Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, - // BackColour, Bold, Italic, Underline, StrikeOut, - // ScaleX, ScaleY, Spacing, Angle, BorderStyle, - // Outline, Shadow, Alignment, MarginL, MarginR, - // MarginV, Encoding - style = style.split(','); - for (const s of line.split(';')) { - if (s == '') continue; - const st = s.trim().split(':'); - if (st[0]) st[0] = st[0].trim(); - if (st[1]) st[1] = st[1].trim(); - let cl, arr, transformed_str; - switch (st[0]) { - case 'font-family': - if (rFont != '') { //do rewrite if rFont is specified - if (stylegroup.startsWith('Subtitle') || stylegroup.startsWith('Song')) { - style[0] = rFont; //dialog to rFont - } else { - style[0] = defaultStyleFont; //non-dialog to Arial - } - } else { //otherwise keep default style - style[0] = st[1].match(/[\s"]*([^",]*)/)![1]; - } - break; - case 'font-size': - style[1] = getPxSize(st[1], style[1]); //scale it based on input style size... so for dialog, this is the dialog font size set in config, for non dialog, it's 40 from default font size - break; - case 'color': - cl = getColor(st[1]); - if (cl !== null) { - if (cl == '&H0000FFFF') { - style[2] = style[3] = '&H00FFFFFF'; - } - else { - style[2] = style[3] = cl; - } - } - break; - case 'font-weight': - if (stylegroup.startsWith('Subtitle') || stylegroup.startsWith('Song')) { //don't touch font-weight if dialog - break; - } - // console.info("Changing bold weight"); - // console.info(stylegroup); - if (st[1] === 'bold') { - style[6] = -1; - break; - } - if (st[1] === 'normal') { - break; - } - break; - case 'text-decoration': - if (st[1] === 'underline') { - style[8] = -1; - } else { - console.warn(`vtt2ass: Unknown text-decoration value: ${st[1]}`); - } - break; - case 'right': - style[17] = 3; - break; - case 'left': - style[17] = 1; - break; - case 'font-style': - if (st[1] === 'italic') { - style[7] = -1; - break; - } - break; - case 'background-color': - case 'background': - if (st[1] === 'none') { - break; - } - break; - case 'text-shadow': - if (stylegroup.startsWith('Subtitle') || stylegroup.startsWith('Song')) { //don't touch shadow if dialog - break; - } - arr = transformed_str = st[1].split(',').map(r => r.trim()); - arr = arr.map(r => { return (r.split(' ').length > 3 ? r.replace(/(\d+)px black$/, '') : r.replace(/black$/, '')).trim(); }); - transformed_str[1] = arr.map(r => r.replace(/-/g, '').replace(/px/g, '').replace(/(^| )0( |$)/g, ' ').trim()).join(' '); - arr = transformed_str[1].split(' '); - if (arr.length != 10) { - console.warn(`VTT2ASS: Can't properly parse text-shadow: ${s.trim()}`); - break; - } - arr = [...new Set(arr)]; - if (arr.length > 1) { - console.warn(`VTT2ASS: Can't properly parse text-shadow: ${s.trim()}`); - break; - } - style[16] = arr[0]; - break; - default: - console.error(`VTT2ASS: Unknown style: ${s.trim()}`); - } - } - return style.join(','); + // Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, + // BackColour, Bold, Italic, Underline, StrikeOut, + // ScaleX, ScaleY, Spacing, Angle, BorderStyle, + // Outline, Shadow, Alignment, MarginL, MarginR, + // MarginV, Encoding + style = style.split(','); + for (const s of line.split(';')) { + if (s == '') continue; + const st = s.trim().split(':'); + if (st[0]) st[0] = st[0].trim(); + if (st[1]) st[1] = st[1].trim(); + let cl, arr, transformed_str; + switch (st[0]) { + case 'font-family': + if (rFont != '') { + //do rewrite if rFont is specified + if (stylegroup.startsWith('Subtitle') || stylegroup.startsWith('Song')) { + style[0] = rFont; //dialog to rFont + } else { + style[0] = defaultStyleFont; //non-dialog to Arial + } + } else { + //otherwise keep default style + style[0] = st[1].match(/[\s"]*([^",]*)/)![1]; + } + break; + case 'font-size': + style[1] = getPxSize(st[1], style[1]); //scale it based on input style size... so for dialog, this is the dialog font size set in config, for non dialog, it's 40 from default font size + break; + case 'color': + cl = getColor(st[1]); + if (cl !== null) { + if (cl == '&H0000FFFF') { + style[2] = style[3] = '&H00FFFFFF'; + } else { + style[2] = style[3] = cl; + } + } + break; + case 'font-weight': + if (stylegroup.startsWith('Subtitle') || stylegroup.startsWith('Song')) { + //don't touch font-weight if dialog + break; + } + // console.info("Changing bold weight"); + // console.info(stylegroup); + if (st[1] === 'bold') { + style[6] = -1; + break; + } + if (st[1] === 'normal') { + break; + } + break; + case 'text-decoration': + if (st[1] === 'underline') { + style[8] = -1; + } else { + console.warn(`vtt2ass: Unknown text-decoration value: ${st[1]}`); + } + break; + case 'right': + style[17] = 3; + break; + case 'left': + style[17] = 1; + break; + case 'font-style': + if (st[1] === 'italic') { + style[7] = -1; + break; + } + break; + case 'background-color': + case 'background': + if (st[1] === 'none') { + break; + } + break; + case 'text-shadow': + if (stylegroup.startsWith('Subtitle') || stylegroup.startsWith('Song')) { + //don't touch shadow if dialog + break; + } + arr = transformed_str = st[1].split(',').map((r) => r.trim()); + arr = arr.map((r) => { + return (r.split(' ').length > 3 ? r.replace(/(\d+)px black$/, '') : r.replace(/black$/, '')).trim(); + }); + transformed_str[1] = arr + .map((r) => + r + .replace(/-/g, '') + .replace(/px/g, '') + .replace(/(^| )0( |$)/g, ' ') + .trim() + ) + .join(' '); + arr = transformed_str[1].split(' '); + if (arr.length != 10) { + console.warn(`VTT2ASS: Can't properly parse text-shadow: ${s.trim()}`); + break; + } + arr = [...new Set(arr)]; + if (arr.length > 1) { + console.warn(`VTT2ASS: Can't properly parse text-shadow: ${s.trim()}`); + break; + } + style[16] = arr[0]; + break; + default: + console.error(`VTT2ASS: Unknown style: ${s.trim()}`); + } + } + return style.join(','); } function getPxSize(size_line: string, font_size: number) { - const m = size_line.trim().match(/([\d.]+)(.*)/); - if (!m) { - console.error(`VTT2ASS: Unknown size: ${size_line}`); - return; - } - let size = parseFloat(m[1]); - if (m[2] === 'em') size *= font_size; - return Math.round(size); + const m = size_line.trim().match(/([\d.]+)(.*)/); + if (!m) { + console.error(`VTT2ASS: Unknown size: ${size_line}`); + return; + } + let size = parseFloat(m[1]); + if (m[2] === 'em') size *= font_size; + return Math.round(size); } function getColor(c: string) { - if (c[0] !== '#') { - c = colors[c as keyof typeof colors]; - } else if (c.length < 7 || c.length > 7) { - c = `#${c[1]}${c[1]}${c[2]}${c[2]}${c[3]}${c[3]}`; - } - const m = c.match(/#(..)(..)(..)/); - if (!m) return null; - return `&H00${m[3]}${m[2]}${m[1]}`.toUpperCase(); + if (c[0] !== '#') { + c = colors[c as keyof typeof colors]; + } else if (c.length < 7 || c.length > 7) { + c = `#${c[1]}${c[1]}${c[2]}${c[2]}${c[3]}${c[3]}`; + } + const m = c.match(/#(..)(..)(..)/); + if (!m) return null; + return `&H00${m[3]}${m[2]}${m[1]}`.toUpperCase(); } function loadVTT(vttStr: string): Vtt[] { - const rx = /^([\d:.]*) --> ([\d:.]*)\s?(.*?)\s*$/; - const lines = vttStr.replace(/\r?\n/g, '\n').split('\n'); - const data = []; - let record: null|Vtt = null; - let lineBuf = []; - for (const l of lines) { - const m = l.match(rx); - if (m) { - let caption = ''; - if (lineBuf.length > 0) { - caption = lineBuf.pop()!; - } - if (caption !== '' && lineBuf.length > 0) { - lineBuf.pop(); - } - if (record !== null) { - record.text = lineBuf.join('\n'); - data.push(record); - } - record = { - caption, - time: { - start: m[1], - end: m[2], - ext: m[3].split(' ').map(x => x.split(':')).reduce((p, c) => ((p as any)[c[0]] = c[1] ?? 'invalid-input') && p, {}), - } - }; - lineBuf = []; - continue; - } - lineBuf.push(l); - } - if (record !== null) { - if (lineBuf[lineBuf.length - 1] === '') { - lineBuf.pop(); - } - record.text = lineBuf.join('\n'); - data.push(record); - } - return data; + const rx = /^([\d:.]*) --> ([\d:.]*)\s?(.*?)\s*$/; + const lines = vttStr.replace(/\r?\n/g, '\n').split('\n'); + const data = []; + let record: null | Vtt = null; + let lineBuf = []; + for (const l of lines) { + const m = l.match(rx); + if (m) { + let caption = ''; + if (lineBuf.length > 0) { + caption = lineBuf.pop()!; + } + if (caption !== '' && lineBuf.length > 0) { + lineBuf.pop(); + } + if (record !== null) { + record.text = lineBuf.join('\n'); + data.push(record); + } + record = { + caption, + time: { + start: m[1], + end: m[2], + ext: m[3] + .split(' ') + .map((x) => x.split(':')) + .reduce((p, c) => ((p as any)[c[0]] = c[1] ?? 'invalid-input') && p, {}) + } + }; + lineBuf = []; + continue; + } + lineBuf.push(l); + } + if (record !== null) { + if (lineBuf[lineBuf.length - 1] === '') { + lineBuf.pop(); + } + record.text = lineBuf.join('\n'); + data.push(record); + } + return data; } function timestampToCentiseconds(timestamp: string) { - const timestamp_split = timestamp.split(':'); - const timestamp_sec_split = timestamp_split[2].split('.'); - const hour = parseInt(timestamp_split[0]); - const minute = parseInt(timestamp_split[1]); - const second = parseInt(timestamp_sec_split[0]); - const centisecond = parseInt(timestamp_sec_split[1]); - - return 360000 * hour + 6000 * minute + 100 * second + centisecond; + const timestamp_split = timestamp.split(':'); + const timestamp_sec_split = timestamp_split[2].split('.'); + const hour = parseInt(timestamp_split[0]); + const minute = parseInt(timestamp_split[1]); + const second = parseInt(timestamp_sec_split[0]); + const centisecond = parseInt(timestamp_sec_split[1]); + + return 360000 * hour + 6000 * minute + 100 * second + centisecond; } function combineLines(events: string[]): string[] { - if (!doCombineLines) { - return events; - } - // This function is for combining adjacent lines with same information - const newLines: string[] = []; - for (const currentLine of events) { - let hasCombined: boolean = false; - // Check previous 7 elements, arbritary lookback amount - for (let j = 1; j < 8 && j < newLines.length; j++) { - const checkLine = newLines[newLines.length - j]; - const checkLineSplit = checkLine.split(','); - const currentLineSplit = currentLine.split(','); - // 1 = start, 2 = end, 3 = style, 9+ = text - if (checkLineSplit.slice(9).join(',') == currentLineSplit.slice(9).join(',') && - checkLineSplit[3] == currentLineSplit[3] && - checkLineSplit[2] == currentLineSplit[1] - ) { - checkLineSplit[2] = currentLineSplit[2]; - newLines[newLines.length - j] = checkLineSplit.join(','); - hasCombined = true; - break; - } - } - if (!hasCombined) { - newLines.push(currentLine); - } - } - return newLines; + if (!doCombineLines) { + return events; + } + // This function is for combining adjacent lines with same information + const newLines: string[] = []; + for (const currentLine of events) { + let hasCombined: boolean = false; + // Check previous 7 elements, arbritary lookback amount + for (let j = 1; j < 8 && j < newLines.length; j++) { + const checkLine = newLines[newLines.length - j]; + const checkLineSplit = checkLine.split(','); + const currentLineSplit = currentLine.split(','); + // 1 = start, 2 = end, 3 = style, 9+ = text + if (checkLineSplit.slice(9).join(',') == currentLineSplit.slice(9).join(',') && checkLineSplit[3] == currentLineSplit[3] && checkLineSplit[2] == currentLineSplit[1]) { + checkLineSplit[2] = currentLineSplit[2]; + newLines[newLines.length - j] = checkLineSplit.join(','); + hasCombined = true; + break; + } + } + if (!hasCombined) { + newLines.push(currentLine); + } + } + return newLines; } function pushBuffer(buffer: ReturnType<typeof convertLine>[], events: string[]) { - buffer.reverse(); - const bufferStrings: string[] = buffer.map(line => - `Dialogue: 1,${line.start},${line.end},${line.style},,0,0,0,,${line.text}`); - events.push(...bufferStrings); - buffer.splice(0,buffer.length); + buffer.reverse(); + const bufferStrings: string[] = buffer.map((line) => `Dialogue: 1,${line.start},${line.end},${line.style},,0,0,0,,${line.text}`); + events.push(...bufferStrings); + buffer.splice(0, buffer.length); } function convert(css: Css, vtt: Vtt[]) { - const stylesMap: Record<string, string> = {}; - let ass = [ - '\ufeff[Script Info]', - 'Title: ' + relGroup, - 'ScriptType: v4.00+', - 'WrapStyle: 0', - 'PlayResX: 1280', - 'PlayResY: 720', - 'ScaledBorderAndShadow: yes', - '', - '[V4+ Styles]', - 'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding', - ]; - for (const s in css) { - ass.push(`Style: ${s},${css[s].params}`); - css[s].list.forEach(x => stylesMap[x] = s); - } - ass = ass.concat([ - '', - '[Events]', - 'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text' - ]); - const events: { - subtitle: string[], - caption: string[], - capt_pos: string[], - song_cap: string[], - } = { - subtitle: [], - caption: [], - capt_pos: [], - song_cap: [], - }; - const linesMap: Record<string, number> = {}; - const buffer: ReturnType<typeof convertLine>[] = []; - const captionsBuffer: string[] = []; - for (const l in vtt) { - const x = convertLine(stylesMap, vtt[l]); - if (x.ind !== '' && linesMap[x.ind] !== undefined) { - if (x.subInd > 1) { - const fx = convertLine(stylesMap, vtt[parseInt(l) - x.subInd + 1]); - if (x.style != fx.style) { - x.text = `{\\r${x.style}}${x.text}{\\r}`; - } - } - events[x.type as keyof typeof events][linesMap[x.ind]] += '\\N' + x.text; - } - else { - events[x.type as keyof typeof events].push(x.res); - if (x.ind !== '') { - linesMap[x.ind] = events[x.type as keyof typeof events].length - 1; - } - } - /** - * What cursed code have I brought upon this land? - * This handles making lines multi-line when neccesary and reverses - * order of subtitles so that they display correctly - */ - if (x.type != 'subtitle') { - // Do nothing - } else if (x.text.includes('\\pos')) { - events['subtitle'].pop(); - captionsBuffer.push(x.res); - } else if (buffer.length > 0) { - const previousBufferLine = buffer[buffer.length - 1]; - const previousStart = timestampToCentiseconds(previousBufferLine.start); - const currentStart = timestampToCentiseconds(x.start); - events['subtitle'].pop(); - if ((currentStart - previousStart) <= 2) { - x.start = previousBufferLine.start; - if (previousBufferLine.style == x.style) { - buffer.pop(); - x.text = previousBufferLine.text + '\\N' + x.text; - } - } else { - pushBuffer(buffer, events['subtitle']); - } - buffer.push(x); - } - else { - events['subtitle'].pop(); - buffer.push(x); - } - } + const stylesMap: Record<string, string> = {}; + let ass = [ + '\ufeff[Script Info]', + 'Title: ' + relGroup, + 'ScriptType: v4.00+', + 'WrapStyle: 0', + 'PlayResX: 1280', + 'PlayResY: 720', + 'ScaledBorderAndShadow: yes', + '', + '[V4+ Styles]', + 'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding' + ]; + for (const s in css) { + ass.push(`Style: ${s},${css[s].params}`); + css[s].list.forEach((x) => (stylesMap[x] = s)); + } + ass = ass.concat(['', '[Events]', 'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text']); + const events: { + subtitle: string[]; + caption: string[]; + capt_pos: string[]; + song_cap: string[]; + } = { + subtitle: [], + caption: [], + capt_pos: [], + song_cap: [] + }; + const linesMap: Record<string, number> = {}; + const buffer: ReturnType<typeof convertLine>[] = []; + const captionsBuffer: string[] = []; + for (const l in vtt) { + const x = convertLine(stylesMap, vtt[l]); + if (x.ind !== '' && linesMap[x.ind] !== undefined) { + if (x.subInd > 1) { + const fx = convertLine(stylesMap, vtt[parseInt(l) - x.subInd + 1]); + if (x.style != fx.style) { + x.text = `{\\r${x.style}}${x.text}{\\r}`; + } + } + events[x.type as keyof typeof events][linesMap[x.ind]] += '\\N' + x.text; + } else { + events[x.type as keyof typeof events].push(x.res); + if (x.ind !== '') { + linesMap[x.ind] = events[x.type as keyof typeof events].length - 1; + } + } + /** + * What cursed code have I brought upon this land? + * This handles making lines multi-line when neccesary and reverses + * order of subtitles so that they display correctly + */ + if (x.type != 'subtitle') { + // Do nothing + } else if (x.text.includes('\\pos')) { + events['subtitle'].pop(); + captionsBuffer.push(x.res); + } else if (buffer.length > 0) { + const previousBufferLine = buffer[buffer.length - 1]; + const previousStart = timestampToCentiseconds(previousBufferLine.start); + const currentStart = timestampToCentiseconds(x.start); + events['subtitle'].pop(); + if (currentStart - previousStart <= 2) { + x.start = previousBufferLine.start; + if (previousBufferLine.style == x.style) { + buffer.pop(); + x.text = previousBufferLine.text + '\\N' + x.text; + } + } else { + pushBuffer(buffer, events['subtitle']); + } + buffer.push(x); + } else { + events['subtitle'].pop(); + buffer.push(x); + } + } - pushBuffer(buffer, events['subtitle']); - events['subtitle'].push(...captionsBuffer); - events['subtitle'] = combineLines(events['subtitle']); + pushBuffer(buffer, events['subtitle']); + events['subtitle'].push(...captionsBuffer); + events['subtitle'] = combineLines(events['subtitle']); - if (events.subtitle.length > 0) { - ass = ass.concat( - //`Comment: 0,0:00:00.00,0:00:00.00,${defaultStyleName},,0,0,0,,** Subtitles **`, - events.subtitle - ); - } - if (events.caption.length > 0) { - ass = ass.concat( - //`Comment: 0,0:00:00.00,0:00:00.00,${defaultStyleName},,0,0,0,,** Captions **`, - events.caption - ); - } - if (events.capt_pos.length > 0) { - ass = ass.concat( - //`Comment: 0,0:00:00.00,0:00:00.00,${defaultStyleName},,0,0,0,,** Captions with position **`, - events.capt_pos - ); - } - if (events.song_cap.length > 0) { - ass = ass.concat( - //`Comment: 0,0:00:00.00,0:00:00.00,${defaultStyleName},,0,0,0,,** Song captions **`, - events.song_cap - ); - } - return ass.join('\r\n') + '\r\n'; + if (events.subtitle.length > 0) { + ass = ass.concat( + //`Comment: 0,0:00:00.00,0:00:00.00,${defaultStyleName},,0,0,0,,** Subtitles **`, + events.subtitle + ); + } + if (events.caption.length > 0) { + ass = ass.concat( + //`Comment: 0,0:00:00.00,0:00:00.00,${defaultStyleName},,0,0,0,,** Captions **`, + events.caption + ); + } + if (events.capt_pos.length > 0) { + ass = ass.concat( + //`Comment: 0,0:00:00.00,0:00:00.00,${defaultStyleName},,0,0,0,,** Captions with position **`, + events.capt_pos + ); + } + if (events.song_cap.length > 0) { + ass = ass.concat( + //`Comment: 0,0:00:00.00,0:00:00.00,${defaultStyleName},,0,0,0,,** Song captions **`, + events.song_cap + ); + } + return ass.join('\r\n') + '\r\n'; } function convertLine(css: Record<string, string>, l: Record<any, any>) { - const start = convertTime(l.time.start); - const end = convertTime(l.time.end); - const txt = convertText(l.text); - let type = txt.style.match(/Caption/i) ? 'caption' : (txt.style.match(/SongCap/i) ? 'song_cap' : 'subtitle'); - type = type == 'caption' && l.time.ext?.position !== undefined ? 'capt_pos' : type; - if (l.time.ext?.align === 'left') { - txt.text = `{\\an7}${txt.text}`; - } - let ind = '', subInd = 1; - const sMinus = 0; // (19.2 * 2); - if (l.time.ext?.position !== undefined) { - const pos = parseInt(l.time.ext.position); - const PosX = pos < 0 ? (1280 / 100 * (100 - pos)) : ((1280 - sMinus) / 100 * pos); - const line = parseInt(l.time.ext.line) || 0; - const PosY = line < 0 ? (720 / 100 * (100 - line)) : ((720 - sMinus) / 100 * line); - txt.text = `{\\pos(${parseFloat(PosX.toFixed(3))},${parseFloat(PosY.toFixed(3))})}${txt.text}`; - } - else if (l.time.ext?.line !== undefined && type == 'caption') { - const line = parseInt(l.time.ext.line); - const PosY = line < 0 ? (720 / 100 * (100 - line)) : ((720 - sMinus) / 100 * line); - txt.text = `{\\pos(640,${parseFloat(PosY.toFixed(3))})}${txt.text}`; - } - else { - const indregx = txt.style.match(/(.*)_(\d+)$/); - if (indregx !== null) { - ind = indregx[1]; - subInd = parseInt(indregx[2]); - } - } - const style = css[txt.style as any] || defaultStyleName; - const res = `Dialogue: 0,${start},${end},${style},,0,0,0,,${txt.text}`; - return { type, ind, subInd, start, end, style, text: txt.text, res }; + const start = convertTime(l.time.start); + const end = convertTime(l.time.end); + const txt = convertText(l.text); + let type = txt.style.match(/Caption/i) ? 'caption' : txt.style.match(/SongCap/i) ? 'song_cap' : 'subtitle'; + type = type == 'caption' && l.time.ext?.position !== undefined ? 'capt_pos' : type; + if (l.time.ext?.align === 'left') { + txt.text = `{\\an7}${txt.text}`; + } + let ind = '', + subInd = 1; + const sMinus = 0; // (19.2 * 2); + if (l.time.ext?.position !== undefined) { + const pos = parseInt(l.time.ext.position); + const PosX = pos < 0 ? (1280 / 100) * (100 - pos) : ((1280 - sMinus) / 100) * pos; + const line = parseInt(l.time.ext.line) || 0; + const PosY = line < 0 ? (720 / 100) * (100 - line) : ((720 - sMinus) / 100) * line; + txt.text = `{\\pos(${parseFloat(PosX.toFixed(3))},${parseFloat(PosY.toFixed(3))})}${txt.text}`; + } else if (l.time.ext?.line !== undefined && type == 'caption') { + const line = parseInt(l.time.ext.line); + const PosY = line < 0 ? (720 / 100) * (100 - line) : ((720 - sMinus) / 100) * line; + txt.text = `{\\pos(640,${parseFloat(PosY.toFixed(3))})}${txt.text}`; + } else { + const indregx = txt.style.match(/(.*)_(\d+)$/); + if (indregx !== null) { + ind = indregx[1]; + subInd = parseInt(indregx[2]); + } + } + const style = css[txt.style as any] || defaultStyleName; + const res = `Dialogue: 0,${start},${end},${style},,0,0,0,,${txt.text}`; + return { type, ind, subInd, start, end, style, text: txt.text, res }; } function convertText(text: string) { - //const m = text.match(/<c\.([^>]*)>([\S\s]*)<\/c>/); - const m = text.match(/<(?:c\.|)([^>]*)>([\S\s]*)<\/(?:c|Default)>/); - let style = ''; - if (m) { - style = m[1]; - text = m[2]; - } - const xtext = text - // .replace(/<c[^>]*>[^<]*<\/c>/g, '') - // .replace(/<ruby[^>]*>[^<]*<\/ruby>/g, '') - .replace(/ \\N$/g, '\\N') - //.replace(/<[^>]>/g, '') - .replace(/\\N$/, '') - .replace(/\r/g, '') - .replace(/\n/g, '\\N') - .replace(/\\N +/g, '\\N') - .replace(/ +\\N/g, '\\N') - .replace(/(\\N)+/g, '\\N') - .replace(/<b[^>]*>([^<]*)<\/b>/g, '{\\b1}$1{\\b0}') - .replace(/<i[^>]*>([^<]*)<\/i>/g, '{\\i1}$1{\\i0}') - .replace(/<u[^>]*>([^<]*)<\/u>/g, '{\\u1}$1{\\u0}') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/&/g, '&') - .replace(/<[^>]>/g, '') - .replace(/\\N$/, '') - .replace(/ +$/, ''); - text = xtext; - return { style, text }; + //const m = text.match(/<c\.([^>]*)>([\S\s]*)<\/c>/); + const m = text.match(/<(?:c\.|)([^>]*)>([\S\s]*)<\/(?:c|Default)>/); + let style = ''; + if (m) { + style = m[1]; + text = m[2]; + } + const xtext = text + // .replace(/<c[^>]*>[^<]*<\/c>/g, '') + // .replace(/<ruby[^>]*>[^<]*<\/ruby>/g, '') + .replace(/ \\N$/g, '\\N') + //.replace(/<[^>]>/g, '') + .replace(/\\N$/, '') + .replace(/\r/g, '') + .replace(/\n/g, '\\N') + .replace(/\\N +/g, '\\N') + .replace(/ +\\N/g, '\\N') + .replace(/(\\N)+/g, '\\N') + .replace(/<b[^>]*>([^<]*)<\/b>/g, '{\\b1}$1{\\b0}') + .replace(/<i[^>]*>([^<]*)<\/i>/g, '{\\i1}$1{\\i0}') + .replace(/<u[^>]*>([^<]*)<\/u>/g, '{\\u1}$1{\\u0}') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&') + .replace(/<[^>]>/g, '') + .replace(/\\N$/, '') + .replace(/ +$/, ''); + text = xtext; + return { style, text }; } function convertTime(tm: string) { - const m = tm.match(/([\d:]*)\.?(\d*)/); - if (!m) return '0:00:00.00'; - return toSubTime(m[0]); + const m = tm.match(/([\d:]*)\.?(\d*)/); + if (!m) return '0:00:00.00'; + return toSubTime(m[0]); } function toSubTime(str: string) { - const n = []; - let sx; - const x: any[] = str.split(/[:.]/).map(x => Number(x)); - x[3] = '0.' + ('00' + x[3]).slice(-3); - sx = (x[0] * 60 * 60 + x[1] * 60 + x[2] + Number(x[3]) - tmMrg).toFixed(2); - sx = sx.toString().split('.'); - n.unshift(sx[1]); - sx = Number(sx[0]); - n.unshift(('0' + ((sx % 60).toString())).slice(-2)); - n.unshift(('0' + ((Math.floor(sx / 60) % 60).toString())).slice(-2)); - n.unshift((Math.floor(sx / 3600) % 60).toString()); - return n.slice(0, 3).join(':') + '.' + n[3]; + const n = []; + let sx; + const x: any[] = str.split(/[:.]/).map((x) => Number(x)); + x[3] = '0.' + ('00' + x[3]).slice(-3); + sx = (x[0] * 60 * 60 + x[1] * 60 + x[2] + Number(x[3]) - tmMrg).toFixed(2); + sx = sx.toString().split('.'); + n.unshift(sx[1]); + sx = Number(sx[0]); + n.unshift(('0' + (sx % 60).toString()).slice(-2)); + n.unshift(('0' + (Math.floor(sx / 60) % 60).toString()).slice(-2)); + n.unshift((Math.floor(sx / 3600) % 60).toString()); + return n.slice(0, 3).join(':') + '.' + n[3]; } -export default function vtt2ass(group: string | undefined, xFontSize: number | undefined, vttStr: string, cssStr: string, timeMargin?: number, replaceFont?: string, combineLines?: boolean) { - relGroup = group ?? ''; - fontSize = xFontSize && xFontSize > 0 ? xFontSize : 34; // 1em to pix - tmMrg = timeMargin ? timeMargin : 0; // - rFont = replaceFont ? replaceFont : rFont; - doCombineLines = combineLines ? combineLines : doCombineLines; - if (vttStr.match(/::cue(?:.(.+)\) *)?{([^}]+)}/g)) { - const cssLines = []; - let defaultCss = ''; - const cssGroups = vttStr.matchAll(/::cue(?:.(.+)\) *)?{([^}]+)}/g); - for (const cssGroup of cssGroups) { - //Below code will bulldoze defined sizes for custom ones - /*if (!options.originalFontSize) { +export default function vtt2ass( + group: string | undefined, + xFontSize: number | undefined, + vttStr: string, + cssStr: string, + timeMargin?: number, + replaceFont?: string, + combineLines?: boolean +) { + relGroup = group ?? ''; + fontSize = xFontSize && xFontSize > 0 ? xFontSize : 34; // 1em to pix + tmMrg = timeMargin ? timeMargin : 0; // + rFont = replaceFont ? replaceFont : rFont; + doCombineLines = combineLines ? combineLines : doCombineLines; + if (vttStr.match(/::cue(?:.(.+)\) *)?{([^}]+)}/g)) { + const cssLines = []; + let defaultCss = ''; + const cssGroups = vttStr.matchAll(/::cue(?:.(.+)\) *)?{([^}]+)}/g); + for (const cssGroup of cssGroups) { + //Below code will bulldoze defined sizes for custom ones + /*if (!options.originalFontSize) { cssGroup[2] = cssGroup[2].replace(/( font-size:.+?;)/g, '').replace(/(font-size:.+?;)/g, ''); }*/ - if (cssGroup[1]) { - cssLines.push(`${cssGroup[1]}{${defaultCss}${cssGroup[2].replace(/(\r\n|\n|\r)/gm, '')}}`); - } else { - defaultCss = cssGroup[2].replace(/(\r\n|\n|\r)/gm, ''); - //cssLines.push(`{${defaultCss}}`); - } - } - cssStr += cssLines.join('\r\n'); - } - return convert( - loadCSS(cssStr), - loadVTT(vttStr) - ); + if (cssGroup[1]) { + cssLines.push(`${cssGroup[1]}{${defaultCss}${cssGroup[2].replace(/(\r\n|\n|\r)/gm, '')}}`); + } else { + defaultCss = cssGroup[2].replace(/(\r\n|\n|\r)/gm, ''); + //cssLines.push(`{${defaultCss}}`); + } + } + cssStr += cssLines.join('\r\n'); + } + return convert(loadCSS(cssStr), loadVTT(vttStr)); } diff --git a/modules/module.vttconvert.ts b/modules/module.vttconvert.ts index 9d53b97..f097eda 100644 --- a/modules/module.vttconvert.ts +++ b/modules/module.vttconvert.ts @@ -1,174 +1,180 @@ // vtt loader export type Record = { - text?: string, - time_start?: string, - time_end?: string, - ext_param?: unknown + text?: string; + time_start?: string; + time_end?: string; + ext_param?: unknown; }; export type NullRecord = Record | null; function loadVtt(vttStr: string) { - const rx = /^([\d:.]*) --> ([\d:.]*)\s?(.*?)\s*$/; - const lines = vttStr.replace(/\r?\n/g, '\n').split('\n'); - const data: Record[] = []; let lineBuf: string[] = [], record: NullRecord = null; - // check lines - for (const l of lines) { - const m = l.match(rx); - if (m) { - if (lineBuf.length > 0) { - lineBuf.pop(); - } - if (record !== null) { - record.text = lineBuf.join('\n'); - data.push(record); - } - record = { - time_start: m[1], - time_end: m[2], - ext_param: m[3].split(' ').map(x => x.split(':')).reduce((p: any, c: any) => (p[c[0]] = c[1]) && p, {}), - }; - lineBuf = []; - continue; - } - lineBuf.push(l); - } - if (record !== null) { - if (lineBuf[lineBuf.length - 1] === '') { - lineBuf.pop(); - } - record.text = lineBuf.join('\n'); - data.push(record); - } - return data; + const rx = /^([\d:.]*) --> ([\d:.]*)\s?(.*?)\s*$/; + const lines = vttStr.replace(/\r?\n/g, '\n').split('\n'); + const data: Record[] = []; + let lineBuf: string[] = [], + record: NullRecord = null; + // check lines + for (const l of lines) { + const m = l.match(rx); + if (m) { + if (lineBuf.length > 0) { + lineBuf.pop(); + } + if (record !== null) { + record.text = lineBuf.join('\n'); + data.push(record); + } + record = { + time_start: m[1], + time_end: m[2], + ext_param: m[3] + .split(' ') + .map((x) => x.split(':')) + .reduce((p: any, c: any) => (p[c[0]] = c[1]) && p, {}) + }; + lineBuf = []; + continue; + } + lineBuf.push(l); + } + if (record !== null) { + if (lineBuf[lineBuf.length - 1] === '') { + lineBuf.pop(); + } + record.text = lineBuf.join('\n'); + data.push(record); + } + return data; } // ass specific -function convertToAss(vttStr: string, lang: string, fontSize: number, fontName?: string){ - let ass = [ - '\ufeff[Script Info]', - `Title: ${lang}`, - 'ScriptType: v4.00+', - 'PlayResX: 1280', - 'PlayResY: 720', - 'WrapStyle: 0', - 'ScaledBorderAndShadow: yes', - '', - '[V4+ Styles]', - 'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, ' - + 'Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, ' - + 'BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding', - `Style: Main,${fontName || 'Noto Sans'},${fontSize},&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,10,10,10,1`, - `Style: MainTop,${fontName || 'Noto Sans'},${fontSize},&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,3,0,8,10,10,10,1`, - '', - '[Events]', - 'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text', - ]; - - const vttData = loadVtt(vttStr); - for (const l of vttData) { - const line = convertToAssLine(l, 'Main'); - ass = ass.concat(line); - } - - return ass.join('\r\n') + '\r\n'; +function convertToAss(vttStr: string, lang: string, fontSize: number, fontName?: string) { + let ass = [ + '\ufeff[Script Info]', + `Title: ${lang}`, + 'ScriptType: v4.00+', + 'PlayResX: 1280', + 'PlayResY: 720', + 'WrapStyle: 0', + 'ScaledBorderAndShadow: yes', + '', + '[V4+ Styles]', + 'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, ' + + 'Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, ' + + 'BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding', + `Style: Main,${fontName || 'Noto Sans'},${fontSize},&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,10,10,10,1`, + `Style: MainTop,${fontName || 'Noto Sans'},${fontSize},&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,3,0,8,10,10,10,1`, + '', + '[Events]', + 'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text' + ]; + + const vttData = loadVtt(vttStr); + for (const l of vttData) { + const line = convertToAssLine(l, 'Main'); + ass = ass.concat(line); + } + + return ass.join('\r\n') + '\r\n'; } function convertToAssLine(l: Record, style: string) { - const start = convertTime(l.time_start as string); - const end = convertTime(l.time_end as string); - const text = convertToAssText(l.text as string); - - // debugger - if ((l.ext_param as any).line === '7%') { - style = 'MainTop'; - } - - if ((l.ext_param as any).line === '10%') { - style = 'MainTop'; - } - - return `Dialogue: 0,${start},${end},${style},,0,0,0,,${text}`; + const start = convertTime(l.time_start as string); + const end = convertTime(l.time_end as string); + const text = convertToAssText(l.text as string); + + // debugger + if ((l.ext_param as any).line === '7%') { + style = 'MainTop'; + } + + if ((l.ext_param as any).line === '10%') { + style = 'MainTop'; + } + + return `Dialogue: 0,${start},${end},${style},,0,0,0,,${text}`; } function convertToAssText(text: string) { - text = text - .replace(/\r/g, '') - .replace(/\n/g, '\\N') - .replace(/\\N +/g, '\\N') - .replace(/ +\\N/g, '\\N') - .replace(/(\\N)+/g, '\\N') - .replace(/<b[^>]*>([^<]*)<\/b>/g, '{\\b1}$1{\\b0}') - .replace(/<i[^>]*>([^<]*)<\/i>/g, '{\\i1}$1{\\i0}') - .replace(/<u[^>]*>([^<]*)<\/u>/g, '{\\u1}$1{\\u0}') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/&/g, '&') - .replace(/<[^>]>/g, '') - .replace(/\\N$/, '') - .replace(/ +$/, ''); - return text; + text = text + .replace(/\r/g, '') + .replace(/\n/g, '\\N') + .replace(/\\N +/g, '\\N') + .replace(/ +\\N/g, '\\N') + .replace(/(\\N)+/g, '\\N') + .replace(/<b[^>]*>([^<]*)<\/b>/g, '{\\b1}$1{\\b0}') + .replace(/<i[^>]*>([^<]*)<\/i>/g, '{\\i1}$1{\\i0}') + .replace(/<u[^>]*>([^<]*)<\/u>/g, '{\\u1}$1{\\u0}') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&') + .replace(/<[^>]>/g, '') + .replace(/\\N$/, '') + .replace(/ +$/, ''); + return text; } // srt specific -function convertToSrt(vttStr: string){ - let srt: string[] = [], srtLineIdx = 0; - - const vttData = loadVtt(vttStr); - for (const l of vttData) { - srtLineIdx++; - const line = convertToSrtLine(l, srtLineIdx); - srt = srt.concat(line); - } - - return srt.join('\r\n') + '\r\n'; +function convertToSrt(vttStr: string) { + let srt: string[] = [], + srtLineIdx = 0; + + const vttData = loadVtt(vttStr); + for (const l of vttData) { + srtLineIdx++; + const line = convertToSrtLine(l, srtLineIdx); + srt = srt.concat(line); + } + + return srt.join('\r\n') + '\r\n'; } -function convertToSrtLine(l: Record, idx: number) : string { - const bom = idx == 1 ? '\ufeff' : ''; - const start = convertTime(l.time_start as string, true); - const end = convertTime(l.time_end as string, true); - const text = l.text; - return `${bom}${idx}\r\n${start} --> ${end}\r\n${text}\r\n`; +function convertToSrtLine(l: Record, idx: number): string { + const bom = idx == 1 ? '\ufeff' : ''; + const start = convertTime(l.time_start as string, true); + const end = convertTime(l.time_end as string, true); + const text = l.text; + return `${bom}${idx}\r\n${start} --> ${end}\r\n${text}\r\n`; } // time parser function convertTime(time: string, srtFormat = false) { - const mTime = time.match(/([\d:]*)\.?(\d*)/); - if (!mTime){ - return srtFormat ? '00:00:00,000' : '0:00:00.00'; - } - return toSubsTime(mTime[0], srtFormat); + const mTime = time.match(/([\d:]*)\.?(\d*)/); + if (!mTime) { + return srtFormat ? '00:00:00,000' : '0:00:00.00'; + } + return toSubsTime(mTime[0], srtFormat); } -function toSubsTime(str: string, srtFormat: boolean) : string { - - const n: string[] = [], x: (string|number)[] = str.split(/[:.]/).map(x => Number(x)); let sx; - - const msLen = srtFormat ? 3 : 2; - const hLen = srtFormat ? 2 : 1; - - x[3] = '0.' + ('' + x[3]).padStart(3, '0'); - sx = (x[0] as number)*60*60 + (x[1] as number)*60 + (x[2] as number) + Number(x[3]); - sx = sx.toFixed(msLen).split('.'); - - - n.unshift(padTimeNum((srtFormat ? ',' : '.'), sx[1], msLen)); - sx = Number(sx[0]); - - n.unshift(padTimeNum(':', sx%60, 2)); - n.unshift(padTimeNum(':', Math.floor(sx/60)%60, 2)); - n.unshift(padTimeNum('', Math.floor(sx/3600)%60, hLen)); - - return n.join(''); +function toSubsTime(str: string, srtFormat: boolean): string { + const n: string[] = [], + x: (string | number)[] = str.split(/[:.]/).map((x) => Number(x)); + let sx; + + const msLen = srtFormat ? 3 : 2; + const hLen = srtFormat ? 2 : 1; + + x[3] = '0.' + ('' + x[3]).padStart(3, '0'); + sx = (x[0] as number) * 60 * 60 + (x[1] as number) * 60 + (x[2] as number) + Number(x[3]); + sx = sx.toFixed(msLen).split('.'); + + n.unshift(padTimeNum(srtFormat ? ',' : '.', sx[1], msLen)); + sx = Number(sx[0]); + + n.unshift(padTimeNum(':', sx % 60, 2)); + n.unshift(padTimeNum(':', Math.floor(sx / 60) % 60, 2)); + n.unshift(padTimeNum('', Math.floor(sx / 3600) % 60, hLen)); + + return n.join(''); } -function padTimeNum(sep: string, input: string|number , pad:number){ - return sep + ('' + input).padStart(pad, '0'); +function padTimeNum(sep: string, input: string | number, pad: number) { + return sep + ('' + input).padStart(pad, '0'); } // export module const _default = (vttStr: string, toSrt: boolean, lang = 'English', fontSize: number, fontName?: string) => { - const convert = toSrt ? convertToSrt : convertToAss; - return convert(vttStr, lang, fontSize, fontName); + const convert = toSrt ? convertToSrt : convertToAss; + return convert(vttStr, lang, fontSize, fontName); }; export default _default; diff --git a/modules/playready/bcert.ts b/modules/playready/bcert.ts index 7f85ab4..da72379 100644 --- a/modules/playready/bcert.ts +++ b/modules/playready/bcert.ts @@ -5,474 +5,446 @@ import ECCKey from './ecc_key'; import { console } from '../log'; function alignUp(length: number, alignment: number): number { - return Math.ceil(length / alignment) * alignment; + return Math.ceil(length / alignment) * alignment; } export class BCertStructs { - static DrmBCertBasicInfo = new Parser() - .buffer('cert_id', { length: 16 }) - .uint32be('security_level') - .uint32be('flags') - .uint32be('cert_type') - .buffer('public_key_digest', { length: 32 }) - .uint32be('expiration_date') - .buffer('client_id', { length: 16 }); + static DrmBCertBasicInfo = new Parser() + .buffer('cert_id', { length: 16 }) + .uint32be('security_level') + .uint32be('flags') + .uint32be('cert_type') + .buffer('public_key_digest', { length: 32 }) + .uint32be('expiration_date') + .buffer('client_id', { length: 16 }); - static DrmBCertDomainInfo = new Parser() - .buffer('service_id', { length: 16 }) - .buffer('account_id', { length: 16 }) - .uint32be('revision_timestamp') - .uint32be('domain_url_length') - .buffer('domain_url', { - length: function () { - return alignUp((this as any).domain_url_length, 4); - }, - }); + static DrmBCertDomainInfo = new Parser() + .buffer('service_id', { length: 16 }) + .buffer('account_id', { length: 16 }) + .uint32be('revision_timestamp') + .uint32be('domain_url_length') + .buffer('domain_url', { + length: function () { + return alignUp((this as any).domain_url_length, 4); + } + }); - static DrmBCertPCInfo = new Parser().uint32be('security_version'); + static DrmBCertPCInfo = new Parser().uint32be('security_version'); - static DrmBCertDeviceInfo = new Parser() - .uint32be('max_license') - .uint32be('max_header') - .uint32be('max_chain_depth'); + static DrmBCertDeviceInfo = new Parser().uint32be('max_license').uint32be('max_header').uint32be('max_chain_depth'); - static DrmBCertFeatureInfo = new Parser() - .uint32be('feature_count') - .array('features', { - type: 'uint32be', - length: 'feature_count', - }); + static DrmBCertFeatureInfo = new Parser().uint32be('feature_count').array('features', { + type: 'uint32be', + length: 'feature_count' + }); - static CertKey = new Parser() - .uint16be('type') - .uint16be('length') - .uint32be('flags') - .buffer('key', { - length: function () { - return (this as any).length / 8; - }, - }) - .uint32be('usages_count') - .array('usages', { - type: 'uint32be', - length: 'usages_count', - }); + static CertKey = new Parser() + .uint16be('type') + .uint16be('length') + .uint32be('flags') + .buffer('key', { + length: function () { + return (this as any).length / 8; + } + }) + .uint32be('usages_count') + .array('usages', { + type: 'uint32be', + length: 'usages_count' + }); - static DrmBCertKeyInfo = new Parser() - .uint32be('key_count') - .array('cert_keys', { - type: BCertStructs.CertKey, - length: 'key_count', - }); + static DrmBCertKeyInfo = new Parser().uint32be('key_count').array('cert_keys', { + type: BCertStructs.CertKey, + length: 'key_count' + }); - static DrmBCertManufacturerInfo = new Parser() - .uint32be('flags') - .uint32be('manufacturer_name_length') - .buffer('manufacturer_name', { - length: function () { - return alignUp((this as any).manufacturer_name_length, 4); - }, - }) - .uint32be('model_name_length') - .buffer('model_name', { - length: function () { - return alignUp((this as any).model_name_length, 4); - }, - }) - .uint32be('model_number_length') - .buffer('model_number', { - length: function () { - return alignUp((this as any).model_number_length, 4); - }, - }); + static DrmBCertManufacturerInfo = new Parser() + .uint32be('flags') + .uint32be('manufacturer_name_length') + .buffer('manufacturer_name', { + length: function () { + return alignUp((this as any).manufacturer_name_length, 4); + } + }) + .uint32be('model_name_length') + .buffer('model_name', { + length: function () { + return alignUp((this as any).model_name_length, 4); + } + }) + .uint32be('model_number_length') + .buffer('model_number', { + length: function () { + return alignUp((this as any).model_number_length, 4); + } + }); - static DrmBCertSignatureInfo = new Parser() - .uint16be('signature_type') - .uint16be('signature_size') - .buffer('signature', { length: 'signature_size' }) - .uint32be('signature_key_size') - .buffer('signature_key', { - length: function () { - return (this as any).signature_key_size / 8; - }, - }); + static DrmBCertSignatureInfo = new Parser() + .uint16be('signature_type') + .uint16be('signature_size') + .buffer('signature', { length: 'signature_size' }) + .uint32be('signature_key_size') + .buffer('signature_key', { + length: function () { + return (this as any).signature_key_size / 8; + } + }); - static DrmBCertSilverlightInfo = new Parser() - .uint32be('security_version') - .uint32be('platform_identifier'); + static DrmBCertSilverlightInfo = new Parser().uint32be('security_version').uint32be('platform_identifier'); - static DrmBCertMeteringInfo = new Parser() - .buffer('metering_id', { length: 16 }) - .uint32be('metering_url_length') - .buffer('metering_url', { - length: function () { - return alignUp((this as any).metering_url_length, 4); - }, - }); + static DrmBCertMeteringInfo = new Parser() + .buffer('metering_id', { length: 16 }) + .uint32be('metering_url_length') + .buffer('metering_url', { + length: function () { + return alignUp((this as any).metering_url_length, 4); + } + }); - static DrmBCertExtDataSignKeyInfo = new Parser() - .uint16be('key_type') - .uint16be('key_length') - .uint32be('flags') - .buffer('key', { - length: function () { - return (this as any).length / 8; - }, - }); + static DrmBCertExtDataSignKeyInfo = new Parser() + .uint16be('key_type') + .uint16be('key_length') + .uint32be('flags') + .buffer('key', { + length: function () { + return (this as any).length / 8; + } + }); - static BCertExtDataRecord = new Parser() - .uint32be('data_size') - .buffer('data', { - length: 'data_size', - }); + static BCertExtDataRecord = new Parser().uint32be('data_size').buffer('data', { + length: 'data_size' + }); - static DrmBCertExtDataSignature = new Parser() - .uint16be('signature_type') - .uint16be('signature_size') - .buffer('signature', { - length: 'signature_size', - }); + static DrmBCertExtDataSignature = new Parser().uint16be('signature_type').uint16be('signature_size').buffer('signature', { + length: 'signature_size' + }); - static BCertExtDataContainer = new Parser() - .uint32be('record_count') - .array('records', { - length: 'record_count', - type: BCertStructs.BCertExtDataRecord, - }) - .nest('signature', { - type: BCertStructs.DrmBCertExtDataSignature, - }); + static BCertExtDataContainer = new Parser() + .uint32be('record_count') + .array('records', { + length: 'record_count', + type: BCertStructs.BCertExtDataRecord + }) + .nest('signature', { + type: BCertStructs.DrmBCertExtDataSignature + }); - static DrmBCertServerInfo = new Parser().uint32be('warning_days'); + static DrmBCertServerInfo = new Parser().uint32be('warning_days'); - static DrmBcertSecurityVersion = new Parser() - .uint32be('security_version') - .uint32be('platform_identifier'); + static DrmBcertSecurityVersion = new Parser().uint32be('security_version').uint32be('platform_identifier'); - static Attribute = new Parser() - .uint16be('flags') - .uint16be('tag') - .uint32be('length') - .choice('attribute', { - tag: 'tag', - choices: { - 1: BCertStructs.DrmBCertBasicInfo, - 2: BCertStructs.DrmBCertDomainInfo, - 3: BCertStructs.DrmBCertPCInfo, - 4: BCertStructs.DrmBCertDeviceInfo, - 5: BCertStructs.DrmBCertFeatureInfo, - 6: BCertStructs.DrmBCertKeyInfo, - 7: BCertStructs.DrmBCertManufacturerInfo, - 8: BCertStructs.DrmBCertSignatureInfo, - 9: BCertStructs.DrmBCertSilverlightInfo, - 10: BCertStructs.DrmBCertMeteringInfo, - 11: BCertStructs.DrmBCertExtDataSignKeyInfo, - 12: BCertStructs.BCertExtDataContainer, - 13: BCertStructs.DrmBCertExtDataSignature, - 14: new Parser().buffer('data', { - length: function () { - return (this as any).length - 8; - }, - }), - 15: BCertStructs.DrmBCertServerInfo, - 16: BCertStructs.DrmBcertSecurityVersion, - 17: BCertStructs.DrmBcertSecurityVersion, - }, - defaultChoice: new Parser().buffer('data', { - length: function () { - return (this as any).length - 8; - }, - }), - }); + static Attribute = new Parser() + .uint16be('flags') + .uint16be('tag') + .uint32be('length') + .choice('attribute', { + tag: 'tag', + choices: { + 1: BCertStructs.DrmBCertBasicInfo, + 2: BCertStructs.DrmBCertDomainInfo, + 3: BCertStructs.DrmBCertPCInfo, + 4: BCertStructs.DrmBCertDeviceInfo, + 5: BCertStructs.DrmBCertFeatureInfo, + 6: BCertStructs.DrmBCertKeyInfo, + 7: BCertStructs.DrmBCertManufacturerInfo, + 8: BCertStructs.DrmBCertSignatureInfo, + 9: BCertStructs.DrmBCertSilverlightInfo, + 10: BCertStructs.DrmBCertMeteringInfo, + 11: BCertStructs.DrmBCertExtDataSignKeyInfo, + 12: BCertStructs.BCertExtDataContainer, + 13: BCertStructs.DrmBCertExtDataSignature, + 14: new Parser().buffer('data', { + length: function () { + return (this as any).length - 8; + } + }), + 15: BCertStructs.DrmBCertServerInfo, + 16: BCertStructs.DrmBcertSecurityVersion, + 17: BCertStructs.DrmBcertSecurityVersion + }, + defaultChoice: new Parser().buffer('data', { + length: function () { + return (this as any).length - 8; + } + }) + }); - static BCert = new Parser() - .string('signature', { length: 4, assert: 'CERT' }) - .int32be('version') - .int32be('total_length') - .int32be('certificate_length') - .array('attributes', { - type: BCertStructs.Attribute, - lengthInBytes: function () { - return (this as any).total_length - 16; - }, - }); + static BCert = new Parser() + .string('signature', { length: 4, assert: 'CERT' }) + .int32be('version') + .int32be('total_length') + .int32be('certificate_length') + .array('attributes', { + type: BCertStructs.Attribute, + lengthInBytes: function () { + return (this as any).total_length - 16; + } + }); - static BCertChain = new Parser() - .string('signature', { length: 4, assert: 'CHAI' }) - .int32be('version') - .int32be('total_length') - .int32be('flags') - .int32be('certificate_count') - .array('certificates', { - type: BCertStructs.BCert, - length: 'certificate_count', - }); + static BCertChain = new Parser() + .string('signature', { length: 4, assert: 'CHAI' }) + .int32be('version') + .int32be('total_length') + .int32be('flags') + .int32be('certificate_count') + .array('certificates', { + type: BCertStructs.BCert, + length: 'certificate_count' + }); } export class Certificate { - parsed: any; - _BCERT: Parser; + parsed: any; + _BCERT: Parser; - constructor(parsed_bcert: any, bcert_obj: Parser = BCertStructs.BCert) { - this.parsed = parsed_bcert; - this._BCERT = bcert_obj; - } + constructor(parsed_bcert: any, bcert_obj: Parser = BCertStructs.BCert) { + this.parsed = parsed_bcert; + this._BCERT = bcert_obj; + } - // UNSTABLE - static new_leaf_cert( - cert_id: Buffer, - security_level: number, - client_id: Buffer, - signing_key: ECCKey, - encryption_key: ECCKey, - group_key: ECCKey, - parent: CertificateChain, - expiry: number = 0xffffffff, - max_license: number = 10240, - max_header: number = 15360, - max_chain_depth: number = 2 - ): Certificate { - const basic_info = { - cert_id: cert_id, - security_level: security_level, - flags: 0, - cert_type: 2, - public_key_digest: signing_key.publicSha256Digest(), - expiration_date: expiry, - client_id: client_id, - }; - const basic_info_attribute = { - flags: 1, - tag: 1, - length: BCertStructs.DrmBCertBasicInfo.encode(basic_info).length + 8, - attribute: basic_info, - }; + // UNSTABLE + static new_leaf_cert( + cert_id: Buffer, + security_level: number, + client_id: Buffer, + signing_key: ECCKey, + encryption_key: ECCKey, + group_key: ECCKey, + parent: CertificateChain, + expiry: number = 0xffffffff, + max_license: number = 10240, + max_header: number = 15360, + max_chain_depth: number = 2 + ): Certificate { + const basic_info = { + cert_id: cert_id, + security_level: security_level, + flags: 0, + cert_type: 2, + public_key_digest: signing_key.publicSha256Digest(), + expiration_date: expiry, + client_id: client_id + }; + const basic_info_attribute = { + flags: 1, + tag: 1, + length: BCertStructs.DrmBCertBasicInfo.encode(basic_info).length + 8, + attribute: basic_info + }; - const device_info = { - max_license: max_license, - max_header: max_header, - max_chain_depth: max_chain_depth, - }; + const device_info = { + max_license: max_license, + max_header: max_header, + max_chain_depth: max_chain_depth + }; - const device_info_attribute = { - flags: 1, - tag: 4, - length: BCertStructs.DrmBCertDeviceInfo.encode(device_info).length + 8, - attribute: device_info, - }; + const device_info_attribute = { + flags: 1, + tag: 4, + length: BCertStructs.DrmBCertDeviceInfo.encode(device_info).length + 8, + attribute: device_info + }; - const feature = { - feature_count: 3, - features: [4, 9, 13], - }; - const feature_attribute = { - flags: 1, - tag: 5, - length: BCertStructs.DrmBCertFeatureInfo.encode(feature).length + 8, - attribute: feature, - }; + const feature = { + feature_count: 3, + features: [4, 9, 13] + }; + const feature_attribute = { + flags: 1, + tag: 5, + length: BCertStructs.DrmBCertFeatureInfo.encode(feature).length + 8, + attribute: feature + }; - const cert_key_sign = { - type: 1, - length: 512, // bits - flags: 0, - key: signing_key.privateBytes(), - usages_count: 1, - usages: [1], - }; - const cert_key_encrypt = { - type: 1, - length: 512, // bits - flags: 0, - key: encryption_key.privateBytes(), - usages_count: 1, - usages: [2], - }; - const key_info = { - key_count: 2, - cert_keys: [cert_key_sign, cert_key_encrypt], - }; - const key_info_attribute = { - flags: 1, - tag: 6, - length: BCertStructs.DrmBCertKeyInfo.encode(key_info).length + 8, - attribute: key_info, - }; + const cert_key_sign = { + type: 1, + length: 512, // bits + flags: 0, + key: signing_key.privateBytes(), + usages_count: 1, + usages: [1] + }; + const cert_key_encrypt = { + type: 1, + length: 512, // bits + flags: 0, + key: encryption_key.privateBytes(), + usages_count: 1, + usages: [2] + }; + const key_info = { + key_count: 2, + cert_keys: [cert_key_sign, cert_key_encrypt] + }; + const key_info_attribute = { + flags: 1, + tag: 6, + length: BCertStructs.DrmBCertKeyInfo.encode(key_info).length + 8, + attribute: key_info + }; - const manufacturer_info = parent.get_certificate(0).get_attribute(7); + const manufacturer_info = parent.get_certificate(0).get_attribute(7); - const new_bcert_container = { - signature: 'CERT', - version: 1, - total_length: 0, - certificate_length: 0, - attributes: [ - basic_info_attribute, - device_info_attribute, - feature_attribute, - key_info_attribute, - manufacturer_info, - ], - }; + const new_bcert_container = { + signature: 'CERT', + version: 1, + total_length: 0, + certificate_length: 0, + attributes: [basic_info_attribute, device_info_attribute, feature_attribute, key_info_attribute, manufacturer_info] + }; - let payload = BCertStructs.BCert.encode(new_bcert_container); - new_bcert_container.certificate_length = payload.length; - new_bcert_container.total_length = payload.length + 144; - payload = BCertStructs.BCert.encode(new_bcert_container); + let payload = BCertStructs.BCert.encode(new_bcert_container); + new_bcert_container.certificate_length = payload.length; + new_bcert_container.total_length = payload.length + 144; + payload = BCertStructs.BCert.encode(new_bcert_container); - const hash = createHash('sha256'); - hash.update(payload); - const digest = hash.digest(); + const hash = createHash('sha256'); + hash.update(payload); + const digest = hash.digest(); - const signatureObj = group_key.keyPair.sign(digest); - const r = Buffer.from(signatureObj.r.toArray('be', 32)); - const s = Buffer.from(signatureObj.s.toArray('be', 32)); - const signature = Buffer.concat([r, s]); + const signatureObj = group_key.keyPair.sign(digest); + const r = Buffer.from(signatureObj.r.toArray('be', 32)); + const s = Buffer.from(signatureObj.s.toArray('be', 32)); + const signature = Buffer.concat([r, s]); - const signature_info = { - signature_type: 1, - signature_size: 64, - signature: signature, - signature_key_size: 512, // bits - signature_key: group_key.publicBytes(), - }; - const signature_info_attribute = { - flags: 1, - tag: 8, - length: - BCertStructs.DrmBCertSignatureInfo.encode(signature_info).length + 8, - attribute: signature_info, - }; - new_bcert_container.attributes.push(signature_info_attribute); + const signature_info = { + signature_type: 1, + signature_size: 64, + signature: signature, + signature_key_size: 512, // bits + signature_key: group_key.publicBytes() + }; + const signature_info_attribute = { + flags: 1, + tag: 8, + length: BCertStructs.DrmBCertSignatureInfo.encode(signature_info).length + 8, + attribute: signature_info + }; + new_bcert_container.attributes.push(signature_info_attribute); - return new Certificate(new_bcert_container); - } + return new Certificate(new_bcert_container); + } - static loads(data: string | Buffer): Certificate { - if (typeof data === 'string') { - data = Buffer.from(data, 'base64'); - } - if (!Buffer.isBuffer(data)) { - throw new Error(`Expecting Bytes or Base64 input, got ${data}`); - } + static loads(data: string | Buffer): Certificate { + if (typeof data === 'string') { + data = Buffer.from(data, 'base64'); + } + if (!Buffer.isBuffer(data)) { + throw new Error(`Expecting Bytes or Base64 input, got ${data}`); + } - const cert = BCertStructs.BCert; - const parsed_bcert = cert.parse(data); - return new Certificate(parsed_bcert, cert); - } + const cert = BCertStructs.BCert; + const parsed_bcert = cert.parse(data); + return new Certificate(parsed_bcert, cert); + } - static load(filePath: string): Certificate { - const data = fs.readFileSync(filePath); - return Certificate.loads(data); - } + static load(filePath: string): Certificate { + const data = fs.readFileSync(filePath); + return Certificate.loads(data); + } - get_attribute(type_: number) { - for (const attribute of this.parsed.attributes) { - if (attribute.tag === type_) { - return attribute; - } - } - } + get_attribute(type_: number) { + for (const attribute of this.parsed.attributes) { + if (attribute.tag === type_) { + return attribute; + } + } + } - get_security_level(): number { - const basic_info_attribute = this.get_attribute(1); - if (basic_info_attribute) { - return basic_info_attribute.attribute.security_level; - } - return 0; - } + get_security_level(): number { + const basic_info_attribute = this.get_attribute(1); + if (basic_info_attribute) { + return basic_info_attribute.attribute.security_level; + } + return 0; + } - private static _unpad(name: Buffer): string { - return name.toString('utf8').replace(/\0+$/, ''); - } + private static _unpad(name: Buffer): string { + return name.toString('utf8').replace(/\0+$/, ''); + } - get_name(): string { - const manufacturer_info_attribute = this.get_attribute(7); - if (manufacturer_info_attribute) { - const manufacturer_info = manufacturer_info_attribute.attribute; - const manufacturer_name = Certificate._unpad( - manufacturer_info.manufacturer_name - ); - const model_name = Certificate._unpad(manufacturer_info.model_name); - const model_number = Certificate._unpad(manufacturer_info.model_number); - return `${manufacturer_name} ${model_name} ${model_number}`; - } - return ''; - } + get_name(): string { + const manufacturer_info_attribute = this.get_attribute(7); + if (manufacturer_info_attribute) { + const manufacturer_info = manufacturer_info_attribute.attribute; + const manufacturer_name = Certificate._unpad(manufacturer_info.manufacturer_name); + const model_name = Certificate._unpad(manufacturer_info.model_name); + const model_number = Certificate._unpad(manufacturer_info.model_number); + return `${manufacturer_name} ${model_name} ${model_number}`; + } + return ''; + } - dumps(): Buffer { - return this._BCERT.encode(this.parsed); - } + dumps(): Buffer { + return this._BCERT.encode(this.parsed); + } - struct(): Parser { - return this._BCERT; - } + struct(): Parser { + return this._BCERT; + } } export class CertificateChain { - parsed: any; - _BCERT_CHAIN: Parser; + parsed: any; + _BCERT_CHAIN: Parser; - constructor( - parsed_bcert_chain: any, - bcert_chain_obj: Parser = BCertStructs.BCertChain - ) { - this.parsed = parsed_bcert_chain; - this._BCERT_CHAIN = bcert_chain_obj; - } + constructor(parsed_bcert_chain: any, bcert_chain_obj: Parser = BCertStructs.BCertChain) { + this.parsed = parsed_bcert_chain; + this._BCERT_CHAIN = bcert_chain_obj; + } - static loads(data: string | Buffer): CertificateChain { - if (typeof data === 'string') { - data = Buffer.from(data, 'base64'); - } - if (!Buffer.isBuffer(data)) { - throw new Error(`Expecting Bytes or Base64 input, got ${data}`); - } + static loads(data: string | Buffer): CertificateChain { + if (typeof data === 'string') { + data = Buffer.from(data, 'base64'); + } + if (!Buffer.isBuffer(data)) { + throw new Error(`Expecting Bytes or Base64 input, got ${data}`); + } - const cert_chain = BCertStructs.BCertChain; - try { - const parsed_bcert_chain = cert_chain.parse(data); - return new CertificateChain(parsed_bcert_chain, cert_chain); - } catch (error) { - console.error('Error during parsing:', error); - throw error; - } - } + const cert_chain = BCertStructs.BCertChain; + try { + const parsed_bcert_chain = cert_chain.parse(data); + return new CertificateChain(parsed_bcert_chain, cert_chain); + } catch (error) { + console.error('Error during parsing:', error); + throw error; + } + } - static load(filePath: string): CertificateChain { - const data = fs.readFileSync(filePath); - return CertificateChain.loads(data); - } + static load(filePath: string): CertificateChain { + const data = fs.readFileSync(filePath); + return CertificateChain.loads(data); + } - dumps(): Buffer { - return this._BCERT_CHAIN.encode(this.parsed); - } + dumps(): Buffer { + return this._BCERT_CHAIN.encode(this.parsed); + } - struct(): Parser { - return this._BCERT_CHAIN; - } + struct(): Parser { + return this._BCERT_CHAIN; + } - get_certificate(index: number): Certificate { - return new Certificate(this.parsed.certificates[index]); - } + get_certificate(index: number): Certificate { + return new Certificate(this.parsed.certificates[index]); + } - get_security_level(): number { - return this.get_certificate(0).get_security_level(); - } + get_security_level(): number { + return this.get_certificate(0).get_security_level(); + } - get_name(): string { - return this.get_certificate(0).get_name(); - } + get_name(): string { + return this.get_certificate(0).get_name(); + } - append(bcert: Certificate): void { - this.parsed.certificate_count += 1; - this.parsed.certificates.push(bcert.parsed); - this.parsed.total_length += bcert.dumps().length; - } + append(bcert: Certificate): void { + this.parsed.certificate_count += 1; + this.parsed.certificates.push(bcert.parsed); + this.parsed.total_length += bcert.dumps().length; + } - prepend(bcert: Certificate): void { - this.parsed.certificate_count += 1; - this.parsed.certificates.unshift(bcert.parsed); - this.parsed.total_length += bcert.dumps().length; - } + prepend(bcert: Certificate): void { + this.parsed.certificate_count += 1; + this.parsed.certificates.unshift(bcert.parsed); + this.parsed.total_length += bcert.dumps().length; + } } diff --git a/modules/playready/cdm.ts b/modules/playready/cdm.ts index a819d35..9bea0b8 100644 --- a/modules/playready/cdm.ts +++ b/modules/playready/cdm.ts @@ -12,267 +12,217 @@ import { Device } from './device'; import { XMLParser } from 'fast-xml-parser'; export default class Cdm { - security_level: number; - certificate_chain: CertificateChain; - encryption_key: ECCKey; - signing_key: ECCKey; - client_version: string; - la_version: number; + security_level: number; + certificate_chain: CertificateChain; + encryption_key: ECCKey; + signing_key: ECCKey; + client_version: string; + la_version: number; - curve: elliptic.ec; - elgamal: ElGamal; + curve: elliptic.ec; + elgamal: ElGamal; - private wmrm_key: elliptic.ec.KeyPair; - private xml_key: XmlKey; + private wmrm_key: elliptic.ec.KeyPair; + private xml_key: XmlKey; - constructor( - security_level: number, - certificate_chain: CertificateChain, - encryption_key: ECCKey, - signing_key: ECCKey, - client_version: string = '2.4.117.27', - la_version: number = 1 - ) { - this.security_level = security_level; - this.certificate_chain = certificate_chain; - this.encryption_key = encryption_key; - this.signing_key = signing_key; - this.client_version = client_version; - this.la_version = la_version; + constructor( + security_level: number, + certificate_chain: CertificateChain, + encryption_key: ECCKey, + signing_key: ECCKey, + client_version: string = '2.4.117.27', + la_version: number = 1 + ) { + this.security_level = security_level; + this.certificate_chain = certificate_chain; + this.encryption_key = encryption_key; + this.signing_key = signing_key; + this.client_version = client_version; + this.la_version = la_version; - this.curve = new elliptic.ec('p256'); - this.elgamal = new ElGamal(this.curve); + this.curve = new elliptic.ec('p256'); + this.elgamal = new ElGamal(this.curve); - const x = - 'c8b6af16ee941aadaa5389b4af2c10e356be42af175ef3face93254e7b0b3d9b'; - const y = - '982b27b5cb2341326e56aa857dbfd5c634ce2cf9ea74fca8f2af5957efeea562'; - this.wmrm_key = this.curve.keyFromPublic({ x, y }, 'hex'); - this.xml_key = new XmlKey(); - } + const x = 'c8b6af16ee941aadaa5389b4af2c10e356be42af175ef3face93254e7b0b3d9b'; + const y = '982b27b5cb2341326e56aa857dbfd5c634ce2cf9ea74fca8f2af5957efeea562'; + this.wmrm_key = this.curve.keyFromPublic({ x, y }, 'hex'); + this.xml_key = new XmlKey(); + } - static fromDevice(device: Device): Cdm { - return new Cdm( - device.security_level, - device.group_certificate, - device.encryption_key, - device.signing_key - ); - } + static fromDevice(device: Device): Cdm { + return new Cdm(device.security_level, device.group_certificate, device.encryption_key, device.signing_key); + } - private getKeyData(): Buffer { - const messagePoint = this.xml_key.getPoint(this.elgamal.curve); - const [point1, point2] = this.elgamal.encrypt( - messagePoint, - this.wmrm_key.getPublic() as Point - ); + private getKeyData(): Buffer { + const messagePoint = this.xml_key.getPoint(this.elgamal.curve); + const [point1, point2] = this.elgamal.encrypt(messagePoint, this.wmrm_key.getPublic() as Point); - const bufferArray = Buffer.concat([ - ElGamal.toBytes(point1.getX()), - ElGamal.toBytes(point1.getY()), - ElGamal.toBytes(point2.getX()), - ElGamal.toBytes(point2.getY()), - ]); + const bufferArray = Buffer.concat([ElGamal.toBytes(point1.getX()), ElGamal.toBytes(point1.getY()), ElGamal.toBytes(point2.getX()), ElGamal.toBytes(point2.getY())]); - return bufferArray; - } + return bufferArray; + } - private getCipherData(): Buffer { - const b64_chain = this.certificate_chain.dumps().toString('base64'); - const body = `<Data><CertificateChains><CertificateChain>${b64_chain}</CertificateChain></CertificateChains><Features><Feature Name="AESCBC"></Feature></Features></Data>`; + private getCipherData(): Buffer { + const b64_chain = this.certificate_chain.dumps().toString('base64'); + const body = `<Data><CertificateChains><CertificateChain>${b64_chain}</CertificateChain></CertificateChains><Features><Feature Name="AESCBC"></Feature></Features></Data>`; - const cipher = crypto.createCipheriv( - 'aes-128-cbc', - this.xml_key.aesKey, - this.xml_key.aesIv - ); + const cipher = crypto.createCipheriv('aes-128-cbc', this.xml_key.aesKey, this.xml_key.aesIv); - const ciphertext = Buffer.concat([ - cipher.update(Buffer.from(body, 'utf-8')), - cipher.final(), - ]); + const ciphertext = Buffer.concat([cipher.update(Buffer.from(body, 'utf-8')), cipher.final()]); - return Buffer.concat([this.xml_key.aesIv, ciphertext]); - } + return Buffer.concat([this.xml_key.aesIv, ciphertext]); + } - private buildDigestContent( - content_header: string, - nonce: string, - wmrm_cipher: string, - cert_cipher: string - ): string { - const clientTime = Math.floor(Date.now() / 1000); + private buildDigestContent(content_header: string, nonce: string, wmrm_cipher: string, cert_cipher: string): string { + const clientTime = Math.floor(Date.now() / 1000); - return ( - '<LA xmlns="http://schemas.microsoft.com/DRM/2007/03/protocols" Id="SignedData" xml:space="preserve">' + - '<Version>4</Version>' + - `<ContentHeader>${content_header}</ContentHeader>` + - '<CLIENTINFO>' + - `<CLIENTVERSION>${this.client_version}</CLIENTVERSION>` + - '</CLIENTINFO>' + - `<LicenseNonce>${nonce}</LicenseNonce>` + - `<ClientTime>${clientTime}</ClientTime>` + - '<EncryptedData xmlns="http://www.w3.org/2001/04/xmlenc#" Type="http://www.w3.org/2001/04/xmlenc#Element">' + - '<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"></EncryptionMethod>' + - '<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">' + - '<EncryptedKey xmlns="http://www.w3.org/2001/04/xmlenc#">' + - '<EncryptionMethod Algorithm="http://schemas.microsoft.com/DRM/2007/03/protocols#ecc256"></EncryptionMethod>' + - '<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">' + - '<KeyName>WMRMServer</KeyName>' + - '</KeyInfo>' + - '<CipherData>' + - `<CipherValue>${wmrm_cipher}</CipherValue>` + - '</CipherData>' + - '</EncryptedKey>' + - '</KeyInfo>' + - '<CipherData>' + - `<CipherValue>${cert_cipher}</CipherValue>` + - '</CipherData>' + - '</EncryptedData>' + - '</LA>' - ); - } + return ( + '<LA xmlns="http://schemas.microsoft.com/DRM/2007/03/protocols" Id="SignedData" xml:space="preserve">' + + '<Version>4</Version>' + + `<ContentHeader>${content_header}</ContentHeader>` + + '<CLIENTINFO>' + + `<CLIENTVERSION>${this.client_version}</CLIENTVERSION>` + + '</CLIENTINFO>' + + `<LicenseNonce>${nonce}</LicenseNonce>` + + `<ClientTime>${clientTime}</ClientTime>` + + '<EncryptedData xmlns="http://www.w3.org/2001/04/xmlenc#" Type="http://www.w3.org/2001/04/xmlenc#Element">' + + '<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"></EncryptionMethod>' + + '<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">' + + '<EncryptedKey xmlns="http://www.w3.org/2001/04/xmlenc#">' + + '<EncryptionMethod Algorithm="http://schemas.microsoft.com/DRM/2007/03/protocols#ecc256"></EncryptionMethod>' + + '<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">' + + '<KeyName>WMRMServer</KeyName>' + + '</KeyInfo>' + + '<CipherData>' + + `<CipherValue>${wmrm_cipher}</CipherValue>` + + '</CipherData>' + + '</EncryptedKey>' + + '</KeyInfo>' + + '<CipherData>' + + `<CipherValue>${cert_cipher}</CipherValue>` + + '</CipherData>' + + '</EncryptedData>' + + '</LA>' + ); + } - private static buildSignedInfo(digest_value: string): string { - return ( - '<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">' + - '<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></CanonicalizationMethod>' + - '<SignatureMethod Algorithm="http://schemas.microsoft.com/DRM/2007/03/protocols#ecdsa-sha256"></SignatureMethod>' + - '<Reference URI="#SignedData">' + - '<DigestMethod Algorithm="http://schemas.microsoft.com/DRM/2007/03/protocols#sha256"></DigestMethod>' + - `<DigestValue>${digest_value}</DigestValue>` + - '</Reference>' + - '</SignedInfo>' - ); - } + private static buildSignedInfo(digest_value: string): string { + return ( + '<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">' + + '<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></CanonicalizationMethod>' + + '<SignatureMethod Algorithm="http://schemas.microsoft.com/DRM/2007/03/protocols#ecdsa-sha256"></SignatureMethod>' + + '<Reference URI="#SignedData">' + + '<DigestMethod Algorithm="http://schemas.microsoft.com/DRM/2007/03/protocols#sha256"></DigestMethod>' + + `<DigestValue>${digest_value}</DigestValue>` + + '</Reference>' + + '</SignedInfo>' + ); + } - getLicenseChallenge(content_header: string): string { - const nonce = randomBytes(16).toString('base64'); - const wmrm_cipher = this.getKeyData().toString('base64'); - const cert_cipher = this.getCipherData().toString('base64'); + getLicenseChallenge(content_header: string): string { + const nonce = randomBytes(16).toString('base64'); + const wmrm_cipher = this.getKeyData().toString('base64'); + const cert_cipher = this.getCipherData().toString('base64'); - const la_content = this.buildDigestContent( - content_header, - nonce, - wmrm_cipher, - cert_cipher - ); + const la_content = this.buildDigestContent(content_header, nonce, wmrm_cipher, cert_cipher); - const la_hash = createHash('sha256').update(la_content, 'utf-8').digest(); + const la_hash = createHash('sha256').update(la_content, 'utf-8').digest(); - const signed_info = Cdm.buildSignedInfo(la_hash.toString('base64')); - const signed_info_digest = createHash('sha256') - .update(signed_info, 'utf-8') - .digest(); + const signed_info = Cdm.buildSignedInfo(la_hash.toString('base64')); + const signed_info_digest = createHash('sha256').update(signed_info, 'utf-8').digest(); - const signatureObj = this.signing_key.keyPair.sign(signed_info_digest); + const signatureObj = this.signing_key.keyPair.sign(signed_info_digest); - const r = signatureObj.r.toArrayLike(Buffer, 'be', 32); - const s = signatureObj.s.toArrayLike(Buffer, 'be', 32); + const r = signatureObj.r.toArrayLike(Buffer, 'be', 32); + const s = signatureObj.s.toArrayLike(Buffer, 'be', 32); - const rawSignature = Buffer.concat([r, s]); - const signatureValue = rawSignature.toString('base64'); + const rawSignature = Buffer.concat([r, s]); + const signatureValue = rawSignature.toString('base64'); - const publicKeyBytes = this.signing_key.keyPair - .getPublic() - .encode('array', false); - const publicKeyBuffer = Buffer.from(publicKeyBytes); - const publicKeyBase64 = publicKeyBuffer.toString('base64'); + const publicKeyBytes = this.signing_key.keyPair.getPublic().encode('array', false); + const publicKeyBuffer = Buffer.from(publicKeyBytes); + const publicKeyBase64 = publicKeyBuffer.toString('base64'); - const main_body = - '<?xml version="1.0" encoding="utf-8"?>' + - '<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' + - 'xmlns:xsd="http://www.w3.org/2001/XMLSchema" ' + - 'xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">' + - '<soap:Body>' + - '<AcquireLicense xmlns="http://schemas.microsoft.com/DRM/2007/03/protocols">' + - '<challenge>' + - '<Challenge xmlns="http://schemas.microsoft.com/DRM/2007/03/protocols/messages">' + - la_content + - '<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">' + - signed_info + - `<SignatureValue>${signatureValue}</SignatureValue>` + - '<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">' + - '<KeyValue>' + - '<ECCKeyValue>' + - `<PublicKey>${publicKeyBase64}</PublicKey>` + - '</ECCKeyValue>' + - '</KeyValue>' + - '</KeyInfo>' + - '</Signature>' + - '</Challenge>' + - '</challenge>' + - '</AcquireLicense>' + - '</soap:Body>' + - '</soap:Envelope>'; + const main_body = + '<?xml version="1.0" encoding="utf-8"?>' + + '<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' + + 'xmlns:xsd="http://www.w3.org/2001/XMLSchema" ' + + 'xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">' + + '<soap:Body>' + + '<AcquireLicense xmlns="http://schemas.microsoft.com/DRM/2007/03/protocols">' + + '<challenge>' + + '<Challenge xmlns="http://schemas.microsoft.com/DRM/2007/03/protocols/messages">' + + la_content + + '<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">' + + signed_info + + `<SignatureValue>${signatureValue}</SignatureValue>` + + '<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">' + + '<KeyValue>' + + '<ECCKeyValue>' + + `<PublicKey>${publicKeyBase64}</PublicKey>` + + '</ECCKeyValue>' + + '</KeyValue>' + + '</KeyInfo>' + + '</Signature>' + + '</Challenge>' + + '</challenge>' + + '</AcquireLicense>' + + '</soap:Body>' + + '</soap:Envelope>'; - return main_body; - } + return main_body; + } - private decryptEcc256Key(encrypted_key: Buffer): Buffer { - const point1 = this.curve.curve.point( - encrypted_key.subarray(0, 32).toString('hex'), - encrypted_key.subarray(32, 64).toString('hex') - ); - const point2 = this.curve.curve.point( - encrypted_key.subarray(64, 96).toString('hex'), - encrypted_key.subarray(96, 128).toString('hex') - ); + private decryptEcc256Key(encrypted_key: Buffer): Buffer { + const point1 = this.curve.curve.point(encrypted_key.subarray(0, 32).toString('hex'), encrypted_key.subarray(32, 64).toString('hex')); + const point2 = this.curve.curve.point(encrypted_key.subarray(64, 96).toString('hex'), encrypted_key.subarray(96, 128).toString('hex')); - const decrypted = ElGamal.decrypt( - [point1, point2], - this.encryption_key.keyPair.getPrivate() - ); - const decryptedBytes = decrypted.getX().toArray('be', 32).slice(16, 32); + const decrypted = ElGamal.decrypt([point1, point2], this.encryption_key.keyPair.getPrivate()); + const decryptedBytes = decrypted.getX().toArray('be', 32).slice(16, 32); - return Buffer.from(decryptedBytes); - } + return Buffer.from(decryptedBytes); + } - parseLicense(license: string | Buffer): { - key_id: string; - key_type: number; - cipher_type: number; - key_length: number; - key: string; - }[] { - try { - const parser = new XMLParser({ - removeNSPrefix: true, - }); - const result = parser.parse(license); + parseLicense(license: string | Buffer): { + key_id: string; + key_type: number; + cipher_type: number; + key_length: number; + key: string; + }[] { + try { + const parser = new XMLParser({ + removeNSPrefix: true + }); + const result = parser.parse(license); - let licenses = - result['Envelope']['Body']['AcquireLicenseResponse'][ - 'AcquireLicenseResult' - ]['Response']['LicenseResponse']['Licenses']['License']; + let licenses = result['Envelope']['Body']['AcquireLicenseResponse']['AcquireLicenseResult']['Response']['LicenseResponse']['Licenses']['License']; - if (!Array.isArray(licenses)) { - licenses = [licenses]; - } + if (!Array.isArray(licenses)) { + licenses = [licenses]; + } - const keys = []; + const keys = []; - for (const licenseElement of licenses) { - const keyMaterial = XmrUtil.parse(Buffer.from(licenseElement, 'base64')) - .license.license.keyMaterial; + for (const licenseElement of licenses) { + const keyMaterial = XmrUtil.parse(Buffer.from(licenseElement, 'base64')).license.license.keyMaterial; - if (!keyMaterial || !keyMaterial.contentKey) - throw new Error('No Content Keys retrieved'); + if (!keyMaterial || !keyMaterial.contentKey) throw new Error('No Content Keys retrieved'); - keys.push( - new Key( - keyMaterial.contentKey.kid, - keyMaterial.contentKey.keyType, - keyMaterial.contentKey.ciphertype, - keyMaterial.contentKey.length, - this.decryptEcc256Key(keyMaterial.contentKey.value) - ) - ); - } + keys.push( + new Key( + keyMaterial.contentKey.kid, + keyMaterial.contentKey.keyType, + keyMaterial.contentKey.ciphertype, + keyMaterial.contentKey.length, + this.decryptEcc256Key(keyMaterial.contentKey.value) + ) + ); + } - return keys; - } catch (error) { - throw new Error(`Unable to parse license, ${error}`); - } - } + return keys; + } catch (error) { + throw new Error(`Unable to parse license, ${error}`); + } + } } diff --git a/modules/playready/device.ts b/modules/playready/device.ts index 5baa792..662cebf 100644 --- a/modules/playready/device.ts +++ b/modules/playready/device.ts @@ -4,98 +4,94 @@ import ECCKey from './ecc_key'; import * as fs from 'fs'; type RawDeviceV2 = { - signature: string; - version: number; - group_certificate_length: number; - group_certificate: Buffer; - encryption_key: Buffer; - signing_key: Buffer; + signature: string; + version: number; + group_certificate_length: number; + group_certificate: Buffer; + encryption_key: Buffer; + signing_key: Buffer; }; class DeviceStructs { - static magic = 'PRD'; + static magic = 'PRD'; - static v1 = new Parser() - .string('signature', { length: 3, assert: DeviceStructs.magic }) - .uint8('version') - .uint32('group_key_length') - .buffer('group_key', { length: 'group_key_length' }) - .uint32('group_certificate_length') - .buffer('group_certificate', { length: 'group_certificate_length' }); + static v1 = new Parser() + .string('signature', { length: 3, assert: DeviceStructs.magic }) + .uint8('version') + .uint32('group_key_length') + .buffer('group_key', { length: 'group_key_length' }) + .uint32('group_certificate_length') + .buffer('group_certificate', { length: 'group_certificate_length' }); - static v2 = new Parser() - .string('signature', { length: 3, assert: DeviceStructs.magic }) - .uint8('version') - .uint32('group_certificate_length') - .buffer('group_certificate', { length: 'group_certificate_length' }) - .buffer('encryption_key', { length: 96 }) - .buffer('signing_key', { length: 96 }); + static v2 = new Parser() + .string('signature', { length: 3, assert: DeviceStructs.magic }) + .uint8('version') + .uint32('group_certificate_length') + .buffer('group_certificate', { length: 'group_certificate_length' }) + .buffer('encryption_key', { length: 96 }) + .buffer('signing_key', { length: 96 }); - static v3 = new Parser() - .string('signature', { length: 3, assert: DeviceStructs.magic }) - .uint8('version') - .buffer('group_key', { length: 96 }) - .buffer('encryption_key', { length: 96 }) - .buffer('signing_key', { length: 96 }) - .uint32('group_certificate_length') - .buffer('group_certificate', { length: 'group_certificate_length' }); + static v3 = new Parser() + .string('signature', { length: 3, assert: DeviceStructs.magic }) + .uint8('version') + .buffer('group_key', { length: 96 }) + .buffer('encryption_key', { length: 96 }) + .buffer('signing_key', { length: 96 }) + .uint32('group_certificate_length') + .buffer('group_certificate', { length: 'group_certificate_length' }); } export class Device { - static CURRENT_STRUCT = DeviceStructs.v3; + static CURRENT_STRUCT = DeviceStructs.v3; - group_certificate: CertificateChain; - encryption_key: ECCKey; - signing_key: ECCKey; - security_level: number; + group_certificate: CertificateChain; + encryption_key: ECCKey; + signing_key: ECCKey; + security_level: number; - constructor(parsedData: RawDeviceV2) { - this.group_certificate = CertificateChain.loads( - parsedData.group_certificate - ); - this.encryption_key = ECCKey.loads(parsedData.encryption_key); - this.signing_key = ECCKey.loads(parsedData.signing_key); - this.security_level = this.group_certificate.get_security_level(); - } + constructor(parsedData: RawDeviceV2) { + this.group_certificate = CertificateChain.loads(parsedData.group_certificate); + this.encryption_key = ECCKey.loads(parsedData.encryption_key); + this.signing_key = ECCKey.loads(parsedData.signing_key); + this.security_level = this.group_certificate.get_security_level(); + } - static loads(data: Buffer): Device { - const parsedData = Device.CURRENT_STRUCT.parse(data); - return new Device(parsedData); - } + static loads(data: Buffer): Device { + const parsedData = Device.CURRENT_STRUCT.parse(data); + return new Device(parsedData); + } - static load(filePath: string): Device { - const data = fs.readFileSync(filePath); - return Device.loads(data); - } + static load(filePath: string): Device { + const data = fs.readFileSync(filePath); + return Device.loads(data); + } - dumps(): Buffer { - const groupCertBytes = this.group_certificate.dumps(); - const encryptionKeyBytes = this.encryption_key.dumps(); - const signingKeyBytes = this.signing_key.dumps(); + dumps(): Buffer { + const groupCertBytes = this.group_certificate.dumps(); + const encryptionKeyBytes = this.encryption_key.dumps(); + const signingKeyBytes = this.signing_key.dumps(); - const buildData = { - signature: DeviceStructs.magic, - version: 2, - group_certificate_length: groupCertBytes.length, - group_certificate: groupCertBytes, - encryption_key: encryptionKeyBytes, - signing_key: signingKeyBytes, - }; + const buildData = { + signature: DeviceStructs.magic, + version: 2, + group_certificate_length: groupCertBytes.length, + group_certificate: groupCertBytes, + encryption_key: encryptionKeyBytes, + signing_key: signingKeyBytes + }; - return Device.CURRENT_STRUCT.encode(buildData); - } + return Device.CURRENT_STRUCT.encode(buildData); + } - dump(filePath: string): void { - const data = this.dumps(); - fs.writeFileSync(filePath, data); - } + dump(filePath: string): void { + const data = this.dumps(); + fs.writeFileSync(filePath, data); + } - get_name(): string { - const name = `${this.group_certificate.get_name()}_sl${ - this.security_level - }`; - return name.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase(); - } + get_name(): string { + const name = `${this.group_certificate.get_name()}_sl${this.security_level}`; + return name.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase(); + } } // Device V2 disabled because unstable provisioning diff --git a/modules/playready/ecc_key.ts b/modules/playready/ecc_key.ts index 6ae9a8d..7b25fea 100644 --- a/modules/playready/ecc_key.ts +++ b/modules/playready/ecc_key.ts @@ -3,91 +3,89 @@ import { createHash } from 'crypto'; import * as fs from 'fs'; export default class ECCKey { - keyPair: elliptic.ec.KeyPair; + keyPair: elliptic.ec.KeyPair; - constructor(keyPair: elliptic.ec.KeyPair) { - this.keyPair = keyPair; - } + constructor(keyPair: elliptic.ec.KeyPair) { + this.keyPair = keyPair; + } - static generate(): ECCKey { - const EC = new elliptic.ec('p256'); - const keyPair = EC.genKeyPair(); - return new ECCKey(keyPair); - } + static generate(): ECCKey { + const EC = new elliptic.ec('p256'); + const keyPair = EC.genKeyPair(); + return new ECCKey(keyPair); + } - static construct(privateKey: Buffer | string | number): ECCKey { - if (Buffer.isBuffer(privateKey)) { - privateKey = privateKey.toString('hex'); - } else if (typeof privateKey === 'number') { - privateKey = privateKey.toString(16); - } + static construct(privateKey: Buffer | string | number): ECCKey { + if (Buffer.isBuffer(privateKey)) { + privateKey = privateKey.toString('hex'); + } else if (typeof privateKey === 'number') { + privateKey = privateKey.toString(16); + } - const EC = new elliptic.ec('p256'); - const keyPair = EC.keyFromPrivate(privateKey, 'hex'); + const EC = new elliptic.ec('p256'); + const keyPair = EC.keyFromPrivate(privateKey, 'hex'); - return new ECCKey(keyPair); - } + return new ECCKey(keyPair); + } - static loads(data: string | Buffer): ECCKey { - if (typeof data === 'string') { - data = Buffer.from(data, 'base64'); - } - if (!Buffer.isBuffer(data)) { - throw new Error(`Expecting Bytes or Base64 input, got ${data}`); - } + static loads(data: string | Buffer): ECCKey { + if (typeof data === 'string') { + data = Buffer.from(data, 'base64'); + } + if (!Buffer.isBuffer(data)) { + throw new Error(`Expecting Bytes or Base64 input, got ${data}`); + } - if (data.length !== 96 && data.length !== 32) { - throw new Error( - `Invalid data length. Expecting 96 or 32 bytes, got ${data.length}` - ); - } + if (data.length !== 96 && data.length !== 32) { + throw new Error(`Invalid data length. Expecting 96 or 32 bytes, got ${data.length}`); + } - const privateKey = data.subarray(0, 32); - return ECCKey.construct(privateKey); - } + const privateKey = data.subarray(0, 32); + return ECCKey.construct(privateKey); + } - static load(filePath: string): ECCKey { - const data = fs.readFileSync(filePath); - return ECCKey.loads(data); - } + static load(filePath: string): ECCKey { + const data = fs.readFileSync(filePath); + return ECCKey.loads(data); + } - dumps(): Buffer { - return Buffer.concat([this.privateBytes(), this.publicBytes()]); - } + dumps(): Buffer { + return Buffer.concat([this.privateBytes(), this.publicBytes()]); + } - dump(filePath: string): void { - fs.writeFileSync(filePath, this.dumps()); - } + dump(filePath: string): void { + fs.writeFileSync(filePath, this.dumps()); + } - getPoint(): { x: string; y: string } { - const publicKey = this.keyPair.getPublic(); - return { - x: publicKey.getX().toString('hex'), - y: publicKey.getY().toString('hex'), - }; - } + getPoint(): { x: string; y: string } { + const publicKey = this.keyPair.getPublic(); + return { + x: publicKey.getX().toString('hex'), + y: publicKey.getY().toString('hex') + }; + } - privateBytes(): Buffer { - const privateKey = this.keyPair.getPrivate(); - return Buffer.from(privateKey.toArray('be', 32)); - } + privateBytes(): Buffer { + const privateKey = this.keyPair.getPrivate(); + return Buffer.from(privateKey.toArray('be', 32)); + } - privateSha256Digest(): Buffer { - const hash = createHash('sha256'); - hash.update(this.privateBytes()); - return hash.digest(); - } + privateSha256Digest(): Buffer { + const hash = createHash('sha256'); + hash.update(this.privateBytes()); + return hash.digest(); + } - publicBytes(): Buffer { - const publicKey = this.keyPair.getPublic(); - const x = publicKey.getX().toArray('be', 32); - const y = publicKey.getY().toArray('be', 32); - return Buffer.concat([Buffer.from(x), Buffer.from(y)]); - } + publicBytes(): Buffer { + const publicKey = this.keyPair.getPublic(); + const x = publicKey.getX().toArray('be', 32); + const y = publicKey.getY().toArray('be', 32); + return Buffer.concat([Buffer.from(x), Buffer.from(y)]); + } - publicSha256Digest(): Buffer { - const hash = createHash('sha256'); - hash.update(this.publicBytes()); - return hash.digest(); - } + publicSha256Digest(): Buffer { + const hash = createHash('sha256'); + hash.update(this.publicBytes()); + return hash.digest(); + } } diff --git a/modules/playready/elgamal.ts b/modules/playready/elgamal.ts index b20c875..d967e7c 100644 --- a/modules/playready/elgamal.ts +++ b/modules/playready/elgamal.ts @@ -3,43 +3,41 @@ import { randomBytes } from 'crypto'; import BN from 'bn.js'; export interface Point { - getY(): BN; - getX(): BN; - add(point: Point): Point; - mul(n: BN | bigint | number): Point; - neg(): Point; + getY(): BN; + getX(): BN; + add(point: Point): Point; + mul(n: BN | bigint | number): Point; + neg(): Point; } export default class ElGamal { - curve: EC; + curve: EC; - constructor(curve: EC) { - this.curve = curve; - } + constructor(curve: EC) { + this.curve = curve; + } - static toBytes(n: BN): Uint8Array { - const byteArray = n.toString(16).padStart(2, '0'); - if (byteArray.length % 2 !== 0) { - return Uint8Array.from(Buffer.from('0' + byteArray, 'hex')); - } - return Uint8Array.from(Buffer.from(byteArray, 'hex')); - } + static toBytes(n: BN): Uint8Array { + const byteArray = n.toString(16).padStart(2, '0'); + if (byteArray.length % 2 !== 0) { + return Uint8Array.from(Buffer.from('0' + byteArray, 'hex')); + } + return Uint8Array.from(Buffer.from(byteArray, 'hex')); + } - encrypt(messagePoint: Point, publicKey: Point): [Point, Point] { - const ephemeralKey = new BN(randomBytes(32).toString('hex'), 16).mod( - this.curve.n! - ); - const ephemeralKeyBigInt = BigInt(ephemeralKey.toString(10)); - const point1 = this.curve.g.mul(ephemeralKeyBigInt); - const point2 = messagePoint.add(publicKey.mul(ephemeralKeyBigInt)); + encrypt(messagePoint: Point, publicKey: Point): [Point, Point] { + const ephemeralKey = new BN(randomBytes(32).toString('hex'), 16).mod(this.curve.n!); + const ephemeralKeyBigInt = BigInt(ephemeralKey.toString(10)); + const point1 = this.curve.g.mul(ephemeralKeyBigInt); + const point2 = messagePoint.add(publicKey.mul(ephemeralKeyBigInt)); - return [point1, point2]; - } + return [point1, point2]; + } - static decrypt(encrypted: [Point, Point], privateKey: BN): Point { - const [point1, point2] = encrypted; - const sharedSecret = point1.mul(privateKey); - const decryptedMessage = point2.add(sharedSecret.neg()); - return decryptedMessage; - } + static decrypt(encrypted: [Point, Point], privateKey: BN): Point { + const [point1, point2] = encrypted; + const sharedSecret = point1.mul(privateKey); + const decryptedMessage = point2.add(sharedSecret.neg()); + return decryptedMessage; + } } diff --git a/modules/playready/key.ts b/modules/playready/key.ts index 7e5ae71..c7dd38d 100644 --- a/modules/playready/key.ts +++ b/modules/playready/key.ts @@ -1,69 +1,63 @@ enum KeyType { - Invalid = 0x0000, - AES128CTR = 0x0001, - RC4 = 0x0002, - AES128ECB = 0x0003, - Cocktail = 0x0004, - AESCBC = 0x0005, - UNKNOWN = 0xffff, + Invalid = 0x0000, + AES128CTR = 0x0001, + RC4 = 0x0002, + AES128ECB = 0x0003, + Cocktail = 0x0004, + AESCBC = 0x0005, + UNKNOWN = 0xffff } function getKeyType(value: number): KeyType { - switch (value) { - case KeyType.Invalid: - case KeyType.AES128CTR: - case KeyType.RC4: - case KeyType.AES128ECB: - case KeyType.Cocktail: - case KeyType.AESCBC: - return value; - default: - return KeyType.UNKNOWN; - } + switch (value) { + case KeyType.Invalid: + case KeyType.AES128CTR: + case KeyType.RC4: + case KeyType.AES128ECB: + case KeyType.Cocktail: + case KeyType.AESCBC: + return value; + default: + return KeyType.UNKNOWN; + } } enum CipherType { - Invalid = 0x0000, - RSA128 = 0x0001, - ChainedLicense = 0x0002, - ECC256 = 0x0003, - ECCforScalableLicenses = 0x0004, - Scalable = 0x0005, - UNKNOWN = 0xffff, + Invalid = 0x0000, + RSA128 = 0x0001, + ChainedLicense = 0x0002, + ECC256 = 0x0003, + ECCforScalableLicenses = 0x0004, + Scalable = 0x0005, + UNKNOWN = 0xffff } function getCipherType(value: number): CipherType { - switch (value) { - case CipherType.Invalid: - case CipherType.RSA128: - case CipherType.ChainedLicense: - case CipherType.ECC256: - case CipherType.ECCforScalableLicenses: - case CipherType.Scalable: - return value; - default: - return CipherType.UNKNOWN; - } + switch (value) { + case CipherType.Invalid: + case CipherType.RSA128: + case CipherType.ChainedLicense: + case CipherType.ECC256: + case CipherType.ECCforScalableLicenses: + case CipherType.Scalable: + return value; + default: + return CipherType.UNKNOWN; + } } export class Key { - key_id: string; - key_type: KeyType; - cipher_type: CipherType; - key_length: number; - key: string; + key_id: string; + key_type: KeyType; + cipher_type: CipherType; + key_length: number; + key: string; - constructor( - key_id: string, - key_type: number, - cipher_type: number, - key_length: number, - key: Buffer - ) { - this.key_id = key_id; - this.key_type = getKeyType(key_type); - this.cipher_type = getCipherType(cipher_type); - this.key_length = key_length; - this.key = key.toString('hex'); - } + constructor(key_id: string, key_type: number, cipher_type: number, key_length: number, key: Buffer) { + this.key_id = key_id; + this.key_type = getKeyType(key_type); + this.cipher_type = getCipherType(cipher_type); + this.key_length = key_length; + this.key = key.toString('hex'); + } } diff --git a/modules/playready/pssh.ts b/modules/playready/pssh.ts index 5843046..8b0436c 100644 --- a/modules/playready/pssh.ts +++ b/modules/playready/pssh.ts @@ -5,125 +5,118 @@ import WRMHeader from './wrmheader'; const SYSTEM_ID = Buffer.from('9a04f07998404286ab92e65be0885f95', 'hex'); const PSSHBox = new Parser() - .uint32('length') - .string('pssh', { length: 4, assert: 'pssh' }) - .uint32('fullbox') - .buffer('system_id', { length: 16 }) - .uint32('data_length') - .buffer('data', { - length: 'data_length', - }); + .uint32('length') + .string('pssh', { length: 4, assert: 'pssh' }) + .uint32('fullbox') + .buffer('system_id', { length: 16 }) + .uint32('data_length') + .buffer('data', { + length: 'data_length' + }); const PlayreadyObject = new Parser() - .useContextVars() - .uint16('type') - .uint16('length') - .choice('data', { - tag: 'type', - choices: { - 1: new Parser().string('data', { - length: function () { - return (this as any).$parent.length; - }, - encoding: 'utf16le', - }), - }, - defaultChoice: new Parser().buffer('data', { - length: function () { - return (this as any).$parent.length; - }, - }), - }); + .useContextVars() + .uint16('type') + .uint16('length') + .choice('data', { + tag: 'type', + choices: { + 1: new Parser().string('data', { + length: function () { + return (this as any).$parent.length; + }, + encoding: 'utf16le' + }) + }, + defaultChoice: new Parser().buffer('data', { + length: function () { + return (this as any).$parent.length; + } + }) + }); -const PlayreadyHeader = new Parser() - .uint32('length') - .uint16('record_count') - .array('records', { - length: 'record_count', - type: PlayreadyObject, - }); +const PlayreadyHeader = new Parser().uint32('length').uint16('record_count').array('records', { + length: 'record_count', + type: PlayreadyObject +}); function isPlayreadyPsshBox(data: Buffer): boolean { - if (data.length < 28) return false; - return data.subarray(12, 28).equals(SYSTEM_ID); + if (data.length < 28) return false; + return data.subarray(12, 28).equals(SYSTEM_ID); } function isUtf16(data: Buffer): boolean { - for (let i = 1; i < data.length; i += 2) { - if (data[i] !== 0) { - return false; - } - } - return true; + for (let i = 1; i < data.length; i += 2) { + if (data[i] !== 0) { + return false; + } + } + return true; } function* getWrmHeaders(wrm_header: any): IterableIterator<string> { - for (const record of wrm_header.records) { - if (record.type === 1 && typeof record.data === 'string') { - yield record.data; - } - } + for (const record of wrm_header.records) { + if (record.type === 1 && typeof record.data === 'string') { + yield record.data; + } + } } export class PSSH { - public wrm_headers: string[]; + public wrm_headers: string[]; - constructor(data: string | Buffer) { - if (!data) { - throw new Error('Data must not be empty'); - } + constructor(data: string | Buffer) { + if (!data) { + throw new Error('Data must not be empty'); + } - if (typeof data === 'string') { - try { - data = Buffer.from(data, 'base64'); - } catch (e) { - throw new Error(`Could not decode data as Base64: ${e}`); - } - } + if (typeof data === 'string') { + try { + data = Buffer.from(data, 'base64'); + } catch (e) { + throw new Error(`Could not decode data as Base64: ${e}`); + } + } - try { - if (isPlayreadyPsshBox(data)) { - const pssh_box = PSSHBox.parse(data); - const psshData = pssh_box.data; + try { + if (isPlayreadyPsshBox(data)) { + const pssh_box = PSSHBox.parse(data); + const psshData = pssh_box.data; - if (isUtf16(psshData)) { - this.wrm_headers = [psshData.toString('utf16le')]; - } else if (isUtf16(psshData.subarray(6))) { - this.wrm_headers = [psshData.subarray(6).toString('utf16le')]; - } else if (isUtf16(psshData.subarray(10))) { - this.wrm_headers = [psshData.subarray(10).toString('utf16le')]; - } else { - const playready_header = PlayreadyHeader.parse(psshData); - this.wrm_headers = Array.from(getWrmHeaders(playready_header)); - } - } else { - if (isUtf16(data)) { - this.wrm_headers = [data.toString('utf16le')]; - } else if (isUtf16(data.subarray(6))) { - this.wrm_headers = [data.subarray(6).toString('utf16le')]; - } else if (isUtf16(data.subarray(10))) { - this.wrm_headers = [data.subarray(10).toString('utf16le')]; - } else { - const playready_header = PlayreadyHeader.parse(data); - this.wrm_headers = Array.from(getWrmHeaders(playready_header)); - } - } - } catch (e) { - throw new Error( - 'Could not parse data as a PSSH Box nor a PlayReadyHeader' - ); - } - } + if (isUtf16(psshData)) { + this.wrm_headers = [psshData.toString('utf16le')]; + } else if (isUtf16(psshData.subarray(6))) { + this.wrm_headers = [psshData.subarray(6).toString('utf16le')]; + } else if (isUtf16(psshData.subarray(10))) { + this.wrm_headers = [psshData.subarray(10).toString('utf16le')]; + } else { + const playready_header = PlayreadyHeader.parse(psshData); + this.wrm_headers = Array.from(getWrmHeaders(playready_header)); + } + } else { + if (isUtf16(data)) { + this.wrm_headers = [data.toString('utf16le')]; + } else if (isUtf16(data.subarray(6))) { + this.wrm_headers = [data.subarray(6).toString('utf16le')]; + } else if (isUtf16(data.subarray(10))) { + this.wrm_headers = [data.subarray(10).toString('utf16le')]; + } else { + const playready_header = PlayreadyHeader.parse(data); + this.wrm_headers = Array.from(getWrmHeaders(playready_header)); + } + } + } catch (e) { + throw new Error('Could not parse data as a PSSH Box nor a PlayReadyHeader'); + } + } - // Header downgrade - public get_wrm_headers(downgrade_to_v4: boolean = false): string[] { - return this.wrm_headers.map( - downgrade_to_v4 ? this.downgradePSSH : (_) => _ - ); - } + // Header downgrade + public get_wrm_headers(downgrade_to_v4: boolean = false): string[] { + return this.wrm_headers.map(downgrade_to_v4 ? this.downgradePSSH : (_) => _); + } - private downgradePSSH(wrm_header: string): string { - const header = new WRMHeader(wrm_header); - return header.to_v4_0_0_0(); - } + private downgradePSSH(wrm_header: string): string { + const header = new WRMHeader(wrm_header); + return header.to_v4_0_0_0(); + } } diff --git a/modules/playready/wrmheader.ts b/modules/playready/wrmheader.ts index e8d0102..072f8e2 100644 --- a/modules/playready/wrmheader.ts +++ b/modules/playready/wrmheader.ts @@ -1,112 +1,88 @@ import { XMLParser } from 'fast-xml-parser'; export class SignedKeyID { - constructor( - public alg_id: string, - public value: string, - public checksum?: string - ) {} + constructor( + public alg_id: string, + public value: string, + public checksum?: string + ) {} } export type Version = '4.0.0.0' | '4.1.0.0' | '4.2.0.0' | '4.3.0.0' | 'UNKNOWN'; -export type ReturnStructure = [ - SignedKeyID[], - string | null, - string | null, - string | null -]; +export type ReturnStructure = [SignedKeyID[], string | null, string | null, string | null]; interface ParsedWRMHeader { - WRMHEADER: { - '@_version': string; - DATA?: any; - }; + WRMHEADER: { + '@_version': string; + DATA?: any; + }; } export default class WRMHeader { - private header: ParsedWRMHeader['WRMHEADER']; - version: Version; + private header: ParsedWRMHeader['WRMHEADER']; + version: Version; - constructor(data: string) { - if (!data) throw new Error('Data must not be empty'); + constructor(data: string) { + if (!data) throw new Error('Data must not be empty'); - const parser = new XMLParser({ - ignoreAttributes: false, - removeNSPrefix: true, - attributeNamePrefix: '@_', - }); - const parsed = parser.parse(data) as ParsedWRMHeader; + const parser = new XMLParser({ + ignoreAttributes: false, + removeNSPrefix: true, + attributeNamePrefix: '@_' + }); + const parsed = parser.parse(data) as ParsedWRMHeader; - if (!parsed.WRMHEADER) throw new Error('Data is not a valid WRMHEADER'); + if (!parsed.WRMHEADER) throw new Error('Data is not a valid WRMHEADER'); - this.header = parsed.WRMHEADER; - this.version = WRMHeader.fromString(this.header['@_version']); - } + this.header = parsed.WRMHEADER; + this.version = WRMHeader.fromString(this.header['@_version']); + } - private static fromString(value: string): Version { - if (['4.0.0.0', '4.1.0.0', '4.2.0.0', '4.3.0.0'].includes(value)) { - return value as Version; - } - return 'UNKNOWN'; - } + private static fromString(value: string): Version { + if (['4.0.0.0', '4.1.0.0', '4.2.0.0', '4.3.0.0'].includes(value)) { + return value as Version; + } + return 'UNKNOWN'; + } - to_v4_0_0_0(): string { - const [key_ids, la_url, lui_url, ds_id] = this.readAttributes(); - if (key_ids.length === 0) throw new Error('No Key IDs available'); - const key_id = key_ids[0]; - return `<WRMHEADER xmlns="http://schemas.microsoft.com/DRM/2007/03/PlayReadyHeader" version="4.0.0.0"><DATA><PROTECTINFO><KEYLEN>16</KEYLEN><ALGID>AESCTR</ALGID></PROTECTINFO><KID>${ - key_id.value - }</KID>${la_url ? `<LA_URL>${la_url}</LA_URL>` : ''}${ - lui_url ? `<LUI_URL>${lui_url}</LUI_URL>` : '' - }${ds_id ? `<DS_ID>${ds_id}</DS_ID>` : ''}${ - key_id.checksum ? `<CHECKSUM>${key_id.checksum}</CHECKSUM>` : '' - }</DATA></WRMHEADER>`; - } + to_v4_0_0_0(): string { + const [key_ids, la_url, lui_url, ds_id] = this.readAttributes(); + if (key_ids.length === 0) throw new Error('No Key IDs available'); + const key_id = key_ids[0]; + return `<WRMHEADER xmlns="http://schemas.microsoft.com/DRM/2007/03/PlayReadyHeader" version="4.0.0.0"><DATA><PROTECTINFO><KEYLEN>16</KEYLEN><ALGID>AESCTR</ALGID></PROTECTINFO><KID>${ + key_id.value + }</KID>${la_url ? `<LA_URL>${la_url}</LA_URL>` : ''}${lui_url ? `<LUI_URL>${lui_url}</LUI_URL>` : ''}${ds_id ? `<DS_ID>${ds_id}</DS_ID>` : ''}${ + key_id.checksum ? `<CHECKSUM>${key_id.checksum}</CHECKSUM>` : '' + }</DATA></WRMHEADER>`; + } - readAttributes(): ReturnStructure { - const data = this.header.DATA; - if (!data) - throw new Error( - 'Not a valid PlayReady Header Record, WRMHEADER/DATA required' - ); - switch (this.version) { - case '4.0.0.0': - return WRMHeader.read_v4(data); - case '4.1.0.0': - case '4.2.0.0': - case '4.3.0.0': - return WRMHeader.read_vX(data); - default: - throw new Error(`Unsupported version: ${this.version}`); - } - } + readAttributes(): ReturnStructure { + const data = this.header.DATA; + if (!data) throw new Error('Not a valid PlayReady Header Record, WRMHEADER/DATA required'); + switch (this.version) { + case '4.0.0.0': + return WRMHeader.read_v4(data); + case '4.1.0.0': + case '4.2.0.0': + case '4.3.0.0': + return WRMHeader.read_vX(data); + default: + throw new Error(`Unsupported version: ${this.version}`); + } + } - private static read_v4(data: any): ReturnStructure { - const protectInfo = data.PROTECTINFO; - return [ - [new SignedKeyID(protectInfo.ALGID, data.KID, data.CHECKSUM)], - data.LA_URL || null, - data.LUI_URL || null, - data.DS_ID || null, - ]; - } + private static read_v4(data: any): ReturnStructure { + const protectInfo = data.PROTECTINFO; + return [[new SignedKeyID(protectInfo.ALGID, data.KID, data.CHECKSUM)], data.LA_URL || null, data.LUI_URL || null, data.DS_ID || null]; + } - private static read_vX(data: any): ReturnStructure { - const protectInfo = data.PROTECTINFO; + private static read_vX(data: any): ReturnStructure { + const protectInfo = data.PROTECTINFO; - const signedKeyID: SignedKeyID | undefined = protectInfo.KIDS.KID - ? new SignedKeyID( - protectInfo.KIDS.KID['@_ALGID'] || '', - protectInfo.KIDS.KID['@_VALUE'], - protectInfo.KIDS.KID['@_CHECKSUM'] - ) - : undefined; - return [ - signedKeyID ? [signedKeyID] : [], - data.LA_URL || null, - data.LUI_URL || null, - data.DS_ID || null, - ]; - } + const signedKeyID: SignedKeyID | undefined = protectInfo.KIDS.KID + ? new SignedKeyID(protectInfo.KIDS.KID['@_ALGID'] || '', protectInfo.KIDS.KID['@_VALUE'], protectInfo.KIDS.KID['@_CHECKSUM']) + : undefined; + return [signedKeyID ? [signedKeyID] : [], data.LA_URL || null, data.LUI_URL || null, data.DS_ID || null]; + } } diff --git a/modules/playready/xml_key.ts b/modules/playready/xml_key.ts index eacbf6c..9ab7784 100644 --- a/modules/playready/xml_key.ts +++ b/modules/playready/xml_key.ts @@ -4,25 +4,25 @@ import ECCKey from './ecc_key'; import ElGamal, { Point } from './elgamal'; export default class XmlKey { - private _sharedPoint: ECCKey; - public sharedKeyX: BN; - public sharedKeyY: BN; - public _shared_key_x_bytes: Uint8Array; - public aesIv: Uint8Array; - public aesKey: Uint8Array; + private _sharedPoint: ECCKey; + public sharedKeyX: BN; + public sharedKeyY: BN; + public _shared_key_x_bytes: Uint8Array; + public aesIv: Uint8Array; + public aesKey: Uint8Array; - constructor() { - this._sharedPoint = ECCKey.generate(); - this.sharedKeyX = this._sharedPoint.keyPair.getPublic().getX(); - this.sharedKeyY = this._sharedPoint.keyPair.getPublic().getY(); - this._shared_key_x_bytes = ElGamal.toBytes(this.sharedKeyX); - this.aesIv = this._shared_key_x_bytes.subarray(0, 16); - this.aesKey = this._shared_key_x_bytes.subarray(16, 32); - } + constructor() { + this._sharedPoint = ECCKey.generate(); + this.sharedKeyX = this._sharedPoint.keyPair.getPublic().getX(); + this.sharedKeyY = this._sharedPoint.keyPair.getPublic().getY(); + this._shared_key_x_bytes = ElGamal.toBytes(this.sharedKeyX); + this.aesIv = this._shared_key_x_bytes.subarray(0, 16); + this.aesKey = this._shared_key_x_bytes.subarray(16, 32); + } - getPoint(curve: EC): Point { - return curve.curve.point(this.sharedKeyX, this.sharedKeyY); - } + getPoint(curve: EC): Point { + return curve.curve.point(this.sharedKeyX, this.sharedKeyY); + } } // Make it more undetectable (not working right now) diff --git a/modules/playready/xmrlicense.ts b/modules/playready/xmrlicense.ts index 960d154..b20180b 100644 --- a/modules/playready/xmrlicense.ts +++ b/modules/playready/xmrlicense.ts @@ -1,251 +1,228 @@ import { Parser } from 'binary-parser'; type ParsedLicense = { - version: number; - rights: string; - length: number; - license: { - length: number; - signature?: { - length: number; - type: string; - value: string; - }; - global_container?: { - revocationInfo?: { - version: number; - }; - securityLevel?: { - level: number; - }; - }; - keyMaterial?: { - contentKey?: { - kid: string; - keyType: number; - ciphertype: number; - length: number; - value: Buffer; - }; - encryptionKey?: { - curve: number; - length: number; - value: string; - }; - auxKeys?: { - count: number; - value: { - location: number; - value: string; - }; - }; - }; - }; + version: number; + rights: string; + length: number; + license: { + length: number; + signature?: { + length: number; + type: string; + value: string; + }; + global_container?: { + revocationInfo?: { + version: number; + }; + securityLevel?: { + level: number; + }; + }; + keyMaterial?: { + contentKey?: { + kid: string; + keyType: number; + ciphertype: number; + length: number; + value: Buffer; + }; + encryptionKey?: { + curve: number; + length: number; + value: string; + }; + auxKeys?: { + count: number; + value: { + location: number; + value: string; + }; + }; + }; + }; }; export class XMRLicenseStructsV2 { - static CONTENT_KEY = new Parser() - .buffer('kid', { length: 16 }) - .uint16('keytype') - .uint16('ciphertype') - .uint16('length') - .buffer('value', { - length: 'length', - }); + static CONTENT_KEY = new Parser().buffer('kid', { length: 16 }).uint16('keytype').uint16('ciphertype').uint16('length').buffer('value', { + length: 'length' + }); - static ECC_KEY = new Parser() - .uint16('curve') - .uint16('length') - .buffer('value', { - length: 'length', - }); + static ECC_KEY = new Parser().uint16('curve').uint16('length').buffer('value', { + length: 'length' + }); - static FTLV = new Parser() - .uint16('flags') - .uint16('type') - .uint32('length') - .buffer('value', { - length: function () { - return (this as any).length - 8; - }, - }); + static FTLV = new Parser() + .uint16('flags') + .uint16('type') + .uint32('length') + .buffer('value', { + length: function () { + return (this as any).length - 8; + } + }); - static AUXILIARY_LOCATIONS = new Parser() - .uint32('location') - .buffer('value', { length: 16 }); + static AUXILIARY_LOCATIONS = new Parser().uint32('location').buffer('value', { length: 16 }); - static AUXILIARY_KEY_OBJECT = new Parser() - .uint16('count') - .array('locations', { - length: 'count', - type: XMRLicenseStructsV2.AUXILIARY_LOCATIONS, - }); + static AUXILIARY_KEY_OBJECT = new Parser().uint16('count').array('locations', { + length: 'count', + type: XMRLicenseStructsV2.AUXILIARY_LOCATIONS + }); - static SIGNATURE = new Parser() - .uint16('type') - .uint16('siglength') - .buffer('signature', { - length: 'siglength', - }); + static SIGNATURE = new Parser().uint16('type').uint16('siglength').buffer('signature', { + length: 'siglength' + }); - static XMR = new Parser() - .string('constant', { length: 4, assert: 'XMR\x00' }) - .int32('version') - .buffer('rightsid', { length: 16 }) - .nest('data', { - type: XMRLicenseStructsV2.FTLV, - }); + static XMR = new Parser().string('constant', { length: 4, assert: 'XMR\x00' }).int32('version').buffer('rightsid', { length: 16 }).nest('data', { + type: XMRLicenseStructsV2.FTLV + }); } enum XMRTYPE { - XMR_OUTER_CONTAINER = 0x0001, - XMR_GLOBAL_POLICY_CONTAINER = 0x0002, - XMR_PLAYBACK_POLICY_CONTAINER = 0x0004, - XMR_KEY_MATERIAL_CONTAINER = 0x0009, - XMR_RIGHTS_SETTINGS = 0x000d, - XMR_EMBEDDED_LICENSE_SETTINGS = 0x0033, - XMR_REVOCATION_INFORMATION_VERSION = 0x0032, - XMR_SECURITY_LEVEL = 0x0034, - XMR_CONTENT_KEY_OBJECT = 0x000a, - XMR_ECC_KEY_OBJECT = 0x002a, - XMR_SIGNATURE_OBJECT = 0x000b, - XMR_OUTPUT_LEVEL_RESTRICTION = 0x0005, - XMR_AUXILIARY_KEY_OBJECT = 0x0051, - XMR_EXPIRATION_RESTRICTION = 0x0012, - XMR_ISSUE_DATE = 0x0013, - XMR_EXPLICIT_ANALOG_CONTAINER = 0x0007, + XMR_OUTER_CONTAINER = 0x0001, + XMR_GLOBAL_POLICY_CONTAINER = 0x0002, + XMR_PLAYBACK_POLICY_CONTAINER = 0x0004, + XMR_KEY_MATERIAL_CONTAINER = 0x0009, + XMR_RIGHTS_SETTINGS = 0x000d, + XMR_EMBEDDED_LICENSE_SETTINGS = 0x0033, + XMR_REVOCATION_INFORMATION_VERSION = 0x0032, + XMR_SECURITY_LEVEL = 0x0034, + XMR_CONTENT_KEY_OBJECT = 0x000a, + XMR_ECC_KEY_OBJECT = 0x002a, + XMR_SIGNATURE_OBJECT = 0x000b, + XMR_OUTPUT_LEVEL_RESTRICTION = 0x0005, + XMR_AUXILIARY_KEY_OBJECT = 0x0051, + XMR_EXPIRATION_RESTRICTION = 0x0012, + XMR_ISSUE_DATE = 0x0013, + XMR_EXPLICIT_ANALOG_CONTAINER = 0x0007 } export class XmrUtil { - public data: Buffer; - public license: ParsedLicense; + public data: Buffer; + public license: ParsedLicense; - constructor(data: Buffer, license: ParsedLicense) { - this.data = data; - this.license = license; - } + constructor(data: Buffer, license: ParsedLicense) { + this.data = data; + this.license = license; + } - static parse(license: Buffer) { - const xmr = XMRLicenseStructsV2.XMR.parse(license); + static parse(license: Buffer) { + const xmr = XMRLicenseStructsV2.XMR.parse(license); - const parsed_license: ParsedLicense = { - version: xmr.version, - rights: Buffer.from(xmr.rightsid).toString('hex'), - length: license.length, - license: { - length: xmr.data.length, - }, - }; - const container = parsed_license.license; - const data = xmr.data; + const parsed_license: ParsedLicense = { + version: xmr.version, + rights: Buffer.from(xmr.rightsid).toString('hex'), + length: license.length, + license: { + length: xmr.data.length + } + }; + const container = parsed_license.license; + const data = xmr.data; - let pos = 0; - while (pos < data.length - 16) { - const value = XMRLicenseStructsV2.FTLV.parse(data.value.slice(pos)); + let pos = 0; + while (pos < data.length - 16) { + const value = XMRLicenseStructsV2.FTLV.parse(data.value.slice(pos)); - // XMR_SIGNATURE_OBJECT - if (value.type === XMRTYPE.XMR_SIGNATURE_OBJECT) { - const signature = XMRLicenseStructsV2.SIGNATURE.parse(value.value); + // XMR_SIGNATURE_OBJECT + if (value.type === XMRTYPE.XMR_SIGNATURE_OBJECT) { + const signature = XMRLicenseStructsV2.SIGNATURE.parse(value.value); - container.signature = { - length: value.length, - type: signature.type, - value: Buffer.from(signature.signature).toString('hex'), - }; - } + container.signature = { + length: value.length, + type: signature.type, + value: Buffer.from(signature.signature).toString('hex') + }; + } - // XMRTYPE.XMR_GLOBAL_POLICY_CONTAINER - if (value.type === XMRTYPE.XMR_GLOBAL_POLICY_CONTAINER) { - container.global_container = {}; + // XMRTYPE.XMR_GLOBAL_POLICY_CONTAINER + if (value.type === XMRTYPE.XMR_GLOBAL_POLICY_CONTAINER) { + container.global_container = {}; - let index = 0; - while (index < value.length - 16) { - const data = XMRLicenseStructsV2.FTLV.parse(value.value.slice(index)); + let index = 0; + while (index < value.length - 16) { + const data = XMRLicenseStructsV2.FTLV.parse(value.value.slice(index)); - // XMRTYPE.XMR_REVOCATION_INFORMATION_VERSION - if (data.type === XMRTYPE.XMR_REVOCATION_INFORMATION_VERSION) { - container.global_container.revocationInfo = { - version: data.value.readUInt32BE(0), - }; - } + // XMRTYPE.XMR_REVOCATION_INFORMATION_VERSION + if (data.type === XMRTYPE.XMR_REVOCATION_INFORMATION_VERSION) { + container.global_container.revocationInfo = { + version: data.value.readUInt32BE(0) + }; + } - // XMRTYPE.XMR_SECURITY_LEVEL - if (data.type === XMRTYPE.XMR_SECURITY_LEVEL) { - container.global_container.securityLevel = { - level: data.value.readUInt16BE(0), - }; - } + // XMRTYPE.XMR_SECURITY_LEVEL + if (data.type === XMRTYPE.XMR_SECURITY_LEVEL) { + container.global_container.securityLevel = { + level: data.value.readUInt16BE(0) + }; + } - index += data.length; - } - } + index += data.length; + } + } - // XMRTYPE.XMR_KEY_MATERIAL_CONTAINER - if (value.type === XMRTYPE.XMR_KEY_MATERIAL_CONTAINER) { - container.keyMaterial = {}; + // XMRTYPE.XMR_KEY_MATERIAL_CONTAINER + if (value.type === XMRTYPE.XMR_KEY_MATERIAL_CONTAINER) { + container.keyMaterial = {}; - let index = 0; - while (index < value.length - 16) { - const data = XMRLicenseStructsV2.FTLV.parse(value.value.slice(index)); + let index = 0; + while (index < value.length - 16) { + const data = XMRLicenseStructsV2.FTLV.parse(value.value.slice(index)); - // XMRTYPE.XMR_CONTENT_KEY_OBJECT - if (data.type === XMRTYPE.XMR_CONTENT_KEY_OBJECT) { - const content_key = XMRLicenseStructsV2.CONTENT_KEY.parse( - data.value - ); + // XMRTYPE.XMR_CONTENT_KEY_OBJECT + if (data.type === XMRTYPE.XMR_CONTENT_KEY_OBJECT) { + const content_key = XMRLicenseStructsV2.CONTENT_KEY.parse(data.value); - container.keyMaterial.contentKey = { - kid: XmrUtil.fixUUID(content_key.kid).toString('hex'), - keyType: content_key.keytype, - ciphertype: content_key.ciphertype, - length: content_key.length, - value: content_key.value, - }; - } + container.keyMaterial.contentKey = { + kid: XmrUtil.fixUUID(content_key.kid).toString('hex'), + keyType: content_key.keytype, + ciphertype: content_key.ciphertype, + length: content_key.length, + value: content_key.value + }; + } - // XMRTYPE.XMR_ECC_KEY_OBJECT - if (data.type === XMRTYPE.XMR_ECC_KEY_OBJECT) { - const ecc_key = XMRLicenseStructsV2.ECC_KEY.parse(data.value); + // XMRTYPE.XMR_ECC_KEY_OBJECT + if (data.type === XMRTYPE.XMR_ECC_KEY_OBJECT) { + const ecc_key = XMRLicenseStructsV2.ECC_KEY.parse(data.value); - container.keyMaterial.encryptionKey = { - curve: ecc_key.curve, - length: ecc_key.length, - value: Buffer.from(ecc_key.value).toString('hex'), - }; - } + container.keyMaterial.encryptionKey = { + curve: ecc_key.curve, + length: ecc_key.length, + value: Buffer.from(ecc_key.value).toString('hex') + }; + } - // XMRTYPE.XMR_AUXILIARY_KEY_OBJECT - if (data.type === XMRTYPE.XMR_AUXILIARY_KEY_OBJECT) { - const aux_keys = XMRLicenseStructsV2.AUXILIARY_KEY_OBJECT.parse( - data.value - ); + // XMRTYPE.XMR_AUXILIARY_KEY_OBJECT + if (data.type === XMRTYPE.XMR_AUXILIARY_KEY_OBJECT) { + const aux_keys = XMRLicenseStructsV2.AUXILIARY_KEY_OBJECT.parse(data.value); - container.keyMaterial.auxKeys = { - count: aux_keys.count, - value: aux_keys.locations.map((a: any) => { - return { - location: a.location, - value: Buffer.from(a.value).toString('hex'), - }; - }), - }; - } - index += data.length; - } - } + container.keyMaterial.auxKeys = { + count: aux_keys.count, + value: aux_keys.locations.map((a: any) => { + return { + location: a.location, + value: Buffer.from(a.value).toString('hex') + }; + }) + }; + } + index += data.length; + } + } - pos += value.length; - } + pos += value.length; + } - return new XmrUtil(license, parsed_license); - } + return new XmrUtil(license, parsed_license); + } - static fixUUID(data: Buffer): Buffer { - return Buffer.concat([ - Buffer.from(data.subarray(0, 4).reverse()), - Buffer.from(data.subarray(4, 6).reverse()), - Buffer.from(data.subarray(6, 8).reverse()), - data.subarray(8, 16), - ]); - } + static fixUUID(data: Buffer): Buffer { + return Buffer.concat([ + Buffer.from(data.subarray(0, 4).reverse()), + Buffer.from(data.subarray(4, 6).reverse()), + Buffer.from(data.subarray(6, 8).reverse()), + data.subarray(8, 16) + ]); + } } diff --git a/modules/widevine/cmac.ts b/modules/widevine/cmac.ts index 95c949d..9fcfcbc 100644 --- a/modules/widevine/cmac.ts +++ b/modules/widevine/cmac.ts @@ -2,112 +2,112 @@ import crypto from 'crypto'; export class AES_CMAC { - private readonly BLOCK_SIZE = 16; - private readonly XOR_RIGHT = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87]); - private readonly EMPTY_BLOCK_SIZE_BUFFER = Buffer.alloc(this.BLOCK_SIZE) as Buffer; + private readonly BLOCK_SIZE = 16; + private readonly XOR_RIGHT = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87]); + private readonly EMPTY_BLOCK_SIZE_BUFFER = Buffer.alloc(this.BLOCK_SIZE) as Buffer; - private _key: Buffer; - private _subkeys: { first: Buffer; second: Buffer }; + private _key: Buffer; + private _subkeys: { first: Buffer; second: Buffer }; - public constructor(key: Buffer) { - if (![16, 24, 32].includes(key.length)) { - throw new Error('Key size must be 128, 192, or 256 bits.'); - } - this._key = key; - this._subkeys = this._generateSubkeys(); - } + public constructor(key: Buffer) { + if (![16, 24, 32].includes(key.length)) { + throw new Error('Key size must be 128, 192, or 256 bits.'); + } + this._key = key; + this._subkeys = this._generateSubkeys(); + } - public calculate(message: Buffer): Buffer { - const blockCount = this._getBlockCount(message); + public calculate(message: Buffer): Buffer { + const blockCount = this._getBlockCount(message); - let x = this.EMPTY_BLOCK_SIZE_BUFFER; - let y; + let x = this.EMPTY_BLOCK_SIZE_BUFFER; + let y; - for (let i = 0; i < blockCount - 1; i++) { - const from = i * this.BLOCK_SIZE; - const block = message.subarray(from, from + this.BLOCK_SIZE); - y = this._xor(x, block); - x = this._aes(y); - } + for (let i = 0; i < blockCount - 1; i++) { + const from = i * this.BLOCK_SIZE; + const block = message.subarray(from, from + this.BLOCK_SIZE); + y = this._xor(x, block); + x = this._aes(y); + } - y = this._xor(x, this._getLastBlock(message)); - x = this._aes(y); + y = this._xor(x, this._getLastBlock(message)); + x = this._aes(y); - return x; - } + return x; + } - private _generateSubkeys(): { first: Buffer; second: Buffer } { - const l = this._aes(this.EMPTY_BLOCK_SIZE_BUFFER); + private _generateSubkeys(): { first: Buffer; second: Buffer } { + const l = this._aes(this.EMPTY_BLOCK_SIZE_BUFFER); - let first = this._bitShiftLeft(l); - if (l[0] & 0x80) { - first = this._xor(first, this.XOR_RIGHT); - } + let first = this._bitShiftLeft(l); + if (l[0] & 0x80) { + first = this._xor(first, this.XOR_RIGHT); + } - let second = this._bitShiftLeft(first); - if (first[0] & 0x80) { - second = this._xor(second, this.XOR_RIGHT); - } + let second = this._bitShiftLeft(first); + if (first[0] & 0x80) { + second = this._xor(second, this.XOR_RIGHT); + } - return { first: first, second: second }; - } + return { first: first, second: second }; + } - private _getBlockCount(message: Buffer): number { - const blockCount = Math.ceil(message.length / this.BLOCK_SIZE); - return blockCount === 0 ? 1 : blockCount; - } + private _getBlockCount(message: Buffer): number { + const blockCount = Math.ceil(message.length / this.BLOCK_SIZE); + return blockCount === 0 ? 1 : blockCount; + } - private _aes(message: Buffer): Buffer { - const cipher = crypto.createCipheriv(`aes-${this._key.length * 8}-cbc`, this._key, Buffer.alloc(this.BLOCK_SIZE)); - const result = cipher.update(message).subarray(0, 16); - cipher.destroy(); - return result; - } + private _aes(message: Buffer): Buffer { + const cipher = crypto.createCipheriv(`aes-${this._key.length * 8}-cbc`, this._key, Buffer.alloc(this.BLOCK_SIZE)); + const result = cipher.update(message).subarray(0, 16); + cipher.destroy(); + return result; + } - private _getLastBlock(message: Buffer): Buffer { - const blockCount = this._getBlockCount(message); - const paddedBlock = this._padding(message, blockCount - 1); + private _getLastBlock(message: Buffer): Buffer { + const blockCount = this._getBlockCount(message); + const paddedBlock = this._padding(message, blockCount - 1); - let complete = false; - if (message.length > 0) { - complete = message.length % this.BLOCK_SIZE === 0; - } + let complete = false; + if (message.length > 0) { + complete = message.length % this.BLOCK_SIZE === 0; + } - const key = complete ? this._subkeys.first : this._subkeys.second; - return this._xor(paddedBlock, key); - } + const key = complete ? this._subkeys.first : this._subkeys.second; + return this._xor(paddedBlock, key); + } - private _padding(message: Buffer, blockIndex: number): Buffer { - const block = Buffer.alloc(this.BLOCK_SIZE); + private _padding(message: Buffer, blockIndex: number): Buffer { + const block = Buffer.alloc(this.BLOCK_SIZE); - const from = blockIndex * this.BLOCK_SIZE; + const from = blockIndex * this.BLOCK_SIZE; - const slice = message.subarray(from, from + this.BLOCK_SIZE); - block.set(slice); + const slice = message.subarray(from, from + this.BLOCK_SIZE); + block.set(slice); - if (slice.length !== this.BLOCK_SIZE) { - block[slice.length] = 0x80; - } + if (slice.length !== this.BLOCK_SIZE) { + block[slice.length] = 0x80; + } - return block; - } + return block; + } - private _bitShiftLeft(input: Buffer): Buffer { - const output = Buffer.alloc(input.length); - let overflow = 0; - for (let i = input.length - 1; i >= 0; i--) { - output[i] = (input[i] << 1) | overflow; - overflow = input[i] & 0x80 ? 1 : 0; - } - return output; - } + private _bitShiftLeft(input: Buffer): Buffer { + const output = Buffer.alloc(input.length); + let overflow = 0; + for (let i = input.length - 1; i >= 0; i--) { + output[i] = (input[i] << 1) | overflow; + overflow = input[i] & 0x80 ? 1 : 0; + } + return output; + } - private _xor(a: Buffer, b: Buffer): Buffer { - const length = Math.min(a.length, b.length); - const output = Buffer.alloc(length); - for (let i = 0; i < length; i++) { - output[i] = a[i] ^ b[i]; - } - return output; - } + private _xor(a: Buffer, b: Buffer): Buffer { + const length = Math.min(a.length, b.length); + const output = Buffer.alloc(length); + for (let i = 0; i < length; i++) { + output[i] = a[i] ^ b[i]; + } + return output; + } } diff --git a/modules/widevine/license.ts b/modules/widevine/license.ts index 97b51e5..f1c30c0 100644 --- a/modules/widevine/license.ts +++ b/modules/widevine/license.ts @@ -3,299 +3,299 @@ import { AES_CMAC } from './cmac'; import forge from 'node-forge'; import { - ClientIdentification, - ClientIdentificationSchema, - DrmCertificateSchema, - EncryptedClientIdentification, - EncryptedClientIdentificationSchema, - LicenseRequest, - LicenseRequest_ContentIdentification_WidevinePsshDataSchema, - LicenseRequest_ContentIdentificationSchema, - LicenseRequest_RequestType, - LicenseRequestSchema, - LicenseSchema, - LicenseType, - ProtocolVersion, - SignedDrmCertificate, - SignedDrmCertificateSchema, - SignedMessage, - SignedMessage_MessageType, - SignedMessageSchema, - WidevinePsshData, - WidevinePsshDataSchema + ClientIdentification, + ClientIdentificationSchema, + DrmCertificateSchema, + EncryptedClientIdentification, + EncryptedClientIdentificationSchema, + LicenseRequest, + LicenseRequest_ContentIdentification_WidevinePsshDataSchema, + LicenseRequest_ContentIdentificationSchema, + LicenseRequest_RequestType, + LicenseRequestSchema, + LicenseSchema, + LicenseType, + ProtocolVersion, + SignedDrmCertificate, + SignedDrmCertificateSchema, + SignedMessage, + SignedMessage_MessageType, + SignedMessageSchema, + WidevinePsshData, + WidevinePsshDataSchema } from './license_protocol_pb3'; import { create, fromBinary, toBinary } from '@bufbuild/protobuf'; const WIDEVINE_SYSTEM_ID = new Uint8Array([0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, 0xd5, 0x1d, 0x21, 0xed]); const WIDEVINE_ROOT_PUBLIC_KEY = new Uint8Array([ - 0x30, 0x82, 0x01, 0x8a, 0x02, 0x82, 0x01, 0x81, 0x00, 0xb4, 0xfe, 0x39, 0xc3, 0x65, 0x90, 0x03, 0xdb, 0x3c, 0x11, 0x97, 0x09, 0xe8, 0x68, 0xcd, 0xf2, 0xc3, 0x5e, 0x9b, 0xf2, - 0xe7, 0x4d, 0x23, 0xb1, 0x10, 0xdb, 0x87, 0x65, 0xdf, 0xdc, 0xfb, 0x9f, 0x35, 0xa0, 0x57, 0x03, 0x53, 0x4c, 0xf6, 0x6d, 0x35, 0x7d, 0xa6, 0x78, 0xdb, 0xb3, 0x36, 0xd2, 0x3f, - 0x9c, 0x40, 0xa9, 0x95, 0x26, 0x72, 0x7f, 0xb8, 0xbe, 0x66, 0xdf, 0xc5, 0x21, 0x98, 0x78, 0x15, 0x16, 0x68, 0x5d, 0x2f, 0x46, 0x0e, 0x43, 0xcb, 0x8a, 0x84, 0x39, 0xab, 0xfb, - 0xb0, 0x35, 0x80, 0x22, 0xbe, 0x34, 0x23, 0x8b, 0xab, 0x53, 0x5b, 0x72, 0xec, 0x4b, 0xb5, 0x48, 0x69, 0x53, 0x3e, 0x47, 0x5f, 0xfd, 0x09, 0xfd, 0xa7, 0x76, 0x13, 0x8f, 0x0f, - 0x92, 0xd6, 0x4c, 0xdf, 0xae, 0x76, 0xa9, 0xba, 0xd9, 0x22, 0x10, 0xa9, 0x9d, 0x71, 0x45, 0xd6, 0xd7, 0xe1, 0x19, 0x25, 0x85, 0x9c, 0x53, 0x9a, 0x97, 0xeb, 0x84, 0xd7, 0xcc, - 0xa8, 0x88, 0x82, 0x20, 0x70, 0x26, 0x20, 0xfd, 0x7e, 0x40, 0x50, 0x27, 0xe2, 0x25, 0x93, 0x6f, 0xbc, 0x3e, 0x72, 0xa0, 0xfa, 0xc1, 0xbd, 0x29, 0xb4, 0x4d, 0x82, 0x5c, 0xc1, - 0xb4, 0xcb, 0x9c, 0x72, 0x7e, 0xb0, 0xe9, 0x8a, 0x17, 0x3e, 0x19, 0x63, 0xfc, 0xfd, 0x82, 0x48, 0x2b, 0xb7, 0xb2, 0x33, 0xb9, 0x7d, 0xec, 0x4b, 0xba, 0x89, 0x1f, 0x27, 0xb8, - 0x9b, 0x88, 0x48, 0x84, 0xaa, 0x18, 0x92, 0x0e, 0x65, 0xf5, 0xc8, 0x6c, 0x11, 0xff, 0x6b, 0x36, 0xe4, 0x74, 0x34, 0xca, 0x8c, 0x33, 0xb1, 0xf9, 0xb8, 0x8e, 0xb4, 0xe6, 0x12, - 0xe0, 0x02, 0x98, 0x79, 0x52, 0x5e, 0x45, 0x33, 0xff, 0x11, 0xdc, 0xeb, 0xc3, 0x53, 0xba, 0x7c, 0x60, 0x1a, 0x11, 0x3d, 0x00, 0xfb, 0xd2, 0xb7, 0xaa, 0x30, 0xfa, 0x4f, 0x5e, - 0x48, 0x77, 0x5b, 0x17, 0xdc, 0x75, 0xef, 0x6f, 0xd2, 0x19, 0x6d, 0xdc, 0xbe, 0x7f, 0xb0, 0x78, 0x8f, 0xdc, 0x82, 0x60, 0x4c, 0xbf, 0xe4, 0x29, 0x06, 0x5e, 0x69, 0x8c, 0x39, - 0x13, 0xad, 0x14, 0x25, 0xed, 0x19, 0xb2, 0xf2, 0x9f, 0x01, 0x82, 0x0d, 0x56, 0x44, 0x88, 0xc8, 0x35, 0xec, 0x1f, 0x11, 0xb3, 0x24, 0xe0, 0x59, 0x0d, 0x37, 0xe4, 0x47, 0x3c, - 0xea, 0x4b, 0x7f, 0x97, 0x31, 0x1c, 0x81, 0x7c, 0x94, 0x8a, 0x4c, 0x7d, 0x68, 0x15, 0x84, 0xff, 0xa5, 0x08, 0xfd, 0x18, 0xe7, 0xe7, 0x2b, 0xe4, 0x47, 0x27, 0x12, 0x11, 0xb8, - 0x23, 0xec, 0x58, 0x93, 0x3c, 0xac, 0x12, 0xd2, 0x88, 0x6d, 0x41, 0x3d, 0xc5, 0xfe, 0x1c, 0xdc, 0xb9, 0xf8, 0xd4, 0x51, 0x3e, 0x07, 0xe5, 0x03, 0x6f, 0xa7, 0x12, 0xe8, 0x12, - 0xf7, 0xb5, 0xce, 0xa6, 0x96, 0x55, 0x3f, 0x78, 0xb4, 0x64, 0x82, 0x50, 0xd2, 0x33, 0x5f, 0x91, 0x02, 0x03, 0x01, 0x00, 0x01 + 0x30, 0x82, 0x01, 0x8a, 0x02, 0x82, 0x01, 0x81, 0x00, 0xb4, 0xfe, 0x39, 0xc3, 0x65, 0x90, 0x03, 0xdb, 0x3c, 0x11, 0x97, 0x09, 0xe8, 0x68, 0xcd, 0xf2, 0xc3, 0x5e, 0x9b, 0xf2, + 0xe7, 0x4d, 0x23, 0xb1, 0x10, 0xdb, 0x87, 0x65, 0xdf, 0xdc, 0xfb, 0x9f, 0x35, 0xa0, 0x57, 0x03, 0x53, 0x4c, 0xf6, 0x6d, 0x35, 0x7d, 0xa6, 0x78, 0xdb, 0xb3, 0x36, 0xd2, 0x3f, + 0x9c, 0x40, 0xa9, 0x95, 0x26, 0x72, 0x7f, 0xb8, 0xbe, 0x66, 0xdf, 0xc5, 0x21, 0x98, 0x78, 0x15, 0x16, 0x68, 0x5d, 0x2f, 0x46, 0x0e, 0x43, 0xcb, 0x8a, 0x84, 0x39, 0xab, 0xfb, + 0xb0, 0x35, 0x80, 0x22, 0xbe, 0x34, 0x23, 0x8b, 0xab, 0x53, 0x5b, 0x72, 0xec, 0x4b, 0xb5, 0x48, 0x69, 0x53, 0x3e, 0x47, 0x5f, 0xfd, 0x09, 0xfd, 0xa7, 0x76, 0x13, 0x8f, 0x0f, + 0x92, 0xd6, 0x4c, 0xdf, 0xae, 0x76, 0xa9, 0xba, 0xd9, 0x22, 0x10, 0xa9, 0x9d, 0x71, 0x45, 0xd6, 0xd7, 0xe1, 0x19, 0x25, 0x85, 0x9c, 0x53, 0x9a, 0x97, 0xeb, 0x84, 0xd7, 0xcc, + 0xa8, 0x88, 0x82, 0x20, 0x70, 0x26, 0x20, 0xfd, 0x7e, 0x40, 0x50, 0x27, 0xe2, 0x25, 0x93, 0x6f, 0xbc, 0x3e, 0x72, 0xa0, 0xfa, 0xc1, 0xbd, 0x29, 0xb4, 0x4d, 0x82, 0x5c, 0xc1, + 0xb4, 0xcb, 0x9c, 0x72, 0x7e, 0xb0, 0xe9, 0x8a, 0x17, 0x3e, 0x19, 0x63, 0xfc, 0xfd, 0x82, 0x48, 0x2b, 0xb7, 0xb2, 0x33, 0xb9, 0x7d, 0xec, 0x4b, 0xba, 0x89, 0x1f, 0x27, 0xb8, + 0x9b, 0x88, 0x48, 0x84, 0xaa, 0x18, 0x92, 0x0e, 0x65, 0xf5, 0xc8, 0x6c, 0x11, 0xff, 0x6b, 0x36, 0xe4, 0x74, 0x34, 0xca, 0x8c, 0x33, 0xb1, 0xf9, 0xb8, 0x8e, 0xb4, 0xe6, 0x12, + 0xe0, 0x02, 0x98, 0x79, 0x52, 0x5e, 0x45, 0x33, 0xff, 0x11, 0xdc, 0xeb, 0xc3, 0x53, 0xba, 0x7c, 0x60, 0x1a, 0x11, 0x3d, 0x00, 0xfb, 0xd2, 0xb7, 0xaa, 0x30, 0xfa, 0x4f, 0x5e, + 0x48, 0x77, 0x5b, 0x17, 0xdc, 0x75, 0xef, 0x6f, 0xd2, 0x19, 0x6d, 0xdc, 0xbe, 0x7f, 0xb0, 0x78, 0x8f, 0xdc, 0x82, 0x60, 0x4c, 0xbf, 0xe4, 0x29, 0x06, 0x5e, 0x69, 0x8c, 0x39, + 0x13, 0xad, 0x14, 0x25, 0xed, 0x19, 0xb2, 0xf2, 0x9f, 0x01, 0x82, 0x0d, 0x56, 0x44, 0x88, 0xc8, 0x35, 0xec, 0x1f, 0x11, 0xb3, 0x24, 0xe0, 0x59, 0x0d, 0x37, 0xe4, 0x47, 0x3c, + 0xea, 0x4b, 0x7f, 0x97, 0x31, 0x1c, 0x81, 0x7c, 0x94, 0x8a, 0x4c, 0x7d, 0x68, 0x15, 0x84, 0xff, 0xa5, 0x08, 0xfd, 0x18, 0xe7, 0xe7, 0x2b, 0xe4, 0x47, 0x27, 0x12, 0x11, 0xb8, + 0x23, 0xec, 0x58, 0x93, 0x3c, 0xac, 0x12, 0xd2, 0x88, 0x6d, 0x41, 0x3d, 0xc5, 0xfe, 0x1c, 0xdc, 0xb9, 0xf8, 0xd4, 0x51, 0x3e, 0x07, 0xe5, 0x03, 0x6f, 0xa7, 0x12, 0xe8, 0x12, + 0xf7, 0xb5, 0xce, 0xa6, 0x96, 0x55, 0x3f, 0x78, 0xb4, 0x64, 0x82, 0x50, 0xd2, 0x33, 0x5f, 0x91, 0x02, 0x03, 0x01, 0x00, 0x01 ]); export const SERVICE_CERTIFICATE_CHALLENGE = new Uint8Array([0x08, 0x04]); const COMMON_SERVICE_CERTIFICATE = new Uint8Array([ - 0x08, 0x05, 0x12, 0xc7, 0x05, 0x0a, 0xc1, 0x02, 0x08, 0x03, 0x12, 0x10, 0x17, 0x05, 0xb9, 0x17, 0xcc, 0x12, 0x04, 0x86, 0x8b, 0x06, 0x33, 0x3a, 0x2f, 0x77, 0x2a, 0x8c, 0x18, - 0x82, 0xb4, 0x82, 0x92, 0x05, 0x22, 0x8e, 0x02, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0x99, 0xed, 0x5b, 0x3b, 0x32, 0x7d, 0xab, 0x5e, 0x24, 0xef, 0xc3, 0xb6, - 0x2a, 0x95, 0xb5, 0x98, 0x52, 0x0a, 0xd5, 0xbc, 0xcb, 0x37, 0x50, 0x3e, 0x06, 0x45, 0xb8, 0x14, 0xd8, 0x76, 0xb8, 0xdf, 0x40, 0x51, 0x04, 0x41, 0xad, 0x8c, 0xe3, 0xad, 0xb1, - 0x1b, 0xb8, 0x8c, 0x4e, 0x72, 0x5a, 0x5e, 0x4a, 0x9e, 0x07, 0x95, 0x29, 0x1d, 0x58, 0x58, 0x40, 0x23, 0xa7, 0xe1, 0xaf, 0x0e, 0x38, 0xa9, 0x12, 0x79, 0x39, 0x30, 0x08, 0x61, - 0x0b, 0x6f, 0x15, 0x8c, 0x87, 0x8c, 0x7e, 0x21, 0xbf, 0xfb, 0xfe, 0xea, 0x77, 0xe1, 0x01, 0x9e, 0x1e, 0x57, 0x81, 0xe8, 0xa4, 0x5f, 0x46, 0x26, 0x3d, 0x14, 0xe6, 0x0e, 0x80, - 0x58, 0xa8, 0x60, 0x7a, 0xdc, 0xe0, 0x4f, 0xac, 0x84, 0x57, 0xb1, 0x37, 0xa8, 0xd6, 0x7c, 0xcd, 0xeb, 0x33, 0x70, 0x5d, 0x98, 0x3a, 0x21, 0xfb, 0x4e, 0xec, 0xbd, 0x4a, 0x10, - 0xca, 0x47, 0x49, 0x0c, 0xa4, 0x7e, 0xaa, 0x5d, 0x43, 0x82, 0x18, 0xdd, 0xba, 0xf1, 0xca, 0xde, 0x33, 0x92, 0xf1, 0x3d, 0x6f, 0xfb, 0x64, 0x42, 0xfd, 0x31, 0xe1, 0xbf, 0x40, - 0xb0, 0xc6, 0x04, 0xd1, 0xc4, 0xba, 0x4c, 0x95, 0x20, 0xa4, 0xbf, 0x97, 0xee, 0xbd, 0x60, 0x92, 0x9a, 0xfc, 0xee, 0xf5, 0x5b, 0xba, 0xf5, 0x64, 0xe2, 0xd0, 0xe7, 0x6c, 0xd7, - 0xc5, 0x5c, 0x73, 0xa0, 0x82, 0xb9, 0x96, 0x12, 0x0b, 0x83, 0x59, 0xed, 0xce, 0x24, 0x70, 0x70, 0x82, 0x68, 0x0d, 0x6f, 0x67, 0xc6, 0xd8, 0x2c, 0x4a, 0xc5, 0xf3, 0x13, 0x44, - 0x90, 0xa7, 0x4e, 0xec, 0x37, 0xaf, 0x4b, 0x2f, 0x01, 0x0c, 0x59, 0xe8, 0x28, 0x43, 0xe2, 0x58, 0x2f, 0x0b, 0x6b, 0x9f, 0x5d, 0xb0, 0xfc, 0x5e, 0x6e, 0xdf, 0x64, 0xfb, 0xd3, - 0x08, 0xb4, 0x71, 0x1b, 0xcf, 0x12, 0x50, 0x01, 0x9c, 0x9f, 0x5a, 0x09, 0x02, 0x03, 0x01, 0x00, 0x01, 0x3a, 0x14, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x2e, 0x77, 0x69, - 0x64, 0x65, 0x76, 0x69, 0x6e, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x12, 0x80, 0x03, 0xae, 0x34, 0x73, 0x14, 0xb5, 0xa8, 0x35, 0x29, 0x7f, 0x27, 0x13, 0x88, 0xfb, 0x7b, 0xb8, 0xcb, - 0x52, 0x77, 0xd2, 0x49, 0x82, 0x3c, 0xdd, 0xd1, 0xda, 0x30, 0xb9, 0x33, 0x39, 0x51, 0x1e, 0xb3, 0xcc, 0xbd, 0xea, 0x04, 0xb9, 0x44, 0xb9, 0x27, 0xc1, 0x21, 0x34, 0x6e, 0xfd, - 0xbd, 0xea, 0xc9, 0xd4, 0x13, 0x91, 0x7e, 0x6e, 0xc1, 0x76, 0xa1, 0x04, 0x38, 0x46, 0x0a, 0x50, 0x3b, 0xc1, 0x95, 0x2b, 0x9b, 0xa4, 0xe4, 0xce, 0x0f, 0xc4, 0xbf, 0xc2, 0x0a, - 0x98, 0x08, 0xaa, 0xaf, 0x4b, 0xfc, 0xd1, 0x9c, 0x1d, 0xcf, 0xcd, 0xf5, 0x74, 0xcc, 0xac, 0x28, 0xd1, 0xb4, 0x10, 0x41, 0x6c, 0xf9, 0xde, 0x88, 0x04, 0x30, 0x1c, 0xbd, 0xb3, - 0x34, 0xca, 0xfc, 0xd0, 0xd4, 0x09, 0x78, 0x42, 0x3a, 0x64, 0x2e, 0x54, 0x61, 0x3d, 0xf0, 0xaf, 0xcf, 0x96, 0xca, 0x4a, 0x92, 0x49, 0xd8, 0x55, 0xe4, 0x2b, 0x3a, 0x70, 0x3e, - 0xf1, 0x76, 0x7f, 0x6a, 0x9b, 0xd3, 0x6d, 0x6b, 0xf8, 0x2b, 0xe7, 0x6b, 0xbf, 0x0c, 0xba, 0x4f, 0xde, 0x59, 0xd2, 0xab, 0xcc, 0x76, 0xfe, 0xb6, 0x42, 0x47, 0xb8, 0x5c, 0x43, - 0x1f, 0xbc, 0xa5, 0x22, 0x66, 0xb6, 0x19, 0xfc, 0x36, 0x97, 0x95, 0x43, 0xfc, 0xa9, 0xcb, 0xbd, 0xbb, 0xfa, 0xfa, 0x0e, 0x1a, 0x55, 0xe7, 0x55, 0xa3, 0xc7, 0xbc, 0xe6, 0x55, - 0xf9, 0x64, 0x6f, 0x58, 0x2a, 0xb9, 0xcf, 0x70, 0xaa, 0x08, 0xb9, 0x79, 0xf8, 0x67, 0xf6, 0x3a, 0x0b, 0x2b, 0x7f, 0xdb, 0x36, 0x2c, 0x5b, 0xc4, 0xec, 0xd5, 0x55, 0xd8, 0x5b, - 0xca, 0xa9, 0xc5, 0x93, 0xc3, 0x83, 0xc8, 0x57, 0xd4, 0x9d, 0xaa, 0xb7, 0x7e, 0x40, 0xb7, 0x85, 0x1d, 0xdf, 0xd2, 0x49, 0x98, 0x80, 0x8e, 0x35, 0xb2, 0x58, 0xe7, 0x5d, 0x78, - 0xea, 0xc0, 0xca, 0x16, 0xf7, 0x04, 0x73, 0x04, 0xc2, 0x0d, 0x93, 0xed, 0xe4, 0xe8, 0xff, 0x1c, 0x6f, 0x17, 0xe6, 0x24, 0x3e, 0x3f, 0x3d, 0xa8, 0xfc, 0x17, 0x09, 0x87, 0x0e, - 0xc4, 0x5f, 0xba, 0x82, 0x3a, 0x26, 0x3f, 0x0c, 0xef, 0xa1, 0xf7, 0x09, 0x3b, 0x19, 0x09, 0x92, 0x83, 0x26, 0x33, 0x37, 0x05, 0x04, 0x3a, 0x29, 0xbd, 0xa6, 0xf9, 0xb4, 0x34, - 0x2c, 0xc8, 0xdf, 0x54, 0x3c, 0xb1, 0xa1, 0x18, 0x2f, 0x7c, 0x5f, 0xff, 0x33, 0xf1, 0x04, 0x90, 0xfa, 0xca, 0x5b, 0x25, 0x36, 0x0b, 0x76, 0x01, 0x5e, 0x9c, 0x5a, 0x06, 0xab, - 0x8e, 0xe0, 0x2f, 0x00, 0xd2, 0xe8, 0xd5, 0x98, 0x61, 0x04, 0xaa, 0xcc, 0x4d, 0xd4, 0x75, 0xfd, 0x96, 0xee, 0x9c, 0xe4, 0xe3, 0x26, 0xf2, 0x1b, 0x83, 0xc7, 0x05, 0x85, 0x77, - 0xb3, 0x87, 0x32, 0xcd, 0xda, 0xbc, 0x6a, 0x6b, 0xed, 0x13, 0xfb, 0x0d, 0x49, 0xd3, 0x8a, 0x45, 0xeb, 0x87, 0xa5, 0xf4 + 0x08, 0x05, 0x12, 0xc7, 0x05, 0x0a, 0xc1, 0x02, 0x08, 0x03, 0x12, 0x10, 0x17, 0x05, 0xb9, 0x17, 0xcc, 0x12, 0x04, 0x86, 0x8b, 0x06, 0x33, 0x3a, 0x2f, 0x77, 0x2a, 0x8c, 0x18, + 0x82, 0xb4, 0x82, 0x92, 0x05, 0x22, 0x8e, 0x02, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0x99, 0xed, 0x5b, 0x3b, 0x32, 0x7d, 0xab, 0x5e, 0x24, 0xef, 0xc3, 0xb6, + 0x2a, 0x95, 0xb5, 0x98, 0x52, 0x0a, 0xd5, 0xbc, 0xcb, 0x37, 0x50, 0x3e, 0x06, 0x45, 0xb8, 0x14, 0xd8, 0x76, 0xb8, 0xdf, 0x40, 0x51, 0x04, 0x41, 0xad, 0x8c, 0xe3, 0xad, 0xb1, + 0x1b, 0xb8, 0x8c, 0x4e, 0x72, 0x5a, 0x5e, 0x4a, 0x9e, 0x07, 0x95, 0x29, 0x1d, 0x58, 0x58, 0x40, 0x23, 0xa7, 0xe1, 0xaf, 0x0e, 0x38, 0xa9, 0x12, 0x79, 0x39, 0x30, 0x08, 0x61, + 0x0b, 0x6f, 0x15, 0x8c, 0x87, 0x8c, 0x7e, 0x21, 0xbf, 0xfb, 0xfe, 0xea, 0x77, 0xe1, 0x01, 0x9e, 0x1e, 0x57, 0x81, 0xe8, 0xa4, 0x5f, 0x46, 0x26, 0x3d, 0x14, 0xe6, 0x0e, 0x80, + 0x58, 0xa8, 0x60, 0x7a, 0xdc, 0xe0, 0x4f, 0xac, 0x84, 0x57, 0xb1, 0x37, 0xa8, 0xd6, 0x7c, 0xcd, 0xeb, 0x33, 0x70, 0x5d, 0x98, 0x3a, 0x21, 0xfb, 0x4e, 0xec, 0xbd, 0x4a, 0x10, + 0xca, 0x47, 0x49, 0x0c, 0xa4, 0x7e, 0xaa, 0x5d, 0x43, 0x82, 0x18, 0xdd, 0xba, 0xf1, 0xca, 0xde, 0x33, 0x92, 0xf1, 0x3d, 0x6f, 0xfb, 0x64, 0x42, 0xfd, 0x31, 0xe1, 0xbf, 0x40, + 0xb0, 0xc6, 0x04, 0xd1, 0xc4, 0xba, 0x4c, 0x95, 0x20, 0xa4, 0xbf, 0x97, 0xee, 0xbd, 0x60, 0x92, 0x9a, 0xfc, 0xee, 0xf5, 0x5b, 0xba, 0xf5, 0x64, 0xe2, 0xd0, 0xe7, 0x6c, 0xd7, + 0xc5, 0x5c, 0x73, 0xa0, 0x82, 0xb9, 0x96, 0x12, 0x0b, 0x83, 0x59, 0xed, 0xce, 0x24, 0x70, 0x70, 0x82, 0x68, 0x0d, 0x6f, 0x67, 0xc6, 0xd8, 0x2c, 0x4a, 0xc5, 0xf3, 0x13, 0x44, + 0x90, 0xa7, 0x4e, 0xec, 0x37, 0xaf, 0x4b, 0x2f, 0x01, 0x0c, 0x59, 0xe8, 0x28, 0x43, 0xe2, 0x58, 0x2f, 0x0b, 0x6b, 0x9f, 0x5d, 0xb0, 0xfc, 0x5e, 0x6e, 0xdf, 0x64, 0xfb, 0xd3, + 0x08, 0xb4, 0x71, 0x1b, 0xcf, 0x12, 0x50, 0x01, 0x9c, 0x9f, 0x5a, 0x09, 0x02, 0x03, 0x01, 0x00, 0x01, 0x3a, 0x14, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x2e, 0x77, 0x69, + 0x64, 0x65, 0x76, 0x69, 0x6e, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x12, 0x80, 0x03, 0xae, 0x34, 0x73, 0x14, 0xb5, 0xa8, 0x35, 0x29, 0x7f, 0x27, 0x13, 0x88, 0xfb, 0x7b, 0xb8, 0xcb, + 0x52, 0x77, 0xd2, 0x49, 0x82, 0x3c, 0xdd, 0xd1, 0xda, 0x30, 0xb9, 0x33, 0x39, 0x51, 0x1e, 0xb3, 0xcc, 0xbd, 0xea, 0x04, 0xb9, 0x44, 0xb9, 0x27, 0xc1, 0x21, 0x34, 0x6e, 0xfd, + 0xbd, 0xea, 0xc9, 0xd4, 0x13, 0x91, 0x7e, 0x6e, 0xc1, 0x76, 0xa1, 0x04, 0x38, 0x46, 0x0a, 0x50, 0x3b, 0xc1, 0x95, 0x2b, 0x9b, 0xa4, 0xe4, 0xce, 0x0f, 0xc4, 0xbf, 0xc2, 0x0a, + 0x98, 0x08, 0xaa, 0xaf, 0x4b, 0xfc, 0xd1, 0x9c, 0x1d, 0xcf, 0xcd, 0xf5, 0x74, 0xcc, 0xac, 0x28, 0xd1, 0xb4, 0x10, 0x41, 0x6c, 0xf9, 0xde, 0x88, 0x04, 0x30, 0x1c, 0xbd, 0xb3, + 0x34, 0xca, 0xfc, 0xd0, 0xd4, 0x09, 0x78, 0x42, 0x3a, 0x64, 0x2e, 0x54, 0x61, 0x3d, 0xf0, 0xaf, 0xcf, 0x96, 0xca, 0x4a, 0x92, 0x49, 0xd8, 0x55, 0xe4, 0x2b, 0x3a, 0x70, 0x3e, + 0xf1, 0x76, 0x7f, 0x6a, 0x9b, 0xd3, 0x6d, 0x6b, 0xf8, 0x2b, 0xe7, 0x6b, 0xbf, 0x0c, 0xba, 0x4f, 0xde, 0x59, 0xd2, 0xab, 0xcc, 0x76, 0xfe, 0xb6, 0x42, 0x47, 0xb8, 0x5c, 0x43, + 0x1f, 0xbc, 0xa5, 0x22, 0x66, 0xb6, 0x19, 0xfc, 0x36, 0x97, 0x95, 0x43, 0xfc, 0xa9, 0xcb, 0xbd, 0xbb, 0xfa, 0xfa, 0x0e, 0x1a, 0x55, 0xe7, 0x55, 0xa3, 0xc7, 0xbc, 0xe6, 0x55, + 0xf9, 0x64, 0x6f, 0x58, 0x2a, 0xb9, 0xcf, 0x70, 0xaa, 0x08, 0xb9, 0x79, 0xf8, 0x67, 0xf6, 0x3a, 0x0b, 0x2b, 0x7f, 0xdb, 0x36, 0x2c, 0x5b, 0xc4, 0xec, 0xd5, 0x55, 0xd8, 0x5b, + 0xca, 0xa9, 0xc5, 0x93, 0xc3, 0x83, 0xc8, 0x57, 0xd4, 0x9d, 0xaa, 0xb7, 0x7e, 0x40, 0xb7, 0x85, 0x1d, 0xdf, 0xd2, 0x49, 0x98, 0x80, 0x8e, 0x35, 0xb2, 0x58, 0xe7, 0x5d, 0x78, + 0xea, 0xc0, 0xca, 0x16, 0xf7, 0x04, 0x73, 0x04, 0xc2, 0x0d, 0x93, 0xed, 0xe4, 0xe8, 0xff, 0x1c, 0x6f, 0x17, 0xe6, 0x24, 0x3e, 0x3f, 0x3d, 0xa8, 0xfc, 0x17, 0x09, 0x87, 0x0e, + 0xc4, 0x5f, 0xba, 0x82, 0x3a, 0x26, 0x3f, 0x0c, 0xef, 0xa1, 0xf7, 0x09, 0x3b, 0x19, 0x09, 0x92, 0x83, 0x26, 0x33, 0x37, 0x05, 0x04, 0x3a, 0x29, 0xbd, 0xa6, 0xf9, 0xb4, 0x34, + 0x2c, 0xc8, 0xdf, 0x54, 0x3c, 0xb1, 0xa1, 0x18, 0x2f, 0x7c, 0x5f, 0xff, 0x33, 0xf1, 0x04, 0x90, 0xfa, 0xca, 0x5b, 0x25, 0x36, 0x0b, 0x76, 0x01, 0x5e, 0x9c, 0x5a, 0x06, 0xab, + 0x8e, 0xe0, 0x2f, 0x00, 0xd2, 0xe8, 0xd5, 0x98, 0x61, 0x04, 0xaa, 0xcc, 0x4d, 0xd4, 0x75, 0xfd, 0x96, 0xee, 0x9c, 0xe4, 0xe3, 0x26, 0xf2, 0x1b, 0x83, 0xc7, 0x05, 0x85, 0x77, + 0xb3, 0x87, 0x32, 0xcd, 0xda, 0xbc, 0x6a, 0x6b, 0xed, 0x13, 0xfb, 0x0d, 0x49, 0xd3, 0x8a, 0x45, 0xeb, 0x87, 0xa5, 0xf4 ]); export type KeyContainer = { - kid: string; - key: string; + kid: string; + key: string; }; export type ContentDecryptionModule = { - privateKey: Buffer; - identifierBlob: Buffer; + privateKey: Buffer; + identifierBlob: Buffer; }; export class Session { - private _devicePrivateKey: forge.pki.rsa.PrivateKey; - private _identifierBlob: ClientIdentification; - private _pssh: Buffer; - private _rawLicenseRequest?: Buffer; - private _serviceCertificate?: SignedDrmCertificate; + private _devicePrivateKey: forge.pki.rsa.PrivateKey; + private _identifierBlob: ClientIdentification; + private _pssh: Buffer; + private _rawLicenseRequest?: Buffer; + private _serviceCertificate?: SignedDrmCertificate; - constructor(contentDecryptionModule: ContentDecryptionModule, pssh: Buffer) { - this._devicePrivateKey = forge.pki.privateKeyFromPem(contentDecryptionModule.privateKey.toString('binary')); + constructor(contentDecryptionModule: ContentDecryptionModule, pssh: Buffer) { + this._devicePrivateKey = forge.pki.privateKeyFromPem(contentDecryptionModule.privateKey.toString('binary')); - this._identifierBlob = fromBinary(ClientIdentificationSchema, contentDecryptionModule.identifierBlob); - this._pssh = pssh; - } + this._identifierBlob = fromBinary(ClientIdentificationSchema, contentDecryptionModule.identifierBlob); + this._pssh = pssh; + } - async setDefaultServiceCertificate() { - await this.setServiceCertificate(Buffer.from(COMMON_SERVICE_CERTIFICATE)); - } + async setDefaultServiceCertificate() { + await this.setServiceCertificate(Buffer.from(COMMON_SERVICE_CERTIFICATE)); + } - async setServiceCertificateFromMessage(rawSignedMessage: Buffer) { - const signedMessage: SignedMessage = fromBinary(SignedMessageSchema, rawSignedMessage); - if (!signedMessage.msg) { - throw new Error('the service certificate message does not contain a message'); - } - await this.setServiceCertificate(Buffer.from(signedMessage.msg)); - } + async setServiceCertificateFromMessage(rawSignedMessage: Buffer) { + const signedMessage: SignedMessage = fromBinary(SignedMessageSchema, rawSignedMessage); + if (!signedMessage.msg) { + throw new Error('the service certificate message does not contain a message'); + } + await this.setServiceCertificate(Buffer.from(signedMessage.msg)); + } - async setServiceCertificate(serviceCertificate: Buffer) { - const signedServiceCertificate: SignedDrmCertificate = fromBinary(SignedDrmCertificateSchema, serviceCertificate); - if (!(await this._verifyServiceCertificate(signedServiceCertificate))) { - throw new Error('Service certificate is not signed by the Widevine root certificate'); - } - this._serviceCertificate = signedServiceCertificate; - } + async setServiceCertificate(serviceCertificate: Buffer) { + const signedServiceCertificate: SignedDrmCertificate = fromBinary(SignedDrmCertificateSchema, serviceCertificate); + if (!(await this._verifyServiceCertificate(signedServiceCertificate))) { + throw new Error('Service certificate is not signed by the Widevine root certificate'); + } + this._serviceCertificate = signedServiceCertificate; + } - createLicenseRequest(licenseType: LicenseType = LicenseType.STREAMING, android: boolean = false): Buffer { - if (!this._pssh.subarray(12, 28).equals(Buffer.from(WIDEVINE_SYSTEM_ID))) { - throw new Error('the pssh is not an actuall pssh'); - } + createLicenseRequest(licenseType: LicenseType = LicenseType.STREAMING, android: boolean = false): Buffer { + if (!this._pssh.subarray(12, 28).equals(Buffer.from(WIDEVINE_SYSTEM_ID))) { + throw new Error('the pssh is not an actuall pssh'); + } - const pssh = this._parsePSSH(this._pssh); - if (!pssh) { - throw new Error('pssh is invalid'); - } + const pssh = this._parsePSSH(this._pssh); + if (!pssh) { + throw new Error('pssh is invalid'); + } - const licenseRequest: LicenseRequest = create(LicenseRequestSchema, { - type: LicenseRequest_RequestType.NEW, - contentId: create(LicenseRequest_ContentIdentificationSchema, { - contentIdVariant: { - case: 'widevinePsshData', - value: create(LicenseRequest_ContentIdentification_WidevinePsshDataSchema, { - psshData: [this._pssh.subarray(32)], - licenseType: licenseType, - requestId: android ? this._generateAndroidIdentifier() : this._generateGenericIdentifier() - }) - } - }), - requestTime: BigInt(Date.now()) / BigInt(1000), - protocolVersion: ProtocolVersion.VERSION_2_1, - keyControlNonce: Math.floor(Math.random() * 2 ** 31) - }); + const licenseRequest: LicenseRequest = create(LicenseRequestSchema, { + type: LicenseRequest_RequestType.NEW, + contentId: create(LicenseRequest_ContentIdentificationSchema, { + contentIdVariant: { + case: 'widevinePsshData', + value: create(LicenseRequest_ContentIdentification_WidevinePsshDataSchema, { + psshData: [this._pssh.subarray(32)], + licenseType: licenseType, + requestId: android ? this._generateAndroidIdentifier() : this._generateGenericIdentifier() + }) + } + }), + requestTime: BigInt(Date.now()) / BigInt(1000), + protocolVersion: ProtocolVersion.VERSION_2_1, + keyControlNonce: Math.floor(Math.random() * 2 ** 31) + }); - if (this._serviceCertificate) { - const encryptedClientIdentification = this._encryptClientIdentification(this._identifierBlob, this._serviceCertificate); - licenseRequest.encryptedClientId = encryptedClientIdentification; - } else { - licenseRequest.clientId = this._identifierBlob; - } + if (this._serviceCertificate) { + const encryptedClientIdentification = this._encryptClientIdentification(this._identifierBlob, this._serviceCertificate); + licenseRequest.encryptedClientId = encryptedClientIdentification; + } else { + licenseRequest.clientId = this._identifierBlob; + } - this._rawLicenseRequest = Buffer.from(toBinary(LicenseRequestSchema, licenseRequest)); + this._rawLicenseRequest = Buffer.from(toBinary(LicenseRequestSchema, licenseRequest)); - const pss: forge.pss.PSS = forge.pss.create({ md: forge.md.sha1.create(), mgf: forge.mgf.mgf1.create(forge.md.sha1.create()), saltLength: 20 }); - const md = forge.md.sha1.create(); - md.update(this._rawLicenseRequest.toString('binary'), 'raw'); - const signature = Buffer.from(this._devicePrivateKey.sign(md, pss), 'binary'); + const pss: forge.pss.PSS = forge.pss.create({ md: forge.md.sha1.create(), mgf: forge.mgf.mgf1.create(forge.md.sha1.create()), saltLength: 20 }); + const md = forge.md.sha1.create(); + md.update(this._rawLicenseRequest.toString('binary'), 'raw'); + const signature = Buffer.from(this._devicePrivateKey.sign(md, pss), 'binary'); - const signedLicenseRequest: SignedMessage = create(SignedMessageSchema, { - type: SignedMessage_MessageType.LICENSE_REQUEST, - msg: this._rawLicenseRequest, - signature: signature - }); + const signedLicenseRequest: SignedMessage = create(SignedMessageSchema, { + type: SignedMessage_MessageType.LICENSE_REQUEST, + msg: this._rawLicenseRequest, + signature: signature + }); - return Buffer.from(toBinary(SignedMessageSchema, signedLicenseRequest)); - } + return Buffer.from(toBinary(SignedMessageSchema, signedLicenseRequest)); + } - parseLicense(rawLicense: Buffer) { - if (!this._rawLicenseRequest) { - throw new Error('please request a license first'); - } + parseLicense(rawLicense: Buffer) { + if (!this._rawLicenseRequest) { + throw new Error('please request a license first'); + } - const signedLicense = fromBinary(SignedMessageSchema, rawLicense); - if (!signedLicense.sessionKey) { - throw new Error('the license does not contain a session key'); - } - if (!signedLicense.msg) { - throw new Error('the license does not contain a message'); - } - if (!signedLicense.signature) { - throw new Error('the license does not contain a signature'); - } + const signedLicense = fromBinary(SignedMessageSchema, rawLicense); + if (!signedLicense.sessionKey) { + throw new Error('the license does not contain a session key'); + } + if (!signedLicense.msg) { + throw new Error('the license does not contain a message'); + } + if (!signedLicense.signature) { + throw new Error('the license does not contain a signature'); + } - const sessionKey = this._devicePrivateKey.decrypt(Buffer.from(signedLicense.sessionKey).toString('binary'), 'RSA-OAEP', { - md: forge.md.sha1.create() - }); + const sessionKey = this._devicePrivateKey.decrypt(Buffer.from(signedLicense.sessionKey).toString('binary'), 'RSA-OAEP', { + md: forge.md.sha1.create() + }); - const cmac = new AES_CMAC(Buffer.from(sessionKey, 'binary')); + const cmac = new AES_CMAC(Buffer.from(sessionKey, 'binary')); - const encKeyBase = Buffer.concat([Buffer.from('ENCRYPTION'), Buffer.from('\x00', 'ascii'), this._rawLicenseRequest, Buffer.from('\x00\x00\x00\x80', 'ascii')]); - const authKeyBase = Buffer.concat([Buffer.from('AUTHENTICATION'), Buffer.from('\x00', 'ascii'), this._rawLicenseRequest, Buffer.from('\x00\x00\x02\x00', 'ascii')]); + const encKeyBase = Buffer.concat([Buffer.from('ENCRYPTION'), Buffer.from('\x00', 'ascii'), this._rawLicenseRequest, Buffer.from('\x00\x00\x00\x80', 'ascii')]); + const authKeyBase = Buffer.concat([Buffer.from('AUTHENTICATION'), Buffer.from('\x00', 'ascii'), this._rawLicenseRequest, Buffer.from('\x00\x00\x02\x00', 'ascii')]); - const encKey = cmac.calculate(Buffer.concat([Buffer.from('\x01'), encKeyBase])); - const serverKey = Buffer.concat([cmac.calculate(Buffer.concat([Buffer.from('\x01'), authKeyBase])), cmac.calculate(Buffer.concat([Buffer.from('\x02'), authKeyBase]))]); - /*const clientKey = Buffer.concat([ + const encKey = cmac.calculate(Buffer.concat([Buffer.from('\x01'), encKeyBase])); + const serverKey = Buffer.concat([cmac.calculate(Buffer.concat([Buffer.from('\x01'), authKeyBase])), cmac.calculate(Buffer.concat([Buffer.from('\x02'), authKeyBase]))]); + /*const clientKey = Buffer.concat([ cmac.calculate(Buffer.concat([Buffer.from("\x03"), authKeyBase])), cmac.calculate(Buffer.concat([Buffer.from("\x04"), authKeyBase])) ]);*/ - const hmac = forge.hmac.create(); - hmac.start(forge.md.sha256.create(), serverKey.toString('binary')); - hmac.update(Buffer.from(signedLicense.msg).toString('binary')); - const calculatedSignature = Buffer.from(hmac.digest().data, 'binary'); + const hmac = forge.hmac.create(); + hmac.start(forge.md.sha256.create(), serverKey.toString('binary')); + hmac.update(Buffer.from(signedLicense.msg).toString('binary')); + const calculatedSignature = Buffer.from(hmac.digest().data, 'binary'); - if (!calculatedSignature.equals(signedLicense.signature)) { - throw new Error('signatures do not match'); - } + if (!calculatedSignature.equals(signedLicense.signature)) { + throw new Error('signatures do not match'); + } - const license = fromBinary(LicenseSchema, signedLicense.msg); + const license = fromBinary(LicenseSchema, signedLicense.msg); - const keyContainers = license.key.map((keyContainer) => { - if (keyContainer.type && keyContainer.key && keyContainer.iv) { - const keyId = keyContainer.id ? Buffer.from(keyContainer.id).toString('hex') : '00000000000000000000000000000000'; - const decipher = forge.cipher.createDecipher('AES-CBC', encKey.toString('binary')); - decipher.start({ iv: Buffer.from(keyContainer.iv).toString('binary') }); - decipher.update(forge.util.createBuffer(new Uint8Array(keyContainer.key))); - decipher.finish(); - const decryptedKey = Buffer.from(decipher.output.data, 'binary'); - const key: KeyContainer = { - kid: keyId, - key: decryptedKey.toString('hex') - }; - return key; - } - }); - if (keyContainers.filter((container) => !!container).length < 1) { - throw new Error('there was not a single valid key in the response'); - } - return keyContainers; - } + const keyContainers = license.key.map((keyContainer) => { + if (keyContainer.type && keyContainer.key && keyContainer.iv) { + const keyId = keyContainer.id ? Buffer.from(keyContainer.id).toString('hex') : '00000000000000000000000000000000'; + const decipher = forge.cipher.createDecipher('AES-CBC', encKey.toString('binary')); + decipher.start({ iv: Buffer.from(keyContainer.iv).toString('binary') }); + decipher.update(forge.util.createBuffer(new Uint8Array(keyContainer.key))); + decipher.finish(); + const decryptedKey = Buffer.from(decipher.output.data, 'binary'); + const key: KeyContainer = { + kid: keyId, + key: decryptedKey.toString('hex') + }; + return key; + } + }); + if (keyContainers.filter((container) => !!container).length < 1) { + throw new Error('there was not a single valid key in the response'); + } + return keyContainers; + } - private _encryptClientIdentification(clientIdentification: ClientIdentification, signedServiceCertificate: SignedDrmCertificate): EncryptedClientIdentification { - if (!signedServiceCertificate.drmCertificate) { - throw new Error('the service certificate does not contain an actual certificate'); - } + private _encryptClientIdentification(clientIdentification: ClientIdentification, signedServiceCertificate: SignedDrmCertificate): EncryptedClientIdentification { + if (!signedServiceCertificate.drmCertificate) { + throw new Error('the service certificate does not contain an actual certificate'); + } - const serviceCertificate = fromBinary(DrmCertificateSchema, signedServiceCertificate.drmCertificate); - if (!serviceCertificate.publicKey) { - throw new Error('the service certificate does not contain a public key'); - } + const serviceCertificate = fromBinary(DrmCertificateSchema, signedServiceCertificate.drmCertificate); + if (!serviceCertificate.publicKey) { + throw new Error('the service certificate does not contain a public key'); + } - const key = forge.random.getBytesSync(16); - const iv = forge.random.getBytesSync(16); - const cipher = forge.cipher.createCipher('AES-CBC', key); - cipher.start({ iv: iv }); - cipher.update(forge.util.createBuffer(toBinary(ClientIdentificationSchema, clientIdentification))); - cipher.finish(); - const rawEncryptedClientIdentification = Buffer.from(cipher.output.data, 'binary'); + const key = forge.random.getBytesSync(16); + const iv = forge.random.getBytesSync(16); + const cipher = forge.cipher.createCipher('AES-CBC', key); + cipher.start({ iv: iv }); + cipher.update(forge.util.createBuffer(toBinary(ClientIdentificationSchema, clientIdentification))); + cipher.finish(); + const rawEncryptedClientIdentification = Buffer.from(cipher.output.data, 'binary'); - const publicKey = forge.pki.publicKeyFromAsn1(forge.asn1.fromDer(Buffer.from(serviceCertificate.publicKey).toString('binary'))); - const encryptedKey = publicKey.encrypt(key, 'RSA-OAEP', { md: forge.md.sha1.create() }); + const publicKey = forge.pki.publicKeyFromAsn1(forge.asn1.fromDer(Buffer.from(serviceCertificate.publicKey).toString('binary'))); + const encryptedKey = publicKey.encrypt(key, 'RSA-OAEP', { md: forge.md.sha1.create() }); - const encryptedClientIdentification: EncryptedClientIdentification = create(EncryptedClientIdentificationSchema, { - encryptedClientId: rawEncryptedClientIdentification, - encryptedClientIdIv: Buffer.from(iv, 'binary'), - encryptedPrivacyKey: Buffer.from(encryptedKey, 'binary'), - providerId: serviceCertificate.providerId, - serviceCertificateSerialNumber: serviceCertificate.serialNumber - }); - return encryptedClientIdentification; - } + const encryptedClientIdentification: EncryptedClientIdentification = create(EncryptedClientIdentificationSchema, { + encryptedClientId: rawEncryptedClientIdentification, + encryptedClientIdIv: Buffer.from(iv, 'binary'), + encryptedPrivacyKey: Buffer.from(encryptedKey, 'binary'), + providerId: serviceCertificate.providerId, + serviceCertificateSerialNumber: serviceCertificate.serialNumber + }); + return encryptedClientIdentification; + } - private async _verifyServiceCertificate(signedServiceCertificate: SignedDrmCertificate): Promise<boolean> { - if (!signedServiceCertificate.drmCertificate) { - throw new Error('the service certificate does not contain an actual certificate'); - } - if (!signedServiceCertificate.signature) { - throw new Error('the service certificate does not contain a signature'); - } + private async _verifyServiceCertificate(signedServiceCertificate: SignedDrmCertificate): Promise<boolean> { + if (!signedServiceCertificate.drmCertificate) { + throw new Error('the service certificate does not contain an actual certificate'); + } + if (!signedServiceCertificate.signature) { + throw new Error('the service certificate does not contain a signature'); + } - const publicKey = forge.pki.publicKeyFromAsn1(forge.asn1.fromDer(Buffer.from(WIDEVINE_ROOT_PUBLIC_KEY).toString('binary'))); - const pss: forge.pss.PSS = forge.pss.create({ md: forge.md.sha1.create(), mgf: forge.mgf.mgf1.create(forge.md.sha1.create()), saltLength: 20 }); - const sha1 = forge.md.sha1.create(); - sha1.update(Buffer.from(signedServiceCertificate.drmCertificate).toString('binary'), 'raw'); - return publicKey.verify(sha1.digest().bytes(), Buffer.from(signedServiceCertificate.signature).toString('binary'), pss); - } + const publicKey = forge.pki.publicKeyFromAsn1(forge.asn1.fromDer(Buffer.from(WIDEVINE_ROOT_PUBLIC_KEY).toString('binary'))); + const pss: forge.pss.PSS = forge.pss.create({ md: forge.md.sha1.create(), mgf: forge.mgf.mgf1.create(forge.md.sha1.create()), saltLength: 20 }); + const sha1 = forge.md.sha1.create(); + sha1.update(Buffer.from(signedServiceCertificate.drmCertificate).toString('binary'), 'raw'); + return publicKey.verify(sha1.digest().bytes(), Buffer.from(signedServiceCertificate.signature).toString('binary'), pss); + } - private _parsePSSH(pssh: Buffer): WidevinePsshData | null { - try { - return fromBinary(WidevinePsshDataSchema, pssh.subarray(32)); - } catch { - return null; - } - } + private _parsePSSH(pssh: Buffer): WidevinePsshData | null { + try { + return fromBinary(WidevinePsshDataSchema, pssh.subarray(32)); + } catch { + return null; + } + } - private _generateAndroidIdentifier(): Buffer { - return Buffer.from(`${forge.util.bytesToHex(forge.random.getBytesSync(8))}${'01'}${'00000000000000'}`); - } + private _generateAndroidIdentifier(): Buffer { + return Buffer.from(`${forge.util.bytesToHex(forge.random.getBytesSync(8))}${'01'}${'00000000000000'}`); + } - private _generateGenericIdentifier(): Buffer { - return Buffer.from(forge.random.getBytesSync(16), 'binary'); - } + private _generateGenericIdentifier(): Buffer { + return Buffer.from(forge.random.getBytesSync(16), 'binary'); + } - get pssh(): Buffer { - return this._pssh; - } + get pssh(): Buffer { + return this._pssh; + } } diff --git a/modules/widevine/license_protocol_pb3.ts b/modules/widevine/license_protocol_pb3.ts index 16227eb..e1cf9dd 100644 --- a/modules/widevine/license_protocol_pb3.ts +++ b/modules/widevine/license_protocol_pb3.ts @@ -10,10 +10,10 @@ import type { Message } from '@bufbuild/protobuf'; * Describes the file license_protocol.proto. */ export const file_license_protocol: GenFile = - /*@__PURE__*/ - fileDesc( - 'ChZsaWNlbnNlX3Byb3RvY29sLnByb3RvEhBsaWNlbnNlX3Byb3RvY29sIq4CChVMaWNlbnNlSWRlbnRpZmljYXRpb24SFwoKcmVxdWVzdF9pZBgBIAEoDEgAiAEBEhcKCnNlc3Npb25faWQYAiABKAxIAYgBARIYCgtwdXJjaGFzZV9pZBgDIAEoDEgCiAEBEjAKBHR5cGUYBCABKA4yHS5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2VUeXBlSAOIAQESFAoHdmVyc2lvbhgFIAEoBUgEiAEBEiMKFnByb3ZpZGVyX3Nlc3Npb25fdG9rZW4YBiABKAxIBYgBAUINCgtfcmVxdWVzdF9pZEINCgtfc2Vzc2lvbl9pZEIOCgxfcHVyY2hhc2VfaWRCBwoFX3R5cGVCCgoIX3ZlcnNpb25CGQoXX3Byb3ZpZGVyX3Nlc3Npb25fdG9rZW4izCAKB0xpY2Vuc2USOAoCaWQYASABKAsyJy5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2VJZGVudGlmaWNhdGlvbkgAiAEBEjUKBnBvbGljeRgCIAEoCzIgLmxpY2Vuc2VfcHJvdG9jb2wuTGljZW5zZS5Qb2xpY3lIAYgBARIzCgNrZXkYAyADKAsyJi5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2UuS2V5Q29udGFpbmVyEh8KEmxpY2Vuc2Vfc3RhcnRfdGltZRgEIAEoA0gCiAEBEigKG3JlbW90ZV9hdHRlc3RhdGlvbl92ZXJpZmllZBgFIAEoCEgDiAEBEiIKFXByb3ZpZGVyX2NsaWVudF90b2tlbhgGIAEoDEgEiAEBEh4KEXByb3RlY3Rpb25fc2NoZW1lGAcgASgNSAWIAQESHAoPc3JtX3JlcXVpcmVtZW50GAggASgMSAaIAQESFwoKc3JtX3VwZGF0ZRgJIAEoDEgHiAEBElcKHHBsYXRmb3JtX3ZlcmlmaWNhdGlvbl9zdGF0dXMYCiABKA4yLC5saWNlbnNlX3Byb3RvY29sLlBsYXRmb3JtVmVyaWZpY2F0aW9uU3RhdHVzSAiIAQESEQoJZ3JvdXBfaWRzGAsgAygMGsoHCgZQb2xpY3kSFQoIY2FuX3BsYXkYASABKAhIAIgBARIYCgtjYW5fcGVyc2lzdBgCIAEoCEgBiAEBEhYKCWNhbl9yZW5ldxgDIAEoCEgCiAEBEiQKF3JlbnRhbF9kdXJhdGlvbl9zZWNvbmRzGAQgASgDSAOIAQESJgoZcGxheWJhY2tfZHVyYXRpb25fc2Vjb25kcxgFIAEoA0gEiAEBEiUKGGxpY2Vuc2VfZHVyYXRpb25fc2Vjb25kcxgGIAEoA0gFiAEBEi4KIXJlbmV3YWxfcmVjb3ZlcnlfZHVyYXRpb25fc2Vjb25kcxgHIAEoA0gGiAEBEh8KEnJlbmV3YWxfc2VydmVyX3VybBgIIAEoCUgHiAEBEiIKFXJlbmV3YWxfZGVsYXlfc2Vjb25kcxgJIAEoA0gIiAEBEisKHnJlbmV3YWxfcmV0cnlfaW50ZXJ2YWxfc2Vjb25kcxgKIAEoA0gJiAEBEh0KEHJlbmV3X3dpdGhfdXNhZ2UYCyABKAhICogBARIlChhhbHdheXNfaW5jbHVkZV9jbGllbnRfaWQYDCABKAhIC4gBARIsCh9wbGF5X3N0YXJ0X2dyYWNlX3BlcmlvZF9zZWNvbmRzGA0gASgDSAyIAQESKwoec29mdF9lbmZvcmNlX3BsYXliYWNrX2R1cmF0aW9uGA4gASgISA2IAQESKQocc29mdF9lbmZvcmNlX3JlbnRhbF9kdXJhdGlvbhgPIAEoCEgOiAEBQgsKCV9jYW5fcGxheUIOCgxfY2FuX3BlcnNpc3RCDAoKX2Nhbl9yZW5ld0IaChhfcmVudGFsX2R1cmF0aW9uX3NlY29uZHNCHAoaX3BsYXliYWNrX2R1cmF0aW9uX3NlY29uZHNCGwoZX2xpY2Vuc2VfZHVyYXRpb25fc2Vjb25kc0IkCiJfcmVuZXdhbF9yZWNvdmVyeV9kdXJhdGlvbl9zZWNvbmRzQhUKE19yZW5ld2FsX3NlcnZlcl91cmxCGAoWX3JlbmV3YWxfZGVsYXlfc2Vjb25kc0IhCh9fcmVuZXdhbF9yZXRyeV9pbnRlcnZhbF9zZWNvbmRzQhMKEV9yZW5ld193aXRoX3VzYWdlQhsKGV9hbHdheXNfaW5jbHVkZV9jbGllbnRfaWRCIgogX3BsYXlfc3RhcnRfZ3JhY2VfcGVyaW9kX3NlY29uZHNCIQofX3NvZnRfZW5mb3JjZV9wbGF5YmFja19kdXJhdGlvbkIfCh1fc29mdF9lbmZvcmNlX3JlbnRhbF9kdXJhdGlvbhreEwoMS2V5Q29udGFpbmVyEg8KAmlkGAEgASgMSACIAQESDwoCaXYYAiABKAxIAYgBARIQCgNrZXkYAyABKAxIAogBARJBCgR0eXBlGAQgASgOMi4ubGljZW5zZV9wcm90b2NvbC5MaWNlbnNlLktleUNvbnRhaW5lci5LZXlUeXBlSAOIAQESSAoFbGV2ZWwYBSABKA4yNC5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2UuS2V5Q29udGFpbmVyLlNlY3VyaXR5TGV2ZWxIBIgBARJZChNyZXF1aXJlZF9wcm90ZWN0aW9uGAYgASgLMjcubGljZW5zZV9wcm90b2NvbC5MaWNlbnNlLktleUNvbnRhaW5lci5PdXRwdXRQcm90ZWN0aW9uSAWIAQESWgoUcmVxdWVzdGVkX3Byb3RlY3Rpb24YByABKAsyNy5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2UuS2V5Q29udGFpbmVyLk91dHB1dFByb3RlY3Rpb25IBogBARJLCgtrZXlfY29udHJvbBgIIAEoCzIxLmxpY2Vuc2VfcHJvdG9jb2wuTGljZW5zZS5LZXlDb250YWluZXIuS2V5Q29udHJvbEgHiAEBEnMKIG9wZXJhdG9yX3Nlc3Npb25fa2V5X3Blcm1pc3Npb25zGAkgASgLMkQubGljZW5zZV9wcm90b2NvbC5MaWNlbnNlLktleUNvbnRhaW5lci5PcGVyYXRvclNlc3Npb25LZXlQZXJtaXNzaW9uc0gIiAEBEmYKHHZpZGVvX3Jlc29sdXRpb25fY29uc3RyYWludHMYCiADKAsyQC5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2UuS2V5Q29udGFpbmVyLlZpZGVvUmVzb2x1dGlvbkNvbnN0cmFpbnQSJgoZYW50aV9yb2xsYmFja191c2FnZV90YWJsZRgLIAEoCEgJiAEBEhgKC3RyYWNrX2xhYmVsGAwgASgJSAqIAQEaWgoKS2V5Q29udHJvbBIeChFrZXlfY29udHJvbF9ibG9jaxgBIAEoDEgAiAEBEg8KAml2GAIgASgMSAGIAQFCFAoSX2tleV9jb250cm9sX2Jsb2NrQgUKA19pdhq7BQoQT3V0cHV0UHJvdGVjdGlvbhJPCgRoZGNwGAEgASgOMjwubGljZW5zZV9wcm90b2NvbC5MaWNlbnNlLktleUNvbnRhaW5lci5PdXRwdXRQcm90ZWN0aW9uLkhEQ1BIAIgBARJVCgpjZ21zX2ZsYWdzGAIgASgOMjwubGljZW5zZV9wcm90b2NvbC5MaWNlbnNlLktleUNvbnRhaW5lci5PdXRwdXRQcm90ZWN0aW9uLkNHTVNIAYgBARJfCg1oZGNwX3NybV9ydWxlGAMgASgOMkMubGljZW5zZV9wcm90b2NvbC5MaWNlbnNlLktleUNvbnRhaW5lci5PdXRwdXRQcm90ZWN0aW9uLkhkY3BTcm1SdWxlSAKIAQESIgoVZGlzYWJsZV9hbmFsb2dfb3V0cHV0GAQgASgISAOIAQESIwoWZGlzYWJsZV9kaWdpdGFsX291dHB1dBgFIAEoCEgEiAEBInkKBEhEQ1ASDQoJSERDUF9OT05FEAASCwoHSERDUF9WMRABEgsKB0hEQ1BfVjIQAhINCglIRENQX1YyXzEQAxINCglIRENQX1YyXzIQBBINCglIRENQX1YyXzMQBRIbChZIRENQX05PX0RJR0lUQUxfT1VUUFVUEP8BIkMKBENHTVMSDQoJQ09QWV9GUkVFEAASDQoJQ0dNU19OT05FECoSDQoJQ09QWV9PTkNFEAISDgoKQ09QWV9ORVZFUhADIjYKC0hkY3BTcm1SdWxlEhYKEkhEQ1BfU1JNX1JVTEVfTk9ORRAAEg8KC0NVUlJFTlRfU1JNEAFCBwoFX2hkY3BCDQoLX2NnbXNfZmxhZ3NCEAoOX2hkY3Bfc3JtX3J1bGVCGAoWX2Rpc2FibGVfYW5hbG9nX291dHB1dEIZChdfZGlzYWJsZV9kaWdpdGFsX291dHB1dBqKAgoZVmlkZW9SZXNvbHV0aW9uQ29uc3RyYWludBIiChVtaW5fcmVzb2x1dGlvbl9waXhlbHMYASABKA1IAIgBARIiChVtYXhfcmVzb2x1dGlvbl9waXhlbHMYAiABKA1IAYgBARJZChNyZXF1aXJlZF9wcm90ZWN0aW9uGAMgASgLMjcubGljZW5zZV9wcm90b2NvbC5MaWNlbnNlLktleUNvbnRhaW5lci5PdXRwdXRQcm90ZWN0aW9uSAKIAQFCGAoWX21pbl9yZXNvbHV0aW9uX3BpeGVsc0IYChZfbWF4X3Jlc29sdXRpb25fcGl4ZWxzQhYKFF9yZXF1aXJlZF9wcm90ZWN0aW9uGuMBCh1PcGVyYXRvclNlc3Npb25LZXlQZXJtaXNzaW9ucxIaCg1hbGxvd19lbmNyeXB0GAEgASgISACIAQESGgoNYWxsb3dfZGVjcnlwdBgCIAEoCEgBiAEBEhcKCmFsbG93X3NpZ24YAyABKAhIAogBARIjChZhbGxvd19zaWduYXR1cmVfdmVyaWZ5GAQgASgISAOIAQFCEAoOX2FsbG93X2VuY3J5cHRCEAoOX2FsbG93X2RlY3J5cHRCDQoLX2FsbG93X3NpZ25CGQoXX2FsbG93X3NpZ25hdHVyZV92ZXJpZnkihAEKB0tleVR5cGUSFgoSS0VZVFlQRV9VTlZFUklGSUVEEAASCwoHU0lHTklORxABEgsKB0NPTlRFTlQQAhIPCgtLRVlfQ09OVFJPTBADEhQKEE9QRVJBVE9SX1NFU1NJT04QBBIPCgtFTlRJVExFTUVOVBAFEg8KC09FTV9DT05URU5UEAYimAEKDVNlY3VyaXR5TGV2ZWwSHAoYU0VDVVJJVFlMRVZFTF9VTlZFUklGSUVEEAASFAoQU1dfU0VDVVJFX0NSWVBUTxABEhQKEFNXX1NFQ1VSRV9ERUNPREUQAhIUChBIV19TRUNVUkVfQ1JZUFRPEAMSFAoQSFdfU0VDVVJFX0RFQ09ERRAEEhEKDUhXX1NFQ1VSRV9BTEwQBUIFCgNfaWRCBQoDX2l2QgYKBF9rZXlCBwoFX3R5cGVCCAoGX2xldmVsQhYKFF9yZXF1aXJlZF9wcm90ZWN0aW9uQhcKFV9yZXF1ZXN0ZWRfcHJvdGVjdGlvbkIOCgxfa2V5X2NvbnRyb2xCIwohX29wZXJhdG9yX3Nlc3Npb25fa2V5X3Blcm1pc3Npb25zQhwKGl9hbnRpX3JvbGxiYWNrX3VzYWdlX3RhYmxlQg4KDF90cmFja19sYWJlbEIFCgNfaWRCCQoHX3BvbGljeUIVChNfbGljZW5zZV9zdGFydF90aW1lQh4KHF9yZW1vdGVfYXR0ZXN0YXRpb25fdmVyaWZpZWRCGAoWX3Byb3ZpZGVyX2NsaWVudF90b2tlbkIUChJfcHJvdGVjdGlvbl9zY2hlbWVCEgoQX3NybV9yZXF1aXJlbWVudEINCgtfc3JtX3VwZGF0ZUIfCh1fcGxhdGZvcm1fdmVyaWZpY2F0aW9uX3N0YXR1cyLAEAoOTGljZW5zZVJlcXVlc3QSPgoJY2xpZW50X2lkGAEgASgLMiYubGljZW5zZV9wcm90b2NvbC5DbGllbnRJZGVudGlmaWNhdGlvbkgAiAEBEk8KCmNvbnRlbnRfaWQYAiABKAsyNi5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2VSZXF1ZXN0LkNvbnRlbnRJZGVudGlmaWNhdGlvbkgBiAEBEj8KBHR5cGUYAyABKA4yLC5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2VSZXF1ZXN0LlJlcXVlc3RUeXBlSAKIAQESGQoMcmVxdWVzdF90aW1lGAQgASgDSAOIAQESKQoca2V5X2NvbnRyb2xfbm9uY2VfZGVwcmVjYXRlZBgFIAEoDEgEiAEBEkAKEHByb3RvY29sX3ZlcnNpb24YBiABKA4yIS5saWNlbnNlX3Byb3RvY29sLlByb3RvY29sVmVyc2lvbkgFiAEBEh4KEWtleV9jb250cm9sX25vbmNlGAcgASgNSAaIAQESUQoTZW5jcnlwdGVkX2NsaWVudF9pZBgIIAEoCzIvLmxpY2Vuc2VfcHJvdG9jb2wuRW5jcnlwdGVkQ2xpZW50SWRlbnRpZmljYXRpb25IB4gBARr3CgoVQ29udGVudElkZW50aWZpY2F0aW9uEmUKEndpZGV2aW5lX3Bzc2hfZGF0YRgBIAEoCzJHLmxpY2Vuc2VfcHJvdG9jb2wuTGljZW5zZVJlcXVlc3QuQ29udGVudElkZW50aWZpY2F0aW9uLldpZGV2aW5lUHNzaERhdGFIABJXCgt3ZWJtX2tleV9pZBgCIAEoCzJALmxpY2Vuc2VfcHJvdG9jb2wuTGljZW5zZVJlcXVlc3QuQ29udGVudElkZW50aWZpY2F0aW9uLldlYm1LZXlJZEgAEmIKEGV4aXN0aW5nX2xpY2Vuc2UYAyABKAsyRi5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2VSZXF1ZXN0LkNvbnRlbnRJZGVudGlmaWNhdGlvbi5FeGlzdGluZ0xpY2Vuc2VIABJUCglpbml0X2RhdGEYBCABKAsyPy5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2VSZXF1ZXN0LkNvbnRlbnRJZGVudGlmaWNhdGlvbi5Jbml0RGF0YUgAGpgBChBXaWRldmluZVBzc2hEYXRhEhEKCXBzc2hfZGF0YRgBIAMoDBI4CgxsaWNlbnNlX3R5cGUYAiABKA4yHS5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2VUeXBlSACIAQESFwoKcmVxdWVzdF9pZBgDIAEoDEgBiAEBQg8KDV9saWNlbnNlX3R5cGVCDQoLX3JlcXVlc3RfaWQangEKCVdlYm1LZXlJZBITCgZoZWFkZXIYASABKAxIAIgBARI4CgxsaWNlbnNlX3R5cGUYAiABKA4yHS5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2VUeXBlSAGIAQESFwoKcmVxdWVzdF9pZBgDIAEoDEgCiAEBQgkKB19oZWFkZXJCDwoNX2xpY2Vuc2VfdHlwZUINCgtfcmVxdWVzdF9pZBqsAgoPRXhpc3RpbmdMaWNlbnNlEkAKCmxpY2Vuc2VfaWQYASABKAsyJy5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2VJZGVudGlmaWNhdGlvbkgAiAEBEiIKFXNlY29uZHNfc2luY2Vfc3RhcnRlZBgCIAEoA0gBiAEBEiYKGXNlY29uZHNfc2luY2VfbGFzdF9wbGF5ZWQYAyABKANIAogBARImChlzZXNzaW9uX3VzYWdlX3RhYmxlX2VudHJ5GAQgASgMSAOIAQFCDQoLX2xpY2Vuc2VfaWRCGAoWX3NlY29uZHNfc2luY2Vfc3RhcnRlZEIcChpfc2Vjb25kc19zaW5jZV9sYXN0X3BsYXllZEIcChpfc2Vzc2lvbl91c2FnZV90YWJsZV9lbnRyeRriAgoISW5pdERhdGESaQoOaW5pdF9kYXRhX3R5cGUYASABKA4yTC5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2VSZXF1ZXN0LkNvbnRlbnRJZGVudGlmaWNhdGlvbi5Jbml0RGF0YS5Jbml0RGF0YVR5cGVIAIgBARIWCglpbml0X2RhdGEYAiABKAxIAYgBARI4CgxsaWNlbnNlX3R5cGUYAyABKA4yHS5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2VUeXBlSAKIAQESFwoKcmVxdWVzdF9pZBgEIAEoDEgDiAEBIj8KDEluaXREYXRhVHlwZRIbChdJTklUREFUQVRZUEVfVU5WRVJJRklFRBAAEggKBENFTkMQARIICgRXRUJNEAJCEQoPX2luaXRfZGF0YV90eXBlQgwKCl9pbml0X2RhdGFCDwoNX2xpY2Vuc2VfdHlwZUINCgtfcmVxdWVzdF9pZEIUChJjb250ZW50X2lkX3ZhcmlhbnQiTAoLUmVxdWVzdFR5cGUSGgoWUkVRVUVTVFRZUEVfVU5WRVJJRklFRBAAEgcKA05FVxABEgsKB1JFTkVXQUwQAhILCgdSRUxFQVNFEANCDAoKX2NsaWVudF9pZEINCgtfY29udGVudF9pZEIHCgVfdHlwZUIPCg1fcmVxdWVzdF90aW1lQh8KHV9rZXlfY29udHJvbF9ub25jZV9kZXByZWNhdGVkQhMKEV9wcm90b2NvbF92ZXJzaW9uQhQKEl9rZXlfY29udHJvbF9ub25jZUIWChRfZW5jcnlwdGVkX2NsaWVudF9pZCKmAgoKTWV0cmljRGF0YRIXCgpzdGFnZV9uYW1lGAEgASgJSACIAQESOwoLbWV0cmljX2RhdGEYAiADKAsyJi5saWNlbnNlX3Byb3RvY29sLk1ldHJpY0RhdGEuVHlwZVZhbHVlGm4KCVR5cGVWYWx1ZRI6CgR0eXBlGAEgASgOMicubGljZW5zZV9wcm90b2NvbC5NZXRyaWNEYXRhLk1ldHJpY1R5cGVIAIgBARISCgV2YWx1ZRgCIAEoA0gBiAEBQgcKBV90eXBlQggKBl92YWx1ZSJDCgpNZXRyaWNUeXBlEhkKFU1FVFJJQ1RZUEVfVU5WRVJJRklFRBAAEgsKB0xBVEVOQ1kQARINCglUSU1FU1RBTVAQAkINCgtfc3RhZ2VfbmFtZSKJAQoLVmVyc2lvbkluZm8SIAoTbGljZW5zZV9zZGtfdmVyc2lvbhgBIAEoCUgAiAEBEiQKF2xpY2Vuc2Vfc2VydmljZV92ZXJzaW9uGAIgASgJSAGIAQFCFgoUX2xpY2Vuc2Vfc2RrX3ZlcnNpb25CGgoYX2xpY2Vuc2Vfc2VydmljZV92ZXJzaW9uIowHCg1TaWduZWRNZXNzYWdlEj4KBHR5cGUYASABKA4yKy5saWNlbnNlX3Byb3RvY29sLlNpZ25lZE1lc3NhZ2UuTWVzc2FnZVR5cGVIAIgBARIQCgNtc2cYAiABKAxIAYgBARIWCglzaWduYXR1cmUYAyABKAxIAogBARIYCgtzZXNzaW9uX2tleRgEIAEoDEgDiAEBEh8KEnJlbW90ZV9hdHRlc3RhdGlvbhgFIAEoDEgEiAEBEjEKC21ldHJpY19kYXRhGAYgAygLMhwubGljZW5zZV9wcm90b2NvbC5NZXRyaWNEYXRhEkAKFHNlcnZpY2VfdmVyc2lvbl9pbmZvGAcgASgLMh0ubGljZW5zZV9wcm90b2NvbC5WZXJzaW9uSW5mb0gFiAEBEk0KEHNlc3Npb25fa2V5X3R5cGUYCCABKA4yLi5saWNlbnNlX3Byb3RvY29sLlNpZ25lZE1lc3NhZ2UuU2Vzc2lvbktleVR5cGVIBogBARIjChZvZW1jcnlwdG9fY29yZV9tZXNzYWdlGAkgASgMSAeIAQEiiAIKC01lc3NhZ2VUeXBlEhoKFk1FU1NBR0VUWVBFX1VOVkVSSUZJRUQQABITCg9MSUNFTlNFX1JFUVVFU1QQARILCgdMSUNFTlNFEAISEgoORVJST1JfUkVTUE9OU0UQAxIfChtTRVJWSUNFX0NFUlRJRklDQVRFX1JFUVVFU1QQBBIXChNTRVJWSUNFX0NFUlRJRklDQVRFEAUSDwoLU1VCX0xJQ0VOU0UQBhIXChNDQVNfTElDRU5TRV9SRVFVRVNUEAcSDwoLQ0FTX0xJQ0VOU0UQCBIcChhFWFRFUk5BTF9MSUNFTlNFX1JFUVVFU1QQCRIUChBFWFRFUk5BTF9MSUNFTlNFEAoiUwoOU2Vzc2lvbktleVR5cGUSDQoJVU5ERUZJTkVEEAASEwoPV1JBUFBFRF9BRVNfS0VZEAESHQoZRVBIRVJNRVJBTF9FQ0NfUFVCTElDX0tFWRACQgcKBV90eXBlQgYKBF9tc2dCDAoKX3NpZ25hdHVyZUIOCgxfc2Vzc2lvbl9rZXlCFQoTX3JlbW90ZV9hdHRlc3RhdGlvbkIXChVfc2VydmljZV92ZXJzaW9uX2luZm9CEwoRX3Nlc3Npb25fa2V5X3R5cGVCGQoXX29lbWNyeXB0b19jb3JlX21lc3NhZ2UijxEKFENsaWVudElkZW50aWZpY2F0aW9uEkMKBHR5cGUYASABKA4yMC5saWNlbnNlX3Byb3RvY29sLkNsaWVudElkZW50aWZpY2F0aW9uLlRva2VuVHlwZUgAiAEBEhIKBXRva2VuGAIgASgMSAGIAQESRQoLY2xpZW50X2luZm8YAyADKAsyMC5saWNlbnNlX3Byb3RvY29sLkNsaWVudElkZW50aWZpY2F0aW9uLk5hbWVWYWx1ZRIiChVwcm92aWRlcl9jbGllbnRfdG9rZW4YBCABKAxIAogBARIcCg9saWNlbnNlX2NvdW50ZXIYBSABKA1IA4gBARJbChNjbGllbnRfY2FwYWJpbGl0aWVzGAYgASgLMjkubGljZW5zZV9wcm90b2NvbC5DbGllbnRJZGVudGlmaWNhdGlvbi5DbGllbnRDYXBhYmlsaXRpZXNIBIgBARIVCgh2bXBfZGF0YRgHIAEoDEgFiAEBElQKEmRldmljZV9jcmVkZW50aWFscxgIIAMoCzI4LmxpY2Vuc2VfcHJvdG9jb2wuQ2xpZW50SWRlbnRpZmljYXRpb24uQ2xpZW50Q3JlZGVudGlhbHMaRQoJTmFtZVZhbHVlEhEKBG5hbWUYASABKAlIAIgBARISCgV2YWx1ZRgCIAEoCUgBiAEBQgcKBV9uYW1lQggKBl92YWx1ZRqnCgoSQ2xpZW50Q2FwYWJpbGl0aWVzEhkKDGNsaWVudF90b2tlbhgBIAEoCEgAiAEBEhoKDXNlc3Npb25fdG9rZW4YAiABKAhIAYgBARIpChx2aWRlb19yZXNvbHV0aW9uX2NvbnN0cmFpbnRzGAMgASgISAKIAQESZAoQbWF4X2hkY3BfdmVyc2lvbhgEIAEoDjJFLmxpY2Vuc2VfcHJvdG9jb2wuQ2xpZW50SWRlbnRpZmljYXRpb24uQ2xpZW50Q2FwYWJpbGl0aWVzLkhkY3BWZXJzaW9uSAOIAQESIwoWb2VtX2NyeXB0b19hcGlfdmVyc2lvbhgFIAEoDUgEiAEBEiYKGWFudGlfcm9sbGJhY2tfdXNhZ2VfdGFibGUYBiABKAhIBYgBARIYCgtzcm1fdmVyc2lvbhgHIAEoDUgGiAEBEhsKDmNhbl91cGRhdGVfc3JtGAggASgISAeIAQESdAoec3VwcG9ydGVkX2NlcnRpZmljYXRlX2tleV90eXBlGAkgAygOMkwubGljZW5zZV9wcm90b2NvbC5DbGllbnRJZGVudGlmaWNhdGlvbi5DbGllbnRDYXBhYmlsaXRpZXMuQ2VydGlmaWNhdGVLZXlUeXBlEnsKGmFuYWxvZ19vdXRwdXRfY2FwYWJpbGl0aWVzGAogASgOMlIubGljZW5zZV9wcm90b2NvbC5DbGllbnRJZGVudGlmaWNhdGlvbi5DbGllbnRDYXBhYmlsaXRpZXMuQW5hbG9nT3V0cHV0Q2FwYWJpbGl0aWVzSAiIAQESJgoZY2FuX2Rpc2FibGVfYW5hbG9nX291dHB1dBgLIAEoCEgJiAEBEiEKFHJlc291cmNlX3JhdGluZ190aWVyGAwgASgNSAqIAQEigAEKC0hkY3BWZXJzaW9uEg0KCUhEQ1BfTk9ORRAAEgsKB0hEQ1BfVjEQARILCgdIRENQX1YyEAISDQoJSERDUF9WMl8xEAMSDQoJSERDUF9WMl8yEAQSDQoJSERDUF9WMl8zEAUSGwoWSERDUF9OT19ESUdJVEFMX09VVFBVVBD/ASJpChJDZXJ0aWZpY2F0ZUtleVR5cGUSDAoIUlNBXzIwNDgQABIMCghSU0FfMzA3MhABEhEKDUVDQ19TRUNQMjU2UjEQAhIRCg1FQ0NfU0VDUDM4NFIxEAMSEQoNRUNDX1NFQ1A1MjFSMRAEIo0BChhBbmFsb2dPdXRwdXRDYXBhYmlsaXRpZXMSGQoVQU5BTE9HX09VVFBVVF9VTktOT1dOEAASFgoSQU5BTE9HX09VVFBVVF9OT05FEAESGwoXQU5BTE9HX09VVFBVVF9TVVBQT1JURUQQAhIhCh1BTkFMT0dfT1VUUFVUX1NVUFBPUlRTX0NHTVNfQRADQg8KDV9jbGllbnRfdG9rZW5CEAoOX3Nlc3Npb25fdG9rZW5CHwodX3ZpZGVvX3Jlc29sdXRpb25fY29uc3RyYWludHNCEwoRX21heF9oZGNwX3ZlcnNpb25CGQoXX29lbV9jcnlwdG9fYXBpX3ZlcnNpb25CHAoaX2FudGlfcm9sbGJhY2tfdXNhZ2VfdGFibGVCDgoMX3NybV92ZXJzaW9uQhEKD19jYW5fdXBkYXRlX3NybUIdChtfYW5hbG9nX291dHB1dF9jYXBhYmlsaXRpZXNCHAoaX2Nhbl9kaXNhYmxlX2FuYWxvZ19vdXRwdXRCFwoVX3Jlc291cmNlX3JhdGluZ190aWVyGn8KEUNsaWVudENyZWRlbnRpYWxzEkMKBHR5cGUYASABKA4yMC5saWNlbnNlX3Byb3RvY29sLkNsaWVudElkZW50aWZpY2F0aW9uLlRva2VuVHlwZUgAiAEBEhIKBXRva2VuGAIgASgMSAGIAQFCBwoFX3R5cGVCCAoGX3Rva2VuInMKCVRva2VuVHlwZRIKCgZLRVlCT1gQABIaChZEUk1fREVWSUNFX0NFUlRJRklDQVRFEAESIgoeUkVNT1RFX0FUVEVTVEFUSU9OX0NFUlRJRklDQVRFEAISGgoWT0VNX0RFVklDRV9DRVJUSUZJQ0FURRADQgcKBV90eXBlQggKBl90b2tlbkIYChZfcHJvdmlkZXJfY2xpZW50X3Rva2VuQhIKEF9saWNlbnNlX2NvdW50ZXJCFgoUX2NsaWVudF9jYXBhYmlsaXRpZXNCCwoJX3ZtcF9kYXRhItcCCh1FbmNyeXB0ZWRDbGllbnRJZGVudGlmaWNhdGlvbhIYCgtwcm92aWRlcl9pZBgBIAEoCUgAiAEBEi4KIXNlcnZpY2VfY2VydGlmaWNhdGVfc2VyaWFsX251bWJlchgCIAEoDEgBiAEBEiAKE2VuY3J5cHRlZF9jbGllbnRfaWQYAyABKAxIAogBARIjChZlbmNyeXB0ZWRfY2xpZW50X2lkX2l2GAQgASgMSAOIAQESIgoVZW5jcnlwdGVkX3ByaXZhY3lfa2V5GAUgASgMSASIAQFCDgoMX3Byb3ZpZGVyX2lkQiQKIl9zZXJ2aWNlX2NlcnRpZmljYXRlX3NlcmlhbF9udW1iZXJCFgoUX2VuY3J5cHRlZF9jbGllbnRfaWRCGQoXX2VuY3J5cHRlZF9jbGllbnRfaWRfaXZCGAoWX2VuY3J5cHRlZF9wcml2YWN5X2tleSKdCQoORHJtQ2VydGlmaWNhdGUSOAoEdHlwZRgBIAEoDjIlLmxpY2Vuc2VfcHJvdG9jb2wuRHJtQ2VydGlmaWNhdGUuVHlwZUgAiAEBEhoKDXNlcmlhbF9udW1iZXIYAiABKAxIAYgBARIiChVjcmVhdGlvbl90aW1lX3NlY29uZHMYAyABKA1IAogBARIkChdleHBpcmF0aW9uX3RpbWVfc2Vjb25kcxgMIAEoDUgDiAEBEhcKCnB1YmxpY19rZXkYBCABKAxIBIgBARIWCglzeXN0ZW1faWQYBSABKA1IBYgBARInChZ0ZXN0X2RldmljZV9kZXByZWNhdGVkGAYgASgIQgIYAUgGiAEBEhgKC3Byb3ZpZGVyX2lkGAcgASgJSAeIAQESQwoNc2VydmljZV90eXBlcxgIIAMoDjIsLmxpY2Vuc2VfcHJvdG9jb2wuRHJtQ2VydGlmaWNhdGUuU2VydmljZVR5cGUSQgoJYWxnb3JpdGhtGAkgASgOMioubGljZW5zZV9wcm90b2NvbC5Ecm1DZXJ0aWZpY2F0ZS5BbGdvcml0aG1ICIgBARITCgZyb3RfaWQYCiABKAxICYgBARJLCg5lbmNyeXB0aW9uX2tleRgLIAEoCzIuLmxpY2Vuc2VfcHJvdG9jb2wuRHJtQ2VydGlmaWNhdGUuRW5jcnlwdGlvbktleUgKiAEBGokBCg1FbmNyeXB0aW9uS2V5EhcKCnB1YmxpY19rZXkYASABKAxIAIgBARJCCglhbGdvcml0aG0YAiABKA4yKi5saWNlbnNlX3Byb3RvY29sLkRybUNlcnRpZmljYXRlLkFsZ29yaXRobUgBiAEBQg0KC19wdWJsaWNfa2V5QgwKCl9hbGdvcml0aG0iTAoEVHlwZRIICgRST09UEAASEAoMREVWSUNFX01PREVMEAESCgoGREVWSUNFEAISCwoHU0VSVklDRRADEg8KC1BST1ZJU0lPTkVSEAQihgEKC1NlcnZpY2VUeXBlEhgKFFVOS05PV05fU0VSVklDRV9UWVBFEAASFgoSTElDRU5TRV9TRVJWRVJfU0RLEAESHAoYTElDRU5TRV9TRVJWRVJfUFJPWFlfU0RLEAISFAoQUFJPVklTSU9OSU5HX1NESxADEhEKDUNBU19QUk9YWV9TREsQBCJkCglBbGdvcml0aG0SFQoRVU5LTk9XTl9BTEdPUklUSE0QABIHCgNSU0EQARIRCg1FQ0NfU0VDUDI1NlIxEAISEQoNRUNDX1NFQ1AzODRSMRADEhEKDUVDQ19TRUNQNTIxUjEQBEIHCgVfdHlwZUIQCg5fc2VyaWFsX251bWJlckIYChZfY3JlYXRpb25fdGltZV9zZWNvbmRzQhoKGF9leHBpcmF0aW9uX3RpbWVfc2Vjb25kc0INCgtfcHVibGljX2tleUIMCgpfc3lzdGVtX2lkQhkKF190ZXN0X2RldmljZV9kZXByZWNhdGVkQg4KDF9wcm92aWRlcl9pZEIMCgpfYWxnb3JpdGhtQgkKB19yb3RfaWRCEQoPX2VuY3J5cHRpb25fa2V5IowCChRTaWduZWREcm1DZXJ0aWZpY2F0ZRIcCg9kcm1fY2VydGlmaWNhdGUYASABKAxIAIgBARIWCglzaWduYXR1cmUYAiABKAxIAYgBARI7CgZzaWduZXIYAyABKAsyJi5saWNlbnNlX3Byb3RvY29sLlNpZ25lZERybUNlcnRpZmljYXRlSAKIAQESQQoOaGFzaF9hbGdvcml0aG0YBCABKA4yJC5saWNlbnNlX3Byb3RvY29sLkhhc2hBbGdvcml0aG1Qcm90b0gDiAEBQhIKEF9kcm1fY2VydGlmaWNhdGVCDAoKX3NpZ25hdHVyZUIJCgdfc2lnbmVyQhEKD19oYXNoX2FsZ29yaXRobSK7CAoQV2lkZXZpbmVQc3NoRGF0YRIPCgdrZXlfaWRzGAIgAygMEhcKCmNvbnRlbnRfaWQYBCABKAxIAIgBARIgChNjcnlwdG9fcGVyaW9kX2luZGV4GAcgASgNSAGIAQESHgoRcHJvdGVjdGlvbl9zY2hlbWUYCSABKA1IAogBARIiChVjcnlwdG9fcGVyaW9kX3NlY29uZHMYCiABKA1IA4gBARI6CgR0eXBlGAsgASgOMicubGljZW5zZV9wcm90b2NvbC5XaWRldmluZVBzc2hEYXRhLlR5cGVIBIgBARIZCgxrZXlfc2VxdWVuY2UYDCABKA1IBYgBARIRCglncm91cF9pZHMYDSADKAwSRQoNZW50aXRsZWRfa2V5cxgOIAMoCzIuLmxpY2Vuc2VfcHJvdG9jb2wuV2lkZXZpbmVQc3NoRGF0YS5FbnRpdGxlZEtleRIaCg12aWRlb19mZWF0dXJlGA8gASgJSAaIAQESSAoJYWxnb3JpdGhtGAEgASgOMiwubGljZW5zZV9wcm90b2NvbC5XaWRldmluZVBzc2hEYXRhLkFsZ29yaXRobUICGAFIB4gBARIZCghwcm92aWRlchgDIAEoCUICGAFICIgBARIbCgp0cmFja190eXBlGAUgASgJQgIYAUgJiAEBEhcKBnBvbGljeRgGIAEoCUICGAFICogBARIgCg9ncm91cGVkX2xpY2Vuc2UYCCABKAxCAhgBSAuIAQEa3wEKC0VudGl0bGVkS2V5Eh8KEmVudGl0bGVtZW50X2tleV9pZBgBIAEoDEgAiAEBEhMKBmtleV9pZBgCIAEoDEgBiAEBEhAKA2tleRgDIAEoDEgCiAEBEg8KAml2GAQgASgMSAOIAQESJwoaZW50aXRsZW1lbnRfa2V5X3NpemVfYnl0ZXMYBSABKA1IBIgBAUIVChNfZW50aXRsZW1lbnRfa2V5X2lkQgkKB19rZXlfaWRCBgoEX2tleUIFCgNfaXZCHQobX2VudGl0bGVtZW50X2tleV9zaXplX2J5dGVzIjUKBFR5cGUSCgoGU0lOR0xFEAASDwoLRU5USVRMRU1FTlQQARIQCgxFTlRJVExFRF9LRVkQAiIoCglBbGdvcml0aG0SDwoLVU5FTkNSWVBURUQQABIKCgZBRVNDVFIQAUINCgtfY29udGVudF9pZEIWChRfY3J5cHRvX3BlcmlvZF9pbmRleEIUChJfcHJvdGVjdGlvbl9zY2hlbWVCGAoWX2NyeXB0b19wZXJpb2Rfc2Vjb25kc0IHCgVfdHlwZUIPCg1fa2V5X3NlcXVlbmNlQhAKDl92aWRlb19mZWF0dXJlQgwKCl9hbGdvcml0aG1CCwoJX3Byb3ZpZGVyQg0KC190cmFja190eXBlQgkKB19wb2xpY3lCEgoQX2dyb3VwZWRfbGljZW5zZSK4AgoKRmlsZUhhc2hlcxITCgZzaWduZXIYASABKAxIAIgBARI6CgpzaWduYXR1cmVzGAIgAygLMiYubGljZW5zZV9wcm90b2NvbC5GaWxlSGFzaGVzLlNpZ25hdHVyZRrNAQoJU2lnbmF0dXJlEhUKCGZpbGVuYW1lGAEgASgJSACIAQESGQoMdGVzdF9zaWduaW5nGAIgASgISAGIAQESFwoKU0hBNTEySGFzaBgDIAEoDEgCiAEBEhUKCG1haW5fZXhlGAQgASgISAOIAQESFgoJc2lnbmF0dXJlGAUgASgMSASIAQFCCwoJX2ZpbGVuYW1lQg8KDV90ZXN0X3NpZ25pbmdCDQoLX1NIQTUxMkhhc2hCCwoJX21haW5fZXhlQgwKCl9zaWduYXR1cmVCCQoHX3NpZ25lcipUCgtMaWNlbnNlVHlwZRIaChZMSUNFTlNFVFlQRV9VTlZFUklGSUVEEAASDQoJU1RSRUFNSU5HEAESCwoHT0ZGTElORRACEg0KCUFVVE9NQVRJQxADKtkBChpQbGF0Zm9ybVZlcmlmaWNhdGlvblN0YXR1cxIXChNQTEFURk9STV9VTlZFUklGSUVEEAASFQoRUExBVEZPUk1fVEFNUEVSRUQQARIeChpQTEFURk9STV9TT0ZUV0FSRV9WRVJJRklFRBACEh4KGlBMQVRGT1JNX0hBUkRXQVJFX1ZFUklGSUVEEAMSHAoYUExBVEZPUk1fTk9fVkVSSUZJQ0FUSU9OEAQSLQopUExBVEZPUk1fU0VDVVJFX1NUT1JBR0VfU09GVFdBUkVfVkVSSUZJRUQQBSpcCg9Qcm90b2NvbFZlcnNpb24SFgoSVkVSU0lPTl9VTlZFUklGSUVEEAASDwoLVkVSU0lPTl8yXzAQFBIPCgtWRVJTSU9OXzJfMRAVEg8KC1ZFUlNJT05fMl8yEBYqhgEKEkhhc2hBbGdvcml0aG1Qcm90bxIeChpIQVNIX0FMR09SSVRITV9VTlNQRUNJRklFRBAAEhgKFEhBU0hfQUxHT1JJVEhNX1NIQV8xEAESGgoWSEFTSF9BTEdPUklUSE1fU0hBXzI1NhACEhoKFkhBU0hfQUxHT1JJVEhNX1NIQV8zODQQA2IGcHJvdG8z' - ); + /*@__PURE__*/ + fileDesc( + 'ChZsaWNlbnNlX3Byb3RvY29sLnByb3RvEhBsaWNlbnNlX3Byb3RvY29sIq4CChVMaWNlbnNlSWRlbnRpZmljYXRpb24SFwoKcmVxdWVzdF9pZBgBIAEoDEgAiAEBEhcKCnNlc3Npb25faWQYAiABKAxIAYgBARIYCgtwdXJjaGFzZV9pZBgDIAEoDEgCiAEBEjAKBHR5cGUYBCABKA4yHS5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2VUeXBlSAOIAQESFAoHdmVyc2lvbhgFIAEoBUgEiAEBEiMKFnByb3ZpZGVyX3Nlc3Npb25fdG9rZW4YBiABKAxIBYgBAUINCgtfcmVxdWVzdF9pZEINCgtfc2Vzc2lvbl9pZEIOCgxfcHVyY2hhc2VfaWRCBwoFX3R5cGVCCgoIX3ZlcnNpb25CGQoXX3Byb3ZpZGVyX3Nlc3Npb25fdG9rZW4izCAKB0xpY2Vuc2USOAoCaWQYASABKAsyJy5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2VJZGVudGlmaWNhdGlvbkgAiAEBEjUKBnBvbGljeRgCIAEoCzIgLmxpY2Vuc2VfcHJvdG9jb2wuTGljZW5zZS5Qb2xpY3lIAYgBARIzCgNrZXkYAyADKAsyJi5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2UuS2V5Q29udGFpbmVyEh8KEmxpY2Vuc2Vfc3RhcnRfdGltZRgEIAEoA0gCiAEBEigKG3JlbW90ZV9hdHRlc3RhdGlvbl92ZXJpZmllZBgFIAEoCEgDiAEBEiIKFXByb3ZpZGVyX2NsaWVudF90b2tlbhgGIAEoDEgEiAEBEh4KEXByb3RlY3Rpb25fc2NoZW1lGAcgASgNSAWIAQESHAoPc3JtX3JlcXVpcmVtZW50GAggASgMSAaIAQESFwoKc3JtX3VwZGF0ZRgJIAEoDEgHiAEBElcKHHBsYXRmb3JtX3ZlcmlmaWNhdGlvbl9zdGF0dXMYCiABKA4yLC5saWNlbnNlX3Byb3RvY29sLlBsYXRmb3JtVmVyaWZpY2F0aW9uU3RhdHVzSAiIAQESEQoJZ3JvdXBfaWRzGAsgAygMGsoHCgZQb2xpY3kSFQoIY2FuX3BsYXkYASABKAhIAIgBARIYCgtjYW5fcGVyc2lzdBgCIAEoCEgBiAEBEhYKCWNhbl9yZW5ldxgDIAEoCEgCiAEBEiQKF3JlbnRhbF9kdXJhdGlvbl9zZWNvbmRzGAQgASgDSAOIAQESJgoZcGxheWJhY2tfZHVyYXRpb25fc2Vjb25kcxgFIAEoA0gEiAEBEiUKGGxpY2Vuc2VfZHVyYXRpb25fc2Vjb25kcxgGIAEoA0gFiAEBEi4KIXJlbmV3YWxfcmVjb3ZlcnlfZHVyYXRpb25fc2Vjb25kcxgHIAEoA0gGiAEBEh8KEnJlbmV3YWxfc2VydmVyX3VybBgIIAEoCUgHiAEBEiIKFXJlbmV3YWxfZGVsYXlfc2Vjb25kcxgJIAEoA0gIiAEBEisKHnJlbmV3YWxfcmV0cnlfaW50ZXJ2YWxfc2Vjb25kcxgKIAEoA0gJiAEBEh0KEHJlbmV3X3dpdGhfdXNhZ2UYCyABKAhICogBARIlChhhbHdheXNfaW5jbHVkZV9jbGllbnRfaWQYDCABKAhIC4gBARIsCh9wbGF5X3N0YXJ0X2dyYWNlX3BlcmlvZF9zZWNvbmRzGA0gASgDSAyIAQESKwoec29mdF9lbmZvcmNlX3BsYXliYWNrX2R1cmF0aW9uGA4gASgISA2IAQESKQocc29mdF9lbmZvcmNlX3JlbnRhbF9kdXJhdGlvbhgPIAEoCEgOiAEBQgsKCV9jYW5fcGxheUIOCgxfY2FuX3BlcnNpc3RCDAoKX2Nhbl9yZW5ld0IaChhfcmVudGFsX2R1cmF0aW9uX3NlY29uZHNCHAoaX3BsYXliYWNrX2R1cmF0aW9uX3NlY29uZHNCGwoZX2xpY2Vuc2VfZHVyYXRpb25fc2Vjb25kc0IkCiJfcmVuZXdhbF9yZWNvdmVyeV9kdXJhdGlvbl9zZWNvbmRzQhUKE19yZW5ld2FsX3NlcnZlcl91cmxCGAoWX3JlbmV3YWxfZGVsYXlfc2Vjb25kc0IhCh9fcmVuZXdhbF9yZXRyeV9pbnRlcnZhbF9zZWNvbmRzQhMKEV9yZW5ld193aXRoX3VzYWdlQhsKGV9hbHdheXNfaW5jbHVkZV9jbGllbnRfaWRCIgogX3BsYXlfc3RhcnRfZ3JhY2VfcGVyaW9kX3NlY29uZHNCIQofX3NvZnRfZW5mb3JjZV9wbGF5YmFja19kdXJhdGlvbkIfCh1fc29mdF9lbmZvcmNlX3JlbnRhbF9kdXJhdGlvbhreEwoMS2V5Q29udGFpbmVyEg8KAmlkGAEgASgMSACIAQESDwoCaXYYAiABKAxIAYgBARIQCgNrZXkYAyABKAxIAogBARJBCgR0eXBlGAQgASgOMi4ubGljZW5zZV9wcm90b2NvbC5MaWNlbnNlLktleUNvbnRhaW5lci5LZXlUeXBlSAOIAQESSAoFbGV2ZWwYBSABKA4yNC5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2UuS2V5Q29udGFpbmVyLlNlY3VyaXR5TGV2ZWxIBIgBARJZChNyZXF1aXJlZF9wcm90ZWN0aW9uGAYgASgLMjcubGljZW5zZV9wcm90b2NvbC5MaWNlbnNlLktleUNvbnRhaW5lci5PdXRwdXRQcm90ZWN0aW9uSAWIAQESWgoUcmVxdWVzdGVkX3Byb3RlY3Rpb24YByABKAsyNy5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2UuS2V5Q29udGFpbmVyLk91dHB1dFByb3RlY3Rpb25IBogBARJLCgtrZXlfY29udHJvbBgIIAEoCzIxLmxpY2Vuc2VfcHJvdG9jb2wuTGljZW5zZS5LZXlDb250YWluZXIuS2V5Q29udHJvbEgHiAEBEnMKIG9wZXJhdG9yX3Nlc3Npb25fa2V5X3Blcm1pc3Npb25zGAkgASgLMkQubGljZW5zZV9wcm90b2NvbC5MaWNlbnNlLktleUNvbnRhaW5lci5PcGVyYXRvclNlc3Npb25LZXlQZXJtaXNzaW9uc0gIiAEBEmYKHHZpZGVvX3Jlc29sdXRpb25fY29uc3RyYWludHMYCiADKAsyQC5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2UuS2V5Q29udGFpbmVyLlZpZGVvUmVzb2x1dGlvbkNvbnN0cmFpbnQSJgoZYW50aV9yb2xsYmFja191c2FnZV90YWJsZRgLIAEoCEgJiAEBEhgKC3RyYWNrX2xhYmVsGAwgASgJSAqIAQEaWgoKS2V5Q29udHJvbBIeChFrZXlfY29udHJvbF9ibG9jaxgBIAEoDEgAiAEBEg8KAml2GAIgASgMSAGIAQFCFAoSX2tleV9jb250cm9sX2Jsb2NrQgUKA19pdhq7BQoQT3V0cHV0UHJvdGVjdGlvbhJPCgRoZGNwGAEgASgOMjwubGljZW5zZV9wcm90b2NvbC5MaWNlbnNlLktleUNvbnRhaW5lci5PdXRwdXRQcm90ZWN0aW9uLkhEQ1BIAIgBARJVCgpjZ21zX2ZsYWdzGAIgASgOMjwubGljZW5zZV9wcm90b2NvbC5MaWNlbnNlLktleUNvbnRhaW5lci5PdXRwdXRQcm90ZWN0aW9uLkNHTVNIAYgBARJfCg1oZGNwX3NybV9ydWxlGAMgASgOMkMubGljZW5zZV9wcm90b2NvbC5MaWNlbnNlLktleUNvbnRhaW5lci5PdXRwdXRQcm90ZWN0aW9uLkhkY3BTcm1SdWxlSAKIAQESIgoVZGlzYWJsZV9hbmFsb2dfb3V0cHV0GAQgASgISAOIAQESIwoWZGlzYWJsZV9kaWdpdGFsX291dHB1dBgFIAEoCEgEiAEBInkKBEhEQ1ASDQoJSERDUF9OT05FEAASCwoHSERDUF9WMRABEgsKB0hEQ1BfVjIQAhINCglIRENQX1YyXzEQAxINCglIRENQX1YyXzIQBBINCglIRENQX1YyXzMQBRIbChZIRENQX05PX0RJR0lUQUxfT1VUUFVUEP8BIkMKBENHTVMSDQoJQ09QWV9GUkVFEAASDQoJQ0dNU19OT05FECoSDQoJQ09QWV9PTkNFEAISDgoKQ09QWV9ORVZFUhADIjYKC0hkY3BTcm1SdWxlEhYKEkhEQ1BfU1JNX1JVTEVfTk9ORRAAEg8KC0NVUlJFTlRfU1JNEAFCBwoFX2hkY3BCDQoLX2NnbXNfZmxhZ3NCEAoOX2hkY3Bfc3JtX3J1bGVCGAoWX2Rpc2FibGVfYW5hbG9nX291dHB1dEIZChdfZGlzYWJsZV9kaWdpdGFsX291dHB1dBqKAgoZVmlkZW9SZXNvbHV0aW9uQ29uc3RyYWludBIiChVtaW5fcmVzb2x1dGlvbl9waXhlbHMYASABKA1IAIgBARIiChVtYXhfcmVzb2x1dGlvbl9waXhlbHMYAiABKA1IAYgBARJZChNyZXF1aXJlZF9wcm90ZWN0aW9uGAMgASgLMjcubGljZW5zZV9wcm90b2NvbC5MaWNlbnNlLktleUNvbnRhaW5lci5PdXRwdXRQcm90ZWN0aW9uSAKIAQFCGAoWX21pbl9yZXNvbHV0aW9uX3BpeGVsc0IYChZfbWF4X3Jlc29sdXRpb25fcGl4ZWxzQhYKFF9yZXF1aXJlZF9wcm90ZWN0aW9uGuMBCh1PcGVyYXRvclNlc3Npb25LZXlQZXJtaXNzaW9ucxIaCg1hbGxvd19lbmNyeXB0GAEgASgISACIAQESGgoNYWxsb3dfZGVjcnlwdBgCIAEoCEgBiAEBEhcKCmFsbG93X3NpZ24YAyABKAhIAogBARIjChZhbGxvd19zaWduYXR1cmVfdmVyaWZ5GAQgASgISAOIAQFCEAoOX2FsbG93X2VuY3J5cHRCEAoOX2FsbG93X2RlY3J5cHRCDQoLX2FsbG93X3NpZ25CGQoXX2FsbG93X3NpZ25hdHVyZV92ZXJpZnkihAEKB0tleVR5cGUSFgoSS0VZVFlQRV9VTlZFUklGSUVEEAASCwoHU0lHTklORxABEgsKB0NPTlRFTlQQAhIPCgtLRVlfQ09OVFJPTBADEhQKEE9QRVJBVE9SX1NFU1NJT04QBBIPCgtFTlRJVExFTUVOVBAFEg8KC09FTV9DT05URU5UEAYimAEKDVNlY3VyaXR5TGV2ZWwSHAoYU0VDVVJJVFlMRVZFTF9VTlZFUklGSUVEEAASFAoQU1dfU0VDVVJFX0NSWVBUTxABEhQKEFNXX1NFQ1VSRV9ERUNPREUQAhIUChBIV19TRUNVUkVfQ1JZUFRPEAMSFAoQSFdfU0VDVVJFX0RFQ09ERRAEEhEKDUhXX1NFQ1VSRV9BTEwQBUIFCgNfaWRCBQoDX2l2QgYKBF9rZXlCBwoFX3R5cGVCCAoGX2xldmVsQhYKFF9yZXF1aXJlZF9wcm90ZWN0aW9uQhcKFV9yZXF1ZXN0ZWRfcHJvdGVjdGlvbkIOCgxfa2V5X2NvbnRyb2xCIwohX29wZXJhdG9yX3Nlc3Npb25fa2V5X3Blcm1pc3Npb25zQhwKGl9hbnRpX3JvbGxiYWNrX3VzYWdlX3RhYmxlQg4KDF90cmFja19sYWJlbEIFCgNfaWRCCQoHX3BvbGljeUIVChNfbGljZW5zZV9zdGFydF90aW1lQh4KHF9yZW1vdGVfYXR0ZXN0YXRpb25fdmVyaWZpZWRCGAoWX3Byb3ZpZGVyX2NsaWVudF90b2tlbkIUChJfcHJvdGVjdGlvbl9zY2hlbWVCEgoQX3NybV9yZXF1aXJlbWVudEINCgtfc3JtX3VwZGF0ZUIfCh1fcGxhdGZvcm1fdmVyaWZpY2F0aW9uX3N0YXR1cyLAEAoOTGljZW5zZVJlcXVlc3QSPgoJY2xpZW50X2lkGAEgASgLMiYubGljZW5zZV9wcm90b2NvbC5DbGllbnRJZGVudGlmaWNhdGlvbkgAiAEBEk8KCmNvbnRlbnRfaWQYAiABKAsyNi5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2VSZXF1ZXN0LkNvbnRlbnRJZGVudGlmaWNhdGlvbkgBiAEBEj8KBHR5cGUYAyABKA4yLC5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2VSZXF1ZXN0LlJlcXVlc3RUeXBlSAKIAQESGQoMcmVxdWVzdF90aW1lGAQgASgDSAOIAQESKQoca2V5X2NvbnRyb2xfbm9uY2VfZGVwcmVjYXRlZBgFIAEoDEgEiAEBEkAKEHByb3RvY29sX3ZlcnNpb24YBiABKA4yIS5saWNlbnNlX3Byb3RvY29sLlByb3RvY29sVmVyc2lvbkgFiAEBEh4KEWtleV9jb250cm9sX25vbmNlGAcgASgNSAaIAQESUQoTZW5jcnlwdGVkX2NsaWVudF9pZBgIIAEoCzIvLmxpY2Vuc2VfcHJvdG9jb2wuRW5jcnlwdGVkQ2xpZW50SWRlbnRpZmljYXRpb25IB4gBARr3CgoVQ29udGVudElkZW50aWZpY2F0aW9uEmUKEndpZGV2aW5lX3Bzc2hfZGF0YRgBIAEoCzJHLmxpY2Vuc2VfcHJvdG9jb2wuTGljZW5zZVJlcXVlc3QuQ29udGVudElkZW50aWZpY2F0aW9uLldpZGV2aW5lUHNzaERhdGFIABJXCgt3ZWJtX2tleV9pZBgCIAEoCzJALmxpY2Vuc2VfcHJvdG9jb2wuTGljZW5zZVJlcXVlc3QuQ29udGVudElkZW50aWZpY2F0aW9uLldlYm1LZXlJZEgAEmIKEGV4aXN0aW5nX2xpY2Vuc2UYAyABKAsyRi5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2VSZXF1ZXN0LkNvbnRlbnRJZGVudGlmaWNhdGlvbi5FeGlzdGluZ0xpY2Vuc2VIABJUCglpbml0X2RhdGEYBCABKAsyPy5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2VSZXF1ZXN0LkNvbnRlbnRJZGVudGlmaWNhdGlvbi5Jbml0RGF0YUgAGpgBChBXaWRldmluZVBzc2hEYXRhEhEKCXBzc2hfZGF0YRgBIAMoDBI4CgxsaWNlbnNlX3R5cGUYAiABKA4yHS5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2VUeXBlSACIAQESFwoKcmVxdWVzdF9pZBgDIAEoDEgBiAEBQg8KDV9saWNlbnNlX3R5cGVCDQoLX3JlcXVlc3RfaWQangEKCVdlYm1LZXlJZBITCgZoZWFkZXIYASABKAxIAIgBARI4CgxsaWNlbnNlX3R5cGUYAiABKA4yHS5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2VUeXBlSAGIAQESFwoKcmVxdWVzdF9pZBgDIAEoDEgCiAEBQgkKB19oZWFkZXJCDwoNX2xpY2Vuc2VfdHlwZUINCgtfcmVxdWVzdF9pZBqsAgoPRXhpc3RpbmdMaWNlbnNlEkAKCmxpY2Vuc2VfaWQYASABKAsyJy5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2VJZGVudGlmaWNhdGlvbkgAiAEBEiIKFXNlY29uZHNfc2luY2Vfc3RhcnRlZBgCIAEoA0gBiAEBEiYKGXNlY29uZHNfc2luY2VfbGFzdF9wbGF5ZWQYAyABKANIAogBARImChlzZXNzaW9uX3VzYWdlX3RhYmxlX2VudHJ5GAQgASgMSAOIAQFCDQoLX2xpY2Vuc2VfaWRCGAoWX3NlY29uZHNfc2luY2Vfc3RhcnRlZEIcChpfc2Vjb25kc19zaW5jZV9sYXN0X3BsYXllZEIcChpfc2Vzc2lvbl91c2FnZV90YWJsZV9lbnRyeRriAgoISW5pdERhdGESaQoOaW5pdF9kYXRhX3R5cGUYASABKA4yTC5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2VSZXF1ZXN0LkNvbnRlbnRJZGVudGlmaWNhdGlvbi5Jbml0RGF0YS5Jbml0RGF0YVR5cGVIAIgBARIWCglpbml0X2RhdGEYAiABKAxIAYgBARI4CgxsaWNlbnNlX3R5cGUYAyABKA4yHS5saWNlbnNlX3Byb3RvY29sLkxpY2Vuc2VUeXBlSAKIAQESFwoKcmVxdWVzdF9pZBgEIAEoDEgDiAEBIj8KDEluaXREYXRhVHlwZRIbChdJTklUREFUQVRZUEVfVU5WRVJJRklFRBAAEggKBENFTkMQARIICgRXRUJNEAJCEQoPX2luaXRfZGF0YV90eXBlQgwKCl9pbml0X2RhdGFCDwoNX2xpY2Vuc2VfdHlwZUINCgtfcmVxdWVzdF9pZEIUChJjb250ZW50X2lkX3ZhcmlhbnQiTAoLUmVxdWVzdFR5cGUSGgoWUkVRVUVTVFRZUEVfVU5WRVJJRklFRBAAEgcKA05FVxABEgsKB1JFTkVXQUwQAhILCgdSRUxFQVNFEANCDAoKX2NsaWVudF9pZEINCgtfY29udGVudF9pZEIHCgVfdHlwZUIPCg1fcmVxdWVzdF90aW1lQh8KHV9rZXlfY29udHJvbF9ub25jZV9kZXByZWNhdGVkQhMKEV9wcm90b2NvbF92ZXJzaW9uQhQKEl9rZXlfY29udHJvbF9ub25jZUIWChRfZW5jcnlwdGVkX2NsaWVudF9pZCKmAgoKTWV0cmljRGF0YRIXCgpzdGFnZV9uYW1lGAEgASgJSACIAQESOwoLbWV0cmljX2RhdGEYAiADKAsyJi5saWNlbnNlX3Byb3RvY29sLk1ldHJpY0RhdGEuVHlwZVZhbHVlGm4KCVR5cGVWYWx1ZRI6CgR0eXBlGAEgASgOMicubGljZW5zZV9wcm90b2NvbC5NZXRyaWNEYXRhLk1ldHJpY1R5cGVIAIgBARISCgV2YWx1ZRgCIAEoA0gBiAEBQgcKBV90eXBlQggKBl92YWx1ZSJDCgpNZXRyaWNUeXBlEhkKFU1FVFJJQ1RZUEVfVU5WRVJJRklFRBAAEgsKB0xBVEVOQ1kQARINCglUSU1FU1RBTVAQAkINCgtfc3RhZ2VfbmFtZSKJAQoLVmVyc2lvbkluZm8SIAoTbGljZW5zZV9zZGtfdmVyc2lvbhgBIAEoCUgAiAEBEiQKF2xpY2Vuc2Vfc2VydmljZV92ZXJzaW9uGAIgASgJSAGIAQFCFgoUX2xpY2Vuc2Vfc2RrX3ZlcnNpb25CGgoYX2xpY2Vuc2Vfc2VydmljZV92ZXJzaW9uIowHCg1TaWduZWRNZXNzYWdlEj4KBHR5cGUYASABKA4yKy5saWNlbnNlX3Byb3RvY29sLlNpZ25lZE1lc3NhZ2UuTWVzc2FnZVR5cGVIAIgBARIQCgNtc2cYAiABKAxIAYgBARIWCglzaWduYXR1cmUYAyABKAxIAogBARIYCgtzZXNzaW9uX2tleRgEIAEoDEgDiAEBEh8KEnJlbW90ZV9hdHRlc3RhdGlvbhgFIAEoDEgEiAEBEjEKC21ldHJpY19kYXRhGAYgAygLMhwubGljZW5zZV9wcm90b2NvbC5NZXRyaWNEYXRhEkAKFHNlcnZpY2VfdmVyc2lvbl9pbmZvGAcgASgLMh0ubGljZW5zZV9wcm90b2NvbC5WZXJzaW9uSW5mb0gFiAEBEk0KEHNlc3Npb25fa2V5X3R5cGUYCCABKA4yLi5saWNlbnNlX3Byb3RvY29sLlNpZ25lZE1lc3NhZ2UuU2Vzc2lvbktleVR5cGVIBogBARIjChZvZW1jcnlwdG9fY29yZV9tZXNzYWdlGAkgASgMSAeIAQEiiAIKC01lc3NhZ2VUeXBlEhoKFk1FU1NBR0VUWVBFX1VOVkVSSUZJRUQQABITCg9MSUNFTlNFX1JFUVVFU1QQARILCgdMSUNFTlNFEAISEgoORVJST1JfUkVTUE9OU0UQAxIfChtTRVJWSUNFX0NFUlRJRklDQVRFX1JFUVVFU1QQBBIXChNTRVJWSUNFX0NFUlRJRklDQVRFEAUSDwoLU1VCX0xJQ0VOU0UQBhIXChNDQVNfTElDRU5TRV9SRVFVRVNUEAcSDwoLQ0FTX0xJQ0VOU0UQCBIcChhFWFRFUk5BTF9MSUNFTlNFX1JFUVVFU1QQCRIUChBFWFRFUk5BTF9MSUNFTlNFEAoiUwoOU2Vzc2lvbktleVR5cGUSDQoJVU5ERUZJTkVEEAASEwoPV1JBUFBFRF9BRVNfS0VZEAESHQoZRVBIRVJNRVJBTF9FQ0NfUFVCTElDX0tFWRACQgcKBV90eXBlQgYKBF9tc2dCDAoKX3NpZ25hdHVyZUIOCgxfc2Vzc2lvbl9rZXlCFQoTX3JlbW90ZV9hdHRlc3RhdGlvbkIXChVfc2VydmljZV92ZXJzaW9uX2luZm9CEwoRX3Nlc3Npb25fa2V5X3R5cGVCGQoXX29lbWNyeXB0b19jb3JlX21lc3NhZ2UijxEKFENsaWVudElkZW50aWZpY2F0aW9uEkMKBHR5cGUYASABKA4yMC5saWNlbnNlX3Byb3RvY29sLkNsaWVudElkZW50aWZpY2F0aW9uLlRva2VuVHlwZUgAiAEBEhIKBXRva2VuGAIgASgMSAGIAQESRQoLY2xpZW50X2luZm8YAyADKAsyMC5saWNlbnNlX3Byb3RvY29sLkNsaWVudElkZW50aWZpY2F0aW9uLk5hbWVWYWx1ZRIiChVwcm92aWRlcl9jbGllbnRfdG9rZW4YBCABKAxIAogBARIcCg9saWNlbnNlX2NvdW50ZXIYBSABKA1IA4gBARJbChNjbGllbnRfY2FwYWJpbGl0aWVzGAYgASgLMjkubGljZW5zZV9wcm90b2NvbC5DbGllbnRJZGVudGlmaWNhdGlvbi5DbGllbnRDYXBhYmlsaXRpZXNIBIgBARIVCgh2bXBfZGF0YRgHIAEoDEgFiAEBElQKEmRldmljZV9jcmVkZW50aWFscxgIIAMoCzI4LmxpY2Vuc2VfcHJvdG9jb2wuQ2xpZW50SWRlbnRpZmljYXRpb24uQ2xpZW50Q3JlZGVudGlhbHMaRQoJTmFtZVZhbHVlEhEKBG5hbWUYASABKAlIAIgBARISCgV2YWx1ZRgCIAEoCUgBiAEBQgcKBV9uYW1lQggKBl92YWx1ZRqnCgoSQ2xpZW50Q2FwYWJpbGl0aWVzEhkKDGNsaWVudF90b2tlbhgBIAEoCEgAiAEBEhoKDXNlc3Npb25fdG9rZW4YAiABKAhIAYgBARIpChx2aWRlb19yZXNvbHV0aW9uX2NvbnN0cmFpbnRzGAMgASgISAKIAQESZAoQbWF4X2hkY3BfdmVyc2lvbhgEIAEoDjJFLmxpY2Vuc2VfcHJvdG9jb2wuQ2xpZW50SWRlbnRpZmljYXRpb24uQ2xpZW50Q2FwYWJpbGl0aWVzLkhkY3BWZXJzaW9uSAOIAQESIwoWb2VtX2NyeXB0b19hcGlfdmVyc2lvbhgFIAEoDUgEiAEBEiYKGWFudGlfcm9sbGJhY2tfdXNhZ2VfdGFibGUYBiABKAhIBYgBARIYCgtzcm1fdmVyc2lvbhgHIAEoDUgGiAEBEhsKDmNhbl91cGRhdGVfc3JtGAggASgISAeIAQESdAoec3VwcG9ydGVkX2NlcnRpZmljYXRlX2tleV90eXBlGAkgAygOMkwubGljZW5zZV9wcm90b2NvbC5DbGllbnRJZGVudGlmaWNhdGlvbi5DbGllbnRDYXBhYmlsaXRpZXMuQ2VydGlmaWNhdGVLZXlUeXBlEnsKGmFuYWxvZ19vdXRwdXRfY2FwYWJpbGl0aWVzGAogASgOMlIubGljZW5zZV9wcm90b2NvbC5DbGllbnRJZGVudGlmaWNhdGlvbi5DbGllbnRDYXBhYmlsaXRpZXMuQW5hbG9nT3V0cHV0Q2FwYWJpbGl0aWVzSAiIAQESJgoZY2FuX2Rpc2FibGVfYW5hbG9nX291dHB1dBgLIAEoCEgJiAEBEiEKFHJlc291cmNlX3JhdGluZ190aWVyGAwgASgNSAqIAQEigAEKC0hkY3BWZXJzaW9uEg0KCUhEQ1BfTk9ORRAAEgsKB0hEQ1BfVjEQARILCgdIRENQX1YyEAISDQoJSERDUF9WMl8xEAMSDQoJSERDUF9WMl8yEAQSDQoJSERDUF9WMl8zEAUSGwoWSERDUF9OT19ESUdJVEFMX09VVFBVVBD/ASJpChJDZXJ0aWZpY2F0ZUtleVR5cGUSDAoIUlNBXzIwNDgQABIMCghSU0FfMzA3MhABEhEKDUVDQ19TRUNQMjU2UjEQAhIRCg1FQ0NfU0VDUDM4NFIxEAMSEQoNRUNDX1NFQ1A1MjFSMRAEIo0BChhBbmFsb2dPdXRwdXRDYXBhYmlsaXRpZXMSGQoVQU5BTE9HX09VVFBVVF9VTktOT1dOEAASFgoSQU5BTE9HX09VVFBVVF9OT05FEAESGwoXQU5BTE9HX09VVFBVVF9TVVBQT1JURUQQAhIhCh1BTkFMT0dfT1VUUFVUX1NVUFBPUlRTX0NHTVNfQRADQg8KDV9jbGllbnRfdG9rZW5CEAoOX3Nlc3Npb25fdG9rZW5CHwodX3ZpZGVvX3Jlc29sdXRpb25fY29uc3RyYWludHNCEwoRX21heF9oZGNwX3ZlcnNpb25CGQoXX29lbV9jcnlwdG9fYXBpX3ZlcnNpb25CHAoaX2FudGlfcm9sbGJhY2tfdXNhZ2VfdGFibGVCDgoMX3NybV92ZXJzaW9uQhEKD19jYW5fdXBkYXRlX3NybUIdChtfYW5hbG9nX291dHB1dF9jYXBhYmlsaXRpZXNCHAoaX2Nhbl9kaXNhYmxlX2FuYWxvZ19vdXRwdXRCFwoVX3Jlc291cmNlX3JhdGluZ190aWVyGn8KEUNsaWVudENyZWRlbnRpYWxzEkMKBHR5cGUYASABKA4yMC5saWNlbnNlX3Byb3RvY29sLkNsaWVudElkZW50aWZpY2F0aW9uLlRva2VuVHlwZUgAiAEBEhIKBXRva2VuGAIgASgMSAGIAQFCBwoFX3R5cGVCCAoGX3Rva2VuInMKCVRva2VuVHlwZRIKCgZLRVlCT1gQABIaChZEUk1fREVWSUNFX0NFUlRJRklDQVRFEAESIgoeUkVNT1RFX0FUVEVTVEFUSU9OX0NFUlRJRklDQVRFEAISGgoWT0VNX0RFVklDRV9DRVJUSUZJQ0FURRADQgcKBV90eXBlQggKBl90b2tlbkIYChZfcHJvdmlkZXJfY2xpZW50X3Rva2VuQhIKEF9saWNlbnNlX2NvdW50ZXJCFgoUX2NsaWVudF9jYXBhYmlsaXRpZXNCCwoJX3ZtcF9kYXRhItcCCh1FbmNyeXB0ZWRDbGllbnRJZGVudGlmaWNhdGlvbhIYCgtwcm92aWRlcl9pZBgBIAEoCUgAiAEBEi4KIXNlcnZpY2VfY2VydGlmaWNhdGVfc2VyaWFsX251bWJlchgCIAEoDEgBiAEBEiAKE2VuY3J5cHRlZF9jbGllbnRfaWQYAyABKAxIAogBARIjChZlbmNyeXB0ZWRfY2xpZW50X2lkX2l2GAQgASgMSAOIAQESIgoVZW5jcnlwdGVkX3ByaXZhY3lfa2V5GAUgASgMSASIAQFCDgoMX3Byb3ZpZGVyX2lkQiQKIl9zZXJ2aWNlX2NlcnRpZmljYXRlX3NlcmlhbF9udW1iZXJCFgoUX2VuY3J5cHRlZF9jbGllbnRfaWRCGQoXX2VuY3J5cHRlZF9jbGllbnRfaWRfaXZCGAoWX2VuY3J5cHRlZF9wcml2YWN5X2tleSKdCQoORHJtQ2VydGlmaWNhdGUSOAoEdHlwZRgBIAEoDjIlLmxpY2Vuc2VfcHJvdG9jb2wuRHJtQ2VydGlmaWNhdGUuVHlwZUgAiAEBEhoKDXNlcmlhbF9udW1iZXIYAiABKAxIAYgBARIiChVjcmVhdGlvbl90aW1lX3NlY29uZHMYAyABKA1IAogBARIkChdleHBpcmF0aW9uX3RpbWVfc2Vjb25kcxgMIAEoDUgDiAEBEhcKCnB1YmxpY19rZXkYBCABKAxIBIgBARIWCglzeXN0ZW1faWQYBSABKA1IBYgBARInChZ0ZXN0X2RldmljZV9kZXByZWNhdGVkGAYgASgIQgIYAUgGiAEBEhgKC3Byb3ZpZGVyX2lkGAcgASgJSAeIAQESQwoNc2VydmljZV90eXBlcxgIIAMoDjIsLmxpY2Vuc2VfcHJvdG9jb2wuRHJtQ2VydGlmaWNhdGUuU2VydmljZVR5cGUSQgoJYWxnb3JpdGhtGAkgASgOMioubGljZW5zZV9wcm90b2NvbC5Ecm1DZXJ0aWZpY2F0ZS5BbGdvcml0aG1ICIgBARITCgZyb3RfaWQYCiABKAxICYgBARJLCg5lbmNyeXB0aW9uX2tleRgLIAEoCzIuLmxpY2Vuc2VfcHJvdG9jb2wuRHJtQ2VydGlmaWNhdGUuRW5jcnlwdGlvbktleUgKiAEBGokBCg1FbmNyeXB0aW9uS2V5EhcKCnB1YmxpY19rZXkYASABKAxIAIgBARJCCglhbGdvcml0aG0YAiABKA4yKi5saWNlbnNlX3Byb3RvY29sLkRybUNlcnRpZmljYXRlLkFsZ29yaXRobUgBiAEBQg0KC19wdWJsaWNfa2V5QgwKCl9hbGdvcml0aG0iTAoEVHlwZRIICgRST09UEAASEAoMREVWSUNFX01PREVMEAESCgoGREVWSUNFEAISCwoHU0VSVklDRRADEg8KC1BST1ZJU0lPTkVSEAQihgEKC1NlcnZpY2VUeXBlEhgKFFVOS05PV05fU0VSVklDRV9UWVBFEAASFgoSTElDRU5TRV9TRVJWRVJfU0RLEAESHAoYTElDRU5TRV9TRVJWRVJfUFJPWFlfU0RLEAISFAoQUFJPVklTSU9OSU5HX1NESxADEhEKDUNBU19QUk9YWV9TREsQBCJkCglBbGdvcml0aG0SFQoRVU5LTk9XTl9BTEdPUklUSE0QABIHCgNSU0EQARIRCg1FQ0NfU0VDUDI1NlIxEAISEQoNRUNDX1NFQ1AzODRSMRADEhEKDUVDQ19TRUNQNTIxUjEQBEIHCgVfdHlwZUIQCg5fc2VyaWFsX251bWJlckIYChZfY3JlYXRpb25fdGltZV9zZWNvbmRzQhoKGF9leHBpcmF0aW9uX3RpbWVfc2Vjb25kc0INCgtfcHVibGljX2tleUIMCgpfc3lzdGVtX2lkQhkKF190ZXN0X2RldmljZV9kZXByZWNhdGVkQg4KDF9wcm92aWRlcl9pZEIMCgpfYWxnb3JpdGhtQgkKB19yb3RfaWRCEQoPX2VuY3J5cHRpb25fa2V5IowCChRTaWduZWREcm1DZXJ0aWZpY2F0ZRIcCg9kcm1fY2VydGlmaWNhdGUYASABKAxIAIgBARIWCglzaWduYXR1cmUYAiABKAxIAYgBARI7CgZzaWduZXIYAyABKAsyJi5saWNlbnNlX3Byb3RvY29sLlNpZ25lZERybUNlcnRpZmljYXRlSAKIAQESQQoOaGFzaF9hbGdvcml0aG0YBCABKA4yJC5saWNlbnNlX3Byb3RvY29sLkhhc2hBbGdvcml0aG1Qcm90b0gDiAEBQhIKEF9kcm1fY2VydGlmaWNhdGVCDAoKX3NpZ25hdHVyZUIJCgdfc2lnbmVyQhEKD19oYXNoX2FsZ29yaXRobSK7CAoQV2lkZXZpbmVQc3NoRGF0YRIPCgdrZXlfaWRzGAIgAygMEhcKCmNvbnRlbnRfaWQYBCABKAxIAIgBARIgChNjcnlwdG9fcGVyaW9kX2luZGV4GAcgASgNSAGIAQESHgoRcHJvdGVjdGlvbl9zY2hlbWUYCSABKA1IAogBARIiChVjcnlwdG9fcGVyaW9kX3NlY29uZHMYCiABKA1IA4gBARI6CgR0eXBlGAsgASgOMicubGljZW5zZV9wcm90b2NvbC5XaWRldmluZVBzc2hEYXRhLlR5cGVIBIgBARIZCgxrZXlfc2VxdWVuY2UYDCABKA1IBYgBARIRCglncm91cF9pZHMYDSADKAwSRQoNZW50aXRsZWRfa2V5cxgOIAMoCzIuLmxpY2Vuc2VfcHJvdG9jb2wuV2lkZXZpbmVQc3NoRGF0YS5FbnRpdGxlZEtleRIaCg12aWRlb19mZWF0dXJlGA8gASgJSAaIAQESSAoJYWxnb3JpdGhtGAEgASgOMiwubGljZW5zZV9wcm90b2NvbC5XaWRldmluZVBzc2hEYXRhLkFsZ29yaXRobUICGAFIB4gBARIZCghwcm92aWRlchgDIAEoCUICGAFICIgBARIbCgp0cmFja190eXBlGAUgASgJQgIYAUgJiAEBEhcKBnBvbGljeRgGIAEoCUICGAFICogBARIgCg9ncm91cGVkX2xpY2Vuc2UYCCABKAxCAhgBSAuIAQEa3wEKC0VudGl0bGVkS2V5Eh8KEmVudGl0bGVtZW50X2tleV9pZBgBIAEoDEgAiAEBEhMKBmtleV9pZBgCIAEoDEgBiAEBEhAKA2tleRgDIAEoDEgCiAEBEg8KAml2GAQgASgMSAOIAQESJwoaZW50aXRsZW1lbnRfa2V5X3NpemVfYnl0ZXMYBSABKA1IBIgBAUIVChNfZW50aXRsZW1lbnRfa2V5X2lkQgkKB19rZXlfaWRCBgoEX2tleUIFCgNfaXZCHQobX2VudGl0bGVtZW50X2tleV9zaXplX2J5dGVzIjUKBFR5cGUSCgoGU0lOR0xFEAASDwoLRU5USVRMRU1FTlQQARIQCgxFTlRJVExFRF9LRVkQAiIoCglBbGdvcml0aG0SDwoLVU5FTkNSWVBURUQQABIKCgZBRVNDVFIQAUINCgtfY29udGVudF9pZEIWChRfY3J5cHRvX3BlcmlvZF9pbmRleEIUChJfcHJvdGVjdGlvbl9zY2hlbWVCGAoWX2NyeXB0b19wZXJpb2Rfc2Vjb25kc0IHCgVfdHlwZUIPCg1fa2V5X3NlcXVlbmNlQhAKDl92aWRlb19mZWF0dXJlQgwKCl9hbGdvcml0aG1CCwoJX3Byb3ZpZGVyQg0KC190cmFja190eXBlQgkKB19wb2xpY3lCEgoQX2dyb3VwZWRfbGljZW5zZSK4AgoKRmlsZUhhc2hlcxITCgZzaWduZXIYASABKAxIAIgBARI6CgpzaWduYXR1cmVzGAIgAygLMiYubGljZW5zZV9wcm90b2NvbC5GaWxlSGFzaGVzLlNpZ25hdHVyZRrNAQoJU2lnbmF0dXJlEhUKCGZpbGVuYW1lGAEgASgJSACIAQESGQoMdGVzdF9zaWduaW5nGAIgASgISAGIAQESFwoKU0hBNTEySGFzaBgDIAEoDEgCiAEBEhUKCG1haW5fZXhlGAQgASgISAOIAQESFgoJc2lnbmF0dXJlGAUgASgMSASIAQFCCwoJX2ZpbGVuYW1lQg8KDV90ZXN0X3NpZ25pbmdCDQoLX1NIQTUxMkhhc2hCCwoJX21haW5fZXhlQgwKCl9zaWduYXR1cmVCCQoHX3NpZ25lcipUCgtMaWNlbnNlVHlwZRIaChZMSUNFTlNFVFlQRV9VTlZFUklGSUVEEAASDQoJU1RSRUFNSU5HEAESCwoHT0ZGTElORRACEg0KCUFVVE9NQVRJQxADKtkBChpQbGF0Zm9ybVZlcmlmaWNhdGlvblN0YXR1cxIXChNQTEFURk9STV9VTlZFUklGSUVEEAASFQoRUExBVEZPUk1fVEFNUEVSRUQQARIeChpQTEFURk9STV9TT0ZUV0FSRV9WRVJJRklFRBACEh4KGlBMQVRGT1JNX0hBUkRXQVJFX1ZFUklGSUVEEAMSHAoYUExBVEZPUk1fTk9fVkVSSUZJQ0FUSU9OEAQSLQopUExBVEZPUk1fU0VDVVJFX1NUT1JBR0VfU09GVFdBUkVfVkVSSUZJRUQQBSpcCg9Qcm90b2NvbFZlcnNpb24SFgoSVkVSU0lPTl9VTlZFUklGSUVEEAASDwoLVkVSU0lPTl8yXzAQFBIPCgtWRVJTSU9OXzJfMRAVEg8KC1ZFUlNJT05fMl8yEBYqhgEKEkhhc2hBbGdvcml0aG1Qcm90bxIeChpIQVNIX0FMR09SSVRITV9VTlNQRUNJRklFRBAAEhgKFEhBU0hfQUxHT1JJVEhNX1NIQV8xEAESGgoWSEFTSF9BTEdPUklUSE1fU0hBXzI1NhACEhoKFkhBU0hfQUxHT1JJVEhNX1NIQV8zODQQA2IGcHJvdG8z' + ); /** * LicenseIdentification is propagated from LicenseRequest to License, @@ -22,35 +22,35 @@ export const file_license_protocol: GenFile = * @generated from message license_protocol.LicenseIdentification */ export type LicenseIdentification = Message<'license_protocol.LicenseIdentification'> & { - /** - * @generated from field: optional bytes request_id = 1; - */ - requestId?: Uint8Array; + /** + * @generated from field: optional bytes request_id = 1; + */ + requestId?: Uint8Array; - /** - * @generated from field: optional bytes session_id = 2; - */ - sessionId?: Uint8Array; + /** + * @generated from field: optional bytes session_id = 2; + */ + sessionId?: Uint8Array; - /** - * @generated from field: optional bytes purchase_id = 3; - */ - purchaseId?: Uint8Array; + /** + * @generated from field: optional bytes purchase_id = 3; + */ + purchaseId?: Uint8Array; - /** - * @generated from field: optional license_protocol.LicenseType type = 4; - */ - type?: LicenseType; + /** + * @generated from field: optional license_protocol.LicenseType type = 4; + */ + type?: LicenseType; - /** - * @generated from field: optional int32 version = 5; - */ - version?: number; + /** + * @generated from field: optional int32 version = 5; + */ + version?: number; - /** - * @generated from field: optional bytes provider_session_token = 6; - */ - providerSessionToken?: Uint8Array; + /** + * @generated from field: optional bytes provider_session_token = 6; + */ + providerSessionToken?: Uint8Array; }; /** @@ -63,84 +63,84 @@ export const LicenseIdentificationSchema: GenMessage<LicenseIdentification> = /* * @generated from message license_protocol.License */ export type License = Message<'license_protocol.License'> & { - /** - * @generated from field: optional license_protocol.LicenseIdentification id = 1; - */ - id?: LicenseIdentification; + /** + * @generated from field: optional license_protocol.LicenseIdentification id = 1; + */ + id?: LicenseIdentification; - /** - * @generated from field: optional license_protocol.License.Policy policy = 2; - */ - policy?: License_Policy; + /** + * @generated from field: optional license_protocol.License.Policy policy = 2; + */ + policy?: License_Policy; - /** - * @generated from field: repeated license_protocol.License.KeyContainer key = 3; - */ - key: License_KeyContainer[]; + /** + * @generated from field: repeated license_protocol.License.KeyContainer key = 3; + */ + key: License_KeyContainer[]; - /** - * Time of the request in seconds (UTC) as set in - * LicenseRequest.request_time. If this time is not set in the request, - * the local time at the license service is used in this field. - * - * @generated from field: optional int64 license_start_time = 4; - */ - licenseStartTime?: bigint; + /** + * Time of the request in seconds (UTC) as set in + * LicenseRequest.request_time. If this time is not set in the request, + * the local time at the license service is used in this field. + * + * @generated from field: optional int64 license_start_time = 4; + */ + licenseStartTime?: bigint; - /** - * @generated from field: optional bool remote_attestation_verified = 5; - */ - remoteAttestationVerified?: boolean; + /** + * @generated from field: optional bool remote_attestation_verified = 5; + */ + remoteAttestationVerified?: boolean; - /** - * Client token generated by the content provider. Optional. - * - * @generated from field: optional bytes provider_client_token = 6; - */ - providerClientToken?: Uint8Array; + /** + * Client token generated by the content provider. Optional. + * + * @generated from field: optional bytes provider_client_token = 6; + */ + providerClientToken?: Uint8Array; - /** - * 4cc code specifying the CENC protection scheme as defined in the CENC 3.0 - * specification. Propagated from Widevine PSSH box. Optional. - * - * @generated from field: optional uint32 protection_scheme = 7; - */ - protectionScheme?: number; + /** + * 4cc code specifying the CENC protection scheme as defined in the CENC 3.0 + * specification. Propagated from Widevine PSSH box. Optional. + * + * @generated from field: optional uint32 protection_scheme = 7; + */ + protectionScheme?: number; - /** - * 8 byte verification field "HDCPDATA" followed by unsigned 32 bit minimum - * HDCP SRM version (whether the version is for HDCP1 SRM or HDCP2 SRM - * depends on client max_hdcp_version). - * Additional details can be found in Widevine Modular DRM Security - * Integration Guide for CENC. - * - * @generated from field: optional bytes srm_requirement = 8; - */ - srmRequirement?: Uint8Array; + /** + * 8 byte verification field "HDCPDATA" followed by unsigned 32 bit minimum + * HDCP SRM version (whether the version is for HDCP1 SRM or HDCP2 SRM + * depends on client max_hdcp_version). + * Additional details can be found in Widevine Modular DRM Security + * Integration Guide for CENC. + * + * @generated from field: optional bytes srm_requirement = 8; + */ + srmRequirement?: Uint8Array; - /** - * If present this contains a signed SRM file (either HDCP1 SRM or HDCP2 SRM - * depending on client max_hdcp_version) that should be installed on the - * client device. - * - * @generated from field: optional bytes srm_update = 9; - */ - srmUpdate?: Uint8Array; + /** + * If present this contains a signed SRM file (either HDCP1 SRM or HDCP2 SRM + * depending on client max_hdcp_version) that should be installed on the + * client device. + * + * @generated from field: optional bytes srm_update = 9; + */ + srmUpdate?: Uint8Array; - /** - * Indicates the status of any type of platform verification performed by the - * server. - * - * @generated from field: optional license_protocol.PlatformVerificationStatus platform_verification_status = 10; - */ - platformVerificationStatus?: PlatformVerificationStatus; + /** + * Indicates the status of any type of platform verification performed by the + * server. + * + * @generated from field: optional license_protocol.PlatformVerificationStatus platform_verification_status = 10; + */ + platformVerificationStatus?: PlatformVerificationStatus; - /** - * IDs of the groups for which keys are delivered in this license, if any. - * - * @generated from field: repeated bytes group_ids = 11; - */ - groupIds: Uint8Array[]; + /** + * IDs of the groups for which keys are delivered in this license, if any. + * + * @generated from field: repeated bytes group_ids = 11; + */ + groupIds: Uint8Array[]; }; /** @@ -153,124 +153,124 @@ export const LicenseSchema: GenMessage<License> = /*@__PURE__*/ messageDesc(file * @generated from message license_protocol.License.Policy */ export type License_Policy = Message<'license_protocol.License.Policy'> & { - /** - * Indicates that playback of the content is allowed. - * - * @generated from field: optional bool can_play = 1; - */ - canPlay?: boolean; + /** + * Indicates that playback of the content is allowed. + * + * @generated from field: optional bool can_play = 1; + */ + canPlay?: boolean; - /** - * Indicates that the license may be persisted to non-volatile - * storage for offline use. - * - * @generated from field: optional bool can_persist = 2; - */ - canPersist?: boolean; + /** + * Indicates that the license may be persisted to non-volatile + * storage for offline use. + * + * @generated from field: optional bool can_persist = 2; + */ + canPersist?: boolean; - /** - * Indicates that renewal of this license is allowed. - * - * @generated from field: optional bool can_renew = 3; - */ - canRenew?: boolean; + /** + * Indicates that renewal of this license is allowed. + * + * @generated from field: optional bool can_renew = 3; + */ + canRenew?: boolean; - /** - * Indicates the rental window. - * - * @generated from field: optional int64 rental_duration_seconds = 4; - */ - rentalDurationSeconds?: bigint; + /** + * Indicates the rental window. + * + * @generated from field: optional int64 rental_duration_seconds = 4; + */ + rentalDurationSeconds?: bigint; - /** - * Indicates the viewing window, once playback has begun. - * - * @generated from field: optional int64 playback_duration_seconds = 5; - */ - playbackDurationSeconds?: bigint; + /** + * Indicates the viewing window, once playback has begun. + * + * @generated from field: optional int64 playback_duration_seconds = 5; + */ + playbackDurationSeconds?: bigint; - /** - * Indicates the time window for this specific license. - * - * @generated from field: optional int64 license_duration_seconds = 6; - */ - licenseDurationSeconds?: bigint; + /** + * Indicates the time window for this specific license. + * + * @generated from field: optional int64 license_duration_seconds = 6; + */ + licenseDurationSeconds?: bigint; - /** - * The window of time, in which playback is allowed to continue while - * renewal is attempted, yet unsuccessful due to backend problems with - * the license server. - * - * @generated from field: optional int64 renewal_recovery_duration_seconds = 7; - */ - renewalRecoveryDurationSeconds?: bigint; + /** + * The window of time, in which playback is allowed to continue while + * renewal is attempted, yet unsuccessful due to backend problems with + * the license server. + * + * @generated from field: optional int64 renewal_recovery_duration_seconds = 7; + */ + renewalRecoveryDurationSeconds?: bigint; - /** - * All renewal requests for this license shall be directed to the - * specified URL. - * - * @generated from field: optional string renewal_server_url = 8; - */ - renewalServerUrl?: string; + /** + * All renewal requests for this license shall be directed to the + * specified URL. + * + * @generated from field: optional string renewal_server_url = 8; + */ + renewalServerUrl?: string; - /** - * How many seconds after license_start_time, before renewal is first - * attempted. - * - * @generated from field: optional int64 renewal_delay_seconds = 9; - */ - renewalDelaySeconds?: bigint; + /** + * How many seconds after license_start_time, before renewal is first + * attempted. + * + * @generated from field: optional int64 renewal_delay_seconds = 9; + */ + renewalDelaySeconds?: bigint; - /** - * Specifies the delay in seconds between subsequent license - * renewal requests, in case of failure. - * - * @generated from field: optional int64 renewal_retry_interval_seconds = 10; - */ - renewalRetryIntervalSeconds?: bigint; + /** + * Specifies the delay in seconds between subsequent license + * renewal requests, in case of failure. + * + * @generated from field: optional int64 renewal_retry_interval_seconds = 10; + */ + renewalRetryIntervalSeconds?: bigint; - /** - * Indicates that the license shall be sent for renewal when usage is - * started. - * - * @generated from field: optional bool renew_with_usage = 11; - */ - renewWithUsage?: boolean; + /** + * Indicates that the license shall be sent for renewal when usage is + * started. + * + * @generated from field: optional bool renew_with_usage = 11; + */ + renewWithUsage?: boolean; - /** - * Indicates to client that license renewal and release requests ought to - * include ClientIdentification (client_id). - * - * @generated from field: optional bool always_include_client_id = 12; - */ - alwaysIncludeClientId?: boolean; + /** + * Indicates to client that license renewal and release requests ought to + * include ClientIdentification (client_id). + * + * @generated from field: optional bool always_include_client_id = 12; + */ + alwaysIncludeClientId?: boolean; - /** - * Duration of grace period before playback_duration_seconds (short window) - * goes into effect. Optional. - * - * @generated from field: optional int64 play_start_grace_period_seconds = 13; - */ - playStartGracePeriodSeconds?: bigint; + /** + * Duration of grace period before playback_duration_seconds (short window) + * goes into effect. Optional. + * + * @generated from field: optional int64 play_start_grace_period_seconds = 13; + */ + playStartGracePeriodSeconds?: bigint; - /** - * Enables "soft enforcement" of playback_duration_seconds, letting the user - * finish playback even if short window expires. Optional. - * - * @generated from field: optional bool soft_enforce_playback_duration = 14; - */ - softEnforcePlaybackDuration?: boolean; + /** + * Enables "soft enforcement" of playback_duration_seconds, letting the user + * finish playback even if short window expires. Optional. + * + * @generated from field: optional bool soft_enforce_playback_duration = 14; + */ + softEnforcePlaybackDuration?: boolean; - /** - * Enables "soft enforcement" of rental_duration_seconds. Initial playback - * must always start before rental duration expires. In order to allow - * subsequent playbacks to start after the rental duration expires, - * soft_enforce_playback_duration must be true. Otherwise, subsequent - * playbacks will not be allowed once rental duration expires. Optional. - * - * @generated from field: optional bool soft_enforce_rental_duration = 15; - */ - softEnforceRentalDuration?: boolean; + /** + * Enables "soft enforcement" of rental_duration_seconds. Initial playback + * must always start before rental duration expires. In order to allow + * subsequent playbacks to start after the rental duration expires, + * soft_enforce_playback_duration must be true. Otherwise, subsequent + * playbacks will not be allowed once rental duration expires. Optional. + * + * @generated from field: optional bool soft_enforce_rental_duration = 15; + */ + softEnforceRentalDuration?: boolean; }; /** @@ -283,82 +283,82 @@ export const License_PolicySchema: GenMessage<License_Policy> = /*@__PURE__*/ me * @generated from message license_protocol.License.KeyContainer */ export type License_KeyContainer = Message<'license_protocol.License.KeyContainer'> & { - /** - * @generated from field: optional bytes id = 1; - */ - id?: Uint8Array; + /** + * @generated from field: optional bytes id = 1; + */ + id?: Uint8Array; - /** - * @generated from field: optional bytes iv = 2; - */ - iv?: Uint8Array; + /** + * @generated from field: optional bytes iv = 2; + */ + iv?: Uint8Array; - /** - * @generated from field: optional bytes key = 3; - */ - key?: Uint8Array; + /** + * @generated from field: optional bytes key = 3; + */ + key?: Uint8Array; - /** - * @generated from field: optional license_protocol.License.KeyContainer.KeyType type = 4; - */ - type?: License_KeyContainer_KeyType; + /** + * @generated from field: optional license_protocol.License.KeyContainer.KeyType type = 4; + */ + type?: License_KeyContainer_KeyType; - /** - * @generated from field: optional license_protocol.License.KeyContainer.SecurityLevel level = 5; - */ - level?: License_KeyContainer_SecurityLevel; + /** + * @generated from field: optional license_protocol.License.KeyContainer.SecurityLevel level = 5; + */ + level?: License_KeyContainer_SecurityLevel; - /** - * @generated from field: optional license_protocol.License.KeyContainer.OutputProtection required_protection = 6; - */ - requiredProtection?: License_KeyContainer_OutputProtection; + /** + * @generated from field: optional license_protocol.License.KeyContainer.OutputProtection required_protection = 6; + */ + requiredProtection?: License_KeyContainer_OutputProtection; - /** - * NOTE: Use of requested_protection is not recommended as it is only - * supported on a small number of platforms. - * - * @generated from field: optional license_protocol.License.KeyContainer.OutputProtection requested_protection = 7; - */ - requestedProtection?: License_KeyContainer_OutputProtection; + /** + * NOTE: Use of requested_protection is not recommended as it is only + * supported on a small number of platforms. + * + * @generated from field: optional license_protocol.License.KeyContainer.OutputProtection requested_protection = 7; + */ + requestedProtection?: License_KeyContainer_OutputProtection; - /** - * @generated from field: optional license_protocol.License.KeyContainer.KeyControl key_control = 8; - */ - keyControl?: License_KeyContainer_KeyControl; + /** + * @generated from field: optional license_protocol.License.KeyContainer.KeyControl key_control = 8; + */ + keyControl?: License_KeyContainer_KeyControl; - /** - * @generated from field: optional license_protocol.License.KeyContainer.OperatorSessionKeyPermissions operator_session_key_permissions = 9; - */ - operatorSessionKeyPermissions?: License_KeyContainer_OperatorSessionKeyPermissions; + /** + * @generated from field: optional license_protocol.License.KeyContainer.OperatorSessionKeyPermissions operator_session_key_permissions = 9; + */ + operatorSessionKeyPermissions?: License_KeyContainer_OperatorSessionKeyPermissions; - /** - * Optional video resolution constraints. If the video resolution of the - * content being decrypted/decoded falls within one of the specified ranges, - * the optional required_protections may be applied. Otherwise an error will - * be reported. - * NOTE: Use of this feature is not recommended, as it is only supported on - * a small number of platforms. - * - * @generated from field: repeated license_protocol.License.KeyContainer.VideoResolutionConstraint video_resolution_constraints = 10; - */ - videoResolutionConstraints: License_KeyContainer_VideoResolutionConstraint[]; + /** + * Optional video resolution constraints. If the video resolution of the + * content being decrypted/decoded falls within one of the specified ranges, + * the optional required_protections may be applied. Otherwise an error will + * be reported. + * NOTE: Use of this feature is not recommended, as it is only supported on + * a small number of platforms. + * + * @generated from field: repeated license_protocol.License.KeyContainer.VideoResolutionConstraint video_resolution_constraints = 10; + */ + videoResolutionConstraints: License_KeyContainer_VideoResolutionConstraint[]; - /** - * Optional flag to indicate the key must only be used if the client - * supports anti rollback of the user table. Content provider can query the - * client capabilities to determine if the client support this feature. - * - * @generated from field: optional bool anti_rollback_usage_table = 11; - */ - antiRollbackUsageTable?: boolean; + /** + * Optional flag to indicate the key must only be used if the client + * supports anti rollback of the user table. Content provider can query the + * client capabilities to determine if the client support this feature. + * + * @generated from field: optional bool anti_rollback_usage_table = 11; + */ + antiRollbackUsageTable?: boolean; - /** - * Optional not limited to commonly known track types such as SD, HD. - * It can be some provider defined label to identify the track. - * - * @generated from field: optional string track_label = 12; - */ - trackLabel?: string; + /** + * Optional not limited to commonly known track types such as SD, HD. + * It can be some provider defined label to identify the track. + * + * @generated from field: optional string track_label = 12; + */ + trackLabel?: string; }; /** @@ -371,21 +371,21 @@ export const License_KeyContainerSchema: GenMessage<License_KeyContainer> = /*@_ * @generated from message license_protocol.License.KeyContainer.KeyControl */ export type License_KeyContainer_KeyControl = Message<'license_protocol.License.KeyContainer.KeyControl'> & { - /** - * |key_control| is documented in: - * Widevine Modular DRM Security Integration Guide for CENC - * If present, the key control must be communicated to the secure - * environment prior to any usage. This message is automatically generated - * by the Widevine License Server SDK. - * - * @generated from field: optional bytes key_control_block = 1; - */ - keyControlBlock?: Uint8Array; + /** + * |key_control| is documented in: + * Widevine Modular DRM Security Integration Guide for CENC + * If present, the key control must be communicated to the secure + * environment prior to any usage. This message is automatically generated + * by the Widevine License Server SDK. + * + * @generated from field: optional bytes key_control_block = 1; + */ + keyControlBlock?: Uint8Array; - /** - * @generated from field: optional bytes iv = 2; - */ - iv?: Uint8Array; + /** + * @generated from field: optional bytes iv = 2; + */ + iv?: Uint8Array; }; /** @@ -398,34 +398,34 @@ export const License_KeyContainer_KeyControlSchema: GenMessage<License_KeyContai * @generated from message license_protocol.License.KeyContainer.OutputProtection */ export type License_KeyContainer_OutputProtection = Message<'license_protocol.License.KeyContainer.OutputProtection'> & { - /** - * @generated from field: optional license_protocol.License.KeyContainer.OutputProtection.HDCP hdcp = 1; - */ - hdcp?: License_KeyContainer_OutputProtection_HDCP; + /** + * @generated from field: optional license_protocol.License.KeyContainer.OutputProtection.HDCP hdcp = 1; + */ + hdcp?: License_KeyContainer_OutputProtection_HDCP; - /** - * @generated from field: optional license_protocol.License.KeyContainer.OutputProtection.CGMS cgms_flags = 2; - */ - cgmsFlags?: License_KeyContainer_OutputProtection_CGMS; + /** + * @generated from field: optional license_protocol.License.KeyContainer.OutputProtection.CGMS cgms_flags = 2; + */ + cgmsFlags?: License_KeyContainer_OutputProtection_CGMS; - /** - * @generated from field: optional license_protocol.License.KeyContainer.OutputProtection.HdcpSrmRule hdcp_srm_rule = 3; - */ - hdcpSrmRule?: License_KeyContainer_OutputProtection_HdcpSrmRule; + /** + * @generated from field: optional license_protocol.License.KeyContainer.OutputProtection.HdcpSrmRule hdcp_srm_rule = 3; + */ + hdcpSrmRule?: License_KeyContainer_OutputProtection_HdcpSrmRule; - /** - * Optional requirement to indicate analog output is not allowed. - * - * @generated from field: optional bool disable_analog_output = 4; - */ - disableAnalogOutput?: boolean; + /** + * Optional requirement to indicate analog output is not allowed. + * + * @generated from field: optional bool disable_analog_output = 4; + */ + disableAnalogOutput?: boolean; - /** - * Optional requirement to indicate digital output is not allowed. - * - * @generated from field: optional bool disable_digital_output = 5; - */ - disableDigitalOutput?: boolean; + /** + * Optional requirement to indicate digital output is not allowed. + * + * @generated from field: optional bool disable_digital_output = 5; + */ + disableDigitalOutput?: boolean; }; /** @@ -441,40 +441,40 @@ export const License_KeyContainer_OutputProtectionSchema: GenMessage<License_Key * @generated from enum license_protocol.License.KeyContainer.OutputProtection.HDCP */ export enum License_KeyContainer_OutputProtection_HDCP { - /** - * @generated from enum value: HDCP_NONE = 0; - */ - HDCP_NONE = 0, + /** + * @generated from enum value: HDCP_NONE = 0; + */ + HDCP_NONE = 0, - /** - * @generated from enum value: HDCP_V1 = 1; - */ - HDCP_V1 = 1, + /** + * @generated from enum value: HDCP_V1 = 1; + */ + HDCP_V1 = 1, - /** - * @generated from enum value: HDCP_V2 = 2; - */ - HDCP_V2 = 2, + /** + * @generated from enum value: HDCP_V2 = 2; + */ + HDCP_V2 = 2, - /** - * @generated from enum value: HDCP_V2_1 = 3; - */ - HDCP_V2_1 = 3, + /** + * @generated from enum value: HDCP_V2_1 = 3; + */ + HDCP_V2_1 = 3, - /** - * @generated from enum value: HDCP_V2_2 = 4; - */ - HDCP_V2_2 = 4, + /** + * @generated from enum value: HDCP_V2_2 = 4; + */ + HDCP_V2_2 = 4, - /** - * @generated from enum value: HDCP_V2_3 = 5; - */ - HDCP_V2_3 = 5, + /** + * @generated from enum value: HDCP_V2_3 = 5; + */ + HDCP_V2_3 = 5, - /** - * @generated from enum value: HDCP_NO_DIGITAL_OUTPUT = 255; - */ - HDCP_NO_DIGITAL_OUTPUT = 255 + /** + * @generated from enum value: HDCP_NO_DIGITAL_OUTPUT = 255; + */ + HDCP_NO_DIGITAL_OUTPUT = 255 } /** @@ -488,25 +488,25 @@ export const License_KeyContainer_OutputProtection_HDCPSchema: GenEnum<License_K * @generated from enum license_protocol.License.KeyContainer.OutputProtection.CGMS */ export enum License_KeyContainer_OutputProtection_CGMS { - /** - * @generated from enum value: COPY_FREE = 0; - */ - COPY_FREE = 0, + /** + * @generated from enum value: COPY_FREE = 0; + */ + COPY_FREE = 0, - /** - * @generated from enum value: CGMS_NONE = 42; - */ - CGMS_NONE = 42, + /** + * @generated from enum value: CGMS_NONE = 42; + */ + CGMS_NONE = 42, - /** - * @generated from enum value: COPY_ONCE = 2; - */ - COPY_ONCE = 2, + /** + * @generated from enum value: COPY_ONCE = 2; + */ + COPY_ONCE = 2, - /** - * @generated from enum value: COPY_NEVER = 3; - */ - COPY_NEVER = 3 + /** + * @generated from enum value: COPY_NEVER = 3; + */ + COPY_NEVER = 3 } /** @@ -518,54 +518,54 @@ export const License_KeyContainer_OutputProtection_CGMSSchema: GenEnum<License_K * @generated from enum license_protocol.License.KeyContainer.OutputProtection.HdcpSrmRule */ export enum License_KeyContainer_OutputProtection_HdcpSrmRule { - /** - * @generated from enum value: HDCP_SRM_RULE_NONE = 0; - */ - HDCP_SRM_RULE_NONE = 0, + /** + * @generated from enum value: HDCP_SRM_RULE_NONE = 0; + */ + HDCP_SRM_RULE_NONE = 0, - /** - * In 'required_protection', this means most current SRM is required. - * Update the SRM on the device. If update cannot happen, - * do not allow the key. - * In 'requested_protection', this means most current SRM is requested. - * Update the SRM on the device. If update cannot happen, - * allow use of the key anyway. - * - * @generated from enum value: CURRENT_SRM = 1; - */ - CURRENT_SRM = 1 + /** + * In 'required_protection', this means most current SRM is required. + * Update the SRM on the device. If update cannot happen, + * do not allow the key. + * In 'requested_protection', this means most current SRM is requested. + * Update the SRM on the device. If update cannot happen, + * allow use of the key anyway. + * + * @generated from enum value: CURRENT_SRM = 1; + */ + CURRENT_SRM = 1 } /** * Describes the enum license_protocol.License.KeyContainer.OutputProtection.HdcpSrmRule. */ export const License_KeyContainer_OutputProtection_HdcpSrmRuleSchema: GenEnum<License_KeyContainer_OutputProtection_HdcpSrmRule> = - /*@__PURE__*/ - enumDesc(file_license_protocol, 1, 1, 1, 2); + /*@__PURE__*/ + enumDesc(file_license_protocol, 1, 1, 1, 2); /** * @generated from message license_protocol.License.KeyContainer.VideoResolutionConstraint */ export type License_KeyContainer_VideoResolutionConstraint = Message<'license_protocol.License.KeyContainer.VideoResolutionConstraint'> & { - /** - * Minimum and maximum video resolutions in the range (height x width). - * - * @generated from field: optional uint32 min_resolution_pixels = 1; - */ - minResolutionPixels?: number; + /** + * Minimum and maximum video resolutions in the range (height x width). + * + * @generated from field: optional uint32 min_resolution_pixels = 1; + */ + minResolutionPixels?: number; - /** - * @generated from field: optional uint32 max_resolution_pixels = 2; - */ - maxResolutionPixels?: number; + /** + * @generated from field: optional uint32 max_resolution_pixels = 2; + */ + maxResolutionPixels?: number; - /** - * Optional output protection requirements for this range. If not - * specified, the OutputProtection in the KeyContainer applies. - * - * @generated from field: optional license_protocol.License.KeyContainer.OutputProtection required_protection = 3; - */ - requiredProtection?: License_KeyContainer_OutputProtection; + /** + * Optional output protection requirements for this range. If not + * specified, the OutputProtection in the KeyContainer applies. + * + * @generated from field: optional license_protocol.License.KeyContainer.OutputProtection required_protection = 3; + */ + requiredProtection?: License_KeyContainer_OutputProtection; }; /** @@ -573,35 +573,35 @@ export type License_KeyContainer_VideoResolutionConstraint = Message<'license_pr * Use `create(License_KeyContainer_VideoResolutionConstraintSchema)` to create a new message. */ export const License_KeyContainer_VideoResolutionConstraintSchema: GenMessage<License_KeyContainer_VideoResolutionConstraint> = - /*@__PURE__*/ - messageDesc(file_license_protocol, 1, 1, 2); + /*@__PURE__*/ + messageDesc(file_license_protocol, 1, 1, 2); /** * @generated from message license_protocol.License.KeyContainer.OperatorSessionKeyPermissions */ export type License_KeyContainer_OperatorSessionKeyPermissions = Message<'license_protocol.License.KeyContainer.OperatorSessionKeyPermissions'> & { - /** - * Permissions/key usage flags for operator service keys - * (type = OPERATOR_SESSION). - * - * @generated from field: optional bool allow_encrypt = 1; - */ - allowEncrypt?: boolean; + /** + * Permissions/key usage flags for operator service keys + * (type = OPERATOR_SESSION). + * + * @generated from field: optional bool allow_encrypt = 1; + */ + allowEncrypt?: boolean; - /** - * @generated from field: optional bool allow_decrypt = 2; - */ - allowDecrypt?: boolean; + /** + * @generated from field: optional bool allow_decrypt = 2; + */ + allowDecrypt?: boolean; - /** - * @generated from field: optional bool allow_sign = 3; - */ - allowSign?: boolean; + /** + * @generated from field: optional bool allow_sign = 3; + */ + allowSign?: boolean; - /** - * @generated from field: optional bool allow_signature_verify = 4; - */ - allowSignatureVerify?: boolean; + /** + * @generated from field: optional bool allow_signature_verify = 4; + */ + allowSignatureVerify?: boolean; }; /** @@ -609,59 +609,59 @@ export type License_KeyContainer_OperatorSessionKeyPermissions = Message<'licens * Use `create(License_KeyContainer_OperatorSessionKeyPermissionsSchema)` to create a new message. */ export const License_KeyContainer_OperatorSessionKeyPermissionsSchema: GenMessage<License_KeyContainer_OperatorSessionKeyPermissions> = - /*@__PURE__*/ - messageDesc(file_license_protocol, 1, 1, 3); + /*@__PURE__*/ + messageDesc(file_license_protocol, 1, 1, 3); /** * @generated from enum license_protocol.License.KeyContainer.KeyType */ export enum License_KeyContainer_KeyType { - /** - * @generated from enum value: KEYTYPE_UNVERIFIED = 0; - */ - KEYTYPE_UNVERIFIED = 0, + /** + * @generated from enum value: KEYTYPE_UNVERIFIED = 0; + */ + KEYTYPE_UNVERIFIED = 0, - /** - * Exactly one key of this type must appear. - * - * @generated from enum value: SIGNING = 1; - */ - SIGNING = 1, + /** + * Exactly one key of this type must appear. + * + * @generated from enum value: SIGNING = 1; + */ + SIGNING = 1, - /** - * Content key. - * - * @generated from enum value: CONTENT = 2; - */ - CONTENT = 2, + /** + * Content key. + * + * @generated from enum value: CONTENT = 2; + */ + CONTENT = 2, - /** - * Key control block for license renewals. No key. - * - * @generated from enum value: KEY_CONTROL = 3; - */ - KEY_CONTROL = 3, + /** + * Key control block for license renewals. No key. + * + * @generated from enum value: KEY_CONTROL = 3; + */ + KEY_CONTROL = 3, - /** - * wrapped keys for auxiliary crypto operations. - * - * @generated from enum value: OPERATOR_SESSION = 4; - */ - OPERATOR_SESSION = 4, + /** + * wrapped keys for auxiliary crypto operations. + * + * @generated from enum value: OPERATOR_SESSION = 4; + */ + OPERATOR_SESSION = 4, - /** - * Entitlement keys. - * - * @generated from enum value: ENTITLEMENT = 5; - */ - ENTITLEMENT = 5, + /** + * Entitlement keys. + * + * @generated from enum value: ENTITLEMENT = 5; + */ + ENTITLEMENT = 5, - /** - * Partner-specific content key. - * - * @generated from enum value: OEM_CONTENT = 6; - */ - OEM_CONTENT = 6 + /** + * Partner-specific content key. + * + * @generated from enum value: OEM_CONTENT = 6; + */ + OEM_CONTENT = 6 } /** @@ -676,49 +676,49 @@ export const License_KeyContainer_KeyTypeSchema: GenEnum<License_KeyContainer_Ke * @generated from enum license_protocol.License.KeyContainer.SecurityLevel */ export enum License_KeyContainer_SecurityLevel { - /** - * @generated from enum value: SECURITYLEVEL_UNVERIFIED = 0; - */ - SECURITYLEVEL_UNVERIFIED = 0, + /** + * @generated from enum value: SECURITYLEVEL_UNVERIFIED = 0; + */ + SECURITYLEVEL_UNVERIFIED = 0, - /** - * Software-based whitebox crypto is required. - * - * @generated from enum value: SW_SECURE_CRYPTO = 1; - */ - SW_SECURE_CRYPTO = 1, + /** + * Software-based whitebox crypto is required. + * + * @generated from enum value: SW_SECURE_CRYPTO = 1; + */ + SW_SECURE_CRYPTO = 1, - /** - * Software crypto and an obfuscated decoder is required. - * - * @generated from enum value: SW_SECURE_DECODE = 2; - */ - SW_SECURE_DECODE = 2, + /** + * Software crypto and an obfuscated decoder is required. + * + * @generated from enum value: SW_SECURE_DECODE = 2; + */ + SW_SECURE_DECODE = 2, - /** - * The key material and crypto operations must be performed within a - * hardware backed trusted execution environment. - * - * @generated from enum value: HW_SECURE_CRYPTO = 3; - */ - HW_SECURE_CRYPTO = 3, + /** + * The key material and crypto operations must be performed within a + * hardware backed trusted execution environment. + * + * @generated from enum value: HW_SECURE_CRYPTO = 3; + */ + HW_SECURE_CRYPTO = 3, - /** - * The crypto and decoding of content must be performed within a hardware - * backed trusted execution environment. - * - * @generated from enum value: HW_SECURE_DECODE = 4; - */ - HW_SECURE_DECODE = 4, + /** + * The crypto and decoding of content must be performed within a hardware + * backed trusted execution environment. + * + * @generated from enum value: HW_SECURE_DECODE = 4; + */ + HW_SECURE_DECODE = 4, - /** - * The crypto, decoding and all handling of the media (compressed and - * uncompressed) must be handled within a hardware backed trusted - * execution environment. - * - * @generated from enum value: HW_SECURE_ALL = 5; - */ - HW_SECURE_ALL = 5 + /** + * The crypto, decoding and all handling of the media (compressed and + * uncompressed) must be handled within a hardware backed trusted + * execution environment. + * + * @generated from enum value: HW_SECURE_ALL = 5; + */ + HW_SECURE_ALL = 5 } /** @@ -730,59 +730,59 @@ export const License_KeyContainer_SecurityLevelSchema: GenEnum<License_KeyContai * @generated from message license_protocol.LicenseRequest */ export type LicenseRequest = Message<'license_protocol.LicenseRequest'> & { - /** - * The client_id provides information authenticating the calling device. It - * contains the Widevine keybox token that was installed on the device at the - * factory. This field or encrypted_client_id below is required for a valid - * license request, but both should never be present in the same request. - * - * @generated from field: optional license_protocol.ClientIdentification client_id = 1; - */ - clientId?: ClientIdentification; + /** + * The client_id provides information authenticating the calling device. It + * contains the Widevine keybox token that was installed on the device at the + * factory. This field or encrypted_client_id below is required for a valid + * license request, but both should never be present in the same request. + * + * @generated from field: optional license_protocol.ClientIdentification client_id = 1; + */ + clientId?: ClientIdentification; - /** - * @generated from field: optional license_protocol.LicenseRequest.ContentIdentification content_id = 2; - */ - contentId?: LicenseRequest_ContentIdentification; + /** + * @generated from field: optional license_protocol.LicenseRequest.ContentIdentification content_id = 2; + */ + contentId?: LicenseRequest_ContentIdentification; - /** - * @generated from field: optional license_protocol.LicenseRequest.RequestType type = 3; - */ - type?: LicenseRequest_RequestType; + /** + * @generated from field: optional license_protocol.LicenseRequest.RequestType type = 3; + */ + type?: LicenseRequest_RequestType; - /** - * Time of the request in seconds (UTC) as set by the client. - * - * @generated from field: optional int64 request_time = 4; - */ - requestTime?: bigint; + /** + * Time of the request in seconds (UTC) as set by the client. + * + * @generated from field: optional int64 request_time = 4; + */ + requestTime?: bigint; - /** - * Old-style decimal-encoded string key control nonce. - * - * @generated from field: optional bytes key_control_nonce_deprecated = 5; - */ - keyControlNonceDeprecated?: Uint8Array; + /** + * Old-style decimal-encoded string key control nonce. + * + * @generated from field: optional bytes key_control_nonce_deprecated = 5; + */ + keyControlNonceDeprecated?: Uint8Array; - /** - * @generated from field: optional license_protocol.ProtocolVersion protocol_version = 6; - */ - protocolVersion?: ProtocolVersion; + /** + * @generated from field: optional license_protocol.ProtocolVersion protocol_version = 6; + */ + protocolVersion?: ProtocolVersion; - /** - * New-style uint32 key control nonce, please use instead of - * key_control_nonce_deprecated. - * - * @generated from field: optional uint32 key_control_nonce = 7; - */ - keyControlNonce?: number; + /** + * New-style uint32 key control nonce, please use instead of + * key_control_nonce_deprecated. + * + * @generated from field: optional uint32 key_control_nonce = 7; + */ + keyControlNonce?: number; - /** - * Encrypted ClientIdentification message, used for privacy purposes. - * - * @generated from field: optional license_protocol.EncryptedClientIdentification encrypted_client_id = 8; - */ - encryptedClientId?: EncryptedClientIdentification; + /** + * Encrypted ClientIdentification message, used for privacy purposes. + * + * @generated from field: optional license_protocol.EncryptedClientIdentification encrypted_client_id = 8; + */ + encryptedClientId?: EncryptedClientIdentification; }; /** @@ -795,41 +795,41 @@ export const LicenseRequestSchema: GenMessage<LicenseRequest> = /*@__PURE__*/ me * @generated from message license_protocol.LicenseRequest.ContentIdentification */ export type LicenseRequest_ContentIdentification = Message<'license_protocol.LicenseRequest.ContentIdentification'> & { - /** - * @generated from oneof license_protocol.LicenseRequest.ContentIdentification.content_id_variant - */ - contentIdVariant: - | { - /** - * Exactly one of these must be present. - * - * @generated from field: license_protocol.LicenseRequest.ContentIdentification.WidevinePsshData widevine_pssh_data = 1; - */ - value: LicenseRequest_ContentIdentification_WidevinePsshData; - case: 'widevinePsshData'; - } - | { - /** - * @generated from field: license_protocol.LicenseRequest.ContentIdentification.WebmKeyId webm_key_id = 2; - */ - value: LicenseRequest_ContentIdentification_WebmKeyId; - case: 'webmKeyId'; - } - | { - /** - * @generated from field: license_protocol.LicenseRequest.ContentIdentification.ExistingLicense existing_license = 3; - */ - value: LicenseRequest_ContentIdentification_ExistingLicense; - case: 'existingLicense'; - } - | { - /** - * @generated from field: license_protocol.LicenseRequest.ContentIdentification.InitData init_data = 4; - */ - value: LicenseRequest_ContentIdentification_InitData; - case: 'initData'; - } - | { case: undefined; value?: undefined }; + /** + * @generated from oneof license_protocol.LicenseRequest.ContentIdentification.content_id_variant + */ + contentIdVariant: + | { + /** + * Exactly one of these must be present. + * + * @generated from field: license_protocol.LicenseRequest.ContentIdentification.WidevinePsshData widevine_pssh_data = 1; + */ + value: LicenseRequest_ContentIdentification_WidevinePsshData; + case: 'widevinePsshData'; + } + | { + /** + * @generated from field: license_protocol.LicenseRequest.ContentIdentification.WebmKeyId webm_key_id = 2; + */ + value: LicenseRequest_ContentIdentification_WebmKeyId; + case: 'webmKeyId'; + } + | { + /** + * @generated from field: license_protocol.LicenseRequest.ContentIdentification.ExistingLicense existing_license = 3; + */ + value: LicenseRequest_ContentIdentification_ExistingLicense; + case: 'existingLicense'; + } + | { + /** + * @generated from field: license_protocol.LicenseRequest.ContentIdentification.InitData init_data = 4; + */ + value: LicenseRequest_ContentIdentification_InitData; + case: 'initData'; + } + | { case: undefined; value?: undefined }; }; /** @@ -842,22 +842,22 @@ export const LicenseRequest_ContentIdentificationSchema: GenMessage<LicenseReque * @generated from message license_protocol.LicenseRequest.ContentIdentification.WidevinePsshData */ export type LicenseRequest_ContentIdentification_WidevinePsshData = Message<'license_protocol.LicenseRequest.ContentIdentification.WidevinePsshData'> & { - /** - * @generated from field: repeated bytes pssh_data = 1; - */ - psshData: Uint8Array[]; + /** + * @generated from field: repeated bytes pssh_data = 1; + */ + psshData: Uint8Array[]; - /** - * @generated from field: optional license_protocol.LicenseType license_type = 2; - */ - licenseType?: LicenseType; + /** + * @generated from field: optional license_protocol.LicenseType license_type = 2; + */ + licenseType?: LicenseType; - /** - * Opaque, client-specified. - * - * @generated from field: optional bytes request_id = 3; - */ - requestId?: Uint8Array; + /** + * Opaque, client-specified. + * + * @generated from field: optional bytes request_id = 3; + */ + requestId?: Uint8Array; }; /** @@ -865,29 +865,29 @@ export type LicenseRequest_ContentIdentification_WidevinePsshData = Message<'lic * Use `create(LicenseRequest_ContentIdentification_WidevinePsshDataSchema)` to create a new message. */ export const LicenseRequest_ContentIdentification_WidevinePsshDataSchema: GenMessage<LicenseRequest_ContentIdentification_WidevinePsshData> = - /*@__PURE__*/ - messageDesc(file_license_protocol, 2, 0, 0); + /*@__PURE__*/ + messageDesc(file_license_protocol, 2, 0, 0); /** * @generated from message license_protocol.LicenseRequest.ContentIdentification.WebmKeyId */ export type LicenseRequest_ContentIdentification_WebmKeyId = Message<'license_protocol.LicenseRequest.ContentIdentification.WebmKeyId'> & { - /** - * @generated from field: optional bytes header = 1; - */ - header?: Uint8Array; + /** + * @generated from field: optional bytes header = 1; + */ + header?: Uint8Array; - /** - * @generated from field: optional license_protocol.LicenseType license_type = 2; - */ - licenseType?: LicenseType; + /** + * @generated from field: optional license_protocol.LicenseType license_type = 2; + */ + licenseType?: LicenseType; - /** - * Opaque, client-specified. - * - * @generated from field: optional bytes request_id = 3; - */ - requestId?: Uint8Array; + /** + * Opaque, client-specified. + * + * @generated from field: optional bytes request_id = 3; + */ + requestId?: Uint8Array; }; /** @@ -895,32 +895,32 @@ export type LicenseRequest_ContentIdentification_WebmKeyId = Message<'license_pr * Use `create(LicenseRequest_ContentIdentification_WebmKeyIdSchema)` to create a new message. */ export const LicenseRequest_ContentIdentification_WebmKeyIdSchema: GenMessage<LicenseRequest_ContentIdentification_WebmKeyId> = - /*@__PURE__*/ - messageDesc(file_license_protocol, 2, 0, 1); + /*@__PURE__*/ + messageDesc(file_license_protocol, 2, 0, 1); /** * @generated from message license_protocol.LicenseRequest.ContentIdentification.ExistingLicense */ export type LicenseRequest_ContentIdentification_ExistingLicense = Message<'license_protocol.LicenseRequest.ContentIdentification.ExistingLicense'> & { - /** - * @generated from field: optional license_protocol.LicenseIdentification license_id = 1; - */ - licenseId?: LicenseIdentification; + /** + * @generated from field: optional license_protocol.LicenseIdentification license_id = 1; + */ + licenseId?: LicenseIdentification; - /** - * @generated from field: optional int64 seconds_since_started = 2; - */ - secondsSinceStarted?: bigint; + /** + * @generated from field: optional int64 seconds_since_started = 2; + */ + secondsSinceStarted?: bigint; - /** - * @generated from field: optional int64 seconds_since_last_played = 3; - */ - secondsSinceLastPlayed?: bigint; + /** + * @generated from field: optional int64 seconds_since_last_played = 3; + */ + secondsSinceLastPlayed?: bigint; - /** - * @generated from field: optional bytes session_usage_table_entry = 4; - */ - sessionUsageTableEntry?: Uint8Array; + /** + * @generated from field: optional bytes session_usage_table_entry = 4; + */ + sessionUsageTableEntry?: Uint8Array; }; /** @@ -928,32 +928,32 @@ export type LicenseRequest_ContentIdentification_ExistingLicense = Message<'lice * Use `create(LicenseRequest_ContentIdentification_ExistingLicenseSchema)` to create a new message. */ export const LicenseRequest_ContentIdentification_ExistingLicenseSchema: GenMessage<LicenseRequest_ContentIdentification_ExistingLicense> = - /*@__PURE__*/ - messageDesc(file_license_protocol, 2, 0, 2); + /*@__PURE__*/ + messageDesc(file_license_protocol, 2, 0, 2); /** * @generated from message license_protocol.LicenseRequest.ContentIdentification.InitData */ export type LicenseRequest_ContentIdentification_InitData = Message<'license_protocol.LicenseRequest.ContentIdentification.InitData'> & { - /** - * @generated from field: optional license_protocol.LicenseRequest.ContentIdentification.InitData.InitDataType init_data_type = 1; - */ - initDataType?: LicenseRequest_ContentIdentification_InitData_InitDataType; + /** + * @generated from field: optional license_protocol.LicenseRequest.ContentIdentification.InitData.InitDataType init_data_type = 1; + */ + initDataType?: LicenseRequest_ContentIdentification_InitData_InitDataType; - /** - * @generated from field: optional bytes init_data = 2; - */ - initData?: Uint8Array; + /** + * @generated from field: optional bytes init_data = 2; + */ + initData?: Uint8Array; - /** - * @generated from field: optional license_protocol.LicenseType license_type = 3; - */ - licenseType?: LicenseType; + /** + * @generated from field: optional license_protocol.LicenseType license_type = 3; + */ + licenseType?: LicenseType; - /** - * @generated from field: optional bytes request_id = 4; - */ - requestId?: Uint8Array; + /** + * @generated from field: optional bytes request_id = 4; + */ + requestId?: Uint8Array; }; /** @@ -961,59 +961,59 @@ export type LicenseRequest_ContentIdentification_InitData = Message<'license_pro * Use `create(LicenseRequest_ContentIdentification_InitDataSchema)` to create a new message. */ export const LicenseRequest_ContentIdentification_InitDataSchema: GenMessage<LicenseRequest_ContentIdentification_InitData> = - /*@__PURE__*/ - messageDesc(file_license_protocol, 2, 0, 3); + /*@__PURE__*/ + messageDesc(file_license_protocol, 2, 0, 3); /** * @generated from enum license_protocol.LicenseRequest.ContentIdentification.InitData.InitDataType */ export enum LicenseRequest_ContentIdentification_InitData_InitDataType { - /** - * @generated from enum value: INITDATATYPE_UNVERIFIED = 0; - */ - INITDATATYPE_UNVERIFIED = 0, + /** + * @generated from enum value: INITDATATYPE_UNVERIFIED = 0; + */ + INITDATATYPE_UNVERIFIED = 0, - /** - * @generated from enum value: CENC = 1; - */ - CENC = 1, + /** + * @generated from enum value: CENC = 1; + */ + CENC = 1, - /** - * @generated from enum value: WEBM = 2; - */ - WEBM = 2 + /** + * @generated from enum value: WEBM = 2; + */ + WEBM = 2 } /** * Describes the enum license_protocol.LicenseRequest.ContentIdentification.InitData.InitDataType. */ export const LicenseRequest_ContentIdentification_InitData_InitDataTypeSchema: GenEnum<LicenseRequest_ContentIdentification_InitData_InitDataType> = - /*@__PURE__*/ - enumDesc(file_license_protocol, 2, 0, 3, 0); + /*@__PURE__*/ + enumDesc(file_license_protocol, 2, 0, 3, 0); /** * @generated from enum license_protocol.LicenseRequest.RequestType */ export enum LicenseRequest_RequestType { - /** - * @generated from enum value: REQUESTTYPE_UNVERIFIED = 0; - */ - REQUESTTYPE_UNVERIFIED = 0, + /** + * @generated from enum value: REQUESTTYPE_UNVERIFIED = 0; + */ + REQUESTTYPE_UNVERIFIED = 0, - /** - * @generated from enum value: NEW = 1; - */ - NEW = 1, + /** + * @generated from enum value: NEW = 1; + */ + NEW = 1, - /** - * @generated from enum value: RENEWAL = 2; - */ - RENEWAL = 2, + /** + * @generated from enum value: RENEWAL = 2; + */ + RENEWAL = 2, - /** - * @generated from enum value: RELEASE = 3; - */ - RELEASE = 3 + /** + * @generated from enum value: RELEASE = 3; + */ + RELEASE = 3 } /** @@ -1025,19 +1025,19 @@ export const LicenseRequest_RequestTypeSchema: GenEnum<LicenseRequest_RequestTyp * @generated from message license_protocol.MetricData */ export type MetricData = Message<'license_protocol.MetricData'> & { - /** - * 'stage' that is currently processing the SignedMessage. Required. - * - * @generated from field: optional string stage_name = 1; - */ - stageName?: string; + /** + * 'stage' that is currently processing the SignedMessage. Required. + * + * @generated from field: optional string stage_name = 1; + */ + stageName?: string; - /** - * metric and associated value. - * - * @generated from field: repeated license_protocol.MetricData.TypeValue metric_data = 2; - */ - metricData: MetricData_TypeValue[]; + /** + * metric and associated value. + * + * @generated from field: repeated license_protocol.MetricData.TypeValue metric_data = 2; + */ + metricData: MetricData_TypeValue[]; }; /** @@ -1050,18 +1050,18 @@ export const MetricDataSchema: GenMessage<MetricData> = /*@__PURE__*/ messageDes * @generated from message license_protocol.MetricData.TypeValue */ export type MetricData_TypeValue = Message<'license_protocol.MetricData.TypeValue'> & { - /** - * @generated from field: optional license_protocol.MetricData.MetricType type = 1; - */ - type?: MetricData_MetricType; + /** + * @generated from field: optional license_protocol.MetricData.MetricType type = 1; + */ + type?: MetricData_MetricType; - /** - * The value associated with 'type'. For example if type == LATENCY, the - * value would be the time in microseconds spent in this 'stage'. - * - * @generated from field: optional int64 value = 2; - */ - value?: bigint; + /** + * The value associated with 'type'. For example if type == LATENCY, the + * value would be the time in microseconds spent in this 'stage'. + * + * @generated from field: optional int64 value = 2; + */ + value?: bigint; }; /** @@ -1074,25 +1074,25 @@ export const MetricData_TypeValueSchema: GenMessage<MetricData_TypeValue> = /*@_ * @generated from enum license_protocol.MetricData.MetricType */ export enum MetricData_MetricType { - /** - * @generated from enum value: METRICTYPE_UNVERIFIED = 0; - */ - METRICTYPE_UNVERIFIED = 0, + /** + * @generated from enum value: METRICTYPE_UNVERIFIED = 0; + */ + METRICTYPE_UNVERIFIED = 0, - /** - * The time spent in the 'stage', specified in microseconds. - * - * @generated from enum value: LATENCY = 1; - */ - LATENCY = 1, + /** + * The time spent in the 'stage', specified in microseconds. + * + * @generated from enum value: LATENCY = 1; + */ + LATENCY = 1, - /** - * The UNIX epoch timestamp at which the 'stage' was first accessed in - * microseconds. - * - * @generated from enum value: TIMESTAMP = 2; - */ - TIMESTAMP = 2 + /** + * The UNIX epoch timestamp at which the 'stage' was first accessed in + * microseconds. + * + * @generated from enum value: TIMESTAMP = 2; + */ + TIMESTAMP = 2 } /** @@ -1104,21 +1104,21 @@ export const MetricData_MetricTypeSchema: GenEnum<MetricData_MetricType> = /*@__ * @generated from message license_protocol.VersionInfo */ export type VersionInfo = Message<'license_protocol.VersionInfo'> & { - /** - * License SDK version reported by the Widevine License SDK. This field - * is populated automatically by the SDK. - * - * @generated from field: optional string license_sdk_version = 1; - */ - licenseSdkVersion?: string; + /** + * License SDK version reported by the Widevine License SDK. This field + * is populated automatically by the SDK. + * + * @generated from field: optional string license_sdk_version = 1; + */ + licenseSdkVersion?: string; - /** - * Version of the service hosting the license SDK. This field is optional. - * It may be provided by the hosting service. - * - * @generated from field: optional string license_service_version = 2; - */ - licenseServiceVersion?: string; + /** + * Version of the service hosting the license SDK. This field is optional. + * It may be provided by the hosting service. + * + * @generated from field: optional string license_service_version = 2; + */ + licenseServiceVersion?: string; }; /** @@ -1131,75 +1131,75 @@ export const VersionInfoSchema: GenMessage<VersionInfo> = /*@__PURE__*/ messageD * @generated from message license_protocol.SignedMessage */ export type SignedMessage = Message<'license_protocol.SignedMessage'> & { - /** - * @generated from field: optional license_protocol.SignedMessage.MessageType type = 1; - */ - type?: SignedMessage_MessageType; + /** + * @generated from field: optional license_protocol.SignedMessage.MessageType type = 1; + */ + type?: SignedMessage_MessageType; - /** - * @generated from field: optional bytes msg = 2; - */ - msg?: Uint8Array; + /** + * @generated from field: optional bytes msg = 2; + */ + msg?: Uint8Array; - /** - * Required field that contains the signature of the bytes of msg. - * For license requests, the signing algorithm is determined by the - * certificate contained in the request. - * For license responses, the signing algorithm is HMAC with signing key based - * on |session_key|. - * - * @generated from field: optional bytes signature = 3; - */ - signature?: Uint8Array; + /** + * Required field that contains the signature of the bytes of msg. + * For license requests, the signing algorithm is determined by the + * certificate contained in the request. + * For license responses, the signing algorithm is HMAC with signing key based + * on |session_key|. + * + * @generated from field: optional bytes signature = 3; + */ + signature?: Uint8Array; - /** - * If populated, the contents of this field will be signaled by the - * |session_key_type| type. If the |session_key_type| is WRAPPED_AES_KEY the - * key is the bytes of an encrypted AES key. If the |session_key_type| is - * EPHERMERAL_ECC_PUBLIC_KEY the field contains the bytes of an RFC5208 ASN1 - * serialized ECC public key. - * - * @generated from field: optional bytes session_key = 4; - */ - sessionKey?: Uint8Array; + /** + * If populated, the contents of this field will be signaled by the + * |session_key_type| type. If the |session_key_type| is WRAPPED_AES_KEY the + * key is the bytes of an encrypted AES key. If the |session_key_type| is + * EPHERMERAL_ECC_PUBLIC_KEY the field contains the bytes of an RFC5208 ASN1 + * serialized ECC public key. + * + * @generated from field: optional bytes session_key = 4; + */ + sessionKey?: Uint8Array; - /** - * Remote attestation data which will be present in the initial license - * request for ChromeOS client devices operating in verified mode. Remote - * attestation challenge data is |msg| field above. Optional. - * - * @generated from field: optional bytes remote_attestation = 5; - */ - remoteAttestation?: Uint8Array; + /** + * Remote attestation data which will be present in the initial license + * request for ChromeOS client devices operating in verified mode. Remote + * attestation challenge data is |msg| field above. Optional. + * + * @generated from field: optional bytes remote_attestation = 5; + */ + remoteAttestation?: Uint8Array; - /** - * @generated from field: repeated license_protocol.MetricData metric_data = 6; - */ - metricData: MetricData[]; + /** + * @generated from field: repeated license_protocol.MetricData metric_data = 6; + */ + metricData: MetricData[]; - /** - * Version information from the SDK and license service. This information is - * provided in the license response. - * - * @generated from field: optional license_protocol.VersionInfo service_version_info = 7; - */ - serviceVersionInfo?: VersionInfo; + /** + * Version information from the SDK and license service. This information is + * provided in the license response. + * + * @generated from field: optional license_protocol.VersionInfo service_version_info = 7; + */ + serviceVersionInfo?: VersionInfo; - /** - * Optional field that contains the algorithm type used to generate the - * session_key and signature in a LICENSE message. - * - * @generated from field: optional license_protocol.SignedMessage.SessionKeyType session_key_type = 8; - */ - sessionKeyType?: SignedMessage_SessionKeyType; + /** + * Optional field that contains the algorithm type used to generate the + * session_key and signature in a LICENSE message. + * + * @generated from field: optional license_protocol.SignedMessage.SessionKeyType session_key_type = 8; + */ + sessionKeyType?: SignedMessage_SessionKeyType; - /** - * The core message is the simple serialization of fields used by OEMCrypto. - * This field was introduced in OEMCrypto API v16. - * - * @generated from field: optional bytes oemcrypto_core_message = 9; - */ - oemcryptoCoreMessage?: Uint8Array; + /** + * The core message is the simple serialization of fields used by OEMCrypto. + * This field was introduced in OEMCrypto API v16. + * + * @generated from field: optional bytes oemcrypto_core_message = 9; + */ + oemcryptoCoreMessage?: Uint8Array; }; /** @@ -1212,60 +1212,60 @@ export const SignedMessageSchema: GenMessage<SignedMessage> = /*@__PURE__*/ mess * @generated from enum license_protocol.SignedMessage.MessageType */ export enum SignedMessage_MessageType { - /** - * @generated from enum value: MESSAGETYPE_UNVERIFIED = 0; - */ - MESSAGETYPE_UNVERIFIED = 0, + /** + * @generated from enum value: MESSAGETYPE_UNVERIFIED = 0; + */ + MESSAGETYPE_UNVERIFIED = 0, - /** - * @generated from enum value: LICENSE_REQUEST = 1; - */ - LICENSE_REQUEST = 1, + /** + * @generated from enum value: LICENSE_REQUEST = 1; + */ + LICENSE_REQUEST = 1, - /** - * @generated from enum value: LICENSE = 2; - */ - LICENSE = 2, + /** + * @generated from enum value: LICENSE = 2; + */ + LICENSE = 2, - /** - * @generated from enum value: ERROR_RESPONSE = 3; - */ - ERROR_RESPONSE = 3, + /** + * @generated from enum value: ERROR_RESPONSE = 3; + */ + ERROR_RESPONSE = 3, - /** - * @generated from enum value: SERVICE_CERTIFICATE_REQUEST = 4; - */ - SERVICE_CERTIFICATE_REQUEST = 4, + /** + * @generated from enum value: SERVICE_CERTIFICATE_REQUEST = 4; + */ + SERVICE_CERTIFICATE_REQUEST = 4, - /** - * @generated from enum value: SERVICE_CERTIFICATE = 5; - */ - SERVICE_CERTIFICATE = 5, + /** + * @generated from enum value: SERVICE_CERTIFICATE = 5; + */ + SERVICE_CERTIFICATE = 5, - /** - * @generated from enum value: SUB_LICENSE = 6; - */ - SUB_LICENSE = 6, + /** + * @generated from enum value: SUB_LICENSE = 6; + */ + SUB_LICENSE = 6, - /** - * @generated from enum value: CAS_LICENSE_REQUEST = 7; - */ - CAS_LICENSE_REQUEST = 7, + /** + * @generated from enum value: CAS_LICENSE_REQUEST = 7; + */ + CAS_LICENSE_REQUEST = 7, - /** - * @generated from enum value: CAS_LICENSE = 8; - */ - CAS_LICENSE = 8, + /** + * @generated from enum value: CAS_LICENSE = 8; + */ + CAS_LICENSE = 8, - /** - * @generated from enum value: EXTERNAL_LICENSE_REQUEST = 9; - */ - EXTERNAL_LICENSE_REQUEST = 9, + /** + * @generated from enum value: EXTERNAL_LICENSE_REQUEST = 9; + */ + EXTERNAL_LICENSE_REQUEST = 9, - /** - * @generated from enum value: EXTERNAL_LICENSE = 10; - */ - EXTERNAL_LICENSE = 10 + /** + * @generated from enum value: EXTERNAL_LICENSE = 10; + */ + EXTERNAL_LICENSE = 10 } /** @@ -1277,20 +1277,20 @@ export const SignedMessage_MessageTypeSchema: GenEnum<SignedMessage_MessageType> * @generated from enum license_protocol.SignedMessage.SessionKeyType */ export enum SignedMessage_SessionKeyType { - /** - * @generated from enum value: UNDEFINED = 0; - */ - UNDEFINED = 0, + /** + * @generated from enum value: UNDEFINED = 0; + */ + UNDEFINED = 0, - /** - * @generated from enum value: WRAPPED_AES_KEY = 1; - */ - WRAPPED_AES_KEY = 1, + /** + * @generated from enum value: WRAPPED_AES_KEY = 1; + */ + WRAPPED_AES_KEY = 1, - /** - * @generated from enum value: EPHERMERAL_ECC_PUBLIC_KEY = 2; - */ - EPHERMERAL_ECC_PUBLIC_KEY = 2 + /** + * @generated from enum value: EPHERMERAL_ECC_PUBLIC_KEY = 2; + */ + EPHERMERAL_ECC_PUBLIC_KEY = 2 } /** @@ -1304,62 +1304,62 @@ export const SignedMessage_SessionKeyTypeSchema: GenEnum<SignedMessage_SessionKe * @generated from message license_protocol.ClientIdentification */ export type ClientIdentification = Message<'license_protocol.ClientIdentification'> & { - /** - * Type of factory-provisioned device root of trust. Optional. - * - * @generated from field: optional license_protocol.ClientIdentification.TokenType type = 1; - */ - type?: ClientIdentification_TokenType; + /** + * Type of factory-provisioned device root of trust. Optional. + * + * @generated from field: optional license_protocol.ClientIdentification.TokenType type = 1; + */ + type?: ClientIdentification_TokenType; - /** - * Factory-provisioned device root of trust. Required. - * - * @generated from field: optional bytes token = 2; - */ - token?: Uint8Array; + /** + * Factory-provisioned device root of trust. Required. + * + * @generated from field: optional bytes token = 2; + */ + token?: Uint8Array; - /** - * Optional client information name/value pairs. - * - * @generated from field: repeated license_protocol.ClientIdentification.NameValue client_info = 3; - */ - clientInfo: ClientIdentification_NameValue[]; + /** + * Optional client information name/value pairs. + * + * @generated from field: repeated license_protocol.ClientIdentification.NameValue client_info = 3; + */ + clientInfo: ClientIdentification_NameValue[]; - /** - * Client token generated by the content provider. Optional. - * - * @generated from field: optional bytes provider_client_token = 4; - */ - providerClientToken?: Uint8Array; + /** + * Client token generated by the content provider. Optional. + * + * @generated from field: optional bytes provider_client_token = 4; + */ + providerClientToken?: Uint8Array; - /** - * Number of licenses received by the client to which the token above belongs. - * Only present if client_token is specified. - * - * @generated from field: optional uint32 license_counter = 5; - */ - licenseCounter?: number; + /** + * Number of licenses received by the client to which the token above belongs. + * Only present if client_token is specified. + * + * @generated from field: optional uint32 license_counter = 5; + */ + licenseCounter?: number; - /** - * List of non-baseline client capabilities. - * - * @generated from field: optional license_protocol.ClientIdentification.ClientCapabilities client_capabilities = 6; - */ - clientCapabilities?: ClientIdentification_ClientCapabilities; + /** + * List of non-baseline client capabilities. + * + * @generated from field: optional license_protocol.ClientIdentification.ClientCapabilities client_capabilities = 6; + */ + clientCapabilities?: ClientIdentification_ClientCapabilities; - /** - * Serialized VmpData message. Optional. - * - * @generated from field: optional bytes vmp_data = 7; - */ - vmpData?: Uint8Array; + /** + * Serialized VmpData message. Optional. + * + * @generated from field: optional bytes vmp_data = 7; + */ + vmpData?: Uint8Array; - /** - * Optional field that may contain additional provisioning credentials. - * - * @generated from field: repeated license_protocol.ClientIdentification.ClientCredentials device_credentials = 8; - */ - deviceCredentials: ClientIdentification_ClientCredentials[]; + /** + * Optional field that may contain additional provisioning credentials. + * + * @generated from field: repeated license_protocol.ClientIdentification.ClientCredentials device_credentials = 8; + */ + deviceCredentials: ClientIdentification_ClientCredentials[]; }; /** @@ -1372,15 +1372,15 @@ export const ClientIdentificationSchema: GenMessage<ClientIdentification> = /*@_ * @generated from message license_protocol.ClientIdentification.NameValue */ export type ClientIdentification_NameValue = Message<'license_protocol.ClientIdentification.NameValue'> & { - /** - * @generated from field: optional string name = 1; - */ - name?: string; + /** + * @generated from field: optional string name = 1; + */ + name?: string; - /** - * @generated from field: optional string value = 2; - */ - value?: string; + /** + * @generated from field: optional string value = 2; + */ + value?: string; }; /** @@ -1396,82 +1396,82 @@ export const ClientIdentification_NameValueSchema: GenMessage<ClientIdentificati * @generated from message license_protocol.ClientIdentification.ClientCapabilities */ export type ClientIdentification_ClientCapabilities = Message<'license_protocol.ClientIdentification.ClientCapabilities'> & { - /** - * @generated from field: optional bool client_token = 1; - */ - clientToken?: boolean; + /** + * @generated from field: optional bool client_token = 1; + */ + clientToken?: boolean; - /** - * @generated from field: optional bool session_token = 2; - */ - sessionToken?: boolean; + /** + * @generated from field: optional bool session_token = 2; + */ + sessionToken?: boolean; - /** - * @generated from field: optional bool video_resolution_constraints = 3; - */ - videoResolutionConstraints?: boolean; + /** + * @generated from field: optional bool video_resolution_constraints = 3; + */ + videoResolutionConstraints?: boolean; - /** - * @generated from field: optional license_protocol.ClientIdentification.ClientCapabilities.HdcpVersion max_hdcp_version = 4; - */ - maxHdcpVersion?: ClientIdentification_ClientCapabilities_HdcpVersion; + /** + * @generated from field: optional license_protocol.ClientIdentification.ClientCapabilities.HdcpVersion max_hdcp_version = 4; + */ + maxHdcpVersion?: ClientIdentification_ClientCapabilities_HdcpVersion; - /** - * @generated from field: optional uint32 oem_crypto_api_version = 5; - */ - oemCryptoApiVersion?: number; + /** + * @generated from field: optional uint32 oem_crypto_api_version = 5; + */ + oemCryptoApiVersion?: number; - /** - * Client has hardware support for protecting the usage table, such as - * storing the generation number in secure memory. For Details, see: - * Widevine Modular DRM Security Integration Guide for CENC - * - * @generated from field: optional bool anti_rollback_usage_table = 6; - */ - antiRollbackUsageTable?: boolean; + /** + * Client has hardware support for protecting the usage table, such as + * storing the generation number in secure memory. For Details, see: + * Widevine Modular DRM Security Integration Guide for CENC + * + * @generated from field: optional bool anti_rollback_usage_table = 6; + */ + antiRollbackUsageTable?: boolean; - /** - * The client shall report |srm_version| if available. - * - * @generated from field: optional uint32 srm_version = 7; - */ - srmVersion?: number; + /** + * The client shall report |srm_version| if available. + * + * @generated from field: optional uint32 srm_version = 7; + */ + srmVersion?: number; - /** - * A device may have SRM data, and report a version, but may not be capable - * of updating SRM data. - * - * @generated from field: optional bool can_update_srm = 8; - */ - canUpdateSrm?: boolean; + /** + * A device may have SRM data, and report a version, but may not be capable + * of updating SRM data. + * + * @generated from field: optional bool can_update_srm = 8; + */ + canUpdateSrm?: boolean; - /** - * @generated from field: repeated license_protocol.ClientIdentification.ClientCapabilities.CertificateKeyType supported_certificate_key_type = 9; - */ - supportedCertificateKeyType: ClientIdentification_ClientCapabilities_CertificateKeyType[]; + /** + * @generated from field: repeated license_protocol.ClientIdentification.ClientCapabilities.CertificateKeyType supported_certificate_key_type = 9; + */ + supportedCertificateKeyType: ClientIdentification_ClientCapabilities_CertificateKeyType[]; - /** - * @generated from field: optional license_protocol.ClientIdentification.ClientCapabilities.AnalogOutputCapabilities analog_output_capabilities = 10; - */ - analogOutputCapabilities?: ClientIdentification_ClientCapabilities_AnalogOutputCapabilities; + /** + * @generated from field: optional license_protocol.ClientIdentification.ClientCapabilities.AnalogOutputCapabilities analog_output_capabilities = 10; + */ + analogOutputCapabilities?: ClientIdentification_ClientCapabilities_AnalogOutputCapabilities; - /** - * @generated from field: optional bool can_disable_analog_output = 11; - */ - canDisableAnalogOutput?: boolean; + /** + * @generated from field: optional bool can_disable_analog_output = 11; + */ + canDisableAnalogOutput?: boolean; - /** - * Clients can indicate a performance level supported by OEMCrypto. - * This will allow applications and providers to choose an appropriate - * quality of content to serve. Currently defined tiers are - * 1 (low), 2 (medium) and 3 (high). Any other value indicates that - * the resource rating is unavailable or reporting erroneous values - * for that device. For details see, - * Widevine Modular DRM Security Integration Guide for CENC - * - * @generated from field: optional uint32 resource_rating_tier = 12; - */ - resourceRatingTier?: number; + /** + * Clients can indicate a performance level supported by OEMCrypto. + * This will allow applications and providers to choose an appropriate + * quality of content to serve. Currently defined tiers are + * 1 (low), 2 (medium) and 3 (high). Any other value indicates that + * the resource rating is unavailable or reporting erroneous values + * for that device. For details see, + * Widevine Modular DRM Security Integration Guide for CENC + * + * @generated from field: optional uint32 resource_rating_tier = 12; + */ + resourceRatingTier?: number; }; /** @@ -1484,131 +1484,131 @@ export const ClientIdentification_ClientCapabilitiesSchema: GenMessage<ClientIde * @generated from enum license_protocol.ClientIdentification.ClientCapabilities.HdcpVersion */ export enum ClientIdentification_ClientCapabilities_HdcpVersion { - /** - * @generated from enum value: HDCP_NONE = 0; - */ - HDCP_NONE = 0, + /** + * @generated from enum value: HDCP_NONE = 0; + */ + HDCP_NONE = 0, - /** - * @generated from enum value: HDCP_V1 = 1; - */ - HDCP_V1 = 1, + /** + * @generated from enum value: HDCP_V1 = 1; + */ + HDCP_V1 = 1, - /** - * @generated from enum value: HDCP_V2 = 2; - */ - HDCP_V2 = 2, + /** + * @generated from enum value: HDCP_V2 = 2; + */ + HDCP_V2 = 2, - /** - * @generated from enum value: HDCP_V2_1 = 3; - */ - HDCP_V2_1 = 3, + /** + * @generated from enum value: HDCP_V2_1 = 3; + */ + HDCP_V2_1 = 3, - /** - * @generated from enum value: HDCP_V2_2 = 4; - */ - HDCP_V2_2 = 4, + /** + * @generated from enum value: HDCP_V2_2 = 4; + */ + HDCP_V2_2 = 4, - /** - * @generated from enum value: HDCP_V2_3 = 5; - */ - HDCP_V2_3 = 5, + /** + * @generated from enum value: HDCP_V2_3 = 5; + */ + HDCP_V2_3 = 5, - /** - * @generated from enum value: HDCP_NO_DIGITAL_OUTPUT = 255; - */ - HDCP_NO_DIGITAL_OUTPUT = 255 + /** + * @generated from enum value: HDCP_NO_DIGITAL_OUTPUT = 255; + */ + HDCP_NO_DIGITAL_OUTPUT = 255 } /** * Describes the enum license_protocol.ClientIdentification.ClientCapabilities.HdcpVersion. */ export const ClientIdentification_ClientCapabilities_HdcpVersionSchema: GenEnum<ClientIdentification_ClientCapabilities_HdcpVersion> = - /*@__PURE__*/ - enumDesc(file_license_protocol, 6, 1, 0); + /*@__PURE__*/ + enumDesc(file_license_protocol, 6, 1, 0); /** * @generated from enum license_protocol.ClientIdentification.ClientCapabilities.CertificateKeyType */ export enum ClientIdentification_ClientCapabilities_CertificateKeyType { - /** - * @generated from enum value: RSA_2048 = 0; - */ - RSA_2048 = 0, + /** + * @generated from enum value: RSA_2048 = 0; + */ + RSA_2048 = 0, - /** - * @generated from enum value: RSA_3072 = 1; - */ - RSA_3072 = 1, + /** + * @generated from enum value: RSA_3072 = 1; + */ + RSA_3072 = 1, - /** - * @generated from enum value: ECC_SECP256R1 = 2; - */ - ECC_SECP256R1 = 2, + /** + * @generated from enum value: ECC_SECP256R1 = 2; + */ + ECC_SECP256R1 = 2, - /** - * @generated from enum value: ECC_SECP384R1 = 3; - */ - ECC_SECP384R1 = 3, + /** + * @generated from enum value: ECC_SECP384R1 = 3; + */ + ECC_SECP384R1 = 3, - /** - * @generated from enum value: ECC_SECP521R1 = 4; - */ - ECC_SECP521R1 = 4 + /** + * @generated from enum value: ECC_SECP521R1 = 4; + */ + ECC_SECP521R1 = 4 } /** * Describes the enum license_protocol.ClientIdentification.ClientCapabilities.CertificateKeyType. */ export const ClientIdentification_ClientCapabilities_CertificateKeyTypeSchema: GenEnum<ClientIdentification_ClientCapabilities_CertificateKeyType> = - /*@__PURE__*/ - enumDesc(file_license_protocol, 6, 1, 1); + /*@__PURE__*/ + enumDesc(file_license_protocol, 6, 1, 1); /** * @generated from enum license_protocol.ClientIdentification.ClientCapabilities.AnalogOutputCapabilities */ export enum ClientIdentification_ClientCapabilities_AnalogOutputCapabilities { - /** - * @generated from enum value: ANALOG_OUTPUT_UNKNOWN = 0; - */ - ANALOG_OUTPUT_UNKNOWN = 0, + /** + * @generated from enum value: ANALOG_OUTPUT_UNKNOWN = 0; + */ + ANALOG_OUTPUT_UNKNOWN = 0, - /** - * @generated from enum value: ANALOG_OUTPUT_NONE = 1; - */ - ANALOG_OUTPUT_NONE = 1, + /** + * @generated from enum value: ANALOG_OUTPUT_NONE = 1; + */ + ANALOG_OUTPUT_NONE = 1, - /** - * @generated from enum value: ANALOG_OUTPUT_SUPPORTED = 2; - */ - ANALOG_OUTPUT_SUPPORTED = 2, + /** + * @generated from enum value: ANALOG_OUTPUT_SUPPORTED = 2; + */ + ANALOG_OUTPUT_SUPPORTED = 2, - /** - * @generated from enum value: ANALOG_OUTPUT_SUPPORTS_CGMS_A = 3; - */ - ANALOG_OUTPUT_SUPPORTS_CGMS_A = 3 + /** + * @generated from enum value: ANALOG_OUTPUT_SUPPORTS_CGMS_A = 3; + */ + ANALOG_OUTPUT_SUPPORTS_CGMS_A = 3 } /** * Describes the enum license_protocol.ClientIdentification.ClientCapabilities.AnalogOutputCapabilities. */ export const ClientIdentification_ClientCapabilities_AnalogOutputCapabilitiesSchema: GenEnum<ClientIdentification_ClientCapabilities_AnalogOutputCapabilities> = - /*@__PURE__*/ - enumDesc(file_license_protocol, 6, 1, 2); + /*@__PURE__*/ + enumDesc(file_license_protocol, 6, 1, 2); /** * @generated from message license_protocol.ClientIdentification.ClientCredentials */ export type ClientIdentification_ClientCredentials = Message<'license_protocol.ClientIdentification.ClientCredentials'> & { - /** - * @generated from field: optional license_protocol.ClientIdentification.TokenType type = 1; - */ - type?: ClientIdentification_TokenType; + /** + * @generated from field: optional license_protocol.ClientIdentification.TokenType type = 1; + */ + type?: ClientIdentification_TokenType; - /** - * @generated from field: optional bytes token = 2; - */ - token?: Uint8Array; + /** + * @generated from field: optional bytes token = 2; + */ + token?: Uint8Array; }; /** @@ -1621,25 +1621,25 @@ export const ClientIdentification_ClientCredentialsSchema: GenMessage<ClientIden * @generated from enum license_protocol.ClientIdentification.TokenType */ export enum ClientIdentification_TokenType { - /** - * @generated from enum value: KEYBOX = 0; - */ - KEYBOX = 0, + /** + * @generated from enum value: KEYBOX = 0; + */ + KEYBOX = 0, - /** - * @generated from enum value: DRM_DEVICE_CERTIFICATE = 1; - */ - DRM_DEVICE_CERTIFICATE = 1, + /** + * @generated from enum value: DRM_DEVICE_CERTIFICATE = 1; + */ + DRM_DEVICE_CERTIFICATE = 1, - /** - * @generated from enum value: REMOTE_ATTESTATION_CERTIFICATE = 2; - */ - REMOTE_ATTESTATION_CERTIFICATE = 2, + /** + * @generated from enum value: REMOTE_ATTESTATION_CERTIFICATE = 2; + */ + REMOTE_ATTESTATION_CERTIFICATE = 2, - /** - * @generated from enum value: OEM_DEVICE_CERTIFICATE = 3; - */ - OEM_DEVICE_CERTIFICATE = 3 + /** + * @generated from enum value: OEM_DEVICE_CERTIFICATE = 3; + */ + OEM_DEVICE_CERTIFICATE = 3 } /** @@ -1654,43 +1654,43 @@ export const ClientIdentification_TokenTypeSchema: GenEnum<ClientIdentification_ * @generated from message license_protocol.EncryptedClientIdentification */ export type EncryptedClientIdentification = Message<'license_protocol.EncryptedClientIdentification'> & { - /** - * Provider ID for which the ClientIdentifcation is encrypted (owner of - * service certificate). - * - * @generated from field: optional string provider_id = 1; - */ - providerId?: string; + /** + * Provider ID for which the ClientIdentifcation is encrypted (owner of + * service certificate). + * + * @generated from field: optional string provider_id = 1; + */ + providerId?: string; - /** - * Serial number for the service certificate for which ClientIdentification is - * encrypted. - * - * @generated from field: optional bytes service_certificate_serial_number = 2; - */ - serviceCertificateSerialNumber?: Uint8Array; + /** + * Serial number for the service certificate for which ClientIdentification is + * encrypted. + * + * @generated from field: optional bytes service_certificate_serial_number = 2; + */ + serviceCertificateSerialNumber?: Uint8Array; - /** - * Serialized ClientIdentification message, encrypted with the privacy key - * using AES-128-CBC with PKCS#5 padding. - * - * @generated from field: optional bytes encrypted_client_id = 3; - */ - encryptedClientId?: Uint8Array; + /** + * Serialized ClientIdentification message, encrypted with the privacy key + * using AES-128-CBC with PKCS#5 padding. + * + * @generated from field: optional bytes encrypted_client_id = 3; + */ + encryptedClientId?: Uint8Array; - /** - * Initialization vector needed to decrypt encrypted_client_id. - * - * @generated from field: optional bytes encrypted_client_id_iv = 4; - */ - encryptedClientIdIv?: Uint8Array; + /** + * Initialization vector needed to decrypt encrypted_client_id. + * + * @generated from field: optional bytes encrypted_client_id_iv = 4; + */ + encryptedClientIdIv?: Uint8Array; - /** - * AES-128 privacy key, encrypted with the service public key using RSA-OAEP. - * - * @generated from field: optional bytes encrypted_privacy_key = 5; - */ - encryptedPrivacyKey?: Uint8Array; + /** + * AES-128 privacy key, encrypted with the service public key using RSA-OAEP. + * + * @generated from field: optional bytes encrypted_privacy_key = 5; + */ + encryptedPrivacyKey?: Uint8Array; }; /** @@ -1706,109 +1706,109 @@ export const EncryptedClientIdentificationSchema: GenMessage<EncryptedClientIden * @generated from message license_protocol.DrmCertificate */ export type DrmCertificate = Message<'license_protocol.DrmCertificate'> & { - /** - * Type of certificate. Required. - * - * @generated from field: optional license_protocol.DrmCertificate.Type type = 1; - */ - type?: DrmCertificate_Type; + /** + * Type of certificate. Required. + * + * @generated from field: optional license_protocol.DrmCertificate.Type type = 1; + */ + type?: DrmCertificate_Type; - /** - * 128-bit globally unique serial number of certificate. - * Value is 0 for root certificate. Required. - * - * @generated from field: optional bytes serial_number = 2; - */ - serialNumber?: Uint8Array; + /** + * 128-bit globally unique serial number of certificate. + * Value is 0 for root certificate. Required. + * + * @generated from field: optional bytes serial_number = 2; + */ + serialNumber?: Uint8Array; - /** - * POSIX time, in seconds, when the certificate was created. Required. - * - * @generated from field: optional uint32 creation_time_seconds = 3; - */ - creationTimeSeconds?: number; + /** + * POSIX time, in seconds, when the certificate was created. Required. + * + * @generated from field: optional uint32 creation_time_seconds = 3; + */ + creationTimeSeconds?: number; - /** - * POSIX time, in seconds, when the certificate should expire. Value of zero - * denotes indefinite expiry time. For more information on limited lifespan - * DRM certificates see (go/limited-lifespan-drm-certificates). - * - * @generated from field: optional uint32 expiration_time_seconds = 12; - */ - expirationTimeSeconds?: number; + /** + * POSIX time, in seconds, when the certificate should expire. Value of zero + * denotes indefinite expiry time. For more information on limited lifespan + * DRM certificates see (go/limited-lifespan-drm-certificates). + * + * @generated from field: optional uint32 expiration_time_seconds = 12; + */ + expirationTimeSeconds?: number; - /** - * Device public key. PKCS#1 ASN.1 DER-encoded. Required. - * - * @generated from field: optional bytes public_key = 4; - */ - publicKey?: Uint8Array; + /** + * Device public key. PKCS#1 ASN.1 DER-encoded. Required. + * + * @generated from field: optional bytes public_key = 4; + */ + publicKey?: Uint8Array; - /** - * Widevine system ID for the device. Required for intermediate and - * user device certificates. - * - * @generated from field: optional uint32 system_id = 5; - */ - systemId?: number; + /** + * Widevine system ID for the device. Required for intermediate and + * user device certificates. + * + * @generated from field: optional uint32 system_id = 5; + */ + systemId?: number; - /** - * Deprecated field, which used to indicate whether the device was a test - * (non-production) device. The test_device field in ProvisionedDeviceInfo - * below should be observed instead. - * - * @generated from field: optional bool test_device_deprecated = 6 [deprecated = true]; - * @deprecated - */ - testDeviceDeprecated?: boolean; + /** + * Deprecated field, which used to indicate whether the device was a test + * (non-production) device. The test_device field in ProvisionedDeviceInfo + * below should be observed instead. + * + * @generated from field: optional bool test_device_deprecated = 6 [deprecated = true]; + * @deprecated + */ + testDeviceDeprecated?: boolean; - /** - * Service identifier (web origin) for the provider which owns the - * certificate. Required for service and provisioner certificates. - * - * @generated from field: optional string provider_id = 7; - */ - providerId?: string; + /** + * Service identifier (web origin) for the provider which owns the + * certificate. Required for service and provisioner certificates. + * + * @generated from field: optional string provider_id = 7; + */ + providerId?: string; - /** - * This field is used only when type = SERVICE to specify which SDK uses - * service certificate. This repeated field is treated as a set. A certificate - * may be used for the specified service SDK if the appropriate ServiceType - * is specified in this field. - * - * @generated from field: repeated license_protocol.DrmCertificate.ServiceType service_types = 8; - */ - serviceTypes: DrmCertificate_ServiceType[]; + /** + * This field is used only when type = SERVICE to specify which SDK uses + * service certificate. This repeated field is treated as a set. A certificate + * may be used for the specified service SDK if the appropriate ServiceType + * is specified in this field. + * + * @generated from field: repeated license_protocol.DrmCertificate.ServiceType service_types = 8; + */ + serviceTypes: DrmCertificate_ServiceType[]; - /** - * Required. The algorithm field contains the curve used to create the - * |public_key| if algorithm is one of the ECC types. - * The |algorithm| is used for both to determine the if the certificate is ECC - * or RSA. The |algorithm| also specifies the parameters that were used to - * create |public_key| and are used to create an ephemeral session key. - * - * @generated from field: optional license_protocol.DrmCertificate.Algorithm algorithm = 9; - */ - algorithm?: DrmCertificate_Algorithm; + /** + * Required. The algorithm field contains the curve used to create the + * |public_key| if algorithm is one of the ECC types. + * The |algorithm| is used for both to determine the if the certificate is ECC + * or RSA. The |algorithm| also specifies the parameters that were used to + * create |public_key| and are used to create an ephemeral session key. + * + * @generated from field: optional license_protocol.DrmCertificate.Algorithm algorithm = 9; + */ + algorithm?: DrmCertificate_Algorithm; - /** - * Optional. May be present in DEVICE certificate types. This is the root - * of trust identifier that holds an encrypted value that identifies the - * keybox or other root of trust that was used to provision a DEVICE drm - * certificate. - * - * @generated from field: optional bytes rot_id = 10; - */ - rotId?: Uint8Array; + /** + * Optional. May be present in DEVICE certificate types. This is the root + * of trust identifier that holds an encrypted value that identifies the + * keybox or other root of trust that was used to provision a DEVICE drm + * certificate. + * + * @generated from field: optional bytes rot_id = 10; + */ + rotId?: Uint8Array; - /** - * Optional. May be present in devices that explicitly support dual keys. When - * present the |public_key| is used for verification of received license - * request messages. - * - * @generated from field: optional license_protocol.DrmCertificate.EncryptionKey encryption_key = 11; - */ - encryptionKey?: DrmCertificate_EncryptionKey; + /** + * Optional. May be present in devices that explicitly support dual keys. When + * present the |public_key| is used for verification of received license + * request messages. + * + * @generated from field: optional license_protocol.DrmCertificate.EncryptionKey encryption_key = 11; + */ + encryptionKey?: DrmCertificate_EncryptionKey; }; /** @@ -1821,23 +1821,23 @@ export const DrmCertificateSchema: GenMessage<DrmCertificate> = /*@__PURE__*/ me * @generated from message license_protocol.DrmCertificate.EncryptionKey */ export type DrmCertificate_EncryptionKey = Message<'license_protocol.DrmCertificate.EncryptionKey'> & { - /** - * Device public key. PKCS#1 ASN.1 DER-encoded. Required. - * - * @generated from field: optional bytes public_key = 1; - */ - publicKey?: Uint8Array; + /** + * Device public key. PKCS#1 ASN.1 DER-encoded. Required. + * + * @generated from field: optional bytes public_key = 1; + */ + publicKey?: Uint8Array; - /** - * Required. The algorithm field contains the curve used to create the - * |public_key| if algorithm is one of the ECC types. - * The |algorithm| is used for both to determine the if the certificate is - * ECC or RSA. The |algorithm| also specifies the parameters that were used - * to create |public_key| and are used to create an ephemeral session key. - * - * @generated from field: optional license_protocol.DrmCertificate.Algorithm algorithm = 2; - */ - algorithm?: DrmCertificate_Algorithm; + /** + * Required. The algorithm field contains the curve used to create the + * |public_key| if algorithm is one of the ECC types. + * The |algorithm| is used for both to determine the if the certificate is + * ECC or RSA. The |algorithm| also specifies the parameters that were used + * to create |public_key| and are used to create an ephemeral session key. + * + * @generated from field: optional license_protocol.DrmCertificate.Algorithm algorithm = 2; + */ + algorithm?: DrmCertificate_Algorithm; }; /** @@ -1850,32 +1850,32 @@ export const DrmCertificate_EncryptionKeySchema: GenMessage<DrmCertificate_Encry * @generated from enum license_protocol.DrmCertificate.Type */ export enum DrmCertificate_Type { - /** - * ProtoBestPractices: ignore. - * - * @generated from enum value: ROOT = 0; - */ - ROOT = 0, + /** + * ProtoBestPractices: ignore. + * + * @generated from enum value: ROOT = 0; + */ + ROOT = 0, - /** - * @generated from enum value: DEVICE_MODEL = 1; - */ - DEVICE_MODEL = 1, + /** + * @generated from enum value: DEVICE_MODEL = 1; + */ + DEVICE_MODEL = 1, - /** - * @generated from enum value: DEVICE = 2; - */ - DEVICE = 2, + /** + * @generated from enum value: DEVICE = 2; + */ + DEVICE = 2, - /** - * @generated from enum value: SERVICE = 3; - */ - SERVICE = 3, + /** + * @generated from enum value: SERVICE = 3; + */ + SERVICE = 3, - /** - * @generated from enum value: PROVISIONER = 4; - */ - PROVISIONER = 4 + /** + * @generated from enum value: PROVISIONER = 4; + */ + PROVISIONER = 4 } /** @@ -1887,30 +1887,30 @@ export const DrmCertificate_TypeSchema: GenEnum<DrmCertificate_Type> = /*@__PURE * @generated from enum license_protocol.DrmCertificate.ServiceType */ export enum DrmCertificate_ServiceType { - /** - * @generated from enum value: UNKNOWN_SERVICE_TYPE = 0; - */ - UNKNOWN_SERVICE_TYPE = 0, + /** + * @generated from enum value: UNKNOWN_SERVICE_TYPE = 0; + */ + UNKNOWN_SERVICE_TYPE = 0, - /** - * @generated from enum value: LICENSE_SERVER_SDK = 1; - */ - LICENSE_SERVER_SDK = 1, + /** + * @generated from enum value: LICENSE_SERVER_SDK = 1; + */ + LICENSE_SERVER_SDK = 1, - /** - * @generated from enum value: LICENSE_SERVER_PROXY_SDK = 2; - */ - LICENSE_SERVER_PROXY_SDK = 2, + /** + * @generated from enum value: LICENSE_SERVER_PROXY_SDK = 2; + */ + LICENSE_SERVER_PROXY_SDK = 2, - /** - * @generated from enum value: PROVISIONING_SDK = 3; - */ - PROVISIONING_SDK = 3, + /** + * @generated from enum value: PROVISIONING_SDK = 3; + */ + PROVISIONING_SDK = 3, - /** - * @generated from enum value: CAS_PROXY_SDK = 4; - */ - CAS_PROXY_SDK = 4 + /** + * @generated from enum value: CAS_PROXY_SDK = 4; + */ + CAS_PROXY_SDK = 4 } /** @@ -1922,30 +1922,30 @@ export const DrmCertificate_ServiceTypeSchema: GenEnum<DrmCertificate_ServiceTyp * @generated from enum license_protocol.DrmCertificate.Algorithm */ export enum DrmCertificate_Algorithm { - /** - * @generated from enum value: UNKNOWN_ALGORITHM = 0; - */ - UNKNOWN_ALGORITHM = 0, + /** + * @generated from enum value: UNKNOWN_ALGORITHM = 0; + */ + UNKNOWN_ALGORITHM = 0, - /** - * @generated from enum value: RSA = 1; - */ - RSA = 1, + /** + * @generated from enum value: RSA = 1; + */ + RSA = 1, - /** - * @generated from enum value: ECC_SECP256R1 = 2; - */ - ECC_SECP256R1 = 2, + /** + * @generated from enum value: ECC_SECP256R1 = 2; + */ + ECC_SECP256R1 = 2, - /** - * @generated from enum value: ECC_SECP384R1 = 3; - */ - ECC_SECP384R1 = 3, + /** + * @generated from enum value: ECC_SECP384R1 = 3; + */ + ECC_SECP384R1 = 3, - /** - * @generated from enum value: ECC_SECP521R1 = 4; - */ - ECC_SECP521R1 = 4 + /** + * @generated from enum value: ECC_SECP521R1 = 4; + */ + ECC_SECP521R1 = 4 } /** @@ -1959,34 +1959,34 @@ export const DrmCertificate_AlgorithmSchema: GenEnum<DrmCertificate_Algorithm> = * @generated from message license_protocol.SignedDrmCertificate */ export type SignedDrmCertificate = Message<'license_protocol.SignedDrmCertificate'> & { - /** - * Serialized certificate. Required. - * - * @generated from field: optional bytes drm_certificate = 1; - */ - drmCertificate?: Uint8Array; + /** + * Serialized certificate. Required. + * + * @generated from field: optional bytes drm_certificate = 1; + */ + drmCertificate?: Uint8Array; - /** - * Signature of certificate. Signed with root or intermediate - * certificate specified below. Required. - * - * @generated from field: optional bytes signature = 2; - */ - signature?: Uint8Array; + /** + * Signature of certificate. Signed with root or intermediate + * certificate specified below. Required. + * + * @generated from field: optional bytes signature = 2; + */ + signature?: Uint8Array; - /** - * SignedDrmCertificate used to sign this certificate. - * - * @generated from field: optional license_protocol.SignedDrmCertificate signer = 3; - */ - signer?: SignedDrmCertificate; + /** + * SignedDrmCertificate used to sign this certificate. + * + * @generated from field: optional license_protocol.SignedDrmCertificate signer = 3; + */ + signer?: SignedDrmCertificate; - /** - * Optional field that indicates the hash algorithm used in signature scheme. - * - * @generated from field: optional license_protocol.HashAlgorithmProto hash_algorithm = 4; - */ - hashAlgorithm?: HashAlgorithmProto; + /** + * Optional field that indicates the hash algorithm used in signature scheme. + * + * @generated from field: optional license_protocol.HashAlgorithmProto hash_algorithm = 4; + */ + hashAlgorithm?: HashAlgorithmProto; }; /** @@ -1999,143 +1999,143 @@ export const SignedDrmCertificateSchema: GenMessage<SignedDrmCertificate> = /*@_ * @generated from message license_protocol.WidevinePsshData */ export type WidevinePsshData = Message<'license_protocol.WidevinePsshData'> & { - /** - * Entitlement or content key IDs. Can onnly present in SINGLE or ENTITLEMENT - * PSSHs. May be repeated to facilitate delivery of multiple keys in a - * single license. Cannot be used in conjunction with content_id or - * group_ids, which are the preferred mechanism. - * - * @generated from field: repeated bytes key_ids = 2; - */ - keyIds: Uint8Array[]; + /** + * Entitlement or content key IDs. Can onnly present in SINGLE or ENTITLEMENT + * PSSHs. May be repeated to facilitate delivery of multiple keys in a + * single license. Cannot be used in conjunction with content_id or + * group_ids, which are the preferred mechanism. + * + * @generated from field: repeated bytes key_ids = 2; + */ + keyIds: Uint8Array[]; - /** - * Content identifier which may map to multiple entitlement or content key - * IDs to facilitate the delivery of multiple keys in a single license. - * Cannot be present in conjunction with key_ids, but if used must be in all - * PSSHs. - * - * @generated from field: optional bytes content_id = 4; - */ - contentId?: Uint8Array; + /** + * Content identifier which may map to multiple entitlement or content key + * IDs to facilitate the delivery of multiple keys in a single license. + * Cannot be present in conjunction with key_ids, but if used must be in all + * PSSHs. + * + * @generated from field: optional bytes content_id = 4; + */ + contentId?: Uint8Array; - /** - * Crypto period index, for media using key rotation. Always corresponds to - * The content key period. This means that if using entitlement licensing - * the ENTITLED_KEY PSSHs will have sequential crypto_period_index's, whereas - * the ENTITELEMENT PSSHs will have gaps in the sequence. Required if doing - * key rotation. - * - * @generated from field: optional uint32 crypto_period_index = 7; - */ - cryptoPeriodIndex?: number; + /** + * Crypto period index, for media using key rotation. Always corresponds to + * The content key period. This means that if using entitlement licensing + * the ENTITLED_KEY PSSHs will have sequential crypto_period_index's, whereas + * the ENTITELEMENT PSSHs will have gaps in the sequence. Required if doing + * key rotation. + * + * @generated from field: optional uint32 crypto_period_index = 7; + */ + cryptoPeriodIndex?: number; - /** - * Protection scheme identifying the encryption algorithm. The protection - * scheme is represented as a uint32 value. The uint32 contains 4 bytes each - * representing a single ascii character in one of the 4CC protection scheme - * values. To be deprecated in favor of signaling from content. - * 'cenc' (AES-CTR) protection_scheme = 0x63656E63, - * 'cbc1' (AES-CBC) protection_scheme = 0x63626331, - * 'cens' (AES-CTR pattern encryption) protection_scheme = 0x63656E73, - * 'cbcs' (AES-CBC pattern encryption) protection_scheme = 0x63626373. - * - * @generated from field: optional uint32 protection_scheme = 9; - */ - protectionScheme?: number; + /** + * Protection scheme identifying the encryption algorithm. The protection + * scheme is represented as a uint32 value. The uint32 contains 4 bytes each + * representing a single ascii character in one of the 4CC protection scheme + * values. To be deprecated in favor of signaling from content. + * 'cenc' (AES-CTR) protection_scheme = 0x63656E63, + * 'cbc1' (AES-CBC) protection_scheme = 0x63626331, + * 'cens' (AES-CTR pattern encryption) protection_scheme = 0x63656E73, + * 'cbcs' (AES-CBC pattern encryption) protection_scheme = 0x63626373. + * + * @generated from field: optional uint32 protection_scheme = 9; + */ + protectionScheme?: number; - /** - * Optional. For media using key rotation, this represents the duration - * of each crypto period in seconds. - * - * @generated from field: optional uint32 crypto_period_seconds = 10; - */ - cryptoPeriodSeconds?: number; + /** + * Optional. For media using key rotation, this represents the duration + * of each crypto period in seconds. + * + * @generated from field: optional uint32 crypto_period_seconds = 10; + */ + cryptoPeriodSeconds?: number; - /** - * Type of PSSH. Required if not SINGLE. - * - * @generated from field: optional license_protocol.WidevinePsshData.Type type = 11; - */ - type?: WidevinePsshData_Type; + /** + * Type of PSSH. Required if not SINGLE. + * + * @generated from field: optional license_protocol.WidevinePsshData.Type type = 11; + */ + type?: WidevinePsshData_Type; - /** - * Key sequence for Widevine-managed keys. Optional. - * - * @generated from field: optional uint32 key_sequence = 12; - */ - keySequence?: number; + /** + * Key sequence for Widevine-managed keys. Optional. + * + * @generated from field: optional uint32 key_sequence = 12; + */ + keySequence?: number; - /** - * Group identifiers for all groups to which the content belongs. This can - * be used to deliver licenses to unlock multiple titles / channels. - * Optional, and may only be present in ENTITLEMENT and ENTITLED_KEY PSSHs, and - * not in conjunction with key_ids. - * - * @generated from field: repeated bytes group_ids = 13; - */ - groupIds: Uint8Array[]; + /** + * Group identifiers for all groups to which the content belongs. This can + * be used to deliver licenses to unlock multiple titles / channels. + * Optional, and may only be present in ENTITLEMENT and ENTITLED_KEY PSSHs, and + * not in conjunction with key_ids. + * + * @generated from field: repeated bytes group_ids = 13; + */ + groupIds: Uint8Array[]; - /** - * Copy/copies of the content key used to decrypt the media stream in which - * the PSSH box is embedded, each wrapped with a different entitlement key. - * May also contain sub-licenses to support devices with OEMCrypto 13 or - * older. May be repeated if using group entitlement keys. Present only in - * PSSHs of type ENTITLED_KEY. - * - * @generated from field: repeated license_protocol.WidevinePsshData.EntitledKey entitled_keys = 14; - */ - entitledKeys: WidevinePsshData_EntitledKey[]; + /** + * Copy/copies of the content key used to decrypt the media stream in which + * the PSSH box is embedded, each wrapped with a different entitlement key. + * May also contain sub-licenses to support devices with OEMCrypto 13 or + * older. May be repeated if using group entitlement keys. Present only in + * PSSHs of type ENTITLED_KEY. + * + * @generated from field: repeated license_protocol.WidevinePsshData.EntitledKey entitled_keys = 14; + */ + entitledKeys: WidevinePsshData_EntitledKey[]; - /** - * Video feature identifier, which is used in conjunction with |content_id| - * to determine the set of keys to be returned in the license. Cannot be - * present in conjunction with |key_ids|. - * Current values are "HDR". - * - * @generated from field: optional string video_feature = 15; - */ - videoFeature?: string; + /** + * Video feature identifier, which is used in conjunction with |content_id| + * to determine the set of keys to be returned in the license. Cannot be + * present in conjunction with |key_ids|. + * Current values are "HDR". + * + * @generated from field: optional string video_feature = 15; + */ + videoFeature?: string; - /** - * @generated from field: optional license_protocol.WidevinePsshData.Algorithm algorithm = 1 [deprecated = true]; - * @deprecated - */ - algorithm?: WidevinePsshData_Algorithm; + /** + * @generated from field: optional license_protocol.WidevinePsshData.Algorithm algorithm = 1 [deprecated = true]; + * @deprecated + */ + algorithm?: WidevinePsshData_Algorithm; - /** - * Content provider name. - * - * @generated from field: optional string provider = 3 [deprecated = true]; - * @deprecated - */ - provider?: string; + /** + * Content provider name. + * + * @generated from field: optional string provider = 3 [deprecated = true]; + * @deprecated + */ + provider?: string; - /** - * Track type. Acceptable values are SD, HD and AUDIO. Used to - * differentiate content keys used by an asset. - * - * @generated from field: optional string track_type = 5 [deprecated = true]; - * @deprecated - */ - trackType?: string; + /** + * Track type. Acceptable values are SD, HD and AUDIO. Used to + * differentiate content keys used by an asset. + * + * @generated from field: optional string track_type = 5 [deprecated = true]; + * @deprecated + */ + trackType?: string; - /** - * The name of a registered policy to be used for this asset. - * - * @generated from field: optional string policy = 6 [deprecated = true]; - * @deprecated - */ - policy?: string; + /** + * The name of a registered policy to be used for this asset. + * + * @generated from field: optional string policy = 6 [deprecated = true]; + * @deprecated + */ + policy?: string; - /** - * Optional protected context for group content. The grouped_license is a - * serialized SignedMessage. - * - * @generated from field: optional bytes grouped_license = 8 [deprecated = true]; - * @deprecated - */ - groupedLicense?: Uint8Array; + /** + * Optional protected context for group content. The grouped_license is a + * serialized SignedMessage. + * + * @generated from field: optional bytes grouped_license = 8 [deprecated = true]; + * @deprecated + */ + groupedLicense?: Uint8Array; }; /** @@ -2148,40 +2148,40 @@ export const WidevinePsshDataSchema: GenMessage<WidevinePsshData> = /*@__PURE__* * @generated from message license_protocol.WidevinePsshData.EntitledKey */ export type WidevinePsshData_EntitledKey = Message<'license_protocol.WidevinePsshData.EntitledKey'> & { - /** - * ID of entitlement key used for wrapping |key|. - * - * @generated from field: optional bytes entitlement_key_id = 1; - */ - entitlementKeyId?: Uint8Array; + /** + * ID of entitlement key used for wrapping |key|. + * + * @generated from field: optional bytes entitlement_key_id = 1; + */ + entitlementKeyId?: Uint8Array; - /** - * ID of the entitled key. - * - * @generated from field: optional bytes key_id = 2; - */ - keyId?: Uint8Array; + /** + * ID of the entitled key. + * + * @generated from field: optional bytes key_id = 2; + */ + keyId?: Uint8Array; - /** - * Wrapped key. Required. - * - * @generated from field: optional bytes key = 3; - */ - key?: Uint8Array; + /** + * Wrapped key. Required. + * + * @generated from field: optional bytes key = 3; + */ + key?: Uint8Array; - /** - * IV used for wrapping |key|. Required. - * - * @generated from field: optional bytes iv = 4; - */ - iv?: Uint8Array; + /** + * IV used for wrapping |key|. Required. + * + * @generated from field: optional bytes iv = 4; + */ + iv?: Uint8Array; - /** - * Size of entitlement key used for wrapping |key|. - * - * @generated from field: optional uint32 entitlement_key_size_bytes = 5; - */ - entitlementKeySizeBytes?: number; + /** + * Size of entitlement key used for wrapping |key|. + * + * @generated from field: optional uint32 entitlement_key_size_bytes = 5; + */ + entitlementKeySizeBytes?: number; }; /** @@ -2194,26 +2194,26 @@ export const WidevinePsshData_EntitledKeySchema: GenMessage<WidevinePsshData_Ent * @generated from enum license_protocol.WidevinePsshData.Type */ export enum WidevinePsshData_Type { - /** - * Single PSSH to be used to retrieve content keys. - * - * @generated from enum value: SINGLE = 0; - */ - SINGLE = 0, + /** + * Single PSSH to be used to retrieve content keys. + * + * @generated from enum value: SINGLE = 0; + */ + SINGLE = 0, - /** - * Primary PSSH used to retrieve entitlement keys. - * - * @generated from enum value: ENTITLEMENT = 1; - */ - ENTITLEMENT = 1, + /** + * Primary PSSH used to retrieve entitlement keys. + * + * @generated from enum value: ENTITLEMENT = 1; + */ + ENTITLEMENT = 1, - /** - * Secondary PSSH containing entitled key(s). - * - * @generated from enum value: ENTITLED_KEY = 2; - */ - ENTITLED_KEY = 2 + /** + * Secondary PSSH containing entitled key(s). + * + * @generated from enum value: ENTITLED_KEY = 2; + */ + ENTITLED_KEY = 2 } /** @@ -2227,15 +2227,15 @@ export const WidevinePsshData_TypeSchema: GenEnum<WidevinePsshData_Type> = /*@__ * @generated from enum license_protocol.WidevinePsshData.Algorithm */ export enum WidevinePsshData_Algorithm { - /** - * @generated from enum value: UNENCRYPTED = 0; - */ - UNENCRYPTED = 0, + /** + * @generated from enum value: UNENCRYPTED = 0; + */ + UNENCRYPTED = 0, - /** - * @generated from enum value: AESCTR = 1; - */ - AESCTR = 1 + /** + * @generated from enum value: AESCTR = 1; + */ + AESCTR = 1 } /** @@ -2249,15 +2249,15 @@ export const WidevinePsshData_AlgorithmSchema: GenEnum<WidevinePsshData_Algorith * @generated from message license_protocol.FileHashes */ export type FileHashes = Message<'license_protocol.FileHashes'> & { - /** - * @generated from field: optional bytes signer = 1; - */ - signer?: Uint8Array; + /** + * @generated from field: optional bytes signer = 1; + */ + signer?: Uint8Array; - /** - * @generated from field: repeated license_protocol.FileHashes.Signature signatures = 2; - */ - signatures: FileHashes_Signature[]; + /** + * @generated from field: repeated license_protocol.FileHashes.Signature signatures = 2; + */ + signatures: FileHashes_Signature[]; }; /** @@ -2270,34 +2270,34 @@ export const FileHashesSchema: GenMessage<FileHashes> = /*@__PURE__*/ messageDes * @generated from message license_protocol.FileHashes.Signature */ export type FileHashes_Signature = Message<'license_protocol.FileHashes.Signature'> & { - /** - * @generated from field: optional string filename = 1; - */ - filename?: string; + /** + * @generated from field: optional string filename = 1; + */ + filename?: string; - /** - * 0 - release, 1 - testing - * - * @generated from field: optional bool test_signing = 2; - */ - testSigning?: boolean; + /** + * 0 - release, 1 - testing + * + * @generated from field: optional bool test_signing = 2; + */ + testSigning?: boolean; - /** - * @generated from field: optional bytes SHA512Hash = 3; - */ - SHA512Hash?: Uint8Array; + /** + * @generated from field: optional bytes SHA512Hash = 3; + */ + SHA512Hash?: Uint8Array; - /** - * 0 for dlls, 1 for exe, this is field 3 in file - * - * @generated from field: optional bool main_exe = 4; - */ - mainExe?: boolean; + /** + * 0 for dlls, 1 for exe, this is field 3 in file + * + * @generated from field: optional bool main_exe = 4; + */ + mainExe?: boolean; - /** - * @generated from field: optional bytes signature = 5; - */ - signature?: Uint8Array; + /** + * @generated from field: optional bytes signature = 5; + */ + signature?: Uint8Array; }; /** @@ -2310,27 +2310,27 @@ export const FileHashes_SignatureSchema: GenMessage<FileHashes_Signature> = /*@_ * @generated from enum license_protocol.LicenseType */ export enum LicenseType { - /** - * @generated from enum value: LICENSETYPE_UNVERIFIED = 0; - */ - LICENSETYPE_UNVERIFIED = 0, + /** + * @generated from enum value: LICENSETYPE_UNVERIFIED = 0; + */ + LICENSETYPE_UNVERIFIED = 0, - /** - * @generated from enum value: STREAMING = 1; - */ - STREAMING = 1, + /** + * @generated from enum value: STREAMING = 1; + */ + STREAMING = 1, - /** - * @generated from enum value: OFFLINE = 2; - */ - OFFLINE = 2, + /** + * @generated from enum value: OFFLINE = 2; + */ + OFFLINE = 2, - /** - * License type decision is left to provider. - * - * @generated from enum value: AUTOMATIC = 3; - */ - AUTOMATIC = 3 + /** + * License type decision is left to provider. + * + * @generated from enum value: AUTOMATIC = 3; + */ + AUTOMATIC = 3 } /** @@ -2342,48 +2342,48 @@ export const LicenseTypeSchema: GenEnum<LicenseType> = /*@__PURE__*/ enumDesc(fi * @generated from enum license_protocol.PlatformVerificationStatus */ export enum PlatformVerificationStatus { - /** - * The platform is not verified. - * - * @generated from enum value: PLATFORM_UNVERIFIED = 0; - */ - PLATFORM_UNVERIFIED = 0, + /** + * The platform is not verified. + * + * @generated from enum value: PLATFORM_UNVERIFIED = 0; + */ + PLATFORM_UNVERIFIED = 0, - /** - * Tampering detected on the platform. - * - * @generated from enum value: PLATFORM_TAMPERED = 1; - */ - PLATFORM_TAMPERED = 1, + /** + * Tampering detected on the platform. + * + * @generated from enum value: PLATFORM_TAMPERED = 1; + */ + PLATFORM_TAMPERED = 1, - /** - * The platform has been verified by means of software. - * - * @generated from enum value: PLATFORM_SOFTWARE_VERIFIED = 2; - */ - PLATFORM_SOFTWARE_VERIFIED = 2, + /** + * The platform has been verified by means of software. + * + * @generated from enum value: PLATFORM_SOFTWARE_VERIFIED = 2; + */ + PLATFORM_SOFTWARE_VERIFIED = 2, - /** - * The platform has been verified by means of hardware (e.g. secure boot). - * - * @generated from enum value: PLATFORM_HARDWARE_VERIFIED = 3; - */ - PLATFORM_HARDWARE_VERIFIED = 3, + /** + * The platform has been verified by means of hardware (e.g. secure boot). + * + * @generated from enum value: PLATFORM_HARDWARE_VERIFIED = 3; + */ + PLATFORM_HARDWARE_VERIFIED = 3, - /** - * Platform verification was not performed. - * - * @generated from enum value: PLATFORM_NO_VERIFICATION = 4; - */ - PLATFORM_NO_VERIFICATION = 4, + /** + * Platform verification was not performed. + * + * @generated from enum value: PLATFORM_NO_VERIFICATION = 4; + */ + PLATFORM_NO_VERIFICATION = 4, - /** - * Platform and secure storage capability have been verified by means of - * software. - * - * @generated from enum value: PLATFORM_SECURE_STORAGE_SOFTWARE_VERIFIED = 5; - */ - PLATFORM_SECURE_STORAGE_SOFTWARE_VERIFIED = 5 + /** + * Platform and secure storage capability have been verified by means of + * software. + * + * @generated from enum value: PLATFORM_SECURE_STORAGE_SOFTWARE_VERIFIED = 5; + */ + PLATFORM_SECURE_STORAGE_SOFTWARE_VERIFIED = 5 } /** @@ -2395,25 +2395,25 @@ export const PlatformVerificationStatusSchema: GenEnum<PlatformVerificationStatu * @generated from enum license_protocol.ProtocolVersion */ export enum ProtocolVersion { - /** - * @generated from enum value: VERSION_UNVERIFIED = 0; - */ - VERSION_UNVERIFIED = 0, + /** + * @generated from enum value: VERSION_UNVERIFIED = 0; + */ + VERSION_UNVERIFIED = 0, - /** - * @generated from enum value: VERSION_2_0 = 20; - */ - VERSION_2_0 = 20, + /** + * @generated from enum value: VERSION_2_0 = 20; + */ + VERSION_2_0 = 20, - /** - * @generated from enum value: VERSION_2_1 = 21; - */ - VERSION_2_1 = 21, + /** + * @generated from enum value: VERSION_2_1 = 21; + */ + VERSION_2_1 = 21, - /** - * @generated from enum value: VERSION_2_2 = 22; - */ - VERSION_2_2 = 22 + /** + * @generated from enum value: VERSION_2_2 = 22; + */ + VERSION_2_2 = 22 } /** @@ -2425,28 +2425,28 @@ export const ProtocolVersionSchema: GenEnum<ProtocolVersion> = /*@__PURE__*/ enu * @generated from enum license_protocol.HashAlgorithmProto */ export enum HashAlgorithmProto { - /** - * Unspecified hash algorithm: SHA_256 shall be used for ECC based algorithms - * and SHA_1 shall be used otherwise. - * - * @generated from enum value: HASH_ALGORITHM_UNSPECIFIED = 0; - */ - HASH_ALGORITHM_UNSPECIFIED = 0, + /** + * Unspecified hash algorithm: SHA_256 shall be used for ECC based algorithms + * and SHA_1 shall be used otherwise. + * + * @generated from enum value: HASH_ALGORITHM_UNSPECIFIED = 0; + */ + HASH_ALGORITHM_UNSPECIFIED = 0, - /** - * @generated from enum value: HASH_ALGORITHM_SHA_1 = 1; - */ - HASH_ALGORITHM_SHA_1 = 1, + /** + * @generated from enum value: HASH_ALGORITHM_SHA_1 = 1; + */ + HASH_ALGORITHM_SHA_1 = 1, - /** - * @generated from enum value: HASH_ALGORITHM_SHA_256 = 2; - */ - HASH_ALGORITHM_SHA_256 = 2, + /** + * @generated from enum value: HASH_ALGORITHM_SHA_256 = 2; + */ + HASH_ALGORITHM_SHA_256 = 2, - /** - * @generated from enum value: HASH_ALGORITHM_SHA_384 = 3; - */ - HASH_ALGORITHM_SHA_384 = 3 + /** + * @generated from enum value: HASH_ALGORITHM_SHA_384 = 3; + */ + HASH_ALGORITHM_SHA_384 = 3 } /** diff --git a/package.json b/package.json index 4501083..7dda998 100644 --- a/package.json +++ b/package.json @@ -1,120 +1,123 @@ { - "name": "multi-downloader-nx", - "short_name": "aniDL", - "version": "5.5.7", - "description": "Downloader for Crunchyroll, Hidive, AnimeOnegai, and AnimationDigitalNetwork with CLI and GUI", - "keywords": [ - "download", - "downloader", - "hidive", - "crunchy", - "crunchyroll", - "util", - "utility", - "cli", - "gui" - ], - "engines": { - "node": ">=18", - "pnpm": ">=7" - }, - "author": "AnimeDL <AnimeDL@users.noreply.github.com>", - "contributors": [ - { - "name": "AnimeDL <AnimeDL@users.noreply.github.com>" - }, - { - "name": "AniDL <AniDL@users.noreply.github.com>" - }, - { - "name": "AnidlSupport <AnidlSupport@users.noreply.github.com>" - } - ], - "homepage": "https://github.com/anidl/multi-downloader-nx", - "repository": { - "type": "git", - "url": "https://github.com/anidl/multi-downloader-nx.git" - }, - "bugs": { - "url": "https://github.com/anidl/multi-downloader-nx/issues" - }, - "license": "MIT", - "dependencies": { - "@bufbuild/buf": "^1.57.2", - "@bufbuild/protobuf": "^2.8.0", - "@bufbuild/protoc-gen-es": "^2.8.0", - "@yao-pkg/pkg": "^6.6.0", - "binary-parser": "^2.2.1", - "binary-parser-encoder": "^1.5.3", - "bn.js": "^5.2.2", - "cors": "^2.8.5", - "elliptic": "^6.6.1", - "esbuild": "^0.25.10", - "express": "^5.1.0", - "fast-xml-parser": "^5.2.5", - "ffprobe": "^1.1.2", - "fs-extra": "^11.3.2", - "iso-639": "^0.2.2", - "leven": "^3.1.0", - "log4js": "^6.9.1", - "long": "^5.3.2", - "lookpath": "^1.2.3", - "m3u8-parsed": "^2.0.0", - "mpd-parser": "^1.3.1", - "node-forge": "^1.3.1", - "ofetch": "^1.4.1", - "open": "^8.4.2", - "protobufjs": "^7.5.4", - "puppeteer-real-browser": "^1.4.4", - "ws": "^8.18.3", - "yaml": "^2.8.1", - "yargs": "^17.7.2" - }, - "devDependencies": { - "@eslint/js": "^9.35.0", - "@types/bn.js": "^5.2.0", - "@types/cors": "^2.8.19", - "@types/elliptic": "^6.4.18", - "@types/express": "^5.0.3", - "@types/ffprobe": "^1.1.8", - "@types/fs-extra": "^11.0.4", - "@types/node": "^24.5.1", - "@types/node-forge": "^1.3.14", - "@types/ws": "^8.18.1", - "@types/yargs": "^17.0.33", - "@typescript-eslint/eslint-plugin": "^8.44.0", - "@typescript-eslint/parser": "^8.44.0", - "eslint": "^9.35.0", - "protoc": "^1.1.3", - "removeNPMAbsolutePaths": "^3.0.1", - "ts-node": "^10.9.2", - "typescript": "^5.9.2", - "typescript-eslint": "^8.44.0" - }, - "scripts": { - "prestart": "pnpm run tsc test", - "start": "pnpm prestart && cd lib && node gui.js", - "gui": "cd ./gui/react/ && pnpm start", - "docs": "ts-node modules/build-docs.ts", - "tsc": "ts-node tsc.ts", - "proto:compile": "protoc --plugin=protoc-gen-ts_proto=.\\node_modules\\.bin\\protoc-gen-ts_proto.cmd --ts_proto_opt=\"esModuleInterop=true\" --ts_proto_opt=\"forceLong=long\" --ts_proto_opt=\"env=node\" --ts_proto_out=. modules/*.proto", - "prebuild-cli": "pnpm run tsc false false", - "build-windows-cli": "pnpm run prebuild-cli && cd lib && node modules/build windows-x64", - "build-linux-cli": "pnpm run prebuild-cli && cd lib && node modules/build linuxstatic-x64", - "build-arm-cli": "pnpm run prebuild-cli && cd lib && node modules/build linux-arm64", - "build-macos-cli": "pnpm run prebuild-cli && cd lib && node modules/build macos-x64", - "build-alpine-cli": "pnpm run prebuild-cli && cd lib && node modules/build alpine-x64", - "build-android-cli": "pnpm run prebuild-cli && cd lib && node modules/build linuxstatic-armv7", - "prebuild-gui": "pnpm run tsc", - "build-windows-gui": "pnpm run prebuild-gui && cd lib && node modules/build windows-x64 true", - "build-linux-gui": "pnpm run prebuild-gui && cd lib && node modules/build linuxstatic-x64 true", - "build-arm-gui": "pnpm run prebuild-gui && cd lib && node modules/build linux-arm64 true", - "build-macos-gui": "pnpm run prebuild-gui && cd lib && node modules/build macos-x64 true", - "build-alpine-gui": "pnpm run prebuild-gui && cd lib && node modules/build alpine-x64 true", - "build-android-gui": "pnpm run prebuild-gui && cd lib && node modules/build linuxstatic-armv7 true", - "eslint": "npx eslint .", - "eslint-fix": "npx eslint . --fix", - "pretest": "pnpm run tsc", - "test": "pnpm run pretest && cd lib && node modules/build windows-x64 && node modules/build linuxstatic-x64 && node modules/build macos-x64" - } + "name": "multi-downloader-nx", + "short_name": "aniDL", + "version": "5.5.7", + "description": "Downloader for Crunchyroll, Hidive, AnimeOnegai, and AnimationDigitalNetwork with CLI and GUI", + "keywords": [ + "download", + "downloader", + "hidive", + "crunchy", + "crunchyroll", + "util", + "utility", + "cli", + "gui" + ], + "engines": { + "node": ">=18", + "pnpm": ">=7" + }, + "author": "AnimeDL <AnimeDL@users.noreply.github.com>", + "contributors": [ + { + "name": "AnimeDL <AnimeDL@users.noreply.github.com>" + }, + { + "name": "AniDL <AniDL@users.noreply.github.com>" + }, + { + "name": "AnidlSupport <AnidlSupport@users.noreply.github.com>" + } + ], + "homepage": "https://github.com/anidl/multi-downloader-nx", + "repository": { + "type": "git", + "url": "https://github.com/anidl/multi-downloader-nx.git" + }, + "bugs": { + "url": "https://github.com/anidl/multi-downloader-nx/issues" + }, + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "^2.9.0", + "binary-parser": "^2.2.1", + "binary-parser-encoder": "^1.5.3", + "bn.js": "^5.2.2", + "cors": "^2.8.5", + "elliptic": "^6.6.1", + "esbuild": "^0.25.10", + "express": "^5.1.0", + "fast-xml-parser": "^5.2.5", + "ffprobe": "^1.1.2", + "fs-extra": "^11.3.2", + "iso-639": "^0.2.2", + "leven": "^3.1.0", + "log4js": "^6.9.1", + "long": "^5.3.2", + "lookpath": "^1.2.3", + "m3u8-parsed": "^2.0.0", + "mpd-parser": "^1.3.1", + "node-forge": "^1.3.1", + "ofetch": "^1.4.1", + "open": "^8.4.2", + "protobufjs": "^7.5.4", + "puppeteer-real-browser": "^1.4.4", + "ws": "^8.18.3", + "yaml": "^2.8.1", + "yargs": "17.7.2" + }, + "devDependencies": { + "@bufbuild/buf": "^1.57.2", + "@bufbuild/protoc-gen-es": "^2.9.0", + "@eslint/js": "^9.36.0", + "@types/bn.js": "^5.2.0", + "@types/cors": "^2.8.19", + "@types/elliptic": "^6.4.18", + "@types/express": "^5.0.3", + "@types/ffprobe": "^1.1.8", + "@types/fs-extra": "^11.0.4", + "@types/node": "^24.6.0", + "@types/node-forge": "^1.3.14", + "@types/ws": "^8.18.1", + "@types/yargs": "^17.0.33", + "@typescript-eslint/eslint-plugin": "^8.45.0", + "@typescript-eslint/parser": "^8.45.0", + "@yao-pkg/pkg": "^6.7.0", + "eslint": "^9.36.0", + "eslint-config-prettier": "^10.1.8", + "prettier": "^3.6.2", + "protoc": "^1.1.3", + "removeNPMAbsolutePaths": "^3.0.1", + "ts-node": "^10.9.2", + "typescript": "^5.9.2", + "typescript-eslint": "^8.45.0" + }, + "scripts": { + "prestart": "pnpm run tsc test", + "start": "pnpm prestart && cd lib && node gui.js", + "gui": "cd ./gui/react/ && pnpm start", + "docs": "ts-node modules/build-docs.ts", + "tsc": "ts-node tsc.ts", + "proto:compile": "protoc --plugin=protoc-gen-ts_proto=.\\node_modules\\.bin\\protoc-gen-ts_proto.cmd --ts_proto_opt=\"esModuleInterop=true\" --ts_proto_opt=\"forceLong=long\" --ts_proto_opt=\"env=node\" --ts_proto_out=. modules/*.proto", + "prebuild-cli": "pnpm run tsc false false", + "build-windows-cli": "pnpm run prebuild-cli && cd lib && node modules/build windows-x64", + "build-linux-cli": "pnpm run prebuild-cli && cd lib && node modules/build linuxstatic-x64", + "build-arm-cli": "pnpm run prebuild-cli && cd lib && node modules/build linux-arm64", + "build-macos-cli": "pnpm run prebuild-cli && cd lib && node modules/build macos-x64", + "build-alpine-cli": "pnpm run prebuild-cli && cd lib && node modules/build alpine-x64", + "build-android-cli": "pnpm run prebuild-cli && cd lib && node modules/build linuxstatic-armv7", + "prebuild-gui": "pnpm run tsc", + "build-windows-gui": "pnpm run prebuild-gui && cd lib && node modules/build windows-x64 true", + "build-linux-gui": "pnpm run prebuild-gui && cd lib && node modules/build linuxstatic-x64 true", + "build-arm-gui": "pnpm run prebuild-gui && cd lib && node modules/build linux-arm64 true", + "build-macos-gui": "pnpm run prebuild-gui && cd lib && node modules/build macos-x64 true", + "build-alpine-gui": "pnpm run prebuild-gui && cd lib && node modules/build alpine-x64 true", + "build-android-gui": "pnpm run prebuild-gui && cd lib && node modules/build linuxstatic-armv7 true", + "eslint": "npx eslint . --quiet", + "prettier": "npx prettier . --check", + "prettier-fix": "npx prettier . --write", + "pretest": "pnpm run tsc", + "test": "pnpm run pretest && cd lib && node modules/build windows-x64 && node modules/build linuxstatic-x64 && node modules/build macos-x64" + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4481fb5..b023d61 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,18 +8,9 @@ importers: .: dependencies: - '@bufbuild/buf': - specifier: ^1.57.2 - version: 1.57.2 '@bufbuild/protobuf': - specifier: ^2.8.0 - version: 2.8.0 - '@bufbuild/protoc-gen-es': - specifier: ^2.8.0 - version: 2.8.0(@bufbuild/protobuf@2.8.0) - '@yao-pkg/pkg': - specifier: ^6.6.0 - version: 6.6.0 + specifier: ^2.9.0 + version: 2.9.0 binary-parser: specifier: ^2.2.1 version: 2.2.1 @@ -93,12 +84,18 @@ importers: specifier: ^2.8.1 version: 2.8.1 yargs: - specifier: ^17.7.2 + specifier: 17.7.2 version: 17.7.2 devDependencies: + '@bufbuild/buf': + specifier: ^1.57.2 + version: 1.57.2 + '@bufbuild/protoc-gen-es': + specifier: ^2.9.0 + version: 2.9.0(@bufbuild/protobuf@2.9.0) '@eslint/js': - specifier: ^9.35.0 - version: 9.35.0 + specifier: ^9.36.0 + version: 9.36.0 '@types/bn.js': specifier: ^5.2.0 version: 5.2.0 @@ -118,8 +115,8 @@ importers: specifier: ^11.0.4 version: 11.0.4 '@types/node': - specifier: ^24.5.1 - version: 24.5.1 + specifier: ^24.6.0 + version: 24.6.0 '@types/node-forge': specifier: ^1.3.14 version: 1.3.14 @@ -130,14 +127,23 @@ importers: specifier: ^17.0.33 version: 17.0.33 '@typescript-eslint/eslint-plugin': - specifier: ^8.44.0 - version: 8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0)(typescript@5.9.2))(eslint@9.35.0)(typescript@5.9.2) + specifier: ^8.45.0 + version: 8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2) '@typescript-eslint/parser': - specifier: ^8.44.0 - version: 8.44.0(eslint@9.35.0)(typescript@5.9.2) + specifier: ^8.45.0 + version: 8.45.0(eslint@9.36.0)(typescript@5.9.2) + '@yao-pkg/pkg': + specifier: ^6.7.0 + version: 6.7.0 eslint: - specifier: ^9.35.0 - version: 9.35.0 + specifier: ^9.36.0 + version: 9.36.0 + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@9.36.0) + prettier: + specifier: ^3.6.2 + version: 3.6.2 protoc: specifier: ^1.1.3 version: 1.1.3 @@ -146,13 +152,13 @@ importers: version: 3.0.1 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@24.5.1)(typescript@5.9.2) + version: 10.9.2(@types/node@24.6.0)(typescript@5.9.2) typescript: specifier: ^5.9.2 version: 5.9.2 typescript-eslint: - specifier: ^8.44.0 - version: 8.44.0(eslint@9.35.0)(typescript@5.9.2) + specifier: ^8.45.0 + version: 8.45.0(eslint@9.36.0)(typescript@5.9.2) packages: @@ -228,21 +234,21 @@ packages: engines: {node: '>=12'} hasBin: true - '@bufbuild/protobuf@2.8.0': - resolution: {integrity: sha512-r1/0w5C9dkbcdjyxY8ZHsC5AOWg4Pnzhm2zu7LO4UHSounp2tMm6Y+oioV9zlGbLveE7YaWRDUk48WLxRDgoqg==} + '@bufbuild/protobuf@2.9.0': + resolution: {integrity: sha512-rnJenoStJ8nvmt9Gzye8nkYd6V22xUAnu4086ER7h1zJ508vStko4pMvDeQ446ilDTFpV5wnoc5YS7XvMwwMqA==} - '@bufbuild/protoc-gen-es@2.8.0': - resolution: {integrity: sha512-wyCXK7/FOgcHdqP6dWnGNZBJyXcYYUcvZBMBQVgeYjbF5u0OgVsInEYnr2Gu4E2HC7HoxcQZcVlVbwyxeiebDA==} + '@bufbuild/protoc-gen-es@2.9.0': + resolution: {integrity: sha512-g54rrHLKc7fnxN25ikynstRxR19M5G5l/hyqut2JoypJ9iU9QgUE63zEm8PNvVfBd4r5PEzWPfbWy5MNOeuMKQ==} engines: {node: '>=20'} hasBin: true peerDependencies: - '@bufbuild/protobuf': 2.8.0 + '@bufbuild/protobuf': 2.9.0 peerDependenciesMeta: '@bufbuild/protobuf': optional: true - '@bufbuild/protoplugin@2.8.0': - resolution: {integrity: sha512-kwZeZAz6jmDC571Wf/f5cDX02IjvPmtdspdxW/hbLS+bnZHkXMW4StaW+irGd4TkykvHD4fFgBnPG/O1x8OgXw==} + '@bufbuild/protoplugin@2.9.0': + resolution: {integrity: sha512-uoiwNVYoTq+AyqaV1L6pBazGx5fXOO89L0NSR9/7hEfo0Y8n9T1jsKGu4mkitLmP3z+8gJREaule1mMuKBPyYw==} '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} @@ -430,8 +436,8 @@ packages: resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.35.0': - resolution: {integrity: sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==} + '@eslint/js@9.36.0': + resolution: {integrity: sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.6': @@ -594,8 +600,8 @@ packages: '@types/node-forge@1.3.14': resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==} - '@types/node@24.5.1': - resolution: {integrity: sha512-/SQdmUP2xa+1rdx7VwB9yPq8PaKej8TD5cQ+XfKDPWWC+VDJU4rvVVagXqKUzhKjtFoNA8rXDJAkCxQPAe00+Q==} + '@types/node@24.6.0': + resolution: {integrity: sha512-F1CBxgqwOMc4GKJ7eY22hWhBVQuMYTtqI8L0FcszYcpYX0fzfDGpez22Xau8Mgm7O9fI+zA/TYIdq3tGWfweBA==} '@types/qs@6.14.0': resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} @@ -621,63 +627,63 @@ packages: '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - '@typescript-eslint/eslint-plugin@8.44.0': - resolution: {integrity: sha512-EGDAOGX+uwwekcS0iyxVDmRV9HX6FLSM5kzrAToLTsr9OWCIKG/y3lQheCq18yZ5Xh78rRKJiEpP0ZaCs4ryOQ==} + '@typescript-eslint/eslint-plugin@8.45.0': + resolution: {integrity: sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.44.0 + '@typescript-eslint/parser': ^8.45.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.44.0': - resolution: {integrity: sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw==} + '@typescript-eslint/parser@8.45.0': + resolution: {integrity: sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.44.0': - resolution: {integrity: sha512-ZeaGNraRsq10GuEohKTo4295Z/SuGcSq2LzfGlqiuEvfArzo/VRrT0ZaJsVPuKZ55lVbNk8U6FcL+ZMH8CoyVA==} + '@typescript-eslint/project-service@8.45.0': + resolution: {integrity: sha512-3pcVHwMG/iA8afdGLMuTibGR7pDsn9RjDev6CCB+naRsSYs2pns5QbinF4Xqw6YC/Sj3lMrm/Im0eMfaa61WUg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.44.0': - resolution: {integrity: sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA==} + '@typescript-eslint/scope-manager@8.45.0': + resolution: {integrity: sha512-clmm8XSNj/1dGvJeO6VGH7EUSeA0FMs+5au/u3lrA3KfG8iJ4u8ym9/j2tTEoacAffdW1TVUzXO30W1JTJS7dA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.44.0': - resolution: {integrity: sha512-x5Y0+AuEPqAInc6yd0n5DAcvtoQ/vyaGwuX5HE9n6qAefk1GaedqrLQF8kQGylLUb9pnZyLf+iEiL9fr8APDtQ==} + '@typescript-eslint/tsconfig-utils@8.45.0': + resolution: {integrity: sha512-aFdr+c37sc+jqNMGhH+ajxPXwjv9UtFZk79k8pLoJ6p4y0snmYpPA52GuWHgt2ZF4gRRW6odsEj41uZLojDt5w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.44.0': - resolution: {integrity: sha512-9cwsoSxJ8Sak67Be/hD2RNt/fsqmWnNE1iHohG8lxqLSNY8xNfyY7wloo5zpW3Nu9hxVgURevqfcH6vvKCt6yg==} + '@typescript-eslint/type-utils@8.45.0': + resolution: {integrity: sha512-bpjepLlHceKgyMEPglAeULX1vixJDgaKocp0RVJ5u4wLJIMNuKtUXIczpJCPcn2waII0yuvks/5m5/h3ZQKs0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.44.0': - resolution: {integrity: sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA==} + '@typescript-eslint/types@8.45.0': + resolution: {integrity: sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.44.0': - resolution: {integrity: sha512-lqNj6SgnGcQZwL4/SBJ3xdPEfcBuhCG8zdcwCPgYcmiPLgokiNDKlbPzCwEwu7m279J/lBYWtDYL+87OEfn8Jw==} + '@typescript-eslint/typescript-estree@8.45.0': + resolution: {integrity: sha512-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.44.0': - resolution: {integrity: sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg==} + '@typescript-eslint/utils@8.45.0': + resolution: {integrity: sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.44.0': - resolution: {integrity: sha512-zaz9u8EJ4GBmnehlrpoKvj/E3dNbuQ7q0ucyZImm3cLqJ8INTc970B1qEqDX/Rzq65r3TvVTN7kHWPBoyW7DWw==} + '@typescript-eslint/visitor-keys@8.45.0': + resolution: {integrity: sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript/vfs@1.6.1': @@ -697,12 +703,12 @@ packages: resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} engines: {node: '>=10.0.0'} - '@yao-pkg/pkg-fetch@3.5.24': - resolution: {integrity: sha512-FPESCH1uXCYui6jeDp2aayWuFHR39w+uU1r88nI6JWRvPYOU64cHPUV/p6GSFoQdpna7ip92HnrZKbBC60l0gA==} + '@yao-pkg/pkg-fetch@3.5.25': + resolution: {integrity: sha512-6deLQjwn5EJVCGRb9Rsy5c8TZixRgiBHMu3cIFVakwZR6ebidE16/Oc7WDBvhQg9N3B3ExgDi7QA19w7Z2GZkA==} hasBin: true - '@yao-pkg/pkg@6.6.0': - resolution: {integrity: sha512-3/oiaSm7fS0Fc7dzp22r9B7vFaguGhO9vERgEReRYj2EUzdi5ssyYhe1uYJG4ec/dmo2GG6RRHOUAT8savl79Q==} + '@yao-pkg/pkg@6.7.0': + resolution: {integrity: sha512-Q06diprlqZrZ0SFefUUhvVj06QboHsBOLyml2CpzWvMUdV7fleSZ8wq5tBrVfmjWu2/ka4bDWHwlocHuYD7HOQ==} engines: {node: '>=18.0.0'} hasBin: true @@ -757,8 +763,16 @@ packages: resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} engines: {node: '>=4'} - b4a@1.7.1: - resolution: {integrity: sha512-ZovbrBV0g6JxK5cGUF1Suby1vLfKjv4RWi8IxoaO/Mon8BDD9I21RxjHFtgQ+kskJqLAVyQZly3uMBui+vhc8Q==} + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + async-generator-function@1.0.0: + resolution: {integrity: sha512-+NAXNqgCrB95ya4Sr66i1CL2hqLVckAk7xwRYWdcm39/ELQ6YNn1aw5r0bdQtqNZgQpEWzc5yc/igXc7aL5SLA==} + engines: {node: '>= 0.4'} + + b4a@1.7.3: + resolution: {integrity: sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==} peerDependencies: react-native-b4a: '*' peerDependenciesMeta: @@ -771,8 +785,8 @@ packages: bare-events@2.7.0: resolution: {integrity: sha512-b3N5eTW1g7vXkw+0CXh/HazGTcO5KYuu/RCNaJbDMPI6LHDi+7qe8EmxKUVe1sUbY2KZOVZFyj62x0OEz9qyAA==} - bare-fs@4.4.4: - resolution: {integrity: sha512-Q8yxM1eLhJfuM7KXVP3zjhBvtMJCYRByoTT+wHXjpdMELv0xICFJX+1w4c7csa+WZEOsq4ItJ4RGwvzid6m/dw==} + bare-fs@4.4.5: + resolution: {integrity: sha512-TCtu93KGLu6/aiGWzMr12TmSRS6nKdfhAnzTQRbXoSWxkbb9eRd53jQ51jG7g1gYjjtto3hbBrrhzg6djcgiKg==} engines: {bare: '>=1.16.0'} peerDependencies: bare-buffer: '*' @@ -902,8 +916,8 @@ packages: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} - chrome-launcher@1.2.0: - resolution: {integrity: sha512-JbuGuBNss258bvGil7FT4HKdC3SC2K7UAEUqiPy3ACS3Yxo3hAW6bvFpCu2HsIJLgTqxgEX6BkujvzZfLpUD0Q==} + chrome-launcher@1.2.1: + resolution: {integrity: sha512-qmFR5PLMzHyuNJHwOloHPAHhbaNglkfeV/xDtt5b7xiFFyU1I+AZZX0PYseMuhenJSSirgxELYIbswcoc+5H4A==} engines: {node: '>=12.13.0'} hasBin: true @@ -1027,8 +1041,8 @@ packages: destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} - detect-libc@2.1.0: - resolution: {integrity: sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==} + detect-libc@2.1.1: + resolution: {integrity: sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==} engines: {node: '>=8'} devtools-protocol@0.0.1367902: @@ -1097,6 +1111,12 @@ packages: engines: {node: '>=6.0'} hasBin: true + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + eslint-scope@8.4.0: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1109,8 +1129,8 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.35.0: - resolution: {integrity: sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==} + eslint@9.36.0: + resolution: {integrity: sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -1148,6 +1168,9 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -1263,12 +1286,16 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + generator-function@2.0.0: + resolution: {integrity: sha512-xPypGGincdfyl/AiSGa7GjXLkvld9V7GjZlowup9SHIJnQnHLFiLODCd/DqKOp0PBagbHJ68r1KJI9Mut7m4sA==} + engines: {node: '>= 0.4'} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + get-intrinsic@1.3.1: + resolution: {integrity: sha512-fk1ZVEeOX9hVZ6QzoBNEC55+Ucqg4sTVwrVuigZhuRPESVFpMyXnd3sbXvPOwp7Y9riVyANiqhEuRF0G1aVSeQ==} engines: {node: '>= 0.4'} get-proto@1.0.1: @@ -1577,8 +1604,8 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - minizlib@3.0.2: - resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} mitt@3.0.1: @@ -1591,11 +1618,6 @@ packages: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true - mkdirp@3.0.1: - resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} - engines: {node: '>=10'} - hasBin: true - mpd-parser@1.3.1: resolution: {integrity: sha512-1FuyEWI5k2HcmhS1HkKnUAQV7yFPfXPht2DnRRGtoiiAAW+ESTbtEXIDpRkwdU+XyrQuwrIym7UkoPKsZ0SyFw==} hasBin: true @@ -1753,6 +1775,11 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -1980,8 +2007,8 @@ packages: resolution: {integrity: sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==} engines: {node: '>=8.0'} - streamx@2.22.1: - resolution: {integrity: sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==} + streamx@2.23.0: + resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==} string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} @@ -2029,8 +2056,8 @@ packages: tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} - tar@7.4.3: - resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} + tar@7.5.1: + resolution: {integrity: sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==} engines: {node: '>=18'} text-decoder@1.2.3: @@ -2098,8 +2125,8 @@ packages: typed-query-selector@2.12.0: resolution: {integrity: sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==} - typescript-eslint@8.44.0: - resolution: {integrity: sha512-ib7mCkYuIzYonCq9XWF5XNw+fkj2zg629PSa9KNIQ47RXFF763S5BIX4wqz1+FLPogTZoiw8KmCiRPRa8bL3qw==} + typescript-eslint@8.45.0: + resolution: {integrity: sha512-qzDmZw/Z5beNLUrXfd0HIW6MzIaAV5WNDxmMs9/3ojGOpYavofgNAAD/nC6tGV2PczIi0iw8vot2eAe/sBn7zg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -2121,8 +2148,8 @@ packages: unbzip2-stream@1.4.3: resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} - undici-types@7.12.0: - resolution: {integrity: sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==} + undici-types@7.13.0: + resolution: {integrity: sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==} universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} @@ -2309,19 +2336,19 @@ snapshots: '@bufbuild/buf-win32-arm64': 1.57.2 '@bufbuild/buf-win32-x64': 1.57.2 - '@bufbuild/protobuf@2.8.0': {} + '@bufbuild/protobuf@2.9.0': {} - '@bufbuild/protoc-gen-es@2.8.0(@bufbuild/protobuf@2.8.0)': + '@bufbuild/protoc-gen-es@2.9.0(@bufbuild/protobuf@2.9.0)': dependencies: - '@bufbuild/protoplugin': 2.8.0 + '@bufbuild/protoplugin': 2.9.0 optionalDependencies: - '@bufbuild/protobuf': 2.8.0 + '@bufbuild/protobuf': 2.9.0 transitivePeerDependencies: - supports-color - '@bufbuild/protoplugin@2.8.0': + '@bufbuild/protoplugin@2.9.0': dependencies: - '@bufbuild/protobuf': 2.8.0 + '@bufbuild/protobuf': 2.9.0 '@typescript/vfs': 1.6.1(typescript@5.4.5) typescript: 5.4.5 transitivePeerDependencies: @@ -2409,9 +2436,9 @@ snapshots: '@esbuild/win32-x64@0.25.10': optional: true - '@eslint-community/eslint-utils@4.9.0(eslint@9.35.0)': + '@eslint-community/eslint-utils@4.9.0(eslint@9.36.0)': dependencies: - eslint: 9.35.0 + eslint: 9.36.0 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -2444,7 +2471,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.35.0': {} + '@eslint/js@9.36.0': {} '@eslint/object-schema@2.1.6': {} @@ -2551,20 +2578,20 @@ snapshots: '@types/bn.js@5.2.0': dependencies: - '@types/node': 24.5.1 + '@types/node': 24.6.0 '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 24.5.1 + '@types/node': 24.6.0 '@types/connect@3.4.38': dependencies: - '@types/node': 24.5.1 + '@types/node': 24.6.0 '@types/cors@2.8.19': dependencies: - '@types/node': 24.5.1 + '@types/node': 24.6.0 '@types/debug@4.1.12': dependencies: @@ -2578,7 +2605,7 @@ snapshots: '@types/express-serve-static-core@5.0.7': dependencies: - '@types/node': 24.5.1 + '@types/node': 24.6.0 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 0.17.5 @@ -2594,7 +2621,7 @@ snapshots: '@types/fs-extra@11.0.4': dependencies: '@types/jsonfile': 6.1.4 - '@types/node': 24.5.1 + '@types/node': 24.6.0 '@types/http-errors@2.0.5': {} @@ -2602,7 +2629,7 @@ snapshots: '@types/jsonfile@6.1.4': dependencies: - '@types/node': 24.5.1 + '@types/node': 24.6.0 '@types/mime@1.3.5': {} @@ -2610,11 +2637,11 @@ snapshots: '@types/node-forge@1.3.14': dependencies: - '@types/node': 24.5.1 + '@types/node': 24.6.0 - '@types/node@24.5.1': + '@types/node@24.6.0': dependencies: - undici-types: 7.12.0 + undici-types: 7.13.0 '@types/qs@6.14.0': {} @@ -2623,17 +2650,17 @@ snapshots: '@types/send@0.17.5': dependencies: '@types/mime': 1.3.5 - '@types/node': 24.5.1 + '@types/node': 24.6.0 '@types/serve-static@1.15.8': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 24.5.1 + '@types/node': 24.6.0 '@types/send': 0.17.5 '@types/ws@8.18.1': dependencies: - '@types/node': 24.5.1 + '@types/node': 24.6.0 '@types/yargs-parser@21.0.3': {} @@ -2643,18 +2670,18 @@ snapshots: '@types/yauzl@2.10.3': dependencies: - '@types/node': 24.5.1 + '@types/node': 24.6.0 optional: true - '@typescript-eslint/eslint-plugin@8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0)(typescript@5.9.2))(eslint@9.35.0)(typescript@5.9.2)': + '@typescript-eslint/eslint-plugin@8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.44.0(eslint@9.35.0)(typescript@5.9.2) - '@typescript-eslint/scope-manager': 8.44.0 - '@typescript-eslint/type-utils': 8.44.0(eslint@9.35.0)(typescript@5.9.2) - '@typescript-eslint/utils': 8.44.0(eslint@9.35.0)(typescript@5.9.2) - '@typescript-eslint/visitor-keys': 8.44.0 - eslint: 9.35.0 + '@typescript-eslint/parser': 8.45.0(eslint@9.36.0)(typescript@5.9.2) + '@typescript-eslint/scope-manager': 8.45.0 + '@typescript-eslint/type-utils': 8.45.0(eslint@9.36.0)(typescript@5.9.2) + '@typescript-eslint/utils': 8.45.0(eslint@9.36.0)(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 8.45.0 + eslint: 9.36.0 graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -2663,56 +2690,56 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.44.0(eslint@9.35.0)(typescript@5.9.2)': + '@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.2)': dependencies: - '@typescript-eslint/scope-manager': 8.44.0 - '@typescript-eslint/types': 8.44.0 - '@typescript-eslint/typescript-estree': 8.44.0(typescript@5.9.2) - '@typescript-eslint/visitor-keys': 8.44.0 + '@typescript-eslint/scope-manager': 8.45.0 + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 8.45.0 debug: 4.4.3 - eslint: 9.35.0 + eslint: 9.36.0 typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.44.0(typescript@5.9.2)': + '@typescript-eslint/project-service@8.45.0(typescript@5.9.2)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.44.0(typescript@5.9.2) - '@typescript-eslint/types': 8.44.0 + '@typescript-eslint/tsconfig-utils': 8.45.0(typescript@5.9.2) + '@typescript-eslint/types': 8.45.0 debug: 4.4.3 typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.44.0': + '@typescript-eslint/scope-manager@8.45.0': dependencies: - '@typescript-eslint/types': 8.44.0 - '@typescript-eslint/visitor-keys': 8.44.0 + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/visitor-keys': 8.45.0 - '@typescript-eslint/tsconfig-utils@8.44.0(typescript@5.9.2)': + '@typescript-eslint/tsconfig-utils@8.45.0(typescript@5.9.2)': dependencies: typescript: 5.9.2 - '@typescript-eslint/type-utils@8.44.0(eslint@9.35.0)(typescript@5.9.2)': + '@typescript-eslint/type-utils@8.45.0(eslint@9.36.0)(typescript@5.9.2)': dependencies: - '@typescript-eslint/types': 8.44.0 - '@typescript-eslint/typescript-estree': 8.44.0(typescript@5.9.2) - '@typescript-eslint/utils': 8.44.0(eslint@9.35.0)(typescript@5.9.2) + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.2) + '@typescript-eslint/utils': 8.45.0(eslint@9.36.0)(typescript@5.9.2) debug: 4.4.3 - eslint: 9.35.0 + eslint: 9.36.0 ts-api-utils: 2.1.0(typescript@5.9.2) typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.44.0': {} + '@typescript-eslint/types@8.45.0': {} - '@typescript-eslint/typescript-estree@8.44.0(typescript@5.9.2)': + '@typescript-eslint/typescript-estree@8.45.0(typescript@5.9.2)': dependencies: - '@typescript-eslint/project-service': 8.44.0(typescript@5.9.2) - '@typescript-eslint/tsconfig-utils': 8.44.0(typescript@5.9.2) - '@typescript-eslint/types': 8.44.0 - '@typescript-eslint/visitor-keys': 8.44.0 + '@typescript-eslint/project-service': 8.45.0(typescript@5.9.2) + '@typescript-eslint/tsconfig-utils': 8.45.0(typescript@5.9.2) + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/visitor-keys': 8.45.0 debug: 4.4.3 fast-glob: 3.3.3 is-glob: 4.0.3 @@ -2723,20 +2750,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.44.0(eslint@9.35.0)(typescript@5.9.2)': + '@typescript-eslint/utils@8.45.0(eslint@9.36.0)(typescript@5.9.2)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0) - '@typescript-eslint/scope-manager': 8.44.0 - '@typescript-eslint/types': 8.44.0 - '@typescript-eslint/typescript-estree': 8.44.0(typescript@5.9.2) - eslint: 9.35.0 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0) + '@typescript-eslint/scope-manager': 8.45.0 + '@typescript-eslint/types': 8.45.0 + '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.2) + eslint: 9.36.0 typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.44.0': + '@typescript-eslint/visitor-keys@8.45.0': dependencies: - '@typescript-eslint/types': 8.44.0 + '@typescript-eslint/types': 8.45.0 eslint-visitor-keys: 4.2.1 '@typescript/vfs@1.6.1(typescript@5.4.5)': @@ -2759,7 +2786,7 @@ snapshots: '@xmldom/xmldom@0.8.11': {} - '@yao-pkg/pkg-fetch@3.5.24': + '@yao-pkg/pkg-fetch@3.5.25': dependencies: https-proxy-agent: 5.0.1 node-fetch: 2.7.0 @@ -2772,12 +2799,12 @@ snapshots: - encoding - supports-color - '@yao-pkg/pkg@6.6.0': + '@yao-pkg/pkg@6.7.0': dependencies: '@babel/generator': 7.28.3 '@babel/parser': 7.28.4 '@babel/types': 7.28.4 - '@yao-pkg/pkg-fetch': 3.5.24 + '@yao-pkg/pkg-fetch': 3.5.25 into-stream: 6.0.0 minimist: 1.2.8 multistream: 4.1.0 @@ -2786,7 +2813,7 @@ snapshots: prebuild-install: 7.1.3 resolve: 1.22.10 stream-meter: 1.0.4 - tar: 7.4.3 + tar: 7.5.1 tinyglobby: 0.2.15 unzipper: 0.12.3 transitivePeerDependencies: @@ -2842,14 +2869,17 @@ snapshots: dependencies: tslib: 2.8.1 - b4a@1.7.1: {} + async-function@1.0.0: {} + + async-generator-function@1.0.0: {} + + b4a@1.7.3: {} balanced-match@1.0.2: {} - bare-events@2.7.0: - optional: true + bare-events@2.7.0: {} - bare-fs@4.4.4: + bare-fs@4.4.5: dependencies: bare-events: 2.7.0 bare-path: 3.0.0 @@ -2870,7 +2900,7 @@ snapshots: bare-stream@2.7.0(bare-events@2.7.0): dependencies: - streamx: 2.22.1 + streamx: 2.23.0 optionalDependencies: bare-events: 2.7.0 transitivePeerDependencies: @@ -2965,7 +2995,7 @@ snapshots: call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 + get-intrinsic: 1.3.1 callsites@3.1.0: {} @@ -2982,9 +3012,9 @@ snapshots: chownr@3.0.0: {} - chrome-launcher@1.2.0: + chrome-launcher@1.2.1: dependencies: - '@types/node': 24.5.1 + '@types/node': 24.6.0 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 lighthouse-logger: 2.0.2 @@ -3091,7 +3121,7 @@ snapshots: destr@2.0.5: {} - detect-libc@2.1.0: {} + detect-libc@2.1.1: {} devtools-protocol@0.0.1367902: {} @@ -3180,6 +3210,10 @@ snapshots: optionalDependencies: source-map: 0.6.1 + eslint-config-prettier@10.1.8(eslint@9.36.0): + dependencies: + eslint: 9.36.0 + eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 @@ -3189,15 +3223,15 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.35.0: + eslint@9.36.0: dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.21.0 '@eslint/config-helpers': 0.3.1 '@eslint/core': 0.15.2 '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.35.0 + '@eslint/js': 9.36.0 '@eslint/plugin-kit': 0.3.5 '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 @@ -3251,6 +3285,10 @@ snapshots: etag@1.8.1: {} + events-universal@1.0.1: + dependencies: + bare-events: 2.7.0 + expand-template@2.0.3: {} express@5.1.0: @@ -3407,15 +3445,20 @@ snapshots: function-bind@1.1.2: {} + generator-function@2.0.0: {} + get-caller-file@2.0.5: {} - get-intrinsic@1.3.0: + get-intrinsic@1.3.1: dependencies: + async-function: 1.0.0 + async-generator-function: 1.0.0 call-bind-apply-helpers: 1.0.2 es-define-property: 1.0.1 es-errors: 1.3.0 es-object-atoms: 1.1.1 function-bind: 1.1.2 + generator-function: 2.0.0 get-proto: 1.0.1 gopd: 1.2.0 has-symbols: 1.1.0 @@ -3715,7 +3758,7 @@ snapshots: minipass@7.1.2: {} - minizlib@3.0.2: + minizlib@3.1.0: dependencies: minipass: 7.1.2 @@ -3727,8 +3770,6 @@ snapshots: dependencies: minimist: 1.2.8 - mkdirp@3.0.1: {} - mpd-parser@1.3.1: dependencies: '@babel/runtime': 7.28.4 @@ -3865,7 +3906,7 @@ snapshots: prebuild-install@7.1.3: dependencies: - detect-libc: 2.1.0 + detect-libc: 2.1.1 expand-template: 2.0.3 github-from-package: 0.0.0 minimist: 1.2.8 @@ -3880,6 +3921,8 @@ snapshots: prelude-ls@1.2.1: {} + prettier@3.6.2: {} + process-nextick-args@2.0.1: {} process@0.11.10: {} @@ -3898,7 +3941,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 24.5.1 + '@types/node': 24.6.0 long: 5.3.2 protoc@1.1.3: @@ -3948,7 +3991,7 @@ snapshots: puppeteer-real-browser@1.4.4: dependencies: - chrome-launcher: 1.2.0 + chrome-launcher: 1.2.1 ghost-cursor: 1.4.1 puppeteer-extra: 3.3.6 rebrowser-puppeteer-core: 23.10.3 @@ -4111,14 +4154,14 @@ snapshots: dependencies: call-bound: 1.0.4 es-errors: 1.3.0 - get-intrinsic: 1.3.0 + get-intrinsic: 1.3.1 object-inspect: 1.13.4 side-channel-weakmap@1.0.2: dependencies: call-bound: 1.0.4 es-errors: 1.3.0 - get-intrinsic: 1.3.0 + get-intrinsic: 1.3.1 object-inspect: 1.13.4 side-channel-map: 1.0.1 @@ -4177,12 +4220,11 @@ snapshots: transitivePeerDependencies: - supports-color - streamx@2.22.1: + streamx@2.23.0: dependencies: + events-universal: 1.0.1 fast-fifo: 1.3.2 text-decoder: 1.2.3 - optionalDependencies: - bare-events: 2.7.0 transitivePeerDependencies: - react-native-b4a @@ -4228,7 +4270,7 @@ snapshots: pump: 3.0.3 tar-stream: 3.1.7 optionalDependencies: - bare-fs: 4.4.4 + bare-fs: 4.4.5 bare-path: 3.0.0 transitivePeerDependencies: - bare-buffer @@ -4244,24 +4286,23 @@ snapshots: tar-stream@3.1.7: dependencies: - b4a: 1.7.1 + b4a: 1.7.3 fast-fifo: 1.3.2 - streamx: 2.22.1 + streamx: 2.23.0 transitivePeerDependencies: - react-native-b4a - tar@7.4.3: + tar@7.5.1: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 minipass: 7.1.2 - minizlib: 3.0.2 - mkdirp: 3.0.1 + minizlib: 3.1.0 yallist: 5.0.0 text-decoder@1.2.3: dependencies: - b4a: 1.7.1 + b4a: 1.7.3 transitivePeerDependencies: - react-native-b4a @@ -4288,14 +4329,14 @@ snapshots: dependencies: typescript: 5.9.2 - ts-node@10.9.2(@types/node@24.5.1)(typescript@5.9.2): + ts-node@10.9.2(@types/node@24.6.0)(typescript@5.9.2): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 24.5.1 + '@types/node': 24.6.0 acorn: 8.15.0 acorn-walk: 8.3.4 arg: 4.1.3 @@ -4324,13 +4365,13 @@ snapshots: typed-query-selector@2.12.0: {} - typescript-eslint@8.44.0(eslint@9.35.0)(typescript@5.9.2): + typescript-eslint@8.45.0(eslint@9.36.0)(typescript@5.9.2): dependencies: - '@typescript-eslint/eslint-plugin': 8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0)(typescript@5.9.2))(eslint@9.35.0)(typescript@5.9.2) - '@typescript-eslint/parser': 8.44.0(eslint@9.35.0)(typescript@5.9.2) - '@typescript-eslint/typescript-estree': 8.44.0(typescript@5.9.2) - '@typescript-eslint/utils': 8.44.0(eslint@9.35.0)(typescript@5.9.2) - eslint: 9.35.0 + '@typescript-eslint/eslint-plugin': 8.45.0(@typescript-eslint/parser@8.45.0(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2) + '@typescript-eslint/parser': 8.45.0(eslint@9.36.0)(typescript@5.9.2) + '@typescript-eslint/typescript-estree': 8.45.0(typescript@5.9.2) + '@typescript-eslint/utils': 8.45.0(eslint@9.36.0)(typescript@5.9.2) + eslint: 9.36.0 typescript: 5.9.2 transitivePeerDependencies: - supports-color @@ -4346,7 +4387,7 @@ snapshots: buffer: 5.7.1 through: 2.3.8 - undici-types@7.12.0: {} + undici-types@7.13.0: {} universalify@0.1.2: {} diff --git a/tsc.ts b/tsc.ts index 81f0cac..0669a41 100644 --- a/tsc.ts +++ b/tsc.ts @@ -9,19 +9,9 @@ let buildIgnore: string[] = []; const isTest = argv.length > 0 && argv[0] === 'test'; const isGUI = !(argv.length > 1 && argv[1] === 'false'); -if (!isTest) - buildIgnore = [ - '*/\\.env', - './config/setup.json' - ]; - -if (!isGUI) - buildIgnore = buildIgnore.concat([ - './gui*', - './build*', - 'gui.ts' - ]); +if (!isTest) buildIgnore = ['*/\\.env', './config/setup.json']; +if (!isGUI) buildIgnore = buildIgnore.concat(['./gui*', './build*', 'gui.ts']); const ignore = [ ...buildIgnore, @@ -44,13 +34,19 @@ const ignore = [ './widevine/*', './playready/*', './videos/*', - './logs/*', -].map(a => a.replace(/\*/g, '[^]*').replace(/\.\//g, escapeRegExp(__dirname) + '/').replace(/\//g, path.sep === '\\' ? '\\\\' : '/')).map(a => new RegExp(a, 'i')); + './logs/*' +] + .map((a) => + a + .replace(/\*/g, '[^]*') + .replace(/\.\//g, escapeRegExp(__dirname) + '/') + .replace(/\//g, path.sep === '\\' ? '\\\\' : '/') + ) + .map((a) => new RegExp(a, 'i')); export { ignore }; (async () => { - const waitForProcess = async (proc: ChildProcess) => { return new Promise((resolve, reject) => { proc.stdout?.on('data', console.log); @@ -76,7 +72,7 @@ export { ignore }; process.stdout.write('✓\nBuilding react... '); const installReactDependencies = exec('pnpm install', { - cwd: path.join(__dirname, 'gui', 'react'), + cwd: path.join(__dirname, 'gui', 'react') }); await waitForProcess(installReactDependencies); @@ -98,11 +94,10 @@ export { ignore }; } const files = readDir(__dirname); - files.forEach(item => { + files.forEach((item) => { const itemPath = path.join(__dirname, 'lib', item.path.replace(__dirname, '')); if (item.stats.isDirectory()) { - if (!fs.existsSync(itemPath)) - fs.mkdirSync(itemPath); + if (!fs.existsSync(itemPath)) fs.mkdirSync(itemPath); } else { copyFileSync(item.path, itemPath); } @@ -120,19 +115,18 @@ export { ignore }; })(); function readDir(dir: string): { - path: string, - stats: fs.Stats + path: string; + stats: fs.Stats; }[] { const items: { - path: string, - stats: fs.Stats + path: string; + stats: fs.Stats; }[] = []; const content = fs.readdirSync(dir); itemLoop: for (const item of content) { const itemPath = path.join(dir, item); for (const ignoreItem of ignore) { - if (ignoreItem.test(itemPath)) - continue itemLoop; + if (ignoreItem.test(itemPath)) continue itemLoop; } const stats = fs.statSync(itemPath); items.push({ @@ -154,12 +148,10 @@ async function copyDir(src: string, dest: string) { const srcPath = path.join(src, entry.name); const destPath = path.join(dest, entry.name); - entry.isDirectory() ? - await copyDir(srcPath, destPath) : - await fs.promises.copyFile(srcPath, destPath); + entry.isDirectory() ? await copyDir(srcPath, destPath) : await fs.promises.copyFile(srcPath, destPath); } } function escapeRegExp(string: string): string { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string -} \ No newline at end of file +} diff --git a/tsconfig.json b/tsconfig.json index 8a11d1f..df074ff 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,10 +11,5 @@ "downlevelIteration": true, "jsx": "react" }, - "exclude": [ - "./videos", - "./tsc.ts", - "lib/**/*", - "gui/react/**/*" - ] -} \ No newline at end of file + "exclude": ["./videos", "./tsc.ts", "lib/**/*", "gui/react/**/*"] +}